Skip to content

serdes

Utilities for type translation, serialization, and deserialization.

Typical Usage

>>> from typelib import serdes
>>>
>>> serdes.load("1")
1
>>> import datetime
>>> from typelib import serdes
>>>
>>> serdes.unixtime(datetime.datetime(2020, 1, 1))
1577854800.0
>>> serdes.isoformat(datetime.timedelta(hours=1))
'PT1H'
>>> import dataclasses
>>> @dataclasses.dataclass
... class Class:
...     attr: str
...
>>> instance = Class(attr="value")
>>> dict(serdes.iteritems(instance))
{'attr': 'value'}

MarshalledValueT module-attribute

MarshalledValueT: TypeAlias = 'PythonPrimitiveT | dict[PythonPrimitiveT, MarshalledValueT] | list[MarshalledValueT]'

Type alias for a Python value which is ready for over-the-wire serialization.

PythonPrimitiveT module-attribute

PythonPrimitiveT: TypeAlias = 'bool | int | float | str | None'

Type alias for serializable, non-container Python types.

PythonValueT module-attribute

PythonValueT: TypeAlias = 'PythonPrimitiveT | dict[PythonPrimitiveT, PythonValueT] | list[PythonValueT] | tuple[PythonValueT, ...] | set[PythonValueT]'

Type alias for any Python builtin type.

dateparse

dateparse(val: str, t: type[DateTimeT]) -> DateTimeT

Parse a date string into a datetime object.

Examples:

>>> import datetime
>>> from typelib import serdes
>>> serdes.dateparse("1970-01-01",t=datetime.datetime)
DateTime(1970, 1, 1, 0, 0, 0, tzinfo=Timezone('UTC'))

Parameters:

  • val (str) –

    The date string to parse.

  • t (type[DateTimeT]) –

    The target datetime type.

Returns:

  • DateTimeT

    The parsed datetime object.

Raises:

  • ValueError

    If val is not a date string or does not resolve to an instance of the target datetime type.

Source code in src/typelib/serdes.py
@compat.lru_cache(maxsize=100_000)
def dateparse(val: str, t: type[DateTimeT]) -> DateTimeT:
    """Parse a date string into a datetime object.

    Examples:
        >>> import datetime
        >>> from typelib import serdes
        >>> serdes.dateparse("1970-01-01",t=datetime.datetime)
        DateTime(1970, 1, 1, 0, 0, 0, tzinfo=Timezone('UTC'))

    Args:
        val: The date string to parse.
        t: The target datetime type.

    Returns:
        The parsed datetime object.

    Raises:
        ValueError:
            If `val` is not a date string or does not resolve to an instance of
            the target datetime type.
    """
    try:
        # When `exact=False`, the only two possibilities are DateTime and Duration.
        parsed: pendulum.DateTime | pendulum.Duration = pendulum.parse(val)  # type: ignore[assignment]
        normalized = _nomalize_dt(val=val, parsed=parsed, td=t)
        return normalized
    except ValueError:
        if val.isdigit() or val.isdecimal():
            return _normalize_number(numval=float(val), td=t)
        raise

decode

decode(val: Any, *, encoding: str = constants.DEFAULT_ENCODING) -> Any

Decode a bytes-like object into a str.

Note

If a non-bytes-like object is passed, it will be returned unchanged.

Examples:

>>> from typelib import serdes
>>> serdes.decode(b"abc")
'abc'
>>> serdes.decode(memoryview(b"abc"))
'abc'

Parameters:

  • val (Any) –

    The object to be decoded.

  • encoding (str, default: DEFAULT_ENCODING ) –

    The encoding to use when decoding the object (defaults "utf8").

Source code in src/typelib/serdes.py
def decode(val: t.Any, *, encoding: str = constants.DEFAULT_ENCODING) -> t.Any:
    """Decode a bytes-like object into a str.

    Note:
        If a non-bytes-like object is passed, it will be returned unchanged.

    Examples:
        >>> from typelib import serdes
        >>> serdes.decode(b"abc")
        'abc'
        >>> serdes.decode(memoryview(b"abc"))
        'abc'

    Args:
        val: The object to be decoded.
        encoding: The encoding to use when decoding the object (defaults "utf8").
    """
    val = val.tobytes() if isinstance(val, memoryview) else val
    if isinstance(val, (bytes, bytearray)):
        decoded = val.decode(encoding)
        return decoded
    return val

get_items_iter

get_items_iter(tp: type) -> Callable[[Any], Iterable[tuple[Any, Any]]]

Given a type, return a callable which will produce an iterator over (field, value) pairs.

Examples:

>>> import dataclasses
>>> from typelib import serdes
>>>
>>> @dataclasses.dataclass
... class Class:
...     attr: str
...
>>> instance = Class(attr="value")
>>> iteritems = get_items_iter(Class)
>>> next(iteritems(instance))
('attr', 'value')

Parameters:

  • tp (type) –

    The type to create an iterator for.

Source code in src/typelib/serdes.py
@compat.cache
def get_items_iter(tp: type) -> t.Callable[[t.Any], t.Iterable[tuple[t.Any, t.Any]]]:
    """Given a type, return a callable which will produce an iterator over (field, value) pairs.

    Examples:
        >>> import dataclasses
        >>> from typelib import serdes
        >>>
        >>> @dataclasses.dataclass
        ... class Class:
        ...     attr: str
        ...
        >>> instance = Class(attr="value")
        >>> iteritems = get_items_iter(Class)
        >>> next(iteritems(instance))
        ('attr', 'value')

    Args:
        tp: The type to create an iterator for.
    """
    ismapping, isnamedtuple, isiterable = (
        inspection.ismappingtype(tp),
        inspection.isnamedtuple(tp),
        inspection.isiterabletype(tp),
    )
    if ismapping:
        return _itemscaller
    if isnamedtuple:
        return _namedtupleitems
    if isiterable:
        return enumerate
    return _make_fields_iterator(tp)

isoformat

isoformat(dt: date | time | timedelta) -> str

Format any date/time object into an ISO-8601 string.

Note

While the standard library includes isoformat() methods for datetime.date, datetime.time, & datetime.datetime, they do not include a method for serializing datetime.timedelta, even though durations are included in the ISO 8601 specification. This function fills that gap.

Examples:

>>> import datetime
>>> from typelib import serdes
>>> serdes.isoformat(datetime.date(1970, 1, 1))
'1970-01-01'
>>> serdes.isoformat(datetime.time())
'00:00:00'
>>> serdes.isoformat(datetime.datetime(1970, 1, 1))
'1970-01-01T00:00:00'
>>> serdes.isoformat(datetime.timedelta(hours=1))
'PT1H'
Source code in src/typelib/serdes.py
@compat.lru_cache(maxsize=100_000)
def isoformat(dt: datetime.date | datetime.time | datetime.timedelta) -> str:
    """Format any date/time object into an ISO-8601 string.

    Note:
        While the standard library includes `isoformat()` methods for
        [`datetime.date`][], [`datetime.time`][], &
        [`datetime.datetime`][], they do not include a method for serializing
        [`datetime.timedelta`][], even though durations are included in the
        ISO 8601 specification. This function fills that gap.

    Examples:
        >>> import datetime
        >>> from typelib import serdes
        >>> serdes.isoformat(datetime.date(1970, 1, 1))
        '1970-01-01'
        >>> serdes.isoformat(datetime.time())
        '00:00:00'
        >>> serdes.isoformat(datetime.datetime(1970, 1, 1))
        '1970-01-01T00:00:00'
        >>> serdes.isoformat(datetime.timedelta(hours=1))
        'PT1H'
    """
    if isinstance(dt, (datetime.date, datetime.time)):
        return dt.isoformat()
    dur: pendulum.Duration = (
        dt
        if isinstance(dt, pendulum.Duration)
        else pendulum.duration(
            days=dt.days,
            seconds=dt.seconds,
            microseconds=dt.microseconds,
        )
    )
    datepart = "".join(
        f"{p}{s}"
        for p, s in ((dur.years, "Y"), (dur.months, "M"), (dur.remaining_days, "D"))
        if p
    )
    timepart = "".join(
        f"{p}{s}"
        for p, s in (
            (dur.hours, "H"),
            (dur.minutes, "M"),
            (
                f"{dur.remaining_seconds}.{dur.microseconds:06}"
                if dur.microseconds
                else dur.remaining_seconds,
                "S",
            ),
        )
        if p
    )
    period = f"P{datepart}T{timepart}"
    return period

iteritems

iteritems(val: Any) -> Iterable[tuple[Any, Any]]

Iterate over (field, value) pairs for any object.

Examples:

>>> import dataclasses
>>> from typelib import serdes
>>>
>>> @dataclasses.dataclass
... class Class:
...     attr: str
...
>>> instance = Class(attr="value")
>>> [*serdes.iteritems(instance)]
[('attr', 'value')]
>>> [*serdes.iteritems("string")]
[(0, 's'), (1, 't'), (2, 'r'), (3, 'i'), (4, 'n'), (5, 'g')]
>>> [*serdes.iteritems(serdes.iteritems(instance))]
[('attr', 'value')]
Note

If the given item is detected to be an iterable of pairs (e.g., [('a', 1), ('b', 2)]), we will iterate directly over that.

Otherwise, we will create an iterator over (field, value) pairs with the following strategy:

  • For mappings -> ((key, value), ...)
  • For structured objects (user-defined classes) -> ((field, value), ...)
  • For all other iterables -> ((index, value), ...).

Parameters:

  • val (Any) –

    The object to iterate over.

Source code in src/typelib/serdes.py
def iteritems(val: t.Any) -> t.Iterable[tuple[t.Any, t.Any]]:
    """Iterate over (field, value) pairs for any object.

    Examples:
        >>> import dataclasses
        >>> from typelib import serdes
        >>>
        >>> @dataclasses.dataclass
        ... class Class:
        ...     attr: str
        ...
        >>> instance = Class(attr="value")
        >>> [*serdes.iteritems(instance)]
        [('attr', 'value')]
        >>> [*serdes.iteritems("string")]
        [(0, 's'), (1, 't'), (2, 'r'), (3, 'i'), (4, 'n'), (5, 'g')]
        >>> [*serdes.iteritems(serdes.iteritems(instance))]
        [('attr', 'value')]

    Note:
        If the given item is detected to be an iterable of pairs (e.g., `[('a', 1), ('b', 2)]`),
        we will iterate directly over that.

        Otherwise, we will create an iterator over (field, value) pairs with the following
        strategy:

        - For mappings -> `((key, value), ...)`
        - For structured objects (user-defined classes) -> `((field, value), ...)`
        - For all other iterables -> `((index, value), ...)`.

    Args:
        val: The object to iterate over.
    """
    # If the given value is an iterable, we will create a `peekable` so we can inspect it.
    #   In that case, we want to continue using the peekable, since the act of peeking
    #   is destructive to the original iterable in the event it is an iterator.
    is_pairs, it = _is_iterable_of_pairs(val)
    if is_pairs:
        return iter(it)

    iterate = get_items_iter(val.__class__)
    return iterate(it)

itervalues

itervalues(val: Any) -> Iterator[Any]

Iterate over the contained values for any object.

Examples:

>>> import dataclasses
>>> from typelib import serdes
>>>
>>> @dataclasses.dataclass
... class Class:
...     attr: str
...
>>> instance = Class(attr="value")
>>> [*serdes.itervalues(instance)]
['value']

Parameters:

  • val (Any) –

    The object to iterate over.

Source code in src/typelib/serdes.py
def itervalues(val: t.Any) -> t.Iterator[t.Any]:
    """Iterate over the contained values for any object.

    Examples:
        >>> import dataclasses
        >>> from typelib import serdes
        >>>
        >>> @dataclasses.dataclass
        ... class Class:
        ...     attr: str
        ...
        >>> instance = Class(attr="value")
        >>> [*serdes.itervalues(instance)]
        ['value']

    Args:
        val: The object to iterate over.
    """
    iterate = get_items_iter(val.__class__)
    return (v for k, v in iterate(val))

load

load(val: _T) -> PythonValueT | _T

Attempt to decode val if it is a text-like object.

Otherwise, return val unchanged.

Examples:

>>> from typelib import serdes
>>> serdes.load(1)
1
>>> serdes.load("1")
1
>>> serdes.load("1,2")
(1, 2)
>>> serdes.load(b'{"a": 1, "b": 2}')
{'a': 1, 'b': 2}

Parameters:

  • val (_T) –

    The value to decode.

Source code in src/typelib/serdes.py
def load(val: _T) -> PythonValueT | _T:
    """Attempt to decode `val` if it is a text-like object.

    Otherwise, return `val` unchanged.

    Examples:
        >>> from typelib import serdes
        >>> serdes.load(1)
        1
        >>> serdes.load("1")
        1
        >>> serdes.load("1,2")
        (1, 2)
        >>> serdes.load(b'{"a": 1, "b": 2}')
        {'a': 1, 'b': 2}

    Args:
        val: The value to decode.
    """
    return strload(val) if inspection.istexttype(val.__class__) else val  # type: ignore[arg-type]

strload

strload(val: str | bytes | bytearray | memoryview) -> PythonValueT

Attempt to decode a string-like input into a Python value.

Examples:

>>> from typelib import serdes
>>> serdes.strload("1")
1
>>> serdes.strload("1,2")
(1, 2)
>>> serdes.strload(b'{"a": 1, "b": 2}')
{'a': 1, 'b': 2}
Tip

This function is memoized and only safe for text-type inputs.

See Also

Parameters:

Source code in src/typelib/serdes.py
@compat.lru_cache(maxsize=100_000)
def strload(val: str | bytes | bytearray | memoryview) -> PythonValueT:
    """Attempt to decode a string-like input into a Python value.

    Examples:
        >>> from typelib import serdes
        >>> serdes.strload("1")
        1
        >>> serdes.strload("1,2")
        (1, 2)
        >>> serdes.strload(b'{"a": 1, "b": 2}')
        {'a': 1, 'b': 2}


    Tip:
        This function is memoized and only safe for text-type inputs.

    See Also:
         - [`load`][typelib.serdes.load]

    Args:
        val: The string-like input to be decoded.
    """
    with contextlib.suppress(ValueError):
        return compat.json.loads(val)

    decoded = decode(val)
    with contextlib.suppress(ValueError, TypeError, SyntaxError):
        return ast.literal_eval(decoded)

    return decoded

unixtime

unixtime(dt: date | time | timedelta) -> float

Convert a date/time object to a unix timestamp.

Note

Time is messy. Here is how we've decided to make this work:

  • datetime.datetime instances will preserve the current tzinfo (even if naive).
  • datetime.time instances will default to today, preserving the tzinfo (even if naive).
  • datetime.date instances will assume UTC.
  • datetime.timedelta instances be reflected as total seconds since epoch (January 1, 1970).

If you find yourself in a situation where this does not work for you, your best bet is to stop using tz-naive date/time objects. It's always best to keep your time explicit!

Parameters:

Examples:

>>> import datetime
>>> from typelib import serdes
>>>
>>> serdes.unixtime(datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc))
0.0
>>> serdes.unixtime(datetime.date(1970, 1, 1))
0.0
Source code in src/typelib/serdes.py
def unixtime(dt: datetime.date | datetime.time | datetime.timedelta) -> float:
    """Convert a date/time object to a unix timestamp.

    Note:
        Time is messy. Here is how we've decided to make this work:

        - `datetime.datetime` instances will preserve the current tzinfo (even if naive).
        - `datetime.time` instances will default to today, preserving the tzinfo (even if naive).
        - `datetime.date` instances will assume UTC.
        - `datetime.timedelta` instances be reflected as total seconds since epoch (January 1, 1970).

        If you find yourself in a situation where this does not work for you, your best
        bet is to stop using tz-naive date/time objects. *It's always best to keep your time
        explicit!*

    Args:
        dt: The object to be converted.

    Examples:
        >>> import datetime
        >>> from typelib import serdes
        >>>
        >>> serdes.unixtime(datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc))
        0.0
        >>> serdes.unixtime(datetime.date(1970, 1, 1))
        0.0
    """
    if isinstance(dt, datetime.timedelta):
        return dt.total_seconds()

    if isinstance(dt, datetime.time):
        dt = datetime.datetime.now(tz=dt.tzinfo).replace(
            hour=dt.hour,
            minute=dt.minute,
            second=dt.second,
            microsecond=dt.microsecond,
        )
    if isinstance(dt, datetime.date) and not isinstance(dt, datetime.datetime):
        dt = datetime.datetime(
            year=dt.year,
            month=dt.month,
            day=dt.day,
            tzinfo=datetime.timezone.utc,
        )

    return dt.timestamp()