commit 21a17b771743b23ee41d11a90ed8fdc3433468ce
Author: jableader <jacobdunk@gmail.com>
Date: Mon Oct 20 00:12:02 2025 +1100
Completed tooling improvements, fixed remaining errors
commit 7db48e222e3aa1065c326197c33ba6439720f65a
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 22:05:37 2025 +1100
autoformat
commit 5705ce24b64c2aa6f0b9426730a479165fa97e2a
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 22:05:29 2025 +1100
tooling changes
commit f0a6b2fd147bb86b484927afd57b9ba0ac07bf47
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 21:25:49 2025 +1100
Plan
111 lines
4 KiB
Python
111 lines
4 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
|
def extend_with_problem_and_cookie_auth(app: FastAPI) -> None:
|
|
"""Augment FastAPI's OpenAPI spec with RFC7807 responses and cookie auth.
|
|
|
|
This mutates the app's OpenAPI generation in-place while delegating to the
|
|
original generator for the base schema.
|
|
"""
|
|
original_openapi = app.openapi
|
|
|
|
def custom_openapi() -> dict[str, Any]:
|
|
spec = original_openapi()
|
|
components = spec.setdefault("components", {})
|
|
responses = components.setdefault("responses", {})
|
|
security_schemes = components.setdefault("securitySchemes", {})
|
|
|
|
# Standard ProblemDetails responses
|
|
responses.setdefault(
|
|
"Problem400",
|
|
{
|
|
"description": "Bad Request",
|
|
"content": {
|
|
"application/problem+json": {},
|
|
"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}},
|
|
},
|
|
},
|
|
)
|
|
responses.setdefault(
|
|
"Problem404",
|
|
{
|
|
"description": "Not Found",
|
|
"content": {
|
|
"application/problem+json": {},
|
|
"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}},
|
|
},
|
|
},
|
|
)
|
|
responses.setdefault(
|
|
"Problem422",
|
|
{
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/problem+json": {
|
|
"schema": {"$ref": "#/components/schemas/ProblemDetails"}
|
|
},
|
|
"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}},
|
|
},
|
|
},
|
|
)
|
|
|
|
# Cookie-based auth for documentation (does not enforce at runtime)
|
|
security_schemes.setdefault(
|
|
"cookieAuth",
|
|
{
|
|
"type": "apiKey",
|
|
"in": "cookie",
|
|
"name": "user_id",
|
|
"description": "Authentication via user_id cookie (session-style).",
|
|
},
|
|
)
|
|
|
|
# Normalize v1 responses and mark cookie security for known endpoints
|
|
paths = spec.get("paths", {})
|
|
protected_ops: set[str] = {
|
|
"parseRecipe",
|
|
"createRecipe",
|
|
"deleteRecipe",
|
|
"markMealConsumed",
|
|
"deleteMeal",
|
|
"purchaseIngredients",
|
|
"getMyShoppingList",
|
|
"syncMyShoppingList",
|
|
"requestMeal",
|
|
"unrequestMeal",
|
|
"refresh",
|
|
}
|
|
for path, ops in paths.items():
|
|
if not isinstance(path, str) or not path.startswith("/api/v1/"):
|
|
continue
|
|
if not isinstance(ops, dict):
|
|
continue
|
|
for _method, op in ops.items():
|
|
if not isinstance(op, dict):
|
|
continue
|
|
resp = op.get("responses")
|
|
if not isinstance(resp, dict):
|
|
continue
|
|
if "400" in resp:
|
|
resp["400"] = {"$ref": "#/components/responses/Problem400"}
|
|
if "404" in resp:
|
|
resp["404"] = {"$ref": "#/components/responses/Problem404"}
|
|
if "422" not in resp:
|
|
resp["422"] = {"$ref": "#/components/responses/Problem422"}
|
|
|
|
op_id = op.get("operationId")
|
|
if isinstance(op_id, str) and op_id in protected_ops:
|
|
security = op.setdefault("security", [])
|
|
if not any(isinstance(s, dict) and "cookieAuth" in s for s in security):
|
|
security.append({"cookieAuth": []})
|
|
|
|
# Keep endpoint-specific schemas driven by route declarations only (no forced overrides)
|
|
|
|
return spec
|
|
|
|
# Rebind app.openapi to our generator (FastAPI supports this pattern). Mypy needs a narrow ignore here.
|
|
app.openapi = custom_openapi # type: ignore[method-assign]
|