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/utils.py
CHANGED
@@ -3,8 +3,10 @@ import datetime as dt
|
|
3
3
|
import inspect
|
4
4
|
import logging
|
5
5
|
import re
|
6
|
+
import sys
|
6
7
|
import uuid
|
7
8
|
import warnings
|
9
|
+
from collections.abc import Callable, Mapping, Sequence
|
8
10
|
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
|
9
11
|
from dataclasses import is_dataclass
|
10
12
|
from decimal import Decimal
|
@@ -13,42 +15,42 @@ from types import NoneType, UnionType
|
|
13
15
|
from typing import (
|
14
16
|
TYPE_CHECKING,
|
15
17
|
Any,
|
16
|
-
Callable,
|
17
|
-
Dict,
|
18
18
|
ForwardRef,
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
Sequence,
|
23
|
-
Set,
|
24
|
-
Tuple,
|
25
|
-
Type,
|
19
|
+
Protocol,
|
20
|
+
TypeGuard,
|
21
|
+
TypeVar,
|
26
22
|
Union,
|
27
|
-
_eval_type,
|
28
|
-
_GenericAlias,
|
23
|
+
_eval_type, # type: ignore
|
29
24
|
get_args,
|
30
25
|
get_origin,
|
31
26
|
)
|
32
27
|
|
33
|
-
import dpath
|
28
|
+
import dpath
|
34
29
|
import marshmallow as ma
|
35
30
|
import marshmallow.fields as mf
|
36
|
-
import
|
31
|
+
import marshmallow_dataclass2.collection_field as collection_field
|
37
32
|
import typing_inspect
|
38
33
|
from marshmallow.validate import Equal, OneOf
|
34
|
+
from marshmallow_dataclass2 import class_schema, is_generic_alias_of_dataclass
|
35
|
+
from marshmallow_dataclass2.union_field import Union as UnionField
|
39
36
|
from starlette.responses import Response
|
40
37
|
from typing_inspect import is_final_type, is_generic_type, is_literal_type
|
41
38
|
|
42
39
|
from starmallow.concurrency import contextmanager_in_threadpool
|
43
40
|
from starmallow.datastructures import DefaultPlaceholder, DefaultType
|
44
|
-
|
41
|
+
|
42
|
+
if sys.version_info >= (3, 11):
|
43
|
+
from typing import NotRequired
|
44
|
+
else:
|
45
|
+
# Python 3.10 and below
|
46
|
+
from typing_extensions import NotRequired
|
45
47
|
|
46
48
|
if TYPE_CHECKING: # pragma: nocover
|
47
49
|
from starmallow.routing import APIRoute
|
48
50
|
|
49
51
|
logger = logging.getLogger(__name__)
|
50
52
|
|
51
|
-
status_code_ranges:
|
53
|
+
status_code_ranges: dict[str, str] = {
|
52
54
|
"1XX": "Information",
|
53
55
|
"2XX": "Success",
|
54
56
|
"3XX": "Redirection",
|
@@ -57,7 +59,7 @@ status_code_ranges: Dict[str, str] = {
|
|
57
59
|
"DEFAULT": "Default Response",
|
58
60
|
}
|
59
61
|
|
60
|
-
MARSHMALLOW_ITERABLES:
|
62
|
+
MARSHMALLOW_ITERABLES: tuple[type[mf.Field], ...] = (
|
61
63
|
mf.Dict,
|
62
64
|
mf.List,
|
63
65
|
mf.Mapping,
|
@@ -82,33 +84,38 @@ PY_TO_MF_MAPPING = {
|
|
82
84
|
|
83
85
|
PY_ITERABLES = [
|
84
86
|
list,
|
85
|
-
|
87
|
+
list,
|
86
88
|
collections.abc.Sequence,
|
87
89
|
Sequence,
|
88
90
|
tuple,
|
89
|
-
|
91
|
+
tuple,
|
92
|
+
set,
|
90
93
|
set,
|
91
|
-
Set,
|
92
94
|
frozenset,
|
93
|
-
|
95
|
+
frozenset,
|
96
|
+
dict,
|
94
97
|
dict,
|
95
|
-
Dict,
|
96
98
|
collections.abc.Mapping,
|
97
99
|
Mapping,
|
98
100
|
]
|
99
101
|
|
102
|
+
T = TypeVar("T")
|
103
|
+
|
100
104
|
|
101
|
-
def get_model_field(model: Any, **kwargs) -> mf.Field:
|
105
|
+
def get_model_field(model: Any, **kwargs) -> mf.Field | None:
|
102
106
|
if model == inspect._empty:
|
103
107
|
return None
|
104
108
|
|
105
109
|
if is_marshmallow_dataclass(model):
|
106
110
|
model = model.Schema
|
107
111
|
|
112
|
+
if is_generic_alias_of_dataclass(model):
|
113
|
+
model = class_schema(model)
|
114
|
+
|
108
115
|
if is_marshmallow_schema(model):
|
109
116
|
return mf.Nested(model if isinstance(model, ma.Schema) else model())
|
110
117
|
|
111
|
-
if
|
118
|
+
if is_marshmallow_field_or_generic(model):
|
112
119
|
return model if isinstance(model, mf.Field) else model()
|
113
120
|
|
114
121
|
# Native Python handling
|
@@ -128,10 +135,7 @@ def get_model_field(model: Any, **kwargs) -> mf.Field:
|
|
128
135
|
|
129
136
|
if is_final_type(model):
|
130
137
|
arguments = get_args(model)
|
131
|
-
if arguments
|
132
|
-
subtyp = arguments[0]
|
133
|
-
else:
|
134
|
-
subtyp = Any
|
138
|
+
subtyp = arguments[0] if arguments else Any
|
135
139
|
return get_model_field(subtyp, **kwargs)
|
136
140
|
|
137
141
|
# enumerations
|
@@ -142,9 +146,9 @@ def get_model_field(model: Any, **kwargs) -> mf.Field:
|
|
142
146
|
if typing_inspect.is_union_type(model):
|
143
147
|
if typing_inspect.is_optional_type(model):
|
144
148
|
kwargs["allow_none"] = kwargs.get("allow_none", True)
|
145
|
-
kwargs["dump_default"] = kwargs.get("dump_default"
|
149
|
+
kwargs["dump_default"] = kwargs.get("dump_default")
|
146
150
|
if not kwargs.get("required"):
|
147
|
-
kwargs["load_default"] = kwargs.get("load_default"
|
151
|
+
kwargs["load_default"] = kwargs.get("load_default")
|
148
152
|
kwargs.setdefault("required", False)
|
149
153
|
|
150
154
|
arguments = get_args(model)
|
@@ -152,44 +156,47 @@ def get_model_field(model: Any, **kwargs) -> mf.Field:
|
|
152
156
|
if len(subtypes) == 1:
|
153
157
|
return get_model_field(model, **kwargs)
|
154
158
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
+
union_types = []
|
160
|
+
for subtyp in subtypes:
|
161
|
+
field = get_model_field(subtyp, required=True)
|
162
|
+
if field is not None:
|
163
|
+
union_types.append((subtyp, field))
|
164
|
+
|
165
|
+
return UnionField(union_types, **kwargs)
|
159
166
|
|
160
167
|
origin = get_origin(model)
|
161
168
|
if origin not in PY_ITERABLES:
|
162
169
|
raise Exception(f'Unknown model type, model is {model}')
|
163
170
|
|
164
171
|
arguments = get_args(model)
|
165
|
-
if origin in (list,
|
172
|
+
if origin in (list, list):
|
166
173
|
child_type = get_model_field(arguments[0])
|
167
|
-
return mf.List(child_type, **kwargs)
|
174
|
+
return mf.List(child_type, **kwargs) # type: ignore
|
168
175
|
|
169
176
|
if origin in (collections.abc.Sequence, Sequence) or (
|
170
|
-
origin in (tuple,
|
177
|
+
origin in (tuple, tuple)
|
171
178
|
and len(arguments) == 2
|
172
179
|
and arguments[1] is Ellipsis
|
173
180
|
):
|
174
181
|
child_type = get_model_field(arguments[0])
|
175
|
-
return collection_field.Sequence(child_type, **kwargs)
|
182
|
+
return collection_field.Sequence(child_type, **kwargs) # type: ignore
|
176
183
|
|
177
|
-
if origin in (set,
|
184
|
+
if origin in (set, set):
|
178
185
|
child_type = get_model_field(arguments[0])
|
179
|
-
return collection_field.Set(child_type, frozen=False, **kwargs)
|
186
|
+
return collection_field.Set(child_type, frozen=False, **kwargs) # type: ignore
|
180
187
|
|
181
|
-
if origin in (frozenset,
|
188
|
+
if origin in (frozenset, frozenset):
|
182
189
|
child_type = get_model_field(arguments[0])
|
183
|
-
return collection_field.Set(child_type, frozen=True, **kwargs)
|
190
|
+
return collection_field.Set(child_type, frozen=True, **kwargs) # type: ignore
|
184
191
|
|
185
|
-
if origin in (tuple,
|
186
|
-
child_types = (
|
192
|
+
if origin in (tuple, tuple):
|
193
|
+
child_types = tuple(
|
187
194
|
get_model_field(arg)
|
188
195
|
for arg in arguments
|
189
196
|
)
|
190
|
-
return mf.Tuple(child_types, **kwargs)
|
197
|
+
return mf.Tuple(child_types, **kwargs) # type: ignore
|
191
198
|
|
192
|
-
if origin in (dict,
|
199
|
+
if origin in (dict, dict, collections.abc.Mapping, Mapping):
|
193
200
|
key_type = get_model_field(arguments[0])
|
194
201
|
value_type = get_model_field(arguments[1])
|
195
202
|
return mf.Dict(keys=key_type, values=value_type, **kwargs)
|
@@ -216,7 +223,7 @@ def is_optional(field):
|
|
216
223
|
return get_origin(field) in (Union, UnionType) and type(None) in get_args(field)
|
217
224
|
|
218
225
|
|
219
|
-
def get_path_param_names(path: str) ->
|
226
|
+
def get_path_param_names(path: str) -> set[str]:
|
220
227
|
return set(re.findall("{(.*?)}", path))
|
221
228
|
|
222
229
|
|
@@ -229,16 +236,47 @@ def generate_unique_id(route: "APIRoute") -> str:
|
|
229
236
|
return operation_id
|
230
237
|
|
231
238
|
|
232
|
-
|
233
|
-
|
239
|
+
class MaDataclassProtocol(Protocol):
|
240
|
+
Schema: NotRequired[type[ma.Schema]]
|
241
|
+
|
242
|
+
|
243
|
+
def is_marshmallow_schema(obj: Any) -> TypeGuard[ma.Schema | type[ma.Schema]]:
|
244
|
+
try:
|
245
|
+
return (inspect.isclass(obj) and issubclass(obj, ma.Schema)) or isinstance(obj, ma.Schema)
|
246
|
+
except TypeError:
|
247
|
+
# This is a workaround for the case where obj is a generic type
|
248
|
+
# and issubclass raises a TypeError.
|
249
|
+
return False
|
250
|
+
|
251
|
+
|
252
|
+
def is_marshmallow_field(obj: Any) -> TypeGuard[mf.Field | type[mf.Field]]:
|
253
|
+
try:
|
254
|
+
return (inspect.isclass(obj) and issubclass(obj, mf.Field)) or isinstance(obj, mf.Field)
|
255
|
+
except TypeError:
|
256
|
+
# This is a workaround for the case where obj is a generic type
|
257
|
+
# and issubclass raises a TypeError.
|
258
|
+
return False
|
234
259
|
|
235
260
|
|
236
|
-
def
|
237
|
-
|
261
|
+
def is_marshmallow_field_or_generic(obj: Any) -> TypeGuard[mf.Field | type[mf.Field]]:
|
262
|
+
try:
|
263
|
+
return (
|
264
|
+
(inspect.isclass(obj) and issubclass(obj, mf.Field))
|
265
|
+
or isinstance(obj, mf.Field)
|
266
|
+
or (
|
267
|
+
isinstance(obj, typing_inspect.typingGenericAlias)
|
268
|
+
and lenient_issubclass(get_origin(obj), mf.Field)
|
269
|
+
)
|
270
|
+
)
|
271
|
+
except TypeError:
|
272
|
+
# This is a workaround for the case where obj is a generic type
|
273
|
+
# and issubclass raises a TypeError.
|
274
|
+
return False
|
238
275
|
|
276
|
+
def is_marshmallow_dataclass(obj: MaDataclassProtocol | Any) -> TypeGuard[MaDataclassProtocol]:
|
277
|
+
schema = getattr(obj, 'Schema', None)
|
239
278
|
|
240
|
-
|
241
|
-
return is_dataclass(obj) and hasattr(obj, 'Schema') and is_marshmallow_schema(obj.Schema)
|
279
|
+
return is_dataclass(obj) and schema is not None and is_marshmallow_schema(schema)
|
242
280
|
|
243
281
|
|
244
282
|
def is_async_gen_callable(call: Callable[..., Any]) -> bool:
|
@@ -259,29 +297,32 @@ async def solve_generator(
|
|
259
297
|
*,
|
260
298
|
call: Callable[..., Any],
|
261
299
|
stack: AsyncExitStack,
|
262
|
-
gen_kwargs:
|
300
|
+
gen_kwargs: dict[str, Any],
|
263
301
|
) -> Any:
|
264
302
|
if is_gen_callable(call):
|
265
303
|
cm = contextmanager_in_threadpool(contextmanager(call)(**gen_kwargs))
|
266
304
|
elif is_async_gen_callable(call):
|
267
305
|
cm = asynccontextmanager(call)(**gen_kwargs)
|
306
|
+
else:
|
307
|
+
raise ValueError(f"Cannot solve generator for {call}")
|
268
308
|
return await stack.enter_async_context(cm)
|
269
309
|
|
270
310
|
|
271
|
-
def lenient_issubclass(cls: Any, class_or_tuple:
|
311
|
+
def lenient_issubclass(cls: Any, class_or_tuple: type[Any] | tuple[type[Any | UnionType], ...] | UnionType) -> bool:
|
272
312
|
try:
|
273
|
-
return isinstance(cls, type) and issubclass(cls, class_or_tuple)
|
313
|
+
return isinstance(cls, type) and issubclass(cls, class_or_tuple)
|
274
314
|
except TypeError:
|
275
|
-
|
276
|
-
return False
|
277
|
-
raise # pragma: no cover
|
315
|
+
return False
|
278
316
|
|
279
317
|
|
280
|
-
def eq_marshmallow_fields(left: mf.Field, right: mf.Field) -> bool:
|
318
|
+
def eq_marshmallow_fields(left: mf.Field | Any, right: mf.Field | Any) -> bool:
|
281
319
|
'''
|
282
320
|
Marshmallow Fields don't have an __eq__ functions.
|
283
321
|
This compares them instead.
|
284
322
|
'''
|
323
|
+
if not (isinstance(left, mf.Field) and isinstance(right, mf.Field)):
|
324
|
+
return False
|
325
|
+
|
285
326
|
left_dict = left.__dict__.copy()
|
286
327
|
left_dict.pop('_creation_index', None)
|
287
328
|
right_dict = right.__dict__.copy()
|
@@ -290,7 +331,7 @@ def eq_marshmallow_fields(left: mf.Field, right: mf.Field) -> bool:
|
|
290
331
|
return left_dict == right_dict
|
291
332
|
|
292
333
|
|
293
|
-
def
|
334
|
+
def _dict_creator(current, segments, i, hints: Sequence | None = None):
|
294
335
|
'''
|
295
336
|
Create missing path components. Always create a dictionary.
|
296
337
|
|
@@ -299,17 +340,17 @@ def __dict_creator__(current, segments, i, hints=()):
|
|
299
340
|
segment = segments[i]
|
300
341
|
|
301
342
|
# Infer the type from the hints provided.
|
302
|
-
if i < len(hints):
|
343
|
+
if hints and i < len(hints):
|
303
344
|
current[segment] = hints[i][1]()
|
304
345
|
else:
|
305
346
|
current[segment] = {}
|
306
347
|
|
307
348
|
|
308
|
-
def dict_safe_add(d:
|
309
|
-
dpath.new(d, path, value, separator='.', creator=
|
349
|
+
def dict_safe_add(d: dict, path: str, value: Any):
|
350
|
+
dpath.new(d, path, value, separator='.', creator=_dict_creator)
|
310
351
|
|
311
352
|
|
312
|
-
def deep_dict_update(main_dict:
|
353
|
+
def deep_dict_update(main_dict: dict[Any, Any], update_dict: dict[Any, Any]) -> None:
|
313
354
|
for key, value in update_dict.items():
|
314
355
|
if (
|
315
356
|
key in main_dict
|
@@ -327,7 +368,7 @@ def deep_dict_update(main_dict: Dict[Any, Any], update_dict: Dict[Any, Any]) ->
|
|
327
368
|
main_dict[key] = value
|
328
369
|
|
329
370
|
|
330
|
-
def create_response_model(type_:
|
371
|
+
def create_response_model(type_: type[Any] | Any | ma.Schema | mf.Field) -> mf.Field | None:
|
331
372
|
if type_ in [inspect._empty, None] or (inspect.isclass(type_) and issubclass(type_, Response)):
|
332
373
|
return None
|
333
374
|
|
@@ -343,7 +384,7 @@ def create_response_model(type_: Type[Any]) -> ma.Schema | mf.Field | None:
|
|
343
384
|
def get_value_or_default(
|
344
385
|
first_item: DefaultPlaceholder | DefaultType,
|
345
386
|
*extra_items: DefaultPlaceholder | DefaultType,
|
346
|
-
) ->
|
387
|
+
) -> Any:
|
347
388
|
"""
|
348
389
|
Pass items or `DefaultPlaceholder`s by descending priority.
|
349
390
|
|
@@ -351,12 +392,15 @@ def get_value_or_default(
|
|
351
392
|
|
352
393
|
Otherwise, the first item (a `DefaultPlaceholder`) will be returned.
|
353
394
|
"""
|
354
|
-
items = (first_item,
|
395
|
+
items = (first_item, *extra_items)
|
355
396
|
for item in items:
|
356
397
|
if not isinstance(item, DefaultPlaceholder):
|
357
398
|
return item
|
358
|
-
return first_item
|
359
399
|
|
400
|
+
if isinstance(first_item, DefaultPlaceholder):
|
401
|
+
return first_item.value
|
402
|
+
else:
|
403
|
+
return first_item
|
360
404
|
|
361
405
|
def get_name(endpoint: Callable) -> str:
|
362
406
|
if inspect.isroutine(endpoint) or inspect.isclass(endpoint):
|
@@ -383,14 +427,14 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
|
383
427
|
return typed_signature
|
384
428
|
|
385
429
|
|
386
|
-
def get_typed_annotation(annotation: Any, globalns:
|
430
|
+
def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
|
387
431
|
if isinstance(annotation, str):
|
388
432
|
annotation = ForwardRef(annotation)
|
389
433
|
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
390
434
|
return annotation
|
391
435
|
|
392
436
|
|
393
|
-
def get_typed_return_annotation(call: Callable[...,
|
437
|
+
def get_typed_return_annotation(call: Callable[..., T]) -> T | None:
|
394
438
|
signature = inspect.signature(call)
|
395
439
|
annotation = signature.return_annotation
|
396
440
|
|
starmallow/websockets.py
CHANGED
@@ -13,7 +13,7 @@ class APIWebSocket(WebSocket):
|
|
13
13
|
async def receive_json(
|
14
14
|
self,
|
15
15
|
mode: str = "text",
|
16
|
-
model: ma.Schema = None,
|
16
|
+
model: ma.Schema | type[ma.Schema] | None = None,
|
17
17
|
) -> Any:
|
18
18
|
if mode not in {"text", "binary"}:
|
19
19
|
raise RuntimeError('The "mode" argument should be "text" or "binary".')
|
@@ -24,10 +24,7 @@ class APIWebSocket(WebSocket):
|
|
24
24
|
message = await self.receive()
|
25
25
|
self._raise_on_disconnect(message)
|
26
26
|
|
27
|
-
if mode == "text"
|
28
|
-
text = message["text"]
|
29
|
-
else:
|
30
|
-
text = message["bytes"].decode("utf-8")
|
27
|
+
text = message["text"] if mode == "text" else message["bytes"].decode("utf-8")
|
31
28
|
|
32
29
|
if model:
|
33
30
|
if isinstance(model, ma.Schema):
|
@@ -44,7 +41,7 @@ class APIWebSocket(WebSocket):
|
|
44
41
|
self,
|
45
42
|
data: Any,
|
46
43
|
mode: str = "text",
|
47
|
-
model: ma.Schema = None,
|
44
|
+
model: ma.Schema | type[ma.Schema] | None = None,
|
48
45
|
) -> None:
|
49
46
|
if mode not in {"text", "binary"}:
|
50
47
|
raise RuntimeError('The "mode" argument should be "text" or "binary".')
|
@@ -1,10 +1,11 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: starmallow
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.9.0
|
4
4
|
Summary: StarMallow framework
|
5
5
|
Project-URL: Homepage, https://github.com/mvanderlee/starmallow
|
6
6
|
Author-email: Michiel Vanderlee <jmt.vanderlee@gmail.com>
|
7
|
-
License: MIT
|
7
|
+
License-Expression: MIT
|
8
|
+
License-File: LICENSE.md
|
8
9
|
Classifier: Development Status :: 3 - Alpha
|
9
10
|
Classifier: Environment :: Web Environment
|
10
11
|
Classifier: Framework :: AsyncIO
|
@@ -17,6 +18,8 @@ Classifier: Programming Language :: Python
|
|
17
18
|
Classifier: Programming Language :: Python :: 3 :: Only
|
18
19
|
Classifier: Programming Language :: Python :: 3.10
|
19
20
|
Classifier: Programming Language :: Python :: 3.11
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
20
23
|
Classifier: Topic :: Internet
|
21
24
|
Classifier: Topic :: Internet :: WWW/HTTP
|
22
25
|
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
@@ -28,29 +31,27 @@ Classifier: Typing :: Typed
|
|
28
31
|
Requires-Python: >=3.10
|
29
32
|
Requires-Dist: apispec[marshmallow]<7,>=6
|
30
33
|
Requires-Dist: dpath<3,>=2.1.0
|
31
|
-
Requires-Dist: marshmallow-
|
32
|
-
Requires-Dist: marshmallow
|
33
|
-
Requires-Dist: python-multipart
|
34
|
+
Requires-Dist: marshmallow-dataclass2<9,>=8.8.1
|
35
|
+
Requires-Dist: marshmallow>=3.18.0
|
36
|
+
Requires-Dist: python-multipart>=0.0.20
|
34
37
|
Requires-Dist: pyyaml>=5.4.1
|
35
|
-
Requires-Dist: starlette
|
38
|
+
Requires-Dist: starlette>=0.46
|
36
39
|
Provides-Extra: all
|
37
40
|
Requires-Dist: orjson; extra == 'all'
|
38
41
|
Requires-Dist: ujson>=3.2.1; extra == 'all'
|
39
42
|
Requires-Dist: uvicorn[standard]>=0.12.0; extra == 'all'
|
40
43
|
Provides-Extra: dev
|
41
|
-
Requires-Dist:
|
42
|
-
Requires-Dist:
|
43
|
-
Requires-Dist:
|
44
|
-
Requires-Dist: uvicorn[standard]<0.22.0,>=0.17.0; extra == 'dev'
|
44
|
+
Requires-Dist: pre-commit>=4; extra == 'dev'
|
45
|
+
Requires-Dist: ruff==0.11.6; extra == 'dev'
|
46
|
+
Requires-Dist: uvicorn[standard]>=0.34; extra == 'dev'
|
45
47
|
Provides-Extra: publish
|
46
48
|
Requires-Dist: hatch>=1.7.0; extra == 'publish'
|
47
49
|
Provides-Extra: test
|
48
50
|
Requires-Dist: coverage[toml]<8.0,>=6.5.0; extra == 'test'
|
49
51
|
Requires-Dist: httpx>=0.22.0; extra == 'test'
|
50
|
-
Requires-Dist: isort<6.0.0,>=5.0.6; extra == 'test'
|
51
52
|
Requires-Dist: mypy<2,>=1.1.1; extra == 'test'
|
52
|
-
Requires-Dist: pytest
|
53
|
-
Requires-Dist: ruff==0.
|
53
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
54
|
+
Requires-Dist: ruff==0.11.6; extra == 'test'
|
54
55
|
Description-Content-Type: text/markdown
|
55
56
|
|
56
57
|
# StarMallow
|
@@ -68,7 +69,7 @@ Create a file `main.py` with:
|
|
68
69
|
|
69
70
|
```python
|
70
71
|
from typing import Annotated
|
71
|
-
from
|
72
|
+
from marshmallow_dataclass2 import dataclass
|
72
73
|
from starmallow import Body, Path, StarMallow
|
73
74
|
|
74
75
|
app = StarMallow()
|
@@ -131,7 +132,7 @@ INFO: Application startup complete.
|
|
131
132
|
You can also use class-based views. This can make it easier to organize your code and gives you an easy migration path if you use [flask-smorest](https://flask-smorest.readthedocs.io/)
|
132
133
|
|
133
134
|
```python
|
134
|
-
from
|
135
|
+
from marshmallow_dataclass2 import dataclass
|
135
136
|
from starmallow import StarMallow
|
136
137
|
from starmallow.decorators import route
|
137
138
|
from starmallow.endpoints import APIHTTPEndpoint
|
@@ -0,0 +1,43 @@
|
|
1
|
+
starmallow/__init__.py,sha256=tGlfbHYbodKJlydvWAPia0GpXyB1nRcjV5Quz2iZeUw,322
|
2
|
+
starmallow/applications.py,sha256=wI3mViPAgMAGDUy0PzDLyd6GFQaFY_d1-85LBASATNY,30396
|
3
|
+
starmallow/background.py,sha256=asjTMgO25zqZiKsxcEVBGPKd_Nb7RVZDEmzR4PNy6-k,996
|
4
|
+
starmallow/concurrency.py,sha256=YNIFo8jmHZYXfFkqzL1xFiE5QFwWYnGUsYgAROv041Q,1404
|
5
|
+
starmallow/constants.py,sha256=u0h8cJKhJY0oIZqzr7wpEZG2bPLrw5FroMnn3d8KBNQ,129
|
6
|
+
starmallow/dataclasses.py,sha256=P8Eft25Q5UBhDp-3b0T-n2IOtjrQpxmRUxs3WAwlRFQ,2787
|
7
|
+
starmallow/datastructures.py,sha256=oq2Dz6zcoQx9ctMSSviZMAX_wvNTT9ytkSBtZjcg7bY,853
|
8
|
+
starmallow/decorators.py,sha256=VGzfFYualOcplRK6L3Tu2GrCl3a5yrOgADqWMokqzcQ,4036
|
9
|
+
starmallow/delimited_field.py,sha256=MKQnCp-B8q1R69G6aCgthtWVCISqkKBFpTSnwcfC_8A,4429
|
10
|
+
starmallow/docs.py,sha256=tc077aDFAzHTjpnEpqS8BVMhVolaYFmrqQ2obd1Jsbg,6229
|
11
|
+
starmallow/endpoint.py,sha256=JVJTglAkAydVKIsgKr4dR_X9gzso6OHr_d6xUhq1MNU,17794
|
12
|
+
starmallow/endpoints.py,sha256=2RuKhEX0EpEyWUdlKVfD0WXz_8zNQEduCoZ4UJkybZc,953
|
13
|
+
starmallow/exception_handlers.py,sha256=gr2qLYWEtsIEH28n7OreEiiLVz6Y7b6osRyS9esJbBk,891
|
14
|
+
starmallow/exceptions.py,sha256=arXkENa6dV626t_IDWZKqrh6laeV54PWbVIW0dafT2o,843
|
15
|
+
starmallow/fields.py,sha256=5zXP2JLyTpVnVhl23GYHY9W3Sc5Oc_kRvOWmI7WWNRM,1283
|
16
|
+
starmallow/generics.py,sha256=CE_Wf0vIqkup0mirGa-PL5511DT-avaCkC1Jx4sMe_U,1102
|
17
|
+
starmallow/params.py,sha256=EDnUNVNfRY7uv5D1rzfmIrWKJ85R2ku4l13w1QKG4E8,8720
|
18
|
+
starmallow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
+
starmallow/request_resolver.py,sha256=o9OID0wOUCG1iUuvJVT6-ghzG61Pv08QiznFjzZZ8RE,11689
|
20
|
+
starmallow/requests.py,sha256=o_yNhH9WJ35uAGuoXa5_gihevHeaveOvkP525dbwQSU,843
|
21
|
+
starmallow/responses.py,sha256=H6Ze0R5JG9O8eaS4dffiAn3lI6Y9-hz3aDHdWKGk_nw,2023
|
22
|
+
starmallow/routing.py,sha256=dK6ayN_Ruqz3ZGNQ3pFwqnguF7Vl0oIfD7e8OnnED_Y,45364
|
23
|
+
starmallow/schema_generator.py,sha256=Y4o8v5OUgTZA2byTOcUKONimdF8JjiwD8FZLxCVOF0I,19210
|
24
|
+
starmallow/serializers.py,sha256=Z-42L6is9klknpJ3r1DcGjB7t_txPfRvp-Rvj87_n70,12622
|
25
|
+
starmallow/types.py,sha256=uPjoKwKF06n7b2yTtm0WAO1a_SOwRYJG1xzDy_GKAUg,879
|
26
|
+
starmallow/utils.py,sha256=CudZ2x7iWHr6xi7RT848aSIZuAfDUFzWVCxHuyKvk-E,14146
|
27
|
+
starmallow/websockets.py,sha256=54ctFGgA4A3VFwpUFmlu8aVmHOo4R6x3O_-z5r2BsdI,2232
|
28
|
+
starmallow/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
29
|
+
starmallow/ext/marshmallow/__init__.py,sha256=33jENGdfPq4-CDG0LOmN3KOGW1pXTy7a2oMwy4hrYzM,208
|
30
|
+
starmallow/ext/marshmallow/openapi.py,sha256=SkaGp80qvQxDSatxygN3IIKXQ7GTa01rioY_uddTxEs,10164
|
31
|
+
starmallow/middleware/__init__.py,sha256=vtNm85Z9pUPjJd-9giJGg3YL1wO7Jm5ooXBm31pDOK8,53
|
32
|
+
starmallow/middleware/asyncexitstack.py,sha256=wxugZgPg5yxuXxZjPm-_PMG7hAfOeo2lRLR07eaSWS8,1160
|
33
|
+
starmallow/security/__init__.py,sha256=1rQFBIGnEbE51XDZSSi9NgPjXLScFq3RoLu4vk0KVYw,191
|
34
|
+
starmallow/security/api_key.py,sha256=8WH52R4OjLKYSkXj35AgayFsXM7JQVp1Pf0DzpMl4ms,3108
|
35
|
+
starmallow/security/base.py,sha256=d2bMKCbPB8wh4Ce0b5xSYS9ZHeeyVLnyjpwK_NE505M,1404
|
36
|
+
starmallow/security/http.py,sha256=oOG436aohU9uNM40B0LP0Vg8Pcroj5pjzE1NRGiqOOs,6683
|
37
|
+
starmallow/security/oauth2.py,sha256=G72-wJyvrGyHUbg9hbzf44RBN8zFalZYnHKpRC2I1po,10108
|
38
|
+
starmallow/security/open_id_connect_url.py,sha256=9E-Zwnt-IR3jimOMkvIwnGHTuJMlGmjs7LCf1SGtKT8,1415
|
39
|
+
starmallow/security/utils.py,sha256=7tziAa2Cwa7xhwM_NF4DSY3BHoqVaWgJ21VuV8LvhrY,253
|
40
|
+
starmallow-0.9.0.dist-info/METADATA,sha256=ZQ_7tnMWg4IhxxCo3FhmBwa8OGc42qBRdOwBnBeK9y8,5611
|
41
|
+
starmallow-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
42
|
+
starmallow-0.9.0.dist-info/licenses/LICENSE.md,sha256=QelyGgOzch8CXzy6HrYwHh7nmj0rlWkDA0YzmZ3CPaY,1084
|
43
|
+
starmallow-0.9.0.dist-info/RECORD,,
|
starmallow/union_field.py
DELETED
@@ -1,86 +0,0 @@
|
|
1
|
-
'''Copied from marshmallow_dataclass, https://github.com/lovasoa/marshmallow_dataclass/blob/master/marshmallow_dataclass/union_field.py
|
2
|
-
Didn't want to add the dependency to this project1
|
3
|
-
'''
|
4
|
-
|
5
|
-
import copy
|
6
|
-
import inspect
|
7
|
-
from typing import Any, List, Optional, Tuple
|
8
|
-
|
9
|
-
import typeguard
|
10
|
-
from marshmallow import Schema, ValidationError, fields
|
11
|
-
|
12
|
-
try:
|
13
|
-
from typeguard import TypeCheckError # type: ignore[attr-defined]
|
14
|
-
except ImportError:
|
15
|
-
# typeguard < 3
|
16
|
-
TypeCheckError = TypeError # type: ignore[misc, assignment]
|
17
|
-
|
18
|
-
if "argname" not in inspect.signature(typeguard.check_type).parameters:
|
19
|
-
|
20
|
-
def _check_type(value, expected_type, argname: str):
|
21
|
-
return typeguard.check_type(value=value, expected_type=expected_type)
|
22
|
-
|
23
|
-
else:
|
24
|
-
# typeguard < 3.0.0rc2
|
25
|
-
def _check_type(value, expected_type, argname: str):
|
26
|
-
return typeguard.check_type( # type: ignore[call-overload]
|
27
|
-
value=value, expected_type=expected_type, argname=argname,
|
28
|
-
)
|
29
|
-
|
30
|
-
|
31
|
-
class Union(fields.Field):
|
32
|
-
"""A union field, composed other `Field` classes or instances.
|
33
|
-
This field serializes elements based on their type, with one of its child fields.
|
34
|
-
|
35
|
-
Example: ::
|
36
|
-
|
37
|
-
number_or_string = UnionField([
|
38
|
-
(float, fields.Float()),
|
39
|
-
(str, fields.Str())
|
40
|
-
])
|
41
|
-
|
42
|
-
:param union_fields: A list of types and their associated field instance.
|
43
|
-
:param kwargs: The same keyword arguments that :class:`Field` receives.
|
44
|
-
"""
|
45
|
-
|
46
|
-
def __init__(self, union_fields: List[Tuple[type, fields.Field]], **kwargs):
|
47
|
-
super().__init__(**kwargs)
|
48
|
-
self.union_fields = union_fields
|
49
|
-
|
50
|
-
def _bind_to_schema(self, field_name: str, schema: Schema) -> None:
|
51
|
-
super()._bind_to_schema(field_name, schema)
|
52
|
-
new_union_fields = []
|
53
|
-
for typ, field in self.union_fields:
|
54
|
-
field = copy.deepcopy(field)
|
55
|
-
field._bind_to_schema(field_name, self)
|
56
|
-
new_union_fields.append((typ, field))
|
57
|
-
|
58
|
-
self.union_fields = new_union_fields
|
59
|
-
|
60
|
-
def _serialize(self, value: Any, attr: Optional[str], obj, **kwargs) -> Any:
|
61
|
-
errors = []
|
62
|
-
if value is None:
|
63
|
-
return value
|
64
|
-
for typ, field in self.union_fields:
|
65
|
-
try:
|
66
|
-
_check_type(value=value, expected_type=typ, argname=attr or "anonymous")
|
67
|
-
return field._serialize(value, attr, obj, **kwargs)
|
68
|
-
except TypeCheckError as e:
|
69
|
-
errors.append(e)
|
70
|
-
raise TypeError(
|
71
|
-
f"Unable to serialize value with any of the fields in the union: {errors}",
|
72
|
-
)
|
73
|
-
|
74
|
-
def _deserialize(self, value: Any, attr: Optional[str], data, **kwargs) -> Any:
|
75
|
-
errors = []
|
76
|
-
for typ, field in self.union_fields:
|
77
|
-
try:
|
78
|
-
result = field.deserialize(value, **kwargs)
|
79
|
-
_check_type(
|
80
|
-
value=result, expected_type=typ, argname=attr or "anonymous",
|
81
|
-
)
|
82
|
-
return result
|
83
|
-
except (TypeCheckError, ValidationError) as e:
|
84
|
-
errors.append(e)
|
85
|
-
|
86
|
-
raise ValidationError(errors)
|