# Copyright (c) 2006,2007,2008 Mitch Garnaat http://garnaat.org/
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from boto.sdb.db.property import Property
from boto.sdb.db.key import Key
from boto.sdb.db.query import Query
import boto
from boto.compat import filter
class ModelMeta(type):
"Metaclass for all Models"
def __init__(cls, name, bases, dict):
super(ModelMeta, cls).__init__(name, bases, dict)
# Make sure this is a subclass of Model - mainly copied from django ModelBase (thanks!)
cls.__sub_classes__ = []
# Do a delayed import to prevent possible circular import errors.
from boto.sdb.db.manager import get_manager
try:
if filter(lambda b: issubclass(b, Model), bases):
for base in bases:
base.__sub_classes__.append(cls)
cls._manager = get_manager(cls)
# look for all of the Properties and set their names
for key in dict.keys():
if isinstance(dict[key], Property):
property = dict[key]
property.__property_config__(cls, key)
prop_names = []
props = cls.properties()
for prop in props:
if not prop.__class__.__name__.startswith('_'):
prop_names.append(prop.name)
setattr(cls, '_prop_names', prop_names)
except NameError:
# 'Model' isn't defined yet, meaning we're looking at our own
# Model class, defined below.
pass
class Model(object):
__metaclass__ = ModelMeta
__consistent__ = False # Consistent is set off by default
id = None
@classmethod
def get_lineage(cls):
l = [c.__name__ for c in cls.mro()]
l.reverse()
return '.'.join(l)
@classmethod
def kind(cls):
return cls.__name__
@classmethod
def _get_by_id(cls, id, manager=None):
if not manager:
manager = cls._manager
return manager.get_object(cls, id)
@classmethod
def get_by_id(cls, ids=None, parent=None):
if isinstance(ids, list):
objs = [cls._get_by_id(id) for id in ids]
return objs
else:
return cls._get_by_id(ids)
get_by_ids = get_by_id
@classmethod
def get_by_key_name(cls, key_names, parent=None):
raise NotImplementedError("Key Names are not currently supported")
@classmethod
def find(cls, limit=None, next_token=None, **params):
q = Query(cls, limit=limit, next_token=next_token)
for key, value in params.items():
q.filter('%s =' % key, value)
return q
@classmethod
def all(cls, limit=None, next_token=None):
return cls.find(limit=limit, next_token=next_token)
@classmethod
def get_or_insert(key_name, **kw):
raise NotImplementedError("get_or_insert not currently supported")
@classmethod
def properties(cls, hidden=True):
properties = []
while cls:
for key in cls.__dict__.keys():
prop = cls.__dict__[key]
if isinstance(prop, Property):
if hidden or not prop.__class__.__name__.startswith('_'):
properties.append(prop)
if len(cls.__bases__) > 0:
cls = cls.__bases__[0]
else:
cls = None
return properties
@classmethod
def find_property(cls, prop_name):
property = None
while cls:
for key in cls.__dict__.keys():
prop = cls.__dict__[key]
if isinstance(prop, Property):
if not prop.__class__.__name__.startswith('_') and prop_name == prop.name:
property = prop
if len(cls.__bases__) > 0:
cls = cls.__bases__[0]
else:
cls = None
return property
@classmethod
def get_xmlmanager(cls):
if not hasattr(cls, '_xmlmanager'):
from boto.sdb.db.manager.xmlmanager import XMLManager
cls._xmlmanager = XMLManager(cls, None, None, None,
None, None, None, None, False)
return cls._xmlmanager
@classmethod
def from_xml(cls, fp):
xmlmanager = cls.get_xmlmanager()
return xmlmanager.unmarshal_object(fp)
def __init__(self, id=None, **kw):
self._loaded = False
# first try to initialize all properties to their default values
for prop in self.properties(hidden=False):
try:
setattr(self, prop.name, prop.default_value())
except ValueError:
pass
if 'manager' in kw:
self._manager = kw['manager']
self.id = id
for key in kw:
if key != 'manager':
# We don't want any errors populating up when loading an object,
# so if it fails we just revert to it's default value
try:
setattr(self, key, kw[key])
except Exception as e:
boto.log.exception(e)
def __repr__(self):
return '%s<%s>' % (self.__class__.__name__, self.id)
def __str__(self):
return str(self.id)
def __eq__(self, other):
return other and isinstance(other, Model) and self.id == other.id
def _get_raw_item(self):
return self._manager.get_raw_item(self)
def load(self):
if self.id and not self._loaded:
self._manager.load_object(self)
def reload(self):
if self.id:
self._loaded = False
self._manager.load_object(self)
def put(self, expected_value=None):
"""
Save this object as it is, with an optional expected value
:param expected_value: Optional tuple of Attribute, and Value that
must be the same in order to save this object. If this
condition is not met, an SDBResponseError will be raised with a
Confict status code.
:type expected_value: tuple or list
:return: This object
:rtype: :class:`boto.sdb.db.model.Model`
"""
self._manager.save_object(self, expected_value)
return self
save = put
def put_attributes(self, attrs):
"""
Save just these few attributes, not the whole object
:param attrs: Attributes to save, key->value dict
:type attrs: dict
:return: self
:rtype: :class:`boto.sdb.db.model.Model`
"""
assert(isinstance(attrs, dict)), "Argument must be a dict of key->values to save"
for prop_name in attrs:
value = attrs[prop_name]
prop = self.find_property(prop_name)
assert(prop), "Property not found: %s" % prop_name
self._manager.set_property(prop, self, prop_name, value)
self.reload()
return self
def delete_attributes(self, attrs):
"""
Delete just these attributes, not the whole object.
:param attrs: Attributes to save, as a list of string names
:type attrs: list
:return: self
:rtype: :class:`boto.sdb.db.model.Model`
"""
assert(isinstance(attrs, list)), "Argument must be a list of names of keys to delete."
self._manager.domain.delete_attributes(self.id, attrs)
self.reload()
return self
save_attributes = put_attributes
def delete(self):
self._manager.delete_object(self)
def key(self):
return Key(obj=self)
def set_manager(self, manager):
self._manager = manager
def to_dict(self):
props = {}
for prop in self.properties(hidden=False):
props[prop.name] = getattr(self, prop.name)
obj = {'properties': props,
'id': self.id}
return {self.__class__.__name__: obj}
def to_xml(self, doc=None):
xmlmanager = self.get_xmlmanager()
doc = xmlmanager.marshal_object(self, doc)
return doc
@classmethod
def find_subclass(cls, name):
"""Find a subclass with a given name"""
if name == cls.__name__:
return cls
for sc in cls.__sub_classes__:
r = sc.find_subclass(name)
if r is not None:
return r
class Expando(Model):
def __setattr__(self, name, value):
if name in self._prop_names:
object.__setattr__(self, name, value)
elif name.startswith('_'):
object.__setattr__(self, name, value)
elif name == 'id':
object.__setattr__(self, name, value)
else:
self._manager.set_key_value(self, name, value)
object.__setattr__(self, name, value)
def __getattr__(self, name):
if not name.startswith('_'):
value = self._manager.get_key_value(self, name)
if value:
object.__setattr__(self, name, value)
return value
raise AttributeError