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.

Files changed (135) hide show
  1. {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/workflows/ci.yaml +10 -13
  2. {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/workflows/publish_docs.yaml +2 -2
  3. {cadwyn-4.4.3 → cadwyn-4.4.5}/.pre-commit-config.yaml +4 -4
  4. {cadwyn-4.4.3 → cadwyn-4.4.5}/CHANGELOG.md +13 -1
  5. {cadwyn-4.4.3 → cadwyn-4.4.5}/PKG-INFO +2 -2
  6. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/_asts.py +10 -8
  7. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/_importer.py +1 -1
  8. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/_utils.py +15 -1
  9. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/changelogs.py +1 -1
  10. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/route_generation.py +7 -7
  11. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/routing.py +5 -10
  12. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/schema_generation.py +28 -22
  13. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/data.py +8 -8
  14. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/endpoints.py +1 -1
  15. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/schemas.py +1 -1
  16. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/versions.py +2 -14
  17. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/index.md +1 -1
  18. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/main_app.md +1 -1
  19. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/methodology.md +1 -1
  20. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/version_changes.md +11 -8
  21. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_openapi_schemas/add_field.md +1 -1
  22. cadwyn-4.4.5/docs/img/sponsor_logos/monite.png +0 -0
  23. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/plugin.py +1 -3
  24. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/theory/how_we_got_here.md +2 -2
  25. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/theory/literature.md +1 -0
  26. {cadwyn-4.4.3 → cadwyn-4.4.5}/pyproject.toml +19 -1
  27. {cadwyn-4.4.3 → cadwyn-4.4.5}/ruff.toml +50 -18
  28. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/app_for_testing_routing.py +5 -5
  29. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/versioned_app/__init__.py +0 -0
  30. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/versioned_app/app.py +0 -0
  31. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/versioned_app/v2022_01_02.py +0 -0
  32. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/versioned_app/webhooks.py +0 -0
  33. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/conftest.py +0 -2
  34. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_applications.py +2 -5
  35. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_data_migrations.py +4 -6
  36. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_router_generation.py +4 -6
  37. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_routing.py +2 -2
  38. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_schema_generation/test_schema_field.py +3 -4
  39. {cadwyn-4.4.3 → cadwyn-4.4.5}/uv.lock +365 -332
  40. cadwyn-4.4.3/docs/img/sponsor_logos/monite.png +0 -0
  41. {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/CODE_OF_CONDUCT.md +0 -0
  42. {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  43. {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  44. {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/actions/setup-python-uv/action.yaml +0 -0
  45. {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/workflows/daily_tests.yaml +0 -0
  46. {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/workflows/release.yaml +0 -0
  47. {cadwyn-4.4.3 → cadwyn-4.4.5}/.github/workflows/validate_links.yaml +0 -0
  48. {cadwyn-4.4.3 → cadwyn-4.4.5}/.gitignore +0 -0
  49. {cadwyn-4.4.3 → cadwyn-4.4.5}/LICENSE +0 -0
  50. {cadwyn-4.4.3 → cadwyn-4.4.5}/Makefile +0 -0
  51. {cadwyn-4.4.3 → cadwyn-4.4.5}/README.md +0 -0
  52. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/__init__.py +9 -9
  53. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/__main__.py +0 -0
  54. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/_render.py +0 -0
  55. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/applications.py +0 -0
  56. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/exceptions.py +0 -0
  57. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/middleware.py +0 -0
  58. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/py.typed +0 -0
  59. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/static/__init__.py +0 -0
  60. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/static/docs.html +0 -0
  61. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/__init__.py +7 -7
  62. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/common.py +0 -0
  63. {cadwyn-4.4.3 → cadwyn-4.4.5}/cadwyn/structure/enums.py +0 -0
  64. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/CNAME +0 -0
  65. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/__init__.py +0 -0
  66. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/api_version_header_and_context_variables.md +0 -0
  67. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/beware_of_data_versioning.md +0 -0
  68. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/changelogs.md +0 -0
  69. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/cli.md +0 -0
  70. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/endpoint_migrations.md +0 -0
  71. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/enum_migrations.md +0 -0
  72. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/schema_generation.md +0 -0
  73. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/schema_migrations.md +0 -0
  74. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/concepts/testing.md +0 -0
  75. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/home/CONTRIBUTING.md +0 -0
  76. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_business_logic/index.md +0 -0
  77. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_endpoints/index.md +0 -0
  78. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_openapi_schemas/change_field_type.md +0 -0
  79. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_openapi_schemas/changing_constraints.md +0 -0
  80. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_openapi_schemas/remove_field.md +0 -0
  81. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/change_openapi_schemas/rename_a_field_in_schema.md +0 -0
  82. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/how_to/index.md +0 -0
  83. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/img/dashboard_with_one_version.png +0 -0
  84. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/img/dashboard_with_two_versions.png +0 -0
  85. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/img/get_users_endpoint_from_prior_version.png +0 -0
  86. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/img/simplified_migration_model.png +0 -0
  87. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/img/unversioned_dashboard.png +0 -0
  88. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/index.md +0 -0
  89. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/quickstart/setup.md +0 -0
  90. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/quickstart/tutorial.md +0 -0
  91. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs/theory/how_to_build_versioning_framework.md +0 -0
  92. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/__init__.py +0 -0
  93. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/__init__.py +0 -0
  94. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/setup/__init__.py +0 -0
  95. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/setup/block001.sh +0 -0
  96. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/setup/block002.py +0 -0
  97. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/setup/tests/__init__.py +0 -0
  98. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/setup/tests/test_block002.py +0 -0
  99. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/__init__.py +0 -0
  100. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/block001.py +0 -0
  101. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/block002.py +0 -0
  102. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/block003.py +0 -0
  103. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/tests/__init__.py +0 -0
  104. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/tests/test_block001.py +0 -0
  105. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/tests/test_block002.py +0 -0
  106. {cadwyn-4.4.3 → cadwyn-4.4.5}/docs_src/quickstart/tutorial/tests/test_block003.py +0 -0
  107. {cadwyn-4.4.3 → cadwyn-4.4.5}/mkdocs.yml +0 -0
  108. {cadwyn-4.4.3 → cadwyn-4.4.5}/scripts/fix_links.py +0 -0
  109. {cadwyn-4.4.3 → cadwyn-4.4.5}/scripts/split_md.py +0 -0
  110. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/__init__.py +0 -0
  111. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_data/__init__.py +0 -0
  112. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_data/unversioned_schema_dir/__init__.py +0 -0
  113. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_data/unversioned_schema_dir/unversioned_schemas.py +0 -0
  114. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_data/unversioned_schemas.py +0 -0
  115. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/__init__.py +0 -0
  116. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/__init__.py +0 -0
  117. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/classes.py +0 -0
  118. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/complex/__init__.py +0 -0
  119. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/complex/classes.py +0 -0
  120. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/complex/versions.py +0 -0
  121. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/render/versions.py +0 -0
  122. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/utils.py +0 -0
  123. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/_resources/versioned_app/v2021_01_01.py +0 -0
  124. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_auth_dependencies.py +0 -0
  125. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_changelog.py +0 -0
  126. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_cli.py +0 -0
  127. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_render.py +0 -0
  128. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_schema_generation/__init__.py +0 -0
  129. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_schema_generation/test_enum.py +0 -0
  130. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_schema_generation/test_schema.py +0 -0
  131. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_schema_generation/test_schema_validator.py +0 -0
  132. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/test_structure.py +0 -0
  133. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/tutorial/__init__.py +0 -0
  134. {cadwyn-4.4.3 → cadwyn-4.4.5}/tests/tutorial/main.py +0 -0
  135. {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@v3
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: main-tests-coverage-results
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@v3
64
+ uses: actions/upload-artifact@v4
64
65
  with:
65
- name: docs-tests-coverage-results
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 main tests coverage info
73
- uses: actions/download-artifact@v3
73
+ - name: Download coverage info
74
+ uses: actions/download-artifact@v4
74
75
  with:
75
- name: main-tests-coverage-results
76
- path: coverage/
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@v4
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@v3
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: v4.6.0
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.6.1
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.18.0" # replace with latest tag on GitHub
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.41.0
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 overriden 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.
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.3
1
+ Metadata-Version: 2.4
2
2
  Name: cadwyn
3
- Version: 4.4.3
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
- for key in empty_obj.__dataclass_fields__: # pyright: ignore[reportAttributeAccessIssue]
63
- if getattr(value, key) != getattr(empty_obj, key):
64
- modified_fields.append((key, getattr(value, key)))
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
- field_mapping, definitions = get_definitions(
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 _, router in routers.items():
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 _, webhook_router in webhook_routers.items():
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
- for route in routes:
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
- found_routes.append(route)
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
- this class should be a root router of the FastAPI app when using header based
28
- versioning. It will be used to route the requests to the correct versioned route
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
- its a copy-paste from starlette.routing.Router
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 by applying all version changes from latest until that version
164
- in reverse order and wrapping the result in the correct version of latest_response_model.
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. I.e. The ones that were either defined or overriden "
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 issubclass(base, BaseModel):
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
- """__eq__ and __hash__ are needed to make sure that dependency overrides work correctly.
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
- """FastAPI uses __globals__ to resolve forward references in type hints
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 issubclass(annotation, BaseModel | Enum):
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", "model_bundle", "concrete_models"
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 not isinstance(model, type) or not issubclass(model, BaseModel | Enum) or model in (BaseModel, RootModel):
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 issubclass(model, BaseModel):
657
+ if lenient_issubclass(model, BaseModel):
652
658
  wrapper = _wrap_pydantic_model(model)
653
659
  self.model_bundle.schemas[model] = wrapper
654
- elif issubclass(model, Enum):
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__ = ("body", "headers", "_cookies", "_query_params", "_request")
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__ = ("body", "_response")
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
- ## Requests
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
- ## Responses
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 enpoint:
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
@@ -186,7 +186,7 @@ class AlterFieldInstructionFactory:
186
186
  self,
187
187
  *,
188
188
  type: Any,
189
- info: FieldInfo | None = None,
189
+ info: FieldInfo | Any | None = None,
190
190
  ) -> FieldExistedAsInstruction:
191
191
  if info is None:
192
192
  info = cast(FieldInfo, Field())
@@ -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, reportCallIssue]
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 applicaton. 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.
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. Currerntly, 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`.
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 an only if we have breaking changes
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 intructions. Let's take [Stripe's description](https://stripe.com/blog/api-versioning) to one of their version changes as an example:
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. Insteaad, it is much better to mention the openapi model name that you changed, the fields you changed, and why you changed them
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
- In cadwyn, this approach is **highly** discouraged. It is recommended that you avoid side effects like this at any cost because each one makes your core logic harder to understand. But if you cannot, then I urge you to at least abstract away versions and versioning from your business logic which will make your code much easier to read.
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 existense in an external service. "
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.