cadwyn 4.6.0__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 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(arg=attr, value=ast.parse(get_fancy_repr(attr_value), mode="eval").body)
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 datetime
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.middleware import HeaderVersioningMiddleware, _get_api_version_dependency
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 _RootHeaderAPIRouter
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: str = "x-api-version",
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.router: _RootHeaderAPIRouter = _RootHeaderAPIRouter( # pyright: ignore[reportIncompatibleVariableOverride]
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
- api_version_header_name=api_version_header_name,
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
- HeaderVersioningMiddleware,
177
- api_version_header_name=self.router.api_version_header_name,
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.add_header_versioned_routers(router, header_value=version.isoformat())
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
- raw_version = req.query_params.get("version") or req.headers.get(self.router.api_version_header_name)
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 isinstance(version, date) and version in self.router.versioned_routers:
332
+ if version in self.router.versioned_routers:
283
333
  routes = self.router.versioned_routers[version].routes
284
- formatted_version = version.isoformat()
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 not_found_error
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 isinstance(version, date) and version in self._versioned_webhook_routers:
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.sorted_versions}
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
- header_value_as_dt = date.fromisoformat(header_value)
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 header_value_as_dt not in self.router.versioned_routers: # pragma: no branch
379
- self.router.versioned_routers[header_value_as_dt] = APIRouter(**self._kwargs_to_router)
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[header_value_as_dt]
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[header_value_as_dt].include_router(
451
+ self.router.versioned_routers[version].include_router(
393
452
  router,
394
- dependencies=[Depends(_get_api_version_dependency(self.router.api_version_header_name, header_value))],
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 _RootHeaderAPIRouter
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: _RootHeaderAPIRouter) -> "CadwynChangelogResource":
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.isoformat()]
73
- generator_from_older_version = schema_generators[older_version.value.isoformat()]
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: datetime.date
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: date) -> None:
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!s}. "
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
- from contextlib import AsyncExitStack
5
+ import re
6
+ from collections.abc import Awaitable, Callable
3
7
  from contextvars import ContextVar
4
- from datetime import date
5
- from typing import Annotated, Any, cast
8
+ from typing import Annotated, Any, Literal, Protocol
6
9
 
7
- from fastapi import Header, Request, Response
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 _get_api_version_dependency(api_version_header_name: str, version_example: str):
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
- api_version_header_name.replace("-", "_"),
67
+ api_version_pythonic_parameter_name,
23
68
  inspect.Parameter.KEYWORD_ONLY,
24
- annotation=Annotated[date, Header(examples=[version_example])],
25
- default=version_example,
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 HeaderVersioningMiddleware(BaseHTTPMiddleware):
78
+ class VersionPickingMiddleware(BaseHTTPMiddleware):
33
79
  def __init__(
34
80
  self,
35
81
  app: ASGIApp,
36
82
  *,
37
- api_version_header_name: str,
38
- api_version_var: ContextVar[date] | ContextVar[date | None],
39
- default_response_class: type[Response] = JSONResponse,
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
- self.api_version_header_name = api_version_header_name
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.default_response_class = default_response_class
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: date | None = None
61
- if self.api_version_header_name in request.headers:
62
- async with AsyncExitStack() as async_exit_stack:
63
- solved_result = await solve_dependencies(
64
- request=request,
65
- dependant=self.version_header_validation_dependant,
66
- async_exit_stack=async_exit_stack,
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
- response.headers[self.api_version_header_name] = api_version.isoformat()
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
@@ -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, VersionDate
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[VersionDate, _R]
66
- webhooks: dict[VersionDate, _WR]
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[VersionDate, _R] = {}
129
- webhook_routers: dict[VersionDate, _WR] = {}
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 .route_generation import generate_versioned_routers
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 _RootHeaderAPIRouter(APIRouter):
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
- api_version_header_name: str,
44
- api_version_var: ContextVar[date] | ContextVar[date | None],
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[date, APIRouter] = {}
49
- self.api_version_header_name = api_version_header_name.lower()
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
- @cached_property
54
- def sorted_versions(self):
55
- return sorted(self.versioned_routers.keys())
56
-
57
- @cached_property
58
- def min_routes_version(self):
59
- return min(self.sorted_versions)
60
-
61
- def find_closest_date_but_not_new(self, request_version: date) -> date:
62
- index = bisect.bisect_left(self.sorted_versions, request_version)
63
- # as bisect_left returns the index where to insert item x in list a, assuming a is sorted
64
- # we need to get the previous item and that will be a match
65
- return self.sorted_versions[index - 1]
66
-
67
- def pick_version(self, request_header_value: date) -> list[BaseRoute]:
68
- request_version = request_header_value.isoformat()
69
-
70
- if self.min_routes_version > request_header_value:
71
- # then the request version is older that the oldest route we have
72
- _logger.info(
73
- "Request version is older than the oldest version. No route can match this version",
74
- extra={
75
- "oldest_version": self.min_routes_version.isoformat(),
76
- "request_version": request_version,
77
- },
78
- )
79
- return []
80
- version_chosen = self.find_closest_date_but_not_new(request_header_value)
81
- _logger.info(
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
- header_value = self.api_version_var.get(None)
74
+ version = self.api_version_var.get(None)
99
75
 
100
- # if header_value is None, then it's an unversioned request and we need to use the unversioned routes
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 header_value:
78
+ if not version:
103
79
  routes = self.unversioned_routes
104
- elif header_value in self.versioned_routers:
105
- routes = self.versioned_routers[header_value].routes
80
+ elif version in self.versioned_routers:
81
+ routes = self.versioned_routers[version].routes
106
82
  else:
107
- routes = self.pick_version(request_header_value=header_value)
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)
@@ -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 VersionDate
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: VersionDate | str,
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, str):
169
- version = date.fromisoformat(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
  ]
@@ -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
- VersionDate = datetime.date
8
+ VersionType: TypeAlias = str
10
9
  _P = ParamSpec("_P")
11
10
  _R = TypeVar("_R")
12
11
  Endpoint: TypeAlias = Callable[_P, _R]
@@ -33,7 +33,7 @@ from cadwyn.exceptions import (
33
33
  )
34
34
 
35
35
  from .._utils import Sentinel
36
- from .common import Endpoint, VersionDate, VersionedModel
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[VersionDate | None] | ContextVar[VersionDate]
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: VersionDate | str, *changes: type[VersionChange]) -> None:
204
+ def __init__(self, value: str | date, *changes: type[VersionChange]) -> None:
205
205
  super().__init__()
206
206
 
207
- if isinstance(value, str):
208
- value = date.fromisoformat(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: APIVersionVarType | None = None,
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
- if sorted(self.versions, key=lambda v: v.value, reverse=True) != list(self.versions):
271
- raise CadwynStructureError(
272
- "Versions are not sorted correctly. Please sort them in descending order.",
273
- )
274
- if not self.versions:
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 version_values:
285
- version_values.add(version.value)
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
- @functools.cached_property
303
- def _all_versions(self):
304
- return (self.head_version, *self.versions)
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: VersionDate):
334
- for defined_version in self.version_dates:
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: VersionDate,
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
- for v in reversed(self.versions):
362
- if v.value <= current_version:
363
- continue
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: VersionDate,
392
+ current_version: VersionType,
395
393
  head_response_model: type[BaseModel],
396
394
  path: str,
397
395
  method: str,
398
396
  ) -> ResponseInfo:
399
- for v in self.versions:
400
- if v.value <= current_version:
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: 4.6.0
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,,
@@ -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.0.dist-info/METADATA,sha256=KFK2JacLS-rYQju0mqFUk-c1niFrYZa1Kitkj7gOYGw,4504
25
- cadwyn-4.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
- cadwyn-4.6.0.dist-info/entry_points.txt,sha256=mGX8wl-Xfhpr5M93SUmkykaqinUaYAvW9rtDSX54gx0,47
27
- cadwyn-4.6.0.dist-info/licenses/LICENSE,sha256=KeCWewiDQYpmSnzF-p_0YpoWiyDcUPaCuG8OWQs4ig4,1072
28
- cadwyn-4.6.0.dist-info/RECORD,,