cadwyn 3.12.0__py3-none-any.whl → 3.15.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cadwyn might be problematic. Click here for more details.

cadwyn/__init__.py CHANGED
@@ -2,8 +2,12 @@ import importlib.metadata
2
2
 
3
3
  from .applications import Cadwyn
4
4
  from .codegen import generate_code_for_versioned_packages
5
- from .route_generation import InternalRepresentationOf, VersionedAPIRouter, generate_versioned_routers
6
- from .structure import VersionBundle
5
+ from .route_generation import (
6
+ InternalRepresentationOf, # pyright: ignore[reportDeprecated]
7
+ VersionedAPIRouter,
8
+ generate_versioned_routers,
9
+ )
10
+ from .structure import HeadVersion, Version, VersionBundle
7
11
 
8
12
  __version__ = importlib.metadata.version("cadwyn")
9
13
  __all__ = [
@@ -11,6 +15,8 @@ __all__ = [
11
15
  "VersionedAPIRouter",
12
16
  "generate_code_for_versioned_packages",
13
17
  "VersionBundle",
18
+ "HeadVersion",
19
+ "Version",
14
20
  "generate_versioned_routers",
15
21
  "InternalRepresentationOf",
16
22
  ]
cadwyn/__main__.py CHANGED
@@ -60,7 +60,7 @@ def deprecated_generate_versioned_packages(
60
60
  possibly_version_bundle = getattr(version_bundle_module, version_bundle_variable_name)
61
61
  version_bundle = _get_version_bundle(possibly_version_bundle)
62
62
 
63
- return generate_code_for_versioned_packages(
63
+ return generate_code_for_versioned_packages( # pyright: ignore[reportDeprecated]
64
64
  template_package,
65
65
  version_bundle,
66
66
  ignore_coverage_for_latest_aliases=ignore_coverage_for_latest_aliases,
@@ -115,8 +115,7 @@ def _get_version_bundle(possibly_version_bundle: Any) -> VersionBundle:
115
115
  @app.callback()
116
116
  def main(
117
117
  version: bool = typer.Option(None, "-V", "--version", callback=version_callback, is_eager=True),
118
- ):
119
- ...
118
+ ): ...
120
119
 
121
120
 
122
121
  if __name__ == "__main__":
cadwyn/_asts.py CHANGED
@@ -10,6 +10,7 @@ from typing import ( # noqa: UP035
10
10
  TYPE_CHECKING,
11
11
  Any,
12
12
  List,
13
+ cast,
13
14
  get_args,
14
15
  get_origin,
15
16
  )
@@ -32,7 +33,7 @@ _RE_CAMEL_TO_SNAKE = re.compile(r"(?<!^)(?=[A-Z])")
32
33
 
33
34
 
34
35
  # A parent type of typing._GenericAlias
35
- _BaseGenericAlias = type(List[int]).mro()[1] # noqa: UP006
36
+ _BaseGenericAlias = cast(type, type(List[int])).mro()[1] # noqa: UP006
36
37
 
37
38
  # type(list[int]) and type(List[int]) are different which is why we have to do this.
38
39
  # Please note that this problem is much wider than just lists which is why we use typing._BaseGenericAlias
@@ -100,7 +101,7 @@ def transform_generic_alias(value: GenericAliasUnion) -> Any:
100
101
  return f"{get_fancy_repr(get_origin(value))}[{', '.join(get_fancy_repr(a) for a in get_args(value))}]"
101
102
 
102
103
 
103
- def transform_none(_: NoneType) -> Any:
104
+ def transform_none(_: Any) -> Any:
104
105
  return "None"
105
106
 
106
107
 
@@ -134,7 +135,7 @@ def transform_auto(_: auto) -> Any:
134
135
  return PlainRepr("auto()")
135
136
 
136
137
 
137
- def transform_union(value: UnionType) -> Any: # pyright: ignore[reportInvalidTypeForm]
138
+ def transform_union(value: UnionType) -> Any:
138
139
  return "typing.Union[" + (", ".join(get_fancy_repr(a) for a in get_args(value))) + "]"
139
140
 
140
141
 
@@ -155,7 +156,6 @@ def transform_other(value: Any) -> Any:
155
156
  def _get_lambda_source_from_default_factory(source: str) -> str:
156
157
  found_lambdas: list[ast.Lambda] = []
157
158
 
158
- ast.parse(source)
159
159
  for node in ast.walk(ast.parse(source)):
160
160
  if isinstance(node, ast.keyword) and node.arg == "default_factory" and isinstance(node.value, ast.Lambda):
161
161
  found_lambdas.append(node.value)
@@ -231,7 +231,7 @@ def delete_keyword_from_call(attr_name: str, call: ast.Call):
231
231
  def get_ast_keyword_from_argument_name_and_value(name: str, value: Any):
232
232
  if not isinstance(value, ast.AST):
233
233
  value = ast.parse(get_fancy_repr(value), mode="eval").body
234
- return ast.keyword(arg=name, value=value)
234
+ return ast.keyword(arg=name, value=value) # pyright: ignore[reportArgumentType, reportCallIssue]
235
235
 
236
236
 
237
237
  def pop_docstring_from_cls_body(cls_body: list[ast.stmt]) -> list[ast.stmt]:
cadwyn/_compat.py CHANGED
@@ -61,7 +61,7 @@ class PydanticFieldWrapper:
61
61
 
62
62
  annotation: Any
63
63
 
64
- init_model_field: dataclasses.InitVar[ModelField] # pyright: ignore[reportInvalidTypeForm]
64
+ init_model_field: dataclasses.InitVar[ModelField]
65
65
  field_info: FieldInfo = dataclasses.field(init=False)
66
66
 
67
67
  annotation_ast: ast.expr | None = None
@@ -69,7 +69,7 @@ class PydanticFieldWrapper:
69
69
  # the value_ast is "None" and "Field(default=None)" respectively
70
70
  value_ast: ast.expr | None = None
71
71
 
72
- def __post_init__(self, init_model_field: ModelField): # pyright: ignore[reportInvalidTypeForm]
72
+ def __post_init__(self, init_model_field: ModelField):
73
73
  if isinstance(init_model_field, FieldInfo):
74
74
  self.field_info = init_model_field
75
75
  else:
@@ -111,6 +111,13 @@ class PydanticFieldWrapper:
111
111
  return attributes | extras
112
112
 
113
113
 
114
+ def get_annotation_from_model_field(model: ModelField) -> Any:
115
+ if PYDANTIC_V2:
116
+ return model.field_info.annotation
117
+ else:
118
+ return model.annotation
119
+
120
+
114
121
  def model_fields(model: type[BaseModel]) -> dict[str, FieldInfo]:
115
122
  if PYDANTIC_V2:
116
123
  return model.model_fields
cadwyn/applications.py CHANGED
@@ -1,3 +1,5 @@
1
+ import dataclasses
2
+ import datetime
1
3
  from collections.abc import Callable, Coroutine, Sequence
2
4
  from datetime import date
3
5
  from logging import getLogger
@@ -7,9 +9,14 @@ from typing import Any, cast
7
9
 
8
10
  from fastapi import APIRouter, FastAPI, HTTPException, routing
9
11
  from fastapi.datastructures import Default
10
- from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
12
+ from fastapi.openapi.docs import (
13
+ get_redoc_html,
14
+ get_swagger_ui_html,
15
+ get_swagger_ui_oauth2_redirect_html,
16
+ )
11
17
  from fastapi.openapi.utils import get_openapi
12
18
  from fastapi.params import Depends
19
+ from fastapi.responses import HTMLResponse
13
20
  from fastapi.templating import Jinja2Templates
14
21
  from fastapi.utils import generate_unique_id
15
22
  from starlette.middleware import Middleware
@@ -19,7 +26,6 @@ from starlette.routing import BaseRoute, Route
19
26
  from starlette.types import Lifespan
20
27
  from typing_extensions import Self, deprecated
21
28
 
22
- from cadwyn._utils import same_definition_as_in
23
29
  from cadwyn.middleware import HeaderVersioningMiddleware, _get_api_version_dependency
24
30
  from cadwyn.route_generation import generate_versioned_routers
25
31
  from cadwyn.routing import _RootHeaderAPIRouter
@@ -29,6 +35,11 @@ CURR_DIR = Path(__file__).resolve()
29
35
  logger = getLogger(__name__)
30
36
 
31
37
 
38
+ @dataclasses.dataclass(slots=True)
39
+ class FakeDependencyOverridesProvider:
40
+ dependency_overrides: dict[Callable[..., Any], Callable[..., Any]]
41
+
42
+
32
43
  class Cadwyn(FastAPI):
33
44
  _templates = Jinja2Templates(directory=CURR_DIR.parent / "static")
34
45
 
@@ -54,8 +65,13 @@ class Cadwyn(FastAPI):
54
65
  swagger_ui_oauth2_redirect_url: str | None = "/docs/oauth2-redirect",
55
66
  swagger_ui_init_oauth: dict[str, Any] | None = None,
56
67
  middleware: Sequence[Middleware] | None = None,
57
- exception_handlers: dict[int | type[Exception], Callable[[Request, Any], Coroutine[Any, Any, Response]]]
58
- | None = None,
68
+ exception_handlers: (
69
+ dict[
70
+ int | type[Exception],
71
+ Callable[[Request, Any], Coroutine[Any, Any, Response]],
72
+ ]
73
+ | None
74
+ ) = None,
59
75
  on_startup: Sequence[Callable[[], Any]] | None = None,
60
76
  on_shutdown: Sequence[Callable[[], Any]] | None = None,
61
77
  lifespan: Lifespan[Self] | None = None,
@@ -71,7 +87,9 @@ class Cadwyn(FastAPI):
71
87
  deprecated: bool | None = None,
72
88
  include_in_schema: bool = True,
73
89
  swagger_ui_parameters: dict[str, Any] | None = None,
74
- generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(generate_unique_id), # noqa: B008
90
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( # noqa: B008
91
+ generate_unique_id
92
+ ),
75
93
  separate_input_output_schemas: bool = True,
76
94
  **extra: Any,
77
95
  ) -> None:
@@ -80,6 +98,7 @@ class Cadwyn(FastAPI):
80
98
  latest_schemas_package = extra.pop("latest_schemas_package", None) or self.versions.head_schemas_package
81
99
  self.versions.head_schemas_package = latest_schemas_package
82
100
  self._latest_schemas_package = cast(ModuleType, latest_schemas_package)
101
+ self._dependency_overrides_provider = FakeDependencyOverridesProvider({})
83
102
 
84
103
  super().__init__(
85
104
  debug=debug,
@@ -143,11 +162,10 @@ class Cadwyn(FastAPI):
143
162
  self.redoc_url = redoc_url
144
163
  self.openapi_url = openapi_url
145
164
  self.redoc_url = redoc_url
146
- self.swaggers = {}
147
165
 
148
166
  unversioned_router = APIRouter(**self._kwargs_to_router)
149
167
  self._add_openapi_endpoints(unversioned_router)
150
- self.add_unversioned_routers(unversioned_router)
168
+ self.include_router(unversioned_router)
151
169
  self.add_middleware(
152
170
  HeaderVersioningMiddleware,
153
171
  api_version_header_name=self.router.api_version_header_name,
@@ -155,6 +173,20 @@ class Cadwyn(FastAPI):
155
173
  default_response_class=default_response_class,
156
174
  )
157
175
 
176
+ @property
177
+ def dependency_overrides(self) -> dict[Callable[..., Any], Callable[..., Any]]:
178
+ # This is only necessary because we cannot send self to versioned router generator
179
+ # because it takes a deepcopy of the router and self.versions.head_schemas_package is a module
180
+ # which cannot be copied.
181
+ return self._dependency_overrides_provider.dependency_overrides
182
+
183
+ @dependency_overrides.setter
184
+ def dependency_overrides( # pyright: ignore[reportIncompatibleVariableOverride]
185
+ self,
186
+ value: dict[Callable[..., Any], Callable[..., Any]],
187
+ ) -> None:
188
+ self._dependency_overrides_provider.dependency_overrides = value
189
+
158
190
  @property # pragma: no cover
159
191
  @deprecated("It is going to be deleted in the future. Use VersionBundle.head_schemas_package instead")
160
192
  def latest_schemas_package(self):
@@ -178,6 +210,18 @@ class Cadwyn(FastAPI):
178
210
  endpoint=self.swagger_dashboard,
179
211
  include_in_schema=False,
180
212
  )
213
+ if self.swagger_ui_oauth2_redirect_url:
214
+
215
+ async def swagger_ui_redirect(req: Request) -> HTMLResponse:
216
+ return (
217
+ get_swagger_ui_oauth2_redirect_html() # pragma: no cover # unimportant right now but # TODO
218
+ )
219
+
220
+ self.add_route(
221
+ self.swagger_ui_oauth2_redirect_url,
222
+ swagger_ui_redirect,
223
+ include_in_schema=False,
224
+ )
181
225
  if self.redoc_url is not None:
182
226
  unversioned_router.add_route(
183
227
  path=self.redoc_url,
@@ -186,7 +230,7 @@ class Cadwyn(FastAPI):
186
230
  )
187
231
 
188
232
  def generate_and_include_versioned_routers(self, *routers: APIRouter) -> None:
189
- root_router = APIRouter()
233
+ root_router = APIRouter(dependency_overrides_provider=self._dependency_overrides_provider)
190
234
  for router in routers:
191
235
  root_router.include_router(router)
192
236
  router_versions = generate_versioned_routers(
@@ -196,77 +240,85 @@ class Cadwyn(FastAPI):
196
240
  for version, router in router_versions.items():
197
241
  self.add_header_versioned_routers(router, header_value=version.isoformat())
198
242
 
199
- def enrich_swagger(self):
200
- """
201
- This method goes through all header-based apps and collect a dict[openapi_version, openapi_json]
202
-
203
- For each route a `X-API-VERSION` header with value is added
204
-
205
- """
206
- unversioned_routes_openapi = get_openapi(
207
- title=self.title,
208
- version=self.version,
209
- openapi_version=self.openapi_version,
210
- description=self.description,
211
- terms_of_service=self.terms_of_service,
212
- contact=self.contact,
213
- license_info=self.license_info,
214
- routes=self.router.routes,
215
- tags=self.openapi_tags,
216
- servers=self.servers,
243
+ async def openapi_jsons(self, req: Request) -> JSONResponse:
244
+ raw_version = req.query_params.get("version") or req.headers.get(self.router.api_version_header_name)
245
+ not_found_error = HTTPException(
246
+ status_code=404,
247
+ detail=f"OpenApi file of with version `{raw_version}` not found",
217
248
  )
218
- if unversioned_routes_openapi["paths"]:
219
- self.swaggers["unversioned"] = unversioned_routes_openapi
220
-
221
- for header_value, router in self.router.versioned_routers.items():
222
- header_value_str = header_value.isoformat()
223
- openapi = get_openapi(
249
+ try:
250
+ version = datetime.date.fromisoformat(raw_version) # pyright: ignore[reportArgumentType]
251
+ # TypeError when raw_version is None
252
+ # ValueError when raw_version is of the non-iso format
253
+ except (ValueError, TypeError):
254
+ version = raw_version
255
+
256
+ if version in self.router.versioned_routers:
257
+ routes = self.router.versioned_routers[version].routes
258
+ formatted_version = version.isoformat()
259
+ elif version == "unversioned" and self._there_are_public_unversioned_routes():
260
+ routes = self.router.unversioned_routes
261
+ formatted_version = "unversioned"
262
+ else:
263
+ raise not_found_error
264
+
265
+ return JSONResponse(
266
+ get_openapi(
224
267
  title=self.title,
225
- version=self.version,
268
+ version=formatted_version,
226
269
  openapi_version=self.openapi_version,
227
270
  description=self.description,
228
271
  terms_of_service=self.terms_of_service,
229
272
  contact=self.contact,
230
273
  license_info=self.license_info,
231
- routes=router.routes,
274
+ routes=routes,
232
275
  tags=self.openapi_tags,
233
276
  servers=self.servers,
234
277
  )
235
- # in current implementation we expect header_value to be a date
236
- self.swaggers[header_value_str] = openapi
237
-
238
- async def openapi_jsons(self, req: Request) -> JSONResponse:
239
- version = req.query_params.get("version") or req.headers.get(self.router.api_version_header_name)
240
- openapi_of_a_version = self.swaggers.get(version)
241
- if not openapi_of_a_version:
242
- raise HTTPException(
243
- status_code=404,
244
- detail=f"OpenApi file of with version `{version}` not found",
245
- )
278
+ )
246
279
 
247
- return JSONResponse(openapi_of_a_version)
280
+ def _there_are_public_unversioned_routes(self):
281
+ return any(isinstance(route, Route) and route.include_in_schema for route in self.router.unversioned_routes)
248
282
 
249
283
  async def swagger_dashboard(self, req: Request) -> Response:
250
- return self._render_docs_dashboard_or_concrete_verssion(get_swagger_ui_html, req, cast(str, self.docs_url))
284
+ version = req.query_params.get("version")
251
285
 
252
- async def redoc_dashboard(self, req: Request) -> Response:
253
- return self._render_docs_dashboard_or_concrete_verssion(get_redoc_html, req, cast(str, self.redoc_url))
286
+ if version:
287
+ root_path = self._extract_root_path(req)
288
+ openapi_url = root_path + f"{self.openapi_url}?version={version}"
289
+ oauth2_redirect_url = self.swagger_ui_oauth2_redirect_url
290
+ if oauth2_redirect_url:
291
+ oauth2_redirect_url = root_path + oauth2_redirect_url
292
+ return get_swagger_ui_html(
293
+ openapi_url=openapi_url,
294
+ title=f"{self.title} - Swagger UI",
295
+ oauth2_redirect_url=oauth2_redirect_url,
296
+ init_oauth=self.swagger_ui_init_oauth,
297
+ swagger_ui_parameters=self.swagger_ui_parameters,
298
+ )
299
+ return self._render_docs_dashboard(req, cast(str, self.docs_url))
254
300
 
255
- def _render_docs_dashboard_or_concrete_verssion(self, render_docs: Callable[..., Any], req: Request, docs_url: str):
256
- base_url = str(req.base_url).rstrip("/")
301
+ async def redoc_dashboard(self, req: Request) -> Response:
257
302
  version = req.query_params.get("version")
258
303
 
259
304
  if version:
260
- return render_docs(
261
- openapi_url=f"{self.openapi_url}?version={version}",
262
- title="Swagger UI",
263
- )
305
+ root_path = self._extract_root_path(req)
306
+ openapi_url = root_path + f"{self.openapi_url}?version={version}"
307
+ return get_redoc_html(openapi_url=openapi_url, title=f"{self.title} - ReDoc")
308
+
309
+ return self._render_docs_dashboard(req, docs_url=cast(str, self.redoc_url))
310
+
311
+ def _extract_root_path(self, req: Request):
312
+ return req.scope.get("root_path", "").rstrip("/")
313
+
314
+ def _render_docs_dashboard(self, req: Request, docs_url: str):
315
+ base_url = str(req.base_url).rstrip("/")
316
+ table = {version: f"{base_url}{docs_url}?version={version}" for version in self.router.sorted_versions}
317
+ if self._there_are_public_unversioned_routes():
318
+ table |= {"unversioned": f"{base_url}{docs_url}?version=unversioned"}
264
319
  return self._templates.TemplateResponse(
265
320
  "docs.html",
266
- {
267
- "request": req,
268
- "table": {version: f"{base_url}{docs_url}?version={version}" for version in sorted(self.swaggers)},
269
- },
321
+ {"request": req, "table": table},
270
322
  )
271
323
 
272
324
  def add_header_versioned_routers(
@@ -281,109 +333,41 @@ class Cadwyn(FastAPI):
281
333
  except ValueError as e:
282
334
  raise ValueError("header_value should be in ISO 8601 format") from e
283
335
 
336
+ added_routes: list[BaseRoute] = []
284
337
  if header_value_as_dt not in self.router.versioned_routers: # pragma: no branch
285
338
  self.router.versioned_routers[header_value_as_dt] = APIRouter(**self._kwargs_to_router)
286
- if self.openapi_url is not None: # pragma: no branch
287
- self.router.versioned_routers[header_value_as_dt].add_route(
288
- path=self.openapi_url,
289
- endpoint=self.openapi_jsons,
290
- include_in_schema=False,
291
- )
292
339
 
293
- added_routes: list[BaseRoute] = []
340
+ versioned_router = self.router.versioned_routers[header_value_as_dt]
341
+ if self.openapi_url is not None: # pragma: no branch
342
+ versioned_router.add_route(
343
+ path=self.openapi_url,
344
+ endpoint=self.openapi_jsons,
345
+ include_in_schema=False,
346
+ )
347
+ added_routes.append(versioned_router.routes[-1])
348
+
349
+ added_route_count = 0
294
350
  for router in (first_router, *other_routers):
295
351
  self.router.versioned_routers[header_value_as_dt].include_router(
296
352
  router,
297
353
  dependencies=[Depends(_get_api_version_dependency(self.router.api_version_header_name, header_value))],
298
354
  )
355
+ added_route_count += len(router.routes)
299
356
 
300
- self.enrich_swagger()
301
- return added_routes
357
+ added_routes.extend(versioned_router.routes[-added_route_count:])
358
+ self.router.routes.extend(added_routes)
302
359
 
303
- @same_definition_as_in(FastAPI.include_router)
304
- def include_router(self, *args: Any, **kwargs: Any):
305
- route = super().include_router(*args, **kwargs)
306
- self.enrich_swagger()
307
- return route
308
-
309
- @same_definition_as_in(FastAPI.post)
310
- def post(self, *args: Any, **kwargs: Any):
311
- route = super().post(*args, **kwargs)
312
- self.enrich_swagger()
313
- return route
314
-
315
- @same_definition_as_in(FastAPI.get)
316
- def get(self, *args: Any, **kwargs: Any):
317
- route = super().get(*args, **kwargs)
318
- self.enrich_swagger()
319
- return route
320
-
321
- @same_definition_as_in(FastAPI.patch)
322
- def patch(self, *args: Any, **kwargs: Any):
323
- route = super().patch(*args, **kwargs)
324
- self.enrich_swagger()
325
- return route
326
-
327
- @same_definition_as_in(FastAPI.delete)
328
- def delete(self, *args: Any, **kwargs: Any):
329
- route = super().delete(*args, **kwargs)
330
- self.enrich_swagger()
331
- return route
332
-
333
- @same_definition_as_in(FastAPI.put)
334
- def put(self, *args: Any, **kwargs: Any):
335
- route = super().put(*args, **kwargs)
336
- self.enrich_swagger()
337
- return route
338
-
339
- @same_definition_as_in(FastAPI.trace)
340
- def trace(self, *args: Any, **kwargs: Any): # pragma: no cover
341
- route = super().trace(*args, **kwargs)
342
- self.enrich_swagger()
343
- return route
344
-
345
- @same_definition_as_in(FastAPI.options)
346
- def options(self, *args: Any, **kwargs: Any):
347
- route = super().options(*args, **kwargs)
348
- self.enrich_swagger()
349
- return route
350
-
351
- @same_definition_as_in(FastAPI.head)
352
- def head(self, *args: Any, **kwargs: Any):
353
- route = super().head(*args, **kwargs)
354
- self.enrich_swagger()
355
- return route
356
-
357
- @same_definition_as_in(FastAPI.add_api_route)
358
- def add_api_route(self, *args: Any, **kwargs: Any):
359
- route = super().add_api_route(*args, **kwargs)
360
- self.enrich_swagger()
361
- return route
362
-
363
- @same_definition_as_in(FastAPI.api_route)
364
- def api_route(self, *args: Any, **kwargs: Any):
365
- route = super().api_route(*args, **kwargs)
366
- self.enrich_swagger()
367
- return route
368
-
369
- @same_definition_as_in(FastAPI.add_api_websocket_route)
370
- def add_api_websocket_route(self, *args: Any, **kwargs: Any): # pragma: no cover
371
- route = super().add_api_websocket_route(*args, **kwargs)
372
- self.enrich_swagger()
373
- return route
374
-
375
- @same_definition_as_in(FastAPI.websocket)
376
- def websocket(self, *args: Any, **kwargs: Any): # pragma: no cover
377
- route = super().websocket(*args, **kwargs)
378
- self.enrich_swagger()
379
- return route
360
+ return added_routes
380
361
 
362
+ @deprecated("Use builtin FastAPI methods such as include_router instead")
381
363
  def add_unversioned_routers(self, *routers: APIRouter):
382
364
  for router in routers:
383
- self.router.include_router(router)
384
- self.enrich_swagger()
365
+ self.include_router(router)
385
366
 
386
- @deprecated("Use add add_unversioned_routers instead")
367
+ @deprecated("Use builtin FastAPI methods such as add_api_route instead")
387
368
  def add_unversioned_routes(self, *routes: Route):
388
369
  router = APIRouter(routes=list(routes))
389
370
  self.include_router(router)
371
+
372
+ @deprecated("It no longer does anything")
373
+ def enrich_swagger(self): ...
cadwyn/codegen/_common.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import ast
2
2
  import dataclasses
3
3
  import inspect
4
+ import textwrap
4
5
  from dataclasses import dataclass
5
6
  from enum import Enum
6
7
  from functools import cache
@@ -89,7 +90,7 @@ def get_fields_and_validators_from_model(
89
90
  {},
90
91
  )
91
92
  else:
92
- cls_ast = cast(ast.ClassDef, ast.parse(source).body[0])
93
+ cls_ast = cast(ast.ClassDef, ast.parse(textwrap.dedent(source)).body[0])
93
94
  validators: dict[str, _ValidatorWrapper] = {}
94
95
 
95
96
  validators_and_nones = (
cadwyn/codegen/_main.py CHANGED
@@ -53,8 +53,7 @@ def generate_code_for_versioned_packages(
53
53
  codegen_plugins: Sequence[CodegenPlugin] = DEFAULT_CODEGEN_PLUGINS,
54
54
  migration_plugins: Sequence[MigrationPlugin] = DEFAULT_CODEGEN_MIGRATION_PLUGINS,
55
55
  extra_context: dict[str, Any] | None = None,
56
- ):
57
- ...
56
+ ): ...
58
57
 
59
58
 
60
59
  @overload
@@ -70,8 +69,7 @@ def generate_code_for_versioned_packages(
70
69
  codegen_plugins: Sequence[CodegenPlugin] = DEFAULT_CODEGEN_PLUGINS,
71
70
  migration_plugins: Sequence[MigrationPlugin] = DEFAULT_CODEGEN_MIGRATION_PLUGINS,
72
71
  extra_context: dict[str, Any] | None = None,
73
- ):
74
- ...
72
+ ): ...
75
73
 
76
74
 
77
75
  def generate_code_for_versioned_packages(
@@ -168,7 +166,7 @@ def _generate_directory_for_version(
168
166
  version_dir,
169
167
  ):
170
168
  file_source = read_python_module(template_module)
171
- parsed_file = ast.parse(file_source)
169
+ parsed_file = ast_comments.parse(file_source)
172
170
  context = _build_context(global_context, template_dir, version_dir, template_module, parallel_file, parsed_file)
173
171
 
174
172
  parsed_file = _apply_module_level_plugins(plugins, parsed_file, context)
@@ -178,9 +176,9 @@ def _generate_directory_for_version(
178
176
 
179
177
  def _apply_module_level_plugins(
180
178
  plugins: Collection[CodegenPlugin],
181
- parsed_file: ast.Module,
179
+ parsed_file: ast_comments.Module,
182
180
  context: CodegenContext,
183
- ) -> ast.Module:
181
+ ) -> ast_comments.Module:
184
182
  node_type = type(parsed_file)
185
183
  for plugin in plugins:
186
184
  if issubclass(node_type, plugin.node_type):
@@ -190,9 +188,9 @@ def _apply_module_level_plugins(
190
188
 
191
189
  def _apply_per_node_plugins(
192
190
  plugins: Collection[CodegenPlugin],
193
- parsed_file: ast.Module,
191
+ parsed_file: ast_comments.Module,
194
192
  context: CodegenContext,
195
- ) -> ast.Module:
193
+ ) -> ast_comments.Module:
196
194
  new_body = []
197
195
 
198
196
  for node in parsed_file.body:
@@ -202,7 +200,7 @@ def _apply_per_node_plugins(
202
200
  node = plugin(node, context) # noqa: PLW2901
203
201
  new_body.append(node)
204
202
 
205
- return ast.Module(body=new_body, type_ignores=[])
203
+ return ast_comments.Module(body=new_body, type_ignores=[])
206
204
 
207
205
 
208
206
  def _build_context(
@@ -211,7 +209,7 @@ def _build_context(
211
209
  version_dir: Path,
212
210
  template_module: ModuleType,
213
211
  parallel_file: Path,
214
- parsed_file: ast.Module,
212
+ parsed_file: ast_comments.Module,
215
213
  ):
216
214
  if template_module.__name__.endswith(".__init__"):
217
215
  module_python_path = template_module.__name__.removesuffix(".__init__")
@@ -80,9 +80,9 @@ def _modify_schema_cls(
80
80
  cls_node.name = model_info.name
81
81
 
82
82
  field_definitions = [
83
- ast.AnnAssign(
83
+ ast.AnnAssign( # pyright: ignore[reportCallIssue]
84
84
  target=ast.Name(name, ctx=ast.Store()),
85
- annotation=copy.deepcopy(field.annotation_ast),
85
+ annotation=copy.deepcopy(field.annotation_ast), # pyright: ignore[reportArgumentType]
86
86
  # We do this because next plugins **might** use a transformer which will edit the ast within the field
87
87
  # and break rendering
88
88
  value=copy.deepcopy(field.value_ast),
cadwyn/exceptions.py CHANGED
@@ -46,6 +46,18 @@ class RouterPathParamsModifiedError(RouterGenerationError):
46
46
  pass
47
47
 
48
48
 
49
+ class RouteResponseBySchemaConverterDoesNotApplyToAnythingError(RouterGenerationError):
50
+ pass
51
+
52
+
53
+ class RouteRequestBySchemaConverterDoesNotApplyToAnythingError(RouterGenerationError):
54
+ pass
55
+
56
+
57
+ class RouteByPathConverterDoesNotApplyToAnythingError(RouterGenerationError):
58
+ pass
59
+
60
+
49
61
  class RouteAlreadyExistsError(RouterGenerationError):
50
62
  def __init__(self, *routes: APIRoute):
51
63
  self.routes = routes
cadwyn/middleware.py CHANGED
@@ -64,11 +64,11 @@ class HeaderVersioningMiddleware(BaseHTTPMiddleware):
64
64
  request=request,
65
65
  dependant=self.version_header_validation_dependant,
66
66
  async_exit_stack=async_exit_stack,
67
+ embed_body_fields=False,
67
68
  )
68
- values, errors, *_ = solved_result
69
- if errors:
70
- return self.default_response_class(status_code=422, content=_normalize_errors(errors))
71
- api_version = cast(date, values[self.api_version_header_name.replace("-", "_")])
69
+ if solved_result.errors:
70
+ return self.default_response_class(status_code=422, content=_normalize_errors(solved_result.errors))
71
+ api_version = cast(date, solved_result.values[self.api_version_header_name.replace("-", "_")])
72
72
  self.api_version_var.set(api_version)
73
73
 
74
74
  response = await call_next(request)