cadwyn 5.0.0__py3-none-any.whl → 5.1.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 +10 -7
- cadwyn/_render.py +7 -7
- cadwyn/_utils.py +25 -2
- cadwyn/applications.py +34 -32
- cadwyn/changelogs.py +182 -181
- cadwyn/middleware.py +7 -7
- cadwyn/route_generation.py +10 -9
- cadwyn/routing.py +2 -2
- cadwyn/schema_generation.py +114 -46
- cadwyn/structure/common.py +5 -3
- cadwyn/structure/data.py +10 -7
- cadwyn/structure/endpoints.py +24 -21
- cadwyn/structure/enums.py +13 -7
- cadwyn/structure/schemas.py +63 -53
- cadwyn/structure/versions.py +50 -40
- {cadwyn-5.0.0.dist-info → cadwyn-5.1.0.dist-info}/METADATA +4 -3
- cadwyn-5.1.0.dist-info/RECORD +28 -0
- cadwyn-5.0.0.dist-info/RECORD +0 -28
- {cadwyn-5.0.0.dist-info → cadwyn-5.1.0.dist-info}/WHEEL +0 -0
- {cadwyn-5.0.0.dist-info → cadwyn-5.1.0.dist-info}/entry_points.txt +0 -0
- {cadwyn-5.0.0.dist-info → cadwyn-5.1.0.dist-info}/licenses/LICENSE +0 -0
cadwyn/schema_generation.py
CHANGED
|
@@ -1,33 +1,24 @@
|
|
|
1
|
+
import ast
|
|
1
2
|
import copy
|
|
2
3
|
import dataclasses
|
|
3
4
|
import functools
|
|
4
5
|
import inspect
|
|
6
|
+
import sys
|
|
7
|
+
import textwrap
|
|
5
8
|
import types
|
|
6
9
|
import typing
|
|
7
10
|
from collections.abc import Callable, Sequence
|
|
8
11
|
from datetime import date
|
|
9
12
|
from enum import Enum
|
|
10
13
|
from functools import cache
|
|
11
|
-
from typing import
|
|
12
|
-
TYPE_CHECKING,
|
|
13
|
-
Annotated,
|
|
14
|
-
Any,
|
|
15
|
-
Generic,
|
|
16
|
-
TypeAlias,
|
|
17
|
-
TypeVar,
|
|
18
|
-
_BaseGenericAlias, # pyright: ignore[reportAttributeAccessIssue]
|
|
19
|
-
cast,
|
|
20
|
-
final,
|
|
21
|
-
get_args,
|
|
22
|
-
get_origin,
|
|
23
|
-
overload,
|
|
24
|
-
)
|
|
14
|
+
from typing import TYPE_CHECKING, Annotated, Generic, Union, cast
|
|
25
15
|
|
|
26
16
|
import fastapi.params
|
|
27
17
|
import fastapi.security.base
|
|
28
18
|
import fastapi.utils
|
|
29
19
|
import pydantic
|
|
30
20
|
import pydantic._internal._decorators
|
|
21
|
+
import typing_extensions
|
|
31
22
|
from fastapi import Response
|
|
32
23
|
from fastapi.routing import APIRoute
|
|
33
24
|
from pydantic import BaseModel, Field, RootModel
|
|
@@ -42,9 +33,30 @@ from pydantic._internal._decorators import (
|
|
|
42
33
|
)
|
|
43
34
|
from pydantic._internal._typing_extra import try_eval_type as pydantic_try_eval_type
|
|
44
35
|
from pydantic.fields import ComputedFieldInfo, FieldInfo
|
|
45
|
-
from typing_extensions import
|
|
36
|
+
from typing_extensions import (
|
|
37
|
+
Any,
|
|
38
|
+
Doc,
|
|
39
|
+
NewType,
|
|
40
|
+
Self,
|
|
41
|
+
TypeAlias,
|
|
42
|
+
TypeVar,
|
|
43
|
+
_AnnotatedAlias,
|
|
44
|
+
assert_never,
|
|
45
|
+
final,
|
|
46
|
+
get_args,
|
|
47
|
+
get_origin,
|
|
48
|
+
overload,
|
|
49
|
+
)
|
|
46
50
|
|
|
47
|
-
from cadwyn._utils import
|
|
51
|
+
from cadwyn._utils import (
|
|
52
|
+
DATACLASS_KW_ONLY,
|
|
53
|
+
DATACLASS_SLOTS,
|
|
54
|
+
Sentinel,
|
|
55
|
+
UnionType,
|
|
56
|
+
fully_unwrap_decorator,
|
|
57
|
+
get_name_of_function_wrapped_in_pydantic_validator,
|
|
58
|
+
lenient_issubclass,
|
|
59
|
+
)
|
|
48
60
|
from cadwyn.exceptions import CadwynError, InvalidGenerationInstructionError
|
|
49
61
|
from cadwyn.structure.common import VersionType
|
|
50
62
|
from cadwyn.structure.data import ResponseInfo
|
|
@@ -65,10 +77,15 @@ from cadwyn.structure.versions import _CADWYN_REQUEST_PARAM_NAME, _CADWYN_RESPON
|
|
|
65
77
|
if TYPE_CHECKING:
|
|
66
78
|
from cadwyn.structure.versions import HeadVersion, Version, VersionBundle
|
|
67
79
|
|
|
80
|
+
if sys.version_info >= (3, 10):
|
|
81
|
+
from typing import _BaseGenericAlias # pyright: ignore[reportAttributeAccessIssue]
|
|
82
|
+
else:
|
|
83
|
+
from typing_extensions import _BaseGenericAlias # pyright: ignore[reportAttributeAccessIssue]
|
|
84
|
+
|
|
68
85
|
_Call = TypeVar("_Call", bound=Callable[..., Any])
|
|
69
86
|
|
|
70
87
|
_FieldName: TypeAlias = str
|
|
71
|
-
_T_ANY_MODEL = TypeVar("_T_ANY_MODEL", bound=BaseModel
|
|
88
|
+
_T_ANY_MODEL = TypeVar("_T_ANY_MODEL", bound=Union[BaseModel, Enum])
|
|
72
89
|
_T_ENUM = TypeVar("_T_ENUM", bound=Enum)
|
|
73
90
|
|
|
74
91
|
_T_PYDANTIC_MODEL = TypeVar("_T_PYDANTIC_MODEL", bound=BaseModel)
|
|
@@ -98,7 +115,7 @@ _empty_field_info = Field()
|
|
|
98
115
|
dict_of_empty_field_info = {k: getattr(_empty_field_info, k) for k in FieldInfo.__slots__}
|
|
99
116
|
|
|
100
117
|
|
|
101
|
-
@dataclasses.dataclass(
|
|
118
|
+
@dataclasses.dataclass(**DATACLASS_SLOTS)
|
|
102
119
|
class PydanticFieldWrapper:
|
|
103
120
|
"""We DO NOT maintain field.metadata at all"""
|
|
104
121
|
|
|
@@ -136,16 +153,16 @@ def _extract_passed_field_attributes(field_info: FieldInfo):
|
|
|
136
153
|
return attributes
|
|
137
154
|
|
|
138
155
|
|
|
139
|
-
@dataclasses.dataclass(
|
|
156
|
+
@dataclasses.dataclass(**DATACLASS_SLOTS)
|
|
140
157
|
class _ModelBundle:
|
|
141
158
|
enums: dict[type[Enum], "_EnumWrapper"]
|
|
142
159
|
schemas: dict[type[BaseModel], "_PydanticModelWrapper"]
|
|
143
160
|
|
|
144
161
|
|
|
145
|
-
@dataclasses.dataclass(
|
|
162
|
+
@dataclasses.dataclass(**DATACLASS_SLOTS, **DATACLASS_KW_ONLY)
|
|
146
163
|
class _RuntimeSchemaGenContext:
|
|
147
164
|
version_bundle: "VersionBundle"
|
|
148
|
-
current_version: "Version
|
|
165
|
+
current_version: "Union[Version, HeadVersion]"
|
|
149
166
|
models: _ModelBundle
|
|
150
167
|
latest_version: "Version" = dataclasses.field(init=False)
|
|
151
168
|
|
|
@@ -158,7 +175,7 @@ def migrate_response_body(
|
|
|
158
175
|
latest_response_model: type[pydantic.BaseModel],
|
|
159
176
|
*,
|
|
160
177
|
latest_body: Any,
|
|
161
|
-
version: VersionType
|
|
178
|
+
version: Union[VersionType, date],
|
|
162
179
|
) -> Any:
|
|
163
180
|
"""Convert the data to a specific version
|
|
164
181
|
|
|
@@ -191,7 +208,7 @@ def _unwrap_model(model: type[_T_ANY_MODEL]) -> type[_T_ANY_MODEL]:
|
|
|
191
208
|
return model
|
|
192
209
|
|
|
193
210
|
|
|
194
|
-
@dataclasses.dataclass(
|
|
211
|
+
@dataclasses.dataclass(**DATACLASS_SLOTS, **DATACLASS_KW_ONLY)
|
|
195
212
|
class _ValidatorWrapper:
|
|
196
213
|
kwargs: dict[str, Any]
|
|
197
214
|
func: Callable
|
|
@@ -199,9 +216,9 @@ class _ValidatorWrapper:
|
|
|
199
216
|
is_deleted: bool = False
|
|
200
217
|
|
|
201
218
|
|
|
202
|
-
@dataclasses.dataclass(
|
|
219
|
+
@dataclasses.dataclass(**DATACLASS_SLOTS, **DATACLASS_KW_ONLY)
|
|
203
220
|
class _PerFieldValidatorWrapper(_ValidatorWrapper):
|
|
204
|
-
fields: list[str]
|
|
221
|
+
fields: list[str] = dataclasses.field(default_factory=list)
|
|
205
222
|
|
|
206
223
|
|
|
207
224
|
def _wrap_validator(func: Callable, is_pydantic_v1_style_validator: Any, decorator_info: _decorators.DecoratorInfo):
|
|
@@ -247,7 +264,6 @@ def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticModelWrapp
|
|
|
247
264
|
|
|
248
265
|
wrapped_validator = _wrap_validator(decorator_wrapper.func, decorator_wrapper.shim, decorator_wrapper.info)
|
|
249
266
|
validators[decorator_wrapper.cls_var_name] = wrapped_validator
|
|
250
|
-
|
|
251
267
|
annotations = {
|
|
252
268
|
name: value
|
|
253
269
|
if not isinstance(value, str)
|
|
@@ -255,6 +271,21 @@ def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticModelWrapp
|
|
|
255
271
|
for name, value in model.__annotations__.items()
|
|
256
272
|
}
|
|
257
273
|
|
|
274
|
+
if sys.version_info >= (3, 10):
|
|
275
|
+
defined_fields = model.__annotations__
|
|
276
|
+
else:
|
|
277
|
+
# Before 3.9, pydantic fills model_fields with all fields -- even the ones that were inherited.
|
|
278
|
+
# So we need to get the list of fields from the AST.
|
|
279
|
+
try:
|
|
280
|
+
defined_fields, _ = _get_field_and_validator_names_from_model(model)
|
|
281
|
+
except OSError: # pragma: no cover
|
|
282
|
+
defined_fields = model.model_fields
|
|
283
|
+
annotations = {
|
|
284
|
+
name: value
|
|
285
|
+
for name, value in annotations.items()
|
|
286
|
+
# We need to filter out fields that were inherited
|
|
287
|
+
if name not in model.model_fields or name in defined_fields
|
|
288
|
+
}
|
|
258
289
|
fields = {
|
|
259
290
|
field_name: PydanticFieldWrapper(
|
|
260
291
|
model.model_fields[field_name],
|
|
@@ -262,6 +293,7 @@ def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticModelWrapp
|
|
|
262
293
|
field_name,
|
|
263
294
|
)
|
|
264
295
|
for field_name in model.__annotations__
|
|
296
|
+
if field_name in defined_fields
|
|
265
297
|
}
|
|
266
298
|
|
|
267
299
|
main_attributes = fields | validators
|
|
@@ -271,6 +303,7 @@ def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticModelWrapp
|
|
|
271
303
|
if attr_name not in main_attributes
|
|
272
304
|
and not (_is_dunder(attr_name) or attr_name in {"_abc_impl", "model_fields", "model_computed_fields"})
|
|
273
305
|
}
|
|
306
|
+
|
|
274
307
|
other_attributes |= {
|
|
275
308
|
"model_config": model.model_config,
|
|
276
309
|
"__module__": model.__module__,
|
|
@@ -287,12 +320,49 @@ def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticModelWrapp
|
|
|
287
320
|
)
|
|
288
321
|
|
|
289
322
|
|
|
323
|
+
@cache
|
|
324
|
+
def _get_field_and_validator_names_from_model(cls: type) -> tuple[set[_FieldName], set[str]]:
|
|
325
|
+
fields = cls.model_fields
|
|
326
|
+
source = inspect.getsource(cls)
|
|
327
|
+
cls_ast = cast(ast.ClassDef, ast.parse(textwrap.dedent(source)).body[0])
|
|
328
|
+
validator_names = (
|
|
329
|
+
_get_validator_info_or_none(node)
|
|
330
|
+
for node in cls_ast.body
|
|
331
|
+
if isinstance(node, ast.FunctionDef) and node.decorator_list
|
|
332
|
+
)
|
|
333
|
+
validator_names = {name for name in validator_names if name is not None}
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
{
|
|
337
|
+
node.target.id
|
|
338
|
+
for node in cls_ast.body
|
|
339
|
+
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id in fields
|
|
340
|
+
},
|
|
341
|
+
validator_names,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _get_validator_info_or_none(method: ast.FunctionDef) -> Union[str, None]:
|
|
346
|
+
for decorator in method.decorator_list:
|
|
347
|
+
# The cases we handle here:
|
|
348
|
+
# * `Name(id="root_validator")`
|
|
349
|
+
# * `Call(func=Name(id="validator"), args=[Constant(value="foo")])`
|
|
350
|
+
# * `Attribute(value=Name(id="pydantic"), attr="root_validator")`
|
|
351
|
+
# * `Call(func=Attribute(value=Name(id="pydantic"), attr="root_validator"), args=[])`
|
|
352
|
+
|
|
353
|
+
if (isinstance(decorator, ast.Call) and ast.unparse(decorator.func).endswith("validator")) or (
|
|
354
|
+
isinstance(decorator, (ast.Name, ast.Attribute)) and ast.unparse(decorator).endswith("validator")
|
|
355
|
+
):
|
|
356
|
+
return method.name
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
|
|
290
360
|
@final
|
|
291
|
-
@dataclasses.dataclass(
|
|
361
|
+
@dataclasses.dataclass(**DATACLASS_SLOTS)
|
|
292
362
|
class _PydanticModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
293
363
|
cls: type[_T_PYDANTIC_MODEL] = dataclasses.field(repr=False)
|
|
294
364
|
name: str
|
|
295
|
-
doc: str
|
|
365
|
+
doc: Union[str, None] = dataclasses.field(repr=False)
|
|
296
366
|
fields: Annotated[
|
|
297
367
|
dict["_FieldName", PydanticFieldWrapper],
|
|
298
368
|
Doc(
|
|
@@ -300,10 +370,10 @@ class _PydanticModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
|
300
370
|
"I.e. The ones that were either defined or overridden "
|
|
301
371
|
),
|
|
302
372
|
] = dataclasses.field(repr=False)
|
|
303
|
-
validators: dict[str, _PerFieldValidatorWrapper
|
|
373
|
+
validators: dict[str, Union[_PerFieldValidatorWrapper, _ValidatorWrapper]] = dataclasses.field(repr=False)
|
|
304
374
|
other_attributes: dict[str, Any] = dataclasses.field(repr=False)
|
|
305
375
|
annotations: dict[str, Any] = dataclasses.field(repr=False)
|
|
306
|
-
_parents: list[Self]
|
|
376
|
+
_parents: Union[list[Self], None] = dataclasses.field(init=False, default=None, repr=False)
|
|
307
377
|
|
|
308
378
|
def __post_init__(self):
|
|
309
379
|
# This isn't actually supposed to run, it's just a precaution
|
|
@@ -375,7 +445,6 @@ class _PydanticModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
|
375
445
|
if not validator.is_deleted and type(validator) == _ValidatorWrapper # noqa: E721
|
|
376
446
|
}
|
|
377
447
|
fields = {name: field.generate_field_copy(generator) for name, field in self.fields.items()}
|
|
378
|
-
|
|
379
448
|
model_copy = type(self.cls)(
|
|
380
449
|
self.name,
|
|
381
450
|
tuple(generator[cast(type[BaseModel], base)] for base in self.cls.__bases__),
|
|
@@ -389,13 +458,12 @@ class _PydanticModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
|
389
458
|
"__qualname__": self.cls.__qualname__.removesuffix(self.cls.__name__) + self.name,
|
|
390
459
|
},
|
|
391
460
|
)
|
|
392
|
-
|
|
393
461
|
model_copy.__cadwyn_original_model__ = self.cls
|
|
394
462
|
return model_copy
|
|
395
463
|
|
|
396
464
|
|
|
397
465
|
def is_regular_function(call: Callable):
|
|
398
|
-
return isinstance(call, types.FunctionType
|
|
466
|
+
return isinstance(call, (types.FunctionType, types.MethodType))
|
|
399
467
|
|
|
400
468
|
|
|
401
469
|
class _CallableWrapper:
|
|
@@ -457,7 +525,7 @@ class _AnnotationTransformer:
|
|
|
457
525
|
for key, value in annotation.items()
|
|
458
526
|
}
|
|
459
527
|
|
|
460
|
-
elif isinstance(annotation, list
|
|
528
|
+
elif isinstance(annotation, (list, tuple)):
|
|
461
529
|
return type(annotation)(self.change_version_of_annotation(v) for v in annotation)
|
|
462
530
|
else:
|
|
463
531
|
return self.change_versions_of_a_non_container_annotation(annotation)
|
|
@@ -486,7 +554,7 @@ class _AnnotationTransformer:
|
|
|
486
554
|
self._remake_endpoint_dependencies(route)
|
|
487
555
|
|
|
488
556
|
def _change_version_of_a_non_container_annotation(self, annotation: Any) -> Any:
|
|
489
|
-
if isinstance(annotation, _BaseGenericAlias
|
|
557
|
+
if isinstance(annotation, (_BaseGenericAlias, types.GenericAlias)):
|
|
490
558
|
return get_origin(annotation)[tuple(self.change_version_of_annotation(arg) for arg in get_args(annotation))]
|
|
491
559
|
elif isinstance(annotation, fastapi.params.Security):
|
|
492
560
|
return fastapi.params.Security(
|
|
@@ -499,12 +567,12 @@ class _AnnotationTransformer:
|
|
|
499
567
|
self.change_version_of_annotation(annotation.dependency),
|
|
500
568
|
use_cache=annotation.use_cache,
|
|
501
569
|
)
|
|
502
|
-
elif isinstance(annotation, UnionType):
|
|
570
|
+
elif isinstance(annotation, UnionType): # pragma: no cover
|
|
503
571
|
getitem = typing.Union.__getitem__ # pyright: ignore[reportAttributeAccessIssue]
|
|
504
572
|
return getitem(
|
|
505
573
|
tuple(self.change_version_of_annotation(a) for a in get_args(annotation)),
|
|
506
574
|
)
|
|
507
|
-
elif annotation is Any or isinstance(annotation,
|
|
575
|
+
elif annotation is typing.Any or annotation is typing_extensions.Any or isinstance(annotation, NewType):
|
|
508
576
|
return annotation
|
|
509
577
|
elif isinstance(annotation, type):
|
|
510
578
|
return self._change_version_of_type(annotation)
|
|
@@ -527,7 +595,7 @@ class _AnnotationTransformer:
|
|
|
527
595
|
return annotation
|
|
528
596
|
|
|
529
597
|
def _change_version_of_type(self, annotation: type):
|
|
530
|
-
if lenient_issubclass(annotation, BaseModel
|
|
598
|
+
if lenient_issubclass(annotation, (BaseModel, Enum)):
|
|
531
599
|
return self.generator[annotation]
|
|
532
600
|
else:
|
|
533
601
|
return annotation
|
|
@@ -648,7 +716,7 @@ class SchemaGenerator:
|
|
|
648
716
|
def __getitem__(self, model: type[_T_ANY_MODEL], /) -> type[_T_ANY_MODEL]:
|
|
649
717
|
if (
|
|
650
718
|
not isinstance(model, type)
|
|
651
|
-
or not lenient_issubclass(model, BaseModel
|
|
719
|
+
or not lenient_issubclass(model, (BaseModel, Enum))
|
|
652
720
|
or model in (BaseModel, RootModel)
|
|
653
721
|
):
|
|
654
722
|
return model
|
|
@@ -668,8 +736,8 @@ class SchemaGenerator:
|
|
|
668
736
|
def _get_wrapper_for_model(self, model: type[Enum]) -> "_EnumWrapper[Enum]": ...
|
|
669
737
|
|
|
670
738
|
def _get_wrapper_for_model(
|
|
671
|
-
self, model: type[BaseModel
|
|
672
|
-
) -> "_PydanticModelWrapper[BaseModel]
|
|
739
|
+
self, model: type[Union[BaseModel, Enum]]
|
|
740
|
+
) -> "Union[_PydanticModelWrapper[BaseModel], _EnumWrapper[Enum]]":
|
|
673
741
|
model = _unwrap_model(model)
|
|
674
742
|
|
|
675
743
|
if model in self.model_bundle.schemas:
|
|
@@ -730,14 +798,14 @@ def _migrate_classes(context: _RuntimeSchemaGenContext) -> None:
|
|
|
730
798
|
|
|
731
799
|
def _apply_alter_schema_instructions(
|
|
732
800
|
modified_schemas: dict[type, _PydanticModelWrapper],
|
|
733
|
-
alter_schema_instructions: Sequence[AlterSchemaSubInstruction
|
|
801
|
+
alter_schema_instructions: Sequence[Union[AlterSchemaSubInstruction, SchemaHadInstruction]],
|
|
734
802
|
version_change_name: str,
|
|
735
803
|
) -> None:
|
|
736
804
|
for alter_schema_instruction in alter_schema_instructions:
|
|
737
805
|
schema_info = modified_schemas[alter_schema_instruction.schema]
|
|
738
806
|
if isinstance(alter_schema_instruction, FieldExistedAsInstruction):
|
|
739
807
|
_add_field_to_model(schema_info, modified_schemas, alter_schema_instruction, version_change_name)
|
|
740
|
-
elif isinstance(alter_schema_instruction, FieldHadInstruction
|
|
808
|
+
elif isinstance(alter_schema_instruction, (FieldHadInstruction, FieldDidntHaveInstruction)):
|
|
741
809
|
_change_field_in_model(
|
|
742
810
|
schema_info,
|
|
743
811
|
modified_schemas,
|
|
@@ -747,7 +815,7 @@ def _apply_alter_schema_instructions(
|
|
|
747
815
|
elif isinstance(alter_schema_instruction, FieldDidntExistInstruction):
|
|
748
816
|
_delete_field_from_model(schema_info, alter_schema_instruction.name, version_change_name)
|
|
749
817
|
elif isinstance(alter_schema_instruction, ValidatorExistedInstruction):
|
|
750
|
-
validator_name = alter_schema_instruction.validator
|
|
818
|
+
validator_name = get_name_of_function_wrapped_in_pydantic_validator(alter_schema_instruction.validator)
|
|
751
819
|
raw_validator = cast(
|
|
752
820
|
pydantic._internal._decorators.PydanticDescriptorProxy, alter_schema_instruction.validator
|
|
753
821
|
)
|
|
@@ -838,7 +906,7 @@ def _add_field_to_model(
|
|
|
838
906
|
def _change_field_in_model(
|
|
839
907
|
model: _PydanticModelWrapper,
|
|
840
908
|
schemas: "dict[type, _PydanticModelWrapper]",
|
|
841
|
-
alter_schema_instruction: FieldHadInstruction
|
|
909
|
+
alter_schema_instruction: Union[FieldHadInstruction, FieldDidntHaveInstruction],
|
|
842
910
|
version_change_name: str,
|
|
843
911
|
):
|
|
844
912
|
defined_annotations = model._get_defined_annotations_through_mro(schemas)
|
|
@@ -879,7 +947,7 @@ def _change_field(
|
|
|
879
947
|
version_change_name: str,
|
|
880
948
|
defined_annotations: dict[str, Any],
|
|
881
949
|
field: PydanticFieldWrapper,
|
|
882
|
-
annotation: Any
|
|
950
|
+
annotation: Union[Any, None],
|
|
883
951
|
):
|
|
884
952
|
if alter_schema_instruction.type is not Sentinel:
|
|
885
953
|
if field.annotation == alter_schema_instruction.type:
|
cadwyn/structure/common.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import ParamSpec, TypeAlias, TypeVar
|
|
4
3
|
|
|
5
4
|
from pydantic import BaseModel
|
|
5
|
+
from typing_extensions import ParamSpec, TypeAlias, TypeVar
|
|
6
|
+
|
|
7
|
+
from cadwyn._utils import DATACLASS_KW_ONLY, DATACLASS_SLOTS
|
|
6
8
|
|
|
7
9
|
VersionedModel = BaseModel
|
|
8
10
|
VersionType: TypeAlias = str
|
|
@@ -11,6 +13,6 @@ _R = TypeVar("_R")
|
|
|
11
13
|
Endpoint: TypeAlias = Callable[_P, _R]
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
@dataclass(
|
|
16
|
+
@dataclass(**DATACLASS_SLOTS, **DATACLASS_KW_ONLY)
|
|
15
17
|
class _HiddenAttributeMixin:
|
|
16
|
-
is_hidden_from_changelog: bool
|
|
18
|
+
is_hidden_from_changelog: bool
|
cadwyn/structure/data.py
CHANGED
|
@@ -2,10 +2,11 @@ import functools
|
|
|
2
2
|
import inspect
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import ClassVar, Union, cast
|
|
6
6
|
|
|
7
7
|
from fastapi import Request, Response
|
|
8
8
|
from starlette.datastructures import MutableHeaders
|
|
9
|
+
from typing_extensions import Any, ParamSpec, overload
|
|
9
10
|
|
|
10
11
|
from cadwyn._utils import same_definition_as_in
|
|
11
12
|
from cadwyn.structure.endpoints import _validate_that_strings_are_valid_http_methods
|
|
@@ -82,7 +83,7 @@ class _AlterDataInstruction:
|
|
|
82
83
|
def __set_name__(self, owner: type, name: str):
|
|
83
84
|
self.owner = owner
|
|
84
85
|
|
|
85
|
-
def __call__(self, __request_or_response: RequestInfo
|
|
86
|
+
def __call__(self, __request_or_response: Union[RequestInfo, ResponseInfo], /) -> None:
|
|
86
87
|
return self.transformer(__request_or_response)
|
|
87
88
|
|
|
88
89
|
|
|
@@ -127,8 +128,8 @@ def convert_request_to_next_version_for(path: str, methods: list[str], /) -> "ty
|
|
|
127
128
|
|
|
128
129
|
|
|
129
130
|
def convert_request_to_next_version_for(
|
|
130
|
-
schema_or_path: type
|
|
131
|
-
methods_or_second_schema: list[str]
|
|
131
|
+
schema_or_path: Union[type, str],
|
|
132
|
+
methods_or_second_schema: Union[list[str], None, type] = None,
|
|
132
133
|
/,
|
|
133
134
|
*additional_schemas: type,
|
|
134
135
|
check_usage: bool = True,
|
|
@@ -199,8 +200,8 @@ def convert_response_to_previous_version_for(
|
|
|
199
200
|
|
|
200
201
|
|
|
201
202
|
def convert_response_to_previous_version_for(
|
|
202
|
-
schema_or_path: type
|
|
203
|
-
methods_or_second_schema: list[str]
|
|
203
|
+
schema_or_path: Union[type, str],
|
|
204
|
+
methods_or_second_schema: Union[list[str], type, None] = None,
|
|
204
205
|
/,
|
|
205
206
|
*additional_schemas: type,
|
|
206
207
|
migrate_http_errors: bool = False,
|
|
@@ -233,7 +234,9 @@ def convert_response_to_previous_version_for(
|
|
|
233
234
|
|
|
234
235
|
|
|
235
236
|
def _validate_decorator_args(
|
|
236
|
-
schema_or_path: type
|
|
237
|
+
schema_or_path: Union[type, str],
|
|
238
|
+
methods_or_second_schema: Union[list[str], type, None],
|
|
239
|
+
additional_schemas: tuple[type, ...],
|
|
237
240
|
) -> None:
|
|
238
241
|
if isinstance(schema_or_path, str):
|
|
239
242
|
if not isinstance(methods_or_second_schema, list):
|
cadwyn/structure/endpoints.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections.abc import Callable, Collection, Sequence
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, Union
|
|
5
5
|
|
|
6
6
|
from fastapi import Response
|
|
7
7
|
from fastapi.params import Depends
|
|
@@ -10,13 +10,13 @@ from starlette.routing import BaseRoute
|
|
|
10
10
|
|
|
11
11
|
from cadwyn.exceptions import LintingError
|
|
12
12
|
|
|
13
|
-
from .._utils import Sentinel
|
|
13
|
+
from .._utils import DATACLASS_SLOTS, Sentinel
|
|
14
14
|
from .common import _HiddenAttributeMixin
|
|
15
15
|
|
|
16
16
|
HTTP_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"}
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
@dataclass(
|
|
19
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
20
20
|
class EndpointAttributesPayload:
|
|
21
21
|
# Fastapi API routes also have "endpoint" and "dependency_overrides_provider" fields.
|
|
22
22
|
# We do not use them because:
|
|
@@ -33,7 +33,7 @@ class EndpointAttributesPayload:
|
|
|
33
33
|
path: str
|
|
34
34
|
response_model: Any
|
|
35
35
|
status_code: int
|
|
36
|
-
tags: list[str
|
|
36
|
+
tags: list[Union[str, Enum]]
|
|
37
37
|
# Adding/removing dependencies between versions seems like a bad choice.
|
|
38
38
|
# It makes the system overly complex. Instead, we allow people to
|
|
39
39
|
# overwrite all dependencies of a route at once. Hence you always know exactly
|
|
@@ -43,7 +43,7 @@ class EndpointAttributesPayload:
|
|
|
43
43
|
summary: str
|
|
44
44
|
description: str
|
|
45
45
|
response_description: str
|
|
46
|
-
responses: dict[int
|
|
46
|
+
responses: dict[Union[int, str], dict[str, Any]]
|
|
47
47
|
deprecated: bool
|
|
48
48
|
methods: set[str]
|
|
49
49
|
operation_id: str
|
|
@@ -55,47 +55,49 @@ class EndpointAttributesPayload:
|
|
|
55
55
|
generate_unique_id_function: Callable[[APIRoute], str]
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
@dataclass(
|
|
58
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
59
59
|
class EndpointHadInstruction(_HiddenAttributeMixin):
|
|
60
60
|
endpoint_path: str
|
|
61
61
|
endpoint_methods: set[str]
|
|
62
|
-
endpoint_func_name: str
|
|
62
|
+
endpoint_func_name: Union[str, None]
|
|
63
63
|
attributes: EndpointAttributesPayload
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
@dataclass(
|
|
66
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
67
67
|
class EndpointExistedInstruction(_HiddenAttributeMixin):
|
|
68
68
|
endpoint_path: str
|
|
69
69
|
endpoint_methods: set[str]
|
|
70
|
-
endpoint_func_name: str
|
|
70
|
+
endpoint_func_name: Union[str, None]
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
@dataclass(
|
|
73
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
74
74
|
class EndpointDidntExistInstruction(_HiddenAttributeMixin):
|
|
75
75
|
endpoint_path: str
|
|
76
76
|
endpoint_methods: set[str]
|
|
77
|
-
endpoint_func_name: str
|
|
77
|
+
endpoint_func_name: Union[str, None]
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
@dataclass(
|
|
80
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
81
81
|
class EndpointInstructionFactory:
|
|
82
82
|
endpoint_path: str
|
|
83
83
|
endpoint_methods: set[str]
|
|
84
|
-
endpoint_func_name: str
|
|
84
|
+
endpoint_func_name: Union[str, None]
|
|
85
85
|
|
|
86
86
|
@property
|
|
87
87
|
def didnt_exist(self) -> EndpointDidntExistInstruction:
|
|
88
88
|
return EndpointDidntExistInstruction(
|
|
89
|
-
|
|
90
|
-
self.
|
|
89
|
+
is_hidden_from_changelog=False,
|
|
90
|
+
endpoint_path=self.endpoint_path,
|
|
91
|
+
endpoint_methods=self.endpoint_methods,
|
|
91
92
|
endpoint_func_name=self.endpoint_func_name,
|
|
92
93
|
)
|
|
93
94
|
|
|
94
95
|
@property
|
|
95
96
|
def existed(self) -> EndpointExistedInstruction:
|
|
96
97
|
return EndpointExistedInstruction(
|
|
97
|
-
|
|
98
|
-
self.
|
|
98
|
+
is_hidden_from_changelog=False,
|
|
99
|
+
endpoint_path=self.endpoint_path,
|
|
100
|
+
endpoint_methods=self.endpoint_methods,
|
|
99
101
|
endpoint_func_name=self.endpoint_func_name,
|
|
100
102
|
)
|
|
101
103
|
|
|
@@ -105,12 +107,12 @@ class EndpointInstructionFactory:
|
|
|
105
107
|
path: str = Sentinel,
|
|
106
108
|
response_model: Any = Sentinel,
|
|
107
109
|
status_code: int = Sentinel,
|
|
108
|
-
tags: list[str
|
|
110
|
+
tags: list[Union[str, Enum]] = Sentinel,
|
|
109
111
|
dependencies: Sequence[Depends] = Sentinel,
|
|
110
112
|
summary: str = Sentinel,
|
|
111
113
|
description: str = Sentinel,
|
|
112
114
|
response_description: str = Sentinel,
|
|
113
|
-
responses: dict[int
|
|
115
|
+
responses: dict[Union[int, str], dict[str, Any]] = Sentinel,
|
|
114
116
|
deprecated: bool = Sentinel,
|
|
115
117
|
methods: list[str] = Sentinel,
|
|
116
118
|
operation_id: str = Sentinel,
|
|
@@ -122,6 +124,7 @@ class EndpointInstructionFactory:
|
|
|
122
124
|
generate_unique_id_function: Callable[[APIRoute], str] = Sentinel,
|
|
123
125
|
):
|
|
124
126
|
return EndpointHadInstruction(
|
|
127
|
+
is_hidden_from_changelog=False,
|
|
125
128
|
endpoint_path=self.endpoint_path,
|
|
126
129
|
endpoint_methods=self.endpoint_methods,
|
|
127
130
|
endpoint_func_name=self.endpoint_func_name,
|
|
@@ -148,7 +151,7 @@ class EndpointInstructionFactory:
|
|
|
148
151
|
)
|
|
149
152
|
|
|
150
153
|
|
|
151
|
-
def endpoint(path: str, methods: list[str], /, *, func_name: str
|
|
154
|
+
def endpoint(path: str, methods: list[str], /, *, func_name: Union[str, None] = None) -> EndpointInstructionFactory:
|
|
152
155
|
_validate_that_strings_are_valid_http_methods(methods)
|
|
153
156
|
|
|
154
157
|
return EndpointInstructionFactory(path, set(methods), func_name)
|
|
@@ -164,4 +167,4 @@ def _validate_that_strings_are_valid_http_methods(methods: Collection[str]):
|
|
|
164
167
|
)
|
|
165
168
|
|
|
166
169
|
|
|
167
|
-
AlterEndpointSubInstruction = EndpointDidntExistInstruction
|
|
170
|
+
AlterEndpointSubInstruction = Union[EndpointDidntExistInstruction, EndpointExistedInstruction, EndpointHadInstruction]
|
cadwyn/structure/enums.py
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
from collections.abc import Mapping
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, Union
|
|
5
|
+
|
|
6
|
+
from cadwyn._utils import DATACLASS_SLOTS
|
|
5
7
|
|
|
6
8
|
from .common import _HiddenAttributeMixin
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
@dataclass(
|
|
11
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
10
12
|
class EnumHadMembersInstruction(_HiddenAttributeMixin):
|
|
11
13
|
enum: type[Enum]
|
|
12
14
|
members: Mapping[str, Any]
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
@dataclass(
|
|
17
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
16
18
|
class EnumDidntHaveMembersInstruction(_HiddenAttributeMixin):
|
|
17
19
|
enum: type[Enum]
|
|
18
20
|
members: tuple[str, ...]
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
@dataclass(
|
|
23
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
22
24
|
class EnumInstructionFactory:
|
|
23
25
|
enum_class: type[Enum]
|
|
24
26
|
|
|
25
27
|
def had(self, **enum_member_to_value_mapping: Any) -> EnumHadMembersInstruction:
|
|
26
|
-
return EnumHadMembersInstruction(
|
|
28
|
+
return EnumHadMembersInstruction(
|
|
29
|
+
is_hidden_from_changelog=False, enum=self.enum_class, members=enum_member_to_value_mapping
|
|
30
|
+
)
|
|
27
31
|
|
|
28
32
|
def didnt_have(self, *enum_members: str) -> EnumDidntHaveMembersInstruction:
|
|
29
|
-
return EnumDidntHaveMembersInstruction(
|
|
33
|
+
return EnumDidntHaveMembersInstruction(
|
|
34
|
+
is_hidden_from_changelog=False, enum=self.enum_class, members=enum_members
|
|
35
|
+
)
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
def enum(enum_class: type[Enum], /) -> EnumInstructionFactory:
|
|
33
39
|
return EnumInstructionFactory(enum_class)
|
|
34
40
|
|
|
35
41
|
|
|
36
|
-
AlterEnumSubInstruction = EnumHadMembersInstruction
|
|
42
|
+
AlterEnumSubInstruction = Union[EnumHadMembersInstruction, EnumDidntHaveMembersInstruction]
|