cadwyn 3.12.0__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.
- {cadwyn-3.12.0 → cadwyn-3.15.10}/PKG-INFO +5 -4
- {cadwyn-3.12.0 → cadwyn-3.15.10}/README.md +2 -2
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/__init__.py +8 -2
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/__main__.py +2 -3
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/_asts.py +5 -5
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/_compat.py +9 -2
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/applications.py +132 -148
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/codegen/_common.py +2 -1
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/codegen/_main.py +9 -11
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/class_rebuilding.py +2 -2
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/exceptions.py +12 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/middleware.py +4 -4
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/route_generation.py +215 -106
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/routing.py +38 -7
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/structure/data.py +17 -17
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/structure/endpoints.py +7 -3
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/structure/modules.py +2 -1
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/structure/versions.py +64 -43
- cadwyn-3.15.10/pyproject.toml +142 -0
- cadwyn-3.12.0/pyproject.toml +0 -247
- {cadwyn-3.12.0 → cadwyn-3.15.10}/LICENSE +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/_package_utils.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/_utils.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/codegen/README.md +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/codegen/__init__.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/__init__.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/class_migrations.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/class_renaming.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/import_auto_adding.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/codegen/_plugins/module_migrations.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/main.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/py.typed +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/static/__init__.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/static/docs.html +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/structure/__init__.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/structure/common.py +0 -0
- {cadwyn-3.12.0 → cadwyn-3.15.10}/cadwyn/structure/enums.py +0 -0
- {cadwyn-3.12.0 → 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.
|
|
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.
|
|
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/
|
|
57
|
-
<img src="https://img.shields.io/codecov/c/github/
|
|
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/
|
|
12
|
-
<img src="https://img.shields.io/codecov/c/github/
|
|
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
|
|
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
|
]
|
|
@@ -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(_:
|
|
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]:
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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:
|
|
58
|
-
|
|
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(
|
|
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.
|
|
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
|
|
200
|
-
""
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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=
|
|
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=
|
|
274
|
+
routes=routes,
|
|
232
275
|
tags=self.openapi_tags,
|
|
233
276
|
servers=self.servers,
|
|
234
277
|
)
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
+
version = req.query_params.get("version")
|
|
251
285
|
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
357
|
+
added_routes.extend(versioned_router.routes[-added_route_count:])
|
|
358
|
+
self.router.routes.extend(added_routes)
|
|
302
359
|
|
|
303
|
-
|
|
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.
|
|
384
|
-
self.enrich_swagger()
|
|
365
|
+
self.include_router(router)
|
|
385
366
|
|
|
386
|
-
@deprecated("Use
|
|
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): ...
|
|
@@ -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 =
|
|
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),
|