cadwyn 5.2.1__tar.gz → 5.3.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.

Files changed (157) hide show
  1. {cadwyn-5.2.1 → cadwyn-5.3.0}/CHANGELOG.md +12 -0
  2. {cadwyn-5.2.1 → cadwyn-5.3.0}/PKG-INFO +1 -1
  3. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/applications.py +7 -1
  4. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/middleware.py +9 -2
  5. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/route_generation.py +59 -25
  6. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/schema_generation.py +1 -2
  7. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/versions.py +16 -17
  8. cadwyn-5.2.1/docs/concepts/where_to_put_the_version_and_how_to_format_it.md → cadwyn-5.3.0/docs/concepts/api_version_parameter.md +19 -1
  9. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/endpoint_migrations.md +5 -3
  10. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/schema_migrations.md +2 -0
  11. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/version_changes.md +2 -0
  12. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/add_field.md +1 -3
  13. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/version_with_paths_and_numbers_instead_of_headers_and_dates.md +1 -1
  14. {cadwyn-5.2.1 → cadwyn-5.3.0}/mkdocs.yml +1 -2
  15. {cadwyn-5.2.1 → cadwyn-5.3.0}/pyproject.toml +1 -1
  16. {cadwyn-5.2.1 → cadwyn-5.3.0}/ruff.toml +1 -1
  17. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_router_generation.py +46 -0
  18. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/tutorial/main.py +7 -2
  19. {cadwyn-5.2.1 → cadwyn-5.3.0}/uv.lock +1 -1
  20. cadwyn-5.2.1/docs/concepts/api_version_parameter_and_context_variables.md +0 -5
  21. {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/CODE_OF_CONDUCT.md +0 -0
  22. {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  23. {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  24. {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/actions/setup-python-uv/action.yaml +0 -0
  25. {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/workflows/ci.yaml +0 -0
  26. {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/workflows/daily_tests.yaml +0 -0
  27. {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/workflows/publish_docs.yaml +0 -0
  28. {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/workflows/release.yaml +0 -0
  29. {cadwyn-5.2.1 → cadwyn-5.3.0}/.gitignore +0 -0
  30. {cadwyn-5.2.1 → cadwyn-5.3.0}/.pre-commit-config.yaml +0 -0
  31. {cadwyn-5.2.1 → cadwyn-5.3.0}/LICENSE +0 -0
  32. {cadwyn-5.2.1 → cadwyn-5.3.0}/Makefile +0 -0
  33. {cadwyn-5.2.1 → cadwyn-5.3.0}/README.md +0 -0
  34. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/__init__.py +0 -0
  35. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/__main__.py +0 -0
  36. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_asts.py +0 -0
  37. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_importer.py +0 -0
  38. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_internal/__init__.py +0 -0
  39. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_internal/context_vars.py +0 -0
  40. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_render.py +0 -0
  41. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_utils.py +0 -0
  42. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/changelogs.py +0 -0
  43. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/dependencies.py +0 -0
  44. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/exceptions.py +0 -0
  45. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/py.typed +0 -0
  46. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/routing.py +0 -0
  47. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/static/__init__.py +0 -0
  48. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/static/docs.html +0 -0
  49. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/__init__.py +0 -0
  50. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/common.py +0 -0
  51. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/data.py +0 -0
  52. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/endpoints.py +0 -0
  53. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/enums.py +0 -0
  54. {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/schemas.py +0 -0
  55. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/CNAME +0 -0
  56. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/__init__.py +0 -0
  57. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/beware_of_data_versioning.md +0 -0
  58. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/changelogs.md +0 -0
  59. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/cli.md +0 -0
  60. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/enum_migrations.md +0 -0
  61. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/index.md +0 -0
  62. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/main_app.md +0 -0
  63. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/methodology.md +0 -0
  64. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/schema_generation.md +0 -0
  65. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/testing.md +0 -0
  66. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/home/CONTRIBUTING.md +0 -0
  67. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_business_logic/index.md +0 -0
  68. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_endpoints/index.md +0 -0
  69. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/change_field_type.md +0 -0
  70. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/change_schema_without_endpoint.md +0 -0
  71. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/changing_constraints.md +0 -0
  72. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/remove_field.md +0 -0
  73. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/rename_a_field_in_schema.md +0 -0
  74. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/index.md +0 -0
  75. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/dashboard_with_one_version.png +0 -0
  76. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/dashboard_with_two_versions.png +0 -0
  77. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/get_users_endpoint_from_prior_version.png +0 -0
  78. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/simplified_migration_model.png +0 -0
  79. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/sponsor_logos/monite.png +0 -0
  80. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/unversioned_dashboard.png +0 -0
  81. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/index.md +0 -0
  82. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/plugin.py +0 -0
  83. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/quickstart/setup.md +0 -0
  84. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/quickstart/tutorial.md +0 -0
  85. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/theory/how_to_build_versioning_framework.md +0 -0
  86. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/theory/how_we_got_here.md +0 -0
  87. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/theory/literature.md +0 -0
  88. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/__init__.py +0 -0
  89. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/__init__.py +0 -0
  90. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/__init__.py +0 -0
  91. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/__init__.py +0 -0
  92. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block001.py +0 -0
  93. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block002.py +0 -0
  94. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/__init__.py +0 -0
  95. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block001.py +0 -0
  96. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block002.py +0 -0
  97. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/__init__.py +0 -0
  98. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/block001.py +0 -0
  99. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/tests/__init__.py +0 -0
  100. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/tests/test_block_001.py +0 -0
  101. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/__init__.py +0 -0
  102. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/setup/__init__.py +0 -0
  103. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/setup/block001.sh +0 -0
  104. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/setup/block002.py +0 -0
  105. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/setup/tests/__init__.py +0 -0
  106. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/setup/tests/test_block002.py +0 -0
  107. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/__init__.py +0 -0
  108. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/block001.py +0 -0
  109. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/block002.py +0 -0
  110. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/block003.py +0 -0
  111. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/tests/__init__.py +0 -0
  112. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/tests/test_block001.py +0 -0
  113. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/tests/test_block002.py +0 -0
  114. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/tests/test_block003.py +0 -0
  115. {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/ruff.toml +0 -0
  116. {cadwyn-5.2.1 → cadwyn-5.3.0}/scripts/fix_links.py +0 -0
  117. {cadwyn-5.2.1 → cadwyn-5.3.0}/scripts/split_md.py +0 -0
  118. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/__init__.py +0 -0
  119. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_data/__init__.py +0 -0
  120. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_data/unversioned_schema_dir/__init__.py +0 -0
  121. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_data/unversioned_schema_dir/unversioned_schemas.py +0 -0
  122. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_data/unversioned_schemas.py +0 -0
  123. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/__init__.py +0 -0
  124. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/app_for_testing_routing.py +0 -0
  125. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/__init__.py +0 -0
  126. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/classes.py +0 -0
  127. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/complex/__init__.py +0 -0
  128. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/complex/classes.py +0 -0
  129. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/complex/versions.py +0 -0
  130. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/versions.py +0 -0
  131. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/utils.py +0 -0
  132. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/versioned_app/__init__.py +0 -0
  133. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/versioned_app/app.py +0 -0
  134. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/versioned_app/v2021_01_01.py +0 -0
  135. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/versioned_app/v2022_01_02.py +0 -0
  136. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/versioned_app/webhooks.py +0 -0
  137. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/conftest.py +0 -0
  138. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_applications.py +0 -0
  139. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_auth_dependencies.py +0 -0
  140. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_changelog.py +0 -0
  141. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_cli.py +0 -0
  142. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_data_migrations.py +0 -0
  143. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_render.py +0 -0
  144. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_router_generation_with_from_future_annotations.py +0 -0
  145. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_routing.py +0 -0
  146. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/__init__.py +0 -0
  147. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_enum.py +0 -0
  148. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_schema.py +0 -0
  149. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_schema_field.py +0 -0
  150. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_schema_validator.py +0 -0
  151. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_schema_with_future_annotations.py +0 -0
  152. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_structure.py +0 -0
  153. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/tutorial/__init__.py +0 -0
  154. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/tutorial/test_example.py +0 -0
  155. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/versioning_styles/__init__.py +0 -0
  156. {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/versioning_styles/test_versioning_formats.py +0 -0
  157. {cadwyn-5.2.1 → cadwyn-5.3.0}/tox.ini +0 -0
@@ -5,6 +5,18 @@ Please follow [the Keep a Changelog standard](https://keepachangelog.com/en/1.0.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [5.3.0]
9
+
10
+ ### Added
11
+
12
+ * `api_version_title` and `api_version_description` arguments to `Cadwyn` to allow customizing the API version parameter in the OpenAPI schema
13
+
14
+ ## [5.2.2]
15
+
16
+ ### Fixed
17
+
18
+ * Whenever a route was migrated by path, we used the path and methods of the request. So if the request wanted "/v1/webhook_settings" and we renamed them to "/v1/webhook_subscriptions" -- all migrations for "/v1/webhook_subscriptions" would not get applied to any requests that wanted "/v1/webhook_settings". This effectively means that previously route renamings were incompatible with by path converters. Now we use the head route id instead of the path and methods of the request for matching so we always know which migrations to apply.
19
+
8
20
  ## [5.2.1]
9
21
 
10
22
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cadwyn
3
- Version: 5.2.1
3
+ Version: 5.3.0
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
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine, Sequence
4
4
  from datetime import date
5
5
  from logging import getLogger
6
6
  from pathlib import Path
7
- from typing import TYPE_CHECKING, Annotated, Any, Union, cast
7
+ from typing import TYPE_CHECKING, Annotated, Any, Optional, Union, cast
8
8
 
9
9
  import fastapi
10
10
  from fastapi import APIRouter, FastAPI, HTTPException, routing
@@ -71,6 +71,8 @@ class Cadwyn(FastAPI):
71
71
  api_version_format: APIVersionFormat = "date",
72
72
  api_version_parameter_name: str = "x-api-version",
73
73
  api_version_default_value: Union[str, None, Callable[[Request], Awaitable[str]]] = None,
74
+ api_version_title: Optional[str] = None,
75
+ api_version_description: Optional[str] = None,
74
76
  versioning_middleware_class: type[VersionPickingMiddleware] = VersionPickingMiddleware,
75
77
  changelog_url: Union[str, None] = "/changelog",
76
78
  include_changelog_url_in_schema: bool = True,
@@ -207,6 +209,8 @@ class Cadwyn(FastAPI):
207
209
  self.api_version_format = api_version_format
208
210
  self.api_version_parameter_name = api_version_parameter_name
209
211
  self.api_version_pythonic_parameter_name = api_version_parameter_name.replace("-", "_")
212
+ self.api_version_title = api_version_title
213
+ self.api_version_description = api_version_description
210
214
  if api_version_location == "custom_header":
211
215
  self._api_version_manager = HeaderVersionManager(api_version_parameter_name=api_version_parameter_name)
212
216
  self._api_version_fastapi_depends_class = fastapi.Header
@@ -465,6 +469,8 @@ class Cadwyn(FastAPI):
465
469
  default_value=version,
466
470
  fastapi_depends_class=self._api_version_fastapi_depends_class,
467
471
  validation_data_type=self.api_version_validation_data_type,
472
+ title=self.api_version_title,
473
+ description=self.api_version_description,
468
474
  )
469
475
  )
470
476
  ],
@@ -5,7 +5,7 @@ import inspect
5
5
  import re
6
6
  from collections.abc import Awaitable, Callable
7
7
  from contextvars import ContextVar
8
- from typing import Annotated, Any, Literal, Protocol, Union
8
+ from typing import Annotated, Any, Literal, Optional, Protocol, Union
9
9
 
10
10
  import fastapi
11
11
  from fastapi import Request
@@ -58,6 +58,8 @@ def _generate_api_version_dependency(
58
58
  default_value: str,
59
59
  fastapi_depends_class: Callable[..., Any],
60
60
  validation_data_type: Any,
61
+ title: Optional[str] = None,
62
+ description: Optional[str] = None,
61
63
  ):
62
64
  def api_version_dependency(**kwargs: Any):
63
65
  # TODO: What do I return?
@@ -69,7 +71,12 @@ def _generate_api_version_dependency(
69
71
  api_version_pythonic_parameter_name,
70
72
  inspect.Parameter.KEYWORD_ONLY,
71
73
  annotation=Annotated[
72
- validation_data_type, fastapi_depends_class(openapi_examples={"default": {"value": default_value}})
74
+ validation_data_type,
75
+ fastapi_depends_class(
76
+ openapi_examples={"default": {"value": default_value}},
77
+ title=title,
78
+ description=description,
79
+ ),
73
80
  ],
74
81
  # Path-based parameters do not support a default value in FastAPI :(
75
82
  default=default_value if fastapi_depends_class != fastapi.Path else inspect.Signature.empty,
@@ -37,11 +37,13 @@ from cadwyn.schema_generation import (
37
37
  )
38
38
  from cadwyn.structure import Version, VersionBundle
39
39
  from cadwyn.structure.common import Endpoint, VersionType
40
+ from cadwyn.structure.data import _AlterRequestByPathInstruction, _AlterResponseByPathInstruction
40
41
  from cadwyn.structure.endpoints import (
41
42
  EndpointDidntExistInstruction,
42
43
  EndpointExistedInstruction,
43
44
  EndpointHadInstruction,
44
45
  )
46
+ from cadwyn.structure.versions import VersionChange
45
47
 
46
48
  if TYPE_CHECKING:
47
49
  from fastapi.dependencies.models import Dependant
@@ -52,6 +54,9 @@ _WR = TypeVar("_WR", bound=APIRouter, default=APIRouter)
52
54
  _RouteT = TypeVar("_RouteT", bound=BaseRoute)
53
55
  # This is a hack we do because we can't guarantee how the user will use the router.
54
56
  _DELETED_ROUTE_TAG = "_CADWYN_DELETED_ROUTE"
57
+ _RoutePath = str
58
+ _RouteMethod = str
59
+ _RouteId = int
55
60
 
56
61
 
57
62
  @dataclass(**DATACLASS_SLOTS, frozen=True, eq=True)
@@ -123,6 +128,7 @@ class _EndpointTransformer(Generic[_R, _WR]):
123
128
  ]
124
129
 
125
130
  def transform(self) -> GeneratedRouters[_R, _WR]:
131
+ # Copy MUST keep the order and number of routes. Otherwise, a ton of code below will break.
126
132
  router = copy_router(self.parent_router)
127
133
  webhook_router = copy_router(self.parent_webhooks_router)
128
134
  routers: dict[VersionType, _R] = {}
@@ -132,7 +138,7 @@ class _EndpointTransformer(Generic[_R, _WR]):
132
138
  self.schema_generators[str(version.value)].annotation_transformer.migrate_router_to_version(router)
133
139
  self.schema_generators[str(version.value)].annotation_transformer.migrate_router_to_version(webhook_router)
134
140
 
135
- self._validate_all_data_converters_are_applied(router, version)
141
+ self._attach_routes_to_data_converters(router, self.parent_router, version)
136
142
 
137
143
  routers[version.value] = router
138
144
  webhook_routers[version.value] = webhook_router
@@ -193,9 +199,11 @@ class _EndpointTransformer(Generic[_R, _WR]):
193
199
  ]
194
200
  return GeneratedRouters(routers, webhook_routers)
195
201
 
196
- def _validate_all_data_converters_are_applied(self, router: APIRouter, version: Version):
197
- path_to_route_methods_mapping, head_response_models, head_request_bodies = self._extract_all_routes_identifiers(
198
- router
202
+ def _attach_routes_to_data_converters(self, router: APIRouter, head_router: APIRouter, version: Version):
203
+ # This method is way out of its league in terms of complexity. We gotta refactor it.
204
+
205
+ path_to_route_methods_mapping, head_response_models, head_request_bodies = (
206
+ self._extract_all_routes_identifiers_for_route_to_converter_matching(router)
199
207
  )
200
208
 
201
209
  for version_change in version.changes:
@@ -204,21 +212,10 @@ class _EndpointTransformer(Generic[_R, _WR]):
204
212
  *version_change.alter_request_by_path_instructions.values(),
205
213
  ]:
206
214
  for by_path_converter in by_path_converters:
207
- missing_methods = by_path_converter.methods.difference(
208
- path_to_route_methods_mapping[by_path_converter.path]
215
+ self._attach_routes_by_path_converter(
216
+ head_router, path_to_route_methods_mapping, version_change, by_path_converter
209
217
  )
210
218
 
211
- if missing_methods:
212
- raise RouteByPathConverterDoesNotApplyToAnythingError(
213
- f"{by_path_converter.repr_name} "
214
- f'"{version_change.__name__}.{by_path_converter.transformer.__name__}" '
215
- f"failed to find routes with the following methods: {list(missing_methods)}. "
216
- f"This means that you are trying to apply this converter to non-existing endpoint(s). "
217
- "Please, check whether the path and methods are correct. (hint: path must include "
218
- "all path variables and have a name that was used in the version that this "
219
- "VersionChange resides in)"
220
- )
221
-
222
219
  for by_schema_converters in version_change.alter_request_by_schema_instructions.values():
223
220
  for by_schema_converter in by_schema_converters:
224
221
  if not by_schema_converter.check_usage: # pragma: no cover
@@ -249,14 +246,50 @@ class _EndpointTransformer(Generic[_R, _WR]):
249
246
  f"{version_change.__name__}.{by_schema_converter.transformer.__name__}"
250
247
  )
251
248
 
252
- def _extract_all_routes_identifiers(
253
- self, router: APIRouter
254
- ) -> tuple[defaultdict[str, set[str]], set[Any], set[Any]]:
255
- response_models: set[Any] = set()
256
- request_bodies: set[Any] = set()
257
- path_to_route_methods_mapping: dict[str, set[str]] = defaultdict(set)
249
+ def _attach_routes_by_path_converter(
250
+ self,
251
+ head_router: APIRouter,
252
+ path_to_route_methods_mapping: dict[_RoutePath, dict[_RouteMethod, set[_RouteId]]],
253
+ version_change: type[VersionChange],
254
+ by_path_converter: Union[_AlterResponseByPathInstruction, _AlterRequestByPathInstruction],
255
+ ):
256
+ missing_methods = set()
257
+ for method in by_path_converter.methods:
258
+ if method in path_to_route_methods_mapping[by_path_converter.path]:
259
+ for route_index in path_to_route_methods_mapping[by_path_converter.path][method]:
260
+ route = head_router.routes[route_index]
261
+ if isinstance(by_path_converter, _AlterResponseByPathInstruction):
262
+ version_change._route_to_response_migration_mapping[id(route)].append(by_path_converter)
263
+ else:
264
+ version_change._route_to_request_migration_mapping[id(route)].append(by_path_converter)
265
+ else:
266
+ missing_methods.add(method)
267
+
268
+ if missing_methods:
269
+ raise RouteByPathConverterDoesNotApplyToAnythingError(
270
+ f"{by_path_converter.repr_name} "
271
+ f'"{version_change.__name__}.{by_path_converter.transformer.__name__}" '
272
+ f"failed to find routes with the following methods: {list(missing_methods)}. "
273
+ f"This means that you are trying to apply this converter to non-existing endpoint(s). "
274
+ "Please, check whether the path and methods are correct. (hint: path must include "
275
+ "all path variables and have a name that was used in the version that this "
276
+ "VersionChange resides in)"
277
+ )
258
278
 
259
- for route in router.routes:
279
+ def _extract_all_routes_identifiers_for_route_to_converter_matching(
280
+ self, router: APIRouter
281
+ ) -> tuple[dict[_RoutePath, dict[_RouteMethod, set[_RouteId]]], set[Any], set[Any]]:
282
+ # int is the index of the route in the router.routes list.
283
+ # So we essentially keep track of which routes have which response models and request bodies.
284
+ # and their indices in the router.routes list. The indices will allow us to match them to the same
285
+ # routes in the head version. This gives us the ability to later apply changes to these routes
286
+ # without thinking about any renamings or response model changes.
287
+
288
+ response_models = set()
289
+ request_bodies = set()
290
+ path_to_route_methods_mapping: dict[str, dict[str, set[int]]] = defaultdict(lambda: defaultdict(set))
291
+
292
+ for index, route in enumerate(router.routes):
260
293
  if isinstance(route, APIRoute):
261
294
  if route.response_model is not None and lenient_issubclass(route.response_model, BaseModel):
262
295
  response_models.add(route.response_model)
@@ -265,7 +298,8 @@ class _EndpointTransformer(Generic[_R, _WR]):
265
298
  annotation = route.body_field.field_info.annotation
266
299
  if annotation is not None and lenient_issubclass(annotation, BaseModel):
267
300
  request_bodies.add(annotation)
268
- path_to_route_methods_mapping[route.path] |= route.methods
301
+ for method in route.methods:
302
+ path_to_route_methods_mapping[route.path][method].add(index)
269
303
 
270
304
  head_response_models = {model.__cadwyn_original_model__ for model in response_models}
271
305
  head_request_bodies = {getattr(body, "__cadwyn_original_model__", body) for body in request_bodies}
@@ -196,8 +196,7 @@ def migrate_response_body(
196
196
  response,
197
197
  current_version=version,
198
198
  head_response_model=latest_response_model,
199
- path="\0\0\0",
200
- method="GET",
199
+ head_route=None,
201
200
  )
202
201
 
203
202
  versioned_response_model: type[pydantic.BaseModel] = generate_versioned_models(versions)[str(version)][
@@ -53,6 +53,7 @@ _CADWYN_REQUEST_PARAM_NAME = "cadwyn_request_param"
53
53
  _CADWYN_RESPONSE_PARAM_NAME = "cadwyn_response_param"
54
54
  _P = ParamSpec("_P")
55
55
  _R = TypeVar("_R")
56
+ _RouteId = int
56
57
 
57
58
  PossibleInstructions: TypeAlias = Union[
58
59
  AlterSchemaSubInstruction, AlterEndpointSubInstruction, AlterEnumSubInstruction, SchemaHadInstruction, staticmethod
@@ -85,6 +86,8 @@ class VersionChange:
85
86
  alter_response_by_schema_instructions: ClassVar[dict[type, list[_AlterResponseBySchemaInstruction]]] = Sentinel
86
87
  alter_response_by_path_instructions: ClassVar[dict[str, list[_AlterResponseByPathInstruction]]] = Sentinel
87
88
  _bound_version_bundle: "Union[VersionBundle, None]"
89
+ _route_to_request_migration_mapping: ClassVar[dict[_RouteId, list[_AlterRequestByPathInstruction]]] = Sentinel
90
+ _route_to_response_migration_mapping: ClassVar[dict[_RouteId, list[_AlterResponseByPathInstruction]]] = Sentinel
88
91
 
89
92
  def __init_subclass__(cls, _abstract: bool = False) -> None:
90
93
  super().__init_subclass__()
@@ -96,6 +99,8 @@ class VersionChange:
96
99
  cls._extract_body_instructions_into_correct_containers()
97
100
  cls._check_no_subclassing()
98
101
  cls._bound_version_bundle = None
102
+ cls._route_to_request_migration_mapping = defaultdict(list)
103
+ cls._route_to_response_migration_mapping = defaultdict(list)
99
104
 
100
105
  @classmethod
101
106
  def _extract_body_instructions_into_correct_containers(cls):
@@ -358,7 +363,6 @@ class VersionBundle:
358
363
  self,
359
364
  body_type: Union[type[BaseModel], None],
360
365
  head_dependant: Dependant,
361
- path: str,
362
366
  request: FastapiRequest,
363
367
  response: FastapiResponse,
364
368
  request_info: RequestInfo,
@@ -369,18 +373,17 @@ class VersionBundle:
369
373
  embed_body_fields: bool,
370
374
  background_tasks: Union[BackgroundTasks, None],
371
375
  ) -> dict[str, Any]:
372
- method = request.method
373
-
374
376
  start = self.reversed_version_values.index(current_version)
377
+ head_route_id = id(head_route)
375
378
  for v in self.reversed_versions[start + 1 :]:
376
379
  for version_change in v.changes:
377
380
  if body_type is not None and body_type in version_change.alter_request_by_schema_instructions:
378
381
  for instruction in version_change.alter_request_by_schema_instructions[body_type]:
379
382
  instruction(request_info)
380
- if path in version_change.alter_request_by_path_instructions:
381
- for instruction in version_change.alter_request_by_path_instructions[path]:
382
- if method in instruction.methods: # pragma: no branch # safe branch to skip
383
- instruction(request_info)
383
+ if head_route_id in version_change._route_to_request_migration_mapping:
384
+ for instruction in version_change._route_to_request_migration_mapping[head_route_id]:
385
+ instruction(request_info)
386
+
384
387
  request.scope["headers"] = tuple((key.encode(), value.encode()) for key, value in request_info.headers.items())
385
388
  del request._headers
386
389
  # This gives us the ability to tell the user whether cadwyn is running its dependencies or FastAPI
@@ -406,10 +409,10 @@ class VersionBundle:
406
409
  self,
407
410
  response_info: ResponseInfo,
408
411
  current_version: VersionType,
409
- head_response_model: type[BaseModel],
410
- path: str,
411
- method: str,
412
+ head_response_model: Union[type[BaseModel], None],
413
+ head_route: Union[APIRoute, None],
412
414
  ) -> ResponseInfo:
415
+ head_route_id = id(head_route)
413
416
  end = self.version_values.index(current_version)
414
417
  for v in self.versions[:end]:
415
418
  for version_change in v.changes:
@@ -420,10 +423,8 @@ class VersionBundle:
420
423
  version_change.alter_response_by_schema_instructions[head_response_model]
421
424
  )
422
425
 
423
- if path in version_change.alter_response_by_path_instructions:
424
- for instruction in version_change.alter_response_by_path_instructions[path]:
425
- if method in instruction.methods: # pragma: no branch # Safe branch to skip
426
- migrations_to_apply.append(instruction) # noqa: PERF401
426
+ if head_route_id in version_change._route_to_response_migration_mapping:
427
+ migrations_to_apply.extend(version_change._route_to_response_migration_mapping[head_route_id])
427
428
 
428
429
  for migration in migrations_to_apply:
429
430
  if response_info.status_code < 300 or migration.migrate_http_errors:
@@ -576,8 +577,7 @@ class VersionBundle:
576
577
  response_info,
577
578
  api_version,
578
579
  head_route.response_model,
579
- route.path,
580
- method,
580
+ head_route,
581
581
  )
582
582
  if isinstance(response_or_response_body, FastapiResponse):
583
583
  # a webserver (uvicorn for instance) calculates the body at the endpoint level.
@@ -671,7 +671,6 @@ class VersionBundle:
671
671
  new_kwargs = await self._migrate_request(
672
672
  head_body_field,
673
673
  head_dependant,
674
- route.path,
675
674
  request,
676
675
  response,
677
676
  request_info,
@@ -1,4 +1,4 @@
1
- # Where to put the version and how to format it
1
+ # API Version Parameter
2
2
 
3
3
  Cadwyn adds another routing layer to FastAPI by default: by version parameter. This means that before FastAPI tries to route us to the correct route, Cadwyn will first decide on which version of the route to use based on a version parameter. Feel free to look at the example app with URL path version prefixes and arbitrary strings as versions [here](../how_to/version_with_paths_and_numbers_instead_of_headers_and_dates.md).
4
4
 
@@ -91,3 +91,21 @@ If the app has two versions: 2022-01-02 and 2022-01-05, and the request date par
91
91
  Exact match is always preferred over partial match and a request will never be matched to the higher versioned route.
92
92
 
93
93
  We implement routing like this because Cadwyn was born in a microservice architecture and it is extremely convenient to have waterfalling there. For example, imagine that you have two Cadwyn services: Payables and Receivables, each defining its own API versions. Payables service might contain 10 versions while receivables service might contain only 2 versions because it didn't need as many breaking changes. If a client requests a version that does not exist in receivables -- we will just waterfall to some earlier version, making receivables behavior consistent even if API keeps getting new versions.
94
+
95
+ ## API Version Parameter Title and Description
96
+
97
+ You can pass a title and/or a description to `Cadwyn` constructor. They are equivalent to passing `title` and `description` to `fastapi.Path` or `fastapi.Header` constructors.
98
+
99
+ ```python
100
+ app = Cadwyn(
101
+ ...,
102
+ api_version_title="My Great API version parameter",
103
+ api_version_description="Description of my great API version parameter",
104
+ )
105
+ ```
106
+
107
+ ## API Version Context Variables
108
+
109
+ Cadwyn automatically converts your data to a correct version and has "version checks" when dealing with side effects as described in [the section above](./version_changes.md#version-changes-with-side-effects). It can only do so using a special [context variable](https://docs.python.org/3/library/contextvars.html) that stores the current API version.
110
+
111
+ You can also pass a different compatible contextvar to your `cadwyn.VersionBundle` constructor.
@@ -1,10 +1,10 @@
1
1
  # Endpoint migrations
2
2
 
3
- Note that the endpoint constructor contains a second argument that describes the methods of the endpoints you would like to edit. If you have two routes for a single endpoint and you put both of their methods into the instruction -- both of them are going to be changed as you would expect.
3
+ Note that the `endpoint(...)` constructor contains a second argument that describes the methods of the endpoints you would like to edit. If you have two routes for a single endpoint and you put both of their methods into the instruction -- both of them are going to be changed as you would expect.
4
4
 
5
5
  ## Defining endpoints that didn't exist in new versions
6
6
 
7
- If you had an endpoint in old version but do not have it in a new one, you must still define it but mark it as deleted.
7
+ If you had an endpoint in an old version **but want to delete it in a new version**, you must still define it as usual with all your other endpoints but mark it as deleted.
8
8
 
9
9
  ```python
10
10
  @router.only_exists_in_older_versions
@@ -28,7 +28,7 @@ class MyChange(VersionChange):
28
28
 
29
29
  ## Defining endpoints that didn't exist in old versions
30
30
 
31
- If you have an endpoint in your new version that must not exist in older versions, you define it as usual and then mark it as "non-existing" in old versions:
31
+ If you have an endpoint in your new version that must not exist in older versions for some reason, you define it as usual and then mark it as "non-existing" in old versions. Note that this is [not the recommended approach when adding new endpoints](../how_to/change_endpoints/index.md#add-a-new-endpoint).
32
32
 
33
33
  ```python
34
34
  from cadwyn import VersionChange, endpoint
@@ -58,6 +58,8 @@ class MyChange(VersionChange):
58
58
  )
59
59
  ```
60
60
 
61
+ However, you only need to have a migration if it is a breaking change for your users.
62
+
61
63
  ### Dependency alteration warning
62
64
 
63
65
  Note that changing endpoint `dependencies` is only going to affect the initial validation. So Cadwyn will take your altered dependencies and run them on each request to the endpoint but ultimately your endpoint code is always going to use the HEAD version of your dependencies. So be careful.
@@ -2,6 +2,8 @@
2
2
 
3
3
  All of the following instructions affect only openapi schemas and their initial validation. All of your incoming requests will still be converted into your HEAD schemas.
4
4
 
5
+ Please note that you only need to have a migration if it is a breaking change for your users. The scenarios below only describe "what you can do" but not "what you should do". For the "should", please refer to the [how-to docs](../how_to/change_openapi_schemas/add_field.md).
6
+
5
7
  ## Add a field to the older version
6
8
 
7
9
  ```python
@@ -93,6 +93,8 @@ HEAD is very similar to your latest version with a few key differences:
93
93
 
94
94
  `VersionChange` classes describe each atomic group of business capabilities that you have altered in a version.
95
95
 
96
+ Note, however, that you only need to have a migration if it is a breaking change for your users. If you add a new endpoint or add a new field to your response schema, you do not need to have a migration for it because your users' code will not break. So by not having a migration you automatically add this change to all versions.
97
+
96
98
  ### VersionChange.\_\_name\_\_
97
99
 
98
100
  The name of the version change, `RemoveTaxIDEndpoints`, describes what breaking change has happened. It must be a verb and it is the best resource for your new developers to quickly understand what happened between the versions. Do not be shy to use really long names -- it is better to have a long name than to create a misunderstanding. Avoid generic names such as `RefactorUserFields`. Better have an ugly name such as `RenameCreationDatetimeAndUpdateDatetimeToCreatedAtAndUpdatedAt` then to have a generic name such as `RefactorFields`. Because after just a few of such version changes, your versioning structure can become completely unreadable:
@@ -14,7 +14,7 @@ Now you have everything you need at your disposal: field `created_at` is availab
14
14
 
15
15
  Let's say we want our users to be able to specify a middle name but it is nullable. It is not a breaking change so no new version is necessary whether it is requests or responses.
16
16
 
17
- You just need to add a nullable `middle_name` field into `users.BaseUser`
17
+ You just need to add a nullable `middle_name` field into `users.BaseUser` as if you were working with a barebones FastAPI app.
18
18
 
19
19
  ### Field is required
20
20
 
@@ -59,7 +59,6 @@ Let's say that our users had a field `country` that defaulted to `USA` but our p
59
59
  )
60
60
  ```
61
61
 
62
-
63
62
  That's it! Our old schemas will now contain a default but in HEAD country will be required. You might notice a weirdness: if we set a default in the old version, why would we also write a migration? That's because of a sad implementation detail of pydantic that [prevents us](../../concepts/schema_migrations.md#change-a-field-in-the-older-version) from using defaults from old versions.
64
63
 
65
64
  #### With incompatible default value in older versions
@@ -118,5 +117,4 @@ So we will make `phone` nullable in HEAD, then make it required in `latest`, and
118
117
  )
119
118
  ```
120
119
 
121
-
122
120
  See how we didn't remove the `phone` field from old versions? Instead, we allowed a nullable `phone` field to be passed into both old `UserResource` and old `UserCreateRequest`. This gives our users new functionality without needing to update their API version! It is one of the best parts of Cadwyn's approach: our users can get years worth of updates without switching their API version and without their integration getting broken.
@@ -3,7 +3,7 @@
3
3
  Cadwyn uses version headers with ISO dates by default for versioning. However, you can use any strings instead of ISO dates and/or you can use path version prefixes instead of version headers. Here's our quickstart tutorial example but using version numbers and path prefixes:
4
4
 
5
5
  Feel free to mix and match the API version formats and version locations as you see fit.
6
- But beware that Cadwyn does not support [version waterfalling](../concepts/where_to_put_the_version_and_how_to_format_it.md#api-version-waterfalling) for arbitrary strings as versions.
6
+ But beware that Cadwyn does not support [version waterfalling](../concepts/api_version_parameter.md#api-version-waterfalling) for arbitrary strings as versions.
7
7
 
8
8
  ```python
9
9
  {! ./docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/block001.py !}
@@ -119,8 +119,7 @@ nav:
119
119
  - "Endpoint migrations": "concepts/endpoint_migrations.md"
120
120
  - "Enum migrations": "concepts/enum_migrations.md"
121
121
  - "Schema migrations": "concepts/schema_migrations.md"
122
- - "API Version parameter and context variables": "concepts/api_version_parameter_and_context_variables.md"
123
- - "Where to put the version and how to format it": "concepts/where_to_put_the_version_and_how_to_format_it.md"
122
+ - "API Version parameter": "concepts/api_version_parameter.md"
124
123
  - "Changelogs": "concepts/changelogs.md"
125
124
  - "Testing": concepts/testing.md
126
125
  - "Contributing": "home/CONTRIBUTING.md"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cadwyn"
3
- version = "5.2.1"
3
+ version = "5.3.0"
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"
@@ -135,7 +135,7 @@ ignore = [
135
135
  ]
136
136
 
137
137
  [lint.mccabe]
138
- max-complexity = 14
138
+ max-complexity = 15
139
139
 
140
140
  [format]
141
141
  quote-style = "double"
@@ -20,11 +20,13 @@ from starlette.responses import FileResponse
20
20
  from typing_extensions import Any, NewType, TypeAlias, TypeAliasType, get_args
21
21
 
22
22
  from cadwyn import VersionBundle, VersionedAPIRouter
23
+ from cadwyn.applications import Cadwyn
23
24
  from cadwyn.dependencies import current_dependency_solver
24
25
  from cadwyn.exceptions import CadwynError, RouterGenerationError, RouterPathParamsModifiedError
25
26
  from cadwyn.route_generation import generate_versioned_routers
26
27
  from cadwyn.schema_generation import generate_versioned_models
27
28
  from cadwyn.structure import Version, convert_request_to_next_version_for, endpoint, schema
29
+ from cadwyn.structure.data import RequestInfo, ResponseInfo, convert_response_to_previous_version_for
28
30
  from cadwyn.structure.enums import enum
29
31
  from cadwyn.structure.versions import VersionChange
30
32
  from tests._data.unversioned_schema_dir import UnversionedSchema2
@@ -252,6 +254,50 @@ def test__endpoint_had_another_path_variable(
252
254
  )
253
255
 
254
256
 
257
+ def test__endpoint_had__another_path_with_the_other_migration_at_the_same_time__should_require_old_name(
258
+ create_versioned_app: CreateVersionedApp,
259
+ ):
260
+ router = VersionedAPIRouter()
261
+
262
+ @router.post("/A")
263
+ async def test_endpoint_post(body: list[str]):
264
+ return body
265
+
266
+ @router.put("/A")
267
+ async def test_endpoint_put(body: list[str]):
268
+ return body
269
+
270
+ @convert_response_to_previous_version_for("/A", ["POST"])
271
+ def response_migration(response: ResponseInfo):
272
+ response.body.append("response")
273
+
274
+ @convert_request_to_next_version_for("/A", ["POST"])
275
+ def request_migration(request: RequestInfo):
276
+ request.body.append("request")
277
+
278
+ app = Cadwyn(
279
+ versions=VersionBundle(
280
+ Version(
281
+ "2001-01-01",
282
+ version_change(
283
+ endpoint("/A", ["POST"]).had(path="/B"),
284
+ request_migration=request_migration,
285
+ response_migration=response_migration,
286
+ ),
287
+ ),
288
+ Version("2000-01-01"),
289
+ )
290
+ )
291
+ app.generate_and_include_versioned_routers(router)
292
+
293
+ with TestClient(app) as client:
294
+ assert client.post("/B", headers={"X-API-VERSION": "2000-01-01"}, json=[]).json() == ["request", "response"]
295
+ assert client.post("/A", headers={"X-API-VERSION": "2001-01-01"}, json=[]).json() == []
296
+
297
+ assert client.put("/A", headers={"X-API-VERSION": "2000-01-01"}, json=[]).json() == []
298
+ assert client.put("/A", headers={"X-API-VERSION": "2001-01-01"}, json=[]).json() == []
299
+
300
+
255
301
  def test__endpoint_had_dependencies(
256
302
  test_endpoint: Endpoint,
257
303
  test_path: str,
@@ -123,9 +123,14 @@ async def get_user_addresses(user_id: uuid.UUID):
123
123
  return {"data": database_parody[f"addr_{user_id}"]}
124
124
 
125
125
 
126
- app = Cadwyn(versions=version_bundle, title="My amazing API")
126
+ app = Cadwyn(
127
+ versions=version_bundle,
128
+ title="My amazing API",
129
+ api_version_title="My Great API version parameter",
130
+ api_version_description="Description of my great API version parameter",
131
+ )
127
132
  app.generate_and_include_versioned_routers(router)
128
133
 
129
134
 
130
135
  if __name__ == "__main__":
131
- uvicorn.run(app)
136
+ uvicorn.run(app, port=8011)
@@ -94,7 +94,7 @@ wheels = [
94
94
 
95
95
  [[package]]
96
96
  name = "cadwyn"
97
- version = "5.2.1"
97
+ version = "5.3.0"
98
98
  source = { editable = "." }
99
99
  dependencies = [
100
100
  { name = "backports-strenum", marker = "python_full_version < '3.11'" },
@@ -1,5 +0,0 @@
1
- # API Version parameter and context variables
2
-
3
- Cadwyn automatically converts your data to a correct version and has "version checks" when dealing with side effects as described in [the section above](./version_changes.md#version-changes-with-side-effects). It can only do so using a special [context variable](https://docs.python.org/3/library/contextvars.html) that stores the current API version.
4
-
5
- You can also pass a different compatible contextvar to your `cadwyn.VersionBundle` constructor.
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
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