cadwyn 3.15.9__py3-none-any.whl → 4.0.0__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 +25 -8
- cadwyn/__main__.py +46 -90
- cadwyn/_asts.py +9 -133
- cadwyn/_importer.py +31 -0
- cadwyn/_render.py +152 -0
- cadwyn/_utils.py +7 -107
- cadwyn/applications.py +5 -34
- cadwyn/exceptions.py +11 -3
- cadwyn/middleware.py +4 -4
- cadwyn/route_generation.py +22 -450
- cadwyn/routing.py +2 -5
- cadwyn/schema_generation.py +946 -0
- cadwyn/structure/__init__.py +0 -2
- cadwyn/structure/schemas.py +50 -49
- cadwyn/structure/versions.py +24 -137
- {cadwyn-3.15.9.dist-info → cadwyn-4.0.0.dist-info}/METADATA +4 -5
- cadwyn-4.0.0.dist-info/RECORD +27 -0
- cadwyn/_compat.py +0 -151
- cadwyn/_package_utils.py +0 -45
- cadwyn/codegen/README.md +0 -10
- cadwyn/codegen/__init__.py +0 -10
- cadwyn/codegen/_common.py +0 -168
- cadwyn/codegen/_main.py +0 -279
- cadwyn/codegen/_plugins/__init__.py +0 -0
- cadwyn/codegen/_plugins/class_migrations.py +0 -423
- cadwyn/codegen/_plugins/class_rebuilding.py +0 -109
- cadwyn/codegen/_plugins/class_renaming.py +0 -49
- cadwyn/codegen/_plugins/import_auto_adding.py +0 -64
- cadwyn/codegen/_plugins/module_migrations.py +0 -15
- cadwyn/main.py +0 -11
- cadwyn/structure/modules.py +0 -39
- cadwyn-3.15.9.dist-info/RECORD +0 -38
- {cadwyn-3.15.9.dist-info → cadwyn-4.0.0.dist-info}/LICENSE +0 -0
- {cadwyn-3.15.9.dist-info → cadwyn-4.0.0.dist-info}/WHEEL +0 -0
- {cadwyn-3.15.9.dist-info → cadwyn-4.0.0.dist-info}/entry_points.txt +0 -0
cadwyn/route_generation.py
CHANGED
|
@@ -1,28 +1,14 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
import inspect
|
|
3
1
|
import re
|
|
4
|
-
import types
|
|
5
|
-
import typing
|
|
6
|
-
import warnings
|
|
7
2
|
from collections import defaultdict
|
|
8
3
|
from collections.abc import Callable, Sequence
|
|
9
4
|
from copy import deepcopy
|
|
10
5
|
from dataclasses import dataclass
|
|
11
|
-
from enum import Enum
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from types import GenericAlias, MappingProxyType, ModuleType
|
|
14
6
|
from typing import (
|
|
15
7
|
TYPE_CHECKING,
|
|
16
|
-
Annotated,
|
|
17
8
|
Any,
|
|
18
9
|
Generic,
|
|
19
10
|
TypeVar,
|
|
20
|
-
_BaseGenericAlias, # pyright: ignore[reportAttributeAccessIssue]
|
|
21
11
|
cast,
|
|
22
|
-
final,
|
|
23
|
-
get_args,
|
|
24
|
-
get_origin,
|
|
25
|
-
overload,
|
|
26
12
|
)
|
|
27
13
|
|
|
28
14
|
import fastapi.params
|
|
@@ -30,18 +16,13 @@ import fastapi.routing
|
|
|
30
16
|
import fastapi.security.base
|
|
31
17
|
import fastapi.utils
|
|
32
18
|
from fastapi import APIRouter
|
|
33
|
-
from fastapi._compat import ModelField as FastAPIModelField
|
|
34
|
-
from fastapi._compat import create_body_model
|
|
35
|
-
from fastapi.params import Depends
|
|
36
19
|
from fastapi.routing import APIRoute
|
|
37
20
|
from issubclass import issubclass as lenient_issubclass
|
|
38
21
|
from pydantic import BaseModel
|
|
39
22
|
from starlette.routing import BaseRoute
|
|
40
|
-
from typing_extensions import
|
|
23
|
+
from typing_extensions import assert_never
|
|
41
24
|
|
|
42
|
-
from cadwyn.
|
|
43
|
-
from cadwyn._package_utils import get_version_dir_path
|
|
44
|
-
from cadwyn._utils import Sentinel, UnionType, get_another_version_of_cls
|
|
25
|
+
from cadwyn._utils import Sentinel
|
|
45
26
|
from cadwyn.exceptions import (
|
|
46
27
|
CadwynError,
|
|
47
28
|
RouteAlreadyExistsError,
|
|
@@ -51,6 +32,10 @@ from cadwyn.exceptions import (
|
|
|
51
32
|
RouterGenerationError,
|
|
52
33
|
RouterPathParamsModifiedError,
|
|
53
34
|
)
|
|
35
|
+
from cadwyn.schema_generation import (
|
|
36
|
+
_add_request_and_response_params,
|
|
37
|
+
_generate_versioned_models,
|
|
38
|
+
)
|
|
54
39
|
from cadwyn.structure import Version, VersionBundle
|
|
55
40
|
from cadwyn.structure.common import Endpoint, VersionDate
|
|
56
41
|
from cadwyn.structure.endpoints import (
|
|
@@ -58,7 +43,9 @@ from cadwyn.structure.endpoints import (
|
|
|
58
43
|
EndpointExistedInstruction,
|
|
59
44
|
EndpointHadInstruction,
|
|
60
45
|
)
|
|
61
|
-
from cadwyn.structure.versions import
|
|
46
|
+
from cadwyn.structure.versions import (
|
|
47
|
+
VersionChange,
|
|
48
|
+
)
|
|
62
49
|
|
|
63
50
|
if TYPE_CHECKING:
|
|
64
51
|
from fastapi.dependencies.models import Dependant
|
|
@@ -75,45 +62,8 @@ class _EndpointInfo:
|
|
|
75
62
|
endpoint_methods: frozenset[str]
|
|
76
63
|
|
|
77
64
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def __class_getitem__(cls, original_schema: type, /) -> type[Self]:
|
|
81
|
-
return cast(Any, type("InternalRepresentationOf", (cls, original_schema), {}))
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@overload
|
|
85
|
-
def generate_versioned_routers(
|
|
86
|
-
router: _R,
|
|
87
|
-
versions: VersionBundle,
|
|
88
|
-
) -> dict[VersionDate, _R]: ...
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
@overload
|
|
92
|
-
@deprecated("Do not use the latest_schemas_package argument. Put head_schemas_package into your VersionBundle instead")
|
|
93
|
-
def generate_versioned_routers(
|
|
94
|
-
router: _R,
|
|
95
|
-
versions: VersionBundle,
|
|
96
|
-
latest_schemas_package: ModuleType | None,
|
|
97
|
-
) -> dict[VersionDate, _R]: ...
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def generate_versioned_routers(
|
|
101
|
-
router: _R,
|
|
102
|
-
versions: VersionBundle,
|
|
103
|
-
latest_schemas_package: ModuleType | None = None,
|
|
104
|
-
) -> dict[VersionDate, _R]:
|
|
105
|
-
if versions.head_schemas_package is not None:
|
|
106
|
-
head_schemas_package = versions.head_schemas_package
|
|
107
|
-
elif latest_schemas_package is not None: # pragma: no cover
|
|
108
|
-
head_schemas_package = latest_schemas_package
|
|
109
|
-
else: # pragma: no cover
|
|
110
|
-
raise TypeError(
|
|
111
|
-
"TypeError: generate_versioned_routers() must be called with a VersionBundle "
|
|
112
|
-
"that contains a non-null head_schemas_package."
|
|
113
|
-
)
|
|
114
|
-
versions.head_schemas_package = head_schemas_package
|
|
115
|
-
versions._validate_head_schemas_package_structure()
|
|
116
|
-
return _EndpointTransformer(router, versions, head_schemas_package).transform()
|
|
65
|
+
def generate_versioned_routers(router: _R, versions: VersionBundle) -> dict[VersionDate, _R]:
|
|
66
|
+
return _EndpointTransformer(router, versions).transform()
|
|
117
67
|
|
|
118
68
|
|
|
119
69
|
class VersionedAPIRouter(fastapi.routing.APIRouter):
|
|
@@ -131,30 +81,22 @@ class VersionedAPIRouter(fastapi.routing.APIRouter):
|
|
|
131
81
|
|
|
132
82
|
|
|
133
83
|
class _EndpointTransformer(Generic[_R]):
|
|
134
|
-
def __init__(
|
|
135
|
-
self,
|
|
136
|
-
parent_router: _R,
|
|
137
|
-
versions: VersionBundle,
|
|
138
|
-
head_schemas_package: ModuleType,
|
|
139
|
-
) -> None:
|
|
84
|
+
def __init__(self, parent_router: _R, versions: VersionBundle) -> None:
|
|
140
85
|
super().__init__()
|
|
141
86
|
self.parent_router = parent_router
|
|
142
87
|
self.versions = versions
|
|
143
|
-
self.
|
|
88
|
+
self.schema_generators = _generate_versioned_models(versions)
|
|
144
89
|
|
|
145
90
|
self.routes_that_never_existed = [
|
|
146
91
|
route for route in parent_router.routes if isinstance(route, APIRoute) and _DELETED_ROUTE_TAG in route.tags
|
|
147
92
|
]
|
|
148
93
|
|
|
149
94
|
def transform(self) -> dict[VersionDate, _R]:
|
|
150
|
-
schema_to_internal_request_body_representation = _extract_internal_request_schemas_from_router(
|
|
151
|
-
self.parent_router
|
|
152
|
-
)
|
|
153
95
|
router = deepcopy(self.parent_router)
|
|
154
96
|
routers: dict[VersionDate, _R] = {}
|
|
155
97
|
|
|
156
98
|
for version in self.versions:
|
|
157
|
-
self.annotation_transformer.migrate_router_to_version(router
|
|
99
|
+
self.schema_generators[str(version.value)].annotation_transformer.migrate_router_to_version(router)
|
|
158
100
|
|
|
159
101
|
self._validate_all_data_converters_are_applied(router, version)
|
|
160
102
|
|
|
@@ -178,12 +120,6 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
178
120
|
_add_request_and_response_params(head_route)
|
|
179
121
|
copy_of_dependant = deepcopy(head_route.dependant)
|
|
180
122
|
|
|
181
|
-
if _route_has_a_simple_body_schema(head_route):
|
|
182
|
-
self._replace_internal_representation_with_the_versioned_schema(
|
|
183
|
-
copy_of_dependant,
|
|
184
|
-
schema_to_internal_request_body_representation,
|
|
185
|
-
)
|
|
186
|
-
|
|
187
123
|
for older_router in list(routers.values()):
|
|
188
124
|
older_route = older_router.routes[route_index]
|
|
189
125
|
|
|
@@ -192,10 +128,10 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
192
128
|
older_route = cast(APIRoute, older_route)
|
|
193
129
|
# Wait.. Why do we need this code again?
|
|
194
130
|
if older_route.body_field is not None and _route_has_a_simple_body_schema(older_route):
|
|
195
|
-
|
|
196
|
-
older_route.body_field.type_
|
|
197
|
-
|
|
198
|
-
|
|
131
|
+
if hasattr(older_route.body_field.type_, "__cadwyn_original_model__"):
|
|
132
|
+
template_older_body_model = older_route.body_field.type_.__cadwyn_original_model__
|
|
133
|
+
else:
|
|
134
|
+
template_older_body_model = older_route.body_field.type_
|
|
199
135
|
else:
|
|
200
136
|
template_older_body_model = None
|
|
201
137
|
_add_data_migrations_to_route(
|
|
@@ -274,44 +210,19 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
274
210
|
for route in router.routes:
|
|
275
211
|
if isinstance(route, APIRoute):
|
|
276
212
|
if route.response_model is not None and lenient_issubclass(route.response_model, BaseModel):
|
|
277
|
-
# FIXME: This is going to fail on Pydantic 1
|
|
278
213
|
response_models.add(route.response_model)
|
|
279
214
|
# Not sure if it can ever be None when it's a simple schema. Eh, I would rather be safe than sorry
|
|
280
215
|
if _route_has_a_simple_body_schema(route) and route.body_field is not None:
|
|
281
|
-
annotation =
|
|
282
|
-
if lenient_issubclass(annotation, BaseModel):
|
|
283
|
-
# FIXME: This is going to fail on Pydantic 1
|
|
216
|
+
annotation = route.body_field.field_info.annotation
|
|
217
|
+
if annotation is not None and lenient_issubclass(annotation, BaseModel):
|
|
284
218
|
request_bodies.add(annotation)
|
|
285
219
|
path_to_route_methods_mapping[route.path] |= route.methods
|
|
286
220
|
|
|
287
|
-
head_response_models = {
|
|
288
|
-
|
|
289
|
-
model,
|
|
290
|
-
self.versions.versioned_directories_with_head[0],
|
|
291
|
-
)
|
|
292
|
-
for model in response_models
|
|
293
|
-
}
|
|
294
|
-
head_request_bodies = {
|
|
295
|
-
self.annotation_transformer._change_version_of_annotations(
|
|
296
|
-
body,
|
|
297
|
-
self.versions.versioned_directories_with_head[0],
|
|
298
|
-
)
|
|
299
|
-
for body in request_bodies
|
|
300
|
-
}
|
|
221
|
+
head_response_models = {model.__cadwyn_original_model__ for model in response_models}
|
|
222
|
+
head_request_bodies = {getattr(body, "__cadwyn_original_model__", body) for body in request_bodies}
|
|
301
223
|
|
|
302
224
|
return path_to_route_methods_mapping, head_response_models, head_request_bodies
|
|
303
225
|
|
|
304
|
-
def _replace_internal_representation_with_the_versioned_schema(
|
|
305
|
-
self,
|
|
306
|
-
copy_of_dependant: "Dependant",
|
|
307
|
-
schema_to_internal_request_body_representation: dict[type[BaseModel], type[BaseModel]],
|
|
308
|
-
):
|
|
309
|
-
body_param: FastAPIModelField = copy_of_dependant.body_params[0]
|
|
310
|
-
body_schema = body_param.type_
|
|
311
|
-
new_type = schema_to_internal_request_body_representation.get(body_schema, body_schema)
|
|
312
|
-
new_body_param = rebuild_fastapi_body_param(body_param, new_type)
|
|
313
|
-
copy_of_dependant.body_params = [new_body_param]
|
|
314
|
-
|
|
315
226
|
# TODO (https://github.com/zmievsa/cadwyn/issues/28): Simplify
|
|
316
227
|
def _apply_endpoint_changes_to_router( # noqa: C901
|
|
317
228
|
self,
|
|
@@ -437,40 +348,6 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
437
348
|
)
|
|
438
349
|
|
|
439
350
|
|
|
440
|
-
def _extract_internal_request_schemas_from_router(
|
|
441
|
-
router: fastapi.routing.APIRouter,
|
|
442
|
-
) -> dict[type[BaseModel], type[BaseModel]]:
|
|
443
|
-
"""Please note that this functon replaces internal bodies with original bodies in the router"""
|
|
444
|
-
schema_to_internal_request_body_representation = {}
|
|
445
|
-
|
|
446
|
-
def _extract_internal_request_schemas_from_annotations(annotations: dict[str, Any]):
|
|
447
|
-
for key, annotation in annotations.items():
|
|
448
|
-
if isinstance(annotation, type(Annotated[int, int])): # pyright: ignore[reportArgumentType]
|
|
449
|
-
args = get_args(annotation)
|
|
450
|
-
if isinstance(args[1], type) and issubclass( # pragma: no branch
|
|
451
|
-
args[1],
|
|
452
|
-
InternalRepresentationOf, # pyright: ignore[reportDeprecated]
|
|
453
|
-
):
|
|
454
|
-
internal_schema = args[0]
|
|
455
|
-
original_schema = args[1].mro()[2]
|
|
456
|
-
schema_to_internal_request_body_representation[original_schema] = internal_schema
|
|
457
|
-
if len(args[2:]) != 0:
|
|
458
|
-
annotations[key] = Annotated[(original_schema, *args[2:])]
|
|
459
|
-
else:
|
|
460
|
-
annotations[key] = original_schema
|
|
461
|
-
return annotations
|
|
462
|
-
|
|
463
|
-
for route in router.routes:
|
|
464
|
-
if isinstance(route, APIRoute): # pragma: no branch
|
|
465
|
-
route.endpoint = _modify_callable_annotations(
|
|
466
|
-
route.endpoint,
|
|
467
|
-
modify_annotations=_extract_internal_request_schemas_from_annotations,
|
|
468
|
-
annotation_modifying_wrapper_factory=_copy_endpoint,
|
|
469
|
-
)
|
|
470
|
-
_remake_endpoint_dependencies(route)
|
|
471
|
-
return schema_to_internal_request_body_representation
|
|
472
|
-
|
|
473
|
-
|
|
474
351
|
def _validate_no_repetitions_in_routes(routes: list[fastapi.routing.APIRoute]):
|
|
475
352
|
route_map = {}
|
|
476
353
|
|
|
@@ -481,201 +358,6 @@ def _validate_no_repetitions_in_routes(routes: list[fastapi.routing.APIRoute]):
|
|
|
481
358
|
route_map[route_info] = route
|
|
482
359
|
|
|
483
360
|
|
|
484
|
-
@final
|
|
485
|
-
class _AnnotationTransformer:
|
|
486
|
-
__slots__ = (
|
|
487
|
-
"versions",
|
|
488
|
-
"head_schemas_package",
|
|
489
|
-
"head_version_dir",
|
|
490
|
-
"latest_version_dir",
|
|
491
|
-
"change_versions_of_a_non_container_annotation",
|
|
492
|
-
)
|
|
493
|
-
|
|
494
|
-
def __init__(self, head_schemas_package: ModuleType, versions: VersionBundle) -> None:
|
|
495
|
-
self.versions = versions
|
|
496
|
-
self.versions.head_schemas_package = head_schemas_package
|
|
497
|
-
self.head_schemas_package = head_schemas_package
|
|
498
|
-
self.head_version_dir = min(versions.versioned_directories_with_head) # "head" < "v0000_00_00"
|
|
499
|
-
self.latest_version_dir = max(versions.versioned_directories_with_head) # "v2005_11_11" > "v2000_11_11"
|
|
500
|
-
|
|
501
|
-
# This cache is not here for speeding things up. It's for preventing the creation of copies of the same object
|
|
502
|
-
# because such copies could produce weird behaviors at runtime, especially if you/fastapi do any comparisons.
|
|
503
|
-
# It's defined here and not on the method because of this: https://youtu.be/sVjtp6tGo0g
|
|
504
|
-
self.change_versions_of_a_non_container_annotation = functools.cache(
|
|
505
|
-
self._change_versions_of_a_non_container_annotation,
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
def migrate_router_to_version(self, router: fastapi.routing.APIRouter, version: Version):
|
|
509
|
-
version_dir = get_version_dir_path(self.head_schemas_package, version.value)
|
|
510
|
-
if not version_dir.is_dir():
|
|
511
|
-
raise RouterGenerationError(
|
|
512
|
-
f"Versioned schema directory '{version_dir}' does not exist.",
|
|
513
|
-
)
|
|
514
|
-
for route in router.routes:
|
|
515
|
-
if not isinstance(route, fastapi.routing.APIRoute):
|
|
516
|
-
continue
|
|
517
|
-
self.migrate_route_to_version(route, version_dir)
|
|
518
|
-
|
|
519
|
-
def migrate_route_to_version(
|
|
520
|
-
self,
|
|
521
|
-
route: fastapi.routing.APIRoute,
|
|
522
|
-
version_dir: Path,
|
|
523
|
-
*,
|
|
524
|
-
ignore_response_model: bool = False,
|
|
525
|
-
):
|
|
526
|
-
if route.response_model is not None and not ignore_response_model:
|
|
527
|
-
route.response_model = self._change_version_of_annotations(route.response_model, version_dir)
|
|
528
|
-
route.response_field = fastapi.utils.create_model_field(
|
|
529
|
-
name="Response_" + route.unique_id,
|
|
530
|
-
type_=route.response_model,
|
|
531
|
-
mode="serialization",
|
|
532
|
-
)
|
|
533
|
-
route.secure_cloned_response_field = fastapi.utils.create_cloned_field(route.response_field)
|
|
534
|
-
route.dependencies = self._change_version_of_annotations(route.dependencies, version_dir)
|
|
535
|
-
route.endpoint = self._change_version_of_annotations(route.endpoint, version_dir)
|
|
536
|
-
for callback in route.callbacks or []:
|
|
537
|
-
if not isinstance(callback, APIRoute):
|
|
538
|
-
continue
|
|
539
|
-
self.migrate_route_to_version(callback, version_dir, ignore_response_model=ignore_response_model)
|
|
540
|
-
_remake_endpoint_dependencies(route)
|
|
541
|
-
|
|
542
|
-
def _change_versions_of_a_non_container_annotation(self, annotation: Any, version_dir: Path) -> Any:
|
|
543
|
-
if isinstance(annotation, _BaseGenericAlias | GenericAlias):
|
|
544
|
-
return get_origin(annotation)[
|
|
545
|
-
tuple(self._change_version_of_annotations(arg, version_dir) for arg in get_args(annotation))
|
|
546
|
-
]
|
|
547
|
-
elif isinstance(annotation, Depends):
|
|
548
|
-
return Depends(
|
|
549
|
-
self._change_version_of_annotations(annotation.dependency, version_dir),
|
|
550
|
-
use_cache=annotation.use_cache,
|
|
551
|
-
)
|
|
552
|
-
elif isinstance(annotation, UnionType):
|
|
553
|
-
getitem = typing.Union.__getitem__ # pyright: ignore[reportAttributeAccessIssue]
|
|
554
|
-
return getitem(
|
|
555
|
-
tuple(self._change_version_of_annotations(a, version_dir) for a in get_args(annotation)),
|
|
556
|
-
)
|
|
557
|
-
elif annotation is typing.Any or isinstance(annotation, typing.NewType):
|
|
558
|
-
return annotation
|
|
559
|
-
elif isinstance(annotation, type):
|
|
560
|
-
if annotation.__module__ == "pydantic.main" and issubclass(annotation, BaseModel):
|
|
561
|
-
return create_body_model(
|
|
562
|
-
fields=self._change_version_of_annotations(model_fields(annotation), version_dir).values(),
|
|
563
|
-
model_name=annotation.__name__,
|
|
564
|
-
)
|
|
565
|
-
return self._change_version_of_type(annotation, version_dir)
|
|
566
|
-
elif callable(annotation):
|
|
567
|
-
if type(annotation).__module__.startswith(
|
|
568
|
-
("fastapi.", "pydantic.", "pydantic_core.", "starlette.")
|
|
569
|
-
) or isinstance(annotation, fastapi.params.Security | fastapi.security.base.SecurityBase):
|
|
570
|
-
return annotation
|
|
571
|
-
|
|
572
|
-
def modifier(annotation: Any):
|
|
573
|
-
return self._change_version_of_annotations(annotation, version_dir)
|
|
574
|
-
|
|
575
|
-
return _modify_callable_annotations(
|
|
576
|
-
annotation,
|
|
577
|
-
modifier,
|
|
578
|
-
modifier,
|
|
579
|
-
annotation_modifying_wrapper_factory=_copy_function_through_class_based_wrapper,
|
|
580
|
-
)
|
|
581
|
-
else:
|
|
582
|
-
return annotation
|
|
583
|
-
|
|
584
|
-
def _change_version_of_annotations(self, annotation: Any, version_dir: Path) -> Any:
|
|
585
|
-
"""Recursively go through all annotations and if they were taken from any versioned package, change them to the
|
|
586
|
-
annotations corresponding to the version_dir passed.
|
|
587
|
-
|
|
588
|
-
So if we had a annotation "UserResponse" from "head" version, and we passed version_dir of "v1_0_1", it would
|
|
589
|
-
replace "UserResponse" with the the same class but from the "v1_0_1" version.
|
|
590
|
-
|
|
591
|
-
"""
|
|
592
|
-
if isinstance(annotation, dict):
|
|
593
|
-
return {
|
|
594
|
-
self._change_version_of_annotations(key, version_dir): self._change_version_of_annotations(
|
|
595
|
-
value,
|
|
596
|
-
version_dir,
|
|
597
|
-
)
|
|
598
|
-
for key, value in annotation.items()
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
elif isinstance(annotation, list | tuple):
|
|
602
|
-
return type(annotation)(self._change_version_of_annotations(v, version_dir) for v in annotation)
|
|
603
|
-
else:
|
|
604
|
-
return self.change_versions_of_a_non_container_annotation(annotation, version_dir)
|
|
605
|
-
|
|
606
|
-
def _change_version_of_type(self, annotation: type, version_dir: Path):
|
|
607
|
-
if issubclass(annotation, BaseModel | Enum):
|
|
608
|
-
if version_dir == self.latest_version_dir:
|
|
609
|
-
source_file = inspect.getsourcefile(annotation)
|
|
610
|
-
if source_file is None: # pragma: no cover # I am not even sure how to cover this
|
|
611
|
-
warnings.warn(
|
|
612
|
-
f'Failed to find where the type annotation "{annotation}" is located.'
|
|
613
|
-
"Please, double check that it's located in the right directory",
|
|
614
|
-
stacklevel=7,
|
|
615
|
-
)
|
|
616
|
-
else:
|
|
617
|
-
self._validate_source_file_is_located_in_template_dir(annotation, source_file)
|
|
618
|
-
return get_another_version_of_cls(annotation, version_dir, self.versions.versioned_directories_with_head)
|
|
619
|
-
else:
|
|
620
|
-
return annotation
|
|
621
|
-
|
|
622
|
-
def _validate_source_file_is_located_in_template_dir(self, annotation: type, source_file: str):
|
|
623
|
-
template_dir = str(self.head_version_dir)
|
|
624
|
-
dir_with_versions = str(self.head_version_dir.parent)
|
|
625
|
-
# So if it is somewhere close to version dirs (either within them or next to them),
|
|
626
|
-
# but not located in "head",
|
|
627
|
-
# but also not located in any other version dir
|
|
628
|
-
if (
|
|
629
|
-
source_file.startswith(dir_with_versions)
|
|
630
|
-
and not source_file.startswith(template_dir)
|
|
631
|
-
and any(source_file.startswith(str(d)) for d in self.versions.versioned_directories_with_head)
|
|
632
|
-
):
|
|
633
|
-
raise RouterGenerationError(
|
|
634
|
-
f'"{annotation}" is not defined in "{self.head_version_dir}" even though it must be. '
|
|
635
|
-
f'It is defined in "{Path(source_file).parent}". '
|
|
636
|
-
"It probably means that you used a specific version of the class in fastapi dependencies "
|
|
637
|
-
'or pydantic schemas instead of "head".',
|
|
638
|
-
)
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
def _modify_callable_annotations(
|
|
642
|
-
call: _Call,
|
|
643
|
-
modify_annotations: Callable[[dict[str, Any]], dict[str, Any]] = lambda a: a,
|
|
644
|
-
modify_defaults: Callable[[tuple[Any, ...]], tuple[Any, ...]] = lambda a: a,
|
|
645
|
-
*,
|
|
646
|
-
annotation_modifying_wrapper_factory: Callable[[_Call], _Call],
|
|
647
|
-
) -> _Call:
|
|
648
|
-
annotation_modifying_wrapper = annotation_modifying_wrapper_factory(call)
|
|
649
|
-
old_params = inspect.signature(call).parameters
|
|
650
|
-
callable_annotations = annotation_modifying_wrapper.__annotations__
|
|
651
|
-
annotation_modifying_wrapper.__annotations__ = modify_annotations(callable_annotations)
|
|
652
|
-
annotation_modifying_wrapper.__defaults__ = modify_defaults(
|
|
653
|
-
tuple(p.default for p in old_params.values() if p.default is not inspect.Signature.empty),
|
|
654
|
-
)
|
|
655
|
-
annotation_modifying_wrapper.__signature__ = _generate_signature(
|
|
656
|
-
annotation_modifying_wrapper,
|
|
657
|
-
old_params,
|
|
658
|
-
)
|
|
659
|
-
|
|
660
|
-
return annotation_modifying_wrapper
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
def _remake_endpoint_dependencies(route: fastapi.routing.APIRoute):
|
|
664
|
-
# Unlike get_dependant, APIRoute is the public API of FastAPI and it's (almost) guaranteed to be stable.
|
|
665
|
-
|
|
666
|
-
route_copy = fastapi.routing.APIRoute(route.path, route.endpoint, dependencies=route.dependencies)
|
|
667
|
-
route.dependant = route_copy.dependant
|
|
668
|
-
route.body_field = route_copy.body_field
|
|
669
|
-
_add_request_and_response_params(route)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
def _add_request_and_response_params(route: APIRoute):
|
|
673
|
-
if not route.dependant.request_param_name:
|
|
674
|
-
route.dependant.request_param_name = _CADWYN_REQUEST_PARAM_NAME
|
|
675
|
-
if not route.dependant.response_param_name:
|
|
676
|
-
route.dependant.response_param_name = _CADWYN_RESPONSE_PARAM_NAME
|
|
677
|
-
|
|
678
|
-
|
|
679
361
|
def _add_data_migrations_to_route(
|
|
680
362
|
route: APIRoute,
|
|
681
363
|
head_route: Any,
|
|
@@ -734,42 +416,6 @@ def _apply_endpoint_had_instruction(
|
|
|
734
416
|
setattr(original_route, attr_name, attr)
|
|
735
417
|
|
|
736
418
|
|
|
737
|
-
def _generate_signature(
|
|
738
|
-
new_callable: Callable,
|
|
739
|
-
old_params: MappingProxyType[str, inspect.Parameter],
|
|
740
|
-
):
|
|
741
|
-
parameters = []
|
|
742
|
-
default_counter = 0
|
|
743
|
-
for param in old_params.values():
|
|
744
|
-
if param.default is not inspect.Signature.empty:
|
|
745
|
-
assert new_callable.__defaults__ is not None, ( # noqa: S101
|
|
746
|
-
"Defaults cannot be None here. If it is, you have found a bug in Cadwyn. "
|
|
747
|
-
"Please, report it in our issue tracker."
|
|
748
|
-
)
|
|
749
|
-
default = new_callable.__defaults__[default_counter]
|
|
750
|
-
default_counter += 1
|
|
751
|
-
else:
|
|
752
|
-
default = inspect.Signature.empty
|
|
753
|
-
parameters.append(
|
|
754
|
-
inspect.Parameter(
|
|
755
|
-
param.name,
|
|
756
|
-
param.kind,
|
|
757
|
-
default=default,
|
|
758
|
-
annotation=new_callable.__annotations__.get(
|
|
759
|
-
param.name,
|
|
760
|
-
inspect.Signature.empty,
|
|
761
|
-
),
|
|
762
|
-
),
|
|
763
|
-
)
|
|
764
|
-
return inspect.Signature(
|
|
765
|
-
parameters=parameters,
|
|
766
|
-
return_annotation=new_callable.__annotations__.get(
|
|
767
|
-
"return",
|
|
768
|
-
inspect.Signature.empty,
|
|
769
|
-
),
|
|
770
|
-
)
|
|
771
|
-
|
|
772
|
-
|
|
773
419
|
def _get_routes(
|
|
774
420
|
routes: Sequence[BaseRoute],
|
|
775
421
|
endpoint_path: str,
|
|
@@ -802,80 +448,6 @@ def _get_route_from_func(
|
|
|
802
448
|
return None
|
|
803
449
|
|
|
804
450
|
|
|
805
|
-
def _copy_endpoint(function: Any) -> Any:
|
|
806
|
-
function = _unwrap_callable(function)
|
|
807
|
-
function_copy: Any = types.FunctionType(
|
|
808
|
-
function.__code__,
|
|
809
|
-
function.__globals__,
|
|
810
|
-
name=function.__name__,
|
|
811
|
-
argdefs=function.__defaults__,
|
|
812
|
-
closure=function.__closure__,
|
|
813
|
-
)
|
|
814
|
-
function_copy = functools.update_wrapper(function_copy, function)
|
|
815
|
-
# Otherwise it will have the same signature as __wrapped__ due to how inspect module works
|
|
816
|
-
del function_copy.__wrapped__
|
|
817
|
-
|
|
818
|
-
function_copy._original_callable = function
|
|
819
|
-
function.__kwdefaults__ = function.__kwdefaults__.copy() if function.__kwdefaults__ is not None else {}
|
|
820
|
-
|
|
821
|
-
return function_copy
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
class _CallableWrapper:
|
|
825
|
-
"""__eq__ and __hash__ are needed to make sure that dependency overrides work correctly.
|
|
826
|
-
They are based on putting dependencies (functions) as keys for the dictionary so if we want to be able to
|
|
827
|
-
override the wrapper, we need to make sure that it is equivalent to the original in __hash__ and __eq__
|
|
828
|
-
"""
|
|
829
|
-
|
|
830
|
-
def __init__(self, original_callable: Callable) -> None:
|
|
831
|
-
super().__init__()
|
|
832
|
-
self._original_callable = original_callable
|
|
833
|
-
functools.update_wrapper(self, original_callable)
|
|
834
|
-
|
|
835
|
-
@property
|
|
836
|
-
def __globals__(self):
|
|
837
|
-
"""FastAPI uses __globals__ to resolve forward references in type hints
|
|
838
|
-
It's supposed to be an attribute on the function but we use it as property to prevent python
|
|
839
|
-
from trying to pickle globals when we deepcopy this wrapper
|
|
840
|
-
"""
|
|
841
|
-
#
|
|
842
|
-
return self._original_callable.__globals__
|
|
843
|
-
|
|
844
|
-
def __call__(self, *args: Any, **kwargs: Any):
|
|
845
|
-
return self._original_callable(*args, **kwargs)
|
|
846
|
-
|
|
847
|
-
def __hash__(self):
|
|
848
|
-
return hash(self._original_callable)
|
|
849
|
-
|
|
850
|
-
def __eq__(self, value: object) -> bool:
|
|
851
|
-
return self._original_callable == value # pyright: ignore[reportUnnecessaryComparison]
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
class _AsyncCallableWrapper(_CallableWrapper):
|
|
855
|
-
async def __call__(self, *args: Any, **kwargs: Any):
|
|
856
|
-
return await self._original_callable(*args, **kwargs)
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
def _copy_function_through_class_based_wrapper(call: Any):
|
|
860
|
-
"""Separate from copy_endpoint because endpoints MUST be functions in FastAPI, they cannot be cls instances"""
|
|
861
|
-
call = _unwrap_callable(call)
|
|
862
|
-
|
|
863
|
-
if inspect.iscoroutinefunction(call):
|
|
864
|
-
return _AsyncCallableWrapper(call)
|
|
865
|
-
else:
|
|
866
|
-
return _CallableWrapper(call)
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
def _unwrap_callable(call: Any) -> Any:
|
|
870
|
-
while hasattr(call, "_original_callable"):
|
|
871
|
-
call = call._original_callable
|
|
872
|
-
if not isinstance(call, types.FunctionType | types.MethodType):
|
|
873
|
-
# This means that the callable is actually an instance of a regular class
|
|
874
|
-
call = call.__call__
|
|
875
|
-
|
|
876
|
-
return call
|
|
877
|
-
|
|
878
|
-
|
|
879
451
|
def _route_has_a_simple_body_schema(route: APIRoute) -> bool:
|
|
880
452
|
# Remember this: if len(body_params) == 1, then route.body_schema == route.dependant.body_params[0]
|
|
881
453
|
return len(route.dependant.body_params) == 1
|
cadwyn/routing.py
CHANGED
|
@@ -14,13 +14,10 @@ from starlette.types import Receive, Scope, Send
|
|
|
14
14
|
|
|
15
15
|
from cadwyn._utils import same_definition_as_in
|
|
16
16
|
|
|
17
|
-
from .route_generation import
|
|
18
|
-
InternalRepresentationOf, # pyright: ignore[reportDeprecated]
|
|
19
|
-
generate_versioned_routers,
|
|
20
|
-
)
|
|
17
|
+
from .route_generation import generate_versioned_routers
|
|
21
18
|
|
|
22
19
|
# TODO: Remove this in a major version. This is only here for backwards compatibility
|
|
23
|
-
__all__ = ["
|
|
20
|
+
__all__ = ["generate_versioned_routers"]
|
|
24
21
|
|
|
25
22
|
_logger = getLogger(__name__)
|
|
26
23
|
|