cadwyn 3.12.1__py3-none-any.whl → 3.13.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/_asts.py +3 -2
- cadwyn/_compat.py +9 -2
- cadwyn/exceptions.py +12 -0
- cadwyn/route_generation.py +115 -48
- cadwyn/structure/data.py +13 -9
- cadwyn/structure/endpoints.py +7 -3
- cadwyn/structure/versions.py +26 -23
- {cadwyn-3.12.1.dist-info → cadwyn-3.13.0.dist-info}/METADATA +2 -1
- {cadwyn-3.12.1.dist-info → cadwyn-3.13.0.dist-info}/RECORD +12 -12
- {cadwyn-3.12.1.dist-info → cadwyn-3.13.0.dist-info}/LICENSE +0 -0
- {cadwyn-3.12.1.dist-info → cadwyn-3.13.0.dist-info}/WHEEL +0 -0
- {cadwyn-3.12.1.dist-info → cadwyn-3.13.0.dist-info}/entry_points.txt +0 -0
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
|
|
@@ -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
|
|
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/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/route_generation.py
CHANGED
|
@@ -15,7 +15,6 @@ from typing import (
|
|
|
15
15
|
Annotated,
|
|
16
16
|
Any,
|
|
17
17
|
Generic,
|
|
18
|
-
TypeAlias,
|
|
19
18
|
TypeVar,
|
|
20
19
|
_BaseGenericAlias, # pyright: ignore[reportAttributeAccessIssue]
|
|
21
20
|
cast,
|
|
@@ -29,6 +28,7 @@ import fastapi.params
|
|
|
29
28
|
import fastapi.routing
|
|
30
29
|
import fastapi.security.base
|
|
31
30
|
import fastapi.utils
|
|
31
|
+
from fastapi import APIRouter
|
|
32
32
|
from fastapi._compat import ModelField as FastAPIModelField
|
|
33
33
|
from fastapi._compat import create_body_model
|
|
34
34
|
from fastapi.dependencies.models import Dependant
|
|
@@ -39,6 +39,7 @@ from fastapi.dependencies.utils import (
|
|
|
39
39
|
)
|
|
40
40
|
from fastapi.params import Depends
|
|
41
41
|
from fastapi.routing import APIRoute
|
|
42
|
+
from issubclass import issubclass as lenient_issubclass
|
|
42
43
|
from pydantic import BaseModel
|
|
43
44
|
from starlette.routing import (
|
|
44
45
|
BaseRoute,
|
|
@@ -46,12 +47,15 @@ from starlette.routing import (
|
|
|
46
47
|
)
|
|
47
48
|
from typing_extensions import Self, assert_never, deprecated
|
|
48
49
|
|
|
49
|
-
from cadwyn._compat import model_fields, rebuild_fastapi_body_param
|
|
50
|
+
from cadwyn._compat import get_annotation_from_model_field, model_fields, rebuild_fastapi_body_param
|
|
50
51
|
from cadwyn._package_utils import get_version_dir_path
|
|
51
52
|
from cadwyn._utils import Sentinel, UnionType, get_another_version_of_cls
|
|
52
53
|
from cadwyn.exceptions import (
|
|
53
54
|
CadwynError,
|
|
54
55
|
RouteAlreadyExistsError,
|
|
56
|
+
RouteByPathConverterDoesNotApplyToAnythingError,
|
|
57
|
+
RouteRequestBySchemaConverterDoesNotApplyToAnythingError,
|
|
58
|
+
RouteResponseBySchemaConverterDoesNotApplyToAnythingError,
|
|
55
59
|
RouterGenerationError,
|
|
56
60
|
RouterPathParamsModifiedError,
|
|
57
61
|
)
|
|
@@ -68,8 +72,6 @@ _T = TypeVar("_T", bound=Callable[..., Any])
|
|
|
68
72
|
_R = TypeVar("_R", bound=fastapi.routing.APIRouter)
|
|
69
73
|
# This is a hack we do because we can't guarantee how the user will use the router.
|
|
70
74
|
_DELETED_ROUTE_TAG = "_CADWYN_DELETED_ROUTE"
|
|
71
|
-
_EndpointPath: TypeAlias = str
|
|
72
|
-
_EndpointMethod: TypeAlias = str
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
@dataclass(slots=True, frozen=True, eq=True)
|
|
@@ -78,13 +80,6 @@ class _EndpointInfo:
|
|
|
78
80
|
endpoint_methods: frozenset[str]
|
|
79
81
|
|
|
80
82
|
|
|
81
|
-
@dataclass(slots=True)
|
|
82
|
-
class _RouterInfo(Generic[_R]):
|
|
83
|
-
router: _R
|
|
84
|
-
routes_with_migrated_requests: dict[_EndpointPath, set[_EndpointMethod]]
|
|
85
|
-
route_bodies_with_migrated_requests: set[type[BaseModel]]
|
|
86
|
-
|
|
87
|
-
|
|
88
83
|
@deprecated("It will soon be deleted. Use HeadVersion version changes instead.")
|
|
89
84
|
class InternalRepresentationOf:
|
|
90
85
|
def __class_getitem__(cls, original_schema: type, /) -> type[Self]:
|
|
@@ -161,22 +156,15 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
161
156
|
self.parent_router
|
|
162
157
|
)
|
|
163
158
|
router = deepcopy(self.parent_router)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
route_bodies_with_migrated_requests: set[type[BaseModel]] = set()
|
|
159
|
+
routers: dict[VersionDate, _R] = {}
|
|
160
|
+
|
|
167
161
|
for version in self.versions:
|
|
168
162
|
self.annotation_transformer.migrate_router_to_version(router, version)
|
|
169
163
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
route_bodies_with_migrated_requests,
|
|
174
|
-
)
|
|
164
|
+
self._validate_all_data_converters_are_applied(router, version)
|
|
165
|
+
|
|
166
|
+
routers[version.value] = router
|
|
175
167
|
# Applying changes for the next version
|
|
176
|
-
routes_with_migrated_requests = _get_migrated_routes_by_path(version)
|
|
177
|
-
route_bodies_with_migrated_requests = {
|
|
178
|
-
schema for change in version.version_changes for schema in change.alter_request_by_schema_instructions
|
|
179
|
-
}
|
|
180
168
|
router = deepcopy(router)
|
|
181
169
|
self._apply_endpoint_changes_to_router(router, version)
|
|
182
170
|
|
|
@@ -194,21 +182,21 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
194
182
|
continue
|
|
195
183
|
_add_request_and_response_params(head_route)
|
|
196
184
|
copy_of_dependant = deepcopy(head_route.dependant)
|
|
197
|
-
|
|
198
|
-
if
|
|
185
|
+
|
|
186
|
+
if _route_has_a_simple_body_schema(head_route):
|
|
199
187
|
self._replace_internal_representation_with_the_versioned_schema(
|
|
200
188
|
copy_of_dependant,
|
|
201
189
|
schema_to_internal_request_body_representation,
|
|
202
190
|
)
|
|
203
191
|
|
|
204
|
-
for
|
|
205
|
-
older_route =
|
|
192
|
+
for older_router in list(routers.values()):
|
|
193
|
+
older_route = older_router.routes[route_index]
|
|
206
194
|
|
|
207
195
|
# We know they are APIRoutes because of the check at the very beginning of the top loop.
|
|
208
196
|
# I.e. Because head_route is an APIRoute, both routes are APIRoutes too
|
|
209
197
|
older_route = cast(APIRoute, older_route)
|
|
210
198
|
# Wait.. Why do we need this code again?
|
|
211
|
-
if older_route.body_field is not None and
|
|
199
|
+
if older_route.body_field is not None and _route_has_a_simple_body_schema(older_route):
|
|
212
200
|
template_older_body_model = self.annotation_transformer._change_version_of_annotations(
|
|
213
201
|
older_route.body_field.type_,
|
|
214
202
|
self.annotation_transformer.head_version_dir,
|
|
@@ -224,13 +212,99 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
224
212
|
copy_of_dependant,
|
|
225
213
|
self.versions,
|
|
226
214
|
)
|
|
227
|
-
for _,
|
|
228
|
-
|
|
215
|
+
for _, router in routers.items():
|
|
216
|
+
router.routes = [
|
|
229
217
|
route
|
|
230
|
-
for route in
|
|
218
|
+
for route in router.routes
|
|
231
219
|
if not (isinstance(route, fastapi.routing.APIRoute) and _DELETED_ROUTE_TAG in route.tags)
|
|
232
220
|
]
|
|
233
|
-
return
|
|
221
|
+
return routers
|
|
222
|
+
|
|
223
|
+
def _validate_all_data_converters_are_applied(self, router: APIRouter, version: Version):
|
|
224
|
+
path_to_route_methods_mapping, head_response_models, head_request_bodies = self._extract_all_routes_identifiers(
|
|
225
|
+
router
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
for version_change in version.version_changes:
|
|
229
|
+
for by_path_converters in [
|
|
230
|
+
*version_change.alter_response_by_path_instructions.values(),
|
|
231
|
+
*version_change.alter_request_by_path_instructions.values(),
|
|
232
|
+
]:
|
|
233
|
+
for by_path_converter in by_path_converters:
|
|
234
|
+
missing_methods = by_path_converter.methods.difference(
|
|
235
|
+
path_to_route_methods_mapping[by_path_converter.path]
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if missing_methods:
|
|
239
|
+
raise RouteByPathConverterDoesNotApplyToAnythingError(
|
|
240
|
+
f"{by_path_converter.repr_name} "
|
|
241
|
+
f'"{version_change.__name__}.{by_path_converter.transformer.__name__}" '
|
|
242
|
+
f"failed to find routes with the following methods: {list(missing_methods)}. "
|
|
243
|
+
f"This means that you are trying to apply this converter to non-existing endpoint(s). "
|
|
244
|
+
"Please, check whether the path and methods are correct. (hint: path must include "
|
|
245
|
+
"all path variables and have a name that was used in the version that this "
|
|
246
|
+
"VersionChange resides in)"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
for by_schema_converters in version_change.alter_request_by_schema_instructions.values():
|
|
250
|
+
for by_schema_converter in by_schema_converters:
|
|
251
|
+
missing_models = set(by_schema_converter.schemas) - head_request_bodies
|
|
252
|
+
if missing_models:
|
|
253
|
+
raise RouteRequestBySchemaConverterDoesNotApplyToAnythingError(
|
|
254
|
+
f"Request by body schema converter "
|
|
255
|
+
f'"{version_change.__name__}.{by_schema_converter.transformer.__name__}" '
|
|
256
|
+
f"failed to find routes with the following body schemas: "
|
|
257
|
+
f"{[m.__name__ for m in missing_models]}. "
|
|
258
|
+
f"This means that you are trying to apply this converter to non-existing endpoint(s). "
|
|
259
|
+
)
|
|
260
|
+
for by_schema_converters in version_change.alter_response_by_schema_instructions.values():
|
|
261
|
+
for by_schema_converter in by_schema_converters:
|
|
262
|
+
missing_models = set(by_schema_converter.schemas) - head_response_models
|
|
263
|
+
if missing_models:
|
|
264
|
+
raise RouteResponseBySchemaConverterDoesNotApplyToAnythingError(
|
|
265
|
+
f"Response by response model converter "
|
|
266
|
+
f'"{version_change.__name__}.{by_schema_converter.transformer.__name__}" '
|
|
267
|
+
f"failed to find routes with the following response models: "
|
|
268
|
+
f"{[m.__name__ for m in missing_models]}. "
|
|
269
|
+
f"This means that you are trying to apply this converter to non-existing endpoint(s). "
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def _extract_all_routes_identifiers(
|
|
273
|
+
self, router: APIRouter
|
|
274
|
+
) -> tuple[defaultdict[str, set[str]], set[Any], set[Any]]:
|
|
275
|
+
response_models: set[Any] = set()
|
|
276
|
+
request_bodies: set[Any] = set()
|
|
277
|
+
path_to_route_methods_mapping: dict[str, set[str]] = defaultdict(set)
|
|
278
|
+
|
|
279
|
+
for route in router.routes:
|
|
280
|
+
if isinstance(route, APIRoute):
|
|
281
|
+
if route.response_model is not None and lenient_issubclass(route.response_model, BaseModel):
|
|
282
|
+
# FIXME: This is going to fail on Pydantic 1
|
|
283
|
+
response_models.add(route.response_model)
|
|
284
|
+
# Not sure if it can ever be None when it's a simple schema. Eh, I would rather be safe than sorry
|
|
285
|
+
if _route_has_a_simple_body_schema(route) and route.body_field is not None:
|
|
286
|
+
annotation = get_annotation_from_model_field(route.body_field)
|
|
287
|
+
if lenient_issubclass(annotation, BaseModel):
|
|
288
|
+
# FIXME: This is going to fail on Pydantic 1
|
|
289
|
+
request_bodies.add(annotation)
|
|
290
|
+
path_to_route_methods_mapping[route.path] |= route.methods
|
|
291
|
+
|
|
292
|
+
head_response_models = {
|
|
293
|
+
self.annotation_transformer._change_version_of_annotations(
|
|
294
|
+
model,
|
|
295
|
+
self.versions.versioned_directories_with_head[0],
|
|
296
|
+
)
|
|
297
|
+
for model in response_models
|
|
298
|
+
}
|
|
299
|
+
head_request_bodies = {
|
|
300
|
+
self.annotation_transformer._change_version_of_annotations(
|
|
301
|
+
body,
|
|
302
|
+
self.versions.versioned_directories_with_head[0],
|
|
303
|
+
)
|
|
304
|
+
for body in request_bodies
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return path_to_route_methods_mapping, head_response_models, head_request_bodies
|
|
234
308
|
|
|
235
309
|
def _replace_internal_representation_with_the_versioned_schema(
|
|
236
310
|
self,
|
|
@@ -422,8 +496,8 @@ class _AnnotationTransformer:
|
|
|
422
496
|
self.versions = versions
|
|
423
497
|
self.versions.head_schemas_package = head_schemas_package
|
|
424
498
|
self.head_schemas_package = head_schemas_package
|
|
425
|
-
self.head_version_dir = min(versions.
|
|
426
|
-
self.latest_version_dir = max(versions.
|
|
499
|
+
self.head_version_dir = min(versions.versioned_directories_with_head) # "head" < "v0000_00_00"
|
|
500
|
+
self.latest_version_dir = max(versions.versioned_directories_with_head) # "v2005_11_11" > "v2000_11_11"
|
|
427
501
|
|
|
428
502
|
# This cache is not here for speeding things up. It's for preventing the creation of copies of the same object
|
|
429
503
|
# because such copies could produce weird behaviors at runtime, especially if you/fastapi do any comparisons.
|
|
@@ -537,7 +611,7 @@ class _AnnotationTransformer:
|
|
|
537
611
|
)
|
|
538
612
|
else:
|
|
539
613
|
self._validate_source_file_is_located_in_template_dir(annotation, source_file)
|
|
540
|
-
return get_another_version_of_cls(annotation, version_dir, self.versions.
|
|
614
|
+
return get_another_version_of_cls(annotation, version_dir, self.versions.versioned_directories_with_head)
|
|
541
615
|
else:
|
|
542
616
|
return annotation
|
|
543
617
|
|
|
@@ -550,7 +624,7 @@ class _AnnotationTransformer:
|
|
|
550
624
|
if (
|
|
551
625
|
source_file.startswith(dir_with_versions)
|
|
552
626
|
and not source_file.startswith(template_dir)
|
|
553
|
-
and any(source_file.startswith(str(d)) for d in self.versions.
|
|
627
|
+
and any(source_file.startswith(str(d)) for d in self.versions.versioned_directories_with_head)
|
|
554
628
|
):
|
|
555
629
|
raise RouterGenerationError(
|
|
556
630
|
f'"{annotation}" is not defined in "{self.head_version_dir}" even though it must be. '
|
|
@@ -725,18 +799,6 @@ def _get_route_from_func(
|
|
|
725
799
|
return None
|
|
726
800
|
|
|
727
801
|
|
|
728
|
-
def _get_migrated_routes_by_path(version: Version) -> dict[_EndpointPath, set[_EndpointMethod]]:
|
|
729
|
-
request_by_path_migration_instructions = [
|
|
730
|
-
version_change.alter_request_by_path_instructions for version_change in version.version_changes
|
|
731
|
-
]
|
|
732
|
-
migrated_routes = defaultdict(set)
|
|
733
|
-
for instruction_dict in request_by_path_migration_instructions:
|
|
734
|
-
for path, instruction_list in instruction_dict.items():
|
|
735
|
-
for instruction in instruction_list:
|
|
736
|
-
migrated_routes[path] |= instruction.methods
|
|
737
|
-
return migrated_routes
|
|
738
|
-
|
|
739
|
-
|
|
740
802
|
def _copy_function(function: _T) -> _T:
|
|
741
803
|
while hasattr(function, "__alt_wrapped__"):
|
|
742
804
|
function = function.__alt_wrapped__
|
|
@@ -768,3 +830,8 @@ def _copy_function(function: _T) -> _T:
|
|
|
768
830
|
del annotation_modifying_wrapper.__wrapped__
|
|
769
831
|
|
|
770
832
|
return cast(_T, annotation_modifying_wrapper)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _route_has_a_simple_body_schema(route: APIRoute) -> bool:
|
|
836
|
+
# Remember this: if len(body_params) == 1, then route.body_schema == route.dependant.body_params[0]
|
|
837
|
+
return len(route.dependant.body_params) == 1
|
cadwyn/structure/data.py
CHANGED
|
@@ -8,6 +8,7 @@ from fastapi import Request, Response
|
|
|
8
8
|
from starlette.datastructures import MutableHeaders
|
|
9
9
|
|
|
10
10
|
from cadwyn._utils import same_definition_as_in
|
|
11
|
+
from cadwyn.structure.endpoints import _validate_that_strings_are_valid_http_methods
|
|
11
12
|
|
|
12
13
|
_P = ParamSpec("_P")
|
|
13
14
|
|
|
@@ -96,14 +97,15 @@ class _BaseAlterRequestInstruction(_AlterDataInstruction):
|
|
|
96
97
|
|
|
97
98
|
|
|
98
99
|
@dataclass
|
|
99
|
-
class
|
|
100
|
+
class _AlterRequestBySchemaInstruction(_BaseAlterRequestInstruction):
|
|
100
101
|
schemas: tuple[Any, ...]
|
|
101
102
|
|
|
102
103
|
|
|
103
104
|
@dataclass
|
|
104
|
-
class
|
|
105
|
+
class _AlterRequestByPathInstruction(_BaseAlterRequestInstruction):
|
|
105
106
|
path: str
|
|
106
107
|
methods: set[str]
|
|
108
|
+
repr_name = "Request by path converter"
|
|
107
109
|
|
|
108
110
|
|
|
109
111
|
@overload
|
|
@@ -126,7 +128,7 @@ def convert_request_to_next_version_for(
|
|
|
126
128
|
|
|
127
129
|
def decorator(transformer: Callable[[RequestInfo], None]) -> Any:
|
|
128
130
|
if isinstance(schema_or_path, str):
|
|
129
|
-
return
|
|
131
|
+
return _AlterRequestByPathInstruction(
|
|
130
132
|
path=schema_or_path,
|
|
131
133
|
methods=set(cast(list, methods_or_second_schema)),
|
|
132
134
|
transformer=transformer,
|
|
@@ -136,7 +138,7 @@ def convert_request_to_next_version_for(
|
|
|
136
138
|
schemas = (schema_or_path,)
|
|
137
139
|
else:
|
|
138
140
|
schemas = (schema_or_path, methods_or_second_schema, *additional_schemas)
|
|
139
|
-
return
|
|
141
|
+
return _AlterRequestBySchemaInstruction(
|
|
140
142
|
schemas=schemas,
|
|
141
143
|
transformer=transformer,
|
|
142
144
|
)
|
|
@@ -156,14 +158,15 @@ class _BaseAlterResponseInstruction(_AlterDataInstruction):
|
|
|
156
158
|
|
|
157
159
|
|
|
158
160
|
@dataclass
|
|
159
|
-
class
|
|
161
|
+
class _AlterResponseBySchemaInstruction(_BaseAlterResponseInstruction):
|
|
160
162
|
schemas: tuple[Any, ...]
|
|
161
163
|
|
|
162
164
|
|
|
163
165
|
@dataclass
|
|
164
|
-
class
|
|
166
|
+
class _AlterResponseByPathInstruction(_BaseAlterResponseInstruction):
|
|
165
167
|
path: str
|
|
166
168
|
methods: set[str]
|
|
169
|
+
repr_name = "Response by path converter"
|
|
167
170
|
|
|
168
171
|
|
|
169
172
|
@overload
|
|
@@ -197,7 +200,7 @@ def convert_response_to_previous_version_for(
|
|
|
197
200
|
def decorator(transformer: Callable[[ResponseInfo], None]) -> Any:
|
|
198
201
|
if isinstance(schema_or_path, str):
|
|
199
202
|
# The validation above checks that methods is not None
|
|
200
|
-
return
|
|
203
|
+
return _AlterResponseByPathInstruction(
|
|
201
204
|
path=schema_or_path,
|
|
202
205
|
methods=set(cast(list, methods_or_second_schema)),
|
|
203
206
|
transformer=transformer,
|
|
@@ -208,7 +211,7 @@ def convert_response_to_previous_version_for(
|
|
|
208
211
|
schemas = (schema_or_path,)
|
|
209
212
|
else:
|
|
210
213
|
schemas = (schema_or_path, methods_or_second_schema, *additional_schemas)
|
|
211
|
-
return
|
|
214
|
+
return _AlterResponseBySchemaInstruction(
|
|
212
215
|
schemas=schemas,
|
|
213
216
|
transformer=transformer,
|
|
214
217
|
migrate_http_errors=migrate_http_errors,
|
|
@@ -219,10 +222,11 @@ def convert_response_to_previous_version_for(
|
|
|
219
222
|
|
|
220
223
|
def _validate_decorator_args(
|
|
221
224
|
schema_or_path: type | str, methods_or_second_schema: list[str] | type | None, additional_schemas: tuple[type, ...]
|
|
222
|
-
):
|
|
225
|
+
) -> None:
|
|
223
226
|
if isinstance(schema_or_path, str):
|
|
224
227
|
if not isinstance(methods_or_second_schema, list):
|
|
225
228
|
raise TypeError("If path was provided as a first argument, methods must be provided as a second argument")
|
|
229
|
+
_validate_that_strings_are_valid_http_methods(methods_or_second_schema)
|
|
226
230
|
if additional_schemas:
|
|
227
231
|
raise TypeError("If path was provided as a first argument, then additional schemas cannot be added")
|
|
228
232
|
|
cadwyn/structure/endpoints.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from collections.abc import Callable, Sequence
|
|
1
|
+
from collections.abc import Callable, Collection, Sequence
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Any
|
|
@@ -148,6 +148,12 @@ class EndpointInstructionFactory:
|
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
def endpoint(path: str, methods: list[str], /, *, func_name: str | None = None) -> EndpointInstructionFactory:
|
|
151
|
+
_validate_that_strings_are_valid_http_methods(methods)
|
|
152
|
+
|
|
153
|
+
return EndpointInstructionFactory(path, set(methods), func_name)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _validate_that_strings_are_valid_http_methods(methods: Collection[str]):
|
|
151
157
|
invalid_methods = set(methods) - HTTP_METHODS
|
|
152
158
|
if invalid_methods:
|
|
153
159
|
invalid_methods = ", ".join(sorted(invalid_methods))
|
|
@@ -156,7 +162,5 @@ def endpoint(path: str, methods: list[str], /, *, func_name: str | None = None)
|
|
|
156
162
|
"Please use valid HTTP methods such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.",
|
|
157
163
|
)
|
|
158
164
|
|
|
159
|
-
return EndpointInstructionFactory(path, set(methods), func_name)
|
|
160
|
-
|
|
161
165
|
|
|
162
166
|
AlterEndpointSubInstruction = EndpointDidntExistInstruction | EndpointExistedInstruction | EndpointHadInstruction
|
cadwyn/structure/versions.py
CHANGED
|
@@ -39,12 +39,12 @@ from cadwyn.exceptions import CadwynError, CadwynHeadRequestValidationError, Cad
|
|
|
39
39
|
from .._utils import Sentinel
|
|
40
40
|
from .common import Endpoint, VersionDate, VersionedModel
|
|
41
41
|
from .data import (
|
|
42
|
-
AlterRequestByPathInstruction,
|
|
43
|
-
AlterRequestBySchemaInstruction,
|
|
44
|
-
AlterResponseByPathInstruction,
|
|
45
|
-
AlterResponseBySchemaInstruction,
|
|
46
42
|
RequestInfo,
|
|
47
43
|
ResponseInfo,
|
|
44
|
+
_AlterRequestByPathInstruction,
|
|
45
|
+
_AlterRequestBySchemaInstruction,
|
|
46
|
+
_AlterResponseByPathInstruction,
|
|
47
|
+
_AlterResponseBySchemaInstruction,
|
|
48
48
|
_BaseAlterResponseInstruction,
|
|
49
49
|
)
|
|
50
50
|
from .endpoints import AlterEndpointSubInstruction
|
|
@@ -74,12 +74,12 @@ class VersionChange:
|
|
|
74
74
|
alter_enum_instructions: ClassVar[list[AlterEnumSubInstruction]] = Sentinel
|
|
75
75
|
alter_module_instructions: ClassVar[list[AlterModuleInstruction]] = Sentinel
|
|
76
76
|
alter_endpoint_instructions: ClassVar[list[AlterEndpointSubInstruction]] = Sentinel
|
|
77
|
-
alter_request_by_schema_instructions: ClassVar[dict[type[BaseModel], list[
|
|
77
|
+
alter_request_by_schema_instructions: ClassVar[dict[type[BaseModel], list[_AlterRequestBySchemaInstruction]]] = (
|
|
78
78
|
Sentinel
|
|
79
79
|
)
|
|
80
|
-
alter_request_by_path_instructions: ClassVar[dict[str, list[
|
|
81
|
-
alter_response_by_schema_instructions: ClassVar[dict[type, list[
|
|
82
|
-
alter_response_by_path_instructions: ClassVar[dict[str, list[
|
|
80
|
+
alter_request_by_path_instructions: ClassVar[dict[str, list[_AlterRequestByPathInstruction]]] = Sentinel
|
|
81
|
+
alter_response_by_schema_instructions: ClassVar[dict[type, list[_AlterResponseBySchemaInstruction]]] = Sentinel
|
|
82
|
+
alter_response_by_path_instructions: ClassVar[dict[str, list[_AlterResponseByPathInstruction]]] = Sentinel
|
|
83
83
|
_bound_version_bundle: "VersionBundle | None"
|
|
84
84
|
|
|
85
85
|
def __init_subclass__(cls, _abstract: bool = False) -> None:
|
|
@@ -96,15 +96,15 @@ class VersionChange:
|
|
|
96
96
|
@classmethod
|
|
97
97
|
def _extract_body_instructions_into_correct_containers(cls):
|
|
98
98
|
for instruction in cls.__dict__.values():
|
|
99
|
-
if isinstance(instruction,
|
|
99
|
+
if isinstance(instruction, _AlterRequestBySchemaInstruction):
|
|
100
100
|
for schema in instruction.schemas:
|
|
101
101
|
cls.alter_request_by_schema_instructions[schema].append(instruction)
|
|
102
|
-
elif isinstance(instruction,
|
|
102
|
+
elif isinstance(instruction, _AlterRequestByPathInstruction):
|
|
103
103
|
cls.alter_request_by_path_instructions[instruction.path].append(instruction)
|
|
104
|
-
elif isinstance(instruction,
|
|
104
|
+
elif isinstance(instruction, _AlterResponseBySchemaInstruction):
|
|
105
105
|
for schema in instruction.schemas:
|
|
106
106
|
cls.alter_response_by_schema_instructions[schema].append(instruction)
|
|
107
|
-
elif isinstance(instruction,
|
|
107
|
+
elif isinstance(instruction, _AlterResponseByPathInstruction):
|
|
108
108
|
cls.alter_response_by_path_instructions[instruction.path].append(instruction)
|
|
109
109
|
|
|
110
110
|
@classmethod
|
|
@@ -154,10 +154,10 @@ class VersionChange:
|
|
|
154
154
|
for attr_name, attr_value in cls.__dict__.items():
|
|
155
155
|
if not isinstance(
|
|
156
156
|
attr_value,
|
|
157
|
-
|
|
158
|
-
|
|
|
159
|
-
|
|
|
160
|
-
|
|
|
157
|
+
_AlterRequestBySchemaInstruction
|
|
158
|
+
| _AlterRequestByPathInstruction
|
|
159
|
+
| _AlterResponseBySchemaInstruction
|
|
160
|
+
| _AlterResponseByPathInstruction,
|
|
161
161
|
) and attr_name not in {
|
|
162
162
|
"description",
|
|
163
163
|
"side_effects",
|
|
@@ -385,7 +385,7 @@ class VersionBundle:
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
@functools.cached_property
|
|
388
|
-
def
|
|
388
|
+
def versioned_directories_with_head(self) -> tuple[Path, ...]:
|
|
389
389
|
if self.head_schemas_package is None:
|
|
390
390
|
raise CadwynError(
|
|
391
391
|
f"You cannot call 'VersionBundle.{self.migrate_response_body.__name__}' because it has no access to "
|
|
@@ -397,6 +397,10 @@ class VersionBundle:
|
|
|
397
397
|
+ [get_version_dir_path(self.head_schemas_package, version.value) for version in self]
|
|
398
398
|
)
|
|
399
399
|
|
|
400
|
+
@functools.cached_property
|
|
401
|
+
def versioned_directories_without_head(self) -> tuple[Path, ...]:
|
|
402
|
+
return self.versioned_directories_with_head[1:]
|
|
403
|
+
|
|
400
404
|
def migrate_response_body(self, latest_response_model: type[BaseModel], *, latest_body: Any, version: VersionDate):
|
|
401
405
|
"""Convert the data to a specific version by applying all version changes from latest until that version
|
|
402
406
|
in reverse order and wrapping the result in the correct version of latest_response_model.
|
|
@@ -411,11 +415,10 @@ class VersionBundle:
|
|
|
411
415
|
)
|
|
412
416
|
|
|
413
417
|
version = self._get_closest_lesser_version(version)
|
|
414
|
-
|
|
415
|
-
version_dir = self.versioned_directories[self.version_dates.index(version) + 1]
|
|
418
|
+
version_dir = self.versioned_directories_without_head[self.version_dates.index(version)]
|
|
416
419
|
|
|
417
420
|
versioned_response_model: type[BaseModel] = get_another_version_of_cls(
|
|
418
|
-
latest_response_model, version_dir, self.
|
|
421
|
+
latest_response_model, version_dir, self.versioned_directories_with_head
|
|
419
422
|
)
|
|
420
423
|
return versioned_response_model.parse_obj(migrated_response.body)
|
|
421
424
|
|
|
@@ -455,7 +458,7 @@ class VersionBundle:
|
|
|
455
458
|
instruction(request_info)
|
|
456
459
|
if path in version_change.alter_request_by_path_instructions:
|
|
457
460
|
for instruction in version_change.alter_request_by_path_instructions[path]:
|
|
458
|
-
if method in instruction.methods:
|
|
461
|
+
if method in instruction.methods: # pragma: no branch # safe branch to skip
|
|
459
462
|
instruction(request_info)
|
|
460
463
|
request.scope["headers"] = tuple((key.encode(), value.encode()) for key, value in request_info.headers.items())
|
|
461
464
|
del request._headers
|
|
@@ -507,7 +510,7 @@ class VersionBundle:
|
|
|
507
510
|
|
|
508
511
|
if path in version_change.alter_response_by_path_instructions:
|
|
509
512
|
for instruction in version_change.alter_response_by_path_instructions[path]:
|
|
510
|
-
if method in instruction.methods:
|
|
513
|
+
if method in instruction.methods: # pragma: no branch # Safe branch to skip
|
|
511
514
|
migrations_to_apply.append(instruction)
|
|
512
515
|
|
|
513
516
|
for migration in migrations_to_apply:
|
|
@@ -723,7 +726,7 @@ class VersionBundle:
|
|
|
723
726
|
and body_field_alias in kwargs
|
|
724
727
|
):
|
|
725
728
|
raw_body: BaseModel | None = kwargs.get(body_field_alias)
|
|
726
|
-
if raw_body is None:
|
|
729
|
+
if raw_body is None: # pragma: no cover # This is likely an impossible case but we would like to be safe
|
|
727
730
|
body = None
|
|
728
731
|
# It means we have a dict or a list instead of a full model.
|
|
729
732
|
# This covers the following use case in the endpoint definition: "payload: dict = Body(None)"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cadwyn
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.13.0
|
|
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
|
|
@@ -34,6 +34,7 @@ Classifier: Typing :: Typed
|
|
|
34
34
|
Provides-Extra: cli
|
|
35
35
|
Requires-Dist: better-ast-comments (>=1.2.1,<1.3.0)
|
|
36
36
|
Requires-Dist: fastapi (>=0.110.0)
|
|
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)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
cadwyn/__init__.py,sha256=XgF-CtZo-fPk5730sKlY2fAmPsTQRIsFbrfNFeUZyFY,495
|
|
2
2
|
cadwyn/__main__.py,sha256=q7oNhnJ_hNRib3o6cAo4QC0cME0prVi-6P0G14tRdkw,4339
|
|
3
|
-
cadwyn/_asts.py,sha256=
|
|
4
|
-
cadwyn/_compat.py,sha256=
|
|
3
|
+
cadwyn/_asts.py,sha256=OF1qQKPqTbgYhH1tYF-MB8CCU0r6YITZpMFegzmk0Ic,10118
|
|
4
|
+
cadwyn/_compat.py,sha256=yAPmfGl2vVEYXlNHHPMoa2JkEJCVPjbP_Uz0WOIVOp4,5494
|
|
5
5
|
cadwyn/_package_utils.py,sha256=trxTYLmppv-10SKhScfyDQJh21rsQGFoLaOtHycKKR0,1443
|
|
6
6
|
cadwyn/_utils.py,sha256=BFsfZBpdoL5RMAaT1V1cXJVpTZCmwksQ-Le2MTHivGI,4841
|
|
7
7
|
cadwyn/applications.py,sha256=MAVsgYojgQO4PrUETVMAsp49k6baW4h4LtS6z12gTZs,15767
|
|
@@ -15,24 +15,24 @@ cadwyn/codegen/_plugins/class_rebuilding.py,sha256=zNlB_VxoEAtdC5Ydiqa7pu6Ka-pKn
|
|
|
15
15
|
cadwyn/codegen/_plugins/class_renaming.py,sha256=oc9Ms6YnpJKaq1iOehcBfA_OFUFL-CAAZJiaQPlkKHs,1773
|
|
16
16
|
cadwyn/codegen/_plugins/import_auto_adding.py,sha256=krAVzsmsW-CbKP-W9oCkQsL7aPfhHzRq4STgai6Tm5s,2543
|
|
17
17
|
cadwyn/codegen/_plugins/module_migrations.py,sha256=TeWJk4Iu4SRQ9K2iI3v3sCs1110jrltKlPdfU9mXIsQ,722
|
|
18
|
-
cadwyn/exceptions.py,sha256=
|
|
18
|
+
cadwyn/exceptions.py,sha256=aJKx1qgzZqShL4MX3COjS780qzNJcdZFeGzYYa5gbzw,1726
|
|
19
19
|
cadwyn/main.py,sha256=kt2Vn7TIA4ZnD_xrgz57TOjUk-4zVP8SV8nuTZBEaaU,218
|
|
20
20
|
cadwyn/middleware.py,sha256=8cuBri_yRkl0goe6G0MLwtL04WGbW9Infah3wy9hUVM,3372
|
|
21
21
|
cadwyn/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
cadwyn/route_generation.py,sha256=
|
|
22
|
+
cadwyn/route_generation.py,sha256=kpcOknl-R2TBsFtsliA0MZ_bdqH4qZ9-J7eCbejCXfE,39747
|
|
23
23
|
cadwyn/routing.py,sha256=ObH4-ETYPQjm3bMVCNzGttEKv1LL9q2sbi9eJD4W3lY,6250
|
|
24
24
|
cadwyn/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
cadwyn/static/docs.html,sha256=WNm5ANJVy51TcIUFOaqKf1Z8eF86CC85TTHPxACtkzw,3455
|
|
26
26
|
cadwyn/structure/__init__.py,sha256=HjaNd6H4m4Cia42-dCO7A7sLWuVII7oldjaCabhbs_o,697
|
|
27
27
|
cadwyn/structure/common.py,sha256=6Z4nI97XPWTCinn6np73m-rLPyYNrz2fWXKJlqjsiaQ,269
|
|
28
|
-
cadwyn/structure/data.py,sha256=
|
|
29
|
-
cadwyn/structure/endpoints.py,sha256=
|
|
28
|
+
cadwyn/structure/data.py,sha256=1ALPhBBCE_t4GrxM0Fa3hQ-jkORJgeWNySnZ42bsi0g,7382
|
|
29
|
+
cadwyn/structure/endpoints.py,sha256=JhTgVrqLjm5LkE9thjvU1UuWcSCmDgW2bMdqznsZb2Y,5777
|
|
30
30
|
cadwyn/structure/enums.py,sha256=iMokxA2QYJ61SzyB-Pmuq3y7KL7-e6TsnjLVUaVZQnw,954
|
|
31
31
|
cadwyn/structure/modules.py,sha256=1FK-lLm-zOTXEvn-QtyBH38aDRht5PDQiZrOPCsBlM4,1268
|
|
32
32
|
cadwyn/structure/schemas.py,sha256=0ylArAkUw626VkUOJSulOwJs7CS6lrGBRECEG5HFD4Q,8897
|
|
33
|
-
cadwyn/structure/versions.py,sha256=
|
|
34
|
-
cadwyn-3.
|
|
35
|
-
cadwyn-3.
|
|
36
|
-
cadwyn-3.
|
|
37
|
-
cadwyn-3.
|
|
38
|
-
cadwyn-3.
|
|
33
|
+
cadwyn/structure/versions.py,sha256=Z9TQs4QsuLOAWxjKeq0KERYVCHneFIXyeoUo4NdmMo8,37199
|
|
34
|
+
cadwyn-3.13.0.dist-info/LICENSE,sha256=KeCWewiDQYpmSnzF-p_0YpoWiyDcUPaCuG8OWQs4ig4,1072
|
|
35
|
+
cadwyn-3.13.0.dist-info/METADATA,sha256=1BGYb7exKyPC1Da58xWpJziBDSzxZlzxq5hs9XMTJW8,4403
|
|
36
|
+
cadwyn-3.13.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
37
|
+
cadwyn-3.13.0.dist-info/entry_points.txt,sha256=eO05hLn9GoRzzpwT9GONPmXKsonjuMNssM2D2WHWKGk,46
|
|
38
|
+
cadwyn-3.13.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|