Skip to content

Commit e7a5777

Browse files
committed
Merge 0.2.x into stable.
--HG-- branch : stable
2 parents 8b00307 + 292e392 commit e7a5777

File tree

9 files changed

+319
-141
lines changed

9 files changed

+319
-141
lines changed

.hgtags

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
96c3973c580212367e3d6ea5420bdfd100cd4879 0.1.0
22
68f07cd75cf8d6186736a54472b3baaa06ecba5d 0.1.0
33
8c1dad6b5a178f1ebdea55f18c980a52a94578e6 0.1.0
4+
82181e48019e85608e8128d8056f541565f54730 0.2.0
5+
ce837fb130eec700edc912151d72e49446d0d299 0.2.0
6+
e46bf4f09a7042420586a152eef6fd48bdf472ba 0.2.0

ChangeLog.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
Version 0.2
2+
http://couchdb-python.googlecode.com/svn/tags/0.2.0
3+
(Nov 21, 2007, from branches/0.2.x)
4+
5+
* Added __len__ and __iter__ to the `schema.Schema` class to iterate
6+
over and get the number of items in a document or compound field.
7+
* The "version" property of client.Server now returns a plain string
8+
instead of a tuple of ints.
9+
* The client library now identifies itself with a meaningful
10+
User-Agent string.
11+
* `schema.Document.store()` now returns the document object instance,
12+
instead of just the document ID.
13+
* The string representation of `schema.Document` objects is now more
14+
comprehensive.
15+
* Only the view parameters "key", "startkey", and "endkey" are JSON
16+
encoded, anything else is left alone.
17+
* Slashes in document IDs are now URL-quoted until CouchDB supports
18+
them.
19+
* Allow the content-type to be passed for temp views via
20+
`client.Database.query()` so that view languages other than
21+
Javascript can be used.
22+
* Added `client.Database.update()` method to bulk insert/update
23+
documents in a database.
24+
* The view-server script wrapper has been renamed to `couchpy`.
25+
* `couchpy` now supports `--help` and `--version` options.
26+
* Updated for compatibility with CouchDB release 0.7.0.
27+
28+
129
Version 0.1
230
http://couchdb-python.googlecode.com/svn/tags/0.1.0
331
(Sep 23, 2007, from branches/0.1.x)

couchdb/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@
77
# you should have received as part of this distribution.
88

99
from couchdb.client import *
10+
11+
try:
12+
__version__ = __import__('pkg_resources').get_distribution('CouchDB').version
13+
except:
14+
__version__ = '?'

couchdb/client.py

Lines changed: 104 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import simplejson as json
3030

3131
__all__ = ['ResourceNotFound', 'ResourceConflict', 'ServerError', 'Server',
32-
'Database', 'View']
32+
'Database', 'Document', 'View']
3333
__docformat__ = 'restructuredtext en'
3434

3535

@@ -46,17 +46,15 @@ class ResourceConflict(Exception):
4646

4747

4848
class ServerError(Exception):
49-
"""Exception raised when a 500 HTTP error is received in response to a
50-
request.
49+
"""Exception raised when an unexpected HTTP error is received in response
50+
to a request.
5151
"""
5252

5353

5454
class Server(object):
5555
"""Representation of a CouchDB server.
5656
5757
>>> server = Server('http://localhost:8888/')
58-
>>> server.version
59-
(0, 6, 4)
6058
6159
This class behaves like a dictionary of databases. For example, to get a
6260
list of database names on the server, you can simply iterate over the
@@ -133,16 +131,14 @@ def __getitem__(self, name):
133131
http=self.resource.http)
134132

135133
def _get_version(self):
136-
data = self.resource.get()
137-
version = data['version']
138-
return tuple([int(part) for part in version.split('.')])
134+
return self.resource.get()['version']
139135
version = property(_get_version, doc="""\
140136
The version number tuple for the CouchDB server.
141137
142138
Note that this results in a request being made, and can also be used
143139
to check for the availability of the server.
144140
145-
:type: `tuple`
141+
:type: `unicode`
146142
""")
147143

148144
def create(self, name):
@@ -172,7 +168,7 @@ class Database(object):
172168
173169
>>> doc = db[doc_id]
174170
>>> doc #doctest: +ELLIPSIS
175-
<Row u'...'@... {...}>
171+
<Document u'...'@... {...}>
176172
177173
Documents are represented as instances of the `Row` class, which is
178174
basically just a normal dictionary with the additional attributes ``id`` and
@@ -221,14 +217,14 @@ def __contains__(self, id):
221217
:return: `True` if a document with the ID exists, `False` otherwise
222218
"""
223219
try:
224-
self.resource.get(id) # FIXME: should use HEAD
220+
self.resource.head(id) # FIXME: should use HEAD
225221
return True
226222
except ResourceNotFound:
227223
return False
228224

229225
def __iter__(self):
230226
"""Return the IDs of all documents in the database."""
231-
return (item.id for item in self.view('_all_docs'))
227+
return iter([item.id for item in self.view('_all_docs')])
232228

233229
def __len__(self):
234230
"""Return the number of documents in the database."""
@@ -249,7 +245,7 @@ def __getitem__(self, id):
249245
:return: a `Row` object representing the requested document
250246
:rtype: `Row`
251247
"""
252-
return Row(self.resource.get(id))
248+
return Document(self.resource.get(id))
253249

254250
def __setitem__(self, id, content):
255251
"""Create or update a document with the specified ID.
@@ -259,9 +255,8 @@ def __setitem__(self, id, content):
259255
new documents, or a `Row` object for existing
260256
documents
261257
"""
262-
data = self.resource.put(id, content=content)
263-
content['_id'] = data['_id']
264-
content['_rev'] = data['_rev']
258+
result = self.resource.put(id, content=content)
259+
content.update({'_id': result['id'], '_rev': result['rev']})
265260

266261
def _get_name(self):
267262
if self._name is None:
@@ -279,10 +274,9 @@ def create(self, data):
279274
:return: the ID of the created document
280275
:rtype: `unicode`
281276
"""
282-
data = self.resource.post(content=data)
283-
return data['_id']
277+
return self.resource.post(content=data)['id']
284278

285-
def get(self, id, default=None):
279+
def get(self, id, default=None, **options):
286280
"""Return the document with the specified ID.
287281
288282
:param id: the document ID
@@ -293,12 +287,12 @@ def get(self, id, default=None):
293287
:rtype: `Row`
294288
"""
295289
try:
296-
return self[id]
290+
return Document(self.resource.get(id, **options))
297291
except ResourceNotFound:
298292
return default
299293

300-
def query(self, code, **options):
301-
"""Execute an ad-hoc query against the database.
294+
def query(self, code, content_type='text/javascript', **options):
295+
"""Execute an ad-hoc query (a "temp view") against the database.
302296
303297
>>> server = Server('http://localhost:8888/')
304298
>>> db = server.create('python-tests')
@@ -307,34 +301,73 @@ def query(self, code, **options):
307301
>>> db['gotham'] = dict(type='City', name='Gotham City')
308302
>>> code = '''function(doc) {
309303
... if (doc.type=='Person')
310-
... return {'key': doc.name};
304+
... map(doc.name, null);
311305
... }'''
312306
>>> for row in db.query(code):
313-
... print row['key']
307+
... print row.key
314308
John Doe
315309
Mary Jane
316310
317-
>>> for row in db.query(code, reverse=True):
318-
... print row['key']
311+
>>> for row in db.query(code, descending=True):
312+
... print row.key
319313
Mary Jane
320314
John Doe
321315
322316
>>> for row in db.query(code, key='John Doe'):
323-
... print row['key']
317+
... print row.key
324318
John Doe
325319
326320
>>> del server['python-tests']
327321
328322
:param code: the code of the view function
323+
:param content_type: the MIME type of the code, which determines the
324+
language the view function is written in
329325
:return: an iterable over the resulting `Row` objects
330326
:rtype: ``generator``
331327
"""
332-
json_options = {}
333328
for name, value in options.items():
334-
json_options[name] = json.dumps(value)
335-
data = self.resource.post('_temp_view', content=code, **json_options)
329+
if name in ('key', 'startkey', 'endkey') \
330+
or not isinstance(value, basestring):
331+
options[name] = json.dumps(value)
332+
headers = {}
333+
if content_type:
334+
headers['Content-Type'] = content_type
335+
data = self.resource.post('_temp_view', content=code, headers=headers,
336+
**options)
336337
for row in data['rows']:
337-
yield Row(row)
338+
yield Row(row['id'], row['key'], row['value'])
339+
340+
def update(self, documents):
341+
"""Perform a bulk update or insertion of the given documents using a
342+
single HTTP request.
343+
344+
>>> server = Server('http://localhost:8888/')
345+
>>> db = server.create('python-tests')
346+
>>> for doc in db.update([
347+
... Document(type='Person', name='John Doe'),
348+
... Document(type='Person', name='Mary Jane'),
349+
... Document(type='City', name='Gotham City')
350+
... ]):
351+
... print repr(doc) #doctest: +ELLIPSIS
352+
<Document u'...'@u'...' {'type': 'Person', 'name': 'John Doe'}>
353+
<Document u'...'@u'...' {'type': 'Person', 'name': 'Mary Jane'}>
354+
<Document u'...'@u'...' {'type': 'City', 'name': 'Gotham City'}>
355+
356+
>>> del server['python-tests']
357+
358+
:param documents: a sequence of dictionaries or `Document` objects
359+
:return: an iterable over the resulting documents
360+
:rtype: ``generator``
361+
362+
:since: version 0.2
363+
"""
364+
documents = list(documents)
365+
data = self.resource.post('_bulk_docs', content=documents)
366+
for idx, result in enumerate(data['results']):
367+
assert 'ok' in result # FIXME: how should error handling work here?
368+
doc = documents[idx]
369+
doc.update({'_id': result['id'], '_rev': result['rev']})
370+
yield doc
338371

339372
def view(self, name, **options):
340373
"""Execute a predefined view.
@@ -352,11 +385,30 @@ def view(self, name, **options):
352385
:return: a `View` object
353386
:rtype: `View`
354387
"""
355-
view = View(uri(self.resource.uri, name), name,
388+
view = View(uri(self.resource.uri, *name.split('/')), name,
356389
http=self.resource.http)
357390
return view(**options)
358391

359392

393+
class Document(dict):
394+
"""Representation of a document in the database.
395+
396+
This is basically just a dictionary with the two additional properties
397+
`id` and `rev`, which contain the document ID and revision, respectively.
398+
"""
399+
400+
def __init__(self, *args, **kwargs):
401+
dict.__init__(self, *args, **kwargs)
402+
403+
def __repr__(self):
404+
return '<%s %r@%r %r>' % (type(self).__name__, self.id, self.rev,
405+
dict([(k,v) for k,v in self.items()
406+
if k not in ('_id', '_rev')]))
407+
408+
id = property(lambda self: self['_id'])
409+
rev = property(lambda self: self['_rev'])
410+
411+
360412
class View(object):
361413
"""Representation of a permanent view on the server."""
362414

@@ -368,34 +420,33 @@ def __repr__(self):
368420
return '<%s %r>' % (type(self).__name__, self.name)
369421

370422
def __call__(self, **options):
371-
json_options = {}
372423
for name, value in options.items():
373-
json_options[name] = json.dumps(value)
374-
data = self.resource.get(**json_options)
424+
if name in ('key', 'startkey', 'endkey') \
425+
or not isinstance(value, basestring):
426+
options[name] = json.dumps(value)
427+
data = self.resource.get(**options)
375428
for row in data['rows']:
376-
yield Row(row)
429+
yield Row(row['id'], row['key'], row['value'])
377430

378431
def __iter__(self):
379432
return self()
380433

381434

382-
class Row(dict):
435+
class Row(object):
383436
"""Representation of a row as returned by database views.
384437
385438
This is basically just a dictionary with the two additional properties
386439
`id` and `rev`, which contain the document ID and revision, respectively.
387440
"""
388441

389-
def __init__(self, content):
390-
dict.__init__(self, content)
442+
def __init__(self, id, key, value):
443+
self.id = id
444+
self.key = key
445+
self.value = value
391446

392447
def __repr__(self):
393-
return '<%s %r@%r %r>' % (type(self).__name__, self.id, self.rev,
394-
dict([(k,v) for k,v in self.items()
395-
if k not in ('_id', '_rev')]))
396-
397-
id = property(lambda self: self.get('_id'))
398-
rev = property(lambda self: self.get('_rev'))
448+
return '<%s id=%r, key=%r, value=%r>' % (type(self).__name__, self.id,
449+
self.key, self.value)
399450

400451

401452
# Internals
@@ -429,7 +480,10 @@ def put(self, path=None, content=None, headers=None, **params):
429480

430481
def _request(self, method, path=None, content=None, headers=None,
431482
**params):
483+
from couchdb import __version__
432484
headers = headers or {}
485+
headers.setdefault('Accept', 'application/json')
486+
headers.setdefault('User-Agent', 'couchdb-python %s' % __version__)
433487
body = None
434488
if content:
435489
if not isinstance(content, basestring):
@@ -441,23 +495,23 @@ def _request(self, method, path=None, content=None, headers=None,
441495
resp, data = self.http.request(uri(self.uri, path, **params), method,
442496
body=body, headers=headers)
443497
status_code = int(resp.status)
444-
if data:# FIXME and resp.get('content-type') == 'application/json':
498+
if data and resp.get('content-type') == 'application/json':
445499
try:
446500
data = json.loads(data)
447501
except ValueError:
448502
pass
449503

450504
if status_code >= 400:
451505
if type(data) is dict:
452-
error = data.get('error', {}).get('reason', data)
506+
error = (data.get('error'), data.get('reason'))
453507
else:
454508
error = data
455509
if status_code == 404:
456510
raise ResourceNotFound(error)
457511
elif status_code == 409:
458512
raise ResourceConflict(error)
459513
else:
460-
raise ServerError(error)
514+
raise ServerError((status_code, error))
461515

462516
return data
463517

@@ -492,10 +546,10 @@ def uri(base, *path, **query):
492546

493547
return ''.join(retval)
494548

495-
def unicode_quote(string):
549+
def unicode_quote(string, safe=''):
496550
if isinstance(string, unicode):
497551
string = string.encode('utf-8')
498-
return quote(string, '/:')
552+
return quote(string, safe)
499553

500554
def unicode_urlencode(data):
501555
if isinstance(data, dict):

0 commit comments

Comments
 (0)