cadwyn 3.4.5__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.4.5 → cadwyn-3.6.0}/PKG-INFO +2 -5
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/__init__.py +1 -1
- {cadwyn-3.4.5/cadwyn/codegen → cadwyn-3.6.0/cadwyn}/_asts.py +2 -2
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/_compat.py +5 -5
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/_utils.py +17 -3
- cadwyn-3.4.5/cadwyn/main.py → cadwyn-3.6.0/cadwyn/applications.py +15 -5
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/_common.py +8 -6
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/_main.py +1 -1
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/class_migrations.py +1 -1
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/class_rebuilding.py +2 -2
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/exceptions.py +17 -0
- cadwyn-3.6.0/cadwyn/main.py +11 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/routing.py +13 -26
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/structure/data.py +25 -5
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/structure/schemas.py +2 -2
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/structure/versions.py +74 -12
- {cadwyn-3.4.5 → cadwyn-3.6.0}/pyproject.toml +2 -1
- cadwyn-3.4.5/setup.py +0 -42
- {cadwyn-3.4.5 → cadwyn-3.6.0}/LICENSE +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/README.md +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/__main__.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/_package_utils.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/README.md +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/__init__.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/__init__.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/class_renaming.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/import_auto_adding.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/latest_version_aliasing.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/codegen/_plugins/module_migrations.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/py.typed +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/structure/__init__.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/structure/common.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/structure/endpoints.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/structure/enums.py +0 -0
- {cadwyn-3.4.5 → cadwyn-3.6.0}/cadwyn/structure/modules.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cadwyn
|
|
3
|
-
Version: 3.
|
|
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
|
|
@@ -22,9 +22,6 @@ Classifier: Programming Language :: Python
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
-
Classifier: Programming Language :: Python :: 3
|
|
26
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
27
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
28
25
|
Classifier: Programming Language :: Python :: 3.12
|
|
29
26
|
Classifier: Topic :: Internet
|
|
30
27
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
@@ -38,7 +35,7 @@ Provides-Extra: cli
|
|
|
38
35
|
Requires-Dist: better-ast-comments (>=1.2.1,<1.3.0)
|
|
39
36
|
Requires-Dist: fastapi (>=0.96.1)
|
|
40
37
|
Requires-Dist: pydantic (>=1.0.0)
|
|
41
|
-
Requires-Dist: typer (>=0.7.0); extra == "cli"
|
|
38
|
+
Requires-Dist: typer (>=0.7.0) ; extra == "cli"
|
|
42
39
|
Requires-Dist: typing-extensions
|
|
43
40
|
Requires-Dist: verselect (>=0.0.6)
|
|
44
41
|
Project-URL: Documentation, https://docs.cadwyn.dev
|
|
@@ -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
|
|
|
@@ -76,7 +76,7 @@ def transform_grouped_metadata(value: "annotated_types.GroupedMetadata"):
|
|
|
76
76
|
modified_fields = []
|
|
77
77
|
empty_obj = type(value)
|
|
78
78
|
|
|
79
|
-
for key in empty_obj.__dataclass_fields__: # pyright: ignore[
|
|
79
|
+
for key in empty_obj.__dataclass_fields__: # pyright: ignore[reportAttributeAccessIssue]
|
|
80
80
|
if getattr(value, key) != getattr(empty_obj, key):
|
|
81
81
|
modified_fields.append((key, getattr(value, key)))
|
|
82
82
|
|
|
@@ -136,7 +136,7 @@ def transform_auto(_: auto) -> Any:
|
|
|
136
136
|
return PlainRepr("auto()")
|
|
137
137
|
|
|
138
138
|
|
|
139
|
-
def transform_union(value: UnionType) -> Any:
|
|
139
|
+
def transform_union(value: UnionType) -> Any: # pyright: ignore[reportInvalidTypeForm]
|
|
140
140
|
return "typing.Union[" + (", ".join(get_fancy_repr(a) for a in get_args(value))) + "]"
|
|
141
141
|
|
|
142
142
|
|
|
@@ -7,7 +7,7 @@ import pydantic
|
|
|
7
7
|
from fastapi._compat import ModelField as FastAPIModelField
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
|
-
ModelField: TypeAlias = Any # pyright: ignore[
|
|
10
|
+
ModelField: TypeAlias = Any # pyright: ignore[reportRedeclaration]
|
|
11
11
|
PydanticUndefined: TypeAlias = Any
|
|
12
12
|
VALIDATOR_CONFIG_KEY = "__validators__"
|
|
13
13
|
|
|
@@ -61,7 +61,7 @@ class PydanticFieldWrapper:
|
|
|
61
61
|
|
|
62
62
|
annotation: Any
|
|
63
63
|
|
|
64
|
-
init_model_field: dataclasses.InitVar[ModelField] # pyright: ignore[
|
|
64
|
+
init_model_field: dataclasses.InitVar[ModelField] # pyright: ignore[reportInvalidTypeForm]
|
|
65
65
|
field_info: FieldInfo = dataclasses.field(init=False)
|
|
66
66
|
|
|
67
67
|
annotation_ast: ast.expr | None = None
|
|
@@ -69,7 +69,7 @@ class PydanticFieldWrapper:
|
|
|
69
69
|
# the value_ast is "None" and "Field(default=None)" respectively
|
|
70
70
|
value_ast: ast.expr | None = None
|
|
71
71
|
|
|
72
|
-
def __post_init__(self, init_model_field: ModelField): # pyright: ignore[
|
|
72
|
+
def __post_init__(self, init_model_field: ModelField): # pyright: ignore[reportInvalidTypeForm]
|
|
73
73
|
if isinstance(init_model_field, FieldInfo):
|
|
74
74
|
self.field_info = init_model_field
|
|
75
75
|
else:
|
|
@@ -134,10 +134,10 @@ def rebuild_fastapi_body_param(old_body_param: FastAPIModelField, new_body_param
|
|
|
134
134
|
kwargs.update(
|
|
135
135
|
{
|
|
136
136
|
"type_": new_body_param_type,
|
|
137
|
-
"class_validators": old_body_param.class_validators, # pyright: ignore[
|
|
137
|
+
"class_validators": old_body_param.class_validators, # pyright: ignore[reportAttributeAccessIssue]
|
|
138
138
|
"default": old_body_param.default,
|
|
139
139
|
"required": old_body_param.required,
|
|
140
|
-
"model_config": old_body_param.model_config, # pyright: ignore[
|
|
140
|
+
"model_config": old_body_param.model_config, # pyright: ignore[reportAttributeAccessIssue]
|
|
141
141
|
"alias": old_body_param.alias,
|
|
142
142
|
},
|
|
143
143
|
)
|
|
@@ -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
|
|
@@ -35,15 +36,28 @@ class PlainRepr(str):
|
|
|
35
36
|
|
|
36
37
|
def same_definition_as_in(t: _T) -> Callable[[Callable], _T]:
|
|
37
38
|
def decorator(f: Callable) -> _T:
|
|
38
|
-
return f # pyright: ignore[
|
|
39
|
+
return f # pyright: ignore[reportReturnType]
|
|
39
40
|
|
|
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
|
|
@@ -17,7 +16,7 @@ from typing import (
|
|
|
17
16
|
Generic,
|
|
18
17
|
TypeAlias,
|
|
19
18
|
TypeVar,
|
|
20
|
-
_BaseGenericAlias, # pyright: ignore[
|
|
19
|
+
_BaseGenericAlias, # pyright: ignore[reportAttributeAccessIssue]
|
|
21
20
|
cast,
|
|
22
21
|
final,
|
|
23
22
|
get_args,
|
|
@@ -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)[
|
|
@@ -470,7 +457,7 @@ class _AnnotationTransformer:
|
|
|
470
457
|
use_cache=annotation.use_cache,
|
|
471
458
|
)
|
|
472
459
|
elif isinstance(annotation, UnionType):
|
|
473
|
-
getitem = typing.Union.__getitem__ # pyright: ignore[
|
|
460
|
+
getitem = typing.Union.__getitem__ # pyright: ignore[reportAttributeAccessIssue]
|
|
474
461
|
return getitem(
|
|
475
462
|
tuple(self._change_version_of_annotations(a, version_dir) for a in get_args(annotation)),
|
|
476
463
|
)
|
|
@@ -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. '
|
|
@@ -734,7 +721,7 @@ def _copy_function(function: _T) -> _T:
|
|
|
734
721
|
if inspect.iscoroutinefunction(function):
|
|
735
722
|
|
|
736
723
|
@functools.wraps(function)
|
|
737
|
-
async def annotation_modifying_decorator( # pyright: ignore[
|
|
724
|
+
async def annotation_modifying_decorator( # pyright: ignore[reportRedeclaration]
|
|
738
725
|
*args: Any,
|
|
739
726
|
**kwargs: Any,
|
|
740
727
|
) -> Any:
|
|
@@ -750,7 +737,7 @@ def _copy_function(function: _T) -> _T:
|
|
|
750
737
|
return function(*args, **kwargs)
|
|
751
738
|
|
|
752
739
|
# Otherwise it will have the same signature as __wrapped__ due to how inspect module works
|
|
753
|
-
annotation_modifying_decorator.__alt_wrapped__ = ( # pyright: ignore[
|
|
740
|
+
annotation_modifying_decorator.__alt_wrapped__ = ( # pyright: ignore[reportAttributeAccessIssue]
|
|
754
741
|
annotation_modifying_decorator.__wrapped__
|
|
755
742
|
)
|
|
756
743
|
del annotation_modifying_decorator.__wrapped__
|
|
@@ -136,7 +136,7 @@ def convert_request_to_next_version_for(
|
|
|
136
136
|
transformer=transformer,
|
|
137
137
|
)
|
|
138
138
|
|
|
139
|
-
return decorator # pyright: ignore[
|
|
139
|
+
return decorator # pyright: ignore[reportReturnType]
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
############
|
|
@@ -144,8 +144,10 @@ def convert_request_to_next_version_for(
|
|
|
144
144
|
############
|
|
145
145
|
|
|
146
146
|
|
|
147
|
+
@dataclass
|
|
147
148
|
class _BaseAlterResponseInstruction(_AlterDataInstruction):
|
|
148
149
|
_payload_arg_name = "response"
|
|
150
|
+
migrate_http_errors: bool
|
|
149
151
|
|
|
150
152
|
|
|
151
153
|
@dataclass
|
|
@@ -160,12 +162,23 @@ class AlterResponseByPathInstruction(_BaseAlterResponseInstruction):
|
|
|
160
162
|
|
|
161
163
|
|
|
162
164
|
@overload
|
|
163
|
-
def convert_response_to_previous_version_for(
|
|
165
|
+
def convert_response_to_previous_version_for(
|
|
166
|
+
schema: type,
|
|
167
|
+
/,
|
|
168
|
+
*,
|
|
169
|
+
migrate_http_errors: bool = False,
|
|
170
|
+
) -> "type[staticmethod[_P, None]]":
|
|
164
171
|
...
|
|
165
172
|
|
|
166
173
|
|
|
167
174
|
@overload
|
|
168
|
-
def convert_response_to_previous_version_for(
|
|
175
|
+
def convert_response_to_previous_version_for(
|
|
176
|
+
path: str,
|
|
177
|
+
methods: list[str],
|
|
178
|
+
/,
|
|
179
|
+
*,
|
|
180
|
+
migrate_http_errors: bool = False,
|
|
181
|
+
) -> "type[staticmethod[_P, None]]":
|
|
169
182
|
...
|
|
170
183
|
|
|
171
184
|
|
|
@@ -173,6 +186,8 @@ def convert_response_to_previous_version_for(
|
|
|
173
186
|
schema_or_path: type | str,
|
|
174
187
|
methods: list[str] | None = None,
|
|
175
188
|
/,
|
|
189
|
+
*,
|
|
190
|
+
migrate_http_errors: bool = False,
|
|
176
191
|
) -> "type[staticmethod[_P, None]]":
|
|
177
192
|
_validate_decorator_args(schema_or_path, methods)
|
|
178
193
|
|
|
@@ -183,11 +198,16 @@ def convert_response_to_previous_version_for(
|
|
|
183
198
|
path=schema_or_path,
|
|
184
199
|
methods=set(cast(list, methods)),
|
|
185
200
|
transformer=transformer,
|
|
201
|
+
migrate_http_errors=migrate_http_errors,
|
|
186
202
|
)
|
|
187
203
|
else:
|
|
188
|
-
return AlterResponseBySchemaInstruction(
|
|
204
|
+
return AlterResponseBySchemaInstruction(
|
|
205
|
+
schema=schema_or_path,
|
|
206
|
+
transformer=transformer,
|
|
207
|
+
migrate_http_errors=migrate_http_errors,
|
|
208
|
+
)
|
|
189
209
|
|
|
190
|
-
return decorator # pyright: ignore[
|
|
210
|
+
return decorator # pyright: ignore[reportReturnType]
|
|
191
211
|
|
|
192
212
|
|
|
193
213
|
def _validate_decorator_args(schema_or_path: type | str, methods: list[str] | None):
|
|
@@ -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
|
|
@@ -39,6 +45,7 @@ from .data import (
|
|
|
39
45
|
AlterResponseBySchemaInstruction,
|
|
40
46
|
RequestInfo,
|
|
41
47
|
ResponseInfo,
|
|
48
|
+
_BaseAlterResponseInstruction,
|
|
42
49
|
)
|
|
43
50
|
from .endpoints import AlterEndpointSubInstruction
|
|
44
51
|
from .enums import AlterEnumSubInstruction
|
|
@@ -223,10 +230,13 @@ class VersionBundle:
|
|
|
223
230
|
/,
|
|
224
231
|
*other_versions: Version,
|
|
225
232
|
api_version_var: APIVersionVarType | None = None,
|
|
233
|
+
latest_schemas_package: ModuleType | None = None,
|
|
226
234
|
) -> None:
|
|
227
235
|
super().__init__()
|
|
228
236
|
|
|
237
|
+
self.latest_schemas_package: ModuleType | None = latest_schemas_package
|
|
229
238
|
self.versions = (latest_version, *other_versions)
|
|
239
|
+
self.version_dates = tuple(version.value for version in self.versions)
|
|
230
240
|
if api_version_var is None:
|
|
231
241
|
api_version_var = ContextVar("cadwyn_api_version")
|
|
232
242
|
self.api_version_var = api_version_var
|
|
@@ -291,6 +301,47 @@ class VersionBundle:
|
|
|
291
301
|
for instruction in version_change.alter_module_instructions
|
|
292
302
|
}
|
|
293
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
|
+
|
|
294
345
|
@functools.cached_property
|
|
295
346
|
def _version_changes_to_version_mapping(
|
|
296
347
|
self,
|
|
@@ -340,7 +391,9 @@ class VersionBundle:
|
|
|
340
391
|
**kwargs,
|
|
341
392
|
)
|
|
342
393
|
if errors:
|
|
343
|
-
raise
|
|
394
|
+
raise CadwynLatestRequestValidationError(
|
|
395
|
+
_normalize_errors(errors), body=request_info.body, version=current_version
|
|
396
|
+
)
|
|
344
397
|
return dependencies
|
|
345
398
|
raise NotImplementedError("This code should not be reachable. If it was reached -- it's a bug.")
|
|
346
399
|
|
|
@@ -348,7 +401,7 @@ class VersionBundle:
|
|
|
348
401
|
self,
|
|
349
402
|
response_info: ResponseInfo,
|
|
350
403
|
current_version: VersionDate,
|
|
351
|
-
|
|
404
|
+
latest_response_model: type[BaseModel],
|
|
352
405
|
path: str,
|
|
353
406
|
method: str,
|
|
354
407
|
) -> ResponseInfo:
|
|
@@ -367,15 +420,24 @@ class VersionBundle:
|
|
|
367
420
|
if v.value <= current_version:
|
|
368
421
|
break
|
|
369
422
|
for version_change in v.version_changes:
|
|
423
|
+
migrations_to_apply: list[_BaseAlterResponseInstruction] = []
|
|
424
|
+
|
|
370
425
|
if (
|
|
371
|
-
|
|
372
|
-
and
|
|
426
|
+
latest_response_model
|
|
427
|
+
and latest_response_model in version_change.alter_response_by_schema_instructions
|
|
373
428
|
):
|
|
374
|
-
|
|
429
|
+
migrations_to_apply.append(
|
|
430
|
+
version_change.alter_response_by_schema_instructions[latest_response_model]
|
|
431
|
+
)
|
|
432
|
+
|
|
375
433
|
if path in version_change.alter_response_by_path_instructions:
|
|
376
434
|
for instruction in version_change.alter_response_by_path_instructions[path]:
|
|
377
435
|
if method in instruction.methods:
|
|
378
|
-
instruction
|
|
436
|
+
migrations_to_apply.append(instruction)
|
|
437
|
+
|
|
438
|
+
for migration in migrations_to_apply:
|
|
439
|
+
if response_info.status_code < 300 or migration.migrate_http_errors:
|
|
440
|
+
migration(response_info)
|
|
379
441
|
return response_info
|
|
380
442
|
|
|
381
443
|
# TODO (https://github.com/zmievsa/cadwyn/issues/113): Refactor this function and all functions it calls.
|
|
@@ -475,7 +537,7 @@ class VersionBundle:
|
|
|
475
537
|
|
|
476
538
|
response_info = ResponseInfo(response_or_response_body, body)
|
|
477
539
|
else:
|
|
478
|
-
if fastapi_response_dependency.status_code is not None:
|
|
540
|
+
if fastapi_response_dependency.status_code is not None: # pyright: ignore[reportUnnecessaryComparison]
|
|
479
541
|
status_code = fastapi_response_dependency.status_code
|
|
480
542
|
elif route.status_code is not None:
|
|
481
543
|
status_code = route.status_code
|
|
@@ -497,7 +559,7 @@ class VersionBundle:
|
|
|
497
559
|
response_info = self._migrate_response(
|
|
498
560
|
response_info,
|
|
499
561
|
api_version,
|
|
500
|
-
latest_route,
|
|
562
|
+
latest_route.response_model,
|
|
501
563
|
route.path,
|
|
502
564
|
method,
|
|
503
565
|
)
|
|
@@ -564,7 +626,7 @@ class VersionBundle:
|
|
|
564
626
|
body = raw_body
|
|
565
627
|
else:
|
|
566
628
|
body = model_dump(raw_body, by_alias=True, exclude_unset=True)
|
|
567
|
-
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]
|
|
568
630
|
body = body["__root__"]
|
|
569
631
|
else:
|
|
570
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]
|
cadwyn-3.4.5/setup.py
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
from setuptools import setup
|
|
3
|
-
|
|
4
|
-
packages = \
|
|
5
|
-
['cadwyn', 'cadwyn.codegen', 'cadwyn.codegen._plugins', 'cadwyn.structure']
|
|
6
|
-
|
|
7
|
-
package_data = \
|
|
8
|
-
{'': ['*']}
|
|
9
|
-
|
|
10
|
-
install_requires = \
|
|
11
|
-
['better-ast-comments>=1.2.1,<1.3.0',
|
|
12
|
-
'fastapi>=0.96.1',
|
|
13
|
-
'pydantic>=1.0.0',
|
|
14
|
-
'typing-extensions',
|
|
15
|
-
'verselect>=0.0.6']
|
|
16
|
-
|
|
17
|
-
extras_require = \
|
|
18
|
-
{'cli': ['typer>=0.7.0']}
|
|
19
|
-
|
|
20
|
-
entry_points = \
|
|
21
|
-
{'console_scripts': ['cadwyn = cadwyn.__main__:app']}
|
|
22
|
-
|
|
23
|
-
setup_kwargs = {
|
|
24
|
-
'name': 'cadwyn',
|
|
25
|
-
'version': '3.4.5',
|
|
26
|
-
'description': 'Production-ready community-driven modern Stripe-like API versioning in FastAPI',
|
|
27
|
-
'long_description': '# Cadwyn\n\nProduction-ready community-driven modern [Stripe-like](https://stripe.com/blog/api-versioning) API versioning in FastAPI\n\n---\n\n<p align="center">\n<a href="https://github.com/zmievsa/cadwyn/actions?query=workflow%3ATests+event%3Apush+branch%3Amain" target="_blank">\n <img src="https://github.com/zmievsa/cadwyn/actions/workflows/test.yaml/badge.svg?branch=main&event=push" alt="Test">\n</a>\n<a href="https://codecov.io/gh/ovsyanka83/cadwyn" target="_blank">\n <img src="https://img.shields.io/codecov/c/github/ovsyanka83/cadwyn?color=%2334D058" alt="Coverage">\n</a>\n<a href="https://pypi.org/project/cadwyn/" target="_blank">\n <img alt="PyPI" src="https://img.shields.io/pypi/v/cadwyn?color=%2334D058&label=pypi%20package" alt="Package version">\n</a>\n<a href="https://pypi.org/project/cadwyn/" target="_blank">\n <img src="https://img.shields.io/pypi/pyversions/cadwyn?color=%2334D058" alt="Supported Python versions">\n</a>\n</p>\n\n## Who is this for?\n\nCadwyn allows you to support a single version of your code while auto-generating the schemas and routes for older versions. You keep API versioning encapsulated in small and independent "version change" modules while your business logic stays simple and knows nothing about versioning.\n\nIts [approach](https://docs.cadwyn.dev/theory/#ii-migration-based-response-building) will be useful if you want to:\n\n1. Support many API versions for a long time\n2. Effortlessly backport features and bugfixes to older API versions\n\nWhether you are a newbie in API versioning, a pro looking for a sophisticated tool, an experimenter looking to build a similar framework, or even someone who just wants to learn about all approaches to API versioning -- Cadwyn has the functionality, theory, and documentation to cover all the mentioned use cases.\n\n## Get started\n\nThe [documentation](https://docs.cadwyn.dev) has everything you need to succeed.\n\n## Sponsors\n\nThese are our gorgeous sponsors. They are using Cadwyn and are sponsoring it through various means. Contact [me](https://github.com/zmievsa) if you would like to become one too!\n\n[](https://docs.monite.com/)\n',
|
|
28
|
-
'author': 'Stanislav Zmiev',
|
|
29
|
-
'author_email': 'zmievsa@gmail.com',
|
|
30
|
-
'maintainer': 'None',
|
|
31
|
-
'maintainer_email': 'None',
|
|
32
|
-
'url': 'https://github.com/zmievsa/cadwyn',
|
|
33
|
-
'packages': packages,
|
|
34
|
-
'package_data': package_data,
|
|
35
|
-
'install_requires': install_requires,
|
|
36
|
-
'extras_require': extras_require,
|
|
37
|
-
'entry_points': entry_points,
|
|
38
|
-
'python_requires': '>=3.10,<4.0',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
setup(**setup_kwargs)
|
|
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
|