cadwyn 4.1.0__py3-none-any.whl → 4.2.1__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/__init__.py +2 -0
- cadwyn/_render.py +2 -2
- cadwyn/applications.py +28 -3
- cadwyn/changelogs.py +499 -0
- cadwyn/route_generation.py +6 -9
- cadwyn/schema_generation.py +45 -31
- cadwyn/structure/common.py +6 -0
- cadwyn/structure/endpoints.py +6 -5
- cadwyn/structure/enums.py +4 -2
- cadwyn/structure/schemas.py +15 -38
- cadwyn/structure/versions.py +25 -16
- {cadwyn-4.1.0.dist-info → cadwyn-4.2.1.dist-info}/METADATA +2 -1
- cadwyn-4.2.1.dist-info/RECORD +28 -0
- cadwyn-4.1.0.dist-info/RECORD +0 -27
- {cadwyn-4.1.0.dist-info → cadwyn-4.2.1.dist-info}/LICENSE +0 -0
- {cadwyn-4.1.0.dist-info → cadwyn-4.2.1.dist-info}/WHEEL +0 -0
- {cadwyn-4.1.0.dist-info → cadwyn-4.2.1.dist-info}/entry_points.txt +0 -0
cadwyn/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
2
|
|
|
3
3
|
from .applications import Cadwyn
|
|
4
|
+
from .changelogs import hidden
|
|
4
5
|
from .route_generation import VersionedAPIRouter, generate_versioned_routers
|
|
5
6
|
from .schema_generation import generate_versioned_models, migrate_response_body
|
|
6
7
|
from .structure import (
|
|
@@ -37,4 +38,5 @@ __all__ = [
|
|
|
37
38
|
"RequestInfo",
|
|
38
39
|
"ResponseInfo",
|
|
39
40
|
"generate_versioned_models",
|
|
41
|
+
"hidden",
|
|
40
42
|
]
|
cadwyn/_render.py
CHANGED
|
@@ -13,7 +13,7 @@ from cadwyn.exceptions import CadwynRenderError
|
|
|
13
13
|
from cadwyn.schema_generation import (
|
|
14
14
|
PydanticFieldWrapper,
|
|
15
15
|
_EnumWrapper,
|
|
16
|
-
|
|
16
|
+
_PydanticModelWrapper,
|
|
17
17
|
generate_versioned_models,
|
|
18
18
|
)
|
|
19
19
|
from cadwyn.structure.versions import VersionBundle, get_cls_pythonpath
|
|
@@ -107,7 +107,7 @@ def _render_enum_model(wrapper: _EnumWrapper, original_cls_node: ast.ClassDef):
|
|
|
107
107
|
return original_cls_node
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
def _render_pydantic_model(wrapper:
|
|
110
|
+
def _render_pydantic_model(wrapper: _PydanticModelWrapper, original_cls_node: ast.ClassDef):
|
|
111
111
|
# This is for possible schema renaming
|
|
112
112
|
original_cls_node.name = wrapper.name
|
|
113
113
|
|
cadwyn/applications.py
CHANGED
|
@@ -25,6 +25,7 @@ from starlette.routing import BaseRoute, Route
|
|
|
25
25
|
from starlette.types import Lifespan
|
|
26
26
|
from typing_extensions import Self
|
|
27
27
|
|
|
28
|
+
from cadwyn.changelogs import CadwynChangelogResource, _generate_changelog
|
|
28
29
|
from cadwyn.middleware import HeaderVersioningMiddleware, _get_api_version_dependency
|
|
29
30
|
from cadwyn.route_generation import generate_versioned_routers
|
|
30
31
|
from cadwyn.routing import _RootHeaderAPIRouter
|
|
@@ -47,6 +48,8 @@ class Cadwyn(FastAPI):
|
|
|
47
48
|
*,
|
|
48
49
|
versions: VersionBundle,
|
|
49
50
|
api_version_header_name: str = "x-api-version",
|
|
51
|
+
changelog_url: str | None = "/changelog",
|
|
52
|
+
include_changelog_url_in_schema: bool = True,
|
|
50
53
|
debug: bool = False,
|
|
51
54
|
title: str = "FastAPI",
|
|
52
55
|
summary: str | None = None,
|
|
@@ -154,13 +157,17 @@ class Cadwyn(FastAPI):
|
|
|
154
157
|
api_version_var=self.versions.api_version_var,
|
|
155
158
|
)
|
|
156
159
|
|
|
160
|
+
self.changelog_url = changelog_url
|
|
161
|
+
self.include_changelog_url_in_schema = include_changelog_url_in_schema
|
|
162
|
+
|
|
157
163
|
self.docs_url = docs_url
|
|
158
164
|
self.redoc_url = redoc_url
|
|
159
165
|
self.openapi_url = openapi_url
|
|
160
166
|
self.redoc_url = redoc_url
|
|
161
167
|
|
|
162
168
|
unversioned_router = APIRouter(**self._kwargs_to_router)
|
|
163
|
-
self.
|
|
169
|
+
self._add_utility_endpoints(unversioned_router)
|
|
170
|
+
self._add_default_versioned_routers()
|
|
164
171
|
self.include_router(unversioned_router)
|
|
165
172
|
self.add_middleware(
|
|
166
173
|
HeaderVersioningMiddleware,
|
|
@@ -169,6 +176,10 @@ class Cadwyn(FastAPI):
|
|
|
169
176
|
default_response_class=default_response_class,
|
|
170
177
|
)
|
|
171
178
|
|
|
179
|
+
def _add_default_versioned_routers(self) -> None:
|
|
180
|
+
for version in self.versions:
|
|
181
|
+
self.router.versioned_routers[version.value] = APIRouter(**self._kwargs_to_router)
|
|
182
|
+
|
|
172
183
|
@property
|
|
173
184
|
def dependency_overrides(self) -> dict[Callable[..., Any], Callable[..., Any]]:
|
|
174
185
|
# TODO: Remove this approach as it is no longer necessary
|
|
@@ -184,7 +195,19 @@ class Cadwyn(FastAPI):
|
|
|
184
195
|
) -> None:
|
|
185
196
|
self._dependency_overrides_provider.dependency_overrides = value
|
|
186
197
|
|
|
187
|
-
def
|
|
198
|
+
def generate_changelog(self) -> CadwynChangelogResource:
|
|
199
|
+
return _generate_changelog(self.versions, self.router)
|
|
200
|
+
|
|
201
|
+
def _add_utility_endpoints(self, unversioned_router: APIRouter):
|
|
202
|
+
if self.changelog_url is not None:
|
|
203
|
+
unversioned_router.add_api_route(
|
|
204
|
+
path=self.changelog_url,
|
|
205
|
+
endpoint=self.generate_changelog,
|
|
206
|
+
response_model=CadwynChangelogResource,
|
|
207
|
+
methods=["GET"],
|
|
208
|
+
include_in_schema=self.include_changelog_url_in_schema,
|
|
209
|
+
)
|
|
210
|
+
|
|
188
211
|
if self.openapi_url is not None:
|
|
189
212
|
unversioned_router.add_route(
|
|
190
213
|
path=self.openapi_url,
|
|
@@ -296,7 +319,9 @@ class Cadwyn(FastAPI):
|
|
|
296
319
|
return req.scope.get("root_path", "").rstrip("/")
|
|
297
320
|
|
|
298
321
|
def _render_docs_dashboard(self, req: Request, docs_url: str):
|
|
299
|
-
|
|
322
|
+
base_host = str(req.base_url).rstrip("/")
|
|
323
|
+
root_path = req.scope.get("root_path", "")
|
|
324
|
+
base_url = base_host + root_path
|
|
300
325
|
table = {version: f"{base_url}{docs_url}?version={version}" for version in self.router.sorted_versions}
|
|
301
326
|
if self._there_are_public_unversioned_routes():
|
|
302
327
|
table |= {"unversioned": f"{base_url}{docs_url}?version=unversioned"}
|
cadwyn/changelogs.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import datetime
|
|
3
|
+
import sys
|
|
4
|
+
from enum import auto
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from typing import Any, Literal, TypeVar, cast, get_args
|
|
7
|
+
|
|
8
|
+
from fastapi._compat import (
|
|
9
|
+
GenerateJsonSchema,
|
|
10
|
+
ModelField,
|
|
11
|
+
get_compat_model_name_map,
|
|
12
|
+
get_definitions,
|
|
13
|
+
)
|
|
14
|
+
from fastapi.openapi.constants import REF_TEMPLATE
|
|
15
|
+
from fastapi.openapi.utils import (
|
|
16
|
+
get_fields_from_routes,
|
|
17
|
+
get_openapi,
|
|
18
|
+
)
|
|
19
|
+
from fastapi.routing import APIRoute
|
|
20
|
+
from pydantic import BaseModel, Field, RootModel
|
|
21
|
+
|
|
22
|
+
from cadwyn._asts import GenericAliasUnion
|
|
23
|
+
from cadwyn._utils import Sentinel
|
|
24
|
+
from cadwyn.route_generation import _get_routes
|
|
25
|
+
from cadwyn.routing import _RootHeaderAPIRouter
|
|
26
|
+
from cadwyn.schema_generation import SchemaGenerator, _change_field_in_model, generate_versioned_models
|
|
27
|
+
from cadwyn.structure.versions import PossibleInstructions, VersionBundle, VersionChange, VersionChangeWithSideEffects
|
|
28
|
+
|
|
29
|
+
from .structure.endpoints import (
|
|
30
|
+
EndpointDidntExistInstruction,
|
|
31
|
+
EndpointExistedInstruction,
|
|
32
|
+
EndpointHadInstruction,
|
|
33
|
+
)
|
|
34
|
+
from .structure.enums import EnumDidntHaveMembersInstruction, EnumHadMembersInstruction
|
|
35
|
+
from .structure.schemas import (
|
|
36
|
+
FieldDidntExistInstruction,
|
|
37
|
+
FieldDidntHaveInstruction,
|
|
38
|
+
FieldExistedAsInstruction,
|
|
39
|
+
FieldHadInstruction,
|
|
40
|
+
SchemaHadInstruction,
|
|
41
|
+
ValidatorDidntExistInstruction,
|
|
42
|
+
ValidatorExistedInstruction,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if sys.version_info >= (3, 11): # pragma: no cover
|
|
46
|
+
from enum import StrEnum
|
|
47
|
+
else: # pragma: no cover
|
|
48
|
+
from backports.strenum import StrEnum
|
|
49
|
+
|
|
50
|
+
_logger = getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
T = TypeVar("T", bound=PossibleInstructions | type[VersionChange])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def hidden(instruction_or_version_change: T) -> T:
|
|
56
|
+
if isinstance(
|
|
57
|
+
instruction_or_version_change, staticmethod | ValidatorDidntExistInstruction | ValidatorExistedInstruction
|
|
58
|
+
):
|
|
59
|
+
return instruction_or_version_change
|
|
60
|
+
|
|
61
|
+
instruction_or_version_change.is_hidden_from_changelog = True
|
|
62
|
+
return instruction_or_version_change
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _generate_changelog(versions: VersionBundle, router: _RootHeaderAPIRouter) -> "CadwynChangelogResource":
|
|
66
|
+
changelog = CadwynChangelogResource()
|
|
67
|
+
schema_generators = generate_versioned_models(versions)
|
|
68
|
+
for version, older_version in zip(versions, versions.versions[1:], strict=False):
|
|
69
|
+
routes_from_newer_version = router.versioned_routers[version.value].routes
|
|
70
|
+
schemas_from_older_version = get_fields_from_routes(router.versioned_routers[older_version.value].routes)
|
|
71
|
+
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()]
|
|
74
|
+
for version_change in version.changes:
|
|
75
|
+
if version_change.is_hidden_from_changelog:
|
|
76
|
+
continue
|
|
77
|
+
version_change_changelog = CadwynVersionChange(
|
|
78
|
+
description=version_change.description,
|
|
79
|
+
side_effects=isinstance(version_change, VersionChangeWithSideEffects),
|
|
80
|
+
)
|
|
81
|
+
for instruction in [
|
|
82
|
+
*version_change.alter_endpoint_instructions,
|
|
83
|
+
*version_change.alter_enum_instructions,
|
|
84
|
+
*version_change.alter_schema_instructions,
|
|
85
|
+
]:
|
|
86
|
+
if (
|
|
87
|
+
isinstance(instruction, ValidatorDidntExistInstruction | ValidatorExistedInstruction)
|
|
88
|
+
or instruction.is_hidden_from_changelog
|
|
89
|
+
):
|
|
90
|
+
continue
|
|
91
|
+
changelog_entry = _convert_version_change_instruction_to_changelog_entry(
|
|
92
|
+
instruction,
|
|
93
|
+
version_change,
|
|
94
|
+
generator_from_newer_version,
|
|
95
|
+
generator_from_older_version,
|
|
96
|
+
schemas_from_older_version,
|
|
97
|
+
cast(list[APIRoute], routes_from_newer_version),
|
|
98
|
+
)
|
|
99
|
+
if changelog_entry is not None: # pragma: no branch # This should never happen
|
|
100
|
+
version_change_changelog.instructions.append(CadwynVersionChangeInstruction(changelog_entry))
|
|
101
|
+
version_changelog.changes.append(version_change_changelog)
|
|
102
|
+
changelog.versions.append(version_changelog)
|
|
103
|
+
return changelog
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _get_older_field_name(
|
|
107
|
+
schema: type[BaseModel], new_field_name: str, generator_from_older_version: SchemaGenerator
|
|
108
|
+
) -> str:
|
|
109
|
+
older_model_wrapper = generator_from_older_version._get_wrapper_for_model(schema)
|
|
110
|
+
newer_names_mapping = {
|
|
111
|
+
field.name_from_newer_version: old_name for old_name, field in older_model_wrapper.fields.items()
|
|
112
|
+
}
|
|
113
|
+
return newer_names_mapping[new_field_name]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _get_affected_model_names(
|
|
117
|
+
instruction: FieldExistedAsInstruction
|
|
118
|
+
| FieldDidntExistInstruction
|
|
119
|
+
| FieldHadInstruction
|
|
120
|
+
| FieldDidntHaveInstruction,
|
|
121
|
+
generator_from_newer_version: SchemaGenerator,
|
|
122
|
+
schemas_from_last_version: list[ModelField],
|
|
123
|
+
):
|
|
124
|
+
changed_model = generator_from_newer_version._get_wrapper_for_model(instruction.schema)
|
|
125
|
+
annotations = [model.field_info.annotation for model in schemas_from_last_version]
|
|
126
|
+
basemodel_annotations: list[type[BaseModel]] = []
|
|
127
|
+
for annotation in annotations:
|
|
128
|
+
basemodel_annotations.extend(_get_all_pydantic_models_from_generic(annotation))
|
|
129
|
+
models = {generator_from_newer_version._get_wrapper_for_model(annotation) for annotation in basemodel_annotations}
|
|
130
|
+
return [
|
|
131
|
+
model.name
|
|
132
|
+
for model in models
|
|
133
|
+
if changed_model == model
|
|
134
|
+
or (
|
|
135
|
+
instruction.name not in model.fields
|
|
136
|
+
and changed_model in (parents := model._get_parents(generator_from_newer_version.model_bundle.schemas))
|
|
137
|
+
and all(instruction.name not in parent.fields for parent in parents[: parents.index(changed_model)])
|
|
138
|
+
)
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _get_all_pydantic_models_from_generic(annotation: Any) -> list[type[BaseModel]]:
|
|
143
|
+
if not isinstance(annotation, GenericAliasUnion):
|
|
144
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
145
|
+
return [annotation]
|
|
146
|
+
else:
|
|
147
|
+
return []
|
|
148
|
+
sub_annotations = get_args(annotation)
|
|
149
|
+
models = []
|
|
150
|
+
|
|
151
|
+
for sub_annotation in sub_annotations:
|
|
152
|
+
models.extend(_get_all_pydantic_models_from_generic(sub_annotation))
|
|
153
|
+
|
|
154
|
+
return models
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _get_openapi_representation_of_a_field(model: type[BaseModel], field_name: str) -> dict:
|
|
158
|
+
class CadwynDummyModelForRepresentation(BaseModel):
|
|
159
|
+
my_field: model
|
|
160
|
+
|
|
161
|
+
model_name_map = get_compat_model_name_map([CadwynDummyModelForRepresentation.model_fields["my_field"]])
|
|
162
|
+
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
|
163
|
+
field_mapping, definitions = get_definitions(
|
|
164
|
+
fields=[ModelField(CadwynDummyModelForRepresentation.model_fields["my_field"], "my_field")],
|
|
165
|
+
schema_generator=schema_generator,
|
|
166
|
+
model_name_map=model_name_map,
|
|
167
|
+
separate_input_output_schemas=False,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return definitions[model.__name__]["properties"][field_name]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ChangelogEntryType(StrEnum):
|
|
174
|
+
endpoint_added = "endpoint.added"
|
|
175
|
+
endpoint_removed = "endpoint.removed"
|
|
176
|
+
endpoint_changed = "endpoint.changed"
|
|
177
|
+
enum_members_added = "enum.members.added"
|
|
178
|
+
enum_members_removed = "enum.members.removed"
|
|
179
|
+
schema_changed = "schema.changed"
|
|
180
|
+
schema_field_removed = "schema.field.removed"
|
|
181
|
+
schema_field_added = "schema.field.added"
|
|
182
|
+
schema_field_attributes_changed = "schema.field.attributes.changed"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class CadwynAttributeChangeStatus(StrEnum):
|
|
186
|
+
added = auto()
|
|
187
|
+
changed = auto()
|
|
188
|
+
removed = auto()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class CadwynEndpointAttributeChange(BaseModel):
|
|
192
|
+
name: str
|
|
193
|
+
new_value: Any
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class CadwynAttributeChange(BaseModel):
|
|
197
|
+
name: str
|
|
198
|
+
status: CadwynAttributeChangeStatus
|
|
199
|
+
old_value: Any
|
|
200
|
+
new_value: Any
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class CadwynFieldAttributesWereChangedChangelogEntry(BaseModel):
|
|
204
|
+
type: Literal[ChangelogEntryType.schema_field_attributes_changed] = (
|
|
205
|
+
ChangelogEntryType.schema_field_attributes_changed
|
|
206
|
+
)
|
|
207
|
+
models: list[str]
|
|
208
|
+
field: str
|
|
209
|
+
attribute_changes: list[CadwynAttributeChange]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class CadwynModelModifiedAttributes(BaseModel):
|
|
213
|
+
name: str | None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class CadwynSchemaWasChangedChangelogEntry(BaseModel):
|
|
217
|
+
type: Literal[ChangelogEntryType.schema_changed] = ChangelogEntryType.schema_changed
|
|
218
|
+
model: str
|
|
219
|
+
modified_attributes: CadwynModelModifiedAttributes
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class CadwynSchemaFieldWasAddedChangelogEntry(BaseModel):
|
|
223
|
+
type: Literal[ChangelogEntryType.schema_field_added] = ChangelogEntryType.schema_field_added
|
|
224
|
+
models: list[str]
|
|
225
|
+
field: str
|
|
226
|
+
field_info: dict[str, Any]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class CadwynSchemaFieldWasRemovedChangelogEntry(BaseModel):
|
|
230
|
+
type: Literal[ChangelogEntryType.schema_field_removed] = ChangelogEntryType.schema_field_removed
|
|
231
|
+
models: list[str]
|
|
232
|
+
field: str
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class CadwynEnumMember(BaseModel):
|
|
236
|
+
name: str
|
|
237
|
+
value: Any
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class CadwynEnumMembersWereAddedChangelogEntry(BaseModel):
|
|
241
|
+
type: Literal[ChangelogEntryType.enum_members_added] = ChangelogEntryType.enum_members_added
|
|
242
|
+
enum: str
|
|
243
|
+
members: list[CadwynEnumMember]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class CadwynEnumMembersWereChangedChangelogEntry(BaseModel):
|
|
247
|
+
type: Literal[ChangelogEntryType.enum_members_removed] = ChangelogEntryType.enum_members_removed
|
|
248
|
+
enum: str
|
|
249
|
+
member_changes: list[CadwynAttributeChange]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class HTTPMethod(StrEnum):
|
|
253
|
+
GET = "GET"
|
|
254
|
+
PUT = "PUT"
|
|
255
|
+
POST = "POST"
|
|
256
|
+
DELETE = "DELETE"
|
|
257
|
+
OPTIONS = "OPTIONS"
|
|
258
|
+
HEAD = "HEAD"
|
|
259
|
+
PATCH = "PATCH"
|
|
260
|
+
TRACE = "TRACE"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class CadwynEndpointHadChangelogEntry(BaseModel):
|
|
264
|
+
type: Literal[ChangelogEntryType.endpoint_changed] = ChangelogEntryType.endpoint_changed
|
|
265
|
+
path: str
|
|
266
|
+
methods: list[HTTPMethod]
|
|
267
|
+
changes: list[CadwynEndpointAttributeChange]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class CadwynEndpointWasAddedChangelogEntry(BaseModel):
|
|
271
|
+
type: Literal[ChangelogEntryType.endpoint_added] = ChangelogEntryType.endpoint_added
|
|
272
|
+
path: str
|
|
273
|
+
methods: list[HTTPMethod]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class CadwynEndpointWasRemovedChangelogEntry(BaseModel):
|
|
277
|
+
type: Literal[ChangelogEntryType.endpoint_removed] = ChangelogEntryType.endpoint_removed
|
|
278
|
+
path: str
|
|
279
|
+
methods: list[HTTPMethod]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class CadwynChangelogResource(BaseModel):
|
|
283
|
+
versions: "list[CadwynVersion]" = Field(default_factory=list)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class CadwynVersion(BaseModel):
|
|
287
|
+
value: datetime.date
|
|
288
|
+
changes: "list[CadwynVersionChange]" = Field(default_factory=list)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class CadwynVersionChange(BaseModel):
|
|
292
|
+
description: str
|
|
293
|
+
side_effects: bool
|
|
294
|
+
instructions: "list[CadwynVersionChangeInstruction]" = Field(default_factory=list)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
CadwynVersionChangeInstruction = RootModel[
|
|
298
|
+
CadwynEnumMembersWereAddedChangelogEntry
|
|
299
|
+
| CadwynEnumMembersWereChangedChangelogEntry
|
|
300
|
+
| CadwynEndpointWasAddedChangelogEntry
|
|
301
|
+
| CadwynEndpointWasRemovedChangelogEntry
|
|
302
|
+
| CadwynSchemaFieldWasRemovedChangelogEntry
|
|
303
|
+
| CadwynSchemaFieldWasAddedChangelogEntry
|
|
304
|
+
| CadwynFieldAttributesWereChangedChangelogEntry
|
|
305
|
+
| CadwynEndpointHadChangelogEntry
|
|
306
|
+
| CadwynSchemaWasChangedChangelogEntry
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _convert_version_change_instruction_to_changelog_entry( # noqa: C901
|
|
311
|
+
instruction: PossibleInstructions,
|
|
312
|
+
version_change: type[VersionChange],
|
|
313
|
+
generator_from_newer_version: SchemaGenerator,
|
|
314
|
+
generator_from_older_version: SchemaGenerator,
|
|
315
|
+
schemas_from_older_version: list[ModelField],
|
|
316
|
+
routes_from_newer_version: list[APIRoute],
|
|
317
|
+
):
|
|
318
|
+
match instruction:
|
|
319
|
+
case EndpointDidntExistInstruction():
|
|
320
|
+
return CadwynEndpointWasAddedChangelogEntry(
|
|
321
|
+
path=instruction.endpoint_path,
|
|
322
|
+
methods=cast(Any, instruction.endpoint_methods),
|
|
323
|
+
)
|
|
324
|
+
case EndpointExistedInstruction():
|
|
325
|
+
return CadwynEndpointWasRemovedChangelogEntry(
|
|
326
|
+
path=instruction.endpoint_path,
|
|
327
|
+
methods=cast(Any, instruction.endpoint_methods),
|
|
328
|
+
)
|
|
329
|
+
case EndpointHadInstruction():
|
|
330
|
+
if instruction.attributes.include_in_schema is not Sentinel:
|
|
331
|
+
return CadwynEndpointWasRemovedChangelogEntry(
|
|
332
|
+
path=instruction.endpoint_path,
|
|
333
|
+
methods=cast(Any, instruction.endpoint_methods),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
renaming_map = {"operation_id": "operationId"}
|
|
337
|
+
|
|
338
|
+
attribute_changes = []
|
|
339
|
+
|
|
340
|
+
for attr in ["path", "methods", "summary", "description", "tags", "deprecated", "operation_id"]:
|
|
341
|
+
attr_value = getattr(instruction.attributes, attr)
|
|
342
|
+
if attr_value is not Sentinel:
|
|
343
|
+
attribute_changes.append(
|
|
344
|
+
CadwynEndpointAttributeChange(name=renaming_map.get(attr, attr), new_value=attr_value)
|
|
345
|
+
)
|
|
346
|
+
if instruction.attributes.name is not Sentinel and instruction.attributes.summary is Sentinel:
|
|
347
|
+
attribute_changes.append(
|
|
348
|
+
CadwynEndpointAttributeChange(name="summary", new_value=instruction.attributes.name)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if any(
|
|
352
|
+
getattr(instruction.attributes, attr) is not Sentinel
|
|
353
|
+
for attr in ["path", "methods", "summary", "description", "tags", "deprecated"]
|
|
354
|
+
):
|
|
355
|
+
pass
|
|
356
|
+
if any(
|
|
357
|
+
attr is not Sentinel
|
|
358
|
+
for attr in [
|
|
359
|
+
instruction.attributes.response_model,
|
|
360
|
+
instruction.attributes.response_class,
|
|
361
|
+
instruction.attributes.responses,
|
|
362
|
+
instruction.attributes.status_code,
|
|
363
|
+
]
|
|
364
|
+
):
|
|
365
|
+
newer_routes = _get_routes(
|
|
366
|
+
routes_from_newer_version,
|
|
367
|
+
instruction.endpoint_path,
|
|
368
|
+
instruction.endpoint_methods,
|
|
369
|
+
instruction.endpoint_func_name,
|
|
370
|
+
is_deleted=False,
|
|
371
|
+
)
|
|
372
|
+
newer_openapi = get_openapi(title="", version="", routes=newer_routes)
|
|
373
|
+
changed_responses = {
|
|
374
|
+
method: route_openapi["responses"]
|
|
375
|
+
for method, route_openapi in newer_openapi["paths"][instruction.endpoint_path].items()
|
|
376
|
+
}
|
|
377
|
+
attribute_changes.append(CadwynEndpointAttributeChange(name="responses", new_value=changed_responses))
|
|
378
|
+
return CadwynEndpointHadChangelogEntry(
|
|
379
|
+
path=instruction.endpoint_path,
|
|
380
|
+
methods=cast(Any, instruction.endpoint_methods),
|
|
381
|
+
changes=attribute_changes,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
case FieldHadInstruction() | FieldDidntHaveInstruction():
|
|
385
|
+
old_field_name = _get_older_field_name(instruction.schema, instruction.name, generator_from_older_version)
|
|
386
|
+
|
|
387
|
+
if isinstance(instruction, FieldHadInstruction) and instruction.new_name is not Sentinel:
|
|
388
|
+
old_field_name_from_this_instruction = instruction.new_name
|
|
389
|
+
attribute_changes = [
|
|
390
|
+
CadwynAttributeChange(
|
|
391
|
+
name="name",
|
|
392
|
+
status=CadwynAttributeChangeStatus.changed,
|
|
393
|
+
old_value=old_field_name,
|
|
394
|
+
new_value=instruction.name,
|
|
395
|
+
)
|
|
396
|
+
]
|
|
397
|
+
else:
|
|
398
|
+
old_field_name_from_this_instruction = instruction.name
|
|
399
|
+
attribute_changes = []
|
|
400
|
+
newer_model_wrapper = generator_from_newer_version._get_wrapper_for_model(instruction.schema)
|
|
401
|
+
newer_model_wrapper_with_migrated_field = copy.deepcopy(newer_model_wrapper)
|
|
402
|
+
_change_field_in_model(
|
|
403
|
+
newer_model_wrapper_with_migrated_field,
|
|
404
|
+
generator_from_newer_version.model_bundle.schemas,
|
|
405
|
+
alter_schema_instruction=instruction,
|
|
406
|
+
version_change_name=version_change.__name__,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
older_model = newer_model_wrapper_with_migrated_field.generate_model_copy(generator_from_newer_version)
|
|
410
|
+
newer_model = newer_model_wrapper.generate_model_copy(generator_from_newer_version)
|
|
411
|
+
|
|
412
|
+
newer_field_openapi = _get_openapi_representation_of_a_field(newer_model, instruction.name)
|
|
413
|
+
older_field_openapi = _get_openapi_representation_of_a_field(
|
|
414
|
+
older_model, old_field_name_from_this_instruction
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
attribute_changes += [
|
|
418
|
+
CadwynAttributeChange(
|
|
419
|
+
name=key,
|
|
420
|
+
status=CadwynAttributeChangeStatus.changed
|
|
421
|
+
if key in newer_field_openapi
|
|
422
|
+
else CadwynAttributeChangeStatus.removed,
|
|
423
|
+
old_value=old_value,
|
|
424
|
+
new_value=newer_field_openapi.get(key),
|
|
425
|
+
)
|
|
426
|
+
for key, old_value in older_field_openapi.items()
|
|
427
|
+
if old_value != newer_field_openapi.get(key)
|
|
428
|
+
]
|
|
429
|
+
attribute_changes += [
|
|
430
|
+
CadwynAttributeChange(
|
|
431
|
+
name=key,
|
|
432
|
+
status=CadwynAttributeChangeStatus.added,
|
|
433
|
+
old_value=None,
|
|
434
|
+
new_value=new_value,
|
|
435
|
+
)
|
|
436
|
+
for key, new_value in newer_field_openapi.items()
|
|
437
|
+
if key not in older_field_openapi
|
|
438
|
+
]
|
|
439
|
+
|
|
440
|
+
return CadwynFieldAttributesWereChangedChangelogEntry(
|
|
441
|
+
models=_get_affected_model_names(instruction, generator_from_newer_version, schemas_from_older_version),
|
|
442
|
+
field=old_field_name,
|
|
443
|
+
attribute_changes=attribute_changes,
|
|
444
|
+
)
|
|
445
|
+
case EnumDidntHaveMembersInstruction():
|
|
446
|
+
enum = generator_from_newer_version._get_wrapper_for_model(instruction.enum)
|
|
447
|
+
|
|
448
|
+
return CadwynEnumMembersWereAddedChangelogEntry(
|
|
449
|
+
enum=enum.name,
|
|
450
|
+
members=[CadwynEnumMember(name=name, value=value) for name, value in enum.members.items()],
|
|
451
|
+
)
|
|
452
|
+
case EnumHadMembersInstruction():
|
|
453
|
+
new_enum = generator_from_newer_version[instruction.enum]
|
|
454
|
+
old_enum = generator_from_older_version[instruction.enum]
|
|
455
|
+
|
|
456
|
+
return CadwynEnumMembersWereChangedChangelogEntry(
|
|
457
|
+
enum=new_enum.__name__,
|
|
458
|
+
member_changes=[
|
|
459
|
+
CadwynAttributeChange(
|
|
460
|
+
name=name,
|
|
461
|
+
old_value=old_enum.__members__.get(name),
|
|
462
|
+
new_value=new_enum.__members__.get(name),
|
|
463
|
+
status=CadwynAttributeChangeStatus.changed
|
|
464
|
+
if name in new_enum.__members__
|
|
465
|
+
else CadwynAttributeChangeStatus.removed,
|
|
466
|
+
)
|
|
467
|
+
for name in instruction.members
|
|
468
|
+
],
|
|
469
|
+
)
|
|
470
|
+
case SchemaHadInstruction():
|
|
471
|
+
model = generator_from_newer_version._get_wrapper_for_model(instruction.schema)
|
|
472
|
+
|
|
473
|
+
return CadwynSchemaWasChangedChangelogEntry(
|
|
474
|
+
model=instruction.name, modified_attributes=CadwynModelModifiedAttributes(name=model.name)
|
|
475
|
+
)
|
|
476
|
+
case FieldExistedAsInstruction():
|
|
477
|
+
affected_model_names = _get_affected_model_names(
|
|
478
|
+
instruction, generator_from_newer_version, schemas_from_older_version
|
|
479
|
+
)
|
|
480
|
+
return CadwynSchemaFieldWasRemovedChangelogEntry(models=affected_model_names, field=instruction.name)
|
|
481
|
+
case FieldDidntExistInstruction():
|
|
482
|
+
model = generator_from_newer_version[instruction.schema]
|
|
483
|
+
affected_model_names = _get_affected_model_names(
|
|
484
|
+
instruction, generator_from_newer_version, schemas_from_older_version
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
return CadwynSchemaFieldWasAddedChangelogEntry(
|
|
488
|
+
models=affected_model_names,
|
|
489
|
+
field=instruction.name,
|
|
490
|
+
field_info=_get_openapi_representation_of_a_field(model, instruction.name),
|
|
491
|
+
)
|
|
492
|
+
case _: # pragma: no cover
|
|
493
|
+
_logger.warning(
|
|
494
|
+
"Encountered an unknown instruction. "
|
|
495
|
+
"This should not have happened. "
|
|
496
|
+
"Please, contact the author and show him this message and your version bundle: %s.",
|
|
497
|
+
instruction,
|
|
498
|
+
)
|
|
499
|
+
return None
|
cadwyn/route_generation.py
CHANGED
|
@@ -43,9 +43,6 @@ from cadwyn.structure.endpoints import (
|
|
|
43
43
|
EndpointExistedInstruction,
|
|
44
44
|
EndpointHadInstruction,
|
|
45
45
|
)
|
|
46
|
-
from cadwyn.structure.versions import (
|
|
47
|
-
VersionChange,
|
|
48
|
-
)
|
|
49
46
|
|
|
50
47
|
if TYPE_CHECKING:
|
|
51
48
|
from fastapi.dependencies.models import Dependant
|
|
@@ -156,7 +153,7 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
156
153
|
router
|
|
157
154
|
)
|
|
158
155
|
|
|
159
|
-
for version_change in version.
|
|
156
|
+
for version_change in version.changes:
|
|
160
157
|
for by_path_converters in [
|
|
161
158
|
*version_change.alter_response_by_path_instructions.values(),
|
|
162
159
|
*version_change.alter_request_by_path_instructions.values(),
|
|
@@ -230,7 +227,7 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
230
227
|
version: Version,
|
|
231
228
|
):
|
|
232
229
|
routes = router.routes
|
|
233
|
-
for version_change in version.
|
|
230
|
+
for version_change in version.changes:
|
|
234
231
|
for instruction in version_change.alter_endpoint_instructions:
|
|
235
232
|
original_routes = _get_routes(
|
|
236
233
|
routes,
|
|
@@ -330,7 +327,7 @@ class _EndpointTransformer(Generic[_R]):
|
|
|
330
327
|
elif isinstance(instruction, EndpointHadInstruction):
|
|
331
328
|
for original_route in original_routes:
|
|
332
329
|
methods_to_which_we_applied_changes |= original_route.methods
|
|
333
|
-
_apply_endpoint_had_instruction(version_change, instruction, original_route)
|
|
330
|
+
_apply_endpoint_had_instruction(version_change.__name__, instruction, original_route)
|
|
334
331
|
err = (
|
|
335
332
|
'Endpoint "{endpoint_methods} {endpoint_path}" you tried to change in'
|
|
336
333
|
' "{version_change_name}" doesn\'t exist'
|
|
@@ -385,7 +382,7 @@ def _add_data_migrations_to_route(
|
|
|
385
382
|
|
|
386
383
|
|
|
387
384
|
def _apply_endpoint_had_instruction(
|
|
388
|
-
|
|
385
|
+
version_change_name: str,
|
|
389
386
|
instruction: EndpointHadInstruction,
|
|
390
387
|
original_route: APIRoute,
|
|
391
388
|
):
|
|
@@ -396,7 +393,7 @@ def _apply_endpoint_had_instruction(
|
|
|
396
393
|
raise RouterGenerationError(
|
|
397
394
|
f'Expected attribute "{attr_name}" of endpoint'
|
|
398
395
|
f' "{list(original_route.methods)} {original_route.path}"'
|
|
399
|
-
f' to be different in "{
|
|
396
|
+
f' to be different in "{version_change_name}", but it was the same.'
|
|
400
397
|
" It means that your version change has no effect on the attribute"
|
|
401
398
|
" and can be removed.",
|
|
402
399
|
)
|
|
@@ -406,7 +403,7 @@ def _apply_endpoint_had_instruction(
|
|
|
406
403
|
if new_path_params != original_path_params:
|
|
407
404
|
raise RouterPathParamsModifiedError(
|
|
408
405
|
f'When altering the path of "{list(original_route.methods)} {original_route.path}" '
|
|
409
|
-
f'in "{
|
|
406
|
+
f'in "{version_change_name}", you have tried to change its path params '
|
|
410
407
|
f'from "{list(original_path_params)}" to "{list(new_path_params)}". It is not allowed to '
|
|
411
408
|
"change the path params of a route because the endpoint was created to handle the old path "
|
|
412
409
|
"params. In fact, there is no need to change them because the change of path params is "
|