cadwyn 5.2.1__tar.gz → 5.2.2__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.2.2}/CHANGELOG.md +6 -0
  2. {cadwyn-5.2.1 → cadwyn-5.2.2}/PKG-INFO +1 -1
  3. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/route_generation.py +59 -25
  4. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/schema_generation.py +1 -2
  5. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/structure/versions.py +16 -17
  6. {cadwyn-5.2.1 → cadwyn-5.2.2}/pyproject.toml +1 -1
  7. {cadwyn-5.2.1 → cadwyn-5.2.2}/ruff.toml +1 -1
  8. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_router_generation.py +46 -0
  9. {cadwyn-5.2.1 → cadwyn-5.2.2}/uv.lock +1 -1
  10. {cadwyn-5.2.1 → cadwyn-5.2.2}/.github/CODE_OF_CONDUCT.md +0 -0
  11. {cadwyn-5.2.1 → cadwyn-5.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {cadwyn-5.2.1 → cadwyn-5.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {cadwyn-5.2.1 → cadwyn-5.2.2}/.github/actions/setup-python-uv/action.yaml +0 -0
  14. {cadwyn-5.2.1 → cadwyn-5.2.2}/.github/workflows/ci.yaml +0 -0
  15. {cadwyn-5.2.1 → cadwyn-5.2.2}/.github/workflows/daily_tests.yaml +0 -0
  16. {cadwyn-5.2.1 → cadwyn-5.2.2}/.github/workflows/publish_docs.yaml +0 -0
  17. {cadwyn-5.2.1 → cadwyn-5.2.2}/.github/workflows/release.yaml +0 -0
  18. {cadwyn-5.2.1 → cadwyn-5.2.2}/.gitignore +0 -0
  19. {cadwyn-5.2.1 → cadwyn-5.2.2}/.pre-commit-config.yaml +0 -0
  20. {cadwyn-5.2.1 → cadwyn-5.2.2}/LICENSE +0 -0
  21. {cadwyn-5.2.1 → cadwyn-5.2.2}/Makefile +0 -0
  22. {cadwyn-5.2.1 → cadwyn-5.2.2}/README.md +0 -0
  23. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/__init__.py +0 -0
  24. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/__main__.py +0 -0
  25. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/_asts.py +0 -0
  26. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/_importer.py +0 -0
  27. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/_internal/__init__.py +0 -0
  28. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/_internal/context_vars.py +0 -0
  29. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/_render.py +0 -0
  30. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/_utils.py +0 -0
  31. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/applications.py +0 -0
  32. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/changelogs.py +0 -0
  33. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/dependencies.py +0 -0
  34. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/exceptions.py +0 -0
  35. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/middleware.py +0 -0
  36. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/py.typed +0 -0
  37. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/routing.py +0 -0
  38. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/static/__init__.py +0 -0
  39. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/static/docs.html +0 -0
  40. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/structure/__init__.py +0 -0
  41. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/structure/common.py +0 -0
  42. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/structure/data.py +0 -0
  43. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/structure/endpoints.py +0 -0
  44. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/structure/enums.py +0 -0
  45. {cadwyn-5.2.1 → cadwyn-5.2.2}/cadwyn/structure/schemas.py +0 -0
  46. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/CNAME +0 -0
  47. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/__init__.py +0 -0
  48. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/api_version_parameter_and_context_variables.md +0 -0
  49. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/beware_of_data_versioning.md +0 -0
  50. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/changelogs.md +0 -0
  51. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/cli.md +0 -0
  52. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/endpoint_migrations.md +0 -0
  53. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/enum_migrations.md +0 -0
  54. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/index.md +0 -0
  55. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/main_app.md +0 -0
  56. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/methodology.md +0 -0
  57. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/schema_generation.md +0 -0
  58. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/schema_migrations.md +0 -0
  59. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/testing.md +0 -0
  60. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/version_changes.md +0 -0
  61. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/concepts/where_to_put_the_version_and_how_to_format_it.md +0 -0
  62. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/home/CONTRIBUTING.md +0 -0
  63. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/how_to/change_business_logic/index.md +0 -0
  64. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/how_to/change_endpoints/index.md +0 -0
  65. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/how_to/change_openapi_schemas/add_field.md +0 -0
  66. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/how_to/change_openapi_schemas/change_field_type.md +0 -0
  67. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/how_to/change_openapi_schemas/change_schema_without_endpoint.md +0 -0
  68. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/how_to/change_openapi_schemas/changing_constraints.md +0 -0
  69. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/how_to/change_openapi_schemas/remove_field.md +0 -0
  70. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/how_to/change_openapi_schemas/rename_a_field_in_schema.md +0 -0
  71. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/how_to/index.md +0 -0
  72. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/how_to/version_with_paths_and_numbers_instead_of_headers_and_dates.md +0 -0
  73. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/img/dashboard_with_one_version.png +0 -0
  74. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/img/dashboard_with_two_versions.png +0 -0
  75. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/img/get_users_endpoint_from_prior_version.png +0 -0
  76. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/img/simplified_migration_model.png +0 -0
  77. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/img/sponsor_logos/monite.png +0 -0
  78. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/img/unversioned_dashboard.png +0 -0
  79. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/index.md +0 -0
  80. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/plugin.py +0 -0
  81. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/quickstart/setup.md +0 -0
  82. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/quickstart/tutorial.md +0 -0
  83. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/theory/how_to_build_versioning_framework.md +0 -0
  84. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/theory/how_we_got_here.md +0 -0
  85. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs/theory/literature.md +0 -0
  86. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/__init__.py +0 -0
  87. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/__init__.py +0 -0
  88. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/change_openapi_schemas/__init__.py +0 -0
  89. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/__init__.py +0 -0
  90. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block001.py +0 -0
  91. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block002.py +0 -0
  92. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/__init__.py +0 -0
  93. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block001.py +0 -0
  94. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block002.py +0 -0
  95. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/__init__.py +0 -0
  96. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/block001.py +0 -0
  97. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/tests/__init__.py +0 -0
  98. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/tests/test_block_001.py +0 -0
  99. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/__init__.py +0 -0
  100. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/setup/__init__.py +0 -0
  101. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/setup/block001.sh +0 -0
  102. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/setup/block002.py +0 -0
  103. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/setup/tests/__init__.py +0 -0
  104. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/setup/tests/test_block002.py +0 -0
  105. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/tutorial/__init__.py +0 -0
  106. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/tutorial/block001.py +0 -0
  107. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/tutorial/block002.py +0 -0
  108. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/tutorial/block003.py +0 -0
  109. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/tutorial/tests/__init__.py +0 -0
  110. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/tutorial/tests/test_block001.py +0 -0
  111. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/tutorial/tests/test_block002.py +0 -0
  112. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/quickstart/tutorial/tests/test_block003.py +0 -0
  113. {cadwyn-5.2.1 → cadwyn-5.2.2}/docs_src/ruff.toml +0 -0
  114. {cadwyn-5.2.1 → cadwyn-5.2.2}/mkdocs.yml +0 -0
  115. {cadwyn-5.2.1 → cadwyn-5.2.2}/scripts/fix_links.py +0 -0
  116. {cadwyn-5.2.1 → cadwyn-5.2.2}/scripts/split_md.py +0 -0
  117. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/__init__.py +0 -0
  118. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_data/__init__.py +0 -0
  119. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_data/unversioned_schema_dir/__init__.py +0 -0
  120. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_data/unversioned_schema_dir/unversioned_schemas.py +0 -0
  121. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_data/unversioned_schemas.py +0 -0
  122. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/__init__.py +0 -0
  123. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/app_for_testing_routing.py +0 -0
  124. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/render/__init__.py +0 -0
  125. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/render/classes.py +0 -0
  126. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/render/complex/__init__.py +0 -0
  127. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/render/complex/classes.py +0 -0
  128. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/render/complex/versions.py +0 -0
  129. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/render/versions.py +0 -0
  130. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/utils.py +0 -0
  131. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/versioned_app/__init__.py +0 -0
  132. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/versioned_app/app.py +0 -0
  133. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/versioned_app/v2021_01_01.py +0 -0
  134. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/versioned_app/v2022_01_02.py +0 -0
  135. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/_resources/versioned_app/webhooks.py +0 -0
  136. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/conftest.py +0 -0
  137. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_applications.py +0 -0
  138. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_auth_dependencies.py +0 -0
  139. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_changelog.py +0 -0
  140. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_cli.py +0 -0
  141. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_data_migrations.py +0 -0
  142. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_render.py +0 -0
  143. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_router_generation_with_from_future_annotations.py +0 -0
  144. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_routing.py +0 -0
  145. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_schema_generation/__init__.py +0 -0
  146. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_schema_generation/test_enum.py +0 -0
  147. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_schema_generation/test_schema.py +0 -0
  148. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_schema_generation/test_schema_field.py +0 -0
  149. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_schema_generation/test_schema_validator.py +0 -0
  150. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_schema_generation/test_schema_with_future_annotations.py +0 -0
  151. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/test_structure.py +0 -0
  152. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/tutorial/__init__.py +0 -0
  153. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/tutorial/main.py +0 -0
  154. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/tutorial/test_example.py +0 -0
  155. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/versioning_styles/__init__.py +0 -0
  156. {cadwyn-5.2.1 → cadwyn-5.2.2}/tests/versioning_styles/test_versioning_formats.py +0 -0
  157. {cadwyn-5.2.1 → cadwyn-5.2.2}/tox.ini +0 -0
@@ -5,6 +5,12 @@ Please follow [the Keep a Changelog standard](https://keepachangelog.com/en/1.0.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [5.2.2]
9
+
10
+ ### Fixed
11
+
12
+ * 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.
13
+
8
14
  ## [5.2.1]
9
15
 
10
16
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cadwyn
3
- Version: 5.2.1
3
+ Version: 5.2.2
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
@@ -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,6 +1,6 @@
1
1
  [project]
2
2
  name = "cadwyn"
3
- version = "5.2.1"
3
+ version = "5.2.2"
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,
@@ -94,7 +94,7 @@ wheels = [
94
94
 
95
95
  [[package]]
96
96
  name = "cadwyn"
97
- version = "5.2.1"
97
+ version = "5.2.2"
98
98
  source = { editable = "." }
99
99
  dependencies = [
100
100
  { name = "backports-strenum", marker = "python_full_version < '3.11'" },
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
File without changes
File without changes
File without changes
File without changes