cadwyn 4.6.0a1__py3-none-any.whl → 5.0.0a1__py3-none-any.whl
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/_render.py +4 -1
- cadwyn/applications.py +114 -46
- cadwyn/changelogs.py +5 -6
- cadwyn/exceptions.py +2 -3
- cadwyn/middleware.py +76 -37
- cadwyn/route_generation.py +5 -5
- cadwyn/routing.py +48 -68
- cadwyn/schema_generation.py +8 -7
- cadwyn/structure/common.py +1 -2
- cadwyn/structure/versions.py +38 -41
- {cadwyn-4.6.0a1.dist-info → cadwyn-5.0.0a1.dist-info}/METADATA +1 -1
- cadwyn-5.0.0a1.dist-info/RECORD +28 -0
- cadwyn-4.6.0a1.dist-info/RECORD +0 -28
- {cadwyn-4.6.0a1.dist-info → cadwyn-5.0.0a1.dist-info}/WHEEL +0 -0
- {cadwyn-4.6.0a1.dist-info → cadwyn-5.0.0a1.dist-info}/entry_points.txt +0 -0
- {cadwyn-4.6.0a1.dist-info → cadwyn-5.0.0a1.dist-info}/licenses/LICENSE +0 -0
cadwyn/_render.py
CHANGED
|
@@ -146,7 +146,10 @@ def _generate_field_ast(field: PydanticFieldWrapper) -> ast.Call:
|
|
|
146
146
|
func=ast.Name("Field"),
|
|
147
147
|
args=[],
|
|
148
148
|
keywords=[
|
|
149
|
-
ast.keyword(
|
|
149
|
+
ast.keyword(
|
|
150
|
+
arg=attr,
|
|
151
|
+
value=ast.parse(get_fancy_repr(attr_value), mode="eval").body,
|
|
152
|
+
)
|
|
150
153
|
for attr, attr_value in field.passed_field_attributes.items()
|
|
151
154
|
],
|
|
152
155
|
)
|
cadwyn/applications.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
-
import
|
|
3
|
-
from collections.abc import Callable, Coroutine, Sequence
|
|
2
|
+
import warnings
|
|
3
|
+
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 Any, cast
|
|
7
|
+
from typing import TYPE_CHECKING, Annotated, Any, cast
|
|
8
8
|
|
|
9
|
+
import fastapi
|
|
9
10
|
from fastapi import APIRouter, FastAPI, HTTPException, routing
|
|
10
11
|
from fastapi.datastructures import Default
|
|
11
12
|
from fastapi.openapi.docs import (
|
|
@@ -23,15 +24,26 @@ from starlette.requests import Request
|
|
|
23
24
|
from starlette.responses import JSONResponse, Response
|
|
24
25
|
from starlette.routing import BaseRoute, Route
|
|
25
26
|
from starlette.types import Lifespan
|
|
26
|
-
from typing_extensions import Self
|
|
27
|
+
from typing_extensions import Self, assert_never, deprecated
|
|
27
28
|
|
|
28
29
|
from cadwyn._utils import same_definition_as_in
|
|
29
30
|
from cadwyn.changelogs import CadwynChangelogResource, _generate_changelog
|
|
30
|
-
from cadwyn.
|
|
31
|
+
from cadwyn.exceptions import CadwynStructureError
|
|
32
|
+
from cadwyn.middleware import (
|
|
33
|
+
APIVersionFormat,
|
|
34
|
+
APIVersionLocation,
|
|
35
|
+
HeaderVersionManager,
|
|
36
|
+
URLVersionManager,
|
|
37
|
+
VersionPickingMiddleware,
|
|
38
|
+
_generate_api_version_dependency,
|
|
39
|
+
)
|
|
31
40
|
from cadwyn.route_generation import generate_versioned_routers
|
|
32
|
-
from cadwyn.routing import
|
|
41
|
+
from cadwyn.routing import _RootCadwynAPIRouter
|
|
33
42
|
from cadwyn.structure import VersionBundle
|
|
34
43
|
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from cadwyn.structure.common import VersionType
|
|
46
|
+
|
|
35
47
|
CURR_DIR = Path(__file__).resolve()
|
|
36
48
|
logger = getLogger(__name__)
|
|
37
49
|
|
|
@@ -48,7 +60,18 @@ class Cadwyn(FastAPI):
|
|
|
48
60
|
self,
|
|
49
61
|
*,
|
|
50
62
|
versions: VersionBundle,
|
|
51
|
-
api_version_header_name:
|
|
63
|
+
api_version_header_name: Annotated[
|
|
64
|
+
str | None,
|
|
65
|
+
deprecated(
|
|
66
|
+
"api_version_header_name is deprecated and will be removed in the future. "
|
|
67
|
+
"Use api_version_parameter_name instead."
|
|
68
|
+
),
|
|
69
|
+
] = None,
|
|
70
|
+
api_version_location: APIVersionLocation = "custom_header",
|
|
71
|
+
api_version_format: APIVersionFormat = "date",
|
|
72
|
+
api_version_parameter_name: str = "x-api-version",
|
|
73
|
+
api_version_default_value: str | None | Callable[[Request], Awaitable[str]] = None,
|
|
74
|
+
versioning_middleware_class: type[VersionPickingMiddleware] = VersionPickingMiddleware,
|
|
52
75
|
changelog_url: str | None = "/changelog",
|
|
53
76
|
include_changelog_url_in_schema: bool = True,
|
|
54
77
|
debug: bool = False,
|
|
@@ -100,6 +123,15 @@ class Cadwyn(FastAPI):
|
|
|
100
123
|
self._dependency_overrides_provider = FakeDependencyOverridesProvider({})
|
|
101
124
|
self._cadwyn_initialized = False
|
|
102
125
|
|
|
126
|
+
if api_version_header_name is not None:
|
|
127
|
+
warnings.warn(
|
|
128
|
+
"api_version_header_name is deprecated and will be removed in the future. "
|
|
129
|
+
"Use api_version_parameter_name instead.",
|
|
130
|
+
DeprecationWarning,
|
|
131
|
+
stacklevel=2,
|
|
132
|
+
)
|
|
133
|
+
api_version_parameter_name = api_version_header_name
|
|
134
|
+
|
|
103
135
|
super().__init__(
|
|
104
136
|
debug=debug,
|
|
105
137
|
title=title,
|
|
@@ -137,6 +169,18 @@ class Cadwyn(FastAPI):
|
|
|
137
169
|
separate_input_output_schemas=separate_input_output_schemas,
|
|
138
170
|
**extra,
|
|
139
171
|
)
|
|
172
|
+
|
|
173
|
+
self._versioned_webhook_routers: dict[VersionType, APIRouter] = {}
|
|
174
|
+
self._latest_version_router = APIRouter(dependency_overrides_provider=self._dependency_overrides_provider)
|
|
175
|
+
|
|
176
|
+
self.changelog_url = changelog_url
|
|
177
|
+
self.include_changelog_url_in_schema = include_changelog_url_in_schema
|
|
178
|
+
|
|
179
|
+
self.docs_url = docs_url
|
|
180
|
+
self.redoc_url = redoc_url
|
|
181
|
+
self.openapi_url = openapi_url
|
|
182
|
+
self.redoc_url = redoc_url
|
|
183
|
+
|
|
140
184
|
self._kwargs_to_router: dict[str, Any] = {
|
|
141
185
|
"routes": routes,
|
|
142
186
|
"redirect_slashes": redirect_slashes,
|
|
@@ -152,32 +196,48 @@ class Cadwyn(FastAPI):
|
|
|
152
196
|
"responses": responses,
|
|
153
197
|
"generate_unique_id_function": generate_unique_id_function,
|
|
154
198
|
}
|
|
155
|
-
self.
|
|
199
|
+
self.api_version_format = api_version_format
|
|
200
|
+
self.api_version_parameter_name = api_version_parameter_name
|
|
201
|
+
self.api_version_pythonic_parameter_name = api_version_parameter_name.replace("-", "_")
|
|
202
|
+
if api_version_location == "custom_header":
|
|
203
|
+
self._api_version_manager = HeaderVersionManager(api_version_parameter_name=api_version_parameter_name)
|
|
204
|
+
self._api_version_fastapi_depends_class = fastapi.Header
|
|
205
|
+
elif api_version_location == "path":
|
|
206
|
+
self._api_version_manager = URLVersionManager(possible_version_values=self.versions._version_values_set)
|
|
207
|
+
self._api_version_fastapi_depends_class = fastapi.Path
|
|
208
|
+
else:
|
|
209
|
+
assert_never(api_version_location)
|
|
210
|
+
# TODO: Add a test validating the error message when there are no versions
|
|
211
|
+
default_version_example = next(iter(self.versions._version_values_set))
|
|
212
|
+
if api_version_format == "date":
|
|
213
|
+
self.api_version_validation_data_type = date
|
|
214
|
+
elif api_version_format == "string":
|
|
215
|
+
self.api_version_validation_data_type = str
|
|
216
|
+
else:
|
|
217
|
+
assert_never(default_version_example)
|
|
218
|
+
self.router: _RootCadwynAPIRouter = _RootCadwynAPIRouter( # pyright: ignore[reportIncompatibleVariableOverride]
|
|
156
219
|
**self._kwargs_to_router,
|
|
157
|
-
|
|
220
|
+
api_version_parameter_name=api_version_parameter_name,
|
|
158
221
|
api_version_var=self.versions.api_version_var,
|
|
222
|
+
api_version_format=api_version_format,
|
|
159
223
|
)
|
|
160
|
-
self._versioned_webhook_routers: dict[date, APIRouter] = {}
|
|
161
|
-
self._latest_version_router = APIRouter(dependency_overrides_provider=self._dependency_overrides_provider)
|
|
162
|
-
|
|
163
|
-
self.changelog_url = changelog_url
|
|
164
|
-
self.include_changelog_url_in_schema = include_changelog_url_in_schema
|
|
165
|
-
|
|
166
|
-
self.docs_url = docs_url
|
|
167
|
-
self.redoc_url = redoc_url
|
|
168
|
-
self.openapi_url = openapi_url
|
|
169
|
-
self.redoc_url = redoc_url
|
|
170
|
-
|
|
171
224
|
unversioned_router = APIRouter(**self._kwargs_to_router)
|
|
172
225
|
self._add_utility_endpoints(unversioned_router)
|
|
173
226
|
self._add_default_versioned_routers()
|
|
174
227
|
self.include_router(unversioned_router)
|
|
175
228
|
self.add_middleware(
|
|
176
|
-
|
|
177
|
-
|
|
229
|
+
versioning_middleware_class,
|
|
230
|
+
api_version_parameter_name=api_version_parameter_name,
|
|
231
|
+
api_version_manager=self._api_version_manager,
|
|
232
|
+
api_version_default_value=api_version_default_value,
|
|
178
233
|
api_version_var=self.versions.api_version_var,
|
|
179
|
-
default_response_class=default_response_class,
|
|
180
234
|
)
|
|
235
|
+
if self.api_version_format == "date" and (
|
|
236
|
+
sorted(self.versions.versions, key=lambda v: v.value, reverse=True) != list(self.versions.versions)
|
|
237
|
+
):
|
|
238
|
+
raise CadwynStructureError(
|
|
239
|
+
"Versions are not sorted correctly. Please sort them in descending order.",
|
|
240
|
+
)
|
|
181
241
|
|
|
182
242
|
@same_definition_as_in(FastAPI.__call__)
|
|
183
243
|
async def __call__(self, scope: Any, receive: Any, send: Any) -> None:
|
|
@@ -193,7 +253,7 @@ class Cadwyn(FastAPI):
|
|
|
193
253
|
versions=self.versions,
|
|
194
254
|
)
|
|
195
255
|
for version, router in generated_routers.endpoints.items():
|
|
196
|
-
self.
|
|
256
|
+
self._add_versioned_routers(router, version=version)
|
|
197
257
|
|
|
198
258
|
for version, router in generated_routers.webhooks.items():
|
|
199
259
|
self._versioned_webhook_routers[version] = router
|
|
@@ -267,26 +327,19 @@ class Cadwyn(FastAPI):
|
|
|
267
327
|
self._latest_version_router.include_router(router)
|
|
268
328
|
|
|
269
329
|
async def openapi_jsons(self, req: Request) -> JSONResponse:
|
|
270
|
-
|
|
271
|
-
not_found_error = HTTPException(
|
|
272
|
-
status_code=404,
|
|
273
|
-
detail=f"OpenApi file of with version `{raw_version}` not found",
|
|
274
|
-
)
|
|
275
|
-
try:
|
|
276
|
-
version = datetime.date.fromisoformat(raw_version) # pyright: ignore[reportArgumentType]
|
|
277
|
-
# TypeError when raw_version is None
|
|
278
|
-
# ValueError when raw_version is of the non-iso format
|
|
279
|
-
except (ValueError, TypeError):
|
|
280
|
-
version = raw_version
|
|
330
|
+
version = req.query_params.get("version") or req.headers.get(self.router.api_version_parameter_name)
|
|
281
331
|
|
|
282
|
-
if
|
|
332
|
+
if version in self.router.versioned_routers:
|
|
283
333
|
routes = self.router.versioned_routers[version].routes
|
|
284
|
-
formatted_version = version
|
|
334
|
+
formatted_version = version
|
|
285
335
|
elif version == "unversioned" and self._there_are_public_unversioned_routes():
|
|
286
336
|
routes = self.router.unversioned_routes
|
|
287
337
|
formatted_version = "unversioned"
|
|
288
338
|
else:
|
|
289
|
-
raise
|
|
339
|
+
raise HTTPException(
|
|
340
|
+
status_code=404,
|
|
341
|
+
detail=f"OpenApi file of with version `{version}` not found",
|
|
342
|
+
)
|
|
290
343
|
|
|
291
344
|
# Add root path to servers when mounted as sub-app or proxy is used
|
|
292
345
|
urls = (server_data.get("url") for server_data in self.servers)
|
|
@@ -296,7 +349,7 @@ class Cadwyn(FastAPI):
|
|
|
296
349
|
self.servers.insert(0, {"url": root_path})
|
|
297
350
|
|
|
298
351
|
webhook_routes = None
|
|
299
|
-
if
|
|
352
|
+
if version in self._versioned_webhook_routers:
|
|
300
353
|
webhook_routes = self._versioned_webhook_routers[version].routes
|
|
301
354
|
|
|
302
355
|
return JSONResponse(
|
|
@@ -354,7 +407,7 @@ class Cadwyn(FastAPI):
|
|
|
354
407
|
base_host = str(req.base_url).rstrip("/")
|
|
355
408
|
root_path = self._extract_root_path(req)
|
|
356
409
|
base_url = base_host + root_path
|
|
357
|
-
table = {version: f"{base_url}{docs_url}?version={version}" for version in self.router.
|
|
410
|
+
table = {version: f"{base_url}{docs_url}?version={version}" for version in self.router.versions}
|
|
358
411
|
if self._there_are_public_unversioned_routes():
|
|
359
412
|
table |= {"unversioned": f"{base_url}{docs_url}?version=unversioned"}
|
|
360
413
|
return self._templates.TemplateResponse(
|
|
@@ -362,6 +415,7 @@ class Cadwyn(FastAPI):
|
|
|
362
415
|
{"request": req, "table": table},
|
|
363
416
|
)
|
|
364
417
|
|
|
418
|
+
@deprecated("Use generate_and_include_versioned_routers and VersionBundle versions instead")
|
|
365
419
|
def add_header_versioned_routers(
|
|
366
420
|
self,
|
|
367
421
|
first_router: APIRouter,
|
|
@@ -370,15 +424,20 @@ class Cadwyn(FastAPI):
|
|
|
370
424
|
) -> list[BaseRoute]:
|
|
371
425
|
"""Add all routes from routers to be routed using header_value and return the added routes"""
|
|
372
426
|
try:
|
|
373
|
-
|
|
427
|
+
date.fromisoformat(header_value)
|
|
374
428
|
except ValueError as e:
|
|
375
429
|
raise ValueError("header_value should be in ISO 8601 format") from e
|
|
376
430
|
|
|
431
|
+
return self._add_versioned_routers(first_router, *other_routers, version=header_value)
|
|
432
|
+
|
|
433
|
+
def _add_versioned_routers(
|
|
434
|
+
self, first_router: APIRouter, *other_routers: APIRouter, version: str
|
|
435
|
+
) -> list[BaseRoute]:
|
|
377
436
|
added_routes: list[BaseRoute] = []
|
|
378
|
-
if
|
|
379
|
-
self.router.versioned_routers[
|
|
437
|
+
if version not in self.router.versioned_routers: # pragma: no branch
|
|
438
|
+
self.router.versioned_routers[version] = APIRouter(**self._kwargs_to_router)
|
|
380
439
|
|
|
381
|
-
versioned_router = self.router.versioned_routers[
|
|
440
|
+
versioned_router = self.router.versioned_routers[version]
|
|
382
441
|
if self.openapi_url is not None: # pragma: no branch
|
|
383
442
|
versioned_router.add_route(
|
|
384
443
|
path=self.openapi_url,
|
|
@@ -389,9 +448,18 @@ class Cadwyn(FastAPI):
|
|
|
389
448
|
|
|
390
449
|
added_route_count = 0
|
|
391
450
|
for router in (first_router, *other_routers):
|
|
392
|
-
self.router.versioned_routers[
|
|
451
|
+
self.router.versioned_routers[version].include_router(
|
|
393
452
|
router,
|
|
394
|
-
dependencies=[
|
|
453
|
+
dependencies=[
|
|
454
|
+
Depends(
|
|
455
|
+
_generate_api_version_dependency(
|
|
456
|
+
api_version_pythonic_parameter_name=self.api_version_pythonic_parameter_name,
|
|
457
|
+
default_value=version,
|
|
458
|
+
fastapi_depends_class=self._api_version_fastapi_depends_class,
|
|
459
|
+
validation_data_type=self.api_version_validation_data_type,
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
],
|
|
395
463
|
)
|
|
396
464
|
added_route_count += len(router.routes)
|
|
397
465
|
|
cadwyn/changelogs.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import copy
|
|
2
|
-
import datetime
|
|
3
2
|
import sys
|
|
4
3
|
from enum import auto
|
|
5
4
|
from logging import getLogger
|
|
@@ -22,7 +21,7 @@ from pydantic import BaseModel, Field, RootModel
|
|
|
22
21
|
from cadwyn._asts import GenericAliasUnion
|
|
23
22
|
from cadwyn._utils import Sentinel
|
|
24
23
|
from cadwyn.route_generation import _get_routes
|
|
25
|
-
from cadwyn.routing import
|
|
24
|
+
from cadwyn.routing import _RootCadwynAPIRouter
|
|
26
25
|
from cadwyn.schema_generation import SchemaGenerator, _change_field_in_model, generate_versioned_models
|
|
27
26
|
from cadwyn.structure.versions import PossibleInstructions, VersionBundle, VersionChange, VersionChangeWithSideEffects
|
|
28
27
|
|
|
@@ -62,15 +61,15 @@ def hidden(instruction_or_version_change: T) -> T:
|
|
|
62
61
|
return instruction_or_version_change
|
|
63
62
|
|
|
64
63
|
|
|
65
|
-
def _generate_changelog(versions: VersionBundle, router:
|
|
64
|
+
def _generate_changelog(versions: VersionBundle, router: _RootCadwynAPIRouter) -> "CadwynChangelogResource":
|
|
66
65
|
changelog = CadwynChangelogResource()
|
|
67
66
|
schema_generators = generate_versioned_models(versions)
|
|
68
67
|
for version, older_version in zip(versions, versions.versions[1:], strict=False):
|
|
69
68
|
routes_from_newer_version = router.versioned_routers[version.value].routes
|
|
70
69
|
schemas_from_older_version = get_fields_from_routes(router.versioned_routers[older_version.value].routes)
|
|
71
70
|
version_changelog = CadwynVersion(value=version.value)
|
|
72
|
-
generator_from_newer_version = schema_generators[version.value
|
|
73
|
-
generator_from_older_version = schema_generators[older_version.value
|
|
71
|
+
generator_from_newer_version = schema_generators[version.value]
|
|
72
|
+
generator_from_older_version = schema_generators[older_version.value]
|
|
74
73
|
for version_change in version.changes:
|
|
75
74
|
if version_change.is_hidden_from_changelog:
|
|
76
75
|
continue
|
|
@@ -284,7 +283,7 @@ class CadwynChangelogResource(BaseModel):
|
|
|
284
283
|
|
|
285
284
|
|
|
286
285
|
class CadwynVersion(BaseModel):
|
|
287
|
-
value:
|
|
286
|
+
value: str
|
|
288
287
|
changes: "list[CadwynVersionChange]" = Field(default_factory=list)
|
|
289
288
|
|
|
290
289
|
|
cadwyn/exceptions.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from datetime import date
|
|
3
2
|
from typing import Any
|
|
4
3
|
|
|
5
4
|
from fastapi.routing import APIRoute
|
|
@@ -14,12 +13,12 @@ class CadwynError(Exception):
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class CadwynHeadRequestValidationError(CadwynError):
|
|
17
|
-
def __init__(self, errors: list[Any], body: Any, version:
|
|
16
|
+
def __init__(self, errors: list[Any], body: Any, version: str) -> None:
|
|
18
17
|
self.errors = errors
|
|
19
18
|
self.body = body
|
|
20
19
|
self.version = version
|
|
21
20
|
super().__init__(
|
|
22
|
-
f"We failed to migrate the request with version={self.version
|
|
21
|
+
f"We failed to migrate the request with version={self.version}. "
|
|
23
22
|
"This means that there is some error in your migrations or schema structure that makes it impossible "
|
|
24
23
|
"to migrate the request of that version to latest.\n"
|
|
25
24
|
f"body={self.body}\n\nerrors={json.dumps(self.errors, indent=4, ensure_ascii=False)}"
|
cadwyn/middleware.py
CHANGED
|
@@ -1,54 +1,97 @@
|
|
|
1
|
+
# NOTE: It's OK that any_string might not be correctly sortable such as v10 vs v9.
|
|
2
|
+
# we can simply remove waterfalling from any_string api version style.
|
|
3
|
+
|
|
1
4
|
import inspect
|
|
2
|
-
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
3
7
|
from contextvars import ContextVar
|
|
4
|
-
from
|
|
5
|
-
from typing import Annotated, Any, cast
|
|
8
|
+
from typing import Annotated, Any, Literal, Protocol
|
|
6
9
|
|
|
7
|
-
from fastapi import
|
|
8
|
-
from fastapi._compat import _normalize_errors
|
|
9
|
-
from fastapi.dependencies.utils import get_dependant, solve_dependencies
|
|
10
|
-
from fastapi.responses import JSONResponse
|
|
10
|
+
from fastapi import Request
|
|
11
11
|
from starlette.middleware.base import BaseHTTPMiddleware, DispatchFunction, RequestResponseEndpoint
|
|
12
12
|
from starlette.types import ASGIApp
|
|
13
13
|
|
|
14
|
+
from cadwyn.structure.common import VersionType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VersionManager(Protocol):
|
|
18
|
+
def get(self, request: Request) -> str | None: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
VersionValidatorC = Callable[[str], VersionType]
|
|
22
|
+
VersionDependencyFactoryC = Callable[[], Callable[..., Any]]
|
|
23
|
+
|
|
24
|
+
APIVersionLocation = Literal["custom_header", "path"]
|
|
25
|
+
APIVersionFormat = Literal["date", "string"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HeaderVersionManager:
|
|
29
|
+
__slots__ = ("api_version_parameter_name",)
|
|
30
|
+
|
|
31
|
+
def __init__(self, *, api_version_parameter_name: str) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.api_version_parameter_name = api_version_parameter_name
|
|
14
34
|
|
|
15
|
-
def
|
|
35
|
+
def get(self, request: Request) -> str | None:
|
|
36
|
+
return request.headers.get(self.api_version_parameter_name)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class URLVersionManager:
|
|
40
|
+
__slots__ = ("possible_version_values", "url_version_regex")
|
|
41
|
+
|
|
42
|
+
def __init__(self, *, possible_version_values: set[str]) -> None:
|
|
43
|
+
super().__init__()
|
|
44
|
+
self.possible_version_values = possible_version_values
|
|
45
|
+
self.url_version_regex = re.compile(f"/({'|'.join(re.escape(v) for v in possible_version_values)})/")
|
|
46
|
+
|
|
47
|
+
def get(self, request: Request) -> str | None:
|
|
48
|
+
if m := self.url_version_regex.search(request.url.path):
|
|
49
|
+
return m.group(1)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _generate_api_version_dependency(
|
|
54
|
+
*,
|
|
55
|
+
api_version_pythonic_parameter_name: str,
|
|
56
|
+
default_value: str,
|
|
57
|
+
fastapi_depends_class: Callable[..., Any],
|
|
58
|
+
validation_data_type: Any,
|
|
59
|
+
):
|
|
16
60
|
def api_version_dependency(**kwargs: Any):
|
|
61
|
+
# TODO: What do I return?
|
|
17
62
|
return next(iter(kwargs.values()))
|
|
18
63
|
|
|
19
64
|
api_version_dependency.__signature__ = inspect.Signature(
|
|
20
65
|
parameters=[
|
|
21
66
|
inspect.Parameter(
|
|
22
|
-
|
|
67
|
+
api_version_pythonic_parameter_name,
|
|
23
68
|
inspect.Parameter.KEYWORD_ONLY,
|
|
24
|
-
annotation=Annotated[
|
|
25
|
-
|
|
69
|
+
annotation=Annotated[
|
|
70
|
+
validation_data_type, fastapi_depends_class(openapi_examples={"default": {"value": default_value}})
|
|
71
|
+
],
|
|
26
72
|
),
|
|
27
73
|
],
|
|
28
74
|
)
|
|
29
75
|
return api_version_dependency
|
|
30
76
|
|
|
31
77
|
|
|
32
|
-
class
|
|
78
|
+
class VersionPickingMiddleware(BaseHTTPMiddleware):
|
|
33
79
|
def __init__(
|
|
34
80
|
self,
|
|
35
81
|
app: ASGIApp,
|
|
36
82
|
*,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
83
|
+
api_version_parameter_name: str,
|
|
84
|
+
api_version_default_value: str | None | Callable[[Request], Awaitable[str]],
|
|
85
|
+
api_version_var: ContextVar[VersionType | None],
|
|
86
|
+
api_version_manager: VersionManager,
|
|
40
87
|
dispatch: DispatchFunction | None = None,
|
|
41
88
|
) -> None:
|
|
42
89
|
super().__init__(app, dispatch)
|
|
43
|
-
|
|
90
|
+
|
|
91
|
+
self.api_version_parameter_name = api_version_parameter_name
|
|
92
|
+
self._api_version_manager = api_version_manager
|
|
44
93
|
self.api_version_var = api_version_var
|
|
45
|
-
self.
|
|
46
|
-
# We use the dependant to apply fastapi's validation to the header, making validation at middleware level
|
|
47
|
-
# consistent with validation and route level.
|
|
48
|
-
self.version_header_validation_dependant = get_dependant(
|
|
49
|
-
path="",
|
|
50
|
-
call=_get_api_version_dependency(api_version_header_name, "2000-08-23"),
|
|
51
|
-
)
|
|
94
|
+
self.api_version_default_value = api_version_default_value
|
|
52
95
|
|
|
53
96
|
async def dispatch(
|
|
54
97
|
self,
|
|
@@ -57,24 +100,20 @@ class HeaderVersioningMiddleware(BaseHTTPMiddleware):
|
|
|
57
100
|
):
|
|
58
101
|
# We handle api version at middleware level because if we try to add a Dependency to all routes, it won't work:
|
|
59
102
|
# we use this header for routing so the user will simply get a 404 if the header is invalid.
|
|
60
|
-
api_version
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
embed_body_fields=False,
|
|
68
|
-
)
|
|
69
|
-
if solved_result.errors:
|
|
70
|
-
return self.default_response_class(status_code=422, content=_normalize_errors(solved_result.errors))
|
|
71
|
-
api_version = cast(date, solved_result.values[self.api_version_header_name.replace("-", "_")])
|
|
72
|
-
self.api_version_var.set(api_version)
|
|
103
|
+
api_version = self._api_version_manager.get(request)
|
|
104
|
+
|
|
105
|
+
if api_version is None:
|
|
106
|
+
if callable(self.api_version_default_value): # pragma: no cover # TODO
|
|
107
|
+
api_version = await self.api_version_default_value(request)
|
|
108
|
+
else:
|
|
109
|
+
api_version = self.api_version_default_value
|
|
73
110
|
|
|
111
|
+
self.api_version_var.set(api_version)
|
|
74
112
|
response = await call_next(request)
|
|
75
113
|
|
|
76
114
|
if api_version is not None:
|
|
77
115
|
# We return it because we will be returning the **matched** version, not the requested one.
|
|
78
|
-
|
|
116
|
+
# In date-based versioning with waterfalling, it makes sense.
|
|
117
|
+
response.headers[self.api_version_parameter_name] = api_version
|
|
79
118
|
|
|
80
119
|
return response
|
cadwyn/route_generation.py
CHANGED
|
@@ -36,7 +36,7 @@ from cadwyn.schema_generation import (
|
|
|
36
36
|
generate_versioned_models,
|
|
37
37
|
)
|
|
38
38
|
from cadwyn.structure import Version, VersionBundle
|
|
39
|
-
from cadwyn.structure.common import Endpoint,
|
|
39
|
+
from cadwyn.structure.common import Endpoint, VersionType
|
|
40
40
|
from cadwyn.structure.endpoints import (
|
|
41
41
|
EndpointDidntExistInstruction,
|
|
42
42
|
EndpointExistedInstruction,
|
|
@@ -62,8 +62,8 @@ class _EndpointInfo:
|
|
|
62
62
|
|
|
63
63
|
@dataclass(slots=True, frozen=True)
|
|
64
64
|
class GeneratedRouters(Generic[_R, _WR]):
|
|
65
|
-
endpoints: dict[
|
|
66
|
-
webhooks: dict[
|
|
65
|
+
endpoints: dict[VersionType, _R]
|
|
66
|
+
webhooks: dict[VersionType, _WR]
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def generate_versioned_routers(
|
|
@@ -125,8 +125,8 @@ class _EndpointTransformer(Generic[_R, _WR]):
|
|
|
125
125
|
def transform(self) -> GeneratedRouters[_R, _WR]:
|
|
126
126
|
router = copy_router(self.parent_router)
|
|
127
127
|
webhook_router = copy_router(self.parent_webhooks_router)
|
|
128
|
-
routers: dict[
|
|
129
|
-
webhook_routers: dict[
|
|
128
|
+
routers: dict[VersionType, _R] = {}
|
|
129
|
+
webhook_routers: dict[VersionType, _WR] = {}
|
|
130
130
|
|
|
131
131
|
for version in self.versions:
|
|
132
132
|
self.schema_generators[str(version.value)].annotation_transformer.migrate_router_to_version(router)
|
cadwyn/routing.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import bisect
|
|
2
2
|
from collections.abc import Sequence
|
|
3
3
|
from contextvars import ContextVar
|
|
4
|
-
from datetime import date
|
|
5
4
|
from functools import cached_property
|
|
6
5
|
from logging import getLogger
|
|
7
6
|
from typing import Any
|
|
@@ -13,79 +12,56 @@ from starlette.routing import BaseRoute, Match
|
|
|
13
12
|
from starlette.types import Receive, Scope, Send
|
|
14
13
|
|
|
15
14
|
from cadwyn._utils import same_definition_as_in
|
|
16
|
-
|
|
17
|
-
from .
|
|
18
|
-
|
|
19
|
-
# TODO: Remove this in a major version. This is only here for backwards compatibility
|
|
20
|
-
__all__ = ["generate_versioned_routers"]
|
|
15
|
+
from cadwyn.middleware import APIVersionFormat
|
|
16
|
+
from cadwyn.structure.common import VersionType
|
|
21
17
|
|
|
22
18
|
_logger = getLogger(__name__)
|
|
23
19
|
|
|
24
20
|
|
|
25
|
-
class
|
|
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
|
-
based on the headers. It also supports waterflowing the requests to the latest
|
|
30
|
-
version of the API if the request header doesn't match any of the versions.
|
|
31
|
-
|
|
32
|
-
If the app has two versions: 2022-01-02 and 2022-01-05, and the request header
|
|
33
|
-
is 2022-01-03, then the request will be routed to 2022-01-02 version as it the closest
|
|
34
|
-
version, but lower than the request header.
|
|
35
|
-
|
|
36
|
-
Exact match is always preferred over partial match and a request will never be
|
|
37
|
-
matched to the higher versioned route
|
|
38
|
-
"""
|
|
39
|
-
|
|
21
|
+
class _RootCadwynAPIRouter(APIRouter):
|
|
40
22
|
def __init__(
|
|
41
23
|
self,
|
|
42
24
|
*args: Any,
|
|
43
|
-
|
|
44
|
-
api_version_var: ContextVar[
|
|
25
|
+
api_version_parameter_name: str,
|
|
26
|
+
api_version_var: ContextVar[str | None],
|
|
27
|
+
api_version_format: APIVersionFormat,
|
|
45
28
|
**kwargs: Any,
|
|
46
29
|
):
|
|
47
30
|
super().__init__(*args, **kwargs)
|
|
48
|
-
self.versioned_routers: dict[
|
|
49
|
-
self.
|
|
31
|
+
self.versioned_routers: dict[VersionType, APIRouter] = {}
|
|
32
|
+
self.api_version_parameter_name = api_version_parameter_name.lower()
|
|
50
33
|
self.api_version_var = api_version_var
|
|
51
34
|
self.unversioned_routes: list[BaseRoute] = []
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"Partial match. The endpoint with a lower version was selected for the API call",
|
|
83
|
-
extra={
|
|
84
|
-
"version_chosen": version_chosen,
|
|
85
|
-
"request_version": request_version,
|
|
86
|
-
},
|
|
87
|
-
)
|
|
88
|
-
return self.versioned_routers[version_chosen].routes
|
|
35
|
+
self.api_version_format = api_version_format
|
|
36
|
+
|
|
37
|
+
async def _get_routes_from_closest_suitable_version(self, version: VersionType) -> list[BaseRoute]:
|
|
38
|
+
"""Pick the versioned routes for the given version in case we failed to pick a concrete version
|
|
39
|
+
|
|
40
|
+
If the app has two versions: 2022-01-02 and 2022-01-05, and the request header
|
|
41
|
+
is 2022-01-03, then the request will be routed to 2022-01-02 version as it the closest
|
|
42
|
+
version, but lower than the request header.
|
|
43
|
+
|
|
44
|
+
Exact match is always preferred over partial match and a request will never be
|
|
45
|
+
matched to the higher versioned route.
|
|
46
|
+
|
|
47
|
+
We implement routing like this because it is extremely convenient with microservice
|
|
48
|
+
architecture. For example, imagine that you have two Cadwyn services: Payables and Receivables,
|
|
49
|
+
each defining its own API versions. Payables service might contain 10 versions while receivables
|
|
50
|
+
service might contain only 2 versions because it didn't need as many breaking changes.
|
|
51
|
+
If a client requests a version that does not exist in receivables -- we will just waterfall
|
|
52
|
+
to some earlier version, making receivables behavior consistent even if API keeps getting new versions.
|
|
53
|
+
"""
|
|
54
|
+
if self.api_version_format == "date":
|
|
55
|
+
index = bisect.bisect_left(self.versions, version)
|
|
56
|
+
# That's when we try to get a version earlier than the earliest possible version
|
|
57
|
+
if index == 0:
|
|
58
|
+
return []
|
|
59
|
+
picked_version = self.versions[index - 1]
|
|
60
|
+
self.api_version_var.set(picked_version)
|
|
61
|
+
# as bisect_left returns the index where to insert item x in list a, assuming a is sorted
|
|
62
|
+
# we need to get the previous item and that will be a match
|
|
63
|
+
return self.versioned_routers[picked_version].routes
|
|
64
|
+
return [] # pragma: no cover # This should not be possible
|
|
89
65
|
|
|
90
66
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
91
67
|
if "router" not in scope: # pragma: no cover
|
|
@@ -95,18 +71,22 @@ class _RootHeaderAPIRouter(APIRouter):
|
|
|
95
71
|
await self.lifespan(scope, receive, send)
|
|
96
72
|
return
|
|
97
73
|
|
|
98
|
-
|
|
74
|
+
version = self.api_version_var.get(None)
|
|
99
75
|
|
|
100
|
-
# if
|
|
76
|
+
# if version is None, then it's an unversioned request and we need to use the unversioned routes
|
|
101
77
|
# if there will be a value, we search for the most suitable version
|
|
102
|
-
if not
|
|
78
|
+
if not version:
|
|
103
79
|
routes = self.unversioned_routes
|
|
104
|
-
elif
|
|
105
|
-
routes = self.versioned_routers[
|
|
80
|
+
elif version in self.versioned_routers:
|
|
81
|
+
routes = self.versioned_routers[version].routes
|
|
106
82
|
else:
|
|
107
|
-
routes = self.
|
|
83
|
+
routes = await self._get_routes_from_closest_suitable_version(version)
|
|
108
84
|
await self.process_request(scope=scope, receive=receive, send=send, routes=routes)
|
|
109
85
|
|
|
86
|
+
@cached_property
|
|
87
|
+
def versions(self):
|
|
88
|
+
return sorted(self.versioned_routers.keys())
|
|
89
|
+
|
|
110
90
|
@same_definition_as_in(APIRouter.add_api_route)
|
|
111
91
|
def add_api_route(self, *args: Any, **kwargs: Any):
|
|
112
92
|
super().add_api_route(*args, **kwargs)
|
cadwyn/schema_generation.py
CHANGED
|
@@ -45,8 +45,8 @@ from pydantic.fields import ComputedFieldInfo, FieldInfo
|
|
|
45
45
|
from typing_extensions import Doc, Self, _AnnotatedAlias, assert_never
|
|
46
46
|
|
|
47
47
|
from cadwyn._utils import Sentinel, UnionType, fully_unwrap_decorator, lenient_issubclass
|
|
48
|
-
from cadwyn.exceptions import InvalidGenerationInstructionError
|
|
49
|
-
from cadwyn.structure.common import
|
|
48
|
+
from cadwyn.exceptions import CadwynError, InvalidGenerationInstructionError
|
|
49
|
+
from cadwyn.structure.common import VersionType
|
|
50
50
|
from cadwyn.structure.data import ResponseInfo
|
|
51
51
|
from cadwyn.structure.enums import AlterEnumSubInstruction, EnumDidntHaveMembersInstruction, EnumHadMembersInstruction
|
|
52
52
|
from cadwyn.structure.schemas import (
|
|
@@ -158,15 +158,18 @@ def migrate_response_body(
|
|
|
158
158
|
latest_response_model: type[pydantic.BaseModel],
|
|
159
159
|
*,
|
|
160
160
|
latest_body: Any,
|
|
161
|
-
version:
|
|
161
|
+
version: VersionType | date,
|
|
162
162
|
) -> Any:
|
|
163
163
|
"""Convert the data to a specific version
|
|
164
164
|
|
|
165
165
|
Apply all version changes from latest until the passed version in reverse order
|
|
166
166
|
and wrap the result in the correct version of latest_response_model
|
|
167
167
|
"""
|
|
168
|
-
if isinstance(version,
|
|
169
|
-
version =
|
|
168
|
+
if isinstance(version, date):
|
|
169
|
+
version = version.isoformat()
|
|
170
|
+
version = versions._get_closest_lesser_version(version)
|
|
171
|
+
if version not in versions._version_values_set:
|
|
172
|
+
raise CadwynError(f"Version {version} not found in version bundle")
|
|
170
173
|
response = ResponseInfo(Response(status_code=200), body=latest_body)
|
|
171
174
|
migrated_response = versions._migrate_response(
|
|
172
175
|
response,
|
|
@@ -176,8 +179,6 @@ def migrate_response_body(
|
|
|
176
179
|
method="GET",
|
|
177
180
|
)
|
|
178
181
|
|
|
179
|
-
version = versions._get_closest_lesser_version(version)
|
|
180
|
-
|
|
181
182
|
versioned_response_model: type[pydantic.BaseModel] = generate_versioned_models(versions)[str(version)][
|
|
182
183
|
latest_response_model
|
|
183
184
|
]
|
cadwyn/structure/common.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import datetime
|
|
2
1
|
from collections.abc import Callable
|
|
3
2
|
from dataclasses import dataclass
|
|
4
3
|
from typing import ParamSpec, TypeAlias, TypeVar
|
|
@@ -6,7 +5,7 @@ from typing import ParamSpec, TypeAlias, TypeVar
|
|
|
6
5
|
from pydantic import BaseModel
|
|
7
6
|
|
|
8
7
|
VersionedModel = BaseModel
|
|
9
|
-
|
|
8
|
+
VersionType: TypeAlias = str
|
|
10
9
|
_P = ParamSpec("_P")
|
|
11
10
|
_R = TypeVar("_R")
|
|
12
11
|
Endpoint: TypeAlias = Callable[_P, _R]
|
cadwyn/structure/versions.py
CHANGED
|
@@ -33,7 +33,7 @@ from cadwyn.exceptions import (
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
from .._utils import Sentinel
|
|
36
|
-
from .common import Endpoint,
|
|
36
|
+
from .common import Endpoint, VersionedModel, VersionType
|
|
37
37
|
from .data import (
|
|
38
38
|
RequestInfo,
|
|
39
39
|
ResponseInfo,
|
|
@@ -58,7 +58,7 @@ PossibleInstructions: TypeAlias = (
|
|
|
58
58
|
| SchemaHadInstruction
|
|
59
59
|
| staticmethod
|
|
60
60
|
)
|
|
61
|
-
APIVersionVarType: TypeAlias = ContextVar[
|
|
61
|
+
APIVersionVarType: TypeAlias = ContextVar[VersionType | None] | ContextVar[VersionType]
|
|
62
62
|
IdentifierPythonPath = str
|
|
63
63
|
|
|
64
64
|
|
|
@@ -201,11 +201,11 @@ class VersionChangeWithSideEffects(VersionChange, _abstract=True):
|
|
|
201
201
|
|
|
202
202
|
|
|
203
203
|
class Version:
|
|
204
|
-
def __init__(self, value:
|
|
204
|
+
def __init__(self, value: str | date, *changes: type[VersionChange]) -> None:
|
|
205
205
|
super().__init__()
|
|
206
206
|
|
|
207
|
-
if isinstance(value,
|
|
208
|
-
value =
|
|
207
|
+
if isinstance(value, date):
|
|
208
|
+
value = value.isoformat()
|
|
209
209
|
self.value = value
|
|
210
210
|
self.changes = changes
|
|
211
211
|
|
|
@@ -252,7 +252,7 @@ class VersionBundle:
|
|
|
252
252
|
latest_version_or_head_version: Version | HeadVersion,
|
|
253
253
|
/,
|
|
254
254
|
*other_versions: Version,
|
|
255
|
-
api_version_var:
|
|
255
|
+
api_version_var: ContextVar[VersionType | None] | None = None,
|
|
256
256
|
) -> None:
|
|
257
257
|
super().__init__()
|
|
258
258
|
|
|
@@ -262,27 +262,21 @@ class VersionBundle:
|
|
|
262
262
|
else:
|
|
263
263
|
self.head_version = HeadVersion()
|
|
264
264
|
self.versions = (latest_version_or_head_version, *other_versions)
|
|
265
|
+
self.reversed_versions = tuple(reversed(self.versions))
|
|
265
266
|
|
|
266
|
-
self.version_dates = tuple(version.value for version in self.versions)
|
|
267
267
|
if api_version_var is None:
|
|
268
268
|
api_version_var = ContextVar("cadwyn_api_version")
|
|
269
|
+
self.version_values = tuple(version.value for version in self.versions)
|
|
270
|
+
self.reversed_version_values = tuple(reversed(self.version_values))
|
|
269
271
|
self.api_version_var = api_version_var
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
raise CadwynStructureError("You must define at least one non-head version in a VersionBundle.")
|
|
276
|
-
if self.versions[-1].changes:
|
|
277
|
-
raise CadwynStructureError(
|
|
278
|
-
f'The first version "{self.versions[-1].value}" cannot have any version changes. '
|
|
279
|
-
"Version changes are defined to migrate to/from a previous version so you "
|
|
280
|
-
"cannot define one for the very first version.",
|
|
281
|
-
)
|
|
282
|
-
version_values = set()
|
|
272
|
+
self._all_versions = (self.head_version, *self.versions)
|
|
273
|
+
self._version_changes_to_version_mapping = {
|
|
274
|
+
version_change: version.value for version in self.versions for version_change in version.changes
|
|
275
|
+
}
|
|
276
|
+
self._version_values_set: set[str] = set()
|
|
283
277
|
for version in self.versions:
|
|
284
|
-
if version.value not in
|
|
285
|
-
|
|
278
|
+
if version.value not in self._version_values_set:
|
|
279
|
+
self._version_values_set.add(version.value)
|
|
286
280
|
else:
|
|
287
281
|
raise CadwynStructureError(
|
|
288
282
|
f"You tried to define two versions with the same value in the same "
|
|
@@ -295,13 +289,23 @@ class VersionBundle:
|
|
|
295
289
|
"It is prohibited.",
|
|
296
290
|
)
|
|
297
291
|
version_change._bound_version_bundle = self
|
|
292
|
+
if not self.versions:
|
|
293
|
+
raise CadwynStructureError("You must define at least one non-head version in a VersionBundle.")
|
|
294
|
+
|
|
295
|
+
if self.versions[-1].changes:
|
|
296
|
+
raise CadwynStructureError(
|
|
297
|
+
f'The first version "{self.versions[-1].value}" cannot have any version changes. '
|
|
298
|
+
"Version changes are defined to migrate to/from a previous version so you "
|
|
299
|
+
"cannot define one for the very first version.",
|
|
300
|
+
)
|
|
298
301
|
|
|
299
302
|
def __iter__(self) -> Iterator[Version]:
|
|
300
303
|
yield from self.versions
|
|
301
304
|
|
|
302
|
-
@
|
|
303
|
-
|
|
304
|
-
|
|
305
|
+
@property
|
|
306
|
+
@deprecated("Use 'version_values' instead.")
|
|
307
|
+
def version_dates(self): # pragma: no cover
|
|
308
|
+
return self.version_values
|
|
305
309
|
|
|
306
310
|
@functools.cached_property
|
|
307
311
|
def versioned_schemas(self) -> dict[IdentifierPythonPath, type[VersionedModel]]:
|
|
@@ -330,18 +334,12 @@ class VersionBundle:
|
|
|
330
334
|
for instruction in version_change.alter_enum_instructions
|
|
331
335
|
}
|
|
332
336
|
|
|
333
|
-
def _get_closest_lesser_version(self, version:
|
|
334
|
-
for defined_version in self.
|
|
337
|
+
def _get_closest_lesser_version(self, version: VersionType):
|
|
338
|
+
for defined_version in self.version_values:
|
|
335
339
|
if defined_version <= version:
|
|
336
340
|
return defined_version
|
|
337
341
|
raise CadwynError("You tried to migrate to version that is earlier than the first version which is prohibited.")
|
|
338
342
|
|
|
339
|
-
@functools.cached_property
|
|
340
|
-
def _version_changes_to_version_mapping(
|
|
341
|
-
self,
|
|
342
|
-
) -> dict[type[VersionChange] | type[VersionChangeWithSideEffects], VersionDate]:
|
|
343
|
-
return {version_change: version.value for version in self.versions for version_change in version.changes}
|
|
344
|
-
|
|
345
343
|
async def _migrate_request(
|
|
346
344
|
self,
|
|
347
345
|
body_type: type[BaseModel] | None,
|
|
@@ -350,7 +348,7 @@ class VersionBundle:
|
|
|
350
348
|
request: FastapiRequest,
|
|
351
349
|
response: FastapiResponse,
|
|
352
350
|
request_info: RequestInfo,
|
|
353
|
-
current_version:
|
|
351
|
+
current_version: VersionType,
|
|
354
352
|
head_route: APIRoute,
|
|
355
353
|
*,
|
|
356
354
|
exit_stack: AsyncExitStack,
|
|
@@ -358,9 +356,9 @@ class VersionBundle:
|
|
|
358
356
|
background_tasks: BackgroundTasks | None,
|
|
359
357
|
) -> dict[str, Any]:
|
|
360
358
|
method = request.method
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
359
|
+
|
|
360
|
+
start = self.reversed_version_values.index(current_version)
|
|
361
|
+
for v in self.reversed_versions[start + 1 :]:
|
|
364
362
|
for version_change in v.changes:
|
|
365
363
|
if body_type is not None and body_type in version_change.alter_request_by_schema_instructions:
|
|
366
364
|
for instruction in version_change.alter_request_by_schema_instructions[body_type]:
|
|
@@ -391,14 +389,13 @@ class VersionBundle:
|
|
|
391
389
|
def _migrate_response(
|
|
392
390
|
self,
|
|
393
391
|
response_info: ResponseInfo,
|
|
394
|
-
current_version:
|
|
392
|
+
current_version: VersionType,
|
|
395
393
|
head_response_model: type[BaseModel],
|
|
396
394
|
path: str,
|
|
397
395
|
method: str,
|
|
398
396
|
) -> ResponseInfo:
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
break
|
|
397
|
+
end = self.version_values.index(current_version)
|
|
398
|
+
for v in self.versions[:end]:
|
|
402
399
|
for version_change in v.changes:
|
|
403
400
|
migrations_to_apply: list[_BaseAlterResponseInstruction] = []
|
|
404
401
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cadwyn
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0a1
|
|
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
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
cadwyn/__init__.py,sha256=gWsp-oNP5LhBqWF_rsrv6sEYP9KchTu8xhxAR8uaPm0,1035
|
|
2
|
+
cadwyn/__main__.py,sha256=fGoKJPNVueqqXW4rqwmRUBBMoDGeLEyATdT-rel5Pvs,2449
|
|
3
|
+
cadwyn/_asts.py,sha256=kd6Ngwr8c-uTNx-R-pP48Scf0OdjrTyEeSYe5DLoGqo,5095
|
|
4
|
+
cadwyn/_importer.py,sha256=QV6HqODCG9K2oL4Vc15fAqL2-plMvUWw_cgaj4Ln4C8,1075
|
|
5
|
+
cadwyn/_render.py,sha256=VArS68879hXRntMKCQJ7LUpCv8aToavHZV1JuBFmtfY,5513
|
|
6
|
+
cadwyn/_utils.py,sha256=rlD1SkswtZ1bWgKj6PLYbVaHYkD-NzY4iUcHdPd2Y68,1475
|
|
7
|
+
cadwyn/applications.py,sha256=bBO5kK-lDbjk3Rrbx8TdSY5EHDq4BkwlOzQruZ4Vj00,20128
|
|
8
|
+
cadwyn/changelogs.py,sha256=-ft0Rpx_xDoVWNuAH8NYmUpE9UnSZfreyL-Qa2fP78Y,20004
|
|
9
|
+
cadwyn/exceptions.py,sha256=gLCikeUPeLJwVjM8_DoSTIFHwmNI7n7vw1atpgHvbMU,1803
|
|
10
|
+
cadwyn/middleware.py,sha256=lP8bFHHhXEMjOyDdljxiC2fH5K826qPe07qERq1nnG8,4274
|
|
11
|
+
cadwyn/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
cadwyn/route_generation.py,sha256=EclCM9CI05ZRQW1qa052cBnF3Zb8Qj4lbIvHQSy0g_8,25152
|
|
13
|
+
cadwyn/routing.py,sha256=cG3ieQ9NlF-PvslT9fy5AHBNco4lMgDoqQu42klnCLk,6899
|
|
14
|
+
cadwyn/schema_generation.py,sha256=pM5bM0Tfi10ahSWRaD4h3zjzH-ERRkySh1iVURwf010,42089
|
|
15
|
+
cadwyn/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
cadwyn/static/docs.html,sha256=WNm5ANJVy51TcIUFOaqKf1Z8eF86CC85TTHPxACtkzw,3455
|
|
17
|
+
cadwyn/structure/__init__.py,sha256=Wgvjdq3vfl9Yhe-BkcFGAMi_Co11YOfTmJQqgF5Gzx4,655
|
|
18
|
+
cadwyn/structure/common.py,sha256=3UEbxzAjDfHT2GPMQ3tNA3pEqJd9ZnDclqsj2ZDpzv4,399
|
|
19
|
+
cadwyn/structure/data.py,sha256=uViRW4uOOonXZj90hOlPNk02AIwp0fvDNoF8M5_CEes,7707
|
|
20
|
+
cadwyn/structure/endpoints.py,sha256=8lrc4xanCt7gat106yYRIQC0TNxzFkLF-urIml_d_X0,5934
|
|
21
|
+
cadwyn/structure/enums.py,sha256=bZL-iUOUFi9ZYlMZJw-tAix2yrgCp3gH3N2gwO44LUU,1043
|
|
22
|
+
cadwyn/structure/schemas.py,sha256=v_wDTn84SgHVDFDlTgoalUzBXpDbT5Hl73Skp0UyGAM,10081
|
|
23
|
+
cadwyn/structure/versions.py,sha256=p0Ui00RX8DW9M-wndiaXBK2t85Bd4ypGigPCfVZ9PiE,33167
|
|
24
|
+
cadwyn-5.0.0a1.dist-info/METADATA,sha256=qJdUYZjLL1OUUixdtsSZOaj-Ee6ZIfDZgDKStnJEPys,4506
|
|
25
|
+
cadwyn-5.0.0a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
26
|
+
cadwyn-5.0.0a1.dist-info/entry_points.txt,sha256=mGX8wl-Xfhpr5M93SUmkykaqinUaYAvW9rtDSX54gx0,47
|
|
27
|
+
cadwyn-5.0.0a1.dist-info/licenses/LICENSE,sha256=KeCWewiDQYpmSnzF-p_0YpoWiyDcUPaCuG8OWQs4ig4,1072
|
|
28
|
+
cadwyn-5.0.0a1.dist-info/RECORD,,
|
cadwyn-4.6.0a1.dist-info/RECORD
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
cadwyn/__init__.py,sha256=gWsp-oNP5LhBqWF_rsrv6sEYP9KchTu8xhxAR8uaPm0,1035
|
|
2
|
-
cadwyn/__main__.py,sha256=fGoKJPNVueqqXW4rqwmRUBBMoDGeLEyATdT-rel5Pvs,2449
|
|
3
|
-
cadwyn/_asts.py,sha256=kd6Ngwr8c-uTNx-R-pP48Scf0OdjrTyEeSYe5DLoGqo,5095
|
|
4
|
-
cadwyn/_importer.py,sha256=QV6HqODCG9K2oL4Vc15fAqL2-plMvUWw_cgaj4Ln4C8,1075
|
|
5
|
-
cadwyn/_render.py,sha256=LJ-R1TrBgMJpTkJb6pQdRWaMjKyw3R6eTlXXEieqUw0,5466
|
|
6
|
-
cadwyn/_utils.py,sha256=rlD1SkswtZ1bWgKj6PLYbVaHYkD-NzY4iUcHdPd2Y68,1475
|
|
7
|
-
cadwyn/applications.py,sha256=-MQL8_WWQSvnP1Z3zS7BlVwjrUWUGW9s1xVV1mKha6E,17008
|
|
8
|
-
cadwyn/changelogs.py,sha256=uveMizeeqNv0JuXza9Rkg0ulDEWyJL24bRxcHkHlwjI,20054
|
|
9
|
-
cadwyn/exceptions.py,sha256=VlJKRmEGfFTDtHbOWc8kXK4yMi2N172K684Y2UIV8rI,1832
|
|
10
|
-
cadwyn/middleware.py,sha256=kUZK2dmoricMbv6knPCIHpXEInX2670XIwAj0v_XQxk,3408
|
|
11
|
-
cadwyn/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
cadwyn/route_generation.py,sha256=tdIsdDIBY4hVUlTGMh4pTMM92LzxhV9CmGs6J8iAhaw,25152
|
|
13
|
-
cadwyn/routing.py,sha256=mZRe7ivfTY2qdgrCBO4AHvKQq6Dazf7g_8B6aCrTgN8,7221
|
|
14
|
-
cadwyn/schema_generation.py,sha256=W3SnBPF4fRK48bXGF-EooJHhkKoQMhzJgP-jC5FP98k,41951
|
|
15
|
-
cadwyn/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
cadwyn/static/docs.html,sha256=WNm5ANJVy51TcIUFOaqKf1Z8eF86CC85TTHPxACtkzw,3455
|
|
17
|
-
cadwyn/structure/__init__.py,sha256=Wgvjdq3vfl9Yhe-BkcFGAMi_Co11YOfTmJQqgF5Gzx4,655
|
|
18
|
-
cadwyn/structure/common.py,sha256=GUclfxKLRlFwPjT237fCtLIzdvjvC9gI3acuxBizwbg,414
|
|
19
|
-
cadwyn/structure/data.py,sha256=uViRW4uOOonXZj90hOlPNk02AIwp0fvDNoF8M5_CEes,7707
|
|
20
|
-
cadwyn/structure/endpoints.py,sha256=8lrc4xanCt7gat106yYRIQC0TNxzFkLF-urIml_d_X0,5934
|
|
21
|
-
cadwyn/structure/enums.py,sha256=bZL-iUOUFi9ZYlMZJw-tAix2yrgCp3gH3N2gwO44LUU,1043
|
|
22
|
-
cadwyn/structure/schemas.py,sha256=v_wDTn84SgHVDFDlTgoalUzBXpDbT5Hl73Skp0UyGAM,10081
|
|
23
|
-
cadwyn/structure/versions.py,sha256=L07vdC9d7_h4WKV8TArgZKfgfzB8-M4f62FXZm8o1TM,33232
|
|
24
|
-
cadwyn-4.6.0a1.dist-info/METADATA,sha256=xeePYEinj4vRCvFbrEs6lhJns_CJykK2zq4uq7Vq5Ak,4506
|
|
25
|
-
cadwyn-4.6.0a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
26
|
-
cadwyn-4.6.0a1.dist-info/entry_points.txt,sha256=mGX8wl-Xfhpr5M93SUmkykaqinUaYAvW9rtDSX54gx0,47
|
|
27
|
-
cadwyn-4.6.0a1.dist-info/licenses/LICENSE,sha256=KeCWewiDQYpmSnzF-p_0YpoWiyDcUPaCuG8OWQs4ig4,1072
|
|
28
|
-
cadwyn-4.6.0a1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|