toms-fast 0.2.1__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.
- toms_fast-0.2.1.dist-info/METADATA +467 -0
- toms_fast-0.2.1.dist-info/RECORD +60 -0
- toms_fast-0.2.1.dist-info/WHEEL +4 -0
- toms_fast-0.2.1.dist-info/entry_points.txt +2 -0
- tomskit/__init__.py +0 -0
- tomskit/celery/README.md +693 -0
- tomskit/celery/__init__.py +4 -0
- tomskit/celery/celery.py +306 -0
- tomskit/celery/config.py +377 -0
- tomskit/cli/__init__.py +207 -0
- tomskit/cli/__main__.py +8 -0
- tomskit/cli/scaffold.py +123 -0
- tomskit/cli/templates/__init__.py +42 -0
- tomskit/cli/templates/base.py +348 -0
- tomskit/cli/templates/celery.py +101 -0
- tomskit/cli/templates/extensions.py +213 -0
- tomskit/cli/templates/fastapi.py +400 -0
- tomskit/cli/templates/migrations.py +281 -0
- tomskit/cli/templates_config.py +122 -0
- tomskit/logger/README.md +466 -0
- tomskit/logger/__init__.py +4 -0
- tomskit/logger/config.py +106 -0
- tomskit/logger/logger.py +290 -0
- tomskit/py.typed +0 -0
- tomskit/redis/README.md +462 -0
- tomskit/redis/__init__.py +6 -0
- tomskit/redis/config.py +85 -0
- tomskit/redis/redis_pool.py +87 -0
- tomskit/redis/redis_sync.py +66 -0
- tomskit/server/__init__.py +47 -0
- tomskit/server/config.py +117 -0
- tomskit/server/exceptions.py +412 -0
- tomskit/server/middleware.py +371 -0
- tomskit/server/parser.py +312 -0
- tomskit/server/resource.py +464 -0
- tomskit/server/server.py +276 -0
- tomskit/server/type.py +263 -0
- tomskit/sqlalchemy/README.md +590 -0
- tomskit/sqlalchemy/__init__.py +20 -0
- tomskit/sqlalchemy/config.py +125 -0
- tomskit/sqlalchemy/database.py +125 -0
- tomskit/sqlalchemy/pagination.py +359 -0
- tomskit/sqlalchemy/property.py +19 -0
- tomskit/sqlalchemy/sqlalchemy.py +131 -0
- tomskit/sqlalchemy/types.py +32 -0
- tomskit/task/README.md +67 -0
- tomskit/task/__init__.py +4 -0
- tomskit/task/task_manager.py +124 -0
- tomskit/tools/README.md +63 -0
- tomskit/tools/__init__.py +18 -0
- tomskit/tools/config.py +70 -0
- tomskit/tools/warnings.py +37 -0
- tomskit/tools/woker.py +81 -0
- tomskit/utils/README.md +666 -0
- tomskit/utils/README_SERIALIZER.md +644 -0
- tomskit/utils/__init__.py +35 -0
- tomskit/utils/fields.py +434 -0
- tomskit/utils/marshal_utils.py +137 -0
- tomskit/utils/response_utils.py +13 -0
- tomskit/utils/serializers.py +447 -0
tomskit/utils/fields.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""
|
|
2
|
+
字段类,支持从异步数据源(如异步数据库查询)获取数据。
|
|
3
|
+
|
|
4
|
+
基于 flask-restful 的 fields 实现,但所有方法都是异步的,以支持异步数据获取。
|
|
5
|
+
"""
|
|
6
|
+
import inspect
|
|
7
|
+
from calendar import timegm
|
|
8
|
+
from decimal import Decimal as MyDecimal, ROUND_HALF_EVEN
|
|
9
|
+
from email.utils import formatdate
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"String",
|
|
13
|
+
"FormattedString",
|
|
14
|
+
"DateTime",
|
|
15
|
+
"Float",
|
|
16
|
+
"Integer",
|
|
17
|
+
"Arbitrary",
|
|
18
|
+
"Nested",
|
|
19
|
+
"List",
|
|
20
|
+
"Raw",
|
|
21
|
+
"Boolean",
|
|
22
|
+
"Fixed",
|
|
23
|
+
"Price",
|
|
24
|
+
"MarshallingException",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MarshallingException(Exception):
|
|
29
|
+
"""
|
|
30
|
+
This is an encapsulating Exception in case of marshalling error.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, underlying_exception):
|
|
34
|
+
# just put the contextual representation of the error to hint on what
|
|
35
|
+
# went wrong without exposing internals
|
|
36
|
+
super().__init__(str(underlying_exception))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_indexable_but_not_string(obj):
|
|
40
|
+
"""检查对象是否可索引但不是字符串"""
|
|
41
|
+
return not isinstance(obj, str) and hasattr(obj, "__iter__")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def get_value(key, obj, default=None):
|
|
45
|
+
"""Helper for pulling a keyed value off various types of objects.
|
|
46
|
+
|
|
47
|
+
支持异步属性获取,如果属性是协程对象,会自动 await。
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(key, int):
|
|
50
|
+
return await _get_value_for_key(key, obj, default)
|
|
51
|
+
elif callable(key):
|
|
52
|
+
value = key(obj)
|
|
53
|
+
if inspect.isawaitable(value):
|
|
54
|
+
value = await value
|
|
55
|
+
# 只检查 None,不检查其他假值(如 0, False, '')
|
|
56
|
+
if value is None:
|
|
57
|
+
return default
|
|
58
|
+
return value
|
|
59
|
+
else:
|
|
60
|
+
return await _get_value_for_keys(key.split('.'), obj, default)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def _get_value_for_keys(keys, obj, default):
|
|
64
|
+
"""递归获取嵌套键的值"""
|
|
65
|
+
if len(keys) == 1:
|
|
66
|
+
return await _get_value_for_key(keys[0], obj, default)
|
|
67
|
+
else:
|
|
68
|
+
intermediate_obj = await _get_value_for_key(keys[0], obj, default)
|
|
69
|
+
if intermediate_obj is None:
|
|
70
|
+
return default
|
|
71
|
+
return await _get_value_for_keys(keys[1:], intermediate_obj, default)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def _get_value_for_key(key, obj, default):
|
|
75
|
+
"""从对象中获取单个键的值,支持异步属性"""
|
|
76
|
+
if is_indexable_but_not_string(obj):
|
|
77
|
+
try:
|
|
78
|
+
value = obj[key]
|
|
79
|
+
if inspect.isawaitable(value):
|
|
80
|
+
value = await value
|
|
81
|
+
# 只检查 None,不检查其他假值(如 0, False, '')
|
|
82
|
+
if value is None:
|
|
83
|
+
return default
|
|
84
|
+
return value
|
|
85
|
+
except (IndexError, TypeError, KeyError):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# 尝试从对象属性获取值
|
|
89
|
+
if hasattr(obj, key):
|
|
90
|
+
value = getattr(obj, key)
|
|
91
|
+
if inspect.isawaitable(value):
|
|
92
|
+
value = await value
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
# 如果属性不存在,返回 default
|
|
96
|
+
# 如果 default 是协程,也需要 await
|
|
97
|
+
if inspect.isawaitable(default):
|
|
98
|
+
return await default
|
|
99
|
+
return default
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def to_marshallable_type(obj):
|
|
103
|
+
"""Helper for converting an object to a dictionary only if it is not
|
|
104
|
+
dictionary already or an indexable object nor a simple type.
|
|
105
|
+
|
|
106
|
+
支持异步的 __marshallable__ 方法。
|
|
107
|
+
"""
|
|
108
|
+
if obj is None:
|
|
109
|
+
return None # make it idempotent for None
|
|
110
|
+
|
|
111
|
+
if hasattr(obj, '__marshallable__'):
|
|
112
|
+
result = obj.__marshallable__()
|
|
113
|
+
# 如果 __marshallable__ 返回的是协程,需要 await
|
|
114
|
+
if inspect.isawaitable(result):
|
|
115
|
+
result = await result
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
if hasattr(obj, '__getitem__'):
|
|
119
|
+
return obj # it is indexable it is ok
|
|
120
|
+
|
|
121
|
+
return dict(obj.__dict__)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Raw:
|
|
125
|
+
"""Raw provides a base field class from which others should extend. It
|
|
126
|
+
applies no formatting by default, and should only be used in cases where
|
|
127
|
+
data does not need to be formatted before being serialized. Fields should
|
|
128
|
+
throw a :class:`MarshallingException` in case of parsing problem.
|
|
129
|
+
|
|
130
|
+
:param default: The default value for the field, if no value is
|
|
131
|
+
specified.
|
|
132
|
+
:param attribute: If the public facing value differs from the internal
|
|
133
|
+
value, use this to retrieve a different attribute from the response
|
|
134
|
+
than the publicly named value.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, default=None, attribute=None):
|
|
138
|
+
self.attribute = attribute
|
|
139
|
+
self.default = default
|
|
140
|
+
|
|
141
|
+
async def format(self, value):
|
|
142
|
+
"""Formats a field's value. No-op by default - field classes that
|
|
143
|
+
modify how the value of existing object keys should be presented should
|
|
144
|
+
override this and apply the appropriate formatting.
|
|
145
|
+
|
|
146
|
+
:param value: The value to format
|
|
147
|
+
:exception MarshallingException: In case of formatting problem
|
|
148
|
+
|
|
149
|
+
Ex::
|
|
150
|
+
|
|
151
|
+
class TitleCase(Raw):
|
|
152
|
+
async def format(self, value):
|
|
153
|
+
return str(value).title()
|
|
154
|
+
"""
|
|
155
|
+
return value
|
|
156
|
+
|
|
157
|
+
async def output(self, key, obj):
|
|
158
|
+
"""Pulls the value for the given key from the object, applies the
|
|
159
|
+
field's formatting and returns the result. If the key is not found
|
|
160
|
+
in the object, returns the default value. Field classes that create
|
|
161
|
+
values which do not require the existence of the key in the object
|
|
162
|
+
should override this and return the desired value.
|
|
163
|
+
|
|
164
|
+
:exception MarshallingException: In case of formatting problem
|
|
165
|
+
"""
|
|
166
|
+
value = await get_value(key if self.attribute is None else self.attribute, obj)
|
|
167
|
+
|
|
168
|
+
if value is None:
|
|
169
|
+
return self.default
|
|
170
|
+
|
|
171
|
+
return await self.format(value)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Nested(Raw):
|
|
175
|
+
"""Allows you to nest one set of fields inside another.
|
|
176
|
+
See :ref:`nested-field` for more information
|
|
177
|
+
|
|
178
|
+
:param dict nested: The dictionary to nest
|
|
179
|
+
:param bool allow_null: Whether to return None instead of a dictionary
|
|
180
|
+
with null keys, if a nested dictionary has all-null keys
|
|
181
|
+
:param kwargs: If ``default`` keyword argument is present, a nested
|
|
182
|
+
dictionary will be marshaled as its value if nested dictionary is
|
|
183
|
+
all-null keys (e.g. lets you return an empty JSON object instead of
|
|
184
|
+
null)
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(self, nested, allow_null=False, **kwargs):
|
|
188
|
+
self.nested = nested
|
|
189
|
+
self.allow_null = allow_null
|
|
190
|
+
super().__init__(**kwargs)
|
|
191
|
+
|
|
192
|
+
async def output(self, key, obj):
|
|
193
|
+
value = await get_value(key if self.attribute is None else self.attribute, obj)
|
|
194
|
+
if value is None:
|
|
195
|
+
if self.allow_null:
|
|
196
|
+
return None
|
|
197
|
+
elif self.default is not None:
|
|
198
|
+
return self.default
|
|
199
|
+
|
|
200
|
+
# 延迟导入以避免循环导入
|
|
201
|
+
from tomskit.utils.marshal_utils import marshal
|
|
202
|
+
return await marshal(value, self.nested)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class List(Raw):
|
|
206
|
+
"""
|
|
207
|
+
Field for marshalling lists of other fields.
|
|
208
|
+
|
|
209
|
+
See :ref:`list-field` for more information.
|
|
210
|
+
|
|
211
|
+
:param cls_or_instance: The field type the list will contain.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def __init__(self, cls_or_instance, **kwargs):
|
|
215
|
+
super().__init__(**kwargs)
|
|
216
|
+
error_msg = ("The type of the list elements must be a subclass of "
|
|
217
|
+
"tomskit.utils.fields.Raw")
|
|
218
|
+
if isinstance(cls_or_instance, type):
|
|
219
|
+
if not issubclass(cls_or_instance, Raw):
|
|
220
|
+
raise MarshallingException(error_msg)
|
|
221
|
+
self.container = cls_or_instance()
|
|
222
|
+
else:
|
|
223
|
+
if not isinstance(cls_or_instance, Raw):
|
|
224
|
+
raise MarshallingException(error_msg)
|
|
225
|
+
self.container = cls_or_instance
|
|
226
|
+
|
|
227
|
+
async def format(self, value):
|
|
228
|
+
# Convert all instances in typed list to container type
|
|
229
|
+
if isinstance(value, set):
|
|
230
|
+
value = list(value)
|
|
231
|
+
|
|
232
|
+
results = []
|
|
233
|
+
for idx, val in enumerate(value):
|
|
234
|
+
item = val if (isinstance(val, dict)
|
|
235
|
+
or (self.container.attribute
|
|
236
|
+
and hasattr(val, self.container.attribute))
|
|
237
|
+
) and not isinstance(self.container, Nested) and type(self.container) is not Raw else value
|
|
238
|
+
output = await self.container.output(idx, item)
|
|
239
|
+
results.append(output)
|
|
240
|
+
return results
|
|
241
|
+
|
|
242
|
+
async def output(self, key, data):
|
|
243
|
+
value = await get_value(key if self.attribute is None else self.attribute, data)
|
|
244
|
+
# we cannot really test for external dict behavior
|
|
245
|
+
if is_indexable_but_not_string(value) and not isinstance(value, dict):
|
|
246
|
+
return await self.format(value)
|
|
247
|
+
|
|
248
|
+
if value is None:
|
|
249
|
+
return self.default
|
|
250
|
+
|
|
251
|
+
# 延迟导入以避免循环导入
|
|
252
|
+
from tomskit.utils.marshal_utils import marshal
|
|
253
|
+
return [await marshal(value, self.container.nested)]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class String(Raw):
|
|
257
|
+
"""
|
|
258
|
+
Marshal a value as a string. Uses ``str`` so values will
|
|
259
|
+
be converted to string.
|
|
260
|
+
"""
|
|
261
|
+
async def format(self, value):
|
|
262
|
+
try:
|
|
263
|
+
return str(value)
|
|
264
|
+
except ValueError as ve:
|
|
265
|
+
raise MarshallingException(ve)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class Integer(Raw):
|
|
269
|
+
""" Field for outputting an integer value.
|
|
270
|
+
|
|
271
|
+
:param int default: The default value for the field, if no value is
|
|
272
|
+
specified.
|
|
273
|
+
"""
|
|
274
|
+
def __init__(self, default=0, **kwargs):
|
|
275
|
+
super().__init__(default=default, **kwargs)
|
|
276
|
+
|
|
277
|
+
async def format(self, value):
|
|
278
|
+
try:
|
|
279
|
+
if value is None:
|
|
280
|
+
return self.default
|
|
281
|
+
return int(value)
|
|
282
|
+
except ValueError as ve:
|
|
283
|
+
raise MarshallingException(ve)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class Boolean(Raw):
|
|
287
|
+
"""
|
|
288
|
+
Field for outputting a boolean value.
|
|
289
|
+
|
|
290
|
+
Empty collections such as ``""``, ``{}``, ``[]``, etc. will be converted to
|
|
291
|
+
``False``.
|
|
292
|
+
"""
|
|
293
|
+
async def format(self, value):
|
|
294
|
+
return bool(value)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class FormattedString(Raw):
|
|
298
|
+
"""
|
|
299
|
+
FormattedString is used to interpolate other values from
|
|
300
|
+
the response into this field. The syntax for the source string is
|
|
301
|
+
the same as the string :meth:`~str.format` method from the python
|
|
302
|
+
stdlib.
|
|
303
|
+
|
|
304
|
+
Ex::
|
|
305
|
+
|
|
306
|
+
fields = {
|
|
307
|
+
'name': fields.String(),
|
|
308
|
+
'greeting': fields.FormattedString("Hello {name}")
|
|
309
|
+
}
|
|
310
|
+
data = {
|
|
311
|
+
'name': 'Doug',
|
|
312
|
+
}
|
|
313
|
+
result = await marshal(data, fields)
|
|
314
|
+
"""
|
|
315
|
+
def __init__(self, src_str):
|
|
316
|
+
"""
|
|
317
|
+
:param string src_str: the string to format with the other
|
|
318
|
+
values from the response.
|
|
319
|
+
"""
|
|
320
|
+
super().__init__()
|
|
321
|
+
self.src_str = str(src_str)
|
|
322
|
+
|
|
323
|
+
async def output(self, key, obj):
|
|
324
|
+
try:
|
|
325
|
+
data = await to_marshallable_type(obj)
|
|
326
|
+
if data is None:
|
|
327
|
+
data = {}
|
|
328
|
+
return self.src_str.format(**data)
|
|
329
|
+
except (TypeError, IndexError) as error:
|
|
330
|
+
raise MarshallingException(error)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class Float(Raw):
|
|
334
|
+
"""
|
|
335
|
+
A double as IEEE-754 double precision.
|
|
336
|
+
ex : 3.141592653589793 3.1415926535897933e-06 3.141592653589793e+24 nan inf
|
|
337
|
+
-inf
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
async def format(self, value):
|
|
341
|
+
try:
|
|
342
|
+
return float(value)
|
|
343
|
+
except ValueError as ve:
|
|
344
|
+
raise MarshallingException(ve)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class Arbitrary(Raw):
|
|
348
|
+
"""
|
|
349
|
+
A floating point number with an arbitrary precision
|
|
350
|
+
ex: 634271127864378216478362784632784678324.23432
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
async def format(self, value):
|
|
354
|
+
return str(MyDecimal(value))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class DateTime(Raw):
|
|
358
|
+
"""
|
|
359
|
+
Return a formatted datetime string in UTC. Supported formats are RFC 822
|
|
360
|
+
and ISO 8601.
|
|
361
|
+
|
|
362
|
+
See :func:`email.utils.formatdate` for more info on the RFC 822 format.
|
|
363
|
+
|
|
364
|
+
See :meth:`datetime.datetime.isoformat` for more info on the ISO 8601
|
|
365
|
+
format.
|
|
366
|
+
|
|
367
|
+
:param dt_format: ``'rfc822'`` or ``'iso8601'``
|
|
368
|
+
:type dt_format: str
|
|
369
|
+
"""
|
|
370
|
+
def __init__(self, dt_format='rfc822', **kwargs):
|
|
371
|
+
super().__init__(**kwargs)
|
|
372
|
+
self.dt_format = dt_format
|
|
373
|
+
|
|
374
|
+
async def format(self, value):
|
|
375
|
+
try:
|
|
376
|
+
if self.dt_format == 'rfc822':
|
|
377
|
+
return _rfc822(value)
|
|
378
|
+
elif self.dt_format == 'iso8601':
|
|
379
|
+
return _iso8601(value)
|
|
380
|
+
else:
|
|
381
|
+
raise MarshallingException(
|
|
382
|
+
f'Unsupported date format {self.dt_format}'
|
|
383
|
+
)
|
|
384
|
+
except AttributeError as ae:
|
|
385
|
+
raise MarshallingException(ae)
|
|
386
|
+
|
|
387
|
+
ZERO = MyDecimal()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class Fixed(Raw):
|
|
391
|
+
"""
|
|
392
|
+
A decimal number with a fixed precision.
|
|
393
|
+
"""
|
|
394
|
+
def __init__(self, decimals=5, **kwargs):
|
|
395
|
+
super().__init__(**kwargs)
|
|
396
|
+
self.precision = MyDecimal('0.' + '0' * (decimals - 1) + '1')
|
|
397
|
+
|
|
398
|
+
async def format(self, value):
|
|
399
|
+
dvalue = MyDecimal(value)
|
|
400
|
+
if not dvalue.is_normal() and dvalue != ZERO:
|
|
401
|
+
raise MarshallingException('Invalid Fixed precision number.')
|
|
402
|
+
return str(dvalue.quantize(self.precision, rounding=ROUND_HALF_EVEN))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
"""Alias for :class:`~fields.Fixed`"""
|
|
406
|
+
Price = Fixed
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _rfc822(dt):
|
|
410
|
+
"""Turn a datetime object into a formatted date.
|
|
411
|
+
|
|
412
|
+
Example::
|
|
413
|
+
|
|
414
|
+
fields._rfc822(datetime(2011, 1, 1)) => "Sat, 01 Jan 2011 00:00:00 -0000"
|
|
415
|
+
|
|
416
|
+
:param dt: The datetime to transform
|
|
417
|
+
:type dt: datetime
|
|
418
|
+
:return: A RFC 822 formatted date string
|
|
419
|
+
"""
|
|
420
|
+
return formatdate(timegm(dt.utctimetuple()))
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _iso8601(dt):
|
|
424
|
+
"""Turn a datetime object into an ISO8601 formatted date.
|
|
425
|
+
|
|
426
|
+
Example::
|
|
427
|
+
|
|
428
|
+
fields._iso8601(datetime(2012, 1, 1, 0, 0)) => "2012-01-01T00:00:00"
|
|
429
|
+
|
|
430
|
+
:param dt: The datetime to transform
|
|
431
|
+
:type dt: datetime
|
|
432
|
+
:return: A ISO 8601 formatted date string
|
|
433
|
+
"""
|
|
434
|
+
return dt.isoformat()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
marshal 函数,支持异步数据序列化。
|
|
3
|
+
|
|
4
|
+
基于 flask-restful 的 marshal 实现,但所有操作都是异步的,以支持从异步数据源(如异步数据库查询)获取数据。
|
|
5
|
+
"""
|
|
6
|
+
import inspect
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from collections.abc import AsyncIterable
|
|
9
|
+
from functools import wraps
|
|
10
|
+
|
|
11
|
+
from fastapi.responses import JSONResponse
|
|
12
|
+
|
|
13
|
+
from tomskit.utils.response_utils import unpack
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def marshal(data, fields, envelope=None):
|
|
17
|
+
"""接受原始数据(以字典、列表、对象的形式)和输出字段的字典,
|
|
18
|
+
并根据这些字段过滤数据。支持异步数据获取。
|
|
19
|
+
|
|
20
|
+
:param data: 实际的数据对象(可以是单个对象或对象列表)
|
|
21
|
+
:param fields: 包含输出字段的字典
|
|
22
|
+
:param envelope: 可选的键,用于包裹序列化的响应
|
|
23
|
+
"""
|
|
24
|
+
def make(cls):
|
|
25
|
+
if isinstance(cls, type):
|
|
26
|
+
return cls()
|
|
27
|
+
return cls
|
|
28
|
+
|
|
29
|
+
if data is None:
|
|
30
|
+
return OrderedDict() if envelope is None else OrderedDict([(envelope, None)])
|
|
31
|
+
|
|
32
|
+
# 处理列表和元组
|
|
33
|
+
if isinstance(data, (list, tuple)):
|
|
34
|
+
results = []
|
|
35
|
+
for item in data:
|
|
36
|
+
result = await marshal(item, fields)
|
|
37
|
+
results.append(result)
|
|
38
|
+
return OrderedDict([(envelope, results)]) if envelope else results
|
|
39
|
+
|
|
40
|
+
# 处理异步可迭代对象
|
|
41
|
+
elif isinstance(data, AsyncIterable):
|
|
42
|
+
results = []
|
|
43
|
+
async for item in data:
|
|
44
|
+
result = await marshal(item, fields)
|
|
45
|
+
results.append(result)
|
|
46
|
+
return OrderedDict([(envelope, results)]) if envelope else results
|
|
47
|
+
|
|
48
|
+
# 处理单个对象
|
|
49
|
+
items = OrderedDict()
|
|
50
|
+
for k, v in fields.items():
|
|
51
|
+
if isinstance(v, dict):
|
|
52
|
+
value = await marshal(data, v)
|
|
53
|
+
else:
|
|
54
|
+
field_instance = make(v)
|
|
55
|
+
if hasattr(field_instance, 'output'):
|
|
56
|
+
output = field_instance.output(k, data)
|
|
57
|
+
if inspect.isawaitable(output):
|
|
58
|
+
value = await output
|
|
59
|
+
else:
|
|
60
|
+
value = output
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"字段 '{k}' 不是有效的 Field 实例。")
|
|
63
|
+
items[k] = value
|
|
64
|
+
|
|
65
|
+
return OrderedDict([(envelope, items)]) if envelope else items
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class marshal_with:
|
|
69
|
+
"""A decorator that apply marshalling to the return values of your methods.
|
|
70
|
+
|
|
71
|
+
see :meth:`marshal_utils.marshal`
|
|
72
|
+
"""
|
|
73
|
+
def __init__(self, fields, envelope=None):
|
|
74
|
+
"""
|
|
75
|
+
:param fields: a dict of whose keys will make up the final
|
|
76
|
+
serialized response output
|
|
77
|
+
:param envelope: optional key that will be used to envelop the serialized
|
|
78
|
+
response
|
|
79
|
+
"""
|
|
80
|
+
self.fields = fields
|
|
81
|
+
self.envelope = envelope
|
|
82
|
+
|
|
83
|
+
def __call__(self, f):
|
|
84
|
+
@wraps(f)
|
|
85
|
+
async def wrapper(*args, **kwargs):
|
|
86
|
+
resp = await f(*args, **kwargs)
|
|
87
|
+
if isinstance(resp, tuple):
|
|
88
|
+
data, code, headers = unpack(resp)
|
|
89
|
+
content = await marshal(data, self.fields, self.envelope)
|
|
90
|
+
return JSONResponse(content=content, status_code=code, headers=headers)
|
|
91
|
+
elif isinstance(resp, JSONResponse):
|
|
92
|
+
return resp
|
|
93
|
+
else:
|
|
94
|
+
content = await marshal(resp, self.fields, self.envelope)
|
|
95
|
+
return JSONResponse(content=content)
|
|
96
|
+
return wrapper
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class marshal_with_field:
|
|
100
|
+
"""
|
|
101
|
+
A decorator that formats the return values of your methods with a single field.
|
|
102
|
+
|
|
103
|
+
>>> from tomskit.utils import marshal_utils, fields
|
|
104
|
+
>>> @marshal_with_field(fields.List(fields.Integer))
|
|
105
|
+
... async def get():
|
|
106
|
+
... return ['1', 2, 3.0]
|
|
107
|
+
...
|
|
108
|
+
>>> await get()
|
|
109
|
+
[1, 2, 3]
|
|
110
|
+
|
|
111
|
+
see :meth:`marshal_utils.marshal_with`
|
|
112
|
+
"""
|
|
113
|
+
def __init__(self, field):
|
|
114
|
+
"""
|
|
115
|
+
:param field: a single field with which to marshal the output.
|
|
116
|
+
"""
|
|
117
|
+
if isinstance(field, type):
|
|
118
|
+
self.field = field()
|
|
119
|
+
else:
|
|
120
|
+
self.field = field
|
|
121
|
+
|
|
122
|
+
def __call__(self, f):
|
|
123
|
+
@wraps(f)
|
|
124
|
+
async def wrapper(*args, **kwargs):
|
|
125
|
+
resp = await f(*args, **kwargs)
|
|
126
|
+
|
|
127
|
+
if isinstance(resp, tuple):
|
|
128
|
+
data, code, headers = unpack(resp)
|
|
129
|
+
content = await self.field.format(data)
|
|
130
|
+
return JSONResponse(content=content, status_code=code, headers=headers)
|
|
131
|
+
elif isinstance(resp, JSONResponse):
|
|
132
|
+
return resp
|
|
133
|
+
else:
|
|
134
|
+
content = await self.field.format(resp)
|
|
135
|
+
return JSONResponse(content=content)
|
|
136
|
+
|
|
137
|
+
return wrapper
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
def unpack(value):
|
|
3
|
+
"""Return a three-tuple of (data, code, headers)"""
|
|
4
|
+
if not isinstance(value, tuple):
|
|
5
|
+
return value, 200, {}
|
|
6
|
+
|
|
7
|
+
if len(value) == 3:
|
|
8
|
+
return value
|
|
9
|
+
elif len(value) == 2:
|
|
10
|
+
data, code = value
|
|
11
|
+
return data, code, {}
|
|
12
|
+
else:
|
|
13
|
+
return value, 200, {}
|