diff --git a/docs/openapi_first/templates/health_app/index.md b/docs/openapi_first/templates/health_app/index.md new file mode 100644 index 0000000..8a59ca4 --- /dev/null +++ b/docs/openapi_first/templates/health_app/index.md @@ -0,0 +1,3 @@ +# Health App + +::: openapi_first.templates.health_app diff --git a/docs/openapi_first/templates/health_app/main.md b/docs/openapi_first/templates/health_app/main.md new file mode 100644 index 0000000..bf0fa94 --- /dev/null +++ b/docs/openapi_first/templates/health_app/main.md @@ -0,0 +1,3 @@ +# Main + +::: openapi_first.templates.health_app.main diff --git a/docs/openapi_first/templates/health_app/routes.md b/docs/openapi_first/templates/health_app/routes.md new file mode 100644 index 0000000..96bcfde --- /dev/null +++ b/docs/openapi_first/templates/health_app/routes.md @@ -0,0 +1,3 @@ +# Routes + +::: openapi_first.templates.health_app.routes diff --git a/docs/openapi_first/templates/index.md b/docs/openapi_first/templates/index.md new file mode 100644 index 0000000..0ad74c4 --- /dev/null +++ b/docs/openapi_first/templates/index.md @@ -0,0 +1,3 @@ +# Templates + +::: openapi_first.templates diff --git a/manage_docs.py b/manage_docs.py index c6f99aa..554911a 100644 --- a/manage_docs.py +++ b/manage_docs.py @@ -68,28 +68,42 @@ def generate_docs_from_nav( docs_root.mkdir(parents=True, exist_ok=True) - for py_file in package_dir.rglob("*.py"): - rel = py_file.relative_to(project_root) + # Collect all package directories (those containing __init__.py) + package_dirs: set[Path] = { + p.parent + for p in package_dir.rglob("__init__.py") + } - if py_file.name == "__init__.py": - # Package → index.md - module_path = ".".join(rel.parent.parts) - md_path = docs_root / rel.parent / "index.md" - title = rel.parent.name.replace("_", " ").title() - else: - # Regular module → .md - module_path = ".".join(rel.with_suffix("").parts) - md_path = docs_root / rel.with_suffix(".md") - title = md_path.stem.replace("_", " ").title() + for pkg_dir in sorted(package_dirs): + rel_pkg = pkg_dir.relative_to(project_root) + module_base = ".".join(rel_pkg.parts) - md_path.parent.mkdir(parents=True, exist_ok=True) + # index.md for the package itself + index_md = docs_root / rel_pkg / "index.md" + index_md.parent.mkdir(parents=True, exist_ok=True) - content = f"""# {title} + title = pkg_dir.name.replace("_", " ").title() + index_md.write_text( + f"# {title}\n\n::: {module_base}\n", + encoding="utf-8", + ) -::: {module_path} -""" + # Document modules inside this package only + for py_file in pkg_dir.iterdir(): + if ( + py_file.suffix != ".py" + or py_file.name == "__init__.py" + ): + continue - md_path.write_text(content, encoding="utf-8") + module_path = f"{module_base}.{py_file.stem}" + md_path = docs_root / rel_pkg / f"{py_file.stem}.md" + + title = py_file.stem.replace("_", " ").title() + md_path.write_text( + f"# {title}\n\n::: {module_path}\n", + encoding="utf-8", + ) def load_mkdocs_config(): diff --git a/mkdocs.yml b/mkdocs.yml index f0c0c32..efe989c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,5 +47,12 @@ nav: - CLI: - Home: openapi_first/cli.md + - Templates: + - Home: openapi_first/templates/index.md + - Health App: + - Home: openapi_first/templates/health_app/index.md + - App: openapi_first/templates/health_app/main.md + - Routes: openapi_first/templates/health_app/routes.md + - Errors: - Error Hierarchy: openapi_first/errors.md diff --git a/openapi_first/__init__.py b/openapi_first/__init__.py index 8bb9b02..50ee867 100644 --- a/openapi_first/__init__.py +++ b/openapi_first/__init__.py @@ -49,6 +49,32 @@ Runtime dependencies are intentionally minimal: The ASGI server (e.g., uvicorn) is an application-level dependency and is not bundled with this library. +---------------------------------------------------------------------- +Command-Line Interface (Scaffolding, Templates) +---------------------------------------------------------------------- + +FastAPI OpenAPI First ships with a small CLI for bootstrapping +OpenAPI-first FastAPI applications from bundled templates. + +List available application templates: + + openapi-first --list + +Create a new application using the default template: + + openapi-first + +Create a new application using a specific template: + + openapi-first health_app + +Create a new application in a custom directory: + + openapi-first health_app my-service + +The CLI copies template files verbatim into the target directory. +No code is generated or modified beyond the copied scaffold. + ---------------------------------------------------------------------- Server-Side Usage (OpenAPI → FastAPI) ---------------------------------------------------------------------- diff --git a/openapi_first/cli.py b/openapi_first/cli.py index e148183..9450d81 100644 --- a/openapi_first/cli.py +++ b/openapi_first/cli.py @@ -4,16 +4,8 @@ openapi_first.cli Command-line interface for FastAPI OpenAPI-first scaffolding utilities. -This module provides a small, focused CLI intended to help developers -quickly bootstrap OpenAPI-first FastAPI services using bundled project -templates. - -Currently supported scaffolds: -- Health check service (minimal OpenAPI-first application) - -The CLI copies versioned templates packaged with the library into a -user-specified directory, allowing rapid local development without -manual setup. +This CLI bootstraps OpenAPI-first FastAPI applications from versioned, +bundled templates packaged with the library. """ import argparse @@ -22,68 +14,75 @@ from pathlib import Path from importlib import resources -def copy_health_app_template(target_dir: Path) -> None: +DEFAULT_TEMPLATE = "health_app" + + +def available_templates() -> list[str]: """ - Copy the bundled OpenAPI-first health app template into a directory. + Return a list of available application templates. + """ + root = resources.files("openapi_first.templates") + return sorted( + item.name + for item in root.iterdir() + if item.is_dir() and not item.name.startswith("_") + ) - This function copies a fully working, minimal OpenAPI-first FastAPI - health check application from the package's embedded templates into - the specified target directory. - The target directory will be created if it does not already exist. - Existing files may be overwritten. - - Parameters - ---------- - target_dir : pathlib.Path - Destination directory into which the health app template - should be copied. - - Raises - ------ - FileNotFoundError - If the bundled health app template cannot be located. +def copy_template(template: str, target_dir: Path) -> None: + """ + Copy a bundled OpenAPI-first application template into a directory. """ target_dir = target_dir.resolve() target_dir.mkdir(parents=True, exist_ok=True) - with resources.files("openapi_first.templates").joinpath( - "health_app" - ) as src: - shutil.copytree(src, target_dir, dirs_exist_ok=True) + root = resources.files("openapi_first.templates") + src = root / template + + if not src.exists(): + raise FileNotFoundError( + f"Template '{template}' not found. " + f"Available templates: {', '.join(available_templates())}" + ) + + with resources.as_file(src) as path: + shutil.copytree(path, target_dir, dirs_exist_ok=True) def main() -> None: - """ - Entry point for the FastAPI OpenAPI-first CLI. - - Parses command-line arguments and initializes a new OpenAPI-first - health check application by copying the bundled template into the - specified directory. - - If no target path is provided, the scaffold is created in a directory - named ``health-app`` in the current working directory. - - Example - ------- - Create a health app in the default directory:: - - openapi-first - - Create a health app in a custom directory:: - - openapi-first my-service - """ parser = argparse.ArgumentParser( description="FastAPI OpenAPI-first scaffolding tools" ) + parser.add_argument( + "template", + nargs="?", + default=DEFAULT_TEMPLATE, + help=f"Template name (default: {DEFAULT_TEMPLATE})", + ) parser.add_argument( "path", nargs="?", - default="health-app", - help="Target directory for the health app", + default=None, + help="Target directory (defaults to template name)", + ) + parser.add_argument( + "--list", + action="store_true", + help="List available templates and exit", ) args = parser.parse_args() - copy_health_app_template(Path(args.path)) - print(f"Health app created at {args.path}") + + if args.list: + for name in available_templates(): + print(name) + return + + target = Path(args.path or args.template.replace("_", "-")) + + try: + copy_template(args.template, target) + except Exception as exc: + raise SystemExit(str(exc)) from exc + + print(f"Template '{args.template}' created at {target}") diff --git a/openapi_first/templates/__init__.py b/openapi_first/templates/__init__.py new file mode 100644 index 0000000..844983d --- /dev/null +++ b/openapi_first/templates/__init__.py @@ -0,0 +1,18 @@ +""" +Application templates for FastAPI OpenAPI First. + +This package contains example and scaffolding templates intended to be +copied into user projects via the ``openapi-first`` CLI. + +Templates in this package are: +- Reference implementations of OpenAPI-first services +- Not part of the ``openapi_first`` public or internal API +- Not intended to be imported as runtime dependencies + +The presence of this file exists solely to: +- Mark the directory as an explicit Python package +- Enable deterministic tooling behavior (documentation, packaging) +- Avoid accidental traversal of non-package directories + +No code in this package should be imported by library consumers. +""" diff --git a/openapi_first/templates/health_app/__init__.py b/openapi_first/templates/health_app/__init__.py new file mode 100644 index 0000000..c6dd9f1 --- /dev/null +++ b/openapi_first/templates/health_app/__init__.py @@ -0,0 +1,65 @@ +""" +OpenAPI-first FastAPI application template. + +This package contains a minimal, fully working example of an +OpenAPI-first FastAPI service built using the ``openapi_first`` library. + +The application is assembled exclusively from: +- an OpenAPI specification (``openapi.yaml``) +- a handler namespace (``routes``) + +No routing decorators, implicit behavior, or framework-specific +convenience abstractions are used. All HTTP routes, methods, and +operation bindings are defined in OpenAPI and enforced at application +startup. + +This package is intended to be copied as a starting point for new +services via the ``openapi-first`` CLI. It is not part of the +``openapi_first`` library API surface. + +---------------------------------------------------------------------- +Scaffolding via CLI +---------------------------------------------------------------------- + +Create a new OpenAPI-first health check service using the bundled +template: + + openapi-first health_app + +Create the service in a custom directory: + + openapi-first health_app my-health-service + +List all available application templates: + + openapi-first --list + +The CLI copies template files verbatim into the target directory. +No code is generated or modified beyond the copied scaffold. + +---------------------------------------------------------------------- +Client Usage Example +---------------------------------------------------------------------- + +The same OpenAPI specification used by the server can be used to +construct a strict, operationId-driven HTTP client. + +Example client call for the ``get_health`` operation: + + from openapi_first.loader import load_openapi + from openapi_first.client import OpenAPIClient + + spec = load_openapi("openapi.yaml") + client = OpenAPIClient(spec) + + response = client.get_health() + + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + +Client guarantees: +- One callable per OpenAPI ``operationId`` +- No hardcoded URLs or HTTP methods in user code +- Path and request parameters must match the OpenAPI specification +- Invalid or incomplete OpenAPI specs fail at client construction time +""" diff --git a/openapi_first/templates/health_app/main.py b/openapi_first/templates/health_app/main.py index 0c8677f..af92485 100644 --- a/openapi_first/templates/health_app/main.py +++ b/openapi_first/templates/health_app/main.py @@ -1,3 +1,31 @@ +""" +Application entry point for an OpenAPI-first FastAPI service. + +This module constructs a FastAPI application exclusively from an +OpenAPI specification and a handler namespace, without using +decorator-driven routing. + +All HTTP routes, methods, and operation bindings are defined in the +OpenAPI document referenced by ``openapi_path``. Python callables +defined in the ``routes`` module are bound to OpenAPI operations +strictly via ``operationId``. + +This module contains no routing logic, request handling, or framework +configuration beyond application assembly. + +Design guarantees: +- OpenAPI is the single source of truth +- No undocumented routes can exist +- Every OpenAPI operationId must resolve to exactly one handler +- All contract violations fail at application startup + +This file is intended to be used as the ASGI entry point. + +Example: + uvicorn main:app +""" + + from openapi_first.app import OpenAPIFirstApp import routes diff --git a/openapi_first/templates/health_app/routes.py b/openapi_first/templates/health_app/routes.py index e155d2b..2e8b180 100644 --- a/openapi_first/templates/health_app/routes.py +++ b/openapi_first/templates/health_app/routes.py @@ -1,2 +1,32 @@ +""" +OpenAPI operation handlers. + +This module defines pure Python callables that implement OpenAPI +operations for this service. Functions in this module are bound to HTTP +routes exclusively via OpenAPI ``operationId`` values. + +No routing decorators, HTTP metadata, or framework-specific logic +should appear here. All request/response semantics are defined in the +OpenAPI specification. + +This module serves solely as an operationId namespace. +""" + + def get_health(): + """ + Health check operation handler. + + This function implements the OpenAPI operation identified by + ``operationId: get_health``. + + It contains no routing metadata or framework-specific logic. + Request binding, HTTP method, and response semantics are defined + exclusively by the OpenAPI specification. + + Returns + ------- + dict + A minimal liveness payload indicating service health. + """ return {"status": "ok"} diff --git a/pyproject.toml b/pyproject.toml index adacf92..e8f2593 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ Versions = "https://git.aetoskia.com/aetos/openapi-first/tags" packages = { find = { include = ["openapi_first*"] } } [tool.setuptools.package-data] -fastapi_openapi_first = ["templates/**/*"] +openapi_first = ["templates/**/*"] [tool.ruff]