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
22import datetime
23from boto.sdb.db.key import Key
24from boto.utils import Password
25from boto.sdb.db.query import Query
26import re
27import boto
28import boto.s3.key
29from boto.sdb.db.blob import Blob
30from boto.compat import six, long_type
31
32
33class Property(object):
34
35    data_type = str
36    type_name = ''
37    name = ''
38    verbose_name = ''
39
40    def __init__(self, verbose_name=None, name=None, default=None,
41                 required=False, validator=None, choices=None, unique=False):
42        self.verbose_name = verbose_name
43        self.name = name
44        self.default = default
45        self.required = required
46        self.validator = validator
47        self.choices = choices
48        if self.name:
49            self.slot_name = '_' + self.name
50        else:
51            self.slot_name = '_'
52        self.unique = unique
53
54    def __get__(self, obj, objtype):
55        if obj:
56            obj.load()
57            return getattr(obj, self.slot_name)
58        else:
59            return None
60
61    def __set__(self, obj, value):
62        self.validate(value)
63
64        # Fire off any on_set functions
65        try:
66            if obj._loaded and hasattr(obj, "on_set_%s" % self.name):
67                fnc = getattr(obj, "on_set_%s" % self.name)
68                value = fnc(value)
69        except Exception:
70            boto.log.exception("Exception running on_set_%s" % self.name)
71
72        setattr(obj, self.slot_name, value)
73
74    def __property_config__(self, model_class, property_name):
75        self.model_class = model_class
76        self.name = property_name
77        self.slot_name = '_' + self.name
78
79    def default_validator(self, value):
80        if isinstance(value, six.string_types) or value == self.default_value():
81            return
82        if not isinstance(value, self.data_type):
83            raise TypeError('Validation Error, %s.%s expecting %s, got %s' % (self.model_class.__name__, self.name, self.data_type, type(value)))
84
85    def default_value(self):
86        return self.default
87
88    def validate(self, value):
89        if self.required and value is None:
90            raise ValueError('%s is a required property' % self.name)
91        if self.choices and value and value not in self.choices:
92            raise ValueError('%s not a valid choice for %s.%s' % (value, self.model_class.__name__, self.name))
93        if self.validator:
94            self.validator(value)
95        else:
96            self.default_validator(value)
97        return value
98
99    def empty(self, value):
100        return not value
101
102    def get_value_for_datastore(self, model_instance):
103        return getattr(model_instance, self.name)
104
105    def make_value_from_datastore(self, value):
106        return value
107
108    def get_choices(self):
109        if callable(self.choices):
110            return self.choices()
111        return self.choices
112
113
114def validate_string(value):
115    if value is None:
116        return
117    elif isinstance(value, six.string_types):
118        if len(value) > 1024:
119            raise ValueError('Length of value greater than maxlength')
120    else:
121        raise TypeError('Expecting String, got %s' % type(value))
122
123
124class StringProperty(Property):
125
126    type_name = 'String'
127
128    def __init__(self, verbose_name=None, name=None, default='',
129                 required=False, validator=validate_string,
130                 choices=None, unique=False):
131        super(StringProperty, self).__init__(verbose_name, name, default, required,
132                          validator, choices, unique)
133
134
135class TextProperty(Property):
136
137    type_name = 'Text'
138
139    def __init__(self, verbose_name=None, name=None, default='',
140                 required=False, validator=None, choices=None,
141                 unique=False, max_length=None):
142        super(TextProperty, self).__init__(verbose_name, name, default, required,
143                          validator, choices, unique)
144        self.max_length = max_length
145
146    def validate(self, value):
147        value = super(TextProperty, self).validate(value)
148        if not isinstance(value, six.string_types):
149            raise TypeError('Expecting Text, got %s' % type(value))
150        if self.max_length and len(value) > self.max_length:
151            raise ValueError('Length of value greater than maxlength %s' % self.max_length)
152
153
154class PasswordProperty(StringProperty):
155    """
156
157    Hashed property whose original value can not be
158    retrieved, but still can be compared.
159
160    Works by storing a hash of the original value instead
161    of the original value.  Once that's done all that
162    can be retrieved is the hash.
163
164    The comparison
165
166       obj.password == 'foo'
167
168    generates a hash of 'foo' and compares it to the
169    stored hash.
170
171    Underlying data type for hashing, storing, and comparing
172    is boto.utils.Password.  The default hash function is
173    defined there ( currently sha512 in most cases, md5
174    where sha512 is not available )
175
176    It's unlikely you'll ever need to use a different hash
177    function, but if you do, you can control the behavior
178    in one of two ways:
179
180      1) Specifying hashfunc in PasswordProperty constructor
181
182         import hashlib
183
184         class MyModel(model):
185             password = PasswordProperty(hashfunc=hashlib.sha224)
186
187      2) Subclassing Password and PasswordProperty
188
189         class SHA224Password(Password):
190             hashfunc=hashlib.sha224
191
192         class SHA224PasswordProperty(PasswordProperty):
193             data_type=MyPassword
194             type_name="MyPassword"
195
196         class MyModel(Model):
197             password = SHA224PasswordProperty()
198
199    """
200    data_type = Password
201    type_name = 'Password'
202
203    def __init__(self, verbose_name=None, name=None, default='', required=False,
204                 validator=None, choices=None, unique=False, hashfunc=None):
205
206        """
207           The hashfunc parameter overrides the default hashfunc in boto.utils.Password.
208
209           The remaining parameters are passed through to StringProperty.__init__"""
210
211        super(PasswordProperty, self).__init__(verbose_name, name, default, required,
212                                validator, choices, unique)
213        self.hashfunc = hashfunc
214
215    def make_value_from_datastore(self, value):
216        p = self.data_type(value, hashfunc=self.hashfunc)
217        return p
218
219    def get_value_for_datastore(self, model_instance):
220        value = super(PasswordProperty, self).get_value_for_datastore(model_instance)
221        if value and len(value):
222            return str(value)
223        else:
224            return None
225
226    def __set__(self, obj, value):
227        if not isinstance(value, self.data_type):
228            p = self.data_type(hashfunc=self.hashfunc)
229            p.set(value)
230            value = p
231        super(PasswordProperty, self).__set__(obj, value)
232
233    def __get__(self, obj, objtype):
234        return self.data_type(super(PasswordProperty, self).__get__(obj, objtype), hashfunc=self.hashfunc)
235
236    def validate(self, value):
237        value = super(PasswordProperty, self).validate(value)
238        if isinstance(value, self.data_type):
239            if len(value) > 1024:
240                raise ValueError('Length of value greater than maxlength')
241        else:
242            raise TypeError('Expecting %s, got %s' % (type(self.data_type), type(value)))
243
244
245class BlobProperty(Property):
246    data_type = Blob
247    type_name = "blob"
248
249    def __set__(self, obj, value):
250        if value != self.default_value():
251            if not isinstance(value, Blob):
252                oldb = self.__get__(obj, type(obj))
253                id = None
254                if oldb:
255                    id = oldb.id
256                b = Blob(value=value, id=id)
257                value = b
258        super(BlobProperty, self).__set__(obj, value)
259
260
261class S3KeyProperty(Property):
262
263    data_type = boto.s3.key.Key
264    type_name = 'S3Key'
265    validate_regex = "^s3:\/\/([^\/]*)\/(.*)$"
266
267    def __init__(self, verbose_name=None, name=None, default=None,
268                 required=False, validator=None, choices=None, unique=False):
269        super(S3KeyProperty, self).__init__(verbose_name, name, default, required,
270                          validator, choices, unique)
271
272    def validate(self, value):
273        value = super(S3KeyProperty, self).validate(value)
274        if value == self.default_value() or value == str(self.default_value()):
275            return self.default_value()
276        if isinstance(value, self.data_type):
277            return
278        match = re.match(self.validate_regex, value)
279        if match:
280            return
281        raise TypeError('Validation Error, expecting %s, got %s' % (self.data_type, type(value)))
282
283    def __get__(self, obj, objtype):
284        value = super(S3KeyProperty, self).__get__(obj, objtype)
285        if value:
286            if isinstance(value, self.data_type):
287                return value
288            match = re.match(self.validate_regex, value)
289            if match:
290                s3 = obj._manager.get_s3_connection()
291                bucket = s3.get_bucket(match.group(1), validate=False)
292                k = bucket.get_key(match.group(2))
293                if not k:
294                    k = bucket.new_key(match.group(2))
295                    k.set_contents_from_string("")
296                return k
297        else:
298            return value
299
300    def get_value_for_datastore(self, model_instance):
301        value = super(S3KeyProperty, self).get_value_for_datastore(model_instance)
302        if value:
303            return "s3://%s/%s" % (value.bucket.name, value.name)
304        else:
305            return None
306
307
308class IntegerProperty(Property):
309
310    data_type = int
311    type_name = 'Integer'
312
313    def __init__(self, verbose_name=None, name=None, default=0, required=False,
314                 validator=None, choices=None, unique=False, max=2147483647, min=-2147483648):
315        super(IntegerProperty, self).__init__(verbose_name, name, default, required, validator, choices, unique)
316        self.max = max
317        self.min = min
318
319    def validate(self, value):
320        value = int(value)
321        value = super(IntegerProperty, self).validate(value)
322        if value > self.max:
323            raise ValueError('Maximum value is %d' % self.max)
324        if value < self.min:
325            raise ValueError('Minimum value is %d' % self.min)
326        return value
327
328    def empty(self, value):
329        return value is None
330
331    def __set__(self, obj, value):
332        if value == "" or value is None:
333            value = 0
334        return super(IntegerProperty, self).__set__(obj, value)
335
336
337class LongProperty(Property):
338
339    data_type = long_type
340    type_name = 'Long'
341
342    def __init__(self, verbose_name=None, name=None, default=0, required=False,
343                 validator=None, choices=None, unique=False):
344        super(LongProperty, self).__init__(verbose_name, name, default, required, validator, choices, unique)
345
346    def validate(self, value):
347        value = long_type(value)
348        value = super(LongProperty, self).validate(value)
349        min = -9223372036854775808
350        max = 9223372036854775807
351        if value > max:
352            raise ValueError('Maximum value is %d' % max)
353        if value < min:
354            raise ValueError('Minimum value is %d' % min)
355        return value
356
357    def empty(self, value):
358        return value is None
359
360
361class BooleanProperty(Property):
362
363    data_type = bool
364    type_name = 'Boolean'
365
366    def __init__(self, verbose_name=None, name=None, default=False, required=False,
367                 validator=None, choices=None, unique=False):
368        super(BooleanProperty, self).__init__(verbose_name, name, default, required, validator, choices, unique)
369
370    def empty(self, value):
371        return value is None
372
373
374class FloatProperty(Property):
375
376    data_type = float
377    type_name = 'Float'
378
379    def __init__(self, verbose_name=None, name=None, default=0.0, required=False,
380                 validator=None, choices=None, unique=False):
381        super(FloatProperty, self).__init__(verbose_name, name, default, required, validator, choices, unique)
382
383    def validate(self, value):
384        value = float(value)
385        value = super(FloatProperty, self).validate(value)
386        return value
387
388    def empty(self, value):
389        return value is None
390
391
392class DateTimeProperty(Property):
393    """This class handles both the datetime.datetime object
394    And the datetime.date objects. It can return either one,
395    depending on the value stored in the database"""
396
397    data_type = datetime.datetime
398    type_name = 'DateTime'
399
400    def __init__(self, verbose_name=None, auto_now=False, auto_now_add=False, name=None,
401                 default=None, required=False, validator=None, choices=None, unique=False):
402        super(DateTimeProperty, self).__init__(verbose_name, name, default, required, validator, choices, unique)
403        self.auto_now = auto_now
404        self.auto_now_add = auto_now_add
405
406    def default_value(self):
407        if self.auto_now or self.auto_now_add:
408            return self.now()
409        return super(DateTimeProperty, self).default_value()
410
411    def validate(self, value):
412        if value is None:
413            return
414        if isinstance(value, datetime.date):
415            return value
416        return super(DateTimeProperty, self).validate(value)
417
418    def get_value_for_datastore(self, model_instance):
419        if self.auto_now:
420            setattr(model_instance, self.name, self.now())
421        return super(DateTimeProperty, self).get_value_for_datastore(model_instance)
422
423    def now(self):
424        return datetime.datetime.utcnow()
425
426
427class DateProperty(Property):
428
429    data_type = datetime.date
430    type_name = 'Date'
431
432    def __init__(self, verbose_name=None, auto_now=False, auto_now_add=False, name=None,
433                 default=None, required=False, validator=None, choices=None, unique=False):
434        super(DateProperty, self).__init__(verbose_name, name, default, required, validator, choices, unique)
435        self.auto_now = auto_now
436        self.auto_now_add = auto_now_add
437
438    def default_value(self):
439        if self.auto_now or self.auto_now_add:
440            return self.now()
441        return super(DateProperty, self).default_value()
442
443    def validate(self, value):
444        value = super(DateProperty, self).validate(value)
445        if value is None:
446            return
447        if not isinstance(value, self.data_type):
448            raise TypeError('Validation Error, expecting %s, got %s' % (self.data_type, type(value)))
449
450    def get_value_for_datastore(self, model_instance):
451        if self.auto_now:
452            setattr(model_instance, self.name, self.now())
453        val = super(DateProperty, self).get_value_for_datastore(model_instance)
454        if isinstance(val, datetime.datetime):
455            val = val.date()
456        return val
457
458    def now(self):
459        return datetime.date.today()
460
461
462class TimeProperty(Property):
463    data_type = datetime.time
464    type_name = 'Time'
465
466    def __init__(self, verbose_name=None, name=None,
467                 default=None, required=False, validator=None, choices=None, unique=False):
468        super(TimeProperty, self).__init__(verbose_name, name, default, required, validator, choices, unique)
469
470    def validate(self, value):
471        value = super(TimeProperty, self).validate(value)
472        if value is None:
473            return
474        if not isinstance(value, self.data_type):
475            raise TypeError('Validation Error, expecting %s, got %s' % (self.data_type, type(value)))
476
477
478class ReferenceProperty(Property):
479
480    data_type = Key
481    type_name = 'Reference'
482
483    def __init__(self, reference_class=None, collection_name=None,
484                 verbose_name=None, name=None, default=None, required=False, validator=None, choices=None, unique=False):
485        super(ReferenceProperty, self).__init__(verbose_name, name, default, required, validator, choices, unique)
486        self.reference_class = reference_class
487        self.collection_name = collection_name
488
489    def __get__(self, obj, objtype):
490        if obj:
491            value = getattr(obj, self.slot_name)
492            if value == self.default_value():
493                return value
494            # If the value is still the UUID for the referenced object, we need to create
495            # the object now that is the attribute has actually been accessed.  This lazy
496            # instantiation saves unnecessary roundtrips to SimpleDB
497            if isinstance(value, six.string_types):
498                value = self.reference_class(value)
499                setattr(obj, self.name, value)
500            return value
501
502    def __set__(self, obj, value):
503        """Don't allow this object to be associated to itself
504        This causes bad things to happen"""
505        if value is not None and (obj.id == value or (hasattr(value, "id") and obj.id == value.id)):
506            raise ValueError("Can not associate an object with itself!")
507        return super(ReferenceProperty, self).__set__(obj, value)
508
509    def __property_config__(self, model_class, property_name):
510        super(ReferenceProperty, self).__property_config__(model_class, property_name)
511        if self.collection_name is None:
512            self.collection_name = '%s_%s_set' % (model_class.__name__.lower(), self.name)
513        if hasattr(self.reference_class, self.collection_name):
514            raise ValueError('duplicate property: %s' % self.collection_name)
515        setattr(self.reference_class, self.collection_name,
516                _ReverseReferenceProperty(model_class, property_name, self.collection_name))
517
518    def check_uuid(self, value):
519        # This does a bit of hand waving to "type check" the string
520        t = value.split('-')
521        if len(t) != 5:
522            raise ValueError
523
524    def check_instance(self, value):
525        try:
526            obj_lineage = value.get_lineage()
527            cls_lineage = self.reference_class.get_lineage()
528            if obj_lineage.startswith(cls_lineage):
529                return
530            raise TypeError('%s not instance of %s' % (obj_lineage, cls_lineage))
531        except:
532            raise ValueError('%s is not a Model' % value)
533
534    def validate(self, value):
535        if self.validator:
536            self.validator(value)
537        if self.required and value is None:
538            raise ValueError('%s is a required property' % self.name)
539        if value == self.default_value():
540            return
541        if not isinstance(value, six.string_types):
542            self.check_instance(value)
543
544
545class _ReverseReferenceProperty(Property):
546    data_type = Query
547    type_name = 'query'
548
549    def __init__(self, model, prop, name):
550        self.__model = model
551        self.__property = prop
552        self.collection_name = prop
553        self.name = name
554        self.item_type = model
555
556    def __get__(self, model_instance, model_class):
557        """Fetches collection of model instances of this collection property."""
558        if model_instance is not None:
559            query = Query(self.__model)
560            if isinstance(self.__property, list):
561                props = []
562                for prop in self.__property:
563                    props.append("%s =" % prop)
564                return query.filter(props, model_instance)
565            else:
566                return query.filter(self.__property + ' =', model_instance)
567        else:
568            return self
569
570    def __set__(self, model_instance, value):
571        """Not possible to set a new collection."""
572        raise ValueError('Virtual property is read-only')
573
574
575class CalculatedProperty(Property):
576
577    def __init__(self, verbose_name=None, name=None, default=None,
578                 required=False, validator=None, choices=None,
579                 calculated_type=int, unique=False, use_method=False):
580        super(CalculatedProperty, self).__init__(verbose_name, name, default, required,
581                          validator, choices, unique)
582        self.calculated_type = calculated_type
583        self.use_method = use_method
584
585    def __get__(self, obj, objtype):
586        value = self.default_value()
587        if obj:
588            try:
589                value = getattr(obj, self.slot_name)
590                if self.use_method:
591                    value = value()
592            except AttributeError:
593                pass
594        return value
595
596    def __set__(self, obj, value):
597        """Not possible to set a new AutoID."""
598        pass
599
600    def _set_direct(self, obj, value):
601        if not self.use_method:
602            setattr(obj, self.slot_name, value)
603
604    def get_value_for_datastore(self, model_instance):
605        if self.calculated_type in [str, int, bool]:
606            value = self.__get__(model_instance, model_instance.__class__)
607            return value
608        else:
609            return None
610
611
612class ListProperty(Property):
613
614    data_type = list
615    type_name = 'List'
616
617    def __init__(self, item_type, verbose_name=None, name=None, default=None, **kwds):
618        if default is None:
619            default = []
620        self.item_type = item_type
621        super(ListProperty, self).__init__(verbose_name, name, default=default, required=True, **kwds)
622
623    def validate(self, value):
624        if self.validator:
625            self.validator(value)
626        if value is not None:
627            if not isinstance(value, list):
628                value = [value]
629
630        if self.item_type in six.integer_types:
631            item_type = six.integer_types
632        elif self.item_type in six.string_types:
633            item_type = six.string_types
634        else:
635            item_type = self.item_type
636
637        for item in value:
638            if not isinstance(item, item_type):
639                if item_type == six.integer_types:
640                    raise ValueError('Items in the %s list must all be integers.' % self.name)
641                else:
642                    raise ValueError('Items in the %s list must all be %s instances' %
643                                     (self.name, self.item_type.__name__))
644        return value
645
646    def empty(self, value):
647        return value is None
648
649    def default_value(self):
650        return list(super(ListProperty, self).default_value())
651
652    def __set__(self, obj, value):
653        """Override the set method to allow them to set the property to an instance of the item_type instead of requiring a list to be passed in"""
654        if self.item_type in six.integer_types:
655            item_type = six.integer_types
656        elif self.item_type in six.string_types:
657            item_type = six.string_types
658        else:
659            item_type = self.item_type
660        if isinstance(value, item_type):
661            value = [value]
662        elif value is None:  # Override to allow them to set this to "None" to remove everything
663            value = []
664        return super(ListProperty, self).__set__(obj, value)
665
666
667class MapProperty(Property):
668
669    data_type = dict
670    type_name = 'Map'
671
672    def __init__(self, item_type=str, verbose_name=None, name=None, default=None, **kwds):
673        if default is None:
674            default = {}
675        self.item_type = item_type
676        super(MapProperty, self).__init__(verbose_name, name, default=default, required=True, **kwds)
677
678    def validate(self, value):
679        value = super(MapProperty, self).validate(value)
680        if value is not None:
681            if not isinstance(value, dict):
682                raise ValueError('Value must of type dict')
683
684        if self.item_type in six.integer_types:
685            item_type = six.integer_types
686        elif self.item_type in six.string_types:
687            item_type = six.string_types
688        else:
689            item_type = self.item_type
690
691        for key in value:
692            if not isinstance(value[key], item_type):
693                if item_type == six.integer_types:
694                    raise ValueError('Values in the %s Map must all be integers.' % self.name)
695                else:
696                    raise ValueError('Values in the %s Map must all be %s instances' %
697                                     (self.name, self.item_type.__name__))
698        return value
699
700    def empty(self, value):
701        return value is None
702
703    def default_value(self):
704        return {}
705