1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2007-2008 Christopher Lenz
5# All rights reserved.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution.
9
10"""Implementation of a view server for functions written in Python."""
11
12from codecs import BOM_UTF8
13import logging
14import os
15import sys
16import traceback
17from types import FunctionType
18
19from couchdb import json
20
21__all__ = ['main', 'run']
22__docformat__ = 'restructuredtext en'
23
24log = logging.getLogger('couchdb.view')
25
26
27def run(input=sys.stdin, output=sys.stdout):
28    r"""CouchDB view function handler implementation for Python.
29
30    :param input: the readable file-like object to read input from
31    :param output: the writable file-like object to write output to
32    """
33    functions = []
34
35    def _writejson(obj):
36        obj = json.encode(obj)
37        if isinstance(obj, unicode):
38            obj = obj.encode('utf-8')
39        output.write(obj)
40        output.write('\n')
41        output.flush()
42
43    def _log(message):
44        if not isinstance(message, basestring):
45            message = json.encode(message)
46        _writejson({'log': message})
47
48    def reset(config=None):
49        del functions[:]
50        return True
51
52    def add_fun(string):
53        string = BOM_UTF8 + string.encode('utf-8')
54        globals_ = {}
55        try:
56            exec string in {'log': _log}, globals_
57        except Exception, e:
58            return {'error': {
59                'id': 'map_compilation_error',
60                'reason': e.args[0]
61            }}
62        err = {'error': {
63            'id': 'map_compilation_error',
64            'reason': 'string must eval to a function '
65                      '(ex: "def(doc): return 1")'
66        }}
67        if len(globals_) != 1:
68            return err
69        function = globals_.values()[0]
70        if type(function) is not FunctionType:
71            return err
72        functions.append(function)
73        return True
74
75    def map_doc(doc):
76        results = []
77        for function in functions:
78            try:
79                results.append([[key, value] for key, value in function(doc)])
80            except Exception, e:
81                log.error('runtime error in map function: %s', e,
82                          exc_info=True)
83                results.append([])
84                _log(traceback.format_exc())
85        return results
86
87    def reduce(*cmd, **kwargs):
88        code = BOM_UTF8 + cmd[0][0].encode('utf-8')
89        args = cmd[1]
90        globals_ = {}
91        try:
92            exec code in {'log': _log}, globals_
93        except Exception, e:
94            log.error('runtime error in reduce function: %s', e,
95                      exc_info=True)
96            return {'error': {
97                'id': 'reduce_compilation_error',
98                'reason': e.args[0]
99            }}
100        err = {'error': {
101            'id': 'reduce_compilation_error',
102            'reason': 'string must eval to a function '
103                      '(ex: "def(keys, values): return 1")'
104        }}
105        if len(globals_) != 1:
106            return err
107        function = globals_.values()[0]
108        if type(function) is not FunctionType:
109            return err
110
111        rereduce = kwargs.get('rereduce', False)
112        results = []
113        if rereduce:
114            keys = None
115            vals = args
116        else:
117            if args:
118                keys, vals = zip(*args)
119            else:
120                keys, vals = [], []
121        if function.func_code.co_argcount == 3:
122            results = function(keys, vals, rereduce)
123        else:
124            results = function(keys, vals)
125        return [True, [results]]
126
127    def rereduce(*cmd):
128        # Note: weird kwargs is for Python 2.5 compat
129        return reduce(*cmd, **{'rereduce': True})
130
131    handlers = {'reset': reset, 'add_fun': add_fun, 'map_doc': map_doc,
132                'reduce': reduce, 'rereduce': rereduce}
133
134    try:
135        while True:
136            line = input.readline()
137            if not line:
138                break
139            try:
140                cmd = json.decode(line)
141                log.debug('Processing %r', cmd)
142            except ValueError, e:
143                log.error('Error: %s', e, exc_info=True)
144                return 1
145            else:
146                retval = handlers[cmd[0]](*cmd[1:])
147                log.debug('Returning  %r', retval)
148                _writejson(retval)
149    except KeyboardInterrupt:
150        return 0
151    except Exception, e:
152        log.error('Error: %s', e, exc_info=True)
153        return 1
154
155
156_VERSION = """%(name)s - CouchDB Python %(version)s
157
158Copyright (C) 2007 Christopher Lenz <cmlenz@gmx.de>.
159"""
160
161_HELP = """Usage: %(name)s [OPTION]
162
163The %(name)s command runs the CouchDB Python view server.
164
165The exit status is 0 for success or 1 for failure.
166
167Options:
168
169  --version             display version information and exit
170  -h, --help            display a short help message and exit
171  --json-module=<name>  set the JSON module to use ('simplejson', 'cjson',
172                        or 'json' are supported)
173  --log-file=<file>     name of the file to write log messages to, or '-' to
174                        enable logging to the standard error stream
175  --debug               enable debug logging; requires --log-file to be
176                        specified
177
178Report bugs via the web at <http://code.google.com/p/couchdb-python>.
179"""
180
181
182def main():
183    """Command-line entry point for running the view server."""
184    import getopt
185    from couchdb import __version__ as VERSION
186
187    try:
188        option_list, argument_list = getopt.gnu_getopt(
189            sys.argv[1:], 'h',
190            ['version', 'help', 'json-module=', 'debug', 'log-file=']
191        )
192
193        message = None
194        for option, value in option_list:
195            if option in ('--version'):
196                message = _VERSION % dict(name=os.path.basename(sys.argv[0]),
197                                      version=VERSION)
198            elif option in ('-h', '--help'):
199                message = _HELP % dict(name=os.path.basename(sys.argv[0]))
200            elif option in ('--json-module'):
201                json.use(module=value)
202            elif option in ('--debug'):
203                log.setLevel(logging.DEBUG)
204            elif option in ('--log-file'):
205                if value == '-':
206                    handler = logging.StreamHandler(sys.stderr)
207                    handler.setFormatter(logging.Formatter(
208                        ' -> [%(levelname)s] %(message)s'
209                    ))
210                else:
211                    handler = logging.FileHandler(value)
212                    handler.setFormatter(logging.Formatter(
213                        '[%(asctime)s] [%(levelname)s] %(message)s'
214                    ))
215                log.addHandler(handler)
216        if message:
217            sys.stdout.write(message)
218            sys.stdout.flush()
219            sys.exit(0)
220
221    except getopt.GetoptError, error:
222        message = '%s\n\nTry `%s --help` for more information.\n' % (
223            str(error), os.path.basename(sys.argv[0])
224        )
225        sys.stderr.write(message)
226        sys.stderr.flush()
227        sys.exit(1)
228
229    sys.exit(run())
230
231
232if __name__ == '__main__':
233    main()
234