"""A setup so you can have many, powerful to_dict() implementations.
Our main objective is to **remove any JSON concerns from model classes**.
These should NOT contain ``as_dict()`` implementations because the system
may need multiple converters for the same model -- for instance,
with varying fields, verbosities, levels of detail,
optional inclusion of related entities etc.
**It is bad if AVerySpecificModel.as_dict() needs a different signature**
to accept a parameter that all the other as_dict() methods do not need.
Here is the solution:
By using `Reg <http://reg.readthedocs.io/en/latest/>`_ we now have a
``to_dict(obj, flavor='', **kw)`` function that dispatches
on the type of ``obj`` and ``flavor``. This allows you to register
more than one implementation (with a "flavor" name) for each of your
model classes.
In order to understand this, you need to know what Reg does:
multiple dispatch.
Our to_dict() function also has a default implementation, so it can be used
directly.
This way you can create a situation in which::
to_dict(Address, flavor="") # calls one implementation, and
to_dict(Person, flavor="") # calls another implementation, and
to_dict(Person, flavor="table") # calls yet another implementation.
If the user code omits the *flavor* argument, the default one -- whose
value is an empty string -- gets used.
And because our ``to_dict()`` accepts keyword arguments, you can create
a very powerful version of it, that can take such arguments as
a repository, a user object, the current date etc.
Please see a usage example in
`our tests <https://github.com/nandoflorestan/kerno/blob/master/tests/test_web_to_dict.py>`_.
"""
from collections import OrderedDict
from datetime import date, datetime
from decimal import Decimal
from typing import Any, Iterable, Sequence
from warnings import warn
import reg
warn(
"to_dict will be removed in a future version of kerno. "
"Use jsonright instead.",
DeprecationWarning,
)
[docs]def keys_from(obj: Any) -> Iterable:
"""Return the names of the instance variables of ``obj``."""
return obj.__dict__.keys()
[docs]def only_relevant(keys: Iterable[str]) -> Iterable[str]:
"""Ignore strings that start in dunder ("__") or in "_sa_".
These are usually keeping SQLAlchemy state.
"""
return filter(
lambda key: not key.startswith("__") and not key.startswith("_sa_"),
keys,
)
[docs]def excluding(blacklist: Sequence, keys: Iterable) -> Iterable: # noqa
return filter(lambda k: k not in blacklist, keys)
[docs]def reuse_dict(
obj: Any,
keys: Iterable = (),
for_json: bool = True,
sort: bool = True,
**kw,
) -> OrderedDict:
"""Dump the instance variables of ``obj`` into an OrderedDict.
This function is reusable and free of Reg dispatch.
If the ``for_json`` flag is True, convert certain types.
If the ``sort`` flag is True, sort the OrderedDict.
"""
amap: OrderedDict[str, Any] = OrderedDict()
kk = keys or excluding(("password",), only_relevant(keys_from(obj)))
if sort:
kk = sorted(kk)
for key in kk:
val = getattr(obj, key)
# TODO Probably should treat Python types elsewhere!
if for_json and (isinstance(val, datetime) or isinstance(val, date)):
amap[key] = val.isoformat()
elif for_json and isinstance(val, Decimal):
amap[key] = float(str(val))
elif for_json and not isinstance(
val, (str, int, float, list, dict, bool, type(None))
):
continue
else:
amap[key] = val
return amap
[docs]@reg.dispatch( # Dispatch on type of *obj* and value of *flavor*.
reg.match_instance("obj"),
reg.match_key("flavor", lambda obj, flavor, **kw: flavor),
)
# Cannot type-annotate this function, Reg 0.11 does not support it
def to_dict(obj, flavor="", **kw):
"""Overloadable version of our function ``reuse_dict``.
You can register your own implementations depending on *obj* and *flavor*.
"""
return reuse_dict(obj, **kw)
"""
Ideas
-----
- Use a JSON serializer without tree traversal, but support Python data types such as datetime
- convert datetime to {"_T": "Date", "val": "..."}... NO. Convert to string and the client must know that such and such attributes must be converted to Date. Maybe provide the mapping (or even JS code) as another string?
- provide SQLAlchemy converter based on mine -- No, because https://pypi.python.org/pypi/colander_jsonschema/
- Convert trees of objects
- optionally provide "_typ" in output... but better as a property of a collection?
Other JSON related projects
---------------------------
jsonplus has "exact" and "compat" serializers:
https://pypi.python.org/pypi/jsonplus/0.8.0
morejson converts to and from stdlib types:
https://pypi.python.org/pypi/morejson/1.1.5
https://pypi.python.org/pypi/jsontyping/1.0.3
https://github.com/atsuoishimoto/emitjson
https://pypi.python.org/pypi/hgijson/3.1.0
Annoying declarative API, but achieves a lot:
http://hgi-json.readthedocs.io/en/latest/functionality/
https://pypi.python.org/pypi/json_tricks/3.11.3
Outside my scope and purpose, but probably great
------------------------------------------------
jsonpickle does arbitrary trees (which we don't really want).
https://pypi.python.org/pypi/jsonpickle/
https://pypi.python.org/pypi/json-model/1.0.1
https://pypi.python.org/pypi/json-delta/2.0
https://pypi.python.org/pypi/json-merger/0.4.0
"""