Skip to content
7 changes: 7 additions & 0 deletions examples/hello_world.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import air

app = air.Air()

@app.page
def index():
return air.H1('hello world')
15 changes: 10 additions & 5 deletions src/air/cli.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""Air CLI - Command-line interface for running Air applications."""

import sys
from importlib.metadata import version
from pathlib import Path
from typing import Annotated

import typer
import uvicorn
from rich.console import Console

from air.constants import AIR_VERSION, ATTRIBUTION, DEFAULT_REDOC_URL, DEFAULT_SWAGGER_URL

app = typer.Typer(add_completion=False, rich_markup_mode="rich")
console = Console()

Expand Down Expand Up @@ -37,7 +38,7 @@

def _version_callback(value: bool) -> None: # noqa: FBT001 - Typer callback signature
if value:
typer.echo(f"Air {version('air')}\nCrafted with care by Two Scoops authors pydanny and audreyfeldroy")
typer.echo(f"Air {AIR_VERSION}\n{ATTRIBUTION}")
raise typer.Exit


Expand Down Expand Up @@ -95,11 +96,15 @@ def run(

# Print startup banner
url = f"http://{host}:{port}"
swagger_url = f"{url}{DEFAULT_SWAGGER_URL}"
redoc_url = f"{url}{DEFAULT_REDOC_URL}"
console.print()
console.print(f" [bold cyan]Air[/bold cyan] v{version('air')}")
console.print(f" [bold cyan]Air[/bold cyan] v{AIR_VERSION}")
console.print()
console.print(f" [dim]➜[/dim] [bold]App:[/bold] {app_path}")
console.print(f" [dim]➜[/dim] [bold]Server:[/bold] [link={url}]{url}[/link]")
console.print(f" [dim]➜[/dim] [bold]App:[/bold] {app_path}")
console.print(f" [dim]➜[/dim] [bold]Server:[/bold] [link={url}]{url}[/link]")
console.print(f" [dim]➜[/dim] [bold]API docs:[/bold] [link={swagger_url}]{swagger_url}[/link]")
console.print(f" [dim]➜[/dim] [link={redoc_url}]{redoc_url}[/link]")
console.print()

uvicorn.run(
Expand Down
18 changes: 18 additions & 0 deletions src/air/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Shared constants used across multiple Air modules.

Module-specific constants (like DEFAULT_TAGS in applications.py
or LOG_CONFIG in cli.py) are kept with their consumers for locality.
"""

from importlib.metadata import version as get_version

# Version
AIR_VERSION = get_version("air")

# Configuration defaults
DEFAULT_TITLE = "Air"
DEFAULT_SWAGGER_URL = "/_swagger"
DEFAULT_REDOC_URL = "/_redoc"

# Brand voice
ATTRIBUTION = "Crafted with care by Two Scoops authors pydanny and audreyfeldroy"
12 changes: 12 additions & 0 deletions tests/test_applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,15 @@ def test_fastapi_app_property() -> None:

assert isinstance(app.fastapi_app, FastAPI)
assert app.fastapi_app is app._app


def test_health_endpoint() -> None:
"""Test that Air apps have a built-in /health endpoint."""
app = air.Air()
client = TestClient(app)

response = client.get("/health")

assert response.status_code == 200
assert response.headers["content-type"] == "application/json"
assert response.json() == {"status": "ok"}
Loading