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:
3
docs/openapi_first/templates/health_app/index.md
Normal file
3
docs/openapi_first/templates/health_app/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Health App
|
||||||
|
|
||||||
|
::: openapi_first.templates.health_app
|
||||||
3
docs/openapi_first/templates/health_app/main.md
Normal file
3
docs/openapi_first/templates/health_app/main.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Main
|
||||||
|
|
||||||
|
::: openapi_first.templates.health_app.main
|
||||||
3
docs/openapi_first/templates/health_app/routes.md
Normal file
3
docs/openapi_first/templates/health_app/routes.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Routes
|
||||||
|
|
||||||
|
::: openapi_first.templates.health_app.routes
|
||||||
3
docs/openapi_first/templates/index.md
Normal file
3
docs/openapi_first/templates/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Templates
|
||||||
|
|
||||||
|
::: openapi_first.templates
|
||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
18
openapi_first/templates/__init__.py
Normal file
18
openapi_first/templates/__init__.py
Normal 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.
|
||||||
|
"""
|
||||||
65
openapi_first/templates/health_app/__init__.py
Normal file
65
openapi_first/templates/health_app/__init__.py
Normal 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
|
||||||
|
"""
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user