"""JSON Tree Library
"""
import collections
import datetime
import json
import json.scanner
import re
__version__ = [0,3,0]
__version_string__ = '.'.join(str(x) for x in __version__)
__author__ = 'Doug Napoleone'
__email__ = 'doug.napoleone+jsontree@gmail.com'
# ISO/UTC date examples:
# 2013-04-29T22:45:35.294303Z
# 2013-04-29T22:45:35.294303
# 2013-04-29 22:45:35
# 2013-04-29T22:45:35.4361-0400
# 2013-04-29T22:45:35.4361-04:00
_datetime_iso_re = re.compile(
r'^(?P<parsable>\d{4}-\d{2}-\d{2}(?P<T>[ T])\d{2}:\d{2}:\d{2}'
r'(?P<f>\.\d{1,7})?)(?P<z>[-+]\d{2}\:?\d{2})?(?P<Z>Z)?')
_date = "%Y-%m-%d"
_time = "%H:%M:%S"
_f = '.%f'
_z = '%z'
class _FixedTzOffset(datetime.tzinfo):
def __init__(self, offset_str):
hours = int(offset_str[1:3], 10)
mins = int(offset_str[-2:], 10)
if offset_str[0] == '-':
hours = -hours
mins = -mins
self.__offset = datetime.timedelta(hours=hours,
minutes=mins)
self.__dst = datetime.timedelta(hours=hours-1,
minutes=mins)
self.__name = ''
def utcoffset(self, dt):
return self.__offset
def tzname(self, dt):
return self.__name
def dst(self, dt):
return self.__dst
[docs]class jsontree(collections.defaultdict):
"""Default dictionary where keys can be accessed as attributes and
new entries recursively default to be this class. This means the following
code is valid:
>>> mytree = jsontree()
>>> mytree.something.there = 3
>>> mytree['something']['there'] == 3
True
"""
def __init__(self, *args, **kwdargs):
super(jsontree, self).__init__(jsontree, *args, **kwdargs)
def __getattribute__(self, name):
try:
return object.__getattribute__(self, name)
except AttributeError:
return self[name]
def __setattr__(self, name, value):
self[name] = value
return value
[docs]def mapped_jsontree_class(mapping):
"""Return a class which is a jsontree, but with a supplied attribute name
mapping. The mapping argument can be a mapping object
(dict, jsontree, etc.) or it can be a callable which takes a single
argument (the attribute name), and returns a new name.
This is useful in situations where you have a jsontree with keys that are
not valid python attribute names, to simplify communication with a client
library, or allow for configurable names.
For example:
>>> numjt = mapped_jsontree_class(dict(one=1, two=2, three=3, four=4))
>>> number = numjt()
>>> number.one = 'something'
>>> number
defaultdict(<class 'jsontree.mapped_jsontree'>, {1: 'something'})
This is very useful for abstracting field names that may change between
a development sandbox and production environment. Both FogBugz and Jira
bug trackers have custom fields with dynamically generated values. These
field names can be abstracted out into a configruation mapping, and the
jsontree code can be standardized.
This can also be iseful for JavaScript API's (PHPCake) which insist on
having spaces in some key names. A function can be supplied which maps
all '_'s in the attribute name to spaces:
>>> spacify = lambda name: name.replace('_', ' ')
>>> spacemapped = mapped_jsontree_class(spacify)
>>> sm = spacemapped()
>>> sm.hello_there = 5
>>> sm.hello_there
5
>>> sm.keys()
['hello there']
"""
mapper = mapping
if not callable(mapping):
if not isinstance(mapping, collections.Mapping):
raise TypeError, ("Argument mapping is not collable or an instance "
"of collections.Mapping")
mapper = lambda name: mapping.get(name, name)
class mapped_jsontree(collections.defaultdict):
def __init__(self, *args, **kwdargs):
super(mapped_jsontree, self).__init__(mapped_jsontree, *args, **kwdargs)
def __getattribute__(self, name):
mapped_name = mapper(name)
if not isinstance(mapped_name, basestring):
return self[mapped_name]
try:
return object.__getattribute__(self, mapped_name)
except AttributeError:
return self[mapped_name]
def __setattr__(self, name, value):
mapped_name = mapper(name)
self[mapped_name] = value
return value
return mapped_jsontree
[docs]def mapped_jsontree(mapping, *args, **kwdargs):
"""Helper function that calls mapped_jsontree_class, and passing the
rest of the arguments to the constructor of the new class.
>>> number = mapped_jsontree(dict(one=1, two=2, three=3, four=4),
... {1: 'something', 2: 'hello'})
>>> number.two
'hello'
>>> number.items()
[(1, 'something'), (2, 'hello')]
"""
return mapped_jsontree_class(mapping)(*args, **kwdargs)
[docs]class JSONTreeEncoder(json.JSONEncoder):
"""JSON encoder class that serializes out jsontree object structures and
datetime objects into ISO strings.
"""
def default(self, obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat()
else:
return super(JSONTreeEncoder, self).default(obj)
[docs]class JSONTreeDecoder(json.JSONDecoder):
"""JSON decoder class for deserializing to a jsontree object structure
and building datetime objects from strings with the ISO datetime format.
"""
def __init__(self, *args, **kwdargs):
jsontreecls = jsontree
if 'jsontreecls' in kwdargs:
jsontreecls = kwdargs.pop('jsontreecls')
super(JSONTreeDecoder, self).__init__(*args, **kwdargs)
self.__parse_object = self.parse_object
self.__parse_string = self.parse_string
self.parse_object = self._parse_object
self.parse_string = self._parse_string
self.scan_once = json.scanner.py_make_scanner(self)
self.jsontreecls = jsontreecls
def _parse_object(self, *args, **kwdargs):
result = self.__parse_object(*args, **kwdargs)
return self.jsontreecls(result[0]), result[1]
def _parse_string(self, *args, **kwdargs):
value, idx = self.__parse_string(*args, **kwdargs)
match = _datetime_iso_re.match(value)
if match:
gd = match.groupdict()
T = gd['T']
strptime = _date + T + _time
if gd['f']:
strptime += '.%f'
if gd['Z']:
strptime += 'Z'
try:
result = datetime.datetime.strptime(gd['parsable'], strptime)
if gd['z']:
result = result.replace(tzinfo=_FixedTzOffset(gd['z']))
return result, idx
except ValueError:
return value, idx
return value, idx
[docs]def clone(root, jsontreecls=jsontree):
"""Clone an object by first searializing out and then loading it back in.
"""
return json.loads(json.dumps(root, cls=JSONTreeEncoder),
cls=JSONTreeDecoder, jsontreecls=jsontreecls)
[docs]def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
allow_nan=True, cls=JSONTreeEncoder, indent=None, separators=None,
encoding="utf-8", default=None, sort_keys=False, **kw):
"""JSON serialize to file function that defaults the encoding class to be
JSONTreeEncoder
"""
return json.dump(obj, fp, skipkeys, ensure_ascii, check_circular,
allow_nan, cls, indent, separators, encoding, default,
sort_keys, **kw)
[docs]def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
allow_nan=True, cls=JSONTreeEncoder, indent=None, separators=None,
encoding="utf-8", default=None, sort_keys=False, **kw):
"""JSON serialize to string function that defaults the encoding class to be
JSONTreeEncoder
"""
return json.dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan,
cls, indent, separators, encoding, default, sort_keys,
**kw)
[docs]def load(fp, encoding=None, cls=JSONTreeDecoder, object_hook=None,
parse_float=None, parse_int=None, parse_constant=None,
object_pairs_hook=None, **kargs):
"""JSON load from file function that defaults the loading class to be
JSONTreeDecoder
"""
return json.load(fp, encoding, cls, object_hook,
parse_float, parse_int, parse_constant,
object_pairs_hook, **kargs)
[docs]def loads(s, encoding=None, cls=JSONTreeDecoder, object_hook=None,
parse_float=None, parse_int=None, parse_constant=None,
object_pairs_hook=None, **kargs):
"""JSON load from string function that defaults the loading class to be
JSONTreeDecoder
"""
return json.loads(s, encoding, cls, object_hook,
parse_float, parse_int, parse_constant,
object_pairs_hook, **kargs)