cadwyn 3.15.5__tar.gz → 3.15.7__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.15.5 → cadwyn-3.15.7}/PKG-INFO +1 -1
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/__init__.py +3 -1
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/route_generation.py +94 -53
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/versions.py +4 -3
- {cadwyn-3.15.5 → cadwyn-3.15.7}/pyproject.toml +3 -2
- {cadwyn-3.15.5 → cadwyn-3.15.7}/LICENSE +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/README.md +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/__main__.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/_asts.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/_compat.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/_package_utils.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/_utils.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/applications.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/README.md +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/__init__.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_common.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_main.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/__init__.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/class_migrations.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/class_rebuilding.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/class_renaming.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/import_auto_adding.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/module_migrations.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/exceptions.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/main.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/middleware.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/py.typed +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/routing.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/static/__init__.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/static/docs.html +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/__init__.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/common.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/data.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/endpoints.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/enums.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/modules.py +0 -0
- {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/schemas.py +0 -0
|
@@ -7,7 +7,7 @@ from .route_generation import (
|
|
|
7
7
|
VersionedAPIRouter,
|
|
8
8
|
generate_versioned_routers,
|
|
9
9
|
)
|
|
10
|
-
from .structure import VersionBundle
|
|
10
|
+
from .structure import HeadVersion, Version, VersionBundle
|
|
11
11
|
|
|
12
12
|
__version__ = importlib.metadata.version("cadwyn")
|
|
13
13
|
__all__ = [
|
|
@@ -15,6 +15,8 @@ __all__ = [
|
|
|
15
15
|
"VersionedAPIRouter",
|
|
16
16
|
"generate_code_for_versioned_packages",
|
|
17
17
|
"VersionBundle",
|
|
18
|
+
"HeadVersion",
|
|
19
|
+
"Version",
|
|
18
20
|
"generate_versioned_routers",
|
|
19
21
|
"InternalRepresentationOf",
|
|
20
22
|
]
|
|
@@ -12,6 +12,7 @@ from enum import Enum
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from types import GenericAlias, MappingProxyType, ModuleType
|
|
14
14
|
from typing import (
|
|
15
|
+
TYPE_CHECKING,
|
|
15
16
|
Annotated,
|
|
16
17
|
Any,
|
|
17
18
|
Generic,
|
|
@@ -31,20 +32,11 @@ import fastapi.utils
|
|
|
31
32
|
from fastapi import APIRouter
|
|
32
33
|
from fastapi._compat import ModelField as FastAPIModelField
|
|
33
34
|
from fastapi._compat import create_body_model
|
|
34
|
-
from fastapi.dependencies.models import Dependant
|
|
35
|
-
from fastapi.dependencies.utils import (
|
|
36
|
-
get_body_field,
|
|
37
|
-
get_dependant,
|
|
38
|
-
get_parameterless_sub_dependant,
|
|
39
|
-
)
|
|
40
35
|
from fastapi.params import Depends
|
|
41
36
|
from fastapi.routing import APIRoute
|
|
42
37
|
from issubclass import issubclass as lenient_issubclass
|
|
43
38
|
from pydantic import BaseModel
|
|
44
|
-
from starlette.routing import
|
|
45
|
-
BaseRoute,
|
|
46
|
-
request_response,
|
|
47
|
-
)
|
|
39
|
+
from starlette.routing import BaseRoute
|
|
48
40
|
from typing_extensions import Self, assert_never, deprecated
|
|
49
41
|
|
|
50
42
|
from cadwyn._compat import get_annotation_from_model_field, model_fields, rebuild_fastapi_body_param
|
|
@@ -68,7 +60,10 @@ from cadwyn.structure.endpoints import (
|
|
|
68
60
|
)
|
|
69
61
|
from cadwyn.structure.versions import _CADWYN_REQUEST_PARAM_NAME, _CADWYN_RESPONSE_PARAM_NAME, VersionChange
|
|
70
62
|
|
|
71
|
-
|
|
63
|
+
if TYPE_CHECKING:
|
|
64
|
+
from fastapi.dependencies.models import Dependant
|
|
65
|
+
|
|
66
|
+
_Call = TypeVar("_Call", bound=Callable[..., Any])
|
|
72
67
|
_R = TypeVar("_R", bound=fastapi.routing.APIRouter)
|
|
73
68
|
# This is a hack we do because we can't guarantee how the user will use the router.
|
|
74
69
|
_DELETED_ROUTE_TAG = "_CADWYN_DELETED_ROUTE"
|
|
@@ -122,7 +117,7 @@ def generate_versioned_routers(
|
|
|
122
117
|
|
|
123
118
|
|
|
124
119
|
class VersionedAPIRouter(fastapi.routing.APIRouter):
|
|
125
|
-
def only_exists_in_older_versions(self, endpoint:
|
|
120
|
+
def only_exists_in_older_versions(self, endpoint: _Call) -> _Call:
|
|
126
121
|
route = _get_route_from_func(self.routes, endpoint)
|
|
127
122
|
if route is None:
|
|
128
123
|
raise LookupError(
|
|
@@ -308,7 +303,7 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
308
303
|
|
|
309
304
|
def _replace_internal_representation_with_the_versioned_schema(
|
|
310
305
|
self,
|
|
311
|
-
copy_of_dependant: Dependant,
|
|
306
|
+
copy_of_dependant: "Dependant",
|
|
312
307
|
schema_to_internal_request_body_representation: dict[type[BaseModel], type[BaseModel]],
|
|
313
308
|
):
|
|
314
309
|
body_param: FastAPIModelField = copy_of_dependant.body_params[0]
|
|
@@ -467,9 +462,10 @@ def _extract_internal_request_schemas_from_router(
|
|
|
467
462
|
|
|
468
463
|
for route in router.routes:
|
|
469
464
|
if isinstance(route, APIRoute): # pragma: no branch
|
|
470
|
-
route.endpoint =
|
|
465
|
+
route.endpoint = _modify_callable_annotations(
|
|
471
466
|
route.endpoint,
|
|
472
467
|
modify_annotations=_extract_internal_request_schemas_from_annotations,
|
|
468
|
+
annotation_modifying_wrapper_factory=_copy_endpoint,
|
|
473
469
|
)
|
|
474
470
|
_remake_endpoint_dependencies(route)
|
|
475
471
|
return schema_to_internal_request_body_representation
|
|
@@ -576,7 +572,12 @@ class _AnnotationTransformer:
|
|
|
576
572
|
def modifier(annotation: Any):
|
|
577
573
|
return self._change_version_of_annotations(annotation, version_dir)
|
|
578
574
|
|
|
579
|
-
return
|
|
575
|
+
return _modify_callable_annotations(
|
|
576
|
+
annotation,
|
|
577
|
+
modifier,
|
|
578
|
+
modifier,
|
|
579
|
+
annotation_modifying_wrapper_factory=_copy_function_through_class_based_wrapper,
|
|
580
|
+
)
|
|
580
581
|
else:
|
|
581
582
|
return annotation
|
|
582
583
|
|
|
@@ -637,12 +638,14 @@ class _AnnotationTransformer:
|
|
|
637
638
|
)
|
|
638
639
|
|
|
639
640
|
|
|
640
|
-
def
|
|
641
|
-
call:
|
|
641
|
+
def _modify_callable_annotations(
|
|
642
|
+
call: _Call,
|
|
642
643
|
modify_annotations: Callable[[dict[str, Any]], dict[str, Any]] = lambda a: a,
|
|
643
644
|
modify_defaults: Callable[[tuple[Any, ...]], tuple[Any, ...]] = lambda a: a,
|
|
644
|
-
|
|
645
|
-
|
|
645
|
+
*,
|
|
646
|
+
annotation_modifying_wrapper_factory: Callable[[_Call], _Call],
|
|
647
|
+
) -> _Call:
|
|
648
|
+
annotation_modifying_wrapper = annotation_modifying_wrapper_factory(call)
|
|
646
649
|
old_params = inspect.signature(call).parameters
|
|
647
650
|
callable_annotations = annotation_modifying_wrapper.__annotations__
|
|
648
651
|
annotation_modifying_wrapper.__annotations__ = modify_annotations(callable_annotations)
|
|
@@ -658,15 +661,12 @@ def _modify_callable(
|
|
|
658
661
|
|
|
659
662
|
|
|
660
663
|
def _remake_endpoint_dependencies(route: fastapi.routing.APIRoute):
|
|
661
|
-
|
|
664
|
+
# Unlike get_dependant, APIRoute is the public API of FastAPI and it's (almost) guaranteed to be stable.
|
|
665
|
+
|
|
666
|
+
route_copy = fastapi.routing.APIRoute(route.path, route.endpoint, dependencies=route.dependencies)
|
|
667
|
+
route.dependant = route_copy.dependant
|
|
668
|
+
route.body_field = route_copy.body_field
|
|
662
669
|
_add_request_and_response_params(route)
|
|
663
|
-
route.body_field = get_body_field(dependant=route.dependant, name=route.unique_id)
|
|
664
|
-
for depends in route.dependencies[::-1]:
|
|
665
|
-
route.dependant.dependencies.insert(
|
|
666
|
-
0,
|
|
667
|
-
get_parameterless_sub_dependant(depends=depends, path=route.path_format),
|
|
668
|
-
)
|
|
669
|
-
route.app = request_response(route.get_route_handler())
|
|
670
670
|
|
|
671
671
|
|
|
672
672
|
def _add_request_and_response_params(route: APIRoute):
|
|
@@ -681,7 +681,7 @@ def _add_data_migrations_to_route(
|
|
|
681
681
|
head_route: Any,
|
|
682
682
|
template_body_field: type[BaseModel] | None,
|
|
683
683
|
template_body_field_name: str | None,
|
|
684
|
-
dependant_for_request_migrations: Dependant,
|
|
684
|
+
dependant_for_request_migrations: "Dependant",
|
|
685
685
|
versions: VersionBundle,
|
|
686
686
|
):
|
|
687
687
|
if not (route.dependant.request_param_name and route.dependant.response_param_name): # pragma: no cover
|
|
@@ -802,37 +802,78 @@ def _get_route_from_func(
|
|
|
802
802
|
return None
|
|
803
803
|
|
|
804
804
|
|
|
805
|
-
def
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
805
|
+
def _copy_endpoint(function: Any) -> Any:
|
|
806
|
+
function = _unwrap_callable(function)
|
|
807
|
+
function_copy: Any = types.FunctionType(
|
|
808
|
+
function.__code__,
|
|
809
|
+
function.__globals__,
|
|
810
|
+
name=function.__name__,
|
|
811
|
+
argdefs=function.__defaults__,
|
|
812
|
+
closure=function.__closure__,
|
|
813
|
+
)
|
|
814
|
+
function_copy = functools.update_wrapper(function_copy, function)
|
|
815
|
+
# Otherwise it will have the same signature as __wrapped__ due to how inspect module works
|
|
816
|
+
del function_copy.__wrapped__
|
|
817
|
+
|
|
818
|
+
function_copy._original_callable = function
|
|
819
|
+
function.__kwdefaults__ = function.__kwdefaults__.copy() if function.__kwdefaults__ is not None else {}
|
|
820
|
+
|
|
821
|
+
return function_copy
|
|
822
|
+
|
|
812
823
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
return await function(*args, **kwargs)
|
|
824
|
+
class _CallableWrapper:
|
|
825
|
+
"""__eq__ and __hash__ are needed to make sure that dependency overrides work correctly.
|
|
826
|
+
They are based on putting dependencies (functions) as keys for the dictionary so if we want to be able to
|
|
827
|
+
override the wrapper, we need to make sure that it is equivalent to the original in __hash__ and __eq__
|
|
828
|
+
"""
|
|
819
829
|
|
|
830
|
+
def __init__(self, original_callable: Callable) -> None:
|
|
831
|
+
super().__init__()
|
|
832
|
+
self._original_callable = original_callable
|
|
833
|
+
functools.update_wrapper(self, original_callable)
|
|
834
|
+
|
|
835
|
+
@property
|
|
836
|
+
def __globals__(self):
|
|
837
|
+
"""FastAPI uses __globals__ to resolve forward references in type hints
|
|
838
|
+
It's supposed to be an attribute on the function but we use it as property to prevent python
|
|
839
|
+
from trying to pickle globals when we deepcopy this wrapper
|
|
840
|
+
"""
|
|
841
|
+
#
|
|
842
|
+
return self._original_callable.__globals__
|
|
843
|
+
|
|
844
|
+
def __call__(self, *args: Any, **kwargs: Any):
|
|
845
|
+
return self._original_callable(*args, **kwargs)
|
|
846
|
+
|
|
847
|
+
def __hash__(self):
|
|
848
|
+
return hash(self._original_callable)
|
|
849
|
+
|
|
850
|
+
def __eq__(self, value: object) -> bool:
|
|
851
|
+
return self._original_callable == value # pyright: ignore[reportUnnecessaryComparison]
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
class _AsyncCallableWrapper(_CallableWrapper):
|
|
855
|
+
async def __call__(self, *args: Any, **kwargs: Any):
|
|
856
|
+
return await self._original_callable(*args, **kwargs)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _copy_function_through_class_based_wrapper(call: Any):
|
|
860
|
+
"""Separate from copy_endpoint because endpoints MUST be functions in FastAPI, they cannot be cls instances"""
|
|
861
|
+
call = _unwrap_callable(call)
|
|
862
|
+
|
|
863
|
+
if inspect.iscoroutinefunction(call):
|
|
864
|
+
return _AsyncCallableWrapper(call)
|
|
820
865
|
else:
|
|
866
|
+
return _CallableWrapper(call)
|
|
821
867
|
|
|
822
|
-
@functools.wraps(function)
|
|
823
|
-
def annotation_modifying_wrapper(
|
|
824
|
-
*args: Any,
|
|
825
|
-
**kwargs: Any,
|
|
826
|
-
) -> Any:
|
|
827
|
-
return function(*args, **kwargs)
|
|
828
868
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
)
|
|
833
|
-
|
|
869
|
+
def _unwrap_callable(call: Any) -> Any:
|
|
870
|
+
while hasattr(call, "_original_callable"):
|
|
871
|
+
call = call._original_callable
|
|
872
|
+
if not isinstance(call, types.FunctionType | types.MethodType):
|
|
873
|
+
# This means that the callable is actually an instance of a regular class
|
|
874
|
+
call = call.__call__
|
|
834
875
|
|
|
835
|
-
return
|
|
876
|
+
return call
|
|
836
877
|
|
|
837
878
|
|
|
838
879
|
def _route_has_a_simple_body_schema(route: APIRoute) -> bool:
|
|
@@ -285,6 +285,8 @@ class VersionBundle:
|
|
|
285
285
|
raise CadwynStructureError(
|
|
286
286
|
"Versions are not sorted correctly. Please sort them in descending order.",
|
|
287
287
|
)
|
|
288
|
+
if not self.versions:
|
|
289
|
+
raise CadwynStructureError("You must define at least one non-head version in a VersionBundle.")
|
|
288
290
|
if self.versions[-1].version_changes:
|
|
289
291
|
raise CadwynStructureError(
|
|
290
292
|
f'The first version "{self.versions[-1].value}" cannot have any version changes. '
|
|
@@ -325,7 +327,7 @@ class VersionBundle:
|
|
|
325
327
|
raise CadwynStructureError(
|
|
326
328
|
f'The head schemas package must be a package. "{head_schemas_package.__name__}" is not a package.',
|
|
327
329
|
)
|
|
328
|
-
elif head_schemas_package.__name__.endswith(".head"):
|
|
330
|
+
elif head_schemas_package.__name__.endswith(".head") or head_schemas_package.__name__ == "head":
|
|
329
331
|
return "head"
|
|
330
332
|
elif head_schemas_package.__name__.endswith(".latest"):
|
|
331
333
|
warnings.warn(
|
|
@@ -463,7 +465,6 @@ class VersionBundle:
|
|
|
463
465
|
request.scope["headers"] = tuple((key.encode(), value.encode()) for key, value in request_info.headers.items())
|
|
464
466
|
del request._headers
|
|
465
467
|
# Remember this: if len(body_params) == 1, then route.body_schema == route.dependant.body_params[0]
|
|
466
|
-
|
|
467
468
|
dependencies, errors, _, _, _ = await solve_dependencies(
|
|
468
469
|
request=request,
|
|
469
470
|
response=response,
|
|
@@ -803,7 +804,7 @@ async def _get_body(
|
|
|
803
804
|
) from e
|
|
804
805
|
except HTTPException:
|
|
805
806
|
raise
|
|
806
|
-
except Exception as e:
|
|
807
|
+
except Exception as e:
|
|
807
808
|
raise HTTPException(status_code=400, detail="There was an error parsing the body") from e
|
|
808
809
|
return body
|
|
809
810
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "cadwyn"
|
|
3
|
-
version = "3.15.
|
|
3
|
+
version = "3.15.7"
|
|
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"
|
|
@@ -68,7 +68,6 @@ cli = ["typer"]
|
|
|
68
68
|
pytest = ">=7.2.1"
|
|
69
69
|
pytest-cov = ">=4.0.0"
|
|
70
70
|
uvicorn = "*"
|
|
71
|
-
devtools = "*"
|
|
72
71
|
pdbpp = "^0.10.3"
|
|
73
72
|
httpx = "*"
|
|
74
73
|
pytest-fixture-classes = ">=1.0.3"
|
|
@@ -78,6 +77,8 @@ mkdocs-material = ">=9.3.1"
|
|
|
78
77
|
python-multipart = ">=0.0.6"
|
|
79
78
|
mkdocs-simple-hooks = ">=0.1.5"
|
|
80
79
|
pytest-sugar = "^1.0.0"
|
|
80
|
+
better-devtools = "^0.13.3"
|
|
81
|
+
svcs = "^24.1.0"
|
|
81
82
|
|
|
82
83
|
[tool.poetry.scripts]
|
|
83
84
|
cadwyn = "cadwyn.__main__:app"
|
|
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
|
|
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
|