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
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']
33
35 if isinstance(value, basestring) and not value.startswith('_'):
36 return value
37 raise TypeError('id "%s" is invalid' % value)
38
40
41 - def __new__(cls, name, bases, attrs):
42
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
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
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
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
134 del properties[attr_name]
135
141
142 @classmethod
144 """ get dict of defined properties """
145 return cls._properties.copy()
146
153
159
160
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
230
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
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
257 """ add a property
258 """
259 setattr(self, key, value)
260
261
263 """ delete a property
264 """
265 try:
266 delattr(self, key)
267 except AttributeError, e:
268 raise KeyError, e
269
270
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
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
294 """ return list of items
295 """
296 return [(k, self[k]) for k in self.all_properties().keys()]
297
298
300 """ get number of properties
301 """
302 return len(self._doc or ())
303
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
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):
355
356 @classmethod
357 - def build(cls, **kwargs):
374
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
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
414 """ Set document db"""
415 cls._db = db
416
417 @classmethod
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):
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):
470
471 @classmethod
472 - def get_or_create(cls, docid=None, db=None, dynamic_properties=True, **params):
494
495 new_document = property(lambda self: self._doc.get('_rev') is None)
496
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
507 db.delete_doc(self._id)
508
509
510 del self._doc['_id']
511 del self._doc['_rev']
512
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
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
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
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
625