Package couchdbkit :: Module client
[hide private]
[frames] | no frames]

Source Code for Module couchdbkit.client

   1  # -*- coding: utf-8 - 
   2  # 
   3  # This file is part of couchdbkit released under the MIT license.  
   4  # See the NOTICE for more information. 
   5   
   6  """ 
   7  Client implementation for CouchDB access. It allows you to manage a CouchDB 
   8  server, databases, documents and views. All objects mostly reflect python 
   9  objects for convenience. Server and Database objects for example, can be 
  10  used as easy as a dict. 
  11   
  12  Example: 
  13   
  14      >>> from couchdbkit import Server 
  15      >>> server = Server() 
  16      >>> db = server.create_db('couchdbkit_test') 
  17      >>> doc = { 'string': 'test', 'number': 4 } 
  18      >>> db.save_doc(doc) 
  19      >>> docid = doc['_id'] 
  20      >>> doc2 = db.get(docid) 
  21      >>> doc['string'] 
  22      u'test' 
  23      >>> del db[docid] 
  24      >>> docid in db 
  25      False 
  26      >>> del server['simplecouchdb_test'] 
  27   
  28  """ 
  29   
  30  UNKOWN_INFO = {} 
  31   
  32   
  33  import base64 
  34  import cgi 
  35  from collections import deque 
  36  from itertools import groupby 
  37  from mimetypes import guess_type 
  38  import re 
  39  import time 
  40  import urlparse 
  41  import warnings 
  42   
  43  from restkit.util import url_quote 
  44   
  45  from .exceptions import InvalidAttachment, NoResultFound, \ 
  46  ResourceNotFound, ResourceConflict  
  47  from . import resource 
  48  from .utils import validate_dbname, json 
  49   
  50   
  51  DEFAULT_UUID_BATCH_COUNT = 1000 
52 53 -def _maybe_serialize(doc):
54 if hasattr(doc, "to_json"): 55 return doc.to_json(), True 56 elif isinstance(doc, dict): 57 return doc.copy(), False 58 59 return doc, False
60
61 -class Server(object):
62 """ Server object that allows you to access and manage a couchdb node. 63 A Server object can be used like any `dict` object. 64 """ 65 66 resource_class = resource.CouchdbResource 67
68 - def __init__(self, uri='http://127.0.0.1:5984', 69 uuid_batch_count=DEFAULT_UUID_BATCH_COUNT, 70 resource_class=None, resource_instance=None, 71 **client_opts):
72 73 """ constructor for Server object 74 75 @param uri: uri of CouchDb host 76 @param uuid_batch_count: max of uuids to get in one time 77 @param resource_instance: `restkit.resource.CouchdbDBResource` instance. 78 It alows you to set a resource class with custom parameters. 79 """ 80 81 if not uri or uri is None: 82 raise ValueError("Server uri is missing") 83 84 if uri.endswith("/"): 85 uri = uri[:-1] 86 87 self.uri = uri 88 self.uuid_batch_count = uuid_batch_count 89 self._uuid_batch_count = uuid_batch_count 90 91 if resource_class is not None: 92 self.resource_class = resource_class 93 94 if resource_instance and isinstance(resource_instance, 95 resource.CouchdbResource): 96 resource_instance.initial['uri'] = uri 97 self.res = resource_instance.clone() 98 if client_opts: 99 self.res.client_opts.update(client_opts) 100 else: 101 self.res = self.resource_class(uri, **client_opts) 102 self._uuids = deque()
103
104 - def info(self):
105 """ info of server 106 107 @return: dict 108 109 """ 110 try: 111 resp = self.res.get() 112 except Exception, e: 113 return UNKOWN_INFO 114 115 return resp.json_body
116
117 - def all_dbs(self):
118 """ get list of databases in CouchDb host 119 120 """ 121 return self.res.get('/_all_dbs').json_body
122
123 - def create_db(self, dbname, **params):
124 """ Create a database on CouchDb host 125 126 @param dname: str, name of db 127 @param param: custom parameters to pass to create a db. For 128 example if you use couchdbkit to access to cloudant or bigcouch: 129 130 Ex: q=12 or n=4 131 132 See https://github.com/cloudant/bigcouch for more info. 133 134 @return: Database instance if it's ok or dict message 135 """ 136 return Database(self._db_uri(dbname), create=True, 137 server=self, **params)
138
139 - def get_or_create_db(self, dbname, **params):
140 """ 141 Try to return a Database object for dbname. If 142 database doest't exist, it will be created. 143 144 """ 145 return Database(self._db_uri(dbname), create=True, 146 server=self, **params)
147
148 - def delete_db(self, dbname):
149 """ 150 Delete database 151 """ 152 del self[dbname]
153 154 #TODO: maintain list of replications
155 - def replicate(self, source, target, **params):
156 """ 157 simple handler for replication 158 159 @param source: str, URI or dbname of the source 160 @param target: str, URI or dbname of the target 161 @param params: replication options 162 163 More info about replication here : 164 http://wiki.apache.org/couchdb/Replication 165 166 """ 167 payload = { 168 "source": source, 169 "target": target, 170 } 171 payload.update(params) 172 resp = self.res.post('/_replicate', payload=payload) 173 return resp.json_body
174
175 - def active_tasks(self):
176 """ return active tasks """ 177 resp = self.res.get('/_active_tasks') 178 return resp.json_body
179
180 - def uuids(self, count=1):
181 return self.res.get('/_uuids', count=count).json_body
182
183 - def next_uuid(self, count=None):
184 """ 185 return an available uuid from couchdbkit 186 """ 187 if count is not None: 188 self._uuid_batch_count = count 189 else: 190 self._uuid_batch_count = self.uuid_batch_count 191 192 try: 193 return self._uuids.pop() 194 except IndexError: 195 self._uuids.extend(self.uuids(count=self._uuid_batch_count)["uuids"]) 196 return self._uuids.pop()
197
198 - def __getitem__(self, dbname):
199 return Database(self._db_uri(dbname), server=self)
200
201 - def __delitem__(self, dbname):
202 ret = self.res.delete('/%s/' % url_quote(dbname, 203 safe=":")).json_body 204 return ret
205
206 - def __contains__(self, dbname):
207 try: 208 self.res.head('/%s/' % url_quote(dbname, safe=":")) 209 except: 210 return False 211 return True
212
213 - def __iter__(self):
214 for dbname in self.all_dbs(): 215 yield Database(self._db_uri(dbname), server=self)
216
217 - def __len__(self):
218 return len(self.all_dbs())
219
220 - def __nonzero__(self):
221 return (len(self) > 0)
222
223 - def _db_uri(self, dbname):
224 if dbname.startswith("/"): 225 dbname = dbname[1:] 226 227 dbname = url_quote(dbname, safe=":") 228 return "/".join([self.uri, dbname])
229
230 -class Database(object):
231 """ Object that abstract access to a CouchDB database 232 A Database object can act as a Dict object. 233 """ 234
235 - def __init__(self, uri, create=False, server=None, **params):
236 """Constructor for Database 237 238 @param uri: str, Database uri 239 @param create: boolean, False by default, 240 if True try to create the database. 241 @param server: Server instance 242 243 """ 244 self.uri = uri 245 self.server_uri, self.dbname = uri.rsplit("/", 1) 246 247 if server is not None: 248 if not hasattr(server, 'next_uuid'): 249 raise TypeError('%s is not a couchdbkit.Server instance' % 250 server.__class__.__name__) 251 self.server = server 252 else: 253 self.server = server = Server(self.server_uri, **params) 254 255 validate_dbname(self.dbname) 256 if create: 257 try: 258 self.server.res.head('/%s/' % self.dbname) 259 except ResourceNotFound: 260 self.server.res.put('/%s/' % self.dbname, **params).json_body 261 262 263 self.res = server.res(self.dbname)
264
265 - def __repr__(self):
266 return "<%s %s>" % (self.__class__.__name__, self.dbname)
267
268 - def info(self):
269 """ 270 Get database information 271 272 @return: dict 273 """ 274 return self.res.get().json_body
275 276
277 - def compact(self, dname=None):
278 """ compact database 279 @param dname: string, name of design doc. Usefull to 280 compact a view. 281 """ 282 path = "/_compact" 283 if dname is not None: 284 path = "%s/%s" % (path, resource.escape_docid(dname)) 285 res = self.res.post(path, headers={"Content-Type": 286 "application/json"}) 287 return res.json_body
288
289 - def view_cleanup(self):
290 res = self.res.post('/_view_cleanup', headers={"Content-Type": 291 "application/json"}) 292 return res.json_body
293
294 - def flush(self):
295 """ Remove all docs from a database 296 except design docs.""" 297 # save ddocs 298 all_ddocs = self.all_docs(startkey="_design", 299 endkey="_design/"+u"\u9999", 300 include_docs=True) 301 ddocs = [] 302 for ddoc in all_ddocs: 303 ddoc['doc'].pop('_rev') 304 ddocs.append(ddoc['doc']) 305 306 # delete db 307 self.server.delete_db(self.dbname) 308 309 # we let a chance to the system to sync 310 time.sleep(0.2) 311 312 # recreate db + ddocs 313 self.server.create_db(self.dbname) 314 self.bulk_save(ddocs)
315
316 - def doc_exist(self, docid):
317 """Test if document exists in a database 318 319 @param docid: str, document id 320 @return: boolean, True if document exist 321 """ 322 323 try: 324 self.res.head(resource.escape_docid(docid)) 325 except ResourceNotFound: 326 return False 327 return True
328
329 - def open_doc(self, docid, **params):
330 """Get document from database 331 332 Args: 333 @param docid: str, document id to retrieve 334 @param wrapper: callable. function that takes dict as a param. 335 Used to wrap an object. 336 @param **params: See doc api for parameters to use: 337 http://wiki.apache.org/couchdb/HTTP_Document_API 338 339 @return: dict, representation of CouchDB document as 340 a dict. 341 """ 342 wrapper = None 343 if "wrapper" in params: 344 wrapper = params.pop("wrapper") 345 elif "schema" in params: 346 schema = params.pop("schema") 347 if not hasattr(schema, "wrap"): 348 raise TypeError("invalid schema") 349 wrapper = schema.wrap 350 351 docid = resource.escape_docid(docid) 352 doc = self.res.get(docid, **params).json_body 353 if wrapper is not None: 354 if not callable(wrapper): 355 raise TypeError("wrapper isn't a callable") 356 357 return wrapper(doc) 358 359 return doc
360 get = open_doc 361
362 - def all_docs(self, by_seq=False, **params):
363 """Get all documents from a database 364 365 This method has the same behavior as a view. 366 367 `all_docs( **params )` is the same as `view('_all_docs', **params)` 368 and `all_docs( by_seq=True, **params)` is the same as 369 `view('_all_docs_by_seq', **params)` 370 371 You can use all(), one(), first() just like views 372 373 Args: 374 @param by_seq: bool, if True the "_all_docs_by_seq" is passed to 375 couchdb. It will return an updated list of all documents. 376 377 @return: list, results of the view 378 """ 379 if by_seq: 380 try: 381 return self.view('_all_docs_by_seq', **params) 382 except ResourceNotFound: 383 # CouchDB 0.11 or sup 384 raise AttributeError("_all_docs_by_seq isn't supported on Couchdb %s" % self.server.info()[1]) 385 386 return self.view('_all_docs', **params)
387
388 - def get_rev(self, docid):
389 """ Get last revision from docid (the '_rev' member) 390 @param docid: str, undecoded document id. 391 392 @return rev: str, the last revision of document. 393 """ 394 response = self.res.head(resource.escape_docid(docid)) 395 return response['etag'].strip('"')
396
397 - def save_doc(self, doc, encode_attachments=True, force_update=False, 398 **params):
399 """ Save a document. It will use the `_id` member of the document 400 or request a new uuid from CouchDB. IDs are attached to 401 documents on the client side because POST has the curious property of 402 being automatically retried by proxies in the event of network 403 segmentation and lost responses. (Idee from `Couchrest <http://github.com/jchris/couchrest/>`) 404 405 @param doc: dict. doc is updated 406 with doc '_id' and '_rev' properties returned 407 by CouchDB server when you save. 408 @param force_update: boolean, if there is conlict, try to update 409 with latest revision 410 @param params, list of optionnal params, like batch="ok" 411 412 @return res: result of save. doc is updated in the mean time 413 """ 414 if doc is None: 415 doc1 = {} 416 else: 417 doc1, schema = _maybe_serialize(doc) 418 419 if '_attachments' in doc1 and encode_attachments: 420 doc1['_attachments'] = resource.encode_attachments(doc['_attachments']) 421 422 if '_id' in doc: 423 docid = doc1['_id'] 424 docid1 = resource.escape_docid(doc1['_id']) 425 try: 426 res = self.res.put(docid1, payload=doc1, 427 **params).json_body 428 except ResourceConflict: 429 if force_update: 430 doc1['_rev'] = self.get_rev(docid) 431 res =self.res.put(docid1, payload=doc1, 432 **params).json_body 433 else: 434 raise 435 else: 436 try: 437 doc['_id'] = self.server.next_uuid() 438 res = self.res.put(doc['_id'], payload=doc1, 439 **params).json_body 440 except: 441 res = self.res.post(payload=doc1, **params).json_body 442 443 if 'batch' in params and 'id' in res: 444 doc1.update({ '_id': res['id']}) 445 else: 446 doc1.update({'_id': res['id'], '_rev': res['rev']}) 447 448 449 if schema: 450 doc._doc = doc1 451 else: 452 doc.update(doc1) 453 return res
454
455 - def save_docs(self, docs, use_uuids=True, all_or_nothing=False):
456 """ bulk save. Modify Multiple Documents With a Single Request 457 458 @param docs: list of docs 459 @param use_uuids: add _id in doc who don't have it already set. 460 @param all_or_nothing: In the case of a power failure, when the database 461 restarts either all the changes will have been saved or none of them. 462 However, it does not do conflict checking, so the documents will 463 464 .. seealso:: `HTTP Bulk Document API <http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API>` 465 466 """ 467 468 docs1 = [] 469 docs_schema = [] 470 for doc in docs: 471 doc1, schema = _maybe_serialize(doc) 472 docs1.append(doc1) 473 docs_schema.append(schema) 474 475 def is_id(doc): 476 return '_id' in doc
477 478 if use_uuids: 479 ids = [] 480 noids = [] 481 for k, g in groupby(docs1, is_id): 482 if not k: 483 noids = list(g) 484 else: 485 ids = list(g) 486 487 uuid_count = max(len(noids), self.server.uuid_batch_count) 488 for doc in noids: 489 nextid = self.server.next_uuid(count=uuid_count) 490 if nextid: 491 doc['_id'] = nextid 492 493 payload = { "docs": docs1 } 494 if all_or_nothing: 495 payload["all_or_nothing"] = True 496 497 # update docs 498 results = self.res.post('/_bulk_docs', 499 payload=payload).json_body 500 501 errors = [] 502 for i, res in enumerate(results): 503 if 'error' in res: 504 errors.append(res) 505 else: 506 if docs_schema[i]: 507 docs[i]._doc.update({ 508 '_id': res['id'], 509 '_rev': res['rev'] 510 }) 511 else: 512 docs[i].update({ 513 '_id': res['id'], 514 '_rev': res['rev'] 515 }) 516 if errors: 517 raise BulkSaveError(errors, results) 518 return results
519 bulk_save = save_docs 520
521 - def delete_docs(self, docs, all_or_nothing=False):
522 """ bulk delete. 523 It adds '_deleted' member to doc then uses bulk_save to save them. 524 525 """ 526 for doc in docs: 527 doc['_deleted'] = True 528 529 return self.bulk_save(docs, use_uuids=False, 530 all_or_nothing=all_or_nothing)
531 532 bulk_delete = delete_docs 533
534 - def delete_doc(self, doc):
535 """ delete a document or a list of documents 536 @param doc: str or dict, document id or full doc. 537 @return: dict like: 538 539 .. code-block:: python 540 541 {"ok":true,"rev":"2839830636"} 542 """ 543 result = { 'ok': False } 544 545 doc1, schema = _maybe_serialize(doc) 546 if isinstance(doc1, dict): 547 if not '_id' or not '_rev' in doc1: 548 raise KeyError('_id and _rev are required to delete a doc') 549 550 docid = resource.escape_docid(doc1['_id']) 551 result = self.res.delete(docid, rev=doc1['_rev']).json_body 552 elif isinstance(doc1, basestring): # we get a docid 553 rev = self.get_rev(doc1) 554 docid = resource.escape_docid(doc1) 555 result = self.res.delete(docid, rev=rev).json_body 556 557 if schema: 558 doc._doc.update({ 559 "_rev": result['rev'], 560 "_deleted": True 561 }) 562 elif isinstance(doc, dict): 563 doc.update({ 564 "_rev": result['rev'], 565 "_deleted": True 566 }) 567 return result
568
569 - def copy_doc(self, doc, dest=None):
570 """ copy an existing document to a new id. If dest is None, a new uuid will be requested 571 @param doc: dict or string, document or document id 572 @param dest: basestring or dict. if _rev is specified in dict it will override the doc 573 """ 574 575 doc1, schema = _maybe_serialize(doc) 576 if isinstance(doc1, basestring): 577 docid = doc1 578 else: 579 if not '_id' in doc1: 580 raise KeyError('_id is required to copy a doc') 581 docid = doc1['_id'] 582 583 if dest is None: 584 destination = self.server.next_uuid(count=1) 585 elif isinstance(dest, basestring): 586 if dest in self: 587 dest = self.get(dest)['_rev'] 588 destination = "%s?rev=%s" % (dest['_id'], dest['_rev']) 589 else: 590 destination = dest 591 elif isinstance(dest, dict): 592 if '_id' in dest and '_rev' in dest and dest['_id'] in self: 593 rev = dest['_rev'] 594 destination = "%s?rev=%s" % (dest['_id'], dest['_rev']) 595 else: 596 raise KeyError("dest doesn't exist or this not a document ('_id' or '_rev' missig).") 597 598 if destination: 599 result = self.res.copy('/%s' % docid, headers={ 600 "Destination": str(destination) 601 }).json_body 602 return result 603 604 return { 'ok': False }
605 606
607 - def view(self, view_name, schema=None, wrapper=None, **params):
608 """ get view results from database. viewname is generally 609 a string like `designname/viewname". It return an ViewResults 610 object on which you could iterate, list, ... . You could wrap 611 results in wrapper function, a wrapper function take a row 612 as argument. Wrapping could be also done by passing an Object 613 in obj arguments. This Object should have a `wrap` method 614 that work like a simple wrapper function. 615 616 @param view_name, string could be '_all_docs', '_all_docs_by_seq', 617 'designname/viewname' if view_name start with a "/" it won't be parsed 618 and beginning slash will be removed. Usefull with c-l for example. 619 @param schema, Object with a wrapper function 620 @param wrapper: function used to wrap results 621 @param params: params of the view 622 623 """ 624 def get_multi_wrapper(classes, **params): 625 def wrapper(row): 626 data = row.get('value') 627 docid = row.get('id') 628 doc = row.get('doc') 629 if doc is not None and params.get('wrap_doc', True): 630 cls = classes.get(doc.get('doc_type')) 631 cls._allow_dynamic_properties = params.get('dynamic_properties', True) 632 return cls.wrap(doc) 633 634 elif not data or data is None: 635 return row 636 elif not isinstance(data, dict) or not docid: 637 return row 638 else: 639 cls = classes.get(data.get('doc_type')) 640 data['_id'] = docid 641 if 'rev' in data: 642 data['_rev'] = data.pop('rev') 643 cls._allow_dynamic_properties = params.get('dynamic_properties', True) 644 return cls.wrap(data)
645 return wrapper 646 647 if view_name.startswith('/'): 648 view_name = view_name[1:] 649 if view_name == '_all_docs': 650 view_path = view_name 651 elif view_name == '_all_docs_by_seq': 652 view_path = view_name 653 else: 654 view_name = view_name.split('/') 655 dname = view_name.pop(0) 656 vname = '/'.join(view_name) 657 view_path = '_design/%s/_view/%s' % (dname, vname) 658 if schema is not None: 659 if hasattr(schema, 'wrap'): 660 wrapper = schema.wrap 661 elif isinstance(schema, dict): 662 wrapper = get_multi_wrapper(schema, **params) 663 elif isinstance(schema, list): 664 classes = dict( (c._doc_type, c) for c in schema) 665 wrapper = get_multi_wrapper(classes, **params) 666 else: 667 raise AttributeError("schema argument %s must either have a 'wrap' method, or be a dict or list)" % str(schema)) 668 669 return View(self, view_path, wrapper=wrapper)(**params) 670
671 - def temp_view(self, design, schema=None, wrapper=None, **params):
672 """ get adhoc view results. Like view it reeturn a ViewResult object.""" 673 if schema is not None: 674 if not hasattr(schema, 'wrap'): 675 raise AttributeError("no 'wrap' method found in obj %s)" % str(schema)) 676 wrapper = schema.wrap 677 return TempView(self, design, wrapper=wrapper)(**params)
678
679 - def search( self, view_name, handler='_fti/_design', wrapper=None, **params):
680 """ Search. Return results from search. Use couchdb-lucene 681 with its default settings by default.""" 682 return View(self, "/%s/%s" % (handler, view_name), wrapper=wrapper)(**params)
683
684 - def documents(self, wrapper=None, **params):
685 """ return a ViewResults objects containing all documents. 686 This is a shorthand to view function. 687 """ 688 return View(self, '_all_docs', wrapper=wrapper)(**params)
689 iterdocuments = documents 690
691 - def put_attachment(self, doc, content, name=None, content_type=None, 692 content_length=None):
693 """ Add attachement to a document. All attachments are streamed. 694 695 @param doc: dict, document object 696 @param content: string or :obj:`File` object. 697 @param name: name or attachment (file name). 698 @param content_type: string, mimetype of attachment. 699 If you don't set it, it will be autodetected. 700 @param content_lenght: int, size of attachment. 701 702 @return: bool, True if everything was ok. 703 704 705 Example: 706 707 >>> from simplecouchdb import server 708 >>> server = server() 709 >>> db = server.create_db('couchdbkit_test') 710 >>> doc = { 'string': 'test', 'number': 4 } 711 >>> db.save(doc) 712 >>> text_attachment = u'un texte attaché' 713 >>> db.put_attachment(doc, text_attachment, "test", "text/plain") 714 True 715 >>> file = db.fetch_attachment(doc, 'test') 716 >>> result = db.delete_attachment(doc, 'test') 717 >>> result['ok'] 718 True 719 >>> db.fetch_attachment(doc, 'test') 720 >>> del server['couchdbkit_test'] 721 {u'ok': True} 722 """ 723 724 headers = {} 725 726 if not content: 727 content = "" 728 content_length = 0 729 if name is None: 730 if hasattr(content, "name"): 731 name = content.name 732 else: 733 raise InvalidAttachment('You should provide a valid attachment name') 734 name = url_quote(name, safe="") 735 if content_type is None: 736 content_type = ';'.join(filter(None, guess_type(name))) 737 738 if content_type: 739 headers['Content-Type'] = content_type 740 741 # add appropriate headers 742 if content_length and content_length is not None: 743 headers['Content-Length'] = content_length 744 745 doc1, schema = _maybe_serialize(doc) 746 747 docid = resource.escape_docid(doc1['_id']) 748 res = self.res(docid).put(name, payload=content, 749 headers=headers, rev=doc1['_rev']).json_body 750 751 if res['ok']: 752 new_doc = self.get(doc1['_id'], rev=res['rev']) 753 doc.update(new_doc) 754 return res['ok']
755
756 - def delete_attachment(self, doc, name):
757 """ delete attachement to the document 758 759 @param doc: dict, document object in python 760 @param name: name of attachement 761 762 @return: dict, with member ok set to True if delete was ok. 763 """ 764 doc1, schema = _maybe_serialize(doc) 765 766 docid = resource.escape_docid(doc1['_id']) 767 name = url_quote(name, safe="") 768 769 res = self.res(docid).delete(name, rev=doc1['_rev']).json_body 770 if res['ok']: 771 new_doc = self.get(doc1['_id'], rev=res['rev']) 772 doc.update(new_doc) 773 return res['ok']
774 775
776 - def fetch_attachment(self, id_or_doc, name, stream=False):
777 """ get attachment in a document 778 779 @param id_or_doc: str or dict, doc id or document dict 780 @param name: name of attachment default: default result 781 @param stream: boolean, if True return a file object 782 @return: `restkit.httpc.Response` object 783 """ 784 785 if isinstance(id_or_doc, basestring): 786 docid = id_or_doc 787 else: 788 doc, schema = _maybe_serialize(id_or_doc) 789 docid = doc['_id'] 790 791 docid = resource.escape_docid(docid) 792 name = url_quote(name, safe="") 793 794 resp = self.res(docid).get(name) 795 if stream: 796 return resp.body_stream() 797 return resp.body_string(charset="utf-8")
798 799
800 - def ensure_full_commit(self):
801 """ commit all docs in memory """ 802 return self.res.post('_ensure_full_commit', headers={ 803 "Content-Type": "application/json" 804 }).json_body
805
806 - def __len__(self):
807 return self.info()['doc_count']
808
809 - def __contains__(self, docid):
810 return self.doc_exist(docid)
811
812 - def __getitem__(self, docid):
813 return self.get(docid)
814
815 - def __setitem__(self, docid, doc):
816 doc['_id'] = docid 817 self.save_doc(doc)
818 819
820 - def __delitem__(self, docid):
821 self.delete_doc(docid)
822
823 - def __iter__(self):
824 return self.documents().iterator()
825
826 - def __nonzero__(self):
827 return (len(self) > 0)
828
829 -class ViewResults(object):
830 """ 831 Object to retrieve view results. 832 """ 833
834 - def __init__(self, view, **params):
835 """ 836 Constructor of ViewResults object 837 838 @param view: Object inherited from :mod:`couchdbkit.client.view.ViewInterface 839 @param params: params to apply when fetching view. 840 841 """ 842 self.view = view 843 self.params = params 844 self._result_cache = None 845 self._total_rows = None 846 self._offset = 0 847 self._dynamic_keys = []
848
849 - def iterator(self):
850 self._fetch_if_needed() 851 rows = self._result_cache.get('rows', []) 852 wrapper = self.view._wrapper 853 for row in rows: 854 if wrapper is not None: 855 yield self.view._wrapper(row) 856 else: 857 yield row
858
859 - def first(self):
860 """ 861 Return the first result of this query or None if the result doesn’t contain any row. 862 863 This results in an execution of the underlying query. 864 """ 865 866 try: 867 return list(self)[0] 868 except IndexError: 869 return None
870
871 - def one(self, except_all=False):
872 """ 873 Return exactly one result or raise an exception. 874 875 876 Raises `couchdbkit.exceptions.MultipleResultsFound` if multiple rows are returned. 877 If except_all is True, raises `couchdbkit.exceptions.NoResultFound` 878 if the query selects no rows. 879 880 This results in an execution of the underlying query. 881 """ 882 883 length = len(self) 884 if length > 1: 885 raise MultipleResultsFound("%s results found." % length) 886 887 result = self.first() 888 if result is None and except_all: 889 raise NoResultFound 890 return result
891
892 - def all(self):
893 """ return list of all results """ 894 return list(self.iterator())
895
896 - def count(self):
897 """ return number of returned results """ 898 self._fetch_if_needed() 899 return len(self._result_cache.get('rows', []))
900
901 - def fetch(self):
902 """ fetch results and cache them """ 903 # reset dynamic keys 904 for key in self._dynamic_keys: 905 try: 906 delattr(self, key) 907 except: 908 pass 909 self._dynamic_keys = [] 910 911 self._result_cache = self.view._exec(**self.params).json_body 912 self._total_rows = self._result_cache.get('total_rows') 913 self._offset = self._result_cache.get('offset', 0) 914 915 # add key in view results that could be added by an external 916 # like couchdb-lucene 917 for key in self._result_cache.keys(): 918 if key not in ["total_rows", "offset", "rows"]: 919 self._dynamic_keys.append(key) 920 setattr(self, key, self._result_cache[key])
921 922
923 - def fetch_raw(self):
924 """ retrive the raw result """ 925 return self.view._exec(**self.params)
926
927 - def _fetch_if_needed(self):
928 if not self._result_cache: 929 self.fetch()
930 931 @property
932 - def total_rows(self):
933 """ return number of total rows in the view """ 934 self._fetch_if_needed() 935 # reduce case, count number of lines 936 if self._total_rows is None: 937 return self.count() 938 return self._total_rows
939 940 @property
941 - def offset(self):
942 """ current position in the view """ 943 self._fetch_if_needed() 944 return self._offset
945
946 - def __getitem__(self, key):
947 params = self.params.copy() 948 if type(key) is slice: 949 if key.start is not None: 950 params['startkey'] = key.start 951 if key.stop is not None: 952 params['endkey'] = key.stop 953 elif isinstance(key, (list, tuple,)): 954 params['keys'] = key 955 else: 956 params['key'] = key 957 958 return ViewResults(self.view, **params)
959
960 - def __iter__(self):
961 return self.iterator()
962
963 - def __len__(self):
964 return self.count()
965
966 - def __nonzero__(self):
967 return bool(len(self))
968
969 970 -class ViewInterface(object):
971 """ Generic object interface used by View and TempView objects. """ 972
973 - def __init__(self, db, wrapper=None):
974 self._db = db 975 self._wrapper = wrapper
976
977 - def __call__(self, **params):
978 return ViewResults(self, **params)
979
980 - def __iter__(self):
981 return self()
982
983 - def _exec(self, **params):
984 raise NotImplementedError
985
986 -class View(ViewInterface):
987 """ Object used to wrap a view and return ViewResults. 988 Generally called via the `view` method in a `Database` instance. """ 989
990 - def __init__(self, db, view_path, wrapper=None):
991 ViewInterface.__init__(self, db, wrapper=wrapper) 992 self.view_path = view_path
993
994 - def _exec(self, **params):
995 if 'keys' in params: 996 keys = params.pop('keys') 997 return self._db.res.post(self.view_path, payload={ 'keys': keys }, **params) 998 else: 999 return self._db.res.get(self.view_path, **params)
1000
1001 -class TempView(ViewInterface):
1002 """ Object used to wrap a temporary and return ViewResults. """
1003 - def __init__(self, db, design, wrapper=None):
1004 ViewInterface.__init__(self, db, wrapper=wrapper) 1005 self.design = design 1006 self._wrapper = wrapper
1007
1008 - def _exec(self, **params):
1009 return self._db.res.post('_temp_view', payload=self.design, 1010 **params)
1011