28
28
import re
29
29
import simplejson as json
30
30
31
- __all__ = ['ResourceNotFound' , 'ResourceConflict' , 'ServerError' , 'Server' ,
32
- 'Database' , 'Document' , 'ViewResults' , 'Row' ]
31
+ __all__ = ['PreconditionFailed' , 'ResourceNotFound' , 'ResourceConflict' ,
32
+ 'ServerError' , 'Server' , 'Database' , 'Document' , 'ViewResults' ,
33
+ 'Row' ]
33
34
__docformat__ = 'restructuredtext en'
34
35
35
36
37
+ class PreconditionFailed (Exception ):
38
+ """Exception raised when a 412 HTTP error is received in response to a
39
+ request.
40
+ """
41
+
42
+
36
43
class ResourceNotFound (Exception ):
37
44
"""Exception raised when a 404 HTTP error is received in response to a
38
45
request.
@@ -84,7 +91,7 @@ def __init__(self, uri):
84
91
:param uri: the URI of the server (for example
85
92
``http://localhost:5984/``)
86
93
"""
87
- self .resource = Resource (httplib2 . Http () , uri )
94
+ self .resource = Resource (None , uri )
88
95
89
96
def __contains__ (self , name ):
90
97
"""Return whether the server contains a database with the specified
@@ -196,8 +203,6 @@ class Database(object):
196
203
True
197
204
>>> len(db)
198
205
2
199
- >>> list(db) #doctest: +ELLIPSIS
200
- [u'...', u'JohnDoe']
201
206
202
207
>>> del server['python-tests']
203
208
"""
@@ -260,7 +265,7 @@ def __setitem__(self, id, content):
260
265
261
266
def _get_name (self ):
262
267
if self ._name is None :
263
- self ._name = self .resource . get ()['db_name' ]
268
+ self ._name = self .info ()['db_name' ]
264
269
return self ._name
265
270
name = property (_get_name )
266
271
@@ -291,43 +296,59 @@ def get(self, id, default=None, **options):
291
296
except ResourceNotFound :
292
297
return default
293
298
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 ):
296
313
"""Execute an ad-hoc query (a "temp view") against the database.
297
314
298
315
>>> server = Server('http://localhost:5984/')
299
316
>>> db = server.create('python-tests')
300
317
>>> db['johndoe'] = dict(type='Person', name='John Doe')
301
318
>>> db['maryjane'] = dict(type='Person', name='Mary Jane')
302
319
>>> 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);
306
323
... }'''
307
- >>> for row in db.query(code ):
324
+ >>> for row in db.query(map_fun ):
308
325
... print row.key
309
326
John Doe
310
327
Mary Jane
311
328
312
- >>> for row in db.query(code , descending=True):
329
+ >>> for row in db.query(map_fun , descending=True):
313
330
... print row.key
314
331
Mary Jane
315
332
John Doe
316
333
317
- >>> for row in db.query(code , key='John Doe'):
334
+ >>> for row in db.query(map_fun , key='John Doe'):
318
335
... print row.key
319
336
John Doe
320
337
321
338
>>> del server['python-tests']
322
339
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
326
347
:return: the view reults
327
348
:rtype: `ViewResults`
328
349
"""
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 ,
331
352
http = self .resource .http )(** options )
332
353
333
354
def update (self , documents ):
@@ -355,12 +376,14 @@ def update(self, documents):
355
376
:since: version 0.2
356
377
"""
357
378
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 ()
364
387
365
388
def view (self , name , wrapper = None , ** options ):
366
389
"""Execute a predefined view.
@@ -375,9 +398,16 @@ def view(self, name, wrapper=None, **options):
375
398
376
399
>>> del server['python-tests']
377
400
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
378
406
:return: the view results
379
407
:rtype: `ViewResults`
380
408
"""
409
+ if not name .startswith ('_' ):
410
+ name = '_view/' + name
381
411
return PermanentView (uri (self .resource .uri , * name .split ('/' )), name ,
382
412
wrapper = wrapper ,
383
413
http = self .resource .http )(** options )
@@ -390,9 +420,6 @@ class Document(dict):
390
420
`id` and `rev`, which contain the document ID and revision, respectively.
391
421
"""
392
422
393
- def __init__ (self , * args , ** kwargs ):
394
- dict .__init__ (self , * args , ** kwargs )
395
-
396
423
def __repr__ (self ):
397
424
return '<%s %r@%r %r>' % (type (self ).__name__ , self .id , self .rev ,
398
425
dict ([(k ,v ) for k ,v in self .items ()
@@ -420,7 +447,7 @@ def _encode_options(self, options):
420
447
for name , value in options .items ():
421
448
if name in ('key' , 'startkey' , 'endkey' ) \
422
449
or not isinstance (value , basestring ):
423
- value = json .dumps (value )
450
+ value = json .dumps (value , ensure_ascii = False )
424
451
retval [name ] = value
425
452
return retval
426
453
@@ -446,21 +473,25 @@ def _exec(self, options):
446
473
class TemporaryView (View ):
447
474
"""Representation of a temporary view."""
448
475
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 ):
451
478
View .__init__ (self , uri , wrapper = wrapper , http = http )
452
479
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
455
483
456
484
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 )
458
487
459
488
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' },
464
495
** self ._encode_options (options ))
465
496
466
497
@@ -476,10 +507,10 @@ class ViewResults(object):
476
507
>>> db['johndoe'] = dict(type='Person', name='John Doe')
477
508
>>> db['maryjane'] = dict(type='Person', name='Mary Jane')
478
509
>>> 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);
481
512
... }'''
482
- >>> results = db.query(code )
513
+ >>> results = db.query(map_fun )
483
514
484
515
At this point, the view has not actually been accessed yet. It is accessed
485
516
as soon as it is iterated over, its length is requested, or one of its
@@ -540,8 +571,9 @@ def __len__(self):
540
571
541
572
def _fetch (self ):
542
573
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' )
545
577
self ._offset = data .get ('offset' , 0 )
546
578
547
579
def _get_rows (self ):
@@ -555,22 +587,26 @@ def _get_rows(self):
555
587
""" )
556
588
557
589
def _get_total_rows (self ):
558
- if self ._total_rows is None :
590
+ if self ._rows is None :
559
591
self ._fetch ()
560
592
return self ._total_rows
561
593
total_rows = property (_get_total_rows , doc = """\
562
594
The total number of rows in this view.
563
595
564
- :type: `int`
596
+ This value is `None` for reduce views.
597
+
598
+ :type: `int` or ``NoneType`` for reduce views
565
599
""" )
566
600
567
601
def _get_offset (self ):
568
- if self ._offset is None :
602
+ if self ._rows is None :
569
603
self ._fetch ()
570
604
return self ._offset
571
605
offset = property (_get_offset , doc = """\
572
606
The offset of the results from the first row in the view.
573
607
608
+ This value is 0 for reduce views.
609
+
574
610
:type: `int`
575
611
""" )
576
612
@@ -579,11 +615,14 @@ class Row(object):
579
615
"""Representation of a row as returned by database views."""
580
616
581
617
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
583
619
self .key = key #: The key of the row
584
620
self .value = value #: The value of the row
585
621
586
622
def __repr__ (self ):
623
+ if self .id is None :
624
+ return '<%s key=%r, value=%r>' % (type (self ).__name__ , self .key ,
625
+ self .value )
587
626
return '<%s id=%r, key=%r, value=%r>' % (type (self ).__name__ , self .id ,
588
627
self .key , self .value )
589
628
@@ -624,9 +663,9 @@ def _request(self, method, path=None, content=None, headers=None,
624
663
headers .setdefault ('Accept' , 'application/json' )
625
664
headers .setdefault ('User-Agent' , 'couchdb-python %s' % __version__ )
626
665
body = None
627
- if content :
666
+ if content is not None :
628
667
if not isinstance (content , basestring ):
629
- body = json .dumps (content )
668
+ body = json .dumps (content , ensure_ascii = False ). encode ( 'utf-8' )
630
669
headers .setdefault ('Content-Type' , 'application/json' )
631
670
else :
632
671
body = content
@@ -649,6 +688,8 @@ def _request(self, method, path=None, content=None, headers=None,
649
688
raise ResourceNotFound (error )
650
689
elif status_code == 409 :
651
690
raise ResourceConflict (error )
691
+ elif status_code == 412 :
692
+ raise PreconditionFailed (error )
652
693
else :
653
694
raise ServerError ((status_code , error ))
654
695
@@ -679,6 +720,10 @@ def uri(base, *path, **query):
679
720
if type (value ) in (list , tuple ):
680
721
params .extend ([(name , i ) for i in value if i is not None ])
681
722
elif value is not None :
723
+ if value is True :
724
+ value = 'true'
725
+ elif value is False :
726
+ value = 'false'
682
727
params .append ((name , value ))
683
728
if params :
684
729
retval .extend (['?' , unicode_urlencode (params )])
0 commit comments