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