Compare commits
18 Commits
0.0.3
...
d912053368
| Author | SHA1 | Date | |
|---|---|---|---|
| d912053368 | |||
| 0f591b666b | |||
| 808ffa8fed | |||
| 1e1d7fcde9 | |||
| d4b64d630a | |||
| 85aac955ac | |||
| 8299445b68 | |||
| eb845c5bf4 | |||
| 514f6e5f7c | |||
| 1c48f58578 | |||
| 80f8defcc2 | |||
| f03e250763 | |||
| 37b892f695 | |||
| 14ed19d2d5 | |||
| 6bafa435f1 | |||
| 29c1579f40 | |||
| a3c063b569 | |||
| 72b5be6976 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -38,3 +38,4 @@ Thumbs.db
|
||||
*.swo
|
||||
*~
|
||||
*.tmp
|
||||
site
|
||||
|
||||
96
README.md
Normal file
96
README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# openapi_first
|
||||
|
||||
# Summary
|
||||
|
||||
FastAPI OpenAPI First — strict OpenAPI-first application bootstrap for FastAPI.
|
||||
|
||||
FastAPI OpenAPI First is a **contract-first infrastructure library** that
|
||||
enforces OpenAPI as the single source of truth for FastAPI services.
|
||||
|
||||
The library removes decorator-driven routing and replaces it with
|
||||
deterministic, spec-driven application assembly. Every HTTP route,
|
||||
method, and operation is defined in OpenAPI first and bound to Python
|
||||
handlers explicitly via `operationId`.
|
||||
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
Install using pip:
|
||||
|
||||
```bash
|
||||
pip install openapi-first
|
||||
```
|
||||
|
||||
Or with Poetry:
|
||||
|
||||
```bash
|
||||
poetry add openapi-first
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Quick Start
|
||||
|
||||
Minimal OpenAPI-first FastAPI application:
|
||||
|
||||
```python
|
||||
from openapi_first import app
|
||||
import my_service.routes as routes
|
||||
|
||||
api = app.OpenAPIFirstApp(
|
||||
openapi_path="openapi.yaml",
|
||||
routes_module=routes,
|
||||
title="My Service",
|
||||
version="1.0.0",
|
||||
)
|
||||
```
|
||||
|
||||
OperationId-driven HTTP client:
|
||||
|
||||
```python
|
||||
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()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
The library is structured around four core responsibilities:
|
||||
|
||||
- `loader`: Load and validate OpenAPI 3.x specifications (JSON/YAML).
|
||||
- `binder`: Bind OpenAPI operations to FastAPI routes via `operationId`.
|
||||
- `app`: OpenAPI-first FastAPI application bootstrap.
|
||||
- `client`: OpenAPI-first HTTP client driven by the same specification.
|
||||
- `errors`: Explicit error hierarchy for contract violations.
|
||||
|
||||
---
|
||||
|
||||
# Public API
|
||||
|
||||
The supported public API consists of the following top-level modules:
|
||||
|
||||
- `openapi_first.app`
|
||||
- `openapi_first.binder`
|
||||
- `openapi_first.loader`
|
||||
- `openapi_first.client`
|
||||
- `openapi_first.errors`
|
||||
|
||||
---
|
||||
|
||||
# Design Guarantees
|
||||
|
||||
- OpenAPI is the single source of truth.
|
||||
- No undocumented routes can exist.
|
||||
- No OpenAPI operation can exist without a handler or client callable.
|
||||
- All contract violations fail at application startup or client creation.
|
||||
- No hidden FastAPI magic or implicit behavior.
|
||||
- Deterministic, testable application assembly.
|
||||
|
||||
---
|
||||
10
docforge.nav.yml
Normal file
10
docforge.nav.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
home: index.md
|
||||
groups:
|
||||
Application Bootstrap:
|
||||
- app.md
|
||||
- binder.md
|
||||
Core Utilities:
|
||||
- loader.md
|
||||
- errors.md
|
||||
OpenAPI Client:
|
||||
- client.md
|
||||
@@ -1,3 +1,3 @@
|
||||
# Openapi First
|
||||
# openapi_first
|
||||
|
||||
::: openapi_first
|
||||
3
docs/templates/crud_app/data.md
vendored
Normal file
3
docs/templates/crud_app/data.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Data
|
||||
|
||||
::: openapi_first.templates.crud_app.data
|
||||
3
docs/templates/crud_app/index.md
vendored
Normal file
3
docs/templates/crud_app/index.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Crud App
|
||||
|
||||
::: openapi_first.templates.crud_app
|
||||
3
docs/templates/crud_app/main.md
vendored
Normal file
3
docs/templates/crud_app/main.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Main
|
||||
|
||||
::: openapi_first.templates.crud_app.main
|
||||
3
docs/templates/crud_app/routes.md
vendored
Normal file
3
docs/templates/crud_app/routes.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Routes
|
||||
|
||||
::: openapi_first.templates.crud_app.routes
|
||||
3
docs/templates/crud_app/test_crud_app.md
vendored
Normal file
3
docs/templates/crud_app/test_crud_app.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Test Crud App
|
||||
|
||||
::: openapi_first.templates.crud_app.test_crud_app
|
||||
3
docs/templates/health_app/index.md
vendored
Normal file
3
docs/templates/health_app/index.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Health App
|
||||
|
||||
::: openapi_first.templates.health_app
|
||||
3
docs/templates/health_app/main.md
vendored
Normal file
3
docs/templates/health_app/main.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Main
|
||||
|
||||
::: openapi_first.templates.health_app.main
|
||||
3
docs/templates/health_app/routes.md
vendored
Normal file
3
docs/templates/health_app/routes.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Routes
|
||||
|
||||
::: openapi_first.templates.health_app.routes
|
||||
3
docs/templates/index.md
vendored
Normal file
3
docs/templates/index.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Templates
|
||||
|
||||
::: openapi_first.templates
|
||||
3
docs/templates/model_app/data.md
vendored
Normal file
3
docs/templates/model_app/data.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Data
|
||||
|
||||
::: openapi_first.templates.model_app.data
|
||||
3
docs/templates/model_app/index.md
vendored
Normal file
3
docs/templates/model_app/index.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Model App
|
||||
|
||||
::: openapi_first.templates.model_app
|
||||
3
docs/templates/model_app/main.md
vendored
Normal file
3
docs/templates/model_app/main.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Main
|
||||
|
||||
::: openapi_first.templates.model_app.main
|
||||
3
docs/templates/model_app/models.md
vendored
Normal file
3
docs/templates/model_app/models.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Models
|
||||
|
||||
::: openapi_first.templates.model_app.models
|
||||
3
docs/templates/model_app/routes.md
vendored
Normal file
3
docs/templates/model_app/routes.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Routes
|
||||
|
||||
::: openapi_first.templates.model_app.routes
|
||||
3
docs/templates/model_app/test_model_app.md
vendored
Normal file
3
docs/templates/model_app/test_model_app.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Test Model App
|
||||
|
||||
::: openapi_first.templates.model_app.test_model_app
|
||||
159
manage_docs.py
159
manage_docs.py
@@ -1,159 +0,0 @@
|
||||
"""
|
||||
MkDocs documentation management CLI.
|
||||
|
||||
This script provides a proper CLI interface to:
|
||||
- Generate MkDocs Markdown files with mkdocstrings directives
|
||||
- Build the documentation site
|
||||
- Serve the documentation site locally
|
||||
|
||||
All operations are performed by calling MkDocs as a Python library
|
||||
(no shell command invocation).
|
||||
|
||||
Requirements:
|
||||
- mkdocs
|
||||
- mkdocs-material
|
||||
- mkdocstrings[python]
|
||||
|
||||
Usage:
|
||||
python manage_docs.py generate
|
||||
python manage_docs.py build
|
||||
python manage_docs.py serve
|
||||
|
||||
Optional flags:
|
||||
--docs-dir PATH Path to docs directory (default: ./docs)
|
||||
--package-root NAME Root Python package name (default: mail_intake)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from mkdocs.commands import build as mkdocs_build
|
||||
from mkdocs.commands import serve as mkdocs_serve
|
||||
from mkdocs.config import load_config
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
DEFAULT_DOCS_DIR = PROJECT_ROOT / "docs"
|
||||
DEFAULT_PACKAGE_ROOT = "openapi_first"
|
||||
MKDOCS_YML = PROJECT_ROOT / "mkdocs.yml"
|
||||
|
||||
|
||||
def generate_docs_from_nav(
|
||||
project_root: Path,
|
||||
docs_root: Path,
|
||||
package_root: str,
|
||||
) -> None:
|
||||
"""
|
||||
Create and populate MkDocs Markdown files with mkdocstrings directives.
|
||||
|
||||
This function:
|
||||
- Walks the Python package structure
|
||||
- Mirrors it under the docs directory
|
||||
- Creates missing .md files
|
||||
- Creates index.md for packages (__init__.py)
|
||||
- Overwrites content with ::: package.module
|
||||
|
||||
Examples:
|
||||
mail_intake/__init__.py -> docs/mail_intake/index.md
|
||||
mail_intake/config.py -> docs/mail_intake/config.md
|
||||
mail_intake/adapters/__init__.py -> docs/mail_intake/adapters/index.md
|
||||
mail_intake/adapters/base.py -> docs/mail_intake/adapters/base.md
|
||||
"""
|
||||
|
||||
package_dir = project_root / package_root
|
||||
if not package_dir.exists():
|
||||
raise FileNotFoundError(f"Package not found: {package_dir}")
|
||||
|
||||
docs_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for py_file in package_dir.rglob("*.py"):
|
||||
rel = py_file.relative_to(project_root)
|
||||
|
||||
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 → <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)
|
||||
|
||||
content = f"""# {title}
|
||||
|
||||
::: {module_path}
|
||||
"""
|
||||
|
||||
md_path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def load_mkdocs_config():
|
||||
if not MKDOCS_YML.exists():
|
||||
raise FileNotFoundError("mkdocs.yml not found at project root")
|
||||
return load_config(str(MKDOCS_YML))
|
||||
|
||||
|
||||
def cmd_generate(args: argparse.Namespace) -> None:
|
||||
generate_docs_from_nav(
|
||||
project_root=PROJECT_ROOT,
|
||||
docs_root=args.docs_dir,
|
||||
package_root=args.package_root,
|
||||
)
|
||||
|
||||
|
||||
def cmd_build(_: argparse.Namespace) -> None:
|
||||
config = load_mkdocs_config()
|
||||
mkdocs_build.build(config)
|
||||
|
||||
|
||||
def cmd_serve(_: argparse.Namespace) -> None:
|
||||
mkdocs_serve.serve(
|
||||
config_file=str(MKDOCS_YML)
|
||||
)
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="manage_docs.py",
|
||||
description="Manage MkDocs documentation for the project",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--docs-dir",
|
||||
type=Path,
|
||||
default=DEFAULT_DOCS_DIR,
|
||||
help="Path to the docs directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--package-root",
|
||||
default=DEFAULT_PACKAGE_ROOT,
|
||||
help="Root Python package name",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
subparsers.add_parser(
|
||||
"generate",
|
||||
help="Generate Markdown files with mkdocstrings directives",
|
||||
).set_defaults(func=cmd_generate)
|
||||
|
||||
subparsers.add_parser(
|
||||
"build",
|
||||
help="Build the MkDocs site",
|
||||
).set_defaults(func=cmd_build)
|
||||
|
||||
subparsers.add_parser(
|
||||
"serve",
|
||||
help="Serve the MkDocs site locally",
|
||||
).set_defaults(func=cmd_serve)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
6
mcp_docs/index.json
Normal file
6
mcp_docs/index.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"project": "openapi_first",
|
||||
"type": "docforge-model",
|
||||
"modules_count": 22,
|
||||
"source": "docforge"
|
||||
}
|
||||
53
mcp_docs/modules/openapi_first.app.json
Normal file
53
mcp_docs/modules/openapi_first.app.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"module": "openapi_first.app",
|
||||
"content": {
|
||||
"path": "openapi_first.app",
|
||||
"docstring": "# Summary\n\nOpenAPI-first application bootstrap for FastAPI.\n\nThis module provides `OpenAPIFirstApp`, a thin but strict abstraction\nthat enforces OpenAPI as the single source of truth for a FastAPI service.\n\nNotes:\n **Core Principles:**\n\n - The OpenAPI specification (JSON or YAML) defines the entire API surface.\n - Every `operationId` in the OpenAPI spec must have a corresponding\n Python handler function.\n - Handlers are plain Python callables (no FastAPI decorators).\n - FastAPI route registration is derived exclusively from the spec.\n - FastAPI's autogenerated OpenAPI schema is fully overridden.\n\n **Responsibilities:**\n\n - Loads and validates an OpenAPI 3.x specification.\n - Dynamically binds HTTP routes to handler functions using `operationId`.\n - Registers routes with FastAPI at application startup.\n - Ensures runtime behavior matches the OpenAPI contract exactly.\n\n **Constraints:**\n\n - This module intentionally does NOT:\n - Generate OpenAPI specs.\n - Generate client code.\n - Introduce a new framework or lifecycle.\n - Alter FastAPI dependency injection semantics.",
|
||||
"objects": {
|
||||
"FastAPI": {
|
||||
"name": "FastAPI",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.app.FastAPI",
|
||||
"signature": "<bound method Alias.signature of Alias('FastAPI', 'fastapi.FastAPI')>",
|
||||
"docstring": null
|
||||
},
|
||||
"load_openapi": {
|
||||
"name": "load_openapi",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.app.load_openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
|
||||
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
|
||||
},
|
||||
"bind_routes": {
|
||||
"name": "bind_routes",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.app.bind_routes",
|
||||
"signature": "<bound method Alias.signature of Alias('bind_routes', 'openapi_first.binder.bind_routes')>",
|
||||
"docstring": "Bind OpenAPI operations to FastAPI routes.\n\nArgs:\n app (fastapi.FastAPI):\n The FastAPI application instance to which routes will be added.\n spec (dict):\n Parsed OpenAPI 3.x specification dictionary.\n routes_module (module):\n Python module containing handler functions. Each handler's name MUST\n exactly match an OpenAPI `operationId`.\n\nRaises:\n MissingOperationHandler:\n If an `operationId` is missing from the spec or if no corresponding\n handler function exists in the routes module.\n\nNotes:\n **Responsibilities:**\n\n - Iterates through the OpenAPI specification paths and methods.\n - Resolves each `operationId` to a handler function, and registers\n a corresponding `APIRoute` on the FastAPI application.\n\n **Guarantees:**\n\n - Route registration is deterministic and spec-driven. No route\n decorators are required or supported. Handler resolution errors\n surface at application startup."
|
||||
},
|
||||
"OpenAPIFirstApp": {
|
||||
"name": "OpenAPIFirstApp",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.app.OpenAPIFirstApp",
|
||||
"signature": "<bound method Class.signature of Class('OpenAPIFirstApp', 41, 115)>",
|
||||
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
|
||||
"members": {
|
||||
"openapi": {
|
||||
"name": "openapi",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.app.OpenAPIFirstApp.openapi",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"Any": {
|
||||
"name": "Any",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.app.Any",
|
||||
"signature": "<bound method Alias.signature of Alias('Any', 'typing.Any')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
mcp_docs/modules/openapi_first.binder.json
Normal file
51
mcp_docs/modules/openapi_first.binder.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"module": "openapi_first.binder",
|
||||
"content": {
|
||||
"path": "openapi_first.binder",
|
||||
"docstring": "# Summary\n\nOpenAPI-driven route binding for FastAPI.\n\nThis module is responsible for translating an OpenAPI 3.x specification\ninto concrete FastAPI routes. It enforces a strict one-to-one mapping\nbetween OpenAPI operations and Python handler functions using `operationId`.\n\nNotes:\n **Core Responsibility:**\n\n - Read path + method definitions from an OpenAPI specification.\n - Resolve each `operationId` to a Python callable.\n - Register routes with FastAPI using `APIRoute`.\n - Fail fast when contract violations are detected.\n\n **Design Constraints:**\n\n - All routes MUST be declared in the OpenAPI specification.\n - All OpenAPI operations MUST define an `operationId`.\n - Every `operationId` MUST resolve to a handler function.\n - Handlers are plain Python callables (no decorators required).\n - No implicit route creation or inference is allowed.\n\n **Constraints:**\n\n - This module intentionally does NOT:\n - Perform request or response validation.\n - Generate Pydantic models.\n - Modify FastAPI dependency injection.\n - Interpret OpenAPI semantics beyond routing metadata.",
|
||||
"objects": {
|
||||
"APIRoute": {
|
||||
"name": "APIRoute",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.binder.APIRoute",
|
||||
"signature": "<bound method Alias.signature of Alias('APIRoute', 'fastapi.routing.APIRoute')>",
|
||||
"docstring": null
|
||||
},
|
||||
"MissingOperationHandler": {
|
||||
"name": "MissingOperationHandler",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.binder.MissingOperationHandler",
|
||||
"signature": "<bound method Alias.signature of Alias('MissingOperationHandler', 'openapi_first.errors.MissingOperationHandler')>",
|
||||
"docstring": "Raised when an OpenAPI operation cannot be resolved to a handler.\n\nNotes:\n **Scenarios:**\n\n - An OpenAPI operation does not define an `operationId`.\n - An `operationId` is defined but no matching function exists in\n the provided routes module.\n\n **Guarantees:**\n\n - This represents a violation of the OpenAPI-first contract and\n indicates that the specification and implementation are out of\n sync."
|
||||
},
|
||||
"bind_routes": {
|
||||
"name": "bind_routes",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.binder.bind_routes",
|
||||
"signature": "<bound method Function.signature of Function('bind_routes', 40, 100)>",
|
||||
"docstring": "Bind OpenAPI operations to FastAPI routes.\n\nArgs:\n app (fastapi.FastAPI):\n The FastAPI application instance to which routes will be added.\n spec (dict):\n Parsed OpenAPI 3.x specification dictionary.\n routes_module (module):\n Python module containing handler functions. Each handler's name MUST\n exactly match an OpenAPI `operationId`.\n\nRaises:\n MissingOperationHandler:\n If an `operationId` is missing from the spec or if no corresponding\n handler function exists in the routes module.\n\nNotes:\n **Responsibilities:**\n\n - Iterates through the OpenAPI specification paths and methods.\n - Resolves each `operationId` to a handler function, and registers\n a corresponding `APIRoute` on the FastAPI application.\n\n **Guarantees:**\n\n - Route registration is deterministic and spec-driven. No route\n decorators are required or supported. Handler resolution errors\n surface at application startup."
|
||||
},
|
||||
"Any": {
|
||||
"name": "Any",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.binder.Any",
|
||||
"signature": "<bound method Alias.signature of Alias('Any', 'typing.Any')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Dict": {
|
||||
"name": "Dict",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.binder.Dict",
|
||||
"signature": "<bound method Alias.signature of Alias('Dict', 'typing.Dict')>",
|
||||
"docstring": null
|
||||
},
|
||||
"FastAPI": {
|
||||
"name": "FastAPI",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.binder.FastAPI",
|
||||
"signature": "<bound method Alias.signature of Alias('FastAPI', 'fastapi.FastAPI')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
mcp_docs/modules/openapi_first.cli.json
Normal file
65
mcp_docs/modules/openapi_first.cli.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"module": "openapi_first.cli",
|
||||
"content": {
|
||||
"path": "openapi_first.cli",
|
||||
"docstring": "Command-line interface for FastAPI OpenAPI-first scaffolding utilities.\n\n---\n\n## Summary\n\nThis CLI bootstraps OpenAPI-first FastAPI applications from versioned,\nbundled templates packaged with the library.",
|
||||
"objects": {
|
||||
"argparse": {
|
||||
"name": "argparse",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.cli.argparse",
|
||||
"signature": "<bound method Alias.signature of Alias('argparse', 'argparse')>",
|
||||
"docstring": null
|
||||
},
|
||||
"shutil": {
|
||||
"name": "shutil",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.cli.shutil",
|
||||
"signature": "<bound method Alias.signature of Alias('shutil', 'shutil')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Path": {
|
||||
"name": "Path",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.cli.Path",
|
||||
"signature": "<bound method Alias.signature of Alias('Path', 'pathlib.Path')>",
|
||||
"docstring": null
|
||||
},
|
||||
"resources": {
|
||||
"name": "resources",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.cli.resources",
|
||||
"signature": "<bound method Alias.signature of Alias('resources', 'importlib.resources')>",
|
||||
"docstring": null
|
||||
},
|
||||
"DEFAULT_TEMPLATE": {
|
||||
"name": "DEFAULT_TEMPLATE",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.cli.DEFAULT_TEMPLATE",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"available_templates": {
|
||||
"name": "available_templates",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.cli.available_templates",
|
||||
"signature": "<bound method Function.signature of Function('available_templates', 21, 34)>",
|
||||
"docstring": "Return a list of available application templates.\n\nReturns:\n list[str]:\n Sorted list of template names found in the internal templates directory."
|
||||
},
|
||||
"copy_template": {
|
||||
"name": "copy_template",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.cli.copy_template",
|
||||
"signature": "<bound method Function.signature of Function('copy_template', 37, 64)>",
|
||||
"docstring": "Copy a bundled OpenAPI-first application template into a directory.\n\nArgs:\n template (str):\n Name of the template to copy.\n target_dir (Path):\n Filesystem path where the template should be copied.\n\nRaises:\n FileNotFoundError:\n If the requested template does not exist."
|
||||
},
|
||||
"main": {
|
||||
"name": "main",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.cli.main",
|
||||
"signature": "<bound method Function.signature of Function('main', 67, 103)>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
mcp_docs/modules/openapi_first.client.json
Normal file
102
mcp_docs/modules/openapi_first.client.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"module": "openapi_first.client",
|
||||
"content": {
|
||||
"path": "openapi_first.client",
|
||||
"docstring": "# Summary\n\nOpenAPI-first HTTP client for contract-driven services.\n\nThis module provides `OpenAPIClient`, a thin, strict HTTP client that\nderives all callable operations directly from an OpenAPI 3.x specification.\n\nIt is the client counterpart to `OpenAPIFirstApp`.\n\nNotes:\n **Core Principles:**\n\n - The OpenAPI specification is the single source of truth\n - Each operationId becomes a callable Python method\n - No implicit schema mutation or inference\n - No code generation step\n - Minimal abstraction over httpx\n\n **Responsibilities:**\n\n - Parses an OpenAPI 3.x specification\n - Dynamically creates one callable per operationId\n - Enforces presence of servers, paths, and operationId\n - Formats path parameters safely\n - Handles JSON request bodies explicitly\n - Returns raw `httpx.Response` objects\n\n **Constraints:**\n\n - This module intentionally does NOT: Generate client code, validate request/response schemas, deserialize responses, retry requests, implement authentication helpers, or assume non-2xx responses are failures.",
|
||||
"objects": {
|
||||
"Any": {
|
||||
"name": "Any",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.client.Any",
|
||||
"signature": "<bound method Alias.signature of Alias('Any', 'typing.Any')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Callable": {
|
||||
"name": "Callable",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.client.Callable",
|
||||
"signature": "<bound method Alias.signature of Alias('Callable', 'typing.Callable')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Dict": {
|
||||
"name": "Dict",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.client.Dict",
|
||||
"signature": "<bound method Alias.signature of Alias('Dict', 'typing.Dict')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Optional": {
|
||||
"name": "Optional",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.client.Optional",
|
||||
"signature": "<bound method Alias.signature of Alias('Optional', 'typing.Optional')>",
|
||||
"docstring": null
|
||||
},
|
||||
"urljoin": {
|
||||
"name": "urljoin",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.client.urljoin",
|
||||
"signature": "<bound method Alias.signature of Alias('urljoin', 'urllib.parse.urljoin')>",
|
||||
"docstring": null
|
||||
},
|
||||
"httpx": {
|
||||
"name": "httpx",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.client.httpx",
|
||||
"signature": "<bound method Alias.signature of Alias('httpx', 'httpx')>",
|
||||
"docstring": null
|
||||
},
|
||||
"OpenAPIFirstError": {
|
||||
"name": "OpenAPIFirstError",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.client.OpenAPIFirstError",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstError', 'openapi_first.errors.OpenAPIFirstError')>",
|
||||
"docstring": "Base exception for all OpenAPI-first enforcement errors.\n\nNotes:\n **Responsibilities:**\n\n - This exception exists to allow callers, test suites, and CI\n pipelines to catch and distinguish OpenAPI contract violations\n from unrelated runtime errors.\n - All exceptions raised by the OpenAPI-first core should inherit\n from this type."
|
||||
},
|
||||
"OpenAPIClientError": {
|
||||
"name": "OpenAPIClientError",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.client.OpenAPIClientError",
|
||||
"signature": "<bound method Class.signature of Class('OpenAPIClientError', 42, 45)>",
|
||||
"docstring": "Raised when an OpenAPI client operation fails."
|
||||
},
|
||||
"OpenAPIClient": {
|
||||
"name": "OpenAPIClient",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.client.OpenAPIClient",
|
||||
"signature": "<bound method Class.signature of Class('OpenAPIClient', 48, 257)>",
|
||||
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
|
||||
"members": {
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.client.OpenAPIClient.spec",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.client.OpenAPIClient.base_url",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.client.OpenAPIClient.client",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"operations": {
|
||||
"name": "operations",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.client.OpenAPIClient.operations",
|
||||
"signature": "<bound method Function.signature of Function('operations', 126, 127)>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
mcp_docs/modules/openapi_first.errors.json
Normal file
30
mcp_docs/modules/openapi_first.errors.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"module": "openapi_first.errors",
|
||||
"content": {
|
||||
"path": "openapi_first.errors",
|
||||
"docstring": "# Summary\n\nExceptions for OpenAPI-first FastAPI applications.\n\nThis module defines a small hierarchy of explicit, intention-revealing\nexceptions used to signal contract violations between an OpenAPI\nspecification and its Python implementation.\n\nNotes:\n **Design Principles:**\n\n - Errors represent programmer mistakes, not runtime conditions.\n - All errors are raised during application startup.\n - Messages are actionable and suitable for CI/CD output.\n - Exceptions are explicit rather than reused from generic built-ins.\n\n These errors should normally cause immediate application failure.",
|
||||
"objects": {
|
||||
"OpenAPIFirstError": {
|
||||
"name": "OpenAPIFirstError",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.errors.OpenAPIFirstError",
|
||||
"signature": "<bound method Class.signature of Class('OpenAPIFirstError', 21, 34)>",
|
||||
"docstring": "Base exception for all OpenAPI-first enforcement errors.\n\nNotes:\n **Responsibilities:**\n\n - This exception exists to allow callers, test suites, and CI\n pipelines to catch and distinguish OpenAPI contract violations\n from unrelated runtime errors.\n - All exceptions raised by the OpenAPI-first core should inherit\n from this type."
|
||||
},
|
||||
"MissingOperationHandler": {
|
||||
"name": "MissingOperationHandler",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.errors.MissingOperationHandler",
|
||||
"signature": "<bound method Class.signature of Class('MissingOperationHandler', 37, 78)>",
|
||||
"docstring": "Raised when an OpenAPI operation cannot be resolved to a handler.\n\nNotes:\n **Scenarios:**\n\n - An OpenAPI operation does not define an `operationId`.\n - An `operationId` is defined but no matching function exists in\n the provided routes module.\n\n **Guarantees:**\n\n - This represents a violation of the OpenAPI-first contract and\n indicates that the specification and implementation are out of\n sync."
|
||||
},
|
||||
"Optional": {
|
||||
"name": "Optional",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.errors.Optional",
|
||||
"signature": "<bound method Alias.signature of Alias('Optional', 'typing.Optional')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1091
mcp_docs/modules/openapi_first.json
Normal file
1091
mcp_docs/modules/openapi_first.json
Normal file
File diff suppressed because it is too large
Load Diff
79
mcp_docs/modules/openapi_first.loader.json
Normal file
79
mcp_docs/modules/openapi_first.loader.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"module": "openapi_first.loader",
|
||||
"content": {
|
||||
"path": "openapi_first.loader",
|
||||
"docstring": "# Summary\n\nOpenAPI specification loading and validation utilities.\n\nThis module is responsible for loading an OpenAPI 3.x specification\nfrom disk and validating it before it is used by the application.\n\nIt enforces the principle that an invalid or malformed OpenAPI document\nmust never reach the routing or runtime layers.\n\nNotes:\n **Design Principles:**\n\n - OpenAPI is treated as an authoritative contract.\n - Invalid specifications fail fast at application startup.\n - Supported formats are JSON and YAML.\n - Validation errors are surfaced clearly and early.\n\n **Constraints:**\n\n - This module intentionally does NOT:\n - Modify the OpenAPI document.\n - Infer missing fields.\n - Generate models or code.\n - Perform request/response validation at runtime.",
|
||||
"objects": {
|
||||
"json": {
|
||||
"name": "json",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.loader.json",
|
||||
"signature": "<bound method Alias.signature of Alias('json', 'json')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Path": {
|
||||
"name": "Path",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.loader.Path",
|
||||
"signature": "<bound method Alias.signature of Alias('Path', 'pathlib.Path')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Any": {
|
||||
"name": "Any",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.loader.Any",
|
||||
"signature": "<bound method Alias.signature of Alias('Any', 'typing.Any')>",
|
||||
"docstring": null
|
||||
},
|
||||
"yaml": {
|
||||
"name": "yaml",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.loader.yaml",
|
||||
"signature": "<bound method Alias.signature of Alias('yaml', 'yaml')>",
|
||||
"docstring": null
|
||||
},
|
||||
"validate_spec": {
|
||||
"name": "validate_spec",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.loader.validate_spec",
|
||||
"signature": "<bound method Alias.signature of Alias('validate_spec', 'openapi_spec_validator.validate_spec')>",
|
||||
"docstring": null
|
||||
},
|
||||
"OpenAPIFirstError": {
|
||||
"name": "OpenAPIFirstError",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.loader.OpenAPIFirstError",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstError', 'openapi_first.errors.OpenAPIFirstError')>",
|
||||
"docstring": "Base exception for all OpenAPI-first enforcement errors.\n\nNotes:\n **Responsibilities:**\n\n - This exception exists to allow callers, test suites, and CI\n pipelines to catch and distinguish OpenAPI contract violations\n from unrelated runtime errors.\n - All exceptions raised by the OpenAPI-first core should inherit\n from this type."
|
||||
},
|
||||
"OpenAPISpecLoadError": {
|
||||
"name": "OpenAPISpecLoadError",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.loader.OpenAPISpecLoadError",
|
||||
"signature": "<bound method Class.signature of Class('OpenAPISpecLoadError', 39, 49)>",
|
||||
"docstring": "Raised when an OpenAPI specification cannot be loaded or validated.\n\nNotes:\n **Guarantees:**\n\n - This error indicates that the OpenAPI document is unreadable,\n malformed, or violates the OpenAPI 3.x specification."
|
||||
},
|
||||
"load_openapi": {
|
||||
"name": "load_openapi",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.loader.load_openapi",
|
||||
"signature": "<bound method Function.signature of Function('load_openapi', 52, 109)>",
|
||||
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
|
||||
},
|
||||
"Dict": {
|
||||
"name": "Dict",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.loader.Dict",
|
||||
"signature": "<bound method Alias.signature of Alias('Dict', 'typing.Dict')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Union": {
|
||||
"name": "Union",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.loader.Union",
|
||||
"signature": "<bound method Alias.signature of Alias('Union', 'typing.Union')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
mcp_docs/modules/openapi_first.templates.crud_app.data.json
Normal file
51
mcp_docs/modules/openapi_first.templates.crud_app.data.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"module": "openapi_first.templates.crud_app.data",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.crud_app.data",
|
||||
"docstring": "In-memory mock data store for CRUD example.\n\nThis module intentionally avoids persistence and concurrency guarantees.\nIt is suitable for demos, tests, and scaffolding only.\n\nIt intentionally avoids\n- persistence\n- concurrency guarantees\n- validation\n- error handling\n\nThe implementation is suitable for:\n- demonstrations\n- tests\n- scaffolding and example services\n\nIt is explicitly NOT suitable for production use.\n\nThis module is not part of the ``openapi_first`` library API surface.",
|
||||
"objects": {
|
||||
"Dict": {
|
||||
"name": "Dict",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.data.Dict",
|
||||
"signature": "<bound method Alias.signature of Alias('Dict', 'typing.Dict')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 36, 48)>",
|
||||
"docstring": "Return all items in the data store.\n\nThis function performs no filtering, pagination, or sorting.\nThe returned collection reflects the current in-memory state.\n\nReturns\n-------\nlist[dict]\n A list of item representations."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 51, 68)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nThis function assumes the item exists and will raise ``KeyError``\nif the ID is not present in the store.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\ndict\n The stored item representation."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 71, 92)>",
|
||||
"docstring": "Create a new item in the data store.\n\nA new integer ID is assigned automatically. No validation is\nperformed on the provided payload.\n\nParameters\n----------\npayload : dict\n Item attributes excluding the ``id`` field.\n\nReturns\n-------\ndict\n The newly created item, including its assigned ID."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 95, 117)>",
|
||||
"docstring": "Replace an existing item in the data store.\n\nThis function overwrites the existing item entirely and does not\nperform partial updates or validation. If the item does not exist,\nit will be created implicitly.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : dict\n Item attributes excluding the ``id`` field.\n\nReturns\n-------\ndict\n The updated item representation."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 120, 132)>",
|
||||
"docstring": "Remove an item from the data store.\n\nThis function assumes the item exists and will raise ``KeyError``\nif the ID is not present.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
273
mcp_docs/modules/openapi_first.templates.crud_app.json
Normal file
273
mcp_docs/modules/openapi_first.templates.crud_app.json
Normal file
@@ -0,0 +1,273 @@
|
||||
{
|
||||
"module": "openapi_first.templates.crud_app",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.crud_app",
|
||||
"docstring": "OpenAPI-first CRUD application template.\n\nThis package contains a complete, minimal example of an OpenAPI-first\nCRUD service built using the ``openapi_first`` library.\n\nThe application is assembled exclusively from:\n- an OpenAPI specification (``openapi.yaml``)\n- a handler namespace implementing CRUD operations (``routes``)\n- an in-memory mock data store (``data``)\n\nAll HTTP routes, methods, schemas, and operation bindings are defined\nin the OpenAPI specification and enforced at application startup.\nNo decorator-driven routing or implicit framework behavior is used.\n\nThis template demonstrates:\n- operationId-driven server-side route binding\n- explicit HTTP status code control in handlers\n- operationId-driven client usage against the same OpenAPI contract\n- end-to-end validation using in-memory data and tests\n\n----------------------------------------------------------------------\nScaffolding via CLI\n----------------------------------------------------------------------\n\nCreate a new CRUD example service using the bundled template:\n\n openapi-first crud_app\n\nCreate the service in a custom directory:\n\n openapi-first crud_app my-crud-service\n\nList all available application templates:\n\n openapi-first --list\n\nThe CLI copies template files verbatim into the target directory.\nNo code is generated or modified beyond the copied scaffold.\n\n----------------------------------------------------------------------\nClient Usage Example\n----------------------------------------------------------------------\n\nThe same OpenAPI specification used by the server can be used to\nconstruct a strict, operationId-driven HTTP client.\n\nExample client calls for CRUD operations:\n\n from openapi_first.loader import load_openapi\n from openapi_first.client import OpenAPIClient\n\n spec = load_openapi(\"openapi.yaml\")\n client = OpenAPIClient(spec)\n\n # List items\n response = client.list_items()\n\n # Get item by ID\n response = client.get_item(\n path_params={\"item_id\": 1}\n )\n\n # Create item\n response = client.create_item(\n body={\"name\": \"Orange\", \"price\": 0.8}\n )\n\n # Update item\n response = client.update_item(\n path_params={\"item_id\": 1},\n body={\"name\": \"Green Apple\", \"price\": 0.6},\n )\n\n # Delete item\n response = client.delete_item(\n path_params={\"item_id\": 1}\n )\n\nClient guarantees:\n- One callable per OpenAPI ``operationId``\n- No hardcoded URLs or HTTP methods in user code\n- Path and request parameters must match the OpenAPI specification\n- Invalid or incomplete OpenAPI specs fail at client construction time\n\n----------------------------------------------------------------------\nNon-Goals\n----------------------------------------------------------------------\n\nThis template is intentionally minimal and is NOT:\n- production-ready\n- persistent or concurrency-safe\n- a reference architecture for data storage\n\nIt exists solely as a copyable example for learning, testing, and\nbootstrapping OpenAPI-first services.\n\nThis package is not part of the ``openapi_first`` library API surface.",
|
||||
"objects": {
|
||||
"data": {
|
||||
"name": "data",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.crud_app.data",
|
||||
"signature": null,
|
||||
"docstring": "In-memory mock data store for CRUD example.\n\nThis module intentionally avoids persistence and concurrency guarantees.\nIt is suitable for demos, tests, and scaffolding only.\n\nIt intentionally avoids\n- persistence\n- concurrency guarantees\n- validation\n- error handling\n\nThe implementation is suitable for:\n- demonstrations\n- tests\n- scaffolding and example services\n\nIt is explicitly NOT suitable for production use.\n\nThis module is not part of the ``openapi_first`` library API surface.",
|
||||
"members": {
|
||||
"Dict": {
|
||||
"name": "Dict",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.data.Dict",
|
||||
"signature": "<bound method Alias.signature of Alias('Dict', 'typing.Dict')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 36, 48)>",
|
||||
"docstring": "Return all items in the data store.\n\nThis function performs no filtering, pagination, or sorting.\nThe returned collection reflects the current in-memory state.\n\nReturns\n-------\nlist[dict]\n A list of item representations."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 51, 68)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nThis function assumes the item exists and will raise ``KeyError``\nif the ID is not present in the store.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\ndict\n The stored item representation."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 71, 92)>",
|
||||
"docstring": "Create a new item in the data store.\n\nA new integer ID is assigned automatically. No validation is\nperformed on the provided payload.\n\nParameters\n----------\npayload : dict\n Item attributes excluding the ``id`` field.\n\nReturns\n-------\ndict\n The newly created item, including its assigned ID."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 95, 117)>",
|
||||
"docstring": "Replace an existing item in the data store.\n\nThis function overwrites the existing item entirely and does not\nperform partial updates or validation. If the item does not exist,\nit will be created implicitly.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : dict\n Item attributes excluding the ``id`` field.\n\nReturns\n-------\ndict\n The updated item representation."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 120, 132)>",
|
||||
"docstring": "Remove an item from the data store.\n\nThis function assumes the item exists and will raise ``KeyError``\nif the ID is not present.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete."
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"name": "main",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.crud_app.main",
|
||||
"signature": null,
|
||||
"docstring": "Application entry point for an OpenAPI-first CRUD example service.\n\nThis module constructs a FastAPI application exclusively from an\nOpenAPI specification and a handler namespace, without using\ndecorator-driven routing.\n\nAll HTTP routes, methods, request/response schemas, and operation\nbindings are defined in the OpenAPI document referenced by\n``openapi_path``. Python callables defined in the ``routes`` module are\nbound to OpenAPI operations strictly via ``operationId``.\n\nThis module contains no routing logic, persistence concerns, or\nframework configuration beyond application assembly.\n\nDesign guarantees:\n- OpenAPI is the single source of truth\n- No undocumented routes can exist\n- Every OpenAPI operationId must resolve to exactly one handler\n- All contract violations fail at application startup\n\nThis file is intended to be used as the ASGI entry point.\n\nExample:\n uvicorn main:app",
|
||||
"members": {
|
||||
"OpenAPIFirstApp": {
|
||||
"name": "OpenAPIFirstApp",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.crud_app.main.OpenAPIFirstApp",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
|
||||
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
|
||||
"members": {
|
||||
"openapi": {
|
||||
"name": "openapi",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.main.OpenAPIFirstApp.openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('openapi', 'openapi_first.app.OpenAPIFirstApp.openapi')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.main.routes",
|
||||
"signature": "<bound method Alias.signature of Alias('routes', 'routes')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.main.app",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.crud_app.routes",
|
||||
"signature": null,
|
||||
"docstring": "CRUD route handlers bound via OpenAPI operationId.\n\nThese handlers explicitly control HTTP status codes to ensure\nruntime behavior matches the OpenAPI contract.\n\nThis module defines OpenAPI-bound operation handlers for a simple CRUD\nservice. Functions in this module are bound to HTTP routes exclusively\nvia OpenAPI ``operationId`` values.\n\nHandlers explicitly control HTTP response status codes to ensure runtime\nbehavior matches the OpenAPI contract. Error conditions are translated\ninto explicit HTTP responses rather than relying on implicit framework\nbehavior.\n\nNo routing decorators or path definitions appear in this module. All\nrouting, HTTP methods, and schemas are defined in the OpenAPI\nspecification.",
|
||||
"members": {
|
||||
"Response": {
|
||||
"name": "Response",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.routes.Response",
|
||||
"signature": "<bound method Alias.signature of Alias('Response', 'fastapi.Response')>",
|
||||
"docstring": null
|
||||
},
|
||||
"HTTPException": {
|
||||
"name": "HTTPException",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.routes.HTTPException",
|
||||
"signature": "<bound method Alias.signature of Alias('HTTPException', 'fastapi.HTTPException')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 32, 44)>",
|
||||
"docstring": "List all items.\n\nImplements the OpenAPI operation identified by\n``operationId: list_items``.\n\nReturns\n-------\nlist[dict]\n A list of item representations."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 47, 72)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nImplements the OpenAPI operation identified by\n``operationId: get_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\ndict\n The requested item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 75, 96)>",
|
||||
"docstring": "Create a new item.\n\nImplements the OpenAPI operation identified by\n``operationId: create_item``.\n\nParameters\n----------\npayload : dict\n Item attributes excluding the ``id`` field.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\ndict\n The newly created item."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 99, 126)>",
|
||||
"docstring": "Update an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: update_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : dict\n Item attributes excluding the ``id`` field.\n\nReturns\n-------\ndict\n The updated item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 129, 158)>",
|
||||
"docstring": "Delete an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: delete_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\nNone\n No content.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"test_crud_app": {
|
||||
"name": "test_crud_app",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app",
|
||||
"signature": null,
|
||||
"docstring": "End-to-end tests for the OpenAPI-first CRUD example app.\n\nThese tests validate that all CRUD operations behave correctly\nagainst the in-memory mock data store.\n- OpenAPI specification loading\n- OperationId-driven route binding on the server\n- OperationId-driven client invocation\n- Correct HTTP status codes and response payloads\n\nThe tests exercise all CRUD operations against an in-memory mock data\nstore and assume deterministic behavior within a single process.\n\nThe tests assume:\n- OpenAPI-first route binding\n- In-memory storage (no persistence guarantees)\n- Deterministic behavior in a single process\n- One-to-one correspondence between OpenAPI operationId values and\n server/client callables",
|
||||
"members": {
|
||||
"TestClient": {
|
||||
"name": "TestClient",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.TestClient",
|
||||
"signature": "<bound method Alias.signature of Alias('TestClient', 'fastapi.testclient.TestClient')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.app",
|
||||
"signature": "<bound method Alias.signature of Alias('app', 'main.app')>",
|
||||
"docstring": null
|
||||
},
|
||||
"load_openapi": {
|
||||
"name": "load_openapi",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.load_openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
|
||||
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
|
||||
},
|
||||
"OpenAPIClient": {
|
||||
"name": "OpenAPIClient",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
|
||||
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
|
||||
"members": {
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.spec",
|
||||
"signature": "<bound method Alias.signature of Alias('spec', 'openapi_first.client.OpenAPIClient.spec')>",
|
||||
"docstring": null
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.base_url",
|
||||
"signature": "<bound method Alias.signature of Alias('base_url', 'openapi_first.client.OpenAPIClient.base_url')>",
|
||||
"docstring": null
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.client",
|
||||
"signature": "<bound method Alias.signature of Alias('client', 'openapi_first.client.OpenAPIClient.client')>",
|
||||
"docstring": null
|
||||
},
|
||||
"operations": {
|
||||
"name": "operations",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.operations",
|
||||
"signature": "<bound method Alias.signature of Alias('operations', 'openapi_first.client.OpenAPIClient.operations')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.client",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.spec",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"test_list_items_initial": {
|
||||
"name": "test_list_items_initial",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_list_items_initial",
|
||||
"signature": "<bound method Function.signature of Function('test_list_items_initial', 38, 49)>",
|
||||
"docstring": "Initial items should be present."
|
||||
},
|
||||
"test_get_item": {
|
||||
"name": "test_get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_get_item",
|
||||
"signature": "<bound method Function.signature of Function('test_get_item', 52, 62)>",
|
||||
"docstring": "Existing item should be retrievable by ID."
|
||||
},
|
||||
"test_create_item": {
|
||||
"name": "test_create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_create_item",
|
||||
"signature": "<bound method Function.signature of Function('test_create_item', 65, 85)>",
|
||||
"docstring": "Creating a new item should return the created entity."
|
||||
},
|
||||
"test_update_item": {
|
||||
"name": "test_update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_update_item",
|
||||
"signature": "<bound method Function.signature of Function('test_update_item', 88, 112)>",
|
||||
"docstring": "Updating an item should replace its values."
|
||||
},
|
||||
"test_delete_item": {
|
||||
"name": "test_delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_delete_item",
|
||||
"signature": "<bound method Function.signature of Function('test_delete_item', 115, 125)>",
|
||||
"docstring": "Deleting an item should remove it from the store."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
mcp_docs/modules/openapi_first.templates.crud_app.main.json
Normal file
39
mcp_docs/modules/openapi_first.templates.crud_app.main.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"module": "openapi_first.templates.crud_app.main",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.crud_app.main",
|
||||
"docstring": "Application entry point for an OpenAPI-first CRUD example service.\n\nThis module constructs a FastAPI application exclusively from an\nOpenAPI specification and a handler namespace, without using\ndecorator-driven routing.\n\nAll HTTP routes, methods, request/response schemas, and operation\nbindings are defined in the OpenAPI document referenced by\n``openapi_path``. Python callables defined in the ``routes`` module are\nbound to OpenAPI operations strictly via ``operationId``.\n\nThis module contains no routing logic, persistence concerns, or\nframework configuration beyond application assembly.\n\nDesign guarantees:\n- OpenAPI is the single source of truth\n- No undocumented routes can exist\n- Every OpenAPI operationId must resolve to exactly one handler\n- All contract violations fail at application startup\n\nThis file is intended to be used as the ASGI entry point.\n\nExample:\n uvicorn main:app",
|
||||
"objects": {
|
||||
"OpenAPIFirstApp": {
|
||||
"name": "OpenAPIFirstApp",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.crud_app.main.OpenAPIFirstApp",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
|
||||
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
|
||||
"members": {
|
||||
"openapi": {
|
||||
"name": "openapi",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.main.OpenAPIFirstApp.openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('openapi', 'openapi_first.app.OpenAPIFirstApp.openapi')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.main.routes",
|
||||
"signature": "<bound method Alias.signature of Alias('routes', 'routes')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.main.app",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"module": "openapi_first.templates.crud_app.routes",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.crud_app.routes",
|
||||
"docstring": "CRUD route handlers bound via OpenAPI operationId.\n\nThese handlers explicitly control HTTP status codes to ensure\nruntime behavior matches the OpenAPI contract.\n\nThis module defines OpenAPI-bound operation handlers for a simple CRUD\nservice. Functions in this module are bound to HTTP routes exclusively\nvia OpenAPI ``operationId`` values.\n\nHandlers explicitly control HTTP response status codes to ensure runtime\nbehavior matches the OpenAPI contract. Error conditions are translated\ninto explicit HTTP responses rather than relying on implicit framework\nbehavior.\n\nNo routing decorators or path definitions appear in this module. All\nrouting, HTTP methods, and schemas are defined in the OpenAPI\nspecification.",
|
||||
"objects": {
|
||||
"Response": {
|
||||
"name": "Response",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.routes.Response",
|
||||
"signature": "<bound method Alias.signature of Alias('Response', 'fastapi.Response')>",
|
||||
"docstring": null
|
||||
},
|
||||
"HTTPException": {
|
||||
"name": "HTTPException",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.routes.HTTPException",
|
||||
"signature": "<bound method Alias.signature of Alias('HTTPException', 'fastapi.HTTPException')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 32, 44)>",
|
||||
"docstring": "List all items.\n\nImplements the OpenAPI operation identified by\n``operationId: list_items``.\n\nReturns\n-------\nlist[dict]\n A list of item representations."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 47, 72)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nImplements the OpenAPI operation identified by\n``operationId: get_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\ndict\n The requested item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 75, 96)>",
|
||||
"docstring": "Create a new item.\n\nImplements the OpenAPI operation identified by\n``operationId: create_item``.\n\nParameters\n----------\npayload : dict\n Item attributes excluding the ``id`` field.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\ndict\n The newly created item."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 99, 126)>",
|
||||
"docstring": "Update an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: update_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : dict\n Item attributes excluding the ``id`` field.\n\nReturns\n-------\ndict\n The updated item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 129, 158)>",
|
||||
"docstring": "Delete an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: delete_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\nNone\n No content.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"module": "openapi_first.templates.crud_app.test_crud_app",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app",
|
||||
"docstring": "End-to-end tests for the OpenAPI-first CRUD example app.\n\nThese tests validate that all CRUD operations behave correctly\nagainst the in-memory mock data store.\n- OpenAPI specification loading\n- OperationId-driven route binding on the server\n- OperationId-driven client invocation\n- Correct HTTP status codes and response payloads\n\nThe tests exercise all CRUD operations against an in-memory mock data\nstore and assume deterministic behavior within a single process.\n\nThe tests assume:\n- OpenAPI-first route binding\n- In-memory storage (no persistence guarantees)\n- Deterministic behavior in a single process\n- One-to-one correspondence between OpenAPI operationId values and\n server/client callables",
|
||||
"objects": {
|
||||
"TestClient": {
|
||||
"name": "TestClient",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.TestClient",
|
||||
"signature": "<bound method Alias.signature of Alias('TestClient', 'fastapi.testclient.TestClient')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.app",
|
||||
"signature": "<bound method Alias.signature of Alias('app', 'main.app')>",
|
||||
"docstring": null
|
||||
},
|
||||
"load_openapi": {
|
||||
"name": "load_openapi",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.load_openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
|
||||
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
|
||||
},
|
||||
"OpenAPIClient": {
|
||||
"name": "OpenAPIClient",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
|
||||
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
|
||||
"members": {
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.spec",
|
||||
"signature": "<bound method Alias.signature of Alias('spec', 'openapi_first.client.OpenAPIClient.spec')>",
|
||||
"docstring": null
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.base_url",
|
||||
"signature": "<bound method Alias.signature of Alias('base_url', 'openapi_first.client.OpenAPIClient.base_url')>",
|
||||
"docstring": null
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.client",
|
||||
"signature": "<bound method Alias.signature of Alias('client', 'openapi_first.client.OpenAPIClient.client')>",
|
||||
"docstring": null
|
||||
},
|
||||
"operations": {
|
||||
"name": "operations",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.operations",
|
||||
"signature": "<bound method Alias.signature of Alias('operations', 'openapi_first.client.OpenAPIClient.operations')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.client",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.spec",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"test_list_items_initial": {
|
||||
"name": "test_list_items_initial",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_list_items_initial",
|
||||
"signature": "<bound method Function.signature of Function('test_list_items_initial', 38, 49)>",
|
||||
"docstring": "Initial items should be present."
|
||||
},
|
||||
"test_get_item": {
|
||||
"name": "test_get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_get_item",
|
||||
"signature": "<bound method Function.signature of Function('test_get_item', 52, 62)>",
|
||||
"docstring": "Existing item should be retrievable by ID."
|
||||
},
|
||||
"test_create_item": {
|
||||
"name": "test_create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_create_item",
|
||||
"signature": "<bound method Function.signature of Function('test_create_item', 65, 85)>",
|
||||
"docstring": "Creating a new item should return the created entity."
|
||||
},
|
||||
"test_update_item": {
|
||||
"name": "test_update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_update_item",
|
||||
"signature": "<bound method Function.signature of Function('test_update_item', 88, 112)>",
|
||||
"docstring": "Updating an item should replace its values."
|
||||
},
|
||||
"test_delete_item": {
|
||||
"name": "test_delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_delete_item",
|
||||
"signature": "<bound method Function.signature of Function('test_delete_item', 115, 125)>",
|
||||
"docstring": "Deleting an item should remove it from the store."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
mcp_docs/modules/openapi_first.templates.health_app.json
Normal file
64
mcp_docs/modules/openapi_first.templates.health_app.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"module": "openapi_first.templates.health_app",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.health_app",
|
||||
"docstring": "OpenAPI-first FastAPI application template.\n\nThis package contains a minimal, fully working example of an\nOpenAPI-first FastAPI service built using the ``openapi_first`` library.\n\nThe application is assembled exclusively from:\n- an OpenAPI specification (``openapi.yaml``)\n- a handler namespace (``routes``)\n\nNo routing decorators, implicit behavior, or framework-specific\nconvenience abstractions are used. All HTTP routes, methods, and\noperation bindings are defined in OpenAPI and enforced at application\nstartup.\n\nThis package is intended to be copied as a starting point for new\nservices via the ``openapi-first`` CLI. It is not part of the\n``openapi_first`` library API surface.\n\n----------------------------------------------------------------------\nScaffolding via CLI\n----------------------------------------------------------------------\n\nCreate a new OpenAPI-first health check service using the bundled\ntemplate:\n\n openapi-first health_app\n\nCreate the service in a custom directory:\n\n openapi-first health_app my-health-service\n\nList all available application templates:\n\n openapi-first --list\n\nThe CLI copies template files verbatim into the target directory.\nNo code is generated or modified beyond the copied scaffold.\n\n----------------------------------------------------------------------\nClient Usage Example\n----------------------------------------------------------------------\n\nThe same OpenAPI specification used by the server can be used to\nconstruct a strict, operationId-driven HTTP client.\n\nExample client call for the ``get_health`` operation:\n\n from openapi_first.loader import load_openapi\n from openapi_first.client import OpenAPIClient\n\n spec = load_openapi(\"openapi.yaml\")\n client = OpenAPIClient(spec)\n\n response = client.get_health()\n\n assert response.status_code == 200\n assert response.json() == {\"status\": \"ok\"}\n\nClient guarantees:\n- One callable per OpenAPI ``operationId``\n- No hardcoded URLs or HTTP methods in user code\n- Path and request parameters must match the OpenAPI specification\n- Invalid or incomplete OpenAPI specs fail at client construction time",
|
||||
"objects": {
|
||||
"main": {
|
||||
"name": "main",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.health_app.main",
|
||||
"signature": null,
|
||||
"docstring": "Application entry point for an OpenAPI-first FastAPI service.\n\nThis module constructs a FastAPI application exclusively from an\nOpenAPI specification and a handler namespace, without using\ndecorator-driven routing.\n\nAll HTTP routes, methods, and operation bindings are defined in the\nOpenAPI document referenced by ``openapi_path``. Python callables\ndefined in the ``routes`` module are bound to OpenAPI operations\nstrictly via ``operationId``.\n\nThis module contains no routing logic, request handling, or framework\nconfiguration beyond application assembly.\n\nDesign guarantees:\n- OpenAPI is the single source of truth\n- No undocumented routes can exist\n- Every OpenAPI operationId must resolve to exactly one handler\n- All contract violations fail at application startup\n\nThis file is intended to be used as the ASGI entry point.\n\nExample:\n uvicorn main:app",
|
||||
"members": {
|
||||
"OpenAPIFirstApp": {
|
||||
"name": "OpenAPIFirstApp",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.health_app.main.OpenAPIFirstApp",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
|
||||
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
|
||||
"members": {
|
||||
"openapi": {
|
||||
"name": "openapi",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.health_app.main.OpenAPIFirstApp.openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('openapi', 'openapi_first.app.OpenAPIFirstApp.openapi')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.health_app.main.routes",
|
||||
"signature": "<bound method Alias.signature of Alias('routes', 'routes')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.health_app.main.app",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.health_app.routes",
|
||||
"signature": null,
|
||||
"docstring": "OpenAPI operation handlers.\n\nThis module defines pure Python callables that implement OpenAPI\noperations for this service. Functions in this module are bound to HTTP\nroutes exclusively via OpenAPI ``operationId`` values.\n\nNo routing decorators, HTTP metadata, or framework-specific logic\nshould appear here. All request/response semantics are defined in the\nOpenAPI specification.\n\nThis module serves solely as an operationId namespace.",
|
||||
"members": {
|
||||
"get_health": {
|
||||
"name": "get_health",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.health_app.routes.get_health",
|
||||
"signature": "<bound method Function.signature of Function('get_health', 16, 32)>",
|
||||
"docstring": "Health check operation handler.\n\nThis function implements the OpenAPI operation identified by\n``operationId: get_health``.\n\nIt contains no routing metadata or framework-specific logic.\nRequest binding, HTTP method, and response semantics are defined\nexclusively by the OpenAPI specification.\n\nReturns\n-------\ndict\n A minimal liveness payload indicating service health."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"module": "openapi_first.templates.health_app.main",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.health_app.main",
|
||||
"docstring": "Application entry point for an OpenAPI-first FastAPI service.\n\nThis module constructs a FastAPI application exclusively from an\nOpenAPI specification and a handler namespace, without using\ndecorator-driven routing.\n\nAll HTTP routes, methods, and operation bindings are defined in the\nOpenAPI document referenced by ``openapi_path``. Python callables\ndefined in the ``routes`` module are bound to OpenAPI operations\nstrictly via ``operationId``.\n\nThis module contains no routing logic, request handling, or framework\nconfiguration beyond application assembly.\n\nDesign guarantees:\n- OpenAPI is the single source of truth\n- No undocumented routes can exist\n- Every OpenAPI operationId must resolve to exactly one handler\n- All contract violations fail at application startup\n\nThis file is intended to be used as the ASGI entry point.\n\nExample:\n uvicorn main:app",
|
||||
"objects": {
|
||||
"OpenAPIFirstApp": {
|
||||
"name": "OpenAPIFirstApp",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.health_app.main.OpenAPIFirstApp",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
|
||||
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
|
||||
"members": {
|
||||
"openapi": {
|
||||
"name": "openapi",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.health_app.main.OpenAPIFirstApp.openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('openapi', 'openapi_first.app.OpenAPIFirstApp.openapi')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.health_app.main.routes",
|
||||
"signature": "<bound method Alias.signature of Alias('routes', 'routes')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.health_app.main.app",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"module": "openapi_first.templates.health_app.routes",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.health_app.routes",
|
||||
"docstring": "OpenAPI operation handlers.\n\nThis module defines pure Python callables that implement OpenAPI\noperations for this service. Functions in this module are bound to HTTP\nroutes exclusively via OpenAPI ``operationId`` values.\n\nNo routing decorators, HTTP metadata, or framework-specific logic\nshould appear here. All request/response semantics are defined in the\nOpenAPI specification.\n\nThis module serves solely as an operationId namespace.",
|
||||
"objects": {
|
||||
"get_health": {
|
||||
"name": "get_health",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.health_app.routes.get_health",
|
||||
"signature": "<bound method Function.signature of Function('get_health', 16, 32)>",
|
||||
"docstring": "Health check operation handler.\n\nThis function implements the OpenAPI operation identified by\n``operationId: get_health``.\n\nIt contains no routing metadata or framework-specific logic.\nRequest binding, HTTP method, and response semantics are defined\nexclusively by the OpenAPI specification.\n\nReturns\n-------\ndict\n A minimal liveness payload indicating service health."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
702
mcp_docs/modules/openapi_first.templates.json
Normal file
702
mcp_docs/modules/openapi_first.templates.json
Normal file
@@ -0,0 +1,702 @@
|
||||
{
|
||||
"module": "openapi_first.templates",
|
||||
"content": {
|
||||
"path": "openapi_first.templates",
|
||||
"docstring": "Application templates for FastAPI OpenAPI First.\n\nThis package contains example and scaffolding templates intended to be\ncopied into user projects via the ``openapi-first`` CLI.\n\nTemplates in this package are:\n- Reference implementations of OpenAPI-first services\n- Not part of the ``openapi_first`` public or internal API\n- Not intended to be imported as runtime dependencies\n\nThe presence of this file exists solely to:\n- Mark the directory as an explicit Python package\n- Enable deterministic tooling behavior (documentation, packaging)\n- Avoid accidental traversal of non-package directories\n\nNo code in this package should be imported by library consumers.",
|
||||
"objects": {
|
||||
"crud_app": {
|
||||
"name": "crud_app",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.crud_app",
|
||||
"signature": null,
|
||||
"docstring": "OpenAPI-first CRUD application template.\n\nThis package contains a complete, minimal example of an OpenAPI-first\nCRUD service built using the ``openapi_first`` library.\n\nThe application is assembled exclusively from:\n- an OpenAPI specification (``openapi.yaml``)\n- a handler namespace implementing CRUD operations (``routes``)\n- an in-memory mock data store (``data``)\n\nAll HTTP routes, methods, schemas, and operation bindings are defined\nin the OpenAPI specification and enforced at application startup.\nNo decorator-driven routing or implicit framework behavior is used.\n\nThis template demonstrates:\n- operationId-driven server-side route binding\n- explicit HTTP status code control in handlers\n- operationId-driven client usage against the same OpenAPI contract\n- end-to-end validation using in-memory data and tests\n\n----------------------------------------------------------------------\nScaffolding via CLI\n----------------------------------------------------------------------\n\nCreate a new CRUD example service using the bundled template:\n\n openapi-first crud_app\n\nCreate the service in a custom directory:\n\n openapi-first crud_app my-crud-service\n\nList all available application templates:\n\n openapi-first --list\n\nThe CLI copies template files verbatim into the target directory.\nNo code is generated or modified beyond the copied scaffold.\n\n----------------------------------------------------------------------\nClient Usage Example\n----------------------------------------------------------------------\n\nThe same OpenAPI specification used by the server can be used to\nconstruct a strict, operationId-driven HTTP client.\n\nExample client calls for CRUD operations:\n\n from openapi_first.loader import load_openapi\n from openapi_first.client import OpenAPIClient\n\n spec = load_openapi(\"openapi.yaml\")\n client = OpenAPIClient(spec)\n\n # List items\n response = client.list_items()\n\n # Get item by ID\n response = client.get_item(\n path_params={\"item_id\": 1}\n )\n\n # Create item\n response = client.create_item(\n body={\"name\": \"Orange\", \"price\": 0.8}\n )\n\n # Update item\n response = client.update_item(\n path_params={\"item_id\": 1},\n body={\"name\": \"Green Apple\", \"price\": 0.6},\n )\n\n # Delete item\n response = client.delete_item(\n path_params={\"item_id\": 1}\n )\n\nClient guarantees:\n- One callable per OpenAPI ``operationId``\n- No hardcoded URLs or HTTP methods in user code\n- Path and request parameters must match the OpenAPI specification\n- Invalid or incomplete OpenAPI specs fail at client construction time\n\n----------------------------------------------------------------------\nNon-Goals\n----------------------------------------------------------------------\n\nThis template is intentionally minimal and is NOT:\n- production-ready\n- persistent or concurrency-safe\n- a reference architecture for data storage\n\nIt exists solely as a copyable example for learning, testing, and\nbootstrapping OpenAPI-first services.\n\nThis package is not part of the ``openapi_first`` library API surface.",
|
||||
"members": {
|
||||
"data": {
|
||||
"name": "data",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.crud_app.data",
|
||||
"signature": null,
|
||||
"docstring": "In-memory mock data store for CRUD example.\n\nThis module intentionally avoids persistence and concurrency guarantees.\nIt is suitable for demos, tests, and scaffolding only.\n\nIt intentionally avoids\n- persistence\n- concurrency guarantees\n- validation\n- error handling\n\nThe implementation is suitable for:\n- demonstrations\n- tests\n- scaffolding and example services\n\nIt is explicitly NOT suitable for production use.\n\nThis module is not part of the ``openapi_first`` library API surface.",
|
||||
"members": {
|
||||
"Dict": {
|
||||
"name": "Dict",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.data.Dict",
|
||||
"signature": "<bound method Alias.signature of Alias('Dict', 'typing.Dict')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 36, 48)>",
|
||||
"docstring": "Return all items in the data store.\n\nThis function performs no filtering, pagination, or sorting.\nThe returned collection reflects the current in-memory state.\n\nReturns\n-------\nlist[dict]\n A list of item representations."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 51, 68)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nThis function assumes the item exists and will raise ``KeyError``\nif the ID is not present in the store.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\ndict\n The stored item representation."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 71, 92)>",
|
||||
"docstring": "Create a new item in the data store.\n\nA new integer ID is assigned automatically. No validation is\nperformed on the provided payload.\n\nParameters\n----------\npayload : dict\n Item attributes excluding the ``id`` field.\n\nReturns\n-------\ndict\n The newly created item, including its assigned ID."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 95, 117)>",
|
||||
"docstring": "Replace an existing item in the data store.\n\nThis function overwrites the existing item entirely and does not\nperform partial updates or validation. If the item does not exist,\nit will be created implicitly.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : dict\n Item attributes excluding the ``id`` field.\n\nReturns\n-------\ndict\n The updated item representation."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.data.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 120, 132)>",
|
||||
"docstring": "Remove an item from the data store.\n\nThis function assumes the item exists and will raise ``KeyError``\nif the ID is not present.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete."
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"name": "main",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.crud_app.main",
|
||||
"signature": null,
|
||||
"docstring": "Application entry point for an OpenAPI-first CRUD example service.\n\nThis module constructs a FastAPI application exclusively from an\nOpenAPI specification and a handler namespace, without using\ndecorator-driven routing.\n\nAll HTTP routes, methods, request/response schemas, and operation\nbindings are defined in the OpenAPI document referenced by\n``openapi_path``. Python callables defined in the ``routes`` module are\nbound to OpenAPI operations strictly via ``operationId``.\n\nThis module contains no routing logic, persistence concerns, or\nframework configuration beyond application assembly.\n\nDesign guarantees:\n- OpenAPI is the single source of truth\n- No undocumented routes can exist\n- Every OpenAPI operationId must resolve to exactly one handler\n- All contract violations fail at application startup\n\nThis file is intended to be used as the ASGI entry point.\n\nExample:\n uvicorn main:app",
|
||||
"members": {
|
||||
"OpenAPIFirstApp": {
|
||||
"name": "OpenAPIFirstApp",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.crud_app.main.OpenAPIFirstApp",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
|
||||
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
|
||||
"members": {
|
||||
"openapi": {
|
||||
"name": "openapi",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.main.OpenAPIFirstApp.openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('openapi', 'openapi_first.app.OpenAPIFirstApp.openapi')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.main.routes",
|
||||
"signature": "<bound method Alias.signature of Alias('routes', 'routes')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.main.app",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.crud_app.routes",
|
||||
"signature": null,
|
||||
"docstring": "CRUD route handlers bound via OpenAPI operationId.\n\nThese handlers explicitly control HTTP status codes to ensure\nruntime behavior matches the OpenAPI contract.\n\nThis module defines OpenAPI-bound operation handlers for a simple CRUD\nservice. Functions in this module are bound to HTTP routes exclusively\nvia OpenAPI ``operationId`` values.\n\nHandlers explicitly control HTTP response status codes to ensure runtime\nbehavior matches the OpenAPI contract. Error conditions are translated\ninto explicit HTTP responses rather than relying on implicit framework\nbehavior.\n\nNo routing decorators or path definitions appear in this module. All\nrouting, HTTP methods, and schemas are defined in the OpenAPI\nspecification.",
|
||||
"members": {
|
||||
"Response": {
|
||||
"name": "Response",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.routes.Response",
|
||||
"signature": "<bound method Alias.signature of Alias('Response', 'fastapi.Response')>",
|
||||
"docstring": null
|
||||
},
|
||||
"HTTPException": {
|
||||
"name": "HTTPException",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.routes.HTTPException",
|
||||
"signature": "<bound method Alias.signature of Alias('HTTPException', 'fastapi.HTTPException')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 32, 44)>",
|
||||
"docstring": "List all items.\n\nImplements the OpenAPI operation identified by\n``operationId: list_items``.\n\nReturns\n-------\nlist[dict]\n A list of item representations."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 47, 72)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nImplements the OpenAPI operation identified by\n``operationId: get_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\ndict\n The requested item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 75, 96)>",
|
||||
"docstring": "Create a new item.\n\nImplements the OpenAPI operation identified by\n``operationId: create_item``.\n\nParameters\n----------\npayload : dict\n Item attributes excluding the ``id`` field.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\ndict\n The newly created item."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 99, 126)>",
|
||||
"docstring": "Update an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: update_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : dict\n Item attributes excluding the ``id`` field.\n\nReturns\n-------\ndict\n The updated item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.routes.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 129, 158)>",
|
||||
"docstring": "Delete an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: delete_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\nNone\n No content.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"test_crud_app": {
|
||||
"name": "test_crud_app",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app",
|
||||
"signature": null,
|
||||
"docstring": "End-to-end tests for the OpenAPI-first CRUD example app.\n\nThese tests validate that all CRUD operations behave correctly\nagainst the in-memory mock data store.\n- OpenAPI specification loading\n- OperationId-driven route binding on the server\n- OperationId-driven client invocation\n- Correct HTTP status codes and response payloads\n\nThe tests exercise all CRUD operations against an in-memory mock data\nstore and assume deterministic behavior within a single process.\n\nThe tests assume:\n- OpenAPI-first route binding\n- In-memory storage (no persistence guarantees)\n- Deterministic behavior in a single process\n- One-to-one correspondence between OpenAPI operationId values and\n server/client callables",
|
||||
"members": {
|
||||
"TestClient": {
|
||||
"name": "TestClient",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.TestClient",
|
||||
"signature": "<bound method Alias.signature of Alias('TestClient', 'fastapi.testclient.TestClient')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.app",
|
||||
"signature": "<bound method Alias.signature of Alias('app', 'main.app')>",
|
||||
"docstring": null
|
||||
},
|
||||
"load_openapi": {
|
||||
"name": "load_openapi",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.load_openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
|
||||
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
|
||||
},
|
||||
"OpenAPIClient": {
|
||||
"name": "OpenAPIClient",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
|
||||
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
|
||||
"members": {
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.spec",
|
||||
"signature": "<bound method Alias.signature of Alias('spec', 'openapi_first.client.OpenAPIClient.spec')>",
|
||||
"docstring": null
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.base_url",
|
||||
"signature": "<bound method Alias.signature of Alias('base_url', 'openapi_first.client.OpenAPIClient.base_url')>",
|
||||
"docstring": null
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.client",
|
||||
"signature": "<bound method Alias.signature of Alias('client', 'openapi_first.client.OpenAPIClient.client')>",
|
||||
"docstring": null
|
||||
},
|
||||
"operations": {
|
||||
"name": "operations",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient.operations",
|
||||
"signature": "<bound method Alias.signature of Alias('operations', 'openapi_first.client.OpenAPIClient.operations')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.client",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.spec",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"test_list_items_initial": {
|
||||
"name": "test_list_items_initial",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_list_items_initial",
|
||||
"signature": "<bound method Function.signature of Function('test_list_items_initial', 38, 49)>",
|
||||
"docstring": "Initial items should be present."
|
||||
},
|
||||
"test_get_item": {
|
||||
"name": "test_get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_get_item",
|
||||
"signature": "<bound method Function.signature of Function('test_get_item', 52, 62)>",
|
||||
"docstring": "Existing item should be retrievable by ID."
|
||||
},
|
||||
"test_create_item": {
|
||||
"name": "test_create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_create_item",
|
||||
"signature": "<bound method Function.signature of Function('test_create_item', 65, 85)>",
|
||||
"docstring": "Creating a new item should return the created entity."
|
||||
},
|
||||
"test_update_item": {
|
||||
"name": "test_update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_update_item",
|
||||
"signature": "<bound method Function.signature of Function('test_update_item', 88, 112)>",
|
||||
"docstring": "Updating an item should replace its values."
|
||||
},
|
||||
"test_delete_item": {
|
||||
"name": "test_delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.crud_app.test_crud_app.test_delete_item",
|
||||
"signature": "<bound method Function.signature of Function('test_delete_item', 115, 125)>",
|
||||
"docstring": "Deleting an item should remove it from the store."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"health_app": {
|
||||
"name": "health_app",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.health_app",
|
||||
"signature": null,
|
||||
"docstring": "OpenAPI-first FastAPI application template.\n\nThis package contains a minimal, fully working example of an\nOpenAPI-first FastAPI service built using the ``openapi_first`` library.\n\nThe application is assembled exclusively from:\n- an OpenAPI specification (``openapi.yaml``)\n- a handler namespace (``routes``)\n\nNo routing decorators, implicit behavior, or framework-specific\nconvenience abstractions are used. All HTTP routes, methods, and\noperation bindings are defined in OpenAPI and enforced at application\nstartup.\n\nThis package is intended to be copied as a starting point for new\nservices via the ``openapi-first`` CLI. It is not part of the\n``openapi_first`` library API surface.\n\n----------------------------------------------------------------------\nScaffolding via CLI\n----------------------------------------------------------------------\n\nCreate a new OpenAPI-first health check service using the bundled\ntemplate:\n\n openapi-first health_app\n\nCreate the service in a custom directory:\n\n openapi-first health_app my-health-service\n\nList all available application templates:\n\n openapi-first --list\n\nThe CLI copies template files verbatim into the target directory.\nNo code is generated or modified beyond the copied scaffold.\n\n----------------------------------------------------------------------\nClient Usage Example\n----------------------------------------------------------------------\n\nThe same OpenAPI specification used by the server can be used to\nconstruct a strict, operationId-driven HTTP client.\n\nExample client call for the ``get_health`` operation:\n\n from openapi_first.loader import load_openapi\n from openapi_first.client import OpenAPIClient\n\n spec = load_openapi(\"openapi.yaml\")\n client = OpenAPIClient(spec)\n\n response = client.get_health()\n\n assert response.status_code == 200\n assert response.json() == {\"status\": \"ok\"}\n\nClient guarantees:\n- One callable per OpenAPI ``operationId``\n- No hardcoded URLs or HTTP methods in user code\n- Path and request parameters must match the OpenAPI specification\n- Invalid or incomplete OpenAPI specs fail at client construction time",
|
||||
"members": {
|
||||
"main": {
|
||||
"name": "main",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.health_app.main",
|
||||
"signature": null,
|
||||
"docstring": "Application entry point for an OpenAPI-first FastAPI service.\n\nThis module constructs a FastAPI application exclusively from an\nOpenAPI specification and a handler namespace, without using\ndecorator-driven routing.\n\nAll HTTP routes, methods, and operation bindings are defined in the\nOpenAPI document referenced by ``openapi_path``. Python callables\ndefined in the ``routes`` module are bound to OpenAPI operations\nstrictly via ``operationId``.\n\nThis module contains no routing logic, request handling, or framework\nconfiguration beyond application assembly.\n\nDesign guarantees:\n- OpenAPI is the single source of truth\n- No undocumented routes can exist\n- Every OpenAPI operationId must resolve to exactly one handler\n- All contract violations fail at application startup\n\nThis file is intended to be used as the ASGI entry point.\n\nExample:\n uvicorn main:app",
|
||||
"members": {
|
||||
"OpenAPIFirstApp": {
|
||||
"name": "OpenAPIFirstApp",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.health_app.main.OpenAPIFirstApp",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
|
||||
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
|
||||
"members": {
|
||||
"openapi": {
|
||||
"name": "openapi",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.health_app.main.OpenAPIFirstApp.openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('openapi', 'openapi_first.app.OpenAPIFirstApp.openapi')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.health_app.main.routes",
|
||||
"signature": "<bound method Alias.signature of Alias('routes', 'routes')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.health_app.main.app",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.health_app.routes",
|
||||
"signature": null,
|
||||
"docstring": "OpenAPI operation handlers.\n\nThis module defines pure Python callables that implement OpenAPI\noperations for this service. Functions in this module are bound to HTTP\nroutes exclusively via OpenAPI ``operationId`` values.\n\nNo routing decorators, HTTP metadata, or framework-specific logic\nshould appear here. All request/response semantics are defined in the\nOpenAPI specification.\n\nThis module serves solely as an operationId namespace.",
|
||||
"members": {
|
||||
"get_health": {
|
||||
"name": "get_health",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.health_app.routes.get_health",
|
||||
"signature": "<bound method Function.signature of Function('get_health', 16, 32)>",
|
||||
"docstring": "Health check operation handler.\n\nThis function implements the OpenAPI operation identified by\n``operationId: get_health``.\n\nIt contains no routing metadata or framework-specific logic.\nRequest binding, HTTP method, and response semantics are defined\nexclusively by the OpenAPI specification.\n\nReturns\n-------\ndict\n A minimal liveness payload indicating service health."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_app": {
|
||||
"name": "model_app",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app",
|
||||
"signature": null,
|
||||
"docstring": "OpenAPI-first model-based CRUD application template.\n\nThis package contains a complete, minimal example of an OpenAPI-first\nCRUD service that uses explicit Pydantic domain models for request and\nresponse schemas.\n\nThe application is assembled exclusively from:\n- an OpenAPI specification (``openapi.yaml``)\n- a handler namespace implementing CRUD operations (``routes``)\n- Pydantic domain models (``models``)\n- an in-memory mock data store (``data``)\n\nAll HTTP routes, methods, schemas, and operation bindings are defined\nin the OpenAPI specification and enforced at application startup.\nNo decorator-driven routing or implicit framework behavior is used.\n\nThis template demonstrates:\n- operationId-driven server-side route binding\n- explicit request and response modeling with Pydantic\n- explicit HTTP status code control in handlers\n- operationId-driven client usage against the same OpenAPI contract\n- end-to-end validation using in-memory data and tests\n\n----------------------------------------------------------------------\nScaffolding via CLI\n----------------------------------------------------------------------\n\nCreate a new model-based CRUD example service using the bundled template:\n\n openapi-first model_app\n\nCreate the service in a custom directory:\n\n openapi-first model_app my-model-service\n\nList all available application templates:\n\n openapi-first --list\n\nThe CLI copies template files verbatim into the target directory.\nNo code is generated or modified beyond the copied scaffold.\n\n----------------------------------------------------------------------\nClient Usage Example\n----------------------------------------------------------------------\n\nThe same OpenAPI specification used by the server can be used to\nconstruct a strict, operationId-driven HTTP client.\n\nExample client calls for model-based CRUD operations:\n\n from openapi_first.loader import load_openapi\n from openapi_first.client import OpenAPIClient\n\n spec = load_openapi(\"openapi.yaml\")\n client = OpenAPIClient(spec)\n\n # List items\n response = client.list_items()\n\n # Get item by ID\n response = client.get_item(\n path_params={\"item_id\": 1}\n )\n\n # Create item\n response = client.create_item(\n body={\"name\": \"Orange\", \"price\": 0.8}\n )\n\n # Update item\n response = client.update_item(\n path_params={\"item_id\": 1},\n body={\"name\": \"Green Apple\", \"price\": 0.6},\n )\n\n # Delete item\n response = client.delete_item(\n path_params={\"item_id\": 1}\n )\n\nClient guarantees:\n- One callable per OpenAPI ``operationId``\n- No hardcoded URLs or HTTP methods in user code\n- Request and response payloads conform to Pydantic models\n- Invalid or incomplete OpenAPI specs fail at client construction time\n\n----------------------------------------------------------------------\nNon-Goals\n----------------------------------------------------------------------\n\nThis template is intentionally minimal and is NOT:\n- production-ready\n- persistent or concurrency-safe\n- a reference architecture for data storage\n\nIt exists solely as a copyable example for learning, testing, and\nbootstrapping OpenAPI-first services.\n\nThis package is not part of the ``openapi_first`` library API surface.",
|
||||
"members": {
|
||||
"data": {
|
||||
"name": "data",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app.data",
|
||||
"signature": null,
|
||||
"docstring": "In-memory data store using Pydantic models.\n\nThis module is NOT thread-safe and is intended for demos and scaffolds only.\nThis module provides a minimal, process-local data store for the\nmodel-based CRUD example application. It stores and returns domain\nobjects defined using Pydantic models and is intended solely for\ndemonstration and scaffolding purposes.\n\nThe implementation intentionally avoids:\n- persistence\n- concurrency guarantees\n- transactional semantics\n- validation beyond what Pydantic provides\n\nIt is not part of the ``openapi_first`` library API surface.",
|
||||
"members": {
|
||||
"Dict": {
|
||||
"name": "Dict",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.data.Dict",
|
||||
"signature": "<bound method Alias.signature of Alias('Dict', 'typing.Dict')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Item": {
|
||||
"name": "Item",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.data.Item",
|
||||
"signature": "<bound method Alias.signature of Alias('Item', 'models.Item')>",
|
||||
"docstring": null
|
||||
},
|
||||
"ItemCreate": {
|
||||
"name": "ItemCreate",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.data.ItemCreate",
|
||||
"signature": "<bound method Alias.signature of Alias('ItemCreate', 'models.ItemCreate')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 34, 43)>",
|
||||
"docstring": "Return all items in the data store.\n\nReturns\n-------\nlist[Item]\n A list of item domain objects."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 46, 65)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\nItem\n The requested item.\n\nRaises\n------\nKeyError\n If the item does not exist."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 68, 89)>",
|
||||
"docstring": "Create a new item in the data store.\n\nA new identifier is assigned automatically. No additional validation\nis performed beyond Pydantic model validation.\n\nParameters\n----------\npayload : ItemCreate\n Data required to create a new item.\n\nReturns\n-------\nItem\n The newly created item."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 92, 120)>",
|
||||
"docstring": "Replace an existing item in the data store.\n\nThis function performs a full replacement of the stored item.\nPartial updates are not supported.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : ItemCreate\n New item data.\n\nReturns\n-------\nItem\n The updated item.\n\nRaises\n------\nKeyError\n If the item does not exist."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 123, 137)>",
|
||||
"docstring": "Remove an item from the data store.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete.\n\nRaises\n------\nKeyError\n If the item does not exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"name": "main",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app.main",
|
||||
"signature": null,
|
||||
"docstring": "Application entry point for an OpenAPI-first model-based CRUD example service.\n\nThis module constructs a FastAPI application exclusively from an\nOpenAPI specification and a handler namespace, without using\ndecorator-driven routing.\n\nAll HTTP routes, methods, request/response schemas, and operation\nbindings are defined in the OpenAPI document referenced by\n``openapi_path``. Python callables defined in the ``routes`` module are\nbound to OpenAPI operations strictly via ``operationId``.\n\nThis module contains no routing logic, persistence concerns, or\nframework configuration beyond application assembly.\n\nDesign guarantees:\n- OpenAPI is the single source of truth\n- No undocumented routes can exist\n- Every OpenAPI operationId must resolve to exactly one handler\n- All contract violations fail at application startup\n\nThis file is intended to be used as the ASGI entry point.\n\nExample:\n uvicorn main:app",
|
||||
"members": {
|
||||
"OpenAPIFirstApp": {
|
||||
"name": "OpenAPIFirstApp",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.main.OpenAPIFirstApp",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
|
||||
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
|
||||
"members": {
|
||||
"openapi": {
|
||||
"name": "openapi",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.main.OpenAPIFirstApp.openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('openapi', 'openapi_first.app.OpenAPIFirstApp.openapi')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.main.routes",
|
||||
"signature": "<bound method Alias.signature of Alias('routes', 'routes')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.main.app",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
"name": "models",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app.models",
|
||||
"signature": null,
|
||||
"docstring": "Pydantic domain models for the CRUD example.\n\nThis module defines Pydantic models that represent the domain entities\nused by the service. These models are referenced by the OpenAPI\nspecification for request and response schemas.\n\nThe models are declarative and framework-agnostic. They contain no\npersistence logic, validation beyond type constraints, or business\nbehavior.\n\nThis module is not part of the ``openapi_first`` library API surface.\nIt exists solely to support the example application template.",
|
||||
"members": {
|
||||
"BaseModel": {
|
||||
"name": "BaseModel",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.models.BaseModel",
|
||||
"signature": "<bound method Alias.signature of Alias('BaseModel', 'pydantic.BaseModel')>",
|
||||
"docstring": null
|
||||
},
|
||||
"ItemBase": {
|
||||
"name": "ItemBase",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.models.ItemBase",
|
||||
"signature": "<bound method Class.signature of Class('ItemBase', 19, 27)>",
|
||||
"docstring": "Base domain model for an item.\n\nDefines fields common to all item representations.",
|
||||
"members": {
|
||||
"name": {
|
||||
"name": "name",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.models.ItemBase.name",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"price": {
|
||||
"name": "price",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.models.ItemBase.price",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ItemCreate": {
|
||||
"name": "ItemCreate",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.models.ItemCreate",
|
||||
"signature": "<bound method Class.signature of Class('ItemCreate', 30, 39)>",
|
||||
"docstring": "Domain model for item creation requests.\n\nThis model is used for request bodies when creating new items.\nIt intentionally excludes the ``id`` field, which is assigned\nby the service."
|
||||
},
|
||||
"Item": {
|
||||
"name": "Item",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.models.Item",
|
||||
"signature": "<bound method Class.signature of Class('Item', 42, 50)>",
|
||||
"docstring": "Domain model for a persisted item.\n\nThis model represents the full item state returned in responses,\nincluding the server-assigned identifier.",
|
||||
"members": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.models.Item.id",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app.routes",
|
||||
"signature": null,
|
||||
"docstring": "CRUD route handlers bound via OpenAPI operationId.\n\nThis module defines OpenAPI-bound operation handlers for a model-based\nCRUD service. Functions in this module are bound to HTTP routes\nexclusively via OpenAPI ``operationId`` values.\n\nHandlers explicitly control HTTP response status codes to ensure runtime\nbehavior matches the OpenAPI contract. Domain models defined using\nPydantic are used for request and response payloads.\n\nNo routing decorators, path definitions, or implicit framework behavior\nappear in this module. All routing, HTTP methods, and schemas are defined\nin the OpenAPI specification.",
|
||||
"members": {
|
||||
"Response": {
|
||||
"name": "Response",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.routes.Response",
|
||||
"signature": "<bound method Alias.signature of Alias('Response', 'fastapi.Response')>",
|
||||
"docstring": null
|
||||
},
|
||||
"HTTPException": {
|
||||
"name": "HTTPException",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.routes.HTTPException",
|
||||
"signature": "<bound method Alias.signature of Alias('HTTPException', 'fastapi.HTTPException')>",
|
||||
"docstring": null
|
||||
},
|
||||
"ItemCreate": {
|
||||
"name": "ItemCreate",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.routes.ItemCreate",
|
||||
"signature": "<bound method Alias.signature of Alias('ItemCreate', 'models.ItemCreate')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 29, 41)>",
|
||||
"docstring": "List all items.\n\nImplements the OpenAPI operation identified by\n``operationId: list_items``.\n\nReturns\n-------\nlist[Item]\n A list of item domain objects."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 44, 69)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nImplements the OpenAPI operation identified by\n``operationId: get_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\nItem\n The requested item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 72, 93)>",
|
||||
"docstring": "Create a new item.\n\nImplements the OpenAPI operation identified by\n``operationId: create_item``.\n\nParameters\n----------\npayload : ItemCreate\n Request body describing the item to create.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\nItem\n The newly created item."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 96, 123)>",
|
||||
"docstring": "Update an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: update_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : ItemCreate\n New item data.\n\nReturns\n-------\nItem\n The updated item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 126, 156)>",
|
||||
"docstring": "Delete an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: delete_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\nNone\n No content.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"test_model_app": {
|
||||
"name": "test_model_app",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app.test_model_app",
|
||||
"signature": null,
|
||||
"docstring": "End-to-end tests for the OpenAPI-first model CRUD example app.\n\nThese tests validate that all CRUD operations behave correctly\nagainst the in-memory mock data store using Pydantic models.\n- OpenAPI specification loading\n- OperationId-driven route binding on the server\n- OperationId-driven client invocation\n- Pydantic model-based request and response handling\n\nAll CRUD operations are exercised against an in-memory mock data store\nbacked by Pydantic domain models.\n\nThe tests assume:\n- OpenAPI-first route binding\n- Pydantic model validation\n- In-memory storage (no persistence guarantees)\n- Deterministic behavior in a single process",
|
||||
"members": {
|
||||
"TestClient": {
|
||||
"name": "TestClient",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.TestClient",
|
||||
"signature": "<bound method Alias.signature of Alias('TestClient', 'fastapi.testclient.TestClient')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.app",
|
||||
"signature": "<bound method Alias.signature of Alias('app', 'main.app')>",
|
||||
"docstring": null
|
||||
},
|
||||
"load_openapi": {
|
||||
"name": "load_openapi",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.load_openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
|
||||
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
|
||||
},
|
||||
"OpenAPIClient": {
|
||||
"name": "OpenAPIClient",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
|
||||
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
|
||||
"members": {
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.spec",
|
||||
"signature": "<bound method Alias.signature of Alias('spec', 'openapi_first.client.OpenAPIClient.spec')>",
|
||||
"docstring": null
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.base_url",
|
||||
"signature": "<bound method Alias.signature of Alias('base_url', 'openapi_first.client.OpenAPIClient.base_url')>",
|
||||
"docstring": null
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.client",
|
||||
"signature": "<bound method Alias.signature of Alias('client', 'openapi_first.client.OpenAPIClient.client')>",
|
||||
"docstring": null
|
||||
},
|
||||
"operations": {
|
||||
"name": "operations",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.operations",
|
||||
"signature": "<bound method Alias.signature of Alias('operations', 'openapi_first.client.OpenAPIClient.operations')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.client",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.spec",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"test_list_items_initial": {
|
||||
"name": "test_list_items_initial",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_list_items_initial",
|
||||
"signature": "<bound method Function.signature of Function('test_list_items_initial', 37, 48)>",
|
||||
"docstring": "Initial items should be present."
|
||||
},
|
||||
"test_get_item": {
|
||||
"name": "test_get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_get_item",
|
||||
"signature": "<bound method Function.signature of Function('test_get_item', 51, 61)>",
|
||||
"docstring": "Existing item should be retrievable by ID."
|
||||
},
|
||||
"test_create_item": {
|
||||
"name": "test_create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_create_item",
|
||||
"signature": "<bound method Function.signature of Function('test_create_item', 64, 84)>",
|
||||
"docstring": "Creating a new item should return the created entity."
|
||||
},
|
||||
"test_update_item": {
|
||||
"name": "test_update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_update_item",
|
||||
"signature": "<bound method Function.signature of Function('test_update_item', 87, 111)>",
|
||||
"docstring": "Updating an item should replace its values."
|
||||
},
|
||||
"test_delete_item": {
|
||||
"name": "test_delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_delete_item",
|
||||
"signature": "<bound method Function.signature of Function('test_delete_item', 114, 124)>",
|
||||
"docstring": "Deleting an item should remove it from the store."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
mcp_docs/modules/openapi_first.templates.model_app.data.json
Normal file
65
mcp_docs/modules/openapi_first.templates.model_app.data.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"module": "openapi_first.templates.model_app.data",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.model_app.data",
|
||||
"docstring": "In-memory data store using Pydantic models.\n\nThis module is NOT thread-safe and is intended for demos and scaffolds only.\nThis module provides a minimal, process-local data store for the\nmodel-based CRUD example application. It stores and returns domain\nobjects defined using Pydantic models and is intended solely for\ndemonstration and scaffolding purposes.\n\nThe implementation intentionally avoids:\n- persistence\n- concurrency guarantees\n- transactional semantics\n- validation beyond what Pydantic provides\n\nIt is not part of the ``openapi_first`` library API surface.",
|
||||
"objects": {
|
||||
"Dict": {
|
||||
"name": "Dict",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.data.Dict",
|
||||
"signature": "<bound method Alias.signature of Alias('Dict', 'typing.Dict')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Item": {
|
||||
"name": "Item",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.data.Item",
|
||||
"signature": "<bound method Alias.signature of Alias('Item', 'models.Item')>",
|
||||
"docstring": null
|
||||
},
|
||||
"ItemCreate": {
|
||||
"name": "ItemCreate",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.data.ItemCreate",
|
||||
"signature": "<bound method Alias.signature of Alias('ItemCreate', 'models.ItemCreate')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 34, 43)>",
|
||||
"docstring": "Return all items in the data store.\n\nReturns\n-------\nlist[Item]\n A list of item domain objects."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 46, 65)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\nItem\n The requested item.\n\nRaises\n------\nKeyError\n If the item does not exist."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 68, 89)>",
|
||||
"docstring": "Create a new item in the data store.\n\nA new identifier is assigned automatically. No additional validation\nis performed beyond Pydantic model validation.\n\nParameters\n----------\npayload : ItemCreate\n Data required to create a new item.\n\nReturns\n-------\nItem\n The newly created item."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 92, 120)>",
|
||||
"docstring": "Replace an existing item in the data store.\n\nThis function performs a full replacement of the stored item.\nPartial updates are not supported.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : ItemCreate\n New item data.\n\nReturns\n-------\nItem\n The updated item.\n\nRaises\n------\nKeyError\n If the item does not exist."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 123, 137)>",
|
||||
"docstring": "Remove an item from the data store.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete.\n\nRaises\n------\nKeyError\n If the item does not exist."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
356
mcp_docs/modules/openapi_first.templates.model_app.json
Normal file
356
mcp_docs/modules/openapi_first.templates.model_app.json
Normal file
@@ -0,0 +1,356 @@
|
||||
{
|
||||
"module": "openapi_first.templates.model_app",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.model_app",
|
||||
"docstring": "OpenAPI-first model-based CRUD application template.\n\nThis package contains a complete, minimal example of an OpenAPI-first\nCRUD service that uses explicit Pydantic domain models for request and\nresponse schemas.\n\nThe application is assembled exclusively from:\n- an OpenAPI specification (``openapi.yaml``)\n- a handler namespace implementing CRUD operations (``routes``)\n- Pydantic domain models (``models``)\n- an in-memory mock data store (``data``)\n\nAll HTTP routes, methods, schemas, and operation bindings are defined\nin the OpenAPI specification and enforced at application startup.\nNo decorator-driven routing or implicit framework behavior is used.\n\nThis template demonstrates:\n- operationId-driven server-side route binding\n- explicit request and response modeling with Pydantic\n- explicit HTTP status code control in handlers\n- operationId-driven client usage against the same OpenAPI contract\n- end-to-end validation using in-memory data and tests\n\n----------------------------------------------------------------------\nScaffolding via CLI\n----------------------------------------------------------------------\n\nCreate a new model-based CRUD example service using the bundled template:\n\n openapi-first model_app\n\nCreate the service in a custom directory:\n\n openapi-first model_app my-model-service\n\nList all available application templates:\n\n openapi-first --list\n\nThe CLI copies template files verbatim into the target directory.\nNo code is generated or modified beyond the copied scaffold.\n\n----------------------------------------------------------------------\nClient Usage Example\n----------------------------------------------------------------------\n\nThe same OpenAPI specification used by the server can be used to\nconstruct a strict, operationId-driven HTTP client.\n\nExample client calls for model-based CRUD operations:\n\n from openapi_first.loader import load_openapi\n from openapi_first.client import OpenAPIClient\n\n spec = load_openapi(\"openapi.yaml\")\n client = OpenAPIClient(spec)\n\n # List items\n response = client.list_items()\n\n # Get item by ID\n response = client.get_item(\n path_params={\"item_id\": 1}\n )\n\n # Create item\n response = client.create_item(\n body={\"name\": \"Orange\", \"price\": 0.8}\n )\n\n # Update item\n response = client.update_item(\n path_params={\"item_id\": 1},\n body={\"name\": \"Green Apple\", \"price\": 0.6},\n )\n\n # Delete item\n response = client.delete_item(\n path_params={\"item_id\": 1}\n )\n\nClient guarantees:\n- One callable per OpenAPI ``operationId``\n- No hardcoded URLs or HTTP methods in user code\n- Request and response payloads conform to Pydantic models\n- Invalid or incomplete OpenAPI specs fail at client construction time\n\n----------------------------------------------------------------------\nNon-Goals\n----------------------------------------------------------------------\n\nThis template is intentionally minimal and is NOT:\n- production-ready\n- persistent or concurrency-safe\n- a reference architecture for data storage\n\nIt exists solely as a copyable example for learning, testing, and\nbootstrapping OpenAPI-first services.\n\nThis package is not part of the ``openapi_first`` library API surface.",
|
||||
"objects": {
|
||||
"data": {
|
||||
"name": "data",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app.data",
|
||||
"signature": null,
|
||||
"docstring": "In-memory data store using Pydantic models.\n\nThis module is NOT thread-safe and is intended for demos and scaffolds only.\nThis module provides a minimal, process-local data store for the\nmodel-based CRUD example application. It stores and returns domain\nobjects defined using Pydantic models and is intended solely for\ndemonstration and scaffolding purposes.\n\nThe implementation intentionally avoids:\n- persistence\n- concurrency guarantees\n- transactional semantics\n- validation beyond what Pydantic provides\n\nIt is not part of the ``openapi_first`` library API surface.",
|
||||
"members": {
|
||||
"Dict": {
|
||||
"name": "Dict",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.data.Dict",
|
||||
"signature": "<bound method Alias.signature of Alias('Dict', 'typing.Dict')>",
|
||||
"docstring": null
|
||||
},
|
||||
"Item": {
|
||||
"name": "Item",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.data.Item",
|
||||
"signature": "<bound method Alias.signature of Alias('Item', 'models.Item')>",
|
||||
"docstring": null
|
||||
},
|
||||
"ItemCreate": {
|
||||
"name": "ItemCreate",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.data.ItemCreate",
|
||||
"signature": "<bound method Alias.signature of Alias('ItemCreate', 'models.ItemCreate')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 34, 43)>",
|
||||
"docstring": "Return all items in the data store.\n\nReturns\n-------\nlist[Item]\n A list of item domain objects."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 46, 65)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\nItem\n The requested item.\n\nRaises\n------\nKeyError\n If the item does not exist."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 68, 89)>",
|
||||
"docstring": "Create a new item in the data store.\n\nA new identifier is assigned automatically. No additional validation\nis performed beyond Pydantic model validation.\n\nParameters\n----------\npayload : ItemCreate\n Data required to create a new item.\n\nReturns\n-------\nItem\n The newly created item."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 92, 120)>",
|
||||
"docstring": "Replace an existing item in the data store.\n\nThis function performs a full replacement of the stored item.\nPartial updates are not supported.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : ItemCreate\n New item data.\n\nReturns\n-------\nItem\n The updated item.\n\nRaises\n------\nKeyError\n If the item does not exist."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.data.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 123, 137)>",
|
||||
"docstring": "Remove an item from the data store.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete.\n\nRaises\n------\nKeyError\n If the item does not exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"name": "main",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app.main",
|
||||
"signature": null,
|
||||
"docstring": "Application entry point for an OpenAPI-first model-based CRUD example service.\n\nThis module constructs a FastAPI application exclusively from an\nOpenAPI specification and a handler namespace, without using\ndecorator-driven routing.\n\nAll HTTP routes, methods, request/response schemas, and operation\nbindings are defined in the OpenAPI document referenced by\n``openapi_path``. Python callables defined in the ``routes`` module are\nbound to OpenAPI operations strictly via ``operationId``.\n\nThis module contains no routing logic, persistence concerns, or\nframework configuration beyond application assembly.\n\nDesign guarantees:\n- OpenAPI is the single source of truth\n- No undocumented routes can exist\n- Every OpenAPI operationId must resolve to exactly one handler\n- All contract violations fail at application startup\n\nThis file is intended to be used as the ASGI entry point.\n\nExample:\n uvicorn main:app",
|
||||
"members": {
|
||||
"OpenAPIFirstApp": {
|
||||
"name": "OpenAPIFirstApp",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.main.OpenAPIFirstApp",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
|
||||
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
|
||||
"members": {
|
||||
"openapi": {
|
||||
"name": "openapi",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.main.OpenAPIFirstApp.openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('openapi', 'openapi_first.app.OpenAPIFirstApp.openapi')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.main.routes",
|
||||
"signature": "<bound method Alias.signature of Alias('routes', 'routes')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.main.app",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
"name": "models",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app.models",
|
||||
"signature": null,
|
||||
"docstring": "Pydantic domain models for the CRUD example.\n\nThis module defines Pydantic models that represent the domain entities\nused by the service. These models are referenced by the OpenAPI\nspecification for request and response schemas.\n\nThe models are declarative and framework-agnostic. They contain no\npersistence logic, validation beyond type constraints, or business\nbehavior.\n\nThis module is not part of the ``openapi_first`` library API surface.\nIt exists solely to support the example application template.",
|
||||
"members": {
|
||||
"BaseModel": {
|
||||
"name": "BaseModel",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.models.BaseModel",
|
||||
"signature": "<bound method Alias.signature of Alias('BaseModel', 'pydantic.BaseModel')>",
|
||||
"docstring": null
|
||||
},
|
||||
"ItemBase": {
|
||||
"name": "ItemBase",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.models.ItemBase",
|
||||
"signature": "<bound method Class.signature of Class('ItemBase', 19, 27)>",
|
||||
"docstring": "Base domain model for an item.\n\nDefines fields common to all item representations.",
|
||||
"members": {
|
||||
"name": {
|
||||
"name": "name",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.models.ItemBase.name",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"price": {
|
||||
"name": "price",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.models.ItemBase.price",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ItemCreate": {
|
||||
"name": "ItemCreate",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.models.ItemCreate",
|
||||
"signature": "<bound method Class.signature of Class('ItemCreate', 30, 39)>",
|
||||
"docstring": "Domain model for item creation requests.\n\nThis model is used for request bodies when creating new items.\nIt intentionally excludes the ``id`` field, which is assigned\nby the service."
|
||||
},
|
||||
"Item": {
|
||||
"name": "Item",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.models.Item",
|
||||
"signature": "<bound method Class.signature of Class('Item', 42, 50)>",
|
||||
"docstring": "Domain model for a persisted item.\n\nThis model represents the full item state returned in responses,\nincluding the server-assigned identifier.",
|
||||
"members": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.models.Item.id",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app.routes",
|
||||
"signature": null,
|
||||
"docstring": "CRUD route handlers bound via OpenAPI operationId.\n\nThis module defines OpenAPI-bound operation handlers for a model-based\nCRUD service. Functions in this module are bound to HTTP routes\nexclusively via OpenAPI ``operationId`` values.\n\nHandlers explicitly control HTTP response status codes to ensure runtime\nbehavior matches the OpenAPI contract. Domain models defined using\nPydantic are used for request and response payloads.\n\nNo routing decorators, path definitions, or implicit framework behavior\nappear in this module. All routing, HTTP methods, and schemas are defined\nin the OpenAPI specification.",
|
||||
"members": {
|
||||
"Response": {
|
||||
"name": "Response",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.routes.Response",
|
||||
"signature": "<bound method Alias.signature of Alias('Response', 'fastapi.Response')>",
|
||||
"docstring": null
|
||||
},
|
||||
"HTTPException": {
|
||||
"name": "HTTPException",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.routes.HTTPException",
|
||||
"signature": "<bound method Alias.signature of Alias('HTTPException', 'fastapi.HTTPException')>",
|
||||
"docstring": null
|
||||
},
|
||||
"ItemCreate": {
|
||||
"name": "ItemCreate",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.routes.ItemCreate",
|
||||
"signature": "<bound method Alias.signature of Alias('ItemCreate', 'models.ItemCreate')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 29, 41)>",
|
||||
"docstring": "List all items.\n\nImplements the OpenAPI operation identified by\n``operationId: list_items``.\n\nReturns\n-------\nlist[Item]\n A list of item domain objects."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 44, 69)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nImplements the OpenAPI operation identified by\n``operationId: get_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\nItem\n The requested item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 72, 93)>",
|
||||
"docstring": "Create a new item.\n\nImplements the OpenAPI operation identified by\n``operationId: create_item``.\n\nParameters\n----------\npayload : ItemCreate\n Request body describing the item to create.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\nItem\n The newly created item."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 96, 123)>",
|
||||
"docstring": "Update an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: update_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : ItemCreate\n New item data.\n\nReturns\n-------\nItem\n The updated item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 126, 156)>",
|
||||
"docstring": "Delete an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: delete_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\nNone\n No content.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"test_model_app": {
|
||||
"name": "test_model_app",
|
||||
"kind": "module",
|
||||
"path": "openapi_first.templates.model_app.test_model_app",
|
||||
"signature": null,
|
||||
"docstring": "End-to-end tests for the OpenAPI-first model CRUD example app.\n\nThese tests validate that all CRUD operations behave correctly\nagainst the in-memory mock data store using Pydantic models.\n- OpenAPI specification loading\n- OperationId-driven route binding on the server\n- OperationId-driven client invocation\n- Pydantic model-based request and response handling\n\nAll CRUD operations are exercised against an in-memory mock data store\nbacked by Pydantic domain models.\n\nThe tests assume:\n- OpenAPI-first route binding\n- Pydantic model validation\n- In-memory storage (no persistence guarantees)\n- Deterministic behavior in a single process",
|
||||
"members": {
|
||||
"TestClient": {
|
||||
"name": "TestClient",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.TestClient",
|
||||
"signature": "<bound method Alias.signature of Alias('TestClient', 'fastapi.testclient.TestClient')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.app",
|
||||
"signature": "<bound method Alias.signature of Alias('app', 'main.app')>",
|
||||
"docstring": null
|
||||
},
|
||||
"load_openapi": {
|
||||
"name": "load_openapi",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.load_openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
|
||||
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
|
||||
},
|
||||
"OpenAPIClient": {
|
||||
"name": "OpenAPIClient",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
|
||||
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
|
||||
"members": {
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.spec",
|
||||
"signature": "<bound method Alias.signature of Alias('spec', 'openapi_first.client.OpenAPIClient.spec')>",
|
||||
"docstring": null
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.base_url",
|
||||
"signature": "<bound method Alias.signature of Alias('base_url', 'openapi_first.client.OpenAPIClient.base_url')>",
|
||||
"docstring": null
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.client",
|
||||
"signature": "<bound method Alias.signature of Alias('client', 'openapi_first.client.OpenAPIClient.client')>",
|
||||
"docstring": null
|
||||
},
|
||||
"operations": {
|
||||
"name": "operations",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.operations",
|
||||
"signature": "<bound method Alias.signature of Alias('operations', 'openapi_first.client.OpenAPIClient.operations')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.client",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.spec",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"test_list_items_initial": {
|
||||
"name": "test_list_items_initial",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_list_items_initial",
|
||||
"signature": "<bound method Function.signature of Function('test_list_items_initial', 37, 48)>",
|
||||
"docstring": "Initial items should be present."
|
||||
},
|
||||
"test_get_item": {
|
||||
"name": "test_get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_get_item",
|
||||
"signature": "<bound method Function.signature of Function('test_get_item', 51, 61)>",
|
||||
"docstring": "Existing item should be retrievable by ID."
|
||||
},
|
||||
"test_create_item": {
|
||||
"name": "test_create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_create_item",
|
||||
"signature": "<bound method Function.signature of Function('test_create_item', 64, 84)>",
|
||||
"docstring": "Creating a new item should return the created entity."
|
||||
},
|
||||
"test_update_item": {
|
||||
"name": "test_update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_update_item",
|
||||
"signature": "<bound method Function.signature of Function('test_update_item', 87, 111)>",
|
||||
"docstring": "Updating an item should replace its values."
|
||||
},
|
||||
"test_delete_item": {
|
||||
"name": "test_delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_delete_item",
|
||||
"signature": "<bound method Function.signature of Function('test_delete_item', 114, 124)>",
|
||||
"docstring": "Deleting an item should remove it from the store."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
mcp_docs/modules/openapi_first.templates.model_app.main.json
Normal file
39
mcp_docs/modules/openapi_first.templates.model_app.main.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"module": "openapi_first.templates.model_app.main",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.model_app.main",
|
||||
"docstring": "Application entry point for an OpenAPI-first model-based CRUD example service.\n\nThis module constructs a FastAPI application exclusively from an\nOpenAPI specification and a handler namespace, without using\ndecorator-driven routing.\n\nAll HTTP routes, methods, request/response schemas, and operation\nbindings are defined in the OpenAPI document referenced by\n``openapi_path``. Python callables defined in the ``routes`` module are\nbound to OpenAPI operations strictly via ``operationId``.\n\nThis module contains no routing logic, persistence concerns, or\nframework configuration beyond application assembly.\n\nDesign guarantees:\n- OpenAPI is the single source of truth\n- No undocumented routes can exist\n- Every OpenAPI operationId must resolve to exactly one handler\n- All contract violations fail at application startup\n\nThis file is intended to be used as the ASGI entry point.\n\nExample:\n uvicorn main:app",
|
||||
"objects": {
|
||||
"OpenAPIFirstApp": {
|
||||
"name": "OpenAPIFirstApp",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.main.OpenAPIFirstApp",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
|
||||
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
|
||||
"members": {
|
||||
"openapi": {
|
||||
"name": "openapi",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.main.OpenAPIFirstApp.openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('openapi', 'openapi_first.app.OpenAPIFirstApp.openapi')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"name": "routes",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.main.routes",
|
||||
"signature": "<bound method Alias.signature of Alias('routes', 'routes')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.main.app",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"module": "openapi_first.templates.model_app.models",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.model_app.models",
|
||||
"docstring": "Pydantic domain models for the CRUD example.\n\nThis module defines Pydantic models that represent the domain entities\nused by the service. These models are referenced by the OpenAPI\nspecification for request and response schemas.\n\nThe models are declarative and framework-agnostic. They contain no\npersistence logic, validation beyond type constraints, or business\nbehavior.\n\nThis module is not part of the ``openapi_first`` library API surface.\nIt exists solely to support the example application template.",
|
||||
"objects": {
|
||||
"BaseModel": {
|
||||
"name": "BaseModel",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.models.BaseModel",
|
||||
"signature": "<bound method Alias.signature of Alias('BaseModel', 'pydantic.BaseModel')>",
|
||||
"docstring": null
|
||||
},
|
||||
"ItemBase": {
|
||||
"name": "ItemBase",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.models.ItemBase",
|
||||
"signature": "<bound method Class.signature of Class('ItemBase', 19, 27)>",
|
||||
"docstring": "Base domain model for an item.\n\nDefines fields common to all item representations.",
|
||||
"members": {
|
||||
"name": {
|
||||
"name": "name",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.models.ItemBase.name",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"price": {
|
||||
"name": "price",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.models.ItemBase.price",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ItemCreate": {
|
||||
"name": "ItemCreate",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.models.ItemCreate",
|
||||
"signature": "<bound method Class.signature of Class('ItemCreate', 30, 39)>",
|
||||
"docstring": "Domain model for item creation requests.\n\nThis model is used for request bodies when creating new items.\nIt intentionally excludes the ``id`` field, which is assigned\nby the service."
|
||||
},
|
||||
"Item": {
|
||||
"name": "Item",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.models.Item",
|
||||
"signature": "<bound method Class.signature of Class('Item', 42, 50)>",
|
||||
"docstring": "Domain model for a persisted item.\n\nThis model represents the full item state returned in responses,\nincluding the server-assigned identifier.",
|
||||
"members": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.models.Item.id",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"module": "openapi_first.templates.model_app.routes",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.model_app.routes",
|
||||
"docstring": "CRUD route handlers bound via OpenAPI operationId.\n\nThis module defines OpenAPI-bound operation handlers for a model-based\nCRUD service. Functions in this module are bound to HTTP routes\nexclusively via OpenAPI ``operationId`` values.\n\nHandlers explicitly control HTTP response status codes to ensure runtime\nbehavior matches the OpenAPI contract. Domain models defined using\nPydantic are used for request and response payloads.\n\nNo routing decorators, path definitions, or implicit framework behavior\nappear in this module. All routing, HTTP methods, and schemas are defined\nin the OpenAPI specification.",
|
||||
"objects": {
|
||||
"Response": {
|
||||
"name": "Response",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.routes.Response",
|
||||
"signature": "<bound method Alias.signature of Alias('Response', 'fastapi.Response')>",
|
||||
"docstring": null
|
||||
},
|
||||
"HTTPException": {
|
||||
"name": "HTTPException",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.routes.HTTPException",
|
||||
"signature": "<bound method Alias.signature of Alias('HTTPException', 'fastapi.HTTPException')>",
|
||||
"docstring": null
|
||||
},
|
||||
"ItemCreate": {
|
||||
"name": "ItemCreate",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.routes.ItemCreate",
|
||||
"signature": "<bound method Alias.signature of Alias('ItemCreate', 'models.ItemCreate')>",
|
||||
"docstring": null
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.list_items",
|
||||
"signature": "<bound method Function.signature of Function('list_items', 29, 41)>",
|
||||
"docstring": "List all items.\n\nImplements the OpenAPI operation identified by\n``operationId: list_items``.\n\nReturns\n-------\nlist[Item]\n A list of item domain objects."
|
||||
},
|
||||
"get_item": {
|
||||
"name": "get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.get_item",
|
||||
"signature": "<bound method Function.signature of Function('get_item', 44, 69)>",
|
||||
"docstring": "Retrieve a single item by ID.\n\nImplements the OpenAPI operation identified by\n``operationId: get_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to retrieve.\n\nReturns\n-------\nItem\n The requested item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"create_item": {
|
||||
"name": "create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.create_item",
|
||||
"signature": "<bound method Function.signature of Function('create_item', 72, 93)>",
|
||||
"docstring": "Create a new item.\n\nImplements the OpenAPI operation identified by\n``operationId: create_item``.\n\nParameters\n----------\npayload : ItemCreate\n Request body describing the item to create.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\nItem\n The newly created item."
|
||||
},
|
||||
"update_item": {
|
||||
"name": "update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.update_item",
|
||||
"signature": "<bound method Function.signature of Function('update_item', 96, 123)>",
|
||||
"docstring": "Update an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: update_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to update.\npayload : ItemCreate\n New item data.\n\nReturns\n-------\nItem\n The updated item.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.routes.delete_item",
|
||||
"signature": "<bound method Function.signature of Function('delete_item', 126, 156)>",
|
||||
"docstring": "Delete an existing item.\n\nImplements the OpenAPI operation identified by\n``operationId: delete_item``.\n\nParameters\n----------\nitem_id : int\n Identifier of the item to delete.\nresponse : fastapi.Response\n Response object used to set the HTTP status code.\n\nReturns\n-------\nNone\n No content.\n\nRaises\n------\nHTTPException\n 404 if the item does not exist."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"module": "openapi_first.templates.model_app.test_model_app",
|
||||
"content": {
|
||||
"path": "openapi_first.templates.model_app.test_model_app",
|
||||
"docstring": "End-to-end tests for the OpenAPI-first model CRUD example app.\n\nThese tests validate that all CRUD operations behave correctly\nagainst the in-memory mock data store using Pydantic models.\n- OpenAPI specification loading\n- OperationId-driven route binding on the server\n- OperationId-driven client invocation\n- Pydantic model-based request and response handling\n\nAll CRUD operations are exercised against an in-memory mock data store\nbacked by Pydantic domain models.\n\nThe tests assume:\n- OpenAPI-first route binding\n- Pydantic model validation\n- In-memory storage (no persistence guarantees)\n- Deterministic behavior in a single process",
|
||||
"objects": {
|
||||
"TestClient": {
|
||||
"name": "TestClient",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.TestClient",
|
||||
"signature": "<bound method Alias.signature of Alias('TestClient', 'fastapi.testclient.TestClient')>",
|
||||
"docstring": null
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"kind": "alias",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.app",
|
||||
"signature": "<bound method Alias.signature of Alias('app', 'main.app')>",
|
||||
"docstring": null
|
||||
},
|
||||
"load_openapi": {
|
||||
"name": "load_openapi",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.load_openapi",
|
||||
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
|
||||
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
|
||||
},
|
||||
"OpenAPIClient": {
|
||||
"name": "OpenAPIClient",
|
||||
"kind": "class",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient",
|
||||
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
|
||||
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
|
||||
"members": {
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.spec",
|
||||
"signature": "<bound method Alias.signature of Alias('spec', 'openapi_first.client.OpenAPIClient.spec')>",
|
||||
"docstring": null
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.base_url",
|
||||
"signature": "<bound method Alias.signature of Alias('base_url', 'openapi_first.client.OpenAPIClient.base_url')>",
|
||||
"docstring": null
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.client",
|
||||
"signature": "<bound method Alias.signature of Alias('client', 'openapi_first.client.OpenAPIClient.client')>",
|
||||
"docstring": null
|
||||
},
|
||||
"operations": {
|
||||
"name": "operations",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient.operations",
|
||||
"signature": "<bound method Alias.signature of Alias('operations', 'openapi_first.client.OpenAPIClient.operations')>",
|
||||
"docstring": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"name": "client",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.client",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"kind": "attribute",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.spec",
|
||||
"signature": null,
|
||||
"docstring": null
|
||||
},
|
||||
"test_list_items_initial": {
|
||||
"name": "test_list_items_initial",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_list_items_initial",
|
||||
"signature": "<bound method Function.signature of Function('test_list_items_initial', 37, 48)>",
|
||||
"docstring": "Initial items should be present."
|
||||
},
|
||||
"test_get_item": {
|
||||
"name": "test_get_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_get_item",
|
||||
"signature": "<bound method Function.signature of Function('test_get_item', 51, 61)>",
|
||||
"docstring": "Existing item should be retrievable by ID."
|
||||
},
|
||||
"test_create_item": {
|
||||
"name": "test_create_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_create_item",
|
||||
"signature": "<bound method Function.signature of Function('test_create_item', 64, 84)>",
|
||||
"docstring": "Creating a new item should return the created entity."
|
||||
},
|
||||
"test_update_item": {
|
||||
"name": "test_update_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_update_item",
|
||||
"signature": "<bound method Function.signature of Function('test_update_item', 87, 111)>",
|
||||
"docstring": "Updating an item should replace its values."
|
||||
},
|
||||
"test_delete_item": {
|
||||
"name": "test_delete_item",
|
||||
"kind": "function",
|
||||
"path": "openapi_first.templates.model_app.test_model_app.test_delete_item",
|
||||
"signature": "<bound method Function.signature of Function('test_delete_item', 114, 124)>",
|
||||
"docstring": "Deleting an item should remove it from the store."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
mcp_docs/nav.json
Normal file
90
mcp_docs/nav.json
Normal file
@@ -0,0 +1,90 @@
|
||||
[
|
||||
{
|
||||
"module": "openapi_first",
|
||||
"resource": "doc://modules/openapi_first"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.app",
|
||||
"resource": "doc://modules/openapi_first.app"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.binder",
|
||||
"resource": "doc://modules/openapi_first.binder"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.cli",
|
||||
"resource": "doc://modules/openapi_first.cli"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.client",
|
||||
"resource": "doc://modules/openapi_first.client"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.errors",
|
||||
"resource": "doc://modules/openapi_first.errors"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.loader",
|
||||
"resource": "doc://modules/openapi_first.loader"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates",
|
||||
"resource": "doc://modules/openapi_first.templates"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.crud_app",
|
||||
"resource": "doc://modules/openapi_first.templates.crud_app"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.crud_app.data",
|
||||
"resource": "doc://modules/openapi_first.templates.crud_app.data"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.crud_app.main",
|
||||
"resource": "doc://modules/openapi_first.templates.crud_app.main"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.crud_app.routes",
|
||||
"resource": "doc://modules/openapi_first.templates.crud_app.routes"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.crud_app.test_crud_app",
|
||||
"resource": "doc://modules/openapi_first.templates.crud_app.test_crud_app"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.health_app",
|
||||
"resource": "doc://modules/openapi_first.templates.health_app"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.health_app.main",
|
||||
"resource": "doc://modules/openapi_first.templates.health_app.main"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.health_app.routes",
|
||||
"resource": "doc://modules/openapi_first.templates.health_app.routes"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.model_app",
|
||||
"resource": "doc://modules/openapi_first.templates.model_app"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.model_app.data",
|
||||
"resource": "doc://modules/openapi_first.templates.model_app.data"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.model_app.main",
|
||||
"resource": "doc://modules/openapi_first.templates.model_app.main"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.model_app.models",
|
||||
"resource": "doc://modules/openapi_first.templates.model_app.models"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.model_app.routes",
|
||||
"resource": "doc://modules/openapi_first.templates.model_app.routes"
|
||||
},
|
||||
{
|
||||
"module": "openapi_first.templates.model_app.test_model_app",
|
||||
"resource": "doc://modules/openapi_first.templates.model_app.test_model_app"
|
||||
}
|
||||
]
|
||||
108
mkdocs.yml
108
mkdocs.yml
@@ -1,51 +1,75 @@
|
||||
site_name: Aetoskia Mail Intake
|
||||
site_description: Format-agnostic document reading, parsing, and scraping framework
|
||||
|
||||
theme:
|
||||
name: material
|
||||
palette:
|
||||
- scheme: slate
|
||||
primary: deep purple
|
||||
accent: cyan
|
||||
- scheme: slate
|
||||
primary: deep purple
|
||||
accent: cyan
|
||||
font:
|
||||
text: Inter
|
||||
code: JetBrains Mono
|
||||
features:
|
||||
- navigation.tabs
|
||||
- navigation.expand
|
||||
- navigation.top
|
||||
- navigation.instant
|
||||
- content.code.copy
|
||||
- content.code.annotate
|
||||
|
||||
- navigation.sections
|
||||
- navigation.expand
|
||||
- navigation.top
|
||||
- navigation.instant
|
||||
- navigation.tracking
|
||||
- navigation.indexes
|
||||
- content.code.copy
|
||||
- content.code.annotate
|
||||
- content.tabs.link
|
||||
- content.action.edit
|
||||
- search.highlight
|
||||
- search.share
|
||||
- search.suggest
|
||||
plugins:
|
||||
- search
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
paths: ["."]
|
||||
options:
|
||||
docstring_style: google
|
||||
show_source: false
|
||||
show_signature_annotations: true
|
||||
separate_signature: true
|
||||
merge_init_into_class: true
|
||||
inherited_members: true
|
||||
annotations_path: brief
|
||||
show_root_heading: true
|
||||
group_by_category: true
|
||||
|
||||
- search
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
paths:
|
||||
- .
|
||||
options:
|
||||
docstring_style: google
|
||||
show_source: false
|
||||
show_signature_annotations: true
|
||||
separate_signature: true
|
||||
merge_init_into_class: true
|
||||
inherited_members: true
|
||||
annotations_path: brief
|
||||
show_root_heading: true
|
||||
group_by_category: true
|
||||
show_category_heading: true
|
||||
show_object_full_path: false
|
||||
show_symbol_type_heading: true
|
||||
markdown_extensions:
|
||||
- pymdownx.superfences
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences
|
||||
- pymdownx.highlight:
|
||||
linenums: true
|
||||
anchor_linenums: true
|
||||
line_spans: __span
|
||||
pygments_lang_class: true
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- tables
|
||||
- footnotes
|
||||
- pymdownx.caret
|
||||
- pymdownx.tilde
|
||||
- pymdownx.mark
|
||||
site_name: openapi_first
|
||||
nav:
|
||||
- Home: openapi_first/index.md
|
||||
|
||||
- Core:
|
||||
- OpenAPI-First App: openapi_first/app.md
|
||||
- Route Binder: openapi_first/binder.md
|
||||
- Spec Loaders: openapi_first/loader.md
|
||||
- Client: openapi_first/client.md
|
||||
|
||||
- CLI:
|
||||
- Home: openapi_first/cli.md
|
||||
|
||||
- Errors:
|
||||
- Error Hierarchy: openapi_first/errors.md
|
||||
- Home: index.md
|
||||
- Application Bootstrap:
|
||||
- app.md
|
||||
- binder.md
|
||||
- Core Utilities:
|
||||
- loader.md
|
||||
- errors.md
|
||||
- OpenAPI Client:
|
||||
- client.md
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""
|
||||
# Summary
|
||||
|
||||
FastAPI OpenAPI First — strict OpenAPI-first application bootstrap for FastAPI.
|
||||
|
||||
FastAPI OpenAPI First is a **contract-first infrastructure library** that
|
||||
@@ -7,178 +9,90 @@ enforces OpenAPI as the single source of truth for FastAPI services.
|
||||
The library removes decorator-driven routing and replaces it with
|
||||
deterministic, spec-driven application assembly. Every HTTP route,
|
||||
method, and operation is defined in OpenAPI first and bound to Python
|
||||
handlers explicitly via operationId.
|
||||
handlers explicitly via `operationId`.
|
||||
|
||||
The package is intentionally minimal and layered. Each module has a
|
||||
single responsibility and exposes explicit contracts rather than
|
||||
convenience facades.
|
||||
---
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Architecture Overview
|
||||
----------------------------------------------------------------------
|
||||
|
||||
The library is structured around four core responsibilities:
|
||||
|
||||
- loader: load and validate OpenAPI 3.x specifications (JSON/YAML)
|
||||
- binder: bind OpenAPI operations to FastAPI routes via operationId
|
||||
- app: OpenAPI-first FastAPI application bootstrap
|
||||
- client: OpenAPI-first HTTP client driven by the same specification
|
||||
- errors: explicit error hierarchy for contract violations
|
||||
|
||||
The package root acts as a **namespace**, not a facade. Consumers are
|
||||
expected to import functionality explicitly from the appropriate module.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Installation
|
||||
----------------------------------------------------------------------
|
||||
# Installation
|
||||
|
||||
Install using pip:
|
||||
|
||||
pip install openapi-first
|
||||
```bash
|
||||
pip install openapi-first
|
||||
```
|
||||
|
||||
Or with Poetry:
|
||||
|
||||
poetry add openapi-first
|
||||
```bash
|
||||
poetry add openapi-first
|
||||
```
|
||||
|
||||
Runtime dependencies are intentionally minimal:
|
||||
- fastapi (server-side)
|
||||
- httpx (client-side)
|
||||
- openapi-spec-validator
|
||||
- pyyaml (optional, for YAML specs)
|
||||
---
|
||||
|
||||
The ASGI server (e.g., uvicorn) is an application-level dependency and is
|
||||
not bundled with this library.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Server-Side Usage (OpenAPI → FastAPI)
|
||||
----------------------------------------------------------------------
|
||||
# Quick Start
|
||||
|
||||
Minimal OpenAPI-first FastAPI application:
|
||||
|
||||
from openapi_first import app
|
||||
import my_service.routes as routes
|
||||
```python
|
||||
from openapi_first import app
|
||||
import my_service.routes as routes
|
||||
|
||||
api = app.OpenAPIFirstApp(
|
||||
openapi_path="openapi.yaml",
|
||||
routes_module=routes,
|
||||
title="My Service",
|
||||
version="1.0.0",
|
||||
)
|
||||
api = app.OpenAPIFirstApp(
|
||||
openapi_path="openapi.yaml",
|
||||
routes_module=routes,
|
||||
title="My Service",
|
||||
version="1.0.0",
|
||||
)
|
||||
```
|
||||
|
||||
# Run with:
|
||||
# uvicorn my_service.main:api
|
||||
OperationId-driven HTTP client:
|
||||
|
||||
Handler definitions (no decorators):
|
||||
```python
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
def get_health():
|
||||
return {"status": "ok"}
|
||||
spec = load_openapi("openapi.yaml")
|
||||
client = OpenAPIClient(spec)
|
||||
|
||||
OpenAPI snippet:
|
||||
response = client.get_health()
|
||||
```
|
||||
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
operationId: get_health
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
---
|
||||
|
||||
The binder guarantees:
|
||||
- Every OpenAPI operationId has exactly one handler
|
||||
- No undocumented routes exist
|
||||
- All mismatches fail at application startup
|
||||
# Architecture
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Client-Side Usage (OpenAPI → HTTP Client)
|
||||
----------------------------------------------------------------------
|
||||
The library is structured around four core responsibilities:
|
||||
|
||||
The same OpenAPI specification can be used to construct a strict,
|
||||
operationId-driven HTTP client.
|
||||
- `loader`: Load and validate OpenAPI 3.x specifications (JSON/YAML).
|
||||
- `binder`: Bind OpenAPI operations to FastAPI routes via `operationId`.
|
||||
- `app`: OpenAPI-first FastAPI application bootstrap.
|
||||
- `client`: OpenAPI-first HTTP client driven by the same specification.
|
||||
- `errors`: Explicit error hierarchy for contract violations.
|
||||
|
||||
Client construction:
|
||||
---
|
||||
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
spec = load_openapi("openapi.yaml")
|
||||
|
||||
client = OpenAPIClient(spec)
|
||||
|
||||
Calling operations (operationId is the API):
|
||||
|
||||
response = client.get_health()
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
||||
Path parameters must match the OpenAPI specification exactly:
|
||||
|
||||
response = client.get_item(
|
||||
path_params={"item_id": 1}
|
||||
)
|
||||
|
||||
Request bodies are passed explicitly:
|
||||
|
||||
response = client.create_item(
|
||||
body={"name": "Orange", "price": 0.8}
|
||||
)
|
||||
|
||||
Client guarantees:
|
||||
- One callable per OpenAPI operationId
|
||||
- No hardcoded URLs or HTTP methods in user code
|
||||
- Path and body parameters must match the spec exactly
|
||||
- Invalid or incomplete OpenAPI specs fail at client construction time
|
||||
- No schema inference or mutation is performed
|
||||
|
||||
The client is transport-level only and returns `httpx.Response`
|
||||
objects directly. Response interpretation and validation are left to
|
||||
the consumer or higher-level layers.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Extensibility Model
|
||||
----------------------------------------------------------------------
|
||||
|
||||
FastAPI OpenAPI First is designed to be extended via **explicit contracts**:
|
||||
|
||||
- Users MAY extend OpenAPI loading behavior (e.g. multi-file specs)
|
||||
by wrapping or replacing `loader.load_openapi`
|
||||
- Users MAY extend route binding behavior by building on top of
|
||||
`binder.bind_routes`
|
||||
- Users MAY layer additional validation (e.g. signature checks)
|
||||
without modifying core modules
|
||||
|
||||
Users SHOULD NOT rely on FastAPI decorators for routing when using this
|
||||
library. Mixing decorator-driven routes with OpenAPI-first routing
|
||||
defeats the contract guarantees and is explicitly unsupported.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Public API Surface
|
||||
----------------------------------------------------------------------
|
||||
# Public API
|
||||
|
||||
The supported public API consists of the following top-level modules:
|
||||
|
||||
- openapi_first.app
|
||||
- openapi_first.binder
|
||||
- openapi_first.loader
|
||||
- openapi_first.client
|
||||
- openapi_first.errors
|
||||
- `openapi_first.app`
|
||||
- `openapi_first.binder`
|
||||
- `openapi_first.loader`
|
||||
- `openapi_first.client`
|
||||
- `openapi_first.errors`
|
||||
|
||||
Classes and functions should be imported explicitly from these modules.
|
||||
No individual symbols are re-exported at the package root.
|
||||
---
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Design Guarantees
|
||||
----------------------------------------------------------------------
|
||||
# Design Guarantees
|
||||
|
||||
- OpenAPI is the single source of truth
|
||||
- No undocumented routes can exist
|
||||
- No OpenAPI operation can exist without a handler or client callable
|
||||
- All contract violations fail at application startup or client creation
|
||||
- No hidden FastAPI magic or implicit behavior
|
||||
- Deterministic, testable application assembly
|
||||
- CI-friendly failure modes
|
||||
- OpenAPI is the single source of truth.
|
||||
- No undocumented routes can exist.
|
||||
- No OpenAPI operation can exist without a handler or client callable.
|
||||
- All contract violations fail at application startup or client creation.
|
||||
- No hidden FastAPI magic or implicit behavior.
|
||||
- Deterministic, testable application assembly.
|
||||
|
||||
FastAPI OpenAPI First favors correctness, explicitness, and contract
|
||||
enforcement over convenience shortcuts.
|
||||
---
|
||||
"""
|
||||
|
||||
from . import app
|
||||
@@ -186,6 +100,8 @@ from . import binder
|
||||
from . import loader
|
||||
from . import client
|
||||
from . import errors
|
||||
from . import codegen
|
||||
from . import codegen_routes
|
||||
|
||||
__all__ = [
|
||||
"app",
|
||||
@@ -193,4 +109,6 @@ __all__ = [
|
||||
"loader",
|
||||
"client",
|
||||
"errors",
|
||||
"codegen",
|
||||
"codegen_routes",
|
||||
]
|
||||
|
||||
9
openapi_first/__init__.pyi
Normal file
9
openapi_first/__init__.pyi
Normal file
@@ -0,0 +1,9 @@
|
||||
from . import app as app
|
||||
from . import binder as binder
|
||||
from . import loader as loader
|
||||
from . import client as client
|
||||
from . import errors as errors
|
||||
from . import codegen as codegen
|
||||
from . import codegen_routes as codegen_routes
|
||||
|
||||
__all__ = ["app", "binder", "loader", "client", "errors", "codegen", "codegen_routes"]
|
||||
@@ -1,43 +1,35 @@
|
||||
"""
|
||||
openapi_first.app
|
||||
=========================
|
||||
# Summary
|
||||
|
||||
OpenAPI-first application bootstrap for FastAPI.
|
||||
|
||||
This module provides `OpenAPIFirstApp`, a thin but strict abstraction
|
||||
that enforces OpenAPI as the single source of truth for a FastAPI service.
|
||||
|
||||
Core principles
|
||||
---------------
|
||||
- The OpenAPI specification (JSON or YAML) defines the entire API surface.
|
||||
- Every operationId in the OpenAPI spec must have a corresponding
|
||||
Python handler function.
|
||||
- Handlers are plain Python callables (no FastAPI decorators).
|
||||
- FastAPI route registration is derived exclusively from the spec.
|
||||
- FastAPI's autogenerated OpenAPI schema is fully overridden.
|
||||
Notes:
|
||||
**Core Principles:**
|
||||
|
||||
What this module does
|
||||
---------------------
|
||||
- Loads and validates an OpenAPI 3.x specification.
|
||||
- Dynamically binds HTTP routes to handler functions using operationId.
|
||||
- Registers routes with FastAPI at application startup.
|
||||
- Ensures runtime behavior matches the OpenAPI contract exactly.
|
||||
- The OpenAPI specification (JSON or YAML) defines the entire API surface.
|
||||
- Every `operationId` in the OpenAPI spec must have a corresponding
|
||||
Python handler function.
|
||||
- Handlers are plain Python callables (no FastAPI decorators).
|
||||
- FastAPI route registration is derived exclusively from the spec.
|
||||
- FastAPI's autogenerated OpenAPI schema is fully overridden.
|
||||
|
||||
What this module does NOT do
|
||||
----------------------------
|
||||
- It does not generate OpenAPI specs.
|
||||
- It does not generate client code.
|
||||
- It does not introduce a new framework or lifecycle.
|
||||
- It does not alter FastAPI dependency injection semantics.
|
||||
**Responsibilities:**
|
||||
|
||||
Intended usage
|
||||
--------------
|
||||
This module is intended for teams that want:
|
||||
- Loads and validates an OpenAPI 3.x specification.
|
||||
- Dynamically binds HTTP routes to handler functions using `operationId`.
|
||||
- Registers routes with FastAPI at application startup.
|
||||
- Ensures runtime behavior matches the OpenAPI contract exactly.
|
||||
|
||||
- OpenAPI-first API development
|
||||
- Strong contract enforcement
|
||||
- Minimal FastAPI boilerplate
|
||||
- Predictable, CI-friendly failures for spec/implementation drift
|
||||
**Constraints:**
|
||||
|
||||
- This module intentionally does NOT:
|
||||
- Generate OpenAPI specs.
|
||||
- Generate client code.
|
||||
- Introduce a new framework or lifecycle.
|
||||
- Alter FastAPI dependency injection semantics.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
@@ -50,48 +42,33 @@ class OpenAPIFirstApp(FastAPI):
|
||||
"""
|
||||
FastAPI application enforcing OpenAPI-first design.
|
||||
|
||||
`OpenAPIFirstApp` subclasses FastAPI and replaces manual route
|
||||
registration with OpenAPI-driven binding. All routes are derived
|
||||
from the provided OpenAPI specification, and each operationId is
|
||||
mapped to a Python function in the supplied routes module.
|
||||
Notes:
|
||||
**Responsibilities:**
|
||||
|
||||
Parameters
|
||||
----------
|
||||
openapi_path : str
|
||||
Filesystem path to the OpenAPI 3.x specification file.
|
||||
This specification is treated as the authoritative API contract.
|
||||
- `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route
|
||||
registration with OpenAPI-driven binding.
|
||||
- All routes are derived from the provided OpenAPI specification,
|
||||
and each `operationId` is mapped to a Python function in the
|
||||
supplied routes module.
|
||||
|
||||
routes_module : module
|
||||
Python module containing handler functions whose names correspond
|
||||
exactly to OpenAPI operationId values.
|
||||
**Guarantees:**
|
||||
|
||||
**fastapi_kwargs
|
||||
Additional keyword arguments passed directly to `fastapi.FastAPI`
|
||||
(e.g., title, version, middleware, lifespan handlers).
|
||||
- No route can exist without an OpenAPI declaration.
|
||||
- No OpenAPI operation can exist without a handler.
|
||||
- Swagger UI and `/openapi.json` always reflect the provided spec.
|
||||
- Handler functions remain framework-agnostic and testable.
|
||||
|
||||
Raises
|
||||
------
|
||||
OpenAPIFirstError
|
||||
If the OpenAPI specification is invalid, or if any declared
|
||||
operationId does not have a corresponding handler function.
|
||||
Example:
|
||||
```python
|
||||
from openapi_first import OpenAPIFirstApp
|
||||
import app.routes as routes
|
||||
|
||||
Behavior guarantees
|
||||
-------------------
|
||||
- No route can exist without an OpenAPI declaration.
|
||||
- No OpenAPI operation can exist without a handler.
|
||||
- Swagger UI and `/openapi.json` always reflect the provided spec.
|
||||
- Handler functions remain framework-agnostic and testable.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> from openapi_first import OpenAPIFirstApp
|
||||
>>> import app.routes as routes
|
||||
>>>
|
||||
>>> app = OpenAPIFirstApp(
|
||||
... openapi_path="app/openapi.json",
|
||||
... routes_module=routes,
|
||||
... title="Example Service"
|
||||
... )
|
||||
app = OpenAPIFirstApp(
|
||||
openapi_path="app/openapi.json",
|
||||
routes_module=routes,
|
||||
title="Example Service",
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -101,6 +78,26 @@ class OpenAPIFirstApp(FastAPI):
|
||||
routes_module,
|
||||
**fastapi_kwargs,
|
||||
):
|
||||
"""
|
||||
Initialize the application.
|
||||
|
||||
Args:
|
||||
openapi_path (str):
|
||||
Filesystem path to the OpenAPI 3.x specification file. This
|
||||
specification is treated as the authoritative API contract.
|
||||
routes_module (module):
|
||||
Python module containing handler functions whose names correspond
|
||||
exactly to OpenAPI `operationId` values.
|
||||
**fastapi_kwargs (Any):
|
||||
Additional keyword arguments passed directly to
|
||||
`fastapi.FastAPI` (e.g., title, version, middleware, lifespan
|
||||
handlers).
|
||||
|
||||
Raises:
|
||||
OpenAPIFirstError:
|
||||
If the OpenAPI specification is invalid, or if any declared
|
||||
`operationId` does not have a corresponding handler function.
|
||||
"""
|
||||
# Initialize FastAPI normally
|
||||
super().__init__(**fastapi_kwargs)
|
||||
|
||||
|
||||
5
openapi_first/app.pyi
Normal file
5
openapi_first/app.pyi
Normal file
@@ -0,0 +1,5 @@
|
||||
from fastapi import FastAPI
|
||||
from typing import Any
|
||||
|
||||
class OpenAPIFirstApp(FastAPI):
|
||||
def __init__(self, *, openapi_path: str, routes_module: Any, **fastapi_kwargs: Any) -> None: ...
|
||||
@@ -1,36 +1,35 @@
|
||||
"""
|
||||
openapi_first.binder
|
||||
============================
|
||||
# Summary
|
||||
|
||||
OpenAPI-driven route binding for FastAPI.
|
||||
|
||||
This module is responsible for translating an OpenAPI 3.x specification
|
||||
into concrete FastAPI routes. It enforces a strict one-to-one mapping
|
||||
between OpenAPI operations and Python handler functions using operationId.
|
||||
between OpenAPI operations and Python handler functions using `operationId`.
|
||||
|
||||
Core responsibility
|
||||
-------------------
|
||||
- Read path + method definitions from an OpenAPI specification
|
||||
- Resolve each operationId to a Python callable
|
||||
- Register routes with FastAPI using APIRoute
|
||||
- Fail fast when contract violations are detected
|
||||
Notes:
|
||||
**Core Responsibility:**
|
||||
|
||||
Design constraints
|
||||
------------------
|
||||
- All routes MUST be declared in the OpenAPI specification.
|
||||
- All OpenAPI operations MUST define an operationId.
|
||||
- Every operationId MUST resolve to a handler function.
|
||||
- Handlers are plain Python callables (no decorators required).
|
||||
- No implicit route creation or inference is allowed.
|
||||
- Read path + method definitions from an OpenAPI specification.
|
||||
- Resolve each `operationId` to a Python callable.
|
||||
- Register routes with FastAPI using `APIRoute`.
|
||||
- Fail fast when contract violations are detected.
|
||||
|
||||
This module intentionally does NOT:
|
||||
-------------------------------
|
||||
- Perform request or response validation
|
||||
- Generate Pydantic models
|
||||
- Modify FastAPI dependency injection
|
||||
- Interpret OpenAPI semantics beyond routing metadata
|
||||
**Design Constraints:**
|
||||
|
||||
Those concerns belong to other layers or tooling.
|
||||
- All routes MUST be declared in the OpenAPI specification.
|
||||
- All OpenAPI operations MUST define an `operationId`.
|
||||
- Every `operationId` MUST resolve to a handler function.
|
||||
- Handlers are plain Python callables (no decorators required).
|
||||
- No implicit route creation or inference is allowed.
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- This module intentionally does NOT:
|
||||
- Perform request or response validation.
|
||||
- Generate Pydantic models.
|
||||
- Modify FastAPI dependency injection.
|
||||
- Interpret OpenAPI semantics beyond routing metadata.
|
||||
"""
|
||||
|
||||
from fastapi.routing import APIRoute
|
||||
@@ -42,33 +41,32 @@ def bind_routes(app, spec: dict, routes_module) -> None:
|
||||
"""
|
||||
Bind OpenAPI operations to FastAPI routes.
|
||||
|
||||
Iterates through the OpenAPI specification paths and methods,
|
||||
resolves each operationId to a handler function, and registers
|
||||
a corresponding APIRoute on the FastAPI application.
|
||||
Args:
|
||||
app (fastapi.FastAPI):
|
||||
The FastAPI application instance to which routes will be added.
|
||||
spec (dict):
|
||||
Parsed OpenAPI 3.x specification dictionary.
|
||||
routes_module (module):
|
||||
Python module containing handler functions. Each handler's name MUST
|
||||
exactly match an OpenAPI `operationId`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
app : fastapi.FastAPI
|
||||
The FastAPI application instance to which routes will be added.
|
||||
Raises:
|
||||
MissingOperationHandler:
|
||||
If an `operationId` is missing from the spec or if no corresponding
|
||||
handler function exists in the routes module.
|
||||
|
||||
spec : dict
|
||||
Parsed OpenAPI 3.x specification dictionary.
|
||||
Notes:
|
||||
**Responsibilities:**
|
||||
|
||||
routes_module : module
|
||||
Python module containing handler functions. Each handler's
|
||||
name MUST exactly match an OpenAPI operationId.
|
||||
- Iterates through the OpenAPI specification paths and methods.
|
||||
- Resolves each `operationId` to a handler function, and registers
|
||||
a corresponding `APIRoute` on the FastAPI application.
|
||||
|
||||
Raises
|
||||
------
|
||||
MissingOperationHandler
|
||||
If an operationId is missing from the spec or if no corresponding
|
||||
handler function exists in the routes module.
|
||||
**Guarantees:**
|
||||
|
||||
Behavior guarantees
|
||||
-------------------
|
||||
- Route registration is deterministic and spec-driven.
|
||||
- No route decorators are required or supported.
|
||||
- Handler resolution errors surface at application startup.
|
||||
- Route registration is deterministic and spec-driven. No route
|
||||
decorators are required or supported. Handler resolution errors
|
||||
surface at application startup.
|
||||
"""
|
||||
|
||||
paths = spec.get("paths", {})
|
||||
|
||||
4
openapi_first/binder.pyi
Normal file
4
openapi_first/binder.pyi
Normal file
@@ -0,0 +1,4 @@
|
||||
from typing import Any, Dict
|
||||
from fastapi import FastAPI
|
||||
|
||||
def bind_routes(app: FastAPI, spec: Dict[str, Any], routes_module: Any) -> None: ...
|
||||
@@ -1,19 +1,12 @@
|
||||
"""
|
||||
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)
|
||||
## Summary
|
||||
|
||||
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 +15,186 @@ 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.
|
||||
|
||||
This function copies a fully working, minimal OpenAPI-first FastAPI
|
||||
health check application from the package's embedded templates into
|
||||
the specified target directory.
|
||||
Returns:
|
||||
list[str]:
|
||||
Sorted list of template names found in the internal templates directory.
|
||||
"""
|
||||
root = resources.files("openapi_first.templates")
|
||||
return sorted(
|
||||
item.name
|
||||
for item in root.iterdir()
|
||||
if item.is_dir() and not item.name.startswith("_")
|
||||
)
|
||||
|
||||
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.
|
||||
def copy_template(template: str, target_dir: Path) -> None:
|
||||
"""
|
||||
Copy a bundled OpenAPI-first application template into a directory.
|
||||
|
||||
Raises
|
||||
------
|
||||
FileNotFoundError
|
||||
If the bundled health app template cannot be located.
|
||||
Args:
|
||||
template (str):
|
||||
Name of the template to copy.
|
||||
target_dir (Path):
|
||||
Filesystem path where the template should be copied.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError:
|
||||
If the requested template does not exist.
|
||||
"""
|
||||
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"
|
||||
description="FastAPI OpenAPI-first developer tools"
|
||||
)
|
||||
parser.add_argument(
|
||||
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
||||
|
||||
# Scaffold command
|
||||
scaffold_parser = subparsers.add_parser(
|
||||
"scaffold", help="Scaffold a new OpenAPI-first FastAPI application"
|
||||
)
|
||||
scaffold_parser.add_argument(
|
||||
"template",
|
||||
nargs="?",
|
||||
default=DEFAULT_TEMPLATE,
|
||||
help=f"Template name (default: {DEFAULT_TEMPLATE})",
|
||||
)
|
||||
scaffold_parser.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
default="health-app",
|
||||
help="Target directory for the health app",
|
||||
default=None,
|
||||
help="Target directory (defaults to template name)",
|
||||
)
|
||||
scaffold_parser.add_argument(
|
||||
"--list",
|
||||
action="store_true",
|
||||
help="List available templates and exit",
|
||||
)
|
||||
|
||||
# Models command
|
||||
models_parser = subparsers.add_parser(
|
||||
"models", help="Generate Pydantic models from an OpenAPI specification"
|
||||
)
|
||||
models_parser.add_argument(
|
||||
"spec",
|
||||
help="Path to the OpenAPI specification file",
|
||||
)
|
||||
models_parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
required=True,
|
||||
help="Path to the output Python file",
|
||||
)
|
||||
|
||||
# Routes command
|
||||
routes_parser = subparsers.add_parser(
|
||||
"routes",
|
||||
help="Generate route handler stubs from an OpenAPI specification",
|
||||
)
|
||||
routes_parser.add_argument(
|
||||
"spec",
|
||||
help="Path to the OpenAPI specification file",
|
||||
)
|
||||
routes_parser.add_argument(
|
||||
"-o",
|
||||
"--output-dir",
|
||||
required=True,
|
||||
help="Directory where route files will be created",
|
||||
)
|
||||
routes_parser.add_argument(
|
||||
"--use-models",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Import Pydantic models for request bodies",
|
||||
)
|
||||
routes_parser.add_argument(
|
||||
"--models-module",
|
||||
default="models",
|
||||
help="Python module path for models (default: models)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
copy_health_app_template(Path(args.path))
|
||||
print(f"Health app created at {args.path}")
|
||||
|
||||
if args.command == "models":
|
||||
from .codegen import generate_models
|
||||
|
||||
try:
|
||||
generate_models(Path(args.spec), Path(args.output))
|
||||
print(f"Models generated successfully at {args.output}")
|
||||
except Exception as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
return
|
||||
|
||||
if args.command == "routes":
|
||||
from .codegen import generate_routes
|
||||
|
||||
try:
|
||||
files = generate_routes(
|
||||
spec_path=Path(args.spec),
|
||||
output_dir=Path(args.output_dir),
|
||||
use_models=args.use_models,
|
||||
models_module=args.models_module,
|
||||
)
|
||||
print(f"Generated {len(files)} route file(s):")
|
||||
for f in files:
|
||||
print(f" {f}")
|
||||
except Exception as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
return
|
||||
|
||||
# Default to scaffold if no command or scaffold command
|
||||
if args.command == "scaffold" or args.command is None:
|
||||
# Handle the case where someone uses the old CLI style (openapi-first template path)
|
||||
# argparse with subparsers might not automatically handle this if positional args are passed
|
||||
# but let's assume we want to encourage the new style.
|
||||
|
||||
# If no command was provided but positional args were, they might be for scaffolding
|
||||
# This is a bit tricky with argparse subparsers.
|
||||
# For simplicity, let's just support the new explicit commands.
|
||||
|
||||
if args.command is None and not any(vars(args).values()):
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
if getattr(args, "list", False):
|
||||
for name in available_templates():
|
||||
print(name)
|
||||
return
|
||||
|
||||
template = getattr(args, "template", DEFAULT_TEMPLATE)
|
||||
path_arg = getattr(args, "path", None)
|
||||
target = Path(path_arg or template.replace("_", "-"))
|
||||
|
||||
try:
|
||||
copy_template(template, target)
|
||||
print(f"Template '{template}' created at {target}")
|
||||
except Exception as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
return
|
||||
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
"""
|
||||
# Summary
|
||||
|
||||
OpenAPI-first HTTP client for contract-driven services.
|
||||
|
||||
This module provides `OpenAPIClient`, a thin, strict HTTP client that
|
||||
derives all callable operations directly from an OpenAPI 3.x specification.
|
||||
|
||||
It is the client counterpart to `OpenAPIFirstApp`.
|
||||
|
||||
Notes:
|
||||
**Core Principles:**
|
||||
|
||||
- The OpenAPI specification is the single source of truth
|
||||
- Each operationId becomes a callable Python method
|
||||
- No implicit schema mutation or inference
|
||||
- No code generation step
|
||||
- Minimal abstraction over httpx
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Parses an OpenAPI 3.x specification
|
||||
- Dynamically creates one callable per operationId
|
||||
- Enforces presence of servers, paths, and operationId
|
||||
- Formats path parameters safely
|
||||
- Handles JSON request bodies explicitly
|
||||
- Returns raw `httpx.Response` objects
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- This module intentionally does NOT: Generate client code, validate request/response schemas, deserialize responses, retry requests, implement authentication helpers, or assume non-2xx responses are failures.
|
||||
"""
|
||||
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
@@ -7,16 +40,49 @@ from .errors import OpenAPIFirstError
|
||||
|
||||
|
||||
class OpenAPIClientError(OpenAPIFirstError):
|
||||
"""Raised when an OpenAPI client operation fails."""
|
||||
"""
|
||||
Raised when an OpenAPI client operation fails.
|
||||
"""
|
||||
|
||||
|
||||
class OpenAPIClient:
|
||||
"""
|
||||
OpenAPI-first HTTP client (httpx-based).
|
||||
OpenAPI-first HTTP client (`httpx`-based).
|
||||
|
||||
- One callable per operationId
|
||||
- Explicit parameters (path, query, headers, body)
|
||||
- No implicit schema inference or mutation
|
||||
Notes:
|
||||
**Responsibilities:**
|
||||
|
||||
- This client derives all callable methods directly from an
|
||||
OpenAPI 3.x specification. Each `operationId` becomes a method
|
||||
on the client instance.
|
||||
|
||||
**Guarantees:**
|
||||
|
||||
- One callable per `operationId`.
|
||||
- Explicit parameters (path, query, headers, body).
|
||||
- No implicit schema inference or mutation.
|
||||
- Returns raw `httpx.Response` objects.
|
||||
- No response validation or deserialization.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from openapi_first import loader, client
|
||||
|
||||
spec = loader.load_openapi("openapi.yaml")
|
||||
|
||||
api = client.OpenAPIClient(
|
||||
spec=spec,
|
||||
base_url="http://localhost:8000",
|
||||
)
|
||||
|
||||
# Call operationId: getUser
|
||||
response = api.getUser(
|
||||
path_params={"user_id": 123}
|
||||
)
|
||||
|
||||
print(response.status_code)
|
||||
print(response.json())
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -25,6 +91,21 @@ class OpenAPIClient:
|
||||
base_url: Optional[str] = None,
|
||||
client: Optional[httpx.Client] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the OpenAPI client.
|
||||
|
||||
Args:
|
||||
spec (dict[str, Any]):
|
||||
Parsed OpenAPI 3.x specification.
|
||||
base_url (str, optional):
|
||||
Base URL of the target service. If omitted, the first entry in the OpenAPI `servers` list is used.
|
||||
client (httpx.Client, optional):
|
||||
Optional preconfigured httpx client instance.
|
||||
|
||||
Raises:
|
||||
OpenAPIClientError:
|
||||
If no servers are defined, spec has no paths, operationIds are missing/duplicate, or required parameters are missing.
|
||||
"""
|
||||
self.spec = spec
|
||||
self.base_url = base_url or self._resolve_base_url(spec)
|
||||
self.client = client or httpx.Client(base_url=self.base_url)
|
||||
|
||||
13
openapi_first/client.pyi
Normal file
13
openapi_first/client.pyi
Normal file
@@ -0,0 +1,13 @@
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
import httpx
|
||||
from .errors import OpenAPIFirstError
|
||||
|
||||
class OpenAPIClientError(OpenAPIFirstError): ...
|
||||
|
||||
class OpenAPIClient:
|
||||
spec: Dict[str, Any]
|
||||
base_url: str
|
||||
client: httpx.Client
|
||||
def __init__(self, spec: Dict[str, Any], base_url: Optional[str] = ..., client: Optional[httpx.Client] = ...) -> None: ...
|
||||
def __getattr__(self, name: str) -> Callable[..., httpx.Response]: ...
|
||||
def operations(self) -> Dict[str, Callable[..., httpx.Response]]: ...
|
||||
53
openapi_first/codegen.py
Normal file
53
openapi_first/codegen.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
# Summary
|
||||
|
||||
Core logic for generating Python source code from OpenAPI specifications.
|
||||
|
||||
This module provides reusable utilities for code generation, specifically
|
||||
generating Pydantic models and route handler stubs from OpenAPI 3.x schema
|
||||
definitions.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from datamodel_code_generator import (
|
||||
InputFileType,
|
||||
PythonVersion,
|
||||
generate,
|
||||
)
|
||||
|
||||
from .codegen_routes import generate_routes
|
||||
|
||||
|
||||
def generate_models(
|
||||
spec_path: Path,
|
||||
output_path: Path,
|
||||
pydantic_version: int = 2,
|
||||
) -> None:
|
||||
"""
|
||||
Generate Pydantic models from an OpenAPI specification.
|
||||
|
||||
Args:
|
||||
spec_path (Path):
|
||||
Path to the OpenAPI specification file (YAML or JSON).
|
||||
output_path (Path):
|
||||
Path where the generated Python code should be written.
|
||||
pydantic_version (int, optional):
|
||||
The Pydantic version to target (1 or 2). Defaults to 2.
|
||||
|
||||
Notes:
|
||||
**Reusability:**
|
||||
This function is designed to be used by the CLI and can be
|
||||
exposed as an MCP tool without modification.
|
||||
"""
|
||||
generate(
|
||||
input_=spec_path,
|
||||
input_file_type=InputFileType.OpenAPI,
|
||||
output=output_path,
|
||||
target_python_version=PythonVersion.PY_310,
|
||||
use_schema_description=True,
|
||||
use_standard_collections=True,
|
||||
use_union_operator=True,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["generate_models", "generate_routes"]
|
||||
312
openapi_first/codegen_routes.py
Normal file
312
openapi_first/codegen_routes.py
Normal file
@@ -0,0 +1,312 @@
|
||||
"""
|
||||
Route handler code generation from OpenAPI specifications.
|
||||
|
||||
This module generates Python route handler stubs from an OpenAPI 3.x
|
||||
specification. Each resource (derived from the first path segment)
|
||||
gets its own file under the output directory. Every OpenAPI operation
|
||||
must define an ``operationId``, which becomes the handler function name.
|
||||
|
||||
Notes:
|
||||
**Design constraints:**
|
||||
|
||||
- ``operationId`` is required on every operation (matching
|
||||
``binder.bind_routes``).
|
||||
- Handlers are stubs raising ``NotImplementedError``.
|
||||
- Sub-resources (e.g. ``/pets/{id}/photo``) are grouped with their
|
||||
parent resource (``pets``).
|
||||
- Parameter types and defaults are inferred from the spec.
|
||||
- ``response: Response`` is injected for non-200 success codes.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .loader import load_openapi
|
||||
|
||||
|
||||
def generate_routes(
|
||||
spec_path: Path,
|
||||
output_dir: Path,
|
||||
*,
|
||||
use_models: bool = False,
|
||||
models_module: str = "models",
|
||||
) -> list[Path]:
|
||||
"""
|
||||
Generate route handler stubs from an OpenAPI specification.
|
||||
|
||||
Creates one ``<resource>.py`` file per resource in *output_dir*.
|
||||
Resources are derived from the first path segment (e.g. ``/pets``
|
||||
and ``/pets/{id}`` both group under ``pets``).
|
||||
|
||||
Args:
|
||||
spec_path:
|
||||
Path to the OpenAPI specification file (YAML or JSON).
|
||||
output_dir:
|
||||
Directory where the generated route files are written.
|
||||
Created automatically if it does not exist.
|
||||
use_models:
|
||||
If ``True``, import Pydantic models from *models_module*
|
||||
for request-body schemas referenced via ``$ref``.
|
||||
models_module:
|
||||
Dotted Python module path from which to import models
|
||||
(e.g. ``"models"``, ``"app.models"``).
|
||||
|
||||
Returns:
|
||||
list[Path]:
|
||||
Absolute paths of every generated route file.
|
||||
|
||||
Raises:
|
||||
OpenAPISpecLoadError:
|
||||
If the spec cannot be loaded or validated.
|
||||
ValueError:
|
||||
If any operation is missing ``operationId``.
|
||||
"""
|
||||
spec = load_openapi(spec_path)
|
||||
output_dir = Path(output_dir).resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Group paths by resource (first non-param path segment)
|
||||
resources: dict[str, list[tuple[str, str, dict]]] = {}
|
||||
|
||||
paths = spec.get("paths", {})
|
||||
for path, methods in paths.items():
|
||||
segments = [s for s in path.split("/") if s and not s.startswith("{")]
|
||||
if not segments:
|
||||
continue
|
||||
resource = segments[0]
|
||||
if resource not in resources:
|
||||
resources[resource] = []
|
||||
|
||||
for http_method, operation in methods.items():
|
||||
if http_method.startswith("x-"):
|
||||
continue
|
||||
resources[resource].append((path, http_method, operation))
|
||||
|
||||
generated_files: list[Path] = []
|
||||
|
||||
for resource in sorted(resources):
|
||||
operations = resources[resource]
|
||||
_validate_operations(resource, operations)
|
||||
|
||||
file_path = output_dir / f"{resource}.py"
|
||||
content = _generate_resource_file(
|
||||
resource=resource,
|
||||
operations=operations,
|
||||
spec_path=str(spec_path),
|
||||
use_models=use_models,
|
||||
models_module=models_module,
|
||||
)
|
||||
file_path.write_text(content, encoding="utf-8")
|
||||
generated_files.append(file_path)
|
||||
|
||||
return generated_files
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_TYPE_MAP: dict[str, str] = {
|
||||
"integer": "int",
|
||||
"number": "float",
|
||||
"boolean": "bool",
|
||||
"string": "str",
|
||||
"array": "list",
|
||||
"object": "dict",
|
||||
}
|
||||
|
||||
|
||||
def _validate_operations(resource: str, operations: list[tuple[str, str, dict]]) -> None:
|
||||
"""Ensure every operation has an operationId."""
|
||||
for path, http_method, operation in operations:
|
||||
if not operation.get("operationId"):
|
||||
raise ValueError(
|
||||
f"Missing operationId for {http_method.upper()} {path} "
|
||||
f"in resource '{resource}'. "
|
||||
"All operations must have an explicit operationId."
|
||||
)
|
||||
|
||||
|
||||
def _resolve_type(schema: dict[str, Any]) -> str:
|
||||
"""Map an OpenAPI schema type to a Python type annotation."""
|
||||
openapi_type = schema.get("type", "")
|
||||
return _TYPE_MAP.get(openapi_type, "str")
|
||||
|
||||
|
||||
def _get_request_body_schema(operation: dict[str, Any]) -> str | None:
|
||||
"""Extract the ``$ref`` schema name from a request body, if any."""
|
||||
request_body = operation.get("requestBody")
|
||||
if not request_body:
|
||||
return None
|
||||
|
||||
content = request_body.get("content", {})
|
||||
for media_info in content.values():
|
||||
schema = media_info.get("schema", {})
|
||||
ref = schema.get("$ref", "")
|
||||
if ref:
|
||||
return ref.rsplit("/", 1)[-1]
|
||||
return None
|
||||
|
||||
|
||||
def _get_success_status(operation: dict[str, Any]) -> str:
|
||||
"""Return the primary success status code (200, 201, 204, or default)."""
|
||||
responses = operation.get("responses", {})
|
||||
for code in ("201", "200", "204"):
|
||||
if code in responses:
|
||||
return code
|
||||
if "default" in responses:
|
||||
return "default"
|
||||
return "200"
|
||||
|
||||
|
||||
def _needs_any(operations: list[tuple[str, str, dict]]) -> bool:
|
||||
"""Check if any operation uses a type that requires `from typing import Any`."""
|
||||
for _, _, op in operations:
|
||||
for param in op.get("parameters", []):
|
||||
schema = param.get("schema", {})
|
||||
if schema.get("type", "") not in _TYPE_MAP:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# File / function generation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _generate_resource_file(
|
||||
resource: str,
|
||||
operations: list[tuple[str, str, dict]],
|
||||
spec_path: str,
|
||||
use_models: bool,
|
||||
models_module: str,
|
||||
) -> str:
|
||||
"""Assemble the full Python source for a single resource file."""
|
||||
op_ids = [op.get("operationId", "") for _, _, op in operations]
|
||||
lines: list[str] = [
|
||||
f'"""Route handlers for the {resource} resource.',
|
||||
"",
|
||||
f"Generated from OpenAPI spec: {spec_path}",
|
||||
f'Bind via operationIds: {", ".join(op_ids)}',
|
||||
'"""',
|
||||
"",
|
||||
"from fastapi import Response, HTTPException",
|
||||
]
|
||||
|
||||
# Conditional typing import
|
||||
if _needs_any(operations):
|
||||
lines.append("from typing import Any")
|
||||
|
||||
# Conditional model imports
|
||||
if use_models:
|
||||
schemas_needed: set[str] = set()
|
||||
for _, _, op in operations:
|
||||
schema = _get_request_body_schema(op)
|
||||
if schema:
|
||||
schemas_needed.add(schema)
|
||||
if schemas_needed:
|
||||
lines.append(f"from {models_module} import {', '.join(sorted(schemas_needed))}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
for path, http_method, operation in operations:
|
||||
lines.append("")
|
||||
lines.extend(_generate_handler(path, http_method, operation, use_models))
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _generate_handler(
|
||||
path: str,
|
||||
http_method: str,
|
||||
operation: dict[str, Any],
|
||||
use_models: bool,
|
||||
) -> list[str]:
|
||||
"""Build the source lines for a single handler function."""
|
||||
operation_id = operation.get("operationId", "")
|
||||
summary = operation.get("summary", f"{http_method.upper()} {path}")
|
||||
|
||||
params: list[str] = []
|
||||
path_params: list[str] = []
|
||||
query_params: list[str] = []
|
||||
|
||||
for param in operation.get("parameters", []):
|
||||
name: str = param.get("name", "")
|
||||
param_in: str = param.get("in", "")
|
||||
schema: dict[str, Any] = param.get("schema", {})
|
||||
param_type: str = _resolve_type(schema)
|
||||
required: bool = param.get("required", False)
|
||||
description: str = schema.get("description", schema.get("x-description", ""))
|
||||
default_raw = schema.get("default")
|
||||
|
||||
if param_in == "path":
|
||||
path_params.append(f"{name}: {param_type}")
|
||||
elif param_in == "query":
|
||||
if default_raw is not None:
|
||||
default_repr = repr(default_raw)
|
||||
query_params.append(f"{name}: {param_type} = {default_repr}")
|
||||
elif required:
|
||||
query_params.append(f"{name}: {param_type}")
|
||||
else:
|
||||
query_params.append(f"{name}: {param_type} = None")
|
||||
# header / cookie params could be extended here
|
||||
|
||||
params.extend(path_params)
|
||||
params.extend(query_params)
|
||||
|
||||
# Request body
|
||||
schema_name = _get_request_body_schema(operation)
|
||||
if operation.get("requestBody"):
|
||||
if use_models and schema_name:
|
||||
params.append(f"payload: {schema_name}")
|
||||
else:
|
||||
params.append("payload: dict")
|
||||
|
||||
# Inject Response for non-200 success codes
|
||||
success_status = _get_success_status(operation)
|
||||
if success_status in ("201", "204"):
|
||||
params.append("response: Response")
|
||||
|
||||
# Build function body
|
||||
lines: list[str] = []
|
||||
param_str = ", ".join(params)
|
||||
lines.append(f"def {operation_id}({param_str}):")
|
||||
lines.append(f' """{summary}')
|
||||
|
||||
# Document parameters
|
||||
doc_params: list[tuple[str, str, str]] = []
|
||||
for param in operation.get("parameters", []):
|
||||
name: str = param.get("name", "")
|
||||
param_in: str = param.get("in", "")
|
||||
schema: dict[str, Any] = param.get("schema", {})
|
||||
param_type: str = _resolve_type(schema)
|
||||
description: str = param.get("description", schema.get("x-description", ""))
|
||||
if param_in in ("path", "query"):
|
||||
doc_params.append((name, param_type, description))
|
||||
|
||||
if doc_params:
|
||||
lines.append("")
|
||||
lines.append(" Parameters")
|
||||
lines.append(" ----------")
|
||||
for pname, ptype, desc in doc_params:
|
||||
if desc:
|
||||
lines.append(f" {pname} : {ptype}")
|
||||
lines.append(f" {desc}")
|
||||
else:
|
||||
lines.append(f" {pname} : {ptype}")
|
||||
|
||||
if operation.get("requestBody"):
|
||||
if use_models and schema_name:
|
||||
lines.append("")
|
||||
lines.append(f" payload : {schema_name}")
|
||||
lines.append(" Request body.")
|
||||
else:
|
||||
lines.append("")
|
||||
lines.append(" payload : dict")
|
||||
lines.append(" Request body.")
|
||||
|
||||
lines.append(' """')
|
||||
lines.append(" raise NotImplementedError")
|
||||
|
||||
return lines
|
||||
@@ -1,33 +1,35 @@
|
||||
"""
|
||||
openapi_first.errors
|
||||
============================
|
||||
# Summary
|
||||
|
||||
Custom exceptions for OpenAPI-first FastAPI applications.
|
||||
Exceptions for OpenAPI-first FastAPI applications.
|
||||
|
||||
This module defines a small hierarchy of explicit, intention-revealing
|
||||
exceptions used to signal contract violations between an OpenAPI
|
||||
specification and its Python implementation.
|
||||
|
||||
Design principles
|
||||
-----------------
|
||||
- Errors represent *programmer mistakes*, not runtime conditions.
|
||||
- All errors are raised during application startup.
|
||||
- Messages are actionable and suitable for CI/CD output.
|
||||
- Exceptions are explicit rather than reused from generic built-ins.
|
||||
Notes:
|
||||
**Design Principles:**
|
||||
|
||||
These errors should normally cause immediate application failure.
|
||||
- Errors represent programmer mistakes, not runtime conditions.
|
||||
- All errors are raised during application startup.
|
||||
- Messages are actionable and suitable for CI/CD output.
|
||||
- Exceptions are explicit rather than reused from generic built-ins.
|
||||
|
||||
These errors should normally cause immediate application failure.
|
||||
"""
|
||||
|
||||
class OpenAPIFirstError(Exception):
|
||||
"""
|
||||
Base exception for all OpenAPI-first enforcement errors.
|
||||
|
||||
This exception exists to allow callers, test suites, and CI pipelines
|
||||
to catch and distinguish OpenAPI contract violations from unrelated
|
||||
runtime errors.
|
||||
Notes:
|
||||
**Responsibilities:**
|
||||
|
||||
All exceptions raised by the OpenAPI-first core should inherit from
|
||||
this type.
|
||||
- This exception exists to allow callers, test suites, and CI
|
||||
pipelines to catch and distinguish OpenAPI contract violations
|
||||
from unrelated runtime errors.
|
||||
- All exceptions raised by the OpenAPI-first core should inherit
|
||||
from this type.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -36,27 +38,31 @@ class MissingOperationHandler(OpenAPIFirstError):
|
||||
"""
|
||||
Raised when an OpenAPI operation cannot be resolved to a handler.
|
||||
|
||||
This error occurs when:
|
||||
- An OpenAPI operation does not define an operationId, or
|
||||
- An operationId is defined but no matching function exists in the
|
||||
provided routes module.
|
||||
Notes:
|
||||
**Scenarios:**
|
||||
|
||||
This represents a violation of the OpenAPI-first contract and
|
||||
indicates that the specification and implementation are out of sync.
|
||||
- An OpenAPI operation does not define an `operationId`.
|
||||
- An `operationId` is defined but no matching function exists in
|
||||
the provided routes module.
|
||||
|
||||
**Guarantees:**
|
||||
|
||||
- This represents a violation of the OpenAPI-first contract and
|
||||
indicates that the specification and implementation are out of
|
||||
sync.
|
||||
"""
|
||||
|
||||
def __init__(self, *, path: str, method: str, operation_id: str | None = None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
The HTTP path declared in the OpenAPI specification.
|
||||
Initialize the error.
|
||||
|
||||
method : str
|
||||
The HTTP method (as declared in the OpenAPI spec).
|
||||
|
||||
operation_id : str, optional
|
||||
The operationId declared in the OpenAPI spec, if present.
|
||||
Args:
|
||||
path (str):
|
||||
The HTTP path declared in the OpenAPI specification.
|
||||
method (str):
|
||||
The HTTP method (as declared in the OpenAPI spec).
|
||||
operation_id (str, optional):
|
||||
The operationId declared in the OpenAPI spec, if present.
|
||||
"""
|
||||
if operation_id:
|
||||
message = (
|
||||
|
||||
6
openapi_first/errors.pyi
Normal file
6
openapi_first/errors.pyi
Normal file
@@ -0,0 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
class OpenAPIFirstError(Exception): ...
|
||||
|
||||
class MissingOperationHandler(OpenAPIFirstError):
|
||||
def __init__(self, *, path: str, method: str, operation_id: Optional[str] = ...) -> None: ...
|
||||
@@ -1,6 +1,5 @@
|
||||
"""
|
||||
openapi_first.loaders
|
||||
=============================
|
||||
# Summary
|
||||
|
||||
OpenAPI specification loading and validation utilities.
|
||||
|
||||
@@ -10,19 +9,21 @@ from disk and validating it before it is used by the application.
|
||||
It enforces the principle that an invalid or malformed OpenAPI document
|
||||
must never reach the routing or runtime layers.
|
||||
|
||||
Design principles
|
||||
-----------------
|
||||
- OpenAPI is treated as an authoritative contract.
|
||||
- Invalid specifications fail fast at application startup.
|
||||
- Supported formats are JSON and YAML.
|
||||
- Validation errors are surfaced clearly and early.
|
||||
Notes:
|
||||
**Design Principles:**
|
||||
|
||||
This module intentionally does NOT:
|
||||
-----------------------------------
|
||||
- Modify the OpenAPI document
|
||||
- Infer missing fields
|
||||
- Generate models or code
|
||||
- Perform request/response validation at runtime
|
||||
- OpenAPI is treated as an authoritative contract.
|
||||
- Invalid specifications fail fast at application startup.
|
||||
- Supported formats are JSON and YAML.
|
||||
- Validation errors are surfaced clearly and early.
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- This module intentionally does NOT:
|
||||
- Modify the OpenAPI document.
|
||||
- Infer missing fields.
|
||||
- Generate models or code.
|
||||
- Perform request/response validation at runtime.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -39,8 +40,11 @@ class OpenAPISpecLoadError(OpenAPIFirstError):
|
||||
"""
|
||||
Raised when an OpenAPI specification cannot be loaded or validated.
|
||||
|
||||
This error indicates that the OpenAPI document is unreadable,
|
||||
malformed, or violates the OpenAPI 3.x specification.
|
||||
Notes:
|
||||
**Guarantees:**
|
||||
|
||||
- This error indicates that the OpenAPI document is unreadable,
|
||||
malformed, or violates the OpenAPI 3.x specification.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -49,29 +53,27 @@ def load_openapi(path: str | Path) -> dict[str, Any]:
|
||||
"""
|
||||
Load and validate an OpenAPI 3.x specification from disk.
|
||||
|
||||
The specification is parsed based on file extension and validated
|
||||
using a strict OpenAPI schema validator. Any error results in an
|
||||
immediate exception, preventing application startup.
|
||||
Args:
|
||||
path (str | Path):
|
||||
Filesystem path to an OpenAPI specification file. Supported
|
||||
extensions: `.json`, `.yaml`, `.yml`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str or pathlib.Path
|
||||
Filesystem path to an OpenAPI specification file.
|
||||
Supported extensions:
|
||||
- `.json`
|
||||
- `.yaml`
|
||||
- `.yml`
|
||||
Returns:
|
||||
dict[str, Any]:
|
||||
Parsed and validated OpenAPI specification.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Parsed and validated OpenAPI specification.
|
||||
Raises:
|
||||
OpenAPISpecLoadError:
|
||||
If the file does not exist, cannot be parsed, or fails OpenAPI
|
||||
schema validation.
|
||||
|
||||
Raises
|
||||
------
|
||||
OpenAPISpecLoadError
|
||||
If the file does not exist, cannot be parsed, or fails
|
||||
OpenAPI schema validation.
|
||||
Notes:
|
||||
**Guarantees:**
|
||||
|
||||
- The specification is parsed based on file extension and validated
|
||||
using a strict OpenAPI schema validator.
|
||||
- Any error results in an immediate exception, preventing
|
||||
application startup.
|
||||
"""
|
||||
spec_path = Path(path)
|
||||
|
||||
|
||||
8
openapi_first/loader.pyi
Normal file
8
openapi_first/loader.pyi
Normal file
@@ -0,0 +1,8 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
from .errors import OpenAPIFirstError
|
||||
|
||||
class OpenAPISpecLoadError(OpenAPIFirstError): ...
|
||||
|
||||
def load_openapi(path: Union[str, Path]) -> Dict[str, Any]: ...
|
||||
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.
|
||||
"""
|
||||
99
openapi_first/templates/crud_app/__init__.py
Normal file
99
openapi_first/templates/crud_app/__init__.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
OpenAPI-first CRUD application template.
|
||||
|
||||
This package contains a complete, minimal example of an OpenAPI-first
|
||||
CRUD service built using the ``openapi_first`` library.
|
||||
|
||||
The application is assembled exclusively from:
|
||||
- an OpenAPI specification (``openapi.yaml``)
|
||||
- a handler namespace implementing CRUD operations (``routes``)
|
||||
- an in-memory mock data store (``data``)
|
||||
|
||||
All HTTP routes, methods, schemas, and operation bindings are defined
|
||||
in the OpenAPI specification and enforced at application startup.
|
||||
No decorator-driven routing or implicit framework behavior is used.
|
||||
|
||||
This template demonstrates:
|
||||
- operationId-driven server-side route binding
|
||||
- explicit HTTP status code control in handlers
|
||||
- operationId-driven client usage against the same OpenAPI contract
|
||||
- end-to-end validation using in-memory data and tests
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Scaffolding via CLI
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Create a new CRUD example service using the bundled template:
|
||||
|
||||
openapi-first crud_app
|
||||
|
||||
Create the service in a custom directory:
|
||||
|
||||
openapi-first crud_app my-crud-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 calls for CRUD operations:
|
||||
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
spec = load_openapi("openapi.yaml")
|
||||
client = OpenAPIClient(spec)
|
||||
|
||||
# List items
|
||||
response = client.list_items()
|
||||
|
||||
# Get item by ID
|
||||
response = client.get_item(
|
||||
path_params={"item_id": 1}
|
||||
)
|
||||
|
||||
# Create item
|
||||
response = client.create_item(
|
||||
body={"name": "Orange", "price": 0.8}
|
||||
)
|
||||
|
||||
# Update item
|
||||
response = client.update_item(
|
||||
path_params={"item_id": 1},
|
||||
body={"name": "Green Apple", "price": 0.6},
|
||||
)
|
||||
|
||||
# Delete item
|
||||
response = client.delete_item(
|
||||
path_params={"item_id": 1}
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Non-Goals
|
||||
----------------------------------------------------------------------
|
||||
|
||||
This template is intentionally minimal and is NOT:
|
||||
- production-ready
|
||||
- persistent or concurrency-safe
|
||||
- a reference architecture for data storage
|
||||
|
||||
It exists solely as a copyable example for learning, testing, and
|
||||
bootstrapping OpenAPI-first services.
|
||||
|
||||
This package is not part of the ``openapi_first`` library API surface.
|
||||
"""
|
||||
@@ -3,27 +3,88 @@ In-memory mock data store for CRUD example.
|
||||
|
||||
This module intentionally avoids persistence and concurrency guarantees.
|
||||
It is suitable for demos, tests, and scaffolding only.
|
||||
|
||||
It intentionally avoids
|
||||
- persistence
|
||||
- concurrency guarantees
|
||||
- validation
|
||||
- error handling
|
||||
|
||||
The implementation is suitable for:
|
||||
- demonstrations
|
||||
- tests
|
||||
- scaffolding and example services
|
||||
|
||||
It is explicitly NOT suitable for production use.
|
||||
|
||||
This module is not part of the ``openapi_first`` library API surface.
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
|
||||
|
||||
# In-memory storage keyed by item ID.
|
||||
_items: Dict[int, dict] = {
|
||||
1: {"id": 1, "name": "Apple", "price": 0.5},
|
||||
2: {"id": 2, "name": "Banana", "price": 0.3},
|
||||
}
|
||||
|
||||
# Auto-incrementing ID counter.
|
||||
_next_id = 3
|
||||
|
||||
|
||||
def list_items():
|
||||
"""
|
||||
Return all items in the data store.
|
||||
|
||||
This function performs no filtering, pagination, or sorting.
|
||||
The returned collection reflects the current in-memory state.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[dict]
|
||||
A list of item representations.
|
||||
"""
|
||||
return list(_items.values())
|
||||
|
||||
|
||||
def get_item(item_id: int):
|
||||
"""
|
||||
Retrieve a single item by ID.
|
||||
|
||||
This function assumes the item exists and will raise ``KeyError``
|
||||
if the ID is not present in the store.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to retrieve.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The stored item representation.
|
||||
"""
|
||||
return _items[item_id]
|
||||
|
||||
|
||||
def create_item(payload: dict):
|
||||
"""
|
||||
Create a new item in the data store.
|
||||
|
||||
A new integer ID is assigned automatically. No validation is
|
||||
performed on the provided payload.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
payload : dict
|
||||
Item attributes excluding the ``id`` field.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The newly created item, including its assigned ID.
|
||||
"""
|
||||
global _next_id
|
||||
item = {"id": _next_id, **payload}
|
||||
_items[_next_id] = item
|
||||
@@ -32,10 +93,40 @@ def create_item(payload: dict):
|
||||
|
||||
|
||||
def update_item(item_id: int, payload: dict):
|
||||
"""
|
||||
Replace an existing item in the data store.
|
||||
|
||||
This function overwrites the existing item entirely and does not
|
||||
perform partial updates or validation. If the item does not exist,
|
||||
it will be created implicitly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to update.
|
||||
payload : dict
|
||||
Item attributes excluding the ``id`` field.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The updated item representation.
|
||||
"""
|
||||
item = {"id": item_id, **payload}
|
||||
_items[item_id] = item
|
||||
return item
|
||||
|
||||
|
||||
def delete_item(item_id: int):
|
||||
"""
|
||||
Remove an item from the data store.
|
||||
|
||||
This function assumes the item exists and will raise ``KeyError``
|
||||
if the ID is not present.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to delete.
|
||||
"""
|
||||
del _items[item_id]
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
"""
|
||||
Application entry point for an OpenAPI-first CRUD example 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, request/response schemas, 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, persistence concerns, 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
|
||||
|
||||
|
||||
@@ -3,6 +3,19 @@ CRUD route handlers bound via OpenAPI operationId.
|
||||
|
||||
These handlers explicitly control HTTP status codes to ensure
|
||||
runtime behavior matches the OpenAPI contract.
|
||||
|
||||
This module defines OpenAPI-bound operation handlers for a simple CRUD
|
||||
service. Functions in this module are bound to HTTP routes exclusively
|
||||
via OpenAPI ``operationId`` values.
|
||||
|
||||
Handlers explicitly control HTTP response status codes to ensure runtime
|
||||
behavior matches the OpenAPI contract. Error conditions are translated
|
||||
into explicit HTTP responses rather than relying on implicit framework
|
||||
behavior.
|
||||
|
||||
No routing decorators or path definitions appear in this module. All
|
||||
routing, HTTP methods, and schemas are defined in the OpenAPI
|
||||
specification.
|
||||
"""
|
||||
|
||||
from fastapi import Response, HTTPException
|
||||
@@ -17,10 +30,42 @@ from data import (
|
||||
|
||||
|
||||
def list_items():
|
||||
"""
|
||||
List all items.
|
||||
|
||||
Implements the OpenAPI operation identified by
|
||||
``operationId: list_items``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[dict]
|
||||
A list of item representations.
|
||||
"""
|
||||
return _list_items()
|
||||
|
||||
|
||||
def get_item(item_id: int):
|
||||
"""
|
||||
Retrieve a single item by ID.
|
||||
|
||||
Implements the OpenAPI operation identified by
|
||||
``operationId: get_item``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to retrieve.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The requested item.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the item does not exist.
|
||||
"""
|
||||
try:
|
||||
return _get_item(item_id)
|
||||
except KeyError:
|
||||
@@ -28,12 +73,53 @@ def get_item(item_id: int):
|
||||
|
||||
|
||||
def create_item(payload: dict, response: Response):
|
||||
"""
|
||||
Create a new item.
|
||||
|
||||
Implements the OpenAPI operation identified by
|
||||
``operationId: create_item``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
payload : dict
|
||||
Item attributes excluding the ``id`` field.
|
||||
response : fastapi.Response
|
||||
Response object used to set the HTTP status code.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The newly created item.
|
||||
"""
|
||||
item = _create_item(payload)
|
||||
response.status_code = 201
|
||||
return item
|
||||
|
||||
|
||||
def update_item(item_id: int, payload: dict):
|
||||
"""
|
||||
Update an existing item.
|
||||
|
||||
Implements the OpenAPI operation identified by
|
||||
``operationId: update_item``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to update.
|
||||
payload : dict
|
||||
Item attributes excluding the ``id`` field.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The updated item.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the item does not exist.
|
||||
"""
|
||||
try:
|
||||
return _update_item(item_id, payload)
|
||||
except KeyError:
|
||||
@@ -41,10 +127,32 @@ def update_item(item_id: int, payload: dict):
|
||||
|
||||
|
||||
def delete_item(item_id: int, response: Response):
|
||||
"""
|
||||
Delete an existing item.
|
||||
|
||||
Implements the OpenAPI operation identified by
|
||||
``operationId: delete_item``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to delete.
|
||||
response : fastapi.Response
|
||||
Response object used to set the HTTP status code.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
No content.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the item does not exist.
|
||||
"""
|
||||
try:
|
||||
_delete_item(item_id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
|
||||
response.status_code = 204
|
||||
return None
|
||||
|
||||
@@ -3,11 +3,20 @@ End-to-end tests for the OpenAPI-first CRUD example app.
|
||||
|
||||
These tests validate that all CRUD operations behave correctly
|
||||
against the in-memory mock data store.
|
||||
- OpenAPI specification loading
|
||||
- OperationId-driven route binding on the server
|
||||
- OperationId-driven client invocation
|
||||
- Correct HTTP status codes and response payloads
|
||||
|
||||
The tests exercise all CRUD operations against an in-memory mock data
|
||||
store and assume deterministic behavior within a single process.
|
||||
|
||||
The tests assume:
|
||||
- OpenAPI-first route binding
|
||||
- In-memory storage (no persistence guarantees)
|
||||
- Deterministic behavior in a single process
|
||||
- One-to-one correspondence between OpenAPI operationId values and
|
||||
server/client callables
|
||||
"""
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -61,7 +70,7 @@ def test_create_item():
|
||||
}
|
||||
|
||||
response = client.create_item(
|
||||
body=payload
|
||||
body=payload,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
@@ -106,7 +115,7 @@ def test_update_item():
|
||||
def test_delete_item():
|
||||
"""Deleting an item should remove it from the store."""
|
||||
response = client.delete_item(
|
||||
path_params={"item_id": 2}
|
||||
path_params={"item_id": 2},
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
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
|
||||
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():
|
||||
"""
|
||||
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"}
|
||||
|
||||
102
openapi_first/templates/model_app/__init__.py
Normal file
102
openapi_first/templates/model_app/__init__.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
OpenAPI-first model-based CRUD application template.
|
||||
|
||||
This package contains a complete, minimal example of an OpenAPI-first
|
||||
CRUD service that uses explicit Pydantic domain models for request and
|
||||
response schemas.
|
||||
|
||||
The application is assembled exclusively from:
|
||||
- an OpenAPI specification (``openapi.yaml``)
|
||||
- a handler namespace implementing CRUD operations (``routes``)
|
||||
- Pydantic domain models (``models``)
|
||||
- an in-memory mock data store (``data``)
|
||||
|
||||
All HTTP routes, methods, schemas, and operation bindings are defined
|
||||
in the OpenAPI specification and enforced at application startup.
|
||||
No decorator-driven routing or implicit framework behavior is used.
|
||||
|
||||
This template demonstrates:
|
||||
- operationId-driven server-side route binding
|
||||
- explicit request and response modeling with Pydantic
|
||||
- explicit HTTP status code control in handlers
|
||||
- operationId-driven client usage against the same OpenAPI contract
|
||||
- end-to-end validation using in-memory data and tests
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Scaffolding via CLI
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Create a new model-based CRUD example service using the bundled template:
|
||||
|
||||
openapi-first model_app
|
||||
|
||||
Create the service in a custom directory:
|
||||
|
||||
openapi-first model_app my-model-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 calls for model-based CRUD operations:
|
||||
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
spec = load_openapi("openapi.yaml")
|
||||
client = OpenAPIClient(spec)
|
||||
|
||||
# List items
|
||||
response = client.list_items()
|
||||
|
||||
# Get item by ID
|
||||
response = client.get_item(
|
||||
path_params={"item_id": 1}
|
||||
)
|
||||
|
||||
# Create item
|
||||
response = client.create_item(
|
||||
body={"name": "Orange", "price": 0.8}
|
||||
)
|
||||
|
||||
# Update item
|
||||
response = client.update_item(
|
||||
path_params={"item_id": 1},
|
||||
body={"name": "Green Apple", "price": 0.6},
|
||||
)
|
||||
|
||||
# Delete item
|
||||
response = client.delete_item(
|
||||
path_params={"item_id": 1}
|
||||
)
|
||||
|
||||
Client guarantees:
|
||||
- One callable per OpenAPI ``operationId``
|
||||
- No hardcoded URLs or HTTP methods in user code
|
||||
- Request and response payloads conform to Pydantic models
|
||||
- Invalid or incomplete OpenAPI specs fail at client construction time
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Non-Goals
|
||||
----------------------------------------------------------------------
|
||||
|
||||
This template is intentionally minimal and is NOT:
|
||||
- production-ready
|
||||
- persistent or concurrency-safe
|
||||
- a reference architecture for data storage
|
||||
|
||||
It exists solely as a copyable example for learning, testing, and
|
||||
bootstrapping OpenAPI-first services.
|
||||
|
||||
This package is not part of the ``openapi_first`` library API surface.
|
||||
"""
|
||||
@@ -2,29 +2,86 @@
|
||||
In-memory data store using Pydantic models.
|
||||
|
||||
This module is NOT thread-safe and is intended for demos and scaffolds only.
|
||||
This module provides a minimal, process-local data store for the
|
||||
model-based CRUD example application. It stores and returns domain
|
||||
objects defined using Pydantic models and is intended solely for
|
||||
demonstration and scaffolding purposes.
|
||||
|
||||
The implementation intentionally avoids:
|
||||
- persistence
|
||||
- concurrency guarantees
|
||||
- transactional semantics
|
||||
- validation beyond what Pydantic provides
|
||||
|
||||
It is not part of the ``openapi_first`` library API surface.
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from models import Item, ItemCreate
|
||||
|
||||
|
||||
# In-memory storage keyed by item ID.
|
||||
_items: Dict[int, Item] = {
|
||||
1: Item(id=1, name="Apple", price=0.5),
|
||||
2: Item(id=2, name="Banana", price=0.3),
|
||||
}
|
||||
|
||||
# Auto-incrementing identifier.
|
||||
_next_id = 3
|
||||
|
||||
|
||||
def list_items() -> list[Item]:
|
||||
"""
|
||||
Return all items in the data store.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[Item]
|
||||
A list of item domain objects.
|
||||
"""
|
||||
return list(_items.values())
|
||||
|
||||
|
||||
def get_item(item_id: int) -> Item:
|
||||
"""
|
||||
Retrieve a single item by ID.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to retrieve.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Item
|
||||
The requested item.
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
If the item does not exist.
|
||||
"""
|
||||
return _items[item_id]
|
||||
|
||||
|
||||
def create_item(payload: ItemCreate) -> Item:
|
||||
"""
|
||||
Create a new item in the data store.
|
||||
|
||||
A new identifier is assigned automatically. No additional validation
|
||||
is performed beyond Pydantic model validation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
payload : ItemCreate
|
||||
Data required to create a new item.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Item
|
||||
The newly created item.
|
||||
"""
|
||||
global _next_id
|
||||
item = Item(id=_next_id, **payload.model_dump())
|
||||
_items[_next_id] = item
|
||||
@@ -33,6 +90,29 @@ def create_item(payload: ItemCreate) -> Item:
|
||||
|
||||
|
||||
def update_item(item_id: int, payload: ItemCreate) -> Item:
|
||||
"""
|
||||
Replace an existing item in the data store.
|
||||
|
||||
This function performs a full replacement of the stored item.
|
||||
Partial updates are not supported.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to update.
|
||||
payload : ItemCreate
|
||||
New item data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Item
|
||||
The updated item.
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
If the item does not exist.
|
||||
"""
|
||||
if item_id not in _items:
|
||||
raise KeyError(item_id)
|
||||
item = Item(id=item_id, **payload.model_dump())
|
||||
@@ -41,4 +121,17 @@ def update_item(item_id: int, payload: ItemCreate) -> Item:
|
||||
|
||||
|
||||
def delete_item(item_id: int) -> None:
|
||||
"""
|
||||
Remove an item from the data store.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to delete.
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
If the item does not exist.
|
||||
"""
|
||||
del _items[item_id]
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
"""
|
||||
Application entry point for an OpenAPI-first model-based CRUD example 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, request/response schemas, 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, persistence concerns, 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
|
||||
|
||||
|
||||
@@ -1,18 +1,50 @@
|
||||
"""
|
||||
Pydantic domain models for the CRUD example.
|
||||
|
||||
This module defines Pydantic models that represent the domain entities
|
||||
used by the service. These models are referenced by the OpenAPI
|
||||
specification for request and response schemas.
|
||||
|
||||
The models are declarative and framework-agnostic. They contain no
|
||||
persistence logic, validation beyond type constraints, or business
|
||||
behavior.
|
||||
|
||||
This module is not part of the ``openapi_first`` library API surface.
|
||||
It exists solely to support the example application template.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ItemBase(BaseModel):
|
||||
"""
|
||||
Base domain model for an item.
|
||||
|
||||
Defines fields common to all item representations.
|
||||
"""
|
||||
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ItemCreate(ItemBase):
|
||||
"""
|
||||
Domain model for item creation requests.
|
||||
|
||||
This model is used for request bodies when creating new items.
|
||||
It intentionally excludes the ``id`` field, which is assigned
|
||||
by the service.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Item(ItemBase):
|
||||
"""
|
||||
Domain model for a persisted item.
|
||||
|
||||
This model represents the full item state returned in responses,
|
||||
including the server-assigned identifier.
|
||||
"""
|
||||
|
||||
id: int
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
"""
|
||||
CRUD route handlers bound via OpenAPI operationId.
|
||||
|
||||
This module defines OpenAPI-bound operation handlers for a model-based
|
||||
CRUD service. Functions in this module are bound to HTTP routes
|
||||
exclusively via OpenAPI ``operationId`` values.
|
||||
|
||||
Handlers explicitly control HTTP response status codes to ensure runtime
|
||||
behavior matches the OpenAPI contract. Domain models defined using
|
||||
Pydantic are used for request and response payloads.
|
||||
|
||||
No routing decorators, path definitions, or implicit framework behavior
|
||||
appear in this module. All routing, HTTP methods, and schemas are defined
|
||||
in the OpenAPI specification.
|
||||
"""
|
||||
|
||||
from fastapi import Response, HTTPException
|
||||
@@ -15,10 +27,42 @@ from data import (
|
||||
|
||||
|
||||
def list_items():
|
||||
"""
|
||||
List all items.
|
||||
|
||||
Implements the OpenAPI operation identified by
|
||||
``operationId: list_items``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[Item]
|
||||
A list of item domain objects.
|
||||
"""
|
||||
return _list_items()
|
||||
|
||||
|
||||
def get_item(item_id: int):
|
||||
"""
|
||||
Retrieve a single item by ID.
|
||||
|
||||
Implements the OpenAPI operation identified by
|
||||
``operationId: get_item``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to retrieve.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Item
|
||||
The requested item.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the item does not exist.
|
||||
"""
|
||||
try:
|
||||
return _get_item(item_id)
|
||||
except KeyError:
|
||||
@@ -26,12 +70,53 @@ def get_item(item_id: int):
|
||||
|
||||
|
||||
def create_item(payload: ItemCreate, response: Response):
|
||||
"""
|
||||
Create a new item.
|
||||
|
||||
Implements the OpenAPI operation identified by
|
||||
``operationId: create_item``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
payload : ItemCreate
|
||||
Request body describing the item to create.
|
||||
response : fastapi.Response
|
||||
Response object used to set the HTTP status code.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Item
|
||||
The newly created item.
|
||||
"""
|
||||
item = _create_item(payload)
|
||||
response.status_code = 201
|
||||
return item
|
||||
|
||||
|
||||
def update_item(item_id: int, payload: ItemCreate):
|
||||
"""
|
||||
Update an existing item.
|
||||
|
||||
Implements the OpenAPI operation identified by
|
||||
``operationId: update_item``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to update.
|
||||
payload : ItemCreate
|
||||
New item data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Item
|
||||
The updated item.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the item does not exist.
|
||||
"""
|
||||
try:
|
||||
return _update_item(item_id, payload)
|
||||
except KeyError:
|
||||
@@ -39,6 +124,29 @@ def update_item(item_id: int, payload: ItemCreate):
|
||||
|
||||
|
||||
def delete_item(item_id: int, response: Response):
|
||||
"""
|
||||
Delete an existing item.
|
||||
|
||||
Implements the OpenAPI operation identified by
|
||||
``operationId: delete_item``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to delete.
|
||||
response : fastapi.Response
|
||||
Response object used to set the HTTP status code.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
No content.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the item does not exist.
|
||||
"""
|
||||
try:
|
||||
_delete_item(item_id)
|
||||
except KeyError:
|
||||
|
||||
@@ -3,6 +3,13 @@ End-to-end tests for the OpenAPI-first model CRUD example app.
|
||||
|
||||
These tests validate that all CRUD operations behave correctly
|
||||
against the in-memory mock data store using Pydantic models.
|
||||
- OpenAPI specification loading
|
||||
- OperationId-driven route binding on the server
|
||||
- OperationId-driven client invocation
|
||||
- Pydantic model-based request and response handling
|
||||
|
||||
All CRUD operations are exercised against an in-memory mock data store
|
||||
backed by Pydantic domain models.
|
||||
|
||||
The tests assume:
|
||||
- OpenAPI-first route binding
|
||||
@@ -62,7 +69,7 @@ def test_create_item():
|
||||
}
|
||||
|
||||
response = client.create_item(
|
||||
body=payload
|
||||
body=payload,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
@@ -107,7 +114,7 @@ def test_update_item():
|
||||
def test_delete_item():
|
||||
"""Deleting an item should remove it from the store."""
|
||||
response = client.delete_item(
|
||||
path_params={"item_id": 2}
|
||||
path_params={"item_id": 2},
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
97
openapi_first/templates/vet_app/__init__.py
Normal file
97
openapi_first/templates/vet_app/__init__.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
OpenAPI-first Veterinary Clinic application template.
|
||||
|
||||
This package contains a complete, runnable example of an OpenAPI-first
|
||||
veterinary clinic management service. It demonstrates all ``x-`` extension
|
||||
fields consumed by the ``react-openapi`` admin panel renderer.
|
||||
|
||||
The application manages five resources:
|
||||
|
||||
- **Parents** — pet owners with contact details
|
||||
- **Vets** — veterinarians with specializations
|
||||
- **Treatments** — medical procedure catalog
|
||||
- **Pets** — animals with species, age, weight, and photos
|
||||
- **Appointments** — scheduled visits linking pets, vets, and treatments
|
||||
|
||||
All HTTP routes, methods, schemas, and operation bindings are defined
|
||||
in the OpenAPI specification (``openapi.yaml``). Every operation has an
|
||||
explicit ``operationId`` that maps to a Python handler in ``routes.py``.
|
||||
|
||||
This file is a copyable template. It is not part of the ``openapi_first``
|
||||
library API surface.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
OpenAPI x- extension fields demonstrated
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Schema-level extensions (mark a schema as a UI resource):
|
||||
|
||||
``x-resource`` (REQUIRED) Maps schema to URL path segment
|
||||
``x-primary-key`` (REQUIRED) Primary key property name
|
||||
``x-display-format`` (REQUIRED) Human-readable label template
|
||||
``x-list-columns`` (REQUIRED) Columns for the datatable
|
||||
|
||||
Property-level extensions (control UI rendering):
|
||||
|
||||
``x-label`` (REQUIRED) Human-readable field label
|
||||
``x-order`` (REQUIRED) Field ordering in forms/detail
|
||||
``x-description`` (optional) Helper text below form fields
|
||||
``x-hidden`` (optional) Visibility in form / list / detail
|
||||
``x-filterable`` (optional) Allows column filtering
|
||||
``x-sortable`` (optional) Allows column sorting
|
||||
``x-fk`` (optional) Foreign key — renders as dropdown
|
||||
``x-fk.resource`` (REQUIRED for FK) Target resource name
|
||||
``x-fk.prefetch`` (optional) Preload all FK options on mount
|
||||
``x-ui-type`` (optional) Custom UI type (e.g. image upload)
|
||||
``x-upload-url`` (optional) Upload endpoint for binary fields
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Scaffolding via CLI
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Create a new vet clinic service using the bundled template:
|
||||
|
||||
openapi-first vet_app
|
||||
|
||||
Create the service in a custom directory:
|
||||
|
||||
openapi-first vet_app my-vet-clinic
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Client Usage Example
|
||||
----------------------------------------------------------------------
|
||||
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
spec = load_openapi("openapi.yaml")
|
||||
client = OpenAPIClient(spec)
|
||||
|
||||
# List pets with pagination
|
||||
response = client.list_pets(query_params={"limit": 10, "offset": 0})
|
||||
|
||||
# Create a pet with FK references
|
||||
response = client.create_pet(
|
||||
body={"name": "Fido", "species": "dog", "parents": [1, 2]}
|
||||
)
|
||||
|
||||
# Upload a pet photo
|
||||
response = client.upload_pet_photo(
|
||||
path_params={"id": 1},
|
||||
body={"file": open("photo.jpg", "rb")},
|
||||
)
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Non-Goals
|
||||
----------------------------------------------------------------------
|
||||
|
||||
This template is intentionally minimal and is NOT:
|
||||
- production-ready
|
||||
- persistent or concurrency-safe
|
||||
- a reference architecture for data storage
|
||||
|
||||
It exists solely as a copyable example for learning, testing, and
|
||||
bootstrapping OpenAPI-first services.
|
||||
|
||||
This package is not part of the ``openapi_first`` library API surface.
|
||||
"""
|
||||
345
openapi_first/templates/vet_app/data.py
Normal file
345
openapi_first/templates/vet_app/data.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
In-memory data store for the Veterinary Clinic example.
|
||||
|
||||
This module is NOT thread-safe and is intended for demos and scaffolds only.
|
||||
|
||||
It provides minimal, process-local data stores for the five veterinary
|
||||
clinic entities. Each store exposes standard CRUD operations backed by
|
||||
a simple dictionary.
|
||||
|
||||
This module intentionally avoids:
|
||||
- persistence
|
||||
- concurrency guarantees
|
||||
- transactional semantics
|
||||
- validation beyond what Pydantic provides
|
||||
|
||||
This module is not part of the ``openapi_first`` library API surface.
|
||||
"""
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from models import (
|
||||
Parent, ParentCreate,
|
||||
Vet, VetCreate,
|
||||
Treatment, TreatmentCreate,
|
||||
Pet, PetCreate,
|
||||
Appointment, AppointmentCreate,
|
||||
)
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_parents: dict[int, Parent] = {}
|
||||
_parents_next_id = 1
|
||||
|
||||
|
||||
def list_parents() -> list[Parent]:
|
||||
return list(_parents.values())
|
||||
|
||||
|
||||
def get_parent(parent_id: int) -> Parent:
|
||||
return _parents[parent_id]
|
||||
|
||||
|
||||
def create_parent(payload: ParentCreate) -> Parent:
|
||||
global _parents_next_id
|
||||
now = _now()
|
||||
parent = Parent(
|
||||
id=_parents_next_id,
|
||||
name=payload.name,
|
||||
email=payload.email,
|
||||
phone=payload.phone,
|
||||
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
|
||||
)
|
||||
_parents[_parents_next_id] = parent
|
||||
_parents_next_id += 1
|
||||
return parent
|
||||
|
||||
|
||||
def update_parent(parent_id: int, payload: ParentCreate) -> Parent:
|
||||
if parent_id not in _parents:
|
||||
raise KeyError(parent_id)
|
||||
now = _now()
|
||||
current = _parents[parent_id]
|
||||
updated = Parent(
|
||||
id=parent_id,
|
||||
name=payload.name,
|
||||
email=payload.email,
|
||||
phone=payload.phone if payload.phone is not None else current.phone,
|
||||
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
|
||||
)
|
||||
_parents[parent_id] = updated
|
||||
return updated
|
||||
|
||||
|
||||
def delete_parent(parent_id: int) -> None:
|
||||
del _parents[parent_id]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_vets: dict[int, Vet] = {}
|
||||
_vets_next_id = 1
|
||||
|
||||
|
||||
def list_vets() -> list[Vet]:
|
||||
return list(_vets.values())
|
||||
|
||||
|
||||
def get_vet(vet_id: int) -> Vet:
|
||||
return _vets[vet_id]
|
||||
|
||||
|
||||
def create_vet(payload: VetCreate) -> Vet:
|
||||
global _vets_next_id
|
||||
now = _now()
|
||||
vet = Vet(
|
||||
id=_vets_next_id,
|
||||
name=payload.name,
|
||||
specialty=payload.specialty,
|
||||
email=payload.email,
|
||||
phone=payload.phone,
|
||||
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
|
||||
)
|
||||
_vets[_vets_next_id] = vet
|
||||
_vets_next_id += 1
|
||||
return vet
|
||||
|
||||
|
||||
def update_vet(vet_id: int, payload: VetCreate) -> Vet:
|
||||
if vet_id not in _vets:
|
||||
raise KeyError(vet_id)
|
||||
now = _now()
|
||||
current = _vets[vet_id]
|
||||
updated = Vet(
|
||||
id=vet_id,
|
||||
name=payload.name,
|
||||
specialty=payload.specialty if payload.specialty is not None else current.specialty,
|
||||
email=payload.email,
|
||||
phone=payload.phone if payload.phone is not None else current.phone,
|
||||
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
|
||||
)
|
||||
_vets[vet_id] = updated
|
||||
return updated
|
||||
|
||||
|
||||
def delete_vet(vet_id: int) -> None:
|
||||
del _vets[vet_id]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Treatments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_treatments: dict[int, Treatment] = {}
|
||||
_treatments_next_id = 1
|
||||
|
||||
|
||||
def list_treatments() -> list[Treatment]:
|
||||
return list(_treatments.values())
|
||||
|
||||
|
||||
def get_treatment(treatment_id: int) -> Treatment:
|
||||
return _treatments[treatment_id]
|
||||
|
||||
|
||||
def create_treatment(payload: TreatmentCreate) -> Treatment:
|
||||
global _treatments_next_id
|
||||
now = _now()
|
||||
treatment = Treatment(
|
||||
id=_treatments_next_id,
|
||||
label=payload.label,
|
||||
description=payload.description,
|
||||
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
|
||||
)
|
||||
_treatments[_treatments_next_id] = treatment
|
||||
_treatments_next_id += 1
|
||||
return treatment
|
||||
|
||||
|
||||
def update_treatment(treatment_id: int, payload: TreatmentCreate) -> Treatment:
|
||||
if treatment_id not in _treatments:
|
||||
raise KeyError(treatment_id)
|
||||
now = _now()
|
||||
current = _treatments[treatment_id]
|
||||
updated = Treatment(
|
||||
id=treatment_id,
|
||||
label=payload.label,
|
||||
description=payload.description if payload.description is not None else current.description,
|
||||
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
|
||||
)
|
||||
_treatments[treatment_id] = updated
|
||||
return updated
|
||||
|
||||
|
||||
def delete_treatment(treatment_id: int) -> None:
|
||||
del _treatments[treatment_id]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_pets: dict[int, Pet] = {}
|
||||
_pets_next_id = 1
|
||||
|
||||
|
||||
def list_pets() -> list[Pet]:
|
||||
return list(_pets.values())
|
||||
|
||||
|
||||
def get_pet(pet_id: int) -> Pet:
|
||||
return _pets[pet_id]
|
||||
|
||||
|
||||
def create_pet(payload: PetCreate) -> Pet:
|
||||
global _pets_next_id
|
||||
now = _now()
|
||||
parents = [_parents[pid] for pid in payload.parent_ids]
|
||||
pet = Pet(
|
||||
id=_pets_next_id,
|
||||
name=payload.name,
|
||||
species=payload.species,
|
||||
age=payload.age,
|
||||
weight=payload.weight,
|
||||
birthDate=payload.birthDate,
|
||||
photo=payload.photo,
|
||||
parents=parents,
|
||||
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
|
||||
)
|
||||
_pets[_pets_next_id] = pet
|
||||
_pets_next_id += 1
|
||||
return pet
|
||||
|
||||
|
||||
def update_pet(pet_id: int, payload: PetCreate) -> Pet:
|
||||
if pet_id not in _pets:
|
||||
raise KeyError(pet_id)
|
||||
now = _now()
|
||||
parents = [_parents[pid] for pid in payload.parent_ids]
|
||||
current = _pets[pet_id]
|
||||
updated = Pet(
|
||||
id=pet_id,
|
||||
name=payload.name,
|
||||
species=payload.species,
|
||||
age=payload.age if payload.age is not None else current.age,
|
||||
weight=payload.weight if payload.weight is not None else current.weight,
|
||||
birthDate=payload.birthDate if payload.birthDate is not None else current.birthDate,
|
||||
photo=payload.photo if payload.photo is not None else current.photo,
|
||||
parents=parents,
|
||||
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
|
||||
)
|
||||
_pets[pet_id] = updated
|
||||
return updated
|
||||
|
||||
|
||||
def delete_pet(pet_id: int) -> None:
|
||||
del _pets[pet_id]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Appointments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_appointments: dict[int, Appointment] = {}
|
||||
_appointments_next_id = 1
|
||||
|
||||
|
||||
def list_appointments() -> list[Appointment]:
|
||||
return list(_appointments.values())
|
||||
|
||||
|
||||
def get_appointment(appointment_id: int) -> Appointment:
|
||||
return _appointments[appointment_id]
|
||||
|
||||
|
||||
def create_appointment(payload: AppointmentCreate) -> Appointment:
|
||||
global _appointments_next_id
|
||||
now = _now()
|
||||
appointment = Appointment(
|
||||
id=_appointments_next_id,
|
||||
date=payload.date,
|
||||
notes=payload.notes,
|
||||
pet=_pets[payload.pet_id],
|
||||
vet=_vets[payload.vet_id],
|
||||
treatment=_treatments[payload.treatment_id],
|
||||
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
|
||||
)
|
||||
_appointments[_appointments_next_id] = appointment
|
||||
_appointments_next_id += 1
|
||||
return appointment
|
||||
|
||||
|
||||
def update_appointment(appointment_id: int, payload: AppointmentCreate) -> Appointment:
|
||||
if appointment_id not in _appointments:
|
||||
raise KeyError(appointment_id)
|
||||
now = _now()
|
||||
current = _appointments[appointment_id]
|
||||
updated = Appointment(
|
||||
id=appointment_id,
|
||||
date=payload.date,
|
||||
notes=payload.notes if payload.notes is not None else current.notes,
|
||||
pet=_pets.get(payload.pet_id, current.pet),
|
||||
vet=_vets.get(payload.vet_id, current.vet),
|
||||
treatment=_treatments.get(payload.treatment_id, current.treatment),
|
||||
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
|
||||
)
|
||||
_appointments[appointment_id] = updated
|
||||
return updated
|
||||
|
||||
|
||||
def delete_appointment(appointment_id: int) -> None:
|
||||
del _appointments[appointment_id]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Seed data — populate stores so the UI isn't empty on startup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _seed_data():
|
||||
now = _now()
|
||||
meta = {"createdOn": now, "updatedOn": now}
|
||||
global _parents_next_id, _vets_next_id, _treatments_next_id
|
||||
global _pets_next_id, _appointments_next_id
|
||||
|
||||
_parents[1] = Parent(id=1, name="Alice Johnson", email="alice@example.com", phone="555-0101", metadata=meta)
|
||||
_parents[2] = Parent(id=2, name="Bob Smith", email="bob@example.com", phone="555-0102", metadata=meta)
|
||||
_parents[3] = Parent(id=3, name="Carol Williams", email="carol@example.com", phone="555-0103", metadata=meta)
|
||||
_parents[4] = Parent(id=4, name="Dave Brown", email="dave@example.com", phone="555-0104", metadata=meta)
|
||||
_parents_next_id = 5
|
||||
|
||||
_vets[1] = Vet(id=1, name="Sarah Connor", specialty="Surgery", email="sarah@clinic.com", phone="555-0201", metadata=meta)
|
||||
_vets[2] = Vet(id=2, name="James Wilson", specialty="Dentistry", email="james@clinic.com", phone="555-0202", metadata=meta)
|
||||
_vets[3] = Vet(id=3, name="Emily Davis", specialty="General Practice", email="emily@clinic.com", phone="555-0203", metadata=meta)
|
||||
_vets_next_id = 4
|
||||
|
||||
_treatments[1] = Treatment(id=1, label="Annual Checkup", description="Full physical examination", metadata=meta)
|
||||
_treatments[2] = Treatment(id=2, label="Vaccination", description="Core vaccines for common diseases", metadata=meta)
|
||||
_treatments[3] = Treatment(id=3, label="Dental Cleaning", description="Scaling, polishing, and oral exam", metadata=meta)
|
||||
_treatments[4] = Treatment(id=4, label="Spay/Neuter", description="Surgical sterilization", metadata=meta)
|
||||
_treatments[5] = Treatment(id=5, label="Blood Panel", description="Complete blood count and chemistry", metadata=meta)
|
||||
_treatments_next_id = 6
|
||||
|
||||
_pets[1] = Pet(id=1, name="Max", species="dog", age=4, weight=25.5, birthDate=date(2022, 3, 15), parents=[_parents[1]], metadata=meta)
|
||||
_pets[2] = Pet(id=2, name="Luna", species="cat", age=2, weight=4.2, birthDate=date(2024, 1, 10), parents=[_parents[1], _parents[2]], metadata=meta)
|
||||
_pets[3] = Pet(id=3, name="Charlie", species="dog", age=7, weight=18.0, birthDate=date(2019, 8, 22), parents=[_parents[2]], metadata=meta)
|
||||
_pets[4] = Pet(id=4, name="Bella", species="bird", age=1, weight=0.3, birthDate=date(2025, 5, 1), parents=[_parents[3]], metadata=meta)
|
||||
_pets[5] = Pet(id=5, name="Rocky", species="dog", age=3, weight=30.0, birthDate=date(2023, 11, 5), parents=[_parents[4]], metadata=meta)
|
||||
_pets_next_id = 6
|
||||
|
||||
_appointments[1] = Appointment(id=1, date=datetime(2026, 6, 18, 9, 0, tzinfo=timezone.utc), notes="Annual checkup", pet=_pets[1], vet=_vets[1], treatment=_treatments[1], metadata=meta)
|
||||
_appointments[2] = Appointment(id=2, date=datetime(2026, 6, 18, 10, 30, tzinfo=timezone.utc), notes="Dental cleaning", pet=_pets[2], vet=_vets[2], treatment=_treatments[3], metadata=meta)
|
||||
_appointments[3] = Appointment(id=3, date=datetime(2026, 6, 19, 11, 0, tzinfo=timezone.utc), notes="Vaccination booster", pet=_pets[3], vet=_vets[3], treatment=_treatments[2], metadata=meta)
|
||||
_appointments[4] = Appointment(id=4, date=datetime(2026, 6, 20, 14, 0, tzinfo=timezone.utc), notes="Follow-up after surgery", pet=_pets[5], vet=_vets[1], treatment=_treatments[4], metadata=meta)
|
||||
_appointments_next_id = 5
|
||||
|
||||
|
||||
_seed_data()
|
||||
44
openapi_first/templates/vet_app/main.py
Normal file
44
openapi_first/templates/vet_app/main.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Application entry point for an OpenAPI-first Veterinary Clinic 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, request/response schemas, 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, persistence concerns, 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 starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from openapi_first.app import OpenAPIFirstApp
|
||||
import routes
|
||||
|
||||
app = OpenAPIFirstApp(
|
||||
openapi_path="openapi.yaml",
|
||||
routes_module=routes,
|
||||
title="Veterinary Clinic Service",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
90
openapi_first/templates/vet_app/models.py
Normal file
90
openapi_first/templates/vet_app/models.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from datetime import date, datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Metadata(BaseModel):
|
||||
createdOn: datetime | None = None
|
||||
updatedOn: datetime | None = None
|
||||
|
||||
|
||||
class ParentBase(BaseModel):
|
||||
name: str
|
||||
email: str
|
||||
phone: str | None = None
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
class ParentCreate(ParentBase):
|
||||
pass
|
||||
|
||||
|
||||
class Parent(ParentBase):
|
||||
id: int
|
||||
|
||||
|
||||
class VetBase(BaseModel):
|
||||
name: str
|
||||
specialty: str | None = None
|
||||
email: str
|
||||
phone: str | None = None
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
class VetCreate(VetBase):
|
||||
pass
|
||||
|
||||
|
||||
class Vet(VetBase):
|
||||
id: int
|
||||
|
||||
|
||||
class TreatmentBase(BaseModel):
|
||||
label: str
|
||||
description: str | None = None
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
class TreatmentCreate(TreatmentBase):
|
||||
pass
|
||||
|
||||
|
||||
class Treatment(TreatmentBase):
|
||||
id: int
|
||||
|
||||
|
||||
class PetBase(BaseModel):
|
||||
name: str
|
||||
species: str
|
||||
age: int | None = None
|
||||
weight: float | None = None
|
||||
birthDate: date | None = None
|
||||
photo: str | None = None
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
class PetCreate(PetBase):
|
||||
parent_ids: list[int] = []
|
||||
|
||||
|
||||
class Pet(PetBase):
|
||||
id: int
|
||||
parents: list[Parent] = []
|
||||
|
||||
|
||||
class AppointmentBase(BaseModel):
|
||||
date: datetime
|
||||
notes: str | None = None
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
class AppointmentCreate(AppointmentBase):
|
||||
pet_id: int
|
||||
vet_id: int
|
||||
treatment_id: int
|
||||
|
||||
|
||||
class Appointment(AppointmentBase):
|
||||
id: int
|
||||
pet: Pet
|
||||
vet: Vet
|
||||
treatment: Treatment
|
||||
1019
openapi_first/templates/vet_app/openapi.yaml
Normal file
1019
openapi_first/templates/vet_app/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
365
openapi_first/templates/vet_app/routes.py
Normal file
365
openapi_first/templates/vet_app/routes.py
Normal file
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
Veterinary Clinic route handlers bound via OpenAPI operationId.
|
||||
|
||||
Handlers explicitly control HTTP response status codes to ensure runtime
|
||||
behavior matches the OpenAPI contract. Domain models defined using
|
||||
Pydantic are used for request and response payloads.
|
||||
|
||||
No routing decorators, path definitions, or implicit framework behavior
|
||||
appear in this module. All routing, HTTP methods, and schemas are defined
|
||||
in the OpenAPI specification.
|
||||
"""
|
||||
|
||||
from fastapi import Response, HTTPException, UploadFile
|
||||
|
||||
from models import (
|
||||
ParentCreate,
|
||||
VetCreate,
|
||||
TreatmentCreate,
|
||||
PetCreate,
|
||||
AppointmentCreate,
|
||||
)
|
||||
from data import (
|
||||
list_parents as _list_parents,
|
||||
get_parent as _get_parent,
|
||||
create_parent as _create_parent,
|
||||
update_parent as _update_parent,
|
||||
delete_parent as _delete_parent,
|
||||
list_vets as _list_vets,
|
||||
get_vet as _get_vet,
|
||||
create_vet as _create_vet,
|
||||
update_vet as _update_vet,
|
||||
delete_vet as _delete_vet,
|
||||
list_treatments as _list_treatments,
|
||||
get_treatment as _get_treatment,
|
||||
create_treatment as _create_treatment,
|
||||
update_treatment as _update_treatment,
|
||||
delete_treatment as _delete_treatment,
|
||||
list_pets as _list_pets,
|
||||
get_pet as _get_pet,
|
||||
create_pet as _create_pet,
|
||||
update_pet as _update_pet,
|
||||
delete_pet as _delete_pet,
|
||||
list_appointments as _list_appointments,
|
||||
get_appointment as _get_appointment,
|
||||
create_appointment as _create_appointment,
|
||||
update_appointment as _update_appointment,
|
||||
delete_appointment as _delete_appointment,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_parents(limit: int = 20, offset: int = 0):
|
||||
"""List parents (paginated).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
limit : int
|
||||
Maximum number of records to return.
|
||||
offset : int
|
||||
Number of records to skip.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Paginated response with ``total`` and ``items``.
|
||||
"""
|
||||
items = _list_parents()
|
||||
return {"total": len(items), "items": items[offset:offset + limit] if limit else items[offset:]}
|
||||
|
||||
|
||||
def create_parent(payload: ParentCreate, response: Response):
|
||||
"""Create a parent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
payload : ParentCreate
|
||||
Parent data excluding the ``id`` field.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Parent
|
||||
The newly created parent.
|
||||
"""
|
||||
parent = _create_parent(payload)
|
||||
response.status_code = 201
|
||||
return parent
|
||||
|
||||
|
||||
def get_parent(id: int):
|
||||
"""Retrieve a single parent by ID.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int
|
||||
Identifier of the parent.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Parent
|
||||
The requested parent.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the parent does not exist.
|
||||
"""
|
||||
try:
|
||||
return _get_parent(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Parent not found")
|
||||
|
||||
|
||||
def update_parent(id: int, payload: ParentCreate):
|
||||
"""Update an existing parent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int
|
||||
Identifier of the parent.
|
||||
payload : ParentCreate
|
||||
Updated parent data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Parent
|
||||
The updated parent.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the parent does not exist.
|
||||
"""
|
||||
try:
|
||||
return _update_parent(id, payload)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Parent not found")
|
||||
|
||||
|
||||
def delete_parent(id: int, response: Response):
|
||||
"""Delete an existing parent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int
|
||||
Identifier of the parent.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the parent does not exist.
|
||||
"""
|
||||
try:
|
||||
_delete_parent(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Parent not found")
|
||||
response.status_code = 204
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_vets(limit: int = 20, offset: int = 0):
|
||||
"""List vets (paginated)."""
|
||||
items = _list_vets()
|
||||
return {"total": len(items), "items": items[offset:offset + limit] if limit else items[offset:]}
|
||||
|
||||
|
||||
def create_vet(payload: VetCreate, response: Response):
|
||||
"""Create a vet."""
|
||||
vet = _create_vet(payload)
|
||||
response.status_code = 201
|
||||
return vet
|
||||
|
||||
|
||||
def get_vet(id: int):
|
||||
"""Retrieve a single vet by ID."""
|
||||
try:
|
||||
return _get_vet(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Vet not found")
|
||||
|
||||
|
||||
def update_vet(id: int, payload: VetCreate):
|
||||
"""Update an existing vet."""
|
||||
try:
|
||||
return _update_vet(id, payload)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Vet not found")
|
||||
|
||||
|
||||
def delete_vet(id: int, response: Response):
|
||||
"""Delete an existing vet."""
|
||||
try:
|
||||
_delete_vet(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Vet not found")
|
||||
response.status_code = 204
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Treatments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_treatments():
|
||||
"""List treatments (catalogue).
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[Treatment]
|
||||
A list of treatment domain objects.
|
||||
"""
|
||||
return _list_treatments()
|
||||
|
||||
|
||||
def create_treatment(payload: TreatmentCreate, response: Response):
|
||||
"""Add a treatment (admin only)."""
|
||||
treatment = _create_treatment(payload)
|
||||
response.status_code = 201
|
||||
return treatment
|
||||
|
||||
|
||||
def get_treatment(id: int):
|
||||
"""Retrieve a single treatment by ID."""
|
||||
try:
|
||||
return _get_treatment(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Treatment not found")
|
||||
|
||||
|
||||
def update_treatment(id: int, payload: TreatmentCreate):
|
||||
"""Update an existing treatment."""
|
||||
try:
|
||||
return _update_treatment(id, payload)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Treatment not found")
|
||||
|
||||
|
||||
def delete_treatment(id: int, response: Response):
|
||||
"""Delete an existing treatment."""
|
||||
try:
|
||||
_delete_treatment(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Treatment not found")
|
||||
response.status_code = 204
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_pets(limit: int = 20, offset: int = 0):
|
||||
"""List pets (paginated)."""
|
||||
items = _list_pets()
|
||||
return {"total": len(items), "items": items[offset:offset + limit] if limit else items[offset:]}
|
||||
|
||||
|
||||
def create_pet(payload: PetCreate, response: Response):
|
||||
"""Create a pet."""
|
||||
pet = _create_pet(payload)
|
||||
response.status_code = 201
|
||||
return pet
|
||||
|
||||
|
||||
def get_pet(id: int):
|
||||
"""Retrieve a single pet by ID."""
|
||||
try:
|
||||
return _get_pet(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Pet not found")
|
||||
|
||||
|
||||
def update_pet(id: int, payload: PetCreate):
|
||||
"""Update an existing pet."""
|
||||
try:
|
||||
return _update_pet(id, payload)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Pet not found")
|
||||
|
||||
|
||||
def delete_pet(id: int, response: Response):
|
||||
"""Delete an existing pet."""
|
||||
try:
|
||||
_delete_pet(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Pet not found")
|
||||
response.status_code = 204
|
||||
|
||||
|
||||
def upload_pet_photo(id: int, file: UploadFile):
|
||||
"""Upload a pet photo.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int
|
||||
Identifier of the pet.
|
||||
file : UploadFile
|
||||
Image file to upload.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
A confirmation with the pet ID.
|
||||
"""
|
||||
_ = file # In a real app, save to disk / object store
|
||||
return {"id": id, "status": "photo_uploaded"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Appointments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_appointments(limit: int = 20, offset: int = 0, date: str = None, vet: int = None, pet: int = None):
|
||||
"""List appointments (paginated, filterable)."""
|
||||
items = _list_appointments()
|
||||
|
||||
# Basic in-memory filtering
|
||||
if date:
|
||||
items = [a for a in items if a.date.startswith(date)]
|
||||
if vet is not None:
|
||||
items = [a for a in items if a.vet.id == vet]
|
||||
if pet is not None:
|
||||
items = [a for a in items if a.pet.id == pet]
|
||||
|
||||
return {"total": len(items), "items": items[offset:offset + limit] if limit else items[offset:]}
|
||||
|
||||
|
||||
def create_appointment(payload: AppointmentCreate, response: Response):
|
||||
"""Create an appointment."""
|
||||
appointment = _create_appointment(payload)
|
||||
response.status_code = 201
|
||||
return appointment
|
||||
|
||||
|
||||
def get_appointment(id: int):
|
||||
"""Retrieve a single appointment by ID."""
|
||||
try:
|
||||
return _get_appointment(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Appointment not found")
|
||||
|
||||
|
||||
def update_appointment(id: int, payload: AppointmentCreate):
|
||||
"""Update an existing appointment."""
|
||||
try:
|
||||
return _update_appointment(id, payload)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Appointment not found")
|
||||
|
||||
|
||||
def delete_appointment(id: int, response: Response):
|
||||
"""Delete an existing appointment."""
|
||||
try:
|
||||
_delete_appointment(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Appointment not found")
|
||||
response.status_code = 204
|
||||
151
openapi_first/templates/vet_app/test_vet_app.py
Normal file
151
openapi_first/templates/vet_app/test_vet_app.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
End-to-end tests for the OpenAPI-first Veterinary Clinic example app.
|
||||
|
||||
These tests validate that all CRUD operations behave correctly
|
||||
against the in-memory mock data store using Pydantic models.
|
||||
"""
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from main import app
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
|
||||
test_client = TestClient(app)
|
||||
spec = load_openapi("openapi.yaml")
|
||||
client = OpenAPIClient(
|
||||
spec=spec,
|
||||
base_url="http://testserver",
|
||||
client=test_client,
|
||||
)
|
||||
|
||||
|
||||
def test_list_parents():
|
||||
"""List parents returns paginated response."""
|
||||
response = client.list_parents(query={"limit": 10, "offset": 0})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total" in data
|
||||
assert "items" in data
|
||||
|
||||
|
||||
def test_create_parent():
|
||||
"""Creating a parent returns 201 with the created entity."""
|
||||
payload = {"name": "Alice", "email": "alice@example.com"}
|
||||
response = client.create_parent(body=payload)
|
||||
assert response.status_code == 201
|
||||
parent = response.json()
|
||||
assert parent["name"] == "Alice"
|
||||
assert "id" in parent
|
||||
|
||||
|
||||
def test_get_parent():
|
||||
"""Get parent by ID returns the entity."""
|
||||
parent = client.create_parent(body={"name": "Bob", "email": "bob@example.com"}).json()
|
||||
response = client.get_parent(path_params={"id": parent["id"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Bob"
|
||||
|
||||
|
||||
def test_update_parent():
|
||||
"""Update parent replaces its values."""
|
||||
parent = client.create_parent(body={"name": "Carol", "email": "carol@example.com"}).json()
|
||||
payload = {"name": "Carol Smith", "email": "carol.smith@example.com"}
|
||||
response = client.update_parent(path_params={"id": parent["id"]}, body=payload)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Carol Smith"
|
||||
|
||||
|
||||
def test_delete_parent():
|
||||
"""Delete parent returns 204 and removes the entity."""
|
||||
parent = client.create_parent(body={"name": "Dave", "email": "dave@example.com"}).json()
|
||||
response = client.delete_parent(path_params={"id": parent["id"]})
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_list_vets():
|
||||
"""List vets returns paginated response."""
|
||||
response = client.list_vets(query={"limit": 10, "offset": 0})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total" in data
|
||||
assert "items" in data
|
||||
|
||||
|
||||
def test_create_vet():
|
||||
"""Creating a vet returns 201."""
|
||||
payload = {"name": "Dr. Smith", "specialty": "Surgery", "email": "smith@clinic.com"}
|
||||
response = client.create_vet(body=payload)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == "Dr. Smith"
|
||||
|
||||
|
||||
def test_list_treatments():
|
||||
"""List treatments returns an array."""
|
||||
response = client.list_treatments()
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
|
||||
def test_create_treatment():
|
||||
"""Creating a treatment returns 201."""
|
||||
payload = {"label": "Vaccination", "description": "Annual vaccination"}
|
||||
response = client.create_treatment(body=payload)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["label"] == "Vaccination"
|
||||
|
||||
|
||||
def test_create_pet():
|
||||
"""Creating a pet links FK references."""
|
||||
parent = client.create_parent(body={"name": "Owner", "email": "owner@example.com"}).json()
|
||||
payload = {"name": "Fido", "species": "dog", "parent_ids": [parent["id"]]}
|
||||
response = client.create_pet(body=payload)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == "Fido"
|
||||
|
||||
|
||||
def test_upload_pet_photo():
|
||||
"""Upload pet photo returns 200."""
|
||||
pet = client.create_pet(body={"name": "PhotoPet", "species": "cat", "parent_ids": []}).json()
|
||||
response = client.client.post(
|
||||
f"http://testserver/pets/{pet['id']}",
|
||||
files={"file": ("test.jpg", b"fake-image-data", "image/jpeg")},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_list_appointments():
|
||||
"""List appointments returns paginated response with filter params."""
|
||||
response = client.list_appointments(query={"limit": 10, "offset": 0})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total" in data
|
||||
assert "items" in data
|
||||
|
||||
|
||||
def test_full_appointment_lifecycle():
|
||||
"""Create a parent, vet, treatment, pet, then an appointment."""
|
||||
parent = client.create_parent(body={"name": "Eve", "email": "eve@example.com"}).json()
|
||||
vet = client.create_vet(body={"name": "Dr. Jones", "specialty": "Dentistry", "email": "jones@clinic.com"}).json()
|
||||
treatment = client.create_treatment(body={"label": "Cleaning", "description": "Teeth cleaning"}).json()
|
||||
pet = client.create_pet(body={"name": "Max", "species": "dog", "parent_ids": [parent["id"]]}).json()
|
||||
|
||||
payload = {
|
||||
"date": "2025-06-01T10:00:00",
|
||||
"pet_id": pet["id"],
|
||||
"vet_id": vet["id"],
|
||||
"treatment_id": treatment["id"],
|
||||
}
|
||||
response = client.create_appointment(body=payload)
|
||||
assert response.status_code == 201
|
||||
appointment = response.json()
|
||||
assert appointment["pet"]["id"] == pet["id"]
|
||||
|
||||
# Fetch it back
|
||||
get_resp = client.get_appointment(path_params={"id": appointment["id"]})
|
||||
assert get_resp.status_code == 200
|
||||
|
||||
# Delete it
|
||||
del_resp = client.delete_appointment(path_params={"id": appointment["id"]})
|
||||
assert del_resp.status_code == 204
|
||||
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "openapi-first"
|
||||
version = "0.0.3"
|
||||
version = "0.0.4"
|
||||
description = "Strict OpenAPI-first application bootstrap for FastAPI."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -52,6 +52,9 @@ dependencies = [
|
||||
|
||||
# YAML support (optional but recommended)
|
||||
"pyyaml>=6.0.1",
|
||||
|
||||
# Code generation
|
||||
"datamodel-code-generator>=0.25.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -84,7 +87,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]
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
fastapi==0.128.0,
|
||||
openapi-spec-validator==0.7.2
|
||||
pyyaml==6.0.3
|
||||
uvicorn==0.40.0
|
||||
pydantic==2.12.5
|
||||
httpx==0.28.1
|
||||
|
||||
# Test Packages
|
||||
pytest==7.4.0
|
||||
pytest-asyncio==0.21.0
|
||||
pytest-cov==4.1.0
|
||||
|
||||
# Doc Packages
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.6.23
|
||||
neoteroi-mkdocs==1.1.3
|
||||
pymdown-extensions==10.16.1
|
||||
mkdocstrings==1.0.0
|
||||
mkdocstrings-python==2.0.1
|
||||
Reference in New Issue
Block a user