# Copyright 2013-2014, Marten de Vries
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The :mod:`pouchdb` package mostly mirrors the `PouchDB JavaScript
API`_.
.. _PouchDB JavaScript API: http://pouchdb.com/api.html
There are two API versions: one asynchronous one, which almost directly
maps functions to their JavaScript equivalents and supports both
promises and callbacks in most cases, and a synchronous one, which
doesn't have a JavaScript equivalent, but is often easier to use.
API examples
============
**Synchronous:**
>>> environment = setup()
>>> db = environment.PouchDB('example')
>>> printjson(db.put({"_id": 'my_example_doc'})) #doctest: +ELLIPSIS
{"id": "my_example_doc", "ok": true, "rev": "1-..."}
>>> printjson(db.get('my_example_doc')) #doctest: +ELLIPSIS
{"_id": "my_example_doc", "_rev": "1-..."}
and (using :class:`dict`-like syntax):
>>> db["my_example_doc"] = {}
>>> printjson(db["my_example_doc"]) #doctest: +ELLIPSIS
{"_id": "my_example_doc", "_rev": "2-..."}
**Asynchronous:**
>>> def callback(err, resp):
... printjson(["inside callback:", resp])
...
>>> env = setup(async=True)
>>> db = env.PouchDB('example')
>>> promise = db.post({}, callback)
or ``db.post({}).then(callback)``, with the ``err`` argument removed
from the callback function.
Time to run the event loop. Normally, that's done using e.g.
``QtGui.QApplication().exec_()``, but for testing purposes like this
the following can be used:
>>> env.context.waitUntilCalled(callback) #doctest: +ELLIPSIS
["inside callback:", {"id": "...", "ok": true, "rev": "1-..."}]
>>> promise2 = env.destroy('example')
For more examples, see the :doc:`tests` documentation page.
API conventions
===============
Since the asynchronous and synchronous api have the same methods, those
methods are documented in abstract base classes:
:class:`AbstractEnvironment` and :class:`AbstractPouchDB`. Their
subclasses (:class:`AsyncEnvironment`, :class:`SyncEnvironment`,
:class:`AsyncPouchDB` and :class:`SyncPouchDB`) are the ones
actually exposed to you in the API.
The names in the JavaScript API of PouchDB are in camelCase_.
Python-PouchDB provides snake_case_ aliases where necessary, since
that's the preffered form according to the `PEP 8 style guide`_.
.. _camelCase: https://en.wikipedia.org/wiki/CamelCase
.. _snake_case: https://en.wikipedia.org/wiki/Snake_case
.. _PEP 8 style guide: http://legacy.python.org/dev/peps/pep-0008/#function-names
As shown in the example, you can pass in dictionaries as documents.
Additionally, Python-PouchDB allows passing in JSON strings. So, no need
to convert if you already have a JSON string.
"""
from pouchdb import context
from pouchdb import utils
from pouchdb.info import __author__, __version__, __copyright__
import itertools
import functools
import collections
import os
import abc
import contextlib
import tempfile
import shutil
import atexit
with utils.suppress(ImportError):
import faulthandler
faulthandler.enable()
_IGNORED_ERRORS = [
"ArrayBuffer values are deprecated in Blob Constructor.",
"ArrayBuffer is deprecated in XMLHttpRequest.send(). Use ArrayBufferView instead.",
"EVALUATED FUNCTION LOGS:",
]
def _getPromiseResponse(ctx, promise):
returned = {}
def success(resp):
returned["resp"] = resp
def fail(err):
returned["err"] = err
promise.then(success, fail)
ctx.waitUntilCalled([success, fail])
if returned.get("err"):
raise PouchDBError(returned["err"])
return returned["resp"]
@contextlib.contextmanager
def _getting_callback_result(ctx):
returning = {}
def callback(err=None, resp=None):
returning["err"] = err
returning["resp"] = resp
info = {"callback": callback}
yield info
ctx.waitUntilCalled(callback)
if returning.get("err"):
raise PouchDBError(returning["err"])
info["resp"] = returning["resp"]
def _filterNone(args):
return [a for a in args if a is not None]
def _jsonAllowed(value):
if isinstance(value, basestring):
return context.JSON(value)
return value
def _aliasFor(methodName):
def alias(self, *args, **kwargs):
return getattr(self, methodName)(*args, **kwargs)
alias.__doc__ = "Alias for :meth:`%s`." % methodName
return alias
[docs]class AbstractPouchDB(object):
"""**Never** instantiate this class or one of its subclasses
yourself. Create an environment using the :func:`setup` function
instead, and then use its :attr:`AbstractEnvironment.PouchDB`
attribute, with the arguments specified at :meth:`__init__`.
Extra (optional) options for all ``AbstractPouchDB.validating*``
methods compared to their normal ``AbstractPouchDB.*`` counterparts
are:
- ``secObj``: e.g.::
{
"admins": {
"names": [],
"roles": []
},
"members": {
"names": [],
"roles": []
}
}
- ``userCtx``: e.g.::
{
"db": "test_db",
"name": "username",
"roles": [
"_admin"
]
}
- ``checkHttp``: Set this to ``True`` if you want to validate HTTP
database documents offline too. Unnecessary for CouchDB, but handy
for e.g. PouchDB-Server, which doesn't validate itself.
"""
__metaclass__ = abc.ABCMeta
_idCounter = itertools.count()
[docs] def __init__(self, _ctx, name=None, **options):
"""This method creates a database or opens an existing one. If
you use a URL like http://domain.com/dbname then Python-PouchDB
will work as a client to an online CouchDB instance. Otherwise
it will create a local database using the WebSQL backend.
"""
self._ctx = _ctx
self._id = next(self._idCounter)
try:
self._ctx.newObject(self._id, name, options)
except context.JSError, e:
raise PouchDBError(e)
def _asyncCall(self, methodName, *args):
return self._ctx.call(self._id, methodName, *_filterNone(args))
@abc.abstractmethod
[docs] def destroy(self):
"""Delete database."""
@abc.abstractmethod
[docs] def put(self, doc, id=None, rev=None, **options):
"""Create a new document or update an existing document. If the
document already exists, you must specify its revision ``_rev``,
otherwise a conflict will occur.
"""
@abc.abstractmethod
[docs] def post(self, doc, **options):
"""Create a new document and let PouchDB generate an ``_id`` for
it.
"""
@abc.abstractmethod
[docs] def get(self, docid, **options):
"""Retrieves a document, specified by ``docid``."""
@abc.abstractmethod
[docs] def remove(self, doc, **options):
"""Deletes the document. doc is required to be a document with
at least an ``_id`` and a ``_rev`` property. Sending the full
document will work as well.
"""
@abc.abstractmethod
[docs] def bulkDocs(self, docs, **options):
"""Modify, create or delete multiple documents. The docs
argument is an object with property docs which is an array of
documents. You can also specify a new_edits property on the
docs object that when set to false allows you to post existing
documents.
If you omit an ``_id`` parameter on a given document, the
database will create a new document and assign the ID for you.
To update a document, you must include both an ``_id`` parameter
and a ``_rev`` parameter, which should match the ID and revision
of the document on which to base your updates. Finally, to
delete a document, include a ``_deleted`` parameter with the
value ``True``.
"""
bulk_docs = _aliasFor("bulkDocs")
@abc.abstractmethod
[docs] def allDocs(self, **options):
"""Fetch multiple documents. Deleted documents are only included
if ``keys`` is specified.
"""
all_docs = _aliasFor("allDocs")
@abc.abstractmethod
[docs] def changes(self, **options):
"""A list of changes made to documents in the database, in the
order they were made. It returns an object with one method
cancel, which you call if you don't want to listen to new
changes anymore. ``onChange`` will be be called for each change
that is encountered.
"""
@abc.abstractmethod
[docs] def replicateTo(self, remoteDB, **options):
"""A shorthand for :meth:`AbstractEnvironment.replicate`"""
replicate_to = _aliasFor("replicateTo")
@abc.abstractmethod
[docs] def replicateFrom(self, remoteDB, **options):
"""A shorthand for :meth:`AbstractEnvironment.replicate`"""
replicate_from = _aliasFor("replicateFrom")
@abc.abstractmethod
[docs] def sync(self, remoteDB, **options):
"""A shorthand for :meth:`AbstractEnvironment.sync`"""
@abc.abstractmethod
[docs] def putAttachment(self, docId, attachmentId, doc, type, rev=None):
"""Attaches a binary object to a document. Most of
Python-PouchDB's API deals with JSON, but if you're dealing
with large binary data (such as PNGs), you may incur a
performance or storage penalty if you simply include them as
base64- or hex-encoded strings. In these cases, you can store
the binary data as an attachment.
Be aware that the argument order is different than in PouchDB
due to the ``rev`` argument being optional. Byte strings replace
blobs in Python-PouchDB. (e.g. ``b"Hello World!"``)
"""
put_attachment = _aliasFor("putAttachment")
@abc.abstractmethod
[docs] def getAttachment(self, docId, attachmentId, **options):
"""Get attachment data. Returns a dictionary with the following
format::
{
"data": b"Bytes as byte string",
"type": "text/plain"
}
"""
get_attachment = _aliasFor("get_attachment")
@abc.abstractmethod
[docs] def removeAttachment(self, docId, attachmentId, rev):
"""Delete an attachment from a doc."""
remove_attachment = _aliasFor("remove_attachment")
@abc.abstractmethod
[docs] def query(self, fun, **options):
"""Retrieves a view, which allows you to perform more complex
queries on Python-PouchDB. The CouchDB documentation for map/
reduce applies to Python-PouchDB.
Since views perform a full scan of all documents, this method
may be slow, unless you first save your view in a design
document.
"""
@abc.abstractmethod
[docs] def viewCleanup(self, **options):
"""Cleans up any stale map/reduce indexes.
As design docs are deleted or modified, their associated index
files (in CouchDB) or companion databases (in local PouchDBs)
continue to take up space on disk. :meth:`viewCleanup` removes
these unnecessary index files.
"""
view_cleanup = _aliasFor("viewCleanup")
@abc.abstractmethod
[docs] def info(self):
"""Get information about a database."""
@abc.abstractmethod
[docs] def compact(self):
"""Runs compaction of the database. Fires callback when
compaction is done. If you use the http adapter and have
specified a callback, Pouch will ping the remote database in
regular intervals unless the compaction is finished.
"""
@abc.abstractmethod
[docs] def revsDiff(self, diff):
"""Given a set of document/revision IDs, returns the subset of
those that do not correspond to revisions stored in the
database. Primarily used in replication.
"""
revs_diff = _aliasFor("revsDiff")
#gql plug-in
@abc.abstractmethod
[docs] def gql(self, query, **options):
""" Uses the GQL PouchDB plug-in. Check out `its documentation
<http://pouchdb.com/gql.html>`_.
The Google Query Language (GQL) interface provides an
alternative method for accessing data. The version of GQL
implemented here is based on the `Google Visualization API Query
Language
<https://developers.google.com/chart/interactive/docs/querylanguage>`_.
The syntax of GQL queries should be familiar to those who have
used SQL, but the capabilities of GQL are much more limited.
"""
#geopouch plug-in
@abc.abstractmethod
[docs] def spatial(self, fun, **options):
"""Same as requesting ``_spatial`` in CouchDB when GeoCouch is
installed. Wraps the geopouch plug-in.
"""
#search plug-in
@abc.abstractmethod
[docs] def search(self, func, **options):
"""Wraps the pouchdb-search plug-in."""
#authentication plug-in
@abc.abstractmethod
[docs] def signup(self, username, password, **options):
"""Sign up a new user who doesn't exist yet. Throws an error if
the user already exists or if the username is invalid, or if
some network error occurred. CouchDB has some limitations on
user names (e.g. they cannot contain the character ':').
Note: Signing up does not automatically log in a user; you will
need to call :meth:`login` afterwards.
Options:
- `metadata` : Object of metadata you want to store with the
username, e.g. an email address or any other info. Can be as
deeply structured as you want.
"""
signUp = _aliasFor("signup")
sign_up = _aliasFor("signup")
@abc.abstractmethod
[docs] def login(self, username, password, **options):
"""Log in an existing user. Throws an error if the user doesn't
exist yet, the password is wrong, the HTTP server is
unreachable, or a meteor struck your computer.
"""
logIn = _aliasFor("login")
log_in = _aliasFor("login")
@abc.abstractmethod
[docs] def logout(self, **options):
"""Logs out whichever user is currently logged in. If nobody's
logged in, it does nothing and just returns ``{"ok": True}``.
"""
logOut = _aliasFor("logout")
log_out = _aliasFor("log_out")
@abc.abstractmethod
[docs] def getSession(self, **options):
"""Returns information about the current session. In other
words, this tells you which user is currently logged in.
"""
get_session = _aliasFor("getSession")
@abc.abstractmethod
[docs] def getUser(self, username, **options):
"""Returns the user document associated with a username.
(CouchDB, in a pleasing show of consistency, stores users as
JSON documents in the special _users database.) This is the
primary way to get metadata about a user.
"""
get_user = _aliasFor("getUser")
#validation plug-in
@abc.abstractmethod
[docs] def validatingPut(self, doc, id=None, rev=None, **options):
"""Same as :meth:`put`, but validates like in CouchDB. Wraps
the validation PouchDB plug-in.
"""
validating_put = _aliasFor("validatingPut")
@abc.abstractmethod
[docs] def validatingPost(self, doc, **options):
"""Same as :meth:`post`, but validates like in CouchDB. Wraps
the validation PouchDB plug-in.
"""
validating_post = _aliasFor("validatingPost")
@abc.abstractmethod
[docs] def validatingRemove(self, doc, **options):
"""Same as :meth:`remove`, but validates like in CouchDB.
Wraps the validation PouchDB plug-in.
"""
validating_remove = _aliasFor("validatingRemove")
@abc.abstractmethod
[docs] def validatingBulkDocs(self, docs, **options):
"""Same as :meth:`bulkDocs`, but validates like in CouchDB.
Wraps the validation PouchDB plug-in.
"""
validating_bulk_docs = _aliasFor("validatingBulkDocs")
@abc.abstractmethod
[docs] def validatingPutAttachment(self, docId, attachmentId, doc, type, rev=None, **options):
"""Same as :meth:`putAttachment`, but validates like in
CouchDB. Wraps the validation PouchDB plug-in.
"""
validating_put_attachment = _aliasFor("validatingPutAttachment")
@abc.abstractmethod
[docs] def validatingRemoveAttachment(self, docId, attachmentId, rev, **options):
"""Same as :meth:`removeAttachment`, but validates like in
CouchDB. Wraps the validation PouchDB plug-in.
"""
validating_remove_attachment = _aliasFor("validatingRemoveAttachment")
#show plug-in
@abc.abstractmethod
[docs] def show(self, showPath, **options):
"""Same as requesting ``_show`` in CouchDB. Wraps the show
PouchDB plug-in.
"""
#list plug-in
@abc.abstractmethod
[docs] def list(self, listPath, **options):
"""Same as requesting ``_list`` in CouchDB. Wraps the list
PouchDB plug-in.
"""
#update plug-in
@abc.abstractmethod
[docs] def update(self, url, **options):
"""Same as sending a request to ``_update`` in CouchDB. Wraps
the update PouchDB plug-in.
``options``:
- ``withValidation``: If ``True``, the update function uses
:meth:`validatingPut` instead of (the default) :meth:`put`.
"""
#rewrite plug-in
@abc.abstractmethod
[docs] def rewrite(self, url, **options):
"""Same as sending a request to ``_rewrite`` in CouchDB. Wraps
the rewrite PouchDB plug-in. The ``url`` has the form
`designDocName/rewritePath`. Keep in mind that you need to use
``options.query`` instead of appending a ?key=value string to
the ``url``.
``options``:
- ``withValidation``: If ``True``, the ``db.validating*``
functions are used instead of their ``db.*`` counterparts if
the rewrite routes to such a resource where that is relevant.
"""
@abc.abstractmethod
[docs] def rewriteResultRequestObject(self, url, **options):
"""See :meth:`rewrite`. Instead of returning the result of the
function that the rewrite routes to, this returns a CouchDB
request object that points to the resource the rewrite resolves
to. See also the CouchDB documentation `on the request object`_.
.. _on the request object: http://docs.couchdb.org/en/latest/json-structure.html#request-object
"""
@contextlib.contextmanager
def _convert404ToKeyError():
try:
yield
except PouchDBError, e:
if e["status"] == 404:
raise KeyError(str(e))
#probably an authorization problem
raise
[docs]class SyncPouchDB(AbstractPouchDB, collections.MutableMapping):
"""This class is a MutableMapping, which means it can be used like a
:class:`dict`. For details on how that works with revisions, see
the documentations on the following methods: :meth:`__getitem__`,
:meth:`__setitem__` and :meth:`__delitem__`.
See for all the methods that being a MutableMapping offers, `the
docs about it`_.
.. _the docs about it: https://docs.python.org/2/library/collections.html#collections-abstract-base-classes
>>> db = setup()["my-example"]
>>> db["mytest"] = {"test": "ok"}
>>> printjson(list(db))
["mytest"]
>>> len(db)
1
>>> "mytest" in db
True
>>> "abc" in db
False
"""
[docs] def __getitem__(self, docId):
"""A shortcut for the :meth:`AbstractPouchDB.get` method.
Raises a :exc:`KeyError` in place of a :exc:`PouchDBError` if
that error's status is 404 Not Found.
>>> db = setup().PouchDB("example")
>>> db["abc"]
Traceback (most recent call last):
...
KeyError: '{"status":404,"name":"not_found","message":"missing"}'
"""
with _convert404ToKeyError() as e:
return self.get(docId)
[docs] def __delitem__(self, docId):
"""A shortcut for the :meth:`AbstractPouchDB.remove` method.
Raises a :exc:`KeyError` in place of a :exc:`PouchDBError` if
that error's status is 404 Not Found. Removes the current
document from the database (in other words, the **latest
revision** is taken from the database).
>>> db = setup()["example"]
>>> del db["abc"]
Traceback (most recent call last):
...
KeyError: '{"status":404,"name":"not_found","message":"missing"}'
"""
with _convert404ToKeyError() as e:
self.remove(self[docId])
[docs] def __setitem__(self, docId, doc):
"""Sets ``doc``'s _id to ``docId`` and saves the result. When
no ``doc["_rev"]`` is defined, the one from the current document
saved under that id is reused. That is a **difference** from the
normal :meth:`AbstractPouchDB.put` method. When succesful,
``doc["_rev"]`` will have been set to its new value.
"""
doc["_id"] = docId
if not "_rev" in doc:
#get the rev of the current document - if one exists.
with utils.suppress(PouchDBError):
doc["_rev"] = self.get(docId)._rev
resp = self.put(doc)
doc["_rev"] = resp.rev
def __eq__(self, other):
"""Only equal when ``other`` is just another reference to
``self``
"""
return self is other
def __iter__(self):
return (row["id"] for row in self.allDocs()["rows"])
def __len__(self):
return self.allDocs()["total_rows"]
[docs] def get_(self, *args, **kwargs):
"""Because ``MutableMapping.get`` is overwritten by the
:meth:`get` method, it's aliased under this name.
>>> printjson(db.get_("abc", {"hello": "world"}))
{"hello": "world"}
"""
#can't use super() - using MRO doesn't work when two equally
#named methods have different signatures.
return collections.MutableMapping.get(self, *args, **kwargs)
def _syncCall(self, methodName, *args):
promise = self._ctx.call(self._id, methodName, *_filterNone(args))
return _getPromiseResponse(self._ctx, promise)
def destroy(self):
return self._syncCall("destroy")
def put(self, doc, id=None, rev=None, **options):
return self._syncCall("put", _jsonAllowed(doc), id, rev, options)
def post(self, doc, **options):
return self._syncCall("post", _jsonAllowed(doc), options)
def get(self, docid, **options):
return self._syncCall("get", docid, options)
def remove(self, doc, **options):
return self._syncCall("remove", _jsonAllowed(doc), options)
def bulkDocs(self, docs, **options):
return self._syncCall("bulkDocs", _jsonAllowed(docs), options)
def allDocs(self, **options):
return self._syncCall("allDocs", options)
[docs] def changes(self, **options):
"""When the ``live`` or ``continuous`` option is active, this
method acts like its asynchronous equivalent.
"""
return self._callFunc(options)("changes", options)
def _callFunc(self, options):
if options.get("live", False) or options.get("continuous", False):
return self._asyncCall
return self._syncCall
[docs] def replicateTo(self, remoteDB, **options):
"""When the ``live`` or ``continuous`` option is active, this
method acts like its asynchronous equivalent.
"""
return self._callFunc(options)("replicate.to", remoteDB, options)
[docs] def replicateFrom(self, remoteDB, **options):
"""When the ``live`` or ``continuous`` option is active, this
method acts like its asynchronous equivalent.
"""
return self._callFunc(options)("replicate.from", remoteDB, options)
[docs] def sync(self, remoteDB, **options):
"""When the ``live`` or ``continuous`` option is active, this
method acts like its asynchronous equivalent.
"""
return self._callFunc(options)("replicate.sync", remoteDB, options)
def putAttachment(self, docId, attachmentId, doc, type, rev=None):
return self._syncCall("putAttachment", docId, attachmentId, rev, context.Blob(doc), type)
def getAttachment(self, docId, attachmentId, **options):
return self._syncCall("getAttachment", docId, attachmentId, options)
def removeAttachment(self, docId, attachmentId, rev):
return self._syncCall("removeAttachment", docId, attachmentId, rev)
def query(self, fun, **options):
return self._syncCall("query", fun, options)
def viewCleanup(self, **options):
return self._syncCall("viewCleanup", options)
def info(self):
return self._syncCall("info")
def compact(self, **options):
return self._syncCall("compact", options)
def revsDiff(self, diff):
return self._syncCall("revsDiff", _jsonAllowed(diff))
#gql plug-in
def gql(self, query, **options):
with _getting_callback_result(self._ctx) as info:
self._ctx.call(self._id, "gql", query, options, info["callback"])
return info["resp"]
#spatial plug-in
def spatial(self, fun, **options):
return self._syncCall("spatial", fun, options)
#search plug-in
def search(self, func, **options):
return self._syncCall("search", func, options)
#authentication plug-in
def signup(self, username, password, **options):
return self._syncCall("signup", username, password, options)
def login(self, username, password, **options):
return self._syncCall("login", username, password, options)
def logout(self, **options):
return self._syncCall("logout", options)
def getSession(self, **options):
return self._syncCall("getSession", options)
def getUser(self, username, **options):
return self._syncCall("getUser", username, options)
#validation plug-in
def validatingPut(self, doc, id=None, rev=None, **options):
return self._syncCall("validatingPut", _jsonAllowed(doc), id, rev, options)
def validatingPost(self, doc, **options):
return self._syncCall("validatingPost", _jsonAllowed(doc), options)
def validatingRemove(self, doc, **options):
return self._syncCall("validatingRemove", _jsonAllowed(doc), options)
def validatingBulkDocs(self, docs, **options):
return self._syncCall("validatingBulkDocs", _jsonAllowed(docs), options)
def validatingPutAttachment(self, docId, attachmentId, doc, type, rev=None, **options):
return self._syncCall("validatingPutAttachment", docId, attachmentId, rev, context.Blob(doc), type, options)
def validatingRemoveAttachment(self, docId, attachmentId, rev, **options):
return self._syncCall("validatingRemoveAttachment", docId, attachmentId, rev, options)
#show plug-in
def show(self, showPath, **options):
return self._syncCall("show", showPath, options)
#list plug-in
def list(self, listPath, **options):
return self._syncCall("list", listPath, options)
#update plug-in
def update(self, url, **options):
return self._syncCall("update", url, options)
#rewrite plug-in
def rewrite(self, url, **options):
return self._syncCall("rewrite", url, options)
def rewriteResultRequestObject(self, url, **options):
return self._syncCall("rewriteResultRequestObject", url, options)
[docs]class BaseError(Exception):
"""The base class for all errors this module should raise.
"""
[docs]class EnvironmentError(BaseError):
"""Raised when something is wrong relating to the environment in
which Python-PouchDB runs.
"""
[docs]class PouchDBError(BaseError):
"""All error responses PouchDB would normally give you get raised
in the form of this error class in Python-PouchDB. You can use get
item syntax to access properties set on the error object. E.g.:
>>> try:
... setup().PouchDB('new-db').get('unexisting-doc')
... except pouchdb.PouchDBError as e:
... if e["status"] == 404:
... print("Not found")
... else:
... print("Unknown error")
...
Not found
"""
def __init__(self, message, *args, **kwargs):
super(PouchDBError, self).__init__(message, *args, **kwargs)
self.message = message
def __getitem__(self, key):
return self.message[key]
def __contains__(self, key):
return key in self.message
def __str__(self):
with utils.suppress(KeyError, TypeError):
return self.message["toString"]()
return str(self.message)
[docs]class AsyncPouchDB(AbstractPouchDB):
"""The GQL plug-in only supports the callback interface, it doesn't
provide a promise.
"""
def destroy(self, callback=None):
return self._asyncCall("destroy", callback)
def _handlePutArgs(self, doc, id, rev, callback, options):
if callable(id):
callback = id
id = None
if callable(rev):
callback = rev
rev = None
return _jsonAllowed(doc), id, rev, options, callback
def put(self, doc, id=None, rev=None, callback=None, **options):
return self._asyncCall("put", *self._handlePutArgs(doc, id, rev, callback, options))
def post(self, doc, callback=None, **options):
return self._asyncCall("post", _jsonAllowed(doc), options, callback)
def get(self, docid, callback=None, **options):
return self._asyncCall("get", docid, options, callback)
def remove(self, doc, callback=None, **options):
return self._asyncCall("remove", _jsonAllowed(doc), options, callback)
def bulkDocs(self, docs, callback=None, **options):
return self._asyncCall("bulkDocs", _jsonAllowed(docs), options, callback)
def allDocs(self, callback=None, **options):
return self._asyncCall("allDocs", options, callback)
def changes(self, **options):
return self._asyncCall("changes", options)
def replicateTo(self, remoteDB, callback=None, **options):
return self._asyncCall("replicate.to", remoteDB, options, callback)
def replicateFrom(self, remoteDB, callback=None, **options):
return self._asyncCall("replicate.from", remoteDB, options, callback)
def sync(self, remoteDB, callback=None, **options):
return self._asyncCall("replicate.sync", remoteDB, options, callback)
def putAttachment(self, docId, attachmentId, doc, type, rev=None, callback=None):
return self._asyncCall("putAttachment", docId, attachmentId, rev, context.Blob(doc), type, callback)
def getAttachment(self, docId, attachmentId, callback=None, **options):
return self._asyncCall("getAttachment", docId, attachmentId, options, callback)
def removeAttachment(self, docId, attachmentId, rev, callback=None):
return self._asyncCall("removeAttachment", docId, attachmentId, rev, callback)
def query(self, fun, callback=None, **options):
return self._asyncCall("query", fun, options, callback)
def viewCleanup(self, callback=None, **options):
return self._asyncCall("viewCleanup", options, callback)
def info(self, callback=None):
return self._asyncCall("info", callback)
def compact(self, callback=None, **options):
return self._asyncCall("compact", options, callback)
def revsDiff(self, diff, callback=None):
return self._asyncCall("revsDiff", _jsonAllowed(diff), callback)
#gql plug-in
def gql(self, query, callback=None, **options):
return self._asyncCall("gql", query, options, callback)
#spatial plug-in
def spatial(self, fun, callback=None, **options):
return self._asyncCall("spatial", fun, options, callback)
#search plug-in
def search(self, func, callback=None, **options):
return self._asyncCall("search", func, options, callback)
#authentication plug-in
def signup(self, username, password, callback=None, **options):
return self._asyncCall("signup", username, password, options, callback)
def login(self, username, password, callback=None, **options):
return self._asyncCall("login", username, password, options, callback)
def logout(self, callback=None, **options):
return self._asyncCall("logout", options, callback)
def getSession(self, callback=None, **options):
return self._asyncCall("getSession", options, callback)
def getUser(self, username, callback=None, **options):
return self._asyncCall("getUser", username, options, callback)
#validation functions
def validatingPut(self, doc, id=None, rev=None, callback=None, **options):
return self._asyncCall("validatingPut", *self._handlePutArgs(doc, id, rev, callback, options))
def validatingPost(self, doc, callback=None, **options):
return self._asyncCall("validatingPost", _jsonAllowed(doc), options, callback)
def validatingRemove(self, doc, callback=None, **options):
return self._asyncCall("validatingRemove", _jsonAllowed(doc), options, callback)
def validatingBulkDocs(self, docs, callback=None, **options):
return self._asyncCall("validatingBulkDocs", _jsonAllowed(docs), options, callback)
def validatingPutAttachment(self, docId, attachmentId, doc, type, rev=None, callback=None, **options):
return self._asyncCall("validatingPutAttachment", docId, attachmentId, context.Blob(doc), type, rev, options, callback)
def validatingRemoveAttachment(self, docId, attachmentId, rev, callback=None, **options):
return self._asyncCall("validatingRemoveAttachment", docId, attachmentId, rev, options, callback)
#show plug-in
def show(self, showPath, callback=None, **options):
return self._asyncCall("show", showPath, options, callback)
#list plug-in
def list(self, listPath, callback=None, **options):
return self._asyncCall("list", listPath, options, callback)
#update plug-in
def update(self, url, callback=None, **options):
return self._asyncCall("update", url, options, callback)
#rewrite plug-in
def rewrite(self, url, callback=None, **options):
return self._asyncCall("rewrite", url, options, callback)
def rewriteResultRequestObject(self, url, callback=None, **options):
return self._asyncCall("rewriteResultRequestObject", url, options, callback)
[docs]class AbstractEnvironment(object):
"""The environment is `an event emitter
<http://nodejs.org/api/events.html#events_class_events_eventemitter>`_
and will emit a 'created' event when a database is created. A
'destroy' event is emited when a database is destroyed.
**Don't** instantiate this class or one of its subclasses
yourself, use the :func:`setup` function instead.
"""
__metaclass__ = abc.ABCMeta
#A placeholder value when uninstantiated for sphinxdoc.
PouchDB = "<subclass of AbstractPouchDB (approximately)>"
@abc.abstractmethod
def __init__(self, PouchDB, ctx, *args, **kwargs):
super(AbstractEnvironment, self).__init__(*args, **kwargs)
self._ctx = ctx
#:The PouchDB class that you can use to make a new database
#:instance. You should always use this: **never** instantiate
#::class:`SyncPouchDB` or :class:`AsyncPouchDB` manually.
self.PouchDB = functools.partial(PouchDB, self._ctx)
[docs] def __getitem__(self, dbName):
"""A shortcut for the :attr:`PouchDB` attribute. The following
two lines of code are, as you can see, equivalent:
>>> setup()["test"] #doctest: +ELLIPSIS
<pouchdb.SyncPouchDB object at 0x...>
>>> setup().PouchDB('test') #doctest: +ELLIPSIS
<pouchdb.SyncPouchDB object at 0x...>
"""
return self.PouchDB(dbName)
def _asyncCall(self, methodName, *args):
return self._ctx.callStatic(methodName, *_filterNone(args))
@property
[docs] def POUCHDB_VERSION(self):
"""The value of what is ``PouchDB.version`` in JavaScript, in
other words, the version of PouchDB that Python-PouchDB wraps.
"""
return self._ctx.staticProperty("version")
@abc.abstractmethod
[docs] def destroy(self, name_=None, **options):
"""Allows you to destroy a database without creating a new
PouchDB instance first.
The ``name`` argument in PouchDB is called `name_` in
Python-PouchDB, to prevent a clash with the ``options["name"]``
argument. In JavaScript where there are no keyword arguments the
problem doesn't exist, in Python it does, hence the change.
"""
@abc.abstractmethod
[docs] def replicate(self, source, target, **options):
"""Replicate data from source to target. Both the source and
target can be a string representing a CouchDB database url or
the name a local PouchDB database. If ``live`` is true, then
this will track future changes and also replicate them
automatically.
"""
@abc.abstractmethod
[docs] def sync(self, source, target, **options):
"""Sync data from ``source`` to ``target`` and ``target`` to
``source``. This is a convience method for bidirectional data
replication.
"""
@property
[docs] def context(self):
"""Allows access to the internals of Python-Pouch; only useful
for debugging purposes. E.g. :attr:`context`.inspect() and
:attr:`context`.waitUntilCalled(). The last is used by the
test suite.
"""
return self._ctx
#event emitter methods
[docs] def addListener(self, event, listener):
"""Adds a listener to the end of the listeners array for the
specified event.
"""
return self._ctx.callStatic("addListener", event, listener)
add_listener = _aliasFor("addListener")
on = _aliasFor("addListener")
[docs] def emit(self, event, *args):
"""Execute each of the listeners in order with the supplied
``args``. Returns ``True`` if event had listeners, ``False``
otherwise.
"""
return self._asyncCall("emit", event, *args)
[docs] def listeners(self, event):
"""Returns an array of listeners for the specified event."""
return self._asyncCall("listeners", event)
[docs] def once(self, event, listener):
"""Adds a one time listener for the event. This listener is
invoked only the next time the event is fired, after which it
is removed.
"""
return self._asyncCall("once", event, listener)
[docs] def removeListener(self, event, listener):
"""Remove a listener from the listener array for the specified
event. Caution: changes array indices in the listener array
behind the listener.
"""
return self._asyncCall("removeListener", event, listener)
remove_listener = _aliasFor("removeListener")
[docs] def removeAllListeners(self, event=None):
"""Removes all listeners, or those of the specified event."""
return self._asyncCall("removeAllListeners", event)
remove_all_listeners = _aliasFor("removeAllListeners")
[docs] def setMaxListeners(self, n):
"""By default EventEmitters will print a warning if more than 10
listeners are added for a particular event. This is a useful
default which helps finding memory leaks. Obviously not all
Emitters should be limited to 10. This function allows that
to be increased. Set to zero for unlimited.
"""
return self._asyncCall("setMaxListeners", n)
set_max_listeners = _aliasFor("setMaxListeners")
#allDbs plug-in
@abc.abstractmethod
[docs] def allDbs(self):
"""Wraps the pouchdb-all-dbs plug-in. Returns a list of the
names of all local databases.
"""
all_dbs = _aliasFor("allDbs")
@abc.abstractmethod
[docs] def resetAllDbs(self):
"""Wraps the pouchdb-all-dbs plug-in. Destroys the separate
allDbs database. You should never need to call this function.
"""
reset_all_dbs = _aliasFor("resetAllDbs")
[docs]class AsyncEnvironment(AbstractEnvironment):
def __init__(self, *args, **kwargs):
super(AsyncEnvironment, self).__init__(AsyncPouchDB, *args, **kwargs)
def destroy(self, name_=None, callback=None, **options):
if callable(name_):
callback = name_
name_ = None
return self._asyncCall("destroy", name_, options, callback)
def replicate(self, source, target, **options):
return self._asyncCall("replicate", source, target, options)
def sync(self, source, target, **options):
return self._asyncCall("sync", source, target, options)
#allDbs plugin
def allDbs(self, callback=None):
return self._asyncCall("allDbs", callback)
def resetAllDbs(self, callback=None):
return self._asyncCall("resetAllDbs", callback)
[docs]class SyncEnvironment(AbstractEnvironment, collections.Mapping):
"""This class is a Mapping, which means it can be used like a
:class:`dict`. (Except for setting values, in this case).
See for all the methods that being a Mapping offers, `the docs about
it`_.
.. _the docs about it: https://docs.python.org/2/library/collections.html#collections-abstract-base-classes
>>> env = setup()
>>> env.keys() #doctest: +ELLIPSIS
[...]
"""
def __init__(self, *args, **kwargs):
super(SyncEnvironment, self).__init__(SyncPouchDB, *args, **kwargs)
[docs] def __delitem__(self, dbName):
"""A shortcut for the :meth:`AbstractEnvironment.destroy`
method. The last two of the following lines of code are
equivalent:
>>> env = setup()
>>> del env["test"]
>>> env.destroy("test")
"""
self.destroy(dbName)
def __iter__(self):
return iter(self.allDbs())
def __len__(self):
return len(self.allDbs())
def _syncCall(self, methodName, *args):
promise = self._ctx.callStatic(methodName, *_filterNone(args))
return _getPromiseResponse(self._ctx, promise)
def _callFunc(self, options):
if options.get("live", False) or options.get("continuous", False):
return self._asyncCall
return self._syncCall
def destroy(self, name_=None, **options):
self._syncCall("destroy", name_, options)
[docs] def replicate(self, source, target, **options):
"""When the ``live`` or ``continuous`` option is active, this
method acts like its asynchronous equivalent.
"""
return self._callFunc(options)("replicate", source, target, options)
[docs] def sync(self, source, target, **options):
"""When the ``live`` or ``continuous`` option is active, this
method acts like its asynchronous equivalent.
"""
return self._callFunc(options)("sync", source, target, options)
#allDbs plug-in
def allDbs(self):
return self._syncCall("allDbs")
def resetAllDbs(self):
return self._syncCall("resetAllDbs")
#Caching prevents identical environments from being created which saves
#quite a bit of memory and CPU. On top of that, a reference to every
#environment needs to be kept anyway to prevent segfaulting.
_envs = {}
[docs]def setup(storageDir=None, async=False, baseUrl="file:///"):
"""Sets up an environment which allows access to the rest of the
API, which mostly consists out of wrappers around PouchDB's
functions. This environment is then returned to the user.
Depending on the ``async`` parameter, this is either an instance of
:class:`SyncEnvironment` or :class:`AsyncEnvironment`.
The ``storageDir`` is created automatically if it doesn't already
exist. It can be relative, and should be a directory (not a file).
It's where databases are saved (or more specific: WebKit's backend
database files). When it is ``None`` (the default), a temporary
directory is created by Python-PouchDB, which is also removed at
process exit. This function can raise an :exc:`EnvironmentError`
when a ``storageDir`` other than one used earlier is passed in as a
parameter. (That's a Qt restriction, unfortunately.)
In older versions of QtWebKit, the same-origin policy is a little
too strict which makes it impossible to replicate with databases
that aren't on the same domain as ``baseUrl``. In newer versions, as
long as ``baseUrl`` is file:///, this problem doesn't exist. When
you need to support the older version, you can set this to a domain
so you can at least replicate to one site.
"""
if not storageDir:
storageDir = tempfile.mkdtemp()
_tempDirs.add(storageDir)
if not (storageDir, async, baseUrl) in _envs:
js = _getJs()
try:
ctx = context.JSContext(js, "PouchDB", _IGNORED_ERRORS, storageDir, baseUrl)
except ValueError, e:
if "offlineStoragePath" in str(e): #pragma: no branch
raise EnvironmentError("Can't use the value '%s' for storageDir. It has already been set to something else and, because of a backend restriction, can't be changed." % storageDir)
#this shouldn't happen, it's just there so if it does happen
#during development sometime in the future, the error is
#shown in a nicer way.
raise #pragma: no cover
Environment = AsyncEnvironment if async else SyncEnvironment
_envs[(storageDir, async, baseUrl)] = Environment(ctx)
return _envs[(storageDir, async, baseUrl)]
def _getJs(_cache={}):
if not "js" in _cache:
jsPath = os.path.join(os.path.dirname(__file__), "bundle.js")
with open(jsPath) as f:
_cache["js"] = f.read()
return _cache["js"]
_tempDirs = set()
def _removeTempDirs():#pragma: no cover
"""Not covered because coverage.py can't cover functions only
executed atexit.
"""
for tempDir in _tempDirs:
shutil.rmtree(tempDir)
atexit.register(_removeTempDirs)