updated on Wed Jan 18 20:10:41 UTC 2012
[aur-mirror.git] / python-couchdb / client.py
bloba52bdd767272f8955dda0ae3e80a6d5bd30183ed
1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2007-2009 Christopher Lenz
4 # All rights reserved.
6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution.
9 """Python client API for CouchDB.
11 >>> server = Server('http://localhost:5984/')
12 >>> db = server.create('python-tests')
13 >>> doc_id = db.create({'type': 'Person', 'name': 'John Doe'})
14 >>> doc = db[doc_id]
15 >>> doc['type']
16 'Person'
17 >>> doc['name']
18 'John Doe'
19 >>> del db[doc.id]
20 >>> doc.id in db
21 False
23 >>> del server['python-tests']
24 """
26 import httplib2
27 import mimetypes
28 from urllib import quote, urlencode
29 from types import FunctionType
30 from inspect import getsource
31 from textwrap import dedent
32 import re
33 import socket
35 from couchdb import json
37 __all__ = ['PreconditionFailed', 'ResourceNotFound', 'ResourceConflict',
38 'ServerError', 'Server', 'Database', 'Document', 'ViewResults',
39 'Row']
40 __docformat__ = 'restructuredtext en'
43 DEFAULT_BASE_URI = 'http://localhost:5984/'
46 class PreconditionFailed(Exception):
47 """Exception raised when a 412 HTTP error is received in response to a
48 request.
49 """
52 class ResourceNotFound(Exception):
53 """Exception raised when a 404 HTTP error is received in response to a
54 request.
55 """
58 class ResourceConflict(Exception):
59 """Exception raised when a 409 HTTP error is received in response to a
60 request.
61 """
64 class ServerError(Exception):
65 """Exception raised when an unexpected HTTP error is received in response
66 to a request.
67 """
70 class Server(object):
71 """Representation of a CouchDB server.
73 >>> server = Server('http://localhost:5984/')
75 This class behaves like a dictionary of databases. For example, to get a
76 list of database names on the server, you can simply iterate over the
77 server object.
79 New databases can be created using the `create` method:
81 >>> db = server.create('python-tests')
82 >>> db
83 <Database 'python-tests'>
85 You can access existing databases using item access, specifying the database
86 name as the key:
88 >>> db = server['python-tests']
89 >>> db.name
90 'python-tests'
92 Databases can be deleted using a ``del`` statement:
94 >>> del server['python-tests']
95 """
97 def __init__(self, uri=DEFAULT_BASE_URI, cache=None, timeout=None):
98 """Initialize the server object.
100 :param uri: the URI of the server (for example
101 ``http://localhost:5984/``)
102 :param cache: either a cache directory path (as a string) or an object
103 compatible with the ``httplib2.FileCache`` interface. If
104 `None` (the default), no caching is performed.
105 :param timeout: socket timeout in number of seconds, or `None` for no
106 timeout
108 http = httplib2.Http(cache=cache, timeout=timeout)
109 http.force_exception_to_status_code = False
110 self.resource = Resource(http, uri)
112 def __contains__(self, name):
113 """Return whether the server contains a database with the specified
114 name.
116 :param name: the database name
117 :return: `True` if a database with the name exists, `False` otherwise
119 try:
120 self.resource.head(validate_dbname(name))
121 return True
122 except ResourceNotFound:
123 return False
125 def __iter__(self):
126 """Iterate over the names of all databases."""
127 resp, data = self.resource.get('_all_dbs')
128 return iter(data)
130 def __len__(self):
131 """Return the number of databases."""
132 resp, data = self.resource.get('_all_dbs')
133 return len(data)
135 def __nonzero__(self):
136 """Return whether the server is available."""
137 try:
138 self.resource.head()
139 return True
140 except:
141 return False
143 def __repr__(self):
144 return '<%s %r>' % (type(self).__name__, self.resource.uri)
146 def __delitem__(self, name):
147 """Remove the database with the specified name.
149 :param name: the name of the database
150 :raise ResourceNotFound: if no database with that name exists
152 self.resource.delete(validate_dbname(name))
154 def __getitem__(self, name):
155 """Return a `Database` object representing the database with the
156 specified name.
158 :param name: the name of the database
159 :return: a `Database` object representing the database
160 :rtype: `Database`
161 :raise ResourceNotFound: if no database with that name exists
163 db = Database(uri(self.resource.uri, name), validate_dbname(name),
164 http=self.resource.http)
165 db.resource.head() # actually make a request to the database
166 return db
168 @property
169 def config(self):
170 """The configuration of the CouchDB server.
172 The configuration is represented as a nested dictionary of sections and
173 options from the configuration files of the server, or the default
174 values for options that are not explicitly configured.
176 :type: `dict`
178 resp, data = self.resource.get('_config')
179 return data
181 @property
182 def version(self):
183 """The version string of the CouchDB server.
185 Note that this results in a request being made, and can also be used
186 to check for the availability of the server.
188 :type: `unicode`"""
189 resp, data = self.resource.get()
190 return data['version']
192 def stats(self):
193 """Database statistics."""
194 resp, data = self.resource.get('_stats')
195 return data
197 def tasks(self):
198 """A list of tasks currently active on the server."""
199 resp, data = self.resource.get('_active_tasks')
200 return data
202 def create(self, name):
203 """Create a new database with the given name.
205 :param name: the name of the database
206 :return: a `Database` object representing the created database
207 :rtype: `Database`
208 :raise PreconditionFailed: if a database with that name already exists
210 self.resource.put(validate_dbname(name))
211 return self[name]
213 def delete(self, name):
214 """Delete the database with the specified name.
216 :param name: the name of the database
217 :raise ResourceNotFound: if a database with that name does not exist
218 :since: 0.6
220 del self[name]
222 def replicate(self, source, target, **options):
223 """Replicate changes from the source database to the target database.
225 :param source: URL of the source database
226 :param target: URL of the target database
227 :param options: optional replication args, e.g. continuous=True
229 data = {'source': source, 'target': target}
230 data.update(options)
231 resp, data = self.resource.post('_replicate', data)
232 return data
235 class Database(object):
236 """Representation of a database on a CouchDB server.
238 >>> server = Server('http://localhost:5984/')
239 >>> db = server.create('python-tests')
241 New documents can be added to the database using the `create()` method:
243 >>> doc_id = db.create({'type': 'Person', 'name': 'John Doe'})
245 This class provides a dictionary-like interface to databases: documents are
246 retrieved by their ID using item access
248 >>> doc = db[doc_id]
249 >>> doc #doctest: +ELLIPSIS
250 <Document '...'@... {...}>
252 Documents are represented as instances of the `Row` class, which is
253 basically just a normal dictionary with the additional attributes ``id`` and
254 ``rev``:
256 >>> doc.id, doc.rev #doctest: +ELLIPSIS
257 ('...', ...)
258 >>> doc['type']
259 'Person'
260 >>> doc['name']
261 'John Doe'
263 To update an existing document, you use item access, too:
265 >>> doc['name'] = 'Mary Jane'
266 >>> db[doc.id] = doc
268 The `create()` method creates a document with a random ID generated by
269 CouchDB (which is not recommended). If you want to explicitly specify the
270 ID, you'd use item access just as with updating:
272 >>> db['JohnDoe'] = {'type': 'person', 'name': 'John Doe'}
274 >>> 'JohnDoe' in db
275 True
276 >>> len(db)
279 >>> del server['python-tests']
282 def __init__(self, uri, name=None, http=None):
283 self.resource = Resource(http, uri)
284 self._name = name
286 def __repr__(self):
287 return '<%s %r>' % (type(self).__name__, self.name)
289 def __contains__(self, id):
290 """Return whether the database contains a document with the specified
293 :param id: the document ID
294 :return: `True` if a document with the ID exists, `False` otherwise
296 try:
297 self.resource.head(id)
298 return True
299 except ResourceNotFound:
300 return False
302 def __iter__(self):
303 """Return the IDs of all documents in the database."""
304 return iter([item.id for item in self.view('_all_docs')])
306 def __len__(self):
307 """Return the number of documents in the database."""
308 resp, data = self.resource.get()
309 return data['doc_count']
311 def __nonzero__(self):
312 """Return whether the database is available."""
313 try:
314 self.resource.head()
315 return True
316 except:
317 return False
319 def __delitem__(self, id):
320 """Remove the document with the specified ID from the database.
322 :param id: the document ID
324 resp, data = self.resource.head(id)
325 self.resource.delete(id, rev=resp['etag'].strip('"'))
327 def __getitem__(self, id):
328 """Return the document with the specified ID.
330 :param id: the document ID
331 :return: a `Row` object representing the requested document
332 :rtype: `Document`
334 resp, data = self.resource.get(id)
335 return Document(data)
337 def __setitem__(self, id, content):
338 """Create or update a document with the specified ID.
340 :param id: the document ID
341 :param content: the document content; either a plain dictionary for
342 new documents, or a `Row` object for existing
343 documents
345 resp, data = self.resource.put(id, content=content)
346 content.update({'_id': data['id'], '_rev': data['rev']})
348 @property
349 def name(self):
350 """The name of the database.
352 Note that this may require a request to the server unless the name has
353 already been cached by the `info()` method.
355 :type: basestring
357 if self._name is None:
358 self.info()
359 return self._name
361 def create(self, data):
362 """Create a new document in the database with a random ID that is
363 generated by the server.
365 Note that it is generally better to avoid the `create()` method and
366 instead generate document IDs on the client side. This is due to the
367 fact that the underlying HTTP ``POST`` method is not idempotent, and
368 an automatic retry due to a problem somewhere on the networking stack
369 may cause multiple documents being created in the database.
371 To avoid such problems you can generate a UUID on the client side.
372 Python (since version 2.5) comes with a ``uuid`` module that can be
373 used for this::
375 from uuid import uuid4
376 doc_id = uuid4().hex
377 db[doc_id] = {'type': 'person', 'name': 'John Doe'}
379 :param data: the data to store in the document
380 :return: the ID of the created document
381 :rtype: `unicode`
383 resp, data = self.resource.post(content=data)
384 return data['id']
386 def compact(self):
387 """Compact the database.
389 This will try to prune all revisions from the database.
391 :return: a boolean to indicate whether the compaction was initiated
392 successfully
393 :rtype: `bool`
395 resp, data = self.resource.post('_compact')
396 return data['ok']
398 def copy(self, src, dest):
399 """Copy the given document to create a new document.
401 :param src: the ID of the document to copy, or a dictionary or
402 `Document` object representing the source document.
403 :param dest: either the destination document ID as string, or a
404 dictionary or `Document` instance of the document that
405 should be overwritten.
406 :return: the new revision of the destination document
407 :rtype: `str`
408 :since: 0.6
410 if not isinstance(src, basestring):
411 if not isinstance(src, dict):
412 if hasattr(src, 'items'):
413 src = src.items()
414 else:
415 raise TypeError('expected dict or string, got %s' %
416 type(src))
417 src = src['_id']
419 if not isinstance(dest, basestring):
420 if not isinstance(dest, dict):
421 if hasattr(dest, 'items'):
422 dest = dest.items()
423 else:
424 raise TypeError('expected dict or string, got %s' %
425 type(dest))
426 if '_rev' in dest:
427 dest = '%s?%s' % (unicode_quote(dest['_id']),
428 unicode_urlencode({'rev': dest['_rev']}))
429 else:
430 dest = unicode_quote(dest['_id'])
432 resp, data = self.resource._request('COPY', src,
433 headers={'Destination': dest})
434 return data['rev']
436 def delete(self, doc):
437 """Delete the given document from the database.
439 Use this method in preference over ``__del__`` to ensure you're
440 deleting the revision that you had previously retrieved. In the case
441 the document has been updated since it was retrieved, this method will
442 raise a `ResourceConflict` exception.
444 >>> server = Server('http://localhost:5984/')
445 >>> db = server.create('python-tests')
447 >>> doc = dict(type='Person', name='John Doe')
448 >>> db['johndoe'] = doc
449 >>> doc2 = db['johndoe']
450 >>> doc2['age'] = 42
451 >>> db['johndoe'] = doc2
452 >>> db.delete(doc)
453 Traceback (most recent call last):
455 ResourceConflict: ('conflict', 'Document update conflict.')
457 >>> del server['python-tests']
459 :param doc: a dictionary or `Document` object holding the document data
460 :raise ResourceConflict: if the document was updated in the database
461 :since: 0.4.1
463 self.resource.delete(doc['_id'], rev=doc['_rev'])
465 def get(self, id, default=None, **options):
466 """Return the document with the specified ID.
468 :param id: the document ID
469 :param default: the default value to return when the document is not
470 found
471 :return: a `Row` object representing the requested document, or `None`
472 if no document with the ID was found
473 :rtype: `Document`
475 try:
476 resp, data = self.resource.get(id, **options)
477 except ResourceNotFound:
478 return default
479 else:
480 return Document(data)
482 def revisions(self, id, **options):
483 """Return all available revisions of the given document.
485 :param id: the document ID
486 :return: an iterator over Document objects, each a different revision,
487 in reverse chronological order, if any were found
489 try:
490 resp, data = self.resource.get(id, revs=True)
491 except ResourceNotFound:
492 return
494 startrev = data['_revisions']['start']
495 for index, rev in enumerate(data['_revisions']['ids']):
496 options['rev'] = '%d-%s' % (startrev - index, rev)
497 revision = self.get(id, **options)
498 if revision is None:
499 return
500 yield revision
502 def info(self):
503 """Return information about the database as a dictionary.
505 The returned dictionary exactly corresponds to the JSON response to
506 a ``GET`` request on the database URI.
508 :return: a dictionary of database properties
509 :rtype: ``dict``
510 :since: 0.4
512 resp, data = self.resource.get()
513 self._name = data['db_name']
514 return data
516 def delete_attachment(self, doc, filename):
517 """Delete the specified attachment.
519 Note that the provided `doc` is required to have a ``_rev`` field.
520 Thus, if the `doc` is based on a view row, the view row would need to
521 include the ``_rev`` field.
523 :param doc: the dictionary or `Document` object representing the
524 document that the attachment belongs to
525 :param filename: the name of the attachment file
526 :since: 0.4.1
528 resp, data = self.resource(doc['_id']).delete(filename, rev=doc['_rev'])
529 doc['_rev'] = data['rev']
531 def get_attachment(self, id_or_doc, filename, default=None):
532 """Return an attachment from the specified doc id and filename.
534 :param id_or_doc: either a document ID or a dictionary or `Document`
535 object representing the document that the attachment
536 belongs to
537 :param filename: the name of the attachment file
538 :param default: default value to return when the document or attachment
539 is not found
540 :return: the content of the attachment as a string, or the value of the
541 `default` argument if the attachment is not found
542 :since: 0.4.1
544 if isinstance(id_or_doc, basestring):
545 id = id_or_doc
546 else:
547 id = id_or_doc['_id']
548 try:
549 resp, data = self.resource(id).get(filename)
550 return data
551 except ResourceNotFound:
552 return default
554 def put_attachment(self, doc, content, filename=None, content_type=None):
555 """Create or replace an attachment.
557 Note that the provided `doc` is required to have a ``_rev`` field. Thus,
558 if the `doc` is based on a view row, the view row would need to include
559 the ``_rev`` field.
561 :param doc: the dictionary or `Document` object representing the
562 document that the attachment should be added to
563 :param content: the content to upload, either a file-like object or
564 a string
565 :param filename: the name of the attachment file; if omitted, this
566 function tries to get the filename from the file-like
567 object passed as the `content` argument value
568 :param content_type: content type of the attachment; if omitted, the
569 MIME type is guessed based on the file name
570 extension
571 :since: 0.4.1
573 if hasattr(content, 'read'):
574 content = content.read()
575 if filename is None:
576 if hasattr(content, 'name'):
577 filename = content.name
578 else:
579 raise ValueError('no filename specified for attachment')
580 if content_type is None:
581 content_type = ';'.join(filter(None, mimetypes.guess_type(filename)))
583 resp, data = self.resource(doc['_id']).put(filename, content=content,
584 headers={
585 'Content-Type': content_type
586 }, rev=doc['_rev'])
587 doc['_rev'] = data['rev']
589 def query(self, map_fun, reduce_fun=None, language='javascript',
590 wrapper=None, **options):
591 """Execute an ad-hoc query (a "temp view") against the database.
593 >>> server = Server('http://localhost:5984/')
594 >>> db = server.create('python-tests')
595 >>> db['johndoe'] = dict(type='Person', name='John Doe')
596 >>> db['maryjane'] = dict(type='Person', name='Mary Jane')
597 >>> db['gotham'] = dict(type='City', name='Gotham City')
598 >>> map_fun = '''function(doc) {
599 ... if (doc.type == 'Person')
600 ... emit(doc.name, null);
601 ... }'''
602 >>> for row in db.query(map_fun):
603 ... print row.key
604 John Doe
605 Mary Jane
607 >>> for row in db.query(map_fun, descending=True):
608 ... print row.key
609 Mary Jane
610 John Doe
612 >>> for row in db.query(map_fun, key='John Doe'):
613 ... print row.key
614 John Doe
616 >>> del server['python-tests']
618 :param map_fun: the code of the map function
619 :param reduce_fun: the code of the reduce function (optional)
620 :param language: the language of the functions, to determine which view
621 server to use
622 :param wrapper: an optional callable that should be used to wrap the
623 result rows
624 :param options: optional query string parameters
625 :return: the view reults
626 :rtype: `ViewResults`
628 return TemporaryView(uri(self.resource.uri, '_temp_view'), map_fun,
629 reduce_fun, language=language, wrapper=wrapper,
630 http=self.resource.http)(**options)
632 def update(self, documents, **options):
633 """Perform a bulk update or insertion of the given documents using a
634 single HTTP request.
636 >>> server = Server('http://localhost:5984/')
637 >>> db = server.create('python-tests')
638 >>> for doc in db.update([
639 ... Document(type='Person', name='John Doe'),
640 ... Document(type='Person', name='Mary Jane'),
641 ... Document(type='City', name='Gotham City')
642 ... ]):
643 ... print repr(doc) #doctest: +ELLIPSIS
644 (True, '...', '...')
645 (True, '...', '...')
646 (True, '...', '...')
648 >>> del server['python-tests']
650 The return value of this method is a list containing a tuple for every
651 element in the `documents` sequence. Each tuple is of the form
652 ``(success, docid, rev_or_exc)``, where ``success`` is a boolean
653 indicating whether the update succeeded, ``docid`` is the ID of the
654 document, and ``rev_or_exc`` is either the new document revision, or
655 an exception instance (e.g. `ResourceConflict`) if the update failed.
657 If an object in the documents list is not a dictionary, this method
658 looks for an ``items()`` method that can be used to convert the object
659 to a dictionary. Effectively this means you can also use this method
660 with `schema.Document` objects.
662 :param documents: a sequence of dictionaries or `Document` objects, or
663 objects providing a ``items()`` method that can be
664 used to convert them to a dictionary
665 :return: an iterable over the resulting documents
666 :rtype: ``list``
668 :since: version 0.2
670 docs = []
671 for doc in documents:
672 if isinstance(doc, dict):
673 docs.append(doc)
674 elif hasattr(doc, 'items'):
675 docs.append(dict(doc.items()))
676 else:
677 raise TypeError('expected dict, got %s' % type(doc))
679 content = options
680 content.update(docs=docs)
681 resp, data = self.resource.post('_bulk_docs', content=content)
683 results = []
684 for idx, result in enumerate(data):
685 if 'error' in result:
686 if result['error'] == 'conflict':
687 exc_type = ResourceConflict
688 else:
689 # XXX: Any other error types mappable to exceptions here?
690 exc_type = ServerError
691 results.append((False, result['id'],
692 exc_type(result['reason'])))
693 else:
694 doc = documents[idx]
695 if isinstance(doc, dict): # XXX: Is this a good idea??
696 doc.update({'_id': result['id'], '_rev': result['rev']})
697 results.append((True, result['id'], result['rev']))
699 return results
701 def view(self, name, wrapper=None, **options):
702 """Execute a predefined view.
704 >>> server = Server('http://localhost:5984/')
705 >>> db = server.create('python-tests')
706 >>> db['gotham'] = dict(type='City', name='Gotham City')
708 >>> for row in db.view('_all_docs'):
709 ... print row.id
710 gotham
712 >>> del server['python-tests']
714 :param name: the name of the view; for custom views, use the format
715 ``design_docid/viewname``, that is, the document ID of the
716 design document and the name of the view, separated by a
717 slash
718 :param wrapper: an optional callable that should be used to wrap the
719 result rows
720 :param options: optional query string parameters
721 :return: the view results
722 :rtype: `ViewResults`
724 if not name.startswith('_'):
725 design, name = name.split('/', 1)
726 name = '/'.join(['_design', design, '_view', name])
727 return PermanentView(uri(self.resource.uri, *name.split('/')), name,
728 wrapper=wrapper,
729 http=self.resource.http)(**options)
732 class Document(dict):
733 """Representation of a document in the database.
735 This is basically just a dictionary with the two additional properties
736 `id` and `rev`, which contain the document ID and revision, respectively.
739 def __repr__(self):
740 return '<%s %r@%r %r>' % (type(self).__name__, self.id, self.rev,
741 dict([(k,v) for k,v in self.items()
742 if k not in ('_id', '_rev')]))
744 @property
745 def id(self):
746 """The document ID.
748 :type: basestring
750 return self['_id']
752 @property
753 def rev(self):
754 """The document revision.
756 :type: basestring
758 return self['_rev']
761 class View(object):
762 """Abstract representation of a view or query."""
764 def __init__(self, uri, wrapper=None, http=None):
765 self.resource = Resource(http, uri)
766 self.wrapper = wrapper
768 def __call__(self, **options):
769 return ViewResults(self, options)
771 def __iter__(self):
772 return self()
774 def _encode_options(self, options):
775 retval = {}
776 for name, value in options.items():
777 if name in ('key', 'startkey', 'endkey') \
778 or not isinstance(value, basestring):
779 value = json.encode(value)
780 retval[name] = value
781 return retval
783 def _exec(self, options):
784 raise NotImplementedError
787 class PermanentView(View):
788 """Representation of a permanent view on the server."""
790 def __init__(self, uri, name, wrapper=None, http=None):
791 View.__init__(self, uri, wrapper=wrapper, http=http)
792 self.name = name
794 def __repr__(self):
795 return '<%s %r>' % (type(self).__name__, self.name)
797 def _exec(self, options):
798 if 'keys' in options:
799 options = options.copy()
800 keys = {'keys': options.pop('keys')}
801 resp, data = self.resource.post(content=keys,
802 **self._encode_options(options))
803 else:
804 resp, data = self.resource.get(**self._encode_options(options))
805 return data
808 class TemporaryView(View):
809 """Representation of a temporary view."""
811 def __init__(self, uri, map_fun, reduce_fun=None,
812 language='javascript', wrapper=None, http=None):
813 View.__init__(self, uri, wrapper=wrapper, http=http)
814 if isinstance(map_fun, FunctionType):
815 map_fun = getsource(map_fun).rstrip('\n\r')
816 self.map_fun = dedent(map_fun.lstrip('\n\r'))
817 if isinstance(reduce_fun, FunctionType):
818 reduce_fun = getsource(reduce_fun).rstrip('\n\r')
819 if reduce_fun:
820 reduce_fun = dedent(reduce_fun.lstrip('\n\r'))
821 self.reduce_fun = reduce_fun
822 self.language = language
824 def __repr__(self):
825 return '<%s %r %r>' % (type(self).__name__, self.map_fun,
826 self.reduce_fun)
828 def _exec(self, options):
829 body = {'map': self.map_fun, 'language': self.language}
830 if self.reduce_fun:
831 body['reduce'] = self.reduce_fun
832 if 'keys' in options:
833 options = options.copy()
834 body['keys'] = options.pop('keys')
835 content = json.encode(body).encode('utf-8')
836 resp, data = self.resource.post(content=content, headers={
837 'Content-Type': 'application/json'
838 }, **self._encode_options(options))
839 return data
842 class ViewResults(object):
843 """Representation of a parameterized view (either permanent or temporary)
844 and the results it produces.
846 This class allows the specification of ``key``, ``startkey``, and
847 ``endkey`` options using Python slice notation.
849 >>> server = Server('http://localhost:5984/')
850 >>> db = server.create('python-tests')
851 >>> db['johndoe'] = dict(type='Person', name='John Doe')
852 >>> db['maryjane'] = dict(type='Person', name='Mary Jane')
853 >>> db['gotham'] = dict(type='City', name='Gotham City')
854 >>> map_fun = '''function(doc) {
855 ... emit([doc.type, doc.name], doc.name);
856 ... }'''
857 >>> results = db.query(map_fun)
859 At this point, the view has not actually been accessed yet. It is accessed
860 as soon as it is iterated over, its length is requested, or one of its
861 `rows`, `total_rows`, or `offset` properties are accessed:
863 >>> len(results)
866 You can use slices to apply ``startkey`` and/or ``endkey`` options to the
867 view:
869 >>> people = results[['Person']:['Person','ZZZZ']]
870 >>> for person in people:
871 ... print person.value
872 John Doe
873 Mary Jane
874 >>> people.total_rows, people.offset
875 (3, 1)
877 Use plain indexed notation (without a slice) to apply the ``key`` option.
878 Note that as CouchDB makes no claim that keys are unique in a view, this
879 can still return multiple rows:
881 >>> list(results[['City', 'Gotham City']])
882 [<Row id='gotham', key=['City', 'Gotham City'], value='Gotham City'>]
884 >>> del server['python-tests']
887 def __init__(self, view, options):
888 self.view = view
889 self.options = options
890 self._rows = self._total_rows = self._offset = None
892 def __repr__(self):
893 return '<%s %r %r>' % (type(self).__name__, self.view, self.options)
895 def __getitem__(self, key):
896 options = self.options.copy()
897 if type(key) is slice:
898 if key.start is not None:
899 options['startkey'] = key.start
900 if key.stop is not None:
901 options['endkey'] = key.stop
902 return ViewResults(self.view, options)
903 else:
904 options['key'] = key
905 return ViewResults(self.view, options)
907 def __iter__(self):
908 wrapper = self.view.wrapper
909 for row in self.rows:
910 if wrapper is not None:
911 yield wrapper(row)
912 else:
913 yield row
915 def __len__(self):
916 return len(self.rows)
918 def _fetch(self):
919 data = self.view._exec(self.options)
920 self._rows = [Row(row) for row in data['rows']]
921 self._total_rows = data.get('total_rows')
922 self._offset = data.get('offset', 0)
924 @property
925 def rows(self):
926 """The list of rows returned by the view.
928 :type: `list`
930 if self._rows is None:
931 self._fetch()
932 return self._rows
934 @property
935 def total_rows(self):
936 """The total number of rows in this view.
938 This value is `None` for reduce views.
940 :type: `int` or ``NoneType`` for reduce views
942 if self._rows is None:
943 self._fetch()
944 return self._total_rows
946 @property
947 def offset(self):
948 """The offset of the results from the first row in the view.
950 This value is 0 for reduce views.
952 :type: `int`
954 if self._rows is None:
955 self._fetch()
956 return self._offset
959 class Row(dict):
960 """Representation of a row as returned by database views."""
962 def __repr__(self):
963 if self.id is None:
964 return '<%s key=%r, value=%r>' % (type(self).__name__, self.key,
965 self.value)
966 return '<%s id=%r, key=%r, value=%r>' % (type(self).__name__, self.id,
967 self.key, self.value)
969 @property
970 def id(self):
971 """The associated Document ID if it exists. Returns `None` when it
972 doesn't (reduce results).
974 return self.get('id')
976 @property
977 def key(self):
978 """The associated key."""
979 return self['key']
981 @property
982 def value(self):
983 """The associated value."""
984 return self['value']
986 @property
987 def doc(self):
988 """The associated document for the row. This is only present when the
989 view was accessed with ``include_docs=True`` as a query parameter,
990 otherwise this property will be `None`.
992 doc = self.get('doc')
993 if doc:
994 return Document(doc)
997 # Internals
1000 class Resource(object):
1002 def __init__(self, http, uri):
1003 if http is None:
1004 http = httplib2.Http()
1005 http.force_exception_to_status_code = False
1006 self.http = http
1007 self.uri = uri
1009 def __call__(self, path):
1010 return type(self)(self.http, uri(self.uri, path))
1012 def delete(self, path=None, headers=None, **params):
1013 return self._request('DELETE', path, headers=headers, **params)
1015 def get(self, path=None, headers=None, **params):
1016 return self._request('GET', path, headers=headers, **params)
1018 def head(self, path=None, headers=None, **params):
1019 return self._request('HEAD', path, headers=headers, **params)
1021 def post(self, path=None, content=None, headers=None, **params):
1022 return self._request('POST', path, content=content, headers=headers,
1023 **params)
1025 def put(self, path=None, content=None, headers=None, **params):
1026 return self._request('PUT', path, content=content, headers=headers,
1027 **params)
1029 def _request(self, method, path=None, content=None, headers=None,
1030 **params):
1031 from couchdb import __version__
1032 headers = headers or {}
1033 headers.setdefault('Accept', 'application/json')
1034 headers.setdefault('User-Agent', 'couchdb-python %s' % __version__)
1035 body = None
1036 if content is not None:
1037 if not isinstance(content, basestring):
1038 body = json.encode(content).encode('utf-8')
1039 headers.setdefault('Content-Type', 'application/json')
1040 else:
1041 body = content
1042 headers.setdefault('Content-Length', str(len(body)))
1044 def _make_request(retry=1):
1045 try:
1046 return self.http.request(uri(self.uri, path, **params), method,
1047 body=body, headers=headers)
1048 except socket.error, e:
1049 if retry > 0 and e.args[0] == 54: # reset by peer
1050 return _make_request(retry - 1)
1051 raise
1052 resp, data = _make_request()
1054 status_code = int(resp.status)
1055 if data and resp.get('content-type') == 'application/json':
1056 try:
1057 data = json.decode(data)
1058 except ValueError:
1059 pass
1061 if status_code >= 400:
1062 if type(data) is dict:
1063 error = (data.get('error'), data.get('reason'))
1064 else:
1065 error = data
1066 if status_code == 404:
1067 raise ResourceNotFound(error)
1068 elif status_code == 409:
1069 raise ResourceConflict(error)
1070 elif status_code == 412:
1071 raise PreconditionFailed(error)
1072 else:
1073 raise ServerError((status_code, error))
1075 return resp, data
1078 def uri(base, *path, **query):
1079 """Assemble a uri based on a base, any number of path segments, and query
1080 string parameters.
1082 >>> uri('http://example.org', '_all_dbs')
1083 'http://example.org/_all_dbs'
1085 A trailing slash on the uri base is handled gracefully:
1087 >>> uri('http://example.org/', '_all_dbs')
1088 'http://example.org/_all_dbs'
1090 And multiple positional arguments become path parts:
1092 >>> uri('http://example.org/', 'foo', 'bar')
1093 'http://example.org/foo/bar'
1095 All slashes within a path part are escaped:
1097 >>> uri('http://example.org/', 'foo/bar')
1098 'http://example.org/foo%2Fbar'
1099 >>> uri('http://example.org/', 'foo', '/bar/')
1100 'http://example.org/foo/%2Fbar%2F'
1102 if base and base.endswith('/'):
1103 base = base[:-1]
1104 retval = [base]
1106 # build the path
1107 path = '/'.join([''] + [unicode_quote(s) for s in path if s is not None])
1108 if path:
1109 retval.append(path)
1111 # build the query string
1112 params = []
1113 for name, value in query.items():
1114 if type(value) in (list, tuple):
1115 params.extend([(name, i) for i in value if i is not None])
1116 elif value is not None:
1117 if value is True:
1118 value = 'true'
1119 elif value is False:
1120 value = 'false'
1121 params.append((name, value))
1122 if params:
1123 retval.extend(['?', unicode_urlencode(params)])
1125 return ''.join(retval)
1128 def unicode_quote(string, safe=''):
1129 if isinstance(string, unicode):
1130 string = string.encode('utf-8')
1131 return quote(string, safe)
1134 def unicode_urlencode(data):
1135 if isinstance(data, dict):
1136 data = data.items()
1137 params = []
1138 for name, value in data:
1139 if isinstance(value, unicode):
1140 value = value.encode('utf-8')
1141 params.append((name, value))
1142 return urlencode(params)
1145 VALID_DB_NAME = re.compile(r'^[a-z][a-z0-9_$()+-/]*$')
1146 def validate_dbname(name):
1147 if not VALID_DB_NAME.match(name):
1148 raise ValueError('Invalid database name')
1149 return name