Coverage for src/lexigram/web/docs/enrichment.py: 18%
78 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""OpenAPI enrichment utilities.
3Adds security schemes, tags, pagination, and error schemas to OpenAPI spec.
4"""
6from __future__ import annotations
8from typing import Any
11class OpenAPIEnricher:
12 """Enriches OpenAPI specifications with additional details."""
14 def __init__(self, spec: dict[str, Any]):
15 self.spec = spec
16 self._ensure_components()
18 def _ensure_components(self) -> None:
19 """Ensure components section exists."""
20 if "components" not in self.spec:
21 self.spec["components"] = {}
22 if "schemas" not in self.spec["components"]:
23 self.spec["components"]["schemas"] = {}
24 if "securitySchemes" not in self.spec["components"]:
25 self.spec["components"]["securitySchemes"] = {}
27 def add_security_scheme(
28 self,
29 name: str,
30 scheme_type: str,
31 *,
32 bearer_format: str | None = None,
33 flows: dict | None = None,
34 api_key_name: str | None = None,
35 api_key_in: str | None = None,
36 ) -> OpenAPIEnricher:
37 """Add a security scheme to the spec."""
38 schemes = self.spec["components"]["securitySchemes"]
40 if scheme_type == "http":
41 schemes[name] = {
42 "type": "http",
43 "scheme": "bearer",
44 }
45 if bearer_format:
46 schemes[name]["bearerFormat"] = bearer_format
47 elif scheme_type == "oauth2":
48 schemes[name] = {
49 "type": "oauth2",
50 "flows": flows or {},
51 }
52 elif scheme_type == "apiKey":
53 schemes[name] = {
54 "type": "apiKey",
55 "name": api_key_name or name,
56 "in": api_key_in or "header",
57 }
59 return self
61 def add_bearer_auth(self, bearer_format: str = "JWT") -> OpenAPIEnricher:
62 """Add Bearer authentication (JWT, etc.)."""
63 return self.add_security_scheme(
64 "BearerAuth",
65 "http",
66 bearer_format=bearer_format,
67 )
69 def add_api_key_auth(
70 self,
71 name: str = "ApiKeyAuth",
72 header_name: str = "X-API-Key",
73 ) -> OpenAPIEnricher:
74 """Add API key authentication."""
75 return self.add_security_scheme(
76 name,
77 "apiKey",
78 api_key_name=header_name,
79 api_key_in="header",
80 )
82 def apply_security_to_all_paths(
83 self,
84 schemes: list[str],
85 ) -> OpenAPIEnricher:
86 """Apply security schemes to all paths."""
87 security = [{"scheme": s} for s in schemes]
89 for _path, path_item in self.spec.get("paths", {}).items():
90 for method, operation in path_item.items():
91 if method in ("get", "post", "put", "patch", "delete"):
92 if "security" not in operation:
93 operation["security"] = security
95 return self
97 def add_tag(
98 self,
99 name: str,
100 *,
101 description: str | None = None,
102 ) -> OpenAPIEnricher:
103 """Add a tag to the spec."""
104 tags = self.spec.setdefault("tags", [])
106 # Check if tag already exists
107 for tag in tags:
108 if tag["name"] == name:
109 if description:
110 tag["description"] = description
111 return self
113 tag = {"name": name}
114 if description:
115 tag["description"] = description
116 tags.append(tag)
118 return self
120 def add_error_schema(
121 self,
122 error_type: str,
123 status_code: int,
124 ) -> OpenAPIEnricher:
125 """Add an error response schema."""
126 schema = {
127 "type": "object",
128 "properties": {
129 "error": {"type": "string"},
130 "message": {"type": "string"},
131 },
132 }
134 # Add to common error schemas
135 error_name = f"Error{error_type}"
136 self.spec["components"]["schemas"][error_name] = schema
138 # Add to each path that uses it
139 for _path, path_item in self.spec.get("paths", {}).items():
140 for method, operation in path_item.items():
141 if method in ("get", "post", "put", "patch", "delete"):
142 responses = operation.get("responses", {})
143 if str(status_code) not in responses:
144 responses[str(status_code)] = {
145 "description": error_type,
146 "content": {
147 "application/json": {
148 "schema": {
149 "$ref": f"#/components/schemas/{error_name}",
150 },
151 },
152 },
153 }
155 return self
157 def add_pagination_response(
158 self,
159 response_name: str,
160 item_schema: dict[str, Any],
161 ) -> OpenAPIEnricher:
162 """Add a paginated response schema."""
163 schema = {
164 "type": "object",
165 "properties": {
166 "items": {
167 "type": "array",
168 "items": item_schema,
169 },
170 "meta": {
171 "type": "object",
172 "properties": {
173 "total": {"type": "integer"},
174 "page": {"type": "integer"},
175 "size": {"type": "integer"},
176 "pages": {"type": "integer"},
177 "has_next": {"type": "boolean"},
178 "has_prev": {"type": "boolean"},
179 },
180 },
181 },
182 }
184 self.spec["components"]["schemas"][response_name] = schema
185 return self
187 def add_file_upload_parameter(
188 self,
189 name: str,
190 required: bool = False,
191 ) -> dict[str, Any]:
192 """Create a file upload parameter schema."""
193 return {
194 "name": name,
195 "in": "formData",
196 "description": "File to upload",
197 "required": required,
198 "type": "file",
199 }
202def enrich_spec(
203 spec: dict[str, Any],
204 *,
205 add_bearer: bool = True,
206 add_api_key: bool = False,
207 add_error_responses: bool = True,
208) -> dict[str, Any]:
209 """Enrich an OpenAPI spec with common additions."""
210 enricher = OpenAPIEnricher(spec)
212 if add_bearer:
213 enricher.add_bearer_auth()
215 if add_api_key:
216 enricher.add_api_key_auth()
218 if add_error_responses:
219 enricher.add_error_schema("Validation", 400)
220 enricher.add_error_schema("Unauthorized", 401)
221 enricher.add_error_schema("Forbidden", 403)
222 enricher.add_error_schema("NotFound", 404)
223 enricher.add_error_schema("InternalError", 500)
225 return spec