cadwyn 4.1.0__py3-none-any.whl → 4.2.1__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 +2 -0
- cadwyn/_render.py +2 -2
- cadwyn/applications.py +28 -3
- cadwyn/changelogs.py +499 -0
- cadwyn/route_generation.py +6 -9
- cadwyn/schema_generation.py +45 -31
- cadwyn/structure/common.py +6 -0
- cadwyn/structure/endpoints.py +6 -5
- cadwyn/structure/enums.py +4 -2
- cadwyn/structure/schemas.py +15 -38
- cadwyn/structure/versions.py +25 -16
- {cadwyn-4.1.0.dist-info → cadwyn-4.2.1.dist-info}/METADATA +2 -1
- cadwyn-4.2.1.dist-info/RECORD +28 -0
- cadwyn-4.1.0.dist-info/RECORD +0 -27
- {cadwyn-4.1.0.dist-info → cadwyn-4.2.1.dist-info}/LICENSE +0 -0
- {cadwyn-4.1.0.dist-info → cadwyn-4.2.1.dist-info}/WHEEL +0 -0
- {cadwyn-4.1.0.dist-info → cadwyn-4.2.1.dist-info}/entry_points.txt +0 -0
cadwyn/schema_generation.py
CHANGED
|
@@ -20,6 +20,7 @@ from typing import (
|
|
|
20
20
|
final,
|
|
21
21
|
get_args,
|
|
22
22
|
get_origin,
|
|
23
|
+
overload,
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
import fastapi.params
|
|
@@ -104,6 +105,8 @@ class PydanticFieldWrapper:
|
|
|
104
105
|
init_model_field: dataclasses.InitVar[FieldInfo]
|
|
105
106
|
|
|
106
107
|
annotation: Any
|
|
108
|
+
name_from_newer_version: str
|
|
109
|
+
|
|
107
110
|
passed_field_attributes: dict[str, Any] = dataclasses.field(init=False)
|
|
108
111
|
|
|
109
112
|
def __post_init__(self, init_model_field: FieldInfo):
|
|
@@ -136,7 +139,7 @@ def _extract_passed_field_attributes(field_info: FieldInfo):
|
|
|
136
139
|
@dataclasses.dataclass(slots=True)
|
|
137
140
|
class _ModelBundle:
|
|
138
141
|
enums: dict[type[Enum], "_EnumWrapper"]
|
|
139
|
-
schemas: dict[type[BaseModel], "
|
|
142
|
+
schemas: dict[type[BaseModel], "_PydanticModelWrapper"]
|
|
140
143
|
|
|
141
144
|
|
|
142
145
|
@dataclasses.dataclass(slots=True, kw_only=True)
|
|
@@ -222,7 +225,7 @@ def _is_dunder(attr_name: str):
|
|
|
222
225
|
return attr_name.startswith("__") and attr_name.endswith("__")
|
|
223
226
|
|
|
224
227
|
|
|
225
|
-
def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "
|
|
228
|
+
def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticModelWrapper[_T_PYDANTIC_MODEL]":
|
|
226
229
|
decorators = _get_model_decorators(model)
|
|
227
230
|
validators = {}
|
|
228
231
|
for decorator_wrapper in decorators:
|
|
@@ -232,7 +235,7 @@ def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticRuntimeMod
|
|
|
232
235
|
wrapped_validator = _wrap_validator(decorator_wrapper.func, decorator_wrapper.shim, decorator_wrapper.info)
|
|
233
236
|
validators[decorator_wrapper.cls_var_name] = wrapped_validator
|
|
234
237
|
fields = {
|
|
235
|
-
field_name: PydanticFieldWrapper(model.model_fields[field_name], model.__annotations__[field_name])
|
|
238
|
+
field_name: PydanticFieldWrapper(model.model_fields[field_name], model.__annotations__[field_name], field_name)
|
|
236
239
|
for field_name in model.__annotations__
|
|
237
240
|
}
|
|
238
241
|
|
|
@@ -248,7 +251,7 @@ def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticRuntimeMod
|
|
|
248
251
|
"__module__": model.__module__,
|
|
249
252
|
"__qualname__": model.__qualname__,
|
|
250
253
|
}
|
|
251
|
-
return
|
|
254
|
+
return _PydanticModelWrapper(
|
|
252
255
|
model,
|
|
253
256
|
name=model.__name__,
|
|
254
257
|
doc=model.__doc__,
|
|
@@ -261,20 +264,20 @@ def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticRuntimeMod
|
|
|
261
264
|
|
|
262
265
|
@final
|
|
263
266
|
@dataclasses.dataclass(slots=True)
|
|
264
|
-
class
|
|
265
|
-
cls: type[_T_PYDANTIC_MODEL]
|
|
267
|
+
class _PydanticModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
268
|
+
cls: type[_T_PYDANTIC_MODEL] = dataclasses.field(repr=False)
|
|
266
269
|
name: str
|
|
267
|
-
doc: str | None
|
|
270
|
+
doc: str | None = dataclasses.field(repr=False)
|
|
268
271
|
fields: Annotated[
|
|
269
272
|
dict["_FieldName", PydanticFieldWrapper],
|
|
270
273
|
Doc(
|
|
271
274
|
"Fields that belong to this model, not to its parents. I.e. The ones that were either defined or overriden "
|
|
272
275
|
),
|
|
273
|
-
]
|
|
274
|
-
validators: dict[str, _PerFieldValidatorWrapper | _ValidatorWrapper]
|
|
275
|
-
other_attributes: dict[str, Any]
|
|
276
|
-
annotations: dict[str, Any]
|
|
277
|
-
_parents: list[Self] | None = dataclasses.field(init=False, default=None)
|
|
276
|
+
] = dataclasses.field(repr=False)
|
|
277
|
+
validators: dict[str, _PerFieldValidatorWrapper | _ValidatorWrapper] = dataclasses.field(repr=False)
|
|
278
|
+
other_attributes: dict[str, Any] = dataclasses.field(repr=False)
|
|
279
|
+
annotations: dict[str, Any] = dataclasses.field(repr=False)
|
|
280
|
+
_parents: list[Self] | None = dataclasses.field(init=False, default=None, repr=False)
|
|
278
281
|
|
|
279
282
|
def __post_init__(self):
|
|
280
283
|
# This isn't actually supposed to run, it's just a precaution
|
|
@@ -291,7 +294,7 @@ class _PydanticRuntimeModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
|
291
294
|
)
|
|
292
295
|
|
|
293
296
|
def __deepcopy__(self, memo: dict[int, Any]):
|
|
294
|
-
result =
|
|
297
|
+
result = _PydanticModelWrapper(
|
|
295
298
|
self.cls,
|
|
296
299
|
name=self.name,
|
|
297
300
|
doc=self.doc,
|
|
@@ -303,6 +306,9 @@ class _PydanticRuntimeModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
|
303
306
|
memo[id(self)] = result
|
|
304
307
|
return result
|
|
305
308
|
|
|
309
|
+
def __hash__(self) -> int:
|
|
310
|
+
return hash(id(self))
|
|
311
|
+
|
|
306
312
|
def _get_parents(self, schemas: "dict[type, Self]"):
|
|
307
313
|
if self._parents is not None:
|
|
308
314
|
return self._parents
|
|
@@ -345,7 +351,7 @@ class _PydanticRuntimeModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
|
345
351
|
fields = {name: field.generate_field_copy(generator) for name, field in self.fields.items()}
|
|
346
352
|
model_copy = type(self.cls)(
|
|
347
353
|
self.name,
|
|
348
|
-
tuple(generator[base] for base in self.cls.__bases__),
|
|
354
|
+
tuple(generator[cast(type[BaseModel], base)] for base in self.cls.__bases__),
|
|
349
355
|
self.other_attributes
|
|
350
356
|
| per_field_validators
|
|
351
357
|
| root_validators
|
|
@@ -599,9 +605,9 @@ class SchemaGenerator:
|
|
|
599
605
|
for k, wrapper in (self.model_bundle.schemas | self.model_bundle.enums).items()
|
|
600
606
|
}
|
|
601
607
|
|
|
602
|
-
def __getitem__(self, model: type, /) ->
|
|
608
|
+
def __getitem__(self, model: type[_T_ANY_MODEL], /) -> type[_T_ANY_MODEL]:
|
|
603
609
|
if not isinstance(model, type) or not issubclass(model, BaseModel | Enum) or model in (BaseModel, RootModel):
|
|
604
|
-
return model
|
|
610
|
+
return model # pyright: ignore[reportReturnType]
|
|
605
611
|
model = _unwrap_model(model)
|
|
606
612
|
|
|
607
613
|
if model in self.concrete_models:
|
|
@@ -614,9 +620,14 @@ class SchemaGenerator:
|
|
|
614
620
|
self.concrete_models[model] = model_copy
|
|
615
621
|
return model_copy
|
|
616
622
|
|
|
623
|
+
@overload
|
|
624
|
+
def _get_wrapper_for_model(self, model: type[BaseModel]) -> "_PydanticModelWrapper[BaseModel]": ...
|
|
625
|
+
@overload
|
|
626
|
+
def _get_wrapper_for_model(self, model: type[Enum]) -> "_EnumWrapper[Enum]": ...
|
|
627
|
+
|
|
617
628
|
def _get_wrapper_for_model(
|
|
618
629
|
self, model: type[BaseModel | Enum]
|
|
619
|
-
) -> "
|
|
630
|
+
) -> "_PydanticModelWrapper[BaseModel] | _EnumWrapper[Enum]":
|
|
620
631
|
model = _unwrap_model(model)
|
|
621
632
|
|
|
622
633
|
if model in self.model_bundle.schemas:
|
|
@@ -660,7 +671,7 @@ def _create_model_bundle(versions: "VersionBundle"):
|
|
|
660
671
|
|
|
661
672
|
|
|
662
673
|
def _migrate_classes(context: _RuntimeSchemaGenContext) -> None:
|
|
663
|
-
for version_change in context.current_version.
|
|
674
|
+
for version_change in context.current_version.changes:
|
|
664
675
|
_apply_alter_schema_instructions(
|
|
665
676
|
context.models.schemas,
|
|
666
677
|
version_change.alter_schema_instructions,
|
|
@@ -674,7 +685,7 @@ def _migrate_classes(context: _RuntimeSchemaGenContext) -> None:
|
|
|
674
685
|
|
|
675
686
|
|
|
676
687
|
def _apply_alter_schema_instructions(
|
|
677
|
-
modified_schemas: dict[type,
|
|
688
|
+
modified_schemas: dict[type, _PydanticModelWrapper],
|
|
678
689
|
alter_schema_instructions: Sequence[AlterSchemaSubInstruction | SchemaHadInstruction],
|
|
679
690
|
version_change_name: str,
|
|
680
691
|
) -> None:
|
|
@@ -747,7 +758,7 @@ def _apply_alter_enum_instructions(
|
|
|
747
758
|
|
|
748
759
|
|
|
749
760
|
def _change_model(
|
|
750
|
-
model:
|
|
761
|
+
model: _PydanticModelWrapper,
|
|
751
762
|
alter_schema_instruction: SchemaHadInstruction,
|
|
752
763
|
version_change_name: str,
|
|
753
764
|
):
|
|
@@ -761,8 +772,8 @@ def _change_model(
|
|
|
761
772
|
|
|
762
773
|
|
|
763
774
|
def _add_field_to_model(
|
|
764
|
-
model:
|
|
765
|
-
schemas: "dict[type,
|
|
775
|
+
model: _PydanticModelWrapper,
|
|
776
|
+
schemas: "dict[type, _PydanticModelWrapper]",
|
|
766
777
|
alter_schema_instruction: FieldExistedAsInstruction,
|
|
767
778
|
version_change_name: str,
|
|
768
779
|
):
|
|
@@ -773,14 +784,16 @@ def _add_field_to_model(
|
|
|
773
784
|
f'in "{version_change_name}" but there is already a field with that name.',
|
|
774
785
|
)
|
|
775
786
|
|
|
776
|
-
field = PydanticFieldWrapper(
|
|
787
|
+
field = PydanticFieldWrapper(
|
|
788
|
+
alter_schema_instruction.field, alter_schema_instruction.field.annotation, alter_schema_instruction.name
|
|
789
|
+
)
|
|
777
790
|
model.fields[alter_schema_instruction.name] = field
|
|
778
791
|
model.annotations[alter_schema_instruction.name] = alter_schema_instruction.field.annotation
|
|
779
792
|
|
|
780
793
|
|
|
781
794
|
def _change_field_in_model(
|
|
782
|
-
model:
|
|
783
|
-
schemas: "dict[type,
|
|
795
|
+
model: _PydanticModelWrapper,
|
|
796
|
+
schemas: "dict[type, _PydanticModelWrapper]",
|
|
784
797
|
alter_schema_instruction: FieldHadInstruction | FieldDidntHaveInstruction,
|
|
785
798
|
version_change_name: str,
|
|
786
799
|
):
|
|
@@ -817,7 +830,7 @@ def _change_field_in_model(
|
|
|
817
830
|
|
|
818
831
|
|
|
819
832
|
def _change_field(
|
|
820
|
-
model:
|
|
833
|
+
model: _PydanticModelWrapper,
|
|
821
834
|
alter_schema_instruction: FieldHadInstruction,
|
|
822
835
|
version_change_name: str,
|
|
823
836
|
defined_annotations: dict[str, Any],
|
|
@@ -861,7 +874,7 @@ def _change_field(
|
|
|
861
874
|
|
|
862
875
|
|
|
863
876
|
def _delete_field_attributes(
|
|
864
|
-
model:
|
|
877
|
+
model: _PydanticModelWrapper,
|
|
865
878
|
alter_schema_instruction: FieldDidntHaveInstruction,
|
|
866
879
|
version_change_name: str,
|
|
867
880
|
field: PydanticFieldWrapper,
|
|
@@ -884,7 +897,7 @@ def _delete_field_attributes(
|
|
|
884
897
|
)
|
|
885
898
|
|
|
886
899
|
|
|
887
|
-
def _delete_field_from_model(model:
|
|
900
|
+
def _delete_field_from_model(model: _PydanticModelWrapper, field_name: str, version_change_name: str):
|
|
888
901
|
if field_name not in model.fields:
|
|
889
902
|
raise InvalidGenerationInstructionError(
|
|
890
903
|
f'You tried to delete a field "{field_name}" from "{model.name}" '
|
|
@@ -906,10 +919,11 @@ class _DummyEnum(Enum):
|
|
|
906
919
|
|
|
907
920
|
@final
|
|
908
921
|
class _EnumWrapper(Generic[_T_ENUM]):
|
|
909
|
-
__slots__ = "cls", "members"
|
|
922
|
+
__slots__ = "cls", "members", "name"
|
|
910
923
|
|
|
911
924
|
def __init__(self, cls: type[_T_ENUM]):
|
|
912
925
|
self.cls = _unwrap_model(cls)
|
|
926
|
+
self.name = cls.__name__
|
|
913
927
|
self.members = {member.name: member.value for member in cls}
|
|
914
928
|
|
|
915
929
|
def __deepcopy__(self, memo: Any):
|
|
@@ -919,13 +933,13 @@ class _EnumWrapper(Generic[_T_ENUM]):
|
|
|
919
933
|
return result
|
|
920
934
|
|
|
921
935
|
def generate_model_copy(self, generator: "SchemaGenerator") -> type[_T_ENUM]:
|
|
922
|
-
enum_dict = Enum.__prepare__(self.
|
|
936
|
+
enum_dict = Enum.__prepare__(self.name, self.cls.__bases__)
|
|
923
937
|
|
|
924
938
|
raw_member_map = {k: v.value if isinstance(v, Enum) else v for k, v in self.members.items()}
|
|
925
939
|
initialization_namespace = self._get_initialization_namespace_for_enum(self.cls) | raw_member_map
|
|
926
940
|
for attr_name, attr in initialization_namespace.items():
|
|
927
941
|
enum_dict[attr_name] = attr
|
|
928
|
-
model_copy = cast(type[_T_ENUM], type(self.
|
|
942
|
+
model_copy = cast(type[_T_ENUM], type(self.name, self.cls.__bases__, enum_dict))
|
|
929
943
|
model_copy.__cadwyn_original_model__ = self.cls # pyright: ignore[reportAttributeAccessIssue]
|
|
930
944
|
return model_copy
|
|
931
945
|
|
cadwyn/structure/common.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from collections.abc import Callable
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
from typing import ParamSpec, TypeAlias, TypeVar
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel
|
|
@@ -9,3 +10,8 @@ VersionDate = datetime.date
|
|
|
9
10
|
_P = ParamSpec("_P")
|
|
10
11
|
_R = TypeVar("_R")
|
|
11
12
|
Endpoint: TypeAlias = Callable[_P, _R]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True, kw_only=True)
|
|
16
|
+
class _HiddenAttributeMixin:
|
|
17
|
+
is_hidden_from_changelog: bool = False
|
cadwyn/structure/endpoints.py
CHANGED
|
@@ -11,6 +11,7 @@ from starlette.routing import BaseRoute
|
|
|
11
11
|
from cadwyn.exceptions import LintingError
|
|
12
12
|
|
|
13
13
|
from .._utils import Sentinel
|
|
14
|
+
from .common import _HiddenAttributeMixin
|
|
14
15
|
|
|
15
16
|
HTTP_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"}
|
|
16
17
|
|
|
@@ -44,7 +45,7 @@ class EndpointAttributesPayload:
|
|
|
44
45
|
response_description: str
|
|
45
46
|
responses: dict[int | str, dict[str, Any]]
|
|
46
47
|
deprecated: bool
|
|
47
|
-
methods:
|
|
48
|
+
methods: set[str]
|
|
48
49
|
operation_id: str
|
|
49
50
|
include_in_schema: bool
|
|
50
51
|
response_class: type[Response]
|
|
@@ -55,7 +56,7 @@ class EndpointAttributesPayload:
|
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
@dataclass(slots=True)
|
|
58
|
-
class EndpointHadInstruction:
|
|
59
|
+
class EndpointHadInstruction(_HiddenAttributeMixin):
|
|
59
60
|
endpoint_path: str
|
|
60
61
|
endpoint_methods: set[str]
|
|
61
62
|
endpoint_func_name: str | None
|
|
@@ -63,14 +64,14 @@ class EndpointHadInstruction:
|
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
@dataclass(slots=True)
|
|
66
|
-
class EndpointExistedInstruction:
|
|
67
|
+
class EndpointExistedInstruction(_HiddenAttributeMixin):
|
|
67
68
|
endpoint_path: str
|
|
68
69
|
endpoint_methods: set[str]
|
|
69
70
|
endpoint_func_name: str | None
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
@dataclass(slots=True)
|
|
73
|
-
class EndpointDidntExistInstruction:
|
|
74
|
+
class EndpointDidntExistInstruction(_HiddenAttributeMixin):
|
|
74
75
|
endpoint_path: str
|
|
75
76
|
endpoint_methods: set[str]
|
|
76
77
|
endpoint_func_name: str | None
|
|
@@ -135,7 +136,7 @@ class EndpointInstructionFactory:
|
|
|
135
136
|
response_description=response_description,
|
|
136
137
|
responses=responses,
|
|
137
138
|
deprecated=deprecated,
|
|
138
|
-
methods=methods,
|
|
139
|
+
methods=set(methods) if methods is not Sentinel else Sentinel,
|
|
139
140
|
operation_id=operation_id,
|
|
140
141
|
include_in_schema=include_in_schema,
|
|
141
142
|
response_class=response_class,
|
cadwyn/structure/enums.py
CHANGED
|
@@ -3,15 +3,17 @@ from dataclasses import dataclass
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from .common import _HiddenAttributeMixin
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
@dataclass(slots=True)
|
|
8
|
-
class EnumHadMembersInstruction:
|
|
10
|
+
class EnumHadMembersInstruction(_HiddenAttributeMixin):
|
|
9
11
|
enum: type[Enum]
|
|
10
12
|
members: Mapping[str, Any]
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
@dataclass(slots=True)
|
|
14
|
-
class EnumDidntHaveMembersInstruction:
|
|
16
|
+
class EnumDidntHaveMembersInstruction(_HiddenAttributeMixin):
|
|
15
17
|
enum: type[Enum]
|
|
16
18
|
members: tuple[str, ...]
|
|
17
19
|
|
cadwyn/structure/schemas.py
CHANGED
|
@@ -10,6 +10,8 @@ from pydantic.fields import FieldInfo
|
|
|
10
10
|
from cadwyn._utils import Sentinel, fully_unwrap_decorator
|
|
11
11
|
from cadwyn.exceptions import CadwynStructureError
|
|
12
12
|
|
|
13
|
+
from .common import _HiddenAttributeMixin
|
|
14
|
+
|
|
13
15
|
if TYPE_CHECKING:
|
|
14
16
|
from pydantic.typing import AbstractSetIntStr, MappingIntStrAny
|
|
15
17
|
|
|
@@ -21,24 +23,21 @@ PossibleFieldAttributes = Literal[
|
|
|
21
23
|
"title",
|
|
22
24
|
"description",
|
|
23
25
|
"exclude",
|
|
24
|
-
"include",
|
|
25
26
|
"const",
|
|
26
27
|
"gt",
|
|
27
28
|
"ge",
|
|
28
29
|
"lt",
|
|
29
30
|
"le",
|
|
31
|
+
"deprecated",
|
|
32
|
+
"fail_fast",
|
|
30
33
|
"strict",
|
|
31
34
|
"multiple_of",
|
|
32
35
|
"allow_inf_nan",
|
|
33
36
|
"max_digits",
|
|
34
37
|
"decimal_places",
|
|
35
|
-
"min_items",
|
|
36
|
-
"max_items",
|
|
37
|
-
"unique_items",
|
|
38
38
|
"min_length",
|
|
39
39
|
"max_length",
|
|
40
40
|
"allow_mutation",
|
|
41
|
-
"regex",
|
|
42
41
|
"pattern",
|
|
43
42
|
"discriminator",
|
|
44
43
|
"repr",
|
|
@@ -53,8 +52,9 @@ class FieldChanges:
|
|
|
53
52
|
title: str
|
|
54
53
|
description: str
|
|
55
54
|
exclude: "AbstractSetIntStr | MappingIntStrAny | Any"
|
|
56
|
-
include: "AbstractSetIntStr | MappingIntStrAny | Any"
|
|
57
55
|
const: bool
|
|
56
|
+
deprecated: bool
|
|
57
|
+
fail_fast: bool
|
|
58
58
|
gt: float
|
|
59
59
|
ge: float
|
|
60
60
|
lt: float
|
|
@@ -64,20 +64,16 @@ class FieldChanges:
|
|
|
64
64
|
allow_inf_nan: bool
|
|
65
65
|
max_digits: int
|
|
66
66
|
decimal_places: int
|
|
67
|
-
min_items: int
|
|
68
|
-
max_items: int
|
|
69
|
-
unique_items: bool
|
|
70
67
|
min_length: int
|
|
71
68
|
max_length: int
|
|
72
69
|
allow_mutation: bool
|
|
73
|
-
regex: str
|
|
74
70
|
pattern: str
|
|
75
71
|
discriminator: str
|
|
76
72
|
repr: bool
|
|
77
73
|
|
|
78
74
|
|
|
79
75
|
@dataclass(slots=True)
|
|
80
|
-
class FieldHadInstruction:
|
|
76
|
+
class FieldHadInstruction(_HiddenAttributeMixin):
|
|
81
77
|
schema: type[BaseModel]
|
|
82
78
|
name: str
|
|
83
79
|
type: type
|
|
@@ -86,20 +82,20 @@ class FieldHadInstruction:
|
|
|
86
82
|
|
|
87
83
|
|
|
88
84
|
@dataclass(slots=True)
|
|
89
|
-
class FieldDidntHaveInstruction:
|
|
85
|
+
class FieldDidntHaveInstruction(_HiddenAttributeMixin):
|
|
90
86
|
schema: type[BaseModel]
|
|
91
87
|
name: str
|
|
92
88
|
attributes: tuple[str, ...]
|
|
93
89
|
|
|
94
90
|
|
|
95
91
|
@dataclass(slots=True)
|
|
96
|
-
class FieldDidntExistInstruction:
|
|
92
|
+
class FieldDidntExistInstruction(_HiddenAttributeMixin):
|
|
97
93
|
schema: type[BaseModel]
|
|
98
94
|
name: str
|
|
99
95
|
|
|
100
96
|
|
|
101
97
|
@dataclass(slots=True)
|
|
102
|
-
class FieldExistedAsInstruction:
|
|
98
|
+
class FieldExistedAsInstruction(_HiddenAttributeMixin):
|
|
103
99
|
schema: type[BaseModel]
|
|
104
100
|
name: str
|
|
105
101
|
field: FieldInfo
|
|
@@ -122,41 +118,25 @@ class AlterFieldInstructionFactory:
|
|
|
122
118
|
title: str = Sentinel,
|
|
123
119
|
description: str = Sentinel,
|
|
124
120
|
exclude: "AbstractSetIntStr | MappingIntStrAny | Any" = Sentinel,
|
|
125
|
-
include: "AbstractSetIntStr | MappingIntStrAny | Any" = Sentinel,
|
|
126
121
|
const: bool = Sentinel,
|
|
127
122
|
gt: float = Sentinel,
|
|
128
123
|
ge: float = Sentinel,
|
|
129
124
|
lt: float = Sentinel,
|
|
130
125
|
le: float = Sentinel,
|
|
131
126
|
strict: bool = Sentinel,
|
|
127
|
+
deprecated: bool = Sentinel,
|
|
132
128
|
multiple_of: float = Sentinel,
|
|
133
129
|
allow_inf_nan: bool = Sentinel,
|
|
134
130
|
max_digits: int = Sentinel,
|
|
135
131
|
decimal_places: int = Sentinel,
|
|
136
|
-
min_items: int = Sentinel,
|
|
137
|
-
max_items: int = Sentinel,
|
|
138
|
-
unique_items: bool = Sentinel,
|
|
139
132
|
min_length: int = Sentinel,
|
|
140
133
|
max_length: int = Sentinel,
|
|
141
134
|
allow_mutation: bool = Sentinel,
|
|
142
|
-
regex: str = Sentinel,
|
|
143
135
|
pattern: str = Sentinel,
|
|
144
136
|
discriminator: str = Sentinel,
|
|
145
137
|
repr: bool = Sentinel,
|
|
138
|
+
fail_fast: bool = Sentinel,
|
|
146
139
|
) -> FieldHadInstruction:
|
|
147
|
-
if regex is not Sentinel:
|
|
148
|
-
raise CadwynStructureError("`regex` was removed in Pydantic 2. Use `pattern` instead")
|
|
149
|
-
if include is not Sentinel:
|
|
150
|
-
raise CadwynStructureError("`include` was removed in Pydantic 2. Use `exclude` instead")
|
|
151
|
-
if min_items is not Sentinel:
|
|
152
|
-
raise CadwynStructureError("`min_items` was removed in Pydantic 2. Use `min_length` instead")
|
|
153
|
-
if max_items is not Sentinel:
|
|
154
|
-
raise CadwynStructureError("`max_items` was removed in Pydantic 2. Use `max_length` instead")
|
|
155
|
-
if unique_items is not Sentinel:
|
|
156
|
-
raise CadwynStructureError(
|
|
157
|
-
"`unique_items` was removed in Pydantic 2. Use `Set` type annotation instead"
|
|
158
|
-
"(this feature is discussed in https://github.com/pydantic/pydantic-core/issues/296)",
|
|
159
|
-
)
|
|
160
140
|
return FieldHadInstruction(
|
|
161
141
|
schema=self.schema,
|
|
162
142
|
name=self.name,
|
|
@@ -169,27 +149,24 @@ class AlterFieldInstructionFactory:
|
|
|
169
149
|
title=title,
|
|
170
150
|
description=description,
|
|
171
151
|
exclude=exclude,
|
|
172
|
-
include=include,
|
|
173
152
|
const=const,
|
|
174
153
|
gt=gt,
|
|
175
154
|
ge=ge,
|
|
176
155
|
lt=lt,
|
|
177
156
|
le=le,
|
|
157
|
+
deprecated=deprecated,
|
|
178
158
|
strict=strict,
|
|
179
159
|
multiple_of=multiple_of,
|
|
180
160
|
allow_inf_nan=allow_inf_nan,
|
|
181
161
|
max_digits=max_digits,
|
|
182
162
|
decimal_places=decimal_places,
|
|
183
|
-
min_items=min_items,
|
|
184
|
-
max_items=max_items,
|
|
185
|
-
unique_items=unique_items,
|
|
186
163
|
min_length=min_length,
|
|
187
164
|
max_length=max_length,
|
|
188
165
|
allow_mutation=allow_mutation,
|
|
189
|
-
regex=regex,
|
|
190
166
|
pattern=pattern,
|
|
191
167
|
discriminator=discriminator,
|
|
192
168
|
repr=repr,
|
|
169
|
+
fail_fast=fail_fast,
|
|
193
170
|
),
|
|
194
171
|
)
|
|
195
172
|
|
|
@@ -266,7 +243,7 @@ AlterSchemaSubInstruction = (
|
|
|
266
243
|
|
|
267
244
|
|
|
268
245
|
@dataclass(slots=True)
|
|
269
|
-
class SchemaHadInstruction:
|
|
246
|
+
class SchemaHadInstruction(_HiddenAttributeMixin):
|
|
270
247
|
schema: type[BaseModel]
|
|
271
248
|
name: str
|
|
272
249
|
|
cadwyn/structure/versions.py
CHANGED
|
@@ -23,7 +23,7 @@ from fastapi.routing import APIRoute, _prepare_response_content
|
|
|
23
23
|
from pydantic import BaseModel
|
|
24
24
|
from pydantic_core import PydanticUndefined
|
|
25
25
|
from starlette._utils import is_async_callable
|
|
26
|
-
from typing_extensions import assert_never
|
|
26
|
+
from typing_extensions import assert_never, deprecated
|
|
27
27
|
|
|
28
28
|
from cadwyn._utils import classproperty
|
|
29
29
|
from cadwyn.exceptions import (
|
|
@@ -64,6 +64,7 @@ IdentifierPythonPath = str
|
|
|
64
64
|
|
|
65
65
|
class VersionChange:
|
|
66
66
|
description: ClassVar[str] = Sentinel
|
|
67
|
+
is_hidden_from_changelog: bool = False
|
|
67
68
|
instructions_to_migrate_to_previous_version: ClassVar[Sequence[PossibleInstructions]] = Sentinel
|
|
68
69
|
alter_schema_instructions: ClassVar[list[AlterSchemaSubInstruction | SchemaHadInstruction]] = Sentinel
|
|
69
70
|
alter_enum_instructions: ClassVar[list[AlterEnumSubInstruction]] = Sentinel
|
|
@@ -198,24 +199,29 @@ class VersionChangeWithSideEffects(VersionChange, _abstract=True):
|
|
|
198
199
|
|
|
199
200
|
|
|
200
201
|
class Version:
|
|
201
|
-
def __init__(self, value: VersionDate | str, *
|
|
202
|
+
def __init__(self, value: VersionDate | str, *changes: type[VersionChange]) -> None:
|
|
202
203
|
super().__init__()
|
|
203
204
|
|
|
204
205
|
if isinstance(value, str):
|
|
205
206
|
value = date.fromisoformat(value)
|
|
206
207
|
self.value = value
|
|
207
|
-
self.
|
|
208
|
+
self.changes = changes
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
@deprecated("'version_changes' attribute is deprecated and will be removed in Cadwyn 5.x.x. Use 'changes' instead.")
|
|
212
|
+
def version_changes(self): # pragma: no cover
|
|
213
|
+
return self.changes
|
|
208
214
|
|
|
209
215
|
def __repr__(self) -> str:
|
|
210
216
|
return f"Version('{self.value}')"
|
|
211
217
|
|
|
212
218
|
|
|
213
219
|
class HeadVersion:
|
|
214
|
-
def __init__(self, *
|
|
220
|
+
def __init__(self, *changes: type[VersionChange]) -> None:
|
|
215
221
|
super().__init__()
|
|
216
|
-
self.
|
|
222
|
+
self.changes = changes
|
|
217
223
|
|
|
218
|
-
for version_change in
|
|
224
|
+
for version_change in changes:
|
|
219
225
|
if any(
|
|
220
226
|
[
|
|
221
227
|
version_change.alter_request_by_path_instructions,
|
|
@@ -228,6 +234,11 @@ class HeadVersion:
|
|
|
228
234
|
f"HeadVersion does not support request or response migrations but {version_change} contained one."
|
|
229
235
|
)
|
|
230
236
|
|
|
237
|
+
@property
|
|
238
|
+
@deprecated("'version_changes' attribute is deprecated and will be removed in Cadwyn 5.x.x. Use 'changes' instead.")
|
|
239
|
+
def version_changes(self): # pragma: no cover
|
|
240
|
+
return self.changes
|
|
241
|
+
|
|
231
242
|
|
|
232
243
|
def get_cls_pythonpath(cls: type) -> IdentifierPythonPath:
|
|
233
244
|
return f"{cls.__module__}.{cls.__name__}"
|
|
@@ -260,7 +271,7 @@ class VersionBundle:
|
|
|
260
271
|
)
|
|
261
272
|
if not self.versions:
|
|
262
273
|
raise CadwynStructureError("You must define at least one non-head version in a VersionBundle.")
|
|
263
|
-
if self.versions[-1].
|
|
274
|
+
if self.versions[-1].changes:
|
|
264
275
|
raise CadwynStructureError(
|
|
265
276
|
f'The first version "{self.versions[-1].value}" cannot have any version changes. '
|
|
266
277
|
"Version changes are defined to migrate to/from a previous version so you "
|
|
@@ -275,7 +286,7 @@ class VersionBundle:
|
|
|
275
286
|
f"You tried to define two versions with the same value in the same "
|
|
276
287
|
f"{VersionBundle.__name__}: '{version.value}'.",
|
|
277
288
|
)
|
|
278
|
-
for version_change in version.
|
|
289
|
+
for version_change in version.changes:
|
|
279
290
|
if version_change._bound_version_bundle is not None:
|
|
280
291
|
raise CadwynStructureError(
|
|
281
292
|
f"You tried to bind version change '{version_change.__name__}' to two different versions. "
|
|
@@ -295,14 +306,14 @@ class VersionBundle:
|
|
|
295
306
|
altered_schemas = {
|
|
296
307
|
get_cls_pythonpath(instruction.schema): instruction.schema
|
|
297
308
|
for version in self._all_versions
|
|
298
|
-
for version_change in version.
|
|
309
|
+
for version_change in version.changes
|
|
299
310
|
for instruction in list(version_change.alter_schema_instructions)
|
|
300
311
|
}
|
|
301
312
|
|
|
302
313
|
migrated_schemas = {
|
|
303
314
|
get_cls_pythonpath(schema): schema
|
|
304
315
|
for version in self._all_versions
|
|
305
|
-
for version_change in version.
|
|
316
|
+
for version_change in version.changes
|
|
306
317
|
for schema in list(version_change.alter_request_by_schema_instructions.keys())
|
|
307
318
|
}
|
|
308
319
|
|
|
@@ -313,7 +324,7 @@ class VersionBundle:
|
|
|
313
324
|
return {
|
|
314
325
|
get_cls_pythonpath(instruction.enum): instruction.enum
|
|
315
326
|
for version in self._all_versions
|
|
316
|
-
for version_change in version.
|
|
327
|
+
for version_change in version.changes
|
|
317
328
|
for instruction in version_change.alter_enum_instructions
|
|
318
329
|
}
|
|
319
330
|
|
|
@@ -327,9 +338,7 @@ class VersionBundle:
|
|
|
327
338
|
def _version_changes_to_version_mapping(
|
|
328
339
|
self,
|
|
329
340
|
) -> dict[type[VersionChange] | type[VersionChangeWithSideEffects], VersionDate]:
|
|
330
|
-
return {
|
|
331
|
-
version_change: version.value for version in self.versions for version_change in version.version_changes
|
|
332
|
-
}
|
|
341
|
+
return {version_change: version.value for version in self.versions for version_change in version.changes}
|
|
333
342
|
|
|
334
343
|
async def _migrate_request(
|
|
335
344
|
self,
|
|
@@ -347,7 +356,7 @@ class VersionBundle:
|
|
|
347
356
|
for v in reversed(self.versions):
|
|
348
357
|
if v.value <= current_version:
|
|
349
358
|
continue
|
|
350
|
-
for version_change in v.
|
|
359
|
+
for version_change in v.changes:
|
|
351
360
|
if body_type is not None and body_type in version_change.alter_request_by_schema_instructions:
|
|
352
361
|
for instruction in version_change.alter_request_by_schema_instructions[body_type]:
|
|
353
362
|
instruction(request_info)
|
|
@@ -394,7 +403,7 @@ class VersionBundle:
|
|
|
394
403
|
for v in self.versions:
|
|
395
404
|
if v.value <= current_version:
|
|
396
405
|
break
|
|
397
|
-
for version_change in v.
|
|
406
|
+
for version_change in v.changes:
|
|
398
407
|
migrations_to_apply: list[_BaseAlterResponseInstruction] = []
|
|
399
408
|
|
|
400
409
|
if head_response_model and head_response_model in version_change.alter_response_by_schema_instructions:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cadwyn
|
|
3
|
-
Version: 4.1
|
|
3
|
+
Version: 4.2.1
|
|
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
|
|
@@ -32,6 +32,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
32
32
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
33
33
|
Classifier: Typing :: Typed
|
|
34
34
|
Provides-Extra: cli
|
|
35
|
+
Requires-Dist: backports-strenum (>=1.3.1,<2.0.0) ; python_version < "3.11"
|
|
35
36
|
Requires-Dist: fastapi (>=0.110.0)
|
|
36
37
|
Requires-Dist: issubclass (>=0.1.2,<0.2.0)
|
|
37
38
|
Requires-Dist: jinja2 (>=3.1.2)
|