init openapi_first

This commit is contained in:
2026-01-10 17:21:21 +05:30
parent 76e2599903
commit b88ecbd7c5
5 changed files with 542 additions and 0 deletions

141
openapi_first/__init__.py Normal file
View File

@@ -0,0 +1,141 @@
"""
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.
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 three 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
- 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
----------------------------------------------------------------------
Install using pip:
pip install fastapi-openapi-first
Or with Poetry:
poetry add fastapi-openapi-first
Runtime dependencies are intentionally minimal:
- fastapi
- 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.
----------------------------------------------------------------------
Basic Usage
----------------------------------------------------------------------
Minimal OpenAPI-first FastAPI application:
from fastapi_openapi_first import app
import my_service.routes as routes
api = app.OpenAPIFirstApp(
openapi_path="openapi.json",
routes_module=routes,
title="My Service",
version="1.0.0",
)
# Run with:
# uvicorn my_service.main:api
Handler definitions (no decorators):
def get_health():
return {"status": "ok"}
OpenAPI snippet:
paths:
/health:
get:
operationId: get_health
responses:
"200":
description: OK
----------------------------------------------------------------------
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
----------------------------------------------------------------------
The supported public API consists of the following top-level modules:
- fastapi_openapi_first.app
- fastapi_openapi_first.binder
- fastapi_openapi_first.loader
- fastapi_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
----------------------------------------------------------------------
- OpenAPI is the single source of truth
- No undocumented routes can exist
- No OpenAPI operation can exist without a handler
- All contract violations fail at application startup
- No hidden FastAPI magic or implicit behavior
- Deterministic, testable application assembly
- CI-friendly failure modes
FastAPI OpenAPI First favors correctness, explicitness, and contract
enforcement over convenience shortcuts.
"""
from . import app
from . import binder
from . import loader
from . import errors
__all__ = [
"app",
"binder",
"loader",
"errors",
]

118
openapi_first/app.py Normal file
View File

@@ -0,0 +1,118 @@
"""
fastapi_openapi_first.app
=========================
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.
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.
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.
Intended usage
--------------
This module is intended for teams that want:
- OpenAPI-first API development
- Strong contract enforcement
- Minimal FastAPI boilerplate
- Predictable, CI-friendly failures for spec/implementation drift
"""
from fastapi import FastAPI
from .loader import load_openapi
from .binder import bind_routes
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.
Parameters
----------
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
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.
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 fastapi_openapi_first import OpenAPIFirstApp
>>> import app.routes as routes
>>>
>>> app = OpenAPIFirstApp(
... openapi_path="app/openapi.json",
... routes_module=routes,
... title="Example Service"
... )
"""
def __init__(
self,
*,
openapi_path: str,
routes_module,
**fastapi_kwargs,
):
# Initialize FastAPI normally
super().__init__(**fastapi_kwargs)
# Load and validate OpenAPI specification
self._openapi_spec = load_openapi(openapi_path)
# Bind routes strictly from OpenAPI spec
bind_routes(
app=self,
spec=self._openapi_spec,
routes_module=routes_module,
)
# Override FastAPI's OpenAPI generation
self.openapi = lambda: self._openapi_spec

102
openapi_first/binder.py Normal file
View File

@@ -0,0 +1,102 @@
"""
fastapi_openapi_first.binder
============================
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.
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
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.
This module intentionally does NOT:
-------------------------------
- Perform request or response validation
- Generate Pydantic models
- Modify FastAPI dependency injection
- Interpret OpenAPI semantics beyond routing metadata
Those concerns belong to other layers or tooling.
"""
from fastapi.routing import APIRoute
from .errors import MissingOperationHandler
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.
Parameters
----------
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.
Raises
------
MissingOperationHandler
If an operationId is missing from the spec or if no corresponding
handler function exists in the routes module.
Behavior guarantees
-------------------
- 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", {})
for path, methods in paths.items():
for http_method, operation in methods.items():
operation_id = operation.get("operationId")
if not operation_id:
raise MissingOperationHandler(
path=path,
method=http_method,
)
try:
endpoint = getattr(routes_module, operation_id)
except AttributeError:
raise MissingOperationHandler(
path=path,
method=http_method,
operation_id=operation_id,
)
route = APIRoute(
path=path,
endpoint=endpoint,
methods=[http_method.upper()],
name=operation_id,
)
app.router.routes.append(route)

72
openapi_first/errors.py Normal file
View File

@@ -0,0 +1,72 @@
"""
fastapi_openapi_first.errors
============================
Custom 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.
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.
All exceptions raised by the OpenAPI-first core should inherit from
this type.
"""
pass
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.
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.
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 = (
f"Missing handler for operationId '{operation_id}' "
f"({method.upper()} {path})"
)
else:
message = (
f"Missing operationId for operation "
f"({method.upper()} {path})"
)
super().__init__(message)

109
openapi_first/loader.py Normal file
View File

@@ -0,0 +1,109 @@
"""
fastapi_openapi_first.loaders
=============================
OpenAPI specification loading and validation utilities.
This module is responsible for loading an OpenAPI 3.x specification
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.
This module intentionally does NOT:
-----------------------------------
- Modify the OpenAPI document
- Infer missing fields
- Generate models or code
- Perform request/response validation at runtime
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import yaml
from openapi_spec_validator import validate_spec
from .errors import OpenAPIFirstError
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.
"""
pass
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.
Parameters
----------
path : str or pathlib.Path
Filesystem path to an OpenAPI specification file.
Supported extensions:
- `.json`
- `.yaml`
- `.yml`
Returns
-------
dict
Parsed and validated OpenAPI specification.
Raises
------
OpenAPISpecLoadError
If the file does not exist, cannot be parsed, or fails
OpenAPI schema validation.
"""
spec_path = Path(path)
if not spec_path.exists():
raise OpenAPISpecLoadError(f"OpenAPI spec not found: {spec_path}")
try:
if spec_path.suffix == ".json":
with spec_path.open("r", encoding="utf-8") as f:
spec = json.load(f)
elif spec_path.suffix in {".yaml", ".yml"}:
with spec_path.open("r", encoding="utf-8") as f:
spec = yaml.safe_load(f)
else:
raise OpenAPISpecLoadError(
f"Unsupported OpenAPI file format: {spec_path.suffix}"
)
except Exception as exc:
raise OpenAPISpecLoadError(
f"Failed to parse OpenAPI spec: {spec_path}"
) from exc
try:
validate_spec(spec)
except Exception as exc:
raise OpenAPISpecLoadError(
f"OpenAPI spec validation failed: {spec_path}"
) from exc
return spec