cadwyn 3.11.1__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 +8 -2
- cadwyn/__main__.py +2 -3
- cadwyn/_asts.py +5 -5
- cadwyn/_compat.py +9 -2
- cadwyn/applications.py +152 -83
- cadwyn/codegen/_common.py +2 -1
- cadwyn/codegen/_main.py +9 -11
- cadwyn/codegen/_plugins/class_rebuilding.py +2 -2
- cadwyn/exceptions.py +12 -0
- cadwyn/middleware.py +4 -4
- cadwyn/route_generation.py +215 -106
- cadwyn/routing.py +49 -26
- cadwyn/structure/data.py +17 -17
- cadwyn/structure/endpoints.py +7 -3
- cadwyn/structure/modules.py +2 -1
- cadwyn/structure/versions.py +64 -43
- {cadwyn-3.11.1.dist-info → cadwyn-3.15.10.dist-info}/METADATA +5 -4
- cadwyn-3.15.10.dist-info/RECORD +38 -0
- {cadwyn-3.11.1.dist-info → cadwyn-3.15.10.dist-info}/WHEEL +1 -1
- cadwyn-3.11.1.dist-info/RECORD +0 -38
- {cadwyn-3.11.1.dist-info → cadwyn-3.15.10.dist-info}/LICENSE +0 -0
- {cadwyn-3.11.1.dist-info → cadwyn-3.15.10.dist-info}/entry_points.txt +0 -0
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
|
|
6
|
-
|
|
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(_:
|
|
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:
|
|
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]
|
|
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):
|
|
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
|
|
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:
|
|
57
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
166
|
+
unversioned_router = APIRouter(**self._kwargs_to_router)
|
|
140
167
|
self._add_openapi_endpoints(unversioned_router)
|
|
141
|
-
self.
|
|
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
|
|
191
|
-
""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
openapi = get_openapi(
|
|
265
|
+
return JSONResponse(
|
|
266
|
+
get_openapi(
|
|
215
267
|
title=self.title,
|
|
216
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
+
version = req.query_params.get("version")
|
|
242
285
|
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
369
|
+
router = APIRouter(routes=list(routes))
|
|
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 =
|
|
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:
|
|
179
|
+
parsed_file: ast_comments.Module,
|
|
182
180
|
context: CodegenContext,
|
|
183
|
-
) ->
|
|
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:
|
|
191
|
+
parsed_file: ast_comments.Module,
|
|
194
192
|
context: CodegenContext,
|
|
195
|
-
) ->
|
|
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
|
|
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:
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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)
|