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/__init__.py +44 -0
- cadwyn/__main__.py +78 -0
- cadwyn/_asts.py +155 -0
- cadwyn/_importer.py +31 -0
- cadwyn/_internal/__init__.py +0 -0
- cadwyn/_internal/context_vars.py +9 -0
- cadwyn/_render.py +155 -0
- cadwyn/_utils.py +79 -0
- cadwyn/applications.py +484 -0
- cadwyn/changelogs.py +503 -0
- cadwyn/dependencies.py +5 -0
- cadwyn/exceptions.py +78 -0
- cadwyn/middleware.py +131 -0
- cadwyn/py.typed +0 -0
- cadwyn/route_generation.py +536 -0
- cadwyn/routing.py +159 -0
- cadwyn/schema_generation.py +1162 -0
- cadwyn/static/__init__.py +0 -0
- cadwyn/static/docs.html +136 -0
- cadwyn/structure/__init__.py +31 -0
- cadwyn/structure/common.py +18 -0
- cadwyn/structure/data.py +249 -0
- cadwyn/structure/endpoints.py +170 -0
- cadwyn/structure/enums.py +42 -0
- cadwyn/structure/schemas.py +338 -0
- cadwyn/structure/versions.py +756 -0
- cadwyn-5.4.6.dist-info/METADATA +90 -0
- cadwyn-5.4.6.dist-info/RECORD +31 -0
- cadwyn-5.4.6.dist-info/WHEEL +4 -0
- cadwyn-5.4.6.dist-info/entry_points.txt +2 -0
- cadwyn-5.4.6.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
cadwyn/static/docs.html
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>OpenAPI Contracts</title>
|
|
8
|
+
<meta name="robots" content="noindex">
|
|
9
|
+
|
|
10
|
+
<!-- Favicon -->
|
|
11
|
+
<link rel="shortcut icon" type="image/x-icon"
|
|
12
|
+
href="https://cpwebassets.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico">
|
|
13
|
+
<link rel="mask-icon"
|
|
14
|
+
href="https://cpwebassets.codepen.io/assets/favicon/logo-pin-8f3771b1072e3c38bd662872f6b673a722f4b3ca2421637d5596661b4e2132cc.svg"
|
|
15
|
+
color="#111">
|
|
16
|
+
<link rel="canonical" href="https://codepen.io/faaezahmd/pen/dJeRex">
|
|
17
|
+
|
|
18
|
+
<!-- Fonts -->
|
|
19
|
+
<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">
|
|
20
|
+
|
|
21
|
+
<!-- Reset CSS -->
|
|
22
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
|
|
23
|
+
|
|
24
|
+
<!-- Custom Styles -->
|
|
25
|
+
<style>
|
|
26
|
+
body {
|
|
27
|
+
font-family: "Lato", sans-serif;
|
|
28
|
+
background-color: #f4f4f4;
|
|
29
|
+
color: #333;
|
|
30
|
+
margin: 0;
|
|
31
|
+
padding: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.container {
|
|
35
|
+
max-width: 800px;
|
|
36
|
+
margin: 50px auto;
|
|
37
|
+
padding: 20px;
|
|
38
|
+
background-color: #fff;
|
|
39
|
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
40
|
+
border-radius: 5px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
h2 {
|
|
44
|
+
font-size: 32px;
|
|
45
|
+
margin: 0 0 20px;
|
|
46
|
+
text-align: center;
|
|
47
|
+
color: #3498db;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
h2 small {
|
|
51
|
+
display: block;
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
color: #555;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.responsive-table li {
|
|
57
|
+
border-radius: 5px;
|
|
58
|
+
padding: 20px;
|
|
59
|
+
display: flex;
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
margin-bottom: 20px;
|
|
62
|
+
background-color: #ecf0f1;
|
|
63
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.responsive-table .table-header {
|
|
67
|
+
background-color: #3498db;
|
|
68
|
+
font-size: 16px;
|
|
69
|
+
text-transform: uppercase;
|
|
70
|
+
letter-spacing: 0.03em;
|
|
71
|
+
color: #fff;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.responsive-table .col-1,
|
|
75
|
+
.responsive-table .col-2 {
|
|
76
|
+
flex-basis: 50%;
|
|
77
|
+
text-align: left;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.responsive-table .col-1 {
|
|
81
|
+
font-weight: bold;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.responsive-table .col-2 a {
|
|
85
|
+
color: black;
|
|
86
|
+
/* Задаем цвет текста черным */
|
|
87
|
+
text-decoration: none;
|
|
88
|
+
font-weight: bold;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media all and (max-width: 767px) {
|
|
92
|
+
.responsive-table .table-header {
|
|
93
|
+
display: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.responsive-table li {
|
|
97
|
+
display: block;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.responsive-table .col {
|
|
101
|
+
flex-basis: 100%;
|
|
102
|
+
display: flex;
|
|
103
|
+
padding: 10px 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.responsive-table .col:before {
|
|
107
|
+
color: #6C7A89;
|
|
108
|
+
padding-right: 10px;
|
|
109
|
+
content: attr(data-label);
|
|
110
|
+
flex-basis: 50%;
|
|
111
|
+
text-align: right;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
</style>
|
|
116
|
+
</head>
|
|
117
|
+
|
|
118
|
+
<body>
|
|
119
|
+
<div class="container">
|
|
120
|
+
<h2>OpenAPI Contracts</h2>
|
|
121
|
+
<ul class="responsive-table">
|
|
122
|
+
<li class="table-header">
|
|
123
|
+
<div class="col col-1">Version</div>
|
|
124
|
+
<div class="col col-2">URL</div>
|
|
125
|
+
</li>
|
|
126
|
+
{% for name, url in table.items() %}
|
|
127
|
+
<li class="table-row">
|
|
128
|
+
<div class="col col-1">{{ name }}</div>
|
|
129
|
+
<div class="col col-2"><a href="{{ url }}">{{ url }}</a></div>
|
|
130
|
+
</li>
|
|
131
|
+
{% endfor %}
|
|
132
|
+
</ul>
|
|
133
|
+
</div>
|
|
134
|
+
</body>
|
|
135
|
+
|
|
136
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .data import (
|
|
2
|
+
RequestInfo,
|
|
3
|
+
ResponseInfo,
|
|
4
|
+
convert_request_to_next_version_for,
|
|
5
|
+
convert_response_to_previous_version_for,
|
|
6
|
+
)
|
|
7
|
+
from .endpoints import endpoint
|
|
8
|
+
from .enums import enum
|
|
9
|
+
from .schemas import schema
|
|
10
|
+
from .versions import (
|
|
11
|
+
HeadVersion,
|
|
12
|
+
Version,
|
|
13
|
+
VersionBundle,
|
|
14
|
+
VersionChange,
|
|
15
|
+
VersionChangeWithSideEffects,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"HeadVersion",
|
|
20
|
+
"RequestInfo",
|
|
21
|
+
"ResponseInfo",
|
|
22
|
+
"Version",
|
|
23
|
+
"VersionBundle",
|
|
24
|
+
"VersionChange",
|
|
25
|
+
"VersionChangeWithSideEffects",
|
|
26
|
+
"convert_request_to_next_version_for",
|
|
27
|
+
"convert_response_to_previous_version_for",
|
|
28
|
+
"endpoint",
|
|
29
|
+
"enum",
|
|
30
|
+
"schema",
|
|
31
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from typing_extensions import ParamSpec, TypeAlias, TypeVar
|
|
6
|
+
|
|
7
|
+
from cadwyn._utils import DATACLASS_KW_ONLY, DATACLASS_SLOTS
|
|
8
|
+
|
|
9
|
+
VersionedModel = BaseModel
|
|
10
|
+
VersionType: TypeAlias = str
|
|
11
|
+
_P = ParamSpec("_P")
|
|
12
|
+
_R = TypeVar("_R")
|
|
13
|
+
Endpoint: TypeAlias = Callable[_P, _R]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(**DATACLASS_SLOTS, **DATACLASS_KW_ONLY)
|
|
17
|
+
class _HiddenAttributeMixin:
|
|
18
|
+
is_hidden_from_changelog: bool
|
cadwyn/structure/data.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import ClassVar, Union, cast
|
|
6
|
+
|
|
7
|
+
from fastapi import Request, Response
|
|
8
|
+
from starlette.datastructures import MutableHeaders
|
|
9
|
+
from typing_extensions import Any, ParamSpec, overload
|
|
10
|
+
|
|
11
|
+
from cadwyn._utils import same_definition_as_in
|
|
12
|
+
from cadwyn.structure.endpoints import _validate_that_strings_are_valid_http_methods
|
|
13
|
+
|
|
14
|
+
_P = ParamSpec("_P")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# TODO (https://github.com/zmievsa/cadwyn/issues/49): Add form handling
|
|
18
|
+
class RequestInfo:
|
|
19
|
+
__slots__ = ("_cookies", "_query_params", "_request", "body", "headers")
|
|
20
|
+
|
|
21
|
+
def __init__(self, request: Request, body: Any):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.body = body
|
|
24
|
+
self.headers = request.headers.mutablecopy()
|
|
25
|
+
self._cookies = request.cookies
|
|
26
|
+
self._query_params = request.query_params._dict
|
|
27
|
+
self._request = request
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def cookies(self) -> dict[str, str]:
|
|
31
|
+
return self._cookies
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def query_params(self) -> dict[str, str]:
|
|
35
|
+
return self._query_params
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# TODO (https://github.com/zmievsa/cadwyn/issues/111): handle _response.media_type and _response.background
|
|
39
|
+
class ResponseInfo:
|
|
40
|
+
__slots__ = ("_response", "body")
|
|
41
|
+
|
|
42
|
+
def __init__(self, response: Response, body: Any):
|
|
43
|
+
super().__init__()
|
|
44
|
+
self.body = body
|
|
45
|
+
self._response = response
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def status_code(self) -> int:
|
|
49
|
+
return self._response.status_code
|
|
50
|
+
|
|
51
|
+
@status_code.setter
|
|
52
|
+
def status_code(self, value: int):
|
|
53
|
+
self._response.status_code = value
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def headers(self) -> MutableHeaders:
|
|
57
|
+
return self._response.headers
|
|
58
|
+
|
|
59
|
+
@same_definition_as_in(Response.set_cookie)
|
|
60
|
+
def set_cookie(self, *args: Any, **kwargs: Any):
|
|
61
|
+
return self._response.set_cookie(*args, **kwargs)
|
|
62
|
+
|
|
63
|
+
@same_definition_as_in(Response.delete_cookie)
|
|
64
|
+
def delete_cookie(self, *args: Any, **kwargs: Any):
|
|
65
|
+
return self._response.delete_cookie(*args, **kwargs)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class _AlterDataInstruction:
|
|
70
|
+
transformer: Callable[[Any], None]
|
|
71
|
+
owner: type = field(init=False)
|
|
72
|
+
_payload_arg_name: ClassVar[str]
|
|
73
|
+
|
|
74
|
+
def __post_init__(self):
|
|
75
|
+
signature = inspect.signature(self.transformer)
|
|
76
|
+
if list(signature.parameters) != [self._payload_arg_name]:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Method '{self.transformer.__name__}' must have only 1 parameter: {self._payload_arg_name}",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
functools.update_wrapper(self, self.transformer)
|
|
82
|
+
|
|
83
|
+
def __set_name__(self, owner: type, name: str):
|
|
84
|
+
self.owner = owner
|
|
85
|
+
|
|
86
|
+
def __call__(self, __request_or_response: Union[RequestInfo, ResponseInfo], /) -> None:
|
|
87
|
+
return self.transformer(__request_or_response)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class _BaseAlterBySchemaInstruction:
|
|
92
|
+
schemas: tuple[Any, ...]
|
|
93
|
+
check_usage: bool = True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
##########
|
|
97
|
+
# Requests
|
|
98
|
+
##########
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class _BaseAlterRequestInstruction(_AlterDataInstruction):
|
|
103
|
+
_payload_arg_name = "request"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class _AlterRequestBySchemaInstruction(_BaseAlterBySchemaInstruction, _BaseAlterRequestInstruction): ...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class _AlterRequestByPathInstruction(_BaseAlterRequestInstruction):
|
|
112
|
+
path: str
|
|
113
|
+
methods: set[str]
|
|
114
|
+
repr_name = "Request by path converter"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@overload
|
|
118
|
+
def convert_request_to_next_version_for(
|
|
119
|
+
first_schema: type,
|
|
120
|
+
/,
|
|
121
|
+
*additional_schemas: type,
|
|
122
|
+
check_usage: bool = True,
|
|
123
|
+
) -> "type[staticmethod[_P, None]]": ...
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@overload
|
|
127
|
+
def convert_request_to_next_version_for(path: str, methods: list[str], /) -> "type[staticmethod[_P, None]]": ...
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def convert_request_to_next_version_for(
|
|
131
|
+
schema_or_path: Union[type, str],
|
|
132
|
+
methods_or_second_schema: Union[list[str], None, type] = None,
|
|
133
|
+
/,
|
|
134
|
+
*additional_schemas: type,
|
|
135
|
+
check_usage: bool = True,
|
|
136
|
+
) -> "type[staticmethod[_P, None]]":
|
|
137
|
+
_validate_decorator_args(schema_or_path, methods_or_second_schema, additional_schemas)
|
|
138
|
+
|
|
139
|
+
def decorator(transformer: Callable[[RequestInfo], None]) -> Any:
|
|
140
|
+
if isinstance(schema_or_path, str):
|
|
141
|
+
return _AlterRequestByPathInstruction(
|
|
142
|
+
path=schema_or_path,
|
|
143
|
+
methods=set(cast("list", methods_or_second_schema)),
|
|
144
|
+
transformer=transformer,
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
if methods_or_second_schema is None:
|
|
148
|
+
schemas = (schema_or_path,)
|
|
149
|
+
else:
|
|
150
|
+
schemas = (schema_or_path, methods_or_second_schema, *additional_schemas)
|
|
151
|
+
return _AlterRequestBySchemaInstruction(
|
|
152
|
+
schemas=schemas,
|
|
153
|
+
transformer=transformer,
|
|
154
|
+
check_usage=check_usage,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return decorator # pyright: ignore[reportReturnType]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
###########
|
|
161
|
+
# Responses
|
|
162
|
+
###########
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class _BaseAlterResponseInstruction(_AlterDataInstruction):
|
|
167
|
+
_payload_arg_name = "response"
|
|
168
|
+
migrate_http_errors: bool
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class _AlterResponseBySchemaInstruction(_BaseAlterBySchemaInstruction, _BaseAlterResponseInstruction): ...
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class _AlterResponseByPathInstruction(_BaseAlterResponseInstruction):
|
|
177
|
+
path: str
|
|
178
|
+
methods: set[str]
|
|
179
|
+
repr_name = "Response by path converter"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@overload
|
|
183
|
+
def convert_response_to_previous_version_for(
|
|
184
|
+
first_schema: type,
|
|
185
|
+
/,
|
|
186
|
+
*schemas: type,
|
|
187
|
+
migrate_http_errors: bool = False,
|
|
188
|
+
check_usage: bool = True,
|
|
189
|
+
) -> "type[staticmethod[_P, None]]": ...
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@overload
|
|
193
|
+
def convert_response_to_previous_version_for(
|
|
194
|
+
path: str,
|
|
195
|
+
methods: list[str],
|
|
196
|
+
/,
|
|
197
|
+
*,
|
|
198
|
+
migrate_http_errors: bool = False,
|
|
199
|
+
) -> "type[staticmethod[_P, None]]": ...
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def convert_response_to_previous_version_for(
|
|
203
|
+
schema_or_path: Union[type, str],
|
|
204
|
+
methods_or_second_schema: Union[list[str], type, None] = None,
|
|
205
|
+
/,
|
|
206
|
+
*additional_schemas: type,
|
|
207
|
+
migrate_http_errors: bool = False,
|
|
208
|
+
check_usage: bool = True,
|
|
209
|
+
) -> "type[staticmethod[_P, None]]":
|
|
210
|
+
_validate_decorator_args(schema_or_path, methods_or_second_schema, additional_schemas)
|
|
211
|
+
|
|
212
|
+
def decorator(transformer: Callable[[ResponseInfo], None]) -> Any:
|
|
213
|
+
if isinstance(schema_or_path, str):
|
|
214
|
+
# The validation above checks that methods is not None
|
|
215
|
+
return _AlterResponseByPathInstruction(
|
|
216
|
+
path=schema_or_path,
|
|
217
|
+
methods=set(cast("list", methods_or_second_schema)),
|
|
218
|
+
transformer=transformer,
|
|
219
|
+
migrate_http_errors=migrate_http_errors,
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
if methods_or_second_schema is None:
|
|
223
|
+
schemas = (schema_or_path,)
|
|
224
|
+
else:
|
|
225
|
+
schemas = (schema_or_path, methods_or_second_schema, *additional_schemas)
|
|
226
|
+
return _AlterResponseBySchemaInstruction(
|
|
227
|
+
schemas=schemas,
|
|
228
|
+
transformer=transformer,
|
|
229
|
+
migrate_http_errors=migrate_http_errors,
|
|
230
|
+
check_usage=check_usage,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return decorator # pyright: ignore[reportReturnType]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _validate_decorator_args(
|
|
237
|
+
schema_or_path: Union[type, str],
|
|
238
|
+
methods_or_second_schema: Union[list[str], type, None],
|
|
239
|
+
additional_schemas: tuple[type, ...],
|
|
240
|
+
) -> None:
|
|
241
|
+
if isinstance(schema_or_path, str):
|
|
242
|
+
if not isinstance(methods_or_second_schema, list):
|
|
243
|
+
raise TypeError("If path was provided as a first argument, methods must be provided as a second argument")
|
|
244
|
+
_validate_that_strings_are_valid_http_methods(methods_or_second_schema)
|
|
245
|
+
if additional_schemas:
|
|
246
|
+
raise TypeError("If path was provided as a first argument, then additional schemas cannot be added")
|
|
247
|
+
|
|
248
|
+
elif methods_or_second_schema is not None and not isinstance(methods_or_second_schema, type):
|
|
249
|
+
raise TypeError("If schema was provided as a first argument, all other arguments must also be schemas")
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from collections.abc import Callable, Collection, Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Union
|
|
5
|
+
|
|
6
|
+
from fastapi import Response
|
|
7
|
+
from fastapi.params import Depends
|
|
8
|
+
from fastapi.routing import APIRoute
|
|
9
|
+
from starlette.routing import BaseRoute
|
|
10
|
+
|
|
11
|
+
from cadwyn.exceptions import LintingError
|
|
12
|
+
|
|
13
|
+
from .._utils import DATACLASS_SLOTS, Sentinel
|
|
14
|
+
from .common import _HiddenAttributeMixin
|
|
15
|
+
|
|
16
|
+
HTTP_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
20
|
+
class EndpointAttributesPayload:
|
|
21
|
+
# FastAPI API routes also have "endpoint" and "dependency_overrides_provider" fields.
|
|
22
|
+
# We do not use them because:
|
|
23
|
+
# 1. "endpoint" must not change -- otherwise this versioning is doomed
|
|
24
|
+
# 2. "dependency_overrides_provider" is taken from router's attributes
|
|
25
|
+
# 3. "response_model" must not change for the same reason as endpoint
|
|
26
|
+
# The following for the same reason as endpoint:
|
|
27
|
+
# * response_model_include: SetIntStr | DictIntStrAny
|
|
28
|
+
# * response_model_exclude: SetIntStr | DictIntStrAny
|
|
29
|
+
# * response_model_by_alias: bool
|
|
30
|
+
# * response_model_exclude_unset: bool
|
|
31
|
+
# * response_model_exclude_defaults: bool
|
|
32
|
+
# * response_model_exclude_none: bool
|
|
33
|
+
path: str
|
|
34
|
+
response_model: Any
|
|
35
|
+
status_code: int
|
|
36
|
+
tags: list[Union[str, Enum]]
|
|
37
|
+
# Adding/removing dependencies between versions seems like a bad choice.
|
|
38
|
+
# It makes the system overly complex. Instead, we allow people to
|
|
39
|
+
# overwrite all dependencies of a route at once. Hence you always know exactly
|
|
40
|
+
# which dependencies have been specified, no matter how many migrations you have.
|
|
41
|
+
|
|
42
|
+
dependencies: Sequence[Depends]
|
|
43
|
+
summary: str
|
|
44
|
+
description: str
|
|
45
|
+
response_description: str
|
|
46
|
+
responses: dict[Union[int, str], dict[str, Any]]
|
|
47
|
+
deprecated: bool
|
|
48
|
+
methods: set[str]
|
|
49
|
+
operation_id: str
|
|
50
|
+
include_in_schema: bool
|
|
51
|
+
response_class: type[Response]
|
|
52
|
+
name: str
|
|
53
|
+
callbacks: list[BaseRoute]
|
|
54
|
+
openapi_extra: dict[str, Any]
|
|
55
|
+
generate_unique_id_function: Callable[[APIRoute], str]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
59
|
+
class EndpointHadInstruction(_HiddenAttributeMixin):
|
|
60
|
+
endpoint_path: str
|
|
61
|
+
endpoint_methods: set[str]
|
|
62
|
+
endpoint_func_name: Union[str, None]
|
|
63
|
+
attributes: EndpointAttributesPayload
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
67
|
+
class EndpointExistedInstruction(_HiddenAttributeMixin):
|
|
68
|
+
endpoint_path: str
|
|
69
|
+
endpoint_methods: set[str]
|
|
70
|
+
endpoint_func_name: Union[str, None]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
74
|
+
class EndpointDidntExistInstruction(_HiddenAttributeMixin):
|
|
75
|
+
endpoint_path: str
|
|
76
|
+
endpoint_methods: set[str]
|
|
77
|
+
endpoint_func_name: Union[str, None]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
81
|
+
class EndpointInstructionFactory:
|
|
82
|
+
endpoint_path: str
|
|
83
|
+
endpoint_methods: set[str]
|
|
84
|
+
endpoint_func_name: Union[str, None]
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def didnt_exist(self) -> EndpointDidntExistInstruction:
|
|
88
|
+
return EndpointDidntExistInstruction(
|
|
89
|
+
is_hidden_from_changelog=False,
|
|
90
|
+
endpoint_path=self.endpoint_path,
|
|
91
|
+
endpoint_methods=self.endpoint_methods,
|
|
92
|
+
endpoint_func_name=self.endpoint_func_name,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def existed(self) -> EndpointExistedInstruction:
|
|
97
|
+
return EndpointExistedInstruction(
|
|
98
|
+
is_hidden_from_changelog=False,
|
|
99
|
+
endpoint_path=self.endpoint_path,
|
|
100
|
+
endpoint_methods=self.endpoint_methods,
|
|
101
|
+
endpoint_func_name=self.endpoint_func_name,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def had(
|
|
105
|
+
self,
|
|
106
|
+
*,
|
|
107
|
+
path: str = Sentinel,
|
|
108
|
+
response_model: Any = Sentinel,
|
|
109
|
+
status_code: int = Sentinel,
|
|
110
|
+
tags: list[Union[str, Enum]] = Sentinel,
|
|
111
|
+
dependencies: Sequence[Depends] = Sentinel,
|
|
112
|
+
summary: str = Sentinel,
|
|
113
|
+
description: str = Sentinel,
|
|
114
|
+
response_description: str = Sentinel,
|
|
115
|
+
responses: dict[Union[int, str], dict[str, Any]] = Sentinel,
|
|
116
|
+
deprecated: bool = Sentinel,
|
|
117
|
+
methods: list[str] = Sentinel,
|
|
118
|
+
operation_id: str = Sentinel,
|
|
119
|
+
include_in_schema: bool = Sentinel,
|
|
120
|
+
response_class: type[Response] = Sentinel,
|
|
121
|
+
name: str = Sentinel,
|
|
122
|
+
callbacks: list[BaseRoute] = Sentinel,
|
|
123
|
+
openapi_extra: dict[str, Any] = Sentinel,
|
|
124
|
+
generate_unique_id_function: Callable[[APIRoute], str] = Sentinel,
|
|
125
|
+
):
|
|
126
|
+
return EndpointHadInstruction(
|
|
127
|
+
is_hidden_from_changelog=False,
|
|
128
|
+
endpoint_path=self.endpoint_path,
|
|
129
|
+
endpoint_methods=self.endpoint_methods,
|
|
130
|
+
endpoint_func_name=self.endpoint_func_name,
|
|
131
|
+
attributes=EndpointAttributesPayload(
|
|
132
|
+
path=path,
|
|
133
|
+
response_model=response_model,
|
|
134
|
+
status_code=status_code,
|
|
135
|
+
tags=tags,
|
|
136
|
+
dependencies=dependencies,
|
|
137
|
+
summary=summary,
|
|
138
|
+
description=description,
|
|
139
|
+
response_description=response_description,
|
|
140
|
+
responses=responses,
|
|
141
|
+
deprecated=deprecated,
|
|
142
|
+
methods=set(methods) if methods is not Sentinel else Sentinel,
|
|
143
|
+
operation_id=operation_id,
|
|
144
|
+
include_in_schema=include_in_schema,
|
|
145
|
+
response_class=response_class,
|
|
146
|
+
name=name,
|
|
147
|
+
callbacks=callbacks,
|
|
148
|
+
openapi_extra=openapi_extra,
|
|
149
|
+
generate_unique_id_function=generate_unique_id_function,
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def endpoint(path: str, methods: list[str], /, *, func_name: Union[str, None] = None) -> EndpointInstructionFactory:
|
|
155
|
+
_validate_that_strings_are_valid_http_methods(methods)
|
|
156
|
+
|
|
157
|
+
return EndpointInstructionFactory(path, set(methods), func_name)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _validate_that_strings_are_valid_http_methods(methods: Collection[str]):
|
|
161
|
+
invalid_methods = set(methods) - HTTP_METHODS
|
|
162
|
+
if invalid_methods:
|
|
163
|
+
invalid_methods = ", ".join(sorted(invalid_methods))
|
|
164
|
+
raise LintingError(
|
|
165
|
+
f"The following HTTP methods are not valid: {invalid_methods}. "
|
|
166
|
+
"Please use valid HTTP methods such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
AlterEndpointSubInstruction = Union[EndpointDidntExistInstruction, EndpointExistedInstruction, EndpointHadInstruction]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Union
|
|
5
|
+
|
|
6
|
+
from cadwyn._utils import DATACLASS_SLOTS
|
|
7
|
+
|
|
8
|
+
from .common import _HiddenAttributeMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
12
|
+
class EnumHadMembersInstruction(_HiddenAttributeMixin):
|
|
13
|
+
enum: type[Enum]
|
|
14
|
+
members: Mapping[str, Any]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
18
|
+
class EnumDidntHaveMembersInstruction(_HiddenAttributeMixin):
|
|
19
|
+
enum: type[Enum]
|
|
20
|
+
members: tuple[str, ...]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(**DATACLASS_SLOTS)
|
|
24
|
+
class EnumInstructionFactory:
|
|
25
|
+
enum_class: type[Enum]
|
|
26
|
+
|
|
27
|
+
def had(self, **enum_member_to_value_mapping: Any) -> EnumHadMembersInstruction:
|
|
28
|
+
return EnumHadMembersInstruction(
|
|
29
|
+
is_hidden_from_changelog=False, enum=self.enum_class, members=enum_member_to_value_mapping
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def didnt_have(self, *enum_members: str) -> EnumDidntHaveMembersInstruction:
|
|
33
|
+
return EnumDidntHaveMembersInstruction(
|
|
34
|
+
is_hidden_from_changelog=False, enum=self.enum_class, members=enum_members
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def enum(enum_class: type[Enum], /) -> EnumInstructionFactory:
|
|
39
|
+
return EnumInstructionFactory(enum_class)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
AlterEnumSubInstruction = Union[EnumHadMembersInstruction, EnumDidntHaveMembersInstruction]
|