Package couchable :: Module core
[frames] | no frames]

Source Code for Module couchable.core

   1  # Copyright (c) 2010 Eli Stevens 
   2  # 
   3  # Permission is hereby granted, free of charge, to any person obtaining a copy 
   4  # of this software and associated documentation files (the "Software"), to deal 
   5  # in the Software without restriction, including without limitation the rights 
   6  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
   7  # copies of the Software, and to permit persons to whom the Software is 
   8  # furnished to do so, subject to the following conditions: 
   9  # 
  10  # The above copyright notice and this permission notice shall be included in 
  11  # all copies or substantial portions of the Software. 
  12  # 
  13  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
  14  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
  15  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
  16  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
  17  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
  18  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
  19  # THE SOFTWARE. 
  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  #import yaml 
  48  import couchdb 
  49  import couchdb.client 
  50  import couchdb.design 
  51  #import couchdb.mapping 
  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  """ 
79 80 -def importstr(module_str, from_=None):
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
95 -def typestr(type_):
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:'
104 105 -class UncouchableException(Exception):
106 - def __init__(self, msg, cls, obj):
107 Exception.__init__(self, msg) 108 self.cls = cls 109 self.obj = obj
110 111 # type packing / unpacking 112 _pack_handlers = collections.OrderedDict()
113 -def _packer(*args):
114 def func(func_): 115 for type_ in args: 116 packer(type_, func_) 117 return func_
118 return func 119
120 -def packer(type_, func_):
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 #_unpack_handlers = collections.OrderedDict() 128 #def _unpacker(*args): 129 # def func(func_): 130 # for type_ in args: 131 # unpacker(type_, func_) 132 # return func_ 133 # return func 134 # 135 #def unpacker(type_, func_): 136 # """ 137 # This function is still in potential flux. Writing new (un)packers is delicate; please see the source. 138 # """ 139 # _unpack_handlers[type_] = func_ 140 # _unpack_handlers[typestr(type_)] = func_ 141 142 143 144 # function for navigating the above dics of handlers, etc. 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 #if isinstance(cls_or_name, basestring): 162 # for type_, handler in reversed(handler_dict.items()): 163 # if cls_or_name == str(type_): 164 # return type_, handler 165 #el 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
175 -class CouchableDb(object):
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 # This dance is odd due to the semantics of how WVD works. 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
223 - def _init_views(self):
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 #@deprecated 290 #def loadInstances(self, cls): 291 # return self.load(self.db.view('couchable/byclass', include_docs=True, startkey=[cls.__module__, cls.__name__], endkey=[cls.__module__, cls.__name__, {}]).rows) 292
293 - def __deepcopy__(self, memo):
294 return copy.copy(self)
295 296 # cls = type(self) 297 # inst = cls.__new__(cls) 298 # inst.__dict__.update({copy.deepcopy(k): copy.deepcopy(v) for k,v in self.__dict__.items if k not in ['_cdb']}) 299 300
301 - def store(self, what):
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 # Actually (finally) send the data to couchdb. 343 #try: 344 #pprint.pprint([(x[0]._id, getattr(x[0], '_rev', None)) for x in self._done_dict.values()]) 345 #print datetime.datetime.now(), "214: self.db.update" 346 ret_list = self.db.update([x[1] for x in self._done_dict.values()]) 347 #except: 348 # print self._done_dict.values() 349 # raise 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 #print datetime.datetime.now(), "225: self.db.put_attachment" 357 self.db.put_attachment(doc, content, content_name, content_type) 358 359 # This is important, even if there are no attachments 360 obj._rev = doc['_rev'] 361 else: 362 raise _rev # it's actually an exception 363 #print "Error:", ret 364 #print "\tobj:", getattr(obj, '_rev', None), "vs. db:", self.db[_id]['_rev'] 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
376 - def _store(self, obj):
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 # This code matches the code in _pack_object 393 #doc = self._obj2doc_empty(obj) 394 #doc.update(self._pack_dict_keyMeansObject(doc, obj.__dict__, attachment_list, '', True)) 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 #self._pack(doc, obj, attachment_list) 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 #if handler: 414 # try: 415 # return handler(self, parent_doc, data, attachment_list, name, isKey) 416 # except RuntimeError: 417 # print "Error with", cls, data 418 # raise 419 #else: 420 # raise UncouchableException("No _packer for type", cls, data) 421 422 #if cls in _pack_handlers: 423 # return _pack_handlers[cls](self, parent_doc, data, attachment_list, name, isKey) 424 #else: 425 # for types, func in reversed(_pack_handlers.items()): 426 # if isinstance(data, types): 427 # return func(self, parent_doc, data, attachment_list, name, isKey) 428 # break 429 # else: 430 # raise UncouchableException("No _packer for type", cls, data) 431
432 - def _objInfo_doc(self, data, doc):
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
453 - def _objInfo_consargs(self, data, doc, args=None, kwargs=None):
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 #def _obj2doc_dict(self, data): 470 # doc = self._obj2doc_empty(data) 471 # 472 # return doc 473 474 475 # This needs to be first, so that it's the last to match in _pack(...) 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 # This code matches the code in _store 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))
583 - def _pack_native_keyAsRepr(self, parent_doc, data, attachment_list, name, isKey):
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)
605 - def _pack_consargs_keyAsKey(self, parent_doc, data, attachment_list, name, isKey):
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)
660 - def _pack_list_noKey(self, parent_doc, data, attachment_list, name, isKey):
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)
685 - def _pack_dict_keyMeansObject(self, parent_doc, data, attachment_list, name, isObjDict):
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 #assert '_attachments' not in doc, ', '.join([str(data), str(isObjDict)]) 723 724 if private_keys: 725 doc.setdefault(FIELD_NAME, {}) 726 #doc[FIELD_NAME].setdefault('private', {}) 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 #parent_doc.setdefault(FIELD_NAME, {}) 731 #parent_doc[FIELD_NAME]['private'] = {self._pack(parent_doc, k, attachment_list, '{}>{}'.format(name, str(k)), True): 732 # self._pack(parent_doc, v, attachment_list, '{}.{}'.format(name, str(k)), False) 733 # for k,v in data.items() if k in private_keys} 734 735 return doc
736
737 - def _pack_attachment(self, parent_doc, data, attachment_list, name, isKey):
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 #try: 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 #else: 776 # return importstr(*type_str.rsplit('.', 1))(data) 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 # FIXME: error? 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 #del doc[FIELD_NAME] 806 807 cls = importstr(info['module'], info['class']) 808 809 if 'args' in info and 'kwargs' in info: 810 #print cls, doc['args'], doc['kwargs'] 811 inst = cls(*info['args'], **info['kwargs']) 812 else: 813 if inst is None: 814 inst = cls.__new__(cls) 815 # This is important, see test_docCycles 816 #print doc 817 if '_id' in doc: 818 self._obj_by_id[doc['_id']] = inst 819 820 #print "unpack isinstance(doc, dict) doc:", doc.get('_id', 'still no id') 821 #print "unpack isinstance(doc, dict) doc:", doc.get('_rev', 'still no rev') 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 # If we haven't stuffed the cache AND pre-set the id/rev, then this goes into an infinite loop. See test_docCycles 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 #print "unpack isinstance(doc, dict) inst:", inst.__dict__.get('_id', 'still no id') 832 #print "unpack isinstance(doc, dict) inst:", inst.__dict__.get('_rev', 'still no rev') 833 834 #print "Unpacking:", inst 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 #except: 840 # print "Error with:", doc 841 # raise 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 #if not isinstance(what, (list, couchdb.client.ViewResults)): 879 if not isinstance(what, list): 880 load_list = [what] 881 else: 882 load_list = what 883 884 for item in load_list: 885 #print "item", item 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 #print "what", what 902 return [self._load(_id, loaded_dict) for _id in id_list][0] 903 else: 904 #print "id_list", id_list 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 #try: 911 #print datetime.datetime.now(), "690: self.db[_id]" 912 loaded_dict[_id] = self.db[_id] 913 #except: 914 # print "problem:", _id 915 # raise 916 917 doc = loaded_dict[_id] 918 919 #print _id, doc, loaded_dict 920 921 obj = self._obj_by_id.get(_id, None) 922 if obj is None or getattr(obj, '_rev', None) != doc['_rev']: 923 #print obj is None or getattr(obj, '_id', 'no id'), obj is None or getattr(obj, '_rev', 'no rev'), doc['_rev'] 924 #print self._obj_by_id.items() 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 # Docs 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
956 -class CouchableDoc(object):
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 """
964 - def preStore(self, cdb):
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
1040 # Attachments 1041 -def doGzip(data):
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
1056 -def doGunzip(data):
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
1105 -class CouchableAttachment(object):
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
1115 - def pack(obj):
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
1129 - def unpack(data):
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 # eof 1148