cadwyn 3.15.9__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cadwyn might be problematic. Click here for more details.
- cadwyn/__init__.py +25 -8
- cadwyn/__main__.py +46 -90
- cadwyn/_asts.py +9 -133
- cadwyn/_importer.py +31 -0
- cadwyn/_render.py +152 -0
- cadwyn/_utils.py +7 -107
- cadwyn/applications.py +5 -34
- cadwyn/exceptions.py +11 -3
- cadwyn/middleware.py +4 -4
- cadwyn/route_generation.py +22 -450
- cadwyn/routing.py +2 -5
- cadwyn/schema_generation.py +946 -0
- cadwyn/structure/__init__.py +0 -2
- cadwyn/structure/schemas.py +50 -49
- cadwyn/structure/versions.py +24 -137
- {cadwyn-3.15.9.dist-info → cadwyn-4.0.0.dist-info}/METADATA +4 -5
- cadwyn-4.0.0.dist-info/RECORD +27 -0
- cadwyn/_compat.py +0 -151
- cadwyn/_package_utils.py +0 -45
- cadwyn/codegen/README.md +0 -10
- cadwyn/codegen/__init__.py +0 -10
- cadwyn/codegen/_common.py +0 -168
- cadwyn/codegen/_main.py +0 -279
- cadwyn/codegen/_plugins/__init__.py +0 -0
- cadwyn/codegen/_plugins/class_migrations.py +0 -423
- cadwyn/codegen/_plugins/class_rebuilding.py +0 -109
- cadwyn/codegen/_plugins/class_renaming.py +0 -49
- cadwyn/codegen/_plugins/import_auto_adding.py +0 -64
- cadwyn/codegen/_plugins/module_migrations.py +0 -15
- cadwyn/main.py +0 -11
- cadwyn/structure/modules.py +0 -39
- cadwyn-3.15.9.dist-info/RECORD +0 -38
- {cadwyn-3.15.9.dist-info → cadwyn-4.0.0.dist-info}/LICENSE +0 -0
- {cadwyn-3.15.9.dist-info → cadwyn-4.0.0.dist-info}/WHEEL +0 -0
- {cadwyn-3.15.9.dist-info → cadwyn-4.0.0.dist-info}/entry_points.txt +0 -0
cadwyn/structure/__init__.py
CHANGED
|
@@ -6,7 +6,6 @@ from .data import (
|
|
|
6
6
|
)
|
|
7
7
|
from .endpoints import endpoint
|
|
8
8
|
from .enums import enum
|
|
9
|
-
from .modules import module
|
|
10
9
|
from .schemas import schema
|
|
11
10
|
from .versions import (
|
|
12
11
|
HeadVersion,
|
|
@@ -25,7 +24,6 @@ __all__ = [
|
|
|
25
24
|
"endpoint",
|
|
26
25
|
"schema",
|
|
27
26
|
"enum",
|
|
28
|
-
"module",
|
|
29
27
|
"convert_response_to_previous_version_for",
|
|
30
28
|
"convert_request_to_next_version_for",
|
|
31
29
|
"RequestInfo",
|
cadwyn/structure/schemas.py
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import ast
|
|
2
|
-
import inspect
|
|
3
|
-
import textwrap
|
|
4
1
|
from collections.abc import Callable
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Literal
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
7
4
|
|
|
5
|
+
from issubclass import issubclass as lenient_issubclass
|
|
8
6
|
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic._internal._decorators import PydanticDescriptorProxy, unwrap_wrapped_function
|
|
9
8
|
from pydantic.fields import FieldInfo
|
|
10
9
|
|
|
11
|
-
from cadwyn.
|
|
12
|
-
from cadwyn._compat import PYDANTIC_V2
|
|
13
|
-
from cadwyn._utils import Sentinel
|
|
10
|
+
from cadwyn._utils import Sentinel, fully_unwrap_decorator
|
|
14
11
|
from cadwyn.exceptions import CadwynStructureError
|
|
15
12
|
|
|
16
13
|
if TYPE_CHECKING:
|
|
@@ -105,7 +102,6 @@ class FieldDidntExistInstruction:
|
|
|
105
102
|
class FieldExistedAsInstruction:
|
|
106
103
|
schema: type[BaseModel]
|
|
107
104
|
name: str
|
|
108
|
-
type: type
|
|
109
105
|
field: FieldInfo
|
|
110
106
|
|
|
111
107
|
|
|
@@ -148,23 +144,19 @@ class AlterFieldInstructionFactory:
|
|
|
148
144
|
discriminator: str = Sentinel,
|
|
149
145
|
repr: bool = Sentinel,
|
|
150
146
|
) -> FieldHadInstruction:
|
|
151
|
-
if
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
165
|
-
else:
|
|
166
|
-
if pattern is not Sentinel:
|
|
167
|
-
raise CadwynStructureError("`pattern` is only available in Pydantic 2. use `regex` instead")
|
|
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
|
+
)
|
|
168
160
|
return FieldHadInstruction(
|
|
169
161
|
schema=self.schema,
|
|
170
162
|
name=self.name,
|
|
@@ -219,30 +211,28 @@ class AlterFieldInstructionFactory:
|
|
|
219
211
|
type: Any,
|
|
220
212
|
info: FieldInfo | None = None,
|
|
221
213
|
) -> FieldExistedAsInstruction:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
214
|
+
if info is None:
|
|
215
|
+
info = cast(FieldInfo, Field())
|
|
216
|
+
info.annotation = type
|
|
217
|
+
return FieldExistedAsInstruction(self.schema, name=self.name, field=info)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _get_model_decorators(model: type[BaseModel]):
|
|
221
|
+
return [
|
|
222
|
+
*model.__pydantic_decorators__.validators.values(),
|
|
223
|
+
*model.__pydantic_decorators__.field_validators.values(),
|
|
224
|
+
*model.__pydantic_decorators__.root_validators.values(),
|
|
225
|
+
*model.__pydantic_decorators__.field_serializers.values(),
|
|
226
|
+
*model.__pydantic_decorators__.model_serializers.values(),
|
|
227
|
+
*model.__pydantic_decorators__.model_validators.values(),
|
|
228
|
+
*model.__pydantic_decorators__.computed_fields.values(),
|
|
229
|
+
]
|
|
228
230
|
|
|
229
231
|
|
|
230
232
|
@dataclass(slots=True)
|
|
231
233
|
class ValidatorExistedInstruction:
|
|
232
234
|
schema: type[BaseModel]
|
|
233
|
-
validator: Callable[..., Any]
|
|
234
|
-
validator_info: "_ValidatorWrapper" = field(init=False)
|
|
235
|
-
|
|
236
|
-
def __post_init__(self):
|
|
237
|
-
source = textwrap.dedent(inspect.getsource(self.validator))
|
|
238
|
-
validator_ast = ast.parse(source).body[0]
|
|
239
|
-
if not isinstance(validator_ast, ast.FunctionDef):
|
|
240
|
-
raise CadwynStructureError("The passed validator must be a function")
|
|
241
|
-
|
|
242
|
-
validator_info = get_validator_info_or_none(validator_ast)
|
|
243
|
-
if validator_info is None:
|
|
244
|
-
raise CadwynStructureError("The passed function must be a pydantic validator")
|
|
245
|
-
self.validator_info = validator_info
|
|
235
|
+
validator: Callable[..., Any] | PydanticDescriptorProxy
|
|
246
236
|
|
|
247
237
|
|
|
248
238
|
@dataclass(slots=True)
|
|
@@ -254,7 +244,7 @@ class ValidatorDidntExistInstruction:
|
|
|
254
244
|
@dataclass(slots=True)
|
|
255
245
|
class AlterValidatorInstructionFactory:
|
|
256
246
|
schema: type[BaseModel]
|
|
257
|
-
func: Callable[..., Any]
|
|
247
|
+
func: Callable[..., Any] | PydanticDescriptorProxy
|
|
258
248
|
|
|
259
249
|
@property
|
|
260
250
|
def existed(self) -> ValidatorExistedInstruction:
|
|
@@ -288,9 +278,20 @@ class AlterSchemaInstructionFactory:
|
|
|
288
278
|
def field(self, name: str, /) -> AlterFieldInstructionFactory:
|
|
289
279
|
return AlterFieldInstructionFactory(self.schema, name)
|
|
290
280
|
|
|
291
|
-
def validator(
|
|
292
|
-
|
|
293
|
-
|
|
281
|
+
def validator(
|
|
282
|
+
self, func: "Callable[..., Any] | classmethod[Any, Any, Any] | PydanticDescriptorProxy", /
|
|
283
|
+
) -> AlterValidatorInstructionFactory:
|
|
284
|
+
func = cast(Callable | PydanticDescriptorProxy, unwrap_wrapped_function(func))
|
|
285
|
+
|
|
286
|
+
if not isinstance(func, PydanticDescriptorProxy):
|
|
287
|
+
if hasattr(func, "__self__"):
|
|
288
|
+
owner = func.__self__
|
|
289
|
+
if lenient_issubclass(owner, BaseModel) and any( # pragma: no branch
|
|
290
|
+
fully_unwrap_decorator(decorator.func, decorator.shim) == func
|
|
291
|
+
for decorator in _get_model_decorators(owner)
|
|
292
|
+
):
|
|
293
|
+
return AlterValidatorInstructionFactory(self.schema, func)
|
|
294
|
+
raise CadwynStructureError("The passed function must be a pydantic validator")
|
|
294
295
|
return AlterValidatorInstructionFactory(self.schema, func)
|
|
295
296
|
|
|
296
297
|
def had(self, *, name: str) -> SchemaHadInstruction:
|
cadwyn/structure/versions.py
CHANGED
|
@@ -2,20 +2,18 @@ import email.message
|
|
|
2
2
|
import functools
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
|
-
import warnings
|
|
6
5
|
from collections import defaultdict
|
|
7
6
|
from collections.abc import Callable, Iterator, Sequence
|
|
8
7
|
from contextlib import AsyncExitStack
|
|
9
8
|
from contextvars import ContextVar
|
|
9
|
+
from datetime import date
|
|
10
10
|
from enum import Enum
|
|
11
|
-
from
|
|
12
|
-
from types import ModuleType
|
|
13
|
-
from typing import Any, ClassVar, ParamSpec, TypeAlias, TypeVar, cast, overload
|
|
11
|
+
from typing import Any, ClassVar, ParamSpec, TypeAlias, TypeVar
|
|
14
12
|
|
|
15
13
|
from fastapi import HTTPException, params
|
|
16
14
|
from fastapi import Request as FastapiRequest
|
|
17
15
|
from fastapi import Response as FastapiResponse
|
|
18
|
-
from fastapi._compat import _normalize_errors
|
|
16
|
+
from fastapi._compat import ModelField, _normalize_errors
|
|
19
17
|
from fastapi.concurrency import run_in_threadpool
|
|
20
18
|
from fastapi.dependencies.models import Dependant
|
|
21
19
|
from fastapi.dependencies.utils import solve_dependencies
|
|
@@ -23,18 +21,16 @@ from fastapi.exceptions import RequestValidationError
|
|
|
23
21
|
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
|
24
22
|
from fastapi.routing import APIRoute, _prepare_response_content
|
|
25
23
|
from pydantic import BaseModel
|
|
24
|
+
from pydantic_core import PydanticUndefined
|
|
26
25
|
from starlette._utils import is_async_callable
|
|
27
|
-
from typing_extensions import assert_never
|
|
28
|
-
|
|
29
|
-
from cadwyn.
|
|
30
|
-
from cadwyn.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
get_version_dir_path,
|
|
26
|
+
from typing_extensions import assert_never
|
|
27
|
+
|
|
28
|
+
from cadwyn._utils import classproperty
|
|
29
|
+
from cadwyn.exceptions import (
|
|
30
|
+
CadwynError,
|
|
31
|
+
CadwynHeadRequestValidationError,
|
|
32
|
+
CadwynStructureError,
|
|
35
33
|
)
|
|
36
|
-
from cadwyn._utils import classproperty, get_another_version_of_cls
|
|
37
|
-
from cadwyn.exceptions import CadwynError, CadwynHeadRequestValidationError, CadwynStructureError
|
|
38
34
|
|
|
39
35
|
from .._utils import Sentinel
|
|
40
36
|
from .common import Endpoint, VersionDate, VersionedModel
|
|
@@ -49,7 +45,6 @@ from .data import (
|
|
|
49
45
|
)
|
|
50
46
|
from .endpoints import AlterEndpointSubInstruction
|
|
51
47
|
from .enums import AlterEnumSubInstruction
|
|
52
|
-
from .modules import AlterModuleInstruction
|
|
53
48
|
from .schemas import AlterSchemaSubInstruction, SchemaHadInstruction
|
|
54
49
|
|
|
55
50
|
_CADWYN_REQUEST_PARAM_NAME = "cadwyn_request_param"
|
|
@@ -61,10 +56,10 @@ PossibleInstructions: TypeAlias = (
|
|
|
61
56
|
| AlterEndpointSubInstruction
|
|
62
57
|
| AlterEnumSubInstruction
|
|
63
58
|
| SchemaHadInstruction
|
|
64
|
-
| AlterModuleInstruction
|
|
65
59
|
| staticmethod
|
|
66
60
|
)
|
|
67
61
|
APIVersionVarType: TypeAlias = ContextVar[VersionDate | None] | ContextVar[VersionDate]
|
|
62
|
+
IdentifierPythonPath = str
|
|
68
63
|
|
|
69
64
|
|
|
70
65
|
class VersionChange:
|
|
@@ -72,7 +67,6 @@ class VersionChange:
|
|
|
72
67
|
instructions_to_migrate_to_previous_version: ClassVar[Sequence[PossibleInstructions]] = Sentinel
|
|
73
68
|
alter_schema_instructions: ClassVar[list[AlterSchemaSubInstruction | SchemaHadInstruction]] = Sentinel
|
|
74
69
|
alter_enum_instructions: ClassVar[list[AlterEnumSubInstruction]] = Sentinel
|
|
75
|
-
alter_module_instructions: ClassVar[list[AlterModuleInstruction]] = Sentinel
|
|
76
70
|
alter_endpoint_instructions: ClassVar[list[AlterEndpointSubInstruction]] = Sentinel
|
|
77
71
|
alter_request_by_schema_instructions: ClassVar[dict[type[BaseModel], list[_AlterRequestBySchemaInstruction]]] = (
|
|
78
72
|
Sentinel
|
|
@@ -111,7 +105,6 @@ class VersionChange:
|
|
|
111
105
|
def _extract_list_instructions_into_correct_containers(cls):
|
|
112
106
|
cls.alter_schema_instructions = []
|
|
113
107
|
cls.alter_enum_instructions = []
|
|
114
|
-
cls.alter_module_instructions = []
|
|
115
108
|
cls.alter_endpoint_instructions = []
|
|
116
109
|
cls.alter_request_by_schema_instructions = defaultdict(list)
|
|
117
110
|
cls.alter_request_by_path_instructions = defaultdict(list)
|
|
@@ -122,8 +115,6 @@ class VersionChange:
|
|
|
122
115
|
cls.alter_schema_instructions.append(alter_instruction)
|
|
123
116
|
elif isinstance(alter_instruction, AlterEnumSubInstruction):
|
|
124
117
|
cls.alter_enum_instructions.append(alter_instruction)
|
|
125
|
-
elif isinstance(alter_instruction, AlterModuleInstruction):
|
|
126
|
-
cls.alter_module_instructions.append(alter_instruction)
|
|
127
118
|
elif isinstance(alter_instruction, AlterEndpointSubInstruction):
|
|
128
119
|
cls.alter_endpoint_instructions.append(alter_instruction)
|
|
129
120
|
elif isinstance(alter_instruction, staticmethod): # pragma: no cover
|
|
@@ -207,9 +198,11 @@ class VersionChangeWithSideEffects(VersionChange, _abstract=True):
|
|
|
207
198
|
|
|
208
199
|
|
|
209
200
|
class Version:
|
|
210
|
-
def __init__(self, value: VersionDate, *version_changes: type[VersionChange]) -> None:
|
|
201
|
+
def __init__(self, value: VersionDate | str, *version_changes: type[VersionChange]) -> None:
|
|
211
202
|
super().__init__()
|
|
212
203
|
|
|
204
|
+
if isinstance(value, str):
|
|
205
|
+
value = date.fromisoformat(value)
|
|
213
206
|
self.value = value
|
|
214
207
|
self.version_changes = version_changes
|
|
215
208
|
|
|
@@ -236,36 +229,17 @@ class HeadVersion:
|
|
|
236
229
|
)
|
|
237
230
|
|
|
238
231
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def __init__(
|
|
242
|
-
self,
|
|
243
|
-
latest_version_or_head_version: Version | HeadVersion,
|
|
244
|
-
/,
|
|
245
|
-
*other_versions: Version,
|
|
246
|
-
api_version_var: APIVersionVarType | None = None,
|
|
247
|
-
head_schemas_package: ModuleType | None = None,
|
|
248
|
-
) -> None: ...
|
|
232
|
+
def get_cls_pythonpath(cls: type) -> IdentifierPythonPath:
|
|
233
|
+
return f"{cls.__module__}.{cls.__name__}"
|
|
249
234
|
|
|
250
|
-
@overload
|
|
251
|
-
@deprecated("Pass head_version_package instead of latest_schemas_package.")
|
|
252
|
-
def __init__(
|
|
253
|
-
self,
|
|
254
|
-
latest_version_or_head_version: Version | HeadVersion,
|
|
255
|
-
/,
|
|
256
|
-
*other_versions: Version,
|
|
257
|
-
api_version_var: APIVersionVarType | None = None,
|
|
258
|
-
latest_schemas_package: ModuleType | None = None,
|
|
259
|
-
) -> None: ...
|
|
260
235
|
|
|
236
|
+
class VersionBundle:
|
|
261
237
|
def __init__(
|
|
262
238
|
self,
|
|
263
239
|
latest_version_or_head_version: Version | HeadVersion,
|
|
264
240
|
/,
|
|
265
241
|
*other_versions: Version,
|
|
266
242
|
api_version_var: APIVersionVarType | None = None,
|
|
267
|
-
head_schemas_package: ModuleType | None = None,
|
|
268
|
-
latest_schemas_package: ModuleType | None = None,
|
|
269
243
|
) -> None:
|
|
270
244
|
super().__init__()
|
|
271
245
|
|
|
@@ -276,7 +250,6 @@ class VersionBundle:
|
|
|
276
250
|
self.head_version = HeadVersion()
|
|
277
251
|
self.versions = (latest_version_or_head_version, *other_versions)
|
|
278
252
|
|
|
279
|
-
self.head_schemas_package = head_schemas_package or latest_schemas_package
|
|
280
253
|
self.version_dates = tuple(version.value for version in self.versions)
|
|
281
254
|
if api_version_var is None:
|
|
282
255
|
api_version_var = ContextVar("cadwyn_api_version")
|
|
@@ -310,39 +283,9 @@ class VersionBundle:
|
|
|
310
283
|
)
|
|
311
284
|
version_change._bound_version_bundle = self
|
|
312
285
|
|
|
313
|
-
@property # pragma: no cover
|
|
314
|
-
@deprecated("Use head_version_package instead.")
|
|
315
|
-
def latest_schemas_package(self):
|
|
316
|
-
return self.head_schemas_package
|
|
317
|
-
|
|
318
286
|
def __iter__(self) -> Iterator[Version]:
|
|
319
287
|
yield from self.versions
|
|
320
288
|
|
|
321
|
-
def _validate_head_schemas_package_structure(self):
|
|
322
|
-
# This entire function won't be necessary once we start raising an exception
|
|
323
|
-
# upon receiving `latest`.
|
|
324
|
-
|
|
325
|
-
head_schemas_package = cast(ModuleType, self.head_schemas_package)
|
|
326
|
-
if not hasattr(head_schemas_package, "__path__"):
|
|
327
|
-
raise CadwynStructureError(
|
|
328
|
-
f'The head schemas package must be a package. "{head_schemas_package.__name__}" is not a package.',
|
|
329
|
-
)
|
|
330
|
-
elif head_schemas_package.__name__.endswith(".head") or head_schemas_package.__name__ == "head":
|
|
331
|
-
return "head"
|
|
332
|
-
elif head_schemas_package.__name__.endswith(".latest"):
|
|
333
|
-
warnings.warn(
|
|
334
|
-
'The name of the head schemas module must be "head". '
|
|
335
|
-
f'Received "{head_schemas_package.__name__}" instead.',
|
|
336
|
-
DeprecationWarning,
|
|
337
|
-
stacklevel=4,
|
|
338
|
-
)
|
|
339
|
-
return "latest"
|
|
340
|
-
else:
|
|
341
|
-
raise CadwynStructureError(
|
|
342
|
-
'The name of the head schemas module must be "head". '
|
|
343
|
-
f'Received "{head_schemas_package.__name__}" instead.',
|
|
344
|
-
)
|
|
345
|
-
|
|
346
289
|
@functools.cached_property
|
|
347
290
|
def _all_versions(self):
|
|
348
291
|
return (self.head_version, *self.versions)
|
|
@@ -374,56 +317,6 @@ class VersionBundle:
|
|
|
374
317
|
for instruction in version_change.alter_enum_instructions
|
|
375
318
|
}
|
|
376
319
|
|
|
377
|
-
@functools.cached_property
|
|
378
|
-
def versioned_modules(self) -> dict[IdentifierPythonPath, ModuleType]:
|
|
379
|
-
return {
|
|
380
|
-
# We do this because when users import their modules, they might import
|
|
381
|
-
# the __init__.py file directly instead of the package itself
|
|
382
|
-
# which results in this extra `.__init__` suffix in the name
|
|
383
|
-
instruction.module.__name__.removesuffix(".__init__"): instruction.module
|
|
384
|
-
for version in self._all_versions
|
|
385
|
-
for version_change in version.version_changes
|
|
386
|
-
for instruction in version_change.alter_module_instructions
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
@functools.cached_property
|
|
390
|
-
def versioned_directories_with_head(self) -> tuple[Path, ...]:
|
|
391
|
-
if self.head_schemas_package is None:
|
|
392
|
-
raise CadwynError(
|
|
393
|
-
f"You cannot call 'VersionBundle.{self.migrate_response_body.__name__}' because it has no access to "
|
|
394
|
-
"'head_schemas_package'. It likely means that it was not attached "
|
|
395
|
-
"to any Cadwyn application which attaches 'head_schemas_package' during initialization."
|
|
396
|
-
)
|
|
397
|
-
return tuple(
|
|
398
|
-
[get_package_path_from_module(self.head_schemas_package)]
|
|
399
|
-
+ [get_version_dir_path(self.head_schemas_package, version.value) for version in self]
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
@functools.cached_property
|
|
403
|
-
def versioned_directories_without_head(self) -> tuple[Path, ...]:
|
|
404
|
-
return self.versioned_directories_with_head[1:]
|
|
405
|
-
|
|
406
|
-
def migrate_response_body(self, latest_response_model: type[BaseModel], *, latest_body: Any, version: VersionDate):
|
|
407
|
-
"""Convert the data to a specific version by applying all version changes from latest until that version
|
|
408
|
-
in reverse order and wrapping the result in the correct version of latest_response_model.
|
|
409
|
-
"""
|
|
410
|
-
response = ResponseInfo(FastapiResponse(status_code=200), body=latest_body)
|
|
411
|
-
migrated_response = self._migrate_response(
|
|
412
|
-
response,
|
|
413
|
-
current_version=version,
|
|
414
|
-
head_response_model=latest_response_model,
|
|
415
|
-
path="\0\0\0",
|
|
416
|
-
method="GET",
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
version = self._get_closest_lesser_version(version)
|
|
420
|
-
version_dir = self.versioned_directories_without_head[self.version_dates.index(version)]
|
|
421
|
-
|
|
422
|
-
versioned_response_model: type[BaseModel] = get_another_version_of_cls(
|
|
423
|
-
latest_response_model, version_dir, self.versioned_directories_with_head
|
|
424
|
-
)
|
|
425
|
-
return versioned_response_model.parse_obj(migrated_response.body) # pyright: ignore[reportDeprecated]
|
|
426
|
-
|
|
427
320
|
def _get_closest_lesser_version(self, version: VersionDate):
|
|
428
321
|
for defined_version in self.version_dates:
|
|
429
322
|
if defined_version <= version:
|
|
@@ -449,8 +342,6 @@ class VersionBundle:
|
|
|
449
342
|
current_version: VersionDate,
|
|
450
343
|
head_route: APIRoute,
|
|
451
344
|
exit_stack: AsyncExitStack,
|
|
452
|
-
*,
|
|
453
|
-
embed_body_fields: bool,
|
|
454
345
|
) -> dict[str, Any]:
|
|
455
346
|
method = request.method
|
|
456
347
|
for v in reversed(self.versions):
|
|
@@ -467,20 +358,19 @@ class VersionBundle:
|
|
|
467
358
|
request.scope["headers"] = tuple((key.encode(), value.encode()) for key, value in request_info.headers.items())
|
|
468
359
|
del request._headers
|
|
469
360
|
# Remember this: if len(body_params) == 1, then route.body_schema == route.dependant.body_params[0]
|
|
470
|
-
|
|
361
|
+
dependencies, errors, _, _, _ = await solve_dependencies(
|
|
471
362
|
request=request,
|
|
472
363
|
response=response,
|
|
473
364
|
dependant=head_dependant,
|
|
474
365
|
body=request_info.body,
|
|
475
366
|
dependency_overrides_provider=head_route.dependency_overrides_provider,
|
|
476
367
|
async_exit_stack=exit_stack,
|
|
477
|
-
embed_body_fields=embed_body_fields,
|
|
478
368
|
)
|
|
479
|
-
if
|
|
369
|
+
if errors:
|
|
480
370
|
raise CadwynHeadRequestValidationError(
|
|
481
|
-
_normalize_errors(
|
|
371
|
+
_normalize_errors(errors), body=request_info.body, version=current_version
|
|
482
372
|
)
|
|
483
|
-
return
|
|
373
|
+
return dependencies
|
|
484
374
|
|
|
485
375
|
def _migrate_response(
|
|
486
376
|
self,
|
|
@@ -580,7 +470,7 @@ class VersionBundle:
|
|
|
580
470
|
if response_param_name == _CADWYN_RESPONSE_PARAM_NAME:
|
|
581
471
|
_add_keyword_only_parameter(decorator, _CADWYN_RESPONSE_PARAM_NAME, FastapiResponse)
|
|
582
472
|
|
|
583
|
-
return decorator
|
|
473
|
+
return decorator
|
|
584
474
|
|
|
585
475
|
return wrapper
|
|
586
476
|
|
|
@@ -740,9 +630,7 @@ class VersionBundle:
|
|
|
740
630
|
elif not isinstance(raw_body, BaseModel):
|
|
741
631
|
body = raw_body
|
|
742
632
|
else:
|
|
743
|
-
body = model_dump(
|
|
744
|
-
if not PYDANTIC_V2 and raw_body.__custom_root_type__: # pyright: ignore[reportAttributeAccessIssue]
|
|
745
|
-
body = body["__root__"]
|
|
633
|
+
body = raw_body.model_dump(by_alias=True, exclude_unset=True)
|
|
746
634
|
else:
|
|
747
635
|
# This is for requests without body or with complex body such as form or file
|
|
748
636
|
body = await _get_body(request, route.body_field, exit_stack)
|
|
@@ -758,7 +646,6 @@ class VersionBundle:
|
|
|
758
646
|
api_version,
|
|
759
647
|
head_route,
|
|
760
648
|
exit_stack=exit_stack,
|
|
761
|
-
embed_body_fields=route._embed_body_fields,
|
|
762
649
|
)
|
|
763
650
|
# Because we re-added it into our kwargs when we did solve_dependencies
|
|
764
651
|
if _CADWYN_REQUEST_PARAM_NAME in new_kwargs:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cadwyn
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.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
|
|
@@ -32,12 +32,11 @@ 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:
|
|
36
|
-
Requires-Dist: fastapi (>=0.115.2)
|
|
35
|
+
Requires-Dist: fastapi (>=0.110.0)
|
|
37
36
|
Requires-Dist: issubclass (>=0.1.2,<0.2.0)
|
|
38
37
|
Requires-Dist: jinja2 (>=3.1.2)
|
|
39
|
-
Requires-Dist: pydantic (>=
|
|
40
|
-
Requires-Dist: starlette (>=0.
|
|
38
|
+
Requires-Dist: pydantic (>=2.0.0)
|
|
39
|
+
Requires-Dist: starlette (>=0.30.0)
|
|
41
40
|
Requires-Dist: typer (>=0.7.0) ; extra == "cli"
|
|
42
41
|
Requires-Dist: typing-extensions
|
|
43
42
|
Project-URL: Documentation, https://docs.cadwyn.dev
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
cadwyn/__init__.py,sha256=N98kE6Eb7g7mH-YW1XuAqb084W2NKvPhA1cUs_O0nak,930
|
|
2
|
+
cadwyn/__main__.py,sha256=fGoKJPNVueqqXW4rqwmRUBBMoDGeLEyATdT-rel5Pvs,2449
|
|
3
|
+
cadwyn/_asts.py,sha256=kNDXS0Ju0pYZyohAmJNVgJpspwKai5_a9tbekkGehUE,5130
|
|
4
|
+
cadwyn/_importer.py,sha256=2mZrDHlfY2heZsMBW-9RBpvKsCk9I-Wa8pxZ6f2f8gY,1074
|
|
5
|
+
cadwyn/_render.py,sha256=-eY4zMzBEJPteDF_CRwJah5DWFmUl2zHQHrb21mcfb8,5482
|
|
6
|
+
cadwyn/_utils.py,sha256=GK9w_qzyOI_o6UaGVfwLLYhnJFMzXistoYI9fq2E9dE,1159
|
|
7
|
+
cadwyn/applications.py,sha256=g4VlB3SzQMjfAq5vX8u6DYj2OZei2oGBffSUTVVyaAA,14478
|
|
8
|
+
cadwyn/exceptions.py,sha256=VlJKRmEGfFTDtHbOWc8kXK4yMi2N172K684Y2UIV8rI,1832
|
|
9
|
+
cadwyn/middleware.py,sha256=8cuBri_yRkl0goe6G0MLwtL04WGbW9Infah3wy9hUVM,3372
|
|
10
|
+
cadwyn/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
cadwyn/route_generation.py,sha256=GiCUCAKFQoIID3Zv5dYGTIbsGu3TSxDnB62ZUH-Mh58,22961
|
|
12
|
+
cadwyn/routing.py,sha256=9AHSojmuLgUAQlLMIqXz-ViZ9n-fljgOsn7oxha7PjM,7341
|
|
13
|
+
cadwyn/schema_generation.py,sha256=SZoP0IPgVdlJ015ItW5dxwvlWYcA41Lx1pQzCOwV7TE,39386
|
|
14
|
+
cadwyn/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
cadwyn/static/docs.html,sha256=WNm5ANJVy51TcIUFOaqKf1Z8eF86CC85TTHPxACtkzw,3455
|
|
16
|
+
cadwyn/structure/__init__.py,sha256=vej7TdTMSOg8U8Wk7GTNdA4rc6loA9083FWaTg4jAaY,655
|
|
17
|
+
cadwyn/structure/common.py,sha256=6Z4nI97XPWTCinn6np73m-rLPyYNrz2fWXKJlqjsiaQ,269
|
|
18
|
+
cadwyn/structure/data.py,sha256=1ALPhBBCE_t4GrxM0Fa3hQ-jkORJgeWNySnZ42bsi0g,7382
|
|
19
|
+
cadwyn/structure/endpoints.py,sha256=JhTgVrqLjm5LkE9thjvU1UuWcSCmDgW2bMdqznsZb2Y,5777
|
|
20
|
+
cadwyn/structure/enums.py,sha256=iMokxA2QYJ61SzyB-Pmuq3y7KL7-e6TsnjLVUaVZQnw,954
|
|
21
|
+
cadwyn/structure/schemas.py,sha256=dfVeVL6R6RjcNeehbd4yPlCYCkpiHi0Ujrwkq4pCvd8,9285
|
|
22
|
+
cadwyn/structure/versions.py,sha256=3SXzQD9Ps3jukF6prhGTABUAik70jd7KebTApY8B3Ns,32190
|
|
23
|
+
cadwyn-4.0.0.dist-info/LICENSE,sha256=KeCWewiDQYpmSnzF-p_0YpoWiyDcUPaCuG8OWQs4ig4,1072
|
|
24
|
+
cadwyn-4.0.0.dist-info/METADATA,sha256=0Exz0MGTvNEndoEkQ4xJ8Nn_c3Puk52utPDTJtrCIx0,4344
|
|
25
|
+
cadwyn-4.0.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
26
|
+
cadwyn-4.0.0.dist-info/entry_points.txt,sha256=eO05hLn9GoRzzpwT9GONPmXKsonjuMNssM2D2WHWKGk,46
|
|
27
|
+
cadwyn-4.0.0.dist-info/RECORD,,
|
cadwyn/_compat.py
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import ast
|
|
2
|
-
import dataclasses
|
|
3
|
-
import inspect
|
|
4
|
-
from typing import Any, TypeAlias
|
|
5
|
-
|
|
6
|
-
import pydantic
|
|
7
|
-
from fastapi._compat import ModelField as FastAPIModelField
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
9
|
-
|
|
10
|
-
ModelField: TypeAlias = Any # pyright: ignore[reportRedeclaration]
|
|
11
|
-
PydanticUndefined: TypeAlias = Any
|
|
12
|
-
VALIDATOR_CONFIG_KEY = "__validators__"
|
|
13
|
-
|
|
14
|
-
try:
|
|
15
|
-
PYDANTIC_V2 = False
|
|
16
|
-
|
|
17
|
-
from pydantic.fields import FieldInfo, ModelField # pyright: ignore # noqa: PGH003
|
|
18
|
-
from pydantic.fields import Undefined as PydanticUndefined # pyright: ignore # noqa: PGH003
|
|
19
|
-
|
|
20
|
-
_all_field_arg_names = []
|
|
21
|
-
EXTRA_FIELD_NAME = "extra"
|
|
22
|
-
except ImportError:
|
|
23
|
-
PYDANTIC_V2 = True
|
|
24
|
-
|
|
25
|
-
from pydantic.fields import FieldInfo
|
|
26
|
-
from pydantic_core import PydanticUndefined # pyright: ignore # noqa: PGH003
|
|
27
|
-
|
|
28
|
-
ModelField: TypeAlias = FieldInfo # pyright: ignore # noqa: PGH003
|
|
29
|
-
_all_field_arg_names = sorted(
|
|
30
|
-
[
|
|
31
|
-
name
|
|
32
|
-
for name, param in inspect.signature(Field).parameters.items()
|
|
33
|
-
if param.kind in {inspect._ParameterKind.KEYWORD_ONLY, inspect._ParameterKind.POSITIONAL_OR_KEYWORD}
|
|
34
|
-
],
|
|
35
|
-
)
|
|
36
|
-
EXTRA_FIELD_NAME = "json_schema_extra"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
_empty_field_info = Field()
|
|
40
|
-
dict_of_empty_field_info = {k: getattr(_empty_field_info, k) for k in FieldInfo.__slots__}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def is_pydantic_1_constrained_type(value: object):
|
|
44
|
-
"""This method only works for pydanticV1. It is always False in PydanticV2"""
|
|
45
|
-
return isinstance(value, type) and value.__name__.startswith("Constrained") and value.__name__.endswith("Value")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def is_constrained_type(value: object):
|
|
49
|
-
if PYDANTIC_V2:
|
|
50
|
-
import annotated_types
|
|
51
|
-
|
|
52
|
-
return isinstance(value, annotated_types.Len | annotated_types.Interval | pydantic.StringConstraints)
|
|
53
|
-
|
|
54
|
-
else:
|
|
55
|
-
return is_pydantic_1_constrained_type(value)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@dataclasses.dataclass(slots=True)
|
|
59
|
-
class PydanticFieldWrapper:
|
|
60
|
-
"""We DO NOT maintain field.metadata at all"""
|
|
61
|
-
|
|
62
|
-
annotation: Any
|
|
63
|
-
|
|
64
|
-
init_model_field: dataclasses.InitVar[ModelField]
|
|
65
|
-
field_info: FieldInfo = dataclasses.field(init=False)
|
|
66
|
-
|
|
67
|
-
annotation_ast: ast.expr | None = None
|
|
68
|
-
# In the expressions "foo: str | None = None" and "foo: str | None = Field(default=None)"
|
|
69
|
-
# the value_ast is "None" and "Field(default=None)" respectively
|
|
70
|
-
value_ast: ast.expr | None = None
|
|
71
|
-
|
|
72
|
-
def __post_init__(self, init_model_field: ModelField):
|
|
73
|
-
if isinstance(init_model_field, FieldInfo):
|
|
74
|
-
self.field_info = init_model_field
|
|
75
|
-
else:
|
|
76
|
-
self.field_info = init_model_field.field_info
|
|
77
|
-
|
|
78
|
-
def update_attribute(self, *, name: str, value: Any):
|
|
79
|
-
if PYDANTIC_V2:
|
|
80
|
-
self.field_info._attributes_set[name] = value
|
|
81
|
-
else:
|
|
82
|
-
setattr(self.field_info, name, value)
|
|
83
|
-
|
|
84
|
-
def delete_attribute(self, *, name: str) -> None:
|
|
85
|
-
if PYDANTIC_V2:
|
|
86
|
-
self.field_info._attributes_set.pop(name)
|
|
87
|
-
else:
|
|
88
|
-
setattr(self.field_info, name, PydanticUndefined)
|
|
89
|
-
|
|
90
|
-
@property
|
|
91
|
-
def passed_field_attributes(self):
|
|
92
|
-
if PYDANTIC_V2:
|
|
93
|
-
attributes = {
|
|
94
|
-
attr_name: self.field_info._attributes_set[attr_name]
|
|
95
|
-
for attr_name in _all_field_arg_names
|
|
96
|
-
if attr_name in self.field_info._attributes_set
|
|
97
|
-
}
|
|
98
|
-
# PydanticV2 always adds frozen to _attributes_set but we don't want it if it wasn't explicitly set
|
|
99
|
-
if attributes.get("frozen", ...) is None:
|
|
100
|
-
attributes.pop("frozen")
|
|
101
|
-
return attributes
|
|
102
|
-
|
|
103
|
-
else:
|
|
104
|
-
attributes = {
|
|
105
|
-
attr_name: attr_val
|
|
106
|
-
for attr_name, default_attr_val in dict_of_empty_field_info.items()
|
|
107
|
-
if attr_name != EXTRA_FIELD_NAME
|
|
108
|
-
and (attr_val := getattr(self.field_info, attr_name)) != default_attr_val
|
|
109
|
-
}
|
|
110
|
-
extras = getattr(self.field_info, EXTRA_FIELD_NAME) or {}
|
|
111
|
-
return attributes | extras
|
|
112
|
-
|
|
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
|
-
|
|
121
|
-
def model_fields(model: type[BaseModel]) -> dict[str, FieldInfo]:
|
|
122
|
-
if PYDANTIC_V2:
|
|
123
|
-
return model.model_fields
|
|
124
|
-
else:
|
|
125
|
-
return model.__fields__ # pyright: ignore[reportDeprecated]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def model_dump(model: BaseModel, by_alias: bool = False, exclude_unset: bool = False) -> dict[str, Any]:
|
|
129
|
-
if PYDANTIC_V2:
|
|
130
|
-
return model.model_dump(by_alias=by_alias, exclude_unset=exclude_unset)
|
|
131
|
-
else:
|
|
132
|
-
return model.dict(by_alias=by_alias, exclude_unset=exclude_unset) # pyright: ignore[reportDeprecated]
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def rebuild_fastapi_body_param(old_body_param: FastAPIModelField, new_body_param_type: type[BaseModel]):
|
|
136
|
-
kwargs: dict[str, Any] = {"name": old_body_param.name, "field_info": old_body_param.field_info}
|
|
137
|
-
if PYDANTIC_V2:
|
|
138
|
-
old_body_param.field_info.annotation = new_body_param_type
|
|
139
|
-
kwargs.update({"mode": old_body_param.mode})
|
|
140
|
-
else:
|
|
141
|
-
kwargs.update(
|
|
142
|
-
{
|
|
143
|
-
"type_": new_body_param_type,
|
|
144
|
-
"class_validators": old_body_param.class_validators, # pyright: ignore[reportAttributeAccessIssue]
|
|
145
|
-
"default": old_body_param.default,
|
|
146
|
-
"required": old_body_param.required,
|
|
147
|
-
"model_config": old_body_param.model_config, # pyright: ignore[reportAttributeAccessIssue]
|
|
148
|
-
"alias": old_body_param.alias,
|
|
149
|
-
},
|
|
150
|
-
)
|
|
151
|
-
return FastAPIModelField(**kwargs)
|