1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2007-2009 Christopher Lenz
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'})
23 >>> del server['python-tests']
28 from urllib
import quote
, urlencode
29 from types
import FunctionType
30 from inspect
import getsource
31 from textwrap
import dedent
35 from couchdb
import json
37 __all__
= ['PreconditionFailed', 'ResourceNotFound', 'ResourceConflict',
38 'ServerError', 'Server', 'Database', 'Document', 'ViewResults',
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
52 class ResourceNotFound(Exception):
53 """Exception raised when a 404 HTTP error is received in response to a
58 class ResourceConflict(Exception):
59 """Exception raised when a 409 HTTP error is received in response to a
64 class ServerError(Exception):
65 """Exception raised when an unexpected HTTP error is received in response
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
79 New databases can be created using the `create` method:
81 >>> db = server.create('python-tests')
83 <Database 'python-tests'>
85 You can access existing databases using item access, specifying the database
88 >>> db = server['python-tests']
92 Databases can be deleted using a ``del`` statement:
94 >>> del server['python-tests']
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
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
116 :param name: the database name
117 :return: `True` if a database with the name exists, `False` otherwise
120 self
.resource
.head(validate_dbname(name
))
122 except ResourceNotFound
:
126 """Iterate over the names of all databases."""
127 resp
, data
= self
.resource
.get('_all_dbs')
131 """Return the number of databases."""
132 resp
, data
= self
.resource
.get('_all_dbs')
135 def __nonzero__(self
):
136 """Return whether the server is available."""
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
158 :param name: the name of the database
159 :return: a `Database` object representing the 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
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.
178 resp
, data
= self
.resource
.get('_config')
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.
189 resp
, data
= self
.resource
.get()
190 return data
['version']
193 """Database statistics."""
194 resp
, data
= self
.resource
.get('_stats')
198 """A list of tasks currently active on the server."""
199 resp
, data
= self
.resource
.get('_active_tasks')
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
208 :raise PreconditionFailed: if a database with that name already exists
210 self
.resource
.put(validate_dbname(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
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
}
231 resp
, data
= self
.resource
.post('_replicate', 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
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
256 >>> doc.id, doc.rev #doctest: +ELLIPSIS
263 To update an existing document, you use item access, too:
265 >>> doc['name'] = 'Mary Jane'
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'}
279 >>> del server['python-tests']
282 def __init__(self
, uri
, name
=None, http
=None):
283 self
.resource
= Resource(http
, uri
)
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
297 self
.resource
.head(id)
299 except ResourceNotFound
:
303 """Return the IDs of all documents in the database."""
304 return iter([item
.id for item
in self
.view('_all_docs')])
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."""
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
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
345 resp
, data
= self
.resource
.put(id, content
=content
)
346 content
.update({'_id': data
['id'], '_rev': data
['rev']})
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.
357 if self
._name
is None:
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
375 from uuid import uuid4
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
383 resp
, data
= self
.resource
.post(content
=data
)
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
395 resp
, data
= self
.resource
.post('_compact')
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
410 if not isinstance(src
, basestring
):
411 if not isinstance(src
, dict):
412 if hasattr(src
, 'items'):
415 raise TypeError('expected dict or string, got %s' %
419 if not isinstance(dest
, basestring
):
420 if not isinstance(dest
, dict):
421 if hasattr(dest
, 'items'):
424 raise TypeError('expected dict or string, got %s' %
427 dest
= '%s?%s' % (unicode_quote(dest
['_id']),
428 unicode_urlencode({'rev': dest
['_rev']}))
430 dest
= unicode_quote(dest
['_id'])
432 resp
, data
= self
.resource
._request
('COPY', src
,
433 headers
={'Destination': dest
})
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']
451 >>> db['johndoe'] = doc2
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
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
471 :return: a `Row` object representing the requested document, or `None`
472 if no document with the ID was found
476 resp
, data
= self
.resource
.get(id, **options
)
477 except ResourceNotFound
:
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
490 resp
, data
= self
.resource
.get(id, revs
=True)
491 except ResourceNotFound
:
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
)
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
512 resp
, data
= self
.resource
.get()
513 self
._name
= data
['db_name']
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
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
537 :param filename: the name of the attachment file
538 :param default: default value to return when the document or attachment
540 :return: the content of the attachment as a string, or the value of the
541 `default` argument if the attachment is not found
544 if isinstance(id_or_doc
, basestring
):
547 id = id_or_doc
['_id']
549 resp
, data
= self
.resource(id).get(filename
)
551 except ResourceNotFound
:
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
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
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
573 if hasattr(content
, 'read'):
574 content
= content
.read()
576 if hasattr(content
, 'name'):
577 filename
= content
.name
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
,
585 'Content-Type': content_type
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);
602 >>> for row in db.query(map_fun):
607 >>> for row in db.query(map_fun, descending=True):
612 >>> for row in db.query(map_fun, key='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
622 :param wrapper: an optional callable that should be used to wrap the
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
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')
643 ... print repr(doc) #doctest: +ELLIPSIS
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
671 for doc
in documents
:
672 if isinstance(doc
, dict):
674 elif hasattr(doc
, 'items'):
675 docs
.append(dict(doc
.items()))
677 raise TypeError('expected dict, got %s' % type(doc
))
680 content
.update(docs
=docs
)
681 resp
, data
= self
.resource
.post('_bulk_docs', content
=content
)
684 for idx
, result
in enumerate(data
):
685 if 'error' in result
:
686 if result
['error'] == 'conflict':
687 exc_type
= ResourceConflict
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'])))
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']))
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'):
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
718 :param wrapper: an optional callable that should be used to wrap the
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
,
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.
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')]))
754 """The document revision.
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
)
774 def _encode_options(self
, options
):
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
)
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
)
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
))
804 resp
, data
= self
.resource
.get(**self
._encode
_options
(options
))
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')
820 reduce_fun
= dedent(reduce_fun
.lstrip('\n\r'))
821 self
.reduce_fun
= reduce_fun
822 self
.language
= language
825 return '<%s %r %r>' % (type(self
).__name
__, self
.map_fun
,
828 def _exec(self
, options
):
829 body
= {'map': self
.map_fun
, 'language': self
.language
}
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
))
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);
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:
866 You can use slices to apply ``startkey`` and/or ``endkey`` options to the
869 >>> people = results[['Person']:['Person','ZZZZ']]
870 >>> for person in people:
871 ... print person.value
874 >>> people.total_rows, people.offset
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
):
889 self
.options
= options
890 self
._rows
= self
._total
_rows
= self
._offset
= None
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
)
905 return ViewResults(self
.view
, options
)
908 wrapper
= self
.view
.wrapper
909 for row
in self
.rows
:
910 if wrapper
is not None:
916 return len(self
.rows
)
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)
926 """The list of rows returned by the view.
930 if self
._rows
is None:
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:
944 return self
._total
_rows
948 """The offset of the results from the first row in the view.
950 This value is 0 for reduce views.
954 if self
._rows
is None:
960 """Representation of a row as returned by database views."""
964 return '<%s key=%r, value=%r>' % (type(self
).__name
__, self
.key
,
966 return '<%s id=%r, key=%r, value=%r>' % (type(self
).__name
__, self
.id,
967 self
.key
, self
.value
)
971 """The associated Document ID if it exists. Returns `None` when it
972 doesn't (reduce results).
974 return self
.get('id')
978 """The associated key."""
983 """The associated value."""
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')
1000 class Resource(object):
1002 def __init__(self
, http
, uri
):
1004 http
= httplib2
.Http()
1005 http
.force_exception_to_status_code
= False
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
,
1025 def put(self
, path
=None, content
=None, headers
=None, **params
):
1026 return self
._request
('PUT', path
, content
=content
, headers
=headers
,
1029 def _request(self
, method
, path
=None, content
=None, headers
=None,
1031 from couchdb
import __version__
1032 headers
= headers
or {}
1033 headers
.setdefault('Accept', 'application/json')
1034 headers
.setdefault('User-Agent', 'couchdb-python %s' % __version__
)
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')
1042 headers
.setdefault('Content-Length', str(len(body
)))
1044 def _make_request(retry
=1):
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)
1052 resp
, data
= _make_request()
1054 status_code
= int(resp
.status
)
1055 if data
and resp
.get('content-type') == 'application/json':
1057 data
= json
.decode(data
)
1061 if status_code
>= 400:
1062 if type(data
) is dict:
1063 error
= (data
.get('error'), data
.get('reason'))
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
)
1073 raise ServerError((status_code
, error
))
1078 def uri(base
, *path
, **query
):
1079 """Assemble a uri based on a base, any number of path segments, and query
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('/'):
1107 path
= '/'.join([''] + [unicode_quote(s
) for s
in path
if s
is not None])
1111 # build the query string
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:
1119 elif value
is False:
1121 params
.append((name
, value
))
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):
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')