PageRenderTime 55ms CodeModel.GetById 11ms app.highlight 38ms RepoModel.GetById 1ms app.codeStats 0ms

/boto-2.5.2/boto/sdb/db/property.py

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