diff --git a/couchdb/client.py b/couchdb/client.py index 6125e831..8d5f7beb 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -34,6 +34,7 @@ import socket from couchdb import http, json, util +from couchdb.query_utils import Q __all__ = ['Server', 'Database', 'Document', 'ViewResults', 'Row'] __docformat__ = 'restructuredtext en' @@ -811,6 +812,58 @@ def find(self, mango_query, wrapper=None): status, headers, data = self.resource.post_json('_find', mango_query) return map(wrapper or Document, data.get('docs', [])) + def filter(self, *args, **kwargs): + """Execute a mango find-query against the database. + + Note: only available for CouchDB version >= 2.0.0 + + More information on the `mango_query` structure can be found here: + http://docs.couchdb.org/en/master/api/database/find.html#find-selectors + + >>> server = Server() + >>> db = server.create('python-tests') + >>> db['johndoe'] = dict(type='Person', name='John Doe', hobby=dict(main='read', second='game'), feats=[dict(feat1=1, feat2=2), dict(feat1=10, feat2=20)]) + >>> db['maryjane'] = dict(type='Person', name='Mary Jane', hobby=dict(main='read'), feats=[dict(feat1=3, feat2=4), dict(feat1=30, feat2=40)]) + >>> db['sillybilly'] = dict(type='Person', name='Silly Billy', hobby=dict(main='read', second='game'), feats=[dict(feat1=5, feat2=6), dict(feat1=50, feat2=60)]) + >>> db['billysilly'] = dict(type='Person', name='Billy Silly', hobby=dict(main='swim', second='horse riding'), feats=[dict(feat1=1, feat2=2), dict(feat1=30, feat2=60)]) + >>> for row in db.filter(name__eq='John Doe'): # doctest: +SKIP + ... print(row['name']) # doctest: +SKIP + John Doe + >>> for row in db.filter(name__regex='(*UTF)(?i)illy'): # doctest: +SKIP + ... print(row['name']) # doctest: +SKIP + Billy Silly + Silly Billy + >>> from couchdb.query_utils import Q + >>> for row in db.filter(Q(name__regex='(*UTF)(?i)silly') & Q(hobby__main__eq='read')): # doctest: +SKIP + ... print(row['name']) # doctest: +SKIP + Silly Billy + >>> for row in db.filter(feats_L_elemMatch__feat2__in = [20, 4]): # doctest: +SKIP + ... print(row['name']) # doctest: +SKIP + John Doe + Mary Jane + >>> del server['python-tests'] + + :param args: Q objects, parse into a query - Request JSON Object selector: + http://docs.couchdb.org/en/master/api/database/find.html#post--db-_find + :param kwargs: Request JSON Object: + http://docs.couchdb.org/en/master/api/database/find.html#post--db-_find + :return: the query results as a list of `Document` + """ + limit = kwargs.pop('limit', 30) + fields = kwargs.pop('fields', []) + sort = kwargs.pop('sort', []) + if args: + selector = args[0].P + elif kwargs: + selector = Q(**kwargs).P + else: + raise Exception('Need query, for example: name__eq = "Name" or used Q for complex conditions') + query = dict(selector=selector, limit=limit, fields=fields, sort=sort) + data = self.find(mango_query=query) + return data + + + def explain(self, mango_query): """Explain a mango find-query. diff --git a/couchdb/query_utils.py b/couchdb/query_utils.py new file mode 100644 index 00000000..90ee5bcf --- /dev/null +++ b/couchdb/query_utils.py @@ -0,0 +1,70 @@ +try: + from functools import reduce +except ImportError: + pass + +''' +maybe overwrite +''' + +class Q: + """ + Q-object need for creating mango_query in filter function + """ + OR = '$or' + AND = '$and' + + def __init__(self, **kwarg): + """ + :param kwarg: field__subfield1__...__mango_operator = value + separate by double underscore - '__' + """ + self.field_cond = list(kwarg.keys())[0] + self.val = list(kwarg.values())[0] + + @property + def P(self): + """ + P - parser + :return: dict for query + """ + field_cond_list = self.field_cond.split('__') + field = field_cond_list[:-1] + for i, subf in enumerate(field): + if '_L_' in subf: + subf_list = subf.split('_L_') + field.insert(i + 1, '$' + subf_list[1]) + field[i] = subf_list[0] + + cond = '$' + field_cond_list[-1] + parsed_field_cond = reduce(lambda x, y: {y: x}, + reversed(field + [cond, self.val])) + return parsed_field_cond + + def _combine(self, other, cond): + """ + :param other: Q or _FQ objects + :param cond: or, and + :return: _FQ object + """ + query = dict() + query[cond] = [self.P, other.P] + return _FQ(query) + + def __or__(self, other): + return self._combine(other, self.OR) + + def __and__(self, other): + return self._combine(other, self.AND) + + +class _FQ(Q): + ''' + subobject need for _combine method in Q + ''' + def __init__(self, val): + self.val = val + + @property + def P(self): + return self.val \ No newline at end of file