cadwyn 4.4.3__tar.gz → 4.4.5__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.
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/workflows/ci.yaml +10 -13
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/workflows/publish_docs.yaml +2 -2
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.pre-commit-config.yaml +4 -4
- {cadwyn-4.4.3 → cadwyn-4.4.5}/CHANGELOG.md +13 -1
- {cadwyn-4.4.3 → cadwyn-4.4.5}/PKG-INFO +2 -2
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/_asts.py +10 -8
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/_importer.py +1 -1
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/_utils.py +15 -1
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/changelogs.py +1 -1
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/route_generation.py +7 -7
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/routing.py +5 -10
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/schema_generation.py +28 -22
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/data.py +8 -8
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/endpoints.py +1 -1
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/schemas.py +1 -1
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/versions.py +2 -14
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/index.md +1 -1
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/main_app.md +1 -1
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/methodology.md +1 -1
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/version_changes.md +11 -8
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_openapi_schemas/add_field.md +1 -1
- cadwyn-4.4.5/docs/img/sponsor_logos/monite.png +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/plugin.py +1 -3
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/theory/how_we_got_here.md +2 -2
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/theory/literature.md +1 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/pyproject.toml +19 -1
- {cadwyn-4.4.3 → cadwyn-4.4.5}/ruff.toml +50 -18
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/app_for_testing_routing.py +5 -5
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/versioned_app/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/versioned_app/app.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/versioned_app/v2022_01_02.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/versioned_app/webhooks.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/conftest.py +0 -2
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_applications.py +2 -5
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_data_migrations.py +4 -6
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_router_generation.py +4 -6
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_routing.py +2 -2
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_schema_generation/test_schema_field.py +3 -4
- {cadwyn-4.4.3 → cadwyn-4.4.5}/uv.lock +365 -332
- cadwyn-4.4.3/docs/img/sponsor_logos/monite.png +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/CODE_OF_CONDUCT.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/actions/setup-python-uv/action.yaml +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/workflows/daily_tests.yaml +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/workflows/release.yaml +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/workflows/validate_links.yaml +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/.gitignore +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/LICENSE +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/Makefile +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/README.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/__init__.py +9 -9
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/__main__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/_render.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/applications.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/exceptions.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/middleware.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/py.typed +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/static/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/static/docs.html +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/__init__.py +7 -7
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/common.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/enums.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/CNAME +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/api_version_header_and_context_variables.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/beware_of_data_versioning.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/changelogs.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/cli.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/endpoint_migrations.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/enum_migrations.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/schema_generation.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/schema_migrations.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/testing.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/home/CONTRIBUTING.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_business_logic/index.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_endpoints/index.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_openapi_schemas/change_field_type.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_openapi_schemas/changing_constraints.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_openapi_schemas/remove_field.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_openapi_schemas/rename_a_field_in_schema.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/index.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/img/dashboard_with_one_version.png +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/img/dashboard_with_two_versions.png +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/img/get_users_endpoint_from_prior_version.png +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/img/simplified_migration_model.png +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/img/unversioned_dashboard.png +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/index.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/quickstart/setup.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/quickstart/tutorial.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/theory/how_to_build_versioning_framework.md +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/setup/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/setup/block001.sh +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/setup/block002.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/setup/tests/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/setup/tests/test_block002.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/block001.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/block002.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/block003.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/tests/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/tests/test_block001.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/tests/test_block002.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/tests/test_block003.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/mkdocs.yml +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/scripts/fix_links.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/scripts/split_md.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_data/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_data/unversioned_schema_dir/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_data/unversioned_schema_dir/unversioned_schemas.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_data/unversioned_schemas.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/classes.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/complex/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/complex/classes.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/complex/versions.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/versions.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/utils.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/versioned_app/v2021_01_01.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_auth_dependencies.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_changelog.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_cli.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_render.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_schema_generation/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_schema_generation/test_enum.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_schema_generation/test_schema.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_schema_generation/test_schema_validator.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_structure.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/tutorial/__init__.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/tutorial/main.py +0 -0
- {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/tutorial/test_example.py +0 -0
|
@@ -10,6 +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
14
|
- "**.py"
|
|
14
15
|
- "**.toml"
|
|
15
16
|
- "**.lock"
|
|
@@ -42,10 +43,10 @@ jobs:
|
|
|
42
43
|
python-version: ${{ matrix.python-version }}
|
|
43
44
|
- run: uv run coverage run --source=. --parallel-mode -m pytest tests
|
|
44
45
|
- name: Upload coverage results
|
|
45
|
-
uses: actions/upload-artifact@
|
|
46
|
+
uses: actions/upload-artifact@v4
|
|
46
47
|
if: matrix.os == 'ubuntu-latest' # Cross-platform coverage combination doesn't work
|
|
47
48
|
with:
|
|
48
|
-
name:
|
|
49
|
+
name: coverage-results-${{ matrix.python-version }}
|
|
49
50
|
path: coverage/
|
|
50
51
|
Tutorial-tests:
|
|
51
52
|
runs-on: ubuntu-latest
|
|
@@ -60,24 +61,20 @@ jobs:
|
|
|
60
61
|
- run: pip install pytest coverage dirty-equals
|
|
61
62
|
- run: coverage run --source=. --parallel-mode -m pytest docs_src
|
|
62
63
|
- name: Upload coverage results
|
|
63
|
-
uses: actions/upload-artifact@
|
|
64
|
+
uses: actions/upload-artifact@v4
|
|
64
65
|
with:
|
|
65
|
-
name:
|
|
66
|
+
name: coverage-results-docs
|
|
66
67
|
path: coverage/
|
|
67
68
|
Coverage:
|
|
68
69
|
needs: [Tests, Tutorial-tests]
|
|
69
70
|
runs-on: ubuntu-latest
|
|
70
71
|
steps:
|
|
71
72
|
- uses: actions/checkout@v4
|
|
72
|
-
- name: Download
|
|
73
|
-
uses: actions/download-artifact@
|
|
73
|
+
- name: Download coverage info
|
|
74
|
+
uses: actions/download-artifact@v4
|
|
74
75
|
with:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- name: Download docs tests coverage info
|
|
78
|
-
uses: actions/download-artifact@v3
|
|
79
|
-
with:
|
|
80
|
-
name: docs-tests-coverage-results
|
|
76
|
+
pattern: coverage-results-*
|
|
77
|
+
merge-multiple: true
|
|
81
78
|
path: coverage/
|
|
82
79
|
- uses: actions/setup-python@v5
|
|
83
80
|
with:
|
|
@@ -90,7 +87,7 @@ jobs:
|
|
|
90
87
|
env:
|
|
91
88
|
fail_ci_if_error: true
|
|
92
89
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
93
|
-
- run: coverage report --fail-under=100
|
|
90
|
+
- run: coverage report --fail-under=100 --show-missing
|
|
94
91
|
|
|
95
92
|
Lint:
|
|
96
93
|
runs-on: ubuntu-latest
|
|
@@ -14,11 +14,11 @@ jobs:
|
|
|
14
14
|
run: |
|
|
15
15
|
git config user.name github-actions[bot]
|
|
16
16
|
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
|
17
|
-
- uses: actions/setup-python@
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
18
|
with:
|
|
19
19
|
python-version: 3.x
|
|
20
20
|
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
|
21
|
-
- uses: actions/cache@
|
|
21
|
+
- uses: actions/cache@v4
|
|
22
22
|
with:
|
|
23
23
|
key: mkdocs-material-${{ env.cache_id }}
|
|
24
24
|
path: .cache
|
|
@@ -2,7 +2,7 @@ default_language_version:
|
|
|
2
2
|
python: python3.10
|
|
3
3
|
repos:
|
|
4
4
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
5
|
-
rev:
|
|
5
|
+
rev: v5.0.0
|
|
6
6
|
hooks:
|
|
7
7
|
- id: check-added-large-files
|
|
8
8
|
- id: check-yaml
|
|
@@ -18,14 +18,14 @@ repos:
|
|
|
18
18
|
- id: python-check-blanket-noqa
|
|
19
19
|
|
|
20
20
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
21
|
-
rev: v0.
|
|
21
|
+
rev: v0.8.1
|
|
22
22
|
hooks:
|
|
23
23
|
- id: ruff
|
|
24
24
|
args: [--fix, --exit-non-zero-on-fix]
|
|
25
25
|
- id: ruff-format
|
|
26
26
|
|
|
27
27
|
- repo: https://github.com/adamchainz/blacken-docs
|
|
28
|
-
rev: "1.
|
|
28
|
+
rev: "1.19.1" # replace with latest tag on GitHub
|
|
29
29
|
hooks:
|
|
30
30
|
- id: blacken-docs
|
|
31
31
|
additional_dependencies:
|
|
@@ -33,7 +33,7 @@ repos:
|
|
|
33
33
|
args: ["--line-length=80", "--target-version=py310", "--skip-errors"]
|
|
34
34
|
|
|
35
35
|
- repo: https://github.com/igorshubovych/markdownlint-cli
|
|
36
|
-
rev: v0.
|
|
36
|
+
rev: v0.43.0
|
|
37
37
|
hooks:
|
|
38
38
|
- id: markdownlint
|
|
39
39
|
args: ["--disable", "MD013"]
|
|
@@ -5,6 +5,18 @@ Please follow [the Keep a Changelog standard](https://keepachangelog.com/en/1.0.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.4.5]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* Fix invalid migration of pydantic v1 style root validators when pydantic erases information about their "skip_on_failure" attribute
|
|
13
|
+
|
|
14
|
+
## [4.4.4]
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* Type hints for newest pydantic versions
|
|
19
|
+
|
|
8
20
|
## [4.4.3]
|
|
9
21
|
|
|
10
22
|
### Changed
|
|
@@ -299,7 +311,7 @@ Versions 3.x.x are still supported in terms of bug and security fixes but all th
|
|
|
299
311
|
|
|
300
312
|
### Fixed
|
|
301
313
|
|
|
302
|
-
* When a class-based dependency from **fastapi** was used (anything security related), FastAPI had hardcoded `isinstance` checks for it which it used to enrich swagger with functionality. But when the dependencies were wrapped into our function wrappers, these checks stopped passing, thus breaking this functionality in swagger. Now we ignore all dependencies that FastAPI creates. This also introduces a hard-to-solve bug: if fastapi's class-based security dependency was subclassed and then `__call__` was
|
|
314
|
+
* When a class-based dependency from **fastapi** was used (anything security related), FastAPI had hardcoded `isinstance` checks for it which it used to enrich swagger with functionality. But when the dependencies were wrapped into our function wrappers, these checks stopped passing, thus breaking this functionality in swagger. Now we ignore all dependencies that FastAPI creates. This also introduces a hard-to-solve bug: if fastapi's class-based security dependency was subclassed and then `__call__` was overridden with new dependencies that are versioned -- we will not migrate them from version to version. I hope this is an extremely rare use case though. In fact, such use case breaks Liskov Substitution Principle and doesn't make much sense because security classes already include `request` parameter which means that no extra dependencies or parameters are necessary.
|
|
303
315
|
|
|
304
316
|
## [3.6.5]
|
|
305
317
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: cadwyn
|
|
3
|
-
Version: 4.4.
|
|
3
|
+
Version: 4.4.5
|
|
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
|
|
@@ -56,12 +56,13 @@ def get_fancy_repr(value: Any) -> Any:
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def transform_grouped_metadata(value: "annotated_types.GroupedMetadata"):
|
|
59
|
-
modified_fields = []
|
|
60
59
|
empty_obj = type(value)
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
modified_fields = [
|
|
62
|
+
(key, getattr(value, key))
|
|
63
|
+
for key in value.__dataclass_fields__ # pyright: ignore[reportAttributeAccessIssue]
|
|
64
|
+
if getattr(value, key) != getattr(empty_obj, key)
|
|
65
|
+
]
|
|
65
66
|
|
|
66
67
|
return PlainRepr(
|
|
67
68
|
value.__class__.__name__
|
|
@@ -120,11 +121,12 @@ def transform_other(value: Any) -> Any:
|
|
|
120
121
|
|
|
121
122
|
|
|
122
123
|
def _get_lambda_source_from_default_factory(source: str) -> str:
|
|
123
|
-
found_lambdas: list[ast.Lambda] = [
|
|
124
|
+
found_lambdas: list[ast.Lambda] = [
|
|
125
|
+
node.value
|
|
126
|
+
for node in ast.walk(ast.parse(source))
|
|
127
|
+
if isinstance(node, ast.keyword) and node.arg == "default_factory" and isinstance(node.value, ast.Lambda)
|
|
128
|
+
]
|
|
124
129
|
|
|
125
|
-
for node in ast.walk(ast.parse(source)):
|
|
126
|
-
if isinstance(node, ast.keyword) and node.arg == "default_factory" and isinstance(node.value, ast.Lambda):
|
|
127
|
-
found_lambdas.append(node.value)
|
|
128
130
|
if len(found_lambdas) == 1:
|
|
129
131
|
return ast.unparse(found_lambdas[0])
|
|
130
132
|
# These two errors are really hard to cover. Not sure if even possible, honestly :)
|
|
@@ -7,7 +7,7 @@ from cadwyn.exceptions import ImportFromStringError
|
|
|
7
7
|
def import_attribute_from_string(import_str: str) -> Any:
|
|
8
8
|
module_str, _, attrs_str = import_str.partition(":")
|
|
9
9
|
if not module_str or not attrs_str:
|
|
10
|
-
message = 'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
|
10
|
+
message = f'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
|
11
11
|
raise ImportFromStringError(message.format(import_str=import_str))
|
|
12
12
|
|
|
13
13
|
module = import_module_from_string(module_str)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
|
-
from typing import Any, Generic, TypeVar, Union
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union
|
|
3
3
|
|
|
4
4
|
from pydantic._internal._decorators import unwrap_wrapped_function
|
|
5
5
|
|
|
@@ -40,3 +40,17 @@ def fully_unwrap_decorator(func: Callable, is_pydantic_v1_style_validator: Any):
|
|
|
40
40
|
if is_pydantic_v1_style_validator and func.__closure__:
|
|
41
41
|
func = func.__closure__[0].cell_contents
|
|
42
42
|
return unwrap_wrapped_function(func)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
T = TypeVar("T", bound=type[object])
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
lenient_issubclass = issubclass
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
|
|
52
|
+
def lenient_issubclass(cls: type, other: T | tuple[T, ...]) -> bool:
|
|
53
|
+
try:
|
|
54
|
+
return issubclass(cls, other)
|
|
55
|
+
except TypeError: # pragma: no cover
|
|
56
|
+
return False
|
|
@@ -160,7 +160,7 @@ def _get_openapi_representation_of_a_field(model: type[BaseModel], field_name: s
|
|
|
160
160
|
|
|
161
161
|
model_name_map = get_compat_model_name_map([CadwynDummyModelForRepresentation.model_fields["my_field"]])
|
|
162
162
|
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
|
163
|
-
|
|
163
|
+
_, definitions = get_definitions(
|
|
164
164
|
fields=[ModelField(CadwynDummyModelForRepresentation.model_fields["my_field"], "my_field")],
|
|
165
165
|
schema_generator=schema_generator,
|
|
166
166
|
model_name_map=model_name_map,
|
|
@@ -179,13 +179,13 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
179
179
|
copy_of_dependant,
|
|
180
180
|
self.versions,
|
|
181
181
|
)
|
|
182
|
-
for
|
|
182
|
+
for router in routers.values():
|
|
183
183
|
router.routes = [
|
|
184
184
|
route
|
|
185
185
|
for route in router.routes
|
|
186
186
|
if not (isinstance(route, fastapi.routing.APIRoute) and _DELETED_ROUTE_TAG in route.tags)
|
|
187
187
|
]
|
|
188
|
-
for
|
|
188
|
+
for webhook_router in webhook_routers.values():
|
|
189
189
|
webhook_router.routes = [
|
|
190
190
|
route
|
|
191
191
|
for route in webhook_router.routes
|
|
@@ -466,18 +466,18 @@ def _get_routes(
|
|
|
466
466
|
*,
|
|
467
467
|
is_deleted: bool = False,
|
|
468
468
|
) -> list[fastapi.routing.APIRoute]:
|
|
469
|
-
found_routes = []
|
|
470
469
|
endpoint_path = endpoint_path.rstrip("/")
|
|
471
|
-
|
|
470
|
+
return [
|
|
471
|
+
route
|
|
472
|
+
for route in routes
|
|
472
473
|
if (
|
|
473
474
|
isinstance(route, fastapi.routing.APIRoute)
|
|
474
475
|
and route.path.rstrip("/") == endpoint_path
|
|
475
476
|
and set(route.methods).issubset(endpoint_methods)
|
|
476
477
|
and (endpoint_func_name is None or route.endpoint.__name__ == endpoint_func_name)
|
|
477
478
|
and (_DELETED_ROUTE_TAG in route.tags) == is_deleted
|
|
478
|
-
)
|
|
479
|
-
|
|
480
|
-
return found_routes
|
|
479
|
+
)
|
|
480
|
+
]
|
|
481
481
|
|
|
482
482
|
|
|
483
483
|
def _get_route_from_func(
|
|
@@ -23,9 +23,9 @@ _logger = getLogger(__name__)
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class _RootHeaderAPIRouter(APIRouter):
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
"""Root router of the FastAPI app when using header based versioning.
|
|
27
|
+
|
|
28
|
+
It will be used to route the requests to the correct versioned route
|
|
29
29
|
based on the headers. It also supports waterflowing the requests to the latest
|
|
30
30
|
version of the API if the request header doesn't match any of the versions.
|
|
31
31
|
|
|
@@ -88,9 +88,6 @@ class _RootHeaderAPIRouter(APIRouter):
|
|
|
88
88
|
return self.versioned_routers[version_chosen].routes
|
|
89
89
|
|
|
90
90
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
91
|
-
"""
|
|
92
|
-
The main entry point to the Router class.
|
|
93
|
-
"""
|
|
94
91
|
if "router" not in scope: # pragma: no cover
|
|
95
92
|
scope["router"] = self
|
|
96
93
|
|
|
@@ -131,10 +128,8 @@ class _RootHeaderAPIRouter(APIRouter):
|
|
|
131
128
|
self.unversioned_routes.append(self.routes[-1])
|
|
132
129
|
|
|
133
130
|
async def process_request(self, scope: Scope, receive: Receive, send: Send, routes: Sequence[BaseRoute]) -> None:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
but in this version self.routes were replaced with routes from the function arguments
|
|
137
|
-
"""
|
|
131
|
+
# It's a copy-paste from starlette.routing.Router
|
|
132
|
+
# but in this version self.routes were replaced with routes from the function arguments
|
|
138
133
|
|
|
139
134
|
partial = None
|
|
140
135
|
partial_scope = {}
|
|
@@ -30,7 +30,6 @@ import pydantic
|
|
|
30
30
|
import pydantic._internal._decorators
|
|
31
31
|
from fastapi import Response
|
|
32
32
|
from fastapi.routing import APIRoute
|
|
33
|
-
from issubclass import issubclass
|
|
34
33
|
from pydantic import BaseModel, Field, RootModel
|
|
35
34
|
from pydantic._internal import _decorators
|
|
36
35
|
from pydantic._internal._decorators import (
|
|
@@ -44,7 +43,7 @@ from pydantic._internal._decorators import (
|
|
|
44
43
|
from pydantic.fields import ComputedFieldInfo, FieldInfo
|
|
45
44
|
from typing_extensions import Doc, Self, _AnnotatedAlias, assert_never
|
|
46
45
|
|
|
47
|
-
from cadwyn._utils import Sentinel, UnionType, fully_unwrap_decorator
|
|
46
|
+
from cadwyn._utils import Sentinel, UnionType, fully_unwrap_decorator, lenient_issubclass
|
|
48
47
|
from cadwyn.exceptions import InvalidGenerationInstructionError
|
|
49
48
|
from cadwyn.structure.common import VersionDate
|
|
50
49
|
from cadwyn.structure.data import ResponseInfo
|
|
@@ -160,8 +159,10 @@ def migrate_response_body(
|
|
|
160
159
|
latest_body: Any,
|
|
161
160
|
version: VersionDate | str,
|
|
162
161
|
):
|
|
163
|
-
"""Convert the data to a specific version
|
|
164
|
-
|
|
162
|
+
"""Convert the data to a specific version
|
|
163
|
+
|
|
164
|
+
Apply all version changes from latest until the passed version in reverse order
|
|
165
|
+
and wrap the result in the correct version of latest_response_model
|
|
165
166
|
"""
|
|
166
167
|
if isinstance(version, str):
|
|
167
168
|
version = date.fromisoformat(version)
|
|
@@ -213,6 +214,10 @@ def _wrap_validator(func: Callable, is_pydantic_v1_style_validator: Any, decorat
|
|
|
213
214
|
# There's an inconsistency in their interfaces so we gotta resort to this
|
|
214
215
|
mode = kwargs.pop("mode", "after")
|
|
215
216
|
kwargs["pre"] = mode != "after"
|
|
217
|
+
if (
|
|
218
|
+
isinstance(decorator_info, RootValidatorDecoratorInfo) and decorator_info.mode == "after"
|
|
219
|
+
): # pragma: no cover # TODO
|
|
220
|
+
kwargs["skip_on_failure"] = True
|
|
216
221
|
if decorator_fields is not None:
|
|
217
222
|
return _PerFieldValidatorWrapper(
|
|
218
223
|
func=func, fields=list(decorator_fields), decorator=actual_decorator, kwargs=kwargs
|
|
@@ -271,7 +276,8 @@ class _PydanticModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
|
271
276
|
fields: Annotated[
|
|
272
277
|
dict["_FieldName", PydanticFieldWrapper],
|
|
273
278
|
Doc(
|
|
274
|
-
"Fields that belong to this model, not to its parents.
|
|
279
|
+
"Fields that belong to this model, not to its parents. "
|
|
280
|
+
"I.e. The ones that were either defined or overridden "
|
|
275
281
|
),
|
|
276
282
|
] = dataclasses.field(repr=False)
|
|
277
283
|
validators: dict[str, _PerFieldValidatorWrapper | _ValidatorWrapper] = dataclasses.field(repr=False)
|
|
@@ -316,7 +322,7 @@ class _PydanticModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
|
316
322
|
for base in self.cls.mro()[1:]:
|
|
317
323
|
if base in schemas:
|
|
318
324
|
parents.append(schemas[base])
|
|
319
|
-
elif
|
|
325
|
+
elif lenient_issubclass(base, BaseModel):
|
|
320
326
|
parents.append(_wrap_pydantic_model(base))
|
|
321
327
|
self._parents = parents
|
|
322
328
|
return parents
|
|
@@ -372,10 +378,9 @@ def is_regular_function(call: Callable):
|
|
|
372
378
|
|
|
373
379
|
|
|
374
380
|
class _CallableWrapper:
|
|
375
|
-
|
|
376
|
-
They are based on putting dependencies (functions) as keys for the dictionary so if we want to be able to
|
|
377
|
-
override the wrapper, we need to make sure that it is equivalent to the original in __hash__ and __eq__
|
|
378
|
-
"""
|
|
381
|
+
# __eq__ and __hash__ are needed to make sure that dependency overrides work correctly.
|
|
382
|
+
# They are based on putting dependencies (functions) as keys for the dictionary so if we want to be able to
|
|
383
|
+
# override the wrapper, we need to make sure that it is equivalent to the original in __hash__ and __eq__
|
|
379
384
|
|
|
380
385
|
def __init__(self, original_callable: Callable) -> None:
|
|
381
386
|
super().__init__()
|
|
@@ -386,11 +391,9 @@ class _CallableWrapper:
|
|
|
386
391
|
|
|
387
392
|
@property
|
|
388
393
|
def __globals__(self):
|
|
389
|
-
|
|
390
|
-
It's supposed to be an attribute on the function but we use it as property to prevent python
|
|
391
|
-
from trying to pickle globals when we deepcopy this wrapper
|
|
392
|
-
"""
|
|
393
|
-
#
|
|
394
|
+
# FastAPI uses __globals__ to resolve forward references in type hints
|
|
395
|
+
# It's supposed to be an attribute on the function but we use it as property to prevent python
|
|
396
|
+
# from trying to pickle globals when we deepcopy this wrapper
|
|
394
397
|
return self._original_callable.__globals__
|
|
395
398
|
|
|
396
399
|
def __call__(self, *args: Any, **kwargs: Any):
|
|
@@ -420,8 +423,7 @@ class _AnnotationTransformer:
|
|
|
420
423
|
)
|
|
421
424
|
|
|
422
425
|
def change_version_of_annotation(self, annotation: Any) -> Any:
|
|
423
|
-
"""Recursively go through all annotations and change them to the
|
|
424
|
-
annotations corresponding to the version passed.
|
|
426
|
+
"""Recursively go through all annotations and change them to annotations corresponding to the version passed.
|
|
425
427
|
|
|
426
428
|
So if we had a annotation "UserResponse" from "head" version, and we passed version of "2022-11-16", it would
|
|
427
429
|
replace "UserResponse" with the the same class but from the "2022-11-16" version.
|
|
@@ -503,7 +505,7 @@ class _AnnotationTransformer:
|
|
|
503
505
|
return annotation
|
|
504
506
|
|
|
505
507
|
def _change_version_of_type(self, annotation: type):
|
|
506
|
-
if
|
|
508
|
+
if lenient_issubclass(annotation, BaseModel | Enum):
|
|
507
509
|
return self.generator[annotation]
|
|
508
510
|
else:
|
|
509
511
|
return annotation
|
|
@@ -607,7 +609,7 @@ def _add_request_and_response_params(route: APIRoute):
|
|
|
607
609
|
|
|
608
610
|
@final
|
|
609
611
|
class SchemaGenerator:
|
|
610
|
-
__slots__ = "annotation_transformer", "
|
|
612
|
+
__slots__ = "annotation_transformer", "concrete_models", "model_bundle"
|
|
611
613
|
|
|
612
614
|
def __init__(self, model_bundle: _ModelBundle) -> None:
|
|
613
615
|
self.annotation_transformer = _AnnotationTransformer(self)
|
|
@@ -619,7 +621,11 @@ class SchemaGenerator:
|
|
|
619
621
|
}
|
|
620
622
|
|
|
621
623
|
def __getitem__(self, model: type[_T_ANY_MODEL], /) -> type[_T_ANY_MODEL]:
|
|
622
|
-
if
|
|
624
|
+
if (
|
|
625
|
+
not isinstance(model, type)
|
|
626
|
+
or not lenient_issubclass(model, BaseModel | Enum)
|
|
627
|
+
or model in (BaseModel, RootModel)
|
|
628
|
+
):
|
|
623
629
|
return model
|
|
624
630
|
model = _unwrap_model(model)
|
|
625
631
|
|
|
@@ -648,10 +654,10 @@ class SchemaGenerator:
|
|
|
648
654
|
elif model in self.model_bundle.enums:
|
|
649
655
|
return self.model_bundle.enums[model]
|
|
650
656
|
|
|
651
|
-
if
|
|
657
|
+
if lenient_issubclass(model, BaseModel):
|
|
652
658
|
wrapper = _wrap_pydantic_model(model)
|
|
653
659
|
self.model_bundle.schemas[model] = wrapper
|
|
654
|
-
elif
|
|
660
|
+
elif lenient_issubclass(model, Enum):
|
|
655
661
|
wrapper = _EnumWrapper(model)
|
|
656
662
|
self.model_bundle.enums[model] = wrapper
|
|
657
663
|
else:
|
|
@@ -15,7 +15,7 @@ _P = ParamSpec("_P")
|
|
|
15
15
|
|
|
16
16
|
# TODO (https://github.com/zmievsa/cadwyn/issues/49): Add form handling
|
|
17
17
|
class RequestInfo:
|
|
18
|
-
__slots__ = ("
|
|
18
|
+
__slots__ = ("_cookies", "_query_params", "_request", "body", "headers")
|
|
19
19
|
|
|
20
20
|
def __init__(self, request: Request, body: Any):
|
|
21
21
|
super().__init__()
|
|
@@ -36,7 +36,7 @@ class RequestInfo:
|
|
|
36
36
|
|
|
37
37
|
# TODO (https://github.com/zmievsa/cadwyn/issues/111): handle _response.media_type and _response.background
|
|
38
38
|
class ResponseInfo:
|
|
39
|
-
__slots__ = ("
|
|
39
|
+
__slots__ = ("_response", "body")
|
|
40
40
|
|
|
41
41
|
def __init__(self, response: Response, body: Any):
|
|
42
42
|
super().__init__()
|
|
@@ -86,9 +86,9 @@ class _AlterDataInstruction:
|
|
|
86
86
|
return self.transformer(__request_or_response)
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
##########
|
|
90
|
+
# Requests
|
|
91
|
+
##########
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
@dataclass
|
|
@@ -146,9 +146,9 @@ def convert_request_to_next_version_for(
|
|
|
146
146
|
return decorator # pyright: ignore[reportReturnType]
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
###########
|
|
150
|
+
# Responses
|
|
151
|
+
###########
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
@dataclass
|
|
@@ -23,7 +23,7 @@ class EndpointAttributesPayload:
|
|
|
23
23
|
# 1. "endpoint" must not change -- otherwise this versioning is doomed
|
|
24
24
|
# 2. "dependency_overrides_provider" is taken from router's attributes
|
|
25
25
|
# 3. "response_model" must not change for the same reason as endpoint
|
|
26
|
-
# The following for the same reason as
|
|
26
|
+
# The following for the same reason as endpoint:
|
|
27
27
|
# * response_model_include: SetIntStr | DictIntStrAny
|
|
28
28
|
# * response_model_exclude: SetIntStr | DictIntStrAny
|
|
29
29
|
# * response_model_by_alias: bool
|
|
@@ -396,17 +396,6 @@ class VersionBundle:
|
|
|
396
396
|
path: str,
|
|
397
397
|
method: str,
|
|
398
398
|
) -> ResponseInfo:
|
|
399
|
-
"""Convert the data to a specific version by applying all version changes in reverse order.
|
|
400
|
-
|
|
401
|
-
Args:
|
|
402
|
-
endpoint: the function which usually returns this data. Data migrations marked with this endpoint will
|
|
403
|
-
be applied to the passed data
|
|
404
|
-
payload: data to be migrated. Will be mutated during the call
|
|
405
|
-
version: the version to which the data should be converted
|
|
406
|
-
|
|
407
|
-
Returns:
|
|
408
|
-
Modified data
|
|
409
|
-
"""
|
|
410
399
|
for v in self.versions:
|
|
411
400
|
if v.value <= current_version:
|
|
412
401
|
break
|
|
@@ -421,7 +410,7 @@ class VersionBundle:
|
|
|
421
410
|
if path in version_change.alter_response_by_path_instructions:
|
|
422
411
|
for instruction in version_change.alter_response_by_path_instructions[path]:
|
|
423
412
|
if method in instruction.methods: # pragma: no branch # Safe branch to skip
|
|
424
|
-
migrations_to_apply.append(instruction)
|
|
413
|
+
migrations_to_apply.append(instruction) # noqa: PERF401
|
|
425
414
|
|
|
426
415
|
for migration in migrations_to_apply:
|
|
427
416
|
if response_info.status_code < 300 or migration.migrate_http_errors:
|
|
@@ -447,8 +436,7 @@ class VersionBundle:
|
|
|
447
436
|
request_param: FastapiRequest = kwargs[request_param_name]
|
|
448
437
|
response_param: FastapiResponse = kwargs[response_param_name]
|
|
449
438
|
background_tasks: BackgroundTasks | None = kwargs.get(
|
|
450
|
-
background_tasks_param_name, # pyright: ignore[reportArgumentType
|
|
451
|
-
None,
|
|
439
|
+
background_tasks_param_name, # pyright: ignore[reportArgumentType]
|
|
452
440
|
)
|
|
453
441
|
method = request_param.method
|
|
454
442
|
response = Sentinel
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This section covers the entirety of features and their rationale in Cadwyn. It can also be used as a reference documentation until we have a proper one. First, let's talk about the reasons for using Cadwyn at all.
|
|
4
4
|
|
|
5
|
-
Cadwyn aims to be the most accurate and sophisticated API Versioning model out there. First of all, you maintain **zero** duplicated code yourself. Usually, in API versioning you [would need to](../theory/how_we_got_here.md) duplicate and maintain at least some layer of your
|
|
5
|
+
Cadwyn aims to be the most accurate and sophisticated API Versioning model out there. First of all, you maintain **zero** duplicated code yourself. Usually, in API versioning you [would need to](../theory/how_we_got_here.md) duplicate and maintain at least some layer of your application. It could be the database, business logic, schemas, and endpoints. Cadwyn allows you to duplicate none of that. Internally, it duplicates endpoints and schemas at runtime but none of it becomes your tech debt, none of it becomes your code to support. If you test rigorously, then only [some small subset of your tests](./testing.md) will need to be duplicated when existing functionality is changed between versions.
|
|
6
6
|
|
|
7
7
|
You define your database, business logic, routes, and schemas only once. Then, whenever you release a new API version, you use Cadwyn's [version change DSL](./version_changes.md#version-changes) to describe how to convert your app to the previous version. So your business logic and database stay intact and always represent the latest version while the version changes make sure that your clients can continue using the previous versions without ever needing to update their code.
|
|
8
8
|
|
|
@@ -33,7 +33,7 @@ That's it! `generate_and_include_versioned_routers` will generate all versions o
|
|
|
33
33
|
|
|
34
34
|
## Routing
|
|
35
35
|
|
|
36
|
-
Cadwyn is built on header-based routing. First, we route requests to the appropriate API version based on the version header (`x-api-version` by default). Then we route by the appropriate url path and method.
|
|
36
|
+
Cadwyn is built on header-based routing. First, we route requests to the appropriate API version based on the version header (`x-api-version` by default). Then we route by the appropriate url path and method. Currently, Cadwyn only works with ISO date-based versions (such as `2022-11-16`). If the user sends an incorrect API version, Cadwyn picks up the closest lower applicable version. For example, `2022-11-16` in request can be matched by `2022-11-15` and `2000-01-01` but cannot be matched by `2022-11-17`.
|
|
37
37
|
|
|
38
38
|
However, header-based routing is only the standard way to use Cadwyn. If you want to use any other sort of routing, you can use Cadwyn directly through `cadwyn.generate_versioned_routers` or subclass `cadwyn.Cadwyn` to use a different router and middleware. Just remember to update the `VersionBundle.api_version_var` variable each time you route some request to a version. This variable allows Cadwyn to do [side effects](./version_changes.md#version-changes-with-side-effects) and [data migrations](./version_changes.md#data-migrations).
|
|
39
39
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Cadwyn implements a methodology that is based on the following set of principles:
|
|
4
4
|
|
|
5
5
|
* Each version is made up of "version changes" or "compatibility gates" which describe **independent atomic** differences between it and previous version
|
|
6
|
-
* We make a new version if
|
|
6
|
+
* We make a new version if and only if we have breaking changes
|
|
7
7
|
* Versions must have little to no effect on the business logic
|
|
8
8
|
* Versions **must always** be compatible in terms of data
|
|
9
9
|
* Creating new versions is avoided at all costs
|
|
@@ -110,7 +110,7 @@ versions = VersionBundle(
|
|
|
110
110
|
|
|
111
111
|
### VersionChange.description
|
|
112
112
|
|
|
113
|
-
The description field of your version change must be even more detailed. In fact, it is intended to be the **name** and the **summary** of the version change for your clients. It must clearly state to you clients **what happened** and **why**. So you need to make it grammatically correct, detailed, concrete, and written for humans. Note that you do not have to use a strict machine-readable format -- it is a portion of documentation, not a set of
|
|
113
|
+
The description field of your version change must be even more detailed. In fact, it is intended to be the **name** and the **summary** of the version change for your clients. It must clearly state to you clients **what happened** and **why**. So you need to make it grammatically correct, detailed, concrete, and written for humans. Note that you do not have to use a strict machine-readable format -- it is a portion of documentation, not a set of instructions. Let's take [Stripe's description](https://stripe.com/blog/api-versioning) to one of their version changes as an example:
|
|
114
114
|
|
|
115
115
|
```md
|
|
116
116
|
Event objects (and webhooks) will now render `request` subobject that contains a request ID and idempotency key instead of just a string request ID.
|
|
@@ -126,7 +126,7 @@ Changes:
|
|
|
126
126
|
|
|
127
127
|
* Its first line, `Migration from first version (2022-11-16) to 2023-09-01 version.`, duplicates the already-known information -- your developers will know which version `VersionChange` migrates to and from by its location in [VersionBundle](#versionbundle) and most likely by its file name. Your clients will also know that because you can automatically infer this information from So it is simply standing in the way of actually useful parts of the documentation
|
|
128
128
|
* Its second line, `Changes:`, does not make any sense as well because description of a `VersionChange` cannot describe anything but changes. So again, it's stating the obvious and making it harder for our readers to understand the crux of the change
|
|
129
|
-
* Its third line, `Changed schema for 'POST /v1/tax_ids' endpoint`, gives both too much and too little information. First of all, it talks about changing schema but it never mentions what exactly changed. Remember: we are doing this to make it easy for our clients to migrate from one version to another.
|
|
129
|
+
* Its third line, `Changed schema for 'POST /v1/tax_ids' endpoint`, gives both too much and too little information. First of all, it talks about changing schema but it never mentions what exactly changed. Remember: we are doing this to make it easy for our clients to migrate from one version to another. Instead, it is much better to mention the openapi model name that you changed, the fields you changed, and why you changed them
|
|
130
130
|
|
|
131
131
|
### VersionChange.instructions_to_migrate_to_previous_version
|
|
132
132
|
|
|
@@ -413,16 +413,13 @@ or to specify migrations using [endpoint path](#path-based-migration-specificati
|
|
|
413
413
|
Sometimes you will use API versioning to handle a breaking change in your **business logic**, not in the schemas themselves. In such cases, it is tempting to add a version check and just follow the new business logic such as:
|
|
414
414
|
|
|
415
415
|
```python
|
|
416
|
+
# This is wrong. Please, do not do this.
|
|
416
417
|
if api_version_var.get() >= date(2022, 11, 11):
|
|
417
418
|
# do new logic here
|
|
418
419
|
...
|
|
419
420
|
```
|
|
420
421
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
**WARNING**: Side effects are the wrong way to do API Versioning. In 99% of time, you will **not** need them. Please, think twice before using them. API Versioning is about having the same underlying app and data while just changing the schemas and api endpoints to interact with it. By introducing side effects, you leak versioning into your business logic and possibly even your data which makes your code much harder to support in the long term. If each side effect adds a single `if` to your logic, than after 100 versions with side effects, you will have 100 more `if`s. If used correctly, Cadwyn can help you support decades worth of API versions at the same time with minimal costs but side effects make it much harder to do. Changes in the underlying source, structure, or logic of your data should not affect your API or public-facing business logic.
|
|
424
|
-
|
|
425
|
-
To simplify this, cadwyn has a special `VersionChangeWithSideEffects` class. It makes finding dangerous versions that have side effects much easier and provides a nice abstraction for checking whether we are on a version where these side effects have been applied.
|
|
422
|
+
Instead, Cadwyn provides a special `VersionChangeWithSideEffects` class for handling such cases. It makes finding dangerous versions that have side effects much easier and provides a nice abstraction for checking whether we are on a version where these side effects have been applied.
|
|
426
423
|
|
|
427
424
|
As an example, let's use the tutorial section's case with the user and their address. Let's say that we use an external service to check whether user's address is listed in it and return 400 response if it is not. Let's also say that we only added this check in the newest version.
|
|
428
425
|
|
|
@@ -432,7 +429,7 @@ from cadwyn import VersionChangeWithSideEffects
|
|
|
432
429
|
|
|
433
430
|
class UserAddressIsCheckedInExternalService(VersionChangeWithSideEffects):
|
|
434
431
|
description = (
|
|
435
|
-
"User's address is now checked for
|
|
432
|
+
"User's address is now checked for existence in an external service. "
|
|
436
433
|
"If it doesn't exist there, a 400 code is returned."
|
|
437
434
|
)
|
|
438
435
|
```
|
|
@@ -450,3 +447,9 @@ async def create_user(payload):
|
|
|
450
447
|
```
|
|
451
448
|
|
|
452
449
|
So this change can be contained in any version -- your business logic doesn't know which version it has and shouldn't.
|
|
450
|
+
|
|
451
|
+
### Warning against side effects
|
|
452
|
+
|
|
453
|
+
Side effects are a very powerful tool but they must be used with great caution. Are you sure you MUST change your business logic? Are you sure whatever you are trying to do cannot just be done by a migration? 90% of time, you will **not** need them. Please, think twice before using them. API Versioning is about having the same underlying app and data while just changing the schemas and api endpoints to interact with it. By introducing side effects, you leak versioning into your business logic and possibly even your data which makes your code much harder to support in the long term. If each side effect adds a single `if` to your logic, than after 100 versions with side effects, you will have 100 more `if`s. If used correctly, Cadwyn can help you support decades worth of API versions at the same time with minimal costs, and side effects make it much harder to do. Changes in the underlying source, structure, or logic of your data should not affect your API or public-facing business logic.
|
|
454
|
+
|
|
455
|
+
However, the [following use cases](../how_to/change_business_logic/index.md) often necessitate side effects.
|