used for server-side route binding, and refactor documentation and templates to treat the client as a first-class contract consumer. Key changes: - Add OpenAPI-first client module based on httpx - Document client usage alongside server-side binder usage - Update mkdocs navigation to include client documentation - Refactor CRUD and model app templates to call APIs via operationId instead of hardcoded paths - Align package documentation and public API surface with client support - Clarify server/client dependency split (fastapi vs httpx) This establishes strict symmetry between OpenAPI-driven server binding and OpenAPI-driven client invocation, reinforcing OpenAPI as the single source of truth on both sides of the contract.
108 lines
2.9 KiB
Python
108 lines
2.9 KiB
Python
"""
|
|
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
|
|
"""
|
|
|
|
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
|