ypres 1.1.2__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.
ypres/__init__.py ADDED
@@ -0,0 +1,43 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from ypres.fields import (
4
+ BoolField,
5
+ DateField,
6
+ DateTimeField,
7
+ Field,
8
+ FloatField,
9
+ IntField,
10
+ MethodField,
11
+ StaticField,
12
+ StrField,
13
+ )
14
+ from ypres.serializer import (
15
+ AsyncDictSerializer,
16
+ AsyncSerializer,
17
+ DictSerializer,
18
+ Serializer,
19
+ )
20
+
21
+ try:
22
+ __version__ = version("ypres")
23
+ except PackageNotFoundError:
24
+ __version__ = "0+unknown"
25
+
26
+ __author__ = "Andrew Hankinson"
27
+ __license__ = "MIT"
28
+
29
+ __all__ = [
30
+ "Serializer",
31
+ "DictSerializer",
32
+ "AsyncSerializer",
33
+ "AsyncDictSerializer",
34
+ "Field",
35
+ "BoolField",
36
+ "IntField",
37
+ "FloatField",
38
+ "MethodField",
39
+ "StrField",
40
+ "StaticField",
41
+ "DateField",
42
+ "DateTimeField",
43
+ ]
ypres/fields.py ADDED
@@ -0,0 +1,210 @@
1
+ import types
2
+ from datetime import date, datetime, time
3
+ from typing import Any
4
+
5
+
6
+ class Field:
7
+ """:class:`Field` is used to define what attributes will be serialized.
8
+
9
+ A :class:`Field` maps a property or function on an object to a value in the
10
+ serialized result. Subclass this to make custom fields. For most simple
11
+ cases, overriding :meth:`Field.to_value` should give enough flexibility. If
12
+ more control is needed, override :meth:`Field.as_getter`.
13
+
14
+ :param str attr: The attribute to get on the object, using the same format
15
+ as ``operator.attrgetter``. If this is not supplied, the name this
16
+ field was assigned to on the serializer will be used.
17
+ :param bool call: Whether the value should be called after it is retrieved
18
+ from the object. Useful if an object has a method to be serialized.
19
+ :param str label: A label to use as the name of the serialized field
20
+ instead of using the attribute name of the field.
21
+ :param bool required: Whether the field is required. If set to ``False``,
22
+ :meth:`Field.to_value` will not be called if the value is ``None``.
23
+ :param: bool emit_none: Whether the field will emit an explicit ``None``
24
+ value if the value being serialized is ``None``. By default, fields
25
+ that evaluate to ``None`` will be removed from the output. Set this
26
+ to ``True`` to keep the value in the output.
27
+ """
28
+
29
+ #: Set to ``True`` if the value function returned from
30
+ #: :meth:`Field.as_getter` requires the serializer to be passed in as the
31
+ #: first argument. Otherwise, the object will be the only parameter.
32
+ getter_takes_serializer: bool = False
33
+ __slots__ = ["attr", "call", "label", "required", "emit_none"]
34
+
35
+ def __init__(
36
+ self,
37
+ attr: str | None = None,
38
+ call: bool = False,
39
+ label: str | None = None,
40
+ required: bool = True,
41
+ emit_none: bool = False,
42
+ ):
43
+ self.attr: str | None = attr
44
+ self.call: bool = call
45
+ self.label: str | None = label
46
+ self.required: bool = required
47
+ self.emit_none = emit_none
48
+
49
+ def to_value(self, value: Any):
50
+ """Transform the serialized value.
51
+
52
+ Override this method to clean and validate values serialized by this
53
+ field. For example to implement an ``int`` field: ::
54
+
55
+ def to_value(self, value):
56
+ return int(value)
57
+
58
+ :param value: The value fetched from the object being serialized.
59
+ """
60
+ return value
61
+
62
+ to_value._ypres_base_implementation = True # type: ignore
63
+
64
+ def is_to_value_overridden(self) -> bool:
65
+ to_value = self.to_value
66
+ # If to_value isn't a method, it must have been overridden.
67
+ if not isinstance(to_value, types.MethodType):
68
+ return True
69
+ return not getattr(to_value, "_ypres_base_implementation", False)
70
+
71
+ def as_getter(self, serializer_field_name: str, serializer_cls: Any):
72
+ """Returns a function that fetches an attribute from an object.
73
+
74
+ Return ``None`` to use the default getter for the serializer defined in
75
+ :attr:`Serializer.default_getter`.
76
+
77
+ When a :class:`Serializer` is defined, each :class:`Field` will be
78
+ converted into a getter function using this method. During
79
+ serialization, each getter will be called with the object being
80
+ serialized, and the return value will be passed through
81
+ :meth:`Field.to_value`.
82
+
83
+ If a :class:`Field` has ``getter_takes_serializer = True``, then the
84
+ getter returned from this method will be called with the
85
+ :class:`Serializer` instance as the first argument, and the object
86
+ being serialized as the second.
87
+
88
+ :param str serializer_field_name: The name this field was assigned to
89
+ on the serializer.
90
+ :param serializer_cls: The :class:`Serializer` this field is a part of.
91
+ """
92
+ return None
93
+
94
+
95
+ class StaticField(Field):
96
+ """A :class:`Field` that simply repeats a static value."""
97
+
98
+ __slots__ = ["value"]
99
+
100
+ def __init__(
101
+ self,
102
+ value: Any,
103
+ attr: str | None = None,
104
+ call: bool = False,
105
+ label: str | None = None,
106
+ required: bool = True,
107
+ ) -> None:
108
+ super().__init__(attr, call, label, required)
109
+ self.value: Any = value
110
+
111
+ def to_value(self, value: Any) -> Any:
112
+ return self.value
113
+
114
+ def as_getter(self, serializer_field_name: str, serializer_cls: Any) -> Any:
115
+ return self.to_value
116
+
117
+
118
+ class StrField(Field):
119
+ """A :class:`Field` that converts the value to a string.
120
+
121
+ Since Python will happily cast `None` to the string `"None"`,
122
+ this method ensures that values of `None` are handled and
123
+ passed through, instead of cast.
124
+ """
125
+
126
+ to_value: Any = staticmethod(lambda s: str(s) if s else None)
127
+
128
+
129
+ class IntField(Field):
130
+ """A :class:`Field` that converts the value to an integer."""
131
+
132
+ to_value: Any = staticmethod(int)
133
+
134
+
135
+ class FloatField(Field):
136
+ """A :class:`Field` that converts the value to a float."""
137
+
138
+ to_value: Any = staticmethod(float)
139
+
140
+
141
+ class BoolField(Field):
142
+ """A :class:`Field` that converts the value to a boolean.
143
+
144
+ Python will cast a value of `None` to the boolean value of False,
145
+ so this method ensures values of `None` are passed through instead
146
+ of cast to a boolean value.
147
+ """
148
+
149
+ to_value: Any = staticmethod(lambda b: bool(b) if b is not None else None)
150
+
151
+
152
+ class MethodField(Field):
153
+ """A :class:`Field` that calls a method on the :class:`Serializer`.
154
+
155
+ This is useful if a :class:`Field` needs to serialize a value that may come
156
+ from multiple attributes on an object. For example: ::
157
+
158
+ class FooSerializer(Serializer):
159
+ plus = MethodField()
160
+ minus = MethodField('do_minus')
161
+
162
+ def get_plus(self, foo_obj):
163
+ return foo_obj.bar + foo_obj.baz
164
+
165
+ def do_minus(self, foo_obj):
166
+ return foo_obj.bar - foo_obj.baz
167
+
168
+ foo = Foo(bar=5, baz=10)
169
+ FooSerializer(foo).data
170
+ # {'plus': 15, 'minus': -5}
171
+
172
+ :param str method: The method on the serializer to call. Defaults to
173
+ ``'get_<field name>'``.
174
+ """
175
+
176
+ getter_takes_serializer = True
177
+ __slots__ = ["method"]
178
+
179
+ def __init__(self, method: str | None = None, **kwargs): # type: ignore
180
+ super().__init__(**kwargs)
181
+ self.method: str | None = method
182
+
183
+ def as_getter(self, serializer_field_name: str, serializer_cls: Any):
184
+ method_name: str | None = self.method
185
+ if method_name is None:
186
+ method_name = f"get_{serializer_field_name}"
187
+ return getattr(serializer_cls, method_name)
188
+
189
+
190
+ # From https://github.com/PKharlamov/drf-serpy/blob/master/drf_serpy/fields.py
191
+ class DateField(Field):
192
+ """A `Field` that converts the value to a date format."""
193
+
194
+ __slots__ = ["_date_format"]
195
+ date_format = "%Y-%m-%d"
196
+
197
+ def __init__(self, date_format: str | None = None, **kwargs):
198
+ super().__init__(**kwargs)
199
+ self._date_format = date_format or self.date_format
200
+
201
+ def to_value(self, value: datetime | time | date) -> str | None:
202
+ if value:
203
+ return value.strftime(self._date_format)
204
+ return None
205
+
206
+
207
+ class DateTimeField(DateField):
208
+ """A `Field` that converts the value to a date time format."""
209
+
210
+ date_format = "%Y-%m-%dT%H:%M:%S.%fZ"
ypres/py.typed ADDED
File without changes
ypres/serializer.py ADDED
@@ -0,0 +1,569 @@
1
+ import inspect
2
+ import operator
3
+ from abc import abstractmethod
4
+ from collections.abc import AsyncIterable, Callable, Iterable, Mapping
5
+ from typing import Any, NamedTuple
6
+ from warnings import deprecated
7
+
8
+ from ypres import Field
9
+
10
+
11
+ class FieldDefinitions(NamedTuple):
12
+ name: str
13
+ getter: Callable
14
+ to_value: Any
15
+ call: bool
16
+ required: bool
17
+ pass_self: bool
18
+ emit_none: bool
19
+ getter_is_coro: bool
20
+ toval_is_coro: bool
21
+
22
+
23
+ class SerializerBase(Field):
24
+ __slots__: list = [
25
+ "instance",
26
+ "many",
27
+ "context",
28
+ "_emit_none",
29
+ "_data",
30
+ "_serialized",
31
+ "_serialized_many",
32
+ ]
33
+
34
+ def __init__(
35
+ self, # type: ignore
36
+ instance: Any | None = None,
37
+ many: bool = False,
38
+ context: dict | None = None,
39
+ emit_none: bool = False,
40
+ **kwargs,
41
+ ):
42
+ super().__init__(**kwargs)
43
+ if instance and isinstance(instance, list) and not many:
44
+ # if we're serializing a list but have not set many=True then raise a value error.
45
+ raise ValueError("Cannot serialize an object from a list.")
46
+ elif (
47
+ instance
48
+ and many
49
+ and (
50
+ not isinstance(instance, Iterable | AsyncIterable)
51
+ or isinstance(instance, dict)
52
+ )
53
+ ):
54
+ # if we're not serializing a list (or some iterable object EXCEPT dicts) and many=True,
55
+ # then raise a value error.
56
+ raise ValueError("Cannot serialize a list from an object.")
57
+
58
+ self.instance: Any = instance
59
+ self.many: bool = many
60
+ self.context: dict = context or {}
61
+ self._emit_none = emit_none
62
+ self._serialized: dict | None = None
63
+ self._serialized_many: list | None = None
64
+
65
+ @staticmethod
66
+ @abstractmethod
67
+ def default_getter(k: str) -> Any: ...
68
+
69
+ _field_map: dict = {}
70
+ _compiled_fields: list[FieldDefinitions] = []
71
+ _compiled_sync_fields: list[Callable] = []
72
+
73
+
74
+ class SerializerMeta(type):
75
+ @staticmethod
76
+ def _get_fields(direct_fields: Mapping, serializer_cls) -> dict:
77
+ field_map: dict = {}
78
+ # Get all the fields from base classes.
79
+ for cls in serializer_cls.__mro__[::-1]:
80
+ if issubclass(cls, SerializerBase):
81
+ field_map.update(cls._field_map)
82
+ field_map.update(direct_fields)
83
+ return field_map
84
+
85
+ @staticmethod
86
+ def _compile_fields(field_map: dict, serializer_cls) -> list[FieldDefinitions]:
87
+ return [
88
+ _compile_field_to_tuple(field, name, serializer_cls)
89
+ for name, field in field_map.items()
90
+ ]
91
+
92
+ def __new__(mcs, name, bases, attrs: dict):
93
+ # Fields declared directly on the class.
94
+ direct_fields: dict = {}
95
+
96
+ # Take all the Fields from the attributes.
97
+ for attr_name, field in attrs.items():
98
+ if isinstance(field, Field):
99
+ direct_fields[attr_name] = field
100
+ for k in direct_fields:
101
+ del attrs[k]
102
+
103
+ real_cls = super().__new__(mcs, name, bases, attrs)
104
+
105
+ field_map = mcs._get_fields(direct_fields, real_cls)
106
+ compiled_fields = mcs._compile_fields(field_map, real_cls)
107
+ compiled_sync_fields = _compile_sync_fields(compiled_fields)
108
+
109
+ real_cls._field_map = field_map # type: ignore
110
+ real_cls._compiled_fields = compiled_fields # type: ignore
111
+ real_cls._compiled_sync_fields = compiled_sync_fields # type: ignore
112
+
113
+ return real_cls
114
+
115
+
116
+ def _compile_field_to_tuple(
117
+ field: Field, name: str, serializer_cls: type[SerializerBase]
118
+ ) -> FieldDefinitions:
119
+ getter = field.as_getter(name, serializer_cls)
120
+ if getter is None:
121
+ getter = serializer_cls.default_getter(field.attr or name)
122
+
123
+ getter_is_coro: bool = inspect.iscoroutinefunction(getter)
124
+
125
+ # Only set a to_value function if it has been overridden for performance.
126
+ to_value: Callable | None = None
127
+ if field.is_to_value_overridden():
128
+ to_value = field.to_value
129
+
130
+ # we only need to check if to_value is a coroutine if it is not None.
131
+ toval_is_coro: bool = inspect.iscoroutinefunction(to_value) if to_value else False
132
+ # Set the field name to a supplied label; defaults to the attribute name.
133
+ name = field.label or name
134
+
135
+ return FieldDefinitions(
136
+ name=name,
137
+ getter=getter,
138
+ to_value=to_value,
139
+ call=field.call,
140
+ required=field.required,
141
+ pass_self=field.getter_takes_serializer,
142
+ emit_none=field.emit_none,
143
+ getter_is_coro=getter_is_coro,
144
+ toval_is_coro=toval_is_coro,
145
+ )
146
+
147
+
148
+ def _compile_sync_fields(
149
+ fields: list[FieldDefinitions],
150
+ ) -> list[Callable]:
151
+ return [_make_sync_field_writer(field) for field in fields]
152
+
153
+
154
+ def _required_self_call_tval(name: str, getter: Callable, tval: Callable, emit_none: bool) -> Callable:
155
+ if emit_none:
156
+ def emit(serializer, instance, out):
157
+ out[name] = tval(getter(serializer, instance)())
158
+ else:
159
+ def emit(serializer, instance, out):
160
+ result = tval(getter(serializer, instance)())
161
+ if result is None:
162
+ return
163
+ out[name] = result
164
+ return emit
165
+
166
+
167
+ def _required_self_call(name: str, getter: Callable, _tval: Callable | None, emit_none: bool) -> Callable:
168
+ if emit_none:
169
+ def emit(serializer, instance, out):
170
+ out[name] = getter(serializer, instance)()
171
+ else:
172
+ def emit(serializer, instance, out):
173
+ result = getter(serializer, instance)()
174
+ if result is None:
175
+ return
176
+ out[name] = result
177
+ return emit
178
+
179
+
180
+ def _required_self_tval(name: str, getter: Callable, tval: Callable, emit_none: bool) -> Callable:
181
+ if emit_none:
182
+ def emit(serializer, instance, out):
183
+ out[name] = tval(getter(serializer, instance))
184
+ else:
185
+ def emit(serializer, instance, out):
186
+ result = tval(getter(serializer, instance))
187
+ if result is None:
188
+ return
189
+ out[name] = result
190
+ return emit
191
+
192
+
193
+ def _required_self(name: str, getter: Callable, _tval: Callable | None, emit_none: bool) -> Callable:
194
+ if emit_none:
195
+ def emit(serializer, instance, out):
196
+ out[name] = getter(serializer, instance)
197
+ else:
198
+ def emit(serializer, instance, out):
199
+ result = getter(serializer, instance)
200
+ if result is None:
201
+ return
202
+ out[name] = result
203
+ return emit
204
+
205
+
206
+ def _optional_self_call_tval(name: str, getter: Callable, tval: Callable, emit_none: bool) -> Callable:
207
+ def emit(serializer, instance, out):
208
+ try:
209
+ result = getter(serializer, instance)
210
+ except (KeyError, AttributeError):
211
+ return
212
+ if result is None:
213
+ if emit_none:
214
+ out[name] = None
215
+ return
216
+ result = tval(result())
217
+ if result is None and not emit_none:
218
+ return
219
+ out[name] = result
220
+ return emit
221
+
222
+
223
+ def _optional_self_call(name: str, getter: Callable, _tval: Callable | None, emit_none: bool) -> Callable:
224
+ def emit(serializer, instance, out):
225
+ try:
226
+ result = getter(serializer, instance)
227
+ except (KeyError, AttributeError):
228
+ return
229
+ if result is None:
230
+ if emit_none:
231
+ out[name] = None
232
+ return
233
+ result = result()
234
+ if result is None and not emit_none:
235
+ return
236
+ out[name] = result
237
+ return emit
238
+
239
+
240
+ def _optional_self_tval(name: str, getter: Callable, tval: Callable, emit_none: bool) -> Callable:
241
+ def emit(serializer, instance, out):
242
+ try:
243
+ result = getter(serializer, instance)
244
+ except (KeyError, AttributeError):
245
+ return
246
+ if result is None:
247
+ if emit_none:
248
+ out[name] = None
249
+ return
250
+ result = tval(result)
251
+ if result is None and not emit_none:
252
+ return
253
+ out[name] = result
254
+ return emit
255
+
256
+
257
+ def _optional_self(name: str, getter: Callable, _tval: Callable | None, emit_none: bool) -> Callable:
258
+ def emit(serializer, instance, out):
259
+ try:
260
+ result = getter(serializer, instance)
261
+ except (KeyError, AttributeError):
262
+ return
263
+ if result is None:
264
+ if emit_none:
265
+ out[name] = None
266
+ return
267
+ out[name] = result
268
+ return emit
269
+
270
+
271
+ def _required_call_tval(name: str, getter: Callable, tval: Callable, emit_none: bool) -> Callable:
272
+ if emit_none:
273
+ def emit(serializer, instance, out):
274
+ out[name] = tval(getter(instance)())
275
+ else:
276
+ def emit(serializer, instance, out):
277
+ result = tval(getter(instance)())
278
+ if result is None:
279
+ return
280
+ out[name] = result
281
+ return emit
282
+
283
+
284
+ def _required_call(name: str, getter: Callable, _tval: Callable | None, emit_none: bool) -> Callable:
285
+ if emit_none:
286
+ def emit(serializer, instance, out):
287
+ out[name] = getter(instance)()
288
+ else:
289
+ def emit(serializer, instance, out):
290
+ result = getter(instance)()
291
+ if result is None:
292
+ return
293
+ out[name] = result
294
+ return emit
295
+
296
+
297
+ def _required_tval(name: str, getter: Callable, tval: Callable, emit_none: bool) -> Callable:
298
+ if emit_none:
299
+ def emit(serializer, instance, out):
300
+ out[name] = tval(getter(instance))
301
+ else:
302
+ def emit(serializer, instance, out):
303
+ result = tval(getter(instance))
304
+ if result is None:
305
+ return
306
+ out[name] = result
307
+ return emit
308
+
309
+
310
+ def _required_plain(name: str, getter: Callable, _tval: Callable | None, emit_none: bool) -> Callable:
311
+ if emit_none:
312
+ def emit(serializer, instance, out):
313
+ out[name] = getter(instance)
314
+ else:
315
+ def emit(serializer, instance, out):
316
+ result = getter(instance)
317
+ if result is None:
318
+ return
319
+ out[name] = result
320
+ return emit
321
+
322
+
323
+ def _optional_call_tval(name: str, getter: Callable, tval: Callable, emit_none: bool) -> Callable:
324
+ def emit(serializer, instance, out):
325
+ try:
326
+ result = getter(instance)
327
+ except (KeyError, AttributeError):
328
+ return
329
+ if result is None:
330
+ if emit_none:
331
+ out[name] = None
332
+ return
333
+ result = tval(result())
334
+ if result is None and not emit_none:
335
+ return
336
+ out[name] = result
337
+ return emit
338
+
339
+
340
+ def _optional_call(name: str, getter: Callable, _tval: Callable | None, emit_none: bool) -> Callable:
341
+ def emit(serializer, instance, out):
342
+ try:
343
+ result = getter(instance)
344
+ except (KeyError, AttributeError):
345
+ return
346
+ if result is None:
347
+ if emit_none:
348
+ out[name] = None
349
+ return
350
+ result = result()
351
+ if result is None and not emit_none:
352
+ return
353
+ out[name] = result
354
+ return emit
355
+
356
+
357
+ def _optional_tval(name: str, getter: Callable, tval: Callable, emit_none: bool) -> Callable:
358
+ def emit(serializer, instance, out):
359
+ try:
360
+ result = getter(instance)
361
+ except (KeyError, AttributeError):
362
+ return
363
+ if result is None:
364
+ if emit_none:
365
+ out[name] = None
366
+ return
367
+ result = tval(result)
368
+ if result is None and not emit_none:
369
+ return
370
+ out[name] = result
371
+ return emit
372
+
373
+
374
+ def _optional_plain(name: str, getter: Callable, _tval: Callable | None, emit_none: bool) -> Callable:
375
+ def emit(serializer, instance, out):
376
+ try:
377
+ result = getter(instance)
378
+ except (KeyError, AttributeError):
379
+ return
380
+ if result is None:
381
+ if emit_none:
382
+ out[name] = None
383
+ return
384
+ out[name] = result
385
+ return emit
386
+
387
+
388
+ # Each key is `(pass_self, required, call, has_to_value)`.
389
+ # The selected factory returns a specialized writer for that exact field shape,
390
+ # so the runtime serializer loop does not need to branch on these flags.
391
+ _SYNC_FIELD_WRITER_FACTORIES: dict[tuple[bool, bool, bool, bool], Callable] = {
392
+ (True, True, True, True): _required_self_call_tval,
393
+ (True, True, True, False): _required_self_call,
394
+ (True, True, False, True): _required_self_tval,
395
+ (True, True, False, False): _required_self,
396
+ (True, False, True, True): _optional_self_call_tval,
397
+ (True, False, True, False): _optional_self_call,
398
+ (True, False, False, True): _optional_self_tval,
399
+ (True, False, False, False): _optional_self,
400
+ (False, True, True, True): _required_call_tval,
401
+ (False, True, True, False): _required_call,
402
+ (False, True, False, True): _required_tval,
403
+ (False, True, False, False): _required_plain,
404
+ (False, False, True, True): _optional_call_tval,
405
+ (False, False, True, False): _optional_call,
406
+ (False, False, False, True): _optional_tval,
407
+ (False, False, False, False): _optional_plain,
408
+ }
409
+
410
+
411
+ def _make_sync_field_writer(field: FieldDefinitions) -> Callable:
412
+ # `bool(field.to_value)` is the compile-time "has transform" flag used by
413
+ # the dispatch table above. The actual callable is still passed through to
414
+ # the selected factory for execution.
415
+ key = (field.pass_self, field.required, field.call, bool(field.to_value))
416
+ factory = _SYNC_FIELD_WRITER_FACTORIES[key]
417
+ return factory(field.name, field.getter, field.to_value, field.emit_none)
418
+
419
+
420
+ class Serializer(SerializerBase, metaclass=SerializerMeta):
421
+ default_getter: Any = operator.attrgetter
422
+
423
+ def _serialize(self, instance: Any, fields: list[FieldDefinitions]) -> dict:
424
+ v: dict = {}
425
+
426
+ for emit in self._compiled_sync_fields:
427
+ emit(self, instance, v)
428
+
429
+ return v
430
+
431
+ def to_value(self, value: Any) -> list | dict:
432
+ if self.many:
433
+ return self._serialize_list(value)
434
+ return self._serialize_dict(value)
435
+
436
+ def _serialize_dict(self, instance: Any) -> dict:
437
+ self._serialized = self._serialize(instance, self._compiled_fields)
438
+ return self._serialized or {}
439
+
440
+ def _serialize_list(self, instance: Any) -> list:
441
+ self._serialized_many = [
442
+ self._serialize(o, self._compiled_fields) for o in instance
443
+ ]
444
+ return self._serialized_many or []
445
+
446
+ @property
447
+ @deprecated("Use the .serialized and .serialized_many properties.")
448
+ def data(self) -> list | dict:
449
+ """Get the serialized data from the :class:`Serializer`.
450
+
451
+ The data will be cached for future accesses.
452
+ """
453
+ # Cache the data for next time .data is called.
454
+ return self.to_value(self.instance)
455
+
456
+ @property
457
+ def serialized(self) -> dict:
458
+ if self._serialized is not None:
459
+ return self._serialized
460
+ return self._serialize_dict(self.instance)
461
+
462
+ @property
463
+ def serialized_many(self) -> list:
464
+ if self._serialized_many is not None:
465
+ return self._serialized_many
466
+ return self._serialize_list(self.instance)
467
+
468
+
469
+ class DictSerializer(Serializer):
470
+ default_getter: Any = operator.itemgetter
471
+
472
+
473
+ class AsyncSerializer(SerializerBase, metaclass=SerializerMeta):
474
+ default_getter: Any = operator.attrgetter
475
+
476
+ async def _serialize(self, instance: Any, fields: list[FieldDefinitions]) -> dict:
477
+ v: dict = {}
478
+ for (
479
+ name,
480
+ getter,
481
+ tval,
482
+ call,
483
+ required,
484
+ pass_self,
485
+ emit_none,
486
+ getter_coro,
487
+ toval_coro,
488
+ ) in fields:
489
+ try:
490
+ if getter_coro:
491
+ result = (
492
+ await getter(self, instance)
493
+ if pass_self
494
+ else await getter(instance)
495
+ )
496
+ else:
497
+ result = getter(self, instance) if pass_self else getter(instance)
498
+ except (KeyError, AttributeError):
499
+ if required:
500
+ raise
501
+ continue
502
+
503
+ if result is None and not required:
504
+ if emit_none:
505
+ v[name] = result
506
+ continue
507
+ continue
508
+
509
+ if call:
510
+ result = result()
511
+
512
+ if tval:
513
+ if toval_coro:
514
+ result = await tval(result)
515
+ else:
516
+ result = tval(result)
517
+
518
+ if result is None and not emit_none:
519
+ continue
520
+
521
+ v[name] = result
522
+
523
+ return v
524
+
525
+ async def to_value(self, value: Any) -> list | dict:
526
+ if self.many:
527
+ return await self._serialize_list(value)
528
+ return await self._serialize_dict(value)
529
+
530
+ async def _serialize_dict(self, instance: Any) -> dict:
531
+ self._serialized = await self._serialize(instance, self._compiled_fields)
532
+ return self._serialized or {}
533
+
534
+ async def _serialize_list(self, instance: Any) -> list:
535
+ if isinstance(instance, AsyncIterable):
536
+ self._serialized_many = [
537
+ await self._serialize(o, self._compiled_fields) async for o in instance
538
+ ]
539
+ else:
540
+ self._serialized_many = [
541
+ await self._serialize(o, self._compiled_fields) for o in instance
542
+ ]
543
+ return self._serialized_many or []
544
+
545
+ @property
546
+ @deprecated("Use the .serialized and .serialized_many properties.")
547
+ async def data(self) -> list | dict:
548
+ """Get the serialized data from the :class:`Serializer`.
549
+
550
+ The data will be cached for future accesses.
551
+ """
552
+ # Cache the data for next time .data is called.
553
+ return await self.to_value(self.instance)
554
+
555
+ @property
556
+ async def serialized(self) -> dict:
557
+ if self._serialized is not None:
558
+ return self._serialized
559
+ return await self._serialize_dict(self.instance)
560
+
561
+ @property
562
+ async def serialized_many(self) -> list:
563
+ if self._serialized_many is not None:
564
+ return self._serialized_many
565
+ return await self._serialize_list(self.instance)
566
+
567
+
568
+ class AsyncDictSerializer(AsyncSerializer):
569
+ default_getter: Any = operator.itemgetter
@@ -0,0 +1,307 @@
1
+ Metadata-Version: 2.4
2
+ Name: ypres
3
+ Version: 1.1.2
4
+ Summary: ypres is a simple object serialization framework built for speed.
5
+ Project-URL: Homepage, https://github.com/rism-digital/ypres
6
+ Project-URL: Source, https://github.com/rism-digital/ypres
7
+ Project-URL: Issues, https://github.com/rism-digital/ypres/issues
8
+ Author-email: Andrew Hankinson <andrew.hankinson@gmail.com>
9
+ License: The MIT License (MIT)
10
+
11
+ Copyright (c) 2015 Clark DuVall
12
+ Copyright (c) 2023 Andrew Hankinson, RISM Digital Center
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: asyncio,json,serialization,serializer,typing
33
+ Classifier: Development Status :: 5 - Production/Stable
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Programming Language :: Python :: 3.14
42
+ Classifier: Programming Language :: Python :: Implementation :: CPython
43
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
44
+ Classifier: Typing :: Typed
45
+ Requires-Python: <4.0,>=3.11
46
+ Description-Content-Type: text/markdown
47
+
48
+ # ypres: ridiculously fast object serialization
49
+
50
+ This project started as a fork of the amazing [Serpy serializer](https://github.com/clarkduvall/serpy), which has been
51
+ [marked as feature-complete](https://github.com/clarkduvall/serpy/issues/69) by the original author. This fork
52
+ adds some newer features, such as `asyncio` support so that asynchronous
53
+ methods may be called from within a serializer.
54
+
55
+ It was renamed to "ypres" ("serpy" backwards, pronounced like the [Belgian town
56
+ name](https://en.wikipedia.org/wiki/Ypres)) to avoid confusion with the original.
57
+
58
+ Since forking it has undergone numerous changes and rewrites. The core of it
59
+ is still somewhat recognizable, but there have also been many changes.
60
+
61
+ **ypres** is a simple object serialization framework built for
62
+ speed. **ypres** serializes complex datatypes (Django Models, custom
63
+ classes, ...) to simple native types (dicts, lists, strings, ...). The
64
+ native types can easily be converted to JSON or any other format needed.
65
+
66
+ The goal of **ypres** is to be able to do this *simply*, *reliably*, and
67
+ *quickly*. Since serializers are class based, they can be combined,
68
+ extended and customized with very little code duplication.
69
+
70
+ ## Changes from Serpy
71
+
72
+ There are some notable changes from the original Serpy serializer in this fork.
73
+
74
+ ### New Serializer classes: AsyncSerializer and AsyncDictSerializer
75
+
76
+ Serpy did not allow for `MethodField` implementations to use async / await methods.
77
+ For those instances where you wish to embed an async / await coroutine in your serializer,
78
+ two new serializer classes, `AsyncSerializer` and `AsyncDictSerializer`, will automatically
79
+ detect whether the method being called is a coroutine and handle it appropriately.
80
+
81
+ ### New StaticField class
82
+
83
+ When combining many fields and manipulating output, it is sometimes desirable to have
84
+ a fixed value for certain fields in the output. The new `StaticField` class allows
85
+ you to specify a fixed value for the field, and this will always appear in the output.
86
+
87
+ ### Serializers allow a context object
88
+
89
+ Additional context can be passed in to a serializer. This is helpful if you have some context
90
+ that you wish to use when serializing the object. For example, you might pass in a user object
91
+ that could customize the responses in the serializer with their name, or only perform certain
92
+ serialization tasks if they are of a specific class (e.g., admin).
93
+
94
+ ### Date and DateTime Serializer Fields
95
+
96
+ Date and DateTime fields can be serialized, based on the implementation from another fork,
97
+ https://github.com/PKharlamov/drf-serpy/blob/master/drf_serpy/fields.py.
98
+
99
+ ### Deprecated the `.data` property.
100
+
101
+ Since `.data` can return either a `list` (with `many=True`) or a `dict`, type checkers
102
+ complained when you serialized a single object because the calling code does not handle
103
+ the case of `data` being a list.
104
+
105
+ Instead, two new properties, `serialized` and `serialized_many` are introduced that
106
+ return a dict and a list directly. In the course of doing this work the class structure
107
+ for the serializers was reworked to better implement common checks and data in the superclass.
108
+
109
+ ```python
110
+ import ypres
111
+
112
+
113
+ class MySerializer(ypres.Serializer):
114
+ foo = ypres.MethodField()
115
+ blah = ypres.MethodField()
116
+
117
+ def get_foo(self, obj):
118
+ foo_data = obj.foo
119
+ ctx_data = self.context.get("additional", "")
120
+ return f"{foo_data}_{ctx_data}"
121
+
122
+ def get_blah(self, obj):
123
+ blah_data = obj.blah
124
+ ctx_data = self.context.get("additional", "")
125
+ return f"{blah_data}_{ctx_data}"
126
+
127
+
128
+ class Foo:
129
+ foo = "foo"
130
+ blah = "blah"
131
+
132
+ my_data = MySerializer(Foo(), context={"additional": "bar"}).serialized
133
+
134
+ # {"foo": "foo_bar", "blah": "blah_bar"}
135
+ ```
136
+
137
+ ### Changed behaviour of None
138
+
139
+ By default, data that evaluates to a value of `None` will **not** be included
140
+ in the output. To explicitly mark that a field should emit a `None` value,
141
+ it should be instantiated with an `emit_none=True` argument.
142
+
143
+ Note that the combination of `emit_none` and `required` deserve special attention.
144
+
145
+ - If `emit_none` is `False` and `required` is `True` (default), then the object
146
+ being serialized must have the matching attribute available, otherwise it will
147
+ raise an error. The only exception is if the field is a `MethodField`, in which
148
+ case the attribute does not need to be present on the object.
149
+ This behaviour is not changed.
150
+ - If `emit_none` is `False` and `required` is `False` then the object being
151
+ serialized will not appear in the output if its value is `None`
152
+ - If `emit_none` is `True` and `required` is `True`, then the object being
153
+ serialized will attempt to return the value. However, it may fail if the `to_value`
154
+ method being used does not accept `None`. An example of this is the `IntField`
155
+ serializer, where the `to_value` method would effectively be calling `int(None)`.
156
+ In this case, a `TypeError` will be raised. (This is the same as trying to serialize
157
+ a string with an `IntField`, for example)
158
+ - If `emit_none` is `True` and `required` is `False`, then the object being
159
+ serialized will actually skip the `to_value` step and simply return `None`.
160
+
161
+ Further to this, the behaviour of the `StrField` and `BoolField` were changed,
162
+ where calling `StrField` on a value of `None` would actually return the string
163
+ `"None"`. Similarly, calling `bool(None)` evaluates to `False`. In both of these
164
+ cases the `to_value` handler has been modified to return `None` if the incoming
165
+ value is `None`.
166
+
167
+ This prevents unexpected type values from appearing in the
168
+ output. For values that cannot be cast to `None` for `IntField` and `FloatField`,
169
+ a `None` input will raise an exception.
170
+
171
+ ### Modern Python standards
172
+
173
+ The project uses update Python packaging setups with `pyproject.toml`. It also
174
+ adds configurations for `ruff`, works with `uv`, and fully supports type annotations.
175
+
176
+ ## Source
177
+
178
+ Source at: <https://github.com/rism-digital/ypres>
179
+
180
+ If you want a feature, send a pull request!
181
+
182
+ ## Installation
183
+
184
+ ``` bash
185
+ $ pip install git+https://github.com/rism-digital/ypres
186
+ ```
187
+
188
+ ## Examples
189
+
190
+ ### Simple Example
191
+
192
+ ```python
193
+ import ypres
194
+
195
+ class Foo(object):
196
+ """The object to be serialized."""
197
+ y = 'hello'
198
+ z = 9.5
199
+
200
+ def __init__(self, x):
201
+ self.x = x
202
+
203
+
204
+ class FooSerializer(ypres.Serializer):
205
+ """The serializer schema definition."""
206
+ # Use a Field subclass like IntField if you need more validation.
207
+ x = ypres.IntField()
208
+ y = ypres.Field()
209
+ z = ypres.Field()
210
+
211
+ f = Foo(1)
212
+ FooSerializer(f).serialized
213
+ # {'x': 1, 'y': 'hello', 'z': 9.5}
214
+
215
+ fs = [Foo(i) for i in range(100)]
216
+ FooSerializer(fs, many=True).serialized_many
217
+ # [{'x': 0, 'y': 'hello', 'z': 9.5}, {'x': 1, 'y': 'hello', 'z': 9.5}, ...]
218
+ ```
219
+
220
+ ### Nested Example
221
+
222
+ ```python
223
+ import ypres
224
+
225
+ class Nestee(object):
226
+ """An object nested inside another object."""
227
+ n = 'hi'
228
+
229
+
230
+ class Foo(object):
231
+ x = 1
232
+ nested = Nestee()
233
+
234
+
235
+ class NesteeSerializer(ypres.Serializer):
236
+ n = ypres.Field()
237
+
238
+
239
+ class FooSerializer(ypres.Serializer):
240
+ x = ypres.Field()
241
+ # Use another serializer as a field.
242
+ nested = NesteeSerializer()
243
+
244
+ f = Foo()
245
+ FooSerializer(f).serialized
246
+ # {'x': 1, 'nested': {'n': 'hi'}}
247
+ ```
248
+
249
+ ### Complex Example
250
+
251
+ ```python
252
+ import ypres
253
+
254
+ class Foo(object):
255
+ y = 1
256
+ z = 2
257
+ super_long_thing = 10
258
+
259
+ def x(self):
260
+ return 5
261
+
262
+
263
+ class FooSerializer(ypres.Serializer):
264
+ w = ypres.Field(attr='super_long_thing')
265
+ x = ypres.Field(call=True)
266
+ plus = ypres.MethodField()
267
+
268
+ def get_plus(self, obj):
269
+ return obj.y + obj.z
270
+
271
+ f = Foo()
272
+ FooSerializer(f).serialized
273
+ # {'w': 10, 'x': 5, 'plus': 3}
274
+ ```
275
+
276
+ ### Inheritance Example
277
+
278
+ ```python
279
+ import ypres
280
+
281
+ class Foo(object):
282
+ a = 1
283
+ b = 2
284
+
285
+
286
+ class ASerializer(ypres.Serializer):
287
+ a = ypres.Field()
288
+
289
+
290
+ class ABSerializer(ASerializer):
291
+ """ABSerializer inherits the 'a' field from ASerializer.
292
+
293
+ This also works with multiple inheritance and mixins.
294
+ """
295
+ b = ypres.Field()
296
+
297
+ f = Foo()
298
+ ASerializer(f).serialized
299
+ # {'a': 1}
300
+ ABSerializer(f).serialized
301
+ # {'a': 1, 'b': 2}
302
+ ```
303
+
304
+ ## License
305
+
306
+ ypres is free software distributed under the terms of the MIT license.
307
+ See the [LICENSE](https://github.com/clarkduvall/serpy/blob/master/LICENSE) file.
@@ -0,0 +1,8 @@
1
+ ypres/__init__.py,sha256=cfKxBKZQXXMnXDlrf2R7vCxqjlGvx3CiVYv8ipJiyMI,758
2
+ ypres/fields.py,sha256=s22BqXNGSmM-Hk_vYTJsibFJ8GcsziSTzN6E5gwJKLs,7393
3
+ ypres/serializer.py,sha256=eDwjplzjtiWjCClSLYPbleB5CI-0-ixDjn21nV0tNcE,18333
4
+ ypres/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ ypres-1.1.2.dist-info/METADATA,sha256=EBum3qgf1r4nUxiV2vIdjc913TkpVLQL4xJl1-JNP3U,10638
6
+ ypres-1.1.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ ypres-1.1.2.dist-info/licenses/LICENSE,sha256=CEi0Gjqz-WIM4V9noRMkxZpHKugaYv_g2ri2oSiUBJc,1136
8
+ ypres-1.1.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Clark DuVall
4
+ Copyright (c) 2023 Andrew Hankinson, RISM Digital Center
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.