docs, cli: enforce package-bound docs, add template scaffolding, and document CLI usage

- Restrict mkdocstrings generation to real Python packages (require __init__.py)
- Add explicit documentation section for CLI scaffolding and templates
- Generalize CLI to support multiple templates with dynamic discovery
- Package templates correctly for importlib.resources access
- Add fully documented health_app template (app entry point and handlers)
- Fix setuptools package-data configuration for bundled templates

These changes make documentation import-safe, clarify package boundaries,
and provide a deterministic, OpenAPI-first scaffolding workflow via CLI.
This commit is contained in:
2026-01-11 19:26:21 +05:30
parent 6180443327
commit 72b5be6976
13 changed files with 273 additions and 74 deletions

View File

@@ -0,0 +1,3 @@
# Health App
::: openapi_first.templates.health_app

View File

@@ -0,0 +1,3 @@
# Main
::: openapi_first.templates.health_app.main

View File

@@ -0,0 +1,3 @@
# Routes
::: openapi_first.templates.health_app.routes

View File

@@ -0,0 +1,3 @@
# Templates
::: openapi_first.templates

View File

@@ -68,28 +68,42 @@ def generate_docs_from_nav(
docs_root.mkdir(parents=True, exist_ok=True) docs_root.mkdir(parents=True, exist_ok=True)
for py_file in package_dir.rglob("*.py"): # Collect all package directories (those containing __init__.py)
rel = py_file.relative_to(project_root) package_dirs: set[Path] = {
p.parent
for p in package_dir.rglob("__init__.py")
}
if py_file.name == "__init__.py": for pkg_dir in sorted(package_dirs):
# Package → index.md rel_pkg = pkg_dir.relative_to(project_root)
module_path = ".".join(rel.parent.parts) module_base = ".".join(rel_pkg.parts)
md_path = docs_root / rel.parent / "index.md"
title = rel.parent.name.replace("_", " ").title()
else:
# Regular module → <module>.md
module_path = ".".join(rel.with_suffix("").parts)
md_path = docs_root / rel.with_suffix(".md")
title = md_path.stem.replace("_", " ").title()
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(): def load_mkdocs_config():

View File

@@ -47,5 +47,12 @@ nav:
- CLI: - CLI:
- Home: openapi_first/cli.md - 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: - Errors:
- Error Hierarchy: openapi_first/errors.md - Error Hierarchy: openapi_first/errors.md

View File

@@ -49,6 +49,32 @@ Runtime dependencies are intentionally minimal:
The ASGI server (e.g., uvicorn) is an application-level dependency and is The ASGI server (e.g., uvicorn) is an application-level dependency and is
not bundled with this library. 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) Server-Side Usage (OpenAPI → FastAPI)
---------------------------------------------------------------------- ----------------------------------------------------------------------

View File

@@ -4,16 +4,8 @@ openapi_first.cli
Command-line interface for FastAPI OpenAPI-first scaffolding utilities. Command-line interface for FastAPI OpenAPI-first scaffolding utilities.
This module provides a small, focused CLI intended to help developers This CLI bootstraps OpenAPI-first FastAPI applications from versioned,
quickly bootstrap OpenAPI-first FastAPI services using bundled project bundled templates packaged with the library.
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.
""" """
import argparse import argparse
@@ -22,68 +14,75 @@ from pathlib import Path
from importlib import resources 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. def copy_template(template: str, target_dir: Path) -> None:
Existing files may be overwritten. """
Copy a bundled OpenAPI-first application template into a directory.
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.
""" """
target_dir = target_dir.resolve() target_dir = target_dir.resolve()
target_dir.mkdir(parents=True, exist_ok=True) target_dir.mkdir(parents=True, exist_ok=True)
with resources.files("openapi_first.templates").joinpath( root = resources.files("openapi_first.templates")
"health_app" src = root / template
) as src:
shutil.copytree(src, target_dir, dirs_exist_ok=True) 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: 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( parser = argparse.ArgumentParser(
description="FastAPI OpenAPI-first scaffolding tools" description="FastAPI OpenAPI-first scaffolding tools"
) )
parser.add_argument(
"template",
nargs="?",
default=DEFAULT_TEMPLATE,
help=f"Template name (default: {DEFAULT_TEMPLATE})",
)
parser.add_argument( parser.add_argument(
"path", "path",
nargs="?", nargs="?",
default="health-app", default=None,
help="Target directory for the health app", help="Target directory (defaults to template name)",
)
parser.add_argument(
"--list",
action="store_true",
help="List available templates and exit",
) )
args = parser.parse_args() 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}")

View File

@@ -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.
"""

View File

@@ -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
"""

View File

@@ -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 from openapi_first.app import OpenAPIFirstApp
import routes import routes

View File

@@ -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(): 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"} return {"status": "ok"}

View File

@@ -84,7 +84,7 @@ Versions = "https://git.aetoskia.com/aetos/openapi-first/tags"
packages = { find = { include = ["openapi_first*"] } } packages = { find = { include = ["openapi_first*"] } }
[tool.setuptools.package-data] [tool.setuptools.package-data]
fastapi_openapi_first = ["templates/**/*"] openapi_first = ["templates/**/*"]
[tool.ruff] [tool.ruff]