cadwyn 3.5.0__tar.gz → 3.6.0__tar.gz
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-3.5.0 → cadwyn-3.6.0}/PKG-INFO +1 -1
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/__init__.py +1 -1
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/_utils.py +16 -2
- cadwyn-3.5.0/cadwyn/main.py → cadwyn-3.6.0/cadwyn/applications.py +15 -5
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/_common.py +8 -6
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/_main.py +1 -1
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/class_migrations.py +1 -1
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/class_rebuilding.py +2 -2
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/exceptions.py +17 -0
- cadwyn-3.6.0/cadwyn/main.py +11 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/routing.py +9 -22
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/structure/schemas.py +2 -2
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/structure/versions.py +63 -11
- {cadwyn-3.5.0 → cadwyn-3.6.0}/pyproject.toml +2 -1
- {cadwyn-3.5.0 → cadwyn-3.6.0}/LICENSE +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/README.md +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/__main__.py +0 -0
- {cadwyn-3.5.0/cadwyn/codegen → cadwyn-3.6.0/cadwyn}/_asts.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/_compat.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/_package_utils.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/README.md +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/__init__.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/__init__.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/class_renaming.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/import_auto_adding.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/latest_version_aliasing.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/module_migrations.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/py.typed +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/structure/__init__.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/structure/common.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/structure/data.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/structure/endpoints.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/structure/enums.py +0 -0
- {cadwyn-3.5.0 → cadwyn-3.6.0}/cadwyn/structure/modules.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
2
|
|
|
3
|
+
from .applications import Cadwyn
|
|
3
4
|
from .codegen import generate_code_for_versioned_packages
|
|
4
|
-
from .main import Cadwyn
|
|
5
5
|
from .routing import InternalRepresentationOf, VersionedAPIRouter, generate_versioned_routers
|
|
6
6
|
from .structure import VersionBundle
|
|
7
7
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import importlib
|
|
3
3
|
import inspect
|
|
4
|
+
import sys
|
|
4
5
|
from collections.abc import Callable, Collection
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from types import ModuleType
|
|
@@ -40,10 +41,23 @@ def same_definition_as_in(t: _T) -> Callable[[Callable], _T]:
|
|
|
40
41
|
return decorator
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
@functools.cache
|
|
45
|
+
def get_another_version_of_cls(
|
|
46
|
+
cls_from_old_version: type[Any], new_version_dir: Path, version_dirs: frozenset[Path] | tuple[Path, ...]
|
|
47
|
+
):
|
|
48
|
+
# version_dir = /home/myuser/package/companies/v2021_01_01
|
|
49
|
+
module_from_old_version = sys.modules[cls_from_old_version.__module__]
|
|
50
|
+
try:
|
|
51
|
+
module = get_another_version_of_module(module_from_old_version, new_version_dir, version_dirs)
|
|
52
|
+
except ModuleIsNotVersionedError:
|
|
53
|
+
return cls_from_old_version
|
|
54
|
+
return getattr(module, cls_from_old_version.__name__)
|
|
55
|
+
|
|
56
|
+
|
|
43
57
|
def get_another_version_of_module(
|
|
44
58
|
module_from_old_version: ModuleType,
|
|
45
59
|
new_version_dir: Path,
|
|
46
|
-
version_dirs: frozenset[Path],
|
|
60
|
+
version_dirs: frozenset[Path] | tuple[Path, ...],
|
|
47
61
|
):
|
|
48
62
|
new_model_module_python_path = get_pythonpath_to_another_version_of_module(
|
|
49
63
|
module_from_old_version,
|
|
@@ -56,7 +70,7 @@ def get_another_version_of_module(
|
|
|
56
70
|
def get_pythonpath_to_another_version_of_module(
|
|
57
71
|
module_from_old_version: ModuleType,
|
|
58
72
|
new_version_dir: Path,
|
|
59
|
-
version_dirs: frozenset[Path],
|
|
73
|
+
version_dirs: frozenset[Path] | tuple[Path, ...],
|
|
60
74
|
) -> str:
|
|
61
75
|
# ['package', 'companies', 'latest', 'schemas']
|
|
62
76
|
# ^^^^^^
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from collections.abc import Callable, Coroutine, Sequence
|
|
2
|
-
from
|
|
3
|
-
from typing import Any
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
4
3
|
|
|
5
4
|
from fastapi import APIRouter, routing
|
|
6
5
|
from fastapi.datastructures import Default
|
|
@@ -14,16 +13,19 @@ from starlette.types import Lifespan
|
|
|
14
13
|
from typing_extensions import Self
|
|
15
14
|
from verselect import HeaderRoutingFastAPI
|
|
16
15
|
|
|
16
|
+
from cadwyn.exceptions import CadwynError
|
|
17
17
|
from cadwyn.routing import generate_versioned_routers
|
|
18
18
|
from cadwyn.structure import VersionBundle
|
|
19
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from types import ModuleType
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
class Cadwyn(HeaderRoutingFastAPI):
|
|
22
25
|
def __init__(
|
|
23
26
|
self,
|
|
24
27
|
*,
|
|
25
28
|
versions: VersionBundle,
|
|
26
|
-
latest_schemas_package: ModuleType,
|
|
27
29
|
api_version_header_name: str = "x-api-version",
|
|
28
30
|
debug: bool = False,
|
|
29
31
|
title: str = "FastAPI",
|
|
@@ -62,6 +64,16 @@ class Cadwyn(HeaderRoutingFastAPI):
|
|
|
62
64
|
separate_input_output_schemas: bool = True,
|
|
63
65
|
**extra: Any,
|
|
64
66
|
) -> None:
|
|
67
|
+
# TODO: Remove argument entirely in any major version.
|
|
68
|
+
self.versions = versions
|
|
69
|
+
latest_schemas_package = extra.pop("latest_schemas_package", None) or self.versions.latest_schemas_package
|
|
70
|
+
if latest_schemas_package is None:
|
|
71
|
+
raise CadwynError(
|
|
72
|
+
"VersionBundle.latest_schemas_package is None but is required for Cadwyn's correct functioning."
|
|
73
|
+
)
|
|
74
|
+
self.latest_schemas_package: ModuleType = latest_schemas_package
|
|
75
|
+
self.versions.latest_schemas_package = latest_schemas_package
|
|
76
|
+
|
|
65
77
|
super().__init__(
|
|
66
78
|
api_version_header_name=api_version_header_name,
|
|
67
79
|
api_version_var=versions.api_version_var,
|
|
@@ -100,8 +112,6 @@ class Cadwyn(HeaderRoutingFastAPI):
|
|
|
100
112
|
separate_input_output_schemas=separate_input_output_schemas,
|
|
101
113
|
**extra,
|
|
102
114
|
)
|
|
103
|
-
self.versions = versions
|
|
104
|
-
self.latest_schemas_package = latest_schemas_package
|
|
105
115
|
|
|
106
116
|
def generate_and_include_versioned_routers(self, *routers: APIRouter) -> None:
|
|
107
117
|
root_router = APIRouter()
|
|
@@ -6,7 +6,7 @@ from enum import Enum
|
|
|
6
6
|
from functools import cache
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from types import ModuleType
|
|
9
|
-
from typing import Any, Generic, Protocol, TypeAlias, TypeVar, cast
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeAlias, TypeVar, cast
|
|
10
10
|
|
|
11
11
|
from pydantic import BaseModel
|
|
12
12
|
from typing_extensions import Self
|
|
@@ -14,9 +14,11 @@ from typing_extensions import Self
|
|
|
14
14
|
from cadwyn._compat import PydanticFieldWrapper, model_fields
|
|
15
15
|
from cadwyn._package_utils import IdentifierPythonPath
|
|
16
16
|
from cadwyn.exceptions import CodeGenerationError
|
|
17
|
-
from cadwyn.structure.versions import Version
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from cadwyn.structure.versions import Version
|
|
20
|
+
|
|
21
|
+
from .._asts import _ValidatorWrapper, get_validator_info_or_none
|
|
20
22
|
|
|
21
23
|
_FieldName: TypeAlias = str
|
|
22
24
|
_CodegenPluginASTType = TypeVar("_CodegenPluginASTType", bound=ast.AST)
|
|
@@ -122,9 +124,9 @@ class _ModuleWrapper:
|
|
|
122
124
|
|
|
123
125
|
@dataclasses.dataclass(slots=True, kw_only=True)
|
|
124
126
|
class GlobalCodegenContext:
|
|
125
|
-
current_version: Version
|
|
126
|
-
latest_version: Version = dataclasses.field(init=False)
|
|
127
|
-
versions: list[Version]
|
|
127
|
+
current_version: "Version"
|
|
128
|
+
latest_version: "Version" = dataclasses.field(init=False)
|
|
129
|
+
versions: "list[Version]"
|
|
128
130
|
schemas: dict[IdentifierPythonPath, PydanticModelWrapper] = dataclasses.field(repr=False)
|
|
129
131
|
enums: dict[IdentifierPythonPath, _EnumWrapper] = dataclasses.field(repr=False)
|
|
130
132
|
modules: dict[IdentifierPythonPath, _ModuleWrapper] = dataclasses.field(repr=False)
|
|
@@ -10,9 +10,9 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
import ast_comments
|
|
12
12
|
|
|
13
|
+
from cadwyn._asts import get_all_names_defined_at_toplevel_of_module, read_python_module
|
|
13
14
|
from cadwyn._package_utils import IdentifierPythonPath, get_package_path_from_module, get_version_dir_path
|
|
14
15
|
from cadwyn._utils import get_index_of_latest_schema_dir_in_module_python_path
|
|
15
|
-
from cadwyn.codegen._asts import get_all_names_defined_at_toplevel_of_module, read_python_module
|
|
16
16
|
from cadwyn.codegen._common import (
|
|
17
17
|
CodegenContext,
|
|
18
18
|
CodegenPlugin,
|
|
@@ -4,6 +4,7 @@ from typing import Annotated, Any, cast, get_args, get_origin
|
|
|
4
4
|
|
|
5
5
|
from typing_extensions import assert_never
|
|
6
6
|
|
|
7
|
+
from cadwyn._asts import add_keyword_to_call, delete_keyword_from_call, get_fancy_repr
|
|
7
8
|
from cadwyn._compat import (
|
|
8
9
|
PYDANTIC_V2,
|
|
9
10
|
FieldInfo,
|
|
@@ -13,7 +14,6 @@ from cadwyn._compat import (
|
|
|
13
14
|
)
|
|
14
15
|
from cadwyn._package_utils import IdentifierPythonPath, get_cls_pythonpath
|
|
15
16
|
from cadwyn._utils import Sentinel
|
|
16
|
-
from cadwyn.codegen._asts import add_keyword_to_call, delete_keyword_from_call, get_fancy_repr
|
|
17
17
|
from cadwyn.codegen._common import GlobalCodegenContext, PydanticModelWrapper, _EnumWrapper
|
|
18
18
|
from cadwyn.exceptions import InvalidGenerationInstructionError
|
|
19
19
|
from cadwyn.structure.enums import AlterEnumSubInstruction, EnumDidntHaveMembersInstruction, EnumHadMembersInstruction
|
|
@@ -2,11 +2,11 @@ import ast
|
|
|
2
2
|
import copy
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from cadwyn.
|
|
6
|
-
from cadwyn.codegen._asts import (
|
|
5
|
+
from cadwyn._asts import (
|
|
7
6
|
get_fancy_repr,
|
|
8
7
|
pop_docstring_from_cls_body,
|
|
9
8
|
)
|
|
9
|
+
from cadwyn._package_utils import IdentifierPythonPath, get_absolute_python_path_of_import
|
|
10
10
|
from cadwyn.codegen._common import CodegenContext, PydanticModelWrapper, _EnumWrapper
|
|
11
11
|
|
|
12
12
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
1
5
|
from fastapi.routing import APIRoute
|
|
2
6
|
|
|
3
7
|
|
|
@@ -5,6 +9,19 @@ class CadwynError(Exception):
|
|
|
5
9
|
pass
|
|
6
10
|
|
|
7
11
|
|
|
12
|
+
class CadwynLatestRequestValidationError(CadwynError):
|
|
13
|
+
def __init__(self, errors: list[Any], body: Any, version: date) -> None:
|
|
14
|
+
self.errors = errors
|
|
15
|
+
self.body = body
|
|
16
|
+
self.version = version
|
|
17
|
+
super().__init__(
|
|
18
|
+
f"We failed to migrate the request with version={self.version!s}. "
|
|
19
|
+
"This means that there is some error in your migrations or schema structure that makes it impossible "
|
|
20
|
+
"to migrate the request of that version to latest.\n"
|
|
21
|
+
f"body={self.body}\n\nerrors={json.dumps(self.errors, indent=4, ensure_ascii=False)}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
8
25
|
class LintingError(CadwynError):
|
|
9
26
|
pass
|
|
10
27
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import inspect
|
|
3
3
|
import re
|
|
4
|
-
import sys
|
|
5
4
|
import typing
|
|
6
5
|
import warnings
|
|
7
6
|
from collections import defaultdict
|
|
@@ -44,11 +43,10 @@ from starlette.routing import (
|
|
|
44
43
|
from typing_extensions import Self, assert_never
|
|
45
44
|
|
|
46
45
|
from cadwyn._compat import model_fields, rebuild_fastapi_body_param
|
|
47
|
-
from cadwyn._package_utils import
|
|
48
|
-
from cadwyn._utils import Sentinel, UnionType,
|
|
46
|
+
from cadwyn._package_utils import get_version_dir_path
|
|
47
|
+
from cadwyn._utils import Sentinel, UnionType, get_another_version_of_cls
|
|
49
48
|
from cadwyn.exceptions import (
|
|
50
49
|
CadwynError,
|
|
51
|
-
ModuleIsNotVersionedError,
|
|
52
50
|
RouteAlreadyExistsError,
|
|
53
51
|
RouterGenerationError,
|
|
54
52
|
RouterPathParamsModifiedError,
|
|
@@ -381,8 +379,8 @@ def _validate_no_repetitions_in_routes(routes: list[fastapi.routing.APIRoute]):
|
|
|
381
379
|
@final
|
|
382
380
|
class _AnnotationTransformer:
|
|
383
381
|
__slots__ = (
|
|
382
|
+
"versions",
|
|
384
383
|
"latest_schemas_package",
|
|
385
|
-
"version_dirs",
|
|
386
384
|
"template_version_dir",
|
|
387
385
|
"latest_version_dir",
|
|
388
386
|
"change_versions_of_a_non_container_annotation",
|
|
@@ -398,16 +396,14 @@ class _AnnotationTransformer:
|
|
|
398
396
|
'The name of the latest schemas module must be "latest". '
|
|
399
397
|
f'Received "{latest_schemas_package.__name__}" instead.',
|
|
400
398
|
)
|
|
399
|
+
self.versions = versions
|
|
400
|
+
self.versions.latest_schemas_package = latest_schemas_package
|
|
401
401
|
self.latest_schemas_package = latest_schemas_package
|
|
402
|
-
self.version_dirs = frozenset(
|
|
403
|
-
[get_package_path_from_module(latest_schemas_package)]
|
|
404
|
-
+ [get_version_dir_path(latest_schemas_package, version.value) for version in versions],
|
|
405
|
-
)
|
|
406
402
|
# Okay, the naming is confusing, I know. Essentially template_version_dir is a dir of
|
|
407
403
|
# latest_schemas_package while latest_version_dir is a version equivalent to latest but
|
|
408
404
|
# with its own directory. Pick a better naming and make a PR, I am at your mercy.
|
|
409
|
-
self.template_version_dir = min(
|
|
410
|
-
self.latest_version_dir = max(
|
|
405
|
+
self.template_version_dir = min(versions.versioned_directories) # "latest" < "v0000_00_00"
|
|
406
|
+
self.latest_version_dir = max(versions.versioned_directories) # "v2005_11_11" > "v2000_11_11"
|
|
411
407
|
|
|
412
408
|
# This cache is not here for speeding things up. It's for preventing the creation of copies of the same object
|
|
413
409
|
# because such copies could produce weird behaviors at runtime, especially if you/fastapi do any comparisons.
|
|
@@ -450,15 +446,6 @@ class _AnnotationTransformer:
|
|
|
450
446
|
self.migrate_route_to_version(callback, version_dir, ignore_response_model=ignore_response_model)
|
|
451
447
|
_remake_endpoint_dependencies(route)
|
|
452
448
|
|
|
453
|
-
def get_another_version_of_cls(self, cls_from_old_version: type[Any], new_version_dir: Path):
|
|
454
|
-
# version_dir = /home/myuser/package/companies/v2021_01_01
|
|
455
|
-
module_from_old_version = sys.modules[cls_from_old_version.__module__]
|
|
456
|
-
try:
|
|
457
|
-
module = get_another_version_of_module(module_from_old_version, new_version_dir, self.version_dirs)
|
|
458
|
-
except ModuleIsNotVersionedError:
|
|
459
|
-
return cls_from_old_version
|
|
460
|
-
return getattr(module, cls_from_old_version.__name__)
|
|
461
|
-
|
|
462
449
|
def _change_versions_of_a_non_container_annotation(self, annotation: Any, version_dir: Path) -> Any:
|
|
463
450
|
if isinstance(annotation, _BaseGenericAlias | GenericAlias):
|
|
464
451
|
return get_origin(annotation)[
|
|
@@ -526,7 +513,7 @@ class _AnnotationTransformer:
|
|
|
526
513
|
)
|
|
527
514
|
else:
|
|
528
515
|
self._validate_source_file_is_located_in_template_dir(annotation, source_file)
|
|
529
|
-
return
|
|
516
|
+
return get_another_version_of_cls(annotation, version_dir, self.versions.versioned_directories)
|
|
530
517
|
else:
|
|
531
518
|
return annotation
|
|
532
519
|
|
|
@@ -539,7 +526,7 @@ class _AnnotationTransformer:
|
|
|
539
526
|
if (
|
|
540
527
|
source_file.startswith(dir_with_versions)
|
|
541
528
|
and not source_file.startswith(template_dir)
|
|
542
|
-
and any(source_file.startswith(str(d)) for d in self.
|
|
529
|
+
and any(source_file.startswith(str(d)) for d in self.versions.versioned_directories)
|
|
543
530
|
):
|
|
544
531
|
raise RouterGenerationError(
|
|
545
532
|
f'"{annotation}" is not defined in "{self.template_version_dir}" even though it must be. '
|
|
@@ -8,9 +8,9 @@ from typing import TYPE_CHECKING, Any, Literal
|
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
from pydantic.fields import FieldInfo
|
|
10
10
|
|
|
11
|
+
from cadwyn._asts import _ValidatorWrapper, get_validator_info_or_none
|
|
11
12
|
from cadwyn._compat import PYDANTIC_V2
|
|
12
13
|
from cadwyn._utils import Sentinel
|
|
13
|
-
from cadwyn.codegen._asts import _ValidatorWrapper, get_validator_info_or_none
|
|
14
14
|
from cadwyn.exceptions import CadwynStructureError
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
@@ -227,7 +227,7 @@ class AlterFieldInstructionFactory:
|
|
|
227
227
|
class ValidatorExistedInstruction:
|
|
228
228
|
schema: type[BaseModel]
|
|
229
229
|
validator: Callable[..., Any]
|
|
230
|
-
validator_info: _ValidatorWrapper = field(init=False)
|
|
230
|
+
validator_info: "_ValidatorWrapper" = field(init=False)
|
|
231
231
|
|
|
232
232
|
def __post_init__(self):
|
|
233
233
|
source = textwrap.dedent(inspect.getsource(self.validator))
|
|
@@ -7,6 +7,7 @@ from collections.abc import Callable, Sequence
|
|
|
7
7
|
from contextlib import AsyncExitStack
|
|
8
8
|
from contextvars import ContextVar
|
|
9
9
|
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
10
11
|
from types import ModuleType
|
|
11
12
|
from typing import Any, ClassVar, ParamSpec, TypeAlias, TypeVar, cast
|
|
12
13
|
|
|
@@ -26,9 +27,14 @@ from starlette._utils import is_async_callable
|
|
|
26
27
|
from typing_extensions import assert_never
|
|
27
28
|
|
|
28
29
|
from cadwyn._compat import PYDANTIC_V2, ModelField, PydanticUndefined, model_dump
|
|
29
|
-
from cadwyn._package_utils import
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
from cadwyn._package_utils import (
|
|
31
|
+
IdentifierPythonPath,
|
|
32
|
+
get_cls_pythonpath,
|
|
33
|
+
get_package_path_from_module,
|
|
34
|
+
get_version_dir_path,
|
|
35
|
+
)
|
|
36
|
+
from cadwyn._utils import classproperty, get_another_version_of_cls
|
|
37
|
+
from cadwyn.exceptions import CadwynError, CadwynLatestRequestValidationError, CadwynStructureError
|
|
32
38
|
|
|
33
39
|
from .._utils import Sentinel
|
|
34
40
|
from .common import Endpoint, VersionDate, VersionedModel
|
|
@@ -224,10 +230,13 @@ class VersionBundle:
|
|
|
224
230
|
/,
|
|
225
231
|
*other_versions: Version,
|
|
226
232
|
api_version_var: APIVersionVarType | None = None,
|
|
233
|
+
latest_schemas_package: ModuleType | None = None,
|
|
227
234
|
) -> None:
|
|
228
235
|
super().__init__()
|
|
229
236
|
|
|
237
|
+
self.latest_schemas_package: ModuleType | None = latest_schemas_package
|
|
230
238
|
self.versions = (latest_version, *other_versions)
|
|
239
|
+
self.version_dates = tuple(version.value for version in self.versions)
|
|
231
240
|
if api_version_var is None:
|
|
232
241
|
api_version_var = ContextVar("cadwyn_api_version")
|
|
233
242
|
self.api_version_var = api_version_var
|
|
@@ -292,6 +301,47 @@ class VersionBundle:
|
|
|
292
301
|
for instruction in version_change.alter_module_instructions
|
|
293
302
|
}
|
|
294
303
|
|
|
304
|
+
@functools.cached_property
|
|
305
|
+
def versioned_directories(self) -> tuple[Path, ...]:
|
|
306
|
+
if self.latest_schemas_package is None:
|
|
307
|
+
raise CadwynError(
|
|
308
|
+
f"You cannot call 'VersionBundle.{self.migrate_response_body.__name__}' because it has no access to "
|
|
309
|
+
"'latest_schemas_package'. It likely means that it was not attached "
|
|
310
|
+
"to any Cadwyn application which attaches 'latest_schemas_package' during initialization."
|
|
311
|
+
)
|
|
312
|
+
return tuple(
|
|
313
|
+
[get_package_path_from_module(self.latest_schemas_package)]
|
|
314
|
+
+ [get_version_dir_path(self.latest_schemas_package, version.value) for version in self]
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def migrate_response_body(self, latest_response_model: type[BaseModel], *, latest_body: Any, version: VersionDate):
|
|
318
|
+
"""Convert the data to a specific version by applying all version changes from latest until that version
|
|
319
|
+
in reverse order and wrapping the result in the correct version of latest_response_model.
|
|
320
|
+
"""
|
|
321
|
+
response = ResponseInfo(FastapiResponse(status_code=200), body=latest_body)
|
|
322
|
+
migrated_response = self._migrate_response(
|
|
323
|
+
response,
|
|
324
|
+
current_version=version,
|
|
325
|
+
latest_response_model=latest_response_model,
|
|
326
|
+
path="\0\0\0",
|
|
327
|
+
method="GET",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
version = self._get_closest_lesser_version(version)
|
|
331
|
+
# + 1 comes from latest also being in the versioned_directories list
|
|
332
|
+
version_dir = self.versioned_directories[self.version_dates.index(version) + 1]
|
|
333
|
+
|
|
334
|
+
versioned_response_model: type[BaseModel] = get_another_version_of_cls(
|
|
335
|
+
latest_response_model, version_dir, self.versioned_directories
|
|
336
|
+
)
|
|
337
|
+
return versioned_response_model.parse_obj(migrated_response.body)
|
|
338
|
+
|
|
339
|
+
def _get_closest_lesser_version(self, version: VersionDate):
|
|
340
|
+
for defined_version in self.version_dates:
|
|
341
|
+
if defined_version <= version:
|
|
342
|
+
return defined_version
|
|
343
|
+
raise CadwynError("You tried to migrate to version that is earlier than the first version which is prohibited.")
|
|
344
|
+
|
|
295
345
|
@functools.cached_property
|
|
296
346
|
def _version_changes_to_version_mapping(
|
|
297
347
|
self,
|
|
@@ -341,7 +391,9 @@ class VersionBundle:
|
|
|
341
391
|
**kwargs,
|
|
342
392
|
)
|
|
343
393
|
if errors:
|
|
344
|
-
raise
|
|
394
|
+
raise CadwynLatestRequestValidationError(
|
|
395
|
+
_normalize_errors(errors), body=request_info.body, version=current_version
|
|
396
|
+
)
|
|
345
397
|
return dependencies
|
|
346
398
|
raise NotImplementedError("This code should not be reachable. If it was reached -- it's a bug.")
|
|
347
399
|
|
|
@@ -349,7 +401,7 @@ class VersionBundle:
|
|
|
349
401
|
self,
|
|
350
402
|
response_info: ResponseInfo,
|
|
351
403
|
current_version: VersionDate,
|
|
352
|
-
|
|
404
|
+
latest_response_model: type[BaseModel],
|
|
353
405
|
path: str,
|
|
354
406
|
method: str,
|
|
355
407
|
) -> ResponseInfo:
|
|
@@ -371,11 +423,11 @@ class VersionBundle:
|
|
|
371
423
|
migrations_to_apply: list[_BaseAlterResponseInstruction] = []
|
|
372
424
|
|
|
373
425
|
if (
|
|
374
|
-
|
|
375
|
-
and
|
|
426
|
+
latest_response_model
|
|
427
|
+
and latest_response_model in version_change.alter_response_by_schema_instructions
|
|
376
428
|
):
|
|
377
429
|
migrations_to_apply.append(
|
|
378
|
-
version_change.alter_response_by_schema_instructions[
|
|
430
|
+
version_change.alter_response_by_schema_instructions[latest_response_model]
|
|
379
431
|
)
|
|
380
432
|
|
|
381
433
|
if path in version_change.alter_response_by_path_instructions:
|
|
@@ -485,7 +537,7 @@ class VersionBundle:
|
|
|
485
537
|
|
|
486
538
|
response_info = ResponseInfo(response_or_response_body, body)
|
|
487
539
|
else:
|
|
488
|
-
if fastapi_response_dependency.status_code is not None:
|
|
540
|
+
if fastapi_response_dependency.status_code is not None: # pyright: ignore[reportUnnecessaryComparison]
|
|
489
541
|
status_code = fastapi_response_dependency.status_code
|
|
490
542
|
elif route.status_code is not None:
|
|
491
543
|
status_code = route.status_code
|
|
@@ -507,7 +559,7 @@ class VersionBundle:
|
|
|
507
559
|
response_info = self._migrate_response(
|
|
508
560
|
response_info,
|
|
509
561
|
api_version,
|
|
510
|
-
latest_route,
|
|
562
|
+
latest_route.response_model,
|
|
511
563
|
route.path,
|
|
512
564
|
method,
|
|
513
565
|
)
|
|
@@ -574,7 +626,7 @@ class VersionBundle:
|
|
|
574
626
|
body = raw_body
|
|
575
627
|
else:
|
|
576
628
|
body = model_dump(raw_body, by_alias=True, exclude_unset=True)
|
|
577
|
-
if not PYDANTIC_V2 and raw_body.__custom_root_type__: # pyright: ignore[
|
|
629
|
+
if not PYDANTIC_V2 and raw_body.__custom_root_type__: # pyright: ignore[reportAttributeAccessIssue]
|
|
578
630
|
body = body["__root__"]
|
|
579
631
|
else:
|
|
580
632
|
# This is for requests without body or with complex body such as form or file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "cadwyn"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.6.0"
|
|
4
4
|
description = "Production-ready community-driven modern Stripe-like API versioning in FastAPI"
|
|
5
5
|
authors = ["Stanislav Zmiev <zmievsa@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -130,6 +130,7 @@ reportUnsupportedDunderAll = true
|
|
|
130
130
|
reportUnnecessaryTypeIgnoreComment = true
|
|
131
131
|
reportMissingSuperCall = true
|
|
132
132
|
reportFunctionMemberAccess = false
|
|
133
|
+
reportCircularImports = true
|
|
133
134
|
|
|
134
135
|
|
|
135
136
|
[tool.ruff]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|