cadwyn 4.4.5__tar.gz → 4.6.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 (147) hide show
  1. {cadwyn-4.4.5 → cadwyn-4.6.0}/.github/actions/setup-python-uv/action.yaml +3 -3
  2. {cadwyn-4.4.5 → cadwyn-4.6.0}/.github/workflows/ci.yaml +41 -15
  3. {cadwyn-4.4.5 → cadwyn-4.6.0}/.github/workflows/daily_tests.yaml +2 -4
  4. {cadwyn-4.4.5 → cadwyn-4.6.0}/CHANGELOG.md +14 -0
  5. cadwyn-4.6.0/Makefile +11 -0
  6. {cadwyn-4.4.5 → cadwyn-4.6.0}/PKG-INFO +1 -1
  7. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/applications.py +2 -2
  8. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/route_generation.py +7 -0
  9. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/schema_generation.py +37 -5
  10. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/structure/data.py +17 -5
  11. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/structure/schemas.py +64 -15
  12. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/how_to/change_openapi_schemas/change_field_type.md +0 -1
  13. cadwyn-4.6.0/docs/how_to/change_openapi_schemas/change_schema_without_endpoint.md +17 -0
  14. cadwyn-4.6.0/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block001.py +22 -0
  15. cadwyn-4.6.0/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block002.py +22 -0
  16. cadwyn-4.6.0/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block001.py +15 -0
  17. cadwyn-4.6.0/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block002.py +14 -0
  18. cadwyn-4.6.0/docs_src/quickstart/tutorial/tests/test_block003.py +36 -0
  19. {cadwyn-4.4.5 → cadwyn-4.6.0}/mkdocs.yml +1 -0
  20. {cadwyn-4.4.5 → cadwyn-4.6.0}/pyproject.toml +6 -2
  21. {cadwyn-4.4.5 → cadwyn-4.6.0}/ruff.toml +3 -0
  22. cadwyn-4.6.0/tests/_resources/render/complex/__init__.py +0 -0
  23. cadwyn-4.6.0/tests/_resources/versioned_app/__init__.py +0 -0
  24. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_cli.py +2 -10
  25. cadwyn-4.6.0/tests/test_router_generation_with_from_future_annotations.py +62 -0
  26. cadwyn-4.6.0/tests/test_schema_generation/__init__.py +0 -0
  27. cadwyn-4.6.0/tests/tutorial/__init__.py +0 -0
  28. cadwyn-4.6.0/tox.ini +61 -0
  29. {cadwyn-4.4.5 → cadwyn-4.6.0}/uv.lock +409 -377
  30. cadwyn-4.4.5/.github/workflows/validate_links.yaml +0 -41
  31. cadwyn-4.4.5/Makefile +0 -14
  32. cadwyn-4.4.5/docs_src/quickstart/tutorial/tests/test_block003.py +0 -21
  33. {cadwyn-4.4.5 → cadwyn-4.6.0}/.github/CODE_OF_CONDUCT.md +0 -0
  34. {cadwyn-4.4.5 → cadwyn-4.6.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  35. {cadwyn-4.4.5 → cadwyn-4.6.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  36. {cadwyn-4.4.5 → cadwyn-4.6.0}/.github/workflows/publish_docs.yaml +0 -0
  37. {cadwyn-4.4.5 → cadwyn-4.6.0}/.github/workflows/release.yaml +0 -0
  38. {cadwyn-4.4.5 → cadwyn-4.6.0}/.gitignore +0 -0
  39. {cadwyn-4.4.5 → cadwyn-4.6.0}/.pre-commit-config.yaml +0 -0
  40. {cadwyn-4.4.5 → cadwyn-4.6.0}/LICENSE +0 -0
  41. {cadwyn-4.4.5 → cadwyn-4.6.0}/README.md +0 -0
  42. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/__init__.py +0 -0
  43. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/__main__.py +0 -0
  44. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/_asts.py +0 -0
  45. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/_importer.py +0 -0
  46. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/_render.py +0 -0
  47. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/_utils.py +0 -0
  48. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/changelogs.py +0 -0
  49. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/exceptions.py +0 -0
  50. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/middleware.py +0 -0
  51. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/py.typed +0 -0
  52. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/routing.py +0 -0
  53. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/static/__init__.py +0 -0
  54. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/static/docs.html +0 -0
  55. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/structure/__init__.py +0 -0
  56. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/structure/common.py +0 -0
  57. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/structure/endpoints.py +0 -0
  58. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/structure/enums.py +0 -0
  59. {cadwyn-4.4.5 → cadwyn-4.6.0}/cadwyn/structure/versions.py +0 -0
  60. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/CNAME +0 -0
  61. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/__init__.py +0 -0
  62. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/api_version_header_and_context_variables.md +0 -0
  63. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/beware_of_data_versioning.md +0 -0
  64. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/changelogs.md +0 -0
  65. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/cli.md +0 -0
  66. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/endpoint_migrations.md +0 -0
  67. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/enum_migrations.md +0 -0
  68. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/index.md +0 -0
  69. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/main_app.md +0 -0
  70. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/methodology.md +0 -0
  71. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/schema_generation.md +0 -0
  72. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/schema_migrations.md +0 -0
  73. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/testing.md +0 -0
  74. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/concepts/version_changes.md +0 -0
  75. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/home/CONTRIBUTING.md +0 -0
  76. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/how_to/change_business_logic/index.md +0 -0
  77. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/how_to/change_endpoints/index.md +0 -0
  78. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/how_to/change_openapi_schemas/add_field.md +0 -0
  79. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/how_to/change_openapi_schemas/changing_constraints.md +0 -0
  80. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/how_to/change_openapi_schemas/remove_field.md +0 -0
  81. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/how_to/change_openapi_schemas/rename_a_field_in_schema.md +0 -0
  82. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/how_to/index.md +0 -0
  83. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/img/dashboard_with_one_version.png +0 -0
  84. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/img/dashboard_with_two_versions.png +0 -0
  85. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/img/get_users_endpoint_from_prior_version.png +0 -0
  86. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/img/simplified_migration_model.png +0 -0
  87. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/img/sponsor_logos/monite.png +0 -0
  88. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/img/unversioned_dashboard.png +0 -0
  89. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/index.md +0 -0
  90. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/plugin.py +0 -0
  91. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/quickstart/setup.md +0 -0
  92. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/quickstart/tutorial.md +0 -0
  93. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/theory/how_to_build_versioning_framework.md +0 -0
  94. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/theory/how_we_got_here.md +0 -0
  95. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs/theory/literature.md +0 -0
  96. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs_src/__init__.py +0 -0
  97. {cadwyn-4.4.5/docs_src/quickstart → cadwyn-4.6.0/docs_src/how_to}/__init__.py +0 -0
  98. {cadwyn-4.4.5/docs_src/quickstart/setup → cadwyn-4.6.0/docs_src/how_to/change_openapi_schemas}/__init__.py +0 -0
  99. {cadwyn-4.4.5/docs_src/quickstart/setup/tests → cadwyn-4.6.0/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint}/__init__.py +0 -0
  100. {cadwyn-4.4.5/docs_src/quickstart/tutorial → cadwyn-4.6.0/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests}/__init__.py +0 -0
  101. {cadwyn-4.4.5/docs_src/quickstart/tutorial/tests → cadwyn-4.6.0/docs_src/quickstart}/__init__.py +0 -0
  102. {cadwyn-4.4.5/tests/_data → cadwyn-4.6.0/docs_src/quickstart/setup}/__init__.py +0 -0
  103. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs_src/quickstart/setup/block001.sh +0 -0
  104. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs_src/quickstart/setup/block002.py +0 -0
  105. {cadwyn-4.4.5/tests/_resources → cadwyn-4.6.0/docs_src/quickstart/setup/tests}/__init__.py +0 -0
  106. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs_src/quickstart/setup/tests/test_block002.py +0 -0
  107. {cadwyn-4.4.5/tests/_resources/render → cadwyn-4.6.0/docs_src/quickstart/tutorial}/__init__.py +0 -0
  108. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs_src/quickstart/tutorial/block001.py +0 -0
  109. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs_src/quickstart/tutorial/block002.py +0 -0
  110. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs_src/quickstart/tutorial/block003.py +0 -0
  111. {cadwyn-4.4.5/tests/_resources/render/complex → cadwyn-4.6.0/docs_src/quickstart/tutorial/tests}/__init__.py +0 -0
  112. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs_src/quickstart/tutorial/tests/test_block001.py +0 -0
  113. {cadwyn-4.4.5 → cadwyn-4.6.0}/docs_src/quickstart/tutorial/tests/test_block002.py +0 -0
  114. {cadwyn-4.4.5 → cadwyn-4.6.0}/scripts/fix_links.py +0 -0
  115. {cadwyn-4.4.5 → cadwyn-4.6.0}/scripts/split_md.py +0 -0
  116. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/__init__.py +0 -0
  117. {cadwyn-4.4.5/tests/_resources/versioned_app → cadwyn-4.6.0/tests/_data}/__init__.py +0 -0
  118. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_data/unversioned_schema_dir/__init__.py +0 -0
  119. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_data/unversioned_schema_dir/unversioned_schemas.py +0 -0
  120. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_data/unversioned_schemas.py +0 -0
  121. {cadwyn-4.4.5/tests/test_schema_generation → cadwyn-4.6.0/tests/_resources}/__init__.py +0 -0
  122. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_resources/app_for_testing_routing.py +0 -0
  123. {cadwyn-4.4.5/tests/tutorial → cadwyn-4.6.0/tests/_resources/render}/__init__.py +0 -0
  124. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_resources/render/classes.py +0 -0
  125. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_resources/render/complex/classes.py +0 -0
  126. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_resources/render/complex/versions.py +0 -0
  127. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_resources/render/versions.py +0 -0
  128. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_resources/utils.py +0 -0
  129. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_resources/versioned_app/app.py +0 -0
  130. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_resources/versioned_app/v2021_01_01.py +0 -0
  131. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_resources/versioned_app/v2022_01_02.py +0 -0
  132. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/_resources/versioned_app/webhooks.py +0 -0
  133. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/conftest.py +0 -0
  134. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_applications.py +0 -0
  135. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_auth_dependencies.py +0 -0
  136. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_changelog.py +0 -0
  137. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_data_migrations.py +0 -0
  138. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_render.py +0 -0
  139. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_router_generation.py +0 -0
  140. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_routing.py +0 -0
  141. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_schema_generation/test_enum.py +0 -0
  142. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_schema_generation/test_schema.py +0 -0
  143. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_schema_generation/test_schema_field.py +0 -0
  144. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_schema_generation/test_schema_validator.py +0 -0
  145. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/test_structure.py +0 -0
  146. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/tutorial/main.py +0 -0
  147. {cadwyn-4.4.5 → cadwyn-4.6.0}/tests/tutorial/test_example.py +0 -0
@@ -8,7 +8,7 @@ inputs:
8
8
  uv-version:
9
9
  description: "The uv version to set up"
10
10
  required: true
11
- default: "0.4.18"
11
+ default: "0.5.8"
12
12
 
13
13
  runs:
14
14
  using: "composite"
@@ -16,12 +16,12 @@ runs:
16
16
  - uses: actions/setup-python@v5
17
17
  with:
18
18
  python-version: ${{ inputs.python-version }}
19
- - uses: astral-sh/setup-uv@v3
19
+ - uses: astral-sh/setup-uv@v5
20
20
  with:
21
21
  version: ${{ inputs.uv-version }}
22
22
  enable-cache: true
23
23
  cache-dependency-glob: "uv.lock"
24
24
  - run: |
25
25
  uv sync --frozen --all-extras --dev
26
- echo "$(pwd)/.venv/bin" >> $GITHUB_PATH
26
+ uv pip install --system tox tox-uv
27
27
  shell: bash
@@ -10,7 +10,7 @@ on:
10
10
  branches: [main, 3.x.x]
11
11
  types: [opened, synchronize]
12
12
  paths:
13
- - ".github/workflows/ci.yaml" # self
13
+ - ".github/workflows/ci.yaml" # self
14
14
  - "**.py"
15
15
  - "**.toml"
16
16
  - "**.lock"
@@ -41,13 +41,13 @@ jobs:
41
41
  - uses: ./.github/actions/setup-python-uv
42
42
  with:
43
43
  python-version: ${{ matrix.python-version }}
44
- - run: uv run coverage run --source=. --parallel-mode -m pytest tests
44
+ - run: tox run -e py
45
45
  - name: Upload coverage results
46
46
  uses: actions/upload-artifact@v4
47
47
  if: matrix.os == 'ubuntu-latest' # Cross-platform coverage combination doesn't work
48
48
  with:
49
49
  name: coverage-results-${{ matrix.python-version }}
50
- path: coverage/
50
+ path: .coverage*
51
51
  Tutorial-tests:
52
52
  runs-on: ubuntu-latest
53
53
  name: Tutorial tests
@@ -58,13 +58,15 @@ jobs:
58
58
  python-version: "3.10"
59
59
  - name: Install cadwyn with instructions from docs
60
60
  run: sh docs_src/quickstart/setup/block001.sh
61
- - run: pip install pytest coverage dirty-equals
62
- - run: coverage run --source=. --parallel-mode -m pytest docs_src
61
+ - run: |
62
+ pip install uv
63
+ uv pip install --system pytest coverage dirty-equals
64
+ coverage run -m pytest docs_src
63
65
  - name: Upload coverage results
64
66
  uses: actions/upload-artifact@v4
65
67
  with:
66
68
  name: coverage-results-docs
67
- path: coverage/
69
+ path: .coverage*
68
70
  Coverage:
69
71
  needs: [Tests, Tutorial-tests]
70
72
  runs-on: ubuntu-latest
@@ -75,32 +77,56 @@ jobs:
75
77
  with:
76
78
  pattern: coverage-results-*
77
79
  merge-multiple: true
78
- path: coverage/
80
+ path: .
79
81
  - uses: actions/setup-python@v5
80
82
  with:
81
83
  python-version: "3.10"
82
- - run: pip install 'coverage[toml]'
83
- - run: coverage combine
84
- - run: coverage xml
84
+ - run: |
85
+ pip install uv
86
+ uv pip install --system tox tox-uv
87
+ tox run -e coverage_report-ci
85
88
  - name: Upload to Codecov
86
89
  uses: codecov/codecov-action@v4
87
90
  env:
88
91
  fail_ci_if_error: true
89
92
  CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
90
- - run: coverage report --fail-under=100 --show-missing
91
93
 
92
94
  Lint:
93
95
  runs-on: ubuntu-latest
94
96
  steps:
95
97
  - uses: actions/checkout@v4
96
- - uses: pre-commit/action@v3.0.0
98
+ - uses: actions/setup-python@v5
99
+ with:
100
+ python-version: "3.10"
101
+ - run: |
102
+ python -m pip install uv
103
+ uv pip install --system pre-commit pre-commit-uv
104
+ pre-commit run --all-files
97
105
 
98
- Typecheck:
106
+ Validate-links:
99
107
  runs-on: ubuntu-latest
100
108
  steps:
101
109
  - uses: actions/checkout@v4
102
110
  - uses: ./.github/actions/setup-python-uv
111
+ with:
112
+ # When this version is updated,
113
+ # update the pyright `base_python` version in `tox.ini`, too.
114
+ python-version: "3.10"
115
+ - run: tox run -e docs
116
+ - name: Validate links
117
+ uses: umbrelladocs/action-linkspector@v1
118
+ with:
119
+ reporter: github-pr-review
120
+ filter_mode: diff_context
121
+ fail_level: any
103
122
 
104
- - uses: jakebailey/pyright-action@v1
123
+ Typecheck:
124
+ runs-on: ubuntu-latest
125
+ steps:
126
+ - uses: actions/checkout@v4
127
+ - uses: ./.github/actions/setup-python-uv
105
128
  with:
106
- pylance-version: latest-release
129
+ # When this version is updated,
130
+ # update the pyright `base_python` version in `tox.ini`, too.
131
+ python-version: "3.10"
132
+ - run: tox run -e pyright
@@ -21,10 +21,8 @@ jobs:
21
21
  with:
22
22
  python-version: ${{ matrix.python-version }}
23
23
  - run: uv sync --refresh --all-extras --dev --upgrade
24
- - run: pytest .
25
- - uses: jakebailey/pyright-action@v1
26
- with:
27
- pylance-version: latest-release
24
+ - run: uv run pytest . # We intentionally don't use tox here to run tests "as is"
25
+ - run: uv run pyright --version && uv run pyright .
28
26
 
29
27
  notify-on-failure:
30
28
  name: Notify on failure
@@ -5,6 +5,20 @@ Please follow [the Keep a Changelog standard](https://keepachangelog.com/en/1.0.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [4.6.0]
9
+
10
+ ### Added
11
+
12
+ * 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`
13
+ * Support for forwardrefs in body fields (for example, when you use `from __future__ import annotations` in the file with your routes)
14
+ * Support for forwardrefs in route dependencies
15
+
16
+ ## [4.5.0]
17
+
18
+ ### Added
19
+
20
+ * `check_usage` argument to request/response by schema converters. Cadwyn always checks whether a schema mentioned in a converter applies to one or more endpoints to guarantee that the converter will apply to at least one endpoint. Sometimes, however, you do not need this validation. For example, when you use these converters for converting webhook bodies. Setting `check_usage=False` makes it possible to skip the validation
21
+
8
22
  ## [4.4.5]
9
23
 
10
24
  ### Fixed
cadwyn-4.6.0/Makefile ADDED
@@ -0,0 +1,11 @@
1
+ SHELL := /bin/bash
2
+ py_warn = PYTHONDEVMODE=1
3
+
4
+ install:
5
+ uv sync --all-extras --dev
6
+
7
+ lint:
8
+ pre-commit run --all-files
9
+
10
+ test:
11
+ uv run --with tox --with tox-uv tox
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cadwyn
3
- Version: 4.4.5
3
+ Version: 4.6.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
@@ -279,7 +279,7 @@ class Cadwyn(FastAPI):
279
279
  except (ValueError, TypeError):
280
280
  version = raw_version
281
281
 
282
- if version in self.router.versioned_routers:
282
+ if isinstance(version, date) and version in self.router.versioned_routers:
283
283
  routes = self.router.versioned_routers[version].routes
284
284
  formatted_version = version.isoformat()
285
285
  elif version == "unversioned" and self._there_are_public_unversioned_routes():
@@ -296,7 +296,7 @@ class Cadwyn(FastAPI):
296
296
  self.servers.insert(0, {"url": root_path})
297
297
 
298
298
  webhook_routes = None
299
- if version in self._versioned_webhook_routers:
299
+ if isinstance(version, date) and version in self._versioned_webhook_routers:
300
300
  webhook_routes = self._versioned_webhook_routers[version].routes
301
301
 
302
302
  return JSONResponse(
@@ -221,6 +221,8 @@ class _EndpointTransformer(Generic[_R, _WR]):
221
221
 
222
222
  for by_schema_converters in version_change.alter_request_by_schema_instructions.values():
223
223
  for by_schema_converter in by_schema_converters:
224
+ if not by_schema_converter.check_usage: # pragma: no cover
225
+ continue
224
226
  missing_models = set(by_schema_converter.schemas) - head_request_bodies
225
227
  if missing_models:
226
228
  raise RouteRequestBySchemaConverterDoesNotApplyToAnythingError(
@@ -232,6 +234,8 @@ class _EndpointTransformer(Generic[_R, _WR]):
232
234
  )
233
235
  for by_schema_converters in version_change.alter_response_by_schema_instructions.values():
234
236
  for by_schema_converter in by_schema_converters:
237
+ if not by_schema_converter.check_usage: # pragma: no cover
238
+ continue
235
239
  missing_models = set(by_schema_converter.schemas) - head_response_models
236
240
  if missing_models:
237
241
  raise RouteResponseBySchemaConverterDoesNotApplyToAnythingError(
@@ -240,6 +244,9 @@ class _EndpointTransformer(Generic[_R, _WR]):
240
244
  f"failed to find routes with the following response models: "
241
245
  f"{[m.__name__ for m in missing_models]}. "
242
246
  f"This means that you are trying to apply this converter to non-existing endpoint(s). "
247
+ "If this is intentional and this converter really does not apply to any endpoints, then "
248
+ "pass check_usage=False argument to "
249
+ f"{version_change.__name__}.{by_schema_converter.transformer.__name__}"
243
250
  )
244
251
 
245
252
  def _extract_all_routes_identifiers(
@@ -40,6 +40,7 @@ from pydantic._internal._decorators import (
40
40
  RootValidatorDecoratorInfo,
41
41
  ValidatorDecoratorInfo,
42
42
  )
43
+ from pydantic._internal._typing_extra import try_eval_type as pydantic_try_eval_type
43
44
  from pydantic.fields import ComputedFieldInfo, FieldInfo
44
45
  from typing_extensions import Doc, Self, _AnnotatedAlias, assert_never
45
46
 
@@ -158,7 +159,7 @@ def migrate_response_body(
158
159
  *,
159
160
  latest_body: Any,
160
161
  version: VersionDate | str,
161
- ):
162
+ ) -> Any:
162
163
  """Convert the data to a specific version
163
164
 
164
165
  Apply all version changes from latest until the passed version in reverse order
@@ -231,6 +232,12 @@ def _is_dunder(attr_name: str):
231
232
 
232
233
 
233
234
  def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticModelWrapper[_T_PYDANTIC_MODEL]":
235
+ # In case we have a forwardref within one of the fields
236
+ # For example, when "from __future__ import annotations" is used in the file with the schema
237
+ if model is not BaseModel:
238
+ model.model_rebuild(raise_errors=False)
239
+ model = cast(type[_T_PYDANTIC_MODEL], model)
240
+
234
241
  decorators = _get_model_decorators(model)
235
242
  validators = {}
236
243
  for decorator_wrapper in decorators:
@@ -239,8 +246,20 @@ def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticModelWrapp
239
246
 
240
247
  wrapped_validator = _wrap_validator(decorator_wrapper.func, decorator_wrapper.shim, decorator_wrapper.info)
241
248
  validators[decorator_wrapper.cls_var_name] = wrapped_validator
249
+
250
+ annotations = {
251
+ name: value
252
+ if not isinstance(value, str)
253
+ else model.model_fields[name].annotation or model.__annotations__[name]
254
+ for name, value in model.__annotations__.items()
255
+ }
256
+
242
257
  fields = {
243
- field_name: PydanticFieldWrapper(model.model_fields[field_name], model.__annotations__[field_name], field_name)
258
+ field_name: PydanticFieldWrapper(
259
+ model.model_fields[field_name],
260
+ annotations[field_name],
261
+ field_name,
262
+ )
244
263
  for field_name in model.__annotations__
245
264
  }
246
265
 
@@ -263,7 +282,7 @@ def _wrap_pydantic_model(model: type[_T_PYDANTIC_MODEL]) -> "_PydanticModelWrapp
263
282
  fields=fields,
264
283
  other_attributes=other_attributes,
265
284
  validators=validators,
266
- annotations=model.__annotations__.copy(),
285
+ annotations=annotations,
267
286
  )
268
287
 
269
288
 
@@ -355,6 +374,7 @@ class _PydanticModelWrapper(Generic[_T_PYDANTIC_MODEL]):
355
374
  if not validator.is_deleted and type(validator) == _ValidatorWrapper # noqa: E721
356
375
  }
357
376
  fields = {name: field.generate_field_copy(generator) for name, field in self.fields.items()}
377
+
358
378
  model_copy = type(self.cls)(
359
379
  self.name,
360
380
  tuple(generator[cast(type[BaseModel], base)] for base in self.cls.__bases__),
@@ -418,6 +438,7 @@ class _AnnotationTransformer:
418
438
  # because such copies could produce weird behaviors at runtime, especially if you/fastapi do any comparisons.
419
439
  # It's defined here and not on the method because of this: https://youtu.be/sVjtp6tGo0g
420
440
  self.generator = generator
441
+ # TODO: Rewrite this to memoize
421
442
  self.change_versions_of_a_non_container_annotation = functools.cache(
422
443
  self._change_version_of_a_non_container_annotation
423
444
  )
@@ -531,6 +552,9 @@ class _AnnotationTransformer:
531
552
  annotation_modifying_wrapper = annotation_modifying_wrapper_factory(call)
532
553
  old_params = inspect.signature(call).parameters
533
554
  callable_annotations = annotation_modifying_wrapper.__annotations__
555
+ callable_annotations = {
556
+ k: v if type(v) is not str else _try_eval_type(v, call.__globals__) for k, v in callable_annotations.items()
557
+ }
534
558
  annotation_modifying_wrapper.__annotations__ = modify_annotations(callable_annotations)
535
559
  annotation_modifying_wrapper.__defaults__ = modify_defaults(
536
560
  tuple(p.default for p in old_params.values() if p.default is not inspect.Signature.empty),
@@ -631,8 +655,6 @@ class SchemaGenerator:
631
655
 
632
656
  if model in self.concrete_models:
633
657
  return self.concrete_models[model]
634
- else:
635
- wrapper = self._get_wrapper_for_model(model)
636
658
 
637
659
  wrapper = self._get_wrapper_for_model(model)
638
660
  model_copy = wrapper.generate_model_copy(self)
@@ -655,6 +677,8 @@ class SchemaGenerator:
655
677
  return self.model_bundle.enums[model]
656
678
 
657
679
  if lenient_issubclass(model, BaseModel):
680
+ # TODO: My god, what if one of its fields is in our concrete schemas and we don't use it? :O
681
+ # TODO: Add an argument with our concrete schemas for _wrap_pydantic_model
658
682
  wrapper = _wrap_pydantic_model(model)
659
683
  self.model_bundle.schemas[model] = wrapper
660
684
  elif lenient_issubclass(model, Enum):
@@ -977,3 +1001,11 @@ class _EnumWrapper(Generic[_T_ENUM]):
977
1001
  and k not in _DummyEnum.__dict__
978
1002
  and (k not in mro_dict or mro_dict[k] is not v)
979
1003
  }
1004
+
1005
+
1006
+ def _try_eval_type(value: Any, globals: dict[str, Any]) -> Any:
1007
+ new_value, success = pydantic_try_eval_type(value, globals)
1008
+ if success:
1009
+ return new_value
1010
+ else: # pragma: no cover # Can't imagine when this would happen
1011
+ return value
@@ -86,6 +86,12 @@ class _AlterDataInstruction:
86
86
  return self.transformer(__request_or_response)
87
87
 
88
88
 
89
+ @dataclass
90
+ class _BaseAlterBySchemaInstruction:
91
+ schemas: tuple[Any, ...]
92
+ check_usage: bool = True
93
+
94
+
89
95
  ##########
90
96
  # Requests
91
97
  ##########
@@ -97,8 +103,7 @@ class _BaseAlterRequestInstruction(_AlterDataInstruction):
97
103
 
98
104
 
99
105
  @dataclass
100
- class _AlterRequestBySchemaInstruction(_BaseAlterRequestInstruction):
101
- schemas: tuple[Any, ...]
106
+ class _AlterRequestBySchemaInstruction(_BaseAlterBySchemaInstruction, _BaseAlterRequestInstruction): ...
102
107
 
103
108
 
104
109
  @dataclass
@@ -110,7 +115,10 @@ class _AlterRequestByPathInstruction(_BaseAlterRequestInstruction):
110
115
 
111
116
  @overload
112
117
  def convert_request_to_next_version_for(
113
- first_schema: type, /, *additional_schemas: type
118
+ first_schema: type,
119
+ /,
120
+ *additional_schemas: type,
121
+ check_usage: bool = True,
114
122
  ) -> "type[staticmethod[_P, None]]": ...
115
123
 
116
124
 
@@ -123,6 +131,7 @@ def convert_request_to_next_version_for(
123
131
  methods_or_second_schema: list[str] | None | type = None,
124
132
  /,
125
133
  *additional_schemas: type,
134
+ check_usage: bool = True,
126
135
  ) -> "type[staticmethod[_P, None]]":
127
136
  _validate_decorator_args(schema_or_path, methods_or_second_schema, additional_schemas)
128
137
 
@@ -141,6 +150,7 @@ def convert_request_to_next_version_for(
141
150
  return _AlterRequestBySchemaInstruction(
142
151
  schemas=schemas,
143
152
  transformer=transformer,
153
+ check_usage=check_usage,
144
154
  )
145
155
 
146
156
  return decorator # pyright: ignore[reportReturnType]
@@ -158,8 +168,7 @@ class _BaseAlterResponseInstruction(_AlterDataInstruction):
158
168
 
159
169
 
160
170
  @dataclass
161
- class _AlterResponseBySchemaInstruction(_BaseAlterResponseInstruction):
162
- schemas: tuple[Any, ...]
171
+ class _AlterResponseBySchemaInstruction(_BaseAlterBySchemaInstruction, _BaseAlterResponseInstruction): ...
163
172
 
164
173
 
165
174
  @dataclass
@@ -175,6 +184,7 @@ def convert_response_to_previous_version_for(
175
184
  /,
176
185
  *schemas: type,
177
186
  migrate_http_errors: bool = False,
187
+ check_usage: bool = True,
178
188
  ) -> "type[staticmethod[_P, None]]": ...
179
189
 
180
190
 
@@ -194,6 +204,7 @@ def convert_response_to_previous_version_for(
194
204
  /,
195
205
  *additional_schemas: type,
196
206
  migrate_http_errors: bool = False,
207
+ check_usage: bool = True,
197
208
  ) -> "type[staticmethod[_P, None]]":
198
209
  _validate_decorator_args(schema_or_path, methods_or_second_schema, additional_schemas)
199
210
 
@@ -215,6 +226,7 @@ def convert_response_to_previous_version_for(
215
226
  schemas=schemas,
216
227
  transformer=transformer,
217
228
  migrate_http_errors=migrate_http_errors,
229
+ check_usage=check_usage,
218
230
  )
219
231
 
220
232
  return decorator # pyright: ignore[reportReturnType]
@@ -3,7 +3,7 @@ from dataclasses import dataclass
3
3
  from typing import TYPE_CHECKING, Any, Literal, cast
4
4
 
5
5
  from issubclass import issubclass as lenient_issubclass
6
- from pydantic import BaseModel, Field
6
+ from pydantic import AliasChoices, AliasPath, BaseModel, Field
7
7
  from pydantic._internal._decorators import PydanticDescriptorProxy, unwrap_wrapped_function
8
8
  from pydantic.fields import FieldInfo
9
9
 
@@ -18,58 +18,83 @@ if TYPE_CHECKING:
18
18
 
19
19
  PossibleFieldAttributes = Literal[
20
20
  "default",
21
- "default_factory",
22
21
  "alias",
22
+ "alias_priority",
23
+ "default_factory",
24
+ "validation_alias",
25
+ "serialization_alias",
23
26
  "title",
27
+ "field_title_generator",
24
28
  "description",
29
+ "examples",
25
30
  "exclude",
26
31
  "const",
32
+ "deprecated",
33
+ "frozen",
34
+ "validate_default",
35
+ "repr",
36
+ "init",
37
+ "init_var",
38
+ "kw_only",
39
+ "fail_fast",
27
40
  "gt",
28
41
  "ge",
29
42
  "lt",
30
43
  "le",
31
- "deprecated",
32
- "fail_fast",
33
44
  "strict",
45
+ "coerce_numbers_to_str",
34
46
  "multiple_of",
35
47
  "allow_inf_nan",
36
48
  "max_digits",
37
49
  "decimal_places",
38
50
  "min_length",
39
51
  "max_length",
52
+ "union_mode",
40
53
  "allow_mutation",
41
54
  "pattern",
42
55
  "discriminator",
43
- "repr",
44
56
  ]
45
57
 
46
58
 
59
+ # TODO: Add json_schema_extra as a breaking change in a major version
47
60
  @dataclass(slots=True)
48
61
  class FieldChanges:
49
62
  default: Any
63
+ alias: str | None
50
64
  default_factory: Any
51
- alias: str
52
- title: str
65
+ alias_priority: int | None
66
+ validation_alias: str | AliasPath | AliasChoices | None
67
+ serialization_alias: str | None
68
+ title: str | None
69
+ field_title_generator: Callable[[str, FieldInfo], str] | None
53
70
  description: str
71
+ examples: list[Any] | None
54
72
  exclude: "AbstractSetIntStr | MappingIntStrAny | Any"
55
73
  const: bool
56
74
  deprecated: bool
75
+ frozen: bool | None
76
+ validate_default: bool | None
77
+ repr: bool
78
+ init: bool | None
79
+ init_var: bool | None
80
+ kw_only: bool | None
57
81
  fail_fast: bool
58
82
  gt: float
59
83
  ge: float
60
84
  lt: float
61
85
  le: float
62
86
  strict: bool
87
+ coerce_numbers_to_str: bool | None
63
88
  multiple_of: float
64
89
  allow_inf_nan: bool
65
90
  max_digits: int
66
91
  decimal_places: int
67
92
  min_length: int
68
93
  max_length: int
94
+ union_mode: Literal["smart", "left_to_right"]
69
95
  allow_mutation: bool
70
96
  pattern: str
71
97
  discriminator: str
72
- repr: bool
73
98
 
74
99
 
75
100
  @dataclass(slots=True)
@@ -113,29 +138,41 @@ class AlterFieldInstructionFactory:
113
138
  name: str = Sentinel,
114
139
  type: Any = Sentinel,
115
140
  default: Any = Sentinel,
141
+ alias: str | None = Sentinel,
116
142
  default_factory: Callable = Sentinel,
117
- alias: str = Sentinel,
143
+ alias_priority: int = Sentinel,
144
+ validation_alias: str = Sentinel,
145
+ serialization_alias: str = Sentinel,
118
146
  title: str = Sentinel,
147
+ field_title_generator: Callable[[str, FieldInfo], str] = Sentinel,
119
148
  description: str = Sentinel,
149
+ examples: list[Any] = Sentinel,
120
150
  exclude: "AbstractSetIntStr | MappingIntStrAny | Any" = Sentinel,
121
151
  const: bool = Sentinel,
152
+ deprecated: bool = Sentinel,
153
+ frozen: bool = Sentinel,
154
+ validate_default: bool = Sentinel,
155
+ repr: bool = Sentinel,
156
+ init: bool = Sentinel,
157
+ init_var: bool = Sentinel,
158
+ kw_only: bool = Sentinel,
159
+ fail_fast: bool = Sentinel,
122
160
  gt: float = Sentinel,
123
161
  ge: float = Sentinel,
124
162
  lt: float = Sentinel,
125
163
  le: float = Sentinel,
126
164
  strict: bool = Sentinel,
127
- deprecated: bool = Sentinel,
165
+ coerce_numbers_to_str: bool = Sentinel,
128
166
  multiple_of: float = Sentinel,
129
167
  allow_inf_nan: bool = Sentinel,
130
168
  max_digits: int = Sentinel,
131
169
  decimal_places: int = Sentinel,
132
170
  min_length: int = Sentinel,
133
171
  max_length: int = Sentinel,
172
+ union_mode: Literal["smart", "left_to_right"] = Sentinel,
134
173
  allow_mutation: bool = Sentinel,
135
174
  pattern: str = Sentinel,
136
175
  discriminator: str = Sentinel,
137
- repr: bool = Sentinel,
138
- fail_fast: bool = Sentinel,
139
176
  ) -> FieldHadInstruction:
140
177
  return FieldHadInstruction(
141
178
  schema=self.schema,
@@ -145,28 +182,40 @@ class AlterFieldInstructionFactory:
145
182
  field_changes=FieldChanges(
146
183
  default=default,
147
184
  default_factory=default_factory,
185
+ alias_priority=alias_priority,
148
186
  alias=alias,
187
+ validation_alias=validation_alias,
188
+ serialization_alias=serialization_alias,
149
189
  title=title,
190
+ field_title_generator=field_title_generator,
150
191
  description=description,
192
+ examples=examples,
151
193
  exclude=exclude,
152
194
  const=const,
195
+ deprecated=deprecated,
196
+ frozen=frozen,
197
+ validate_default=validate_default,
198
+ repr=repr,
199
+ init=init,
200
+ init_var=init_var,
201
+ kw_only=kw_only,
202
+ fail_fast=fail_fast,
153
203
  gt=gt,
154
204
  ge=ge,
155
205
  lt=lt,
156
206
  le=le,
157
- deprecated=deprecated,
158
207
  strict=strict,
208
+ coerce_numbers_to_str=coerce_numbers_to_str,
159
209
  multiple_of=multiple_of,
160
210
  allow_inf_nan=allow_inf_nan,
161
211
  max_digits=max_digits,
162
212
  decimal_places=decimal_places,
163
213
  min_length=min_length,
164
214
  max_length=max_length,
215
+ union_mode=union_mode,
165
216
  allow_mutation=allow_mutation,
166
217
  pattern=pattern,
167
218
  discriminator=discriminator,
168
- repr=repr,
169
- fail_fast=fail_fast,
170
219
  ),
171
220
  )
172
221
 
@@ -58,7 +58,6 @@ Additional resources:
58
58
  * <https://github.com/OAI/OpenAPI-Specification/issues/1552>
59
59
  * <https://users.rust-lang.org/t/solved-is-adding-an-enum-variant-a-breaking-change/26721/5>
60
60
  * <https://github.com/graphql/graphql-js/issues/968>
61
- * <https://medium.com/@jakob.fiegerl/java-jackson-enum-de-serialization-with-rest-backward-compatibility-9c3ec85ac13d>
62
61
 
63
62
  In these sections, we'll be working with our user's response model: `users.UserResource`. Note that the main theme here is "Will I be able to serialize this change to any of my versions?" as any change to responses can make them incompatible with the data in your database.
64
63
 
@@ -0,0 +1,17 @@
1
+ # Change a schema that is not used in any endpoint
2
+
3
+ In some situations, we may want to use versioning not just for our openapi schemas and endpoints but also within our code such as when we want to send versioned webhooks to our clients.
4
+
5
+ For example, let's say we want to change the type of an "id" field from integer to string:
6
+
7
+ ```python
8
+ {!> ../docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block001.py !}
9
+ ```
10
+
11
+ Unless there is an endpoint that has `User` as its response_model, this code will end up causing an error when we run our Cadwyn app. This is because Cadwyn tries to make sure that all of your converters apply to at least one endpoint. Otherwise, it would be too easy for you to make a mistake when writing converters for the wrong schemas.
12
+
13
+ To avoid it, set `check_usage=False`:
14
+
15
+ ```python hl_lines="21"
16
+ {!> ../docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block002.py !}
17
+ ```
@@ -0,0 +1,22 @@
1
+ from pydantic import BaseModel
2
+
3
+ from cadwyn import ResponseInfo, VersionChange, convert_response_to_previous_version_for, schema
4
+
5
+
6
+ # User from latest version
7
+ class User(BaseModel):
8
+ id: str
9
+
10
+
11
+ class ChangeUserIDToString(VersionChange):
12
+ description = (
13
+ "Change users' ID field to a string to support any kind of ID. "
14
+ "Be careful: if you use a non-integer ID in a new version and "
15
+ "try to get it from the old version, the ID will be zero in response"
16
+ )
17
+ instructions_to_migrate_to_previous_version = [
18
+ schema(User).field("id").had(type=int),
19
+ ]
20
+
21
+ @convert_response_to_previous_version_for(User)
22
+ def change_id_to_int(response: ResponseInfo): ...