summaryrefslogtreecommitdiff
path: root/requests_cache/serializers
diff options
context:
space:
mode:
authorJordan Cook <jordan.cook@pioneer.com>2021-06-22 13:05:56 -0500
committerJordan Cook <jordan.cook@pioneer.com>2021-07-03 15:44:10 -0500
commita08c39ad6b213eb379df5872de0a695348c82542 (patch)
tree6ad91cb2aaf45d4b62c89e2c33fb3b060b42ce77 /requests_cache/serializers
parentc4b9e4d4dcad5470de4a30464a6ac8a875615ad9 (diff)
downloadrequests-cache-a08c39ad6b213eb379df5872de0a695348c82542.tar.gz
Some serialization fixes & updates:
* Fix tests on python 3.6: * Make `cattrs` optional again * Don't run tests for serializers with missing optional dependencies * Show any skipped tests in pytest output * Fix redirect serialization for backends that serialize all values (DynamoDB and Redis) * Otherwise, the redirect value (which is just another key) will get converted into a `CachedResponse` * Make `pickle` serializer use `cattrs` if installed * Make `bson` serializer compatible with both `pymongo` version and standalone `bson` library * Split up `CattrStage` and preconf converters into separate modules * Turn preconf converters into `Stage` objects * Add `DeprecationWarning` for previous method of using `itsdangerous`, now that there's a better way to initialize it via `SerializerPipeline` * Remove `suppress_warnings` kwarg that's now unused * Make `SerializerPipeline`, `Stage`, and `CattrStage` importable from top-level package (`from requests_cache import ...`) * Add some more details to docs and docstrings
Diffstat (limited to 'requests_cache/serializers')
-rw-r--r--requests_cache/serializers/__init__.py93
-rw-r--r--requests_cache/serializers/cattrs.py74
-rw-r--r--requests_cache/serializers/pipeline.py5
-rw-r--r--requests_cache/serializers/preconf.py140
4 files changed, 183 insertions, 129 deletions
diff --git a/requests_cache/serializers/__init__.py b/requests_cache/serializers/__init__.py
index ebf5d15..0c07a77 100644
--- a/requests_cache/serializers/__init__.py
+++ b/requests_cache/serializers/__init__.py
@@ -1,72 +1,49 @@
+# flake8: noqa: F401
import pickle
+from warnings import warn
from .. import get_placeholder_class
-from . import preconf
from .pipeline import SerializerPipeline, Stage
-pickle_serializer = pickle
-
-try:
- from itsdangerous import Signer
-
- def safe_pickle_serializer(secret_key=None, salt="requests-cache", **kwargs):
- return SerializerPipeline(
- [
- pickle_serializer,
- Stage(Signer(secret_key=secret_key, salt=salt), dumps='sign', loads='unsign'),
- ],
- )
-
-
-except ImportError as e:
- safe_pickle_serializer = get_placeholder_class(e)
-
-
-try:
- import ujson as json
-
- json_serializer = SerializerPipeline(
- [
- preconf.ujson_converter, # CachedResponse -> JSON
- json, # JSON -> str
- ],
- )
-except ImportError:
- import json
-
- json_serializer = SerializerPipeline(
- [
- preconf.json_converter, # CachedResponse -> JSON
- json, # JSON -> str
- ],
- )
-
+# If cattrs isn't installed, use plain pickle for pickle_serializer, and placeholders for the rest.
+# Additional checks for format-specific optional libraries are handled in the preconf module.
try:
- import bson.json_util
-
- bson_serializer = SerializerPipeline(
- [
- preconf.bson_converter, # CachedResponse -> BSON
- bson.json_util, # BSON -> str
- ],
+ from .cattrs import CattrStage
+ from .preconf import (
+ bson_serializer,
+ json_serializer,
+ pickle_serializer,
+ safe_pickle_serializer,
+ yaml_serializer,
)
except ImportError as e:
+ CattrStage = get_placeholder_class(e)
bson_serializer = get_placeholder_class(e)
-
-try:
- import yaml
-
- yaml_serializer = SerializerPipeline(
- [preconf.pyyaml_converter, Stage(yaml, loads='load', dumps='dump')]
- )
-except ImportError as e:
+ json_serializer = get_placeholder_class(e)
+ pickle_serializer = pickle
+ safe_pickle_serializer = get_placeholder_class(e)
yaml_serializer = get_placeholder_class(e)
SERIALIZERS = {
- "pickle": pickle_serializer,
- "safe_pickle": safe_pickle_serializer,
- "json": json_serializer,
- "bson": bson_serializer,
- "yaml": yaml_serializer,
+ 'bson': bson_serializer,
+ 'json': json_serializer,
+ 'pickle': pickle_serializer,
+ 'yaml': yaml_serializer,
}
+
+
+def init_serializer(serializer=None, **kwargs):
+ """Initialize a serializer from a name, class, or instance"""
+ serializer = serializer or 'pickle'
+ # Backwards=compatibility with 0.6; will be removed in 0.8
+ if serializer == 'safe_pickle' or (serializer == 'pickle' and 'secret_key' in kwargs):
+ serializer = safe_pickle_serializer(**kwargs)
+ msg = (
+ 'Please initialize with safe_pickle_serializer(secret_key) instead. '
+ 'This usage is deprecated and will be removed in a future version.'
+ )
+ warn(DeprecationWarning(msg))
+ elif isinstance(serializer, str):
+ serializer = SERIALIZERS[serializer]
+ return serializer
diff --git a/requests_cache/serializers/cattrs.py b/requests_cache/serializers/cattrs.py
new file mode 100644
index 0000000..5aa5951
--- /dev/null
+++ b/requests_cache/serializers/cattrs.py
@@ -0,0 +1,74 @@
+from datetime import datetime, timedelta
+from typing import Callable, Dict, ForwardRef, MutableMapping
+
+from cattr import GenConverter
+from requests.cookies import RequestsCookieJar, cookiejar_from_dict
+from requests.structures import CaseInsensitiveDict
+from urllib3.response import HTTPHeaderDict
+
+from ..models import CachedResponse
+from .pipeline import Stage
+
+
+class CattrStage(Stage):
+ """Base serializer class for :py:class:`.CachedResponse` that does pre/post-processing with
+ ``cattrs``. This does the majority of the work needed for any other serialization format,
+ breaking down objects into python builtin types.
+
+ This can be used as a stage within a :py:class:`.SerializerPipeline`. Requires python 3.7+.
+ """
+
+ def __init__(self, factory: Callable[..., GenConverter] = None):
+ self.converter = init_converter(factory)
+
+ def dumps(self, value: CachedResponse) -> Dict:
+ if not isinstance(value, CachedResponse):
+ return value
+ return self.converter.unstructure(value)
+
+ def loads(self, value: Dict) -> CachedResponse:
+ if not isinstance(value, MutableMapping):
+ return value
+ return self.converter.structure(value, cl=CachedResponse)
+
+
+def init_converter(factory: Callable[..., GenConverter] = None):
+ """Make a converter to structure and unstructure nested objects within a :py:class:`.CachedResponse`"""
+ factory = factory or GenConverter
+ converter = factory(omit_if_default=True)
+
+ # Convert datetimes to and from iso-formatted strings
+ converter.register_unstructure_hook(datetime, lambda obj: obj.isoformat() if obj else None)
+ converter.register_structure_hook(datetime, to_datetime)
+
+ # Convert timedeltas to and from float values in seconds
+ converter.register_unstructure_hook(timedelta, lambda obj: obj.total_seconds() if obj else None)
+ converter.register_structure_hook(timedelta, to_timedelta)
+
+ # Convert dict-like objects to and from plain dicts
+ converter.register_unstructure_hook(RequestsCookieJar, lambda obj: dict(obj.items()))
+ converter.register_structure_hook(RequestsCookieJar, lambda obj, cls: cookiejar_from_dict(obj))
+ converter.register_unstructure_hook(CaseInsensitiveDict, dict)
+ converter.register_structure_hook(CaseInsensitiveDict, lambda obj, cls: CaseInsensitiveDict(obj))
+ converter.register_unstructure_hook(HTTPHeaderDict, dict)
+ converter.register_structure_hook(HTTPHeaderDict, lambda obj, cls: HTTPHeaderDict(obj))
+
+ # Tell cattrs that a 'CachedResponse' forward ref is equivalent to the CachedResponse class
+ converter.register_structure_hook(
+ ForwardRef('CachedResponse'),
+ lambda obj, cls: converter.structure(obj, CachedResponse),
+ )
+
+ return converter
+
+
+def to_datetime(obj, cls) -> datetime:
+ if isinstance(obj, str):
+ obj = datetime.fromisoformat(obj)
+ return obj
+
+
+def to_timedelta(obj, cls) -> timedelta:
+ if isinstance(obj, (int, float)):
+ obj = timedelta(seconds=obj)
+ return obj
diff --git a/requests_cache/serializers/pipeline.py b/requests_cache/serializers/pipeline.py
index 9cd2fcf..e16799f 100644
--- a/requests_cache/serializers/pipeline.py
+++ b/requests_cache/serializers/pipeline.py
@@ -4,8 +4,7 @@ from ..models import CachedResponse
class Stage:
- # Generic utility class for aliasing dumps and loads to
- # other methods
+ """Generic class to wrap serialization steps with consistent ``dumps()`` and ``loads()`` methods"""
def __init__(self, obj: Any, dumps: str = "dumps", loads: str = "loads"):
self.obj = obj
@@ -14,6 +13,8 @@ class Stage:
class SerializerPipeline:
+ """A sequence of steps used to serialize and deserialize response objects"""
+
def __init__(self, steps: List):
self.steps = steps
self.dump_steps = [step.dumps for step in steps]
diff --git a/requests_cache/serializers/preconf.py b/requests_cache/serializers/preconf.py
index b044f27..8a15902 100644
--- a/requests_cache/serializers/preconf.py
+++ b/requests_cache/serializers/preconf.py
@@ -1,81 +1,83 @@
-import datetime
-from functools import partial
+"""The ``cattrs`` library includes a number of pre-configured converters that perform some
+additional steps required for specific serialization formats.
-from requests.cookies import RequestsCookieJar, cookiejar_from_dict
-from requests.structures import CaseInsensitiveDict
-from urllib3.response import HTTPHeaderDict
+This module wraps those converters as serializer :py:class:`.Stage` objects. These are then used as
+a stage in a :py:class:`.SerializerPipeline`, which runs after the base converter and before the
+format's ``dumps()`` (or equivalent) method.
+
+For any optional libraries that aren't installed, the corresponding serializer will be a placeholder
+class that raises an ``ImportError`` at initialization time instead of at import time.
+
+Requires python 3.7+.
+"""
+import pickle
+
+from cattr.preconf import bson, json, msgpack, orjson, pyyaml, tomlkit, ujson
from .. import get_placeholder_class
-from ..models import CachedResponse
-from .pipeline import Stage
+from .cattrs import CattrStage
+from .pipeline import SerializerPipeline, Stage
-try:
- from typing import ForwardRef
+base_stage = CattrStage()
+bson_preconf_stage = CattrStage(bson.make_converter)
+json_preconf_stage = CattrStage(json.make_converter)
+msgpack_preconf_stage = CattrStage(msgpack.make_converter)
+orjson_preconf_stage = CattrStage(orjson.make_converter)
+yaml_preconf_stage = CattrStage(pyyaml.make_converter)
+toml_preconf_stage = CattrStage(tomlkit.make_converter)
+ujson_preconf_stage = CattrStage(ujson.make_converter)
- from cattr import GenConverter
- from cattr.preconf import bson, json, msgpack, orjson, pyyaml, tomlkit, ujson
- def to_datetime(obj, cls) -> datetime:
- if isinstance(obj, str):
- obj = datetime.fromisoformat(obj)
- return obj
+# Pickle serializer that uses the cattrs base converter
+pickle_serializer = SerializerPipeline([base_stage, pickle])
- def to_timedelta(obj, cls) -> datetime.timedelta:
- if isinstance(obj, (int, float)):
- obj = datetime.timedelta(seconds=obj)
- return obj
+# Pickle serializer with an additional stage using itsdangerous
+try:
+ from itsdangerous import Signer
- class CattrsStage(Stage):
- def __init__(self, converter, *args, **kwargs):
- super().__init__(converter, *args, **kwargs)
- self.loads = partial(converter.structure, cl=CachedResponse)
+ def signer_stage(secret_key=None, salt='requests-cache'):
+ return Stage(Signer(secret_key=secret_key, salt=salt), dumps='sign', loads='unsign')
- def init_converter(factory: GenConverter = None):
- """Make a converter to structure and unstructure some of the nested objects within a response,
- if cattrs is installed.
+ def safe_pickle_serializer(secret_key=None, salt='requests-cache', **kwargs):
+ """Create a serializer that uses ``itsdangerous`` to add a signature to responses during
+ writes, and validate that signature with a secret key during reads.
"""
- converter = factory(omit_if_default=True)
-
- # Convert datetimes to and from iso-formatted strings
- converter.register_unstructure_hook(datetime, lambda obj: obj.isoformat() if obj else None)
- converter.register_structure_hook(datetime, to_datetime)
-
- # Convert timedeltas to and from float values in seconds
- converter.register_unstructure_hook(
- datetime.timedelta, lambda obj: obj.total_seconds() if obj else None
- )
- converter.register_structure_hook(datetime.timedelta, to_timedelta)
-
- # Convert dict-like objects to and from plain dicts
- converter.register_unstructure_hook(RequestsCookieJar, lambda obj: dict(obj.items()))
- converter.register_structure_hook(RequestsCookieJar, lambda obj, cls: cookiejar_from_dict(obj))
- converter.register_unstructure_hook(CaseInsensitiveDict, dict)
- converter.register_structure_hook(CaseInsensitiveDict, lambda obj, cls: CaseInsensitiveDict(obj))
- converter.register_unstructure_hook(HTTPHeaderDict, dict)
- converter.register_structure_hook(HTTPHeaderDict, lambda obj, cls: HTTPHeaderDict(obj))
-
- # Tell cattrs that a 'CachedResponse' forward ref is equivalent to the CachedResponse class
- converter.register_structure_hook(
- ForwardRef('CachedResponse'),
- lambda obj, cls: converter.structure(obj, CachedResponse),
- )
- converter = CattrsStage(converter, dumps='unstructure', loads='structure')
-
- return converter
-
- bson_converter = init_converter(bson.make_converter)
- json_converter = init_converter(json.make_converter)
- msgpack_converter = init_converter(msgpack.make_converter)
- orjson_converter = init_converter(orjson.make_converter)
- pyyaml_converter = init_converter(pyyaml.make_converter)
- tomlkit_converter = init_converter(tomlkit.make_converter)
- ujson_converter = init_converter(ujson.make_converter)
+ return SerializerPipeline([base_stage, pickle, signer_stage(secret_key, salt)])
+
+
+except ImportError as e:
+ signer_stage = get_placeholder_class(e)
+ safe_pickle_serializer = get_placeholder_class(e)
+
+# BSON serializer using either PyMongo's bson.json_util if installed, otherwise standalone bson codec
+try:
+ try:
+ from bson import json_util as bson
+ except ImportError:
+ import bson
+
+ bson_serializer = SerializerPipeline([bson_preconf_stage, bson])
+except ImportError as e:
+ bson_serializer = get_placeholder_class(e)
+
+# JSON serailizer using ultrajson if installed, otherwise stdlib json
+try:
+ import ujson as json
+
+ converter = ujson_preconf_stage
+except ImportError:
+ import json
+
+ converter = json_preconf_stage
+
+json_serializer = SerializerPipeline([converter, json])
+
+# YAML serializer using pyyaml
+try:
+ import yaml
+ yaml_serializer = SerializerPipeline(
+ [yaml_preconf_stage, Stage(yaml, loads='safe_load', dumps='safe_dump')]
+ )
except ImportError as e:
- bson_converter = get_placeholder_class(e)
- json_converter = get_placeholder_class(e)
- msgpack_converter = get_placeholder_class(e)
- orjson_converter = get_placeholder_class(e)
- pyyaml_converter = get_placeholder_class(e)
- tomlkit_converter = get_placeholder_class(e)
- ujson_converter = get_placeholder_class(e)
+ yaml_serializer = get_placeholder_class(e)