import json
import inspect
from springfield.fields import Field, Empty, get_field_for_type
from springfield.alias import Alias
from springfield import fields
from anticipate.adapt import adapt, register_adapter, AdaptError
from anticipate import adapter
class EntityBase(object):
"""
An empty class that does nothing but allow us to determine
if an Entity references other Entities in the EntityMetaClass.
We can't do this with Entity directly since Entity can't exist
until EntityMetaClass is created but EntityMetaClass can't compare
against Entity since it doesn't exist yet.
"""
class EntityMetaClass(type):
def __new__(mcs, name, bases, attrs):
_fields = {}
aliases = {}
for base in bases:
if hasattr(base, '__fields__'):
_fields.update(base.__fields__)
if hasattr(base, '__aliases__'):
_fields.update(base.__aliases__)
for key, val in attrs.items():
is_cls = inspect.isclass(val)
if isinstance(val, Field):
_fields[key] = val
attrs.pop(key)
elif isinstance(val, Alias):
aliases[key] = val
attrs.pop(key)
elif is_cls and issubclass(val, Field):
_fields[key] = val()
attrs.pop(key)
elif isinstance(val, EntityBase) or (is_cls and issubclass(val, EntityBase)):
# Wrap fields assigned to `Entity`s with an `EntityField`
_fields[key] = fields.EntityField(val)
attrs.pop(key)
elif isinstance(val, list) and len(val) == 1:
attr = val[0]
is_cls = inspect.isclass(attr)
if isinstance(attr, EntityBase) or (is_cls and issubclass(attr, EntityBase)):
# Lists that contain just an Entity class are treated as
# a collection of that Entity
_fields[key] = fields.CollectionField(fields.EntityField(attr))
elif isinstance(attr, Field) or (is_cls and issubclass(attr, Field)):
# Lists that contain just a Field class are treated as
# a collection of that Field
_fields[key] = fields.CollectionField(attr)
for key, field in _fields.iteritems():
attrs[key] = field.make_descriptor(key)
for key, field in aliases.iteritems():
attrs[key] = field.make_descriptor(key)
attrs['__fields__'] = _fields
attrs['__aliases__'] = aliases
new_class = super(EntityMetaClass, mcs).__new__(mcs, name, bases, attrs)
for key, field in _fields.items():
field.init(new_class)
for key, field in aliases.items():
field.init(new_class)
return new_class
[docs]class Entity(EntityBase):
__metaclass__ = EntityMetaClass
__values__ = None
__changes__ = None
__fields__ = None
__aliases__ = None
def __init__(self, **values):
# Where the actual values are stored
object.__setattr__(self, '__values__', {})
# List of field names that have changed
object.__setattr__(self, '__changes__', set([]))
self.update(values)
[docs] def flatten(self):
"""
Get the values as basic Python types
"""
data = {}
for key, val in self.__values__.iteritems():
val = self.__fields__[key].flatten(val)
data[key] = val
return data
[docs] def jsonify(self):
"""
Return a dictionary suitable for JSON encoding.
"""
data = {}
for key, val in self.__values__.iteritems():
val = self.__fields__[key].jsonify(val)
data[key] = val
return data
[docs] def to_json(self):
"""
Convert the entity to a JSON string.
"""
return json.dumps(self.jsonify())
@classmethod
def from_json(cls, data):
return cls(**json.loads(data))
def set(self, key, value):
self.__setattr__(key, value)
[docs] def get(self, key, default=None, empty=False):
"""
Get a value by key. If passed an iterable, get a dictionary of values matching keys.
:param empty: boolean - Include empty values
"""
if isinstance(key, basestring):
return getattr(self, key, default)
else:
d = {}
for k in key:
if empty:
d[k] = getattr(self, k, default)
else:
v = self.__values__.get(k, Empty)
if v is not Empty:
d[k] = v
return d
[docs] def update(self, values):
"""
Update attibutes. Ignore keys that aren't fields.
Allows dot notation.
"""
if hasattr(values, '__values__'):
for key, val in values.__values__.items():
try:
self[key] = val
except KeyError:
pass
else:
for key, val in values.items():
try:
self[key] = val
except KeyError:
pass
def _get_field_path(self, entity, target, path=None):
"""
Use dot notation to get a field and all it's
ancestory fields.
"""
path = path or []
if '.' in target:
name, right = target.split('.', 1)
soak = False
if name.endswith('?'):
# Targets like 'child?.key' use "soak" to allow `child` to be empty
name = name[:-1]
soak = True
field = entity.__fields__[name]
key = '.'.join([f[0] for f in path] + [name])
if isinstance(field, fields.EntityField):
path.append((key, name, field, soak))
return self._get_field_path(field.type, right, path)
else:
raise KeyError('Expected EntityField for %s' % key)
else:
soak = False
if target.endswith('?'):
# Targets like 'child?.key' use "soak" to allow `child` to be empty
target = target[:-1]
soak = True
key = '.'.join([f[0] for f in path] + [target])
path.append((key, target, entity.__fields__[target], soak))
return path
def __setattr__(self, name, value):
"""
Don't allow setting attributes that haven't been defined as fields.
"""
if name in self.__fields__:
object.__setattr__(self, name, value)
else:
raise AttributeError('Field %r not defined.' % name)
# Dict interface
def __getitem__(self, name):
try:
if '.' in name:
pos = self
path = self._get_field_path(self, name)
last = path[-1]
path = path[:-1]
for field_key, field_name, field, soak in path:
if isinstance(field, fields.EntityField):
if not getattr(pos, field_name):
if soak:
return Empty
else:
raise ValueError('%s is empty' % field_key)
pos = getattr(pos, field_name)
else:
raise ValueError('Expected Entity for %s' % field_key)
# This should be the end of our path, just get it
return getattr(pos, last[1])
return getattr(self, name)
except AttributeError:
pass
raise KeyError(name)
def __setitem__(self, name, value):
try:
if '.' in name:
pos = self
path = self._get_field_path(self, name)
last = path[-1]
path = path[:-1]
for field_key, field_name, field, soak in path:
if isinstance(field, fields.EntityField):
if not getattr(pos, field_name):
# Create a new Entity instance
setattr(pos, field_name, field.type())
pos = getattr(pos, field_name)
else:
raise ValueError('Expected Entity for %s' % field_key)
# This should be the end of our path, just set it
return setattr(pos, last[1], value)
return setattr(self, name, value)
except AttributeError:
pass
raise KeyError(name)
def __delitem__(self, name):
if name in self.__fields__:
del self.__values__[name]
else:
raise KeyError('Field %r not defined.' % name)
def __contains__(self, name):
return name in self.__values__
def __len__(self):
return len(self.__values__)
def iteritems(self):
return self.__values__.iteritems()
def items(self):
return self.__values__.items()
def clear(self):
return self.__values__.clear()
def __iter__(self):
return iter(self.__values__)
@classmethod
def adapt(cls, obj):
return adapt(obj, cls)
@classmethod
def adapt_all(cls, obj):
return (adapt(i, cls) for i in obj)
def __repr__(self):
return u'<%s %s>' % (self.__class__.__name__, json.dumps(dict(((k, unicode(v)) for k, v in self.__values__.iteritems()))).replace('"', ''))
def __getstate__(self):
"""Pickle state"""
return {
'__values__' : self.__values__,
'__changes__': self.__changes__
}
def __setstate__(self, data):
"""Restore Pickle state"""
object.__setattr__(self, '__values__', data['__values__'])
object.__setattr__(self, '__changes__', data['__changes__'])
def __eq__(self, other):
return isinstance(other, self.__class__) and \
self.__values__ == other.__values__
def __neq__(self, other):
return not self.__eq__(other)
class FlexEntity(Entity):
"""
An Entity that can have extra attributes added to it.
"""
__flex_fields__ = None
def __init__(self, **values):
object.__setattr__(self, '__flex_fields__', set([]))
super(FlexEntity, self).__init__(**values)
def __setattr__(self, name, value):
if name in self.__fields__:
object.__setattr__(self, name, value)
else:
self.__values__[name] = value
self.__flex_fields__.add(name)
self.__changes__.add(name)
def __getattr__(self, name, default=None):
return self.__values__.get(name, default)
def update(self, values):
for key, val in values.iteritems():
self.set(key, val)
def _flatten_value(self, val):
"""
Have to guess at how to flatten non-fielded values
"""
if val is None:
return None
elif val is Empty:
return None
elif isinstance(val, Entity):
val = val.flatten()
elif isinstance(val, (tuple, list)) or inspect.isgenerator(val):
vals = []
for v in val:
vals.append(self._flatten_value(v))
val = vals
elif isinstance(val, dict):
data = {}
for k,v in val.iteritems():
data[k] = self._flatten_value(v)
val = data
elif not isinstance(val, (basestring, int, float, long)):
val = str(val)
return val
def _jsonify_value(self, val):
if val is None:
val = None
elif val is Empty:
val = None
elif isinstance(val, Entity):
val = val.jsonify()
elif isinstance(val, dict):
data = {}
for k,v in val.iteritems():
data[k] = self._jsonify_value(v)
val = data
elif isinstance(val, (tuple, list)) or inspect.isgenerator(val):
vals = []
for v in val:
vals.append(self._jsonify_value(v))
val = vals
else:
field = fields.get_field_for_type(val)
if field:
val = field.jsonify(val)
return val
def flatten(self):
"""
Get the values as basic Python types
"""
data = {}
for key, val in self.__values__.iteritems():
if key in self.__fields__:
val = self.__fields__[key].flatten(val)
else:
val = self._flatten_value(val)
data[key] = val
return data
def jsonify(self):
"""
Get the values as basic Python types
"""
data = {}
for key, val in self.__values__.iteritems():
if key in self.__fields__:
val = self.__fields__[key].jsonify(val)
else:
val = self._jsonify_value(val)
data[key] = val
return data
@adapter((Entity, dict), Entity)
def to_entity(obj, to_cls):
e = to_cls()
if isinstance(obj, Entity):
# obj is an Entity
e.update(obj.flatten())
return e
elif isinstance(obj, dict):
e.update(obj)
return e
raise AdaptError('to_entity could not adapt.')