diff -Nru python-couchdb-0.8/ChangeLog.rst python-couchdb-0.10/ChangeLog.rst --- python-couchdb-0.8/ChangeLog.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/ChangeLog.rst 2014-07-15 17:11:46.000000000 +0000 @@ -0,0 +1,223 @@ +Version 0.10 (2014-07-15) +------------------------- + + * Now compatible with Python 2.7, 3.3 and 3.4 + * Added batch processing for the `couchdb-dump` tool + * A very basic API to access the `_security` object + * A way to access the `update_seq` value on view results + + +Version 0.9 (2013-04-25) +------------------------ + + * Don't validate database names on the client side. This means some methods + dealing with database names can return different exceptions than before. + * Use HTTP socket more efficiently to avoid the Nagle algorithm, greatly + improving performace. Note: add the `{nodelay, true}` option to the CouchDB + server's httpd/socket_options config. + * Add support for show and list functions. + * Add support for calling update handlers. + * Add support for purging documents. + * Add `iterview()` for more efficient iteration over large view results. + * Add view cleanup API. + * Enhance `Server.stats()` to optionally retrieve a single set of statistics. + * Implement `Session` timeouts. + * Add `error` property to `Row` objects. + * Add `default=None` arg to `mapping.Document.get()` to make it a little more + dict-like. + * Enhance `Database.info()` so it can also be used to get info for a design + doc. + * Add view definition options, e.g. collation. + * Fix support for authentication in dump/load tools. + * Support non-ASCII document IDs in serialization format. + * Protect `ResponseBody` from being iterated/closed multiple times. + * Rename iteration method for ResponseBody chunks to `iterchunks()` to + prevent usage for non-chunked responses. + * JSON encoding exceptions are no longer masked, resulting in better error + messages. + * `cjson` support is now deprecated. + * Fix `Row.value` and `Row.__repr__` to never raise exceptions. + * Fix Python view server's reduce to handle empty map results list. + * Use locale-independent timestamp identifiers for HTTP cache. + * Don't require setuptools/distribute to install the core package. (Still + needed to install the console scripts.) + + +Version 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. diff -Nru python-couchdb-0.8/ChangeLog.txt python-couchdb-0.10/ChangeLog.txt --- python-couchdb-0.8/ChangeLog.txt 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/ChangeLog.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,178 +0,0 @@ -Version 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. diff -Nru python-couchdb-0.8/couchdb/client.py python-couchdb-0.10/couchdb/client.py --- python-couchdb-0.8/couchdb/client.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/couchdb/client.py 2014-07-15 06:59:19.000000000 +0000 @@ -13,9 +13,9 @@ >>> doc_id, doc_rev = db.save({'type': 'Person', 'name': 'John Doe'}) >>> doc = db[doc_id] >>> doc['type'] -'Person' +u'Person' >>> doc['name'] -'John Doe' +u'John Doe' >>> del db[doc.id] >>> doc.id in db False @@ -23,6 +23,7 @@ >>> del server['python-tests'] """ +import itertools import mimetypes import os from types import FunctionType @@ -31,7 +32,7 @@ import re import warnings -from couchdb import http, json +from couchdb import http, json, util __all__ = ['Server', 'Database', 'Document', 'ViewResults', 'Row'] __docformat__ = 'restructuredtext en' @@ -75,7 +76,7 @@ :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): + if isinstance(url, util.strbase): self.resource = http.Resource(url, session or http.Session()) else: self.resource = url # treat as a Resource object @@ -90,7 +91,7 @@ :return: `True` if a database with the name exists, `False` otherwise """ try: - self.resource.head(validate_dbname(name)) + self.resource.head(name) return True except http.ResourceNotFound: return False @@ -113,6 +114,9 @@ except: return False + def __bool__(self): + return self.__nonzero__() + def __repr__(self): return '<%s %r>' % (type(self).__name__, self.resource.url) @@ -122,7 +126,7 @@ :param name: the name of the database :raise ResourceNotFound: if no database with that name exists """ - self.resource.delete_json(validate_dbname(name)) + self.resource.delete_json(name) def __getitem__(self, name): """Return a `Database` object representing the database with the @@ -133,7 +137,7 @@ :rtype: `Database` :raise ResourceNotFound: if no database with that name exists """ - db = Database(self.resource(name), validate_dbname(name)) + db = Database(self.resource(name), name) db.resource.head() # actually make a request to the database return db @@ -159,9 +163,17 @@ status, headers, data = self.resource.get_json() return data['version'] - def stats(self): - """Database statistics.""" - status, headers, data = self.resource.get_json('_stats') + def stats(self, name=None): + """Server statistics. + + :param name: name of single statistic, e.g. httpd/requests + (None -- return all statistics) + """ + if not name: + resource = self.resource('_stats') + else: + resource = self.resource('_stats', *name.split('/')) + status, headers, data = resource.get_json() return data def tasks(self): @@ -190,7 +202,7 @@ :rtype: `Database` :raise PreconditionFailed: if a database with that name already exists """ - self.resource.put_json(validate_dbname(name)) + self.resource.put_json(name) return self[name] def delete(self, name): @@ -230,18 +242,18 @@ >>> 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 - ('...', ...) + (u'...', ...) >>> doc['type'] - 'Person' + u'Person' >>> doc['name'] - 'John Doe' + u'John Doe' To update an existing document, you use item access, too: @@ -263,7 +275,7 @@ """ def __init__(self, url, name=None, session=None): - if isinstance(url, basestring): + if isinstance(url, util.strbase): if not url.startswith('http'): url = DEFAULT_BASE_URL + url self.resource = http.Resource(url, session) @@ -282,7 +294,7 @@ :return: `True` if a document with the ID exists, `False` otherwise """ try: - self.resource.head(id) + _doc_resource(self.resource, id).head() return True except http.ResourceNotFound: return False @@ -304,13 +316,17 @@ except: return False + def __bool__(self): + return self.__nonzero__() + 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('"')) + resource = _doc_resource(self.resource, id) + status, headers, data = resource.head() + resource.delete_json(rev=headers['etag'].strip('"')) def __getitem__(self, id): """Return the document with the specified ID. @@ -319,7 +335,7 @@ :return: a `Row` object representing the requested document :rtype: `Document` """ - _, _, data = self.resource.get_json(id) + _, _, data = _doc_resource(self.resource, id).get_json() return Document(data) def __setitem__(self, id, content): @@ -330,7 +346,8 @@ new documents, or a `Row` object for existing documents """ - status, headers, data = self.resource.put_json(id, body=content) + resource = _doc_resource(self.resource, id) + status, headers, data = resource.put_json(body=content) content.update({'_id': data['id'], '_rev': data['rev']}) @property @@ -346,6 +363,14 @@ self.info() return self._name + @property + def security(self): + return self.resource.get_json('_security')[2] + + @security.setter + def security(self, doc): + self.resource.put_json('_security', body=doc) + def create(self, data): """Create a new document in the database with a random ID that is generated by the server. @@ -401,7 +426,7 @@ :rtype: `tuple` """ if '_id' in doc: - func = self.resource(doc['_id']).put_json + func = _doc_resource(self.resource, doc['_id']).put_json else: func = self.resource.post_json _, _, data = func(body=doc, **options) @@ -411,6 +436,18 @@ doc['_rev'] = rev return id, rev + def cleanup(self): + """Clean up old design document indexes. + + Remove all unused index files from the database storage area. + + :return: a boolean to indicate successful cleanup initiation + :rtype: `bool` + """ + headers = {'Content-Type': 'application/json'} + _, _, data = self.resource('_view_cleanup').post_json(headers=headers) + return data['ok'] + 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 @@ -453,7 +490,7 @@ :rtype: `str` :since: 0.6 """ - if not isinstance(src, basestring): + if not isinstance(src, util.strbase): if not isinstance(src, dict): if hasattr(src, 'items'): src = dict(src.items()) @@ -462,7 +499,7 @@ type(src)) src = src['_id'] - if not isinstance(dest, basestring): + if not isinstance(dest, util.strbase): if not isinstance(dest, dict): if hasattr(dest, 'items'): dest = dict(dest.items()) @@ -477,7 +514,7 @@ _, _, data = self.resource._request('COPY', src, headers={'Destination': dest}) - data = json.decode(data.read()) + data = json.decode(data.read().decode('utf-8')) return data['rev'] def delete(self, doc): @@ -496,10 +533,10 @@ >>> doc2 = db['johndoe'] >>> doc2['age'] = 42 >>> db['johndoe'] = doc2 - >>> db.delete(doc) + >>> db.delete(doc) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - ResourceConflict: ('conflict', 'Document update conflict.') + ResourceConflict: (u'conflict', u'Document update conflict.') >>> del server['python-tests'] @@ -509,7 +546,7 @@ """ if doc['_id'] is None: raise ValueError('document ID cannot be None') - self.resource.delete_json(doc['_id'], rev=doc['_rev']) + _doc_resource(self.resource, doc['_id']).delete_json(rev=doc['_rev']) def get(self, id, default=None, **options): """Return the document with the specified ID. @@ -522,7 +559,7 @@ :rtype: `Document` """ try: - _, _, data = self.resource.get_json(id, **options) + _, _, data = _doc_resource(self.resource, id).get_json(**options) except http.ResourceNotFound: return default if hasattr(data, 'items'): @@ -538,7 +575,8 @@ in reverse chronological order, if any were found """ try: - status, headers, data = self.resource.get_json(id, revs=True) + resource = _doc_resource(self.resource, id) + status, headers, data = resource.get_json(revs=True) except http.ResourceNotFound: return @@ -550,18 +588,25 @@ return yield revision - def info(self): - """Return information about the database as a dictionary. + def info(self, ddoc=None): + """Return information about the database or design document as a + dictionary. + + Without an argument, returns database information. With an argument, + return information for the given design document. The returned dictionary exactly corresponds to the JSON response to - a ``GET`` request on the database URI. + a ``GET`` request on the database or design document's info URI. :return: a dictionary of database properties :rtype: ``dict`` :since: 0.4 """ - _, _, data = self.resource.get_json() - self._name = data['db_name'] + if ddoc is not None: + _, _, data = self.resource('_design', ddoc, '_info').get_json() + else: + _, _, data = self.resource.get_json() + self._name = data['db_name'] return data def delete_attachment(self, doc, filename): @@ -576,7 +621,7 @@ :param filename: the name of the attachment file :since: 0.4.1 """ - resource = self.resource(doc['_id']) + resource = _doc_resource(self.resource, doc['_id']) _, _, data = resource.delete_json(filename, rev=doc['_rev']) doc['_rev'] = data['rev'] @@ -593,12 +638,12 @@ of the `default` argument if the attachment is not found :since: 0.4.1 """ - if isinstance(id_or_doc, basestring): + if isinstance(id_or_doc, util.strbase): id = id_or_doc else: id = id_or_doc['_id'] try: - _, _, data = self.resource(id).get(filename) + _, _, data = _doc_resource(self.resource, id).get(filename) return data except http.ResourceNotFound: return default @@ -632,7 +677,7 @@ filter(None, mimetypes.guess_type(filename)) ) - resource = self.resource(doc['_id']) + resource = _doc_resource(self.resource, doc['_id']) status, headers, data = resource.put_json(filename, body=content, headers={ 'Content-Type': content_type }, rev=doc['_rev']) @@ -652,17 +697,17 @@ ... emit(doc.name, null); ... }''' >>> for row in db.query(map_fun): - ... print row.key + ... print(row.key) John Doe Mary Jane >>> for row in db.query(map_fun, descending=True): - ... print row.key + ... print(row.key) Mary Jane John Doe >>> for row in db.query(map_fun, key='John Doe'): - ... print row.key + ... print(row.key) John Doe >>> del server['python-tests'] @@ -692,10 +737,10 @@ ... Document(type='Person', name='Mary Jane'), ... Document(type='City', name='Gotham City') ... ]): - ... print repr(doc) #doctest: +ELLIPSIS - (True, '...', '...') - (True, '...', '...') - (True, '...', '...') + ... print(repr(doc)) #doctest: +ELLIPSIS + (True, u'...', u'...') + (True, u'...', u'...') + (True, u'...', u'...') >>> del server['python-tests'] @@ -750,6 +795,25 @@ return results + def purge(self, docs): + """Perform purging (complete removing) of the given documents. + + Uses a single HTTP request to purge all given documents. Purged + documents do not leave any meta-data in the storage and are not + replicated. + """ + content = {} + for doc in docs: + if isinstance(doc, dict): + content[doc['_id']] = [doc['_rev']] + elif hasattr(doc, 'items'): + doc = dict(doc.items()) + content[doc['_id']] = [doc['_rev']] + else: + raise TypeError('expected dict, got %s' % type(doc)) + _, _, data = self.resource.post_json('_purge', body=content) + return data + def view(self, name, wrapper=None, **options): """Execute a predefined view. @@ -758,7 +822,7 @@ >>> db['gotham'] = dict(type='City', name='Gotham City') >>> for row in db.view('_all_docs'): - ... print row.id + ... print(row.id) gotham >>> del server['python-tests'] @@ -773,19 +837,112 @@ :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, + path = _path_from_name(name, '_view') + return PermanentView(self.resource(*path), '/'.join(path), wrapper=wrapper)(**options) + def iterview(self, name, batch, wrapper=None, **options): + """Iterate the rows in a view, fetching rows in batches and yielding + one row at a time. + + Since the view's rows are fetched in batches any rows emitted for + documents added, changed or deleted between requests may be missed or + repeated. + + :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 batch: number of rows to fetch per HTTP request. + :param wrapper: an optional callable that should be used to wrap the + result rows + :param options: optional query string parameters + :return: row generator + """ + # Check sane batch size. + if batch <= 0: + raise ValueError('batch must be 1 or more') + # Save caller's limit, it must be handled manually. + limit = options.get('limit') + if limit is not None and limit <= 0: + raise ValueError('limit must be 1 or more') + while True: + loop_limit = min(limit or batch, batch) + # Get rows in batches, with one extra for start of next batch. + options['limit'] = loop_limit + 1 + rows = list(self.view(name, wrapper, **options)) + # Yield rows from this batch. + for row in itertools.islice(rows, loop_limit): + yield row + # Decrement limit counter. + if limit is not None: + limit -= min(len(rows), batch) + # Check if there is nothing else to yield. + if len(rows) <= batch or (limit is not None and limit == 0): + break + # Update options with start keys for next loop. + options.update(startkey=rows[-1]['key'], startkey_docid=rows[-1]['id']) + + def show(self, name, docid=None, **options): + """Call a 'show' function. + + :param name: the name of the show function in the format + ``designdoc/showname`` + :param docid: optional ID of a document to pass to the show function. + :param options: optional query string parameters + :return: (headers, body) tuple, where headers is a dict of headers + returned from the show function and body is a readable + file-like instance + """ + path = _path_from_name(name, '_show') + if docid: + path.append(docid) + status, headers, body = self.resource(*path).get(**options) + return headers, body + + def list(self, name, view, **options): + """Format a view using a 'list' function. + + :param name: the name of the list function in the format + ``designdoc/listname`` + :param view: the name of the view in the format ``designdoc/viewname`` + :param options: optional query string parameters + :return: (headers, body) tuple, where headers is a dict of headers + returned from the list function and body is a readable + file-like instance + """ + path = _path_from_name(name, '_list') + path.extend(view.split('/', 1)) + _, headers, body = _call_viewlike(self.resource(*path), options) + return headers, body + + def update_doc(self, name, docid=None, **options): + """Calls server side update handler. + + :param name: the name of the update handler function in the format + ``designdoc/updatename``. + :param docid: optional ID of a document to pass to the update handler. + :param options: optional query string parameters. + :return: (headers, body) tuple, where headers is a dict of headers + returned from the list function and body is a readable + file-like instance + """ + path = _path_from_name(name, '_update') + if docid is None: + func = self.resource(*path).post + else: + path.append(docid) + func = self.resource(*path).put + _, headers, body = func(**options) + return headers, body + def _changes(self, **opts): _, _, data = self.resource.get('_changes', **opts) - lines = iter(data) + lines = data.iterchunks() for ln in lines: if not ln: # skip heartbeats continue - doc = json.decode(ln) + doc = json.decode(ln.decode('utf-8')) 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 @@ -794,7 +951,8 @@ def changes(self, **opts): """Retrieve a changes feed from the database. - Takes since, feed, heartbeat and timeout options. + :param opts: optional query string parameters + :return: an iterable over change notification dicts """ if opts.get('feed') == 'continuous': return self._changes(**opts) @@ -802,6 +960,26 @@ return data +def _doc_resource(base, doc_id): + """Return the resource for the given document id. + """ + # Split an id that starts with a reserved segment, e.g. _design/foo, so + # that the / that follows the 1st segment does not get escaped. + if doc_id[:1] == '_': + return base(*doc_id.split('/', 1)) + return base(doc_id) + + +def _path_from_name(name, type): + """Expand a 'design/foo' style name to its full path as a list of + segments. + """ + if name.startswith('_'): + return name.split('/') + design, name = name.split('/', 1) + return ['_design', design, type, name] + + class Document(dict): """Representation of a document in the database. @@ -835,7 +1013,7 @@ """Abstract representation of a view or query.""" def __init__(self, url, wrapper=None, session=None): - if isinstance(url, basestring): + if isinstance(url, util.strbase): self.resource = http.Resource(url, session) else: self.resource = url @@ -847,15 +1025,6 @@ 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 @@ -871,13 +1040,7 @@ 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)) + _, _, data = _call_viewlike(self.resource, options) return data @@ -911,10 +1074,34 @@ content = json.encode(body).encode('utf-8') _, _, data = self.resource.post_json(body=content, headers={ 'Content-Type': 'application/json' - }, **self._encode_options(options)) + }, **_encode_view_options(options)) return data +def _encode_view_options(options): + """Encode any items in the options dict that are sent as a JSON string to a + view/list function. + """ + retval = {} + for name, value in options.items(): + if name in ('key', 'startkey', 'endkey') \ + or not isinstance(value, util.strbase): + value = json.encode(value) + retval[name] = value + return retval + + +def _call_viewlike(resource, options): + """Call a resource that takes view-like options. + """ + if 'keys' in options: + options = options.copy() + keys = {'keys': options.pop('keys')} + return resource.post_json(body=keys, **_encode_view_options(options)) + else: + return resource.get_json(**_encode_view_options(options)) + + class ViewResults(object): """Representation of a parameterized view (either permanent or temporary) and the results it produces. @@ -944,7 +1131,7 @@ >>> people = results[['Person']:['Person','ZZZZ']] >>> for person in people: - ... print person.value + ... print(person.value) John Doe Mary Jane >>> people.total_rows, people.offset @@ -955,7 +1142,7 @@ can still return multiple rows: >>> list(results[['City', 'Gotham City']]) - [] + [] >>> del server['python-tests'] """ @@ -963,7 +1150,7 @@ def __init__(self, view, options): self.view = view self.options = options - self._rows = self._total_rows = self._offset = None + self._rows = self._total_rows = self._offset = self._update_seq = None def __repr__(self): return '<%s %r %r>' % (type(self).__name__, self.view, self.options) @@ -992,6 +1179,7 @@ self._rows = [wrapper(row) for row in data['rows']] self._total_rows = data.get('total_rows') self._offset = data.get('offset', 0) + self._update_seq = data.get('update_seq') @property def rows(self): @@ -1027,16 +1215,28 @@ self._fetch() return self._offset + @property + def update_seq(self): + """The database update sequence that the view reflects. + + The update sequence is included in the view result only when it is + explicitly requested using the `update_seq=true` query option. + Otherwise, the value is None. + + :rtype: `int` or `NoneType` depending on the query options + """ + if self._rows is None: + self._fetch() + return self._update_seq + 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) + keys = 'id', 'key', 'error', 'value' + items = ['%s=%r' % (k, self[k]) for k in keys if k in self] + return '<%s %s>' % (type(self).__name__, ', '.join(items)) @property def id(self): @@ -1047,13 +1247,15 @@ @property def key(self): - """The associated key.""" return self['key'] @property def value(self): - """The associated value.""" - return self['value'] + return self.get('value') + + @property + def error(self): + return self.get('error') @property def doc(self): @@ -1064,13 +1266,3 @@ 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 diff -Nru python-couchdb-0.8/couchdb/design.py python-couchdb-0.10/couchdb/design.py --- python-couchdb-0.8/couchdb/design.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/couchdb/design.py 2014-07-15 06:59:19.000000000 +0000 @@ -38,12 +38,12 @@ 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) - + >>> view.sync(db) #doctest: +ELLIPSIS + [(True, u'_design/tests', ...)] >>> design_doc = view.get_doc(db) >>> design_doc #doctest: +ELLIPSIS - - >>> print design_doc['views']['all']['map'] + + >>> print(design_doc['views']['all']['map']) function(doc) { emit(doc._id, null); } @@ -54,11 +54,12 @@ >>> def my_map(doc): ... yield doc['somekey'], doc['somevalue'] >>> view = ViewDefinition('test2', 'somename', my_map, language='python') - >>> view.sync(db) + >>> view.sync(db) #doctest: +ELLIPSIS + [(True, u'_design/test2', ...)] >>> design_doc = view.get_doc(db) >>> design_doc #doctest: +ELLIPSIS - - >>> print design_doc['views']['somename']['map'] + + >>> print(design_doc['views']['somename']['map']) def my_map(doc): yield doc['somekey'], doc['somevalue'] @@ -70,7 +71,8 @@ """ def __init__(self, design, name, map_fun, reduce_fun=None, - language='javascript', wrapper=None, **defaults): + language='javascript', wrapper=None, options=None, + **defaults): """Initialize the view definition. Note that the code in `map_fun` and `reduce_fun` is automatically @@ -84,6 +86,7 @@ :param language: the name of the language used :param wrapper: an optional callable that should be used to wrap the result rows + :param options: view specific options (e.g. {'collation':'raw'}) """ if design.startswith('_design/'): design = design[8:] @@ -99,6 +102,7 @@ self.reduce_fun = reduce_fun self.language = language self.wrapper = wrapper + self.options = options self.defaults = defaults def __call__(self, db, **options): @@ -109,10 +113,11 @@ :return: the view results :rtype: `ViewResults` """ + wrapper = options.pop('wrapper', self.wrapper) merged_options = self.defaults.copy() merged_options.update(options) return db.view('/'.join([self.design, self.name]), - wrapper=self.wrapper, **merged_options) + wrapper=wrapper, **merged_options) def __repr__(self): return '<%s %r>' % (type(self).__name__, '/'.join([ @@ -136,7 +141,7 @@ :param db: the `Database` instance """ - type(self).sync_many(db, [self]) + return type(self).sync_many(db, [self]) @staticmethod def sync_many(db, views, remove_missing=False, callback=None): @@ -160,6 +165,7 @@ """ docs = [] + views = sorted(views, key=attrgetter('design')) for design, views in groupby(views, key=attrgetter('design')): doc_id = '_design/%s' % design doc = db.get(doc_id, {'_id': doc_id}) @@ -171,6 +177,8 @@ funcs = {'map': view.map_fun} if view.reduce_fun: funcs['reduce'] = view.reduce_fun + if view.options: + funcs['options'] = view.options doc.setdefault('views', {})[view.name] = funcs languages.add(view.language) if view.name in missing: @@ -192,7 +200,7 @@ callback(doc) docs.append(doc) - db.update(docs) + return db.update(docs) def _strip_decorators(code): diff -Nru python-couchdb-0.8/couchdb/http.py python-couchdb-0.10/couchdb/http.py --- python-couchdb-0.8/couchdb/http.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/couchdb/http.py 2014-07-15 06:59:19.000000000 +0000 @@ -14,22 +14,27 @@ 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 + +try: + from http.client import BadStatusLine, HTTPConnection, HTTPSConnection +except ImportError: + from httplib import BadStatusLine, HTTPConnection, HTTPSConnection + +try: + from email.Utils import parsedate +except ImportError: + from email.utils import parsedate from couchdb import json +from couchdb import util __all__ = ['HTTPError', 'PreconditionFailed', 'ResourceNotFound', 'ResourceConflict', 'ServerError', 'Unauthorized', 'RedirectLimit', @@ -37,6 +42,91 @@ __docformat__ = 'restructuredtext en' +if sys.version < '2.6': + + class TimeoutMixin: + """Helper mixin to add timeout before socket connection""" + + # taken from original python2.5 httplib source code with timeout setting added + def connect(self): + """Connect to the host and port specified in __init__.""" + msg = "getaddrinfo returns an empty list" + for res in socket.getaddrinfo(self.host, self.port, 0, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + try: + self.sock = socket.socket(af, socktype, proto) + if self.debuglevel > 0: + print("connect: (%s, %s)" % (self.host, self.port)) + + # setting socket timeout + self.sock.settimeout(self.timeout) + + self.sock.connect(sa) + except socket.error as msg: + if self.debuglevel > 0: + print('connect fail:', self.host, self.port) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error(msg) + + _HTTPConnection = HTTPConnection + _HTTPSConnection = HTTPSConnection + + class HTTPConnection(TimeoutMixin, _HTTPConnection): + def __init__(self, *a, **k): + timeout = k.pop('timeout', None) + _HTTPConnection.__init__(self, *a, **k) + self.timeout = timeout + + class HTTPSConnection(TimeoutMixin, _HTTPSConnection): + def __init__(self, *a, **k): + timeout = k.pop('timeout', None) + _HTTPSConnection.__init__(self, *a, **k) + self.timeout = timeout + + +if sys.version < '2.7': + + from httplib import CannotSendHeader, _CS_REQ_STARTED, _CS_REQ_SENT + + class NagleMixin: + """ + Mixin to upgrade httplib connection types so headers and body can be + sent at the same time to avoid triggering Nagle's algorithm. + + Based on code originally copied from Python 2.7's httplib module. + """ + + def endheaders(self, message_body=None): + if self.__dict__['_HTTPConnection__state'] == _CS_REQ_STARTED: + self.__dict__['_HTTPConnection__state'] = _CS_REQ_SENT + else: + raise CannotSendHeader() + self._send_output(message_body) + + def _send_output(self, message_body=None): + self._buffer.extend(("", "")) + msg = "\r\n".join(self._buffer) + del self._buffer[:] + if isinstance(message_body, str): + msg += message_body + message_body = None + self.send(msg) + if message_body is not None: + self.send(message_body) + + class HTTPConnection(NagleMixin, HTTPConnection): + pass + + class HTTPSConnection(NagleMixin, HTTPSConnection): + pass + + class HTTPError(Exception): """Base class for errors based on HTTP status codes >= 400.""" @@ -78,17 +168,32 @@ 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): + def __init__(self, resp, conn_pool, url, conn): self.resp = resp - self.callback = callback + self.chunked = self.resp.msg.get('transfer-encoding') == 'chunked' + self.conn_pool = conn_pool + self.url = url + self.conn = conn + + def __del__(self): + if not self.chunked: + self.close() + else: + self.resp.close() + if self.conn: + # Since chunked responses can be infinite (i.e. for + # feed=continuous), and we want to avoid leaking sockets + # (even if just to prevent ResourceWarnings when running + # the test suite on Python 3), we'll close this connection + # eagerly. We can't get it into the clean state required to + # put it back into the ConnectionPool (since we don't know + # when it ends and we can only do blocking reads). Finding + # out whether it might in fact end would be relatively onerous + # and require a layering violation. + self.conn.close() def read(self, size=None): bytes = self.resp.read(size) @@ -96,19 +201,26 @@ self.close() return bytes + def _release_conn(self): + self.conn_pool.release(self.url, self.conn) + self.conn_pool, self.url, self.conn = None, None, None + def close(self): while not self.resp.isclosed(): - self.read(CHUNK_SIZE) - self.callback() + self.resp.read(CHUNK_SIZE) + if self.conn: + self._release_conn() - def __iter__(self): - assert self.resp.msg.get('transfer-encoding') == 'chunked' + def iterchunks(self): + assert self.chunked while True: + if self.resp.isclosed(): + break chunksz = int(self.resp.fp.readline().strip(), 16) if not chunksz: self.resp.fp.read(2) #crlf self.resp.close() - self.callback() + self._release_conn() break chunk = self.resp.fp.read(chunksz) for ln in chunk.splitlines(): @@ -133,19 +245,26 @@ :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 + timeout (the default) :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 = {} + # XXX We accept a `cache` dict arg, but the ref gets overwritten later + # during cache cleanup. Do we remove the cache arg (does using a shared + # Session instance cover the same use cases?) or fix the cache cleanup? + # For now, let's just assign the dict to the Cache instance to retain + # current behaviour. + if cache is not None: + cache_by_url = cache + cache = Cache() + cache.by_url = cache_by_url + else: + cache = 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.connection_pool = ConnectionPool(timeout) self.retry_delays = list(retry_delays) # We don't want this changing on us. self.retryable_errors = set(retryable_errors) @@ -168,33 +287,30 @@ if etag: headers['If-None-Match'] = etag + if (body is not None and not isinstance(body, util.strbase) and + not hasattr(body, 'read')): + body = json.encode(body).encode('utf-8') + headers.setdefault('Content-Type', 'application/json') + if body is None: headers.setdefault('Content-Length', '0') + elif isinstance(body, util.strbase): + headers.setdefault('Content-Length', str(len(body))) 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' + 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) + path_query = util.urlunsplit(('', '') + util.urlsplit(url)[2:4] + ('',)) + conn = self.connection_pool.get(url) def _try_request_with_retries(retries): while True: try: return _try_request() - except socket.error, e: + except socket.error as e: ecode = e.args[0] if ecode not in self.retryable_errors: raise @@ -208,25 +324,30 @@ 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) + if body is None: + conn.endheaders() + else: + if isinstance(body, util.strbase): + if isinstance(body, util.utype): + conn.endheaders(body.encode('utf-8')) + else: + conn.endheaders(body) else: # assume a file-like object and send in chunks + conn.endheaders() 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') + if isinstance(chunk, util.utype): + chunk = chunk.encode('utf-8') + status = ('%x\r\n' % len(chunk)).encode('utf-8') + conn.send(status + chunk + b'\r\n') + conn.send(b'0\r\n\r\n') return conn.getresponse() - except BadStatusLine, e: + except BadStatusLine as e: # httplib raises a BadStatusLine when it cannot read the status # line saying, "Presumably, the server closed the connection # before sending a valid response." @@ -242,19 +363,19 @@ # Handle conditional response if status == 304 and method in ('GET', 'HEAD'): resp.read() - self._return_connection(url, conn) + self.connection_pool.release(url, conn) status, msg, data = cached_resp if data is not None: - data = StringIO(data) + data = util.StringIO(data) return status, msg, data elif cached_resp: - del self.cache[url] + self.cache.remove(url) # Handle redirects if status == 303 or \ method in ('GET', 'HEAD') and status in (301, 302, 307): resp.read() - self._return_connection(url, conn) + self.connection_pool.release(url, conn) if num_redirects > self.max_redirects: raise RedirectLimit('Redirection limit exceeded') location = resp.getheader('location') @@ -273,29 +394,28 @@ if method == 'HEAD' or resp.getheader('content-length') == '0' or \ status < 200 or status in (204, 304): resp.read() - self._return_connection(url, conn) + self.connection_pool.release(url, conn) # Buffer small non-JSON response bodies - elif int(resp.getheader('content-length', sys.maxint)) < CHUNK_SIZE: + elif int(resp.getheader('content-length', sys.maxsize)) < CHUNK_SIZE: data = resp.read() - self._return_connection(url, conn) + self.connection_pool.release(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)) + data = ResponseBody(resp, self.connection_pool, 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) + data = json.decode(data.decode('utf-8')) error = data.get('error'), data.get('reason') elif method != 'HEAD': error = resp.read() - self._return_connection(url, conn) + self.connection_pool.release(url, conn) else: error = '' if status == 401: @@ -311,46 +431,91 @@ # 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() + self.cache.put(url, (status, resp.msg, data)) if not streamed and data is not None: - data = StringIO(data) + data = util.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] +def cache_sort(i): + return datetime.fromtimestamp(time.mktime(parsedate(i[1][1]['Date']))) + +class Cache(object): + """Content cache.""" + + # Some random values to limit memory use + keep_size, max_size = 10, 75 + + def __init__(self): + self.by_url = {} + + def get(self, url): + return self.by_url.get(url) + + def put(self, url, response): + self.by_url[url] = response + if len(self.by_url) > self.max_size: + self._clean() + + def remove(self, url): + self.by_url.pop(url, None) + + def _clean(self): + ls = sorted(self.by_url.iteritems(), key=cache_sort) + self.by_url = dict(ls[-self.keep_size:]) + + +class ConnectionPool(object): + """HTTP connection pool.""" + + def __init__(self, timeout): + self.timeout = timeout + self.conns = {} # HTTP connections keyed by (scheme, host) + self.lock = Lock() + + def get(self, url): + + scheme, host = util.urlsplit(url, 'http', False)[:2] + + # Try to reuse an existing connection. 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) + conn = None finally: self.lock.release() + + # Create a new connection if nothing was available. + if conn is None: + if scheme == 'http': + cls = HTTPConnection + elif scheme == 'https': + cls = HTTPSConnection + else: + raise ValueError('%s is not a supported scheme' % scheme) + conn = cls(host, timeout=self.timeout) + conn.connect() + return conn - def _return_connection(self, url, conn): - scheme, host = urlsplit(url, 'http', False)[:2] + def release(self, url, conn): + scheme, host = util.urlsplit(url, 'http', False)[:2] self.lock.acquire() try: self.conns.setdefault((scheme, host), []).append(conn) finally: self.lock.release() + def __del__(self): + for key, conns in list(self.conns.items()): + for conn in conns: + conn.close() + class Resource(object): @@ -383,29 +548,19 @@ 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 delete_json(self, path=None, headers=None, **params): + return self._request_json('DELETE', path, headers=headers, **params) - 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 get_json(self, path=None, headers=None, **params): + return self._request_json('GET', path, headers=headers, **params) - 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 post_json(self, path=None, body=None, headers=None, **params): + return self._request_json('POST', path, body=body, headers=headers, + **params) + + def put_json(self, path=None, body=None, headers=None, **params): + return self._request_json('PUT', path, body=body, headers=headers, + **params) def _request(self, method, path=None, body=None, headers=None, **params): all_headers = self.headers.copy() @@ -418,6 +573,14 @@ headers=all_headers, credentials=self.credentials) + def _request_json(self, method, path=None, body=None, headers=None, **params): + status, headers, data = self._request(method, path, body=body, + headers=headers, **params) + if 'application/json' in headers.get('content-type', ''): + data = json.decode(data.read().decode('utf-8')) + return status, headers, data + + def extract_credentials(url): """Extract authentication (user name and password) credentials from the @@ -430,27 +593,34 @@ >>> extract_credentials('http://joe%40example.com:secret@localhost:5984/_config/') ('http://localhost:5984/_config/', ('joe@example.com', 'secret')) """ - parts = urlsplit(url) + parts = util.urlsplit(url) netloc = parts[1] if '@' in netloc: creds, netloc = netloc.split('@') - credentials = tuple(urllib.unquote(i) for i in creds.split(':')) + credentials = tuple(util.urlunquote(i) for i in creds.split(':')) parts = list(parts) parts[1] = netloc else: credentials = None - return urlunsplit(parts), credentials + return util.urlunsplit(parts), credentials def basic_auth(credentials): + """Generates authorization header value for given credentials. + >>> basic_auth(('root', 'relax')) + u'Basic cm9vdDpyZWxheA==' + >>> basic_auth(None) + >>> basic_auth(()) + """ if credentials: - return 'Basic %s' % b64encode('%s:%s' % credentials) + token = b64encode(('%s:%s' % credentials).encode('latin1')) + return 'Basic %s' % (token.strip().decode('latin1')) def quote(string, safe=''): - if isinstance(string, unicode): + if isinstance(string, util.utype): string = string.encode('utf-8') - return urllib.quote(string, safe) + return util.urlquote(string, safe) def urlencode(data): @@ -458,10 +628,10 @@ data = data.items() params = [] for name, value in data: - if isinstance(value, unicode): + if isinstance(value, util.utype): value = value.encode('utf-8') params.append((name, value)) - return urllib.urlencode(params) + return util.urlencode(params) def urljoin(base, *path, **query): diff -Nru python-couchdb-0.8/couchdb/json.py python-couchdb-0.10/couchdb/json.py --- python-couchdb-0.8/couchdb/json.py 2010-06-02 14:02:32.000000000 +0000 +++ python-couchdb-0.10/couchdb/json.py 2014-07-15 06:59:19.000000000 +0000 @@ -33,8 +33,12 @@ __all__ = ['decode', 'encode', 'use'] +from couchdb import util +import warnings +import os + _initialized = False -_using = None +_using = os.environ.get('COUCHDB_PYTHON_JSON') _decode = None _encode = None @@ -69,8 +73,8 @@ """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. + The modules "simplejson" 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 @@ -88,7 +92,7 @@ """ global _decode, _encode, _initialized, _using if module is not None: - if not isinstance(module, basestring): + if not isinstance(module, util.strbase): module = module.__name__ if module not in ('cjson', 'json', 'simplejson'): raise ValueError('Unsupported JSON module %s' % module) @@ -128,6 +132,10 @@ if _using == 'simplejson': _init_simplejson() elif _using == 'cjson': + warnings.warn("Builtin cjson support is deprecated. Please use the " + "default or provide custom decode/encode functions " + "[2011-11-09].", + DeprecationWarning, stacklevel=1) _init_cjson() elif _using == 'json': _init_stdlib() diff -Nru python-couchdb-0.8/couchdb/mapping.py python-couchdb-0.10/couchdb/mapping.py --- python-couchdb-0.8/couchdb/mapping.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/couchdb/mapping.py 2014-07-15 06:59:19.000000000 +0000 @@ -15,7 +15,8 @@ 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 +>>> from datetime import datetime +>>> from couchdb.mapping import Document, TextField, IntegerField, DateTimeField >>> class Person(Document): ... name = TextField() ... age = IntegerField() @@ -57,7 +58,7 @@ >>> del server['python-tests'] """ -import copy +import copy, sys from calendar import timegm from datetime import date, datetime, time @@ -65,6 +66,7 @@ from time import strptime, struct_time from couchdb.design import ViewDefinition +from couchdb import util __all__ = ['Mapping', 'Document', 'Field', 'TextField', 'FloatField', 'IntegerField', 'LongField', 'BooleanField', 'DecimalField', @@ -105,7 +107,7 @@ instance._data[self.name] = value def _to_python(self, value): - return unicode(value) + return util.utype(value) def _to_json(self, value): return self._to_python(value) @@ -126,9 +128,10 @@ d['_fields'] = fields return type.__new__(cls, name, bases, d) +MappingMetaClass = MappingMeta('MappingMetaClass', (object,), {}) -class Mapping(object): - __metaclass__ = MappingMeta + +class Mapping(MappingMetaClass): def __init__(self, **values): self._data = {} @@ -153,7 +156,7 @@ def __setitem__(self, name, value): self._data[name] = value - def get(self, name, default): + def get(self, name, default=None): return self._data.get(name, default) def setdefault(self, name, default): @@ -199,7 +202,7 @@ >>> Person.by_name - >>> print Person.by_name.map_fun + >>> print(Person.by_name.map_fun) function(doc) { emit(doc.name, doc); } @@ -234,7 +237,7 @@ >>> Person.by_name - >>> print Person.by_name.map_fun + >>> print(Person.by_name.map_fun) def by_name(doc): yield doc['name'], doc """ @@ -291,9 +294,10 @@ attrval.name = attrname return MappingMeta.__new__(cls, name, bases, d) +DocumentMetaClass = DocumentMeta('DocumentMetaClass', (object,), {}) + -class Document(Mapping): - __metaclass__ = DocumentMeta +class Document(DocumentMetaClass, Mapping): def __init__(self, id=None, **values): Mapping.__init__(self, **values) @@ -407,7 +411,7 @@ class TextField(Field): """Mapping field for string values.""" - _to_python = unicode + _to_python = util.utype class FloatField(Field): @@ -422,7 +426,7 @@ class LongField(Field): """Mapping field for long integer values.""" - _to_python = long + _to_python = util.ltype class BooleanField(Field): @@ -437,7 +441,7 @@ return Decimal(value) def _to_json(self, value): - return unicode(value) + return util.utype(value) class DateField(Field): @@ -453,7 +457,7 @@ """ def _to_python(self, value): - if isinstance(value, basestring): + if isinstance(value, util.strbase): try: value = date(*strptime(value, '%Y-%m-%d')[:3]) except ValueError: @@ -479,7 +483,7 @@ """ def _to_python(self, value): - if isinstance(value, basestring): + if isinstance(value, util.strbase): try: value = value.split('.', 1)[0] # strip out microseconds value = value.rstrip('Z') # remove timezone separator @@ -509,7 +513,7 @@ """ def _to_python(self, value): - if isinstance(value, basestring): + if isinstance(value, util.strbase): try: value = value.split('.', 1)[0] # strip out microseconds value = time(*strptime(value, '%H:%M:%S')[3:6]) @@ -553,7 +557,7 @@ >>> post.author.email u'john@doe.com' >>> post.extra - {'foo': 'bar'} + {u'foo': u'bar'} >>> del server['python-tests'] """ @@ -603,11 +607,11 @@ >>> post = Post.load(db, post.id) >>> comment = post.comments[0] >>> comment['author'] - 'myself' + u'myself' >>> comment['content'] - 'Bla bla' + u'Bla bla' >>> comment['time'] #doctest: +ELLIPSIS - '...T...Z' + u'...T...Z' >>> del server['python-tests'] """ @@ -660,16 +664,24 @@ return str(self.list) def __unicode__(self): - return unicode(self.list) + return util.utype(self.list) def __delitem__(self, index): - del self.list[index] + if isinstance(index, slice): + self.__delslice__(index.start, index.stop) + else: + del self.list[index] def __getitem__(self, index): + if isinstance(index, slice): + return self.__getslice__(index.start, index.stop) return self.field._to_python(self.list[index]) def __setitem__(self, index, value): - self.list[index] = self.field._to_json(value) + if isinstance(index, slice): + self.__setslice__(index.start, index.stop, value) + else: + self.list[index] = self.field._to_json(value) def __delslice__(self, i, j): del self.list[i:j] diff -Nru python-couchdb-0.8/couchdb/multipart.py python-couchdb-0.10/couchdb/multipart.py --- python-couchdb-0.8/couchdb/multipart.py 2010-06-02 14:02:32.000000000 +0000 +++ python-couchdb-0.10/couchdb/multipart.py 2014-07-15 06:59:19.000000000 +0000 @@ -10,17 +10,20 @@ from base64 import b64encode from cgi import parse_header +from email import header try: from hashlib import md5 except ImportError: from md5 import new as md5 import sys +from couchdb import util + __all__ = ['read_multipart', 'write_multipart'] __docformat__ = 'restructuredtext en' -CRLF = '\r\n' +CRLF = b'\r\n' def read_multipart(fileobj, boundary=None): @@ -47,16 +50,16 @@ buf = [] outer = in_headers = boundary is None - next_boundary = boundary and '--' + boundary + '\n' or None - last_boundary = boundary and '--' + boundary + '--\n' or None + next_boundary = boundary and ('--' + boundary + '\n').encode('ascii') or None + last_boundary = boundary and ('--' + boundary + '--\n').encode('ascii') or None def _current_part(): - payload = ''.join(buf) - if payload.endswith('\r\n'): + payload = b''.join(buf) + if payload.endswith(b'\r\n'): payload = payload[:-2] - elif payload.endswith('\n'): + elif payload.endswith(b'\n'): payload = payload[:-1] - content_md5 = headers.get('content-md5') + content_md5 = headers.get(b'content-md5') if content_md5: h = b64encode(md5(payload).digest()) if content_md5 != h: @@ -65,10 +68,15 @@ 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() + line = line.replace(CRLF, b'\n') + if line != b'\n': + name, value = [item.strip() for item in line.split(b':', 1)] + name = name.lower().decode('ascii') + value, charset = header.decode_header(value.decode('utf-8'))[0] + if charset is None: + headers[name] = value + else: + headers[name] = value.decode(charset) else: in_headers = False mimetype, params = parse_header(headers.get('content-type')) @@ -84,7 +92,7 @@ yield part return - elif line.replace(CRLF, '\n') == next_boundary: + elif line.replace(CRLF, b'\n') == next_boundary: # We've reached the start of a new part, as indicated by the # boundary if headers: @@ -96,7 +104,7 @@ del buf[:] in_headers = True - elif line.replace(CRLF, '\n') == last_boundary: + elif line.replace(CRLF, b'\n') == last_boundary: # We're done with this multipart envelope break @@ -122,29 +130,37 @@ self._write_headers(headers) def open(self, headers=None, subtype='mixed', boundary=None): - self.fileobj.write('--') - self.fileobj.write(self.boundary) + self.fileobj.write(b'--') + self.fileobj.write(self.boundary.encode('utf-8')) 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(b'--') + self.fileobj.write(self.boundary.encode('utf-8')) self.fileobj.write(CRLF) if headers is None: headers = {} - if isinstance(content, unicode): - ctype, params = parse_header(mimetype) + + ctype, params = parse_header(mimetype) + if isinstance(content, util.utype): if 'charset' in params: content = content.encode(params['charset']) else: content = content.encode('utf-8') mimetype = mimetype + ';charset=utf-8' + elif 'charset' not in params: + try: + content.decode('utf-8') + finally: + mimetype = mimetype + ';charset=utf-8' + headers['Content-Type'] = mimetype if content: headers['Content-Length'] = str(len(content)) - headers['Content-MD5'] = b64encode(md5(content).digest()) + hash = b64encode(md5(content).digest()).decode('ascii') + headers['Content-MD5'] = hash self._write_headers(headers) if content: # XXX: throw an exception if a boundary appears in the content?? @@ -152,9 +168,9 @@ self.fileobj.write(CRLF) def close(self): - self.fileobj.write('--') - self.fileobj.write(self.boundary) - self.fileobj.write('--') + self.fileobj.write(b'--') + self.fileobj.write(self.boundary.encode('ascii')) + self.fileobj.write(b'--') self.fileobj.write(CRLF) def _make_boundary(self): @@ -163,16 +179,18 @@ return '==' + uuid4().hex + '==' except ImportError: from random import randrange - token = randrange(sys.maxint) - format = '%%0%dd' % len(repr(sys.maxint - 1)) - return '===============' + (format % token) + '==' + nonce = ('%%0%dd' % len(repr(sys.maxsize - 1))) % token + return '===============' + nonce + '==' 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]) + value = headers[name] + if value.encode('ascii', 'ignore') != value.encode('utf-8'): + value = header.make_header([(value, 'utf-8')]).encode() + self.fileobj.write(name.encode('utf-8')) + self.fileobj.write(b': ') + self.fileobj.write(value.encode('utf-8')) self.fileobj.write(CRLF) self.fileobj.write(CRLF) @@ -191,19 +209,19 @@ envelope you call the ``add(mimetype, content, [headers])`` method for every part, and finally call the ``close()`` method. - >>> from StringIO import StringIO + >>> from couchdb.util 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') + >>> print(buf.getvalue().replace(b'\r\n', b'\n').decode('utf-8')) Content-Type: multipart/mixed; boundary="==123456789==" --==123456789== Content-Length: 12 Content-MD5: nHmX4a6el41B06x2uCpglQ== - Content-Type: text/plain + Content-Type: text/plain;charset=utf-8 Just testing --==123456789==-- @@ -222,7 +240,7 @@ >>> part.add('text/plain', 'Just testing') >>> part.close() >>> envelope.close() - >>> print buf.getvalue().replace('\r\n', '\n') #:doctest +ELLIPSIS + >>> print(buf.getvalue().replace(b'\r\n', b'\n').decode('utf-8')) #:doctest +ELLIPSIS Content-Type: multipart/mixed; boundary="==123456789==" --==123456789== @@ -231,7 +249,7 @@ --==abcdefghi== Content-Length: 12 Content-MD5: nHmX4a6el41B06x2uCpglQ== - Content-Type: text/plain + Content-Type: text/plain;charset=utf-8 Just testing --==abcdefghi==-- diff -Nru python-couchdb-0.8/couchdb/tests/client.py python-couchdb-0.10/couchdb/tests/client.py --- python-couchdb-0.8/couchdb/tests/client.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/client.py 2014-07-15 06:59:19.000000000 +0000 @@ -6,20 +6,17 @@ # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. -import doctest +from datetime import datetime 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 import client, http, util from couchdb.tests import testutil -http.CACHE_SIZE = 2, 3 class ServerTestCase(testutil.TempDatabaseMixin, unittest.TestCase): @@ -45,14 +42,19 @@ def test_server_vars(self): version = self.server.version() - self.assertTrue(isinstance(version, basestring)) + self.assertTrue(isinstance(version, util.strbase)) 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_server_stats(self): + stats = self.server.stats() + self.assertTrue(isinstance(stats, dict)) + stats = self.server.stats('httpd/requests') + self.assertTrue(isinstance(stats, dict)) + self.assertTrue(len(stats) == 1 and len(stats['httpd']) == 1) + def test_get_db_missing(self): self.assertRaises(http.ResourceNotFound, lambda: self.server['couchdb-python/missing']) @@ -77,21 +79,21 @@ 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') + self.assertEqual(result['ok'], True) + self.assertEqual(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') + self.assertEqual(a[id]['test'], 'b') + self.assertEqual(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) + self.assertEqual(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) @@ -185,7 +187,7 @@ def test_disallow_nan(self): try: - self.db['foo'] = {u'number': float('nan')} + self.db['foo'] = {'number': float('nan')} self.fail('Expected ValueError') except ValueError: pass @@ -213,7 +215,7 @@ 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.assertRaises(StopIteration, lambda: next(gen)) self.assertTrue(self.db.compact()) while self.db.info()['compact_running']: @@ -233,45 +235,45 @@ old_rev = doc['_rev'] self.db.put_attachment(doc, 'Foo bar', 'foo.txt', 'text/plain') - self.assertNotEquals(old_rev, doc['_rev']) + self.assertNotEqual(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.assertEqual(b'Foo bar', self.db.get_attachment(doc, 'foo.txt').read()) - self.assertEqual('Foo bar', + self.assertEqual(b'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.assertNotEqual(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') + fileobj = util.StringIO(b'Foo bar baz') self.db.put_attachment(doc, fileobj, 'foo.txt') - self.assertNotEquals(old_rev, doc['_rev']) + self.assertNotEqual(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.assertEqual(b'Foo bar baz', self.db.get_attachment(doc, 'foo.txt').read()) - self.assertEqual('Foo bar baz', + self.assertEqual(b'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.assertNotEqual(old_rev, doc['_rev']) self.assertEqual(None, self.db['foo'].get('_attachments')) def test_empty_attachment(self): @@ -280,7 +282,7 @@ old_rev = doc['_rev'] self.db.put_attachment(doc, '', 'empty.txt') - self.assertNotEquals(old_rev, doc['_rev']) + self.assertNotEqual(old_rev, doc['_rev']) doc = self.db['foo'] attachment = doc['_attachments']['empty.txt'] @@ -301,7 +303,8 @@ f.close() doc = {} self.db['foo'] = doc - self.db.put_attachment(doc, open(tmpfile)) + with open(tmpfile) as f: + self.db.put_attachment(doc, f) doc = self.db.get('foo') self.assertTrue(doc['_attachments']['test.txt']['content_type'] == 'text/plain') shutil.rmtree(tmpdir) @@ -315,7 +318,7 @@ doc = {} self.db['foo'] = doc self.db.put_attachment(doc, '{}', 'test.json', 'application/json') - self.assertEquals(self.db.get_attachment(doc, 'test.json').read(), '{}') + self.assertEqual(self.db.get_attachment(doc, 'test.json').read(), b'{}') def test_include_docs(self): doc = {'foo': 42, 'bar': 40} @@ -332,7 +335,7 @@ 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))) + keys=list(range(1, 6, 2)))) self.assertEqual(3, len(res)) for idx, i in enumerate(range(1, 6, 2)): self.assertEqual(i, res[idx].key) @@ -435,7 +438,7 @@ 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() + first = next(self.db.changes(feed='continuous')) self.assertEqual(first['seq'], 1) self.assertEqual(first['id'], 'foo') @@ -443,8 +446,8 @@ # 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)]) + scheme, netloc = util.urlsplit(client.DEFAULT_BASE_URL)[:2] + self.assertTrue(self.db.resource.session.connection_pool.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 @@ -453,8 +456,8 @@ 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)]) + scheme, netloc = util.urlsplit(client.DEFAULT_BASE_URL)[:2] + self.assertTrue(self.db.resource.session.connection_pool.conns[(scheme, netloc)]) def test_changes_conn_usable(self): # Consume a changes feed to get a used connection in the pool. @@ -471,9 +474,39 @@ for change in self.db.changes(feed='continuous', heartbeat=100): break + def test_purge(self): + doc = {'a': 'b'} + self.db['foo'] = doc + self.assertEqual(self.db.purge([doc])['purge_seq'], 1) + + def test_json_encoding_error(self): + doc = {'now': datetime.now()} + self.assertRaises(TypeError, self.db.save, doc) + + def test_security(self): + security = self.db.security + self.assertEqual(security, {}) + security['members'] = {'names': ['test'], 'roles': []} + self.db.security = security + class ViewTestCase(testutil.TempDatabaseMixin, unittest.TestCase): + def test_row_object(self): + + row = list(self.db.view('_all_docs', keys=['blah']))[0] + self.assertEqual(row.id, None) + self.assertEqual(row.key, 'blah') + self.assertEqual(row.value, None) + self.assertEqual(row.error, 'not_found') + + self.db.save({'_id': 'xyz', 'foo': 'bar'}) + row = list(self.db.view('_all_docs', keys=['xyz']))[0] + self.assertEqual(row.id, 'xyz') + self.assertEqual(row.key, 'xyz') + self.assertEqual(list(row.value.keys()), ['rev']) + self.assertEqual(row.error, None) + def test_view_multi_get(self): for i in range(1, 6): self.db.save({'i': i}) @@ -484,11 +517,21 @@ } } - res = list(self.db.view('test/multi_key', keys=range(1, 6, 2))) + res = list(self.db.view('test/multi_key', keys=list(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_ddoc_info(self): + self.db['_design/test'] = { + 'language': 'javascript', + 'views': { + 'test': {'map': 'function(doc) { emit(doc.type, null); }'} + } + } + info = self.db.info('test') + self.assertEqual(info['view_index']['compact_running'], False) + def test_view_compaction(self): for i in range(1, 6): self.db.save({'i': i}) @@ -502,6 +545,27 @@ self.db.view('test/multi_key') self.assertTrue(self.db.compact('test')) + def test_view_cleanup(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') + + ddoc = self.db['_design/test'] + ddoc['views'] = { + 'ids': {'map': 'function(doc) { emit(doc._id, null); }'} + } + self.db.update([ddoc]) + self.db.view('test/ids') + self.assertTrue(self.db.cleanup()) + def test_view_function_objects(self): if 'python' not in self.server.config()['query_servers']: return @@ -526,12 +590,17 @@ 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) + self.assertEqual(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) + self.assertEqual(len(list(view)), 1) + + def test_update_seq(self): + self.db['foo'] = {} + rows = self.db.view('_all_docs', update_seq=True) + self.assertEqual(rows.update_seq, 1) def test_tmpview_repr(self): mapfunc = "function(doc) {emit(null, null);}" @@ -567,12 +636,178 @@ self.assertTrue('id' not in repr(rows[0])) +class ShowListTestCase(testutil.TempDatabaseMixin, unittest.TestCase): + + show_func = """ + function(doc, req) { + return {"body": req.id + ":" + (req.query.r || "")}; + } + """ + + list_func = """ + function(head, req) { + start({headers: {'Content-Type': 'text/csv'}}); + if (req.query.include_header) { + send('id' + '\\r\\n'); + } + var row; + while (row = getRow()) { + send(row.id + '\\r\\n'); + } + } + """ + + design_doc = {'_id': '_design/foo', + 'shows': {'bar': show_func}, + 'views': {'by_id': {'map': "function(doc) {emit(doc._id, null)}"}, + 'by_name': {'map': "function(doc) {emit(doc.name, null)}"}}, + 'lists': {'list': list_func}} + + def setUp(self): + super(ShowListTestCase, self).setUp() + # Workaround for possible bug in CouchDB. Adding a timestamp avoids a + # 409 Conflict error when pushing the same design doc that existed in a + # now deleted database. + design_doc = dict(self.design_doc) + design_doc['timestamp'] = time.time() + self.db.save(design_doc) + self.db.update([{'_id': '1', 'name': 'one'}, {'_id': '2', 'name': 'two'}]) + + def test_show_urls(self): + self.assertEqual(self.db.show('_design/foo/_show/bar')[1].read(), b'null:') + self.assertEqual(self.db.show('foo/bar')[1].read(), b'null:') + + def test_show_docid(self): + self.assertEqual(self.db.show('foo/bar')[1].read(), b'null:') + self.assertEqual(self.db.show('foo/bar', '1')[1].read(), b'1:') + self.assertEqual(self.db.show('foo/bar', '2')[1].read(), b'2:') + + def test_show_params(self): + self.assertEqual(self.db.show('foo/bar', r='abc')[1].read(), b'null:abc') + + def test_list(self): + self.assertEqual(self.db.list('foo/list', 'foo/by_id')[1].read(), b'1\r\n2\r\n') + self.assertEqual(self.db.list('foo/list', 'foo/by_id', include_header='true')[1].read(), b'id\r\n1\r\n2\r\n') + + def test_list_keys(self): + self.assertEqual(self.db.list('foo/list', 'foo/by_id', keys=['1'])[1].read(), b'1\r\n') + + def test_list_view_params(self): + self.assertEqual(self.db.list('foo/list', 'foo/by_name', startkey='o', endkey='p')[1].read(), b'1\r\n') + self.assertEqual(self.db.list('foo/list', 'foo/by_name', descending=True)[1].read(), b'2\r\n1\r\n') + + +class UpdateHandlerTestCase(testutil.TempDatabaseMixin, unittest.TestCase): + update_func = """ + function(doc, req) { + if (!doc) { + if (req.id) { + return [{_id : req.id}, "new doc"] + } + return [null, "empty doc"]; + } + doc.name = "hello"; + return [doc, "hello doc"]; + } + """ + + design_doc = {'_id': '_design/foo', + 'language': 'javascript', + 'updates': {'bar': update_func}} + + def setUp(self): + super(UpdateHandlerTestCase, self).setUp() + # Workaround for possible bug in CouchDB. Adding a timestamp avoids a + # 409 Conflict error when pushing the same design doc that existed in a + # now deleted database. + design_doc = dict(self.design_doc) + design_doc['timestamp'] = time.time() + self.db.save(design_doc) + self.db.update([{'_id': 'existed', 'name': 'bar'}]) + + def test_empty_doc(self): + self.assertEqual(self.db.update_doc('foo/bar')[1].read(), b'empty doc') + + def test_new_doc(self): + self.assertEqual(self.db.update_doc('foo/bar', 'new')[1].read(), b'new doc') + + def test_update_doc(self): + self.assertEqual(self.db.update_doc('foo/bar', 'existed')[1].read(), b'hello doc') + + +class ViewIterationTestCase(testutil.TempDatabaseMixin, unittest.TestCase): + + num_docs = 100 + + def docfromnum(self, num): + return {'_id': util.utype(num), 'num': int(num / 2)} + + def docfromrow(self, row): + return {'_id': row['id'], 'num': row['key']} + + def setUp(self): + super(ViewIterationTestCase, self).setUp() + design_doc = {'_id': '_design/test', + 'views': {'nums': {'map': 'function(doc) {emit(doc.num, null);}'}, + 'nulls': {'map': 'function(doc) {emit(null, null);}'}}} + self.db.save(design_doc) + self.db.update([self.docfromnum(num) for num in range(self.num_docs)]) + + def test_allrows(self): + rows = list(self.db.iterview('test/nums', 10)) + self.assertEqual(len(rows), self.num_docs) + self.assertEqual([self.docfromrow(row) for row in rows], + [self.docfromnum(num) for num in range(self.num_docs)]) + + def test_batchsizes(self): + # Check silly _batch values. + self.assertRaises(ValueError, lambda: next(self.db.iterview('test/nums', 0))) + self.assertRaises(ValueError, lambda: next(self.db.iterview('test/nums', -1))) + # Test various _batch sizes that are likely to cause trouble. + self.assertEqual(len(list(self.db.iterview('test/nums', 1))), self.num_docs) + self.assertEqual(len(list(self.db.iterview('test/nums', int(self.num_docs / 2)))), self.num_docs) + self.assertEqual(len(list(self.db.iterview('test/nums', self.num_docs * 2))), self.num_docs) + self.assertEqual(len(list(self.db.iterview('test/nums', self.num_docs - 1))), self.num_docs) + self.assertEqual(len(list(self.db.iterview('test/nums', self.num_docs))), self.num_docs) + self.assertEqual(len(list(self.db.iterview('test/nums', self.num_docs + 1))), self.num_docs) + + def test_limit(self): + # limit=0 doesn't make sense for iterview. + self.assertRaises(ValueError, lambda: next(self.db.iterview('test/nums', 10, limit=0))) + # Test various limit sizes that are likely to cause trouble. + for limit in [1, int(self.num_docs / 4), self.num_docs - 1, self.num_docs, + self.num_docs + 1]: + self.assertEqual([self.docfromrow(doc) for doc in self.db.iterview('test/nums', 10, limit=limit)], + [self.docfromnum(x) for x in range(min(limit, self.num_docs))]) + # Test limit same as batch size, in case of weird edge cases. + limit = int(self.num_docs / 4) + self.assertEqual([self.docfromrow(doc) for doc in self.db.iterview('test/nums', limit, limit=limit)], + [self.docfromnum(x) for x in range(limit)]) + + def test_descending(self): + self.assertEqual([self.docfromrow(doc) for doc in self.db.iterview('test/nums', 10, descending=True)], + [self.docfromnum(x) for x in range(self.num_docs - 1, -1, -1)]) + self.assertEqual([self.docfromrow(doc) for doc in self.db.iterview('test/nums', 10, limit=int(self.num_docs / 4), descending=True)], + [self.docfromnum(x) for x in range(self.num_docs - 1, int(self.num_docs * 3 / 4) - 1, -1)]) + + def test_startkey(self): + self.assertEqual([self.docfromrow(doc) for doc in self.db.iterview('test/nums', 10, startkey=int(self.num_docs / 2) - 1)], + [self.docfromnum(x) for x in range(self.num_docs - 2, self.num_docs)]) + self.assertEqual([self.docfromrow(doc) for doc in self.db.iterview('test/nums', 10, startkey=1, descending=True)], + [self.docfromnum(x) for x in range(3, -1, -1)]) + + def test_nullkeys(self): + self.assertEqual(len(list(self.db.iterview('test/nulls', 10))), self.num_docs) + 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)) + suite.addTest(unittest.makeSuite(ShowListTestCase, 'test')) + suite.addTest(unittest.makeSuite(UpdateHandlerTestCase, 'test')) + suite.addTest(unittest.makeSuite(ViewIterationTestCase, 'test')) + suite.addTest(testutil.doctest_suite(client)) return suite diff -Nru python-couchdb-0.8/couchdb/tests/couchhttp.py python-couchdb-0.10/couchdb/tests/couchhttp.py --- python-couchdb-0.8/couchdb/tests/couchhttp.py 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/couchhttp.py 2014-07-15 06:59:19.000000000 +0000 @@ -0,0 +1,90 @@ +# -*- 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 socket +import time +import unittest + +from couchdb import http, util +from couchdb.tests import testutil + + +class SessionTestCase(testutil.TempDatabaseMixin, unittest.TestCase): + + def test_timeout(self): + dbname, db = self.temp_db() + timeout = 1 + session = http.Session(timeout=timeout) + start = time.time() + status, headers, body = session.request('GET', db.resource.url + '/_changes?feed=longpoll&since=1000&timeout=%s' % (timeout*2*1000,)) + self.assertRaises(socket.timeout, body.read) + self.assertTrue(time.time() - start < timeout * 1.3) + + +class ResponseBodyTestCase(unittest.TestCase): + def test_close(self): + class TestStream(util.StringIO): + def isclosed(self): + return len(self.getvalue()) == self.tell() + + class ConnPool(object): + def __init__(self): + self.value = 0 + def release(self, url, conn): + self.value += 1 + + conn_pool = ConnPool() + stream = TestStream(b'foobar') + stream.msg = {} + response = http.ResponseBody(stream, conn_pool, 'a', 'b') + + response.read(10) # read more than stream has. close() is called + response.read() # steam ended. another close() call + + self.assertEqual(conn_pool.value, 1) + + def test_double_iteration_over_same_response_body(self): + class TestHttpResp(object): + msg = {'transfer-encoding': 'chunked'} + def __init__(self, fp): + self.fp = fp + def close(self): + pass + def isclosed(self): + return len(self.fp.getvalue()) == self.fp.tell() + + data = b'foobarbaz' + data = b'\n'.join([hex(len(data))[2:].encode('utf-8'), data]) + response = http.ResponseBody(TestHttpResp(util.StringIO(data)), + None, None, None) + self.assertEqual(list(response.iterchunks()), [b'foobarbaz']) + self.assertEqual(list(response.iterchunks()), []) + + +class CacheTestCase(testutil.TempDatabaseMixin, unittest.TestCase): + + def test_remove_miss(self): + """Check that a cache remove miss is handled gracefully.""" + url = 'http://localhost:5984/foo' + cache = http.Cache() + cache.put(url, (None, None, None)) + cache.remove(url) + cache.remove(url) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(testutil.doctest_suite(http)) + suite.addTest(unittest.makeSuite(SessionTestCase, 'test')) + suite.addTest(unittest.makeSuite(ResponseBodyTestCase, 'test')) + suite.addTest(unittest.makeSuite(CacheTestCase, 'test')) + return suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff -Nru python-couchdb-0.8/couchdb/tests/couch_tests.py python-couchdb-0.10/couchdb/tests/couch_tests.py --- python-couchdb-0.8/couchdb/tests/couch_tests.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/couch_tests.py 2014-07-15 06:59:19.000000000 +0000 @@ -223,7 +223,7 @@ } }""" rows = iter(self.db.query(query)) - self.assertEqual(None, rows.next().value) + self.assertEqual(None, next(rows).value) for idx, row in enumerate(rows): self.assertEqual(values[idx + 1], row.key) diff -Nru python-couchdb-0.8/couchdb/tests/design.py python-couchdb-0.10/couchdb/tests/design.py --- python-couchdb-0.8/couchdb/tests/design.py 2010-06-02 08:09:31.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/design.py 2014-07-15 06:59:19.000000000 +0000 @@ -6,15 +6,52 @@ # 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 +from couchdb.tests import testutil + + +class DesignTestCase(testutil.TempDatabaseMixin, unittest.TestCase): + + def test_options(self): + options = {'collation': 'raw'} + view = design.ViewDefinition( + 'foo', 'foo', + 'function(doc) {emit(doc._id, doc._rev)}', + options=options) + _, db = self.temp_db() + view.sync(db) + design_doc = db.get('_design/foo') + self.assertTrue(design_doc['views']['foo']['options'] == options) + + def test_retrieve_view_defn(self): + '''see issue 183''' + view_def = design.ViewDefinition('foo', 'bar', 'baz') + result = view_def.sync(self.db) + self.assertTrue(isinstance(result, list)) + self.assertEqual(result[0][0], True) + self.assertEqual(result[0][1], '_design/foo') + doc = self.db[result[0][1]] + self.assertEqual(result[0][2], doc['_rev']) + + def test_sync_many(self): + '''see issue 218''' + func = 'function(doc) { emit(doc._id, doc._rev); }' + first_view = design.ViewDefinition('design_doc', 'view_one', func) + second_view = design.ViewDefinition('design_doc_two', 'view_one', func) + third_view = design.ViewDefinition('design_doc', 'view_two', func) + _, db = self.temp_db() + results = design.ViewDefinition.sync_many( + db, (first_view, second_view, third_view)) + self.assertEqual( + len(results), 2, 'There should only be two design documents') def suite(): suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(design)) + suite.addTest(unittest.makeSuite(DesignTestCase)) + suite.addTest(testutil.doctest_suite(design)) return suite diff -Nru python-couchdb-0.8/couchdb/tests/http.py python-couchdb-0.10/couchdb/tests/http.py --- python-couchdb-0.8/couchdb/tests/http.py 2010-06-02 08:09:31.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/http.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,22 +0,0 @@ -# -*- 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') diff -Nru python-couchdb-0.8/couchdb/tests/__init__.py python-couchdb-0.10/couchdb/tests/__init__.py --- python-couchdb-0.8/couchdb/tests/__init__.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/__init__.py 2014-07-15 06:59:19.000000000 +0000 @@ -1,29 +0,0 @@ -# -*- 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') diff -Nru python-couchdb-0.8/couchdb/tests/__main__.py python-couchdb-0.10/couchdb/tests/__main__.py --- python-couchdb-0.8/couchdb/tests/__main__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/__main__.py 2014-07-15 06:59:19.000000000 +0000 @@ -0,0 +1,30 @@ +# -*- 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, couchhttp, \ + multipart, mapping, view, package, tools + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(client.suite()) + suite.addTest(design.suite()) + suite.addTest(couchhttp.suite()) + suite.addTest(multipart.suite()) + suite.addTest(mapping.suite()) + suite.addTest(view.suite()) + suite.addTest(couch_tests.suite()) + suite.addTest(package.suite()) + suite.addTest(tools.suite()) + return suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff -Nru python-couchdb-0.8/couchdb/tests/mapping.py python-couchdb-0.10/couchdb/tests/mapping.py --- python-couchdb-0.8/couchdb/tests/mapping.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/mapping.py 2014-07-15 06:59:19.000000000 +0000 @@ -7,7 +7,6 @@ # you should have received as part of this distribution. from decimal import Decimal -import doctest import unittest from couchdb import design, mapping @@ -59,7 +58,7 @@ try: post.id = 'foo_bar' self.fail('Excepted AttributeError') - except AttributeError, e: + except AttributeError as e: self.assertEqual('id can only be set on new documents', e.args[0]) def test_batch_update(self): @@ -82,7 +81,13 @@ def test_old_datetime(self): dt = mapping.DateTimeField() - assert dt._to_python(u'1880-01-01T00:00:00Z') + assert dt._to_python('1880-01-01T00:00:00Z') + + def test_get_has_default(self): + doc = mapping.Document() + doc.get('foo') + doc.get('foo', None) + class ListFieldTestCase(testutil.TempDatabaseMixin, unittest.TestCase): @@ -195,7 +200,7 @@ self.assertEqual(thing.numbers[4], Decimal('4.0')) self.assertEqual(len(thing.numbers), 5) del thing.numbers[3:] - self.assertEquals(len(thing.numbers), 3) + self.assertEqual(len(thing.numbers), 3) def test_mutable_fields(self): class Thing(mapping.Document): @@ -225,29 +230,29 @@ 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) + self.assertEqual(type(results.rows[0]), self.Item) results = self.Item.without_include_docs(self.db) - self.assertEquals(type(results.rows[0]), self.Item) + self.assertEqual(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) + self.assertEqual(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) + self.assertEqual(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) + self.assertEqual(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) + self.assertEqual(type(results.rows[0]), self.Item) def suite(): suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(mapping)) + suite.addTest(testutil.doctest_suite(mapping)) suite.addTest(unittest.makeSuite(DocumentTestCase, 'test')) suite.addTest(unittest.makeSuite(ListFieldTestCase, 'test')) suite.addTest(unittest.makeSuite(WrappingTestCase, 'test')) diff -Nru python-couchdb-0.8/couchdb/tests/multipart.py python-couchdb-0.10/couchdb/tests/multipart.py --- python-couchdb-0.8/couchdb/tests/multipart.py 2010-06-02 14:02:32.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/multipart.py 2014-07-15 06:59:19.000000000 +0000 @@ -6,17 +6,16 @@ # 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 - +from couchdb.util import StringIO +from couchdb.tests import testutil class ReadMultipartTestCase(unittest.TestCase): def test_flat(self): - text = '''\ + text = b'''\ Content-Type: multipart/mixed; boundary="===============1946781859==" --===============1946781859== @@ -48,18 +47,18 @@ 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) + self.assertEqual(b'{\n "_id": "bar",\n ' + b'"_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) + self.assertEqual(b'{\n "_id": "foo",\n "_rev": "1-2182689334",' + b'\n "something": "cool"\n}', payload) num += 1 self.assertEqual(num, 2) def test_nested(self): - text = '''\ + text = b'''\ Content-Type: multipart/mixed; boundary="===============1946781859==" --===============1946781859== @@ -112,8 +111,8 @@ 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) + self.assertEqual(b'{\n "_id": "bar", \n ' + b'"_rev": "1-4229094393"\n}', payload) elif num == 1: self.assertEqual(is_multipart, True) self.assertEqual('foo', headers['content-id']) @@ -125,14 +124,14 @@ if partnum == 0: self.assertEqual('application/json', headers['content-type']) - self.assertEqual('{\n "_id": "foo", \n "_rev": ' - '"1-919589747", \n "something": ' - '"cool"\n}', payload) + self.assertEqual(b'{\n "_id": "foo", \n "_rev": ' + b'"1-919589747", \n "something": ' + b'"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) + self.assertEqual(b'Hello, friends.\nHow are you doing?' + b'\n\nRegards, Chris', payload) partnum += 1 @@ -141,13 +140,30 @@ 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) + self.assertEqual(b'{\n "_id": "baz", \n ' + b'"_rev": "1-3482142493"\n}', payload) num += 1 self.assertEqual(num, 3) + def test_unicode_headers(self): + # http://code.google.com/p/couchdb-python/issues/detail?id=179 + dump = u'''Content-Type: multipart/mixed; boundary="==123456789==" + +--==123456789== +Content-ID: =?utf-8?b?5paH5qGj?= +Content-Length: 63 +Content-MD5: Cpw3iC3xPua8YzKeWLzwvw== +Content-Type: application/json + +{"_rev": "3-bc27b6930ca514527d8954c7c43e6a09", "_id": "文档"} +''' + parts = multipart.read_multipart(StringIO(dump.encode('utf-8'))) + for headers, is_multipart, payload in parts: + self.assertEqual(headers['content-id'], u'文档') + break + class WriteMultipartTestCase(unittest.TestCase): @@ -156,7 +172,7 @@ 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==" + self.assertEqual(u'''Content-Type: multipart/mixed; boundary="==123456789==" --==123456789== Content-Length: 27 @@ -165,7 +181,7 @@ Iñtërnâtiônàlizætiøn --==123456789==-- -''', buf.getvalue().replace('\r\n', '\n')) +'''.encode('utf-8'), buf.getvalue().replace(b'\r\n', b'\n')) def test_unicode_content_ascii(self): buf = StringIO() @@ -173,10 +189,29 @@ self.assertRaises(UnicodeEncodeError, envelope.add, 'text/plain;charset=ascii', u'Iñtërnâtiônàlizætiøn') + def test_unicode_headers(self): + # http://code.google.com/p/couchdb-python/issues/detail?id=179 + buf = StringIO() + envelope = multipart.write_multipart(buf, boundary='==123456789==') + envelope.add('application/json', + '{"_rev": "3-bc27b6930ca514527d8954c7c43e6a09",' + ' "_id": "文档"}', + headers={'Content-ID': u"文档"}) + self.assertEqual(u'''Content-Type: multipart/mixed; boundary="==123456789==" + +--==123456789== +Content-ID: =?utf-8?b?5paH5qGj?= +Content-Length: 63 +Content-MD5: Cpw3iC3xPua8YzKeWLzwvw== +Content-Type: application/json;charset=utf-8 + +{"_rev": "3-bc27b6930ca514527d8954c7c43e6a09", "_id": "文档"} +'''.encode('utf-8'), buf.getvalue().replace(b'\r\n', b'\n')) + def suite(): suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(multipart)) + suite.addTest(testutil.doctest_suite(multipart)) suite.addTest(unittest.makeSuite(ReadMultipartTestCase, 'test')) suite.addTest(unittest.makeSuite(WriteMultipartTestCase, 'test')) return suite diff -Nru python-couchdb-0.8/couchdb/tests/testutil.py python-couchdb-0.10/couchdb/tests/testutil.py --- python-couchdb-0.8/couchdb/tests/testutil.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/testutil.py 2014-07-15 06:59:19.000000000 +0000 @@ -6,10 +6,23 @@ # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. +import doctest import random +import re import sys + from couchdb import client +class Py23DocChecker(doctest.OutputChecker): + def check_output(self, want, got, optionflags): + if sys.version_info[0] > 2: + want = re.sub("u'(.*?)'", "'\\1'", want) + want = re.sub('u"(.*?)"', '"\\1"', want) + return doctest.OutputChecker.check_output(self, want, got, optionflags) + +def doctest_suite(mod): + return doctest.DocTestSuite(mod, checker=Py23DocChecker()) + class TempDatabaseMixin(object): temp_dbs = None @@ -28,10 +41,9 @@ self.temp_dbs = {} # Find an unused database name while True: - name = 'couchdb-python/%d' % random.randint(0, sys.maxint) + name = 'couchdb-python/%d' % random.randint(0, sys.maxsize) 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 diff -Nru python-couchdb-0.8/couchdb/tests/tools.py python-couchdb-0.10/couchdb/tests/tools.py --- python-couchdb-0.8/couchdb/tests/tools.py 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/tools.py 2014-07-15 06:59:19.000000000 +0000 @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2012 Alexander Shorin +# 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.util import StringIO +from couchdb import Unauthorized +from couchdb.tools import load, dump +from couchdb.tests import testutil + + +class ToolLoadTestCase(testutil.TempDatabaseMixin, unittest.TestCase): + + def test_handle_credentials(self): + # Issue 194: couchdb-load attribute error: 'Resource' object has no attribute 'http' + # http://code.google.com/p/couchdb-python/issues/detail?id=194 + load.load_db(StringIO(b''), self.db.resource.url, 'foo', 'bar') + + +class ToolDumpTestCase(testutil.TempDatabaseMixin, unittest.TestCase): + + def test_handle_credentials(self): + # Similar to issue 194 + # Fixing: AttributeError: 'Resource' object has no attribute 'http' + try: + dump.dump_db(self.db.resource.url, 'foo', 'bar', output=StringIO()) + except Unauthorized: + # This is ok, since we provided dummy credentials. + pass + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ToolLoadTestCase, 'test')) + return suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') + diff -Nru python-couchdb-0.8/couchdb/tests/view.py python-couchdb-0.10/couchdb/tests/view.py --- python-couchdb-0.8/couchdb/tests/view.py 2010-06-02 14:02:32.000000000 +0000 +++ python-couchdb-0.10/couchdb/tests/view.py 2014-07-15 06:59:19.000000000 +0000 @@ -6,98 +6,106 @@ # 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.util import StringIO from couchdb import view +from couchdb.tests import testutil class ViewServerTestCase(unittest.TestCase): def test_reset(self): - input = StringIO('["reset"]\n') + input = StringIO(b'["reset"]\n') output = StringIO() view.run(input=input, output=output) - self.assertEquals(output.getvalue(), 'true\n') + self.assertEqual(output.getvalue(), b'true\n') def test_add_fun(self): - input = StringIO('["add_fun", "def fun(doc): yield None, doc"]\n') + input = StringIO(b'["add_fun", "def fun(doc): yield None, doc"]\n') output = StringIO() view.run(input=input, output=output) - self.assertEquals(output.getvalue(), 'true\n') + self.assertEqual(output.getvalue(), b'true\n') def test_map_doc(self): - input = StringIO('["add_fun", "def fun(doc): yield None, doc"]\n' - '["map_doc", {"foo": "bar"}]\n') + input = StringIO(b'["add_fun", "def fun(doc): yield None, doc"]\n' + b'["map_doc", {"foo": "bar"}]\n') output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), - 'true\n' - '[[[null, {"foo": "bar"}]]]\n') + b'true\n' + b'[[[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') + input = StringIO(b'["add_fun", "def fun(doc): yield doc[\\"test\\"], doc"]\n' + b'["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') + b'true\n' + b'[[["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) + fun = b'def fun(doc): log(\'running\'); yield None, doc' + input = StringIO(b'["add_fun", "' + fun + b'"]\n' + b'["map_doc", {"foo": "bar"}]\n') output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), - 'true\n' - '{"log": "running"}\n' - '[[[null, {"foo": "bar"}]]]\n') + b'true\n' + b'{"log": "running"}\n' + b'[[[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) + fun = b'def fun(doc): log([1, 2, 3]); yield None, doc' + input = StringIO(b'["add_fun", "' + fun + b'"]\n' + b'["map_doc", {"foo": "bar"}]\n') output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), - 'true\n' - '{"log": "[1, 2, 3]"}\n' - '[[[null, {"foo": "bar"}]]]\n') + b'true\n' + b'{"log": "[1, 2, 3]"}\n' + b'[[[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') + input = StringIO(b'["reduce", ' + b'["def fun(keys, values): return sum(values)"], ' + b'[[null, 1], [null, 2], [null, 3]]]\n') output = StringIO() view.run(input=input, output=output) - self.assertEqual(output.getvalue(), '[true, [6]]\n') + self.assertEqual(output.getvalue(), b'[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') + input = StringIO(b'["reduce", ' + b'["def fun(keys, values): log(\'Summing %r\' % (values,)); return sum(values)"], ' + b'[[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') + b'{"log": "Summing (1, 2, 3)"}\n' + b'[true, [6]]\n') def test_rereduce(self): - input = StringIO('["rereduce", ' - '["def fun(keys, values, rereduce): return sum(values)"], ' - '[1, 2, 3]]\n') + input = StringIO(b'["rereduce", ' + b'["def fun(keys, values, rereduce): return sum(values)"], ' + b'[1, 2, 3]]\n') output = StringIO() view.run(input=input, output=output) - self.assertEqual(output.getvalue(), '[true, [6]]\n') + self.assertEqual(output.getvalue(), b'[true, [6]]\n') + def test_reduce_empty(self): + input = StringIO(b'["reduce", ' + b'["def fun(keys, values): return sum(values)"], ' + b'[]]\n') + output = StringIO() + view.run(input=input, output=output) + self.assertEqual(output.getvalue(), + b'[true, [0]]\n') def suite(): suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(view)) + suite.addTest(testutil.doctest_suite(view)) suite.addTest(unittest.makeSuite(ViewServerTestCase, 'test')) return suite diff -Nru python-couchdb-0.8/couchdb/tools/dump.py python-couchdb-0.10/couchdb/tools/dump.py --- python-couchdb-0.8/couchdb/tools/dump.py 2010-06-02 14:02:32.000000000 +0000 +++ python-couchdb-0.10/couchdb/tools/dump.py 2014-07-15 06:59:19.000000000 +0000 @@ -20,17 +20,11 @@ from couchdb.client import Database from couchdb.multipart import write_multipart +BULK_SIZE = 1000 -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) +def dump_docs(envelope, docs): + for doc in docs: - 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) @@ -57,6 +51,20 @@ 'ETag': '"%s"' % doc.rev }) +def dump_db(dburl, username=None, password=None, boundary=None, + output=sys.stdout, bulk_size=BULK_SIZE): + + db = Database(dburl) + if username is not None and password is not None: + db.resource.credentials = username, password + + envelope = write_multipart(output, boundary=boundary) + start, num = 0, db.info()['doc_count'] + while start < num: + opts = {'limit': bulk_size, 'skip': start, 'include_docs': True} + dump_docs(envelope, [row.doc for row in db.view('_all_docs', **opts)]) + start += bulk_size + envelope.close() @@ -69,6 +77,9 @@ help='the username to use for authentication') parser.add_option('-p', '--password', action='store', dest='password', help='the password to use for authentication') + parser.add_option('-b', '--bulk-size', action='store', dest='bulk_size', + type='int', default=BULK_SIZE, + help='number of docs retrieved from database') parser.set_defaults() options, args = parser.parse_args() @@ -78,7 +89,8 @@ if options.json_module: json.use(options.json_module) - dump_db(args[0], username=options.username, password=options.password) + dump_db(args[0], username=options.username, password=options.password, + bulk_size=options.bulk_size) if __name__ == '__main__': diff -Nru python-couchdb-0.8/couchdb/tools/load.py python-couchdb-0.10/couchdb/tools/load.py --- python-couchdb-0.8/couchdb/tools/load.py 2010-06-02 14:02:32.000000000 +0000 +++ python-couchdb-0.10/couchdb/tools/load.py 2014-07-15 06:59:19.000000000 +0000 @@ -24,7 +24,7 @@ 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) + db.resource.credentials = (username, password) for headers, is_multipart, payload in read_multipart(fileobj): docid = headers['content-id'] @@ -48,7 +48,7 @@ print>>sys.stderr, 'Loading document %r' % docid try: db[docid] = doc - except Exception, e: + except Exception as e: if not ignore_errors: raise print>>sys.stderr, 'Error: %s' % e diff -Nru python-couchdb-0.8/couchdb/tools/replicate.py python-couchdb-0.10/couchdb/tools/replicate.py --- python-couchdb-0.8/couchdb/tools/replicate.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/couchdb/tools/replicate.py 2014-07-15 06:59:19.000000000 +0000 @@ -17,12 +17,10 @@ Use 'python replicate.py --help' to get more detailed usage instructions. """ -from couchdb import http, client +from couchdb import http, client, util import optparse import sys import time -import urllib -import urlparse import fnmatch def findpath(parser, s): @@ -33,7 +31,7 @@ if not s.startswith('http'): return client.DEFAULT_BASE_URL, s - bits = urlparse.urlparse(s) + bits = util.urlparse(s) res = http.Resource('%s://%s/' % (bits.scheme, bits.netloc), None) parts = bits.path.split('/')[1:] if parts and not parts[-1]: @@ -83,41 +81,46 @@ 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: + sources = [i for i in all if fnmatch.fnmatchcase(i, spath)] + if not sources: raise parser.error("no source databases match glob '%s'" % spath) + if len(sources) > 1 and tpath: + raise parser.error('target path must be empty with multiple sources') + elif len(sources) == 1: + databases = [(sources[0], tpath)] + else: + databases = [(i, i) for i in sources] + # do the actual replication for sdb, tdb in databases: start = time.time() - print sdb, '->', tdb, + print(sdb, '->', tdb) sys.stdout.flush() if tdb not in target: target.create(tdb) - print "created", + sys.stdout.write("created") sys.stdout.flush() - sdb = '%s%s' % (sbase, urllib.quote(sdb, '')) + sdb = '%s%s' % (sbase, util.urlquote(sdb, '')) if options.continuous: target.replicate(sdb, tdb, continuous=options.continuous) else: target.replicate(sdb, tdb) - print '%.1fs' % (time.time() - start) + print('%.1fs' % (time.time() - start)) sys.stdout.flush() if options.compact: for (sdb, tdb) in databases: - print 'compact', tdb + print('compact', tdb) target[tdb].compact() if __name__ == '__main__': diff -Nru python-couchdb-0.8/couchdb/util2.py python-couchdb-0.10/couchdb/util2.py --- python-couchdb-0.8/couchdb/util2.py 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/couchdb/util2.py 2014-07-15 06:59:19.000000000 +0000 @@ -0,0 +1,22 @@ + +__all__ = [ + 'StringIO', 'urlsplit', 'urlunsplit', 'urlquote', 'urlunquote', + 'urlencode', 'utype', 'ltype', 'pyexec', 'strbase', 'funcode', + 'urlparse', +] + +utype = unicode +ltype = long +strbase = str, bytes, unicode + +from io import BytesIO as StringIO +from urlparse import urlparse, urlsplit, urlunsplit +from urllib import quote as urlquote +from urllib import unquote as urlunquote +from urllib import urlencode + +def pyexec(code, gns, lns): + exec code in gns, lns + +def funcode(fun): + return fun.func_code diff -Nru python-couchdb-0.8/couchdb/util3.py python-couchdb-0.10/couchdb/util3.py --- python-couchdb-0.8/couchdb/util3.py 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/couchdb/util3.py 2014-07-15 06:59:19.000000000 +0000 @@ -0,0 +1,20 @@ + +__all__ = [ + 'StringIO', 'urlsplit', 'urlunsplit', 'urlquote', 'urlunquote', + 'urlencode', 'utype', 'ltype', 'pyexec', 'strbase', 'funcode', + 'urlparse', +] + +utype = str +ltype = int +strbase = str, bytes + +from io import BytesIO as StringIO +from urllib.parse import urlsplit, urlunsplit, urlencode, urlparse +from urllib.parse import quote as urlquote +from urllib.parse import unquote as urlunquote + +pyexec = exec + +def funcode(fun): + return fun.__code__ diff -Nru python-couchdb-0.8/couchdb/util.py python-couchdb-0.10/couchdb/util.py --- python-couchdb-0.8/couchdb/util.py 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/couchdb/util.py 2014-07-15 06:59:19.000000000 +0000 @@ -0,0 +1,6 @@ +import sys + +if sys.version_info[0] < 3: + from couchdb.util2 import * +else: + from couchdb.util3 import * diff -Nru python-couchdb-0.8/couchdb/view.py python-couchdb-0.10/couchdb/view.py --- python-couchdb-0.8/couchdb/view.py 2010-06-06 16:38:53.000000000 +0000 +++ python-couchdb-0.10/couchdb/view.py 2014-07-15 06:59:19.000000000 +0000 @@ -16,7 +16,7 @@ import traceback from types import FunctionType -from couchdb import json +from couchdb import json, util __all__ = ['main', 'run'] __docformat__ = 'restructuredtext en' @@ -34,14 +34,14 @@ def _writejson(obj): obj = json.encode(obj) - if isinstance(obj, unicode): + if isinstance(obj, util.utype): obj = obj.encode('utf-8') output.write(obj) - output.write('\n') + output.write(b'\n') output.flush() def _log(message): - if not isinstance(message, basestring): + if not isinstance(message, util.strbase): message = json.encode(message) _writejson({'log': message}) @@ -53,8 +53,8 @@ string = BOM_UTF8 + string.encode('utf-8') globals_ = {} try: - exec string in {'log': _log}, globals_ - except Exception, e: + util.pyexec(string, {'log': _log}, globals_) + except Exception as e: return {'error': { 'id': 'map_compilation_error', 'reason': e.args[0] @@ -66,7 +66,7 @@ }} if len(globals_) != 1: return err - function = globals_.values()[0] + function = list(globals_.values())[0] if type(function) is not FunctionType: return err functions.append(function) @@ -77,7 +77,7 @@ for function in functions: try: results.append([[key, value] for key, value in function(doc)]) - except Exception, e: + except Exception as e: log.error('runtime error in map function: %s', e, exc_info=True) results.append([]) @@ -89,8 +89,8 @@ args = cmd[1] globals_ = {} try: - exec code in {'log': _log}, globals_ - except Exception, e: + util.pyexec(code, {'log': _log}, globals_) + except Exception as e: log.error('runtime error in reduce function: %s', e, exc_info=True) return {'error': { @@ -104,7 +104,7 @@ }} if len(globals_) != 1: return err - function = globals_.values()[0] + function = list(globals_.values())[0] if type(function) is not FunctionType: return err @@ -114,8 +114,11 @@ keys = None vals = args else: - keys, vals = zip(*args) - if function.func_code.co_argcount == 3: + if args: + keys, vals = zip(*args) + else: + keys, vals = [], [] + if util.funcode(function).co_argcount == 3: results = function(keys, vals, rereduce) else: results = function(keys, vals) @@ -136,7 +139,7 @@ try: cmd = json.decode(line) log.debug('Processing %r', cmd) - except ValueError, e: + except ValueError as e: log.error('Error: %s', e, exc_info=True) return 1 else: @@ -145,7 +148,7 @@ _writejson(retval) except KeyboardInterrupt: return 0 - except Exception, e: + except Exception as e: log.error('Error: %s', e, exc_info=True) return 1 @@ -215,7 +218,7 @@ sys.stdout.flush() sys.exit(0) - except getopt.GetoptError, error: + except getopt.GetoptError as error: message = '%s\n\nTry `%s --help` for more information.\n' % ( str(error), os.path.basename(sys.argv[0]) ) diff -Nru python-couchdb-0.8/CouchDB.egg-info/PKG-INFO python-couchdb-0.10/CouchDB.egg-info/PKG-INFO --- python-couchdb-0.8/CouchDB.egg-info/PKG-INFO 2010-08-13 12:03:54.000000000 +0000 +++ python-couchdb-0.10/CouchDB.egg-info/PKG-INFO 2014-07-15 18:01:00.000000000 +0000 @@ -1,8 +1,8 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: CouchDB -Version: 0.8 +Version: 0.10 Summary: Python library for working with CouchDB -Home-page: http://code.google.com/p/couchdb-python/ +Home-page: https://github.com/djc/couchdb-python/ Author: Christopher Lenz Author-email: cmlenz@gmx.de License: BSD @@ -13,6 +13,10 @@ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Database :: Front-Ends Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -Nru python-couchdb-0.8/CouchDB.egg-info/SOURCES.txt python-couchdb-0.10/CouchDB.egg-info/SOURCES.txt --- python-couchdb-0.8/CouchDB.egg-info/SOURCES.txt 2010-08-13 12:03:54.000000000 +0000 +++ python-couchdb-0.10/CouchDB.egg-info/SOURCES.txt 2014-07-15 18:01:00.000000000 +0000 @@ -1,8 +1,8 @@ COPYING -ChangeLog.txt +ChangeLog.rst MANIFEST.in Makefile -README.txt +README.rst setup.cfg setup.py CouchDB.egg-info/PKG-INFO @@ -18,16 +18,21 @@ couchdb/json.py couchdb/mapping.py couchdb/multipart.py +couchdb/util.py +couchdb/util2.py +couchdb/util3.py couchdb/view.py couchdb/tests/__init__.py +couchdb/tests/__main__.py couchdb/tests/client.py couchdb/tests/couch_tests.py +couchdb/tests/couchhttp.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/tools.py couchdb/tests/view.py couchdb/tools/__init__.py couchdb/tools/dump.py diff -Nru python-couchdb-0.8/debian/changelog python-couchdb-0.10/debian/changelog --- python-couchdb-0.8/debian/changelog 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/debian/changelog 2014-11-13 06:18:46.000000000 +0000 @@ -1,22 +1,48 @@ -python-couchdb (0.8-0ubuntu2) precise; urgency=low +python-couchdb (0.10-1.1) unstable; urgency=medium - * Build using dh_python2 + * Non-maintainer upload. + * Add debian/patches/util3_pycompile.diff to disable unused pyexec and + fix install failure during pycompile (Closes: #765086) + - Patch thanks to Michael Vogt + + -- Scott Kitterman Thu, 13 Nov 2014 01:17:25 -0500 + +python-couchdb (0.10-1) unstable; urgency=medium + + * New upstream version + * Fix typo in debian/rules (Closes: #750225) + * Standards-Version bumped to 3.9.6, no changes needed + * Bumped debhelper compatibility to 9 + * Make canonical Vcs-* urls + * Patches refreshed + * Updated debian/copyright + * Use dh_python2 instead of pysupport + + -- David Paleino Sun, 28 Sep 2014 18:14:29 +0200 + +python-couchdb (0.8-1) unstable; urgency=low + + * New upstream version (Closes: #616354) + * Package adopted. + * Package moved to collab-maint, git. + * Patches converted to quilt-format, added DEP-3 header. + * Patches refreshed, 02-python2.5_compatibility.patch removed (httplib2 + not used anymore) + * Use "3.0 (quilt)" source format. + * Add XS-Python-Version to debian/control. + * debian/rules rewritten using dh7 + * debhelper compatibility level bumped to 8 + * Added myself to debian/copyright + * Standards-Version bumped to 3.9.2 + * Removed dependencies on python-httplib2 + * Switch from epydoc to sphinx documentation build + * Added doc-base hint + * Don't point to the BSD file in /usr/share/common-licenses/, but + rather include the right text + * Use system-wide jQuery instead of the embedded one + * Enable view server in CouchDB (Closes: #554580) - -- Matthias Klose Sat, 17 Dec 2011 19:23:21 +0000 - -python-couchdb (0.8-0ubuntu1) natty; urgency=low - - [Chad Miller] - * New upstream release. - * Remove debian/patches/improve-bin-scripts.patch and fix up debian/rules - and generate man pages from actual scripts. - * Include replication tool in binaries, and poke version into man page. - * Remove debian/patches/python2.5_compatibility.patch . - * Use explicit BSD license to placate lintian. - * Fix broken build-docs rule. Add 'python-sphinx' as build-dep. - * Add constraint so we don't break older desktopcouch. - - -- Chad MILLER Wed, 12 Jan 2011 15:01:31 -0600 + -- David Paleino Mon, 18 Apr 2011 12:42:00 +0200 python-couchdb (0.6-1) unstable; urgency=low diff -Nru python-couchdb-0.8/debian/clean python-couchdb-0.10/debian/clean --- python-couchdb-0.8/debian/clean 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/clean 2014-09-28 16:14:36.000000000 +0000 @@ -0,0 +1,2 @@ +*.1 +couchdb/tools/__init__.py diff -Nru python-couchdb-0.8/debian/compat python-couchdb-0.10/debian/compat --- python-couchdb-0.8/debian/compat 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/debian/compat 2014-09-28 16:14:36.000000000 +0000 @@ -1 +1 @@ -5 +9 diff -Nru python-couchdb-0.8/debian/control python-couchdb-0.10/debian/control --- python-couchdb-0.8/debian/control 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/debian/control 2014-09-28 16:14:36.000000000 +0000 @@ -1,22 +1,32 @@ Source: python-couchdb Section: python Priority: optional -Maintainer: Noah Slater -Uploaders: Debian Python Modules Team -Standards-Version: 3.8.3 -Build-Depends: cdbs (>= 0.4.42), debhelper (>= 7.2.11), python -Build-Depends-Indep: help2man, python-docutils, python-epydoc, - python-sphinx, python-simplejson, python-setuptools (>= 0.6b3) +Maintainer: David Paleino +Build-Depends: + debhelper (>= 9~) + , python +Build-Depends-Indep: + help2man + , python-sphinx + , python-pygments + , python-simplejson + , python-setuptools (>= 0.6b3) + , dh-python +Standards-Version: 3.9.6 +XS-Python-Version: >= 2.5 Homepage: http://pypi.python.org/pypi/CouchDB -Vcs-Svn: svn://svn.debian.org/python-modules/packages/python-couchdb/trunk/ -Vcs-Browser: http://svn.debian.org/viewsvn/python-modules/packages/python-couchdb/trunk/ +Vcs-Git: git://anonscm.debian.org/collab-maint/python-couchdb.git +Vcs-Browser: http://anonscm.debian.org/cgit/collab-maint/python-couchdb.git Package: python-couchdb Architecture: all -Depends: ${python:Depends}, ${misc:Depends}, - python-simplejson | python (>= 2.6) | python-cjson +Depends: + ${python:Depends} + , ${misc:Depends} + , python-simplejson | python (>= 2.6) | python-cjson + , libjs-jquery + , libjs-underscore Suggests: couchdb -Breaks: desktopcouch (<< 1.0) Description: library for working with Apache CouchDB Provides a high-level client library for Apache CouchDB, a view server and dump and load utilities that can be used as migration tools when upgrading or moving diff -Nru python-couchdb-0.8/debian/copyright python-couchdb-0.10/debian/copyright --- python-couchdb-0.8/debian/copyright 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/debian/copyright 2014-09-28 16:14:36.000000000 +0000 @@ -1,11 +1,21 @@ -Format-Specification: http://wiki.debian.org/Proposals/CopyrightFormat?action=recall&rev=180 +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: CouchDB Upstream-Maintainer: Christopher Lenz Upstream-Source: http://pypi.python.org/pypi/CouchDB Files: * -Copyright: Copyright 2007-2009, Christopher Lenz - Copyright 2007-2009, Jan lehnardt +Copyright: © 2007-2009, Christopher Lenz + © 2007-2009, Jan lehnardt +License: BSD-3 + +Files: debian/* +Copyright: © 2009, Noah Slater + © 2011-2014, David Paleino +License: GAP + Copying and distribution of this package, with or without modification, are + permitted in any medium without royalty provided the copyright notice and this + notice are preserved. + License: BSD-3 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions @@ -21,21 +31,14 @@ 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. - -Files: debian/* -Copyright: Copyright 2009, Noah Slater -License: GAP - Copying and distribution of this package, with or without modification, are - permitted in any medium without royalty provided the copyright notice and this - notice are preserved. + 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. diff -Nru python-couchdb-0.8/debian/doc-base python-couchdb-0.10/debian/doc-base --- python-couchdb-0.8/debian/doc-base 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/doc-base 2014-09-28 16:14:36.000000000 +0000 @@ -0,0 +1,7 @@ +Document: python-couchdb +Title: Python CouchDB Documentation +Section: Programming/Python + +Format: HTML +Index: /usr/share/doc/python-couchdb/html/index.html +Files: /usr/share/doc/python-couchdb/html/*.html diff -Nru python-couchdb-0.8/debian/docs python-couchdb-0.10/debian/docs --- python-couchdb-0.8/debian/docs 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/debian/docs 2014-09-28 16:14:36.000000000 +0000 @@ -1,2 +1,2 @@ -doc/*.* -README.txt +doc/build/html/ +README.rst diff -Nru python-couchdb-0.8/debian/extra/python-couchdb python-couchdb-0.10/debian/extra/python-couchdb --- python-couchdb-0.8/debian/extra/python-couchdb 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/extra/python-couchdb 2014-09-28 16:14:36.000000000 +0000 @@ -0,0 +1,2 @@ +[query_servers] +python=/usr/bin/couchpy diff -Nru python-couchdb-0.8/debian/install python-couchdb-0.10/debian/install --- python-couchdb-0.8/debian/install 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/install 2014-09-28 16:14:36.000000000 +0000 @@ -0,0 +1,5 @@ +couchdb-dump usr/bin/ +couchdb-load usr/bin/ +couchpy usr/bin/ + +debian/extra/python-couchdb etc/couchdb/default.d/ diff -Nru python-couchdb-0.8/debian/links python-couchdb-0.10/debian/links --- python-couchdb-0.8/debian/links 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/links 2014-09-28 16:14:36.000000000 +0000 @@ -0,0 +1,2 @@ +/usr/share/javascript/jquery/jquery.js usr/share/doc/python-couchdb/html/_static/jquery.js +/usr/share/javascript/underscore/underscore.js usr/share/doc/python-couchdb/html/_static/underscore.js diff -Nru python-couchdb-0.8/debian/manpages python-couchdb-0.10/debian/manpages --- python-couchdb-0.8/debian/manpages 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/debian/manpages 2014-09-28 16:14:36.000000000 +0000 @@ -1,4 +1,3 @@ couchdb-dump.1 couchdb-load.1 couchpy.1 -couchdb-replicate.1 diff -Nru python-couchdb-0.8/debian/patches/01-improve_bin_scripts.patch python-couchdb-0.10/debian/patches/01-improve_bin_scripts.patch --- python-couchdb-0.8/debian/patches/01-improve_bin_scripts.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/patches/01-improve_bin_scripts.patch 2014-09-28 16:14:36.000000000 +0000 @@ -0,0 +1,61 @@ +From: Noah Slater +Subject: remove dependency on pkg_resources +Origin: vendor + +--- + couchdb-dump | 5 +++++ + couchdb-load | 5 +++++ + couchdb-replicate | 5 +++++ + couchpy | 5 +++++ + setup.py | 8 -------- + 5 files changed, 20 insertions(+), 8 deletions(-) + +--- /dev/null ++++ python-couchdb/couchdb-dump +@@ -0,0 +1,5 @@ ++#!/usr/bin/python ++ ++from couchdb.tools import dump ++ ++dump.main() +--- /dev/null ++++ python-couchdb/couchdb-load +@@ -0,0 +1,5 @@ ++#!/usr/bin/python ++ ++from couchdb.tools import load ++ ++load.main() +--- /dev/null ++++ python-couchdb/couchdb-replicate +@@ -0,0 +1,5 @@ ++#!/usr/bin/python ++ ++from couchdb.tools import replicate ++ ++replicate.main() +--- /dev/null ++++ python-couchdb/couchpy +@@ -0,0 +1,5 @@ ++#!/usr/bin/python ++ ++from couchdb import view ++ ++view.main() +--- python-couchdb.orig/setup.py ++++ python-couchdb/setup.py +@@ -32,14 +32,6 @@ if not has_setuptools: + setuptools_options = {} + else: + setuptools_options = { +- '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', +- ], +- }, + 'install_requires': requirements, + 'test_suite': 'couchdb.tests.__main__.suite', + 'zip_safe': True, diff -Nru python-couchdb-0.8/debian/patches/03-remove_module_shebang.patch python-couchdb-0.10/debian/patches/03-remove_module_shebang.patch --- python-couchdb-0.8/debian/patches/03-remove_module_shebang.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/patches/03-remove_module_shebang.patch 2014-09-28 16:14:36.000000000 +0000 @@ -0,0 +1,16 @@ +From: Noah Slater +Subject: remove module shebang +Origin: vendor +Forwarded: not-needed + +--- + couchdb/view.py | 1 - + 1 file changed, 1 deletion(-) + +--- python-couchdb.orig/couchdb/view.py ++++ python-couchdb/couchdb/view.py +@@ -1,4 +1,3 @@ +-#!/usr/bin/env python + # -*- coding: utf-8 -*- + # + # Copyright (C) 2007-2008 Christopher Lenz diff -Nru python-couchdb-0.8/debian/patches/build-docs-with-sphinx.patch python-couchdb-0.10/debian/patches/build-docs-with-sphinx.patch --- python-couchdb-0.8/debian/patches/build-docs-with-sphinx.patch 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/debian/patches/build-docs-with-sphinx.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,76 +0,0 @@ ---- setup.py 2010-11-30 21:04:26.538646670 -0500 -+++ setup.py 2010-11-30 21:05:39.135514432 -0500 -@@ -22,69 +22,18 @@ - 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'] -+ boolean_options = [] - - def initialize_options(self): -- self.force = False -- self.without_apidocs = False -+ pass - - 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.' -+ from sphinx import main as sphinxmain -+ sphinxmain(('build-sphinx', 'doc', 'doc')) - - - class test_doc(Command): diff -Nru python-couchdb-0.8/debian/patches/correct-readme.patch python-couchdb-0.10/debian/patches/correct-readme.patch --- python-couchdb-0.8/debian/patches/correct-readme.patch 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/debian/patches/correct-readme.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ -diff -Nur python-couchdb-0.1/README.txt python-couchdb-0.1.new/README.txt ---- python-couchdb-0.1/README.txt 2007-09-23 17:18:26.000000000 +0100 -+++ python-couchdb-0.1.new/README.txt 2007-10-30 00:19:12.000000000 +0000 -@@ -5,5 +5,6 @@ - - - --Please see the files in the `doc` folder or browse the documentation online at: -+Please see the `index.html' file, files under the `api` directory, the -+docstrings in the code for module documentation, or online docs. - - - diff -Nru python-couchdb-0.8/debian/patches/remove-module-shebang.patch python-couchdb-0.10/debian/patches/remove-module-shebang.patch --- python-couchdb-0.8/debian/patches/remove-module-shebang.patch 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/debian/patches/remove-module-shebang.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -diff -Nur python-couchdb-0.1/couchdb/view.py python-couchdb-0.1.new/couchdb/view.py ---- python-couchdb-0.1/couchdb/view.py 2007-10-23 13:20:15.000000000 +0100 -+++ python-couchdb-0.1.new/couchdb/view.py 2007-10-30 22:38:26.000000000 +0000 -@@ -1,4 +1,3 @@ --#!/usr/bin/env python - # -*- coding: utf-8 -*- - # - # Copyright (C) 2007 Christopher Lenz diff -Nru python-couchdb-0.8/debian/patches/series python-couchdb-0.10/debian/patches/series --- python-couchdb-0.8/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/patches/series 2014-11-13 06:10:42.000000000 +0000 @@ -0,0 +1,3 @@ +01-improve_bin_scripts.patch +03-remove_module_shebang.patch +util3_pycompile.diff diff -Nru python-couchdb-0.8/debian/patches/util3_pycompile.diff python-couchdb-0.10/debian/patches/util3_pycompile.diff --- python-couchdb-0.8/debian/patches/util3_pycompile.diff 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/patches/util3_pycompile.diff 2014-11-13 06:16:34.000000000 +0000 @@ -0,0 +1,26 @@ +Description: Disable pyexec in util3.py to avoid pycompile issue + couchdb/util3.py is not used with python (only python3, which is not + currently supported by the package), but is still executed by pycompile. + Pycompile will attempt to compile all files in the directory on install and + the python3 only syntax used in util3.py causes it to fail. Commenting out + the pyexec works around the issue and has no effect since python3 isn't + supported. + In the long run, this should be fixed properly, but this is a reasonable +Author: Michael Vogt +Bug-Debian: http://bugs.debian.org/765086 +Origin: vendor +Forwarded: no +Reviewed-By: Scott Kitterman +Last-Update: 2014-11-13 + +--- python-couchdb-0.10.orig/couchdb/util3.py ++++ python-couchdb-0.10/couchdb/util3.py +@@ -14,7 +14,7 @@ from urllib.parse import urlsplit, urlun + from urllib.parse import quote as urlquote + from urllib.parse import unquote as urlunquote + +-pyexec = exec ++#pyexec = exec + + def funcode(fun): + return fun.__code__ diff -Nru python-couchdb-0.8/debian/pycompat python-couchdb-0.10/debian/pycompat --- python-couchdb-0.8/debian/pycompat 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/pycompat 2014-09-28 16:14:36.000000000 +0000 @@ -0,0 +1 @@ +2 diff -Nru python-couchdb-0.8/debian/rules python-couchdb-0.10/debian/rules --- python-couchdb-0.8/debian/rules 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/debian/rules 2014-09-28 16:14:36.000000000 +0000 @@ -1,43 +1,37 @@ #!/usr/bin/make -f +# -*- makefile -*- -# Copyright 2009, Noah Slater - -# Copying and distribution of this file, with or without modification, are -# permitted in any medium without royalty provided the copyright notice and this -# notice are preserved. - -include /usr/share/cdbs/1/rules/buildcore.mk -include /usr/share/cdbs/1/rules/debhelper.mk -include /usr/share/cdbs/1/class/python-distutils.mk -include /usr/share/cdbs/1/rules/simple-patchsys.mk - -DEB_PYTHON_BUILD_ARGS = build_doc - -DEB_PYTHON_INSTALL_ARGS_ALL += --single-version-externally-managed +UPVER=$(shell dpkg-parsechangelog | grep ^Version | cut -d\ -f2 | cut -d- -f1) VERSION = __import__('pkg_resources').get_distribution('CouchDB').version -# @@ workaround for #486848 -binary-arch binary-indep: build - -clean:: - rm -rf *.1 couchdb/tools/__init__.py doc/index.html doc/api - -cleanbuilddir:: - sed -i -e "s/\"$(DEB_UPSTREAM_VERSION)\"/$(VERSION)/" couchdb/__init__.py +%: + dh $@ \ + -Spython_distutils \ + --with python2 -post-patches:: - sed -i -e "s/$(VERSION)/\"$(DEB_UPSTREAM_VERSION)\"/" couchdb/__init__.py +override_dh_auto_build: + sed -i -e "s/$(VERSION)/\"$(UPVER)\"/" couchdb/__init__.py touch couchdb/tools/__init__.py + dh_auto_build -- build_sphinx + + chmod 755 couchdb-* couchpy + help2man -N -n "a CouchDB dump utility" ./couchdb-dump > couchdb-dump.1 + help2man -N -n "a CouchDB load utility" ./couchdb-load > couchdb-load.1 + help2man -N -n "a CouchDB Python view server" ./couchpy > couchpy.1 + +override_dh_auto_install: + dh_auto_install -- --single-version-externally-managed + + -rm -rf $(CURDIR)/debian/usr/share/doc/python-couchdb/html/_static/jquery.js + +override_dh_clean: + dh_clean + -rm -rf doc/build + -rm -rf CouchDB.egg-info/ -install/python-couchdb:: - chmod +x debian/python-couchdb/usr/bin/* - PYTHONPATH=$(wildcard debian/python-couchdb/usr/lib/python*/site-packages) help2man -N -n "a CouchDB dump utility" debian/python-couchdb/usr/bin/couchdb-dump > couchdb-dump.1 - PYTHONPATH=$(wildcard debian/python-couchdb/usr/lib/python*/site-packages) help2man -N -n "a CouchDB load utility" debian/python-couchdb/usr/bin/couchdb-load > couchdb-load.1 - PYTHONPATH=$(wildcard debian/python-couchdb/usr/lib/python*/site-packages) help2man -N -n "a CouchDB Python view server" debian/python-couchdb/usr/bin/couchpy > couchpy.1 - PYTHONPATH=$(wildcard debian/python-couchdb/usr/lib/python*/site-packages) help2man -N -n "a CouchDB Python replication tool" --version-string=$(DEB_UPSTREAM_VERSION) debian/python-couchdb/usr/bin/couchdb-replicate > couchdb-replicate.1 + sed -i -e "s/\"$(UPVER)\"/$(VERSION)/" couchdb/__init__.py -# @@ only works from source directory, see #494141 .PHONY: get-orig-source get-orig-source: - uscan --force-download --rename --download-version=$(DEB_UPSTREAM_VERSION) --destdir . + uscan --force-download --rename --download-version=$(UPVER) --destdir . diff -Nru python-couchdb-0.8/debian/source/format python-couchdb-0.10/debian/source/format --- python-couchdb-0.8/debian/source/format 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/debian/source/format 2014-09-28 16:14:36.000000000 +0000 @@ -0,0 +1 @@ +3.0 (quilt) diff -Nru python-couchdb-0.8/doc/changes.rst python-couchdb-0.10/doc/changes.rst --- python-couchdb-0.8/doc/changes.rst 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/doc/changes.rst 2014-07-15 17:05:18.000000000 +0000 @@ -1,4 +1,4 @@ Changes ======= -.. include:: ../ChangeLog.txt +.. include:: ../ChangeLog.rst diff -Nru python-couchdb-0.8/doc/conf.py python-couchdb-0.10/doc/conf.py --- python-couchdb-0.8/doc/conf.py 2010-08-13 11:56:42.000000000 +0000 +++ python-couchdb-0.10/doc/conf.py 2014-07-15 06:59:19.000000000 +0000 @@ -39,17 +39,17 @@ master_doc = 'index' # General information about the project. -project = u'couchdb-python' -copyright = u'2010, Dirkjan Ochtman' +project = 'couchdb-python' +copyright = '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' +version = '0.10' # The full version, including alpha/beta/rc tags. -release = '0.8' +release = '0.10' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -174,8 +174,8 @@ # 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'), + ('index', 'couchdb-python.tex', 'couchdb-python Documentation', + 'Dirkjan Ochtman', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of diff -Nru python-couchdb-0.8/Makefile python-couchdb-0.10/Makefile --- python-couchdb-0.8/Makefile 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/Makefile 2014-07-15 06:59:19.000000000 +0000 @@ -1,7 +1,12 @@ .PHONY: test doc upload-doc -test: - PYTHONPATH=. python couchdb/tests/__init__.py +test: test2 test3 + +test2: + PYTHONPATH=. python -m couchdb.tests + +test3: + PYTHONPATH=. python3 -m couchdb.tests doc: python setup.py build_sphinx diff -Nru python-couchdb-0.8/MANIFEST.in python-couchdb-0.10/MANIFEST.in --- python-couchdb-0.8/MANIFEST.in 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/MANIFEST.in 2014-07-15 17:05:41.000000000 +0000 @@ -1,5 +1,5 @@ include COPYING include Makefile -include ChangeLog.txt +include ChangeLog.rst include doc/conf.py include doc/*.rst diff -Nru python-couchdb-0.8/.pc/applied-patches python-couchdb-0.10/.pc/applied-patches --- python-couchdb-0.8/.pc/applied-patches 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/.pc/applied-patches 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -python2.5_compatibility.patch diff -Nru python-couchdb-0.8/.pc/python2.5_compatibility.patch/couchdb/client.py python-couchdb-0.10/.pc/python2.5_compatibility.patch/couchdb/client.py --- python-couchdb-0.8/.pc/python2.5_compatibility.patch/couchdb/client.py 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/.pc/python2.5_compatibility.patch/couchdb/client.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,1092 +0,0 @@ -# -*- 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('http://localhost:5984/') ->>> db = server.create('python-tests') ->>> doc_id = db.create({'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 httplib2 -import mimetypes -from urllib import quote, urlencode -from types import FunctionType -from inspect import getsource -from textwrap import dedent -import re -import socket - -from couchdb import json - -__all__ = ['PreconditionFailed', 'ResourceNotFound', 'ResourceConflict', - 'ServerError', 'Server', 'Database', 'Document', 'ViewResults', - 'Row'] -__docformat__ = 'restructuredtext en' - - -DEFAULT_BASE_URI = 'http://localhost:5984/' - - -class PreconditionFailed(Exception): - """Exception raised when a 412 HTTP error is received in response to a - request. - """ - - -class ResourceNotFound(Exception): - """Exception raised when a 404 HTTP error is received in response to a - request. - """ - - -class ResourceConflict(Exception): - """Exception raised when a 409 HTTP error is received in response to a - request. - """ - - -class ServerError(Exception): - """Exception raised when an unexpected HTTP error is received in response - to a request. - """ - - -class Server(object): - """Representation of a CouchDB server. - - >>> server = Server('http://localhost:5984/') - - 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, uri=DEFAULT_BASE_URI, cache=None, timeout=None): - """Initialize the server object. - - :param uri: the URI of the server (for example - ``http://localhost:5984/``) - :param cache: either a cache directory path (as a string) or an object - compatible with the ``httplib2.FileCache`` interface. If - `None` (the default), no caching is performed. - :param timeout: socket timeout in number of seconds, or `None` for no - timeout - """ - http = httplib2.Http(cache=cache, timeout=timeout) - http.force_exception_to_status_code = False - self.resource = Resource(http, uri) - - 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 ResourceNotFound: - return False - - def __iter__(self): - """Iterate over the names of all databases.""" - resp, data = self.resource.get('_all_dbs') - return iter(data) - - def __len__(self): - """Return the number of databases.""" - resp, data = self.resource.get('_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.uri) - - 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(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(uri(self.resource.uri, name), validate_dbname(name), - http=self.resource.http) - db.resource.head() # actually make a request to the database - return db - - @property - 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. - - :type: `dict` - """ - resp, data = self.resource.get('_config') - return data - - @property - 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. - - :type: `unicode`""" - resp, data = self.resource.get() - return data['version'] - - 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(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] - - -class Database(object): - """Representation of a database on a CouchDB server. - - >>> server = Server('http://localhost:5984/') - >>> db = server.create('python-tests') - - New documents can be added to the database using the `create()` method: - - >>> doc_id = db.create({'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 `create()` 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, uri, name=None, http=None): - self.resource = Resource(http, uri) - 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 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.""" - resp, data = self.resource.get() - 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 - """ - resp, data = self.resource.head(id) - self.resource.delete(id, rev=resp['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` - """ - resp, data = self.resource.get(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 - """ - resp, data = self.resource.put(id, content=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. - - :type: 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` - """ - resp, data = self.resource.post(content=data) - return data['id'] - - def compact(self): - """Compact the database. - - This will try to prune all revisions from the database. - - :return: a boolean to indicate whether the compaction was initiated - successfully - :rtype: `bool` - """ - resp, data = self.resource.post('_compact') - 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 = 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 = dest.items() - else: - raise TypeError('expected dict or string, got %s' % - type(dest)) - if '_rev' in dest: - dest = '%s?%s' % (unicode_quote(dest['_id']), - unicode_urlencode({'rev': dest['_rev']})) - else: - dest = unicode_quote(dest['_id']) - - resp, data = self.resource._request('COPY', src, - headers={'Destination': dest}) - 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('http://localhost:5984/') - >>> 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 - """ - self.resource.delete(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: - resp, data = self.resource.get(id, **options) - except ResourceNotFound: - return default - else: - return Document(data) - - 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 - """ - resp, data = self.resource.get() - 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 - """ - resp, data = self.resource(doc['_id']).delete(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: the content of the attachment as a string, 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: - resp, data = self.resource(id).get(filename) - return data - except 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 hasattr(content, 'read'): - content = content.read() - if filename is None: - if hasattr(content, 'name'): - filename = content.name - else: - raise ValueError('no filename specified for attachment') - if content_type is None: - content_type = ';'.join(filter(None, mimetypes.guess_type(filename))) - - resp, data = self.resource(doc['_id']).put(filename, content=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('http://localhost:5984/') - >>> 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(uri(self.resource.uri, '_temp_view'), map_fun, - reduce_fun, language=language, wrapper=wrapper, - http=self.resource.http)(**options) - - def update(self, documents, **options): - """Perform a bulk update or insertion of the given documents using a - single HTTP request. - - >>> server = Server('http://localhost:5984/') - >>> 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 `schema.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) - resp, data = self.resource.post('_bulk_docs', content=content) - - results = [] - for idx, result in enumerate(data): - if 'error' in result: - if result['error'] == 'conflict': - exc_type = ResourceConflict - else: - # XXX: Any other error types mappable to exceptions here? - exc_type = 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('http://localhost:5984/') - >>> 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(uri(self.resource.uri, *name.split('/')), name, - wrapper=wrapper, - http=self.resource.http)(**options) - - -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. - - :type: basestring - """ - return self['_id'] - - @property - def rev(self): - """The document revision. - - :type: basestring - """ - return self['_rev'] - - -class View(object): - """Abstract representation of a view or query.""" - - def __init__(self, uri, wrapper=None, http=None): - self.resource = Resource(http, uri) - self.wrapper = wrapper - - def __call__(self, **options): - return ViewResults(self, options) - - def __iter__(self): - return 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, http=None): - View.__init__(self, uri, wrapper=wrapper, http=http) - 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')} - resp, data = self.resource.post(content=keys, - **self._encode_options(options)) - else: - resp, data = self.resource.get(**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, http=None): - View.__init__(self, uri, wrapper=wrapper, http=http) - 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') - resp, data = self.resource.post(content=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('http://localhost:5984/') - >>> 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): - wrapper = self.view.wrapper - for row in self.rows: - if wrapper is not None: - yield wrapper(row) - else: - yield row - - def __len__(self): - return len(self.rows) - - def _fetch(self): - data = self.view._exec(self.options) - self._rows = [Row(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. - - :type: `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. - - :type: `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. - - :type: `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) - - -# Internals - - -class Resource(object): - - def __init__(self, http, uri): - if http is None: - http = httplib2.Http() - http.force_exception_to_status_code = False - self.http = http - self.uri = uri - - def __call__(self, path): - return type(self)(self.http, uri(self.uri, path)) - - 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, content=None, headers=None, **params): - return self._request('POST', path, content=content, headers=headers, - **params) - - def put(self, path=None, content=None, headers=None, **params): - return self._request('PUT', path, content=content, headers=headers, - **params) - - def _request(self, method, path=None, content=None, headers=None, - **params): - from couchdb import __version__ - headers = headers or {} - headers.setdefault('Accept', 'application/json') - headers.setdefault('User-Agent', 'couchdb-python %s' % __version__) - body = None - if content is not None: - if not isinstance(content, basestring): - body = json.encode(content).encode('utf-8') - headers.setdefault('Content-Type', 'application/json') - else: - body = content - headers.setdefault('Content-Length', str(len(body))) - - def _make_request(retry=1): - try: - return self.http.request(uri(self.uri, path, **params), method, - body=body, headers=headers) - except socket.error, e: - if retry > 0 and e.args[0] == 54: # reset by peer - return _make_request(retry - 1) - raise - resp, data = _make_request() - - status_code = int(resp.status) - if data and resp.get('content-type') == 'application/json': - try: - data = json.decode(data) - except ValueError: - pass - - if status_code >= 400: - if type(data) is dict: - error = (data.get('error'), data.get('reason')) - else: - error = data - if status_code == 404: - raise ResourceNotFound(error) - elif status_code == 409: - raise ResourceConflict(error) - elif status_code == 412: - raise PreconditionFailed(error) - else: - raise ServerError((status_code, error)) - - return resp, data - - -def uri(base, *path, **query): - """Assemble a uri based on a base, any number of path segments, and query - string parameters. - - >>> uri('http://example.org/', '/_all_dbs') - 'http://example.org/_all_dbs' - """ - if base and base.endswith('/'): - base = base[:-1] - retval = [base] - - # build the path - path = '/'.join([''] + - [unicode_quote(s.strip('/')) for s in path - if s is not None]) - 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(['?', unicode_urlencode(params)]) - - return ''.join(retval) - - -def unicode_quote(string, safe=''): - if isinstance(string, unicode): - string = string.encode('utf-8') - return quote(string, safe) - - -def unicode_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 urlencode(params) - - -VALID_DB_NAME = re.compile(r'^[a-z][a-z0-9_$()+-/]*$') -def validate_dbname(name): - if not VALID_DB_NAME.match(name): - raise ValueError('Invalid database name') - return name diff -Nru python-couchdb-0.8/.pc/.version python-couchdb-0.10/.pc/.version --- python-couchdb-0.8/.pc/.version 2016-01-08 16:38:42.000000000 +0000 +++ python-couchdb-0.10/.pc/.version 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -2 diff -Nru python-couchdb-0.8/PKG-INFO python-couchdb-0.10/PKG-INFO --- python-couchdb-0.8/PKG-INFO 2010-08-13 12:03:55.000000000 +0000 +++ python-couchdb-0.10/PKG-INFO 2014-07-15 18:01:01.000000000 +0000 @@ -1,8 +1,8 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: CouchDB -Version: 0.8 +Version: 0.10 Summary: Python library for working with CouchDB -Home-page: http://code.google.com/p/couchdb-python/ +Home-page: https://github.com/djc/couchdb-python/ Author: Christopher Lenz Author-email: cmlenz@gmx.de License: BSD @@ -13,6 +13,10 @@ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Database :: Front-Ends Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -Nru python-couchdb-0.8/README.rst python-couchdb-0.10/README.rst --- python-couchdb-0.8/README.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-couchdb-0.10/README.rst 2014-07-15 17:07:34.000000000 +0000 @@ -0,0 +1,29 @@ +CouchDB-Python Library +====================== + +A Python library for working with CouchDB. `Downloads`_ are available via `PyPI`_. +Our `documentation`_ is also hosted there. We have a `mailing list`_. + +This package currently encompasses four primary modules: + +* ``couchdb.client``: the basic client library +* ``couchdb.design``: management of design documents +* ``couchdb.mapping``: a higher-level API for mapping between CouchDB documents and Python objects +* ``couchdb.view``: a CouchDB view server that allows writing view functions in Python + +It also provides a couple of command-line tools: + +* ``couchdb-dump``: writes a snapshot of a CouchDB database (including documents, attachments, and design documents) to MIME multipart file +* ``couchdb-load``: reads a MIME multipart file as generated by couchdb-dump and loads all the documents, attachments, and design documents into a CouchDB database +* ``couchdb-replicate``: can be used as an update-notification script to trigger replication between databases when data is changed + +Prerequisites: + +* simplejson (or Python >= 2.6, which comes with a simplejson-based JSON module in the standard library) +* Python 2.6 or later +* CouchDB 0.10.x or later (0.9.x should probably work, as well) + +.. _Downloads: http://pypi.python.org/pypi/CouchDB +.. _PyPI: http://pypi.python.org/ +.. _documentation: http://packages.python.org/CouchDB/ +.. _mailing list: http://groups.google.com/group/couchdb-python diff -Nru python-couchdb-0.8/README.txt python-couchdb-0.10/README.txt --- python-couchdb-0.8/README.txt 2010-08-13 11:58:24.000000000 +0000 +++ python-couchdb-0.10/README.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -CouchDB Python Library -====================== - -This package provides a Python interface to CouchDB. - - - -Please see the files in the `doc` folder or browse the documentation online at: - - diff -Nru python-couchdb-0.8/setup.cfg python-couchdb-0.10/setup.cfg --- python-couchdb-0.8/setup.cfg 2010-08-13 12:03:55.000000000 +0000 +++ python-couchdb-0.10/setup.cfg 2014-07-15 18:01:01.000000000 +0000 @@ -1,13 +1,16 @@ [build_sphinx] -all_files = 1 -build-dir = doc/build source-dir = doc/ +build-dir = doc/build +all_files = 1 + +[bdist_wheel] +universal = 1 + +[upload_sphinx] +upload-dir = doc/build/html [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 -[upload_sphinx] -upload-dir = doc/build/html - diff -Nru python-couchdb-0.8/setup.py python-couchdb-0.10/setup.py --- python-couchdb-0.8/setup.py 2010-08-13 11:53:40.000000000 +0000 +++ python-couchdb-0.10/setup.py 2014-07-15 17:04:04.000000000 +0000 @@ -14,103 +14,41 @@ import sys try: from setuptools import setup + has_setuptools = True except ImportError: from distutils.core import setup + has_setuptools = False 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'] +# Build setuptools-specific options (if installed). +if not has_setuptools: + print("WARNING: setuptools/distribute not available. Console scripts will not be installed.") + setuptools_options = {} +else: + setuptools_options = { + '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', + ], + }, + 'install_requires': requirements, + 'test_suite': 'couchdb.tests.__main__.suite', + 'zip_safe': True, + } + + setup( name = 'CouchDB', - version = '0.8', + version = '0.10', description = 'Python library for working with CouchDB', long_description = \ """This is a Python library for CouchDB. It provides a convenient high level @@ -118,31 +56,20 @@ author = 'Christopher Lenz', author_email = 'cmlenz@gmx.de', license = 'BSD', - url = 'http://code.google.com/p/couchdb-python/', - zip_safe = True, - + url = 'https://github.com/djc/couchdb-python/', classifiers = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', - 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', '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} + **setuptools_options )