cadwyn 4.6.0a1__tar.gz → 5.0.0a1__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-4.6.0a1 → cadwyn-5.0.0a1}/.pre-commit-config.yaml +5 -0
  2. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/CHANGELOG.md +24 -3
  3. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/PKG-INFO +1 -1
  4. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/_render.py +4 -1
  5. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/applications.py +114 -46
  6. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/changelogs.py +5 -6
  7. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/exceptions.py +2 -3
  8. cadwyn-5.0.0a1/cadwyn/middleware.py +119 -0
  9. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/route_generation.py +5 -5
  10. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/routing.py +48 -68
  11. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/schema_generation.py +8 -7
  12. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/structure/common.py +1 -2
  13. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/structure/versions.py +38 -41
  14. cadwyn-4.6.0a1/docs/concepts/api_version_header_and_context_variables.md → cadwyn-5.0.0a1/docs/concepts/api_version_parameter_and_context_variables.md +1 -1
  15. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/main_app.md +1 -1
  16. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/version_changes.md +1 -1
  17. cadwyn-5.0.0a1/docs/concepts/where_to_put_the_version_and_how_to_format_it.md +93 -0
  18. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/how_to/change_openapi_schemas/change_schema_without_endpoint.md +2 -2
  19. cadwyn-5.0.0a1/docs/how_to/version_with_paths_and_numbers_instead_of_headers_and_dates.md +10 -0
  20. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/quickstart/setup.md +3 -3
  21. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/quickstart/tutorial.md +4 -4
  22. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block001.py +6 -1
  23. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block002.py +6 -1
  24. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block001.py +9 -3
  25. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block002.py +3 -1
  26. cadwyn-5.0.0a1/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/block001.py +145 -0
  27. cadwyn-5.0.0a1/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/tests/test_block_001.py +71 -0
  28. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/quickstart/tutorial/block003.py +7 -4
  29. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/quickstart/tutorial/tests/test_block001.py +8 -2
  30. cadwyn-5.0.0a1/docs_src/quickstart/tutorial/tests/test_block002.py +34 -0
  31. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/quickstart/tutorial/tests/test_block003.py +22 -6
  32. cadwyn-5.0.0a1/docs_src/ruff.toml +1 -0
  33. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/mkdocs.yml +14 -5
  34. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/pyproject.toml +2 -1
  35. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_resources/app_for_testing_routing.py +7 -7
  36. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_resources/render/complex/versions.py +2 -4
  37. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_resources/render/versions.py +2 -4
  38. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_resources/versioned_app/app.py +19 -7
  39. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/conftest.py +10 -11
  40. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_applications.py +90 -33
  41. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_changelog.py +3 -4
  42. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_data_migrations.py +74 -83
  43. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_render.py +1 -4
  44. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_router_generation.py +66 -67
  45. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_router_generation_with_from_future_annotations.py +4 -4
  46. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_routing.py +12 -4
  47. cadwyn-5.0.0a1/tests/test_schema_generation/__init__.py +0 -0
  48. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_structure.py +37 -39
  49. cadwyn-5.0.0a1/tests/tutorial/__init__.py +0 -0
  50. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/tutorial/main.py +3 -4
  51. cadwyn-5.0.0a1/tests/versioning_styles/__init__.py +0 -0
  52. cadwyn-5.0.0a1/tests/versioning_styles/test_versioning_formats.py +48 -0
  53. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/uv.lock +99 -141
  54. cadwyn-4.6.0a1/cadwyn/middleware.py +0 -80
  55. cadwyn-4.6.0a1/docs_src/quickstart/tutorial/tests/test_block002.py +0 -24
  56. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/.github/CODE_OF_CONDUCT.md +0 -0
  57. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  58. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  59. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/.github/actions/setup-python-uv/action.yaml +0 -0
  60. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/.github/workflows/ci.yaml +0 -0
  61. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/.github/workflows/daily_tests.yaml +0 -0
  62. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/.github/workflows/publish_docs.yaml +0 -0
  63. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/.github/workflows/release.yaml +0 -0
  64. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/.gitignore +0 -0
  65. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/LICENSE +0 -0
  66. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/Makefile +0 -0
  67. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/README.md +0 -0
  68. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/__init__.py +0 -0
  69. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/__main__.py +0 -0
  70. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/_asts.py +0 -0
  71. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/_importer.py +0 -0
  72. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/_utils.py +0 -0
  73. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/py.typed +0 -0
  74. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/static/__init__.py +0 -0
  75. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/static/docs.html +0 -0
  76. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/structure/__init__.py +0 -0
  77. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/structure/data.py +0 -0
  78. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/structure/endpoints.py +0 -0
  79. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/structure/enums.py +0 -0
  80. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/cadwyn/structure/schemas.py +0 -0
  81. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/CNAME +0 -0
  82. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/__init__.py +0 -0
  83. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/beware_of_data_versioning.md +0 -0
  84. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/changelogs.md +0 -0
  85. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/cli.md +0 -0
  86. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/endpoint_migrations.md +0 -0
  87. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/enum_migrations.md +0 -0
  88. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/index.md +0 -0
  89. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/methodology.md +0 -0
  90. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/schema_generation.md +0 -0
  91. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/schema_migrations.md +0 -0
  92. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/concepts/testing.md +0 -0
  93. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/home/CONTRIBUTING.md +0 -0
  94. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/how_to/change_business_logic/index.md +0 -0
  95. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/how_to/change_endpoints/index.md +0 -0
  96. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/how_to/change_openapi_schemas/add_field.md +0 -0
  97. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/how_to/change_openapi_schemas/change_field_type.md +0 -0
  98. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/how_to/change_openapi_schemas/changing_constraints.md +0 -0
  99. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/how_to/change_openapi_schemas/remove_field.md +0 -0
  100. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/how_to/change_openapi_schemas/rename_a_field_in_schema.md +0 -0
  101. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/how_to/index.md +0 -0
  102. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/img/dashboard_with_one_version.png +0 -0
  103. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/img/dashboard_with_two_versions.png +0 -0
  104. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/img/get_users_endpoint_from_prior_version.png +0 -0
  105. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/img/simplified_migration_model.png +0 -0
  106. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/img/sponsor_logos/monite.png +0 -0
  107. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/img/unversioned_dashboard.png +0 -0
  108. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/index.md +0 -0
  109. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/plugin.py +0 -0
  110. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/theory/how_to_build_versioning_framework.md +0 -0
  111. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/theory/how_we_got_here.md +0 -0
  112. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs/theory/literature.md +0 -0
  113. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/__init__.py +0 -0
  114. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/how_to/__init__.py +0 -0
  115. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/how_to/change_openapi_schemas/__init__.py +0 -0
  116. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/__init__.py +0 -0
  117. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/__init__.py +0 -0
  118. {cadwyn-4.6.0a1/docs_src/quickstart → cadwyn-5.0.0a1/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates}/__init__.py +0 -0
  119. {cadwyn-4.6.0a1/docs_src/quickstart/setup → cadwyn-5.0.0a1/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/tests}/__init__.py +0 -0
  120. {cadwyn-4.6.0a1/docs_src/quickstart/setup/tests → cadwyn-5.0.0a1/docs_src/quickstart}/__init__.py +0 -0
  121. {cadwyn-4.6.0a1/docs_src/quickstart/tutorial → cadwyn-5.0.0a1/docs_src/quickstart/setup}/__init__.py +0 -0
  122. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/quickstart/setup/block001.sh +0 -0
  123. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/quickstart/setup/block002.py +0 -0
  124. {cadwyn-4.6.0a1/docs_src/quickstart/tutorial → cadwyn-5.0.0a1/docs_src/quickstart/setup}/tests/__init__.py +0 -0
  125. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/quickstart/setup/tests/test_block002.py +0 -0
  126. {cadwyn-4.6.0a1/tests/_data → cadwyn-5.0.0a1/docs_src/quickstart/tutorial}/__init__.py +0 -0
  127. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/quickstart/tutorial/block001.py +0 -0
  128. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/docs_src/quickstart/tutorial/block002.py +0 -0
  129. {cadwyn-4.6.0a1/tests/_resources → cadwyn-5.0.0a1/docs_src/quickstart/tutorial/tests}/__init__.py +0 -0
  130. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/ruff.toml +0 -0
  131. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/scripts/fix_links.py +0 -0
  132. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/scripts/split_md.py +0 -0
  133. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/__init__.py +0 -0
  134. {cadwyn-4.6.0a1/tests/_resources/render → cadwyn-5.0.0a1/tests/_data}/__init__.py +0 -0
  135. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_data/unversioned_schema_dir/__init__.py +0 -0
  136. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_data/unversioned_schema_dir/unversioned_schemas.py +0 -0
  137. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_data/unversioned_schemas.py +0 -0
  138. {cadwyn-4.6.0a1/tests/_resources/render/complex → cadwyn-5.0.0a1/tests/_resources}/__init__.py +0 -0
  139. {cadwyn-4.6.0a1/tests/_resources/versioned_app → cadwyn-5.0.0a1/tests/_resources/render}/__init__.py +0 -0
  140. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_resources/render/classes.py +0 -0
  141. {cadwyn-4.6.0a1/tests/test_schema_generation → cadwyn-5.0.0a1/tests/_resources/render/complex}/__init__.py +0 -0
  142. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_resources/render/complex/classes.py +0 -0
  143. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_resources/utils.py +0 -0
  144. {cadwyn-4.6.0a1/tests/tutorial → cadwyn-5.0.0a1/tests/_resources/versioned_app}/__init__.py +0 -0
  145. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_resources/versioned_app/v2021_01_01.py +0 -0
  146. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_resources/versioned_app/v2022_01_02.py +0 -0
  147. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/_resources/versioned_app/webhooks.py +0 -0
  148. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_auth_dependencies.py +0 -0
  149. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_cli.py +0 -0
  150. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_schema_generation/test_enum.py +0 -0
  151. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_schema_generation/test_schema.py +0 -0
  152. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_schema_generation/test_schema_field.py +0 -0
  153. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/test_schema_generation/test_schema_validator.py +0 -0
  154. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tests/tutorial/test_example.py +0 -0
  155. {cadwyn-4.6.0a1 → cadwyn-5.0.0a1}/tox.ini +0 -0
@@ -37,3 +37,8 @@ repos:
37
37
  hooks:
38
38
  - id: markdownlint
39
39
  args: ["--disable", "MD013"]
40
+
41
+ - repo: https://github.com/astral-sh/uv-pre-commit
42
+ rev: 0.6.5
43
+ hooks:
44
+ - id: uv-lock
@@ -5,11 +5,32 @@ Please follow [the Keep a Changelog standard](https://keepachangelog.com/en/1.0.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [5.0.0]
9
+
10
+ ### Added
11
+
12
+ * Support for URL path version prefixes instead of version headers. You can control it with the `api_version_location` argument of `cadwyn.Cadwyn`.
13
+ * Support for arbitrary strings as versions. You can control the format of the version with the `api_version_format` argument of `cadwyn.Cadwyn`.
14
+ * Extensibility of version picking logic with a new `VersionPickingMiddleware` class that you can pass to the `versioning_middleware_class` argument of `cadwyn.Cadwyn`.
15
+ * `api_version_default_value` argument to `cadwyn.Cadwyn` to set a default version for unversioned requests. It can be a string or an async callable that returns a string.
16
+
17
+ ### Changed
18
+
19
+ * `cadwyn.Version`, `cadwyn.VersionBundle`, and `cadwyn.VersionBundle.api_version_var` now store versions as strings instead of dates. Date types can still be passed to `cadwyn.Version` but there is no guarantee that they will be supported in the future. However, ISO dates with a string type are guaranteed to be supported.
20
+ * Cadwyn's `api_version_header_name` argument is now deprecated in favor of `api_version_parameter_name`
21
+ * `cadwyn.Cadwyn.add_header_versioned_routers` method is now deprecated in favor of `cadwyn.Cadwyn.generate_and_include_versioned_routers`. It will be removed in version 6.0.0
22
+
23
+ ## Removed
24
+
25
+ * `HeaderVersioningMiddleware` in favor of `VersionPickingMiddleware` because we now support more than just headers
26
+
27
+ ## [4.6.0]
28
+
8
29
  ### Added
9
30
 
10
- * Added support for more field attributes in `schema.had()` and `schema.didnt_have()`: `field_title_generator`, `fail_fast`, `coerce_numbers_to_str`, `union_mode`, `allow_mutation`, `pattern`, `discriminator`
11
- * Added support for forwardrefs in body fields (for example, when you use `from __future__ import annotations` in the file with your routes)
12
- * Added support for forwardrefs in route dependencies
31
+ * Support for more field attributes in `schema.had()` and `schema.didnt_have()`: `field_title_generator`, `fail_fast`, `coerce_numbers_to_str`, `union_mode`, `allow_mutation`, `pattern`, `discriminator`
32
+ * Support for forwardrefs in body fields (for example, when you use `from __future__ import annotations` in the file with your routes)
33
+ * Support for forwardrefs in route dependencies
13
34
 
14
35
  ## [4.5.0]
15
36
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cadwyn
3
- Version: 4.6.0a1
3
+ Version: 5.0.0a1
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
@@ -146,7 +146,10 @@ def _generate_field_ast(field: PydanticFieldWrapper) -> ast.Call:
146
146
  func=ast.Name("Field"),
147
147
  args=[],
148
148
  keywords=[
149
- ast.keyword(arg=attr, value=ast.parse(get_fancy_repr(attr_value), mode="eval").body)
149
+ ast.keyword(
150
+ arg=attr,
151
+ value=ast.parse(get_fancy_repr(attr_value), mode="eval").body,
152
+ )
150
153
  for attr, attr_value in field.passed_field_attributes.items()
151
154
  ],
152
155
  )
@@ -1,11 +1,12 @@
1
1
  import dataclasses
2
- import datetime
3
- from collections.abc import Callable, Coroutine, Sequence
2
+ import warnings
3
+ 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 Any, cast
7
+ from typing import TYPE_CHECKING, Annotated, Any, cast
8
8
 
9
+ import fastapi
9
10
  from fastapi import APIRouter, FastAPI, HTTPException, routing
10
11
  from fastapi.datastructures import Default
11
12
  from fastapi.openapi.docs import (
@@ -23,15 +24,26 @@ from starlette.requests import Request
23
24
  from starlette.responses import JSONResponse, Response
24
25
  from starlette.routing import BaseRoute, Route
25
26
  from starlette.types import Lifespan
26
- from typing_extensions import Self
27
+ from typing_extensions import Self, assert_never, deprecated
27
28
 
28
29
  from cadwyn._utils import same_definition_as_in
29
30
  from cadwyn.changelogs import CadwynChangelogResource, _generate_changelog
30
- from cadwyn.middleware import HeaderVersioningMiddleware, _get_api_version_dependency
31
+ from cadwyn.exceptions import CadwynStructureError
32
+ from cadwyn.middleware import (
33
+ APIVersionFormat,
34
+ APIVersionLocation,
35
+ HeaderVersionManager,
36
+ URLVersionManager,
37
+ VersionPickingMiddleware,
38
+ _generate_api_version_dependency,
39
+ )
31
40
  from cadwyn.route_generation import generate_versioned_routers
32
- from cadwyn.routing import _RootHeaderAPIRouter
41
+ from cadwyn.routing import _RootCadwynAPIRouter
33
42
  from cadwyn.structure import VersionBundle
34
43
 
44
+ if TYPE_CHECKING:
45
+ from cadwyn.structure.common import VersionType
46
+
35
47
  CURR_DIR = Path(__file__).resolve()
36
48
  logger = getLogger(__name__)
37
49
 
@@ -48,7 +60,18 @@ class Cadwyn(FastAPI):
48
60
  self,
49
61
  *,
50
62
  versions: VersionBundle,
51
- api_version_header_name: str = "x-api-version",
63
+ api_version_header_name: Annotated[
64
+ str | None,
65
+ deprecated(
66
+ "api_version_header_name is deprecated and will be removed in the future. "
67
+ "Use api_version_parameter_name instead."
68
+ ),
69
+ ] = None,
70
+ api_version_location: APIVersionLocation = "custom_header",
71
+ api_version_format: APIVersionFormat = "date",
72
+ api_version_parameter_name: str = "x-api-version",
73
+ api_version_default_value: str | None | Callable[[Request], Awaitable[str]] = None,
74
+ versioning_middleware_class: type[VersionPickingMiddleware] = VersionPickingMiddleware,
52
75
  changelog_url: str | None = "/changelog",
53
76
  include_changelog_url_in_schema: bool = True,
54
77
  debug: bool = False,
@@ -100,6 +123,15 @@ class Cadwyn(FastAPI):
100
123
  self._dependency_overrides_provider = FakeDependencyOverridesProvider({})
101
124
  self._cadwyn_initialized = False
102
125
 
126
+ if api_version_header_name is not None:
127
+ warnings.warn(
128
+ "api_version_header_name is deprecated and will be removed in the future. "
129
+ "Use api_version_parameter_name instead.",
130
+ DeprecationWarning,
131
+ stacklevel=2,
132
+ )
133
+ api_version_parameter_name = api_version_header_name
134
+
103
135
  super().__init__(
104
136
  debug=debug,
105
137
  title=title,
@@ -137,6 +169,18 @@ class Cadwyn(FastAPI):
137
169
  separate_input_output_schemas=separate_input_output_schemas,
138
170
  **extra,
139
171
  )
172
+
173
+ self._versioned_webhook_routers: dict[VersionType, APIRouter] = {}
174
+ self._latest_version_router = APIRouter(dependency_overrides_provider=self._dependency_overrides_provider)
175
+
176
+ self.changelog_url = changelog_url
177
+ self.include_changelog_url_in_schema = include_changelog_url_in_schema
178
+
179
+ self.docs_url = docs_url
180
+ self.redoc_url = redoc_url
181
+ self.openapi_url = openapi_url
182
+ self.redoc_url = redoc_url
183
+
140
184
  self._kwargs_to_router: dict[str, Any] = {
141
185
  "routes": routes,
142
186
  "redirect_slashes": redirect_slashes,
@@ -152,32 +196,48 @@ class Cadwyn(FastAPI):
152
196
  "responses": responses,
153
197
  "generate_unique_id_function": generate_unique_id_function,
154
198
  }
155
- self.router: _RootHeaderAPIRouter = _RootHeaderAPIRouter( # pyright: ignore[reportIncompatibleVariableOverride]
199
+ self.api_version_format = api_version_format
200
+ self.api_version_parameter_name = api_version_parameter_name
201
+ self.api_version_pythonic_parameter_name = api_version_parameter_name.replace("-", "_")
202
+ if api_version_location == "custom_header":
203
+ self._api_version_manager = HeaderVersionManager(api_version_parameter_name=api_version_parameter_name)
204
+ self._api_version_fastapi_depends_class = fastapi.Header
205
+ elif api_version_location == "path":
206
+ self._api_version_manager = URLVersionManager(possible_version_values=self.versions._version_values_set)
207
+ self._api_version_fastapi_depends_class = fastapi.Path
208
+ else:
209
+ assert_never(api_version_location)
210
+ # TODO: Add a test validating the error message when there are no versions
211
+ default_version_example = next(iter(self.versions._version_values_set))
212
+ if api_version_format == "date":
213
+ self.api_version_validation_data_type = date
214
+ elif api_version_format == "string":
215
+ self.api_version_validation_data_type = str
216
+ else:
217
+ assert_never(default_version_example)
218
+ self.router: _RootCadwynAPIRouter = _RootCadwynAPIRouter( # pyright: ignore[reportIncompatibleVariableOverride]
156
219
  **self._kwargs_to_router,
157
- api_version_header_name=api_version_header_name,
220
+ api_version_parameter_name=api_version_parameter_name,
158
221
  api_version_var=self.versions.api_version_var,
222
+ api_version_format=api_version_format,
159
223
  )
160
- self._versioned_webhook_routers: dict[date, APIRouter] = {}
161
- self._latest_version_router = APIRouter(dependency_overrides_provider=self._dependency_overrides_provider)
162
-
163
- self.changelog_url = changelog_url
164
- self.include_changelog_url_in_schema = include_changelog_url_in_schema
165
-
166
- self.docs_url = docs_url
167
- self.redoc_url = redoc_url
168
- self.openapi_url = openapi_url
169
- self.redoc_url = redoc_url
170
-
171
224
  unversioned_router = APIRouter(**self._kwargs_to_router)
172
225
  self._add_utility_endpoints(unversioned_router)
173
226
  self._add_default_versioned_routers()
174
227
  self.include_router(unversioned_router)
175
228
  self.add_middleware(
176
- HeaderVersioningMiddleware,
177
- api_version_header_name=self.router.api_version_header_name,
229
+ versioning_middleware_class,
230
+ api_version_parameter_name=api_version_parameter_name,
231
+ api_version_manager=self._api_version_manager,
232
+ api_version_default_value=api_version_default_value,
178
233
  api_version_var=self.versions.api_version_var,
179
- default_response_class=default_response_class,
180
234
  )
235
+ if self.api_version_format == "date" and (
236
+ sorted(self.versions.versions, key=lambda v: v.value, reverse=True) != list(self.versions.versions)
237
+ ):
238
+ raise CadwynStructureError(
239
+ "Versions are not sorted correctly. Please sort them in descending order.",
240
+ )
181
241
 
182
242
  @same_definition_as_in(FastAPI.__call__)
183
243
  async def __call__(self, scope: Any, receive: Any, send: Any) -> None:
@@ -193,7 +253,7 @@ class Cadwyn(FastAPI):
193
253
  versions=self.versions,
194
254
  )
195
255
  for version, router in generated_routers.endpoints.items():
196
- self.add_header_versioned_routers(router, header_value=version.isoformat())
256
+ self._add_versioned_routers(router, version=version)
197
257
 
198
258
  for version, router in generated_routers.webhooks.items():
199
259
  self._versioned_webhook_routers[version] = router
@@ -267,26 +327,19 @@ class Cadwyn(FastAPI):
267
327
  self._latest_version_router.include_router(router)
268
328
 
269
329
  async def openapi_jsons(self, req: Request) -> JSONResponse:
270
- raw_version = req.query_params.get("version") or req.headers.get(self.router.api_version_header_name)
271
- not_found_error = HTTPException(
272
- status_code=404,
273
- detail=f"OpenApi file of with version `{raw_version}` not found",
274
- )
275
- try:
276
- version = datetime.date.fromisoformat(raw_version) # pyright: ignore[reportArgumentType]
277
- # TypeError when raw_version is None
278
- # ValueError when raw_version is of the non-iso format
279
- except (ValueError, TypeError):
280
- version = raw_version
330
+ version = req.query_params.get("version") or req.headers.get(self.router.api_version_parameter_name)
281
331
 
282
- if isinstance(version, date) and version in self.router.versioned_routers:
332
+ if version in self.router.versioned_routers:
283
333
  routes = self.router.versioned_routers[version].routes
284
- formatted_version = version.isoformat()
334
+ formatted_version = version
285
335
  elif version == "unversioned" and self._there_are_public_unversioned_routes():
286
336
  routes = self.router.unversioned_routes
287
337
  formatted_version = "unversioned"
288
338
  else:
289
- raise not_found_error
339
+ raise HTTPException(
340
+ status_code=404,
341
+ detail=f"OpenApi file of with version `{version}` not found",
342
+ )
290
343
 
291
344
  # Add root path to servers when mounted as sub-app or proxy is used
292
345
  urls = (server_data.get("url") for server_data in self.servers)
@@ -296,7 +349,7 @@ class Cadwyn(FastAPI):
296
349
  self.servers.insert(0, {"url": root_path})
297
350
 
298
351
  webhook_routes = None
299
- if isinstance(version, date) and version in self._versioned_webhook_routers:
352
+ if version in self._versioned_webhook_routers:
300
353
  webhook_routes = self._versioned_webhook_routers[version].routes
301
354
 
302
355
  return JSONResponse(
@@ -354,7 +407,7 @@ class Cadwyn(FastAPI):
354
407
  base_host = str(req.base_url).rstrip("/")
355
408
  root_path = self._extract_root_path(req)
356
409
  base_url = base_host + root_path
357
- table = {version: f"{base_url}{docs_url}?version={version}" for version in self.router.sorted_versions}
410
+ table = {version: f"{base_url}{docs_url}?version={version}" for version in self.router.versions}
358
411
  if self._there_are_public_unversioned_routes():
359
412
  table |= {"unversioned": f"{base_url}{docs_url}?version=unversioned"}
360
413
  return self._templates.TemplateResponse(
@@ -362,6 +415,7 @@ class Cadwyn(FastAPI):
362
415
  {"request": req, "table": table},
363
416
  )
364
417
 
418
+ @deprecated("Use generate_and_include_versioned_routers and VersionBundle versions instead")
365
419
  def add_header_versioned_routers(
366
420
  self,
367
421
  first_router: APIRouter,
@@ -370,15 +424,20 @@ class Cadwyn(FastAPI):
370
424
  ) -> list[BaseRoute]:
371
425
  """Add all routes from routers to be routed using header_value and return the added routes"""
372
426
  try:
373
- header_value_as_dt = date.fromisoformat(header_value)
427
+ date.fromisoformat(header_value)
374
428
  except ValueError as e:
375
429
  raise ValueError("header_value should be in ISO 8601 format") from e
376
430
 
431
+ return self._add_versioned_routers(first_router, *other_routers, version=header_value)
432
+
433
+ def _add_versioned_routers(
434
+ self, first_router: APIRouter, *other_routers: APIRouter, version: str
435
+ ) -> list[BaseRoute]:
377
436
  added_routes: list[BaseRoute] = []
378
- if header_value_as_dt not in self.router.versioned_routers: # pragma: no branch
379
- self.router.versioned_routers[header_value_as_dt] = APIRouter(**self._kwargs_to_router)
437
+ if version not in self.router.versioned_routers: # pragma: no branch
438
+ self.router.versioned_routers[version] = APIRouter(**self._kwargs_to_router)
380
439
 
381
- versioned_router = self.router.versioned_routers[header_value_as_dt]
440
+ versioned_router = self.router.versioned_routers[version]
382
441
  if self.openapi_url is not None: # pragma: no branch
383
442
  versioned_router.add_route(
384
443
  path=self.openapi_url,
@@ -389,9 +448,18 @@ class Cadwyn(FastAPI):
389
448
 
390
449
  added_route_count = 0
391
450
  for router in (first_router, *other_routers):
392
- self.router.versioned_routers[header_value_as_dt].include_router(
451
+ self.router.versioned_routers[version].include_router(
393
452
  router,
394
- dependencies=[Depends(_get_api_version_dependency(self.router.api_version_header_name, header_value))],
453
+ dependencies=[
454
+ Depends(
455
+ _generate_api_version_dependency(
456
+ api_version_pythonic_parameter_name=self.api_version_pythonic_parameter_name,
457
+ default_value=version,
458
+ fastapi_depends_class=self._api_version_fastapi_depends_class,
459
+ validation_data_type=self.api_version_validation_data_type,
460
+ )
461
+ )
462
+ ],
395
463
  )
396
464
  added_route_count += len(router.routes)
397
465
 
@@ -1,5 +1,4 @@
1
1
  import copy
2
- import datetime
3
2
  import sys
4
3
  from enum import auto
5
4
  from logging import getLogger
@@ -22,7 +21,7 @@ from pydantic import BaseModel, Field, RootModel
22
21
  from cadwyn._asts import GenericAliasUnion
23
22
  from cadwyn._utils import Sentinel
24
23
  from cadwyn.route_generation import _get_routes
25
- from cadwyn.routing import _RootHeaderAPIRouter
24
+ from cadwyn.routing import _RootCadwynAPIRouter
26
25
  from cadwyn.schema_generation import SchemaGenerator, _change_field_in_model, generate_versioned_models
27
26
  from cadwyn.structure.versions import PossibleInstructions, VersionBundle, VersionChange, VersionChangeWithSideEffects
28
27
 
@@ -62,15 +61,15 @@ def hidden(instruction_or_version_change: T) -> T:
62
61
  return instruction_or_version_change
63
62
 
64
63
 
65
- def _generate_changelog(versions: VersionBundle, router: _RootHeaderAPIRouter) -> "CadwynChangelogResource":
64
+ def _generate_changelog(versions: VersionBundle, router: _RootCadwynAPIRouter) -> "CadwynChangelogResource":
66
65
  changelog = CadwynChangelogResource()
67
66
  schema_generators = generate_versioned_models(versions)
68
67
  for version, older_version in zip(versions, versions.versions[1:], strict=False):
69
68
  routes_from_newer_version = router.versioned_routers[version.value].routes
70
69
  schemas_from_older_version = get_fields_from_routes(router.versioned_routers[older_version.value].routes)
71
70
  version_changelog = CadwynVersion(value=version.value)
72
- generator_from_newer_version = schema_generators[version.value.isoformat()]
73
- generator_from_older_version = schema_generators[older_version.value.isoformat()]
71
+ generator_from_newer_version = schema_generators[version.value]
72
+ generator_from_older_version = schema_generators[older_version.value]
74
73
  for version_change in version.changes:
75
74
  if version_change.is_hidden_from_changelog:
76
75
  continue
@@ -284,7 +283,7 @@ class CadwynChangelogResource(BaseModel):
284
283
 
285
284
 
286
285
  class CadwynVersion(BaseModel):
287
- value: datetime.date
286
+ value: str
288
287
  changes: "list[CadwynVersionChange]" = Field(default_factory=list)
289
288
 
290
289
 
@@ -1,5 +1,4 @@
1
1
  import json
2
- from datetime import date
3
2
  from typing import Any
4
3
 
5
4
  from fastapi.routing import APIRoute
@@ -14,12 +13,12 @@ class CadwynError(Exception):
14
13
 
15
14
 
16
15
  class CadwynHeadRequestValidationError(CadwynError):
17
- def __init__(self, errors: list[Any], body: Any, version: date) -> None:
16
+ def __init__(self, errors: list[Any], body: Any, version: str) -> None:
18
17
  self.errors = errors
19
18
  self.body = body
20
19
  self.version = version
21
20
  super().__init__(
22
- f"We failed to migrate the request with version={self.version!s}. "
21
+ f"We failed to migrate the request with version={self.version}. "
23
22
  "This means that there is some error in your migrations or schema structure that makes it impossible "
24
23
  "to migrate the request of that version to latest.\n"
25
24
  f"body={self.body}\n\nerrors={json.dumps(self.errors, indent=4, ensure_ascii=False)}"
@@ -0,0 +1,119 @@
1
+ # NOTE: It's OK that any_string might not be correctly sortable such as v10 vs v9.
2
+ # we can simply remove waterfalling from any_string api version style.
3
+
4
+ import inspect
5
+ import re
6
+ from collections.abc import Awaitable, Callable
7
+ from contextvars import ContextVar
8
+ from typing import Annotated, Any, Literal, Protocol
9
+
10
+ from fastapi import Request
11
+ from starlette.middleware.base import BaseHTTPMiddleware, DispatchFunction, RequestResponseEndpoint
12
+ from starlette.types import ASGIApp
13
+
14
+ from cadwyn.structure.common import VersionType
15
+
16
+
17
+ class VersionManager(Protocol):
18
+ def get(self, request: Request) -> str | None: ...
19
+
20
+
21
+ VersionValidatorC = Callable[[str], VersionType]
22
+ VersionDependencyFactoryC = Callable[[], Callable[..., Any]]
23
+
24
+ APIVersionLocation = Literal["custom_header", "path"]
25
+ APIVersionFormat = Literal["date", "string"]
26
+
27
+
28
+ class HeaderVersionManager:
29
+ __slots__ = ("api_version_parameter_name",)
30
+
31
+ def __init__(self, *, api_version_parameter_name: str) -> None:
32
+ super().__init__()
33
+ self.api_version_parameter_name = api_version_parameter_name
34
+
35
+ def get(self, request: Request) -> str | None:
36
+ return request.headers.get(self.api_version_parameter_name)
37
+
38
+
39
+ class URLVersionManager:
40
+ __slots__ = ("possible_version_values", "url_version_regex")
41
+
42
+ def __init__(self, *, possible_version_values: set[str]) -> None:
43
+ super().__init__()
44
+ self.possible_version_values = possible_version_values
45
+ self.url_version_regex = re.compile(f"/({'|'.join(re.escape(v) for v in possible_version_values)})/")
46
+
47
+ def get(self, request: Request) -> str | None:
48
+ if m := self.url_version_regex.search(request.url.path):
49
+ return m.group(1)
50
+ return None
51
+
52
+
53
+ def _generate_api_version_dependency(
54
+ *,
55
+ api_version_pythonic_parameter_name: str,
56
+ default_value: str,
57
+ fastapi_depends_class: Callable[..., Any],
58
+ validation_data_type: Any,
59
+ ):
60
+ def api_version_dependency(**kwargs: Any):
61
+ # TODO: What do I return?
62
+ return next(iter(kwargs.values()))
63
+
64
+ api_version_dependency.__signature__ = inspect.Signature(
65
+ parameters=[
66
+ inspect.Parameter(
67
+ api_version_pythonic_parameter_name,
68
+ inspect.Parameter.KEYWORD_ONLY,
69
+ annotation=Annotated[
70
+ validation_data_type, fastapi_depends_class(openapi_examples={"default": {"value": default_value}})
71
+ ],
72
+ ),
73
+ ],
74
+ )
75
+ return api_version_dependency
76
+
77
+
78
+ class VersionPickingMiddleware(BaseHTTPMiddleware):
79
+ def __init__(
80
+ self,
81
+ app: ASGIApp,
82
+ *,
83
+ api_version_parameter_name: str,
84
+ api_version_default_value: str | None | Callable[[Request], Awaitable[str]],
85
+ api_version_var: ContextVar[VersionType | None],
86
+ api_version_manager: VersionManager,
87
+ dispatch: DispatchFunction | None = None,
88
+ ) -> None:
89
+ super().__init__(app, dispatch)
90
+
91
+ self.api_version_parameter_name = api_version_parameter_name
92
+ self._api_version_manager = api_version_manager
93
+ self.api_version_var = api_version_var
94
+ self.api_version_default_value = api_version_default_value
95
+
96
+ async def dispatch(
97
+ self,
98
+ request: Request,
99
+ call_next: RequestResponseEndpoint,
100
+ ):
101
+ # We handle api version at middleware level because if we try to add a Dependency to all routes, it won't work:
102
+ # we use this header for routing so the user will simply get a 404 if the header is invalid.
103
+ api_version = self._api_version_manager.get(request)
104
+
105
+ if api_version is None:
106
+ if callable(self.api_version_default_value): # pragma: no cover # TODO
107
+ api_version = await self.api_version_default_value(request)
108
+ else:
109
+ api_version = self.api_version_default_value
110
+
111
+ self.api_version_var.set(api_version)
112
+ response = await call_next(request)
113
+
114
+ if api_version is not None:
115
+ # We return it because we will be returning the **matched** version, not the requested one.
116
+ # In date-based versioning with waterfalling, it makes sense.
117
+ response.headers[self.api_version_parameter_name] = api_version
118
+
119
+ return response
@@ -36,7 +36,7 @@ from cadwyn.schema_generation import (
36
36
  generate_versioned_models,
37
37
  )
38
38
  from cadwyn.structure import Version, VersionBundle
39
- from cadwyn.structure.common import Endpoint, VersionDate
39
+ from cadwyn.structure.common import Endpoint, VersionType
40
40
  from cadwyn.structure.endpoints import (
41
41
  EndpointDidntExistInstruction,
42
42
  EndpointExistedInstruction,
@@ -62,8 +62,8 @@ class _EndpointInfo:
62
62
 
63
63
  @dataclass(slots=True, frozen=True)
64
64
  class GeneratedRouters(Generic[_R, _WR]):
65
- endpoints: dict[VersionDate, _R]
66
- webhooks: dict[VersionDate, _WR]
65
+ endpoints: dict[VersionType, _R]
66
+ webhooks: dict[VersionType, _WR]
67
67
 
68
68
 
69
69
  def generate_versioned_routers(
@@ -125,8 +125,8 @@ class _EndpointTransformer(Generic[_R, _WR]):
125
125
  def transform(self) -> GeneratedRouters[_R, _WR]:
126
126
  router = copy_router(self.parent_router)
127
127
  webhook_router = copy_router(self.parent_webhooks_router)
128
- routers: dict[VersionDate, _R] = {}
129
- webhook_routers: dict[VersionDate, _WR] = {}
128
+ routers: dict[VersionType, _R] = {}
129
+ webhook_routers: dict[VersionType, _WR] = {}
130
130
 
131
131
  for version in self.versions:
132
132
  self.schema_generators[str(version.value)].annotation_transformer.migrate_router_to_version(router)