cadwyn 5.1.1__tar.gz → 5.1.3__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 (155) hide show
  1. {cadwyn-5.1.1 → cadwyn-5.1.3}/CHANGELOG.md +19 -0
  2. {cadwyn-5.1.1 → cadwyn-5.1.3}/PKG-INFO +2 -2
  3. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/__init__.py +2 -0
  4. cadwyn-5.1.3/cadwyn/dependencies.py +5 -0
  5. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/exceptions.py +1 -2
  6. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/schema_generation.py +33 -1
  7. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/structure/versions.py +9 -1
  8. {cadwyn-5.1.1 → cadwyn-5.1.3}/pyproject.toml +7 -3
  9. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_router_generation.py +108 -4
  10. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_schema_generation/test_enum.py +7 -1
  11. {cadwyn-5.1.1 → cadwyn-5.1.3}/uv.lock +185 -168
  12. {cadwyn-5.1.1 → cadwyn-5.1.3}/.github/CODE_OF_CONDUCT.md +0 -0
  13. {cadwyn-5.1.1 → cadwyn-5.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  14. {cadwyn-5.1.1 → cadwyn-5.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  15. {cadwyn-5.1.1 → cadwyn-5.1.3}/.github/actions/setup-python-uv/action.yaml +0 -0
  16. {cadwyn-5.1.1 → cadwyn-5.1.3}/.github/workflows/ci.yaml +0 -0
  17. {cadwyn-5.1.1 → cadwyn-5.1.3}/.github/workflows/daily_tests.yaml +0 -0
  18. {cadwyn-5.1.1 → cadwyn-5.1.3}/.github/workflows/publish_docs.yaml +0 -0
  19. {cadwyn-5.1.1 → cadwyn-5.1.3}/.github/workflows/release.yaml +0 -0
  20. {cadwyn-5.1.1 → cadwyn-5.1.3}/.gitignore +0 -0
  21. {cadwyn-5.1.1 → cadwyn-5.1.3}/.pre-commit-config.yaml +0 -0
  22. {cadwyn-5.1.1 → cadwyn-5.1.3}/LICENSE +0 -0
  23. {cadwyn-5.1.1 → cadwyn-5.1.3}/Makefile +0 -0
  24. {cadwyn-5.1.1 → cadwyn-5.1.3}/README.md +0 -0
  25. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/__main__.py +0 -0
  26. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/_asts.py +0 -0
  27. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/_importer.py +0 -0
  28. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/_render.py +0 -0
  29. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/_utils.py +0 -0
  30. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/applications.py +0 -0
  31. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/changelogs.py +0 -0
  32. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/middleware.py +0 -0
  33. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/py.typed +0 -0
  34. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/route_generation.py +0 -0
  35. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/routing.py +0 -0
  36. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/static/__init__.py +0 -0
  37. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/static/docs.html +0 -0
  38. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/structure/__init__.py +0 -0
  39. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/structure/common.py +0 -0
  40. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/structure/data.py +0 -0
  41. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/structure/endpoints.py +0 -0
  42. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/structure/enums.py +0 -0
  43. {cadwyn-5.1.1 → cadwyn-5.1.3}/cadwyn/structure/schemas.py +0 -0
  44. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/CNAME +0 -0
  45. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/__init__.py +0 -0
  46. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/api_version_parameter_and_context_variables.md +0 -0
  47. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/beware_of_data_versioning.md +0 -0
  48. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/changelogs.md +0 -0
  49. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/cli.md +0 -0
  50. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/endpoint_migrations.md +0 -0
  51. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/enum_migrations.md +0 -0
  52. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/index.md +0 -0
  53. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/main_app.md +0 -0
  54. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/methodology.md +0 -0
  55. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/schema_generation.md +0 -0
  56. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/schema_migrations.md +0 -0
  57. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/testing.md +0 -0
  58. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/version_changes.md +0 -0
  59. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/concepts/where_to_put_the_version_and_how_to_format_it.md +0 -0
  60. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/home/CONTRIBUTING.md +0 -0
  61. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/how_to/change_business_logic/index.md +0 -0
  62. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/how_to/change_endpoints/index.md +0 -0
  63. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/how_to/change_openapi_schemas/add_field.md +0 -0
  64. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/how_to/change_openapi_schemas/change_field_type.md +0 -0
  65. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/how_to/change_openapi_schemas/change_schema_without_endpoint.md +0 -0
  66. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/how_to/change_openapi_schemas/changing_constraints.md +0 -0
  67. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/how_to/change_openapi_schemas/remove_field.md +0 -0
  68. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/how_to/change_openapi_schemas/rename_a_field_in_schema.md +0 -0
  69. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/how_to/index.md +0 -0
  70. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/how_to/version_with_paths_and_numbers_instead_of_headers_and_dates.md +0 -0
  71. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/img/dashboard_with_one_version.png +0 -0
  72. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/img/dashboard_with_two_versions.png +0 -0
  73. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/img/get_users_endpoint_from_prior_version.png +0 -0
  74. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/img/simplified_migration_model.png +0 -0
  75. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/img/sponsor_logos/monite.png +0 -0
  76. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/img/unversioned_dashboard.png +0 -0
  77. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/index.md +0 -0
  78. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/plugin.py +0 -0
  79. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/quickstart/setup.md +0 -0
  80. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/quickstart/tutorial.md +0 -0
  81. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/theory/how_to_build_versioning_framework.md +0 -0
  82. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/theory/how_we_got_here.md +0 -0
  83. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs/theory/literature.md +0 -0
  84. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/__init__.py +0 -0
  85. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/__init__.py +0 -0
  86. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/change_openapi_schemas/__init__.py +0 -0
  87. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/__init__.py +0 -0
  88. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block001.py +0 -0
  89. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block002.py +0 -0
  90. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/__init__.py +0 -0
  91. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block001.py +0 -0
  92. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block002.py +0 -0
  93. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/__init__.py +0 -0
  94. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/block001.py +0 -0
  95. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/tests/__init__.py +0 -0
  96. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/tests/test_block_001.py +0 -0
  97. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/__init__.py +0 -0
  98. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/setup/__init__.py +0 -0
  99. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/setup/block001.sh +0 -0
  100. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/setup/block002.py +0 -0
  101. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/setup/tests/__init__.py +0 -0
  102. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/setup/tests/test_block002.py +0 -0
  103. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/tutorial/__init__.py +0 -0
  104. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/tutorial/block001.py +0 -0
  105. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/tutorial/block002.py +0 -0
  106. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/tutorial/block003.py +0 -0
  107. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/tutorial/tests/__init__.py +0 -0
  108. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/tutorial/tests/test_block001.py +0 -0
  109. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/tutorial/tests/test_block002.py +0 -0
  110. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/quickstart/tutorial/tests/test_block003.py +0 -0
  111. {cadwyn-5.1.1 → cadwyn-5.1.3}/docs_src/ruff.toml +0 -0
  112. {cadwyn-5.1.1 → cadwyn-5.1.3}/mkdocs.yml +0 -0
  113. {cadwyn-5.1.1 → cadwyn-5.1.3}/ruff.toml +0 -0
  114. {cadwyn-5.1.1 → cadwyn-5.1.3}/scripts/fix_links.py +0 -0
  115. {cadwyn-5.1.1 → cadwyn-5.1.3}/scripts/split_md.py +0 -0
  116. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/__init__.py +0 -0
  117. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_data/__init__.py +0 -0
  118. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_data/unversioned_schema_dir/__init__.py +0 -0
  119. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_data/unversioned_schema_dir/unversioned_schemas.py +0 -0
  120. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_data/unversioned_schemas.py +0 -0
  121. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/__init__.py +0 -0
  122. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/app_for_testing_routing.py +0 -0
  123. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/render/__init__.py +0 -0
  124. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/render/classes.py +0 -0
  125. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/render/complex/__init__.py +0 -0
  126. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/render/complex/classes.py +0 -0
  127. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/render/complex/versions.py +0 -0
  128. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/render/versions.py +0 -0
  129. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/utils.py +0 -0
  130. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/versioned_app/__init__.py +0 -0
  131. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/versioned_app/app.py +0 -0
  132. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/versioned_app/v2021_01_01.py +0 -0
  133. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/versioned_app/v2022_01_02.py +0 -0
  134. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/_resources/versioned_app/webhooks.py +0 -0
  135. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/conftest.py +0 -0
  136. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_applications.py +0 -0
  137. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_auth_dependencies.py +0 -0
  138. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_changelog.py +0 -0
  139. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_cli.py +0 -0
  140. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_data_migrations.py +0 -0
  141. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_render.py +0 -0
  142. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_router_generation_with_from_future_annotations.py +0 -0
  143. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_routing.py +0 -0
  144. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_schema_generation/__init__.py +0 -0
  145. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_schema_generation/test_schema.py +0 -0
  146. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_schema_generation/test_schema_field.py +0 -0
  147. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_schema_generation/test_schema_validator.py +0 -0
  148. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_schema_generation/test_schema_with_future_annotations.py +0 -0
  149. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/test_structure.py +0 -0
  150. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/tutorial/__init__.py +0 -0
  151. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/tutorial/main.py +0 -0
  152. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/tutorial/test_example.py +0 -0
  153. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/versioning_styles/__init__.py +0 -0
  154. {cadwyn-5.1.1 → cadwyn-5.1.3}/tests/versioning_styles/test_versioning_formats.py +0 -0
  155. {cadwyn-5.1.1 → cadwyn-5.1.3}/tox.ini +0 -0
@@ -5,6 +5,25 @@ Please follow [the Keep a Changelog standard](https://keepachangelog.com/en/1.0.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [5.1.3]
9
+
10
+ ### Fixed
11
+
12
+ * `__doc__` attribute is now copied from the original enum to the generated enum
13
+ * Python 3.12 type aliases and their typing_extensions backports are now supported (including `pydantic.JsonValue` and other `typing_extensions.TypeAliasType` instances)
14
+ * The bug when solve_dependencies error on the migration of a request to the latest version responds with a non-json serializable error and cadwyn showed a failed to serialize error instead of the actual error
15
+ * Updated minimum fastapi version to 0.112.4 because embed_body_fields was added in 0.112.4
16
+
17
+ ## [5.1.2]
18
+
19
+ ### Fixed
20
+
21
+ * Generators not being called when fastapi validates the initial request
22
+
23
+ ### Added
24
+
25
+ * `cadwyn.current_dependency_solver` function to check whether cadwyn or fastapi is currently solving dependencies. It can be used as a dependency for versioned endpoints like so: `current_dependency_solver: Annotated[Literal["fastapi", "cadwyn"], Depends(current_dependency_solver)]`. If your dependency has side effects, you would likely want to only run it once per request. If you want to run it once we migrated the request to the latest version, you should only run it when `current_dependency_solver` is `"cadwyn"`. If you want your dependency to run at the very beginning of handling the request, you should only run it when `current_dependency_solver` is `"fastapi"`.
26
+
8
27
  ## [5.1.1]
9
28
 
10
29
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cadwyn
3
- Version: 5.1.1
3
+ Version: 5.1.3
4
4
  Summary: Production-ready community-driven modern Stripe-like API versioning in FastAPI
5
5
  Project-URL: Source code, https://github.com/zmievsa/cadwyn
6
6
  Project-URL: Documentation, https://docs.cadwyn.dev
@@ -35,7 +35,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
35
35
  Classifier: Typing :: Typed
36
36
  Requires-Python: >=3.9
37
37
  Requires-Dist: backports-strenum<2,>=1.3.1; python_version < '3.11'
38
- Requires-Dist: fastapi>=0.112.3
38
+ Requires-Dist: fastapi>=0.112.4
39
39
  Requires-Dist: issubclass>=0.1.2
40
40
  Requires-Dist: jinja2>=3.1.2
41
41
  Requires-Dist: pydantic>=2.0.0
@@ -2,6 +2,7 @@ import importlib.metadata
2
2
 
3
3
  from .applications import Cadwyn
4
4
  from .changelogs import hidden
5
+ from .dependencies import current_dependency_solver
5
6
  from .route_generation import VersionedAPIRouter, generate_versioned_routers
6
7
  from .schema_generation import generate_versioned_models, migrate_response_body
7
8
  from .structure import (
@@ -32,6 +33,7 @@ __all__ = [
32
33
  "VersionedAPIRouter",
33
34
  "convert_request_to_next_version_for",
34
35
  "convert_response_to_previous_version_for",
36
+ "current_dependency_solver",
35
37
  "endpoint",
36
38
  "enum",
37
39
  "generate_versioned_models",
@@ -0,0 +1,5 @@
1
+ from cadwyn.structure.versions import _CURRENT_DEPENDENCY_SOLVER_OPTIONS, _CURRENT_DEPENDENCY_SOLVER_VAR
2
+
3
+
4
+ async def current_dependency_solver() -> _CURRENT_DEPENDENCY_SOLVER_OPTIONS:
5
+ return _CURRENT_DEPENDENCY_SOLVER_VAR.get("fastapi")
@@ -1,4 +1,3 @@
1
- import json
2
1
  from typing import Any
3
2
 
4
3
  from fastapi.routing import APIRoute
@@ -21,7 +20,7 @@ class CadwynHeadRequestValidationError(CadwynError):
21
20
  f"We failed to migrate the request with version={self.version}. "
22
21
  "This means that there is some error in your migrations or schema structure that makes it impossible "
23
22
  "to migrate the request of that version to latest.\n"
24
- f"body={self.body}\n\nerrors={json.dumps(self.errors, indent=4, ensure_ascii=False)}"
23
+ f"body={self.body}\n\nerrors={self.errors}"
25
24
  )
26
25
 
27
26
 
@@ -20,6 +20,7 @@ import pydantic
20
20
  import pydantic._internal._decorators
21
21
  import typing_extensions
22
22
  from fastapi import Response
23
+ from fastapi.dependencies.utils import is_async_gen_callable, is_coroutine_callable, is_gen_callable
23
24
  from fastapi.routing import APIRoute
24
25
  from pydantic import BaseModel, Field, RootModel
25
26
  from pydantic._internal import _decorators
@@ -39,6 +40,7 @@ from typing_extensions import (
39
40
  NewType,
40
41
  Self,
41
42
  TypeAlias,
43
+ TypeAliasType,
42
44
  TypeVar,
43
45
  _AnnotatedAlias,
44
46
  assert_never,
@@ -77,6 +79,7 @@ from cadwyn.structure.versions import _CADWYN_REQUEST_PARAM_NAME, _CADWYN_RESPON
77
79
  if TYPE_CHECKING:
78
80
  from cadwyn.structure.versions import HeadVersion, Version, VersionBundle
79
81
 
82
+
80
83
  if sys.version_info >= (3, 10):
81
84
  from typing import _BaseGenericAlias # pyright: ignore[reportAttributeAccessIssue]
82
85
  else:
@@ -98,6 +101,7 @@ PYDANTIC_DECORATOR_TYPE_TO_DECORATOR_MAP = {
98
101
  ModelSerializerDecoratorInfo: pydantic.model_serializer,
99
102
  ComputedFieldInfo: pydantic.computed_field,
100
103
  }
104
+ _PYDANTIC_ALL_EXPORTED_NAMES = set(pydantic.__all__)
101
105
 
102
106
 
103
107
  VALIDATOR_CONFIG_KEY = "__validators__"
@@ -486,6 +490,7 @@ class _CallableWrapper:
486
490
  self._original_callable = original_callable
487
491
  if not is_regular_function(original_callable):
488
492
  original_callable = original_callable.__call__
493
+
489
494
  functools.update_wrapper(self, original_callable)
490
495
 
491
496
  @property
@@ -510,6 +515,17 @@ class _AsyncCallableWrapper(_CallableWrapper):
510
515
  return await self._original_callable(*args, **kwargs)
511
516
 
512
517
 
518
+ class _GeneratorCallableWrapper(_CallableWrapper):
519
+ def __call__(self, *args: Any, **kwargs: Any):
520
+ yield from self._original_callable(*args, **kwargs)
521
+
522
+
523
+ class _AsyncGeneratorCallableWrapper(_CallableWrapper):
524
+ async def __call__(self, *args: Any, **kwargs: Any):
525
+ async for value in self._original_callable(*args, **kwargs):
526
+ yield value
527
+
528
+
513
529
  @final
514
530
  class _AnnotationTransformer:
515
531
  def __init__(self, generator: "SchemaGenerator") -> None:
@@ -566,6 +582,17 @@ class _AnnotationTransformer:
566
582
  def _change_version_of_a_non_container_annotation(self, annotation: Any) -> Any:
567
583
  if isinstance(annotation, (_BaseGenericAlias, types.GenericAlias)):
568
584
  return get_origin(annotation)[tuple(self.change_version_of_annotation(arg) for arg in get_args(annotation))]
585
+ elif isinstance(annotation, TypeAliasType):
586
+ if (
587
+ annotation.__module__ is not None and (annotation.__module__.startswith("pydantic."))
588
+ ) or annotation.__name__ in _PYDANTIC_ALL_EXPORTED_NAMES:
589
+ return annotation
590
+ else:
591
+ return TypeAliasType( # pyright: ignore[reportGeneralTypeIssues]
592
+ name=annotation.__name__,
593
+ value=self.change_version_of_annotation(annotation.__value__),
594
+ type_params=self.change_version_of_annotation(annotation.__type_params__),
595
+ )
569
596
  elif isinstance(annotation, fastapi.params.Security):
570
597
  return fastapi.params.Security(
571
598
  self.change_version_of_annotation(annotation.dependency),
@@ -690,8 +717,12 @@ class _AnnotationTransformer:
690
717
  actual_call = call.__call__
691
718
  else:
692
719
  actual_call = call
693
- if inspect.iscoroutinefunction(actual_call):
720
+ if is_async_gen_callable(actual_call):
721
+ return _AsyncGeneratorCallableWrapper(call)
722
+ elif is_coroutine_callable(actual_call):
694
723
  return _AsyncCallableWrapper(call)
724
+ elif is_gen_callable(actual_call):
725
+ return _GeneratorCallableWrapper(call)
695
726
  else:
696
727
  return _CallableWrapper(call)
697
728
 
@@ -1061,6 +1092,7 @@ class _EnumWrapper(Generic[_T_ENUM]):
1061
1092
  initialization_namespace = self._get_initialization_namespace_for_enum(self.cls) | raw_member_map
1062
1093
  for attr_name, attr in initialization_namespace.items():
1063
1094
  enum_dict[attr_name] = attr
1095
+ enum_dict["__doc__"] = self.cls.__doc__
1064
1096
  model_copy = cast(type[_T_ENUM], type(self.name, self.cls.__bases__, enum_dict))
1065
1097
  model_copy.__cadwyn_original_model__ = self.cls # pyright: ignore[reportAttributeAccessIssue]
1066
1098
  return model_copy
@@ -23,7 +23,7 @@ from fastapi.routing import APIRoute, _prepare_response_content
23
23
  from pydantic import BaseModel
24
24
  from pydantic_core import PydanticUndefined
25
25
  from starlette._utils import is_async_callable
26
- from typing_extensions import Any, ParamSpec, TypeAlias, TypeVar, assert_never, deprecated, get_args
26
+ from typing_extensions import Any, Literal, ParamSpec, TypeAlias, TypeVar, assert_never, deprecated, get_args
27
27
 
28
28
  from cadwyn._utils import classproperty
29
29
  from cadwyn.exceptions import (
@@ -51,6 +51,11 @@ _CADWYN_REQUEST_PARAM_NAME = "cadwyn_request_param"
51
51
  _CADWYN_RESPONSE_PARAM_NAME = "cadwyn_response_param"
52
52
  _P = ParamSpec("_P")
53
53
  _R = TypeVar("_R")
54
+ _CURRENT_DEPENDENCY_SOLVER_OPTIONS = Literal["cadwyn", "fastapi"]
55
+ _CURRENT_DEPENDENCY_SOLVER_VAR: ContextVar[_CURRENT_DEPENDENCY_SOLVER_OPTIONS] = ContextVar(
56
+ "cadwyn_dependencies_dry_run"
57
+ )
58
+
54
59
  PossibleInstructions: TypeAlias = Union[
55
60
  AlterSchemaSubInstruction, AlterEndpointSubInstruction, AlterEnumSubInstruction, SchemaHadInstruction, staticmethod
56
61
  ]
@@ -276,6 +281,7 @@ class VersionBundle:
276
281
 
277
282
  if api_version_var is None:
278
283
  api_version_var = ContextVar("cadwyn_api_version")
284
+
279
285
  self.version_values = tuple(version.value for version in self.versions)
280
286
  self.reversed_version_values = tuple(reversed(self.version_values))
281
287
  self.api_version_var = api_version_var
@@ -379,6 +385,8 @@ class VersionBundle:
379
385
  instruction(request_info)
380
386
  request.scope["headers"] = tuple((key.encode(), value.encode()) for key, value in request_info.headers.items())
381
387
  del request._headers
388
+ # This gives us the ability to tell the user whether cadwyn is running its dependencies or FastAPI
389
+ _CURRENT_DEPENDENCY_SOLVER_VAR.set("cadwyn")
382
390
  # Remember this: if len(body_params) == 1, then route.body_schema == route.dependant.body_params[0]
383
391
  result = await solve_dependencies(
384
392
  request=request,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cadwyn"
3
- version = "5.1.1"
3
+ version = "5.1.3"
4
4
  description = "Production-ready community-driven modern Stripe-like API versioning in FastAPI"
5
5
  authors = [{ name = "Stanislav Zmiev", email = "zmievsa@gmail.com" }]
6
6
  license = "MIT"
@@ -51,7 +51,7 @@ classifiers = [
51
51
  "Topic :: Internet :: WWW/HTTP",
52
52
  ]
53
53
  dependencies = [
54
- "fastapi >=0.112.3",
54
+ "fastapi >=0.112.4",
55
55
  "starlette >=0.30.0",
56
56
  "pydantic >=2.0.0",
57
57
  "jinja2 >=3.1.2",
@@ -73,7 +73,8 @@ dev-dependencies = [
73
73
  "svcs ~=24.1.0",
74
74
  "httpx >=0.26.0",
75
75
  "pytest-fixture-classes >=1.0.3",
76
- "pytest >=7.2.1",
76
+ # pytest==8.3.5 has a bug that fails our windows python 3.10 tests
77
+ "pytest==8.3.4",
77
78
  "pytest-cov >=4.0.0",
78
79
  "dirty-equals >=0.6.0",
79
80
  "uvicorn ~=0.23.0",
@@ -87,6 +88,7 @@ dev-dependencies = [
87
88
  "mike >=2.1.2, <3",
88
89
  "pdbpp>=0.10.3",
89
90
  "markdown-include-variants>=0.0.4",
91
+ "inline-snapshot>=0.20.7",
90
92
  ]
91
93
 
92
94
  [project.urls]
@@ -97,6 +99,8 @@ Documentation = "https://docs.cadwyn.dev"
97
99
  [project.scripts]
98
100
  cadwyn = "cadwyn.__main__:app"
99
101
 
102
+ [tool.inline-snapshot]
103
+ format-command = "ruff format --stdin-filename {filename}"
100
104
 
101
105
  [tool.coverage.run]
102
106
  parallel = true
@@ -13,12 +13,14 @@ from fastapi.routing import APIRoute
13
13
  from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
14
14
  from fastapi.security.http import HTTPBasic
15
15
  from fastapi.testclient import TestClient
16
- from pydantic import BaseModel
16
+ from inline_snapshot import snapshot
17
+ from pydantic import BaseModel, JsonValue
17
18
  from pytest_fixture_classes import fixture_class
18
19
  from starlette.responses import FileResponse
19
- from typing_extensions import Any, NewType, TypeAlias, get_args
20
+ from typing_extensions import Any, NewType, TypeAlias, TypeAliasType, get_args
20
21
 
21
22
  from cadwyn import VersionBundle, VersionedAPIRouter
23
+ from cadwyn.dependencies import current_dependency_solver
22
24
  from cadwyn.exceptions import CadwynError, RouterGenerationError, RouterPathParamsModifiedError
23
25
  from cadwyn.route_generation import generate_versioned_routers
24
26
  from cadwyn.schema_generation import generate_versioned_models
@@ -798,7 +800,7 @@ def test__router_generation__using_unversioned_models(
798
800
  assert routes_2001[3].dependant.body_params[0].type_ is schemas["2001-01-01"][UnversionedSchema3]
799
801
 
800
802
 
801
- def test__router_generation__using_weird_typehints(
803
+ def test__router_generation__using_newtype_and_union_typehints(
802
804
  router: VersionedAPIRouter,
803
805
  create_versioned_api_routes: CreateVersionedAPIRoutes,
804
806
  ):
@@ -820,7 +822,7 @@ def test__router_generation__using_weird_typehints(
820
822
  assert getattr(routes_2001[1].dependant.body_params[1], TYPE_ATTR) == Union[str, int]
821
823
 
822
824
 
823
- def test__router_generation__using_pydantic_typehints__internal_pydantic_typehints_should_work(
825
+ def test__router_generation__using_uploadfile_typehint(
824
826
  router: VersionedAPIRouter,
825
827
  create_versioned_api_routes: CreateVersionedAPIRoutes,
826
828
  ):
@@ -835,6 +837,54 @@ def test__router_generation__using_pydantic_typehints__internal_pydantic_typehin
835
837
  # We are intentionally not checking anything here. Our goal is to validate that there is no exception
836
838
 
837
839
 
840
+ def test__router_generation__using_jsonvalue_typehint(
841
+ router: VersionedAPIRouter,
842
+ create_versioned_clients: CreateVersionedClients,
843
+ ):
844
+ @router.post("/test")
845
+ async def test(param1: JsonValue = Body()):
846
+ return param1
847
+
848
+ clients = create_versioned_clients(
849
+ version_change(endpoint("/test", ["POST"]).had(response_model=dict[str, str])),
850
+ )
851
+ assert clients["2000-01-01"].post("/test", json={"foo": "bar"}).json() == {"foo": "bar"}
852
+ assert clients["2001-01-01"].post("/test", json={"foo": "bar"}).json() == {"foo": "bar"}
853
+
854
+
855
+ def test__router_generation__using_typealias_type_typehint(
856
+ router: VersionedAPIRouter,
857
+ create_versioned_clients: CreateVersionedClients,
858
+ ):
859
+ class MySchema(BaseModel):
860
+ foo: str
861
+
862
+ typealias = TypeAliasType("typealias", list[MySchema]) # pyright: ignore[reportGeneralTypeIssues]
863
+
864
+ @router.post("/test")
865
+ async def test(param1: typealias = Body()):
866
+ return param1
867
+
868
+ clients = create_versioned_clients(
869
+ version_change(schema(MySchema).field("foo").had(name="baz")),
870
+ )
871
+ assert clients["2000-01-01"].post("/test", json=[{"foo": "bar"}]).json() == snapshot(
872
+ {
873
+ "detail": [
874
+ {
875
+ "type": "missing",
876
+ "loc": ["body", 0, "baz"],
877
+ "msg": "Field required",
878
+ "input": {
879
+ "foo": "bar",
880
+ },
881
+ }
882
+ ]
883
+ }
884
+ )
885
+ assert clients["2001-01-01"].post("/test", json=[{"foo": "bar"}]).json() == [{"foo": "bar"}]
886
+
887
+
838
888
  def test__router_generation__updating_request_depends(
839
889
  router: VersionedAPIRouter, create_versioned_app: CreateVersionedApp
840
890
  ):
@@ -1205,6 +1255,60 @@ def test__basic_router_generation__subclass_of_security_class_based_dependency_w
1205
1255
  ]
1206
1256
 
1207
1257
 
1258
+ def test__router_generation__with_generator_dependencies(
1259
+ router: VersionedAPIRouter,
1260
+ create_versioned_app: CreateVersionedApp,
1261
+ ):
1262
+ dependency_cache = []
1263
+
1264
+ async def my_async_dependency(current_dependency_runner: Annotated[str, Depends(current_dependency_solver)]):
1265
+ dependency_cache.append(f"{current_dependency_runner} async dependency start")
1266
+ yield "async dependency"
1267
+ dependency_cache.append(f"{current_dependency_runner} async dependency end")
1268
+
1269
+ def my_sync_dependency(current_dependency_runner: Annotated[str, Depends(current_dependency_solver)]):
1270
+ dependency_cache.append(f"{current_dependency_runner} sync dependency start")
1271
+ yield "sync dependency"
1272
+ dependency_cache.append(f"{current_dependency_runner} sync dependency end")
1273
+
1274
+ @router.get("/test")
1275
+ async def test(
1276
+ my_async_dep: Annotated[str, Depends(my_async_dependency)],
1277
+ my_sync_dep: Annotated[str, Depends(my_sync_dependency)],
1278
+ ):
1279
+ assert my_async_dep == "async dependency"
1280
+ assert my_sync_dep == "sync dependency"
1281
+
1282
+ client = TestClient(create_versioned_app(version_change()))
1283
+ assert client.get("/test", headers={"x-api-version": "2000-01-01"}).status_code == 200
1284
+ assert dependency_cache == snapshot(
1285
+ [
1286
+ "fastapi async dependency start",
1287
+ "fastapi sync dependency start",
1288
+ "cadwyn async dependency start",
1289
+ "cadwyn sync dependency start",
1290
+ "cadwyn sync dependency end",
1291
+ "cadwyn async dependency end",
1292
+ "fastapi sync dependency end",
1293
+ "fastapi async dependency end",
1294
+ ]
1295
+ )
1296
+ dependency_cache.clear()
1297
+ assert client.get("/test", headers={"x-api-version": "2001-01-01"}).status_code == 200
1298
+ assert dependency_cache == snapshot(
1299
+ [
1300
+ "fastapi async dependency start",
1301
+ "fastapi sync dependency start",
1302
+ "cadwyn async dependency start",
1303
+ "cadwyn sync dependency start",
1304
+ "cadwyn sync dependency end",
1305
+ "cadwyn async dependency end",
1306
+ "fastapi sync dependency end",
1307
+ "fastapi async dependency end",
1308
+ ]
1309
+ )
1310
+
1311
+
1208
1312
  ######################
1209
1313
  # External lib testing
1210
1314
  ######################
@@ -11,7 +11,7 @@ from tests.conftest import CreateRuntimeSchemas, serialize_enum, version_change
11
11
 
12
12
 
13
13
  class EmptyEnum(Enum):
14
- pass
14
+ """My docstring"""
15
15
 
16
16
 
17
17
  class EnumWithOneMember(Enum):
@@ -34,6 +34,12 @@ class EnumWithOneMemberAndMethods(Enum):
34
34
  return cls.world_member # pyright: ignore[reportAttributeAccessIssue]
35
35
 
36
36
 
37
+ def test__enum_had__should_copy_docstring_too(create_runtime_schemas: CreateRuntimeSchemas):
38
+ models = create_runtime_schemas(version_change(enum(EmptyEnum).had(b=auto())))
39
+ assert models["2000-01-01"][EmptyEnum].__doc__ == "My docstring"
40
+ assert models["2001-01-01"][EmptyEnum].__doc__ == "My docstring"
41
+
42
+
37
43
  def test__enum_had__original_enum_is_empty(create_runtime_schemas: CreateRuntimeSchemas):
38
44
  models = create_runtime_schemas(version_change(enum(EmptyEnum).had(b=auto())))
39
45
  assert serialize_enum(models["2000-01-01"][EmptyEnum]) == {"b": 1}