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