cadwyn 3.15.10__tar.gz → 4.0.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 (44) hide show
  1. {cadwyn-3.15.10 → cadwyn-4.0.0}/PKG-INFO +4 -5
  2. cadwyn-4.0.0/cadwyn/__init__.py +39 -0
  3. cadwyn-4.0.0/cadwyn/__main__.py +78 -0
  4. cadwyn-4.0.0/cadwyn/_asts.py +150 -0
  5. cadwyn-4.0.0/cadwyn/_importer.py +31 -0
  6. cadwyn-4.0.0/cadwyn/_render.py +152 -0
  7. cadwyn-4.0.0/cadwyn/_utils.py +42 -0
  8. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/applications.py +5 -34
  9. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/exceptions.py +11 -3
  10. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/middleware.py +4 -4
  11. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/route_generation.py +22 -450
  12. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/routing.py +2 -5
  13. cadwyn-4.0.0/cadwyn/schema_generation.py +946 -0
  14. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/structure/__init__.py +0 -2
  15. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/structure/schemas.py +50 -49
  16. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/structure/versions.py +24 -137
  17. {cadwyn-3.15.10 → cadwyn-4.0.0}/pyproject.toml +5 -5
  18. cadwyn-3.15.10/cadwyn/__init__.py +0 -22
  19. cadwyn-3.15.10/cadwyn/__main__.py +0 -122
  20. cadwyn-3.15.10/cadwyn/_asts.py +0 -274
  21. cadwyn-3.15.10/cadwyn/_compat.py +0 -151
  22. cadwyn-3.15.10/cadwyn/_package_utils.py +0 -45
  23. cadwyn-3.15.10/cadwyn/_utils.py +0 -142
  24. cadwyn-3.15.10/cadwyn/codegen/README.md +0 -10
  25. cadwyn-3.15.10/cadwyn/codegen/__init__.py +0 -10
  26. cadwyn-3.15.10/cadwyn/codegen/_common.py +0 -168
  27. cadwyn-3.15.10/cadwyn/codegen/_main.py +0 -279
  28. cadwyn-3.15.10/cadwyn/codegen/_plugins/class_migrations.py +0 -423
  29. cadwyn-3.15.10/cadwyn/codegen/_plugins/class_rebuilding.py +0 -109
  30. cadwyn-3.15.10/cadwyn/codegen/_plugins/class_renaming.py +0 -49
  31. cadwyn-3.15.10/cadwyn/codegen/_plugins/import_auto_adding.py +0 -64
  32. cadwyn-3.15.10/cadwyn/codegen/_plugins/module_migrations.py +0 -15
  33. cadwyn-3.15.10/cadwyn/main.py +0 -11
  34. cadwyn-3.15.10/cadwyn/static/__init__.py +0 -0
  35. cadwyn-3.15.10/cadwyn/structure/modules.py +0 -39
  36. {cadwyn-3.15.10 → cadwyn-4.0.0}/LICENSE +0 -0
  37. {cadwyn-3.15.10 → cadwyn-4.0.0}/README.md +0 -0
  38. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/py.typed +0 -0
  39. {cadwyn-3.15.10/cadwyn/codegen/_plugins → cadwyn-4.0.0/cadwyn/static}/__init__.py +0 -0
  40. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/static/docs.html +0 -0
  41. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/structure/common.py +0 -0
  42. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/structure/data.py +0 -0
  43. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/structure/endpoints.py +0 -0
  44. {cadwyn-3.15.10 → cadwyn-4.0.0}/cadwyn/structure/enums.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cadwyn
3
- Version: 3.15.10
3
+ Version: 4.0.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,12 +32,11 @@ 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: better-ast-comments (>=1.2.1,<1.3.0)
36
- Requires-Dist: fastapi (>=0.115.2)
35
+ Requires-Dist: fastapi (>=0.110.0)
37
36
  Requires-Dist: issubclass (>=0.1.2,<0.2.0)
38
37
  Requires-Dist: jinja2 (>=3.1.2)
39
- Requires-Dist: pydantic (>=1.0.0)
40
- Requires-Dist: starlette (>=0.36.3)
38
+ Requires-Dist: pydantic (>=2.0.0)
39
+ Requires-Dist: starlette (>=0.30.0)
41
40
  Requires-Dist: typer (>=0.7.0) ; extra == "cli"
42
41
  Requires-Dist: typing-extensions
43
42
  Project-URL: Documentation, https://docs.cadwyn.dev
@@ -0,0 +1,39 @@
1
+ import importlib.metadata
2
+
3
+ from .applications import Cadwyn
4
+ from .route_generation import VersionedAPIRouter, generate_versioned_routers
5
+ from .schema_generation import migrate_response_body
6
+ from .structure import (
7
+ HeadVersion,
8
+ RequestInfo,
9
+ ResponseInfo,
10
+ Version,
11
+ VersionBundle,
12
+ VersionChange,
13
+ VersionChangeWithSideEffects,
14
+ convert_request_to_next_version_for,
15
+ convert_response_to_previous_version_for,
16
+ endpoint,
17
+ enum,
18
+ schema,
19
+ )
20
+
21
+ __version__ = importlib.metadata.version("cadwyn")
22
+ __all__ = [
23
+ "Cadwyn",
24
+ "VersionedAPIRouter",
25
+ "VersionBundle",
26
+ "HeadVersion",
27
+ "Version",
28
+ "migrate_response_body",
29
+ "generate_versioned_routers",
30
+ "VersionChange",
31
+ "VersionChangeWithSideEffects",
32
+ "endpoint",
33
+ "schema",
34
+ "enum",
35
+ "convert_response_to_previous_version_for",
36
+ "convert_request_to_next_version_for",
37
+ "RequestInfo",
38
+ "ResponseInfo",
39
+ ]
@@ -0,0 +1,78 @@
1
+ from datetime import date
2
+ from typing import Annotated
3
+
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.syntax import Syntax
7
+
8
+ from cadwyn._render import render_model_by_path, render_module_by_path
9
+
10
+ _CONSOLE = Console()
11
+ _RAW_ARG = Annotated[bool, typer.Option(help="Output code without color")]
12
+
13
+ app = typer.Typer(
14
+ name="cadwyn",
15
+ add_completion=False,
16
+ help="Modern Stripe-like API versioning in FastAPI",
17
+ )
18
+
19
+ render_subapp = typer.Typer(
20
+ name="render",
21
+ add_completion=False,
22
+ help="Render pydantic models and enums from a certainn version and output them to stdout",
23
+ )
24
+
25
+ app.add_typer(render_subapp)
26
+
27
+
28
+ def version_callback(value: bool):
29
+ if value:
30
+ from . import __version__
31
+
32
+ typer.echo(f"Cadwyn {__version__}")
33
+ raise typer.Exit
34
+
35
+
36
+ def output_code(code: str, raw: bool):
37
+ if raw:
38
+ typer.echo(code)
39
+ else:
40
+ _CONSOLE.print(Syntax(code, "python", line_numbers=True))
41
+
42
+
43
+ @render_subapp.command(
44
+ name="model",
45
+ help="Render a concrete pydantic model or enum from a certain version and output it to stdout",
46
+ short_help="Render a single model or enum",
47
+ )
48
+ def render(
49
+ model: Annotated[str, typer.Argument(metavar="<module>:<attribute>", help="Python path to the model to render")],
50
+ app: Annotated[str, typer.Option(metavar="<module>:<attribute>", help="Python path to the main Cadwyn app")],
51
+ version: Annotated[str, typer.Option(parser=lambda s: str(date.fromisoformat(s)), metavar="ISO-VERSION")],
52
+ raw: _RAW_ARG = False,
53
+ ) -> None:
54
+ output_code(render_model_by_path(model, app, version), raw)
55
+
56
+
57
+ @render_subapp.command(
58
+ name="module",
59
+ help="Render all versioned models and enums within a module from a certain version and output them to stdout",
60
+ short_help="Render all models and enums from an entire module",
61
+ )
62
+ def render_module(
63
+ module: Annotated[str, typer.Argument(metavar="<module>", help="Python path to the module to render")],
64
+ app: Annotated[str, typer.Option(metavar="<module>:<attribute>", help="Python path to the main Cadwyn app")],
65
+ version: Annotated[str, typer.Option(parser=lambda s: str(date.fromisoformat(s)), metavar="ISO-VERSION")],
66
+ raw: _RAW_ARG = False,
67
+ ) -> None:
68
+ output_code(render_module_by_path(module, app, version), raw)
69
+
70
+
71
+ @app.callback()
72
+ def main(
73
+ version: bool = typer.Option(None, "-V", "--version", callback=version_callback, is_eager=True),
74
+ ): ...
75
+
76
+
77
+ if __name__ == "__main__":
78
+ app()
@@ -0,0 +1,150 @@
1
+ import ast
2
+ import inspect
3
+ from collections.abc import Callable
4
+ from enum import Enum, auto
5
+ from types import GenericAlias, LambdaType, NoneType
6
+ from typing import ( # noqa: UP035
7
+ Any,
8
+ List,
9
+ cast,
10
+ get_args,
11
+ get_origin,
12
+ )
13
+
14
+ import annotated_types
15
+
16
+ from cadwyn._utils import PlainRepr, UnionType
17
+ from cadwyn.exceptions import InvalidGenerationInstructionError
18
+
19
+ _LambdaFunctionName = (lambda: None).__name__ # pragma: no branch
20
+
21
+
22
+ # A parent type of typing._GenericAlias
23
+ _BaseGenericAlias = cast(type, type(List[int])).mro()[1] # noqa: UP006
24
+
25
+ # type(list[int]) and type(List[int]) are different which is why we have to do this.
26
+ # Please note that this problem is much wider than just lists which is why we use typing._BaseGenericAlias
27
+ # instead of typing._GenericAlias.
28
+ GenericAliasUnion = GenericAlias | _BaseGenericAlias
29
+
30
+
31
+ def get_fancy_repr(value: Any):
32
+ if isinstance(value, annotated_types.GroupedMetadata) and hasattr(type(value), "__dataclass_fields__"):
33
+ return transform_grouped_metadata(value)
34
+ if isinstance(value, list | tuple | set | frozenset):
35
+ return transform_collection(value)
36
+ if isinstance(value, dict):
37
+ return transform_dict(value)
38
+ if isinstance(value, GenericAliasUnion):
39
+ return transform_generic_alias(value)
40
+ if value is None or value is NoneType:
41
+ return transform_none(value)
42
+ if isinstance(value, type):
43
+ return transform_type(value)
44
+ if isinstance(value, Enum):
45
+ return transform_enum(value)
46
+ if isinstance(value, auto): # pragma: no cover # it works but we no longer use auto
47
+ return transform_auto(value)
48
+ if isinstance(value, UnionType):
49
+ return transform_union(value)
50
+ if isinstance(value, LambdaType) and _LambdaFunctionName == value.__name__:
51
+ return transform_lambda(value)
52
+ if inspect.isfunction(value):
53
+ return transform_function(value)
54
+ else:
55
+ return transform_other(value)
56
+
57
+
58
+ def transform_grouped_metadata(value: "annotated_types.GroupedMetadata"):
59
+ modified_fields = []
60
+ empty_obj = type(value)
61
+
62
+ for key in empty_obj.__dataclass_fields__: # pyright: ignore[reportAttributeAccessIssue]
63
+ if getattr(value, key) != getattr(empty_obj, key):
64
+ modified_fields.append((key, getattr(value, key)))
65
+
66
+ return PlainRepr(
67
+ value.__class__.__name__
68
+ + "("
69
+ + ", ".join(f"{PlainRepr(key)}={get_fancy_repr(v)}" for key, v in modified_fields)
70
+ + ")",
71
+ )
72
+
73
+
74
+ def transform_collection(value: list | tuple | set | frozenset) -> Any:
75
+ return PlainRepr(value.__class__(map(get_fancy_repr, value)))
76
+
77
+
78
+ def transform_dict(value: dict) -> Any:
79
+ return PlainRepr(
80
+ value.__class__((get_fancy_repr(k), get_fancy_repr(v)) for k, v in value.items()),
81
+ )
82
+
83
+
84
+ def transform_generic_alias(value: GenericAliasUnion) -> Any:
85
+ return f"{get_fancy_repr(get_origin(value))}[{', '.join(get_fancy_repr(a) for a in get_args(value))}]"
86
+
87
+
88
+ def transform_none(_: NoneType) -> Any:
89
+ return "None"
90
+
91
+
92
+ def transform_type(value: type) -> Any:
93
+ return value.__name__
94
+
95
+
96
+ def transform_enum(value: Enum) -> Any:
97
+ return PlainRepr(f"{value.__class__.__name__}.{value.name}")
98
+
99
+
100
+ def transform_auto(_: auto) -> Any: # pragma: no cover # it works but we no longer use auto
101
+ return PlainRepr("auto()")
102
+
103
+
104
+ def transform_union(value: UnionType) -> Any:
105
+ return "typing.Union[" + (", ".join(get_fancy_repr(a) for a in get_args(value))) + "]"
106
+
107
+
108
+ def transform_lambda(value: LambdaType) -> Any:
109
+ # We clean source because getsource() can return only a part of the expression which
110
+ # on its own is not a valid expression such as: "\n .had(default_factory=lambda: 91)"
111
+ return _get_lambda_source_from_default_factory(inspect.getsource(value).strip(" \n\t."))
112
+
113
+
114
+ def transform_function(value: Callable) -> Any:
115
+ return PlainRepr(value.__name__)
116
+
117
+
118
+ def transform_other(value: Any) -> Any:
119
+ return PlainRepr(repr(value))
120
+
121
+
122
+ def _get_lambda_source_from_default_factory(source: str) -> str:
123
+ found_lambdas: list[ast.Lambda] = []
124
+
125
+ for node in ast.walk(ast.parse(source)):
126
+ if isinstance(node, ast.keyword) and node.arg == "default_factory" and isinstance(node.value, ast.Lambda):
127
+ found_lambdas.append(node.value)
128
+ if len(found_lambdas) == 1:
129
+ return ast.unparse(found_lambdas[0])
130
+ # These two errors are really hard to cover. Not sure if even possible, honestly :)
131
+ elif len(found_lambdas) == 0: # pragma: no cover
132
+ raise InvalidGenerationInstructionError(
133
+ f"No lambda found in default_factory even though one was passed: {source}",
134
+ )
135
+ else: # pragma: no cover
136
+ raise InvalidGenerationInstructionError(
137
+ "More than one lambda found in default_factory. This is not supported.",
138
+ )
139
+
140
+
141
+ def pop_docstring_from_cls_body(cls_body: list[ast.stmt]) -> list[ast.stmt]:
142
+ if (
143
+ len(cls_body) > 0
144
+ and isinstance(cls_body[0], ast.Expr)
145
+ and isinstance(cls_body[0].value, ast.Constant)
146
+ and isinstance(cls_body[0].value.value, str)
147
+ ):
148
+ return [cls_body.pop(0)]
149
+ else:
150
+ return []
@@ -0,0 +1,31 @@
1
+ import importlib
2
+ from typing import Any
3
+
4
+ from cadwyn.exceptions import ImportFromStringError
5
+
6
+
7
+ def import_attribute_from_string(import_str: str) -> Any:
8
+ module_str, _, attrs_str = import_str.partition(":")
9
+ if not module_str or not attrs_str:
10
+ message = 'Import string "{import_str}" must be in format "<module>:<attribute>".'
11
+ raise ImportFromStringError(message.format(import_str=import_str))
12
+
13
+ module = import_module_from_string(module_str)
14
+
15
+ instance = module
16
+ try:
17
+ for attr_str in attrs_str.split("."):
18
+ instance = getattr(instance, attr_str)
19
+ except AttributeError as e:
20
+ raise ImportFromStringError(f'Attribute "{attrs_str}" not found in module "{module_str}".') from e
21
+
22
+ return instance
23
+
24
+
25
+ def import_module_from_string(module_str: str):
26
+ try:
27
+ return importlib.import_module(module_str)
28
+ except ModuleNotFoundError as e:
29
+ if e.name != module_str: # pragma: no cover
30
+ raise e from None
31
+ raise ImportFromStringError(f'Could not import module "{module_str}".') from e
@@ -0,0 +1,152 @@
1
+ import ast
2
+ import inspect
3
+ import textwrap
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING
6
+
7
+ import typer
8
+ from issubclass import issubclass as lenient_issubclass
9
+ from pydantic import BaseModel
10
+
11
+ from cadwyn._asts import get_fancy_repr, pop_docstring_from_cls_body
12
+ from cadwyn.exceptions import CadwynRenderError
13
+ from cadwyn.schema_generation import (
14
+ PydanticFieldWrapper,
15
+ _EnumWrapper,
16
+ _generate_versioned_models,
17
+ _PydanticRuntimeModelWrapper,
18
+ )
19
+ from cadwyn.structure.versions import VersionBundle, get_cls_pythonpath
20
+
21
+ from ._importer import import_attribute_from_string, import_module_from_string
22
+
23
+ if TYPE_CHECKING:
24
+ from cadwyn.applications import Cadwyn
25
+
26
+
27
+ def render_module_by_path(module_path: str, app_path: str, version: str):
28
+ module = import_module_from_string(module_path)
29
+ app: Cadwyn = import_attribute_from_string(app_path)
30
+ attributes_to_alter = [
31
+ name
32
+ for name, value in module.__dict__.items()
33
+ if lenient_issubclass(value, Enum | BaseModel) and value.__module__ == module.__name__
34
+ ]
35
+
36
+ try:
37
+ module_ast = ast.parse(inspect.getsource(module))
38
+ except (OSError, SyntaxError, ValueError) as e: # pragma: no cover
39
+ raise CadwynRenderError(f"Failed to find the source for module {module.__name__}") from e
40
+
41
+ return ast.unparse(
42
+ ast.Module(
43
+ body=[
44
+ _render_model_from_ast(node, getattr(module, node.name), app.versions, version)
45
+ if isinstance(node, ast.ClassDef) and node.name in attributes_to_alter
46
+ else node
47
+ for node in module_ast.body
48
+ ],
49
+ type_ignores=module_ast.type_ignores,
50
+ )
51
+ )
52
+
53
+
54
+ def render_model_by_path(model_path: str, app_path: str, version: str) -> str:
55
+ # cadwyn render model schemas:MySchema --app=run:app --version=2000-01-01
56
+ model: type[BaseModel | Enum] = import_attribute_from_string(model_path)
57
+ app: Cadwyn = import_attribute_from_string(app_path)
58
+ return render_model(model, app.versions, version)
59
+
60
+
61
+ def render_model(model: type[BaseModel | Enum], versions: VersionBundle, version: str) -> str:
62
+ try:
63
+ original_cls_node = ast.parse(textwrap.dedent(inspect.getsource(model))).body[0]
64
+ except (OSError, SyntaxError, ValueError): # pragma: no cover
65
+ typer.echo(f"Failed to find the source for model {get_cls_pythonpath(model)}")
66
+ return f"class {model.__name__}: 'failed to find the original class source'"
67
+ if not isinstance(original_cls_node, ast.ClassDef):
68
+ raise TypeError(f"{get_cls_pythonpath(model)} is not a class")
69
+
70
+ return ast.unparse(_render_model_from_ast(original_cls_node, model, versions, version))
71
+
72
+
73
+ def _render_model_from_ast(
74
+ model_ast: ast.ClassDef, model: type[BaseModel | Enum], versions: VersionBundle, version: str
75
+ ):
76
+ versioned_models = _generate_versioned_models(versions)
77
+ generator = versioned_models[version]
78
+ wrapper = generator._get_wrapper_for_model(model)
79
+
80
+ if isinstance(wrapper, _EnumWrapper):
81
+ return _render_enum_model(wrapper, model_ast)
82
+ else:
83
+ return _render_pydantic_model(wrapper, model_ast)
84
+
85
+
86
+ def _render_enum_model(wrapper: _EnumWrapper, original_cls_node: ast.ClassDef):
87
+ # This is for possible schema renaming
88
+ original_cls_node.name = wrapper.cls.__name__
89
+
90
+ new_body = [
91
+ ast.Assign(
92
+ targets=[ast.Name(member, ctx=ast.Store())],
93
+ value=ast.Name(get_fancy_repr(member_value)),
94
+ lineno=0,
95
+ )
96
+ for member, member_value in wrapper.members.items()
97
+ ]
98
+
99
+ old_body = [
100
+ n for n in original_cls_node.body if not isinstance(n, ast.AnnAssign | ast.Assign | ast.Pass | ast.Constant)
101
+ ]
102
+ docstring = pop_docstring_from_cls_body(old_body)
103
+
104
+ original_cls_node.body = docstring + new_body + old_body
105
+ if not original_cls_node.body:
106
+ original_cls_node.body = [ast.Pass()]
107
+ return original_cls_node
108
+
109
+
110
+ def _render_pydantic_model(wrapper: _PydanticRuntimeModelWrapper, original_cls_node: ast.ClassDef):
111
+ # This is for possible schema renaming
112
+ original_cls_node.name = wrapper.name
113
+
114
+ field_definitions = [
115
+ ast.AnnAssign(
116
+ target=ast.Name(name),
117
+ annotation=ast.Name(get_fancy_repr(field.annotation)),
118
+ value=_generate_field_ast(field),
119
+ simple=1,
120
+ )
121
+ for name, field in wrapper.fields.items()
122
+ ]
123
+ validator_definitions = [
124
+ ast.parse(textwrap.dedent(inspect.getsource(validator.func))).body[0]
125
+ for validator in wrapper.validators.values()
126
+ if not validator.is_deleted
127
+ ]
128
+
129
+ old_body = [
130
+ n
131
+ for n in original_cls_node.body
132
+ if not (
133
+ isinstance(n, ast.AnnAssign | ast.Assign | ast.Pass | ast.Constant)
134
+ or (isinstance(n, ast.FunctionDef) and n.name in wrapper.validators)
135
+ )
136
+ ]
137
+ docstring = pop_docstring_from_cls_body(old_body)
138
+ original_cls_node.body = docstring + field_definitions + validator_definitions + old_body
139
+ if not original_cls_node.body:
140
+ original_cls_node.body = [ast.Pass()]
141
+ return original_cls_node
142
+
143
+
144
+ def _generate_field_ast(field: PydanticFieldWrapper) -> ast.Call:
145
+ return ast.Call(
146
+ func=ast.Name("Field"),
147
+ args=[],
148
+ keywords=[
149
+ ast.keyword(arg=attr, value=ast.parse(get_fancy_repr(attr_value), mode="eval").body)
150
+ for attr, attr_value in field.passed_field_attributes.items()
151
+ ],
152
+ )
@@ -0,0 +1,42 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, Generic, TypeVar, Union
3
+
4
+ from pydantic._internal._decorators import unwrap_wrapped_function
5
+
6
+ Sentinel: Any = object()
7
+ UnionType = type(int | str) | type(Union[int, str])
8
+ _T = TypeVar("_T", bound=Callable)
9
+
10
+
11
+ _P_T = TypeVar("_P_T")
12
+ _P_R = TypeVar("_P_R")
13
+
14
+
15
+ class classproperty(Generic[_P_T, _P_R]): # noqa: N801
16
+ def __init__(self, func: Callable[[_P_T], _P_R]) -> None:
17
+ super().__init__()
18
+ self.func = func
19
+
20
+ def __get__(self, obj: Any, cls: _P_T) -> _P_R:
21
+ return self.func(cls)
22
+
23
+
24
+ class PlainRepr(str):
25
+ """String class where repr doesn't include quotes"""
26
+
27
+ def __repr__(self) -> str:
28
+ return str(self)
29
+
30
+
31
+ def same_definition_as_in(t: _T) -> Callable[[Callable], _T]:
32
+ def decorator(f: Callable) -> _T:
33
+ return f # pyright: ignore[reportReturnType]
34
+
35
+ return decorator
36
+
37
+
38
+ def fully_unwrap_decorator(func: Callable, is_pydantic_v1_style_validator: Any):
39
+ func = unwrap_wrapped_function(func)
40
+ if is_pydantic_v1_style_validator and func.__closure__:
41
+ func = func.__closure__[0].cell_contents
42
+ return unwrap_wrapped_function(func)
@@ -4,7 +4,6 @@ from collections.abc import Callable, Coroutine, Sequence
4
4
  from datetime import date
5
5
  from logging import getLogger
6
6
  from pathlib import Path
7
- from types import ModuleType
8
7
  from typing import Any, cast
9
8
 
10
9
  from fastapi import APIRouter, FastAPI, HTTPException, routing
@@ -24,7 +23,7 @@ from starlette.requests import Request
24
23
  from starlette.responses import JSONResponse, Response
25
24
  from starlette.routing import BaseRoute, Route
26
25
  from starlette.types import Lifespan
27
- from typing_extensions import Self, deprecated
26
+ from typing_extensions import Self
28
27
 
29
28
  from cadwyn.middleware import HeaderVersioningMiddleware, _get_api_version_dependency
30
29
  from cadwyn.route_generation import generate_versioned_routers
@@ -95,9 +94,6 @@ class Cadwyn(FastAPI):
95
94
  ) -> None:
96
95
  self.versions = versions
97
96
  # TODO: Remove argument entirely in any major version.
98
- latest_schemas_package = extra.pop("latest_schemas_package", None) or self.versions.head_schemas_package
99
- self.versions.head_schemas_package = latest_schemas_package
100
- self._latest_schemas_package = cast(ModuleType, latest_schemas_package)
101
97
  self._dependency_overrides_provider = FakeDependencyOverridesProvider({})
102
98
 
103
99
  super().__init__(
@@ -175,9 +171,10 @@ class Cadwyn(FastAPI):
175
171
 
176
172
  @property
177
173
  def dependency_overrides(self) -> dict[Callable[..., Any], Callable[..., Any]]:
174
+ # TODO: Remove this approach as it is no longer necessary
178
175
  # This is only necessary because we cannot send self to versioned router generator
179
- # because it takes a deepcopy of the router and self.versions.head_schemas_package is a module
180
- # which cannot be copied.
176
+ # because it takes a deepcopy of the router and self.versions.head_schemas_package was a module
177
+ # which couldn't be copied.
181
178
  return self._dependency_overrides_provider.dependency_overrides
182
179
 
183
180
  @dependency_overrides.setter
@@ -187,16 +184,6 @@ class Cadwyn(FastAPI):
187
184
  ) -> None:
188
185
  self._dependency_overrides_provider.dependency_overrides = value
189
186
 
190
- @property # pragma: no cover
191
- @deprecated("It is going to be deleted in the future. Use VersionBundle.head_schemas_package instead")
192
- def latest_schemas_package(self):
193
- return self._latest_schemas_package
194
-
195
- @latest_schemas_package.setter # pragma: no cover
196
- @deprecated("It is going to be deleted in the future. Use VersionBundle.head_schemas_package instead")
197
- def latest_schemas_package(self, value: ModuleType | None):
198
- self._latest_schemas_package = value
199
-
200
187
  def _add_openapi_endpoints(self, unversioned_router: APIRouter):
201
188
  if self.openapi_url is not None:
202
189
  unversioned_router.add_route(
@@ -233,10 +220,7 @@ class Cadwyn(FastAPI):
233
220
  root_router = APIRouter(dependency_overrides_provider=self._dependency_overrides_provider)
234
221
  for router in routers:
235
222
  root_router.include_router(router)
236
- router_versions = generate_versioned_routers(
237
- root_router,
238
- versions=self.versions,
239
- )
223
+ router_versions = generate_versioned_routers(root_router, versions=self.versions)
240
224
  for version, router in router_versions.items():
241
225
  self.add_header_versioned_routers(router, header_value=version.isoformat())
242
226
 
@@ -358,16 +342,3 @@ class Cadwyn(FastAPI):
358
342
  self.router.routes.extend(added_routes)
359
343
 
360
344
  return added_routes
361
-
362
- @deprecated("Use builtin FastAPI methods such as include_router instead")
363
- def add_unversioned_routers(self, *routers: APIRouter):
364
- for router in routers:
365
- self.include_router(router)
366
-
367
- @deprecated("Use builtin FastAPI methods such as add_api_route instead")
368
- def add_unversioned_routes(self, *routes: Route):
369
- router = APIRouter(routes=list(routes))
370
- self.include_router(router)
371
-
372
- @deprecated("It no longer does anything")
373
- def enrich_swagger(self): ...
@@ -5,6 +5,10 @@ from typing import Any
5
5
  from fastapi.routing import APIRoute
6
6
 
7
7
 
8
+ class CadwynRenderError(Exception):
9
+ pass
10
+
11
+
8
12
  class CadwynError(Exception):
9
13
  pass
10
14
 
@@ -26,15 +30,15 @@ class LintingError(CadwynError):
26
30
  pass
27
31
 
28
32
 
29
- class CodeGenerationError(CadwynError):
33
+ class SchemaGenerationError(CadwynError):
30
34
  pass
31
35
 
32
36
 
33
- class ModuleIsNotAvailableAsTextError(CodeGenerationError):
37
+ class ModuleIsNotAvailableAsTextError(SchemaGenerationError):
34
38
  pass
35
39
 
36
40
 
37
- class InvalidGenerationInstructionError(CodeGenerationError):
41
+ class InvalidGenerationInstructionError(SchemaGenerationError):
38
42
  pass
39
43
 
40
44
 
@@ -70,3 +74,7 @@ class CadwynStructureError(CadwynError):
70
74
 
71
75
  class ModuleIsNotVersionedError(ValueError):
72
76
  pass
77
+
78
+
79
+ class ImportFromStringError(CadwynError):
80
+ pass
@@ -64,11 +64,11 @@ class HeaderVersioningMiddleware(BaseHTTPMiddleware):
64
64
  request=request,
65
65
  dependant=self.version_header_validation_dependant,
66
66
  async_exit_stack=async_exit_stack,
67
- embed_body_fields=False,
68
67
  )
69
- if solved_result.errors:
70
- return self.default_response_class(status_code=422, content=_normalize_errors(solved_result.errors))
71
- api_version = cast(date, solved_result.values[self.api_version_header_name.replace("-", "_")])
68
+ values, errors, *_ = solved_result
69
+ if errors:
70
+ return self.default_response_class(status_code=422, content=_normalize_errors(errors))
71
+ api_version = cast(date, values[self.api_version_header_name.replace("-", "_")])
72
72
  self.api_version_var.set(api_version)
73
73
 
74
74
  response = await call_next(request)