cadwyn 4.0.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.
- {cadwyn-4.0.0 → cadwyn-4.2.0}/PKG-INFO +2 -1
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/__init__.py +4 -1
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/_render.py +4 -4
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/applications.py +25 -2
- cadwyn-4.2.0/cadwyn/changelogs.py +499 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/route_generation.py +8 -11
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/schema_generation.py +53 -39
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/structure/common.py +6 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/structure/endpoints.py +6 -5
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/structure/enums.py +4 -2
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/structure/schemas.py +15 -38
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/structure/versions.py +25 -16
- {cadwyn-4.0.0 → cadwyn-4.2.0}/pyproject.toml +3 -2
- {cadwyn-4.0.0 → cadwyn-4.2.0}/LICENSE +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/README.md +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/__main__.py +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/_asts.py +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/_importer.py +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/_utils.py +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/exceptions.py +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/middleware.py +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/py.typed +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/routing.py +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/static/__init__.py +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/static/docs.html +0 -0
- {cadwyn-4.0.0 → cadwyn-4.2.0}/cadwyn/structure/__init__.py +0 -0
- {cadwyn-4.0.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.
|
|
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,8 +1,9 @@
|
|
|
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
|
-
from .schema_generation import migrate_response_body
|
|
6
|
+
from .schema_generation import generate_versioned_models, migrate_response_body
|
|
6
7
|
from .structure import (
|
|
7
8
|
HeadVersion,
|
|
8
9
|
RequestInfo,
|
|
@@ -36,4 +37,6 @@ __all__ = [
|
|
|
36
37
|
"convert_request_to_next_version_for",
|
|
37
38
|
"RequestInfo",
|
|
38
39
|
"ResponseInfo",
|
|
40
|
+
"generate_versioned_models",
|
|
41
|
+
"hidden",
|
|
39
42
|
]
|
|
@@ -13,8 +13,8 @@ from cadwyn.exceptions import CadwynRenderError
|
|
|
13
13
|
from cadwyn.schema_generation import (
|
|
14
14
|
PydanticFieldWrapper,
|
|
15
15
|
_EnumWrapper,
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
_PydanticModelWrapper,
|
|
17
|
+
generate_versioned_models,
|
|
18
18
|
)
|
|
19
19
|
from cadwyn.structure.versions import VersionBundle, get_cls_pythonpath
|
|
20
20
|
|
|
@@ -73,7 +73,7 @@ def render_model(model: type[BaseModel | Enum], versions: VersionBundle, version
|
|
|
73
73
|
def _render_model_from_ast(
|
|
74
74
|
model_ast: ast.ClassDef, model: type[BaseModel | Enum], versions: VersionBundle, version: str
|
|
75
75
|
):
|
|
76
|
-
versioned_models =
|
|
76
|
+
versioned_models = generate_versioned_models(versions)
|
|
77
77
|
generator = versioned_models[version]
|
|
78
78
|
wrapper = generator._get_wrapper_for_model(model)
|
|
79
79
|
|
|
@@ -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
|
|
|
@@ -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,
|
|
@@ -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
|