Package couchdbkit :: Package schema :: Module base
[hide private]
[frames] | no frames]

Source Code for Module couchdbkit.schema.base

  1  # -*- coding: utf-8 - 
  2  # 
  3  # This file is part of couchdbkit released under the MIT license.  
  4  # See the NOTICE for more information. 
  5   
  6  """ module that provides a Document object that allows you 
  7  to map CouchDB document in Python statically, dynamically or both 
  8  """ 
  9   
 10  import datetime 
 11  import decimal 
 12  import re 
 13  import warnings 
 14   
 15  from ..client import Database 
 16  from . import properties as p 
 17  from .properties import value_to_python, \ 
 18  convert_property, MAP_TYPES_PROPERTIES, ALLOWED_PROPERTY_TYPES, \ 
 19  LazyDict, LazyList, value_to_json 
 20  from ..exceptions import DuplicatePropertyError, ResourceNotFound, \ 
 21  ReservedWordError  
 22   
 23   
 24  __all__ = ['ReservedWordError', 'ALLOWED_PROPERTY_TYPES', 'DocumentSchema', 
 25          'SchemaProperties', 'DocumentBase', 'QueryMixin', 'AttachmentMixin', 
 26          'Document', 'StaticDocument', 'valid_id'] 
 27   
 28  _RESERVED_WORDS = ['_id', '_rev', '$schema'] 
 29   
 30  _NODOC_WORDS = ['doc_type'] 
31 32 -def check_reserved_words(attr_name):
33 if attr_name in _RESERVED_WORDS: 34 raise ReservedWordError( 35 "Cannot define property using reserved word '%(attr_name)s'." % 36 locals())
37
38 -def valid_id(value):
39 if isinstance(value, basestring) and not value.startswith('_'): 40 return value 41 raise TypeError('id "%s" is invalid' % value)
42
43 -class SchemaProperties(type):
44
45 - def __new__(cls, name, bases, attrs):
46 # init properties 47 properties = {} 48 defined = set() 49 for base in bases: 50 if hasattr(base, '_properties'): 51 property_keys = base._properties.keys() 52 duplicate_properties = defined.intersection(property_keys) 53 if duplicate_properties: 54 raise DuplicatePropertyError( 55 'Duplicate properties in base class %s already defined: %s' % (base.__name__, list(duplicate_properties))) 56 defined.update(property_keys) 57 properties.update(base._properties) 58 59 doc_type = attrs.get('doc_type', False) 60 if not doc_type: 61 doc_type = name 62 else: 63 del attrs['doc_type'] 64 65 attrs['_doc_type'] = doc_type 66 67 for attr_name, attr in attrs.items(): 68 # map properties 69 if isinstance(attr, p.Property): 70 check_reserved_words(attr_name) 71 if attr_name in defined: 72 raise DuplicatePropertyError('Duplicate property: %s' % attr_name) 73 properties[attr_name] = attr 74 attr.__property_config__(cls, attr_name) 75 # python types 76 elif type(attr) in MAP_TYPES_PROPERTIES and \ 77 not attr_name.startswith('_') and \ 78 attr_name not in _NODOC_WORDS: 79 check_reserved_words(attr_name) 80 if attr_name in defined: 81 raise DuplicatePropertyError('Duplicate property: %s' % attr_name) 82 prop = MAP_TYPES_PROPERTIES[type(attr)](default=attr) 83 properties[attr_name] = prop 84 prop.__property_config__(cls, attr_name) 85 attrs[attr_name] = prop 86 87 attrs['_properties'] = properties 88 return type.__new__(cls, name, bases, attrs)
89
90 91 -class DocumentSchema(object):
92 __metaclass__ = SchemaProperties 93 94 _dynamic_properties = None 95 _allow_dynamic_properties = True 96 _doc = None 97 _db = None 98
99 - def __init__(self, _d=None, **properties):
100 self._dynamic_properties = {} 101 self._doc = {} 102 103 if _d is not None: 104 if not isinstance(_d, dict): 105 raise TypeError('d should be a dict') 106 properties.update(_d) 107 108 doc_type = getattr(self, '_doc_type', self.__class__.__name__) 109 self._doc['doc_type'] = doc_type 110 111 for prop in self._properties.values(): 112 if prop.name in properties: 113 value = properties.pop(prop.name) 114 if value is None: 115 value = prop.default_value() 116 else: 117 value = prop.default_value() 118 prop.__property_init__(self, value) 119 self.__dict__[prop.name] = value 120 121 _dynamic_properties = properties.copy() 122 for attr_name, value in _dynamic_properties.iteritems(): 123 if attr_name not in self._properties \ 124 and value is not None: 125 if isinstance(value, p.Property): 126 value.__property_config__(self, attr_name) 127 value.__property_init__(self, value.default_value()) 128 elif isinstance(value, DocumentSchema): 129 from couchdbkit.schema import SchemaProperty 130 value = SchemaProperty(value) 131 value.__property_config__(self, attr_name) 132 value.__property_init__(self, value.default_value()) 133 134 135 setattr(self, attr_name, value) 136 # remove the kwargs to speed stuff 137 del properties[attr_name]
138
139 - def dynamic_properties(self):
140 """ get dict of dynamic properties """ 141 if self._dynamic_properties is None: 142 return {} 143 return self._dynamic_properties.copy()
144 145 @classmethod
146 - def properties(cls):
147 """ get dict of defined properties """ 148 return cls._properties.copy()
149
150 - def all_properties(self):
151 """ get all properties. 152 Generally we just need to use keys""" 153 all_properties = self._properties.copy() 154 all_properties.update(self.dynamic_properties()) 155 return all_properties
156
157 - def to_json(self):
158 if self._doc.get('doc_type') is None: 159 doc_type = getattr(self, '_doc_type', self.__class__.__name__) 160 self._doc['doc_type'] = doc_type 161 return self._doc
162 163 #TODO: add a way to maintain custom dynamic properties
164 - def __setattr__(self, key, value):
165 """ 166 override __setattr__ . If value is in dir, we just use setattr. 167 If value is not known (dynamic) we test if type and name of value 168 is supported (in ALLOWED_PROPERTY_TYPES, Property instance and not 169 start with '_') a,d add it to `_dynamic_properties` dict. If value is 170 a list or a dict we use LazyList and LazyDict to maintain in the value. 171 """ 172 173 if key == "_id" and valid_id(value): 174 self._doc['_id'] = value 175 elif key == "_deleted": 176 self._doc["_deleted"] = value 177 else: 178 check_reserved_words(key) 179 if not hasattr( self, key ) and not self._allow_dynamic_properties: 180 raise AttributeError("%s is not defined in schema (not a valid property)" % key) 181 182 elif not key.startswith('_') and \ 183 key not in self.properties() and \ 184 key not in dir(self): 185 if type(value) not in ALLOWED_PROPERTY_TYPES and \ 186 not isinstance(value, (p.Property,)): 187 raise TypeError("Document Schema cannot accept values of type '%s'." % 188 type(value).__name__) 189 190 if self._dynamic_properties is None: 191 self._dynamic_properties = {} 192 193 if isinstance(value, dict): 194 if key not in self._doc or not value: 195 self._doc[key] = {} 196 elif not isinstance(self._doc[key], dict): 197 self._doc[key] = {} 198 value = LazyDict(self._doc[key], init_vals=value) 199 elif isinstance(value, list): 200 if key not in self._doc or not value: 201 self._doc[key] = [] 202 elif not isinstance(self._doc[key], list): 203 self._doc[key] = [] 204 value = LazyList(self._doc[key], init_vals=value) 205 206 self._dynamic_properties[key] = value 207 208 if not isinstance(value, (p.Property,)) and \ 209 not isinstance(value, dict) and \ 210 not isinstance(value, list): 211 if callable(value): 212 value = value() 213 self._doc[key] = convert_property(value) 214 else: 215 object.__setattr__(self, key, value)
216
217 - def __delattr__(self, key):
218 """ delete property 219 """ 220 if key in self._doc: 221 del self._doc[key] 222 223 if self._dynamic_properties and key in self._dynamic_properties: 224 del self._dynamic_properties[key] 225 else: 226 object.__delattr__(self, key)
227
228 - def __getattr__(self, key):
229 """ get property value 230 """ 231 if self._dynamic_properties and key in self._dynamic_properties: 232 return self._dynamic_properties[key] 233 elif key in ('_id', '_rev', '_attachments', 'doc_type'): 234 return self._doc.get(key) 235 return self.__dict__[key]
236
237 - def __getitem__(self, key):
238 """ get property value 239 """ 240 try: 241 attr = getattr(self, key) 242 if callable(attr): 243 raise AttributeError 244 return attr 245 except AttributeError, e: 246 if key in self._doc: 247 return self._doc[key] 248 raise
249
250 - def __setitem__(self, key, value):
251 """ add a property 252 """ 253 setattr(self, key, value)
254 255
256 - def __delitem__(self, key):
257 """ delete a property 258 """ 259 try: 260 delattr(self, key) 261 except AttributeError, e: 262 raise KeyError, e
263 264
265 - def __contains__(self, key):
266 """ does object contain this propery ? 267 268 @param key: name of property 269 270 @return: True if key exist. 271 """ 272 if key in self.all_properties(): 273 return True 274 elif key in self._doc: 275 return True 276 return False
277
278 - def __iter__(self):
279 """ iter document instance properties 280 """ 281 for k in self.all_properties().keys(): 282 yield k, self[k] 283 raise StopIteration
284 285 iteritems = __iter__ 286
287 - def items(self):
288 """ return list of items 289 """ 290 return [(k, self[k]) for k in self.all_properties().keys()]
291 292
293 - def __len__(self):
294 """ get number of properties 295 """ 296 return len(self._doc or ())
297
298 - def __getstate__(self):
299 """ let pickle play with us """ 300 obj_dict = self.__dict__.copy() 301 return obj_dict
302 303 @classmethod
304 - def wrap(cls, data):
305 """ wrap `data` dict in object properties """ 306 instance = cls() 307 instance._doc = data 308 for prop in instance._properties.values(): 309 if prop.name in data: 310 value = data[prop.name] 311 if value is not None: 312 value = prop.to_python(value) 313 else: 314 value = prop.default_value() 315 else: 316 value = prop.default_value() 317 prop.__property_init__(instance, value) 318 319 if cls._allow_dynamic_properties: 320 for attr_name, value in data.iteritems(): 321 if attr_name in instance.properties(): 322 continue 323 if value is None: 324 continue 325 elif attr_name.startswith('_'): 326 continue 327 elif attr_name == 'doc_type': 328 continue 329 else: 330 value = value_to_python(value) 331 setattr(instance, attr_name, value) 332 return instance
333
334 - def validate(self, required=True):
335 """ validate a document """ 336 for attr_name, value in self._doc.items(): 337 if attr_name in self._properties: 338 self._properties[attr_name].validate( 339 getattr(self, attr_name), required=required) 340 return True
341
342 - def clone(self, **kwargs):
343 """ clone a document """ 344 for prop_name in self._properties.keys(): 345 try: 346 kwargs[prop_name] = self._doc[prop_name] 347 except KeyError: 348 pass 349 350 kwargs.update(self._dynamic_properties) 351 obj = self.__class__(**kwargs) 352 obj._doc = self._doc 353 354 return obj
355 356 @classmethod
357 - def build(cls, **kwargs):
358 """ build a new instance from this document object. """ 359 obj = cls() 360 properties = {} 361 for attr_name, attr in kwargs.items(): 362 if isinstance(attr, (p.Property,)): 363 properties[attr_name] = attr 364 attr.__property_config__(cls, attr_name) 365 elif type(attr) in MAP_TYPES_PROPERTIES and \ 366 not attr_name.startswith('_') and \ 367 attr_name not in _NODOC_WORDS: 368 check_reserved_words(attr_name) 369 370 prop = MAP_TYPES_PROPERTIES[type(attr)](default=attr) 371 properties[attr_name] = prop 372 prop.__property_config__(cls, attr_name) 373 attrs[attr_name] = prop 374 return type('AnonymousSchema', (cls,), properties)
375
376 -class DocumentBase(DocumentSchema):
377 """ Base Document object that map a CouchDB Document. 378 It allow you to statically map a document by 379 providing fields like you do with any ORM or 380 dynamically. Ie unknown fields are loaded as 381 object property that you can edit, datetime in 382 iso3339 format are automatically translated in 383 python types (date, time & datetime) and decimal too. 384 385 Example of documentass 386 387 .. code-block:: python 388 389 from couchdbkit.schema import * 390 class MyDocument(Document): 391 mystring = StringProperty() 392 myotherstring = unicode() # just use python types 393 394 395 Document fields can be accessed as property or 396 key of dict. These are similar : ``value = instance.key or value = instance['key'].`` 397 398 To delete a property simply do ``del instance[key'] or delattr(instance, key)`` 399 """ 400 _db = None 401
402 - def __init__(self, _d=None, **kwargs):
403 _d = _d or {} 404 405 docid = kwargs.pop('_id', _d.pop("_id", "")) 406 docrev = kwargs.pop('_rev', _d.pop("_rev", "")) 407 408 DocumentSchema.__init__(self, _d, **kwargs) 409 410 if docid: self._doc['_id'] = valid_id(docid) 411 if docrev: self._doc['_rev'] = docrev
412 413 @classmethod
414 - def set_db(cls, db):
415 """ Set document db""" 416 cls._db = db
417 418 @classmethod
419 - def get_db(cls):
420 """ get document db""" 421 db = getattr(cls, '_db', None) 422 if db is None: 423 raise TypeError("doc database required to save document") 424 return db
425
426 - def save(self, **params):
427 """ Save document in database. 428 429 @params db: couchdbkit.core.Database instance 430 """ 431 self.validate() 432 db = self.get_db() 433 434 doc = self.to_json() 435 db.save_doc(doc, **params) 436 if '_id' in doc and '_rev' in doc: 437 self._doc.update(doc) 438 elif '_id' in doc: 439 self._doc.update({'_id': doc['_id']})
440 441 store = save 442 443 @classmethod
444 - def save_docs(cls, docs, use_uuids=True, all_or_nothing=False):
445 """ Save multiple documents in database. 446 447 @params docs: list of couchdbkit.schema.Document instance 448 @param use_uuids: add _id in doc who don't have it already set. 449 @param all_or_nothing: In the case of a power failure, when the database 450 restarts either all the changes will have been saved or none of them. 451 However, it does not do conflict checking, so the documents will 452 be committed even if this creates conflicts. 453 454 """ 455 if cls._db is None: 456 raise TypeError("doc database required to save document") 457 docs_to_save= [doc._doc for doc in docs if doc._doc_type == cls._doc_type] 458 if not len(docs_to_save) == len(docs): 459 raise ValueError("one of your documents does not have the correct type") 460 cls._db.bulk_save(docs_to_save, use_uuids=use_uuids, all_or_nothing=all_or_nothing)
461 462 bulk_save = save_docs 463 464 @classmethod
465 - def get(cls, docid, rev=None, db=None, dynamic_properties=True):
466 """ get document with `docid` 467 """ 468 if not db: 469 db = cls.get_db() 470 cls._allow_dynamic_properties = dynamic_properties 471 return db.get(docid, rev=rev, wrapper=cls.wrap)
472 473 @classmethod
474 - def get_or_create(cls, docid=None, db=None, dynamic_properties=True, **params):
475 """ get or create document with `docid` """ 476 477 if db: 478 cls._db = db 479 cls._allow_dynamic_properties = dynamic_properties 480 481 if cls._db is None: 482 raise TypeError("doc database required to save document") 483 484 if docid is None: 485 obj = cls() 486 obj.save(**params) 487 return obj 488 489 rev = params.pop('rev', None) 490 491 try: 492 return cls._db.get(docid, rev=rev, wrapper=cls.wrap, **params) 493 except ResourceNotFound: 494 obj = cls() 495 obj._id = docid 496 obj.save(**params) 497 return obj
498 499 new_document = property(lambda self: self._doc.get('_rev') is None) 500
501 - def delete(self):
502 """ Delete document from the database. 503 @params db: couchdbkit.core.Database instance 504 """ 505 if self.new_document: 506 raise TypeError("the document is not saved") 507 508 db = self.get_db() 509 510 # delete doc 511 db.delete_doc(self._id) 512 513 # reinit document 514 del self._doc['_id'] 515 del self._doc['_rev']
516
517 -class AttachmentMixin(object):
518 """ 519 mixin to manage doc attachments. 520 521 """ 522
523 - def put_attachment(self, content, name=None, content_type=None, 524 content_length=None):
525 """ Add attachement to a document. 526 527 @param content: string or :obj:`File` object. 528 @param name: name or attachment (file name). 529 @param content_type: string, mimetype of attachment. 530 If you don't set it, it will be autodetected. 531 @param content_lenght: int, size of attachment. 532 533 @return: bool, True if everything was ok. 534 """ 535 db = self.get_db() 536 return db.put_attachment(self._doc, content, name=name, 537 content_type=content_type, content_length=content_length)
538
539 - def delete_attachment(self, name):
540 """ delete document attachment 541 542 @param name: name of attachment 543 544 @return: dict, with member ok set to True if delete was ok. 545 """ 546 547 db = self.get_db() 548 result = db.delete_attachment(self._doc, name) 549 try: 550 self._doc['_attachments'].pop(name) 551 except KeyError: 552 pass 553 return result
554
555 - def fetch_attachment(self, name, stream=False):
556 """ get attachment in a adocument 557 558 @param name: name of attachment default: default result 559 @param stream: boolean, response return a ResponseStream object 560 @param stream_size: int, size in bytes of response stream block 561 562 @return: str or unicode, attachment 563 """ 564 db = self.get_db() 565 return db.fetch_attachment(self._doc, name, stream=stream)
566
567 568 -class QueryMixin(object):
569 """ Mixin that add query methods """ 570 571 @classmethod
572 - def __view(cl, view_type=None, data=None, wrapper=None, 573 dynamic_properties=True, wrap_doc=True, classes=None, **params):
574 """ 575 The default wrapper can distinguish between multiple Document 576 classes and wrap the result accordingly. The known classes are 577 passed either as classes={<doc_type>: <Document-class>, ...} or 578 classes=[<Document-class1>, <Document-class2>, ...] 579 """ 580 581 def default_wrapper(row): 582 data = row.get('value') 583 docid = row.get('id') 584 doc = row.get('doc') 585 if doc is not None and wrap_doc: 586 cls = classes.get(doc.get('doc_type')) if classes else cl 587 cls._allow_dynamic_properties = dynamic_properties 588 return cls.wrap(doc) 589 590 elif not data or data is None: 591 return row 592 elif not isinstance(data, dict) or not docid: 593 return row 594 else: 595 cls = classes.get(data.get('doc_type')) if classes else cl 596 data['_id'] = docid 597 if 'rev' in data: 598 data['_rev'] = data.pop('rev') 599 cls._allow_dynamic_properties = dynamic_properties 600 return cls.wrap(data)
601 602 if isinstance(classes, list): 603 classes = dict([(c._doc_type, c) for c in classes]) 604 605 if wrapper is None: 606 wrapper = default_wrapper 607 608 if not wrapper: 609 wrapper = None 610 elif not callable(wrapper): 611 raise TypeError("wrapper is not a callable") 612 613 db = cl.get_db() 614 if view_type == 'view': 615 return db.view(data, wrapper=wrapper, **params) 616 elif view_type == 'temp_view': 617 return db.temp_view(data, wrapper=wrapper, **params) 618 else: 619 raise RuntimeError("bad view_type : %s" % view_type )
620 621 @classmethod
622 - def view(cls, view_name, wrapper=None, dynamic_properties=True, 623 wrap_doc=True, classes=None, **params):
624 """ Get documents associated view a view. 625 Results of view are automatically wrapped 626 to Document object. 627 628 @params view_name: str, name of view 629 @params wrapper: override default wrapper by your own 630 @dynamic_properties: do we handle properties which aren't in 631 the schema ? Default is True. 632 @wrap_doc: If True, if a doc is present in the row it will be 633 used for wrapping. Default is True. 634 @params params: params of view 635 636 @return: :class:`simplecouchdb.core.ViewResults` instance. All 637 results are wrapped to current document instance. 638 """ 639 return cls.__view(view_type="view", data=view_name, wrapper=wrapper, 640 dynamic_properties=dynamic_properties, wrap_doc=wrap_doc, 641 classes=classes, **params)
642 643 @classmethod
644 - def temp_view(cls, design, wrapper=None, dynamic_properties=True, 645 wrap_doc=True, classes=None, **params):
646 """ Slow view. Like in view method, 647 results are automatically wrapped to 648 Document object. 649 650 @params design: design object, See `simplecouchd.client.Database` 651 @dynamic_properties: do we handle properties which aren't in 652 the schema ? 653 @wrap_doc: If True, if a doc is present in the row it will be 654 used for wrapping. Default is True. 655 @params params: params of view 656 657 @return: Like view, return a :class:`simplecouchdb.core.ViewResults` 658 instance. All results are wrapped to current document instance. 659 """ 660 return cls.__view(view_type="temp_view", data=design, wrapper=wrapper, 661 dynamic_properties=dynamic_properties, wrap_doc=wrap_doc, 662 classes=classes, **params)
663
664 -class Document(DocumentBase, QueryMixin, AttachmentMixin):
665 """ 666 Full featured document object implementing the following : 667 668 :class:`QueryMixin` for view & temp_view that wrap results to this object 669 :class `AttachmentMixin` for attachments function 670 """
671
672 -class StaticDocument(Document):
673 """ 674 Shorthand for a document that disallow dynamic properties. 675 """ 676 _allow_dynamic_properties = False
677