Source code for smarter.lib.json

"""
Overridden JSON utilities for the Smarter platform.

This module wraps the standard :mod:`json` library with the following enhancements:

- Uses :class:`SmarterJSONEncoder` as the default encoder.
- Standardizes indentation to 2 characters.
- Falls back to :func:`str` for non-serializable objects.

:class:`SmarterJSONEncoder` adds serialization support for the following types:

- :class:`datetime.datetime`, :class:`datetime.date`, :class:`datetime.time`, :class:`datetime.timedelta`
- :class:`decimal.Decimal`
- :class:`uuid.UUID`
- ``taggit.managers.TaggableManager``

The following symbols are re-exported unmodified from :mod:`json`:

- :exc:`~json.JSONDecodeError`
- :class:`~json.JSONDecoder`
- :func:`~json.load`
- :func:`~json.loads`
"""

import datetime
import decimal
import json
import logging
import uuid

# pylint: disable=unused-import
from json import (  # unmodified re-export
    JSONDecodeError,
    JSONDecoder,
    load,
    loads,
)

logger = logging.getLogger(__name__)
formatted_logger_prefix = "SmarterJSONEncoder"


class Promise:
    """
    Base class for the proxy class created in the closure of the lazy function.
    It's used to recognize promises in code.
    """


def _get_duration_components(duration):
    days = duration.days
    seconds = duration.seconds
    microseconds = duration.microseconds

    minutes = seconds // 60
    seconds %= 60

    hours = minutes // 60
    minutes %= 60

    return days, hours, minutes, seconds, microseconds


def is_aware(value):
    """
    Determine if a given datetime.datetime is aware.

    The concept is defined in Python's docs:
    https://docs.python.org/library/datetime.html#datetime.tzinfo

    Assuming value.tzinfo is either None or a proper datetime.tzinfo,
    value.utcoffset() implements the appropriate logic.
    """
    return value.utcoffset() is not None


# pylint: disable=C0209
def duration_iso_string(duration):
    if duration < datetime.timedelta(0):
        sign = "-"
        duration *= -1
    else:
        sign = ""

    days, hours, minutes, seconds, microseconds = _get_duration_components(duration)
    ms = ".{:06d}".format(microseconds) if microseconds else ""
    return "{}P{}DT{:02d}H{:02d}M{:02d}{}S".format(sign, days, hours, minutes, seconds, ms)


[docs] class SmarterJSONEncoder(json.JSONEncoder): """ JSONEncoder subclass that knows how to encode odd types like - date/time - decimal - UUIDs - TaggableManager """
[docs] def default(self, o): # TaggableManager support (import inside to avoid startup issues) # most common cases. pass back the super().default(o) if isinstance(o, (str, int, float, type(None), bool)): return super().default(o) # See "Date Time String Format" in the ECMA-262 specification. elif isinstance(o, datetime.datetime): r = o.isoformat() if o.microsecond: r = r[:23] + r[26:] if r.endswith("+00:00"): r = r.removesuffix("+00:00") + "Z" return r elif isinstance(o, datetime.date): return o.isoformat() elif isinstance(o, datetime.time): if is_aware(o): raise ValueError("JSON can't represent timezone-aware times.") r = o.isoformat() if o.microsecond: r = r[:12] return r elif isinstance(o, datetime.timedelta): return duration_iso_string(o) elif isinstance(o, (decimal.Decimal, uuid.UUID, Promise)): return str(o) elif isinstance(o, set): return list(o) else: # Handle Django's GenericRelatedObjectManager and Django's # TaggableManager without importing them directly in order to avoid # import timing issues during startup. if ( type(o).__name__ == "GenericRelatedObjectManager" and getattr(type(o), "__module__", None) == "django.contrib.contenttypes.fields" ): return list(o.all()) # Handle TaggedItem if type(o).__name__ == "TaggedItem" and getattr(type(o), "__module__", None) == "taggit.models": retval = o.tag.name logger.debug("%s.default() Serializing TaggedItem with tag: %s", formatted_logger_prefix, retval) return retval # Handle TaggableManager if ( type(o).__name__ in ("TaggableManager", "_TaggableManager") and getattr(type(o), "__module__", None) == "taggit.managers" ): retval = list(o.all().values_list("name", flat=True)) logger.debug("%s.default() Serializing TaggableManager with tags: %s", formatted_logger_prefix, retval) return retval return super().default(o)
[docs] def dumps( obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw, ): """ JSON dump with - SmarterJSONEncoder as default encoder - indent of 2 - default of str """ return json.dumps( obj, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular, allow_nan=allow_nan, cls=cls or SmarterJSONEncoder, indent=indent or 2, separators=separators, default=default or str, sort_keys=sort_keys, **kw, )
__all__ = [ "JSONDecodeError", "JSONDecoder", "SmarterJSONEncoder", "dumps", "load", "loads", ]