CouchDB-0.8/0000755000175000001440000000000011431232253011303 5ustar djcusersCouchDB-0.8/doc/0000755000175000001440000000000011431232253012050 5ustar djcusersCouchDB-0.8/doc/conf.py0000644000175000001440000001450011431231372013350 0ustar djcusers# -*- coding: utf-8 -*- # # couchdb-python documentation build configuration file, created by # sphinx-quickstart on Thu Apr 29 18:32:43 2010. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os dir = os.path.dirname(__file__) sys.path.insert(0, os.path.abspath(os.path.join(dir, '..'))) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] # Add any paths that contain templates here, relative to this directory. templates_path = ['templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'couchdb-python' copyright = u'2010, Dirkjan Ochtman' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.8' # The full version, including alpha/beta/rc tags. release = '0.8' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'couchdb-pythondoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'couchdb-python.tex', u'couchdb-python Documentation', u'Dirkjan Ochtman', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True CouchDB-0.8/doc/views.rst0000644000175000001440000000136611431231104013737 0ustar djcusersWriting views in Python ======================= The couchdb-python package comes with a view server to allow you to write views in Python instead of JavaScript. When couchdb-python is installed, it will install a script called couchpy that runs the view server. To enable this for your CouchDB server, add the following section to local.ini:: [query_servers] python=/usr/bin/couchpy After restarting CouchDB, the Futon view editor should show ``python`` in the language pull-down menu. Here's some sample view code to get you started:: def fun(doc): if doc['date']: yield doc['date'], doc Note that the ``map`` function uses the Python ``yield`` keyword to emit values, where JavaScript views use an ``emit()`` function. CouchDB-0.8/doc/getting-started.rst0000644000175000001440000000304711431231104015705 0ustar djcusersGetting started with couchdb-python =================================== Some snippets of code to get you started with writing code against CouchDB. Starting off:: >>> import couchdb >>> couch = couchdb.Server() This gets you a Server object, representing a CouchDB server. By default, it assumes CouchDB is running on localhost:5894. If your CouchDB server is running elsewhere, set it up like this: >>> couch = couchdb.Server('http://example.com:5984/') You can create a new database from Python, or use an existing database: >>> db = couch.create('test') # newly created >>> db = couch['mydb'] # existing After selecting a database, create a document and insert it into the db: >>> doc = {'foo': 'bar'} >>> db.save(doc) ('e0658cab843b59e63c8779a9a5000b01', '1-4c6114c65e295552ab1019e2b046b10e') >>> doc {'_rev': '1-4c6114c65e295552ab1019e2b046b10e', 'foo': 'bar', '_id': 'e0658cab843b59e63c8779a9a5000b01'} The ``save()`` method returns the ID and "rev" for the newly created document. You can also set your own ID by including an ``_id`` item in the document. Getting the document out again is easy: >>> db['e0658cab843b59e63c8779a9a5000b01'] To find all your documents, simply iterate over the database: >>> for id in db: ... print id ... 'e0658cab843b59e63c8779a9a5000b01' Now we can clean up the test document and database we created: >>> db.delete(doc) >>> couch.delete('test') CouchDB-0.8/doc/index.rst0000644000175000001440000000231111431231104013700 0ustar djcusers.. -*- mode: rst; encoding: utf-8 -*- .. couchdb-python documentation master file, created by sphinx-quickstart on Thu Apr 29 18:32:43 2010. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Introduction ============ ``couchdb`` is Python package for working with CouchDB_ from Python code. It consists of the following main modules: * ``couchdb.client``: This is the client library for interfacing CouchDB servers. If you don't know where to start, this is likely to be what you're looking for. * ``couchdb.mapping``: This module provides advanced mapping between CouchDB JSON documents and Python objects. Additionally, the ``couchdb.view`` module implements a view server for views written in Python. There may also be more information on the `project website`_. .. _couchdb: http://couchdb.org/ .. _project website: http://code.google.com/p/couchdb-python .. _views written in Python: views Documentation ============= .. toctree:: :maxdepth: 2 :numbered: getting-started.rst views.rst client.rst mapping.rst changes.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` CouchDB-0.8/doc/client.rst0000644000175000001440000000057211431231104014056 0ustar djcusersBasic CouchDB API: couchdb.client ================================= .. automodule:: couchdb.client Server ------ .. autoclass:: Server :members: Database -------- .. autoclass:: Database :members: Document -------- .. autoclass:: Document :members: ViewResults ----------- .. autoclass:: ViewResults :members: Row --- .. autoclass:: Row :members: CouchDB-0.8/doc/mapping.rst0000644000175000001440000000072611431231104014234 0ustar djcusersMapping CouchDB documents to Python objects: couchdb.mapping ============================================================ .. automodule:: couchdb.mapping Field types ----------- .. autoclass:: TextField .. autoclass:: FloatField .. autoclass:: IntegerField .. autoclass:: LongField .. autoclass:: BooleanField .. autoclass:: DecimalField .. autoclass:: DateField .. autoclass:: DateTimeField .. autoclass:: DictField .. autoclass:: ListField .. autoclass:: ViewField CouchDB-0.8/doc/changes.rst0000644000175000001440000000005711431231104014206 0ustar djcusersChanges ======= .. include:: ../ChangeLog.txt CouchDB-0.8/couchdb/0000755000175000001440000000000011431232253012712 5ustar djcusersCouchDB-0.8/couchdb/json.py0000644000175000001440000001053111401462170014235 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. """Thin abstraction layer over the different available modules for decoding and encoding JSON data. This module currently supports the following JSON modules: - ``simplejson``: http://code.google.com/p/simplejson/ - ``cjson``: http://pypi.python.org/pypi/python-cjson - ``json``: This is the version of ``simplejson`` that is bundled with the Python standard library since version 2.6 (see http://docs.python.org/library/json.html) The default behavior is to use ``simplejson`` if installed, and otherwise fallback to the standard library module. To explicitly tell CouchDB-Python which module to use, invoke the `use()` function with the module name:: from couchdb import json json.use('cjson') In addition to choosing one of the above modules, you can also configure CouchDB-Python to use custom decoding and encoding functions:: from couchdb import json json.use(decode=my_decode, encode=my_encode) """ __all__ = ['decode', 'encode', 'use'] _initialized = False _using = None _decode = None _encode = None def decode(string): """Decode the given JSON string. :param string: the JSON string to decode :type string: basestring :return: the corresponding Python data structure :rtype: object """ if not _initialized: _initialize() return _decode(string) def encode(obj): """Encode the given object as a JSON string. :param obj: the Python data structure to encode :type obj: object :return: the corresponding JSON string :rtype: basestring """ if not _initialized: _initialize() return _encode(obj) def use(module=None, decode=None, encode=None): """Set the JSON library that should be used, either by specifying a known module name, or by providing a decode and encode function. The modules "simplejson", "cjson", and "json" are currently supported for the ``module`` parameter. If provided, the ``decode`` parameter must be a callable that accepts a JSON string and returns a corresponding Python data structure. The ``encode`` callable must accept a Python data structure and return the corresponding JSON string. Exceptions raised by decoding and encoding should be propagated up unaltered. :param module: the name of the JSON library module to use, or the module object itself :type module: str or module :param decode: a function for decoding JSON strings :type decode: callable :param encode: a function for encoding objects as JSON strings :type encode: callable """ global _decode, _encode, _initialized, _using if module is not None: if not isinstance(module, basestring): module = module.__name__ if module not in ('cjson', 'json', 'simplejson'): raise ValueError('Unsupported JSON module %s' % module) _using = module _initialized = False else: assert decode is not None and encode is not None _using = 'custom' _decode = decode _encode = encode _initialized = True def _initialize(): global _initialized def _init_simplejson(): global _decode, _encode import simplejson _decode = lambda string, loads=simplejson.loads: loads(string) _encode = lambda obj, dumps=simplejson.dumps: \ dumps(obj, allow_nan=False, ensure_ascii=False) def _init_cjson(): global _decode, _encode import cjson _decode = lambda string, decode=cjson.decode: decode(string) _encode = lambda obj, encode=cjson.encode: encode(obj) def _init_stdlib(): global _decode, _encode json = __import__('json', {}, {}) _decode = lambda string, loads=json.loads: loads(string) _encode = lambda obj, dumps=json.dumps: \ dumps(obj, allow_nan=False, ensure_ascii=False) if _using == 'simplejson': _init_simplejson() elif _using == 'cjson': _init_cjson() elif _using == 'json': _init_stdlib() elif _using != 'custom': try: _init_simplejson() except ImportError: _init_stdlib() _initialized = True CouchDB-0.8/couchdb/tests/0000755000175000001440000000000011431232253014054 5ustar djcusersCouchDB-0.8/couchdb/tests/multipart.py0000644000175000001440000001344011401462170016451 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2008-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. import doctest from StringIO import StringIO import unittest from couchdb import multipart class ReadMultipartTestCase(unittest.TestCase): def test_flat(self): text = '''\ Content-Type: multipart/mixed; boundary="===============1946781859==" --===============1946781859== Content-Type: application/json Content-ID: bar ETag: "1-4229094393" { "_id": "bar", "_rev": "1-4229094393" } --===============1946781859== Content-Type: application/json Content-ID: foo ETag: "1-2182689334" { "_id": "foo", "_rev": "1-2182689334", "something": "cool" } --===============1946781859==-- ''' num = 0 parts = multipart.read_multipart(StringIO(text)) for headers, is_multipart, payload in parts: self.assertEqual(is_multipart, False) self.assertEqual('application/json', headers['content-type']) if num == 0: self.assertEqual('bar', headers['content-id']) self.assertEqual('"1-4229094393"', headers['etag']) self.assertEqual('{\n "_id": "bar",\n ' '"_rev": "1-4229094393"\n}', payload) elif num == 1: self.assertEqual('foo', headers['content-id']) self.assertEqual('"1-2182689334"', headers['etag']) self.assertEqual('{\n "_id": "foo",\n "_rev": "1-2182689334",' '\n "something": "cool"\n}', payload) num += 1 self.assertEqual(num, 2) def test_nested(self): text = '''\ Content-Type: multipart/mixed; boundary="===============1946781859==" --===============1946781859== Content-Type: application/json Content-ID: bar ETag: "1-4229094393" { "_id": "bar", "_rev": "1-4229094393" } --===============1946781859== Content-Type: multipart/mixed; boundary="===============0909101126==" Content-ID: foo ETag: "1-919589747" --===============0909101126== Content-Type: application/json { "_id": "foo", "_rev": "1-919589747", "something": "cool" } --===============0909101126== Content-Type: text/plain Content-ID: mail.txt Hello, friends. How are you doing? Regards, Chris --===============0909101126==-- --===============1946781859== Content-Type: application/json Content-ID: baz ETag: "1-3482142493" { "_id": "baz", "_rev": "1-3482142493" } --===============1946781859==-- ''' num = 0 parts = multipart.read_multipart(StringIO(text)) for headers, is_multipart, payload in parts: if num == 0: self.assertEqual(is_multipart, False) self.assertEqual('application/json', headers['content-type']) self.assertEqual('bar', headers['content-id']) self.assertEqual('"1-4229094393"', headers['etag']) self.assertEqual('{\n "_id": "bar", \n ' '"_rev": "1-4229094393"\n}', payload) elif num == 1: self.assertEqual(is_multipart, True) self.assertEqual('foo', headers['content-id']) self.assertEqual('"1-919589747"', headers['etag']) partnum = 0 for headers, is_multipart, payload in payload: self.assertEqual(is_multipart, False) if partnum == 0: self.assertEqual('application/json', headers['content-type']) self.assertEqual('{\n "_id": "foo", \n "_rev": ' '"1-919589747", \n "something": ' '"cool"\n}', payload) elif partnum == 1: self.assertEqual('text/plain', headers['content-type']) self.assertEqual('mail.txt', headers['content-id']) self.assertEqual('Hello, friends.\nHow are you doing?' '\n\nRegards, Chris', payload) partnum += 1 elif num == 2: self.assertEqual(is_multipart, False) self.assertEqual('application/json', headers['content-type']) self.assertEqual('baz', headers['content-id']) self.assertEqual('"1-3482142493"', headers['etag']) self.assertEqual('{\n "_id": "baz", \n ' '"_rev": "1-3482142493"\n}', payload) num += 1 self.assertEqual(num, 3) class WriteMultipartTestCase(unittest.TestCase): def test_unicode_content(self): buf = StringIO() envelope = multipart.write_multipart(buf, boundary='==123456789==') envelope.add('text/plain', u'Iñtërnâtiônàlizætiøn') envelope.close() self.assertEqual('''Content-Type: multipart/mixed; boundary="==123456789==" --==123456789== Content-Length: 27 Content-MD5: 5eYoIG5zsa5ps3/Gl2Kh4Q== Content-Type: text/plain;charset=utf-8 Iñtërnâtiônàlizætiøn --==123456789==-- ''', buf.getvalue().replace('\r\n', '\n')) def test_unicode_content_ascii(self): buf = StringIO() envelope = multipart.write_multipart(buf, boundary='==123456789==') self.assertRaises(UnicodeEncodeError, envelope.add, 'text/plain;charset=ascii', u'Iñtërnâtiônàlizætiøn') def suite(): suite = unittest.TestSuite() suite.addTest(doctest.DocTestSuite(multipart)) suite.addTest(unittest.makeSuite(ReadMultipartTestCase, 'test')) suite.addTest(unittest.makeSuite(WriteMultipartTestCase, 'test')) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') CouchDB-0.8/couchdb/tests/design.py0000644000175000001440000000070711401410673015704 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2008 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. import doctest import unittest from couchdb import design def suite(): suite = unittest.TestSuite() suite.addTest(doctest.DocTestSuite(design)) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') CouchDB-0.8/couchdb/tests/client.py0000644000175000001440000004776311431231104015717 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. import doctest import os import os.path import shutil from StringIO import StringIO import time import tempfile import threading import unittest import urlparse from couchdb import client, http from couchdb.tests import testutil http.CACHE_SIZE = 2, 3 class ServerTestCase(testutil.TempDatabaseMixin, unittest.TestCase): def test_init_with_resource(self): sess = http.Session() res = http.Resource(client.DEFAULT_BASE_URL, sess) serv = client.Server(url=res) serv.config() def test_init_with_session(self): sess = http.Session() serv = client.Server(client.DEFAULT_BASE_URL, session=sess) serv.config() self.assertTrue(serv.resource.session is sess) def test_exists(self): self.assertTrue(client.Server(client.DEFAULT_BASE_URL)) self.assertFalse(client.Server('http://localhost:9999')) def test_repr(self): repr(self.server) def test_server_vars(self): version = self.server.version() self.assertTrue(isinstance(version, basestring)) config = self.server.config() self.assertTrue(isinstance(config, dict)) stats = self.server.stats() self.assertTrue(isinstance(stats, dict)) tasks = self.server.tasks() self.assertTrue(isinstance(tasks, list)) def test_get_db_missing(self): self.assertRaises(http.ResourceNotFound, lambda: self.server['couchdb-python/missing']) def test_create_db_conflict(self): name, db = self.temp_db() self.assertRaises(http.PreconditionFailed, self.server.create, name) def test_delete_db(self): name, db = self.temp_db() assert name in self.server self.del_db(name) assert name not in self.server def test_delete_db_missing(self): self.assertRaises(http.ResourceNotFound, self.server.delete, 'couchdb-python/missing') def test_replicate(self): aname, a = self.temp_db() bname, b = self.temp_db() id, rev = a.save({'test': 'a'}) result = self.server.replicate(aname, bname) self.assertEquals(result['ok'], True) self.assertEquals(b[id]['test'], 'a') doc = b[id] doc['test'] = 'b' b.update([doc]) self.server.replicate(bname, aname) self.assertEquals(a[id]['test'], 'b') self.assertEquals(b[id]['test'], 'b') def test_replicate_continuous(self): aname, a = self.temp_db() bname, b = self.temp_db() result = self.server.replicate(aname, bname, continuous=True) self.assertEquals(result['ok'], True) version = tuple(int(i) for i in self.server.version().split('.')[:2]) if version >= (0, 10): self.assertTrue('_local_id' in result) def test_iter(self): aname, a = self.temp_db() bname, b = self.temp_db() dbs = list(self.server) self.assertTrue(aname in dbs) self.assertTrue(bname in dbs) def test_len(self): self.temp_db() self.temp_db() self.assertTrue(len(self.server) >= 2) def test_uuids(self): ls = self.server.uuids() assert type(ls) == list ls = self.server.uuids(count=10) assert type(ls) == list and len(ls) == 10 class DatabaseTestCase(testutil.TempDatabaseMixin, unittest.TestCase): def test_save_new(self): doc = {'foo': 'bar'} id, rev = self.db.save(doc) self.assertTrue(id is not None) self.assertTrue(rev is not None) self.assertEqual((id, rev), (doc['_id'], doc['_rev'])) doc = self.db.get(id) self.assertEqual(doc['foo'], 'bar') def test_save_new_with_id(self): doc = {'_id': 'foo'} id, rev = self.db.save(doc) self.assertTrue(doc['_id'] == id == 'foo') self.assertEqual(doc['_rev'], rev) def test_save_existing(self): doc = {} id_rev_old = self.db.save(doc) doc['foo'] = True id_rev_new = self.db.save(doc) self.assertTrue(doc['_rev'] == id_rev_new[1]) self.assertTrue(id_rev_old[1] != id_rev_new[1]) def test_save_new_batch(self): doc = {'_id': 'foo'} id, rev = self.db.save(doc, batch='ok') self.assertTrue(rev is None) self.assertTrue('_rev' not in doc) def test_save_existing_batch(self): doc = {'_id': 'foo'} self.db.save(doc) id_rev_old = self.db.save(doc) id_rev_new = self.db.save(doc, batch='ok') self.assertTrue(id_rev_new[1] is None) self.assertEqual(id_rev_old[1], doc['_rev']) def test_exists(self): self.assertTrue(self.db) self.assertFalse(client.Database('couchdb-python/missing')) def test_name(self): # Access name assigned during creation. name, db = self.temp_db() self.assertTrue(db.name == name) # Access lazily loaded name, self.assertTrue(client.Database(db.resource.url).name == name) def test_commit(self): self.assertTrue(self.db.commit()['ok'] == True) def test_create_large_doc(self): self.db['foo'] = {'data': '0123456789' * 110 * 1024} # 10 MB self.assertEqual('foo', self.db['foo']['_id']) def test_doc_id_quoting(self): self.db['foo/bar'] = {'foo': 'bar'} self.assertEqual('bar', self.db['foo/bar']['foo']) del self.db['foo/bar'] self.assertEqual(None, self.db.get('foo/bar')) def test_unicode(self): self.db[u'føø'] = {u'bår': u'Iñtërnâtiônàlizætiøn', 'baz': 'ASCII'} self.assertEqual(u'Iñtërnâtiônàlizætiøn', self.db[u'føø'][u'bår']) self.assertEqual(u'ASCII', self.db[u'føø'][u'baz']) def test_disallow_nan(self): try: self.db['foo'] = {u'number': float('nan')} self.fail('Expected ValueError') except ValueError: pass def test_disallow_none_id(self): deldoc = lambda: self.db.delete({'_id': None, '_rev': None}) self.assertRaises(ValueError, deldoc) def test_doc_revs(self): doc = {'bar': 42} self.db['foo'] = doc old_rev = doc['_rev'] doc['bar'] = 43 self.db['foo'] = doc new_rev = doc['_rev'] new_doc = self.db.get('foo') self.assertEqual(new_rev, new_doc['_rev']) new_doc = self.db.get('foo', rev=new_rev) self.assertEqual(new_rev, new_doc['_rev']) old_doc = self.db.get('foo', rev=old_rev) self.assertEqual(old_rev, old_doc['_rev']) revs = [i for i in self.db.revisions('foo')] self.assertEqual(revs[0]['_rev'], new_rev) self.assertEqual(revs[1]['_rev'], old_rev) gen = self.db.revisions('crap') self.assertRaises(StopIteration, lambda: gen.next()) self.assertTrue(self.db.compact()) while self.db.info()['compact_running']: pass # 0.10 responds with 404, 0.9 responds with 500, same content doc = 'fail' try: doc = self.db.get('foo', rev=old_rev) except http.ServerError: doc = None assert doc is None def test_attachment_crud(self): doc = {'bar': 42} self.db['foo'] = doc old_rev = doc['_rev'] self.db.put_attachment(doc, 'Foo bar', 'foo.txt', 'text/plain') self.assertNotEquals(old_rev, doc['_rev']) doc = self.db['foo'] attachment = doc['_attachments']['foo.txt'] self.assertEqual(len('Foo bar'), attachment['length']) self.assertEqual('text/plain', attachment['content_type']) self.assertEqual('Foo bar', self.db.get_attachment(doc, 'foo.txt').read()) self.assertEqual('Foo bar', self.db.get_attachment('foo', 'foo.txt').read()) old_rev = doc['_rev'] self.db.delete_attachment(doc, 'foo.txt') self.assertNotEquals(old_rev, doc['_rev']) self.assertEqual(None, self.db['foo'].get('_attachments')) def test_attachment_crud_with_files(self): doc = {'bar': 42} self.db['foo'] = doc old_rev = doc['_rev'] fileobj = StringIO('Foo bar baz') self.db.put_attachment(doc, fileobj, 'foo.txt') self.assertNotEquals(old_rev, doc['_rev']) doc = self.db['foo'] attachment = doc['_attachments']['foo.txt'] self.assertEqual(len('Foo bar baz'), attachment['length']) self.assertEqual('text/plain', attachment['content_type']) self.assertEqual('Foo bar baz', self.db.get_attachment(doc, 'foo.txt').read()) self.assertEqual('Foo bar baz', self.db.get_attachment('foo', 'foo.txt').read()) old_rev = doc['_rev'] self.db.delete_attachment(doc, 'foo.txt') self.assertNotEquals(old_rev, doc['_rev']) self.assertEqual(None, self.db['foo'].get('_attachments')) def test_empty_attachment(self): doc = {} self.db['foo'] = doc old_rev = doc['_rev'] self.db.put_attachment(doc, '', 'empty.txt') self.assertNotEquals(old_rev, doc['_rev']) doc = self.db['foo'] attachment = doc['_attachments']['empty.txt'] self.assertEqual(0, attachment['length']) def test_default_attachment(self): doc = {} self.db['foo'] = doc self.assertTrue(self.db.get_attachment(doc, 'missing.txt') is None) sentinel = object() self.assertTrue(self.db.get_attachment(doc, 'missing.txt', sentinel) is sentinel) def test_attachment_from_fs(self): tmpdir = tempfile.mkdtemp() tmpfile = os.path.join(tmpdir, 'test.txt') f = open(tmpfile, 'w') f.write('Hello!') f.close() doc = {} self.db['foo'] = doc self.db.put_attachment(doc, open(tmpfile)) doc = self.db.get('foo') self.assertTrue(doc['_attachments']['test.txt']['content_type'] == 'text/plain') shutil.rmtree(tmpdir) def test_attachment_no_filename(self): doc = {} self.db['foo'] = doc self.assertRaises(ValueError, self.db.put_attachment, doc, '') def test_json_attachment(self): doc = {} self.db['foo'] = doc self.db.put_attachment(doc, '{}', 'test.json', 'application/json') self.assertEquals(self.db.get_attachment(doc, 'test.json').read(), '{}') def test_include_docs(self): doc = {'foo': 42, 'bar': 40} self.db['foo'] = doc rows = list(self.db.query( 'function(doc) { emit(doc._id, null); }', include_docs=True )) self.assertEqual(1, len(rows)) self.assertEqual(doc, rows[0].doc) def test_query_multi_get(self): for i in range(1, 6): self.db.save({'i': i}) res = list(self.db.query('function(doc) { emit(doc.i, null); }', keys=range(1, 6, 2))) self.assertEqual(3, len(res)) for idx, i in enumerate(range(1, 6, 2)): self.assertEqual(i, res[idx].key) def test_bulk_update_conflict(self): docs = [ dict(type='Person', name='John Doe'), dict(type='Person', name='Mary Jane'), dict(type='City', name='Gotham City') ] self.db.update(docs) # update the first doc to provoke a conflict in the next bulk update doc = docs[0].copy() self.db[doc['_id']] = doc results = self.db.update(docs) self.assertEqual(False, results[0][0]) assert isinstance(results[0][2], http.ResourceConflict) def test_bulk_update_all_or_nothing(self): docs = [ dict(type='Person', name='John Doe'), dict(type='Person', name='Mary Jane'), dict(type='City', name='Gotham City') ] self.db.update(docs) # update the first doc to provoke a conflict in the next bulk update doc = docs[0].copy() doc['name'] = 'Jane Doe' self.db[doc['_id']] = doc results = self.db.update(docs, all_or_nothing=True) self.assertEqual(True, results[0][0]) doc = self.db.get(doc['_id'], conflicts=True) assert '_conflicts' in doc revs = self.db.get(doc['_id'], open_revs='all') assert len(revs) == 2 def test_bulk_update_bad_doc(self): self.assertRaises(TypeError, self.db.update, [object()]) def test_copy_doc(self): self.db['foo'] = {'status': 'testing'} result = self.db.copy('foo', 'bar') self.assertEqual(result, self.db['bar'].rev) def test_copy_doc_conflict(self): self.db['bar'] = {'status': 'idle'} self.db['foo'] = {'status': 'testing'} self.assertRaises(http.ResourceConflict, self.db.copy, 'foo', 'bar') def test_copy_doc_overwrite(self): self.db['bar'] = {'status': 'idle'} self.db['foo'] = {'status': 'testing'} result = self.db.copy('foo', self.db['bar']) doc = self.db['bar'] self.assertEqual(result, doc.rev) self.assertEqual('testing', doc['status']) def test_copy_doc_srcobj(self): self.db['foo'] = {'status': 'testing'} self.db.copy(self.db['foo'], 'bar') self.assertEqual('testing', self.db['bar']['status']) def test_copy_doc_destobj_norev(self): self.db['foo'] = {'status': 'testing'} self.db.copy('foo', {'_id': 'bar'}) self.assertEqual('testing', self.db['bar']['status']) def test_copy_doc_src_dictlike(self): class DictLike(object): def __init__(self, doc): self.doc = doc def items(self): return self.doc.items() self.db['foo'] = {'status': 'testing'} self.db.copy(DictLike(self.db['foo']), 'bar') self.assertEqual('testing', self.db['bar']['status']) def test_copy_doc_dest_dictlike(self): class DictLike(object): def __init__(self, doc): self.doc = doc def items(self): return self.doc.items() self.db['foo'] = {'status': 'testing'} self.db['bar'] = {} self.db.copy('foo', DictLike(self.db['bar'])) self.assertEqual('testing', self.db['bar']['status']) def test_copy_doc_src_baddoc(self): self.assertRaises(TypeError, self.db.copy, object(), 'bar') def test_copy_doc_dest_baddoc(self): self.assertRaises(TypeError, self.db.copy, 'foo', object()) def test_changes(self): self.db['foo'] = {'bar': True} self.assertEqual(self.db.changes(since=0)['last_seq'], 1) first = self.db.changes(feed='continuous').next() self.assertEqual(first['seq'], 1) self.assertEqual(first['id'], 'foo') def test_changes_releases_conn(self): # Consume an entire changes feed to read the whole response, then check # that the HTTP connection made it to the pool. list(self.db.changes(feed='continuous', timeout=0)) scheme, netloc = urlparse.urlsplit(client.DEFAULT_BASE_URL)[:2] self.assertTrue(self.db.resource.session.conns[(scheme, netloc)]) def test_changes_releases_conn_when_lastseq(self): # Consume a changes feed, stopping at the 'last_seq' item, i.e. don't # let the generator run any further, then check the connection made it # to the pool. for obj in self.db.changes(feed='continuous', timeout=0): if 'last_seq' in obj: break scheme, netloc = urlparse.urlsplit(client.DEFAULT_BASE_URL)[:2] self.assertTrue(self.db.resource.session.conns[(scheme, netloc)]) def test_changes_conn_usable(self): # Consume a changes feed to get a used connection in the pool. list(self.db.changes(feed='continuous', timeout=0)) # Try using the connection again to make sure the connection was left # in a good state from the previous request. self.assertTrue(self.db.info()['doc_count'] == 0) def test_changes_heartbeat(self): def wakeup(): time.sleep(.3) self.db.save({}) threading.Thread(target=wakeup).start() for change in self.db.changes(feed='continuous', heartbeat=100): break class ViewTestCase(testutil.TempDatabaseMixin, unittest.TestCase): def test_view_multi_get(self): for i in range(1, 6): self.db.save({'i': i}) self.db['_design/test'] = { 'language': 'javascript', 'views': { 'multi_key': {'map': 'function(doc) { emit(doc.i, null); }'} } } res = list(self.db.view('test/multi_key', keys=range(1, 6, 2))) self.assertEqual(3, len(res)) for idx, i in enumerate(range(1, 6, 2)): self.assertEqual(i, res[idx].key) def test_view_compaction(self): for i in range(1, 6): self.db.save({'i': i}) self.db['_design/test'] = { 'language': 'javascript', 'views': { 'multi_key': {'map': 'function(doc) { emit(doc.i, null); }'} } } self.db.view('test/multi_key') self.assertTrue(self.db.compact('test')) def test_view_function_objects(self): if 'python' not in self.server.config()['query_servers']: return for i in range(1, 4): self.db.save({'i': i, 'j':2*i}) def map_fun(doc): yield doc['i'], doc['j'] res = list(self.db.query(map_fun, language='python')) self.assertEqual(3, len(res)) for idx, i in enumerate(range(1,4)): self.assertEqual(i, res[idx].key) self.assertEqual(2*i, res[idx].value) def reduce_fun(keys, values): return sum(values) res = list(self.db.query(map_fun, reduce_fun, 'python')) self.assertEqual(1, len(res)) self.assertEqual(12, res[0].value) def test_init_with_resource(self): self.db['foo'] = {} view = client.PermanentView(self.db.resource('_all_docs').url, '_all_docs') self.assertEquals(len(list(view())), 1) def test_iter_view(self): self.db['foo'] = {} view = client.PermanentView(self.db.resource('_all_docs').url, '_all_docs') self.assertEquals(len(list(view)), 1) def test_tmpview_repr(self): mapfunc = "function(doc) {emit(null, null);}" view = client.TemporaryView(self.db.resource('_temp_view'), mapfunc) self.assertTrue('TemporaryView' in repr(view)) self.assertTrue(mapfunc in repr(view)) def test_wrapper_iter(self): class Wrapper(object): def __init__(self, doc): pass self.db['foo'] = {} self.assertTrue(isinstance(list(self.db.view('_all_docs', wrapper=Wrapper))[0], Wrapper)) def test_wrapper_rows(self): class Wrapper(object): def __init__(self, doc): pass self.db['foo'] = {} self.assertTrue(isinstance(self.db.view('_all_docs', wrapper=Wrapper).rows[0], Wrapper)) def test_properties(self): for attr in ['rows', 'total_rows', 'offset']: self.assertTrue(getattr(self.db.view('_all_docs'), attr) is not None) def test_rowrepr(self): self.db['foo'] = {} rows = list(self.db.query("function(doc) {emit(null, 1);}")) self.assertTrue('Row' in repr(rows[0])) self.assertTrue('id' in repr(rows[0])) rows = list(self.db.query("function(doc) {emit(null, 1);}", "function(keys, values, combine) {return sum(values);}")) self.assertTrue('Row' in repr(rows[0])) self.assertTrue('id' not in repr(rows[0])) def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(ServerTestCase, 'test')) suite.addTest(unittest.makeSuite(DatabaseTestCase, 'test')) suite.addTest(unittest.makeSuite(ViewTestCase, 'test')) suite.addTest(doctest.DocTestSuite(client)) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') CouchDB-0.8/couchdb/tests/mapping.py0000644000175000001440000002326011431231104016056 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. from decimal import Decimal import doctest import unittest from couchdb import design, mapping from couchdb.tests import testutil class DocumentTestCase(testutil.TempDatabaseMixin, unittest.TestCase): def test_mutable_fields(self): class Test(mapping.Document): d = mapping.DictField() a = Test() b = Test() a.d['x'] = True self.assertTrue(a.d.get('x')) self.assertFalse(b.d.get('x')) def test_automatic_id(self): class Post(mapping.Document): title = mapping.TextField() post = Post(title='Foo bar') assert post.id is None post.store(self.db) assert post.id is not None self.assertEqual('Foo bar', self.db[post.id]['title']) def test_explicit_id_via_init(self): class Post(mapping.Document): title = mapping.TextField() post = Post(id='foo_bar', title='Foo bar') self.assertEqual('foo_bar', post.id) post.store(self.db) self.assertEqual('Foo bar', self.db['foo_bar']['title']) def test_explicit_id_via_setter(self): class Post(mapping.Document): title = mapping.TextField() post = Post(title='Foo bar') post.id = 'foo_bar' self.assertEqual('foo_bar', post.id) post.store(self.db) self.assertEqual('Foo bar', self.db['foo_bar']['title']) def test_change_id_failure(self): class Post(mapping.Document): title = mapping.TextField() post = Post(title='Foo bar') post.store(self.db) post = Post.load(self.db, post.id) try: post.id = 'foo_bar' self.fail('Excepted AttributeError') except AttributeError, e: self.assertEqual('id can only be set on new documents', e.args[0]) def test_batch_update(self): class Post(mapping.Document): title = mapping.TextField() post1 = Post(title='Foo bar') post2 = Post(title='Foo baz') results = self.db.update([post1, post2]) self.assertEqual(2, len(results)) assert results[0][0] is True assert results[1][0] is True def test_store_existing(self): class Post(mapping.Document): title = mapping.TextField() post = Post(title='Foo bar') post.store(self.db) post.store(self.db) self.assertEqual(len(list(self.db.view('_all_docs'))), 1) def test_old_datetime(self): dt = mapping.DateTimeField() assert dt._to_python(u'1880-01-01T00:00:00Z') class ListFieldTestCase(testutil.TempDatabaseMixin, unittest.TestCase): def test_to_json(self): # See class Post(mapping.Document): title = mapping.TextField() comments = mapping.ListField(mapping.DictField( mapping.Mapping.build( author = mapping.TextField(), content = mapping.TextField(), ) )) post = Post(title='Foo bar') post.comments.append(author='myself', content='Bla bla') post.comments = post.comments self.assertEqual([{'content': 'Bla bla', 'author': 'myself'}], post.comments) def test_proxy_append(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing(numbers=[Decimal('1.0'), Decimal('2.0')]) thing.numbers.append(Decimal('3.0')) self.assertEqual(3, len(thing.numbers)) self.assertEqual(Decimal('3.0'), thing.numbers[2]) def test_proxy_append_kwargs(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing() self.assertRaises(TypeError, thing.numbers.append, foo='bar') def test_proxy_contains(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing(numbers=[Decimal('1.0'), Decimal('2.0')]) assert isinstance(thing.numbers, mapping.ListField.Proxy) assert '1.0' not in thing.numbers assert Decimal('1.0') in thing.numbers def test_proxy_count(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing(numbers=[Decimal('1.0'), Decimal('2.0')]) self.assertEqual(1, thing.numbers.count(Decimal('1.0'))) self.assertEqual(0, thing.numbers.count('1.0')) def test_proxy_index(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing(numbers=[Decimal('1.0'), Decimal('2.0')]) self.assertEqual(0, thing.numbers.index(Decimal('1.0'))) self.assertRaises(ValueError, thing.numbers.index, '3.0') def test_proxy_insert(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing(numbers=[Decimal('1.0'), Decimal('2.0')]) thing.numbers.insert(0, Decimal('0.0')) self.assertEqual(3, len(thing.numbers)) self.assertEqual(Decimal('0.0'), thing.numbers[0]) def test_proxy_insert_kwargs(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing() self.assertRaises(TypeError, thing.numbers.insert, 0, foo='bar') def test_proxy_remove(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing() thing.numbers.append(Decimal('1.0')) thing.numbers.remove(Decimal('1.0')) def test_proxy_iter(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) self.db['test'] = {'numbers': ['1.0', '2.0']} thing = Thing.load(self.db, 'test') assert isinstance(thing.numbers[0], Decimal) def test_proxy_iter_dict(self): class Post(mapping.Document): comments = mapping.ListField(mapping.DictField) self.db['test'] = {'comments': [{'author': 'Joe', 'content': 'Hey'}]} post = Post.load(self.db, 'test') assert isinstance(post.comments[0], dict) def test_proxy_pop(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing() thing.numbers = [Decimal('%d' % i) for i in range(3)] self.assertEqual(thing.numbers.pop(), Decimal('2.0')) self.assertEqual(len(thing.numbers), 2) self.assertEqual(thing.numbers.pop(0), Decimal('0.0')) def test_proxy_slices(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing() thing.numbers = [Decimal('%d' % i) for i in range(5)] ll = thing.numbers[1:3] self.assertEqual(len(ll), 2) self.assertEqual(ll[0], Decimal('1.0')) thing.numbers[2:4] = [Decimal('%d' % i) for i in range(6, 8)] self.assertEqual(thing.numbers[2], Decimal('6.0')) self.assertEqual(thing.numbers[4], Decimal('4.0')) self.assertEqual(len(thing.numbers), 5) del thing.numbers[3:] self.assertEquals(len(thing.numbers), 3) def test_mutable_fields(self): class Thing(mapping.Document): numbers = mapping.ListField(mapping.DecimalField) thing = Thing.wrap({'_id': 'foo', '_rev': 1}) # no numbers thing.numbers.append('1.0') thing2 = Thing(id='thing2') self.assertEqual([i for i in thing2.numbers], []) all_map_func = 'function(doc) { emit(doc._id, doc); }' class WrappingTestCase(testutil.TempDatabaseMixin, unittest.TestCase): class Item(mapping.Document): with_include_docs = mapping.ViewField('test', all_map_func, include_docs=True) without_include_docs = mapping.ViewField('test', all_map_func) def setUp(self): super(WrappingTestCase, self).setUp() design.ViewDefinition.sync_many( self.db, [self.Item.with_include_docs, self.Item.without_include_docs]) def test_viewfield_property(self): self.Item().store(self.db) results = self.Item.with_include_docs(self.db) self.assertEquals(type(results.rows[0]), self.Item) results = self.Item.without_include_docs(self.db) self.assertEquals(type(results.rows[0]), self.Item) def test_view(self): self.Item().store(self.db) results = self.Item.view(self.db, 'test/without_include_docs') self.assertEquals(type(results.rows[0]), self.Item) results = self.Item.view(self.db, 'test/without_include_docs', include_docs=True) self.assertEquals(type(results.rows[0]), self.Item) def test_query(self): self.Item().store(self.db) results = self.Item.query(self.db, all_map_func, None) self.assertEquals(type(results.rows[0]), self.Item) results = self.Item.query(self.db, all_map_func, None, include_docs=True) self.assertEquals(type(results.rows[0]), self.Item) def suite(): suite = unittest.TestSuite() suite.addTest(doctest.DocTestSuite(mapping)) suite.addTest(unittest.makeSuite(DocumentTestCase, 'test')) suite.addTest(unittest.makeSuite(ListFieldTestCase, 'test')) suite.addTest(unittest.makeSuite(WrappingTestCase, 'test')) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') CouchDB-0.8/couchdb/tests/view.py0000644000175000001440000000747011401462170015410 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2007-2008 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. import doctest from StringIO import StringIO import unittest from couchdb import view class ViewServerTestCase(unittest.TestCase): def test_reset(self): input = StringIO('["reset"]\n') output = StringIO() view.run(input=input, output=output) self.assertEquals(output.getvalue(), 'true\n') def test_add_fun(self): input = StringIO('["add_fun", "def fun(doc): yield None, doc"]\n') output = StringIO() view.run(input=input, output=output) self.assertEquals(output.getvalue(), 'true\n') def test_map_doc(self): input = StringIO('["add_fun", "def fun(doc): yield None, doc"]\n' '["map_doc", {"foo": "bar"}]\n') output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), 'true\n' '[[[null, {"foo": "bar"}]]]\n') def test_i18n(self): input = StringIO('["add_fun", "def fun(doc): yield doc[\\"test\\"], doc"]\n' '["map_doc", {"test": "b\xc3\xa5r"}]\n') output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), 'true\n' '[[["b\xc3\xa5r", {"test": "b\xc3\xa5r"}]]]\n') def test_map_doc_with_logging(self): fun = 'def fun(doc): log(\'running\'); yield None, doc' input = StringIO('["add_fun", "%s"]\n' '["map_doc", {"foo": "bar"}]\n' % fun) output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), 'true\n' '{"log": "running"}\n' '[[[null, {"foo": "bar"}]]]\n') def test_map_doc_with_logging_json(self): fun = 'def fun(doc): log([1, 2, 3]); yield None, doc' input = StringIO('["add_fun", "%s"]\n' '["map_doc", {"foo": "bar"}]\n' % fun) output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), 'true\n' '{"log": "[1, 2, 3]"}\n' '[[[null, {"foo": "bar"}]]]\n') def test_reduce(self): input = StringIO('["reduce", ' '["def fun(keys, values): return sum(values)"], ' '[[null, 1], [null, 2], [null, 3]]]\n') output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), '[true, [6]]\n') def test_reduce_with_logging(self): input = StringIO('["reduce", ' '["def fun(keys, values): log(\'Summing %r\' % (values,)); return sum(values)"], ' '[[null, 1], [null, 2], [null, 3]]]\n') output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), '{"log": "Summing (1, 2, 3)"}\n' '[true, [6]]\n') def test_rereduce(self): input = StringIO('["rereduce", ' '["def fun(keys, values, rereduce): return sum(values)"], ' '[1, 2, 3]]\n') output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), '[true, [6]]\n') def suite(): suite = unittest.TestSuite() suite.addTest(doctest.DocTestSuite(view)) suite.addTest(unittest.makeSuite(ViewServerTestCase, 'test')) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') CouchDB-0.8/couchdb/tests/package.py0000644000175000001440000000135111431231104016013 0ustar djcusers# -*- coding: utf-8 -*- import unittest import couchdb class PackageTestCase(unittest.TestCase): def test_exports(self): expected = set([ # couchdb.client 'Server', 'Database', 'Document', # couchdb.http 'HTTPError', 'PreconditionFailed', 'ResourceNotFound', 'ResourceConflict', 'ServerError', 'Unauthorized', 'Resource', 'Session' ]) exported = set(e for e in dir(couchdb) if not e.startswith('_')) self.assertTrue(expected <= exported) def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(PackageTestCase, 'test')) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') CouchDB-0.8/couchdb/tests/http.py0000644000175000001440000000070311401410673015406 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. import doctest import unittest from couchdb import http def suite(): suite = unittest.TestSuite() suite.addTest(doctest.DocTestSuite(http)) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') CouchDB-0.8/couchdb/tests/__init__.py0000644000175000001440000000137611431231104016166 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2007 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. import unittest from couchdb.tests import client, couch_tests, design, http, multipart, \ mapping, view, package def suite(): suite = unittest.TestSuite() suite.addTest(client.suite()) suite.addTest(design.suite()) suite.addTest(http.suite()) suite.addTest(multipart.suite()) suite.addTest(mapping.suite()) suite.addTest(view.suite()) suite.addTest(couch_tests.suite()) suite.addTest(package.suite()) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') CouchDB-0.8/couchdb/tests/testutil.py0000644000175000001440000000225311431231104016277 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. import random import sys from couchdb import client class TempDatabaseMixin(object): temp_dbs = None _db = None def setUp(self): self.server = client.Server(full_commit=False) def tearDown(self): if self.temp_dbs: for name in self.temp_dbs: self.server.delete(name) def temp_db(self): if self.temp_dbs is None: self.temp_dbs = {} # Find an unused database name while True: name = 'couchdb-python/%d' % random.randint(0, sys.maxint) if name not in self.temp_dbs: break print '%s already used' % name db = self.server.create(name) self.temp_dbs[name] = db return name, db def del_db(self, name): del self.temp_dbs[name] self.server.delete(name) @property def db(self): if self._db is None: name, self._db = self.temp_db() return self._db CouchDB-0.8/couchdb/tests/couch_tests.py0000644000175000001440000002144011431231104016744 0ustar djcusers#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2007-2008 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. import unittest from couchdb.http import ResourceConflict, ResourceNotFound from couchdb.tests import testutil class CouchTests(testutil.TempDatabaseMixin, unittest.TestCase): def _create_test_docs(self, num): for i in range(num): self.db[str(i)] = {'a': i + 1, 'b': (i + 1) ** 2} def test_basics(self): self.assertEqual(0, len(self.db)) # create a document data = {'a': 1, 'b': 1} self.db['0'] = data self.assertEqual('0', data['_id']) assert '_rev' in data doc = self.db['0'] self.assertEqual('0', doc.id) self.assertEqual(data['_rev'], doc.rev) self.assertEqual(1, len(self.db)) # delete a document del self.db['0'] self.assertRaises(ResourceNotFound, self.db.__getitem__, '0') # test _all_docs self._create_test_docs(4) self.assertEqual(4, len(self.db)) for doc_id in self.db: assert int(doc_id) in range(4) # test a simple query query = """function(doc) { if (doc.a==4) emit(null, doc.b); }""" result = list(self.db.query(query)) self.assertEqual(1, len(result)) self.assertEqual('3', result[0].id) self.assertEqual(16, result[0].value) # modify a document, and redo the query doc = self.db['0'] doc['a'] = 4 self.db['0'] = doc result = list(self.db.query(query)) self.assertEqual(2, len(result)) # add more documents, and redo the query again self.db.save({'a': 3, 'b': 9}) self.db.save({'a': 4, 'b': 16}) result = list(self.db.query(query)) self.assertEqual(3, len(result)) self.assertEqual(6, len(self.db)) # delete a document, and redo the query once more del self.db['0'] result = list(self.db.query(query)) self.assertEqual(2, len(result)) self.assertEqual(5, len(self.db)) def test_conflict_detection(self): doc1 = {'a': 1, 'b': 1} self.db['foo'] = doc1 doc2 = self.db['foo'] self.assertEqual(doc1['_id'], doc2.id) self.assertEqual(doc1['_rev'], doc2.rev) # make conflicting modifications doc1['a'] = 2 doc2['a'] = 3 self.db['foo'] = doc1 self.assertRaises(ResourceConflict, self.db.__setitem__, 'foo', doc2) # try submitting without the revision info data = {'_id': 'foo', 'a': 3, 'b': 1} self.assertRaises(ResourceConflict, self.db.__setitem__, 'foo', data) del self.db['foo'] self.db['foo'] = data def test_lots_of_docs(self): num = 100 # Crank up manually to really test for i in range(num): self.db[str(i)] = {'integer': i, 'string': str(i)} self.assertEqual(num, len(self.db)) query = """function(doc) { emit(doc.integer, null); }""" results = list(self.db.query(query)) self.assertEqual(num, len(results)) for idx, row in enumerate(results): self.assertEqual(idx, row.key) results = list(self.db.query(query, descending=True)) self.assertEqual(num, len(results)) for idx, row in enumerate(results): self.assertEqual(num - idx - 1, row.key) def test_multiple_rows(self): self.db['NC'] = {'cities': ["Charlotte", "Raleigh"]} self.db['MA'] = {'cities': ["Boston", "Lowell", "Worcester", "Cambridge", "Springfield"]} self.db['FL'] = {'cities': ["Miami", "Tampa", "Orlando", "Springfield"]} query = """function(doc){ for (var i = 0; i < doc.cities.length; i++) { emit(doc.cities[i] + ", " + doc._id, null); } }""" results = list(self.db.query(query)) self.assertEqual(11, len(results)) self.assertEqual("Boston, MA", results[0].key); self.assertEqual("Cambridge, MA", results[1].key); self.assertEqual("Charlotte, NC", results[2].key); self.assertEqual("Lowell, MA", results[3].key); self.assertEqual("Miami, FL", results[4].key); self.assertEqual("Orlando, FL", results[5].key); self.assertEqual("Raleigh, NC", results[6].key); self.assertEqual("Springfield, FL", results[7].key); self.assertEqual("Springfield, MA", results[8].key); self.assertEqual("Tampa, FL", results[9].key); self.assertEqual("Worcester, MA", results[10].key); # Add a city and rerun the query doc = self.db['NC'] doc['cities'].append("Wilmington") self.db['NC'] = doc results = list(self.db.query(query)) self.assertEqual(12, len(results)) self.assertEqual("Wilmington, NC", results[10].key) # Remove a document and redo the query again del self.db['MA'] results = list(self.db.query(query)) self.assertEqual(7, len(results)) self.assertEqual("Charlotte, NC", results[0].key); self.assertEqual("Miami, FL", results[1].key); self.assertEqual("Orlando, FL", results[2].key); self.assertEqual("Raleigh, NC", results[3].key); self.assertEqual("Springfield, FL", results[4].key); self.assertEqual("Tampa, FL", results[5].key); self.assertEqual("Wilmington, NC", results[6].key) def test_large_docs(self): size = 100 longtext = '0123456789\n' * size self.db.save({'longtext': longtext}) self.db.save({'longtext': longtext}) self.db.save({'longtext': longtext}) self.db.save({'longtext': longtext}) query = """function(doc) { emit(null, doc.longtext); }""" results = list(self.db.query(query)) self.assertEqual(4, len(results)) def test_utf8_encoding(self): texts = [ u"1. Ascii: hello", u"2. Russian: На берегу пустынных волн", u"3. Math: ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i),", u"4. Geek: STARGΛ̊TE SG-1", u"5. Braille: ⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌" ] for idx, text in enumerate(texts): self.db[str(idx)] = {'text': text} for idx, text in enumerate(texts): doc = self.db[str(idx)] self.assertEqual(text, doc['text']) query = """function(doc) { emit(doc.text, null); }""" for idx, row in enumerate(self.db.query(query)): self.assertEqual(texts[idx], row.key) def test_design_docs(self): for i in range(50): self.db[str(i)] = {'integer': i, 'string': str(i)} self.db['_design/test'] = {'views': { 'all_docs': {'map': 'function(doc) { emit(doc.integer, null) }'}, 'no_docs': {'map': 'function(doc) {}'}, 'single_doc': {'map': 'function(doc) { if (doc._id == "1") emit(null, 1) }'} }} for idx, row in enumerate(self.db.view('test/all_docs')): self.assertEqual(idx, row.key) self.assertEqual(0, len(list(self.db.view('test/no_docs')))) self.assertEqual(1, len(list(self.db.view('test/single_doc')))) def test_collation(self): values = [ None, False, True, 1, 2, 3.0, 4, 'a', 'A', 'aa', 'b', 'B', 'ba', 'bb', ['a'], ['b'], ['b', 'c'], ['b', 'c', 'a'], ['b', 'd'], ['b', 'd', 'e'], {'a': 1}, {'a': 2}, {'b': 1}, {'b': 2}, {'b': 2, 'c': 2}, ] self.db['0'] = {'bar': 0} for idx, value in enumerate(values): self.db[str(idx + 1)] = {'foo': value} query = """function(doc) { if(doc.foo !== undefined) { emit(doc.foo, null); } }""" rows = iter(self.db.query(query)) self.assertEqual(None, rows.next().value) for idx, row in enumerate(rows): self.assertEqual(values[idx + 1], row.key) rows = self.db.query(query, descending=True) for idx, row in enumerate(rows): if idx < len(values): self.assertEqual(values[len(values) - 1- idx], row.key) else: self.assertEqual(None, row.value) for value in values: rows = list(self.db.query(query, key=value)) self.assertEqual(1, len(rows)) self.assertEqual(value, rows[0].key) def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(CouchTests, 'test')) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') CouchDB-0.8/couchdb/tools/0000755000175000001440000000000011431232253014052 5ustar djcusersCouchDB-0.8/couchdb/tools/replicate.py0000755000175000001440000000676211431231104016404 0ustar djcusers#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2009 Maximillian Dornseif # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. """ This script replicates databases from one CouchDB server to an other. This is mainly for backup purposes or "priming" a new server before setting up trigger based replication. But you can also use the '--continuous' option to set up automatic replication on newer CouchDB versions. Use 'python replicate.py --help' to get more detailed usage instructions. """ from couchdb import http, client import optparse import sys import time import urllib import urlparse import fnmatch def findpath(parser, s): '''returns (server url, path component)''' if s == '.': return client.DEFAULT_BASE_URL, '' if not s.startswith('http'): return client.DEFAULT_BASE_URL, s bits = urlparse.urlparse(s) res = http.Resource('%s://%s/' % (bits.scheme, bits.netloc), None) parts = bits.path.split('/')[1:] if parts and not parts[-1]: parts = parts[:-1] cut = None for i in range(0, len(parts) + 1): try: data = res.get_json(parts[:i])[2] except Exception: data = None if data and 'couchdb' in data: cut = i if cut is None: raise parser.error("'%s' does not appear to be a CouchDB" % s) base = res.url + (parts[:cut] and '/'.join(parts[:cut]) or '') return base, '/'.join(parts[cut:]) def main(): usage = '%prog [options] ' parser = optparse.OptionParser(usage=usage) parser.add_option('--continuous', action='store_true', dest='continuous', help='trigger continuous replication in cochdb') parser.add_option('--compact', action='store_true', dest='compact', help='compact target database after replication') options, args = parser.parse_args() if len(args) != 2: raise parser.error('need source and target arguments') # set up server objects src, tgt = args sbase, spath = findpath(parser, src) source = client.Server(sbase) tbase, tpath = findpath(parser, tgt) target = client.Server(tbase) # check database name specs if '*' in tpath: raise parser.error('invalid target path: must be single db or empty') elif '*' in spath and tpath: raise parser.error('target path must be empty with multiple sources') all = sorted(i for i in source if i[0] != '_') # Skip reserved names. if not spath: raise parser.error('source database must be specified') databases = [(i, i) for i in all if fnmatch.fnmatchcase(i, spath)] if not databases: raise parser.error("no source databases match glob '%s'" % spath) # do the actual replication for sdb, tdb in databases: start = time.time() print sdb, '->', tdb, sys.stdout.flush() if tdb not in target: target.create(tdb) print "created", sys.stdout.flush() sdb = '%s%s' % (sbase, urllib.quote(sdb, '')) if options.continuous: target.replicate(sdb, tdb, continuous=options.continuous) else: target.replicate(sdb, tdb) print '%.1fs' % (time.time() - start) sys.stdout.flush() if options.compact: for (sdb, tdb) in databases: print 'compact', tdb target[tdb].compact() if __name__ == '__main__': main() CouchDB-0.8/couchdb/tools/dump.py0000755000175000001440000000527711401462170015407 0ustar djcusers#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. """Utility for dumping a snapshot of a CouchDB database to a multipart MIME file. """ from base64 import b64decode from optparse import OptionParser import sys from couchdb import __version__ as VERSION from couchdb import json from couchdb.client import Database from couchdb.multipart import write_multipart def dump_db(dburl, username=None, password=None, boundary=None, output=sys.stdout): db = Database(dburl) if username is not None and password is not None: db.resource.http.add_credentials(username, password) envelope = write_multipart(output, boundary=boundary) for docid in db: doc = db.get(docid, attachments=True) print >> sys.stderr, 'Dumping document %r' % doc.id attachments = doc.pop('_attachments', {}) jsondoc = json.encode(doc) if attachments: parts = envelope.open({ 'Content-ID': doc.id, 'ETag': '"%s"' % doc.rev }) parts.add('application/json', jsondoc) for name, info in attachments.items(): content_type = info.get('content_type') if content_type is None: # CouchDB < 0.8 content_type = info.get('content-type') parts.add(content_type, b64decode(info['data']), { 'Content-ID': name }) parts.close() else: envelope.add('application/json', jsondoc, { 'Content-ID': doc.id, 'ETag': '"%s"' % doc.rev }) envelope.close() def main(): parser = OptionParser(usage='%prog [options] dburl', version=VERSION) parser.add_option('--json-module', action='store', dest='json_module', help='the JSON module to use ("simplejson", "cjson", ' 'or "json" are supported)') parser.add_option('-u', '--username', action='store', dest='username', help='the username to use for authentication') parser.add_option('-p', '--password', action='store', dest='password', help='the password to use for authentication') parser.set_defaults() options, args = parser.parse_args() if len(args) != 1: return parser.error('incorrect number of arguments') if options.json_module: json.use(options.json_module) dump_db(args[0], username=options.username, password=options.password) if __name__ == '__main__': main() CouchDB-0.8/couchdb/tools/load.py0000755000175000001440000000623511401462170015354 0ustar djcusers#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. """Utility for loading a snapshot of a CouchDB database from a multipart MIME file. """ from base64 import b64encode from optparse import OptionParser import sys from couchdb import __version__ as VERSION from couchdb import json from couchdb.client import Database from couchdb.multipart import read_multipart def load_db(fileobj, dburl, username=None, password=None, ignore_errors=False): db = Database(dburl) if username is not None and password is not None: db.resource.http.add_credentials(username, password) for headers, is_multipart, payload in read_multipart(fileobj): docid = headers['content-id'] if is_multipart: # doc has attachments for headers, _, payload in payload: if 'content-id' not in headers: doc = json.decode(payload) doc['_attachments'] = {} else: doc['_attachments'][headers['content-id']] = { 'data': b64encode(payload), 'content_type': headers['content-type'], 'length': len(payload) } else: # no attachments, just the JSON doc = json.decode(payload) del doc['_rev'] print>>sys.stderr, 'Loading document %r' % docid try: db[docid] = doc except Exception, e: if not ignore_errors: raise print>>sys.stderr, 'Error: %s' % e def main(): parser = OptionParser(usage='%prog [options] dburl', version=VERSION) parser.add_option('--input', action='store', dest='input', metavar='FILE', help='the name of the file to read from') parser.add_option('--ignore-errors', action='store_true', dest='ignore_errors', help='whether to ignore errors in document creation ' 'and continue with the remaining documents') parser.add_option('--json-module', action='store', dest='json_module', help='the JSON module to use ("simplejson", "cjson", ' 'or "json" are supported)') parser.add_option('-u', '--username', action='store', dest='username', help='the username to use for authentication') parser.add_option('-p', '--password', action='store', dest='password', help='the password to use for authentication') parser.set_defaults(input='-') options, args = parser.parse_args() if len(args) != 1: return parser.error('incorrect number of arguments') if options.input != '-': fileobj = open(options.input, 'rb') else: fileobj = sys.stdin if options.json_module: json.use(options.json_module) load_db(fileobj, args[0], username=options.username, password=options.password, ignore_errors=options.ignore_errors) if __name__ == '__main__': main() CouchDB-0.8/couchdb/tools/__init__.py0000644000175000001440000000032611360110237016162 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2008 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. CouchDB-0.8/couchdb/multipart.py0000644000175000001440000002070011401462170015304 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2008-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. """Support for streamed reading and writing of multipart MIME content.""" from base64 import b64encode from cgi import parse_header try: from hashlib import md5 except ImportError: from md5 import new as md5 import sys __all__ = ['read_multipart', 'write_multipart'] __docformat__ = 'restructuredtext en' CRLF = '\r\n' def read_multipart(fileobj, boundary=None): """Simple streaming MIME multipart parser. This function takes a file-like object reading a MIME envelope, and yields a ``(headers, is_multipart, payload)`` tuple for every part found, where ``headers`` is a dictionary containing the MIME headers of that part (with names lower-cased), ``is_multipart`` is a boolean indicating whether the part is itself multipart, and ``payload`` is either a string (if ``is_multipart`` is false), or an iterator over the nested parts. Note that the iterator produced for nested multipart payloads MUST be fully consumed, even if you wish to skip over the content. :param fileobj: a file-like object :param boundary: the part boundary string, will generally be determined automatically from the headers of the outermost multipart envelope :return: an iterator over the parts :since: 0.5 """ headers = {} buf = [] outer = in_headers = boundary is None next_boundary = boundary and '--' + boundary + '\n' or None last_boundary = boundary and '--' + boundary + '--\n' or None def _current_part(): payload = ''.join(buf) if payload.endswith('\r\n'): payload = payload[:-2] elif payload.endswith('\n'): payload = payload[:-1] content_md5 = headers.get('content-md5') if content_md5: h = b64encode(md5(payload).digest()) if content_md5 != h: raise ValueError('data integrity check failed') return headers, False, payload for line in fileobj: if in_headers: line = line.replace(CRLF, '\n') if line != '\n': name, value = line.split(':', 1) headers[name.lower().strip()] = value.strip() else: in_headers = False mimetype, params = parse_header(headers.get('content-type')) if mimetype.startswith('multipart/'): sub_boundary = params['boundary'] sub_parts = read_multipart(fileobj, boundary=sub_boundary) if boundary is not None: yield headers, True, sub_parts headers.clear() del buf[:] else: for part in sub_parts: yield part return elif line.replace(CRLF, '\n') == next_boundary: # We've reached the start of a new part, as indicated by the # boundary if headers: if not outer: yield _current_part() else: outer = False headers.clear() del buf[:] in_headers = True elif line.replace(CRLF, '\n') == last_boundary: # We're done with this multipart envelope break else: buf.append(line) if not outer and headers: yield _current_part() class MultipartWriter(object): def __init__(self, fileobj, headers=None, subtype='mixed', boundary=None): self.fileobj = fileobj if boundary is None: boundary = self._make_boundary() self.boundary = boundary if headers is None: headers = {} headers['Content-Type'] = 'multipart/%s; boundary="%s"' % ( subtype, self.boundary ) self._write_headers(headers) def open(self, headers=None, subtype='mixed', boundary=None): self.fileobj.write('--') self.fileobj.write(self.boundary) self.fileobj.write(CRLF) return MultipartWriter(self.fileobj, headers=headers, subtype=subtype, boundary=boundary) def add(self, mimetype, content, headers=None): self.fileobj.write('--') self.fileobj.write(self.boundary) self.fileobj.write(CRLF) if headers is None: headers = {} if isinstance(content, unicode): ctype, params = parse_header(mimetype) if 'charset' in params: content = content.encode(params['charset']) else: content = content.encode('utf-8') mimetype = mimetype + ';charset=utf-8' headers['Content-Type'] = mimetype if content: headers['Content-Length'] = str(len(content)) headers['Content-MD5'] = b64encode(md5(content).digest()) self._write_headers(headers) if content: # XXX: throw an exception if a boundary appears in the content?? self.fileobj.write(content) self.fileobj.write(CRLF) def close(self): self.fileobj.write('--') self.fileobj.write(self.boundary) self.fileobj.write('--') self.fileobj.write(CRLF) def _make_boundary(self): try: from uuid import uuid4 return '==' + uuid4().hex + '==' except ImportError: from random import randrange token = randrange(sys.maxint) format = '%%0%dd' % len(repr(sys.maxint - 1)) return '===============' + (format % token) + '==' def _write_headers(self, headers): if headers: for name in sorted(headers.keys()): self.fileobj.write(name) self.fileobj.write(': ') self.fileobj.write(headers[name]) self.fileobj.write(CRLF) self.fileobj.write(CRLF) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def write_multipart(fileobj, subtype='mixed', boundary=None): r"""Simple streaming MIME multipart writer. This function returns a `MultipartWriter` object that has a few methods to control the nested MIME parts. For example, to write a flat multipart envelope you call the ``add(mimetype, content, [headers])`` method for every part, and finally call the ``close()`` method. >>> from StringIO import StringIO >>> buf = StringIO() >>> envelope = write_multipart(buf, boundary='==123456789==') >>> envelope.add('text/plain', 'Just testing') >>> envelope.close() >>> print buf.getvalue().replace('\r\n', '\n') Content-Type: multipart/mixed; boundary="==123456789==" --==123456789== Content-Length: 12 Content-MD5: nHmX4a6el41B06x2uCpglQ== Content-Type: text/plain Just testing --==123456789==-- Note that an explicit boundary is only specified for testing purposes. If the `boundary` parameter is omitted, the multipart writer will generate a random string for the boundary. To write nested structures, call the ``open([headers])`` method on the respective envelope, and finish each envelope using the ``close()`` method: >>> buf = StringIO() >>> envelope = write_multipart(buf, boundary='==123456789==') >>> part = envelope.open(boundary='==abcdefghi==') >>> part.add('text/plain', 'Just testing') >>> part.close() >>> envelope.close() >>> print buf.getvalue().replace('\r\n', '\n') #:doctest +ELLIPSIS Content-Type: multipart/mixed; boundary="==123456789==" --==123456789== Content-Type: multipart/mixed; boundary="==abcdefghi==" --==abcdefghi== Content-Length: 12 Content-MD5: nHmX4a6el41B06x2uCpglQ== Content-Type: text/plain Just testing --==abcdefghi==-- --==123456789==-- :param fileobj: a writable file-like object that the output should get written to :param subtype: the subtype of the multipart MIME type (e.g. "mixed") :param boundary: the boundary to use to separate the different parts :since: 0.6 """ return MultipartWriter(fileobj, subtype=subtype, boundary=boundary) CouchDB-0.8/couchdb/design.py0000644000175000001440000001674111431231104014540 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2008-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. """Utility code for managing design documents.""" from copy import deepcopy from inspect import getsource from itertools import groupby from operator import attrgetter from textwrap import dedent from types import FunctionType __all__ = ['ViewDefinition'] __docformat__ = 'restructuredtext en' class ViewDefinition(object): r"""Definition of a view stored in a specific design document. An instance of this class can be used to access the results of the view, as well as to keep the view definition in the design document up to date with the definition in the application code. >>> from couchdb import Server >>> server = Server() >>> db = server.create('python-tests') >>> view = ViewDefinition('tests', 'all', '''function(doc) { ... emit(doc._id, null); ... }''') >>> view.get_doc(db) The view is not yet stored in the database, in fact, design doc doesn't even exist yet. That can be fixed using the `sync` method: >>> view.sync(db) >>> design_doc = view.get_doc(db) >>> design_doc #doctest: +ELLIPSIS >>> print design_doc['views']['all']['map'] function(doc) { emit(doc._id, null); } If you use a Python view server, you can also use Python functions instead of code embedded in strings: >>> def my_map(doc): ... yield doc['somekey'], doc['somevalue'] >>> view = ViewDefinition('test2', 'somename', my_map, language='python') >>> view.sync(db) >>> design_doc = view.get_doc(db) >>> design_doc #doctest: +ELLIPSIS >>> print design_doc['views']['somename']['map'] def my_map(doc): yield doc['somekey'], doc['somevalue'] Use the static `sync_many()` method to create or update a collection of views in the database in an atomic and efficient manner, even across different design documents. >>> del server['python-tests'] """ def __init__(self, design, name, map_fun, reduce_fun=None, language='javascript', wrapper=None, **defaults): """Initialize the view definition. Note that the code in `map_fun` and `reduce_fun` is automatically dedented, that is, any common leading whitespace is removed from each line. :param design: the name of the design document :param name: the name of the view :param map_fun: the map function code :param reduce_fun: the reduce function code (optional) :param language: the name of the language used :param wrapper: an optional callable that should be used to wrap the result rows """ if design.startswith('_design/'): design = design[8:] self.design = design self.name = name if isinstance(map_fun, FunctionType): map_fun = _strip_decorators(getsource(map_fun).rstrip()) self.map_fun = dedent(map_fun.lstrip('\n')) if isinstance(reduce_fun, FunctionType): reduce_fun = _strip_decorators(getsource(reduce_fun).rstrip()) if reduce_fun: reduce_fun = dedent(reduce_fun.lstrip('\n')) self.reduce_fun = reduce_fun self.language = language self.wrapper = wrapper self.defaults = defaults def __call__(self, db, **options): """Execute the view in the given database. :param db: the `Database` instance :param options: optional query string parameters :return: the view results :rtype: `ViewResults` """ merged_options = self.defaults.copy() merged_options.update(options) return db.view('/'.join([self.design, self.name]), wrapper=self.wrapper, **merged_options) def __repr__(self): return '<%s %r>' % (type(self).__name__, '/'.join([ '_design', self.design, '_view', self.name ])) def get_doc(self, db): """Retrieve and return the design document corresponding to this view definition from the given database. :param db: the `Database` instance :return: a `client.Document` instance, or `None` if the design document does not exist in the database :rtype: `Document` """ return db.get('_design/%s' % self.design) def sync(self, db): """Ensure that the view stored in the database matches the view defined by this instance. :param db: the `Database` instance """ type(self).sync_many(db, [self]) @staticmethod def sync_many(db, views, remove_missing=False, callback=None): """Ensure that the views stored in the database that correspond to a given list of `ViewDefinition` instances match the code defined in those instances. This function might update more than one design document. This is done using the CouchDB bulk update feature to ensure atomicity of the operation. :param db: the `Database` instance :param views: a sequence of `ViewDefinition` instances :param remove_missing: whether views found in a design document that are not found in the list of `ViewDefinition` instances should be removed :param callback: a callback function that is invoked when a design document gets updated; the callback gets passed the design document as only parameter, before that doc has actually been saved back to the database """ docs = [] for design, views in groupby(views, key=attrgetter('design')): doc_id = '_design/%s' % design doc = db.get(doc_id, {'_id': doc_id}) orig_doc = deepcopy(doc) languages = set() missing = list(doc.get('views', {}).keys()) for view in views: funcs = {'map': view.map_fun} if view.reduce_fun: funcs['reduce'] = view.reduce_fun doc.setdefault('views', {})[view.name] = funcs languages.add(view.language) if view.name in missing: missing.remove(view.name) if remove_missing and missing: for name in missing: del doc['views'][name] elif missing and 'language' in doc: languages.add(doc['language']) if len(languages) > 1: raise ValueError('Found different language views in one ' 'design document (%r)', list(languages)) doc['language'] = list(languages)[0] if doc != orig_doc: if callback is not None: callback(doc) docs.append(doc) db.update(docs) def _strip_decorators(code): retval = [] beginning = True for line in code.splitlines(): if beginning and not line.isspace(): if line.lstrip().startswith('@'): continue beginning = False retval.append(line) return '\n'.join(retval) CouchDB-0.8/couchdb/client.py0000644000175000001440000011114511431231104014537 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. """Python client API for CouchDB. >>> server = Server() >>> db = server.create('python-tests') >>> doc_id, doc_rev = db.save({'type': 'Person', 'name': 'John Doe'}) >>> doc = db[doc_id] >>> doc['type'] 'Person' >>> doc['name'] 'John Doe' >>> del db[doc.id] >>> doc.id in db False >>> del server['python-tests'] """ import mimetypes import os from types import FunctionType from inspect import getsource from textwrap import dedent import re import warnings from couchdb import http, json __all__ = ['Server', 'Database', 'Document', 'ViewResults', 'Row'] __docformat__ = 'restructuredtext en' DEFAULT_BASE_URL = os.environ.get('COUCHDB_URL', 'http://localhost:5984/') class Server(object): """Representation of a CouchDB server. >>> server = Server() This class behaves like a dictionary of databases. For example, to get a list of database names on the server, you can simply iterate over the server object. New databases can be created using the `create` method: >>> db = server.create('python-tests') >>> db You can access existing databases using item access, specifying the database name as the key: >>> db = server['python-tests'] >>> db.name 'python-tests' Databases can be deleted using a ``del`` statement: >>> del server['python-tests'] """ def __init__(self, url=DEFAULT_BASE_URL, full_commit=True, session=None): """Initialize the server object. :param url: the URI of the server (for example ``http://localhost:5984/``) :param full_commit: turn on the X-Couch-Full-Commit header :param session: an http.Session instance or None for a default session """ if isinstance(url, basestring): self.resource = http.Resource(url, session or http.Session()) else: self.resource = url # treat as a Resource object if not full_commit: self.resource.headers['X-Couch-Full-Commit'] = 'false' def __contains__(self, name): """Return whether the server contains a database with the specified name. :param name: the database name :return: `True` if a database with the name exists, `False` otherwise """ try: self.resource.head(validate_dbname(name)) return True except http.ResourceNotFound: return False def __iter__(self): """Iterate over the names of all databases.""" status, headers, data = self.resource.get_json('_all_dbs') return iter(data) def __len__(self): """Return the number of databases.""" status, headers, data = self.resource.get_json('_all_dbs') return len(data) def __nonzero__(self): """Return whether the server is available.""" try: self.resource.head() return True except: return False def __repr__(self): return '<%s %r>' % (type(self).__name__, self.resource.url) def __delitem__(self, name): """Remove the database with the specified name. :param name: the name of the database :raise ResourceNotFound: if no database with that name exists """ self.resource.delete_json(validate_dbname(name)) def __getitem__(self, name): """Return a `Database` object representing the database with the specified name. :param name: the name of the database :return: a `Database` object representing the database :rtype: `Database` :raise ResourceNotFound: if no database with that name exists """ db = Database(self.resource(name), validate_dbname(name)) db.resource.head() # actually make a request to the database return db def config(self): """The configuration of the CouchDB server. The configuration is represented as a nested dictionary of sections and options from the configuration files of the server, or the default values for options that are not explicitly configured. :rtype: `dict` """ status, headers, data = self.resource.get_json('_config') return data def version(self): """The version string of the CouchDB server. Note that this results in a request being made, and can also be used to check for the availability of the server. :rtype: `unicode`""" status, headers, data = self.resource.get_json() return data['version'] def stats(self): """Database statistics.""" status, headers, data = self.resource.get_json('_stats') return data def tasks(self): """A list of tasks currently active on the server.""" status, headers, data = self.resource.get_json('_active_tasks') return data def uuids(self, count=None): """Retrieve a batch of uuids :param count: a number of uuids to fetch (None -- get as many as the server sends) :return: a list of uuids """ if count is None: _, _, data = self.resource.get_json('_uuids') else: _, _, data = self.resource.get_json('_uuids', count=count) return data['uuids'] def create(self, name): """Create a new database with the given name. :param name: the name of the database :return: a `Database` object representing the created database :rtype: `Database` :raise PreconditionFailed: if a database with that name already exists """ self.resource.put_json(validate_dbname(name)) return self[name] def delete(self, name): """Delete the database with the specified name. :param name: the name of the database :raise ResourceNotFound: if a database with that name does not exist :since: 0.6 """ del self[name] def replicate(self, source, target, **options): """Replicate changes from the source database to the target database. :param source: URL of the source database :param target: URL of the target database :param options: optional replication args, e.g. continuous=True """ data = {'source': source, 'target': target} data.update(options) status, headers, data = self.resource.post_json('_replicate', data) return data class Database(object): """Representation of a database on a CouchDB server. >>> server = Server() >>> db = server.create('python-tests') New documents can be added to the database using the `save()` method: >>> doc_id, doc_rev = db.save({'type': 'Person', 'name': 'John Doe'}) This class provides a dictionary-like interface to databases: documents are retrieved by their ID using item access >>> doc = db[doc_id] >>> doc #doctest: +ELLIPSIS Documents are represented as instances of the `Row` class, which is basically just a normal dictionary with the additional attributes ``id`` and ``rev``: >>> doc.id, doc.rev #doctest: +ELLIPSIS ('...', ...) >>> doc['type'] 'Person' >>> doc['name'] 'John Doe' To update an existing document, you use item access, too: >>> doc['name'] = 'Mary Jane' >>> db[doc.id] = doc The `save()` method creates a document with a random ID generated by CouchDB (which is not recommended). If you want to explicitly specify the ID, you'd use item access just as with updating: >>> db['JohnDoe'] = {'type': 'person', 'name': 'John Doe'} >>> 'JohnDoe' in db True >>> len(db) 2 >>> del server['python-tests'] """ def __init__(self, url, name=None, session=None): if isinstance(url, basestring): if not url.startswith('http'): url = DEFAULT_BASE_URL + url self.resource = http.Resource(url, session) else: self.resource = url self._name = name def __repr__(self): return '<%s %r>' % (type(self).__name__, self.name) def __contains__(self, id): """Return whether the database contains a document with the specified ID. :param id: the document ID :return: `True` if a document with the ID exists, `False` otherwise """ try: self.resource.head(id) return True except http.ResourceNotFound: return False def __iter__(self): """Return the IDs of all documents in the database.""" return iter([item.id for item in self.view('_all_docs')]) def __len__(self): """Return the number of documents in the database.""" _, _, data = self.resource.get_json() return data['doc_count'] def __nonzero__(self): """Return whether the database is available.""" try: self.resource.head() return True except: return False def __delitem__(self, id): """Remove the document with the specified ID from the database. :param id: the document ID """ status, headers, data = self.resource.head(id) self.resource.delete_json(id, rev=headers['etag'].strip('"')) def __getitem__(self, id): """Return the document with the specified ID. :param id: the document ID :return: a `Row` object representing the requested document :rtype: `Document` """ _, _, data = self.resource.get_json(id) return Document(data) def __setitem__(self, id, content): """Create or update a document with the specified ID. :param id: the document ID :param content: the document content; either a plain dictionary for new documents, or a `Row` object for existing documents """ status, headers, data = self.resource.put_json(id, body=content) content.update({'_id': data['id'], '_rev': data['rev']}) @property def name(self): """The name of the database. Note that this may require a request to the server unless the name has already been cached by the `info()` method. :rtype: basestring """ if self._name is None: self.info() return self._name def create(self, data): """Create a new document in the database with a random ID that is generated by the server. Note that it is generally better to avoid the `create()` method and instead generate document IDs on the client side. This is due to the fact that the underlying HTTP ``POST`` method is not idempotent, and an automatic retry due to a problem somewhere on the networking stack may cause multiple documents being created in the database. To avoid such problems you can generate a UUID on the client side. Python (since version 2.5) comes with a ``uuid`` module that can be used for this:: from uuid import uuid4 doc_id = uuid4().hex db[doc_id] = {'type': 'person', 'name': 'John Doe'} :param data: the data to store in the document :return: the ID of the created document :rtype: `unicode` """ warnings.warn('Database.create is deprecated, please use Database.save instead [2010-04-13]', DeprecationWarning, stacklevel=2) _, _, data = self.resource.post_json(body=data) return data['id'] def save(self, doc, **options): """Create a new document or update an existing document. If doc has no _id then the server will allocate a random ID and a new document will be created. Otherwise the doc's _id will be used to identity the document to create or update. Trying to update an existing document with an incorrect _rev will raise a ResourceConflict exception. Note that it is generally better to avoid saving documents with no _id and instead generate document IDs on the client side. This is due to the fact that the underlying HTTP ``POST`` method is not idempotent, and an automatic retry due to a problem somewhere on the networking stack may cause multiple documents being created in the database. To avoid such problems you can generate a UUID on the client side. Python (since version 2.5) comes with a ``uuid`` module that can be used for this:: from uuid import uuid4 doc = {'_id': uuid4().hex, 'type': 'person', 'name': 'John Doe'} db.save(doc) :param doc: the document to store :param options: optional args, e.g. batch='ok' :return: (id, rev) tuple of the save document :rtype: `tuple` """ if '_id' in doc: func = self.resource(doc['_id']).put_json else: func = self.resource.post_json _, _, data = func(body=doc, **options) id, rev = data['id'], data.get('rev') doc['_id'] = id if rev is not None: # Not present for batch='ok' doc['_rev'] = rev return id, rev def commit(self): """If the server is configured to delay commits, or previous requests used the special ``X-Couch-Full-Commit: false`` header to disable immediate commits, this method can be used to ensure that any non-committed changes are committed to physical storage. """ _, _, data = self.resource.post_json( '_ensure_full_commit', headers={'Content-Type': 'application/json'}) return data def compact(self, ddoc=None): """Compact the database or a design document's index. Without an argument, this will try to prune all old revisions from the database. With an argument, it will compact the index cache for all views in the design document specified. :return: a boolean to indicate whether the compaction was initiated successfully :rtype: `bool` """ if ddoc: resource = self.resource('_compact', ddoc) else: resource = self.resource('_compact') _, _, data = resource.post_json( headers={'Content-Type': 'application/json'}) return data['ok'] def copy(self, src, dest): """Copy the given document to create a new document. :param src: the ID of the document to copy, or a dictionary or `Document` object representing the source document. :param dest: either the destination document ID as string, or a dictionary or `Document` instance of the document that should be overwritten. :return: the new revision of the destination document :rtype: `str` :since: 0.6 """ if not isinstance(src, basestring): if not isinstance(src, dict): if hasattr(src, 'items'): src = dict(src.items()) else: raise TypeError('expected dict or string, got %s' % type(src)) src = src['_id'] if not isinstance(dest, basestring): if not isinstance(dest, dict): if hasattr(dest, 'items'): dest = dict(dest.items()) else: raise TypeError('expected dict or string, got %s' % type(dest)) if '_rev' in dest: dest = '%s?%s' % (http.quote(dest['_id']), http.urlencode({'rev': dest['_rev']})) else: dest = http.quote(dest['_id']) _, _, data = self.resource._request('COPY', src, headers={'Destination': dest}) data = json.decode(data.read()) return data['rev'] def delete(self, doc): """Delete the given document from the database. Use this method in preference over ``__del__`` to ensure you're deleting the revision that you had previously retrieved. In the case the document has been updated since it was retrieved, this method will raise a `ResourceConflict` exception. >>> server = Server() >>> db = server.create('python-tests') >>> doc = dict(type='Person', name='John Doe') >>> db['johndoe'] = doc >>> doc2 = db['johndoe'] >>> doc2['age'] = 42 >>> db['johndoe'] = doc2 >>> db.delete(doc) Traceback (most recent call last): ... ResourceConflict: ('conflict', 'Document update conflict.') >>> del server['python-tests'] :param doc: a dictionary or `Document` object holding the document data :raise ResourceConflict: if the document was updated in the database :since: 0.4.1 """ if doc['_id'] is None: raise ValueError('document ID cannot be None') self.resource.delete_json(doc['_id'], rev=doc['_rev']) def get(self, id, default=None, **options): """Return the document with the specified ID. :param id: the document ID :param default: the default value to return when the document is not found :return: a `Row` object representing the requested document, or `None` if no document with the ID was found :rtype: `Document` """ try: _, _, data = self.resource.get_json(id, **options) except http.ResourceNotFound: return default if hasattr(data, 'items'): return Document(data) else: return data def revisions(self, id, **options): """Return all available revisions of the given document. :param id: the document ID :return: an iterator over Document objects, each a different revision, in reverse chronological order, if any were found """ try: status, headers, data = self.resource.get_json(id, revs=True) except http.ResourceNotFound: return startrev = data['_revisions']['start'] for index, rev in enumerate(data['_revisions']['ids']): options['rev'] = '%d-%s' % (startrev - index, rev) revision = self.get(id, **options) if revision is None: return yield revision def info(self): """Return information about the database as a dictionary. The returned dictionary exactly corresponds to the JSON response to a ``GET`` request on the database URI. :return: a dictionary of database properties :rtype: ``dict`` :since: 0.4 """ _, _, data = self.resource.get_json() self._name = data['db_name'] return data def delete_attachment(self, doc, filename): """Delete the specified attachment. Note that the provided `doc` is required to have a ``_rev`` field. Thus, if the `doc` is based on a view row, the view row would need to include the ``_rev`` field. :param doc: the dictionary or `Document` object representing the document that the attachment belongs to :param filename: the name of the attachment file :since: 0.4.1 """ resource = self.resource(doc['_id']) _, _, data = resource.delete_json(filename, rev=doc['_rev']) doc['_rev'] = data['rev'] def get_attachment(self, id_or_doc, filename, default=None): """Return an attachment from the specified doc id and filename. :param id_or_doc: either a document ID or a dictionary or `Document` object representing the document that the attachment belongs to :param filename: the name of the attachment file :param default: default value to return when the document or attachment is not found :return: a file-like object with read and close methods, or the value of the `default` argument if the attachment is not found :since: 0.4.1 """ if isinstance(id_or_doc, basestring): id = id_or_doc else: id = id_or_doc['_id'] try: _, _, data = self.resource(id).get(filename) return data except http.ResourceNotFound: return default def put_attachment(self, doc, content, filename=None, content_type=None): """Create or replace an attachment. Note that the provided `doc` is required to have a ``_rev`` field. Thus, if the `doc` is based on a view row, the view row would need to include the ``_rev`` field. :param doc: the dictionary or `Document` object representing the document that the attachment should be added to :param content: the content to upload, either a file-like object or a string :param filename: the name of the attachment file; if omitted, this function tries to get the filename from the file-like object passed as the `content` argument value :param content_type: content type of the attachment; if omitted, the MIME type is guessed based on the file name extension :since: 0.4.1 """ if filename is None: if hasattr(content, 'name'): filename = os.path.basename(content.name) else: raise ValueError('no filename specified for attachment') if content_type is None: content_type = ';'.join( filter(None, mimetypes.guess_type(filename)) ) resource = self.resource(doc['_id']) status, headers, data = resource.put_json(filename, body=content, headers={ 'Content-Type': content_type }, rev=doc['_rev']) doc['_rev'] = data['rev'] def query(self, map_fun, reduce_fun=None, language='javascript', wrapper=None, **options): """Execute an ad-hoc query (a "temp view") against the database. >>> server = Server() >>> db = server.create('python-tests') >>> db['johndoe'] = dict(type='Person', name='John Doe') >>> db['maryjane'] = dict(type='Person', name='Mary Jane') >>> db['gotham'] = dict(type='City', name='Gotham City') >>> map_fun = '''function(doc) { ... if (doc.type == 'Person') ... emit(doc.name, null); ... }''' >>> for row in db.query(map_fun): ... print row.key John Doe Mary Jane >>> for row in db.query(map_fun, descending=True): ... print row.key Mary Jane John Doe >>> for row in db.query(map_fun, key='John Doe'): ... print row.key John Doe >>> del server['python-tests'] :param map_fun: the code of the map function :param reduce_fun: the code of the reduce function (optional) :param language: the language of the functions, to determine which view server to use :param wrapper: an optional callable that should be used to wrap the result rows :param options: optional query string parameters :return: the view reults :rtype: `ViewResults` """ return TemporaryView(self.resource('_temp_view'), map_fun, reduce_fun, language=language, wrapper=wrapper)(**options) def update(self, documents, **options): """Perform a bulk update or insertion of the given documents using a single HTTP request. >>> server = Server() >>> db = server.create('python-tests') >>> for doc in db.update([ ... Document(type='Person', name='John Doe'), ... Document(type='Person', name='Mary Jane'), ... Document(type='City', name='Gotham City') ... ]): ... print repr(doc) #doctest: +ELLIPSIS (True, '...', '...') (True, '...', '...') (True, '...', '...') >>> del server['python-tests'] The return value of this method is a list containing a tuple for every element in the `documents` sequence. Each tuple is of the form ``(success, docid, rev_or_exc)``, where ``success`` is a boolean indicating whether the update succeeded, ``docid`` is the ID of the document, and ``rev_or_exc`` is either the new document revision, or an exception instance (e.g. `ResourceConflict`) if the update failed. If an object in the documents list is not a dictionary, this method looks for an ``items()`` method that can be used to convert the object to a dictionary. Effectively this means you can also use this method with `mapping.Document` objects. :param documents: a sequence of dictionaries or `Document` objects, or objects providing a ``items()`` method that can be used to convert them to a dictionary :return: an iterable over the resulting documents :rtype: ``list`` :since: version 0.2 """ docs = [] for doc in documents: if isinstance(doc, dict): docs.append(doc) elif hasattr(doc, 'items'): docs.append(dict(doc.items())) else: raise TypeError('expected dict, got %s' % type(doc)) content = options content.update(docs=docs) _, _, data = self.resource.post_json('_bulk_docs', body=content) results = [] for idx, result in enumerate(data): if 'error' in result: if result['error'] == 'conflict': exc_type = http.ResourceConflict else: # XXX: Any other error types mappable to exceptions here? exc_type = http.ServerError results.append((False, result['id'], exc_type(result['reason']))) else: doc = documents[idx] if isinstance(doc, dict): # XXX: Is this a good idea?? doc.update({'_id': result['id'], '_rev': result['rev']}) results.append((True, result['id'], result['rev'])) return results def view(self, name, wrapper=None, **options): """Execute a predefined view. >>> server = Server() >>> db = server.create('python-tests') >>> db['gotham'] = dict(type='City', name='Gotham City') >>> for row in db.view('_all_docs'): ... print row.id gotham >>> del server['python-tests'] :param name: the name of the view; for custom views, use the format ``design_docid/viewname``, that is, the document ID of the design document and the name of the view, separated by a slash :param wrapper: an optional callable that should be used to wrap the result rows :param options: optional query string parameters :return: the view results :rtype: `ViewResults` """ if not name.startswith('_'): design, name = name.split('/', 1) name = '/'.join(['_design', design, '_view', name]) return PermanentView(self.resource(*name.split('/')), name, wrapper=wrapper)(**options) def _changes(self, **opts): _, _, data = self.resource.get('_changes', **opts) lines = iter(data) for ln in lines: if not ln: # skip heartbeats continue doc = json.decode(ln) if 'last_seq' in doc: # consume the rest of the response if this for ln in lines: # was the last line, allows conn reuse pass yield doc def changes(self, **opts): """Retrieve a changes feed from the database. Takes since, feed, heartbeat and timeout options. """ if opts.get('feed') == 'continuous': return self._changes(**opts) _, _, data = self.resource.get_json('_changes', **opts) return data class Document(dict): """Representation of a document in the database. This is basically just a dictionary with the two additional properties `id` and `rev`, which contain the document ID and revision, respectively. """ def __repr__(self): return '<%s %r@%r %r>' % (type(self).__name__, self.id, self.rev, dict([(k,v) for k,v in self.items() if k not in ('_id', '_rev')])) @property def id(self): """The document ID. :rtype: basestring """ return self['_id'] @property def rev(self): """The document revision. :rtype: basestring """ return self['_rev'] class View(object): """Abstract representation of a view or query.""" def __init__(self, url, wrapper=None, session=None): if isinstance(url, basestring): self.resource = http.Resource(url, session) else: self.resource = url self.wrapper = wrapper def __call__(self, **options): return ViewResults(self, options) def __iter__(self): return iter(self()) def _encode_options(self, options): retval = {} for name, value in options.items(): if name in ('key', 'startkey', 'endkey') \ or not isinstance(value, basestring): value = json.encode(value) retval[name] = value return retval def _exec(self, options): raise NotImplementedError class PermanentView(View): """Representation of a permanent view on the server.""" def __init__(self, uri, name, wrapper=None, session=None): View.__init__(self, uri, wrapper=wrapper, session=session) self.name = name def __repr__(self): return '<%s %r>' % (type(self).__name__, self.name) def _exec(self, options): if 'keys' in options: options = options.copy() keys = {'keys': options.pop('keys')} _, _, data = self.resource.post_json(body=keys, **self._encode_options(options)) else: _, _, data = self.resource.get_json(**self._encode_options(options)) return data class TemporaryView(View): """Representation of a temporary view.""" def __init__(self, uri, map_fun, reduce_fun=None, language='javascript', wrapper=None, session=None): View.__init__(self, uri, wrapper=wrapper, session=session) if isinstance(map_fun, FunctionType): map_fun = getsource(map_fun).rstrip('\n\r') self.map_fun = dedent(map_fun.lstrip('\n\r')) if isinstance(reduce_fun, FunctionType): reduce_fun = getsource(reduce_fun).rstrip('\n\r') if reduce_fun: reduce_fun = dedent(reduce_fun.lstrip('\n\r')) self.reduce_fun = reduce_fun self.language = language def __repr__(self): return '<%s %r %r>' % (type(self).__name__, self.map_fun, self.reduce_fun) def _exec(self, options): body = {'map': self.map_fun, 'language': self.language} if self.reduce_fun: body['reduce'] = self.reduce_fun if 'keys' in options: options = options.copy() body['keys'] = options.pop('keys') content = json.encode(body).encode('utf-8') _, _, data = self.resource.post_json(body=content, headers={ 'Content-Type': 'application/json' }, **self._encode_options(options)) return data class ViewResults(object): """Representation of a parameterized view (either permanent or temporary) and the results it produces. This class allows the specification of ``key``, ``startkey``, and ``endkey`` options using Python slice notation. >>> server = Server() >>> db = server.create('python-tests') >>> db['johndoe'] = dict(type='Person', name='John Doe') >>> db['maryjane'] = dict(type='Person', name='Mary Jane') >>> db['gotham'] = dict(type='City', name='Gotham City') >>> map_fun = '''function(doc) { ... emit([doc.type, doc.name], doc.name); ... }''' >>> results = db.query(map_fun) At this point, the view has not actually been accessed yet. It is accessed as soon as it is iterated over, its length is requested, or one of its `rows`, `total_rows`, or `offset` properties are accessed: >>> len(results) 3 You can use slices to apply ``startkey`` and/or ``endkey`` options to the view: >>> people = results[['Person']:['Person','ZZZZ']] >>> for person in people: ... print person.value John Doe Mary Jane >>> people.total_rows, people.offset (3, 1) Use plain indexed notation (without a slice) to apply the ``key`` option. Note that as CouchDB makes no claim that keys are unique in a view, this can still return multiple rows: >>> list(results[['City', 'Gotham City']]) [] >>> del server['python-tests'] """ def __init__(self, view, options): self.view = view self.options = options self._rows = self._total_rows = self._offset = None def __repr__(self): return '<%s %r %r>' % (type(self).__name__, self.view, self.options) def __getitem__(self, key): options = self.options.copy() if type(key) is slice: if key.start is not None: options['startkey'] = key.start if key.stop is not None: options['endkey'] = key.stop return ViewResults(self.view, options) else: options['key'] = key return ViewResults(self.view, options) def __iter__(self): return iter(self.rows) def __len__(self): return len(self.rows) def _fetch(self): data = self.view._exec(self.options) wrapper = self.view.wrapper or Row self._rows = [wrapper(row) for row in data['rows']] self._total_rows = data.get('total_rows') self._offset = data.get('offset', 0) @property def rows(self): """The list of rows returned by the view. :rtype: `list` """ if self._rows is None: self._fetch() return self._rows @property def total_rows(self): """The total number of rows in this view. This value is `None` for reduce views. :rtype: `int` or ``NoneType`` for reduce views """ if self._rows is None: self._fetch() return self._total_rows @property def offset(self): """The offset of the results from the first row in the view. This value is 0 for reduce views. :rtype: `int` """ if self._rows is None: self._fetch() return self._offset class Row(dict): """Representation of a row as returned by database views.""" def __repr__(self): if self.id is None: return '<%s key=%r, value=%r>' % (type(self).__name__, self.key, self.value) return '<%s id=%r, key=%r, value=%r>' % (type(self).__name__, self.id, self.key, self.value) @property def id(self): """The associated Document ID if it exists. Returns `None` when it doesn't (reduce results). """ return self.get('id') @property def key(self): """The associated key.""" return self['key'] @property def value(self): """The associated value.""" return self['value'] @property def doc(self): """The associated document for the row. This is only present when the view was accessed with ``include_docs=True`` as a query parameter, otherwise this property will be `None`. """ doc = self.get('doc') if doc: return Document(doc) SPECIAL_DB_NAMES = set(['_users']) VALID_DB_NAME = re.compile(r'^[a-z][a-z0-9_$()+-/]*$') def validate_dbname(name): if name in SPECIAL_DB_NAMES: return name if not VALID_DB_NAME.match(name): raise ValueError('Invalid database name') return name CouchDB-0.8/couchdb/mapping.py0000644000175000001440000005350111431231104014715 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. """Mapping from raw JSON data structures to Python objects and vice versa. >>> from couchdb import Server >>> server = Server() >>> db = server.create('python-tests') To define a document mapping, you declare a Python class inherited from `Document`, and add any number of `Field` attributes: >>> from couchdb.mapping import TextField, IntegerField, DateField >>> class Person(Document): ... name = TextField() ... age = IntegerField() ... added = DateTimeField(default=datetime.now) >>> person = Person(name='John Doe', age=42) >>> person.store(db) #doctest: +ELLIPSIS >>> person.age 42 You can then load the data from the CouchDB server through your `Document` subclass, and conveniently access all attributes: >>> person = Person.load(db, person.id) >>> old_rev = person.rev >>> person.name u'John Doe' >>> person.age 42 >>> person.added #doctest: +ELLIPSIS datetime.datetime(...) To update a document, simply set the attributes, and then call the ``store()`` method: >>> person.name = 'John R. Doe' >>> person.store(db) #doctest: +ELLIPSIS If you retrieve the document from the server again, you should be getting the updated data: >>> person = Person.load(db, person.id) >>> person.name u'John R. Doe' >>> person.rev != old_rev True >>> del server['python-tests'] """ import copy from calendar import timegm from datetime import date, datetime, time from decimal import Decimal from time import strptime, struct_time from couchdb.design import ViewDefinition __all__ = ['Mapping', 'Document', 'Field', 'TextField', 'FloatField', 'IntegerField', 'LongField', 'BooleanField', 'DecimalField', 'DateField', 'DateTimeField', 'TimeField', 'DictField', 'ListField', 'ViewField'] __docformat__ = 'restructuredtext en' DEFAULT = object() class Field(object): """Basic unit for mapping a piece of data between Python and JSON. Instances of this class can be added to subclasses of `Document` to describe the mapping of a document. """ def __init__(self, name=None, default=None): self.name = name self.default = default def __get__(self, instance, owner): if instance is None: return self value = instance._data.get(self.name) if value is not None: value = self._to_python(value) elif self.default is not None: default = self.default if callable(default): default = default() value = default return value def __set__(self, instance, value): if value is not None: value = self._to_json(value) instance._data[self.name] = value def _to_python(self, value): return unicode(value) def _to_json(self, value): return self._to_python(value) class MappingMeta(type): def __new__(cls, name, bases, d): fields = {} for base in bases: if hasattr(base, '_fields'): fields.update(base._fields) for attrname, attrval in d.items(): if isinstance(attrval, Field): if not attrval.name: attrval.name = attrname fields[attrname] = attrval d['_fields'] = fields return type.__new__(cls, name, bases, d) class Mapping(object): __metaclass__ = MappingMeta def __init__(self, **values): self._data = {} for attrname, field in self._fields.items(): if attrname in values: setattr(self, attrname, values.pop(attrname)) else: setattr(self, attrname, getattr(self, attrname)) def __iter__(self): return iter(self._data) def __len__(self): return len(self._data or ()) def __delitem__(self, name): del self._data[name] def __getitem__(self, name): return self._data[name] def __setitem__(self, name, value): self._data[name] = value def get(self, name, default): return self._data.get(name, default) def setdefault(self, name, default): return self._data.setdefault(name, default) def unwrap(self): return self._data @classmethod def build(cls, **d): fields = {} for attrname, attrval in d.items(): if not attrval.name: attrval.name = attrname fields[attrname] = attrval d['_fields'] = fields return type('AnonymousStruct', (cls,), d) @classmethod def wrap(cls, data): instance = cls() instance._data = data return instance def _to_python(self, value): return self.wrap(value) def _to_json(self, value): return self.unwrap() class ViewField(object): r"""Descriptor that can be used to bind a view definition to a property of a `Document` class. >>> class Person(Document): ... name = TextField() ... age = IntegerField() ... by_name = ViewField('people', '''\ ... function(doc) { ... emit(doc.name, doc); ... }''') >>> Person.by_name >>> print Person.by_name.map_fun function(doc) { emit(doc.name, doc); } That property can be used as a function, which will execute the view. >>> from couchdb import Database >>> db = Database('python-tests') >>> Person.by_name(db, count=3) {'count': 3}> The results produced by the view are automatically wrapped in the `Document` subclass the descriptor is bound to. In this example, it would return instances of the `Person` class. But please note that this requires the values of the view results to be dictionaries that can be mapped to the mapping defined by the containing `Document` class. Alternatively, the ``include_docs`` query option can be used to inline the actual documents in the view results, which will then be used instead of the values. If you use Python view functions, this class can also be used as a decorator: >>> class Person(Document): ... name = TextField() ... age = IntegerField() ... ... @ViewField.define('people') ... def by_name(doc): ... yield doc['name'], doc >>> Person.by_name >>> print Person.by_name.map_fun def by_name(doc): yield doc['name'], doc """ def __init__(self, design, map_fun, reduce_fun=None, name=None, language='javascript', wrapper=DEFAULT, **defaults): """Initialize the view descriptor. :param design: the name of the design document :param map_fun: the map function code :param reduce_fun: the reduce function code (optional) :param name: the actual name of the view in the design document, if it differs from the name the descriptor is assigned to :param language: the name of the language used :param wrapper: an optional callable that should be used to wrap the result rows :param defaults: default query string parameters to apply """ self.design = design self.name = name self.map_fun = map_fun self.reduce_fun = reduce_fun self.language = language self.wrapper = wrapper self.defaults = defaults @classmethod def define(cls, design, name=None, language='python', wrapper=DEFAULT, **defaults): """Factory method for use as a decorator (only suitable for Python view code). """ def view_wrapped(fun): return cls(design, fun, language=language, wrapper=wrapper, **defaults) return view_wrapped def __get__(self, instance, cls=None): if self.wrapper is DEFAULT: wrapper = cls._wrap_row else: wrapper = self.wrapper return ViewDefinition(self.design, self.name, self.map_fun, self.reduce_fun, language=self.language, wrapper=wrapper, **self.defaults) class DocumentMeta(MappingMeta): def __new__(cls, name, bases, d): for attrname, attrval in d.items(): if isinstance(attrval, ViewField): if not attrval.name: attrval.name = attrname return MappingMeta.__new__(cls, name, bases, d) class Document(Mapping): __metaclass__ = DocumentMeta def __init__(self, id=None, **values): Mapping.__init__(self, **values) if id is not None: self.id = id def __repr__(self): return '<%s %r@%r %r>' % (type(self).__name__, self.id, self.rev, dict([(k, v) for k, v in self._data.items() if k not in ('_id', '_rev')])) def _get_id(self): if hasattr(self._data, 'id'): # When data is client.Document return self._data.id return self._data.get('_id') def _set_id(self, value): if self.id is not None: raise AttributeError('id can only be set on new documents') self._data['_id'] = value id = property(_get_id, _set_id, doc='The document ID') @property def rev(self): """The document revision. :rtype: basestring """ if hasattr(self._data, 'rev'): # When data is client.Document return self._data.rev return self._data.get('_rev') def items(self): """Return the fields as a list of ``(name, value)`` tuples. This method is provided to enable easy conversion to native dictionary objects, for example to allow use of `mapping.Document` instances with `client.Database.update`. >>> class Post(Document): ... title = TextField() ... author = TextField() >>> post = Post(id='foo-bar', title='Foo bar', author='Joe') >>> sorted(post.items()) [('_id', 'foo-bar'), ('author', u'Joe'), ('title', u'Foo bar')] :return: a list of ``(name, value)`` tuples """ retval = [] if self.id is not None: retval.append(('_id', self.id)) if self.rev is not None: retval.append(('_rev', self.rev)) for name, value in self._data.items(): if name not in ('_id', '_rev'): retval.append((name, value)) return retval @classmethod def load(cls, db, id): """Load a specific document from the given database. :param db: the `Database` object to retrieve the document from :param id: the document ID :return: the `Document` instance, or `None` if no document with the given ID was found """ doc = db.get(id) if doc is None: return None return cls.wrap(doc) def store(self, db): """Store the document in the given database.""" db.save(self._data) return self @classmethod def query(cls, db, map_fun, reduce_fun, language='javascript', **options): """Execute a CouchDB temporary view and map the result values back to objects of this mapping. Note that by default, any properties of the document that are not included in the values of the view will be treated as if they were missing from the document. If you want to load the full document for every row, set the ``include_docs`` option to ``True``. """ return db.query(map_fun, reduce_fun=reduce_fun, language=language, wrapper=cls._wrap_row, **options) @classmethod def view(cls, db, viewname, **options): """Execute a CouchDB named view and map the result values back to objects of this mapping. Note that by default, any properties of the document that are not included in the values of the view will be treated as if they were missing from the document. If you want to load the full document for every row, set the ``include_docs`` option to ``True``. """ return db.view(viewname, wrapper=cls._wrap_row, **options) @classmethod def _wrap_row(cls, row): doc = row.get('doc') if doc is not None: return cls.wrap(doc) data = row['value'] data['_id'] = row['id'] return cls.wrap(data) class TextField(Field): """Mapping field for string values.""" _to_python = unicode class FloatField(Field): """Mapping field for float values.""" _to_python = float class IntegerField(Field): """Mapping field for integer values.""" _to_python = int class LongField(Field): """Mapping field for long integer values.""" _to_python = long class BooleanField(Field): """Mapping field for boolean values.""" _to_python = bool class DecimalField(Field): """Mapping field for decimal values.""" def _to_python(self, value): return Decimal(value) def _to_json(self, value): return unicode(value) class DateField(Field): """Mapping field for storing dates. >>> field = DateField() >>> field._to_python('2007-04-01') datetime.date(2007, 4, 1) >>> field._to_json(date(2007, 4, 1)) '2007-04-01' >>> field._to_json(datetime(2007, 4, 1, 15, 30)) '2007-04-01' """ def _to_python(self, value): if isinstance(value, basestring): try: value = date(*strptime(value, '%Y-%m-%d')[:3]) except ValueError: raise ValueError('Invalid ISO date %r' % value) return value def _to_json(self, value): if isinstance(value, datetime): value = value.date() return value.isoformat() class DateTimeField(Field): """Mapping field for storing date/time values. >>> field = DateTimeField() >>> field._to_python('2007-04-01T15:30:00Z') datetime.datetime(2007, 4, 1, 15, 30) >>> field._to_json(datetime(2007, 4, 1, 15, 30, 0, 9876)) '2007-04-01T15:30:00Z' >>> field._to_json(date(2007, 4, 1)) '2007-04-01T00:00:00Z' """ def _to_python(self, value): if isinstance(value, basestring): try: value = value.split('.', 1)[0] # strip out microseconds value = value.rstrip('Z') # remove timezone separator value = datetime(*strptime(value, '%Y-%m-%dT%H:%M:%S')[:6]) except ValueError: raise ValueError('Invalid ISO date/time %r' % value) return value def _to_json(self, value): if isinstance(value, struct_time): value = datetime.utcfromtimestamp(timegm(value)) elif not isinstance(value, datetime): value = datetime.combine(value, time(0)) return value.replace(microsecond=0).isoformat() + 'Z' class TimeField(Field): """Mapping field for storing times. >>> field = TimeField() >>> field._to_python('15:30:00') datetime.time(15, 30) >>> field._to_json(time(15, 30)) '15:30:00' >>> field._to_json(datetime(2007, 4, 1, 15, 30)) '15:30:00' """ def _to_python(self, value): if isinstance(value, basestring): try: value = value.split('.', 1)[0] # strip out microseconds value = time(*strptime(value, '%H:%M:%S')[3:6]) except ValueError: raise ValueError('Invalid ISO time %r' % value) return value def _to_json(self, value): if isinstance(value, datetime): value = value.time() return value.replace(microsecond=0).isoformat() class DictField(Field): """Field type for nested dictionaries. >>> from couchdb import Server >>> server = Server() >>> db = server.create('python-tests') >>> class Post(Document): ... title = TextField() ... content = TextField() ... author = DictField(Mapping.build( ... name = TextField(), ... email = TextField() ... )) ... extra = DictField() >>> post = Post( ... title='Foo bar', ... author=dict(name='John Doe', ... email='john@doe.com'), ... extra=dict(foo='bar'), ... ) >>> post.store(db) #doctest: +ELLIPSIS >>> post = Post.load(db, post.id) >>> post.author.name u'John Doe' >>> post.author.email u'john@doe.com' >>> post.extra {'foo': 'bar'} >>> del server['python-tests'] """ def __init__(self, mapping=None, name=None, default=None): default = default or {} Field.__init__(self, name=name, default=lambda: default.copy()) self.mapping = mapping def _to_python(self, value): if self.mapping is None: return value else: return self.mapping.wrap(value) def _to_json(self, value): if self.mapping is None: return value if not isinstance(value, Mapping): value = self.mapping(**value) return value.unwrap() class ListField(Field): """Field type for sequences of other fields. >>> from couchdb import Server >>> server = Server() >>> db = server.create('python-tests') >>> class Post(Document): ... title = TextField() ... content = TextField() ... pubdate = DateTimeField(default=datetime.now) ... comments = ListField(DictField(Mapping.build( ... author = TextField(), ... content = TextField(), ... time = DateTimeField() ... ))) >>> post = Post(title='Foo bar') >>> post.comments.append(author='myself', content='Bla bla', ... time=datetime.now()) >>> len(post.comments) 1 >>> post.store(db) #doctest: +ELLIPSIS >>> post = Post.load(db, post.id) >>> comment = post.comments[0] >>> comment['author'] 'myself' >>> comment['content'] 'Bla bla' >>> comment['time'] #doctest: +ELLIPSIS '...T...Z' >>> del server['python-tests'] """ def __init__(self, field, name=None, default=None): default = default or [] Field.__init__(self, name=name, default=lambda: copy.copy(default)) if type(field) is type: if issubclass(field, Field): field = field() elif issubclass(field, Mapping): field = DictField(field) self.field = field def _to_python(self, value): return self.Proxy(value, self.field) def _to_json(self, value): return [self.field._to_json(item) for item in value] class Proxy(list): def __init__(self, list, field): self.list = list self.field = field def __lt__(self, other): return self.list < other def __le__(self, other): return self.list <= other def __eq__(self, other): return self.list == other def __ne__(self, other): return self.list != other def __gt__(self, other): return self.list > other def __ge__(self, other): return self.list >= other def __repr__(self): return repr(self.list) def __str__(self): return str(self.list) def __unicode__(self): return unicode(self.list) def __delitem__(self, index): del self.list[index] def __getitem__(self, index): return self.field._to_python(self.list[index]) def __setitem__(self, index, value): self.list[index] = self.field._to_json(value) def __delslice__(self, i, j): del self.list[i:j] def __getslice__(self, i, j): return ListField.Proxy(self.list[i:j], self.field) def __setslice__(self, i, j, seq): self.list[i:j] = (self.field._to_json(v) for v in seq) def __contains__(self, value): for item in self.list: if self.field._to_python(item) == value: return True return False def __iter__(self): for index in range(len(self)): yield self[index] def __len__(self): return len(self.list) def __nonzero__(self): return bool(self.list) def append(self, *args, **kwargs): if args or not isinstance(self.field, DictField): if len(args) != 1: raise TypeError('append() takes exactly one argument ' '(%s given)' % len(args)) value = args[0] else: value = kwargs self.list.append(self.field._to_json(value)) def count(self, value): return [i for i in self].count(value) def extend(self, list): for item in list: self.append(item) def index(self, value): return self.list.index(self.field._to_json(value)) def insert(self, idx, *args, **kwargs): if args or not isinstance(self.field, DictField): if len(args) != 1: raise TypeError('insert() takes exactly 2 arguments ' '(%s given)' % len(args)) value = args[0] else: value = kwargs self.list.insert(idx, self.field._to_json(value)) def remove(self, value): return self.list.remove(self.field._to_json(value)) def pop(self, *args): return self.field._to_python(self.list.pop(*args)) CouchDB-0.8/couchdb/view.py0000755000175000001440000001577711402747435014275 0ustar djcusers#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2007-2008 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. """Implementation of a view server for functions written in Python.""" from codecs import BOM_UTF8 import logging import os import sys import traceback from types import FunctionType from couchdb import json __all__ = ['main', 'run'] __docformat__ = 'restructuredtext en' log = logging.getLogger('couchdb.view') def run(input=sys.stdin, output=sys.stdout): r"""CouchDB view function handler implementation for Python. :param input: the readable file-like object to read input from :param output: the writable file-like object to write output to """ functions = [] def _writejson(obj): obj = json.encode(obj) if isinstance(obj, unicode): obj = obj.encode('utf-8') output.write(obj) output.write('\n') output.flush() def _log(message): if not isinstance(message, basestring): message = json.encode(message) _writejson({'log': message}) def reset(config=None): del functions[:] return True def add_fun(string): string = BOM_UTF8 + string.encode('utf-8') globals_ = {} try: exec string in {'log': _log}, globals_ except Exception, e: return {'error': { 'id': 'map_compilation_error', 'reason': e.args[0] }} err = {'error': { 'id': 'map_compilation_error', 'reason': 'string must eval to a function ' '(ex: "def(doc): return 1")' }} if len(globals_) != 1: return err function = globals_.values()[0] if type(function) is not FunctionType: return err functions.append(function) return True def map_doc(doc): results = [] for function in functions: try: results.append([[key, value] for key, value in function(doc)]) except Exception, e: log.error('runtime error in map function: %s', e, exc_info=True) results.append([]) _log(traceback.format_exc()) return results def reduce(*cmd, **kwargs): code = BOM_UTF8 + cmd[0][0].encode('utf-8') args = cmd[1] globals_ = {} try: exec code in {'log': _log}, globals_ except Exception, e: log.error('runtime error in reduce function: %s', e, exc_info=True) return {'error': { 'id': 'reduce_compilation_error', 'reason': e.args[0] }} err = {'error': { 'id': 'reduce_compilation_error', 'reason': 'string must eval to a function ' '(ex: "def(keys, values): return 1")' }} if len(globals_) != 1: return err function = globals_.values()[0] if type(function) is not FunctionType: return err rereduce = kwargs.get('rereduce', False) results = [] if rereduce: keys = None vals = args else: keys, vals = zip(*args) if function.func_code.co_argcount == 3: results = function(keys, vals, rereduce) else: results = function(keys, vals) return [True, [results]] def rereduce(*cmd): # Note: weird kwargs is for Python 2.5 compat return reduce(*cmd, **{'rereduce': True}) handlers = {'reset': reset, 'add_fun': add_fun, 'map_doc': map_doc, 'reduce': reduce, 'rereduce': rereduce} try: while True: line = input.readline() if not line: break try: cmd = json.decode(line) log.debug('Processing %r', cmd) except ValueError, e: log.error('Error: %s', e, exc_info=True) return 1 else: retval = handlers[cmd[0]](*cmd[1:]) log.debug('Returning %r', retval) _writejson(retval) except KeyboardInterrupt: return 0 except Exception, e: log.error('Error: %s', e, exc_info=True) return 1 _VERSION = """%(name)s - CouchDB Python %(version)s Copyright (C) 2007 Christopher Lenz . """ _HELP = """Usage: %(name)s [OPTION] The %(name)s command runs the CouchDB Python view server. The exit status is 0 for success or 1 for failure. Options: --version display version information and exit -h, --help display a short help message and exit --json-module= set the JSON module to use ('simplejson', 'cjson', or 'json' are supported) --log-file= name of the file to write log messages to, or '-' to enable logging to the standard error stream --debug enable debug logging; requires --log-file to be specified Report bugs via the web at . """ def main(): """Command-line entry point for running the view server.""" import getopt from couchdb import __version__ as VERSION try: option_list, argument_list = getopt.gnu_getopt( sys.argv[1:], 'h', ['version', 'help', 'json-module=', 'debug', 'log-file='] ) message = None for option, value in option_list: if option in ('--version'): message = _VERSION % dict(name=os.path.basename(sys.argv[0]), version=VERSION) elif option in ('-h', '--help'): message = _HELP % dict(name=os.path.basename(sys.argv[0])) elif option in ('--json-module'): json.use(module=value) elif option in ('--debug'): log.setLevel(logging.DEBUG) elif option in ('--log-file'): if value == '-': handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter( ' -> [%(levelname)s] %(message)s' )) else: handler = logging.FileHandler(value) handler.setFormatter(logging.Formatter( '[%(asctime)s] [%(levelname)s] %(message)s' )) log.addHandler(handler) if message: sys.stdout.write(message) sys.stdout.flush() sys.exit(0) except getopt.GetoptError, error: message = '%s\n\nTry `%s --help` for more information.\n' % ( str(error), os.path.basename(sys.argv[0]) ) sys.stderr.write(message) sys.stderr.flush() sys.exit(1) sys.exit(run()) if __name__ == '__main__': main() CouchDB-0.8/couchdb/http.py0000644000175000001440000004230711431231104014243 0ustar djcusers#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. """Simple HTTP client implementation based on the ``httplib`` module in the standard library. """ from base64 import b64encode from datetime import datetime import errno from httplib import BadStatusLine, HTTPConnection, HTTPSConnection import socket import time try: from cStringIO import StringIO except ImportError: from StringIO import StringIO import sys try: from threading import Lock except ImportError: from dummy_threading import Lock import urllib from urlparse import urlsplit, urlunsplit from couchdb import json __all__ = ['HTTPError', 'PreconditionFailed', 'ResourceNotFound', 'ResourceConflict', 'ServerError', 'Unauthorized', 'RedirectLimit', 'Session', 'Resource'] __docformat__ = 'restructuredtext en' class HTTPError(Exception): """Base class for errors based on HTTP status codes >= 400.""" class PreconditionFailed(HTTPError): """Exception raised when a 412 HTTP error is received in response to a request. """ class ResourceNotFound(HTTPError): """Exception raised when a 404 HTTP error is received in response to a request. """ class ResourceConflict(HTTPError): """Exception raised when a 409 HTTP error is received in response to a request. """ class ServerError(HTTPError): """Exception raised when an unexpected HTTP error is received in response to a request. """ class Unauthorized(HTTPError): """Exception raised when the server requires authentication credentials but either none are provided, or they are incorrect. """ class RedirectLimit(Exception): """Exception raised when a request is redirected more often than allowed by the maximum number of redirections. """ CHUNK_SIZE = 1024 * 8 CACHE_SIZE = 10, 75 # some random values to limit memory use def cache_sort(i): t = time.mktime(time.strptime(i[1][1]['Date'][5:-4], '%d %b %Y %H:%M:%S')) return datetime.fromtimestamp(t) class ResponseBody(object): def __init__(self, resp, callback): self.resp = resp self.callback = callback def read(self, size=None): bytes = self.resp.read(size) if size is None or len(bytes) < size: self.close() return bytes def close(self): while not self.resp.isclosed(): self.read(CHUNK_SIZE) self.callback() def __iter__(self): assert self.resp.msg.get('transfer-encoding') == 'chunked' while True: chunksz = int(self.resp.fp.readline().strip(), 16) if not chunksz: self.resp.fp.read(2) #crlf self.resp.close() self.callback() break chunk = self.resp.fp.read(chunksz) for ln in chunk.splitlines(): yield ln self.resp.fp.read(2) #crlf RETRYABLE_ERRORS = frozenset([ errno.EPIPE, errno.ETIMEDOUT, errno.ECONNRESET, errno.ECONNREFUSED, errno.ECONNABORTED, errno.EHOSTDOWN, errno.EHOSTUNREACH, errno.ENETRESET, errno.ENETUNREACH, errno.ENETDOWN ]) class Session(object): def __init__(self, cache=None, timeout=None, max_redirects=5, retry_delays=[0], retryable_errors=RETRYABLE_ERRORS): """Initialize an HTTP client session. :param cache: an instance with a dict-like interface or None to allow Session to create a dict for caching. :param timeout: socket timeout in number of seconds, or `None` for no timeout :param retry_delays: list of request retry delays. """ from couchdb import __version__ as VERSION self.user_agent = 'CouchDB-Python/%s' % VERSION if cache is None: cache = {} self.cache = cache self.timeout = timeout self.max_redirects = max_redirects self.perm_redirects = {} self.conns = {} # HTTP connections keyed by (scheme, host) self.lock = Lock() self.retry_delays = list(retry_delays) # We don't want this changing on us. self.retryable_errors = set(retryable_errors) def request(self, method, url, body=None, headers=None, credentials=None, num_redirects=0): if url in self.perm_redirects: url = self.perm_redirects[url] method = method.upper() if headers is None: headers = {} headers.setdefault('Accept', 'application/json') headers['User-Agent'] = self.user_agent cached_resp = None if method in ('GET', 'HEAD'): cached_resp = self.cache.get(url) if cached_resp is not None: etag = cached_resp[1].get('etag') if etag: headers['If-None-Match'] = etag if body is None: headers.setdefault('Content-Length', '0') else: if not isinstance(body, basestring): try: body = json.encode(body).encode('utf-8') except TypeError: pass else: headers.setdefault('Content-Type', 'application/json') if isinstance(body, basestring): headers.setdefault('Content-Length', str(len(body))) else: headers['Transfer-Encoding'] = 'chunked' authorization = basic_auth(credentials) if authorization: headers['Authorization'] = authorization path_query = urlunsplit(('', '') + urlsplit(url)[2:4] + ('',)) conn = self._get_connection(url) def _try_request_with_retries(retries): while True: try: return _try_request() except socket.error, e: ecode = e.args[0] if ecode not in self.retryable_errors: raise try: delay = retries.next() except StopIteration: # No more retries, raise last socket error. raise e time.sleep(delay) conn.close() def _try_request(): try: if conn.sock is None: conn.connect() conn.putrequest(method, path_query, skip_accept_encoding=True) for header in headers: conn.putheader(header, headers[header]) conn.endheaders() if body is not None: if isinstance(body, str): conn.sock.sendall(body) else: # assume a file-like object and send in chunks while 1: chunk = body.read(CHUNK_SIZE) if not chunk: break conn.sock.sendall(('%x\r\n' % len(chunk)) + chunk + '\r\n') conn.sock.sendall('0\r\n\r\n') return conn.getresponse() except BadStatusLine, e: # httplib raises a BadStatusLine when it cannot read the status # line saying, "Presumably, the server closed the connection # before sending a valid response." # Raise as ECONNRESET to simplify retry logic. if e.line == '' or e.line == "''": raise socket.error(errno.ECONNRESET) else: raise resp = _try_request_with_retries(iter(self.retry_delays)) status = resp.status # Handle conditional response if status == 304 and method in ('GET', 'HEAD'): resp.read() self._return_connection(url, conn) status, msg, data = cached_resp if data is not None: data = StringIO(data) return status, msg, data elif cached_resp: del self.cache[url] # Handle redirects if status == 303 or \ method in ('GET', 'HEAD') and status in (301, 302, 307): resp.read() self._return_connection(url, conn) if num_redirects > self.max_redirects: raise RedirectLimit('Redirection limit exceeded') location = resp.getheader('location') if status == 301: self.perm_redirects[url] = location elif status == 303: method = 'GET' return self.request(method, location, body, headers, num_redirects=num_redirects + 1) data = None streamed = False # Read the full response for empty responses so that the connection is # in good state for the next request if method == 'HEAD' or resp.getheader('content-length') == '0' or \ status < 200 or status in (204, 304): resp.read() self._return_connection(url, conn) # Buffer small non-JSON response bodies elif int(resp.getheader('content-length', sys.maxint)) < CHUNK_SIZE: data = resp.read() self._return_connection(url, conn) # For large or chunked response bodies, do not buffer the full body, # and instead return a minimal file-like object else: data = ResponseBody(resp, lambda: self._return_connection(url, conn)) streamed = True # Handle errors if status >= 400: ctype = resp.getheader('content-type') if data is not None and 'application/json' in ctype: data = json.decode(data) error = data.get('error'), data.get('reason') elif method != 'HEAD': error = resp.read() self._return_connection(url, conn) else: error = '' if status == 401: raise Unauthorized(error) elif status == 404: raise ResourceNotFound(error) elif status == 409: raise ResourceConflict(error) elif status == 412: raise PreconditionFailed(error) else: raise ServerError((status, error)) # Store cachable responses if not streamed and method == 'GET' and 'etag' in resp.msg: self.cache[url] = (status, resp.msg, data) if len(self.cache) > CACHE_SIZE[1]: self._clean_cache() if not streamed and data is not None: data = StringIO(data) return status, resp.msg, data def _clean_cache(self): ls = sorted(self.cache.iteritems(), key=cache_sort) self.cache = dict(ls[-CACHE_SIZE[0]:]) def _get_connection(self, url): scheme, host = urlsplit(url, 'http', False)[:2] self.lock.acquire() try: conns = self.conns.setdefault((scheme, host), []) if conns: conn = conns.pop(-1) else: if scheme == 'http': cls = HTTPConnection elif scheme == 'https': cls = HTTPSConnection else: raise ValueError('%s is not a supported scheme' % scheme) conn = cls(host) finally: self.lock.release() return conn def _return_connection(self, url, conn): scheme, host = urlsplit(url, 'http', False)[:2] self.lock.acquire() try: self.conns.setdefault((scheme, host), []).append(conn) finally: self.lock.release() class Resource(object): def __init__(self, url, session, headers=None): self.url, self.credentials = extract_credentials(url) if session is None: session = Session() self.session = session self.headers = headers or {} def __call__(self, *path): obj = type(self)(urljoin(self.url, *path), self.session) obj.credentials = self.credentials obj.headers = self.headers.copy() return obj def delete(self, path=None, headers=None, **params): return self._request('DELETE', path, headers=headers, **params) def get(self, path=None, headers=None, **params): return self._request('GET', path, headers=headers, **params) def head(self, path=None, headers=None, **params): return self._request('HEAD', path, headers=headers, **params) def post(self, path=None, body=None, headers=None, **params): return self._request('POST', path, body=body, headers=headers, **params) def put(self, path=None, body=None, headers=None, **params): return self._request('PUT', path, body=body, headers=headers, **params) def delete_json(self, *a, **k): status, headers, data = self.delete(*a, **k) if 'application/json' in headers.get('content-type'): data = json.decode(data.read()) return status, headers, data def get_json(self, *a, **k): status, headers, data = self.get(*a, **k) if 'application/json' in headers.get('content-type'): data = json.decode(data.read()) return status, headers, data def post_json(self, *a, **k): status, headers, data = self.post(*a, **k) if 'application/json' in headers.get('content-type'): data = json.decode(data.read()) return status, headers, data def put_json(self, *a, **k): status, headers, data = self.put(*a, **k) if 'application/json' in headers.get('content-type'): data = json.decode(data.read()) return status, headers, data def _request(self, method, path=None, body=None, headers=None, **params): all_headers = self.headers.copy() all_headers.update(headers or {}) if path is not None: url = urljoin(self.url, path, **params) else: url = urljoin(self.url, **params) return self.session.request(method, url, body=body, headers=all_headers, credentials=self.credentials) def extract_credentials(url): """Extract authentication (user name and password) credentials from the given URL. >>> extract_credentials('http://localhost:5984/_config/') ('http://localhost:5984/_config/', None) >>> extract_credentials('http://joe:secret@localhost:5984/_config/') ('http://localhost:5984/_config/', ('joe', 'secret')) >>> extract_credentials('http://joe%40example.com:secret@localhost:5984/_config/') ('http://localhost:5984/_config/', ('joe@example.com', 'secret')) """ parts = urlsplit(url) netloc = parts[1] if '@' in netloc: creds, netloc = netloc.split('@') credentials = tuple(urllib.unquote(i) for i in creds.split(':')) parts = list(parts) parts[1] = netloc else: credentials = None return urlunsplit(parts), credentials def basic_auth(credentials): if credentials: return 'Basic %s' % b64encode('%s:%s' % credentials) def quote(string, safe=''): if isinstance(string, unicode): string = string.encode('utf-8') return urllib.quote(string, safe) def urlencode(data): if isinstance(data, dict): data = data.items() params = [] for name, value in data: if isinstance(value, unicode): value = value.encode('utf-8') params.append((name, value)) return urllib.urlencode(params) def urljoin(base, *path, **query): """Assemble a uri based on a base, any number of path segments, and query string parameters. >>> urljoin('http://example.org', '_all_dbs') 'http://example.org/_all_dbs' A trailing slash on the uri base is handled gracefully: >>> urljoin('http://example.org/', '_all_dbs') 'http://example.org/_all_dbs' And multiple positional arguments become path parts: >>> urljoin('http://example.org/', 'foo', 'bar') 'http://example.org/foo/bar' All slashes within a path part are escaped: >>> urljoin('http://example.org/', 'foo/bar') 'http://example.org/foo%2Fbar' >>> urljoin('http://example.org/', 'foo', '/bar/') 'http://example.org/foo/%2Fbar%2F' >>> urljoin('http://example.org/', None) #doctest:+IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: argument 2 to map() must support iteration """ if base and base.endswith('/'): base = base[:-1] retval = [base] # build the path path = '/'.join([''] + [quote(s) for s in path]) if path: retval.append(path) # build the query string params = [] for name, value in query.items(): if type(value) in (list, tuple): params.extend([(name, i) for i in value if i is not None]) elif value is not None: if value is True: value = 'true' elif value is False: value = 'false' params.append((name, value)) if params: retval.extend(['?', urlencode(params)]) return ''.join(retval) CouchDB-0.8/couchdb/__init__.py0000644000175000001440000000102611431231104015014 0ustar djcusers# -*- coding: utf-8 -*- # # Copyright (C) 2007 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. from couchdb.client import Database, Document, Server from couchdb.http import HTTPError, PreconditionFailed, Resource, \ ResourceConflict, ResourceNotFound, ServerError, Session, Unauthorized try: __version__ = __import__('pkg_resources').get_distribution('CouchDB').version except: __version__ = '?' CouchDB-0.8/Makefile0000644000175000001440000000024211431231104012733 0ustar djcusers.PHONY: test doc upload-doc test: PYTHONPATH=. python couchdb/tests/__init__.py doc: python setup.py build_sphinx upload-doc: python setup.py upload_sphinx CouchDB-0.8/CouchDB.egg-info/0000755000175000001440000000000011431232253014244 5ustar djcusersCouchDB-0.8/CouchDB.egg-info/entry_points.txt0000644000175000001440000000025611431232252017544 0ustar djcusers[console_scripts] couchdb-dump = couchdb.tools.dump:main couchpy = couchdb.view:main couchdb-load = couchdb.tools.load:main couchdb-replicate = couchdb.tools.replicate:main CouchDB-0.8/CouchDB.egg-info/PKG-INFO0000644000175000001440000000130411431232252015336 0ustar djcusersMetadata-Version: 1.0 Name: CouchDB Version: 0.8 Summary: Python library for working with CouchDB Home-page: http://code.google.com/p/couchdb-python/ Author: Christopher Lenz Author-email: cmlenz@gmx.de License: BSD Description: This is a Python library for CouchDB. It provides a convenient high level interface for the CouchDB server. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Database :: Front-Ends Classifier: Topic :: Software Development :: Libraries :: Python Modules CouchDB-0.8/CouchDB.egg-info/dependency_links.txt0000644000175000001440000000000111431232252020311 0ustar djcusers CouchDB-0.8/CouchDB.egg-info/zip-safe0000644000175000001440000000000111311445400015671 0ustar djcusers CouchDB-0.8/CouchDB.egg-info/SOURCES.txt0000644000175000001440000000153211431232252016130 0ustar djcusersCOPYING ChangeLog.txt MANIFEST.in Makefile README.txt setup.cfg setup.py CouchDB.egg-info/PKG-INFO CouchDB.egg-info/SOURCES.txt CouchDB.egg-info/dependency_links.txt CouchDB.egg-info/entry_points.txt CouchDB.egg-info/top_level.txt CouchDB.egg-info/zip-safe couchdb/__init__.py couchdb/client.py couchdb/design.py couchdb/http.py couchdb/json.py couchdb/mapping.py couchdb/multipart.py couchdb/view.py couchdb/tests/__init__.py couchdb/tests/client.py couchdb/tests/couch_tests.py couchdb/tests/design.py couchdb/tests/http.py couchdb/tests/mapping.py couchdb/tests/multipart.py couchdb/tests/package.py couchdb/tests/testutil.py couchdb/tests/view.py couchdb/tools/__init__.py couchdb/tools/dump.py couchdb/tools/load.py couchdb/tools/replicate.py doc/changes.rst doc/client.rst doc/conf.py doc/getting-started.rst doc/index.rst doc/mapping.rst doc/views.rstCouchDB-0.8/CouchDB.egg-info/top_level.txt0000644000175000001440000000001011431232252016764 0ustar djcuserscouchdb CouchDB-0.8/PKG-INFO0000644000175000001440000000130411431232253012376 0ustar djcusersMetadata-Version: 1.0 Name: CouchDB Version: 0.8 Summary: Python library for working with CouchDB Home-page: http://code.google.com/p/couchdb-python/ Author: Christopher Lenz Author-email: cmlenz@gmx.de License: BSD Description: This is a Python library for CouchDB. It provides a convenient high level interface for the CouchDB server. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Database :: Front-Ends Classifier: Topic :: Software Development :: Libraries :: Python Modules CouchDB-0.8/setup.cfg0000644000175000001440000000025611431232253013127 0ustar djcusers[build_sphinx] all_files = 1 build-dir = doc/build source-dir = doc/ [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 [upload_sphinx] upload-dir = doc/build/html CouchDB-0.8/README.txt0000644000175000001440000000036711431231540013005 0ustar djcusersCouchDB Python Library ====================== This package provides a Python interface to CouchDB. Please see the files in the `doc` folder or browse the documentation online at: CouchDB-0.8/MANIFEST.in0000644000175000001440000000013511431231104013032 0ustar djcusersinclude COPYING include Makefile include ChangeLog.txt include doc/conf.py include doc/*.rst CouchDB-0.8/setup.py0000755000175000001440000001134111431231104013012 0ustar djcusers#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. from distutils.cmd import Command import doctest from glob import glob import os import sys try: from setuptools import setup except ImportError: from distutils.core import setup import sys class build_doc(Command): description = 'Builds the documentation' user_options = [ ('force', None, "force regeneration even if no reStructuredText files have changed"), ('without-apidocs', None, "whether to skip the generation of API documentaton"), ] boolean_options = ['force', 'without-apidocs'] def initialize_options(self): self.force = False self.without_apidocs = False def finalize_options(self): pass def run(self): from docutils.core import publish_cmdline from docutils.nodes import raw from docutils.parsers import rst docutils_conf = os.path.join('doc', 'conf', 'docutils.ini') epydoc_conf = os.path.join('doc', 'conf', 'epydoc.ini') try: from pygments import highlight from pygments.lexers import get_lexer_by_name from pygments.formatters import HtmlFormatter def code_block(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): lexer = get_lexer_by_name(arguments[0]) html = highlight('\n'.join(content), lexer, HtmlFormatter()) return [raw('', html, format='html')] code_block.arguments = (1, 0, 0) code_block.options = {'language' : rst.directives.unchanged} code_block.content = 1 rst.directives.register_directive('code-block', code_block) except ImportError: print 'Pygments not installed, syntax highlighting disabled' for source in glob('doc/*.txt'): dest = os.path.splitext(source)[0] + '.html' if self.force or not os.path.exists(dest) or \ os.path.getmtime(dest) < os.path.getmtime(source): print 'building documentation file %s' % dest publish_cmdline(writer_name='html', argv=['--config=%s' % docutils_conf, source, dest]) if not self.without_apidocs: try: from epydoc import cli old_argv = sys.argv[1:] sys.argv[1:] = [ '--config=%s' % epydoc_conf, '--no-private', # epydoc bug, not read from config '--simple-term', '--verbose' ] cli.cli() sys.argv[1:] = old_argv except ImportError: print 'epydoc not installed, skipping API documentation.' class test_doc(Command): description = 'Tests the code examples in the documentation' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): for filename in glob('doc/*.txt'): print 'testing documentation file %s' % filename doctest.testfile(filename, False, optionflags=doctest.ELLIPSIS) requirements = [] if sys.version_info < (2, 6): requirements += ['simplejson'] setup( name = 'CouchDB', version = '0.8', description = 'Python library for working with CouchDB', long_description = \ """This is a Python library for CouchDB. It provides a convenient high level interface for the CouchDB server.""", author = 'Christopher Lenz', author_email = 'cmlenz@gmx.de', license = 'BSD', url = 'http://code.google.com/p/couchdb-python/', zip_safe = True, classifiers = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Database :: Front-Ends', 'Topic :: Software Development :: Libraries :: Python Modules', ], packages = ['couchdb', 'couchdb.tools', 'couchdb.tests'], test_suite = 'couchdb.tests.suite', install_requires = requirements, entry_points = { 'console_scripts': [ 'couchpy = couchdb.view:main', 'couchdb-dump = couchdb.tools.dump:main', 'couchdb-load = couchdb.tools.load:main', 'couchdb-replicate = couchdb.tools.replicate:main', ], }, cmdclass = {'build_doc': build_doc, 'test_doc': test_doc} ) CouchDB-0.8/COPYING0000644000175000001440000000261011360110062012327 0ustar djcusersCopyright (C) 2007-2008 Christopher Lenz All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. CouchDB-0.8/ChangeLog.txt0000644000175000001440000002113011431231104013662 0ustar djcusersVersion 0.8 (Aug 13, 2010) -------------------------- * The couchdb-replicate script has changed from being a poor man's version of continuous replication (predating it) to being a simple script to help kick off replication jobs across databases and servers. * Reinclude all http exception types in the 'couchdb' package's scope. * Replaced epydoc API docs by more extensive Sphinx-based documentation. * Request retries schedule and frequency are now customizable. * Allow more kinds of request errors to trigger a retry. * Improve wrapping of view results. * Added a `uuids()` method to the `client.Server` class (issue 122). * Tested with CouchDB 0.10 - 1.0 (and Python 2.4 - 2.7). Version 0.7.0 (Apr 15, 2010) ---------------------------- * Breaking change: the dependency on `httplib2` has been replaced by an internal `couchdb.http` library. This changes the API in several places. Most importantly, `resource.request()` now returns a 3-member tuple. * Breaking change: `couchdb.schema` has been renamed to `couchdb.mapping`. This better reflects what is actually provided. Classes inside `couchdb.mapping` have been similarly renamed (e.g. `Schema` -> `Mapping`). * Breaking change: `couchdb.schema.View` has been renamed to `couchdb.mapping.ViewField`, in order to help distinguish it from `couchdb.client.View`. * Breaking change: the `client.Server` properties `version` and `config` have become methods in order to improve API consistency. * Prevent `schema.ListField` objects from sharing the same default (issue 107). * Added a `changes()` method to the `client.Database` class (issue 103). * Added an optional argument to the 'Database.compact` method to enable view compaction (the rest of issue 37). Version 0.6.1 (Dec 14, 2009) ---------------------------- * Compatible with CouchDB 0.9.x and 0.10.x. * Removed debugging statement from `json` module (issue 82). * Fixed a few bugs resulting from typos. * Added a `replicate()` method to the `client.Server` class (issue 61). * Honor the boundary argument in the dump script code (issue 100). * Added a `stats()` method to the `client.Server` class. * Added a `tasks()` method to the `client.Server` class. * Allow slashes in path components passed to the uri function (issue 96). * `schema.DictField` objects now have a separate backing dictionary for each instance of their `schema.Document` (issue 101). * `schema.ListField` proxy objects now have a more consistent (though somewhat slower) `count()` method (issue 91). * `schema.ListField` objects now have correct behavior for slicing operations and the `pop()` method (issue 92). * Added a `revisions()` method to the Database class (issue 99). * Make sure we always return UTF-8 from the view server (issue 81). Version 0.6 (Jul 2, 2009) ------------------------- * Compatible with CouchDB 0.9.x. * `schema.DictField` instances no longer need to be bound to a `Schema` (issue 51). * Added a `config` property to the `client.Server` class (issue 67). * Added a `compact()` method to the `client.Database` class (issue 37). * Changed the `update()` method of the `client.Database` class to simplify the handling of errors. The method now returns a list of `(success, docid, rev_or_exc)` tuples. See the docstring of that method for the details. * `schema.ListField` proxy objects now support the `__contains__()` and `index()` methods (issue 77). * The results of the `query()` and `view()` methods in the `schema.Document` class are now properly wrapped in objects of the class if the `include_docs` option is set (issue 76). * Removed the `eager` option on the `query()` and `view()` methods of `schema.Document`. Use the `include_docs` option instead, which doesn't require an additional request per document. * Added a `copy()` method to the `client.Database` class, which translates to a HTTP COPY request (issue 74). * Accessing a non-existing database through `Server.__getitem__` now throws a `ResourceNotFound` exception as advertised (issue 41). * Added a `delete()` method to the `client.Server` class for consistency (issue 64). * The `couchdb-dump` tool now operates in a streaming fashion, writing one document at a time to the resulting MIME multipart file (issue 58). * It is now possible to explicitly set the JSON module that should be used for decoding/encoding JSON data. The currently available choices are `simplejson`, `cjson`, and `json` (the standard library module). It is also possible to use custom decoding/encoding functions. * Add logging to the Python view server. It can now be configured to log to a given file or the standard error stream, and the log level can be set debug to see all communication between CouchDB and the view server (issue 55). Version 0.5 (Nov 29, 2008) -------------------------- * `schema.Document` objects can now be used in the documents list passed to `client.Database.update()`. * `Server.__contains__()` and `Database.__contains__()` now use the HTTP HEAD method to avoid unnecessary transmission of data. `Database.__del__()` also uses HEAD to determine the latest revision of the document. * The `Database` class now has a method `delete()` that takes a document dictionary as parameter. This method should be used in preference to `__del__` as it allow conflict detection and handling. * Added `cache` and `timeout` arguments to the `client.Server` initializer. * The `Database` class now provides methods for deleting, retrieving, and updating attachments. * The Python view server now exposes a `log()` function to map and reduce functions (issue 21). * Handling of the rereduce stage in the Python view server has been fixed. * The `Server` and `Database` classes now implement the `__nonzero__` hook so that they produce sensible results in boolean conditions. * The client module will now reattempt a request that failed with a "connection reset by peer" error. * inf/nan values now raise a `ValueError` on the client side instead of triggering an internal server error (issue 31). * Added a new `couchdb.design` module that provides functionality for managing views in design documents, so that they can be defined in the Python application code, and the design documents actually stored in the database can be kept in sync with the definitions in the code. * The `include_docs` option for CouchDB views is now supported by the new `doc` property of row instances in view results. Thanks to Paul Davis for the patch (issue 33). * The `keys` option for views is now supported (issue 35). Version 0.4 (Jun 28, 2008) -------------------------- * Updated for compatibility with CouchDB 0.8.0 * Added command-line scripts for importing/exporting databases. * The `Database.update()` function will now actually perform the `POST` request even when you do not iterate over the results (issue 5). * The `_view` prefix can now be omitted when specifying view names. Version 0.3 (Feb 6, 2008) ------------------------- * The `schema.Document` class now has a `view()` method that can be used to execute a CouchDB view and map the result rows back to objects of that schema. * The test suite now uses the new default port of CouchDB, 5984. * Views now return proxy objects to which you can apply slice syntax for "key", "startkey", and "endkey" filtering. * Add a `query()` classmethod to the `Document` class. Version 0.2 (Nov 21, 2007) -------------------------- * Added __len__ and __iter__ to the `schema.Schema` class to iterate over and get the number of items in a document or compound field. * The "version" property of client.Server now returns a plain string instead of a tuple of ints. * The client library now identifies itself with a meaningful User-Agent string. * `schema.Document.store()` now returns the document object instance, instead of just the document ID. * The string representation of `schema.Document` objects is now more comprehensive. * Only the view parameters "key", "startkey", and "endkey" are JSON encoded, anything else is left alone. * Slashes in document IDs are now URL-quoted until CouchDB supports them. * Allow the content-type to be passed for temp views via `client.Database.query()` so that view languages other than Javascript can be used. * Added `client.Database.update()` method to bulk insert/update documents in a database. * The view-server script wrapper has been renamed to `couchpy`. * `couchpy` now supports `--help` and `--version` options. * Updated for compatibility with CouchDB release 0.7.0. Version 0.1 (Sep 23, 2007) -------------------------- * First public release.