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