1
2
3
4
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']
37
39 if isinstance(value, basestring) and not value.startswith('_'):
40 return value
41 raise TypeError('id "%s" is invalid' % value)
42
44
45 - def __new__(cls, name, bases, attrs):
46
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
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
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
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
137 del properties[attr_name]
138
144
145 @classmethod
147 """ get dict of defined properties """
148 return cls._properties.copy()
149
156
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
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
227
236
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
251 """ add a property
252 """
253 setattr(self, key, value)
254
255
257 """ delete a property
258 """
259 try:
260 delattr(self, key)
261 except AttributeError, e:
262 raise KeyError, e
263
264
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
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
288 """ return list of items
289 """
290 return [(k, self[k]) for k in self.all_properties().keys()]
291
292
294 """ get number of properties
295 """
296 return len(self._doc or ())
297
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
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):
375
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
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
415 """ Set document db"""
416 cls._db = db
417
418 @classmethod
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):
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):
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
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
511 db.delete_doc(self._id)
512
513
514 del self._doc['_id']
515 del self._doc['_rev']
516
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
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
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
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
677