cadwyn 3.5.0__py3-none-any.whl → 3.6.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 CHANGED
@@ -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
 
cadwyn/_utils.py CHANGED
@@ -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
  # ^^^^^^
cadwyn/applications.py ADDED
@@ -0,0 +1,126 @@
1
+ from collections.abc import Callable, Coroutine, Sequence
2
+ from typing import TYPE_CHECKING, Any
3
+
4
+ from fastapi import APIRouter, routing
5
+ from fastapi.datastructures import Default
6
+ from fastapi.params import Depends
7
+ from fastapi.utils import generate_unique_id
8
+ from starlette.middleware import Middleware
9
+ from starlette.requests import Request
10
+ from starlette.responses import JSONResponse, Response
11
+ from starlette.routing import BaseRoute
12
+ from starlette.types import Lifespan
13
+ from typing_extensions import Self
14
+ from verselect import HeaderRoutingFastAPI
15
+
16
+ from cadwyn.exceptions import CadwynError
17
+ from cadwyn.routing import generate_versioned_routers
18
+ from cadwyn.structure import VersionBundle
19
+
20
+ if TYPE_CHECKING:
21
+ from types import ModuleType
22
+
23
+
24
+ class Cadwyn(HeaderRoutingFastAPI):
25
+ def __init__(
26
+ self,
27
+ *,
28
+ versions: VersionBundle,
29
+ api_version_header_name: str = "x-api-version",
30
+ debug: bool = False,
31
+ title: str = "FastAPI",
32
+ summary: str | None = None,
33
+ description: str = "",
34
+ version: str = "0.1.0",
35
+ openapi_url: str | None = "/openapi.json",
36
+ openapi_tags: list[dict[str, Any]] | None = None,
37
+ servers: list[dict[str, str | Any]] | None = None,
38
+ dependencies: Sequence[Depends] | None = None,
39
+ default_response_class: type[Response] = Default(JSONResponse), # noqa: B008
40
+ redirect_slashes: bool = True,
41
+ docs_url: str | None = "/docs",
42
+ redoc_url: None = None,
43
+ swagger_ui_oauth2_redirect_url: str | None = "/docs/oauth2-redirect",
44
+ swagger_ui_init_oauth: dict[str, Any] | None = None,
45
+ middleware: Sequence[Middleware] | None = None,
46
+ exception_handlers: dict[int | type[Exception], Callable[[Request, Any], Coroutine[Any, Any, Response]]]
47
+ | None = None,
48
+ on_startup: Sequence[Callable[[], Any]] | None = None,
49
+ on_shutdown: Sequence[Callable[[], Any]] | None = None,
50
+ lifespan: Lifespan[Self] | None = None,
51
+ terms_of_service: str | None = None,
52
+ contact: dict[str, str | Any] | None = None,
53
+ license_info: dict[str, str | Any] | None = None,
54
+ openapi_prefix: str = "",
55
+ root_path: str = "",
56
+ root_path_in_servers: bool = True,
57
+ responses: dict[int | str, dict[str, Any]] | None = None,
58
+ callbacks: list[BaseRoute] | None = None,
59
+ webhooks: APIRouter | None = None,
60
+ deprecated: bool | None = None,
61
+ include_in_schema: bool = True,
62
+ swagger_ui_parameters: dict[str, Any] | None = None,
63
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(generate_unique_id), # noqa: B008
64
+ separate_input_output_schemas: bool = True,
65
+ **extra: Any,
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
+
77
+ super().__init__(
78
+ api_version_header_name=api_version_header_name,
79
+ api_version_var=versions.api_version_var,
80
+ debug=debug,
81
+ title=title,
82
+ summary=summary,
83
+ description=description,
84
+ version=version,
85
+ openapi_url=openapi_url,
86
+ openapi_tags=openapi_tags,
87
+ servers=servers,
88
+ dependencies=dependencies,
89
+ default_response_class=default_response_class,
90
+ redirect_slashes=redirect_slashes,
91
+ docs_url=docs_url,
92
+ swagger_ui_oauth2_redirect_url=swagger_ui_oauth2_redirect_url,
93
+ swagger_ui_init_oauth=swagger_ui_init_oauth,
94
+ middleware=middleware,
95
+ exception_handlers=exception_handlers,
96
+ on_startup=on_startup,
97
+ on_shutdown=on_shutdown,
98
+ lifespan=lifespan,
99
+ terms_of_service=terms_of_service,
100
+ contact=contact,
101
+ license_info=license_info,
102
+ openapi_prefix=openapi_prefix,
103
+ root_path=root_path,
104
+ root_path_in_servers=root_path_in_servers,
105
+ responses=responses,
106
+ callbacks=callbacks,
107
+ webhooks=webhooks,
108
+ deprecated=deprecated,
109
+ include_in_schema=include_in_schema,
110
+ swagger_ui_parameters=swagger_ui_parameters,
111
+ generate_unique_id_function=generate_unique_id_function,
112
+ separate_input_output_schemas=separate_input_output_schemas,
113
+ **extra,
114
+ )
115
+
116
+ def generate_and_include_versioned_routers(self, *routers: APIRouter) -> None:
117
+ root_router = APIRouter()
118
+ for router in routers:
119
+ root_router.include_router(router)
120
+ router_versions = generate_versioned_routers(
121
+ root_router,
122
+ versions=self.versions,
123
+ latest_schemas_package=self.latest_schemas_package,
124
+ )
125
+ for version, router in router_versions.items():
126
+ self.add_header_versioned_routers(router, header_value=version.isoformat())
cadwyn/codegen/_common.py CHANGED
@@ -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
- from ._asts import _ValidatorWrapper, get_validator_info_or_none
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)
cadwyn/codegen/_main.py CHANGED
@@ -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._package_utils import IdentifierPythonPath, get_absolute_python_path_of_import
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
 
cadwyn/exceptions.py CHANGED
@@ -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
 
cadwyn/main.py CHANGED
@@ -1,116 +1,11 @@
1
- from collections.abc import Callable, Coroutine, Sequence
2
- from types import ModuleType
3
- from typing import Any
1
+ from warnings import warn
4
2
 
5
- from fastapi import APIRouter, routing
6
- from fastapi.datastructures import Default
7
- from fastapi.params import Depends
8
- from fastapi.utils import generate_unique_id
9
- from starlette.middleware import Middleware
10
- from starlette.requests import Request
11
- from starlette.responses import JSONResponse, Response
12
- from starlette.routing import BaseRoute
13
- from starlette.types import Lifespan
14
- from typing_extensions import Self
15
- from verselect import HeaderRoutingFastAPI
3
+ from .applications import Cadwyn
16
4
 
17
- from cadwyn.routing import generate_versioned_routers
18
- from cadwyn.structure import VersionBundle
5
+ warn(
6
+ "'cadwyn.main' module is deprecated. Please use 'cadwyn.applications' instead.",
7
+ DeprecationWarning,
8
+ stacklevel=2,
9
+ )
19
10
 
20
-
21
- class Cadwyn(HeaderRoutingFastAPI):
22
- def __init__(
23
- self,
24
- *,
25
- versions: VersionBundle,
26
- latest_schemas_package: ModuleType,
27
- api_version_header_name: str = "x-api-version",
28
- debug: bool = False,
29
- title: str = "FastAPI",
30
- summary: str | None = None,
31
- description: str = "",
32
- version: str = "0.1.0",
33
- openapi_url: str | None = "/openapi.json",
34
- openapi_tags: list[dict[str, Any]] | None = None,
35
- servers: list[dict[str, str | Any]] | None = None,
36
- dependencies: Sequence[Depends] | None = None,
37
- default_response_class: type[Response] = Default(JSONResponse), # noqa: B008
38
- redirect_slashes: bool = True,
39
- docs_url: str | None = "/docs",
40
- redoc_url: None = None,
41
- swagger_ui_oauth2_redirect_url: str | None = "/docs/oauth2-redirect",
42
- swagger_ui_init_oauth: dict[str, Any] | None = None,
43
- middleware: Sequence[Middleware] | None = None,
44
- exception_handlers: dict[int | type[Exception], Callable[[Request, Any], Coroutine[Any, Any, Response]]]
45
- | None = None,
46
- on_startup: Sequence[Callable[[], Any]] | None = None,
47
- on_shutdown: Sequence[Callable[[], Any]] | None = None,
48
- lifespan: Lifespan[Self] | None = None,
49
- terms_of_service: str | None = None,
50
- contact: dict[str, str | Any] | None = None,
51
- license_info: dict[str, str | Any] | None = None,
52
- openapi_prefix: str = "",
53
- root_path: str = "",
54
- root_path_in_servers: bool = True,
55
- responses: dict[int | str, dict[str, Any]] | None = None,
56
- callbacks: list[BaseRoute] | None = None,
57
- webhooks: APIRouter | None = None,
58
- deprecated: bool | None = None,
59
- include_in_schema: bool = True,
60
- swagger_ui_parameters: dict[str, Any] | None = None,
61
- generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(generate_unique_id), # noqa: B008
62
- separate_input_output_schemas: bool = True,
63
- **extra: Any,
64
- ) -> None:
65
- super().__init__(
66
- api_version_header_name=api_version_header_name,
67
- api_version_var=versions.api_version_var,
68
- debug=debug,
69
- title=title,
70
- summary=summary,
71
- description=description,
72
- version=version,
73
- openapi_url=openapi_url,
74
- openapi_tags=openapi_tags,
75
- servers=servers,
76
- dependencies=dependencies,
77
- default_response_class=default_response_class,
78
- redirect_slashes=redirect_slashes,
79
- docs_url=docs_url,
80
- swagger_ui_oauth2_redirect_url=swagger_ui_oauth2_redirect_url,
81
- swagger_ui_init_oauth=swagger_ui_init_oauth,
82
- middleware=middleware,
83
- exception_handlers=exception_handlers,
84
- on_startup=on_startup,
85
- on_shutdown=on_shutdown,
86
- lifespan=lifespan,
87
- terms_of_service=terms_of_service,
88
- contact=contact,
89
- license_info=license_info,
90
- openapi_prefix=openapi_prefix,
91
- root_path=root_path,
92
- root_path_in_servers=root_path_in_servers,
93
- responses=responses,
94
- callbacks=callbacks,
95
- webhooks=webhooks,
96
- deprecated=deprecated,
97
- include_in_schema=include_in_schema,
98
- swagger_ui_parameters=swagger_ui_parameters,
99
- generate_unique_id_function=generate_unique_id_function,
100
- separate_input_output_schemas=separate_input_output_schemas,
101
- **extra,
102
- )
103
- self.versions = versions
104
- self.latest_schemas_package = latest_schemas_package
105
-
106
- def generate_and_include_versioned_routers(self, *routers: APIRouter) -> None:
107
- root_router = APIRouter()
108
- for router in routers:
109
- root_router.include_router(router)
110
- router_versions = generate_versioned_routers(
111
- root_router,
112
- versions=self.versions,
113
- latest_schemas_package=self.latest_schemas_package,
114
- )
115
- for version, router in router_versions.items():
116
- self.add_header_versioned_routers(router, header_value=version.isoformat())
11
+ __all__ = ["Cadwyn"]
cadwyn/routing.py CHANGED
@@ -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 get_package_path_from_module, get_version_dir_path
48
- from cadwyn._utils import Sentinel, UnionType, get_another_version_of_module
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(self.version_dirs) # "latest" < "v0000_00_00"
410
- self.latest_version_dir = max(self.version_dirs) # "v2005_11_11" > "v2000_11_11"
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 self.get_another_version_of_cls(annotation, version_dir)
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.version_dirs)
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 IdentifierPythonPath, get_cls_pythonpath
30
- from cadwyn._utils import classproperty
31
- from cadwyn.exceptions import CadwynError, CadwynStructureError
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 RequestValidationError(_normalize_errors(errors), body=request_info.body)
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
- latest_route: APIRoute,
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
- latest_route.response_model
375
- and latest_route.response_model in version_change.alter_response_by_schema_instructions
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[latest_route.response_model]
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[reportGeneralTypeIssues]
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
  Metadata-Version: 2.1
2
2
  Name: cadwyn
3
- Version: 3.5.0
3
+ Version: 3.6.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
@@ -1,34 +1,35 @@
1
- cadwyn/__init__.py,sha256=gVLVH3SSBGH0IQYGL5tbro4s0vk--9sAym0UvoG3s1w,478
1
+ cadwyn/__init__.py,sha256=L5OVmOYlh5z3OYvwj6HidGBy8gvstCPMX5l4f4E-wr0,486
2
2
  cadwyn/__main__.py,sha256=JUNmAhwn7tG1EeXI82QmFZE-fpjfAOv2kxFNDfxWbhQ,2851
3
+ cadwyn/_asts.py,sha256=jFdnbkDDu_10YhJemDUVMut_XoqwIvGMtCQpDeitCiM,10206
3
4
  cadwyn/_compat.py,sha256=6QwtzbXn53mIhEFfEizmFjd-f894oLsM6ITxqq2rCpc,5408
4
5
  cadwyn/_package_utils.py,sha256=trxTYLmppv-10SKhScfyDQJh21rsQGFoLaOtHycKKR0,1443
5
- cadwyn/_utils.py,sha256=vgGfZpbCL5AXtOyZX36sw-XjNT8UfYuZscweJqyDvrQ,4267
6
+ cadwyn/_utils.py,sha256=nBBE9PGo9MuHlCgbX8JPz9XcQxF7zIbVtKaBEsgfhGc,4861
7
+ cadwyn/applications.py,sha256=sFx3d7fOx7wGCcLIcF5IoY_QbPX0SooYYzvb4iWtpWg,5397
6
8
  cadwyn/codegen/README.md,sha256=V2Kz2IOz1cTxrC-RnQ7YbWEVCIGYr3tR4IPCvepeq0M,1047
7
9
  cadwyn/codegen/__init__.py,sha256=JgddDjxMTjSfVrMXHwNu1ODgdn2QfPWpccrRKquBV6k,355
8
- cadwyn/codegen/_asts.py,sha256=jFdnbkDDu_10YhJemDUVMut_XoqwIvGMtCQpDeitCiM,10206
9
- cadwyn/codegen/_common.py,sha256=6vU9RtDPkXtuseRDtHeBbWYSTFwGtONv4OCA7BQrr3I,5651
10
- cadwyn/codegen/_main.py,sha256=wiadc3OYn1MlLwirfWuhkanvr2El-GjeQJpmpxHc4jA,9122
10
+ cadwyn/codegen/_common.py,sha256=j-K0zMINmOKPhwsKjtfmrUrNzgEU4gM2EEFC7wvpCdU,5696
11
+ cadwyn/codegen/_main.py,sha256=3aKB2g08WJqu86rc0VYfh1UfeHfipd1-tGncjiDKOlA,9114
11
12
  cadwyn/codegen/_plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- cadwyn/codegen/_plugins/class_migrations.py,sha256=dbmXMdfUGA2X4phsNdtRQQde4SuoOYtNB86XL05U1jY,20159
13
- cadwyn/codegen/_plugins/class_rebuilding.py,sha256=mJR297bqsLUfP4HW5_1GuHlpiYSQd851yHpG_ajjilg,3703
13
+ cadwyn/codegen/_plugins/class_migrations.py,sha256=NS1FfNfrBL2fbg27-0xZOvFYDE6I2wxfdR5VdR38ytk,20151
14
+ cadwyn/codegen/_plugins/class_rebuilding.py,sha256=15SI1AAQgLgIkz176Gf86whaw0NWAYX8_2ogYHUeT1g,3695
14
15
  cadwyn/codegen/_plugins/class_renaming.py,sha256=5ka2W1c18i4maNbEkEpELvGLEFbd8tthvQX3YA3Bu0A,1843
15
16
  cadwyn/codegen/_plugins/import_auto_adding.py,sha256=00zGK99cT-bq2eXKDlYBR5-Z3uHLOGU7dbhB0YFFrt0,2613
16
17
  cadwyn/codegen/_plugins/latest_version_aliasing.py,sha256=9MPW-FMOcjBZ7L05T0sxwSDCfFZHAn6xZV_E1KImbUA,3946
17
18
  cadwyn/codegen/_plugins/module_migrations.py,sha256=TeWJk4Iu4SRQ9K2iI3v3sCs1110jrltKlPdfU9mXIsQ,722
18
- cadwyn/exceptions.py,sha256=0nvauw2r5VOaTg9tQ9TZgLdQd3xiqqi6YD7e3-vOcVs,766
19
- cadwyn/main.py,sha256=_hC2Ke1uwtnjg2WueDXlg_QnzFgJbTcAlpHgqUBTmg4,4899
19
+ cadwyn/exceptions.py,sha256=JUHFjxWl2XlnweVnSpq3mWFUNU0ocyXBBleMMQyCyBk,1442
20
+ cadwyn/main.py,sha256=kt2Vn7TIA4ZnD_xrgz57TOjUk-4zVP8SV8nuTZBEaaU,218
20
21
  cadwyn/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- cadwyn/routing.py,sha256=w9k33VADVX8oPadmne0KLJB6HEM3yjFvFEnRCAfgI64,35568
22
+ cadwyn/routing.py,sha256=njVzrVOruO-WFI74f3JIfATivSiz1C5EJuWjjZyaseI,34937
22
23
  cadwyn/structure/__init__.py,sha256=BjFPlQYCw8ds_4zxdCi2LimarUGqSzyTNmOdT-FkGms,661
23
24
  cadwyn/structure/common.py,sha256=6Z4nI97XPWTCinn6np73m-rLPyYNrz2fWXKJlqjsiaQ,269
24
25
  cadwyn/structure/data.py,sha256=u5-pJ48BRnPSt2JbH6AefHGddTEE5z7YBnyv0iGw3dQ,6133
25
26
  cadwyn/structure/endpoints.py,sha256=VngfAydGBwekhV2tBOtNDPVgl3X1IgYxUCw--VZ5cQY,5627
26
27
  cadwyn/structure/enums.py,sha256=iMokxA2QYJ61SzyB-Pmuq3y7KL7-e6TsnjLVUaVZQnw,954
27
28
  cadwyn/structure/modules.py,sha256=1FK-lLm-zOTXEvn-QtyBH38aDRht5PDQiZrOPCsBlM4,1268
28
- cadwyn/structure/schemas.py,sha256=LIKwDuzorVC9AHg4EN-UYdI133lCk_2MkBTdiyAr-EQ,8808
29
- cadwyn/structure/versions.py,sha256=LUFuumsPK7VX7yD2gEuJyvQ03rw_Gkh6LJl2sqJguAA,30124
30
- cadwyn-3.5.0.dist-info/LICENSE,sha256=KeCWewiDQYpmSnzF-p_0YpoWiyDcUPaCuG8OWQs4ig4,1072
31
- cadwyn-3.5.0.dist-info/METADATA,sha256=TWWKK_R27K5CVpeW1-bCaTGbNoiIBdaAp2K01YsZ3KU,4115
32
- cadwyn-3.5.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
33
- cadwyn-3.5.0.dist-info/entry_points.txt,sha256=eO05hLn9GoRzzpwT9GONPmXKsonjuMNssM2D2WHWKGk,46
34
- cadwyn-3.5.0.dist-info/RECORD,,
29
+ cadwyn/structure/schemas.py,sha256=5hExJEdvEFPK4Cv8G_Gh6E6ltiNMyId_UjGIJz3jz0o,8802
30
+ cadwyn/structure/versions.py,sha256=6M_saZPlzEQ0LNRQywhtrw-YPdjT0P18EEPTRg1dR8Y,32785
31
+ cadwyn-3.6.0.dist-info/LICENSE,sha256=KeCWewiDQYpmSnzF-p_0YpoWiyDcUPaCuG8OWQs4ig4,1072
32
+ cadwyn-3.6.0.dist-info/METADATA,sha256=Z3Y3MtpC_AnXyqeU6lfkfzuye8_di4uiGuUPBrzNjBg,4115
33
+ cadwyn-3.6.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
34
+ cadwyn-3.6.0.dist-info/entry_points.txt,sha256=eO05hLn9GoRzzpwT9GONPmXKsonjuMNssM2D2WHWKGk,46
35
+ cadwyn-3.6.0.dist-info/RECORD,,
File without changes
File without changes