cadwyn 3.11.1__tar.gz → 3.15.10__tar.gz

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.

Files changed (38) hide show
  1. {cadwyn-3.11.1 → cadwyn-3.15.10}/PKG-INFO +5 -4
  2. {cadwyn-3.11.1 → cadwyn-3.15.10}/README.md +2 -2
  3. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/__init__.py +8 -2
  4. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/__main__.py +2 -3
  5. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/_asts.py +5 -5
  6. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/_compat.py +9 -2
  7. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/applications.py +152 -83
  8. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/codegen/_common.py +2 -1
  9. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/codegen/_main.py +9 -11
  10. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/class_rebuilding.py +2 -2
  11. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/exceptions.py +12 -0
  12. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/middleware.py +4 -4
  13. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/route_generation.py +215 -106
  14. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/routing.py +49 -26
  15. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/structure/data.py +17 -17
  16. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/structure/endpoints.py +7 -3
  17. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/structure/modules.py +2 -1
  18. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/structure/versions.py +64 -43
  19. cadwyn-3.15.10/pyproject.toml +142 -0
  20. cadwyn-3.11.1/pyproject.toml +0 -246
  21. {cadwyn-3.11.1 → cadwyn-3.15.10}/LICENSE +0 -0
  22. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/_package_utils.py +0 -0
  23. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/_utils.py +0 -0
  24. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/codegen/README.md +0 -0
  25. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/codegen/__init__.py +0 -0
  26. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/__init__.py +0 -0
  27. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/class_migrations.py +0 -0
  28. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/class_renaming.py +0 -0
  29. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/import_auto_adding.py +0 -0
  30. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/module_migrations.py +0 -0
  31. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/main.py +0 -0
  32. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/py.typed +0 -0
  33. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/static/__init__.py +0 -0
  34. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/static/docs.html +0 -0
  35. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/structure/__init__.py +0 -0
  36. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/structure/common.py +0 -0
  37. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/structure/enums.py +0 -0
  38. {cadwyn-3.11.1 → cadwyn-3.15.10}/cadwyn/structure/schemas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cadwyn
3
- Version: 3.11.1
3
+ Version: 3.15.10
4
4
  Summary: Production-ready community-driven modern Stripe-like API versioning in FastAPI
5
5
  Home-page: https://github.com/zmievsa/cadwyn
6
6
  License: MIT
@@ -33,7 +33,8 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
33
33
  Classifier: Typing :: Typed
34
34
  Provides-Extra: cli
35
35
  Requires-Dist: better-ast-comments (>=1.2.1,<1.3.0)
36
- Requires-Dist: fastapi (>=0.110.0)
36
+ Requires-Dist: fastapi (>=0.115.2)
37
+ Requires-Dist: issubclass (>=0.1.2,<0.2.0)
37
38
  Requires-Dist: jinja2 (>=3.1.2)
38
39
  Requires-Dist: pydantic (>=1.0.0)
39
40
  Requires-Dist: starlette (>=0.36.3)
@@ -53,8 +54,8 @@ Production-ready community-driven modern [Stripe-like](https://stripe.com/blog/a
53
54
  <a href="https://github.com/zmievsa/cadwyn/actions/workflows/ci.yaml?branch=main&event=push" target="_blank">
54
55
  <img src="https://github.com/zmievsa/cadwyn/actions/workflows/ci.yaml/badge.svg?branch=main&event=push" alt="Test">
55
56
  </a>
56
- <a href="https://codecov.io/gh/ovsyanka83/cadwyn" target="_blank">
57
- <img src="https://img.shields.io/codecov/c/github/ovsyanka83/cadwyn?color=%2334D058&logo=codecov" alt="Coverage">
57
+ <a href="https://codecov.io/gh/zmievsa/cadwyn" target="_blank">
58
+ <img src="https://img.shields.io/codecov/c/github/zmievsa/cadwyn?color=%2334D058&logo=codecov" alt="Coverage">
58
59
  </a>
59
60
  <a href="https://pypi.org/project/cadwyn/" target="_blank">
60
61
  <img alt="PyPI" src="https://img.shields.io/pypi/v/cadwyn?color=%2334D058&logo=pypi&label=PyPI package" alt="Package version">
@@ -8,8 +8,8 @@ Production-ready community-driven modern [Stripe-like](https://stripe.com/blog/a
8
8
  <a href="https://github.com/zmievsa/cadwyn/actions/workflows/ci.yaml?branch=main&event=push" target="_blank">
9
9
  <img src="https://github.com/zmievsa/cadwyn/actions/workflows/ci.yaml/badge.svg?branch=main&event=push" alt="Test">
10
10
  </a>
11
- <a href="https://codecov.io/gh/ovsyanka83/cadwyn" target="_blank">
12
- <img src="https://img.shields.io/codecov/c/github/ovsyanka83/cadwyn?color=%2334D058&logo=codecov" alt="Coverage">
11
+ <a href="https://codecov.io/gh/zmievsa/cadwyn" target="_blank">
12
+ <img src="https://img.shields.io/codecov/c/github/zmievsa/cadwyn?color=%2334D058&logo=codecov" alt="Coverage">
13
13
  </a>
14
14
  <a href="https://pypi.org/project/cadwyn/" target="_blank">
15
15
  <img alt="PyPI" src="https://img.shields.io/pypi/v/cadwyn?color=%2334D058&logo=pypi&label=PyPI package" alt="Package version">
@@ -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
  ]
@@ -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__":
@@ -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]:
@@ -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
@@ -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
@@ -28,6 +35,11 @@ CURR_DIR = Path(__file__).resolve()
28
35
  logger = getLogger(__name__)
29
36
 
30
37
 
38
+ @dataclasses.dataclass(slots=True)
39
+ class FakeDependencyOverridesProvider:
40
+ dependency_overrides: dict[Callable[..., Any], Callable[..., Any]]
41
+
42
+
31
43
  class Cadwyn(FastAPI):
32
44
  _templates = Jinja2Templates(directory=CURR_DIR.parent / "static")
33
45
 
@@ -53,8 +65,13 @@ class Cadwyn(FastAPI):
53
65
  swagger_ui_oauth2_redirect_url: str | None = "/docs/oauth2-redirect",
54
66
  swagger_ui_init_oauth: dict[str, Any] | None = None,
55
67
  middleware: Sequence[Middleware] | None = None,
56
- exception_handlers: dict[int | type[Exception], Callable[[Request, Any], Coroutine[Any, Any, Response]]]
57
- | None = None,
68
+ exception_handlers: (
69
+ dict[
70
+ int | type[Exception],
71
+ Callable[[Request, Any], Coroutine[Any, Any, Response]],
72
+ ]
73
+ | None
74
+ ) = None,
58
75
  on_startup: Sequence[Callable[[], Any]] | None = None,
59
76
  on_shutdown: Sequence[Callable[[], Any]] | None = None,
60
77
  lifespan: Lifespan[Self] | None = None,
@@ -70,7 +87,9 @@ class Cadwyn(FastAPI):
70
87
  deprecated: bool | None = None,
71
88
  include_in_schema: bool = True,
72
89
  swagger_ui_parameters: dict[str, Any] | None = None,
73
- 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
+ ),
74
93
  separate_input_output_schemas: bool = True,
75
94
  **extra: Any,
76
95
  ) -> None:
@@ -79,6 +98,7 @@ class Cadwyn(FastAPI):
79
98
  latest_schemas_package = extra.pop("latest_schemas_package", None) or self.versions.head_schemas_package
80
99
  self.versions.head_schemas_package = latest_schemas_package
81
100
  self._latest_schemas_package = cast(ModuleType, latest_schemas_package)
101
+ self._dependency_overrides_provider = FakeDependencyOverridesProvider({})
82
102
 
83
103
  super().__init__(
84
104
  debug=debug,
@@ -117,28 +137,35 @@ class Cadwyn(FastAPI):
117
137
  separate_input_output_schemas=separate_input_output_schemas,
118
138
  **extra,
119
139
  )
140
+ self._kwargs_to_router: dict[str, Any] = {
141
+ "routes": routes,
142
+ "redirect_slashes": redirect_slashes,
143
+ "dependency_overrides_provider": self,
144
+ "on_startup": on_startup,
145
+ "on_shutdown": on_shutdown,
146
+ "lifespan": lifespan,
147
+ "default_response_class": default_response_class,
148
+ "dependencies": dependencies,
149
+ "callbacks": callbacks,
150
+ "deprecated": deprecated,
151
+ "include_in_schema": include_in_schema,
152
+ "responses": responses,
153
+ "generate_unique_id_function": generate_unique_id_function,
154
+ }
120
155
  self.router: _RootHeaderAPIRouter = _RootHeaderAPIRouter( # pyright: ignore[reportIncompatibleVariableOverride]
121
- routes=self.routes,
122
- on_startup=on_startup,
123
- on_shutdown=on_shutdown,
124
- default_response_class=default_response_class,
125
- dependencies=dependencies,
126
- callbacks=callbacks,
127
- deprecated=deprecated,
128
- responses=responses,
156
+ **self._kwargs_to_router,
129
157
  api_version_header_name=api_version_header_name,
130
158
  api_version_var=self.versions.api_version_var,
131
- lifespan=lifespan,
132
159
  )
160
+
133
161
  self.docs_url = docs_url
134
162
  self.redoc_url = redoc_url
135
163
  self.openapi_url = openapi_url
136
164
  self.redoc_url = redoc_url
137
- self.swaggers = {}
138
165
 
139
- unversioned_router = APIRouter(routes=routes)
166
+ unversioned_router = APIRouter(**self._kwargs_to_router)
140
167
  self._add_openapi_endpoints(unversioned_router)
141
- self.add_unversioned_routers(unversioned_router)
168
+ self.include_router(unversioned_router)
142
169
  self.add_middleware(
143
170
  HeaderVersioningMiddleware,
144
171
  api_version_header_name=self.router.api_version_header_name,
@@ -146,6 +173,20 @@ class Cadwyn(FastAPI):
146
173
  default_response_class=default_response_class,
147
174
  )
148
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
+
149
190
  @property # pragma: no cover
150
191
  @deprecated("It is going to be deleted in the future. Use VersionBundle.head_schemas_package instead")
151
192
  def latest_schemas_package(self):
@@ -169,6 +210,18 @@ class Cadwyn(FastAPI):
169
210
  endpoint=self.swagger_dashboard,
170
211
  include_in_schema=False,
171
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
+ )
172
225
  if self.redoc_url is not None:
173
226
  unversioned_router.add_route(
174
227
  path=self.redoc_url,
@@ -177,7 +230,7 @@ class Cadwyn(FastAPI):
177
230
  )
178
231
 
179
232
  def generate_and_include_versioned_routers(self, *routers: APIRouter) -> None:
180
- root_router = APIRouter()
233
+ root_router = APIRouter(dependency_overrides_provider=self._dependency_overrides_provider)
181
234
  for router in routers:
182
235
  root_router.include_router(router)
183
236
  router_versions = generate_versioned_routers(
@@ -187,33 +240,32 @@ class Cadwyn(FastAPI):
187
240
  for version, router in router_versions.items():
188
241
  self.add_header_versioned_routers(router, header_value=version.isoformat())
189
242
 
190
- def enrich_swagger(self):
191
- """
192
- This method goes through all header-based apps and collect a dict[openapi_version, openapi_json]
193
-
194
- For each route a `X-API-VERSION` header with value is added
195
-
196
- """
197
- unversioned_routes_openapi = get_openapi(
198
- title=self.title,
199
- version=self.version,
200
- openapi_version=self.openapi_version,
201
- description=self.description,
202
- terms_of_service=self.terms_of_service,
203
- contact=self.contact,
204
- license_info=self.license_info,
205
- routes=self.router.unversioned_routes,
206
- tags=self.openapi_tags,
207
- 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",
208
248
  )
209
- if unversioned_routes_openapi["paths"]:
210
- self.swaggers["unversioned"] = unversioned_routes_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
211
264
 
212
- for header_value, routes in self.router.versioned_routes.items():
213
- header_value_str = header_value.isoformat()
214
- openapi = get_openapi(
265
+ return JSONResponse(
266
+ get_openapi(
215
267
  title=self.title,
216
- version=self.version,
268
+ version=formatted_version,
217
269
  openapi_version=self.openapi_version,
218
270
  description=self.description,
219
271
  terms_of_service=self.terms_of_service,
@@ -223,41 +275,50 @@ class Cadwyn(FastAPI):
223
275
  tags=self.openapi_tags,
224
276
  servers=self.servers,
225
277
  )
226
- # in current implementation we expect header_value to be a date
227
- self.swaggers[header_value_str] = openapi
228
-
229
- async def openapi_jsons(self, req: Request) -> JSONResponse:
230
- version = req.query_params.get("version") or req.headers.get(self.router.api_version_header_name)
231
- openapi_of_a_version = self.swaggers.get(version)
232
- if not openapi_of_a_version:
233
- raise HTTPException(
234
- status_code=404,
235
- detail=f"OpenApi file of with version `{version}` not found",
236
- )
278
+ )
237
279
 
238
- 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)
239
282
 
240
283
  async def swagger_dashboard(self, req: Request) -> Response:
241
- 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")
242
285
 
243
- async def redoc_dashboard(self, req: Request) -> Response:
244
- 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))
245
300
 
246
- def _render_docs_dashboard_or_concrete_verssion(self, render_docs: Callable[..., Any], req: Request, docs_url: str):
247
- base_url = str(req.base_url).rstrip("/")
301
+ async def redoc_dashboard(self, req: Request) -> Response:
248
302
  version = req.query_params.get("version")
249
303
 
250
304
  if version:
251
- return render_docs(
252
- openapi_url=f"{self.openapi_url}?version={version}",
253
- title="Swagger UI",
254
- )
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"}
255
319
  return self._templates.TemplateResponse(
256
320
  "docs.html",
257
- {
258
- "request": req,
259
- "table": {version: f"{base_url}{docs_url}?version={version}" for version in sorted(self.swaggers)},
260
- },
321
+ {"request": req, "table": table},
261
322
  )
262
323
 
263
324
  def add_header_versioned_routers(
@@ -272,33 +333,41 @@ class Cadwyn(FastAPI):
272
333
  except ValueError as e:
273
334
  raise ValueError("header_value should be in ISO 8601 format") from e
274
335
 
275
- if header_value_as_dt not in self.router.versioned_routes: # pragma: no branch
276
- self.router.versioned_routes[header_value_as_dt] = []
277
- if self.openapi_url is not None: # pragma: no branch
278
- self.router.versioned_routes[header_value_as_dt].append(
279
- Route(path=self.openapi_url, endpoint=self.openapi_jsons, include_in_schema=False)
280
- )
281
-
282
336
  added_routes: list[BaseRoute] = []
283
- for router in (first_router, *other_routers):
284
- added_route_count = len(router.routes)
337
+ if header_value_as_dt not in self.router.versioned_routers: # pragma: no branch
338
+ self.router.versioned_routers[header_value_as_dt] = APIRouter(**self._kwargs_to_router)
285
339
 
286
- self.include_router(
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
350
+ for router in (first_router, *other_routers):
351
+ self.router.versioned_routers[header_value_as_dt].include_router(
287
352
  router,
288
353
  dependencies=[Depends(_get_api_version_dependency(self.router.api_version_header_name, header_value))],
289
354
  )
290
- added_routes.extend(self.routes[len(self.routes) - added_route_count :])
291
- for route in added_routes:
292
- self.router.versioned_routes[header_value_as_dt].append(route)
355
+ added_route_count += len(router.routes)
356
+
357
+ added_routes.extend(versioned_router.routes[-added_route_count:])
358
+ self.router.routes.extend(added_routes)
293
359
 
294
- self.enrich_swagger()
295
360
  return added_routes
296
361
 
362
+ @deprecated("Use builtin FastAPI methods such as include_router instead")
297
363
  def add_unversioned_routers(self, *routers: APIRouter):
298
364
  for router in routers:
299
365
  self.include_router(router)
300
- self.router.unversioned_routes.extend(router.routes)
301
- self.enrich_swagger()
302
366
 
367
+ @deprecated("Use builtin FastAPI methods such as add_api_route instead")
303
368
  def add_unversioned_routes(self, *routes: Route):
304
- self.router.unversioned_routes.extend(routes)
369
+ router = APIRouter(routes=list(routes))
370
+ self.include_router(router)
371
+
372
+ @deprecated("It no longer does anything")
373
+ def enrich_swagger(self): ...
@@ -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 = (
@@ -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),
@@ -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
@@ -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)