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