cadwyn 4.4.0__tar.gz → 4.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cadwyn might be problematic. Click here for more details.
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.github/workflows/ci.yaml +2 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.github/workflows/daily_tests.yaml +9 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/CHANGELOG.md +12 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/Makefile +1 -1
- {cadwyn-4.4.0 → cadwyn-4.4.2}/PKG-INFO +3 -2
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/route_generation.py +26 -6
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/schema_generation.py +20 -7
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/structure/versions.py +2 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/pyproject.toml +13 -11
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/conftest.py +1 -1
- cadwyn-4.4.2/tests/test_auth_dependencies.py +67 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_render.py +10 -5
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_schema_generation/test_schema_validator.py +11 -5
- {cadwyn-4.4.0 → cadwyn-4.4.2}/uv.lock +259 -211
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.github/CODE_OF_CONDUCT.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.github/actions/setup-python-uv/action.yaml +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.github/workflows/publish_docs.yaml +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.github/workflows/release.yaml +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.github/workflows/validate_links.yaml +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.gitignore +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/.pre-commit-config.yaml +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/LICENSE +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/README.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/__main__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/_asts.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/_importer.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/_render.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/_utils.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/applications.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/changelogs.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/exceptions.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/middleware.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/py.typed +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/routing.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/static/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/static/docs.html +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/structure/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/structure/common.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/structure/data.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/structure/endpoints.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/structure/enums.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/cadwyn/structure/schemas.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/CNAME +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/api_version_header_and_context_variables.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/beware_of_data_versioning.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/changelogs.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/cli.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/endpoint_migrations.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/enum_migrations.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/index.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/main_app.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/methodology.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/schema_generation.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/schema_migrations.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/testing.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/concepts/version_changes.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/home/CONTRIBUTING.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/how_to/change_business_logic/index.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/how_to/change_endpoints/index.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/how_to/change_openapi_schemas/add_field.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/how_to/change_openapi_schemas/change_field_type.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/how_to/change_openapi_schemas/changing_constraints.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/how_to/change_openapi_schemas/remove_field.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/how_to/change_openapi_schemas/rename_a_field_in_schema.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/how_to/index.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/img/dashboard_with_one_version.png +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/img/dashboard_with_two_versions.png +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/img/get_users_endpoint_from_prior_version.png +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/img/simplified_migration_model.png +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/img/sponsor_logos/monite.png +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/img/unversioned_dashboard.png +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/index.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/plugin.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/quickstart/setup.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/quickstart/tutorial.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/theory/how_to_build_versioning_framework.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/theory/how_we_got_here.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs/theory/literature.md +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/setup/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/setup/block001.sh +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/setup/block002.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/setup/tests/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/setup/tests/test_block002.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/tutorial/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/tutorial/block001.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/tutorial/block002.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/tutorial/block003.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/tutorial/tests/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/tutorial/tests/test_block001.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/tutorial/tests/test_block002.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/docs_src/quickstart/tutorial/tests/test_block003.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/mkdocs.yml +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/ruff.toml +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/scripts/fix_links.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/scripts/split_md.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_data/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_data/unversioned_schema_dir/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_data/unversioned_schema_dir/unversioned_schemas.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_data/unversioned_schemas.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/app_for_testing_routing.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/render/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/render/classes.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/render/complex/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/render/complex/classes.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/render/complex/versions.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/render/versions.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/utils.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/versioned_app/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/versioned_app/app.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/versioned_app/v2021_01_01.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/versioned_app/v2022_01_02.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/_resources/versioned_app/webhooks.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_applications.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_changelog.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_cli.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_data_migrations.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_router_generation.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_routing.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_schema_generation/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_schema_generation/test_enum.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_schema_generation/test_schema.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_schema_generation/test_schema_field.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/test_structure.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/tutorial/__init__.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/tutorial/main.py +0 -0
- {cadwyn-4.4.0 → cadwyn-4.4.2}/tests/tutorial/test_example.py +0 -0
|
@@ -10,12 +10,21 @@ jobs:
|
|
|
10
10
|
update-dependencies-and-test:
|
|
11
11
|
name: Update dependencies and run tests
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: true
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
13
17
|
|
|
14
18
|
steps:
|
|
15
19
|
- uses: actions/checkout@v4
|
|
16
20
|
- uses: ./.github/actions/setup-python-uv
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
17
23
|
- run: uv sync --refresh --all-extras --dev --upgrade
|
|
18
24
|
- run: pytest .
|
|
25
|
+
- uses: jakebailey/pyright-action@v1
|
|
26
|
+
with:
|
|
27
|
+
pylance-version: latest-release
|
|
19
28
|
|
|
20
29
|
notify-on-failure:
|
|
21
30
|
name: Notify on failure
|
|
@@ -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.2]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* Non-function (object instances) dependencies not supporting dependency overrides in testing
|
|
13
|
+
|
|
14
|
+
## [4.4.1]
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
* Python 3.13 to CI
|
|
19
|
+
|
|
8
20
|
## [4.4.0]
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: cadwyn
|
|
3
|
-
Version: 4.4.
|
|
3
|
+
Version: 4.4.2
|
|
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
|
|
7
7
|
Author-email: Stanislav Zmiev <zmievsa@gmail.com>
|
|
8
8
|
License-Expression: MIT
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Keywords: api,api-versioning,code-generation,fastapi,hints,json-schema,pydantic,python,python310,python311,python312,stripe,versioning
|
|
10
|
+
Keywords: api,api-versioning,code-generation,fastapi,hints,json-schema,pydantic,python,python310,python311,python312,python313,stripe,versioning
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Environment :: Web Environment
|
|
13
13
|
Classifier: Framework :: AsyncIO
|
|
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
25
|
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
27
|
Classifier: Topic :: Internet
|
|
27
28
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
28
29
|
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
from collections.abc import Callable, Sequence
|
|
4
|
-
from copy import deepcopy
|
|
4
|
+
from copy import copy, deepcopy
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from typing import (
|
|
7
7
|
TYPE_CHECKING,
|
|
@@ -49,6 +49,7 @@ if TYPE_CHECKING:
|
|
|
49
49
|
_Call = TypeVar("_Call", bound=Callable[..., Any])
|
|
50
50
|
_R = TypeVar("_R", bound=APIRouter)
|
|
51
51
|
_WR = TypeVar("_WR", bound=APIRouter, default=APIRouter)
|
|
52
|
+
_RouteT = TypeVar("_RouteT", bound=BaseRoute)
|
|
52
53
|
# This is a hack we do because we can't guarantee how the user will use the router.
|
|
53
54
|
_DELETED_ROUTE_TAG = "_CADWYN_DELETED_ROUTE"
|
|
54
55
|
|
|
@@ -90,6 +91,25 @@ class VersionedAPIRouter(fastapi.routing.APIRouter):
|
|
|
90
91
|
return endpoint
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
def copy_router(router: _R) -> _R:
|
|
95
|
+
router = copy(router)
|
|
96
|
+
router.routes = [copy_route(r) for r in router.routes]
|
|
97
|
+
return router
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def copy_route(route: _RouteT) -> _RouteT:
|
|
101
|
+
if not isinstance(route, APIRoute):
|
|
102
|
+
return copy(route)
|
|
103
|
+
|
|
104
|
+
# This is slightly wasteful in terms of resources but it makes it easy for us
|
|
105
|
+
# to make sure that new versions of FastAPI are going to be supported even if
|
|
106
|
+
# APIRoute gets new attributes.
|
|
107
|
+
new_route = deepcopy(route)
|
|
108
|
+
new_route.dependant = copy(route.dependant)
|
|
109
|
+
new_route.dependencies = copy(route.dependencies)
|
|
110
|
+
return new_route
|
|
111
|
+
|
|
112
|
+
|
|
93
113
|
class _EndpointTransformer(Generic[_R, _WR]):
|
|
94
114
|
def __init__(self, parent_router: _R, versions: VersionBundle, webhooks: _WR) -> None:
|
|
95
115
|
super().__init__()
|
|
@@ -103,8 +123,8 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
103
123
|
]
|
|
104
124
|
|
|
105
125
|
def transform(self) -> GeneratedRouters[_R, _WR]:
|
|
106
|
-
router =
|
|
107
|
-
webhook_router =
|
|
126
|
+
router = copy_router(self.parent_router)
|
|
127
|
+
webhook_router = copy_router(self.parent_webhooks_router)
|
|
108
128
|
routers: dict[VersionDate, _R] = {}
|
|
109
129
|
webhook_routers: dict[VersionDate, _WR] = {}
|
|
110
130
|
|
|
@@ -117,8 +137,8 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
117
137
|
routers[version.value] = router
|
|
118
138
|
webhook_routers[version.value] = webhook_router
|
|
119
139
|
# Applying changes for the next version
|
|
120
|
-
router =
|
|
121
|
-
webhook_router =
|
|
140
|
+
router = copy_router(router)
|
|
141
|
+
webhook_router = copy_router(webhook_router)
|
|
122
142
|
self._apply_endpoint_changes_to_router(router.routes + webhook_router.routes, version)
|
|
123
143
|
|
|
124
144
|
if self.routes_that_never_existed:
|
|
@@ -134,7 +154,7 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
134
154
|
if not isinstance(head_route, APIRoute):
|
|
135
155
|
continue
|
|
136
156
|
_add_request_and_response_params(head_route)
|
|
137
|
-
copy_of_dependant =
|
|
157
|
+
copy_of_dependant = copy(head_route.dependant)
|
|
138
158
|
|
|
139
159
|
for older_router in list(routers.values()):
|
|
140
160
|
older_route = older_router.routes[route_index]
|
|
@@ -367,6 +367,10 @@ class _PydanticModelWrapper(Generic[_T_PYDANTIC_MODEL]):
|
|
|
367
367
|
return model_copy
|
|
368
368
|
|
|
369
369
|
|
|
370
|
+
def is_regular_function(call: Callable):
|
|
371
|
+
return isinstance(call, types.FunctionType | types.MethodType)
|
|
372
|
+
|
|
373
|
+
|
|
370
374
|
class _CallableWrapper:
|
|
371
375
|
"""__eq__ and __hash__ are needed to make sure that dependency overrides work correctly.
|
|
372
376
|
They are based on putting dependencies (functions) as keys for the dictionary so if we want to be able to
|
|
@@ -376,6 +380,8 @@ class _CallableWrapper:
|
|
|
376
380
|
def __init__(self, original_callable: Callable) -> None:
|
|
377
381
|
super().__init__()
|
|
378
382
|
self._original_callable = original_callable
|
|
383
|
+
if not is_regular_function(original_callable):
|
|
384
|
+
original_callable = original_callable.__call__
|
|
379
385
|
functools.update_wrapper(self, original_callable)
|
|
380
386
|
|
|
381
387
|
@property
|
|
@@ -458,6 +464,12 @@ class _AnnotationTransformer:
|
|
|
458
464
|
def _change_version_of_a_non_container_annotation(self, annotation: Any) -> Any:
|
|
459
465
|
if isinstance(annotation, _BaseGenericAlias | types.GenericAlias):
|
|
460
466
|
return get_origin(annotation)[tuple(self.change_version_of_annotation(arg) for arg in get_args(annotation))]
|
|
467
|
+
elif isinstance(annotation, fastapi.params.Security):
|
|
468
|
+
return fastapi.params.Security(
|
|
469
|
+
self.change_version_of_annotation(annotation.dependency),
|
|
470
|
+
scopes=annotation.scopes,
|
|
471
|
+
use_cache=annotation.use_cache,
|
|
472
|
+
)
|
|
461
473
|
elif isinstance(annotation, fastapi.params.Depends):
|
|
462
474
|
return fastapi.params.Depends(
|
|
463
475
|
self.change_version_of_annotation(annotation.dependency),
|
|
@@ -475,7 +487,7 @@ class _AnnotationTransformer:
|
|
|
475
487
|
elif callable(annotation):
|
|
476
488
|
if type(annotation).__module__.startswith(
|
|
477
489
|
("fastapi.", "pydantic.", "pydantic_core.", "starlette.")
|
|
478
|
-
) or isinstance(annotation, fastapi.
|
|
490
|
+
) or isinstance(annotation, fastapi.security.base.SecurityBase):
|
|
479
491
|
return annotation
|
|
480
492
|
|
|
481
493
|
def modifier(annotation: Any):
|
|
@@ -568,8 +580,12 @@ class _AnnotationTransformer:
|
|
|
568
580
|
def _copy_function_through_class_based_wrapper(cls, call: Any):
|
|
569
581
|
"""Separate from copy_endpoint because endpoints MUST be functions in FastAPI, they cannot be cls instances"""
|
|
570
582
|
call = cls._unwrap_callable(call)
|
|
571
|
-
|
|
572
|
-
|
|
583
|
+
if not is_regular_function(call):
|
|
584
|
+
# This means that the callable is actually an instance of a regular class
|
|
585
|
+
actual_call = call.__call__
|
|
586
|
+
else:
|
|
587
|
+
actual_call = call
|
|
588
|
+
if inspect.iscoroutinefunction(actual_call):
|
|
573
589
|
return _AsyncCallableWrapper(call)
|
|
574
590
|
else:
|
|
575
591
|
return _CallableWrapper(call)
|
|
@@ -578,9 +594,6 @@ class _AnnotationTransformer:
|
|
|
578
594
|
def _unwrap_callable(call: Any) -> Any:
|
|
579
595
|
while hasattr(call, "_original_callable"):
|
|
580
596
|
call = call._original_callable
|
|
581
|
-
if not isinstance(call, types.FunctionType | types.MethodType):
|
|
582
|
-
# This means that the callable is actually an instance of a regular class
|
|
583
|
-
call = call.__call__
|
|
584
597
|
|
|
585
598
|
return call
|
|
586
599
|
|
|
@@ -607,7 +620,7 @@ class SchemaGenerator:
|
|
|
607
620
|
|
|
608
621
|
def __getitem__(self, model: type[_T_ANY_MODEL], /) -> type[_T_ANY_MODEL]:
|
|
609
622
|
if not isinstance(model, type) or not issubclass(model, BaseModel | Enum) or model in (BaseModel, RootModel):
|
|
610
|
-
return model
|
|
623
|
+
return model
|
|
611
624
|
model = _unwrap_model(model)
|
|
612
625
|
|
|
613
626
|
if model in self.concrete_models:
|
|
@@ -156,6 +156,8 @@ class VersionChange:
|
|
|
156
156
|
"instructions_to_migrate_to_previous_version",
|
|
157
157
|
"__module__",
|
|
158
158
|
"__doc__",
|
|
159
|
+
"__firstlineno__",
|
|
160
|
+
"__static_attributes__",
|
|
159
161
|
}:
|
|
160
162
|
raise CadwynStructureError(
|
|
161
163
|
f"Found: '{attr_name}' attribute of type '{type(attr_value)}' in '{cls.__name__}'."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cadwyn"
|
|
3
|
-
version = "4.4.
|
|
3
|
+
version = "4.4.2"
|
|
4
4
|
description = "Production-ready community-driven modern Stripe-like API versioning in FastAPI"
|
|
5
5
|
authors = [{ name = "Stanislav Zmiev", email = "zmievsa@gmail.com" }]
|
|
6
6
|
license = "MIT"
|
|
@@ -20,6 +20,7 @@ keywords = [
|
|
|
20
20
|
"python310",
|
|
21
21
|
"python311",
|
|
22
22
|
"python312",
|
|
23
|
+
"python313",
|
|
23
24
|
]
|
|
24
25
|
classifiers = [
|
|
25
26
|
"Intended Audience :: Information Technology",
|
|
@@ -30,6 +31,7 @@ classifiers = [
|
|
|
30
31
|
"Programming Language :: Python :: 3.10",
|
|
31
32
|
"Programming Language :: Python :: 3.11",
|
|
32
33
|
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
33
35
|
"Topic :: Internet",
|
|
34
36
|
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
35
37
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
@@ -56,24 +58,16 @@ dependencies = [
|
|
|
56
58
|
"typing-extensions>=4.8.0",
|
|
57
59
|
]
|
|
58
60
|
|
|
61
|
+
|
|
59
62
|
[project.optional-dependencies]
|
|
60
63
|
standard = ["fastapi[standard]>=0.112.3", "typer>=0.7.0"]
|
|
61
64
|
|
|
62
|
-
[project.urls]
|
|
63
|
-
"Source code" = "https://github.com/zmievsa/cadwyn"
|
|
64
|
-
Documentation = "https://docs.cadwyn.dev"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
[project.scripts]
|
|
68
|
-
cadwyn = "cadwyn.__main__:app"
|
|
69
|
-
|
|
70
65
|
[tool.uv]
|
|
71
66
|
dev-dependencies = [
|
|
72
67
|
"pdbpp ~=0.10.3",
|
|
73
68
|
"python-multipart >=0.0.6",
|
|
74
69
|
"better-devtools ~=0.13.3",
|
|
75
70
|
"pytest-sugar ~=1.0.0",
|
|
76
|
-
|
|
77
71
|
# tests
|
|
78
72
|
"svcs ~=24.1.0",
|
|
79
73
|
"httpx >=0.26.0",
|
|
@@ -82,7 +76,6 @@ dev-dependencies = [
|
|
|
82
76
|
"pytest-cov >=4.0.0",
|
|
83
77
|
"dirty-equals >=0.6.0",
|
|
84
78
|
"uvicorn ~=0.23.0",
|
|
85
|
-
|
|
86
79
|
# docs
|
|
87
80
|
"mkdocs >=1.5.2",
|
|
88
81
|
"mkdocs-material >=9.3.1",
|
|
@@ -91,6 +84,15 @@ dev-dependencies = [
|
|
|
91
84
|
"mike >=2.1.2, <3",
|
|
92
85
|
]
|
|
93
86
|
|
|
87
|
+
[project.urls]
|
|
88
|
+
"Source code" = "https://github.com/zmievsa/cadwyn"
|
|
89
|
+
Documentation = "https://docs.cadwyn.dev"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
[project.scripts]
|
|
93
|
+
cadwyn = "cadwyn.__main__:app"
|
|
94
|
+
|
|
95
|
+
|
|
94
96
|
[tool.coverage.run]
|
|
95
97
|
data_file = "coverage/coverage"
|
|
96
98
|
parallel = true
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import NoReturn
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Security
|
|
5
|
+
from fastapi.testclient import TestClient
|
|
6
|
+
|
|
7
|
+
from cadwyn.applications import Cadwyn
|
|
8
|
+
from cadwyn.structure.versions import Version, VersionBundle
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ScarySecurity:
|
|
12
|
+
"""It's IMPORTANT that we use an instance of a class instead of a function because it can be properly copied.
|
|
13
|
+
|
|
14
|
+
It's an edge case.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
async def __call__(self) -> NoReturn:
|
|
18
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
scary_security = ScarySecurity()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def cadwyn_app():
|
|
26
|
+
router = APIRouter()
|
|
27
|
+
|
|
28
|
+
@router.get("/hello")
|
|
29
|
+
async def world(user=Security(scary_security)):
|
|
30
|
+
return {"hello": "world", "user": user}
|
|
31
|
+
|
|
32
|
+
app = Cadwyn(versions=VersionBundle(Version("2023-11-16")))
|
|
33
|
+
app.include_router(router)
|
|
34
|
+
app.generate_and_include_versioned_routers(router)
|
|
35
|
+
|
|
36
|
+
return app
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def user_client(cadwyn_app: Cadwyn):
|
|
41
|
+
with TestClient(app=cadwyn_app, base_url="http://test") as ac:
|
|
42
|
+
yield ac
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def _override_auth_dependency(cadwyn_app: Cadwyn) -> None:
|
|
47
|
+
cadwyn_app.dependency_overrides[scary_security] = lambda: {"id": 123}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test__no_dependency_overrides_with_unversioned_routes(user_client: TestClient):
|
|
51
|
+
response = user_client.get("/hello")
|
|
52
|
+
assert response.status_code == 401, response.json()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test__no_dependency_overrides_with_versioned_routes(user_client: TestClient):
|
|
56
|
+
response = user_client.get("/hello", headers={"x-api-version": "2023-11-16"})
|
|
57
|
+
assert response.status_code == 401, response.json()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test__dependency_overrides_with_unversioned_routes(user_client: TestClient, _override_auth_dependency: None):
|
|
61
|
+
response = user_client.get("/hello")
|
|
62
|
+
assert response.status_code == 200, response.json()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test__dependency_overrides_with_versioned_routes(user_client: TestClient, _override_auth_dependency: None):
|
|
66
|
+
response = user_client.get("/hello", headers={"x-api-version": "2023-11-16"})
|
|
67
|
+
assert response.status_code == 200, response.json()
|
|
@@ -19,11 +19,16 @@ def test__render_model__with_weird_types():
|
|
|
19
19
|
)
|
|
20
20
|
# TODO: sobolevn has created a tool for doing such nocovers in a better manner.
|
|
21
21
|
# hopefully someday we will switch to it.
|
|
22
|
-
if sys.version_info >= (3, 11):
|
|
22
|
+
if sys.version_info >= (3, 11):
|
|
23
23
|
rendered_lambda = "lambda: 83"
|
|
24
|
-
else:
|
|
24
|
+
else:
|
|
25
25
|
rendered_lambda = "lambda : 83"
|
|
26
26
|
|
|
27
|
+
if sys.version_info >= (3, 13):
|
|
28
|
+
rend_ann = "typing.Annotated"
|
|
29
|
+
else:
|
|
30
|
+
rend_ann = "Annotated"
|
|
31
|
+
|
|
27
32
|
# TODO: As you see, we do not rename bases correctly in render. We gotta fix it some day...
|
|
28
33
|
assert code(result) == code(
|
|
29
34
|
f'''
|
|
@@ -32,11 +37,11 @@ class ModelWithWeirdFields(A):
|
|
|
32
37
|
foo: dict = Field(default={{'a': 'b'}})
|
|
33
38
|
bar: list[int] = Field(default_factory=my_default_factory)
|
|
34
39
|
baz: typing.Literal[MyEnum.foo] = Field()
|
|
35
|
-
saz:
|
|
36
|
-
laz:
|
|
40
|
+
saz: {rend_ann}[str, StringConstraints(to_upper=True)] = Field()
|
|
41
|
+
laz: {rend_ann}[int, None, Interval(gt=12, ge=None, lt=None, le=None), None] = Field()
|
|
37
42
|
taz: typing.Union[int, str, None] = Field(default_factory={rendered_lambda})
|
|
38
43
|
naz: list[int] = Field(default=[1, 2, 3])
|
|
39
|
-
gaz:
|
|
44
|
+
gaz: {rend_ann}[bytes, Strict(strict=True), Len(min_length=0, max_length=None)] = Field(min_length=3, title='Hewwo')
|
|
40
45
|
'''
|
|
41
46
|
)
|
|
42
47
|
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
-
from pydantic import
|
|
4
|
+
from pydantic import (
|
|
5
|
+
BaseModel,
|
|
6
|
+
field_validator,
|
|
7
|
+
model_validator,
|
|
8
|
+
root_validator, # pyright: ignore[reportDeprecated]
|
|
9
|
+
validator, # pyright: ignore[reportDeprecated]
|
|
10
|
+
)
|
|
5
11
|
|
|
6
12
|
from cadwyn.exceptions import InvalidGenerationInstructionError
|
|
7
13
|
from cadwyn.structure import schema
|
|
@@ -58,12 +64,12 @@ def test__schema_validator_existed__with_deprecated_validators(
|
|
|
58
64
|
):
|
|
59
65
|
with pytest.warns(DeprecationWarning):
|
|
60
66
|
|
|
61
|
-
@root_validator(pre=True)
|
|
67
|
+
@root_validator(pre=True) # pyright: ignore[reportDeprecated]
|
|
62
68
|
def hewwo(cls, values):
|
|
63
69
|
values["foo"] += "_root"
|
|
64
70
|
return values
|
|
65
71
|
|
|
66
|
-
@validator("foo")
|
|
72
|
+
@validator("foo") # pyright: ignore[reportDeprecated]
|
|
67
73
|
def dawkness(cls, value):
|
|
68
74
|
return value + "_field"
|
|
69
75
|
|
|
@@ -178,7 +184,7 @@ def test__schema_field_didnt_exist__with_validator_that_covers_multiple_fields__
|
|
|
178
184
|
|
|
179
185
|
with pytest.warns(DeprecationWarning):
|
|
180
186
|
|
|
181
|
-
@validator("bar")
|
|
187
|
+
@validator("bar") # pyright: ignore[reportDeprecated]
|
|
182
188
|
def validate_bar(cls, value):
|
|
183
189
|
raise NotImplementedError
|
|
184
190
|
|
|
@@ -191,7 +197,7 @@ def test__schema_field_didnt_exist__with_validator_that_covers_multiple_fields__
|
|
|
191
197
|
class ExpectedSchema(BaseModel):
|
|
192
198
|
bar: str
|
|
193
199
|
|
|
194
|
-
@validator("bar")
|
|
200
|
+
@validator("bar") # pyright: ignore[reportDeprecated]
|
|
195
201
|
def validate_bar(cls, value):
|
|
196
202
|
raise NotImplementedError
|
|
197
203
|
|