cadwyn 5.2.1__tar.gz → 5.3.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.
- {cadwyn-5.2.1 → cadwyn-5.3.0}/CHANGELOG.md +12 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/PKG-INFO +1 -1
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/applications.py +7 -1
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/middleware.py +9 -2
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/route_generation.py +59 -25
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/schema_generation.py +1 -2
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/versions.py +16 -17
- cadwyn-5.2.1/docs/concepts/where_to_put_the_version_and_how_to_format_it.md → cadwyn-5.3.0/docs/concepts/api_version_parameter.md +19 -1
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/endpoint_migrations.md +5 -3
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/schema_migrations.md +2 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/version_changes.md +2 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/add_field.md +1 -3
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/version_with_paths_and_numbers_instead_of_headers_and_dates.md +1 -1
- {cadwyn-5.2.1 → cadwyn-5.3.0}/mkdocs.yml +1 -2
- {cadwyn-5.2.1 → cadwyn-5.3.0}/pyproject.toml +1 -1
- {cadwyn-5.2.1 → cadwyn-5.3.0}/ruff.toml +1 -1
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_router_generation.py +46 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/tutorial/main.py +7 -2
- {cadwyn-5.2.1 → cadwyn-5.3.0}/uv.lock +1 -1
- cadwyn-5.2.1/docs/concepts/api_version_parameter_and_context_variables.md +0 -5
- {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/CODE_OF_CONDUCT.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/actions/setup-python-uv/action.yaml +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/workflows/ci.yaml +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/workflows/daily_tests.yaml +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/workflows/publish_docs.yaml +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/.github/workflows/release.yaml +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/.gitignore +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/.pre-commit-config.yaml +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/LICENSE +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/Makefile +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/README.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/__main__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_asts.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_importer.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_internal/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_internal/context_vars.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_render.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/_utils.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/changelogs.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/dependencies.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/exceptions.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/py.typed +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/routing.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/static/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/static/docs.html +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/common.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/data.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/endpoints.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/enums.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/cadwyn/structure/schemas.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/CNAME +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/beware_of_data_versioning.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/changelogs.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/cli.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/enum_migrations.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/index.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/main_app.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/methodology.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/schema_generation.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/concepts/testing.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/home/CONTRIBUTING.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_business_logic/index.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_endpoints/index.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/change_field_type.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/change_schema_without_endpoint.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/changing_constraints.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/remove_field.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/rename_a_field_in_schema.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/index.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/dashboard_with_one_version.png +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/dashboard_with_two_versions.png +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/get_users_endpoint_from_prior_version.png +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/simplified_migration_model.png +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/sponsor_logos/monite.png +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/img/unversioned_dashboard.png +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/index.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/plugin.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/quickstart/setup.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/quickstart/tutorial.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/theory/how_to_build_versioning_framework.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/theory/how_we_got_here.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs/theory/literature.md +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block001.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/block002.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block001.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/change_openapi_schemas/change_schema_without_endpoint/tests/test_block002.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/block001.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/tests/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/tests/test_block_001.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/setup/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/setup/block001.sh +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/setup/block002.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/setup/tests/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/setup/tests/test_block002.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/block001.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/block002.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/block003.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/tests/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/tests/test_block001.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/tests/test_block002.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/quickstart/tutorial/tests/test_block003.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/docs_src/ruff.toml +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/scripts/fix_links.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/scripts/split_md.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_data/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_data/unversioned_schema_dir/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_data/unversioned_schema_dir/unversioned_schemas.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_data/unversioned_schemas.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/app_for_testing_routing.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/classes.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/complex/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/complex/classes.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/complex/versions.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/render/versions.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/utils.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/versioned_app/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/versioned_app/app.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/versioned_app/v2021_01_01.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/versioned_app/v2022_01_02.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/_resources/versioned_app/webhooks.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/conftest.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_applications.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_auth_dependencies.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_changelog.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_cli.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_data_migrations.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_render.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_router_generation_with_from_future_annotations.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_routing.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_enum.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_schema.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_schema_field.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_schema_validator.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_schema_with_future_annotations.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_structure.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/tutorial/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/tutorial/test_example.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/versioning_styles/__init__.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tests/versioning_styles/test_versioning_formats.py +0 -0
- {cadwyn-5.2.1 → cadwyn-5.3.0}/tox.ini +0 -0
|
@@ -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
|
+
## [5.3.0]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* `api_version_title` and `api_version_description` arguments to `Cadwyn` to allow customizing the API version parameter in the OpenAPI schema
|
|
13
|
+
|
|
14
|
+
## [5.2.2]
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* Whenever a route was migrated by path, we used the path and methods of the request. So if the request wanted "/v1/webhook_settings" and we renamed them to "/v1/webhook_subscriptions" -- all migrations for "/v1/webhook_subscriptions" would not get applied to any requests that wanted "/v1/webhook_settings". This effectively means that previously route renamings were incompatible with by path converters. Now we use the head route id instead of the path and methods of the request for matching so we always know which migrations to apply.
|
|
19
|
+
|
|
8
20
|
## [5.2.1]
|
|
9
21
|
|
|
10
22
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cadwyn
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.3.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
|
|
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine, Sequence
|
|
|
4
4
|
from datetime import date
|
|
5
5
|
from logging import getLogger
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import TYPE_CHECKING, Annotated, Any, Union, cast
|
|
7
|
+
from typing import TYPE_CHECKING, Annotated, Any, Optional, Union, cast
|
|
8
8
|
|
|
9
9
|
import fastapi
|
|
10
10
|
from fastapi import APIRouter, FastAPI, HTTPException, routing
|
|
@@ -71,6 +71,8 @@ class Cadwyn(FastAPI):
|
|
|
71
71
|
api_version_format: APIVersionFormat = "date",
|
|
72
72
|
api_version_parameter_name: str = "x-api-version",
|
|
73
73
|
api_version_default_value: Union[str, None, Callable[[Request], Awaitable[str]]] = None,
|
|
74
|
+
api_version_title: Optional[str] = None,
|
|
75
|
+
api_version_description: Optional[str] = None,
|
|
74
76
|
versioning_middleware_class: type[VersionPickingMiddleware] = VersionPickingMiddleware,
|
|
75
77
|
changelog_url: Union[str, None] = "/changelog",
|
|
76
78
|
include_changelog_url_in_schema: bool = True,
|
|
@@ -207,6 +209,8 @@ class Cadwyn(FastAPI):
|
|
|
207
209
|
self.api_version_format = api_version_format
|
|
208
210
|
self.api_version_parameter_name = api_version_parameter_name
|
|
209
211
|
self.api_version_pythonic_parameter_name = api_version_parameter_name.replace("-", "_")
|
|
212
|
+
self.api_version_title = api_version_title
|
|
213
|
+
self.api_version_description = api_version_description
|
|
210
214
|
if api_version_location == "custom_header":
|
|
211
215
|
self._api_version_manager = HeaderVersionManager(api_version_parameter_name=api_version_parameter_name)
|
|
212
216
|
self._api_version_fastapi_depends_class = fastapi.Header
|
|
@@ -465,6 +469,8 @@ class Cadwyn(FastAPI):
|
|
|
465
469
|
default_value=version,
|
|
466
470
|
fastapi_depends_class=self._api_version_fastapi_depends_class,
|
|
467
471
|
validation_data_type=self.api_version_validation_data_type,
|
|
472
|
+
title=self.api_version_title,
|
|
473
|
+
description=self.api_version_description,
|
|
468
474
|
)
|
|
469
475
|
)
|
|
470
476
|
],
|
|
@@ -5,7 +5,7 @@ import inspect
|
|
|
5
5
|
import re
|
|
6
6
|
from collections.abc import Awaitable, Callable
|
|
7
7
|
from contextvars import ContextVar
|
|
8
|
-
from typing import Annotated, Any, Literal, Protocol, Union
|
|
8
|
+
from typing import Annotated, Any, Literal, Optional, Protocol, Union
|
|
9
9
|
|
|
10
10
|
import fastapi
|
|
11
11
|
from fastapi import Request
|
|
@@ -58,6 +58,8 @@ def _generate_api_version_dependency(
|
|
|
58
58
|
default_value: str,
|
|
59
59
|
fastapi_depends_class: Callable[..., Any],
|
|
60
60
|
validation_data_type: Any,
|
|
61
|
+
title: Optional[str] = None,
|
|
62
|
+
description: Optional[str] = None,
|
|
61
63
|
):
|
|
62
64
|
def api_version_dependency(**kwargs: Any):
|
|
63
65
|
# TODO: What do I return?
|
|
@@ -69,7 +71,12 @@ def _generate_api_version_dependency(
|
|
|
69
71
|
api_version_pythonic_parameter_name,
|
|
70
72
|
inspect.Parameter.KEYWORD_ONLY,
|
|
71
73
|
annotation=Annotated[
|
|
72
|
-
validation_data_type,
|
|
74
|
+
validation_data_type,
|
|
75
|
+
fastapi_depends_class(
|
|
76
|
+
openapi_examples={"default": {"value": default_value}},
|
|
77
|
+
title=title,
|
|
78
|
+
description=description,
|
|
79
|
+
),
|
|
73
80
|
],
|
|
74
81
|
# Path-based parameters do not support a default value in FastAPI :(
|
|
75
82
|
default=default_value if fastapi_depends_class != fastapi.Path else inspect.Signature.empty,
|
|
@@ -37,11 +37,13 @@ from cadwyn.schema_generation import (
|
|
|
37
37
|
)
|
|
38
38
|
from cadwyn.structure import Version, VersionBundle
|
|
39
39
|
from cadwyn.structure.common import Endpoint, VersionType
|
|
40
|
+
from cadwyn.structure.data import _AlterRequestByPathInstruction, _AlterResponseByPathInstruction
|
|
40
41
|
from cadwyn.structure.endpoints import (
|
|
41
42
|
EndpointDidntExistInstruction,
|
|
42
43
|
EndpointExistedInstruction,
|
|
43
44
|
EndpointHadInstruction,
|
|
44
45
|
)
|
|
46
|
+
from cadwyn.structure.versions import VersionChange
|
|
45
47
|
|
|
46
48
|
if TYPE_CHECKING:
|
|
47
49
|
from fastapi.dependencies.models import Dependant
|
|
@@ -52,6 +54,9 @@ _WR = TypeVar("_WR", bound=APIRouter, default=APIRouter)
|
|
|
52
54
|
_RouteT = TypeVar("_RouteT", bound=BaseRoute)
|
|
53
55
|
# This is a hack we do because we can't guarantee how the user will use the router.
|
|
54
56
|
_DELETED_ROUTE_TAG = "_CADWYN_DELETED_ROUTE"
|
|
57
|
+
_RoutePath = str
|
|
58
|
+
_RouteMethod = str
|
|
59
|
+
_RouteId = int
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
@dataclass(**DATACLASS_SLOTS, frozen=True, eq=True)
|
|
@@ -123,6 +128,7 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
123
128
|
]
|
|
124
129
|
|
|
125
130
|
def transform(self) -> GeneratedRouters[_R, _WR]:
|
|
131
|
+
# Copy MUST keep the order and number of routes. Otherwise, a ton of code below will break.
|
|
126
132
|
router = copy_router(self.parent_router)
|
|
127
133
|
webhook_router = copy_router(self.parent_webhooks_router)
|
|
128
134
|
routers: dict[VersionType, _R] = {}
|
|
@@ -132,7 +138,7 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
132
138
|
self.schema_generators[str(version.value)].annotation_transformer.migrate_router_to_version(router)
|
|
133
139
|
self.schema_generators[str(version.value)].annotation_transformer.migrate_router_to_version(webhook_router)
|
|
134
140
|
|
|
135
|
-
self.
|
|
141
|
+
self._attach_routes_to_data_converters(router, self.parent_router, version)
|
|
136
142
|
|
|
137
143
|
routers[version.value] = router
|
|
138
144
|
webhook_routers[version.value] = webhook_router
|
|
@@ -193,9 +199,11 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
193
199
|
]
|
|
194
200
|
return GeneratedRouters(routers, webhook_routers)
|
|
195
201
|
|
|
196
|
-
def
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
def _attach_routes_to_data_converters(self, router: APIRouter, head_router: APIRouter, version: Version):
|
|
203
|
+
# This method is way out of its league in terms of complexity. We gotta refactor it.
|
|
204
|
+
|
|
205
|
+
path_to_route_methods_mapping, head_response_models, head_request_bodies = (
|
|
206
|
+
self._extract_all_routes_identifiers_for_route_to_converter_matching(router)
|
|
199
207
|
)
|
|
200
208
|
|
|
201
209
|
for version_change in version.changes:
|
|
@@ -204,21 +212,10 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
204
212
|
*version_change.alter_request_by_path_instructions.values(),
|
|
205
213
|
]:
|
|
206
214
|
for by_path_converter in by_path_converters:
|
|
207
|
-
|
|
208
|
-
path_to_route_methods_mapping
|
|
215
|
+
self._attach_routes_by_path_converter(
|
|
216
|
+
head_router, path_to_route_methods_mapping, version_change, by_path_converter
|
|
209
217
|
)
|
|
210
218
|
|
|
211
|
-
if missing_methods:
|
|
212
|
-
raise RouteByPathConverterDoesNotApplyToAnythingError(
|
|
213
|
-
f"{by_path_converter.repr_name} "
|
|
214
|
-
f'"{version_change.__name__}.{by_path_converter.transformer.__name__}" '
|
|
215
|
-
f"failed to find routes with the following methods: {list(missing_methods)}. "
|
|
216
|
-
f"This means that you are trying to apply this converter to non-existing endpoint(s). "
|
|
217
|
-
"Please, check whether the path and methods are correct. (hint: path must include "
|
|
218
|
-
"all path variables and have a name that was used in the version that this "
|
|
219
|
-
"VersionChange resides in)"
|
|
220
|
-
)
|
|
221
|
-
|
|
222
219
|
for by_schema_converters in version_change.alter_request_by_schema_instructions.values():
|
|
223
220
|
for by_schema_converter in by_schema_converters:
|
|
224
221
|
if not by_schema_converter.check_usage: # pragma: no cover
|
|
@@ -249,14 +246,50 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
249
246
|
f"{version_change.__name__}.{by_schema_converter.transformer.__name__}"
|
|
250
247
|
)
|
|
251
248
|
|
|
252
|
-
def
|
|
253
|
-
self,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
249
|
+
def _attach_routes_by_path_converter(
|
|
250
|
+
self,
|
|
251
|
+
head_router: APIRouter,
|
|
252
|
+
path_to_route_methods_mapping: dict[_RoutePath, dict[_RouteMethod, set[_RouteId]]],
|
|
253
|
+
version_change: type[VersionChange],
|
|
254
|
+
by_path_converter: Union[_AlterResponseByPathInstruction, _AlterRequestByPathInstruction],
|
|
255
|
+
):
|
|
256
|
+
missing_methods = set()
|
|
257
|
+
for method in by_path_converter.methods:
|
|
258
|
+
if method in path_to_route_methods_mapping[by_path_converter.path]:
|
|
259
|
+
for route_index in path_to_route_methods_mapping[by_path_converter.path][method]:
|
|
260
|
+
route = head_router.routes[route_index]
|
|
261
|
+
if isinstance(by_path_converter, _AlterResponseByPathInstruction):
|
|
262
|
+
version_change._route_to_response_migration_mapping[id(route)].append(by_path_converter)
|
|
263
|
+
else:
|
|
264
|
+
version_change._route_to_request_migration_mapping[id(route)].append(by_path_converter)
|
|
265
|
+
else:
|
|
266
|
+
missing_methods.add(method)
|
|
267
|
+
|
|
268
|
+
if missing_methods:
|
|
269
|
+
raise RouteByPathConverterDoesNotApplyToAnythingError(
|
|
270
|
+
f"{by_path_converter.repr_name} "
|
|
271
|
+
f'"{version_change.__name__}.{by_path_converter.transformer.__name__}" '
|
|
272
|
+
f"failed to find routes with the following methods: {list(missing_methods)}. "
|
|
273
|
+
f"This means that you are trying to apply this converter to non-existing endpoint(s). "
|
|
274
|
+
"Please, check whether the path and methods are correct. (hint: path must include "
|
|
275
|
+
"all path variables and have a name that was used in the version that this "
|
|
276
|
+
"VersionChange resides in)"
|
|
277
|
+
)
|
|
258
278
|
|
|
259
|
-
|
|
279
|
+
def _extract_all_routes_identifiers_for_route_to_converter_matching(
|
|
280
|
+
self, router: APIRouter
|
|
281
|
+
) -> tuple[dict[_RoutePath, dict[_RouteMethod, set[_RouteId]]], set[Any], set[Any]]:
|
|
282
|
+
# int is the index of the route in the router.routes list.
|
|
283
|
+
# So we essentially keep track of which routes have which response models and request bodies.
|
|
284
|
+
# and their indices in the router.routes list. The indices will allow us to match them to the same
|
|
285
|
+
# routes in the head version. This gives us the ability to later apply changes to these routes
|
|
286
|
+
# without thinking about any renamings or response model changes.
|
|
287
|
+
|
|
288
|
+
response_models = set()
|
|
289
|
+
request_bodies = set()
|
|
290
|
+
path_to_route_methods_mapping: dict[str, dict[str, set[int]]] = defaultdict(lambda: defaultdict(set))
|
|
291
|
+
|
|
292
|
+
for index, route in enumerate(router.routes):
|
|
260
293
|
if isinstance(route, APIRoute):
|
|
261
294
|
if route.response_model is not None and lenient_issubclass(route.response_model, BaseModel):
|
|
262
295
|
response_models.add(route.response_model)
|
|
@@ -265,7 +298,8 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
265
298
|
annotation = route.body_field.field_info.annotation
|
|
266
299
|
if annotation is not None and lenient_issubclass(annotation, BaseModel):
|
|
267
300
|
request_bodies.add(annotation)
|
|
268
|
-
|
|
301
|
+
for method in route.methods:
|
|
302
|
+
path_to_route_methods_mapping[route.path][method].add(index)
|
|
269
303
|
|
|
270
304
|
head_response_models = {model.__cadwyn_original_model__ for model in response_models}
|
|
271
305
|
head_request_bodies = {getattr(body, "__cadwyn_original_model__", body) for body in request_bodies}
|
|
@@ -196,8 +196,7 @@ def migrate_response_body(
|
|
|
196
196
|
response,
|
|
197
197
|
current_version=version,
|
|
198
198
|
head_response_model=latest_response_model,
|
|
199
|
-
|
|
200
|
-
method="GET",
|
|
199
|
+
head_route=None,
|
|
201
200
|
)
|
|
202
201
|
|
|
203
202
|
versioned_response_model: type[pydantic.BaseModel] = generate_versioned_models(versions)[str(version)][
|
|
@@ -53,6 +53,7 @@ _CADWYN_REQUEST_PARAM_NAME = "cadwyn_request_param"
|
|
|
53
53
|
_CADWYN_RESPONSE_PARAM_NAME = "cadwyn_response_param"
|
|
54
54
|
_P = ParamSpec("_P")
|
|
55
55
|
_R = TypeVar("_R")
|
|
56
|
+
_RouteId = int
|
|
56
57
|
|
|
57
58
|
PossibleInstructions: TypeAlias = Union[
|
|
58
59
|
AlterSchemaSubInstruction, AlterEndpointSubInstruction, AlterEnumSubInstruction, SchemaHadInstruction, staticmethod
|
|
@@ -85,6 +86,8 @@ class VersionChange:
|
|
|
85
86
|
alter_response_by_schema_instructions: ClassVar[dict[type, list[_AlterResponseBySchemaInstruction]]] = Sentinel
|
|
86
87
|
alter_response_by_path_instructions: ClassVar[dict[str, list[_AlterResponseByPathInstruction]]] = Sentinel
|
|
87
88
|
_bound_version_bundle: "Union[VersionBundle, None]"
|
|
89
|
+
_route_to_request_migration_mapping: ClassVar[dict[_RouteId, list[_AlterRequestByPathInstruction]]] = Sentinel
|
|
90
|
+
_route_to_response_migration_mapping: ClassVar[dict[_RouteId, list[_AlterResponseByPathInstruction]]] = Sentinel
|
|
88
91
|
|
|
89
92
|
def __init_subclass__(cls, _abstract: bool = False) -> None:
|
|
90
93
|
super().__init_subclass__()
|
|
@@ -96,6 +99,8 @@ class VersionChange:
|
|
|
96
99
|
cls._extract_body_instructions_into_correct_containers()
|
|
97
100
|
cls._check_no_subclassing()
|
|
98
101
|
cls._bound_version_bundle = None
|
|
102
|
+
cls._route_to_request_migration_mapping = defaultdict(list)
|
|
103
|
+
cls._route_to_response_migration_mapping = defaultdict(list)
|
|
99
104
|
|
|
100
105
|
@classmethod
|
|
101
106
|
def _extract_body_instructions_into_correct_containers(cls):
|
|
@@ -358,7 +363,6 @@ class VersionBundle:
|
|
|
358
363
|
self,
|
|
359
364
|
body_type: Union[type[BaseModel], None],
|
|
360
365
|
head_dependant: Dependant,
|
|
361
|
-
path: str,
|
|
362
366
|
request: FastapiRequest,
|
|
363
367
|
response: FastapiResponse,
|
|
364
368
|
request_info: RequestInfo,
|
|
@@ -369,18 +373,17 @@ class VersionBundle:
|
|
|
369
373
|
embed_body_fields: bool,
|
|
370
374
|
background_tasks: Union[BackgroundTasks, None],
|
|
371
375
|
) -> dict[str, Any]:
|
|
372
|
-
method = request.method
|
|
373
|
-
|
|
374
376
|
start = self.reversed_version_values.index(current_version)
|
|
377
|
+
head_route_id = id(head_route)
|
|
375
378
|
for v in self.reversed_versions[start + 1 :]:
|
|
376
379
|
for version_change in v.changes:
|
|
377
380
|
if body_type is not None and body_type in version_change.alter_request_by_schema_instructions:
|
|
378
381
|
for instruction in version_change.alter_request_by_schema_instructions[body_type]:
|
|
379
382
|
instruction(request_info)
|
|
380
|
-
if
|
|
381
|
-
for instruction in version_change.
|
|
382
|
-
|
|
383
|
-
|
|
383
|
+
if head_route_id in version_change._route_to_request_migration_mapping:
|
|
384
|
+
for instruction in version_change._route_to_request_migration_mapping[head_route_id]:
|
|
385
|
+
instruction(request_info)
|
|
386
|
+
|
|
384
387
|
request.scope["headers"] = tuple((key.encode(), value.encode()) for key, value in request_info.headers.items())
|
|
385
388
|
del request._headers
|
|
386
389
|
# This gives us the ability to tell the user whether cadwyn is running its dependencies or FastAPI
|
|
@@ -406,10 +409,10 @@ class VersionBundle:
|
|
|
406
409
|
self,
|
|
407
410
|
response_info: ResponseInfo,
|
|
408
411
|
current_version: VersionType,
|
|
409
|
-
head_response_model: type[BaseModel],
|
|
410
|
-
|
|
411
|
-
method: str,
|
|
412
|
+
head_response_model: Union[type[BaseModel], None],
|
|
413
|
+
head_route: Union[APIRoute, None],
|
|
412
414
|
) -> ResponseInfo:
|
|
415
|
+
head_route_id = id(head_route)
|
|
413
416
|
end = self.version_values.index(current_version)
|
|
414
417
|
for v in self.versions[:end]:
|
|
415
418
|
for version_change in v.changes:
|
|
@@ -420,10 +423,8 @@ class VersionBundle:
|
|
|
420
423
|
version_change.alter_response_by_schema_instructions[head_response_model]
|
|
421
424
|
)
|
|
422
425
|
|
|
423
|
-
if
|
|
424
|
-
|
|
425
|
-
if method in instruction.methods: # pragma: no branch # Safe branch to skip
|
|
426
|
-
migrations_to_apply.append(instruction) # noqa: PERF401
|
|
426
|
+
if head_route_id in version_change._route_to_response_migration_mapping:
|
|
427
|
+
migrations_to_apply.extend(version_change._route_to_response_migration_mapping[head_route_id])
|
|
427
428
|
|
|
428
429
|
for migration in migrations_to_apply:
|
|
429
430
|
if response_info.status_code < 300 or migration.migrate_http_errors:
|
|
@@ -576,8 +577,7 @@ class VersionBundle:
|
|
|
576
577
|
response_info,
|
|
577
578
|
api_version,
|
|
578
579
|
head_route.response_model,
|
|
579
|
-
|
|
580
|
-
method,
|
|
580
|
+
head_route,
|
|
581
581
|
)
|
|
582
582
|
if isinstance(response_or_response_body, FastapiResponse):
|
|
583
583
|
# a webserver (uvicorn for instance) calculates the body at the endpoint level.
|
|
@@ -671,7 +671,6 @@ class VersionBundle:
|
|
|
671
671
|
new_kwargs = await self._migrate_request(
|
|
672
672
|
head_body_field,
|
|
673
673
|
head_dependant,
|
|
674
|
-
route.path,
|
|
675
674
|
request,
|
|
676
675
|
response,
|
|
677
676
|
request_info,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# API Version Parameter
|
|
2
2
|
|
|
3
3
|
Cadwyn adds another routing layer to FastAPI by default: by version parameter. This means that before FastAPI tries to route us to the correct route, Cadwyn will first decide on which version of the route to use based on a version parameter. Feel free to look at the example app with URL path version prefixes and arbitrary strings as versions [here](../how_to/version_with_paths_and_numbers_instead_of_headers_and_dates.md).
|
|
4
4
|
|
|
@@ -91,3 +91,21 @@ If the app has two versions: 2022-01-02 and 2022-01-05, and the request date par
|
|
|
91
91
|
Exact match is always preferred over partial match and a request will never be matched to the higher versioned route.
|
|
92
92
|
|
|
93
93
|
We implement routing like this because Cadwyn was born in a microservice architecture and it is extremely convenient to have waterfalling there. For example, imagine that you have two Cadwyn services: Payables and Receivables, each defining its own API versions. Payables service might contain 10 versions while receivables service might contain only 2 versions because it didn't need as many breaking changes. If a client requests a version that does not exist in receivables -- we will just waterfall to some earlier version, making receivables behavior consistent even if API keeps getting new versions.
|
|
94
|
+
|
|
95
|
+
## API Version Parameter Title and Description
|
|
96
|
+
|
|
97
|
+
You can pass a title and/or a description to `Cadwyn` constructor. They are equivalent to passing `title` and `description` to `fastapi.Path` or `fastapi.Header` constructors.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
app = Cadwyn(
|
|
101
|
+
...,
|
|
102
|
+
api_version_title="My Great API version parameter",
|
|
103
|
+
api_version_description="Description of my great API version parameter",
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## API Version Context Variables
|
|
108
|
+
|
|
109
|
+
Cadwyn automatically converts your data to a correct version and has "version checks" when dealing with side effects as described in [the section above](./version_changes.md#version-changes-with-side-effects). It can only do so using a special [context variable](https://docs.python.org/3/library/contextvars.html) that stores the current API version.
|
|
110
|
+
|
|
111
|
+
You can also pass a different compatible contextvar to your `cadwyn.VersionBundle` constructor.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Endpoint migrations
|
|
2
2
|
|
|
3
|
-
Note that the endpoint constructor contains a second argument that describes the methods of the endpoints you would like to edit. If you have two routes for a single endpoint and you put both of their methods into the instruction -- both of them are going to be changed as you would expect.
|
|
3
|
+
Note that the `endpoint(...)` constructor contains a second argument that describes the methods of the endpoints you would like to edit. If you have two routes for a single endpoint and you put both of their methods into the instruction -- both of them are going to be changed as you would expect.
|
|
4
4
|
|
|
5
5
|
## Defining endpoints that didn't exist in new versions
|
|
6
6
|
|
|
7
|
-
If you had an endpoint in old version but
|
|
7
|
+
If you had an endpoint in an old version **but want to delete it in a new version**, you must still define it as usual with all your other endpoints but mark it as deleted.
|
|
8
8
|
|
|
9
9
|
```python
|
|
10
10
|
@router.only_exists_in_older_versions
|
|
@@ -28,7 +28,7 @@ class MyChange(VersionChange):
|
|
|
28
28
|
|
|
29
29
|
## Defining endpoints that didn't exist in old versions
|
|
30
30
|
|
|
31
|
-
If you have an endpoint in your new version that must not exist in older versions, you define it as usual and then mark it as "non-existing" in old versions
|
|
31
|
+
If you have an endpoint in your new version that must not exist in older versions for some reason, you define it as usual and then mark it as "non-existing" in old versions. Note that this is [not the recommended approach when adding new endpoints](../how_to/change_endpoints/index.md#add-a-new-endpoint).
|
|
32
32
|
|
|
33
33
|
```python
|
|
34
34
|
from cadwyn import VersionChange, endpoint
|
|
@@ -58,6 +58,8 @@ class MyChange(VersionChange):
|
|
|
58
58
|
)
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
However, you only need to have a migration if it is a breaking change for your users.
|
|
62
|
+
|
|
61
63
|
### Dependency alteration warning
|
|
62
64
|
|
|
63
65
|
Note that changing endpoint `dependencies` is only going to affect the initial validation. So Cadwyn will take your altered dependencies and run them on each request to the endpoint but ultimately your endpoint code is always going to use the HEAD version of your dependencies. So be careful.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
All of the following instructions affect only openapi schemas and their initial validation. All of your incoming requests will still be converted into your HEAD schemas.
|
|
4
4
|
|
|
5
|
+
Please note that you only need to have a migration if it is a breaking change for your users. The scenarios below only describe "what you can do" but not "what you should do". For the "should", please refer to the [how-to docs](../how_to/change_openapi_schemas/add_field.md).
|
|
6
|
+
|
|
5
7
|
## Add a field to the older version
|
|
6
8
|
|
|
7
9
|
```python
|
|
@@ -93,6 +93,8 @@ HEAD is very similar to your latest version with a few key differences:
|
|
|
93
93
|
|
|
94
94
|
`VersionChange` classes describe each atomic group of business capabilities that you have altered in a version.
|
|
95
95
|
|
|
96
|
+
Note, however, that you only need to have a migration if it is a breaking change for your users. If you add a new endpoint or add a new field to your response schema, you do not need to have a migration for it because your users' code will not break. So by not having a migration you automatically add this change to all versions.
|
|
97
|
+
|
|
96
98
|
### VersionChange.\_\_name\_\_
|
|
97
99
|
|
|
98
100
|
The name of the version change, `RemoveTaxIDEndpoints`, describes what breaking change has happened. It must be a verb and it is the best resource for your new developers to quickly understand what happened between the versions. Do not be shy to use really long names -- it is better to have a long name than to create a misunderstanding. Avoid generic names such as `RefactorUserFields`. Better have an ugly name such as `RenameCreationDatetimeAndUpdateDatetimeToCreatedAtAndUpdatedAt` then to have a generic name such as `RefactorFields`. Because after just a few of such version changes, your versioning structure can become completely unreadable:
|
|
@@ -14,7 +14,7 @@ Now you have everything you need at your disposal: field `created_at` is availab
|
|
|
14
14
|
|
|
15
15
|
Let's say we want our users to be able to specify a middle name but it is nullable. It is not a breaking change so no new version is necessary whether it is requests or responses.
|
|
16
16
|
|
|
17
|
-
You just need to add a nullable `middle_name` field into `users.BaseUser`
|
|
17
|
+
You just need to add a nullable `middle_name` field into `users.BaseUser` as if you were working with a barebones FastAPI app.
|
|
18
18
|
|
|
19
19
|
### Field is required
|
|
20
20
|
|
|
@@ -59,7 +59,6 @@ Let's say that our users had a field `country` that defaulted to `USA` but our p
|
|
|
59
59
|
)
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
63
62
|
That's it! Our old schemas will now contain a default but in HEAD country will be required. You might notice a weirdness: if we set a default in the old version, why would we also write a migration? That's because of a sad implementation detail of pydantic that [prevents us](../../concepts/schema_migrations.md#change-a-field-in-the-older-version) from using defaults from old versions.
|
|
64
63
|
|
|
65
64
|
#### With incompatible default value in older versions
|
|
@@ -118,5 +117,4 @@ So we will make `phone` nullable in HEAD, then make it required in `latest`, and
|
|
|
118
117
|
)
|
|
119
118
|
```
|
|
120
119
|
|
|
121
|
-
|
|
122
120
|
See how we didn't remove the `phone` field from old versions? Instead, we allowed a nullable `phone` field to be passed into both old `UserResource` and old `UserCreateRequest`. This gives our users new functionality without needing to update their API version! It is one of the best parts of Cadwyn's approach: our users can get years worth of updates without switching their API version and without their integration getting broken.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Cadwyn uses version headers with ISO dates by default for versioning. However, you can use any strings instead of ISO dates and/or you can use path version prefixes instead of version headers. Here's our quickstart tutorial example but using version numbers and path prefixes:
|
|
4
4
|
|
|
5
5
|
Feel free to mix and match the API version formats and version locations as you see fit.
|
|
6
|
-
But beware that Cadwyn does not support [version waterfalling](../concepts/
|
|
6
|
+
But beware that Cadwyn does not support [version waterfalling](../concepts/api_version_parameter.md#api-version-waterfalling) for arbitrary strings as versions.
|
|
7
7
|
|
|
8
8
|
```python
|
|
9
9
|
{! ./docs_src/how_to/version_with_path_and_numbers_instead_of_headers_and_dates/block001.py !}
|
|
@@ -119,8 +119,7 @@ nav:
|
|
|
119
119
|
- "Endpoint migrations": "concepts/endpoint_migrations.md"
|
|
120
120
|
- "Enum migrations": "concepts/enum_migrations.md"
|
|
121
121
|
- "Schema migrations": "concepts/schema_migrations.md"
|
|
122
|
-
- "API Version parameter
|
|
123
|
-
- "Where to put the version and how to format it": "concepts/where_to_put_the_version_and_how_to_format_it.md"
|
|
122
|
+
- "API Version parameter": "concepts/api_version_parameter.md"
|
|
124
123
|
- "Changelogs": "concepts/changelogs.md"
|
|
125
124
|
- "Testing": concepts/testing.md
|
|
126
125
|
- "Contributing": "home/CONTRIBUTING.md"
|
|
@@ -20,11 +20,13 @@ from starlette.responses import FileResponse
|
|
|
20
20
|
from typing_extensions import Any, NewType, TypeAlias, TypeAliasType, get_args
|
|
21
21
|
|
|
22
22
|
from cadwyn import VersionBundle, VersionedAPIRouter
|
|
23
|
+
from cadwyn.applications import Cadwyn
|
|
23
24
|
from cadwyn.dependencies import current_dependency_solver
|
|
24
25
|
from cadwyn.exceptions import CadwynError, RouterGenerationError, RouterPathParamsModifiedError
|
|
25
26
|
from cadwyn.route_generation import generate_versioned_routers
|
|
26
27
|
from cadwyn.schema_generation import generate_versioned_models
|
|
27
28
|
from cadwyn.structure import Version, convert_request_to_next_version_for, endpoint, schema
|
|
29
|
+
from cadwyn.structure.data import RequestInfo, ResponseInfo, convert_response_to_previous_version_for
|
|
28
30
|
from cadwyn.structure.enums import enum
|
|
29
31
|
from cadwyn.structure.versions import VersionChange
|
|
30
32
|
from tests._data.unversioned_schema_dir import UnversionedSchema2
|
|
@@ -252,6 +254,50 @@ def test__endpoint_had_another_path_variable(
|
|
|
252
254
|
)
|
|
253
255
|
|
|
254
256
|
|
|
257
|
+
def test__endpoint_had__another_path_with_the_other_migration_at_the_same_time__should_require_old_name(
|
|
258
|
+
create_versioned_app: CreateVersionedApp,
|
|
259
|
+
):
|
|
260
|
+
router = VersionedAPIRouter()
|
|
261
|
+
|
|
262
|
+
@router.post("/A")
|
|
263
|
+
async def test_endpoint_post(body: list[str]):
|
|
264
|
+
return body
|
|
265
|
+
|
|
266
|
+
@router.put("/A")
|
|
267
|
+
async def test_endpoint_put(body: list[str]):
|
|
268
|
+
return body
|
|
269
|
+
|
|
270
|
+
@convert_response_to_previous_version_for("/A", ["POST"])
|
|
271
|
+
def response_migration(response: ResponseInfo):
|
|
272
|
+
response.body.append("response")
|
|
273
|
+
|
|
274
|
+
@convert_request_to_next_version_for("/A", ["POST"])
|
|
275
|
+
def request_migration(request: RequestInfo):
|
|
276
|
+
request.body.append("request")
|
|
277
|
+
|
|
278
|
+
app = Cadwyn(
|
|
279
|
+
versions=VersionBundle(
|
|
280
|
+
Version(
|
|
281
|
+
"2001-01-01",
|
|
282
|
+
version_change(
|
|
283
|
+
endpoint("/A", ["POST"]).had(path="/B"),
|
|
284
|
+
request_migration=request_migration,
|
|
285
|
+
response_migration=response_migration,
|
|
286
|
+
),
|
|
287
|
+
),
|
|
288
|
+
Version("2000-01-01"),
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
app.generate_and_include_versioned_routers(router)
|
|
292
|
+
|
|
293
|
+
with TestClient(app) as client:
|
|
294
|
+
assert client.post("/B", headers={"X-API-VERSION": "2000-01-01"}, json=[]).json() == ["request", "response"]
|
|
295
|
+
assert client.post("/A", headers={"X-API-VERSION": "2001-01-01"}, json=[]).json() == []
|
|
296
|
+
|
|
297
|
+
assert client.put("/A", headers={"X-API-VERSION": "2000-01-01"}, json=[]).json() == []
|
|
298
|
+
assert client.put("/A", headers={"X-API-VERSION": "2001-01-01"}, json=[]).json() == []
|
|
299
|
+
|
|
300
|
+
|
|
255
301
|
def test__endpoint_had_dependencies(
|
|
256
302
|
test_endpoint: Endpoint,
|
|
257
303
|
test_path: str,
|
|
@@ -123,9 +123,14 @@ async def get_user_addresses(user_id: uuid.UUID):
|
|
|
123
123
|
return {"data": database_parody[f"addr_{user_id}"]}
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
app = Cadwyn(
|
|
126
|
+
app = Cadwyn(
|
|
127
|
+
versions=version_bundle,
|
|
128
|
+
title="My amazing API",
|
|
129
|
+
api_version_title="My Great API version parameter",
|
|
130
|
+
api_version_description="Description of my great API version parameter",
|
|
131
|
+
)
|
|
127
132
|
app.generate_and_include_versioned_routers(router)
|
|
128
133
|
|
|
129
134
|
|
|
130
135
|
if __name__ == "__main__":
|
|
131
|
-
uvicorn.run(app)
|
|
136
|
+
uvicorn.run(app, port=8011)
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
# API Version parameter and context variables
|
|
2
|
-
|
|
3
|
-
Cadwyn automatically converts your data to a correct version and has "version checks" when dealing with side effects as described in [the section above](./version_changes.md#version-changes-with-side-effects). It can only do so using a special [context variable](https://docs.python.org/3/library/contextvars.html) that stores the current API version.
|
|
4
|
-
|
|
5
|
-
You can also pass a different compatible contextvar to your `cadwyn.VersionBundle` constructor.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/change_schema_without_endpoint.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cadwyn-5.2.1 → cadwyn-5.3.0}/docs/how_to/change_openapi_schemas/rename_a_field_in_schema.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cadwyn-5.2.1 → cadwyn-5.3.0}/tests/test_schema_generation/test_schema_with_future_annotations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|