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 +43 -0
- ypres/fields.py +210 -0
- ypres/py.typed +0 -0
- ypres/serializer.py +569 -0
- ypres-1.1.2.dist-info/METADATA +307 -0
- ypres-1.1.2.dist-info/RECORD +8 -0
- ypres-1.1.2.dist-info/WHEEL +4 -0
- ypres-1.1.2.dist-info/licenses/LICENSE +22 -0
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,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.
|