Skip to content

Commit 4984fc2

Browse files
committed
Merge 0.3.x into stable.
--HG-- branch : stable
2 parents e7a5777 + 8c11e60 commit 4984fc2

File tree

10 files changed

+288
-81
lines changed

10 files changed

+288
-81
lines changed

.hgtags

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
82181e48019e85608e8128d8056f541565f54730 0.2.0
55
ce837fb130eec700edc912151d72e49446d0d299 0.2.0
66
e46bf4f09a7042420586a152eef6fd48bdf472ba 0.2.0
7+
0f2eb5fe6a7a9d624f5204c77b49a4173f37cea7 0.3.0
8+
20e6fcee4b68d03a7c29740dbbfad774a386aac2 0.3.0

COPYING

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (C) 2007 Christopher Lenz
1+
Copyright (C) 2007-2008 Christopher Lenz
22
All rights reserved.
33

44
Redistribution and use in source and binary forms, with or without

ChangeLog.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
Version 0.3
2+
http://couchdb-python.googlecode.com/svn/tags/0.3.0
3+
(Feb 6, 2008, from branches/0.3.x)
4+
5+
* The `schema.Document` class now has a `view()` method that can be used to
6+
execute a CouchDB view and map the result rows back to objects of that
7+
schema.
8+
* The test suite now uses the new default port of CouchDB, 5984.
9+
* Views now return proxy objects to which you can apply slice syntax for
10+
"key", "startkey", and "endkey" filtering.
11+
* Add a `query()` classmethod to the `Document` class.
12+
13+
114
Version 0.2
215
http://couchdb-python.googlecode.com/svn/tags/0.2.0
316
(Nov 21, 2007, from branches/0.2.x)

README.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ CouchDB Python Library
33

44
This package provides a Python interface to CouchDB.
55

6-
<http://couchdb.com/>
6+
<http://couchdb.org/>
77

88
Please see the files in the `doc` folder and/or the docstrings in
99
the code for documentation.

couchdb/client.py

Lines changed: 188 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# -*- coding: utf-8 -*-
22
#
3-
# Copyright (C) 2007 Christopher Lenz
3+
# Copyright (C) 2007-2008 Christopher Lenz
44
# All rights reserved.
55
#
66
# This software is licensed as described in the file COPYING, which
77
# you should have received as part of this distribution.
88

99
"""Python client API for CouchDB.
1010
11-
>>> server = Server('http://localhost:8888/')
11+
>>> server = Server('http://localhost:5984/')
1212
>>> db = server.create('python-tests')
1313
>>> doc_id = db.create({'type': 'Person', 'name': 'John Doe'})
1414
>>> doc = db[doc_id]
@@ -29,7 +29,7 @@
2929
import simplejson as json
3030

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

3535

@@ -54,7 +54,7 @@ class ServerError(Exception):
5454
class Server(object):
5555
"""Representation of a CouchDB server.
5656
57-
>>> server = Server('http://localhost:8888/')
57+
>>> server = Server('http://localhost:5984/')
5858
5959
This class behaves like a dictionary of databases. For example, to get a
6060
list of database names on the server, you can simply iterate over the
@@ -82,7 +82,7 @@ def __init__(self, uri):
8282
"""Initialize the server object.
8383
8484
:param uri: the URI of the server (for example
85-
``http://localhost:8888/``)
85+
``http://localhost:5984/``)
8686
"""
8787
self.resource = Resource(httplib2.Http(), uri)
8888

@@ -156,7 +156,7 @@ def create(self, name):
156156
class Database(object):
157157
"""Representation of a database on a CouchDB server.
158158
159-
>>> server = Server('http://localhost:8888/')
159+
>>> server = Server('http://localhost:5984/')
160160
>>> db = server.create('python-tests')
161161
162162
New documents can be added to the database using the `create()` method:
@@ -243,7 +243,7 @@ def __getitem__(self, id):
243243
244244
:param id: the document ID
245245
:return: a `Row` object representing the requested document
246-
:rtype: `Row`
246+
:rtype: `Document`
247247
"""
248248
return Document(self.resource.get(id))
249249

@@ -284,17 +284,18 @@ def get(self, id, default=None, **options):
284284
found
285285
:return: a `Row` object representing the requested document, or `None`
286286
if no document with the ID was found
287-
:rtype: `Row`
287+
:rtype: `Document`
288288
"""
289289
try:
290290
return Document(self.resource.get(id, **options))
291291
except ResourceNotFound:
292292
return default
293293

294-
def query(self, code, content_type='text/javascript', **options):
294+
def query(self, code, content_type='text/javascript', wrapper=None,
295+
**options):
295296
"""Execute an ad-hoc query (a "temp view") against the database.
296297
297-
>>> server = Server('http://localhost:8888/')
298+
>>> server = Server('http://localhost:5984/')
298299
>>> db = server.create('python-tests')
299300
>>> db['johndoe'] = dict(type='Person', name='John Doe')
300301
>>> db['maryjane'] = dict(type='Person', name='Mary Jane')
@@ -322,26 +323,18 @@ def query(self, code, content_type='text/javascript', **options):
322323
:param code: the code of the view function
323324
:param content_type: the MIME type of the code, which determines the
324325
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`
327328
"""
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)
339332

340333
def update(self, documents):
341334
"""Perform a bulk update or insertion of the given documents using a
342335
single HTTP request.
343336
344-
>>> server = Server('http://localhost:8888/')
337+
>>> server = Server('http://localhost:5984/')
345338
>>> db = server.create('python-tests')
346339
>>> for doc in db.update([
347340
... Document(type='Person', name='John Doe'),
@@ -369,10 +362,10 @@ def update(self, documents):
369362
doc.update({'_id': result['id'], '_rev': result['rev']})
370363
yield doc
371364

372-
def view(self, name, **options):
365+
def view(self, name, wrapper=None, **options):
373366
"""Execute a predefined view.
374367
375-
>>> server = Server('http://localhost:8888/')
368+
>>> server = Server('http://localhost:5984/')
376369
>>> db = server.create('python-tests')
377370
>>> db['gotham'] = dict(type='City', name='Gotham City')
378371
@@ -382,12 +375,12 @@ def view(self, name, **options):
382375
383376
>>> del server['python-tests']
384377
385-
:return: a `View` object
386-
:rtype: `View`
378+
:return: the view results
379+
:rtype: `ViewResults`
387380
"""
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)
391384

392385

393386
class Document(dict):
@@ -410,39 +403,185 @@ def __repr__(self):
410403

411404

412405
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):
413432
"""Representation of a permanent view on the server."""
414433

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)
416436
self.resource = Resource(http, uri)
417437
self.name = name
418438

419439
def __repr__(self):
420440
return '<%s %r>' % (type(self).__name__, self.name)
421441

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))
430444

431-
def __iter__(self):
432-
return self()
433445

446+
class TemporaryView(View):
447+
"""Representation of a temporary view."""
434448

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
437455

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'>]
440508
"""
441509

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+
442581
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
446585

447586
def __repr__(self):
448587
return '<%s id=%r, key=%r, value=%r>' % (type(self).__name__, self.id,

0 commit comments

Comments
 (0)