1
1
# -*- coding: utf-8 -*-
2
2
#
3
- # Copyright (C) 2007 Christopher Lenz
3
+ # Copyright (C) 2007-2008 Christopher Lenz
4
4
# All rights reserved.
5
5
#
6
6
# This software is licensed as described in the file COPYING, which
7
7
# you should have received as part of this distribution.
8
8
9
9
"""Python client API for CouchDB.
10
10
11
- >>> server = Server('http://localhost:8888 /')
11
+ >>> server = Server('http://localhost:5984 /')
12
12
>>> db = server.create('python-tests')
13
13
>>> doc_id = db.create({'type': 'Person', 'name': 'John Doe'})
14
14
>>> doc = db[doc_id]
29
29
import simplejson as json
30
30
31
31
__all__ = ['ResourceNotFound' , 'ResourceConflict' , 'ServerError' , 'Server' ,
32
- 'Database' , 'Document' , 'View ' ]
32
+ 'Database' , 'Document' , 'ViewResults' , 'Row ' ]
33
33
__docformat__ = 'restructuredtext en'
34
34
35
35
@@ -54,7 +54,7 @@ class ServerError(Exception):
54
54
class Server (object ):
55
55
"""Representation of a CouchDB server.
56
56
57
- >>> server = Server('http://localhost:8888 /')
57
+ >>> server = Server('http://localhost:5984 /')
58
58
59
59
This class behaves like a dictionary of databases. For example, to get a
60
60
list of database names on the server, you can simply iterate over the
@@ -82,7 +82,7 @@ def __init__(self, uri):
82
82
"""Initialize the server object.
83
83
84
84
:param uri: the URI of the server (for example
85
- ``http://localhost:8888 /``)
85
+ ``http://localhost:5984 /``)
86
86
"""
87
87
self .resource = Resource (httplib2 .Http (), uri )
88
88
@@ -156,7 +156,7 @@ def create(self, name):
156
156
class Database (object ):
157
157
"""Representation of a database on a CouchDB server.
158
158
159
- >>> server = Server('http://localhost:8888 /')
159
+ >>> server = Server('http://localhost:5984 /')
160
160
>>> db = server.create('python-tests')
161
161
162
162
New documents can be added to the database using the `create()` method:
@@ -243,7 +243,7 @@ def __getitem__(self, id):
243
243
244
244
:param id: the document ID
245
245
:return: a `Row` object representing the requested document
246
- :rtype: `Row `
246
+ :rtype: `Document `
247
247
"""
248
248
return Document (self .resource .get (id ))
249
249
@@ -284,17 +284,18 @@ def get(self, id, default=None, **options):
284
284
found
285
285
:return: a `Row` object representing the requested document, or `None`
286
286
if no document with the ID was found
287
- :rtype: `Row `
287
+ :rtype: `Document `
288
288
"""
289
289
try :
290
290
return Document (self .resource .get (id , ** options ))
291
291
except ResourceNotFound :
292
292
return default
293
293
294
- def query (self , code , content_type = 'text/javascript' , ** options ):
294
+ def query (self , code , content_type = 'text/javascript' , wrapper = None ,
295
+ ** options ):
295
296
"""Execute an ad-hoc query (a "temp view") against the database.
296
297
297
- >>> server = Server('http://localhost:8888 /')
298
+ >>> server = Server('http://localhost:5984 /')
298
299
>>> db = server.create('python-tests')
299
300
>>> db['johndoe'] = dict(type='Person', name='John Doe')
300
301
>>> db['maryjane'] = dict(type='Person', name='Mary Jane')
@@ -322,26 +323,18 @@ def query(self, code, content_type='text/javascript', **options):
322
323
:param code: the code of the view function
323
324
:param content_type: the MIME type of the code, which determines the
324
325
language the view function is written in
325
- :return: an iterable over the resulting `Row` objects
326
- :rtype: ``generator` `
326
+ :return: the view reults
327
+ :rtype: `ViewResults `
327
328
"""
328
- for name , value in options .items ():
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 )
337
- for row in data ['rows' ]:
338
- yield Row (row ['id' ], row ['key' ], row ['value' ])
329
+ return TemporaryView (uri (self .resource .uri , '_temp_view' ), code ,
330
+ content_type = content_type , wrapper = wrapper ,
331
+ http = self .resource .http )(** options )
339
332
340
333
def update (self , documents ):
341
334
"""Perform a bulk update or insertion of the given documents using a
342
335
single HTTP request.
343
336
344
- >>> server = Server('http://localhost:8888 /')
337
+ >>> server = Server('http://localhost:5984 /')
345
338
>>> db = server.create('python-tests')
346
339
>>> for doc in db.update([
347
340
... Document(type='Person', name='John Doe'),
@@ -369,10 +362,10 @@ def update(self, documents):
369
362
doc .update ({'_id' : result ['id' ], '_rev' : result ['rev' ]})
370
363
yield doc
371
364
372
- def view (self , name , ** options ):
365
+ def view (self , name , wrapper = None , ** options ):
373
366
"""Execute a predefined view.
374
367
375
- >>> server = Server('http://localhost:8888 /')
368
+ >>> server = Server('http://localhost:5984 /')
376
369
>>> db = server.create('python-tests')
377
370
>>> db['gotham'] = dict(type='City', name='Gotham City')
378
371
@@ -382,12 +375,12 @@ def view(self, name, **options):
382
375
383
376
>>> del server['python-tests']
384
377
385
- :return: a `View` object
386
- :rtype: `View `
378
+ :return: the view results
379
+ :rtype: `ViewResults `
387
380
"""
388
- view = View (uri (self .resource .uri , * name .split ('/' )), name ,
389
- http = self . resource . http )
390
- return view (** options )
381
+ return PermanentView (uri (self .resource .uri , * name .split ('/' )), name ,
382
+ wrapper = wrapper ,
383
+ http = self . resource . http ) (** options )
391
384
392
385
393
386
class Document (dict ):
@@ -410,39 +403,185 @@ def __repr__(self):
410
403
411
404
412
405
class View (object ):
406
+ """Abstract representation of a view or query."""
407
+
408
+ def __init__ (self , uri , wrapper = None , http = None ):
409
+ self .resource = Resource (http , uri )
410
+ self .wrapper = wrapper
411
+
412
+ def __call__ (self , ** options ):
413
+ return ViewResults (self , options )
414
+
415
+ def __iter__ (self ):
416
+ return self ()
417
+
418
+ def _encode_options (self , options ):
419
+ retval = {}
420
+ for name , value in options .items ():
421
+ if name in ('key' , 'startkey' , 'endkey' ) \
422
+ or not isinstance (value , basestring ):
423
+ value = json .dumps (value )
424
+ retval [name ] = value
425
+ return retval
426
+
427
+ def _exec (self , options ):
428
+ raise NotImplementedError
429
+
430
+
431
+ class PermanentView (View ):
413
432
"""Representation of a permanent view on the server."""
414
433
415
- def __init__ (self , uri , name , http = None ):
434
+ def __init__ (self , uri , name , wrapper = None , http = None ):
435
+ View .__init__ (self , uri , wrapper = wrapper , http = http )
416
436
self .resource = Resource (http , uri )
417
437
self .name = name
418
438
419
439
def __repr__ (self ):
420
440
return '<%s %r>' % (type (self ).__name__ , self .name )
421
441
422
- def __call__ (self , ** options ):
423
- for name , value in options .items ():
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 )
428
- for row in data ['rows' ]:
429
- yield Row (row ['id' ], row ['key' ], row ['value' ])
442
+ def _exec (self , options ):
443
+ return self .resource .get (** self ._encode_options (options ))
430
444
431
- def __iter__ (self ):
432
- return self ()
433
445
446
+ class TemporaryView (View ):
447
+ """Representation of a temporary view."""
434
448
435
- class Row (object ):
436
- """Representation of a row as returned by database views.
449
+ def __init__ (self , uri , code = None , content_type = 'text/javascript' ,
450
+ wrapper = None , http = None ):
451
+ View .__init__ (self , uri , wrapper = wrapper , http = http )
452
+ self .resource = Resource (http , uri )
453
+ self .code = code
454
+ self .content_type = content_type
437
455
438
- This is basically just a dictionary with the two additional properties
439
- `id` and `rev`, which contain the document ID and revision, respectively.
456
+ def __repr__ (self ):
457
+ return '<%s %r>' % (type (self ).__name__ , self .code )
458
+
459
+ 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 ,
464
+ ** self ._encode_options (options ))
465
+
466
+
467
+ class ViewResults (object ):
468
+ """Representation of a parameterized view (either permanent or temporary)
469
+ and the results it produces.
470
+
471
+ This class allows the specification of ``key``, ``startkey``, and
472
+ ``endkey`` options using Python slice notation.
473
+
474
+ >>> server = Server('http://localhost:5984/')
475
+ >>> db = server.create('python-tests')
476
+ >>> db['johndoe'] = dict(type='Person', name='John Doe')
477
+ >>> db['maryjane'] = dict(type='Person', name='Mary Jane')
478
+ >>> db['gotham'] = dict(type='City', name='Gotham City')
479
+ >>> code = '''function(doc) {
480
+ ... map([doc.type, doc.name], doc.name);
481
+ ... }'''
482
+ >>> results = db.query(code)
483
+
484
+ At this point, the view has not actually been accessed yet. It is accessed
485
+ as soon as it is iterated over, its length is requested, or one of its
486
+ `rows`, `total_rows`, or `offset` properties are accessed:
487
+
488
+ >>> len(results)
489
+ 3
490
+
491
+ You can use slices to apply ``startkey`` and/or ``endkey`` options to the
492
+ view:
493
+
494
+ >>> people = results[['Person']:['Person','ZZZZ']]
495
+ >>> for person in people:
496
+ ... print person.value
497
+ John Doe
498
+ Mary Jane
499
+ >>> people.total_rows, people.offset
500
+ (3, 1)
501
+
502
+ Use plain indexed notation (without a slice) to apply the ``key`` option.
503
+ Note that as CouchDB makes no claim that keys are unique in a view, this
504
+ can still return multiple rows:
505
+
506
+ >>> list(results[['City', 'Gotham City']])
507
+ [<Row id=u'gotham', key=[u'City', u'Gotham City'], value=u'Gotham City'>]
440
508
"""
441
509
510
+ def __init__ (self , view , options ):
511
+ self .view = view
512
+ self .options = options
513
+ self ._rows = self ._total_rows = self ._offset = None
514
+
515
+ def __repr__ (self ):
516
+ return '<%s %r %r>' % (type (self ).__name__ , self .view , self .options )
517
+
518
+ def __getitem__ (self , key ):
519
+ options = self .options .copy ()
520
+ if type (key ) is slice :
521
+ if key .start is not None :
522
+ options ['startkey' ] = key .start
523
+ if key .stop is not None :
524
+ options ['endkey' ] = key .stop
525
+ return ViewResults (self .view , options )
526
+ else :
527
+ options ['key' ] = key
528
+ return ViewResults (self .view , options )
529
+
530
+ def __iter__ (self ):
531
+ wrapper = self .view .wrapper
532
+ for row in self .rows :
533
+ if wrapper is not None :
534
+ yield wrapper (row )
535
+ else :
536
+ yield row
537
+
538
+ def __len__ (self ):
539
+ return len (self .rows )
540
+
541
+ def _fetch (self ):
542
+ 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' ]
545
+ self ._offset = data .get ('offset' , 0 )
546
+
547
+ def _get_rows (self ):
548
+ if self ._rows is None :
549
+ self ._fetch ()
550
+ return self ._rows
551
+ rows = property (_get_rows , doc = """\
552
+ The list of rows returned by the view.
553
+
554
+ :type: `list`
555
+ """ )
556
+
557
+ def _get_total_rows (self ):
558
+ if self ._total_rows is None :
559
+ self ._fetch ()
560
+ return self ._total_rows
561
+ total_rows = property (_get_total_rows , doc = """\
562
+ The total number of rows in this view.
563
+
564
+ :type: `int`
565
+ """ )
566
+
567
+ def _get_offset (self ):
568
+ if self ._offset is None :
569
+ self ._fetch ()
570
+ return self ._offset
571
+ offset = property (_get_offset , doc = """\
572
+ The offset of the results from the first row in the view.
573
+
574
+ :type: `int`
575
+ """ )
576
+
577
+
578
+ class Row (object ):
579
+ """Representation of a row as returned by database views."""
580
+
442
581
def __init__ (self , id , key , value ):
443
- self .id = id
444
- self .key = key
445
- self .value = value
582
+ self .id = id #: The document ID
583
+ self .key = key #: The key of the row
584
+ self .value = value #: The value of the row
446
585
447
586
def __repr__ (self ):
448
587
return '<%s id=%r, key=%r, value=%r>' % (type (self ).__name__ , self .id ,
0 commit comments