29
29
import simplejson as json
30
30
31
31
__all__ = ['ResourceNotFound' , 'ResourceConflict' , 'ServerError' , 'Server' ,
32
- 'Database' , 'View' ]
32
+ 'Database' , 'Document' , ' View' ]
33
33
__docformat__ = 'restructuredtext en'
34
34
35
35
@@ -46,17 +46,15 @@ class ResourceConflict(Exception):
46
46
47
47
48
48
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.
51
51
"""
52
52
53
53
54
54
class Server (object ):
55
55
"""Representation of a CouchDB server.
56
56
57
57
>>> server = Server('http://localhost:8888/')
58
- >>> server.version
59
- (0, 6, 4)
60
58
61
59
This class behaves like a dictionary of databases. For example, to get a
62
60
list of database names on the server, you can simply iterate over the
@@ -133,16 +131,14 @@ def __getitem__(self, name):
133
131
http = self .resource .http )
134
132
135
133
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' ]
139
135
version = property (_get_version , doc = """\
140
136
The version number tuple for the CouchDB server.
141
137
142
138
Note that this results in a request being made, and can also be used
143
139
to check for the availability of the server.
144
140
145
- :type: `tuple `
141
+ :type: `unicode `
146
142
""" )
147
143
148
144
def create (self , name ):
@@ -172,7 +168,7 @@ class Database(object):
172
168
173
169
>>> doc = db[doc_id]
174
170
>>> doc #doctest: +ELLIPSIS
175
- <Row u'...'@... {...}>
171
+ <Document u'...'@... {...}>
176
172
177
173
Documents are represented as instances of the `Row` class, which is
178
174
basically just a normal dictionary with the additional attributes ``id`` and
@@ -221,14 +217,14 @@ def __contains__(self, id):
221
217
:return: `True` if a document with the ID exists, `False` otherwise
222
218
"""
223
219
try :
224
- self .resource .get (id ) # FIXME: should use HEAD
220
+ self .resource .head (id ) # FIXME: should use HEAD
225
221
return True
226
222
except ResourceNotFound :
227
223
return False
228
224
229
225
def __iter__ (self ):
230
226
"""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' )] )
232
228
233
229
def __len__ (self ):
234
230
"""Return the number of documents in the database."""
@@ -249,7 +245,7 @@ def __getitem__(self, id):
249
245
:return: a `Row` object representing the requested document
250
246
:rtype: `Row`
251
247
"""
252
- return Row (self .resource .get (id ))
248
+ return Document (self .resource .get (id ))
253
249
254
250
def __setitem__ (self , id , content ):
255
251
"""Create or update a document with the specified ID.
@@ -259,9 +255,8 @@ def __setitem__(self, id, content):
259
255
new documents, or a `Row` object for existing
260
256
documents
261
257
"""
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' ]})
265
260
266
261
def _get_name (self ):
267
262
if self ._name is None :
@@ -279,10 +274,9 @@ def create(self, data):
279
274
:return: the ID of the created document
280
275
:rtype: `unicode`
281
276
"""
282
- data = self .resource .post (content = data )
283
- return data ['_id' ]
277
+ return self .resource .post (content = data )['id' ]
284
278
285
- def get (self , id , default = None ):
279
+ def get (self , id , default = None , ** options ):
286
280
"""Return the document with the specified ID.
287
281
288
282
:param id: the document ID
@@ -293,12 +287,12 @@ def get(self, id, default=None):
293
287
:rtype: `Row`
294
288
"""
295
289
try :
296
- return self [ id ]
290
+ return Document ( self . resource . get ( id , ** options ))
297
291
except ResourceNotFound :
298
292
return default
299
293
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.
302
296
303
297
>>> server = Server('http://localhost:8888/')
304
298
>>> db = server.create('python-tests')
@@ -307,34 +301,73 @@ def query(self, code, **options):
307
301
>>> db['gotham'] = dict(type='City', name='Gotham City')
308
302
>>> code = '''function(doc) {
309
303
... if (doc.type=='Person')
310
- ... return {'key': doc.name} ;
304
+ ... map( doc.name, null) ;
311
305
... }'''
312
306
>>> for row in db.query(code):
313
- ... print row[' key']
307
+ ... print row. key
314
308
John Doe
315
309
Mary Jane
316
310
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
319
313
Mary Jane
320
314
John Doe
321
315
322
316
>>> for row in db.query(code, key='John Doe'):
323
- ... print row[' key']
317
+ ... print row. key
324
318
John Doe
325
319
326
320
>>> del server['python-tests']
327
321
328
322
: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
329
325
:return: an iterable over the resulting `Row` objects
330
326
:rtype: ``generator``
331
327
"""
332
- json_options = {}
333
328
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 )
336
337
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
338
371
339
372
def view (self , name , ** options ):
340
373
"""Execute a predefined view.
@@ -352,11 +385,30 @@ def view(self, name, **options):
352
385
:return: a `View` object
353
386
:rtype: `View`
354
387
"""
355
- view = View (uri (self .resource .uri , name ), name ,
388
+ view = View (uri (self .resource .uri , * name . split ( '/' ) ), name ,
356
389
http = self .resource .http )
357
390
return view (** options )
358
391
359
392
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
+
360
412
class View (object ):
361
413
"""Representation of a permanent view on the server."""
362
414
@@ -368,34 +420,33 @@ def __repr__(self):
368
420
return '<%s %r>' % (type (self ).__name__ , self .name )
369
421
370
422
def __call__ (self , ** options ):
371
- json_options = {}
372
423
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 )
375
428
for row in data ['rows' ]:
376
- yield Row (row )
429
+ yield Row (row [ 'id' ], row [ 'key' ], row [ 'value' ] )
377
430
378
431
def __iter__ (self ):
379
432
return self ()
380
433
381
434
382
- class Row (dict ):
435
+ class Row (object ):
383
436
"""Representation of a row as returned by database views.
384
437
385
438
This is basically just a dictionary with the two additional properties
386
439
`id` and `rev`, which contain the document ID and revision, respectively.
387
440
"""
388
441
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
391
446
392
447
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 )
399
450
400
451
401
452
# Internals
@@ -429,7 +480,10 @@ def put(self, path=None, content=None, headers=None, **params):
429
480
430
481
def _request (self , method , path = None , content = None , headers = None ,
431
482
** params ):
483
+ from couchdb import __version__
432
484
headers = headers or {}
485
+ headers .setdefault ('Accept' , 'application/json' )
486
+ headers .setdefault ('User-Agent' , 'couchdb-python %s' % __version__ )
433
487
body = None
434
488
if content :
435
489
if not isinstance (content , basestring ):
@@ -441,23 +495,23 @@ def _request(self, method, path=None, content=None, headers=None,
441
495
resp , data = self .http .request (uri (self .uri , path , ** params ), method ,
442
496
body = body , headers = headers )
443
497
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' :
445
499
try :
446
500
data = json .loads (data )
447
501
except ValueError :
448
502
pass
449
503
450
504
if status_code >= 400 :
451
505
if type (data ) is dict :
452
- error = data .get ('error' , {}) .get ('reason' , data )
506
+ error = ( data .get ('error' ), data .get ('reason' ) )
453
507
else :
454
508
error = data
455
509
if status_code == 404 :
456
510
raise ResourceNotFound (error )
457
511
elif status_code == 409 :
458
512
raise ResourceConflict (error )
459
513
else :
460
- raise ServerError (error )
514
+ raise ServerError (( status_code , error ) )
461
515
462
516
return data
463
517
@@ -492,10 +546,10 @@ def uri(base, *path, **query):
492
546
493
547
return '' .join (retval )
494
548
495
- def unicode_quote (string ):
549
+ def unicode_quote (string , safe = '' ):
496
550
if isinstance (string , unicode ):
497
551
string = string .encode ('utf-8' )
498
- return quote (string , '/:' )
552
+ return quote (string , safe )
499
553
500
554
def unicode_urlencode (data ):
501
555
if isinstance (data , dict ):
0 commit comments