From 6f051bcd4879955610d6d2c182b53f09266022e7 Mon Sep 17 00:00:00 2001 From: "aleksey.smaga" Date: Wed, 27 Sep 2017 16:15:05 +0800 Subject: [PATCH 1/7] added filter method for mango query --- couchdb/client.py | 52 ++++++++++++++++++++++++++++++++++ couchdb/query_utils.py | 64 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 couchdb/query_utils.py diff --git a/couchdb/client.py b/couchdb/client.py index 6125e831..814f2c92 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,57 @@ 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')) + >>> db['maryjane'] = dict(type='Person', name='Mary Jane', hobby=dict(main='read')) + >>> db['sillybilly'] = dict(type='Person', name='Silly Billy', hobby=dict(main='read', second='game')) + >>> db['billysilly'] = dict(type='Person', name='Billy Silly', hobby=dict(main='swim', second='horse riding')) + >>> db['gotham'] = dict(type='City', name='Gotham City') + >>> for row in db.filter(name__eq='John Doe') + ... print(row['name']) + John Doe + >>> from couchdb.query_utils import Q + >>> data = db.filter(Q(name__regex='(*UTF)(?i)illy')) + >>> for row in data: + ... print(row['name']) + Silly Billy + Billy Silly + >>> data = db.filter(Q(name__regex='(*UTF)(?i)silly') & Q(hobby__main__eq='read')) + >>> for row in data: # doctest: +SKIP + ... print(row['name']) # doctest: +SKIP + Silly Billy + >>> 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: list of dicts (docs) + """ + 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..ff143182 --- /dev/null +++ b/couchdb/query_utils.py @@ -0,0 +1,64 @@ +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] + 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 From aaf039555c3241c355d6d1186f20d9fdd1d833a5 Mon Sep 17 00:00:00 2001 From: Aleksej Smaga Date: Tue, 3 Oct 2017 23:00:07 +0800 Subject: [PATCH 2/7] added support elemMatch --- couchdb/client.py | 15 +++++++++++---- couchdb/query_utils.py | 6 ++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 814f2c92..8e72a1c1 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -822,10 +822,10 @@ def filter(self, *args, **kwargs): >>> server = Server() >>> db = server.create('python-tests') - >>> db['johndoe'] = dict(type='Person', name='John Doe', hobby=dict(main='read', second='game')) - >>> db['maryjane'] = dict(type='Person', name='Mary Jane', hobby=dict(main='read')) - >>> db['sillybilly'] = dict(type='Person', name='Silly Billy', hobby=dict(main='read', second='game')) - >>> db['billysilly'] = dict(type='Person', name='Billy Silly', hobby=dict(main='swim', second='horse riding')) + >>> 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)) >>> db['gotham'] = dict(type='City', name='Gotham City') >>> for row in db.filter(name__eq='John Doe') ... print(row['name']) @@ -840,6 +840,11 @@ def filter(self, *args, **kwargs): >>> for row in data: # doctest: +SKIP ... print(row['name']) # doctest: +SKIP Silly Billy + >>> data = db.filter(Q(feats_L_elemMatch__feat2__in = [20, 4])) + >>> for row in data: + ... print(row['name']) + John Doe + Mary Jane >>> del server['python-tests'] :param args: Q objects, parse into a query - Request JSON Object selector: @@ -858,6 +863,8 @@ def filter(self, *args, **kwargs): 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) + print(query) + data = self.find(mango_query=query) return data diff --git a/couchdb/query_utils.py b/couchdb/query_utils.py index ff143182..90ee5bcf 100644 --- a/couchdb/query_utils.py +++ b/couchdb/query_utils.py @@ -30,6 +30,12 @@ def P(self): """ 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])) From 18893778de120f0d78c83caa401e88f989802afc Mon Sep 17 00:00:00 2001 From: Aleksej Smaga Date: Thu, 5 Oct 2017 21:42:54 +0800 Subject: [PATCH 3/7] filter doctest fixed --- couchdb/client.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 8e72a1c1..ab4acc6d 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -824,23 +824,22 @@ def filter(self, *args, **kwargs): >>> 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)) - >>> db['gotham'] = dict(type='City', name='Gotham City') - >>> for row in db.filter(name__eq='John Doe') + >>> 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'): ... print(row['name']) John Doe >>> from couchdb.query_utils import Q >>> data = db.filter(Q(name__regex='(*UTF)(?i)illy')) >>> for row in data: ... print(row['name']) - Silly Billy Billy Silly + Silly Billy >>> data = db.filter(Q(name__regex='(*UTF)(?i)silly') & Q(hobby__main__eq='read')) - >>> for row in data: # doctest: +SKIP - ... print(row['name']) # doctest: +SKIP + >>> for row in data: + ... print(row['name']) Silly Billy - >>> data = db.filter(Q(feats_L_elemMatch__feat2__in = [20, 4])) + >>> data = db.filter(feats_L_elemMatch__feat2__in = [20, 4]) >>> for row in data: ... print(row['name']) John Doe @@ -863,8 +862,6 @@ def filter(self, *args, **kwargs): 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) - print(query) - data = self.find(mango_query=query) return data From 645fac77b126b15f7e85773d38faf406a8b514cd Mon Sep 17 00:00:00 2001 From: Aleksej Smaga Date: Thu, 5 Oct 2017 21:59:43 +0800 Subject: [PATCH 4/7] filter doctest fixed 2 --- couchdb/client.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index ab4acc6d..24d6ef33 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -829,18 +829,16 @@ def filter(self, *args, **kwargs): >>> for row in db.filter(name__eq='John Doe'): ... print(row['name']) John Doe - >>> from couchdb.query_utils import Q - >>> data = db.filter(Q(name__regex='(*UTF)(?i)illy')) + >>> data = db.filter(name__regex='(*UTF)(?i)illy') >>> for row in data: ... print(row['name']) Billy Silly Silly Billy - >>> data = db.filter(Q(name__regex='(*UTF)(?i)silly') & Q(hobby__main__eq='read')) - >>> for row in data: + >>> from couchdb.query_utils import Q + >>> for row in db.filter(Q(name__regex='(*UTF)(?i)silly') & Q(hobby__main__eq='read')): ... print(row['name']) Silly Billy - >>> data = db.filter(feats_L_elemMatch__feat2__in = [20, 4]) - >>> for row in data: + >>> for row in db.filter(feats_L_elemMatch__feat2__in = [20, 4]): ... print(row['name']) John Doe Mary Jane From 06359b6ecff0354934f91a558c4c72daf15e575b Mon Sep 17 00:00:00 2001 From: Aleksej Smaga Date: Thu, 5 Oct 2017 22:18:30 +0800 Subject: [PATCH 5/7] filter doctest fixed 3 --- couchdb/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchdb/client.py b/couchdb/client.py index 24d6ef33..bb3193a2 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -848,7 +848,7 @@ def filter(self, *args, **kwargs): 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: list of dicts (docs) + :return: the query results as a list of `Document` """ limit = kwargs.pop('limit', 30) fields = kwargs.pop('fields', []) From 5c02ef56c5f22d0c622a25246fcc2564c0197a89 Mon Sep 17 00:00:00 2001 From: Aleksej Smaga Date: Sat, 7 Oct 2017 11:48:26 +0800 Subject: [PATCH 6/7] filter doctest fixed 4 --- couchdb/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index bb3193a2..08fbf3ca 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -829,8 +829,7 @@ def filter(self, *args, **kwargs): >>> for row in db.filter(name__eq='John Doe'): ... print(row['name']) John Doe - >>> data = db.filter(name__regex='(*UTF)(?i)illy') - >>> for row in data: + >>> for row in db.filter(name__regex='(*UTF)(?i)illy'): ... print(row['name']) Billy Silly Silly Billy From 28c55f1eea5d660e6e4cb2614663fbb80d32bb3e Mon Sep 17 00:00:00 2001 From: al Date: Sun, 15 Oct 2017 14:51:33 +0800 Subject: [PATCH 7/7] added SKIP for doctest --- couchdb/client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 08fbf3ca..8d5f7beb 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -826,19 +826,19 @@ def filter(self, *args, **kwargs): >>> 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'): - ... print(row['name']) + >>> 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'): - ... print(row['name']) + >>> 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')): - ... print(row['name']) + >>> 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]): - ... print(row['name']) + >>> 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']