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.

Files changed (37) hide show
  1. {cadwyn-3.15.5 → cadwyn-3.15.7}/PKG-INFO +1 -1
  2. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/__init__.py +3 -1
  3. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/route_generation.py +94 -53
  4. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/versions.py +4 -3
  5. {cadwyn-3.15.5 → cadwyn-3.15.7}/pyproject.toml +3 -2
  6. {cadwyn-3.15.5 → cadwyn-3.15.7}/LICENSE +0 -0
  7. {cadwyn-3.15.5 → cadwyn-3.15.7}/README.md +0 -0
  8. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/__main__.py +0 -0
  9. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/_asts.py +0 -0
  10. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/_compat.py +0 -0
  11. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/_package_utils.py +0 -0
  12. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/_utils.py +0 -0
  13. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/applications.py +0 -0
  14. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/README.md +0 -0
  15. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/__init__.py +0 -0
  16. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_common.py +0 -0
  17. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_main.py +0 -0
  18. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/__init__.py +0 -0
  19. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/class_migrations.py +0 -0
  20. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/class_rebuilding.py +0 -0
  21. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/class_renaming.py +0 -0
  22. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/import_auto_adding.py +0 -0
  23. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/codegen/_plugins/module_migrations.py +0 -0
  24. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/exceptions.py +0 -0
  25. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/main.py +0 -0
  26. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/middleware.py +0 -0
  27. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/py.typed +0 -0
  28. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/routing.py +0 -0
  29. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/static/__init__.py +0 -0
  30. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/static/docs.html +0 -0
  31. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/__init__.py +0 -0
  32. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/common.py +0 -0
  33. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/data.py +0 -0
  34. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/endpoints.py +0 -0
  35. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/enums.py +0 -0
  36. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/modules.py +0 -0
  37. {cadwyn-3.15.5 → cadwyn-3.15.7}/cadwyn/structure/schemas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cadwyn
3
- Version: 3.15.5
3
+ Version: 3.15.7
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
@@ -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
- _T = TypeVar("_T", bound=Callable[..., Any])
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: _T) -> _T:
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 = _modify_callable(
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 _modify_callable(annotation, modifier, modifier)
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 _modify_callable(
641
- call: Callable,
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
- annotation_modifying_wrapper = _copy_function(call)
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
- route.dependant = get_dependant(path=route.path_format, call=route.endpoint)
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 _copy_function(function: _T) -> _T:
806
- while hasattr(function, "__alt_wrapped__"):
807
- function = function.__alt_wrapped__
808
- if not isinstance(function, types.FunctionType | types.MethodType):
809
- # This means that the callable is actually an instance of a regular class
810
- function = function.__call__
811
- if inspect.iscoroutinefunction(function):
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
- @functools.wraps(function)
814
- async def annotation_modifying_wrapper( # pyright: ignore[reportRedeclaration]
815
- *args: Any,
816
- **kwargs: Any,
817
- ) -> Any:
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
- # Otherwise it will have the same signature as __wrapped__ due to how inspect module works
830
- annotation_modifying_wrapper.__alt_wrapped__ = ( # pyright: ignore[reportAttributeAccessIssue]
831
- annotation_modifying_wrapper.__wrapped__
832
- )
833
- del annotation_modifying_wrapper.__wrapped__
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 cast(_T, annotation_modifying_wrapper)
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: # noqa: BLE001
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.5"
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