diff --git a/examples/hello_world.py b/examples/hello_world.py new file mode 100644 index 000000000..8d42d64d3 --- /dev/null +++ b/examples/hello_world.py @@ -0,0 +1,7 @@ +import air + +app = air.Air() + +@app.page +def index(): + return air.H1('hello world') diff --git a/src/air/cli.py b/src/air/cli.py index 37571e6ca..18be978ca 100644 --- a/src/air/cli.py +++ b/src/air/cli.py @@ -1,7 +1,6 @@ """Air CLI - Command-line interface for running Air applications.""" import sys -from importlib.metadata import version from pathlib import Path from typing import Annotated @@ -9,6 +8,8 @@ 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() @@ -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 @@ -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( diff --git a/src/air/constants.py b/src/air/constants.py new file mode 100644 index 000000000..0000a9455 --- /dev/null +++ b/src/air/constants.py @@ -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" diff --git a/tests/test_applications.py b/tests/test_applications.py index 47f936a4b..71ad221b6 100644 --- a/tests/test_applications.py +++ b/tests/test_applications.py @@ -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"}