1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """
23 foo
24 bar
25 """
26
27 import collections
28 import copy
29 import cPickle as pickle
30 import cStringIO
31 import datetime
32 import gzip
33 import hashlib
34 import inspect
35 import itertools
36 import os
37 import pprint
38 import re
39 import string
40 import subprocess
41 import sys
42 import tempfile
43 import time
44 import uuid
45 import weakref
46
47
48 import couchdb
49 import couchdb.client
50 import couchdb.design
51
52
53 """
54 import couchable
55 import couchdb
56 server = couchdb.Server()
57 try:
58 cdb = server['pykour']
59 except:
60 cdb = server.create('pykour')
61
62 class C(couchable.Couchable):
63 def __init__(self, **kwargs):
64 self.__dict__.update(kwargs)
65
66 c=C(foo=1,bar=2,_baz=3)
67 d=C(_c=c)
68 e=C(c=c, d=d, couchable_foo={'couchable_bar':9})
69 couchable.store(e, cdb)
70 print 'c', c._id
71 print 'd', d._id
72 print 'e', e._id
73 i=e._id
74 del c
75 del d
76 del e
77 e=couchable.load(i, cdb)
78 """
81 """
82 >>> importstr('os')
83 <module 'os' from '.../os.pyc'>
84 >>> importstr('math', 'fabs')
85 <built-in function fabs>
86 """
87 module = __import__(module_str)
88 for sub_str in module_str.split('.')[1:]:
89 module = getattr(module, sub_str)
90
91 if from_:
92 return getattr(module, from_)
93 return module
94
96 if not isinstance(type_, type):
97 type_ = type(type_)
98 if type_.__name__ in __builtins__:
99 return type_.__name__
100 else:
101 return '{}.{}'.format(type_.__module__, type_.__name__)
102
103 FIELD_NAME = 'couchable:'
107 Exception.__init__(self, msg)
108 self.cls = cls
109 self.obj = obj
110
111
112 _pack_handlers = collections.OrderedDict()
114 def func(func_):
115 for type_ in args:
116 packer(type_, func_)
117 return func_
118 return func
119
121 """
122 This function is still in potential flux. Writing new (un)packers is delicate; please see the source.
123 """
124 _pack_handlers[type_] = func_
125 _pack_handlers[typestr(type_)] = func_
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145 -def findHandler(cls_or_name, handler_dict):
146 """
147 >>> class A(object): pass
148 ...
149 >>> class B(A): pass
150 ...
151 >>> class C(object): pass
152 ...
153 >>> handlers={A:'AAA'}
154 >>> findHandler(A, handlers)
155 (<class 'couchable.core.A'>, 'AAA')
156 >>> findHandler(B, handlers)
157 (<class 'couchable.core.A'>, 'AAA')
158 >>> findHandler(C, handlers)
159 (None, None)
160 """
161
162
163
164
165
166 if cls_or_name in handler_dict:
167 return cls_or_name, handler_dict[cls_or_name]
168 else:
169 for type_, handler in reversed(handler_dict.items()):
170 if isinstance(type_, type) and issubclass(cls_or_name, type_):
171 return type_, handler
172
173 return None, None
174
176 """
177 Currently, though it is not documented here, the .db parameter is part of
178 the public API of CouchableDb; it is required for use with views, etc.
179 Please see the couchdb documentation for details:
180
181 U{http://packages.python.org/CouchDB/}
182
183 It is possible that in the future couchdbkit could also be used:
184
185 U{http://couchdbkit.org/}
186 """
187
188 _obj_by_id_cache = weakref.WeakValueDictionary()
189
190 - def __init__(self, name=None, url='http://localhost:5984/', db=None):
191 """
192 Creates a CouchableDb wrapper around a couchdb.Database object. If
193 the database does not yet exist, it will be created.
194
195 @type name: str
196 @param name: Name of the CouchDB database to connect to.
197 @type url: str
198 @param url: The URL of the CouchDB server. Uses the couchdb default of http://localhost:5984/
199 @type db: couchdb.Database
200 @param db: An instance of couchdb.Database that has already been instantiated. Overrides the name and url params.
201 """
202
203
204 cache_key = (url, name)
205 self._obj_by_id = self._obj_by_id_cache.get(cache_key, weakref.WeakValueDictionary())
206 self._obj_by_id_cache[(url, name)] = self._obj_by_id
207
208 self.name = name
209 self.url = url
210
211 if db is None:
212 self.server = couchdb.Server(self.url)
213
214 try:
215 db = self.server[self.name]
216 except:
217 db = self.server.create(self.name)
218
219 self.db = db
220
221 self._init_views()
222
224 byclass_js = '''
225 function(doc) {
226 if ('couchable:' in doc) {
227 var info = doc['couchable:'];
228 emit([info.module, info.class, doc._id], doc);
229 }
230 }'''
231
232 couchdb.design.ViewDefinition('couchable', 'byclass', byclass_js).sync(self.db)
233
234 - def addClassView(self, cls, name, keys=None, multikeys=None, value='1', reduce=None):
235 """
236 Creates a view that only emits records for documents of the specified
237 class. Each record also emits keys based on the parameters given, which
238 can be used for things like "get all Foo instances with bar between 3
239 and 7."
240
241 The view code resembles the following::
242
243 function(doc) {
244 if ('couchable:' in doc) {
245 var info = doc['couchable:'];
246 if (info.module == '$module' && info.class == '$cls') {
247 $emit
248 }
249 }
250 }
251
252 I{This behavior may change during the course of the 0.x.x series of releases.}
253
254 @type cls: type
255 @param cls: The class of objects that the view should be restricted to. Note that sub/superclasses are not considered.
256 @type name: string
257 @param name: The string to suffix the name of the view with (byclass:module.class:name).
258 @type keys: list of strings
259 @param keys: A list of unescaped javascript expressions to use as the key for the view.
260 @type multikeys: list of list of strings
261 @param multikeys: A list of keys (see above). Each key will get a separate emit.
262 @type value: string
263 @param value: A string of unescaped javascript used as the value of each emit.
264 @type reduce: string
265 @param reduce: A CouchDB reduce function. Can be None, javascript, or the built-in '_sum' kind of reduce function.
266 @rtype: str
267 @return: The full name of the view (byclass:module.class:name).
268 """
269 multikeys = multikeys or [keys]
270 emit_js = '\n'.join(['''emit([{}], {});'''.format(', '.join([('info.private.' + key if key[0] == '_' else 'doc.' + key) for key in keys]), value) for keys in multikeys])
271
272 byclass_js = '''
273 function(doc) {
274 if ('couchable:' in doc) {
275 var info = doc['couchable:'];
276 if (info.module == '$module' && info.class == '$cls') {
277 $emit
278 }
279 }
280 }'''
281
282 byclass_js = string.Template(byclass_js).safe_substitute(module=cls.__module__, cls=cls.__name__, emit=emit_js, value=value)
283
284 fullName = 'byclass:{}.{}:{}'.format(cls.__module__, cls.__name__, name)
285 couchdb.design.ViewDefinition('couchable', fullName, byclass_js, reduce).sync(self.db)
286
287 return fullName
288
289
290
291
292
294 return copy.copy(self)
295
296
297
298
299
300
302 """
303 Stores the documents in the C{what} parameter in CouchDB. If a C{._id}
304 does not yet exist on the object, it will be added. If the C{._id} is
305 present, it will be used instead. The C{._rev} of the object(s) must
306 match what is already in the database.
307
308 Any attachments for the document will also be uploaded. As of the
309 current revision (0.0.1b2), each attachment will be uploaded each time
310 the document is stored.
311
312 I{This behavior is expected to change during the course of the 0.x.x series of releases.}
313
314 Any objects referenced by the object(s) in C{what} will also be stored.
315 If those objects are L{registered as document types<registerDocType>},
316 then they will also be stored as top level objects, even if they exist
317 in the database already, and have not changed.
318
319 I{This behavior may change during the course of the 0.x.x series of releases.}
320
321 Any cycles comprised entirely of non-document classes will cause the
322 store call to raise an exception. Cycles where at least one object in
323 the cycle is to be stored as a top-level document are fine.
324
325 I{This behavior may change during the course of the 0.x.x series of releases.}
326
327 @type what: obj or list
328 @param what: The object or list of objects to store in CouchDB.
329 @rtype: str or list
330 @return: The C{._id} of the C{what} parameter, or the list of such IDs if C{what} was a list.
331 """
332 if not isinstance(what, list):
333 store_list = [what]
334 else:
335 store_list = what
336
337 self._done_dict = collections.OrderedDict()
338
339 for obj in store_list:
340 self._store(obj)
341
342
343
344
345
346 ret_list = self.db.update([x[1] for x in self._done_dict.values()])
347
348
349
350
351 for ret, store_tuple in itertools.izip(ret_list, self._done_dict.values()):
352 success, _id, _rev = ret
353 obj, doc, attachment_list = store_tuple
354 if success:
355 for content, content_name, content_type in attachment_list:
356
357 self.db.put_attachment(doc, content, content_name, content_type)
358
359
360 obj._rev = doc['_rev']
361 else:
362 raise _rev
363
364
365
366 self._obj_by_id[obj._id] = obj
367
368 del self._done_dict
369
370 if not isinstance(what, list):
371 return what._id
372 else:
373 return [obj._id for obj in store_list]
374
375
377 if isinstance(obj, (CouchableDb, couchdb.client.Server, couchdb.client.Database)):
378 raise UncouchableException("Illegal to attempt to store objects of type", type(obj), obj)
379
380 base_cls, func_tuple = findHandler(type(obj), _couchable_types)
381 if func_tuple:
382 func_tuple[0](obj, self)
383
384 if not hasattr(obj, '_id'):
385 obj._id = '{}:{}'.format(typestr(obj), uuid.uuid4()).lstrip('_')
386 assert obj._id not in self._obj_by_id
387
388 if obj._id not in self._done_dict:
389 self._done_dict[obj._id] = (obj, {}, [])
390
391 attachment_list = []
392
393
394
395
396 doc = {}
397 doc.update(self._pack_dict_keyMeansObject(doc, obj.__dict__, attachment_list, '', True))
398 doc = self._objInfo_doc(obj, doc)
399
400
401
402 self._done_dict[obj._id] = (obj, doc, attachment_list)
403
404 obj._cdb = self
405
406
407 - def _pack(self, parent_doc, data, attachment_list, name, isKey=False):
408 cls = type(data)
409
410 base_cls, handler = findHandler(cls, _pack_handlers)
411
412 return handler(self, parent_doc, data, attachment_list, name, isKey)
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
433 """
434 >>> cdb=CouchableDb('testing')
435 >>> obj = object()
436 >>> pprint.pprint(cdb._objInfo_doc(obj, {}))
437 {'couchable:': {'class': 'object', 'module': '__builtin__'}}
438 """
439 cls = type(data)
440 doc.setdefault(FIELD_NAME, {})
441 doc[FIELD_NAME]['class'] = cls.__name__
442
443 if hasattr(cls, '__module__'):
444 doc[FIELD_NAME]['module'] = str(cls.__module__)
445
446 try:
447 doc[FIELD_NAME]['src_md5'] = hashlib.md5(inspect.getsource(cls)).hexdigest()
448 except (IOError, TypeError):
449 pass
450
451 return doc
452
454 """
455 >>> cdb=CouchableDb('testing')
456 >>> obj = tuple([1, 2, 3])
457 >>> pprint.pprint(cdb._objInfo_consargs(obj, {}, list(obj), {}))
458 {'couchable:': {'args': [1, 2, 3],
459 'class': 'tuple',
460 'kwargs': {},
461 'module': '__builtin__'}}
462 """
463 doc = self._objInfo_doc(data, doc)
464 doc[FIELD_NAME]['args'] = args or []
465 doc[FIELD_NAME]['kwargs'] = kwargs or {}
466
467 return doc
468
469
470
471
472
473
474
475
476 @_packer(object)
477 - def _pack_object(self, parent_doc, data, attachment_list, name, isKey):
478 """
479 >>> cdb=CouchableDb('testing')
480 >>> parent_doc = {}
481 >>> attachment_list = []
482 >>> class Foo(object):
483 ... def __init__(self):
484 ... self.a = 'a'
485 ... self.b = u'b'
486 ... self.c = 'couchable:'
487 ... self.d = {1:2, (3,4,5):(6,7)}
488 ...
489 >>> data = Foo()
490 >>> pprint.pprint(cdb._pack_object(parent_doc, data, attachment_list, 'myname', False))
491 {'a': 'a',
492 'b': u'b',
493 'c': 'couchable:append:str:couchable:',
494 'couchable:': {'class': 'Foo', 'module': 'couchable.core'},
495 'd': {'couchable:key:tuple:(3, 4, 5)': {'couchable:': {'args': [[6, 7]],
496 'class': 'tuple',
497 'kwargs': {},
498 'module': '__builtin__'}},
499 'couchable:repr:int:1': 2}}
500 >>> pprint.pprint(parent_doc)
501 {'couchable:': {'keys': {'couchable:key:tuple:(3, 4, 5)': {'couchable:':
502 {'args': [[3, 4, 5]],
503 'class': 'tuple',
504 'kwargs': {},
505 'module': '__builtin__'}}}}}
506 """
507 cls = type(data)
508 base_cls, callback_tuple = findHandler(cls, _couchable_types)
509
510 if base_cls:
511 self._store(data)
512
513 return '{}{}:{}'.format(FIELD_NAME, 'id', data._id)
514 else:
515 if isKey:
516 key_str = '{}{}:{}:{!r}'.format(FIELD_NAME, 'key', typestr(cls), data)
517
518 parent_doc.setdefault(FIELD_NAME, {})
519 parent_doc[FIELD_NAME].setdefault('keys', {})
520 parent_doc[FIELD_NAME]['keys'][key_str] = self._pack_object(parent_doc, data, attachment_list, name, False)
521
522 return key_str
523 elif not hasattr(data, '__dict__'):
524 return self._pack_attachment(parent_doc, data, attachment_list, name, isKey)
525 else:
526
527 doc = {}
528 doc.update(self._pack_dict_keyMeansObject(parent_doc, data.__dict__, attachment_list, name, True))
529 doc = self._objInfo_doc(data, doc)
530
531 return doc
532
533 @_packer(type(os))
534 - def _pack_module(self, parent_doc, data, attachment_list, name, isKey):
535 """
536 >> import os.path
537 >>> cdb=CouchableDb('testing')
538 >>> parent_doc = {}
539 >>> attachment_list = []
540
541 >>> data = os.path
542 >>> cdb._pack_module(parent_doc, data, attachment_list, 'myname', False)
543 'couchable:module:os.path'
544 >>> cdb._pack_module(parent_doc, data, attachment_list, 'myname', True)
545 'couchable:module:os.path'
546 """
547
548 for name, module in sys.modules.items():
549 if module == data:
550 return '{}{}:{}'.format(FIELD_NAME, 'module', name)
551
552
553 @_packer(str, unicode)
554 - def _pack_native(self, parent_doc, data, attachment_list, name, isKey):
555 """
556 >>> cdb=CouchableDb('testing')
557 >>> parent_doc = {}
558 >>> attachment_list = []
559
560 >>> data = 'byte string'
561 >>> cdb._pack_native(parent_doc, data, attachment_list, 'myname', False)
562 'byte string'
563 >>> cdb._pack_native(parent_doc, data, attachment_list, 'myname', True)
564 'byte string'
565
566 >>> data = u'unicode string'
567 >>> cdb._pack_native(parent_doc, data, attachment_list, 'myname', False)
568 u'unicode string'
569 >>> cdb._pack_native(parent_doc, data, attachment_list, 'myname', True)
570 u'unicode string'
571
572 >>> data = 'couchable:must escape this'
573 >>> cdb._pack_native(parent_doc, data, attachment_list, 'myname', False)
574 'couchable:append:str:couchable:must escape this'
575 """
576
577 if data.startswith(FIELD_NAME):
578 return '{}{}:{}:{}'.format(FIELD_NAME, 'append', typestr(data), data)
579 else:
580 return data
581
582 @_packer(int, long, float, type(None))
584 """
585 >>> cdb=CouchableDb('testing')
586 >>> parent_doc = {}
587 >>> attachment_list = []
588 >>> data = 1234
589 >>> cdb._pack_native_keyAsRepr(parent_doc, data, attachment_list, 'myname', False)
590 1234
591 >>> cdb._pack_native_keyAsRepr(parent_doc, data, attachment_list, 'myname', True)
592 'couchable:repr:int:1234'
593 >>> data = 12.34
594 >>> cdb._pack_native_keyAsRepr(parent_doc, data, attachment_list, 'myname', False)
595 12.34
596 >>> cdb._pack_native_keyAsRepr(parent_doc, data, attachment_list, 'myname', True)
597 'couchable:repr:float:12.34'
598 """
599 if isKey:
600 return '{}{}:{}:{!r}'.format(FIELD_NAME, 'repr', typestr(data), data)
601 else:
602 return data
603
604 @_packer(tuple, frozenset, set)
606 """
607 >>> cdb=CouchableDb('testing')
608 >>> parent_doc = {}
609 >>> attachment_list = []
610
611 >>> data = tuple([1, 2, 3])
612 >>> pprint.pprint(cdb._pack_consargs_keyAsKey(parent_doc, data, attachment_list, 'myname', False))
613 {'couchable:':
614 {'args': [[1, 2, 3]],
615 'class': 'tuple',
616 'kwargs': {},
617 'module': '__builtin__'}}
618 >>> pprint.pprint(parent_doc)
619 {}
620 >>> pprint.pprint(cdb._pack_consargs_keyAsKey(parent_doc, data, attachment_list, 'myname', True))
621 'couchable:key:tuple:(1, 2, 3)'
622 >>> pprint.pprint(parent_doc)
623 {'couchable:': {'keys': {'couchable:key:tuple:(1, 2, 3)': {'couchable:':
624 {'args': [[1, 2, 3]],
625 'class': 'tuple',
626 'kwargs': {},
627 'module': '__builtin__'}}}}}
628
629 >>> parent_doc = {}
630 >>> data = frozenset([1, 2, 3])
631 >>> pprint.pprint(cdb._pack_consargs_keyAsKey(parent_doc, data, attachment_list, 'myname', False))
632 {'couchable:':
633 {'args': [[1, 2, 3]],
634 'class': 'frozenset',
635 'kwargs': {},
636 'module': '__builtin__'}}
637 >>> pprint.pprint(parent_doc)
638 {}
639 >>> cdb._pack_consargs_keyAsKey(parent_doc, data, attachment_list, 'myname', True)
640 'couchable:key:frozenset:frozenset([1, 2, 3])'
641 >>> pprint.pprint(parent_doc)
642 {'couchable:': {'keys': {'couchable:key:frozenset:frozenset([1, 2, 3])': {'couchable:':
643 {'args': [[1, 2, 3]],
644 'class': 'frozenset',
645 'kwargs': {},
646 'module': '__builtin__'}}}}}
647 """
648 if isKey:
649 key_str = '{}{}:{}:{!r}'.format(FIELD_NAME, 'key', typestr(data), data)
650
651 parent_doc.setdefault(FIELD_NAME, {})
652 parent_doc[FIELD_NAME].setdefault('keys', {})
653 parent_doc[FIELD_NAME]['keys'][key_str] = self._pack_consargs_keyAsKey(parent_doc, data, attachment_list, name, False)
654
655 return key_str
656 else:
657 return self._objInfo_consargs(data, {}, [self._pack_list_noKey(parent_doc, list(data), attachment_list, name, False)])
658
659 @_packer(list)
661 """
662 >>> cdb=CouchableDb('testing')
663 >>> parent_doc = {}
664 >>> attachment_list = []
665
666 >>> data = [1, 2, 3]
667 >>> cdb._pack_list_noKey(parent_doc, data, attachment_list, 'myname', False)
668 [1, 2, 3]
669
670 >>> data = [1, 2, (3, 4, 5)]
671 >>> pprint.pprint(cdb._pack_list_noKey(parent_doc, data, attachment_list, 'myname', False))
672 [1,
673 2,
674 {'couchable:': {'args': [[3, 4, 5]],
675 'class': 'tuple',
676 'kwargs': {},
677 'module': '__builtin__'}}]
678 >>> pprint.pprint(parent_doc)
679 {}
680 """
681 assert not isKey
682 return [self._pack(parent_doc, x, attachment_list, '{}[{}]'.format(name, i), False) for i, x in enumerate(data)]
683
684 @_packer(dict)
686 """
687 >>> cdb=CouchableDb('testing')
688 >>> parent_doc = {}
689 >>> attachment_list = []
690
691 >>> data = {'a': 'b', 'couchable:':'c'}
692 >>> pprint.pprint(cdb._pack_dict_keyMeansObject(parent_doc, data, attachment_list, 'myname', False))
693 {'a': 'b', 'couchable:append:str:couchable:': 'c'}
694
695 >>> data = {1:1, 2:2, 3:(3, 4, 5)}
696 >>> pprint.pprint(cdb._pack_dict_keyMeansObject(parent_doc, data, attachment_list, 'myname', False))
697 {'couchable:repr:int:1': 1,
698 'couchable:repr:int:2': 2,
699 'couchable:repr:int:3': {'couchable:': {'args': [[3, 4, 5]],
700 'class': 'tuple',
701 'kwargs': {},
702 'module': '__builtin__'}}}
703 >>> data = {(3, 4, 5):3}
704 >>> pprint.pprint(cdb._pack_dict_keyMeansObject(parent_doc, data, attachment_list, 'myname', False))
705 {'couchable:key:tuple:(3, 4, 5)': 3}
706 >>> pprint.pprint(parent_doc)
707 {'couchable:': {'keys': {'couchable:key:tuple:(3, 4, 5)': {'couchable:':
708 {'args': [[3, 4, 5]],
709 'class': 'tuple',
710 'kwargs': {},
711 'module': '__builtin__'}}}}}
712 """
713 if isObjDict:
714 private_keys = {k for k in data.keys() if k.startswith('_') and k not in ('_id', '_rev', '_attachments', '_cdb')}
715 else:
716 private_keys = set()
717
718 doc = {self._pack(parent_doc, k, attachment_list, '{}>{}'.format(name, str(k)), True):
719 self._pack(parent_doc, v, attachment_list, '{}.{}'.format(name, str(k)), False)
720 for k,v in data.items() if k not in private_keys and k not in ['_attachments', '_cdb']}
721
722
723
724 if private_keys:
725 doc.setdefault(FIELD_NAME, {})
726
727 doc[FIELD_NAME]['private'] = {self._pack(parent_doc, k, attachment_list, '{}>{}'.format(name, str(k)), True):
728 self._pack(parent_doc, v, attachment_list, '{}.{}'.format(name, str(k)), False)
729 for k,v in data.items() if k in private_keys}
730
731
732
733
734
735 return doc
736
738 cls = type(data)
739
740 base_cls, handler_tuple = findHandler(cls, _attachment_handlers)
741
742 if base_cls is None:
743 content = pickle.dumps(data)
744 attachment_list.append((content, name, 'application/octet-stream'))
745 return '{}{}:{}:{}'.format(FIELD_NAME, 'attachment', 'pickle', name)
746 else:
747 content = handler_tuple[0](data)
748 attachment_list.append((content, name, handler_tuple[2]))
749 return '{}{}:{}:{}'.format(FIELD_NAME, 'attachment', typestr(base_cls), name)
750
751 - def _unpack(self, parent_doc, doc, loaded_dict, inst=None):
752
753 if isinstance(doc, (str, unicode)):
754 if doc.startswith(FIELD_NAME):
755 _, method_str, data = doc.split(':', 2)
756
757 if method_str == 'id':
758 return self._load(data, loaded_dict)
759
760 elif method_str == 'module':
761 return importstr(data)
762
763 type_str, data = data.split(':', 1)
764 if method_str == 'append':
765 if type_str == 'unicode':
766 return unicode(data, 'utf8')
767 if type_str == 'str':
768 return data
769
770 elif method_str == 'repr':
771 if type_str in __builtins__:
772 return __builtins__.get(type_str)(data)
773 elif type_str == '__builtin__.NoneType':
774 return None
775
776
777
778 elif method_str == 'key':
779 return self._unpack(parent_doc, parent_doc[FIELD_NAME]['keys'][doc], loaded_dict)
780
781 elif method_str == 'attachment':
782 if type_str == 'pickle':
783 attachment_response = self.db.get_attachment(parent_doc, data)
784 return pickle.loads(attachment_response.read())
785 else:
786 base_cls, handler_tuple = findHandler(type_str, _attachment_handlers)
787 attachment_response = self.db.get_attachment(parent_doc, data)
788 return handler_tuple[1](attachment_response.read())
789 else:
790
791 pass
792
793 else:
794 return doc
795
796 elif isinstance(doc, (int, float)):
797 return doc
798
799 elif isinstance(doc, list):
800 return [self._unpack(parent_doc, x, loaded_dict) for x in doc]
801
802 elif isinstance(doc, dict):
803 if FIELD_NAME in doc:
804 info = doc[FIELD_NAME]
805
806
807 cls = importstr(info['module'], info['class'])
808
809 if 'args' in info and 'kwargs' in info:
810
811 inst = cls(*info['args'], **info['kwargs'])
812 else:
813 if inst is None:
814 inst = cls.__new__(cls)
815
816
817 if '_id' in doc:
818 self._obj_by_id[doc['_id']] = inst
819
820
821
822
823 inst.__dict__.update(info.get('private', {}))
824 if '_id' in doc:
825 inst.__dict__['_id'] = doc['_id']
826 inst.__dict__['_rev'] = doc['_rev']
827
828
829 inst.__dict__.update({self._unpack(parent_doc, k, loaded_dict): self._unpack(parent_doc, v, loaded_dict) for k,v in doc.items() if k != FIELD_NAME})
830
831
832
833
834
835 return inst
836
837 else:
838 return {self._unpack(parent_doc, k, loaded_dict): self._unpack(parent_doc, v, loaded_dict) for k,v in doc.items()}
839
840
841
842
843 - def load(self, what, loaded=None):
844 """
845 Loads the indicated object(s) out of CouchDB.
846
847 Loading an ID multiple times will result in getting the same object
848 returned each time. Subsequent loads will return the same object
849 again, but with an updated C{__dict__}. Note that this means it is
850 impossible to have both the current version of the object and an older
851 revision loaded at the same time.
852
853 I{Behavior of loading old document revisions is untested at this time.}
854
855 If what is a dict or a couchdb.client.Row, then the values will be used
856 from that object rather than re-fetching from the database. Likewise,
857 the loaded parameter can be used to prevent multiple DB hits. This can
858 be useful when loading multiple documents returned by a view, etc.
859
860 Example use::
861
862 cdb.load(cdb.db.view('couchable/' + viewName, include_docs=True, startkey=[...], endkey=[..., {}]).rows)
863
864 @type what: str, dict, couchdb.client.Row or list of same
865 @param what: A document C{_id}, a dict with an C{'_id'} key, a couchdb.client.Row instance, or a list of any of the preceding.
866 @type loaded: dict, couchdb.client.Row or list of same
867 @param loaded: A mapping of document C{_id}s to documents that have already been loaded out of the database.
868 @rtype: obj or list
869 @return: The object indicated by the C{what} parameter, or a list of such objects if C{what} was a list.
870 """
871 id_list = []
872
873 if isinstance(loaded, list):
874 loaded_dict = {(x.id if isinstance(x, couchdb.client.Row) else x['_id']): (x.doc if isinstance(x, couchdb.client.Row) else x) for x in loaded}
875 else:
876 loaded_dict = loaded or {}
877
878
879 if not isinstance(what, list):
880 load_list = [what]
881 else:
882 load_list = what
883
884 for item in load_list:
885
886 if isinstance(item, basestring):
887 id_list.append(item)
888 elif isinstance(item, couchdb.client.Row):
889 id_list.append(item.id)
890
891 if hasattr(item, 'doc'):
892 loaded_dict[item.id] = item.doc
893
894 elif isinstance(item, dict):
895 id_list.append(item['_id'])
896
897 if len(item) > 2:
898 loaded_dict[item['_id']] = item
899
900 if not isinstance(what, list):
901
902 return [self._load(_id, loaded_dict) for _id in id_list][0]
903 else:
904
905 return [self._load(_id, loaded_dict) for _id in id_list]
906
907
908 - def _load(self, _id, loaded_dict):
909 if _id not in loaded_dict:
910
911
912 loaded_dict[_id] = self.db[_id]
913
914
915
916
917 doc = loaded_dict[_id]
918
919
920
921 obj = self._obj_by_id.get(_id, None)
922 if obj is None or getattr(obj, '_rev', None) != doc['_rev']:
923
924
925 obj = self._unpack(doc, doc, loaded_dict, obj)
926
927 base_cls, func_tuple = findHandler(type(obj), _couchable_types)
928 if func_tuple:
929 func_tuple[1](obj, self)
930
931 obj._cdb = self
932
933 return obj
934
935
936
937 _couchable_types = collections.OrderedDict()
938 -def registerDocType(type_, preStore_func=(lambda obj, cdb: None), postLoad_func=(lambda obj, cdb: None)):
939 """
940 @type type_: type
941 @param type_: Instances of this type will be stored as top-level CouchDB documents.
942 @type preStore_func: callable
943 @param preStore_func: A callback of the form C{lambda obj, cdb: None}, called just before storing the object.
944 @type postLoad_func: callable
945 @param postLoad_func: A callback of the form C{lambda obj, cdb: None}, called just after loading the object.
946 @rtype: type
947 @return: The C{type_} parameter.
948
949 Example: C{registerDocType(CouchableDoc, lambda obj, cdb: obj.preStore(cdb), lambda obj, cdb: obj.postLoad(cdb))}
950 """
951 _couchable_types[type_] = (preStore_func, postLoad_func)
952 _couchable_types[typestr(type_)] = (preStore_func, postLoad_func)
953
954 return type_
955
957 """
958 Base class for types that should be stored as CouchDB documents.
959
960 Note: Deriving from this class is optional; classes may also use L{registerDocType}.
961 The only advantage to subclassing this is that L{registerDocType} has already
962 been called for this class.
963 """
965 """
966 Basic hook point for adding behavior needed just prior to storage.
967
968 Defaults to a no-op.
969
970 @type cdb: CouchableDb
971 @param cdb: CouchableDb object that this object is about to be stored with.
972 """
973 pass
974
975 - def postLoad(self, cdb):
976 """
977 Basic hook point for adding behavior needed just after to loading.
978
979 Defaults to a no-op.
980
981 @type cdb: CouchableDb
982 @param cdb: CouchableDb object that this object was just loaded from.
983 """
984 pass
985
986 registerDocType(CouchableDoc, lambda obj, cdb: obj.preStore(cdb), lambda obj, cdb: obj.postLoad(cdb))
987
988 -def newid(obj, id_func, noUuid=False, noType=False, sep=':'):
989 """
990 Helper function to make document IDs more readable.
991
992 By default, CouchableDb document IDs have the following form:
993
994 C{module.Class:UUID}
995
996 The intent is that each document ID will be reasonably easy to read and
997 identify at a glance. However, for some document classes, there is a more
998 appropriate way to label each instance. For example, a C{Person} class
999 might want to include first and last name as part of the ID, so that casual
1000 examination of the document ID makes it clear which person that ID
1001 corresponds to.
1002
1003 C{newid} has no return data; it I{sets the _id on the object} if one is not already present.
1004
1005 @type obj: object
1006 @param obj: The object to potentially set an C{_id} on.
1007 @type id_func: callable
1008 @param id_func: A callable that takes the object and returns a string to include in the C{_id}.
1009 @type noUuid: bool
1010 @param noUuid: A flag that indicates if a UUID should be appended to the ID.
1011 @type noType: bool
1012 @param noType: A flag that indicates if type information should be prepended to the ID.
1013 @type sep: string
1014 @param sep: The string join the various ID components with. Defaults to C{':'}.
1015
1016 Example::
1017 class ClassA(object):
1018 def __init__(self, name):
1019 self.name = name
1020 # ...
1021 couchable.registerDocType(ClassA,
1022 lambda obj, cdb: couchable.newid(obj, lambda x: x.name),
1023 lambda obj, cdb: None)
1024
1025 couchable.newid(ClassA('foo')) == 'example.ClassA:foo:4094b428-5b45-44fe-bd27-dcb173ec98e8'
1026 """
1027 if not hasattr(obj, '_id'):
1028 id_list = []
1029
1030 if not noType:
1031 id_list.append(typestr(obj))
1032
1033 id_list.append(str(id_func(obj)))
1034
1035 if not noUuid:
1036 id_list.append(str(uuid.uuid4()))
1037
1038 obj._id = sep.join(id_list)
1039
1042 """
1043 Helper function for compressing byte strings.
1044
1045 @type data: byte string
1046 @param data: The data to compress.
1047 @rtype: byte string
1048 @return: The compressed byte string.
1049 """
1050 str_io = cStringIO.StringIO()
1051 gz_file = gzip.GzipFile(mode='wb', fileobj=str_io)
1052 gz_file.write(data)
1053 gz_file.close()
1054 return str_io.getvalue()
1055
1057 """
1058 Helper function for compressing byte strings.
1059
1060 @type data: byte string
1061 @param data: The data to uncompress.
1062 @rtype: byte string
1063 @return: The uncompressed byte string.
1064 """
1065 str_io = cStringIO.StringIO(data)
1066 gz_file = gzip.GzipFile(mode='rb', fileobj=str_io)
1067 return gz_file.read()
1068
1069 _attachment_handlers = collections.OrderedDict()
1070 -def registerAttachmentType(type_,
1071 serialize_func=(lambda obj: pickle.dumps(obj)),
1072 deserialize_func=(lambda data: pickle.loads(data)),
1073 content_type='application/octet-stream', gzip=False):
1074 """
1075 @type type_: type
1076 @param type_: Instances of this type will be stored as attachments instead of CouchDB documents.
1077 @type serialize_func: callable
1078 @param serialize_func: A callback of the form C{lambda obj: pickle.dumps(obj)}, called before attaching the object. The callable needs to accept the object and return a byte string.
1079 @type deserialize_func: callable
1080 @param deserialize_func: A callback of the form C{lambda data: pickle.loads(data)}, called after retreiving the attached object. The callable needs to accept a byte string and return the object.
1081 @type content_type: str
1082 @param content_type: The content type of the attached objected (C{'application/octet-stream'}, etc.).
1083 @type gzip: bool
1084 @param gzip: Indiates if the byte string should be compressed or not.
1085 @rtype: type
1086 @return: The C{type_} parameter.
1087
1088 Example::
1089 registerAttachmentType(CouchableAttachment,
1090 lambda obj: CouchableAttachment.pack(obj),
1091 lambda data: CouchableAttachment.unpack(data),
1092 'application/octet-stream')
1093 """
1094 if gzip:
1095 handler_tuple = (lambda data: doGzip(serialize_func(data)), lambda data: deserialize_func(doGunzip(data)), content_type)
1096 else:
1097 handler_tuple = (serialize_func, deserialize_func, content_type)
1098
1099 _packer(type_)(CouchableDb._pack_attachment)
1100 _attachment_handlers[type_] = handler_tuple
1101 _attachment_handlers[typestr(type_)] = handler_tuple
1102
1103 return type_
1104
1106 """
1107 Base class for types that should be stored as CouchDB attachments.
1108
1109 Note: Deriving from this class is optional; classes may also use L{registerAttachmentType}.
1110 The only advantage to subclassing this class is that L{registerAttachmentType} has already
1111 been called for this class.
1112 """
1113
1114 @staticmethod
1116 """
1117 C{@staticmethod} hook point for serializing the attachment class.
1118
1119 Defaults to C{pickle.dumps(obj)}.
1120
1121 @type obj: object
1122 @param obj: The object to serialize and upload as an attachment.
1123 @rtype: byte string
1124 @return: The serialized data.
1125 """
1126 return pickle.dumps(obj)
1127
1128 @staticmethod
1130 """
1131 C{@staticmethod} hook point for deserializing the attachment class.
1132
1133 Defaults to C{pickle.dumps(obj)}.
1134
1135 @type data: byte string
1136 @param data: The serlized data to unserialize into an object.
1137 @rtype: object
1138 @return: The unserialized object.
1139 """
1140 return pickle.loads(data)
1141
1142 registerAttachmentType(CouchableAttachment,
1143 lambda obj: CouchableAttachment.pack(obj),
1144 lambda data: CouchableAttachment.unpack(data),
1145 'application/octet-stream')
1146
1147
1148