1# Copyright (c) 2006,2007,2008 Mitch Garnaat http://garnaat.org/ 2# 3# Permission is hereby granted, free of charge, to any person obtaining a 4# copy of this software and associated documentation files (the 5# "Software"), to deal in the Software without restriction, including 6# without limitation the rights to use, copy, modify, merge, publish, dis- 7# tribute, sublicense, and/or sell copies of the Software, and to permit 8# persons to whom the Software is furnished to do so, subject to the fol- 9# lowing conditions: 10# 11# The above copyright notice and this permission notice shall be included 12# in all copies or substantial portions of the Software. 13# 14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- 16# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 17# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20# IN THE SOFTWARE. 21 22from boto.sdb.db.property import Property 23from boto.sdb.db.key import Key 24from boto.sdb.db.query import Query 25import boto 26from boto.compat import filter 27 28class ModelMeta(type): 29 "Metaclass for all Models" 30 31 def __init__(cls, name, bases, dict): 32 super(ModelMeta, cls).__init__(name, bases, dict) 33 # Make sure this is a subclass of Model - mainly copied from django ModelBase (thanks!) 34 cls.__sub_classes__ = [] 35 36 # Do a delayed import to prevent possible circular import errors. 37 from boto.sdb.db.manager import get_manager 38 39 try: 40 if filter(lambda b: issubclass(b, Model), bases): 41 for base in bases: 42 base.__sub_classes__.append(cls) 43 cls._manager = get_manager(cls) 44 # look for all of the Properties and set their names 45 for key in dict.keys(): 46 if isinstance(dict[key], Property): 47 property = dict[key] 48 property.__property_config__(cls, key) 49 prop_names = [] 50 props = cls.properties() 51 for prop in props: 52 if not prop.__class__.__name__.startswith('_'): 53 prop_names.append(prop.name) 54 setattr(cls, '_prop_names', prop_names) 55 except NameError: 56 # 'Model' isn't defined yet, meaning we're looking at our own 57 # Model class, defined below. 58 pass 59 60class Model(object): 61 __metaclass__ = ModelMeta 62 __consistent__ = False # Consistent is set off by default 63 id = None 64 65 @classmethod 66 def get_lineage(cls): 67 l = [c.__name__ for c in cls.mro()] 68 l.reverse() 69 return '.'.join(l) 70 71 @classmethod 72 def kind(cls): 73 return cls.__name__ 74 75 @classmethod 76 def _get_by_id(cls, id, manager=None): 77 if not manager: 78 manager = cls._manager 79 return manager.get_object(cls, id) 80 81 @classmethod 82 def get_by_id(cls, ids=None, parent=None): 83 if isinstance(ids, list): 84 objs = [cls._get_by_id(id) for id in ids] 85 return objs 86 else: 87 return cls._get_by_id(ids) 88 89 get_by_ids = get_by_id 90 91 @classmethod 92 def get_by_key_name(cls, key_names, parent=None): 93 raise NotImplementedError("Key Names are not currently supported") 94 95 @classmethod 96 def find(cls, limit=None, next_token=None, **params): 97 q = Query(cls, limit=limit, next_token=next_token) 98 for key, value in params.items(): 99 q.filter('%s =' % key, value) 100 return q 101 102 @classmethod 103 def all(cls, limit=None, next_token=None): 104 return cls.find(limit=limit, next_token=next_token) 105 106 @classmethod 107 def get_or_insert(key_name, **kw): 108 raise NotImplementedError("get_or_insert not currently supported") 109 110 @classmethod 111 def properties(cls, hidden=True): 112 properties = [] 113 while cls: 114 for key in cls.__dict__.keys(): 115 prop = cls.__dict__[key] 116 if isinstance(prop, Property): 117 if hidden or not prop.__class__.__name__.startswith('_'): 118 properties.append(prop) 119 if len(cls.__bases__) > 0: 120 cls = cls.__bases__[0] 121 else: 122 cls = None 123 return properties 124 125 @classmethod 126 def find_property(cls, prop_name): 127 property = None 128 while cls: 129 for key in cls.__dict__.keys(): 130 prop = cls.__dict__[key] 131 if isinstance(prop, Property): 132 if not prop.__class__.__name__.startswith('_') and prop_name == prop.name: 133 property = prop 134 if len(cls.__bases__) > 0: 135 cls = cls.__bases__[0] 136 else: 137 cls = None 138 return property 139 140 @classmethod 141 def get_xmlmanager(cls): 142 if not hasattr(cls, '_xmlmanager'): 143 from boto.sdb.db.manager.xmlmanager import XMLManager 144 cls._xmlmanager = XMLManager(cls, None, None, None, 145 None, None, None, None, False) 146 return cls._xmlmanager 147 148 @classmethod 149 def from_xml(cls, fp): 150 xmlmanager = cls.get_xmlmanager() 151 return xmlmanager.unmarshal_object(fp) 152 153 def __init__(self, id=None, **kw): 154 self._loaded = False 155 # first try to initialize all properties to their default values 156 for prop in self.properties(hidden=False): 157 try: 158 setattr(self, prop.name, prop.default_value()) 159 except ValueError: 160 pass 161 if 'manager' in kw: 162 self._manager = kw['manager'] 163 self.id = id 164 for key in kw: 165 if key != 'manager': 166 # We don't want any errors populating up when loading an object, 167 # so if it fails we just revert to it's default value 168 try: 169 setattr(self, key, kw[key]) 170 except Exception as e: 171 boto.log.exception(e) 172 173 def __repr__(self): 174 return '%s<%s>' % (self.__class__.__name__, self.id) 175 176 def __str__(self): 177 return str(self.id) 178 179 def __eq__(self, other): 180 return other and isinstance(other, Model) and self.id == other.id 181 182 def _get_raw_item(self): 183 return self._manager.get_raw_item(self) 184 185 def load(self): 186 if self.id and not self._loaded: 187 self._manager.load_object(self) 188 189 def reload(self): 190 if self.id: 191 self._loaded = False 192 self._manager.load_object(self) 193 194 def put(self, expected_value=None): 195 """ 196 Save this object as it is, with an optional expected value 197 198 :param expected_value: Optional tuple of Attribute, and Value that 199 must be the same in order to save this object. If this 200 condition is not met, an SDBResponseError will be raised with a 201 Confict status code. 202 :type expected_value: tuple or list 203 :return: This object 204 :rtype: :class:`boto.sdb.db.model.Model` 205 """ 206 self._manager.save_object(self, expected_value) 207 return self 208 209 save = put 210 211 def put_attributes(self, attrs): 212 """ 213 Save just these few attributes, not the whole object 214 215 :param attrs: Attributes to save, key->value dict 216 :type attrs: dict 217 :return: self 218 :rtype: :class:`boto.sdb.db.model.Model` 219 """ 220 assert(isinstance(attrs, dict)), "Argument must be a dict of key->values to save" 221 for prop_name in attrs: 222 value = attrs[prop_name] 223 prop = self.find_property(prop_name) 224 assert(prop), "Property not found: %s" % prop_name 225 self._manager.set_property(prop, self, prop_name, value) 226 self.reload() 227 return self 228 229 def delete_attributes(self, attrs): 230 """ 231 Delete just these attributes, not the whole object. 232 233 :param attrs: Attributes to save, as a list of string names 234 :type attrs: list 235 :return: self 236 :rtype: :class:`boto.sdb.db.model.Model` 237 """ 238 assert(isinstance(attrs, list)), "Argument must be a list of names of keys to delete." 239 self._manager.domain.delete_attributes(self.id, attrs) 240 self.reload() 241 return self 242 243 save_attributes = put_attributes 244 245 def delete(self): 246 self._manager.delete_object(self) 247 248 def key(self): 249 return Key(obj=self) 250 251 def set_manager(self, manager): 252 self._manager = manager 253 254 def to_dict(self): 255 props = {} 256 for prop in self.properties(hidden=False): 257 props[prop.name] = getattr(self, prop.name) 258 obj = {'properties': props, 259 'id': self.id} 260 return {self.__class__.__name__: obj} 261 262 def to_xml(self, doc=None): 263 xmlmanager = self.get_xmlmanager() 264 doc = xmlmanager.marshal_object(self, doc) 265 return doc 266 267 @classmethod 268 def find_subclass(cls, name): 269 """Find a subclass with a given name""" 270 if name == cls.__name__: 271 return cls 272 for sc in cls.__sub_classes__: 273 r = sc.find_subclass(name) 274 if r is not None: 275 return r 276 277class Expando(Model): 278 279 def __setattr__(self, name, value): 280 if name in self._prop_names: 281 object.__setattr__(self, name, value) 282 elif name.startswith('_'): 283 object.__setattr__(self, name, value) 284 elif name == 'id': 285 object.__setattr__(self, name, value) 286 else: 287 self._manager.set_key_value(self, name, value) 288 object.__setattr__(self, name, value) 289 290 def __getattr__(self, name): 291 if not name.startswith('_'): 292 value = self._manager.get_key_value(self, name) 293 if value: 294 object.__setattr__(self, name, value) 295 return value 296 raise AttributeError 297