Source code for buildingmotif.database.utils
import json
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
[docs]class JSONType(sa.types.TypeDecorator):
"""Custom JSON type that uses JSONB on Postgres and JSON on other dialects.
This allows us to use our custom JSON serialization below *and* have the
database enforce uniqueness on JSON-encoded dictionaries
"""
impl = sa.JSON
hashable = False
cache_ok = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
[docs] def load_dialect_impl(self, dialect):
if dialect.name == "postgresql":
# Use the native JSON type.
return dialect.type_descriptor(JSONB())
else:
return dialect.type_descriptor(sa.JSON())
# the custom ser/de handlers are to allow the database to ensure
# uniqueness of dependency bindings (see https://github.com/NREL/BuildingMOTIF/pull/113)
def _custom_json_serializer(obj):
"""
Serializes dictionaries as a sorted list of key-value tuples. All
other items are serialized as normal
"""
if isinstance(obj, dict):
# ensure dictionary has at least 1 pair
if len(obj) == 0:
obj[None] = None
return json.dumps(sorted([(k, v) for k, v in obj.items()]))
return json.dumps(obj)
def _custom_json_deserializer(inp):
"""
Handles deserializing the objects serialied by _custom_json_serializer
"""
obj = json.loads(inp)
# return normal object if it is not a list
if not isinstance(obj, list):
return obj
# if *all* of the items in the list are pairs,
# then we deserialize as a dictionary
if len(obj) > 0 and all(map(lambda x: isinstance(x, list) and len(x) == 2, obj)):
return dict(obj)
# ...otherwise return. It's just a normal list!
return obj