Source code for flexget.utils.serialization

import datetime
from abc import ABC, abstractmethod
from typing import Any

import yaml
from loguru import logger

from flexget.utils import json

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


logger = logger.bind(name='utils.serialization')


[docs] def serialize(value: Any) -> Any: """Convert an object to JSON serializable format. :param value: Object to serialize. :return: JSON serializable representation of this object. """ s = _serializer_for(value) if s: return { 'serializer': s.serializer_name(), 'version': s.serializer_version(), 'value': s.serialize(value), } if isinstance(value, list): return [serialize(v) for v in value] if isinstance(value, dict): return {k: serialize(v) for k, v in value.items()} if isinstance(value, (str, int, float, type(None))): return value raise TypeError(f'`{value!r}` of type {type(value)!r} is not serializable')
[docs] def deserialize(value: Any) -> Any: """Restore an object stored with this serialization system to its original format. :param value: Serialized representation of the object. :return: Deserialized object. """ if isinstance(value, dict): if all(key in value for key in ('serializer', 'version', 'value')): return _deserializer_for(value['serializer']).deserialize( value['value'], value['version'] ) return {k: deserialize(v) for k, v in value.items()} if isinstance(value, list): return [deserialize(v) for v in value] return value
[docs] def dumps(value: Any) -> str: """Dump an object to JSON text using the serialization system.""" serialized = serialize(value) try: return json.dumps(serialized) except TypeError as exc: raise TypeError(f'Error during dumping {exc}. Instance: {serialized!r}') from exc
[docs] def loads(value: str) -> Any: """Restore an object from JSON text created by `dumps`.""" return deserialize(json.loads(value))
[docs] def yaml_dump(data, *args, **kwargs): """Dump an object to YAML text using the serialization system.""" data = serialize(data) kwargs['Dumper'] = FGDumper return yaml.dump(data, *args, **kwargs)
[docs] def yaml_load(stream): """Restore an object from YAML text created by `yaml_dump`.""" return yaml.load(stream, Loader=FGLoader)
[docs] class Serializer(ABC): """Any data types that should be serializable should subclass this, and implement the `serialize` and `deserialize` methods. This is important for data that is stored in `Entry` fields so that it can be stored to the database. """
[docs] @classmethod def serializer_name(cls) -> str: """Return name of the serializer defaults to class name. This can be overridden in subclass implementations if desired. """ return cls.__name__
[docs] @classmethod def serializer_version(cls) -> int: """If the format of serialization changes, this number should be incremented. The `deserialize` method of this class should continue to handle the old versions as well as the current version. """ return 1
[docs] @classmethod def serializer_handles(cls, value: Any) -> bool: """Return True if this serializer can handle `value`.""" return isinstance(value, cls)
[docs] @classmethod @abstractmethod def serialize(cls, value: Any) -> Any: """Return a plain python datatype which is json serializable."""
[docs] @classmethod @abstractmethod def deserialize(cls, data: Any, version: int) -> Any: """Return an instance of the original class, recreated from the serialized form."""
# Dates and DateTimes are not always symmetric using strftime and strptime :eyeroll: # See: # https://bugs.python.org/issue13305 # https://github.com/Flexget/Flexget/issues/2818 # https://github.com/Flexget/Flexget/issues/3304
[docs] class DateTimeSerializer(Serializer):
[docs] @classmethod def serializer_handles(cls, value): return isinstance(value, datetime.datetime)
[docs] @classmethod def serialize(cls, value: datetime.datetime): result = value.strftime(DATETIME_FMT) # See note above year, rest = result.split('-', maxsplit=1) if len(year) != 4: result = f'{value.year:04}-{rest}' return result
[docs] @classmethod def deserialize(cls, data: str, version: int) -> datetime.datetime: try: return datetime.datetime.strptime(data, DATETIME_FMT) except ValueError: logger.error('Error deserializing datetime `{}`', data) return datetime.datetime.min
[docs] class DateSerializer(Serializer):
[docs] @classmethod def serializer_handles(cls, value): return isinstance(value, datetime.date) and not isinstance(value, datetime.datetime)
[docs] @classmethod def serialize(cls, value: datetime.date): result = value.strftime(DATE_FMT) # See note above year, rest = result.split('-', maxsplit=1) if len(year) != 4: result = f'{value.year:04}-{rest}' return result
[docs] @classmethod def deserialize(cls, data: str, version: int) -> datetime.date: try: return datetime.datetime.strptime(data, DATE_FMT).date() except ValueError: logger.error('Error deserializing date `{}`', data) return datetime.date.min
[docs] class SetSerializer(Serializer):
[docs] @classmethod def serializer_handles(cls, value): return isinstance(value, set)
[docs] @classmethod def serialize(cls, value: set) -> list: return serialize(list(value))
[docs] @classmethod def deserialize(cls, data: list, version: int) -> set: return set(deserialize(data))
[docs] class TupleSerializer(Serializer):
[docs] @classmethod def serializer_handles(cls, value): return isinstance(value, tuple)
[docs] @classmethod def serialize(cls, value: tuple) -> list: return serialize(list(value))
[docs] @classmethod def deserialize(cls, data: list, version: int) -> tuple: return tuple(deserialize(data))
[docs] def _serializer_for(value) -> type[Serializer] | None: for s in Serializer.__subclasses__(): if s.serializer_handles(value): return s return None
[docs] def _deserializer_for(serializer_name: str) -> type[Serializer]: for s in Serializer.__subclasses__(): if serializer_name == s.serializer_name(): return s raise ValueError(f'No deserializer for {serializer_name}')
[docs] def _yaml_representer(dumper, data): if isinstance(data, dict) and all(k in data for k in ('value', 'serializer', 'version')): tag = f'!{data["serializer"]}.v{data["version"]}' if isinstance(data['value'], dict): return dumper.represent_mapping(tag, data['value']) if isinstance(data['value'], list): return dumper.represent_sequence(tag, data['value']) return dumper.represent_scalar(tag, data['value']) return dumper.represent_dict(data)
[docs] def _yaml_constructor(loader, node): serializer, version = node.tag.split('.v') serializer = serializer.lstrip('!') version = int(version) if node.id == 'mapping': value = loader.construct_mapping(node, deep=True) elif node.id == 'sequence': value = loader.construct_sequence(node, deep=True) else: value = loader.construct_scalar(node) return _deserializer_for(serializer).deserialize(value, version)
[docs] class FGDumper(yaml.SafeDumper): pass
FGDumper.add_representer(dict, _yaml_representer)
[docs] class FGLoader(yaml.SafeLoader): def __init__(self, stream): for s in Serializer.__subclasses__(): name = s.serializer_name() for v in range(1, s.serializer_version() + 1): self.add_constructor(f'!{name}.v{v}', _yaml_constructor) super().__init__(stream)