cadwyn 4.1.0__tar.gz → 4.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cadwyn might be problematic. Click here for more details.

Files changed (27) hide show
  1. {cadwyn-4.1.0 → cadwyn-4.2.0}/PKG-INFO +2 -1
  2. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/__init__.py +2 -0
  3. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/_render.py +2 -2
  4. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/applications.py +25 -2
  5. cadwyn-4.2.0/cadwyn/changelogs.py +499 -0
  6. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/route_generation.py +6 -9
  7. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/schema_generation.py +45 -31
  8. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/structure/common.py +6 -0
  9. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/structure/endpoints.py +6 -5
  10. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/structure/enums.py +4 -2
  11. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/structure/schemas.py +15 -38
  12. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/structure/versions.py +25 -16
  13. {cadwyn-4.1.0 → cadwyn-4.2.0}/pyproject.toml +3 -2
  14. {cadwyn-4.1.0 → cadwyn-4.2.0}/LICENSE +0 -0
  15. {cadwyn-4.1.0 → cadwyn-4.2.0}/README.md +0 -0
  16. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/__main__.py +0 -0
  17. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/_asts.py +0 -0
  18. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/_importer.py +0 -0
  19. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/_utils.py +0 -0
  20. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/exceptions.py +0 -0
  21. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/middleware.py +0 -0
  22. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/py.typed +0 -0
  23. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/routing.py +0 -0
  24. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/static/__init__.py +0 -0
  25. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/static/docs.html +0 -0
  26. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/structure/__init__.py +0 -0
  27. {cadwyn-4.1.0 → cadwyn-4.2.0}/cadwyn/structure/data.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cadwyn
3
- Version: 4.1.0
3
+ Version: 4.2.0
4
4
  Summary: Production-ready community-driven modern Stripe-like API versioning in FastAPI
5
5
  Home-page: https://github.com/zmievsa/cadwyn
6
6
  License: MIT
@@ -32,6 +32,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
32
32
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
33
33
  Classifier: Typing :: Typed
34
34
  Provides-Extra: cli
35
+ Requires-Dist: backports-strenum (>=1.3.1,<2.0.0) ; python_version < "3.11"
35
36
  Requires-Dist: fastapi (>=0.110.0)
36
37
  Requires-Dist: issubclass (>=0.1.2,<0.2.0)
37
38
  Requires-Dist: jinja2 (>=3.1.2)
@@ -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
  ]
@@ -13,7 +13,7 @@ from cadwyn.exceptions import CadwynRenderError
13
13
  from cadwyn.schema_generation import (
14
14
  PydanticFieldWrapper,
15
15
  _EnumWrapper,
16
- _PydanticRuntimeModelWrapper,
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: _PydanticRuntimeModelWrapper, original_cls_node: ast.ClassDef):
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
 
@@ -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._add_openapi_endpoints(unversioned_router)
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 _add_openapi_endpoints(self, unversioned_router: APIRouter):
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,
@@ -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
@@ -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.version_changes:
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.version_changes:
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
- version_change: type[VersionChange],
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 "{version_change.__name__}", but it was the same.'
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 "{version_change.__name__}", you have tried to change its path params '
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 "