Source code for flexget.utils.json

"""Helper module that can load whatever version of the json module is available.

Plugins can just import the methods from this module.

Also allows date and datetime objects to be encoded/decoded.
"""

import datetime
from collections.abc import Iterable, Mapping
from contextlib import suppress
from typing import Any

from flexget.plugin import DependencyError

try:
    import simplejson as json
except ImportError:
    try:
        import json
    except ImportError:
        try:
            # Google Appengine offers simplejson via django
            from django.utils import simplejson as json
        except ImportError:
            raise DependencyError(missing='simplejson')

DATE_FMT = '%Y-%m-%d'
ISO8601_FMT = '%Y-%m-%dT%H:%M:%SZ'


[docs] class DTDecoder(json.JSONDecoder):
[docs] def decode(self, obj, **kwargs): # The built-in `json` library will `unicode` strings, except for empty strings. patch this for # consistency so that `unicode` is always returned. if obj == b'': return '' if isinstance(obj, str): dt_str = obj.strip('"') try: return datetime.datetime.strptime(dt_str, ISO8601_FMT) except (ValueError, TypeError): with suppress(ValueError, TypeError): return datetime.datetime.strptime(dt_str, DATE_FMT) return super().decode(obj, **kwargs)
[docs] def _datetime_encoder(obj: datetime.datetime | datetime.date) -> str: if isinstance(obj, datetime.datetime): return obj.strftime(ISO8601_FMT) if isinstance(obj, datetime.date): return obj.strftime(DATE_FMT) raise TypeError
[docs] def _datetime_decoder(dict_: dict) -> dict: for key, value in dict_.items(): # The built-in `json` library will `unicode` strings, except for empty strings. patch this for # consistency so that `unicode` is always returned. if value == b'': dict_[key] = '' continue try: datetime_obj = datetime.datetime.strptime(value, ISO8601_FMT) dict_[key] = datetime_obj except (ValueError, TypeError): with suppress(ValueError, TypeError): date_obj = datetime.datetime.strptime(value, DATE_FMT) dict_[key] = date_obj.date() return dict_
[docs] def _empty_unicode_decoder(dict_: dict) -> dict: for key, value in dict_.items(): # The built-in `json` library will `unicode` strings, except for empty strings. patch this for # consistency so that `unicode` is always returned. if value == b'': dict_[key] = '' return dict_
[docs] def dumps(*args, **kwargs) -> str: if kwargs.pop('encode_datetime', False): kwargs['default'] = _datetime_encoder return json.dumps(*args, **kwargs)
[docs] def dump(*args, **kwargs) -> None: if kwargs.pop('encode_datetime', False): kwargs['default'] = _datetime_encoder return json.dump(*args, **kwargs)
[docs] def loads(*args, **kwargs) -> Any: """:param bool decode_datetime: If `True`, dates in ISO8601 format will be deserialized to :class:`datetime.datetime` objects.""" if kwargs.pop('decode_datetime', False): kwargs['object_hook'] = _datetime_decoder kwargs['cls'] = DTDecoder else: kwargs['object_hook'] = _empty_unicode_decoder return json.loads(*args, **kwargs)
[docs] def load(*args, **kwargs) -> Any: """:param bool decode_datetime: If `True`, dates in ISO8601 format will be deserialized to :class:`datetime.datetime` objects.""" if kwargs.pop('decode_datetime', False): kwargs['object_hook'] = _datetime_decoder kwargs['cls'] = DTDecoder else: kwargs['object_hook'] = _empty_unicode_decoder return json.load(*args, **kwargs)
[docs] def coerce(obj) -> str | int | float | bool | dict | list | None: """Coerce a data structure to a JSON serializable form. Will recursively go through data structure, and attempt to turn anything not JSON serializable into a string. """ if isinstance(obj, (str, int, float, bool, type(None))): return obj if isinstance(obj, datetime.datetime): return obj.strftime(ISO8601_FMT) if isinstance(obj, datetime.date): return obj.strftime(DATE_FMT) if isinstance(obj, Mapping): return {k: coerce(v) for k, v in obj.items()} if isinstance(obj, Iterable): return [coerce(v) for v in obj] try: return str(obj) except Exception: return 'NOT JSON SERIALIZABLE'