# Copyright (c) 2022. The Pycroft Authors. See the AUTHORS file.
# This file is part of the Pycroft project and licensed under the terms of
# the Apache License, Version 2.0. See the LICENSE file for details
"""
pycroft.helpers.i18n.serde
~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import annotations
import operator
import typing
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from ..functional import identity
from .types import Interval, NegativeInfinity, PositiveInfinity, Bound, Money
from .utils import qualified_typename
[docs]
def deserialize_money(v):
try:
return Money(Decimal(v[0]), v[1])
except IndexError as e:
raise ValueError from e
[docs]
def serialize_interval(interval: Interval) -> dict[str, typing.Any]:
lower = interval.lower_bound
upper = interval.upper_bound
lower_value = serialize_param(lower.value) if not lower.unbounded else None
upper_value = serialize_param(upper.value) if not upper.unbounded else None
return {
"lower_closed": lower.closed,
"lower_value": lower_value,
"upper_closed": upper.closed,
"upper_value": upper_value,
}
# pycharm does not support building this dynamically.
# Thus, we need to hard-code this type alias.
Serializable = typing.Union[
None,
bool,
str,
int,
float,
Decimal,
Money,
date,
datetime,
time,
timedelta,
Interval,
]
# TODO be more specific about return type
serialize_map: dict[type, typing.Callable] = {
type(None): identity,
bool: identity,
float: identity,
str: identity,
int: identity,
Decimal: str,
Money: lambda m: (str(m.value), m.currency),
date: operator.methodcaller("isoformat"),
datetime: operator.methodcaller("isoformat"),
time: operator.methodcaller("isoformat"),
timedelta: lambda v: {
"days": v.days,
"seconds": v.seconds,
"microseconds": v.microseconds,
},
Interval: serialize_interval,
}
[docs]
def deserialize_interval(value: dict[str, typing.Any]) -> Interval:
try:
lower_value = (
deserialize_param(u)
if (u := value["lower_value"]) is not None
else NegativeInfinity
)
lower_closed = value["lower_closed"]
upper_value = (
deserialize_param(u)
if (u := value["upper_value"]) is not None
else PositiveInfinity
)
upper_closed = value["upper_closed"]
except KeyError as e:
raise ValueError("Could not deserialized from interval (missing key)") from e
if not isinstance(lower_closed, bool):
raise ValueError(
"Could not deserialize to interval: "
f"expected ['lower_closed'] to be bool, got {type(lower_closed)}"
)
if not isinstance(upper_closed, bool):
raise ValueError(
"Could not deserialize to interval: "
f"expected ['upper_closed'] to be bool, got {type(upper_closed)}"
)
return Interval(
lower_bound=Bound(lower_value, lower_closed),
upper_bound=Bound(upper_value, upper_closed),
)
_deserialize_type_map: dict[type, typing.Callable] = {
type(None): identity,
bool: identity,
str: identity,
int: identity,
float: identity,
Decimal: Decimal,
Money: deserialize_money,
date: date.fromisoformat,
datetime: datetime.fromisoformat,
time: time.fromisoformat,
timedelta: lambda v: timedelta(**v),
Interval: deserialize_interval,
}
# TODO be more specific
deserialize_map: dict[str, typing.Callable] = {
qualified_typename(t): f for t, f in _deserialize_type_map.items()
}
[docs]
def serialize_param(param):
concrete_type = type(param)
serializers = (
(type_, serialize_map[type_])
for type_ in concrete_type.__mro__
if type_ in serialize_map
)
try:
type_, serializer = next(serializers)
except StopIteration:
raise TypeError(
"No serialization available for type {} or any"
"supertype".format(qualified_typename(concrete_type))
) from None
return {"type": qualified_typename(type_), "value": serializer(param)}
[docs]
def deserialize_param(param):
type_name = param["type"]
try:
deserializer = deserialize_map[type_name]
except KeyError:
raise TypeError(f"No deserialization available for type {type_name}") from None
return deserializer(param["value"])