from typedpy.commons import wrap_val
from typedpy.structures import Structure, TypedPyDefaults, ADDITIONAL_PROPERTIES
from typedpy.fields import StructureClass, Map, String, OneOf, Boolean, FunctionCall
from .serialization import deserialize_structure, serialize
[docs]class Deserializer(Structure):
"""
A high level API for a deserializer: from a dict or anything else that could be sent as a JSON, to
a :class:`Structure`.
The advantage of this over the lower level function is that it is more explicit and self-validating.
In other words, it prevents the user from creating an invalid mapper.
Arguments:
target_class(:class:`StructureClass`):
A class extending the abstract :class:`Structure` that this deserializer is build for.
Example:
.. code-block:: python
class Foo(Structure):
id = Integer
name = String
Deserializer(target_class=Foo)
mapper(dict): optional
The key is the target attribute name. The value can either be a path of the value in the source dict
using dot notation, for example: "aaa.bbb", or a :class:`FunctionCall`. In the latter case,
the function is the used to preprocess the input prior to deserialization/validation.
The args attribute in the function call is optional. If non provided, the input to the function is
the value with the same key. Otherwise it is the keys of the values in the input that are injected
to the provided function. See working examples in the tests link above.
This class will ensure that the mapper is a valid one for its target_class.
Example:
.. code-block:: python
class Foo(Structure):
m = Map
s = String
i = Integer
mapper = {
"m": "a.b",
"s": FunctionCall(func=lambda x: f'the string is {x}', args=['name.first']),
'i': FunctionCall(func=operator.add, args=['i', 'j'])
}
Deserializer(target_class=Foo, mapper = mapper).deserializer(the_input_dict)
camel_case_convert(bool): Optional
If true, will convert any camelCase key that does not have explicit mapping to a snake_case attribute
name. Default is False.
use_strict_mapping(bool): Optional
If true, if a field is mapped to a different key name, then deserialization will never use the key
that matches the original field name for that field. i.e.:
.. code-block:: python
class Foo(Structure):
a: int
_serialization_mapper = {"a": "b"}
# This raises an exception
Deserializer(Foo, use_strict_mapping=True).deserialize({"a": 5})
"""
target_class = StructureClass
mapper = Map[String, OneOf[String, FunctionCall, Map]]
use_strict_mapping = Boolean(default=False)
camel_case_convert = Boolean(default=False)
_required = ["target_class"]
def __validate__(self):
valid_keys = set(getattr(self.target_class, "get_all_fields_by_name")().keys())
if self.mapper:
for key in self.mapper:
if key.split(".")[0] not in valid_keys:
raise ValueError(
f"Invalid key in mapper for class {self.target_class.__name__}: {key}. Keys must be one of "
"the class fields. "
)
def deserialize(
self, input_data, *, keep_undefined=None, direct_trusted_mapping=False
):
additional_props_allowed = getattr(
self.target_class,
ADDITIONAL_PROPERTIES,
TypedPyDefaults.additional_properties_default,
)
adjusted_keep_undefined = (
keep_undefined
if keep_undefined is not None or additional_props_allowed
else True
)
return deserialize_structure(
self.target_class,
input_data,
mapper=self.mapper,
use_strict_mapping=self.use_strict_mapping,
keep_undefined=adjusted_keep_undefined,
camel_case_convert=self.camel_case_convert,
direct_trusted_mapping=direct_trusted_mapping,
)
[docs]class Serializer(Structure):
"""
A high level API for a serializer: from an instance of :class:`Structure`, to something
that can be sent as a Json(usually a dict). The advantage of this over the lower level
function is that it is more explicit andself-validating.
In other words, it prevents the user from creating an invalid mapper.
Arguments:
source(:class:`Structure`):
An instance of :class:`Structure` that this serializer is build for.
Example:
.. code-block:: python
class Foo(Structure):
i = Integer
f = Float
foo = Foo(f=5.5, i=999)
Serializer(source=foo)
mapper(dict): optional
The key is the target key name. The value can either be a path of the value in the source object
using dot notation, for example: "aaa.bbb", or a :class:`FunctionCall`. In the latter case,
the function is the used to preprocess the input prior to deserialization/validation. \
The args attribute in the function call is optional. If non provided, the input to the function is
the attribute with the same key. Otherwise it is the names of the attributes in the input
that are injected to the provided function. \
See working examples in the tests link above. \
This class will ensure that the mapper is a valid one for its target_class.
Example:
.. code-block:: python
class Foo(Structure):
f = Float
i = Integer
foo = Foo(f=5.5, i=999)
mapper = {
'output_floats': FunctionCall(func=lambda f: [int(f)], args=['i']),
'output_int': FunctionCall(func=lambda x: str(x), args=['f'])
}
assert Serializer(source=foo, mapper=mapper).serialize() == {
'output_floats': [999],
'output_int': '5.5'
}
"""
source = Structure
mapper = Map[String, OneOf[String, FunctionCall, Map]]
_required = ["source"]
def __validate__(self):
def verify_key_in_mapper(key, valid_keys, source_class):
if key.split(".")[0] not in valid_keys:
raise ValueError(
f"Invalid key in mapper for class {source_class.__name__}: {key}. Keys must be one of the class "
"fields. "
)
if isinstance(self.mapper[key], (FunctionCall,)):
args = self.mapper[key].args
if isinstance(args, (list,)):
for arg in args:
if arg not in valid_keys:
raise ValueError(
f"Mapper[{key}] has a function call with an invalid argument: {arg}"
)
source_class = self.source.__class__
valid_keys = set(source_class.get_all_fields_by_name().keys())
if self.mapper:
for key in self.mapper:
verify_key_in_mapper(key, valid_keys, source_class)
def serialize(
self,
compact: bool = None,
camel_case_convert: bool = False,
):
"""
Arguments:
compact(boolean): optional
whether to use a compact form for Structure that is a simple wrapper of a field.
for example: if a Structure has only one field of an int, if compact is True
it will serialize the structure as an int instead of a dictionary.
Default is False.
camel_case_convert(dict): optional
If True, convert any camel-case key that does not have a mapping in the mapper to a snake-case
attribute.
Default is False.
"""
compact = (
TypedPyDefaults.compact_serialization_default
if compact is None
else compact
)
return serialize(
self.source,
mapper=self.mapper,
compact=compact,
camel_case_convert=camel_case_convert,
)
def deserializer_by_discriminator(class_by_discriminator_value, keep_undefined=False):
"""
create deserialized based on discriminator value in the input.
:param class_by_discriminator_value: a dictionary of the Structure class by the discriminator value
:return: A function that should be used in a mapper, for which the first argument is the key for
The discriminator field, and the second is the key for the data field that is deserialized based
on the provided class_by_discriminator_value.
"""
_desererializer_by_type = {
k: Deserializer(t) for (k, t) in class_by_discriminator_value.items()
}
def _get_content(discriminator, data):
if discriminator not in _desererializer_by_type:
raise ValueError(
f"discriminator: got {wrap_val(discriminator)}; Expected one of {list(_desererializer_by_type.keys())}"
)
return _desererializer_by_type[discriminator].deserialize(
data, keep_undefined=keep_undefined
)
return _get_content