starmallow 0.7.0__py3-none-any.whl → 0.9.0__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.
- starmallow/__init__.py +1 -1
- starmallow/applications.py +196 -232
- starmallow/background.py +1 -1
- starmallow/concurrency.py +8 -7
- starmallow/dataclasses.py +11 -10
- starmallow/datastructures.py +1 -1
- starmallow/decorators.py +31 -30
- starmallow/delimited_field.py +37 -15
- starmallow/docs.py +5 -5
- starmallow/endpoint.py +105 -79
- starmallow/endpoints.py +3 -2
- starmallow/exceptions.py +3 -3
- starmallow/ext/marshmallow/openapi.py +13 -16
- starmallow/fields.py +3 -3
- starmallow/generics.py +34 -0
- starmallow/middleware/asyncexitstack.py +1 -2
- starmallow/params.py +20 -21
- starmallow/py.typed +0 -0
- starmallow/request_resolver.py +62 -58
- starmallow/responses.py +5 -4
- starmallow/routing.py +231 -239
- starmallow/schema_generator.py +98 -52
- starmallow/security/api_key.py +11 -11
- starmallow/security/base.py +12 -4
- starmallow/security/http.py +31 -26
- starmallow/security/oauth2.py +48 -48
- starmallow/security/open_id_connect_url.py +7 -7
- starmallow/security/utils.py +2 -5
- starmallow/serializers.py +59 -63
- starmallow/types.py +12 -8
- starmallow/utils.py +114 -70
- starmallow/websockets.py +3 -6
- {starmallow-0.7.0.dist-info → starmallow-0.9.0.dist-info}/METADATA +17 -16
- starmallow-0.9.0.dist-info/RECORD +43 -0
- {starmallow-0.7.0.dist-info → starmallow-0.9.0.dist-info}/WHEEL +1 -1
- starmallow/union_field.py +0 -86
- starmallow-0.7.0.dist-info/RECORD +0 -42
- {starmallow-0.7.0.dist-info → starmallow-0.9.0.dist-info}/licenses/LICENSE.md +0 -0
starmallow/params.py
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
import logging
|
2
|
+
from collections.abc import Callable, Iterable, Sequence
|
2
3
|
from enum import Enum
|
3
|
-
from typing import Any,
|
4
|
+
from typing import Any, ClassVar
|
4
5
|
|
5
6
|
import marshmallow as ma
|
6
7
|
import marshmallow.fields as mf
|
7
|
-
from marshmallow.utils import is_iterable_but_not_string
|
8
8
|
from marshmallow.validate import Length, Range, Regexp
|
9
9
|
|
10
10
|
from starmallow.security.base import SecurityBaseResolver
|
@@ -27,17 +27,17 @@ class ParamType(Enum):
|
|
27
27
|
|
28
28
|
|
29
29
|
class Param:
|
30
|
-
in_: ParamType
|
30
|
+
in_: ClassVar[ParamType]
|
31
31
|
|
32
32
|
def __init__(
|
33
33
|
self,
|
34
34
|
default: Any = Ellipsis,
|
35
35
|
*,
|
36
36
|
# alias to look the param up by.
|
37
|
-
alias:
|
37
|
+
alias: str | None = None,
|
38
38
|
deprecated: bool | None = None,
|
39
39
|
include_in_schema: bool = True,
|
40
|
-
model: ma.Schema | mf.Field = None,
|
40
|
+
model: ma.Schema | mf.Field | None = None,
|
41
41
|
validators: None
|
42
42
|
| (
|
43
43
|
Callable[[Any], Any]
|
@@ -52,7 +52,7 @@ class Param:
|
|
52
52
|
max_length: int | None = None,
|
53
53
|
regex: str | None = None,
|
54
54
|
# OpenAPI title
|
55
|
-
title: str = None,
|
55
|
+
title: str | None = None,
|
56
56
|
) -> None:
|
57
57
|
self.default = default
|
58
58
|
self.deprecated = deprecated
|
@@ -79,7 +79,7 @@ class Param:
|
|
79
79
|
self.validators.append(Regexp(regex))
|
80
80
|
|
81
81
|
if validators:
|
82
|
-
if
|
82
|
+
if isinstance(validators, Iterable) and not isinstance(validators, str):
|
83
83
|
self.validators += validators
|
84
84
|
elif callable(validators):
|
85
85
|
self.validators.append(validators)
|
@@ -89,7 +89,7 @@ class Param:
|
|
89
89
|
if self.model and getattr(self.model, 'validators', None) and self.validators:
|
90
90
|
logger.warning('Provided validators will override model validators')
|
91
91
|
|
92
|
-
def __eq__(self, other: "Param") -> bool:
|
92
|
+
def __eq__(self, other: "object | Param") -> bool:
|
93
93
|
return (
|
94
94
|
isinstance(other, Param)
|
95
95
|
and self.__class__ == other.__class__
|
@@ -120,10 +120,10 @@ class Header(Param):
|
|
120
120
|
default: Any = Ellipsis,
|
121
121
|
*,
|
122
122
|
# alias to look the param up by.
|
123
|
-
alias:
|
123
|
+
alias: str | None = None,
|
124
124
|
deprecated: bool | None = None,
|
125
125
|
include_in_schema: bool = True,
|
126
|
-
model: ma.Schema | mf.Field = None,
|
126
|
+
model: ma.Schema | mf.Field | None = None,
|
127
127
|
validators: None
|
128
128
|
| (
|
129
129
|
Callable[[Any], Any]
|
@@ -138,7 +138,7 @@ class Header(Param):
|
|
138
138
|
max_length: int | None = None,
|
139
139
|
regex: str | None = None,
|
140
140
|
# OpenAPI title
|
141
|
-
title: str = None,
|
141
|
+
title: str | None = None,
|
142
142
|
convert_underscores: bool = True,
|
143
143
|
) -> None:
|
144
144
|
self.convert_underscores = convert_underscores
|
@@ -174,7 +174,7 @@ class Body(Param):
|
|
174
174
|
media_type: str = "application/json",
|
175
175
|
deprecated: bool | None = None,
|
176
176
|
include_in_schema: bool = True,
|
177
|
-
model: ma.Schema | mf.Field = None,
|
177
|
+
model: ma.Schema | mf.Field | None = None,
|
178
178
|
validators: None
|
179
179
|
| (
|
180
180
|
Callable[[Any], Any]
|
@@ -188,7 +188,7 @@ class Body(Param):
|
|
188
188
|
min_length: int | None = None,
|
189
189
|
max_length: int | None = None,
|
190
190
|
regex: str | None = None,
|
191
|
-
title: str = None,
|
191
|
+
title: str | None = None,
|
192
192
|
) -> None:
|
193
193
|
super().__init__(
|
194
194
|
default=default,
|
@@ -218,7 +218,7 @@ class Form(Body):
|
|
218
218
|
media_type: str = "application/x-www-form-urlencoded",
|
219
219
|
deprecated: bool | None = None,
|
220
220
|
include_in_schema: bool = True,
|
221
|
-
model: ma.Schema | mf.Field = None,
|
221
|
+
model: ma.Schema | mf.Field | None = None,
|
222
222
|
validators: None
|
223
223
|
| (
|
224
224
|
Callable[[Any], Any]
|
@@ -232,7 +232,7 @@ class Form(Body):
|
|
232
232
|
min_length: int | None = None,
|
233
233
|
max_length: int | None = None,
|
234
234
|
regex: str | None = None,
|
235
|
-
title: str = None,
|
235
|
+
title: str | None = None,
|
236
236
|
) -> None:
|
237
237
|
super().__init__(
|
238
238
|
default=default,
|
@@ -259,14 +259,13 @@ class NoParam:
|
|
259
259
|
|
260
260
|
Arguments will not be added to Swagger docs or be validated in any way.
|
261
261
|
'''
|
262
|
-
pass
|
263
262
|
|
264
263
|
|
265
264
|
class ResolvedParam:
|
266
|
-
def __init__(self, resolver: Callable[[Any], Any] = None, use_cache: bool = True):
|
265
|
+
def __init__(self, resolver: Callable[[Any], Any] | None = None, use_cache: bool = True):
|
267
266
|
self.resolver = resolver
|
268
267
|
# Set when we resolve the routes in the EnpointMixin
|
269
|
-
self.resolver_params:
|
268
|
+
self.resolver_params: dict[ParamType, dict[str, Param]] = {}
|
270
269
|
self.use_cache = use_cache
|
271
270
|
self.cache_key = (self.resolver, None)
|
272
271
|
|
@@ -275,14 +274,14 @@ class Security(ResolvedParam):
|
|
275
274
|
|
276
275
|
def __init__(
|
277
276
|
self,
|
278
|
-
resolver: SecurityBaseResolver = None,
|
279
|
-
scopes:
|
277
|
+
resolver: SecurityBaseResolver | None = None,
|
278
|
+
scopes: Sequence[str] | None = None,
|
280
279
|
use_cache: bool = True,
|
281
280
|
):
|
282
281
|
# Not calling super so that the resolver typehinting actually works in VSCode
|
283
282
|
self.resolver = resolver
|
284
283
|
# Set when we resolve the routes in the EnpointMixin
|
285
|
-
self.resolver_params:
|
284
|
+
self.resolver_params: dict[ParamType, dict[str, Param]] = {}
|
286
285
|
self.scopes = scopes or []
|
287
286
|
self.use_cache = use_cache
|
288
287
|
self.cache_key = (self.resolver, tuple(sorted(set(self.scopes or []))))
|
starmallow/py.typed
ADDED
File without changes
|
starmallow/request_resolver.py
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
import asyncio
|
2
2
|
import inspect
|
3
3
|
import logging
|
4
|
+
from collections.abc import Callable, Mapping
|
4
5
|
from contextlib import AsyncExitStack
|
5
|
-
from typing import Any,
|
6
|
+
from typing import Any, cast
|
6
7
|
|
7
8
|
import marshmallow as ma
|
8
9
|
import marshmallow.fields as mf
|
9
10
|
from marshmallow.error_store import ErrorStore
|
11
|
+
from marshmallow.exceptions import SCHEMA
|
10
12
|
from marshmallow.utils import missing as missing_
|
11
13
|
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
|
12
14
|
from starlette.datastructures import FormData, Headers, QueryParams
|
@@ -17,6 +19,7 @@ from starlette.websockets import WebSocket
|
|
17
19
|
|
18
20
|
from starmallow.background import BackgroundTasks
|
19
21
|
from starmallow.params import Param, ParamType, ResolvedParam
|
22
|
+
from starmallow.security.base import SecurityBaseResolver
|
20
23
|
from starmallow.utils import (
|
21
24
|
is_async_gen_callable,
|
22
25
|
is_gen_callable,
|
@@ -29,9 +32,9 @@ logger = logging.getLogger(__name__)
|
|
29
32
|
|
30
33
|
async def get_body(
|
31
34
|
request: Request,
|
32
|
-
form_params:
|
33
|
-
body_params:
|
34
|
-
) ->
|
35
|
+
form_params: Mapping[str, Param],
|
36
|
+
body_params: Mapping[str, Param],
|
37
|
+
) -> FormData | bytes | dict[str, Any]:
|
35
38
|
is_body_form = bool(form_params)
|
36
39
|
should_process_body = is_body_form or body_params
|
37
40
|
try:
|
@@ -46,18 +49,14 @@ async def get_body(
|
|
46
49
|
body_bytes = await request.body()
|
47
50
|
if body_bytes:
|
48
51
|
json_body: Any = missing_
|
49
|
-
content_type_value
|
52
|
+
content_type_value = request.headers.get("content-type")
|
50
53
|
if not content_type_value:
|
51
54
|
json_body = await request.json()
|
52
55
|
else:
|
53
56
|
main_type, sub_type = content_type_value.split(';')[0].split('/')
|
54
|
-
if main_type == "application":
|
55
|
-
|
56
|
-
|
57
|
-
if json_body != missing_:
|
58
|
-
body = json_body
|
59
|
-
else:
|
60
|
-
body = body_bytes
|
57
|
+
if main_type == "application" and (sub_type == "json" or sub_type.endswith("+json")):
|
58
|
+
json_body = await request.json()
|
59
|
+
body = json_body if json_body != missing_ else body_bytes
|
61
60
|
|
62
61
|
return body
|
63
62
|
except Exception as e:
|
@@ -67,17 +66,20 @@ async def get_body(
|
|
67
66
|
|
68
67
|
|
69
68
|
def request_params_to_args(
|
70
|
-
received_params:
|
71
|
-
endpoint_params:
|
69
|
+
received_params: Mapping[str, Any] | QueryParams | Headers | None,
|
70
|
+
endpoint_params: Mapping[str, Param] | None,
|
72
71
|
ignore_namespace: bool = True,
|
73
|
-
) ->
|
72
|
+
) -> tuple[dict[str, Any], ErrorStore]:
|
73
|
+
received_params = received_params or {}
|
74
|
+
endpoint_params = endpoint_params or {}
|
74
75
|
values = {}
|
75
76
|
error_store = ErrorStore()
|
76
77
|
for field_name, param in endpoint_params.items():
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
78
|
+
alias = (
|
79
|
+
field_name.replace("_", "-")
|
80
|
+
if not param.alias and getattr(param, "convert_underscores", None)
|
81
|
+
else param.alias or field_name
|
82
|
+
)
|
81
83
|
|
82
84
|
if isinstance(param.model, mf.Field):
|
83
85
|
try:
|
@@ -91,20 +93,20 @@ def request_params_to_args(
|
|
91
93
|
error_store.store_error(error.messages, field_name)
|
92
94
|
elif isinstance(param.model, ma.Schema):
|
93
95
|
try:
|
94
|
-
load_params = received_params if ignore_namespace else received_params.get(alias
|
96
|
+
load_params = received_params if ignore_namespace else received_params.get(alias) or {}
|
95
97
|
|
96
98
|
# Entire model is optional and no data was passed in.
|
97
99
|
if getattr(param.model, 'required', None) is False and not load_params:
|
98
100
|
values[field_name] = None
|
99
101
|
else:
|
100
|
-
values[field_name] = param.model.load(load_params, unknown=ma.EXCLUDE)
|
102
|
+
values[field_name] = param.model.load(cast(Mapping, load_params), unknown=ma.EXCLUDE)
|
101
103
|
|
102
104
|
except ma.ValidationError as error:
|
103
105
|
# Entire model is optional, so ignore errors
|
104
106
|
if getattr(param.model, 'required', None) is False:
|
105
107
|
values[field_name] = None
|
106
108
|
else:
|
107
|
-
error_store.store_error(error.messages)
|
109
|
+
error_store.store_error(error.messages, field_name=SCHEMA if ignore_namespace else field_name)
|
108
110
|
else:
|
109
111
|
raise Exception(f'Invalid model type {type(param.model)}, expected marshmallow Schema or Field')
|
110
112
|
|
@@ -115,8 +117,8 @@ async def resolve_basic_args(
|
|
115
117
|
request: Request | WebSocket,
|
116
118
|
response: Response,
|
117
119
|
background_tasks: StarletteBackgroundTasks,
|
118
|
-
params:
|
119
|
-
):
|
120
|
+
params: Mapping[ParamType, Mapping[str, Param]],
|
121
|
+
) -> tuple[dict[str, Any], dict[str, Any | list | dict]]:
|
120
122
|
path_values, path_errors = request_params_to_args(
|
121
123
|
request.path_params,
|
122
124
|
params.get(ParamType.path),
|
@@ -134,27 +136,28 @@ async def resolve_basic_args(
|
|
134
136
|
params.get(ParamType.cookie),
|
135
137
|
)
|
136
138
|
|
137
|
-
form_params = params.get(ParamType.form)
|
138
|
-
body_params = params.get(ParamType.body)
|
139
|
-
body = await get_body(request, form_params, body_params)
|
140
139
|
form_values, form_errors = {}, None
|
141
140
|
json_values, json_errors = {}, None
|
142
|
-
if
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
141
|
+
if isinstance(request, Request):
|
142
|
+
form_params = params.get(ParamType.form) or {}
|
143
|
+
body_params = params.get(ParamType.body) or {}
|
144
|
+
body = await get_body(request, form_params, body_params)
|
145
|
+
if form_params:
|
146
|
+
form_values, form_errors = request_params_to_args(
|
147
|
+
body if body is not None and isinstance(body, FormData) else {},
|
148
|
+
form_params,
|
149
|
+
# If there is only one parameter defined, then don't namespace by the parameter name
|
150
|
+
# Otherwise we honor the namespace: https://fastapi.tiangolo.com/tutorial/body-multiple-params/
|
151
|
+
ignore_namespace=len(form_params) == 1,
|
152
|
+
)
|
153
|
+
if body_params:
|
154
|
+
json_values, json_errors = request_params_to_args(
|
155
|
+
body if body is not None and isinstance(body, Mapping) else {},
|
156
|
+
body_params,
|
157
|
+
# If there is only one parameter defined, then don't namespace by the parameter name
|
158
|
+
# Otherwise we honor the namespace: https://fastapi.tiangolo.com/tutorial/body-multiple-params/
|
159
|
+
ignore_namespace=len(body_params) == 1,
|
160
|
+
)
|
158
161
|
|
159
162
|
values = {
|
160
163
|
**path_values,
|
@@ -179,7 +182,7 @@ async def resolve_basic_args(
|
|
179
182
|
errors['json'] = json_errors.errors
|
180
183
|
|
181
184
|
# Handle non-field params
|
182
|
-
for param_name, param_type in params.get(ParamType.noparam).items():
|
185
|
+
for param_name, param_type in (params.get(ParamType.noparam) or {}).items():
|
183
186
|
if lenient_issubclass(param_type, (HTTPConnection, Request, WebSocket)):
|
184
187
|
values[param_name] = request
|
185
188
|
elif lenient_issubclass(param_type, Response):
|
@@ -194,12 +197,12 @@ async def call_resolver(
|
|
194
197
|
request: Request | WebSocket,
|
195
198
|
param_name: str,
|
196
199
|
resolved_param: ResolvedParam,
|
197
|
-
resolver_kwargs:
|
200
|
+
resolver_kwargs: dict[str, Any],
|
198
201
|
):
|
199
202
|
# Resolver can be a class with __call__ function
|
200
203
|
resolver = resolved_param.resolver
|
201
204
|
if not inspect.isfunction(resolver) and callable(resolver):
|
202
|
-
resolver = resolver.__call__
|
205
|
+
resolver = resolver.__call__ # type: ignore
|
203
206
|
elif not inspect.isfunction(resolver):
|
204
207
|
raise TypeError(f'{param_name} = {resolved_param} resolver is not a function or callable')
|
205
208
|
|
@@ -219,10 +222,11 @@ async def resolve_subparams(
|
|
219
222
|
request: Request | WebSocket,
|
220
223
|
response: Response,
|
221
224
|
background_tasks: StarletteBackgroundTasks,
|
222
|
-
params:
|
223
|
-
dependency_cache:
|
224
|
-
) ->
|
225
|
+
params: Mapping[str, ResolvedParam] | None,
|
226
|
+
dependency_cache: dict[tuple[Callable[..., Any] | SecurityBaseResolver | None, tuple[str, ...] | None], Any],
|
227
|
+
) -> tuple[dict[str, Any], dict[str, Any | list | dict]]:
|
225
228
|
values = {}
|
229
|
+
params = params or {}
|
226
230
|
for param_name, resolved_param in params.items():
|
227
231
|
if resolved_param.use_cache and resolved_param.cache_key in dependency_cache:
|
228
232
|
values[param_name] = dependency_cache[resolved_param.cache_key]
|
@@ -238,9 +242,9 @@ async def resolve_subparams(
|
|
238
242
|
|
239
243
|
# Exit early since other resolvers may rely on this one, which could raise argument exceptions
|
240
244
|
if resolver_errors:
|
241
|
-
return
|
245
|
+
return {}, resolver_errors
|
242
246
|
|
243
|
-
resolved_value = await call_resolver(request, param_name, resolved_param, resolver_kwargs)
|
247
|
+
resolved_value = await call_resolver(request, param_name, resolved_param, resolver_kwargs or {})
|
244
248
|
values[param_name] = resolved_value
|
245
249
|
if resolved_param.use_cache:
|
246
250
|
dependency_cache[resolved_param.cache_key] = resolved_value
|
@@ -250,11 +254,11 @@ async def resolve_subparams(
|
|
250
254
|
|
251
255
|
async def resolve_params(
|
252
256
|
request: Request | WebSocket,
|
253
|
-
params:
|
254
|
-
background_tasks:
|
255
|
-
response:
|
256
|
-
dependency_cache:
|
257
|
-
) ->
|
257
|
+
params: Mapping[ParamType, Mapping[str, Param]],
|
258
|
+
background_tasks: StarletteBackgroundTasks | None = None,
|
259
|
+
response: Response | None = None,
|
260
|
+
dependency_cache: dict[tuple[Callable[..., Any] | SecurityBaseResolver | None, tuple[str, ...] | None], Any] | None = None,
|
261
|
+
) -> tuple[dict[str, Any] | None, dict[str, Any | list | dict], StarletteBackgroundTasks, Response]:
|
258
262
|
dependency_cache = dependency_cache or {}
|
259
263
|
|
260
264
|
if response is None:
|
@@ -270,7 +274,7 @@ async def resolve_params(
|
|
270
274
|
request,
|
271
275
|
response,
|
272
276
|
background_tasks,
|
273
|
-
params.get(ParamType.security),
|
277
|
+
params.get(ParamType.security), # type: ignore
|
274
278
|
dependency_cache=dependency_cache,
|
275
279
|
)
|
276
280
|
if errors:
|
@@ -289,7 +293,7 @@ async def resolve_params(
|
|
289
293
|
request,
|
290
294
|
response,
|
291
295
|
background_tasks,
|
292
|
-
params.get(ParamType.resolved),
|
296
|
+
params.get(ParamType.resolved), # type: ignore
|
293
297
|
dependency_cache=dependency_cache,
|
294
298
|
)
|
295
299
|
if errors:
|
starmallow/responses.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import json
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any
|
3
3
|
|
4
4
|
import marshmallow as ma
|
5
5
|
import marshmallow.fields as mf
|
@@ -21,14 +21,15 @@ except ImportError: # pragma: nocover
|
|
21
21
|
|
22
22
|
|
23
23
|
class JSONResponse(StarJSONResponse):
|
24
|
+
media_type = "application/json"
|
24
25
|
|
25
26
|
def __init__(
|
26
27
|
self,
|
27
28
|
content: Any,
|
28
29
|
status_code: int = 200,
|
29
|
-
headers:
|
30
|
-
media_type:
|
31
|
-
background:
|
30
|
+
headers: dict[str, str] | None = None,
|
31
|
+
media_type: str | None = None,
|
32
|
+
background: BackgroundTask | None = None,
|
32
33
|
) -> None:
|
33
34
|
super().__init__(content, status_code, headers, media_type, background)
|
34
35
|
|