Skip to content

Commit ddadd82

Browse files
committed
Merge 0.4.x into stable.
--HG-- branch : stable
2 parents 4984fc2 + e494525 commit ddadd82

File tree

11 files changed

+357
-79
lines changed

11 files changed

+357
-79
lines changed

.hgtags

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ ce837fb130eec700edc912151d72e49446d0d299 0.2.0
66
e46bf4f09a7042420586a152eef6fd48bdf472ba 0.2.0
77
0f2eb5fe6a7a9d624f5204c77b49a4173f37cea7 0.3.0
88
20e6fcee4b68d03a7c29740dbbfad774a386aac2 0.3.0
9+
a65367e0db916531d91e05dadce7f7da2f4227aa 0.4.0
10+
7b399fb6bdb40e92eb59dfbc89412bb3ea3d903b 0.4.0
11+
37cf9b4d017deeeb4417274afb53faac058b2a60 0.4.0
12+
4db1825714884019b25822c36663a2eefcb5c9d9 0.4.0

ChangeLog.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
Version 0.4
2+
http://couchdb-python.googlecode.com/svn/tags/0.4.0
3+
(Jun 28, 2008, from branches/0.4.x)
4+
5+
* Updated for compatibility with CouchDB 0.8.0
6+
* Added command-line scripts for importing/exporting databases.
7+
* The `Database.update()` function will now actually perform the `POST`
8+
request even when you do not iterate over the results (issue 5).
9+
* The `_view` prefix can now be omitted when specifying view names.
10+
11+
112
Version 0.3
213
http://couchdb-python.googlecode.com/svn/tags/0.3.0
314
(Feb 6, 2008, from branches/0.3.x)

couchdb/client.py

Lines changed: 94 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,18 @@
2828
import re
2929
import simplejson as json
3030

31-
__all__ = ['ResourceNotFound', 'ResourceConflict', 'ServerError', 'Server',
32-
'Database', 'Document', 'ViewResults', 'Row']
31+
__all__ = ['PreconditionFailed', 'ResourceNotFound', 'ResourceConflict',
32+
'ServerError', 'Server', 'Database', 'Document', 'ViewResults',
33+
'Row']
3334
__docformat__ = 'restructuredtext en'
3435

3536

37+
class PreconditionFailed(Exception):
38+
"""Exception raised when a 412 HTTP error is received in response to a
39+
request.
40+
"""
41+
42+
3643
class ResourceNotFound(Exception):
3744
"""Exception raised when a 404 HTTP error is received in response to a
3845
request.
@@ -84,7 +91,7 @@ def __init__(self, uri):
8491
:param uri: the URI of the server (for example
8592
``http://localhost:5984/``)
8693
"""
87-
self.resource = Resource(httplib2.Http(), uri)
94+
self.resource = Resource(None, uri)
8895

8996
def __contains__(self, name):
9097
"""Return whether the server contains a database with the specified
@@ -196,8 +203,6 @@ class Database(object):
196203
True
197204
>>> len(db)
198205
2
199-
>>> list(db) #doctest: +ELLIPSIS
200-
[u'...', u'JohnDoe']
201206
202207
>>> del server['python-tests']
203208
"""
@@ -260,7 +265,7 @@ def __setitem__(self, id, content):
260265

261266
def _get_name(self):
262267
if self._name is None:
263-
self._name = self.resource.get()['db_name']
268+
self._name = self.info()['db_name']
264269
return self._name
265270
name = property(_get_name)
266271

@@ -291,43 +296,59 @@ def get(self, id, default=None, **options):
291296
except ResourceNotFound:
292297
return default
293298

294-
def query(self, code, content_type='text/javascript', wrapper=None,
295-
**options):
299+
def info(self):
300+
"""Return information about the database as a dictionary.
301+
302+
The returned dictionary exactly corresponds to the JSON response to
303+
a ``GET`` request on the database URI.
304+
305+
:return: a dictionary of database properties
306+
:rtype: ``dict``
307+
:since: 0.4
308+
"""
309+
return self.resource.get()
310+
311+
def query(self, map_fun, reduce_fun=None, language='javascript',
312+
wrapper=None, **options):
296313
"""Execute an ad-hoc query (a "temp view") against the database.
297314
298315
>>> server = Server('http://localhost:5984/')
299316
>>> db = server.create('python-tests')
300317
>>> db['johndoe'] = dict(type='Person', name='John Doe')
301318
>>> db['maryjane'] = dict(type='Person', name='Mary Jane')
302319
>>> db['gotham'] = dict(type='City', name='Gotham City')
303-
>>> code = '''function(doc) {
304-
... if (doc.type=='Person')
305-
... map(doc.name, null);
320+
>>> map_fun = '''function(doc) {
321+
... if (doc.type == 'Person')
322+
... emit(doc.name, null);
306323
... }'''
307-
>>> for row in db.query(code):
324+
>>> for row in db.query(map_fun):
308325
... print row.key
309326
John Doe
310327
Mary Jane
311328
312-
>>> for row in db.query(code, descending=True):
329+
>>> for row in db.query(map_fun, descending=True):
313330
... print row.key
314331
Mary Jane
315332
John Doe
316333
317-
>>> for row in db.query(code, key='John Doe'):
334+
>>> for row in db.query(map_fun, key='John Doe'):
318335
... print row.key
319336
John Doe
320337
321338
>>> del server['python-tests']
322339
323-
:param code: the code of the view function
324-
:param content_type: the MIME type of the code, which determines the
325-
language the view function is written in
340+
:param map_fun: the code of the map function
341+
:param reduce_fun: the code of the reduce function (optional)
342+
:param language: the language of the functions, to determine which view
343+
server to use
344+
:param wrapper: an optional callable that should be used to wrap the
345+
result rows
346+
:param options: optional query string parameters
326347
:return: the view reults
327348
:rtype: `ViewResults`
328349
"""
329-
return TemporaryView(uri(self.resource.uri, '_temp_view'), code,
330-
content_type=content_type, wrapper=wrapper,
350+
return TemporaryView(uri(self.resource.uri, '_temp_view'), map_fun,
351+
reduce_fun, language=language, wrapper=wrapper,
331352
http=self.resource.http)(**options)
332353

333354
def update(self, documents):
@@ -355,12 +376,14 @@ def update(self, documents):
355376
:since: version 0.2
356377
"""
357378
documents = list(documents)
358-
data = self.resource.post('_bulk_docs', content=documents)
359-
for idx, result in enumerate(data['results']):
360-
assert 'ok' in result # FIXME: how should error handling work here?
361-
doc = documents[idx]
362-
doc.update({'_id': result['id'], '_rev': result['rev']})
363-
yield doc
379+
data = self.resource.post('_bulk_docs', content={'docs': documents})
380+
assert data['ok'] # FIXME: Should probably raise a proper exception
381+
def _update():
382+
for idx, result in enumerate(data['new_revs']):
383+
doc = documents[idx]
384+
doc.update({'_id': result['id'], '_rev': result['rev']})
385+
yield doc
386+
return _update()
364387

365388
def view(self, name, wrapper=None, **options):
366389
"""Execute a predefined view.
@@ -375,9 +398,16 @@ def view(self, name, wrapper=None, **options):
375398
376399
>>> del server['python-tests']
377400
401+
:param name: the name of the view, including the ``_view/design_docid``
402+
prefix for custom views
403+
:param wrapper: an optional callable that should be used to wrap the
404+
result rows
405+
:param options: optional query string parameters
378406
:return: the view results
379407
:rtype: `ViewResults`
380408
"""
409+
if not name.startswith('_'):
410+
name = '_view/' + name
381411
return PermanentView(uri(self.resource.uri, *name.split('/')), name,
382412
wrapper=wrapper,
383413
http=self.resource.http)(**options)
@@ -390,9 +420,6 @@ class Document(dict):
390420
`id` and `rev`, which contain the document ID and revision, respectively.
391421
"""
392422

393-
def __init__(self, *args, **kwargs):
394-
dict.__init__(self, *args, **kwargs)
395-
396423
def __repr__(self):
397424
return '<%s %r@%r %r>' % (type(self).__name__, self.id, self.rev,
398425
dict([(k,v) for k,v in self.items()
@@ -420,7 +447,7 @@ def _encode_options(self, options):
420447
for name, value in options.items():
421448
if name in ('key', 'startkey', 'endkey') \
422449
or not isinstance(value, basestring):
423-
value = json.dumps(value)
450+
value = json.dumps(value, ensure_ascii=False)
424451
retval[name] = value
425452
return retval
426453

@@ -446,21 +473,25 @@ def _exec(self, options):
446473
class TemporaryView(View):
447474
"""Representation of a temporary view."""
448475

449-
def __init__(self, uri, code=None, content_type='text/javascript',
450-
wrapper=None, http=None):
476+
def __init__(self, uri, map_fun=None, reduce_fun=None,
477+
language='javascript', wrapper=None, http=None):
451478
View.__init__(self, uri, wrapper=wrapper, http=http)
452479
self.resource = Resource(http, uri)
453-
self.code = code
454-
self.content_type = content_type
480+
self.map_fun = map_fun
481+
self.reduce_fun = reduce_fun
482+
self.language = language
455483

456484
def __repr__(self):
457-
return '<%s %r>' % (type(self).__name__, self.code)
485+
return '<%s %r %r>' % (type(self).__name__, self.map_fun,
486+
self.reduce_fun)
458487

459488
def _exec(self, options):
460-
headers = {}
461-
if self.content_type:
462-
headers['Content-Type'] = self.content_type
463-
return self.resource.post(content=self.code, headers=headers,
489+
body = {'map': self.map_fun, 'language': self.language}
490+
if self.reduce_fun:
491+
body['reduce'] = self.reduce_fun
492+
content = json.dumps(body, ensure_ascii=False).encode('utf-8')
493+
return self.resource.post(content=content,
494+
headers={'Content-Type': 'application/json'},
464495
**self._encode_options(options))
465496

466497

@@ -476,10 +507,10 @@ class ViewResults(object):
476507
>>> db['johndoe'] = dict(type='Person', name='John Doe')
477508
>>> db['maryjane'] = dict(type='Person', name='Mary Jane')
478509
>>> db['gotham'] = dict(type='City', name='Gotham City')
479-
>>> code = '''function(doc) {
480-
... map([doc.type, doc.name], doc.name);
510+
>>> map_fun = '''function(doc) {
511+
... emit([doc.type, doc.name], doc.name);
481512
... }'''
482-
>>> results = db.query(code)
513+
>>> results = db.query(map_fun)
483514
484515
At this point, the view has not actually been accessed yet. It is accessed
485516
as soon as it is iterated over, its length is requested, or one of its
@@ -540,8 +571,9 @@ def __len__(self):
540571

541572
def _fetch(self):
542573
data = self.view._exec(self.options)
543-
self._rows = [Row(r['id'], r['key'], r['value']) for r in data['rows']]
544-
self._total_rows = data['total_rows']
574+
self._rows = [Row(r.get('id'), r['key'], r['value'])
575+
for r in data['rows']]
576+
self._total_rows = data.get('total_rows')
545577
self._offset = data.get('offset', 0)
546578

547579
def _get_rows(self):
@@ -555,22 +587,26 @@ def _get_rows(self):
555587
""")
556588

557589
def _get_total_rows(self):
558-
if self._total_rows is None:
590+
if self._rows is None:
559591
self._fetch()
560592
return self._total_rows
561593
total_rows = property(_get_total_rows, doc="""\
562594
The total number of rows in this view.
563595
564-
:type: `int`
596+
This value is `None` for reduce views.
597+
598+
:type: `int` or ``NoneType`` for reduce views
565599
""")
566600

567601
def _get_offset(self):
568-
if self._offset is None:
602+
if self._rows is None:
569603
self._fetch()
570604
return self._offset
571605
offset = property(_get_offset, doc="""\
572606
The offset of the results from the first row in the view.
573607
608+
This value is 0 for reduce views.
609+
574610
:type: `int`
575611
""")
576612

@@ -579,11 +615,14 @@ class Row(object):
579615
"""Representation of a row as returned by database views."""
580616

581617
def __init__(self, id, key, value):
582-
self.id = id #: The document ID
618+
self.id = id #: The document ID, or `None` for rows in a reduce view
583619
self.key = key #: The key of the row
584620
self.value = value #: The value of the row
585621

586622
def __repr__(self):
623+
if self.id is None:
624+
return '<%s key=%r, value=%r>' % (type(self).__name__, self.key,
625+
self.value)
587626
return '<%s id=%r, key=%r, value=%r>' % (type(self).__name__, self.id,
588627
self.key, self.value)
589628

@@ -624,9 +663,9 @@ def _request(self, method, path=None, content=None, headers=None,
624663
headers.setdefault('Accept', 'application/json')
625664
headers.setdefault('User-Agent', 'couchdb-python %s' % __version__)
626665
body = None
627-
if content:
666+
if content is not None:
628667
if not isinstance(content, basestring):
629-
body = json.dumps(content)
668+
body = json.dumps(content, ensure_ascii=False).encode('utf-8')
630669
headers.setdefault('Content-Type', 'application/json')
631670
else:
632671
body = content
@@ -649,6 +688,8 @@ def _request(self, method, path=None, content=None, headers=None,
649688
raise ResourceNotFound(error)
650689
elif status_code == 409:
651690
raise ResourceConflict(error)
691+
elif status_code == 412:
692+
raise PreconditionFailed(error)
652693
else:
653694
raise ServerError((status_code, error))
654695

@@ -679,6 +720,10 @@ def uri(base, *path, **query):
679720
if type(value) in (list, tuple):
680721
params.extend([(name, i) for i in value if i is not None])
681722
elif value is not None:
723+
if value is True:
724+
value = 'true'
725+
elif value is False:
726+
value = 'false'
682727
params.append((name, value))
683728
if params:
684729
retval.extend(['?', unicode_urlencode(params)])

0 commit comments

Comments
 (0)