chore(release): 0.0.7

Squash-merge lib-standardisation: adopt repo standards across packaging,
tooling, docs, and CI.

- standardize pyproject.toml (canonical packaging, lint config, extras, GSDFC pydoclint)
- add py.typed marker, modernize type hints and Google-style docstrings
- nest generated docs under docs/lib and docs/mcp; add hand-written wiki
- adopt standard Type A PyPI CI pipeline (tag==version guard + quality gate)
- promote generated CHANGELOG.md; bump version to 0.0.7
This commit is contained in:
2026-10-17 02:16:49 +05:30
parent c95de5a6e9
commit 621f10409e
162 changed files with 10165 additions and 2846 deletions

43
tests/conftest.py Normal file
View File

@@ -0,0 +1,43 @@
"""Shared fixtures for the openapi-first smoke test suite."""
import json
import pytest
MINIMAL_SPEC = {
"openapi": "3.0.3",
"info": {"title": "Smoke Test API", "version": "1.0.0"},
"servers": [{"url": "https://api.example.com/v1"}],
"paths": {
"/health": {
"get": {
"operationId": "get_health",
"responses": {"200": {"description": "OK"}},
}
}
},
}
@pytest.fixture
def minimal_spec() -> dict:
"""Return a minimal valid OpenAPI 3.0.3 specification."""
return json.loads(json.dumps(MINIMAL_SPEC))
@pytest.fixture
def spec_file(tmp_path, minimal_spec):
"""Write the minimal spec to a temp JSON file and return its path."""
def _write(name: str, spec: dict | None = None) -> str:
path = tmp_path / name
payload = spec if spec is not None else minimal_spec
if name.endswith(".json"):
path.write_text(json.dumps(payload), encoding="utf-8")
else:
import yaml
path.write_text(yaml.safe_dump(payload), encoding="utf-8")
return str(path)
return _write

89
tests/test_app.py Normal file
View File

@@ -0,0 +1,89 @@
"""Smoke tests for openapi_first.app."""
import types
import pytest
from fastapi.testclient import TestClient
from openapi_first.app import OpenAPIFirstApp
from openapi_first.errors import MissingOperationHandler
SPEC = {
"openapi": "3.0.3",
"info": {"title": "App API", "version": "1.0.0"},
"servers": [{"url": "https://api.example.com/v1"}],
"paths": {
"/health": {
"get": {
"operationId": "get_health",
"responses": {"200": {"description": "OK"}},
}
},
"/greet/{name}": {
"get": {
"operationId": "get_greet",
"parameters": [
{
"name": "name",
"in": "path",
"required": True,
"schema": {"type": "string"},
}
],
"responses": {"200": {"description": "OK"}},
}
},
},
}
def _routes_module():
module = types.ModuleType("smoke_routes")
def get_health():
return {"status": "ok"}
def get_greet(name: str):
return {"greeting": f"Hello, {name}"}
module.get_health = get_health
module.get_greet = get_greet
return module
@pytest.fixture
def spec_file(tmp_path):
import json
path = tmp_path / "openapi.json"
path.write_text(json.dumps(SPEC), encoding="utf-8")
return str(path)
def test_app_routes_served(spec_file):
app = OpenAPIFirstApp(openapi_path=spec_file, routes_module=_routes_module())
client = TestClient(app)
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
def test_app_path_param_route(spec_file):
app = OpenAPIFirstApp(openapi_path=spec_file, routes_module=_routes_module())
client = TestClient(app)
response = client.get("/greet/Ada")
assert response.status_code == 200
assert response.json() == {"greeting": "Hello, Ada"}
def test_app_overrides_openapi_schema(spec_file):
app = OpenAPIFirstApp(openapi_path=spec_file, routes_module=_routes_module())
openapi = app.openapi()
assert openapi["info"]["title"] == "App API"
assert "/health" in openapi["paths"]
def test_app_missing_handler_fails_at_startup(spec_file):
empty = types.ModuleType("empty_routes")
with pytest.raises(MissingOperationHandler):
OpenAPIFirstApp(openapi_path=spec_file, routes_module=empty)

72
tests/test_binder.py Normal file
View File

@@ -0,0 +1,72 @@
"""Smoke tests for openapi_first.binder."""
import types
import pytest
from fastapi import FastAPI
from openapi_first.binder import bind_routes
from openapi_first.errors import MissingOperationHandler
def _routes_module():
module = types.ModuleType("smoke_routes")
def get_health():
return {"status": "ok"}
module.get_health = get_health
return module
def test_bind_routes_registers_matching_routes(minimal_spec):
from fastapi.routing import APIRoute
app = FastAPI()
bind_routes(app, minimal_spec, _routes_module())
bound = [r for r in app.router.routes if isinstance(r, APIRoute)]
assert len(bound) == 1
assert bound[0].name == "get_health"
assert bound[0].path == "/health"
assert "GET" in bound[0].methods
def test_bind_routes_missing_handler(minimal_spec):
app = FastAPI()
module = types.ModuleType("empty_routes")
with pytest.raises(MissingOperationHandler):
bind_routes(app, minimal_spec, module)
def test_bind_routes_missing_operation_id(spec_file):
spec = {
"openapi": "3.0.3",
"info": {"title": "No OpId", "version": "1.0.0"},
"paths": {"/health": {"get": {"responses": {"200": {"description": "OK"}}}}},
}
app = FastAPI()
with pytest.raises(MissingOperationHandler):
bind_routes(app, spec, _routes_module())
def test_bind_routes_ignores_spec_only_paths():
from fastapi.routing import APIRoute
spec = {
"openapi": "3.0.3",
"info": {"title": "Path Item", "version": "1.0.0"},
"paths": {
"/health": {
"parameters": [{"name": "x-trace", "in": "header"}],
"get": {
"operationId": "get_health",
"responses": {"200": {"description": "OK"}},
},
}
},
}
app = FastAPI()
bind_routes(app, spec, _routes_module())
bound = [r for r in app.router.routes if isinstance(r, APIRoute)]
assert len(bound) == 1
assert bound[0].name == "get_health"

130
tests/test_client.py Normal file
View File

@@ -0,0 +1,130 @@
"""Smoke tests for openapi_first.client."""
import types
import pytest
from openapi_first.client import OpenAPIClient, OpenAPIClientError
SPEC = {
"openapi": "3.0.3",
"info": {"title": "Client API", "version": "1.0.0"},
"servers": [{"url": "https://api.example.com/v1"}],
"paths": {
"/users/{user_id}": {
"get": {
"operationId": "get_user",
"parameters": [
{
"name": "user_id",
"in": "path",
"required": True,
"schema": {"type": "integer"},
}
],
"responses": {"200": {"description": "OK"}},
}
},
"/users": {
"post": {
"operationId": "create_user",
"requestBody": {
"content": {"application/json": {"schema": {"type": "object"}}}
},
"responses": {"201": {"description": "Created"}},
}
},
},
}
class _RecordingClient:
"""Minimal httpx.Client stand-in that records and routes requests."""
def __init__(self, base_url=None):
self.base_url = base_url
self.requests = []
def request(
self,
*,
method,
url,
params=None,
headers=None,
json=None,
content=None,
timeout=None,
):
self.requests.append(
{
"method": method,
"url": url,
"params": params,
"headers": headers,
"json": json,
"content": content,
"timeout": timeout,
}
)
response = types.SimpleNamespace(status_code=200, json=lambda: {"ok": True})
return response
def test_default_base_url_from_servers():
client = OpenAPIClient(SPEC)
assert client.base_url == "https://api.example.com/v1/"
def test_operation_methods_are_generated(minimal_spec):
client = OpenAPIClient(minimal_spec, base_url="http://test")
assert "get_health" in client.operations()
assert callable(client.get_health)
def test_unknown_operation_raises_attribute_error(minimal_spec):
client = OpenAPIClient(minimal_spec, base_url="http://test")
with pytest.raises(AttributeError):
client.does_not_exist()
def test_path_param_formatting_and_urljoin():
rec = _RecordingClient()
client = OpenAPIClient(SPEC, base_url="https://host", client=rec)
client.get_user(path_params={"user_id": 42})
assert rec.requests[0]["url"] == "https://host/users/42"
def test_missing_required_path_param():
rec = _RecordingClient()
client = OpenAPIClient(SPEC, base_url="https://host", client=rec)
with pytest.raises(OpenAPIClientError):
client.get_user(path_params={})
def test_json_body_defaults_content_type():
rec = _RecordingClient()
client = OpenAPIClient(SPEC, base_url="https://host", client=rec)
client.create_user(body={"name": "Ada"})
req = rec.requests[0]
assert req["json"] == {"name": "Ada"}
assert req["headers"].get("Content-Type") == "application/json"
def test_missing_server_raises():
spec = dict(SPEC)
spec.pop("servers")
with pytest.raises(OpenAPIClientError):
OpenAPIClient(spec)
def test_duplicate_operation_id_raises():
spec = dict(SPEC)
spec["paths"]["/extra"] = {
"get": {
"operationId": "get_user",
"responses": {"200": {"description": "OK"}},
}
}
with pytest.raises(OpenAPIClientError):
OpenAPIClient(spec)

View File

@@ -0,0 +1,284 @@
"""Smoke tests for openapi_first.codegen_routes."""
import json
import pytest
from openapi_first.codegen_routes import generate_routes
def _write_spec(tmp_path, spec: dict):
path = tmp_path / "openapi.json"
path.write_text(json.dumps(spec), encoding="utf-8")
return path
def _spec(paths: dict, schemas: dict | None = None) -> dict:
spec = {
"openapi": "3.0.3",
"info": {"title": "Codegen Test API", "version": "1.0.0"},
"paths": paths,
}
if schemas is not None:
spec["components"] = {"schemas": schemas}
return spec
def test_flat_output_names_files_by_resource(tmp_path):
spec = _write_spec(
tmp_path,
_spec(
{
"/items": {
"get": {
"operationId": "list_items",
"responses": {"200": {"description": "OK"}},
}
}
}
),
)
files = generate_routes(spec, tmp_path / "routes")
assert [f.name for f in files] == ["items.py"]
def test_resource_name_is_sanitized_to_identifier(tmp_path):
spec = _write_spec(
tmp_path,
_spec(
{
"/fetch-requests": {
"get": {
"operationId": "list_fetch_requests",
"responses": {"200": {"description": "OK"}},
}
}
}
),
)
files = generate_routes(spec, tmp_path / "routes")
assert [f.name for f in files] == ["fetch_requests.py"]
source = files[0].read_text(encoding="utf-8")
compile(source, str(files[0]), "exec")
def test_inject_request_prepends_request_param(tmp_path):
spec = _write_spec(
tmp_path,
_spec(
{
"/items": {
"get": {
"operationId": "list_items",
"responses": {"200": {"description": "OK"}},
}
}
}
),
)
files = generate_routes(spec, tmp_path / "routes", inject_request=True)
source = files[0].read_text(encoding="utf-8")
assert "from fastapi import Request" in source or "Request" in source
assert "def list_items(request: Request):" in source
def test_package_layout_emits_routes_package(tmp_path):
spec = _write_spec(
tmp_path,
_spec(
{
"/items/{id}": {
"get": {
"operationId": "get_item",
"parameters": [
{
"name": "id",
"in": "path",
"required": True,
"schema": {"type": "string"},
}
],
"responses": {"200": {"description": "OK"}},
}
}
}
),
)
out = tmp_path / "interfaces"
files = generate_routes(spec, out, package=True)
assert files[0] == out / "items" / "routes.py"
assert (out / "items" / "__init__.py").exists()
assert (out / "__init__.py").exists()
assert "def get_item(id: str):" in files[0].read_text(encoding="utf-8")
def test_multipart_body_expands_to_uploadfile(tmp_path):
spec = _write_spec(
tmp_path,
_spec(
{
"/pets/{id}/photo": {
"post": {
"operationId": "upload_pet_photo",
"parameters": [
{
"name": "id",
"in": "path",
"required": True,
"schema": {"type": "string"},
}
],
"requestBody": {
"required": True,
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"required": ["file"],
"properties": {
"file": {
"type": "string",
"format": "binary",
},
"caption": {"type": "string"},
},
}
}
},
},
"responses": {"200": {"description": "OK"}},
}
}
}
),
)
files = generate_routes(spec, tmp_path / "routes")
source = files[0].read_text(encoding="utf-8")
assert "UploadFile" in source
assert "file: UploadFile = File(...)" in source
assert "caption: str = Form(None)" in source
assert "payload: dict" not in source
compile(source, str(files[0]), "exec")
def test_json_body_unchanged_by_default(tmp_path):
spec = _write_spec(
tmp_path,
_spec(
{
"/items": {
"post": {
"operationId": "create_item",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ItemInput"}
}
},
},
"responses": {"201": {"description": "Created"}},
}
}
},
{
"ItemInput": {
"type": "object",
"properties": {"name": {"type": "string"}},
}
},
),
)
files = generate_routes(spec, tmp_path / "routes")
source = files[0].read_text(encoding="utf-8")
assert "payload: dict" in source
assert "response: Response" in source
def test_missing_operation_id_raises(tmp_path):
spec = _write_spec(
tmp_path,
_spec({"/items": {"get": {"responses": {"200": {"description": "OK"}}}}}),
)
with pytest.raises(ValueError):
generate_routes(spec, tmp_path / "routes")
def test_template_style_async_body_model(tmp_path):
spec = _write_spec(
tmp_path,
_spec(
{
"/items": {
"post": {
"operationId": "create_item",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ItemInput"}
}
},
},
"responses": {"201": {"description": "Created"}},
}
}
},
{
"ItemInput": {
"type": "object",
"properties": {"name": {"type": "string"}},
}
},
),
)
files = generate_routes(
spec,
tmp_path / "routes",
use_models=True,
models_module="_gen.models",
inject_request=True,
template_style=True,
)
source = files[0].read_text(encoding="utf-8")
assert (
"async def create_item(request: Request, body: ItemInput, response: Response):"
in source
)
assert "payload" not in source
assert "db_resources" in source
compile(source, str(files[0]), "exec")
def test_template_style_dict_body_without_models(tmp_path):
spec = _write_spec(
tmp_path,
_spec(
{
"/items": {
"post": {
"operationId": "create_item",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ItemInput"}
}
},
},
"responses": {"201": {"description": "Created"}},
}
}
}
),
)
files = generate_routes(
spec, tmp_path / "routes", inject_request=True, template_style=True
)
source = files[0].read_text(encoding="utf-8")
assert (
"async def create_item(request: Request, body: dict, response: Response):"
in source
)
assert "payload" not in source
compile(source, str(files[0]), "exec")

63
tests/test_loader.py Normal file
View File

@@ -0,0 +1,63 @@
"""Smoke tests for openapi_first.loader."""
import pytest
from openapi_first.loader import OpenAPISpecLoadError, load_openapi
def test_load_openapi_json(spec_file, minimal_spec):
path = spec_file("openapi.json")
spec = load_openapi(path)
assert spec == minimal_spec
def test_load_openapi_yaml(spec_file, minimal_spec):
path = spec_file("openapi.yaml")
spec = load_openapi(path)
assert spec == minimal_spec
def test_load_missing_file(tmp_path):
with pytest.raises(OpenAPISpecLoadError):
load_openapi(tmp_path / "missing.json")
def test_load_unsupported_extension(tmp_path):
path = tmp_path / "openapi.txt"
path.write_text("{}", encoding="utf-8")
with pytest.raises(OpenAPISpecLoadError):
load_openapi(path)
def test_load_invalid_spec(tmp_path):
path = tmp_path / "invalid.yaml"
path.write_text("not: [valid: openapi", encoding="utf-8")
with pytest.raises(OpenAPISpecLoadError):
load_openapi(path)
def test_load_validation_failure(tmp_path):
path = tmp_path / "bad.yaml"
path.write_text("openapi: 3.0.3\ninfo: {}\n", encoding="utf-8")
with pytest.raises(OpenAPISpecLoadError):
load_openapi(path)
def test_env_var_resolution(spec_file, monkeypatch):
monkeypatch.setenv("AUTH_HOST", "http://auth.internal")
spec = {
"openapi": "3.0.3",
"info": {"title": "Env API", "version": "1.0.0"},
"servers": [{"url": "{AUTH_HOST}/v1"}],
"paths": {
"/ping": {
"get": {
"operationId": "get_ping",
"responses": {"200": {"description": "OK"}},
}
}
},
}
path = spec_file("env.yaml", spec)
loaded = load_openapi(path)
assert loaded["servers"][0]["url"] == "{AUTH_HOST}/v1"