cadwyn 5.4.6__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.
cadwyn/changelogs.py ADDED
@@ -0,0 +1,503 @@
1
+ import copy
2
+ import sys
3
+ from enum import auto
4
+ from logging import getLogger
5
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, cast, get_args
6
+
7
+ from fastapi.openapi.constants import REF_TEMPLATE
8
+ from fastapi.openapi.utils import (
9
+ get_fields_from_routes,
10
+ get_openapi,
11
+ )
12
+ from fastapi.routing import APIRoute
13
+ from pydantic import BaseModel, Field, RootModel
14
+
15
+ from cadwyn._asts import GenericAliasUnionArgs
16
+ from cadwyn._utils import ZIP_STRICT_FALSE, Sentinel
17
+ from cadwyn.route_generation import _get_routes
18
+ from cadwyn.routing import _RootCadwynAPIRouter
19
+ from cadwyn.schema_generation import SchemaGenerator, _change_field_in_model, generate_versioned_models
20
+ from cadwyn.structure.versions import PossibleInstructions, VersionBundle, VersionChange, VersionChangeWithSideEffects
21
+
22
+ from .structure.endpoints import (
23
+ EndpointDidntExistInstruction,
24
+ EndpointExistedInstruction,
25
+ EndpointHadInstruction,
26
+ )
27
+ from .structure.enums import EnumDidntHaveMembersInstruction, EnumHadMembersInstruction
28
+ from .structure.schemas import (
29
+ FieldDidntExistInstruction,
30
+ FieldDidntHaveInstruction,
31
+ FieldExistedAsInstruction,
32
+ FieldHadInstruction,
33
+ SchemaHadInstruction,
34
+ ValidatorDidntExistInstruction,
35
+ ValidatorExistedInstruction,
36
+ )
37
+
38
+ if TYPE_CHECKING:
39
+ from fastapi._compat import ModelField
40
+
41
+ if sys.version_info >= (3, 11): # pragma: no cover
42
+ from enum import StrEnum
43
+ else: # pragma: no cover
44
+ from backports.strenum import StrEnum
45
+
46
+ _logger = getLogger(__name__)
47
+
48
+ T = TypeVar("T", bound=Union[PossibleInstructions, type[VersionChange]])
49
+
50
+
51
+ def hidden(instruction_or_version_change: T) -> T:
52
+ if isinstance(
53
+ instruction_or_version_change, (staticmethod, ValidatorDidntExistInstruction, ValidatorExistedInstruction)
54
+ ):
55
+ return instruction_or_version_change
56
+
57
+ instruction_or_version_change.is_hidden_from_changelog = True
58
+ return instruction_or_version_change
59
+
60
+
61
+ def _generate_changelog(versions: VersionBundle, router: _RootCadwynAPIRouter) -> "CadwynChangelogResource":
62
+ changelog = CadwynChangelogResource()
63
+ schema_generators = generate_versioned_models(versions)
64
+ for version, older_version in zip(versions, versions.versions[1:], **ZIP_STRICT_FALSE):
65
+ routes_from_newer_version = router.versioned_routers[version.value].routes
66
+ schemas_from_older_version = get_fields_from_routes(router.versioned_routers[older_version.value].routes)
67
+ version_changelog = CadwynVersion(value=version.value)
68
+ generator_from_newer_version = schema_generators[version.value]
69
+ generator_from_older_version = schema_generators[older_version.value]
70
+ for version_change in version.changes:
71
+ if version_change.is_hidden_from_changelog:
72
+ continue
73
+ version_change_changelog = CadwynVersionChange(
74
+ description=version_change.description,
75
+ side_effects=isinstance(version_change, VersionChangeWithSideEffects),
76
+ )
77
+ for instruction in [
78
+ *version_change.alter_endpoint_instructions,
79
+ *version_change.alter_enum_instructions,
80
+ *version_change.alter_schema_instructions,
81
+ ]:
82
+ if (
83
+ isinstance(instruction, (ValidatorDidntExistInstruction, ValidatorExistedInstruction))
84
+ or instruction.is_hidden_from_changelog
85
+ ):
86
+ continue
87
+ changelog_entry = _convert_version_change_instruction_to_changelog_entry(
88
+ instruction,
89
+ version_change,
90
+ generator_from_newer_version,
91
+ generator_from_older_version,
92
+ schemas_from_older_version,
93
+ cast("list[APIRoute]", routes_from_newer_version),
94
+ )
95
+ if changelog_entry is not None: # pragma: no branch # This should never happen
96
+ version_change_changelog.instructions.append(CadwynVersionChangeInstruction(changelog_entry))
97
+ version_changelog.changes.append(version_change_changelog)
98
+ changelog.versions.append(version_changelog)
99
+ return changelog
100
+
101
+
102
+ def _get_older_field_name(
103
+ schema: type[BaseModel], new_field_name: str, generator_from_older_version: SchemaGenerator
104
+ ) -> str:
105
+ older_model_wrapper = generator_from_older_version._get_wrapper_for_model(schema)
106
+ newer_names_mapping = {
107
+ field.name_from_newer_version: old_name for old_name, field in older_model_wrapper.fields.items()
108
+ }
109
+ return newer_names_mapping[new_field_name]
110
+
111
+
112
+ def _get_affected_model_names(
113
+ instruction: Union[
114
+ FieldExistedAsInstruction,
115
+ FieldDidntExistInstruction,
116
+ FieldHadInstruction,
117
+ FieldDidntHaveInstruction,
118
+ ],
119
+ generator_from_newer_version: SchemaGenerator,
120
+ schemas_from_last_version: "list[ModelField]",
121
+ ):
122
+ changed_model = generator_from_newer_version._get_wrapper_for_model(instruction.schema)
123
+ annotations = [model.field_info.annotation for model in schemas_from_last_version]
124
+ basemodel_annotations: list[type[BaseModel]] = []
125
+ for annotation in annotations:
126
+ basemodel_annotations.extend(_get_all_pydantic_models_from_generic(annotation))
127
+ models = {generator_from_newer_version._get_wrapper_for_model(annotation) for annotation in basemodel_annotations}
128
+ return [
129
+ model.name
130
+ for model in models
131
+ if changed_model == model
132
+ or (
133
+ instruction.name not in model.fields
134
+ and changed_model in (parents := model._get_parents(generator_from_newer_version.model_bundle.schemas))
135
+ and all(instruction.name not in parent.fields for parent in parents[: parents.index(changed_model)])
136
+ )
137
+ ]
138
+
139
+
140
+ def _get_all_pydantic_models_from_generic(annotation: Any) -> list[type[BaseModel]]:
141
+ if not isinstance(annotation, GenericAliasUnionArgs):
142
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
143
+ return [annotation]
144
+ else:
145
+ return []
146
+ sub_annotations = get_args(annotation)
147
+ models = []
148
+
149
+ for sub_annotation in sub_annotations:
150
+ models.extend(_get_all_pydantic_models_from_generic(sub_annotation))
151
+
152
+ return models
153
+
154
+
155
+ def _get_openapi_representation_of_a_field(model: type[BaseModel], field_name: str) -> dict:
156
+ from fastapi._compat import (
157
+ GenerateJsonSchema,
158
+ ModelField,
159
+ get_compat_model_name_map,
160
+ get_definitions,
161
+ )
162
+
163
+ class CadwynDummyModelForRepresentation(BaseModel):
164
+ my_field: model
165
+
166
+ model_name_map = get_compat_model_name_map([CadwynDummyModelForRepresentation.model_fields["my_field"]])
167
+ schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
168
+ _, definitions = get_definitions(
169
+ fields=[ModelField(CadwynDummyModelForRepresentation.model_fields["my_field"], "my_field")],
170
+ schema_generator=schema_generator,
171
+ model_name_map=model_name_map,
172
+ separate_input_output_schemas=False,
173
+ )
174
+
175
+ return definitions[model.__name__]["properties"][field_name]
176
+
177
+
178
+ class ChangelogEntryType(StrEnum):
179
+ endpoint_added = "endpoint.added"
180
+ endpoint_removed = "endpoint.removed"
181
+ endpoint_changed = "endpoint.changed"
182
+ enum_members_added = "enum.members.added"
183
+ enum_members_removed = "enum.members.removed"
184
+ schema_changed = "schema.changed"
185
+ schema_field_removed = "schema.field.removed"
186
+ schema_field_added = "schema.field.added"
187
+ schema_field_attributes_changed = "schema.field.attributes.changed"
188
+
189
+
190
+ class CadwynAttributeChangeStatus(StrEnum):
191
+ added = auto()
192
+ changed = auto()
193
+ removed = auto()
194
+
195
+
196
+ class CadwynEndpointAttributeChange(BaseModel):
197
+ name: str
198
+ new_value: Any
199
+
200
+
201
+ class CadwynAttributeChange(BaseModel):
202
+ name: str
203
+ status: CadwynAttributeChangeStatus
204
+ old_value: Any
205
+ new_value: Any
206
+
207
+
208
+ class CadwynFieldAttributesWereChangedChangelogEntry(BaseModel):
209
+ type: Literal[ChangelogEntryType.schema_field_attributes_changed] = (
210
+ ChangelogEntryType.schema_field_attributes_changed
211
+ )
212
+ models: list[str]
213
+ field: str
214
+ attribute_changes: list[CadwynAttributeChange]
215
+
216
+
217
+ class CadwynModelModifiedAttributes(BaseModel):
218
+ name: Union[str, None]
219
+
220
+
221
+ class CadwynSchemaWasChangedChangelogEntry(BaseModel):
222
+ type: Literal[ChangelogEntryType.schema_changed] = ChangelogEntryType.schema_changed
223
+ model: str
224
+ modified_attributes: CadwynModelModifiedAttributes
225
+
226
+
227
+ class CadwynSchemaFieldWasAddedChangelogEntry(BaseModel):
228
+ type: Literal[ChangelogEntryType.schema_field_added] = ChangelogEntryType.schema_field_added
229
+ models: list[str]
230
+ field: str
231
+ field_info: dict[str, Any]
232
+
233
+
234
+ class CadwynSchemaFieldWasRemovedChangelogEntry(BaseModel):
235
+ type: Literal[ChangelogEntryType.schema_field_removed] = ChangelogEntryType.schema_field_removed
236
+ models: list[str]
237
+ field: str
238
+
239
+
240
+ class CadwynEnumMember(BaseModel):
241
+ name: str
242
+ value: Any
243
+
244
+
245
+ class CadwynEnumMembersWereAddedChangelogEntry(BaseModel):
246
+ type: Literal[ChangelogEntryType.enum_members_added] = ChangelogEntryType.enum_members_added
247
+ enum: str
248
+ members: list[CadwynEnumMember]
249
+
250
+
251
+ class CadwynEnumMembersWereChangedChangelogEntry(BaseModel):
252
+ type: Literal[ChangelogEntryType.enum_members_removed] = ChangelogEntryType.enum_members_removed
253
+ enum: str
254
+ member_changes: list[CadwynAttributeChange]
255
+
256
+
257
+ class HTTPMethod(StrEnum):
258
+ GET = "GET"
259
+ PUT = "PUT"
260
+ POST = "POST"
261
+ DELETE = "DELETE"
262
+ OPTIONS = "OPTIONS"
263
+ HEAD = "HEAD"
264
+ PATCH = "PATCH"
265
+ TRACE = "TRACE"
266
+
267
+
268
+ class CadwynEndpointHadChangelogEntry(BaseModel):
269
+ type: Literal[ChangelogEntryType.endpoint_changed] = ChangelogEntryType.endpoint_changed
270
+ path: str
271
+ methods: list[HTTPMethod]
272
+ changes: list[CadwynEndpointAttributeChange]
273
+
274
+
275
+ class CadwynEndpointWasAddedChangelogEntry(BaseModel):
276
+ type: Literal[ChangelogEntryType.endpoint_added] = ChangelogEntryType.endpoint_added
277
+ path: str
278
+ methods: list[HTTPMethod]
279
+
280
+
281
+ class CadwynEndpointWasRemovedChangelogEntry(BaseModel):
282
+ type: Literal[ChangelogEntryType.endpoint_removed] = ChangelogEntryType.endpoint_removed
283
+ path: str
284
+ methods: list[HTTPMethod]
285
+
286
+
287
+ class CadwynChangelogResource(BaseModel):
288
+ versions: "list[CadwynVersion]" = Field(default_factory=list)
289
+
290
+
291
+ class CadwynVersion(BaseModel):
292
+ value: str
293
+ changes: "list[CadwynVersionChange]" = Field(default_factory=list)
294
+
295
+
296
+ class CadwynVersionChange(BaseModel):
297
+ description: str
298
+ side_effects: bool
299
+ instructions: "list[CadwynVersionChangeInstruction]" = Field(default_factory=list)
300
+
301
+
302
+ CadwynVersionChangeInstruction = RootModel[
303
+ Union[
304
+ CadwynEnumMembersWereAddedChangelogEntry,
305
+ CadwynEnumMembersWereChangedChangelogEntry,
306
+ CadwynEndpointWasAddedChangelogEntry,
307
+ CadwynEndpointWasRemovedChangelogEntry,
308
+ CadwynSchemaFieldWasRemovedChangelogEntry,
309
+ CadwynSchemaFieldWasAddedChangelogEntry,
310
+ CadwynFieldAttributesWereChangedChangelogEntry,
311
+ CadwynEndpointHadChangelogEntry,
312
+ CadwynSchemaWasChangedChangelogEntry,
313
+ ]
314
+ ]
315
+
316
+
317
+ def _convert_version_change_instruction_to_changelog_entry( # noqa: C901
318
+ instruction: PossibleInstructions,
319
+ version_change: type[VersionChange],
320
+ generator_from_newer_version: SchemaGenerator,
321
+ generator_from_older_version: SchemaGenerator,
322
+ schemas_from_older_version: "list[ModelField]",
323
+ routes_from_newer_version: list[APIRoute],
324
+ ):
325
+ if isinstance(instruction, EndpointDidntExistInstruction):
326
+ return CadwynEndpointWasAddedChangelogEntry(
327
+ path=instruction.endpoint_path,
328
+ methods=cast("Any", instruction.endpoint_methods),
329
+ )
330
+ elif isinstance(instruction, EndpointExistedInstruction):
331
+ return CadwynEndpointWasRemovedChangelogEntry(
332
+ path=instruction.endpoint_path,
333
+ methods=cast("Any", instruction.endpoint_methods),
334
+ )
335
+ elif isinstance(instruction, EndpointHadInstruction):
336
+ if instruction.attributes.include_in_schema is not Sentinel:
337
+ return CadwynEndpointWasRemovedChangelogEntry(
338
+ path=instruction.endpoint_path,
339
+ methods=cast("Any", instruction.endpoint_methods),
340
+ )
341
+
342
+ renaming_map = {"operation_id": "operationId"}
343
+
344
+ attribute_changes = []
345
+
346
+ for attr in ["path", "methods", "summary", "description", "tags", "deprecated", "operation_id"]:
347
+ attr_value = getattr(instruction.attributes, attr)
348
+ if attr_value is not Sentinel:
349
+ attribute_changes.append(
350
+ CadwynEndpointAttributeChange(name=renaming_map.get(attr, attr), new_value=attr_value)
351
+ )
352
+ if instruction.attributes.name is not Sentinel and instruction.attributes.summary is Sentinel:
353
+ attribute_changes.append(
354
+ CadwynEndpointAttributeChange(name="summary", new_value=instruction.attributes.name)
355
+ )
356
+
357
+ if any(
358
+ getattr(instruction.attributes, attr) is not Sentinel
359
+ for attr in ["path", "methods", "summary", "description", "tags", "deprecated"]
360
+ ):
361
+ pass
362
+ if any(
363
+ attr is not Sentinel
364
+ for attr in [
365
+ instruction.attributes.response_model,
366
+ instruction.attributes.response_class,
367
+ instruction.attributes.responses,
368
+ instruction.attributes.status_code,
369
+ ]
370
+ ):
371
+ newer_routes = _get_routes(
372
+ routes_from_newer_version,
373
+ instruction.endpoint_path,
374
+ instruction.endpoint_methods,
375
+ instruction.endpoint_func_name,
376
+ is_deleted=False,
377
+ )
378
+ newer_openapi = get_openapi(title="", version="", routes=newer_routes)
379
+ changed_responses = {
380
+ method: route_openapi["responses"]
381
+ for method, route_openapi in newer_openapi["paths"][instruction.endpoint_path].items()
382
+ }
383
+ attribute_changes.append(CadwynEndpointAttributeChange(name="responses", new_value=changed_responses))
384
+ return CadwynEndpointHadChangelogEntry(
385
+ path=instruction.endpoint_path,
386
+ methods=cast("Any", instruction.endpoint_methods),
387
+ changes=attribute_changes,
388
+ )
389
+
390
+ elif isinstance(instruction, (FieldHadInstruction, FieldDidntHaveInstruction)):
391
+ old_field_name = _get_older_field_name(instruction.schema, instruction.name, generator_from_older_version)
392
+
393
+ if isinstance(instruction, FieldHadInstruction) and instruction.new_name is not Sentinel:
394
+ old_field_name_from_this_instruction = instruction.new_name
395
+ attribute_changes = [
396
+ CadwynAttributeChange(
397
+ name="name",
398
+ status=CadwynAttributeChangeStatus.changed,
399
+ old_value=old_field_name,
400
+ new_value=instruction.name,
401
+ )
402
+ ]
403
+ else:
404
+ old_field_name_from_this_instruction = instruction.name
405
+ attribute_changes = []
406
+ newer_model_wrapper = generator_from_newer_version._get_wrapper_for_model(instruction.schema)
407
+ newer_model_wrapper_with_migrated_field = copy.deepcopy(newer_model_wrapper)
408
+ _change_field_in_model(
409
+ newer_model_wrapper_with_migrated_field,
410
+ generator_from_newer_version.model_bundle.schemas,
411
+ alter_schema_instruction=instruction,
412
+ version_change_name=version_change.__name__,
413
+ )
414
+
415
+ older_model = newer_model_wrapper_with_migrated_field.generate_model_copy(generator_from_newer_version)
416
+ newer_model = newer_model_wrapper.generate_model_copy(generator_from_newer_version)
417
+
418
+ newer_field_openapi = _get_openapi_representation_of_a_field(newer_model, instruction.name)
419
+ older_field_openapi = _get_openapi_representation_of_a_field(older_model, old_field_name_from_this_instruction)
420
+
421
+ attribute_changes += [
422
+ CadwynAttributeChange(
423
+ name=key,
424
+ status=CadwynAttributeChangeStatus.changed
425
+ if key in newer_field_openapi
426
+ else CadwynAttributeChangeStatus.removed,
427
+ old_value=old_value,
428
+ new_value=newer_field_openapi.get(key),
429
+ )
430
+ for key, old_value in older_field_openapi.items()
431
+ if old_value != newer_field_openapi.get(key)
432
+ ]
433
+ attribute_changes += [
434
+ CadwynAttributeChange(
435
+ name=key,
436
+ status=CadwynAttributeChangeStatus.added,
437
+ old_value=None,
438
+ new_value=new_value,
439
+ )
440
+ for key, new_value in newer_field_openapi.items()
441
+ if key not in older_field_openapi
442
+ ]
443
+
444
+ return CadwynFieldAttributesWereChangedChangelogEntry(
445
+ models=_get_affected_model_names(instruction, generator_from_newer_version, schemas_from_older_version),
446
+ field=old_field_name,
447
+ attribute_changes=attribute_changes,
448
+ )
449
+ elif isinstance(instruction, EnumDidntHaveMembersInstruction):
450
+ enum = generator_from_newer_version._get_wrapper_for_model(instruction.enum)
451
+
452
+ return CadwynEnumMembersWereAddedChangelogEntry(
453
+ enum=enum.name,
454
+ members=[CadwynEnumMember(name=name, value=value) for name, value in enum.members.items()],
455
+ )
456
+ elif isinstance(instruction, EnumHadMembersInstruction):
457
+ new_enum = generator_from_newer_version[instruction.enum]
458
+ old_enum = generator_from_older_version[instruction.enum]
459
+
460
+ return CadwynEnumMembersWereChangedChangelogEntry(
461
+ enum=new_enum.__name__,
462
+ member_changes=[
463
+ CadwynAttributeChange(
464
+ name=name,
465
+ old_value=old_enum.__members__.get(name),
466
+ new_value=new_enum.__members__.get(name),
467
+ status=CadwynAttributeChangeStatus.changed
468
+ if name in new_enum.__members__
469
+ else CadwynAttributeChangeStatus.removed,
470
+ )
471
+ for name in instruction.members
472
+ ],
473
+ )
474
+ elif isinstance(instruction, SchemaHadInstruction):
475
+ model = generator_from_newer_version._get_wrapper_for_model(instruction.schema)
476
+
477
+ return CadwynSchemaWasChangedChangelogEntry(
478
+ model=instruction.name, modified_attributes=CadwynModelModifiedAttributes(name=model.name)
479
+ )
480
+ elif isinstance(instruction, FieldExistedAsInstruction):
481
+ affected_model_names = _get_affected_model_names(
482
+ instruction, generator_from_newer_version, schemas_from_older_version
483
+ )
484
+ return CadwynSchemaFieldWasRemovedChangelogEntry(models=affected_model_names, field=instruction.name)
485
+ elif isinstance(instruction, FieldDidntExistInstruction):
486
+ model = generator_from_newer_version[instruction.schema]
487
+ affected_model_names = _get_affected_model_names(
488
+ instruction, generator_from_newer_version, schemas_from_older_version
489
+ )
490
+
491
+ return CadwynSchemaFieldWasAddedChangelogEntry(
492
+ models=affected_model_names,
493
+ field=instruction.name,
494
+ field_info=_get_openapi_representation_of_a_field(model, instruction.name),
495
+ )
496
+ else: # pragma: no cover
497
+ _logger.warning(
498
+ "Encountered an unknown instruction. "
499
+ "This should not have happened. "
500
+ "Please, contact the author and show him this message and your version bundle: %s.",
501
+ instruction,
502
+ )
503
+ return None
cadwyn/dependencies.py ADDED
@@ -0,0 +1,5 @@
1
+ from cadwyn._internal.context_vars import CURRENT_DEPENDENCY_SOLVER_OPTIONS, CURRENT_DEPENDENCY_SOLVER_VAR
2
+
3
+
4
+ async def current_dependency_solver() -> CURRENT_DEPENDENCY_SOLVER_OPTIONS:
5
+ return CURRENT_DEPENDENCY_SOLVER_VAR.get("fastapi")
cadwyn/exceptions.py ADDED
@@ -0,0 +1,78 @@
1
+ from typing import Any
2
+
3
+ from fastapi.routing import APIRoute
4
+
5
+
6
+ class CadwynRenderError(Exception):
7
+ pass
8
+
9
+
10
+ class CadwynError(Exception):
11
+ pass
12
+
13
+
14
+ class CadwynHeadRequestValidationError(CadwynError):
15
+ def __init__(self, errors: list[Any], body: Any, version: str) -> None:
16
+ self.errors = errors
17
+ self.body = body
18
+ self.version = version
19
+ super().__init__(
20
+ f"We failed to migrate the request with version={self.version}. "
21
+ "This means that there is some error in your migrations or schema structure that makes it impossible "
22
+ "to migrate the request of that version to latest.\n"
23
+ f"body={self.body}\n\nerrors={self.errors}"
24
+ )
25
+
26
+
27
+ class LintingError(CadwynError):
28
+ pass
29
+
30
+
31
+ class SchemaGenerationError(CadwynError):
32
+ pass
33
+
34
+
35
+ class ModuleIsNotAvailableAsTextError(SchemaGenerationError):
36
+ pass
37
+
38
+
39
+ class InvalidGenerationInstructionError(SchemaGenerationError):
40
+ pass
41
+
42
+
43
+ class RouterGenerationError(CadwynError):
44
+ pass
45
+
46
+
47
+ class RouterPathParamsModifiedError(RouterGenerationError):
48
+ pass
49
+
50
+
51
+ class RouteResponseBySchemaConverterDoesNotApplyToAnythingError(RouterGenerationError):
52
+ pass
53
+
54
+
55
+ class RouteRequestBySchemaConverterDoesNotApplyToAnythingError(RouterGenerationError):
56
+ pass
57
+
58
+
59
+ class RouteByPathConverterDoesNotApplyToAnythingError(RouterGenerationError):
60
+ pass
61
+
62
+
63
+ class RouteAlreadyExistsError(RouterGenerationError):
64
+ def __init__(self, *routes: APIRoute):
65
+ self.routes = routes
66
+ super().__init__(f"The following routes are duplicates of each other: {routes}")
67
+
68
+
69
+ class CadwynStructureError(CadwynError):
70
+ pass
71
+
72
+
73
+ class ModuleIsNotVersionedError(ValueError):
74
+ pass
75
+
76
+
77
+ class ImportFromStringError(CadwynError):
78
+ pass