# -*- coding: utf-8 -*- """ Tagged JSON ~~~~~~~~~~~ A compact representation for lossless serialization of non-standard JSON types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this to serialize the session data, but it may be useful in other places. It can be extended to support other types. .. autoclass:: TaggedJSONSerializer :members: .. autoclass:: JSONTag :members: Let's seen an example that adds support for :class:`~collections.OrderedDict`. Dicts don't have an order in Python or JSON, so to handle this we will dump the items as a list of ``[key, value]`` pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to identify the type. The session serializer processes dicts first, so insert the new tag at the front of the order since ``OrderedDict`` must be processed before ``dict``. :: from flask.json.tag import JSONTag class TagOrderedDict(JSONTag): __slots__ = ('serializer',) key = ' od' def check(self, value): return isinstance(value, OrderedDict) def to_json(self, value): return [[k, self.serializer.tag(v)] for k, v in iteritems(value)] def to_python(self, value): return OrderedDict(value) app.session_interface.serializer.register(TagOrderedDict, index=0) :copyright: © 2010 by the Pallets team. :license: BSD, see LICENSE for more details. """ from base64 import b64decode, b64encode from datetime import datetime from uuid import UUID from jinja2 import Markup from werkzeug.http import http_date, parse_date from flask._compat import iteritems, text_type from flask.json import dumps, loads class JSONTag(object): """Base class for defining type tags for :class:`TaggedJSONSerializer`.""" __slots__ = ('serializer',) #: The tag to mark the serialized object with. If ``None``, this tag is #: only used as an intermediate step during tagging. key = None def __init__(self, serializer): """Create a tagger for the given serializer.""" self.serializer = serializer def check(self, value): """Check if the given value should be tagged by this tag.""" raise NotImplementedError def to_json(self, value): """Convert the Python object to an object that is a valid JSON type. The tag will be added later.""" raise NotImplementedError def to_python(self, value): """Convert the JSON representation back to the correct type. The tag will already be removed.""" raise NotImplementedError def tag(self, value): """Convert the value to a valid JSON type and add the tag structure around it.""" return {self.key: self.to_json(value)} class TagDict(JSONTag): """Tag for 1-item dicts whose only key matches a registered tag. Internally, the dict key is suffixed with `__`, and the suffix is removed when deserializing. """ __slots__ = () key = ' di' def check(self, value): return ( isinstance(value, dict) and len(value) == 1 and next(iter(value)) in self.serializer.tags ) def to_json(self, value): key = next(iter(value)) return {key + '__': self.serializer.tag(value[key])} def to_python(self, value): key = next(iter(value)) return {key[:-2]: value[key]} class PassDict(JSONTag): __slots__ = () def check(self, value): return isinstance(value, dict) def to_json(self, value): # JSON objects may only have string keys, so don't bother tagging the # key here. return dict((k, self.serializer.tag(v)) for k, v in iteritems(value)) tag = to_json class TagTuple(JSONTag): __slots__ = () key = ' t' def check(self, value): return isinstance(value, tuple) def to_json(self, value): return [self.serializer.tag(item) for item in value] def to_python(self, value): return tuple(value) class PassList(JSONTag): __slots__ = () def check(self, value): return isinstance(value, list) def to_json(self, value): return [self.serializer.tag(item) for item in value] tag = to_json class TagBytes(JSONTag): __slots__ = () key = ' b' def check(self, value): return isinstance(value, bytes) def to_json(self, value): return b64encode(value).decode('ascii') def to_python(self, value): return b64decode(value) class TagMarkup(JSONTag): """Serialize anything matching the :class:`~flask.Markup` API by having a ``__html__`` method to the result of that method. Always deserializes to an instance of :class:`~flask.Markup`.""" __slots__ = () key = ' m' def check(self, value): return callable(getattr(value, '__html__', None)) def to_json(self, value): return text_type(value.__html__()) def to_python(self, value): return Markup(value) class TagUUID(JSONTag): __slots__ = () key = ' u' def check(self, value): return isinstance(value, UUID) def to_json(self, value): return value.hex def to_python(self, value): return UUID(value) class TagDateTime(JSONTag): __slots__ = () key = ' d' def check(self, value): return isinstance(value, datetime) def to_json(self, value): return http_date(value) def to_python(self, value): return parse_date(value) class TaggedJSONSerializer(object): """Serializer that uses a tag system to compactly represent objects that are not JSON types. Passed as the intermediate serializer to :class:`itsdangerous.Serializer`. The following extra types are supported: * :class:`dict` * :class:`tuple` * :class:`bytes` * :class:`~flask.Markup` * :class:`~uuid.UUID` * :class:`~datetime.datetime` """ __slots__ = ('tags', 'order') #: Tag classes to bind when creating the serializer. Other tags can be #: added later using :meth:`~register`. default_tags = [ TagDict, PassDict, TagTuple, PassList, TagBytes, TagMarkup, TagUUID, TagDateTime, ] def __init__(self): self.tags = {} self.order = [] for cls in self.default_tags: self.register(cls) def register(self, tag_class, force=False, index=None): """Register a new tag with this serializer. :param tag_class: tag class to register. Will be instantiated with this serializer instance. :param force: overwrite an existing tag. If false (default), a :exc:`KeyError` is raised. :param index: index to insert the new tag in the tag order. Useful when the new tag is a special case of an existing tag. If ``None`` (default), the tag is appended to the end of the order. :raise KeyError: if the tag key is already registered and ``force`` is not true. """ tag = tag_class(self) key = tag.key if key is not None: if not force and key in self.tags: raise KeyError("Tag '{0}' is already registered.".format(key)) self.tags[key] = tag if index is None: self.order.append(tag) else: self.order.insert(index, tag) def tag(self, value): """Convert a value to a tagged representation if necessary.""" for tag in self.order: if tag.check(value): return tag.tag(value) return value def untag(self, value): """Convert a tagged representation back to the original type.""" if len(value) != 1: return value key = next(iter(value)) if key not in self.tags: return value return self.tags[key].to_python(value[key]) def dumps(self, value): """Tag the value and dump it to a compact JSON string.""" return dumps(self.tag(value), separators=(',', ':')) def loads(self, value): """Load data from a JSON string and deserialized any tagged objects.""" return loads(value, object_hook=self.untag)