From acbfded8d6bcb6f35f8aba425ba82aac7634e70c Mon Sep 17 00:00:00 2001 From: mefyl Date: Mon, 10 Aug 2015 14:07:36 +0200 Subject: [PATCH 1/6] Fix view server binary versus text error on python 3.4. --- couchdb/view.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/couchdb/view.py b/couchdb/view.py index 0bb7e315..8811da24 100755 --- a/couchdb/view.py +++ b/couchdb/view.py @@ -34,10 +34,14 @@ def run(input=sys.stdin, output=sys.stdout): def _writejson(obj): obj = json.encode(obj) - if isinstance(obj, util.utype): - obj = obj.encode('utf-8') - output.write(obj) - output.write(b'\n') + if hasattr(output, 'encoding'): + output.write(obj) + output.write('\n') + else: + if isinstance(obj, util.utype): + obj = obj.encode('utf-8') + output.write(obj) + output.write(b'\n') output.flush() def _log(message): From 9690aa32723d2c5b9ae6c278f4cf8df00aad4c22 Mon Sep 17 00:00:00 2001 From: mefyl Date: Wed, 12 Aug 2015 11:33:37 +0200 Subject: [PATCH 2/6] Extract functions compilation routine. --- couchdb/view.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/couchdb/view.py b/couchdb/view.py index 8811da24..574fb3cf 100755 --- a/couchdb/view.py +++ b/couchdb/view.py @@ -53,7 +53,7 @@ def reset(config=None): del functions[:] return True - def add_fun(string): + def compile_fun(string): string = BOM_UTF8 + string.encode('utf-8') globals_ = {} try: @@ -73,7 +73,10 @@ def add_fun(string): function = list(globals_.values())[0] if type(function) is not FunctionType: return err - functions.append(function) + return function + + def add_fun(string): + functions.append(compile_fun(string)) return True def map_doc(doc): From 63633871415244ad5ee56131668a14ba55af1a62 Mon Sep 17 00:00:00 2001 From: mefyl Date: Wed, 12 Aug 2015 12:46:26 +0200 Subject: [PATCH 3/6] Add basic updates handler support. --- couchdb/tests/view.py | 51 +++++++++++++++++++++++++++++++++++++++++++ couchdb/view.py | 30 +++++++++++++++++++++++-- doc/views.rst | 21 ++++++++++++++---- 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/couchdb/tests/view.py b/couchdb/tests/view.py index 79a18dd9..e92f7882 100644 --- a/couchdb/tests/view.py +++ b/couchdb/tests/view.py @@ -102,6 +102,57 @@ def test_reduce_empty(self): self.assertEqual(output.getvalue(), b'[true, [0]]\n') + def test_update(self): + import json + commands = [ + [ + 'ddoc', + 'new', + '_design/test_update', + { + '_id': '_design/test_update', + '_rev': '8-d7379de23a751dc2a19e5638a7bbc5cc', + 'language': 'python', + 'updates': { + 'inc': { + 'map': '''\ +def fun(obj, req): + if obj is not None: + obj['field'] += 1 + return [obj, {"body": "."}] +''', + } + } + }, + ], + [ + 'ddoc', + '_design/test_update', + ['updates', 'inc'], + [None, {}] + ], + [ + 'ddoc', + '_design/test_update', + ['updates', 'inc'], + [{'field': 41, 'other_field': 'x'}, {}] + ], + ] + input = StringIO(b'\n'.join(json.dumps(c).encode('utf-8') + for c in commands)) + output = StringIO() + view.run(input=input, output=output) + results = [ + json.loads(l.decode('utf-8')) + for l in output.getvalue().strip().split(b'\n') + ] + self.assertEqual(len(results), 3) + self.assertEqual(results[0], True) + self.assertEqual(results[1], ['up', None, {'body': '.'}]) + self.assertEqual( + results[2], + ['up', {'field': 42, 'other_field': 'x'}, {'body': '.'}]) + def suite(): suite = unittest.TestSuite() diff --git a/couchdb/view.py b/couchdb/view.py index 574fb3cf..97ba81a2 100755 --- a/couchdb/view.py +++ b/couchdb/view.py @@ -23,6 +23,7 @@ log = logging.getLogger('couchdb.view') +ddocs = {} def run(input=sys.stdin, output=sys.stdout): r"""CouchDB view function handler implementation for Python. @@ -135,8 +136,33 @@ def rereduce(*cmd): # Note: weird kwargs is for Python 2.5 compat return reduce(*cmd, **{'rereduce': True}) - handlers = {'reset': reset, 'add_fun': add_fun, 'map_doc': map_doc, - 'reduce': reduce, 'rereduce': rereduce} + def ddoc(*cmd): + if cmd[0] == 'new': + ddoc = cmd[2] + ddoc['updates'] = dict( + (name, {'map': compile_fun(value['map'])}) + for name, value in ddoc['updates'].items()) + ddocs[cmd[1]] = ddoc + return True + else: + ddoc = ddocs[cmd[0]] + action = cmd[1] + if action[0] == 'updates': + fun = ddoc['updates'][action[1]]['map'] + doc, body = fun(*cmd[2]) + res = ['up', doc, body] + sys.stderr.flush() + return res + + + handlers = { + 'add_fun': add_fun, + 'ddoc': ddoc, + 'map_doc': map_doc, + 'reduce': reduce, + 'rereduce': rereduce, + 'reset': reset, + } try: while True: diff --git a/doc/views.rst b/doc/views.rst index 6fe913ff..973499ed 100644 --- a/doc/views.rst +++ b/doc/views.rst @@ -1,10 +1,11 @@ Writing views in Python ======================= -The couchdb-python package comes with a view server to allow you to write -views in Python instead of JavaScript. When couchdb-python is installed, it -will install a script called couchpy that runs the view server. To enable -this for your CouchDB server, add the following section to local.ini:: +The couchdb-python package comes with a query server to allow you to +write views or update handlers in Python instead of JavaScript. When +couchdb-python is installed, it will install a script called couchpy +that runs the view server. To enable this for your CouchDB server, add +the following section to local.ini:: [query_servers] python=/usr/bin/couchpy @@ -18,3 +19,15 @@ the language pull-down menu. Here's some sample view code to get you started:: Note that the ``map`` function uses the Python ``yield`` keyword to emit values, where JavaScript views use an ``emit()`` function. + +Here's an example update handler code that will increment the +``field`` member of an existing document. The name of the ``field`` to +increment is passed in the query string:: + + def fun(obj, req): + field = req.query['field'] + if obj is not None and field in obj: + obj[field] += 1 + return [obj, {"body": "incremented"}] + else: + return [None, {"body": "no such document or field"}] From 5ac4bfa9f322f10e07caeabb6953b3b25011d16b Mon Sep 17 00:00:00 2001 From: mefyl Date: Wed, 12 Aug 2015 14:42:48 +0200 Subject: [PATCH 4/6] Remove erroneous map keys expected in update handlers. As reported by Alexander Shorin. --- couchdb/tests/view.py | 4 +--- couchdb/view.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/couchdb/tests/view.py b/couchdb/tests/view.py index e92f7882..eb0b7b3d 100644 --- a/couchdb/tests/view.py +++ b/couchdb/tests/view.py @@ -114,14 +114,12 @@ def test_update(self): '_rev': '8-d7379de23a751dc2a19e5638a7bbc5cc', 'language': 'python', 'updates': { - 'inc': { - 'map': '''\ + 'inc': '''\ def fun(obj, req): if obj is not None: obj['field'] += 1 return [obj, {"body": "."}] ''', - } } }, ], diff --git a/couchdb/view.py b/couchdb/view.py index 97ba81a2..aef1ef79 100755 --- a/couchdb/view.py +++ b/couchdb/view.py @@ -140,7 +140,7 @@ def ddoc(*cmd): if cmd[0] == 'new': ddoc = cmd[2] ddoc['updates'] = dict( - (name, {'map': compile_fun(value['map'])}) + (name, compile_fun(value)) for name, value in ddoc['updates'].items()) ddocs[cmd[1]] = ddoc return True @@ -148,7 +148,7 @@ def ddoc(*cmd): ddoc = ddocs[cmd[0]] action = cmd[1] if action[0] == 'updates': - fun = ddoc['updates'][action[1]]['map'] + fun = ddoc['updates'][action[1]] doc, body = fun(*cmd[2]) res = ['up', doc, body] sys.stderr.flush() From 651acd95699eae2a6afb92bd7821b750a395f100 Mon Sep 17 00:00:00 2001 From: mefyl Date: Wed, 12 Aug 2015 14:58:34 +0200 Subject: [PATCH 5/6] Expect POST when calling update handlers. --- couchdb/tests/view.py | 71 ++++++++++++++++++++++++++++++++----------- couchdb/view.py | 15 ++++++--- 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/couchdb/tests/view.py b/couchdb/tests/view.py index eb0b7b3d..e6b05578 100644 --- a/couchdb/tests/view.py +++ b/couchdb/tests/view.py @@ -102,38 +102,46 @@ def test_reduce_empty(self): self.assertEqual(output.getvalue(), b'[true, [0]]\n') - def test_update(self): - import json - commands = [ - [ - 'ddoc', - 'new', - '_design/test_update', - { - '_id': '_design/test_update', - '_rev': '8-d7379de23a751dc2a19e5638a7bbc5cc', - 'language': 'python', - 'updates': { - 'inc': '''\ + def command_ddoc_add(self): + return [ + 'ddoc', + 'new', + '_design/test_update', + { + '_id': '_design/test_update', + '_rev': '8-d7379de23a751dc2a19e5638a7bbc5cc', + 'language': 'python', + 'updates': { + 'inc': '''\ def fun(obj, req): if obj is not None: obj['field'] += 1 return [obj, {"body": "."}] ''', - } - }, - ], + } + }, + ] + + def test_update(self): + import json + request = { + 'method': 'POST', + 'query': {}, + 'body': '', + } + commands = [ + self.command_ddoc_add(), [ 'ddoc', '_design/test_update', ['updates', 'inc'], - [None, {}] + [None, request] ], [ 'ddoc', '_design/test_update', ['updates', 'inc'], - [{'field': 41, 'other_field': 'x'}, {}] + [{'field': 41, 'other_field': 'x'}, request] ], ] input = StringIO(b'\n'.join(json.dumps(c).encode('utf-8') @@ -151,6 +159,33 @@ def fun(obj, req): results[2], ['up', {'field': 42, 'other_field': 'x'}, {'body': '.'}]) + def test_update_wrong_method(self): + import json + request = { + 'method': 'GET', + 'query': {}, + 'body': '', + } + commands = [ + self.command_ddoc_add(), + [ + 'ddoc', + '_design/test_update', + ['updates', 'inc'], + [None, request] + ], + ] + input = StringIO(b'\n'.join(json.dumps(c).encode('utf-8') + for c in commands)) + output = StringIO() + view.run(input=input, output=output) + results = [ + json.loads(l.decode('utf-8')) + for l in output.getvalue().strip().split(b'\n') + ] + self.assertEqual(len(results), 2) + self.assertEqual(results[1][2]['status'], 405) + def suite(): suite = unittest.TestSuite() diff --git a/couchdb/view.py b/couchdb/view.py index aef1ef79..5e77b061 100755 --- a/couchdb/view.py +++ b/couchdb/view.py @@ -148,10 +148,17 @@ def ddoc(*cmd): ddoc = ddocs[cmd[0]] action = cmd[1] if action[0] == 'updates': - fun = ddoc['updates'][action[1]] - doc, body = fun(*cmd[2]) - res = ['up', doc, body] - sys.stderr.flush() + if cmd[2][1]['method'] != 'POST': + return [ + 'up', + None, + {'status': 405, 'body': 'Method not allowed'}, + ] + else: + fun = ddoc['updates'][action[1]] + doc, body = fun(*cmd[2]) + res = ['up', doc, body] + sys.stderr.flush() return res From 7ec2ecde2e9cfbc744aeea1cca0313fc832a5a0e Mon Sep 17 00:00:00 2001 From: mefyl Date: Wed, 19 Aug 2015 10:29:06 +0200 Subject: [PATCH 6/6] Accept PUT to call update handlers in query server. --- couchdb/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchdb/view.py b/couchdb/view.py index 5e77b061..6d2d4da6 100755 --- a/couchdb/view.py +++ b/couchdb/view.py @@ -148,7 +148,7 @@ def ddoc(*cmd): ddoc = ddocs[cmd[0]] action = cmd[1] if action[0] == 'updates': - if cmd[2][1]['method'] != 'POST': + if cmd[2][1]['method'] not in ('POST', 'PUT'): return [ 'up', None,