1#
2# Copyright 2008-2010 Zuza Software Foundation
3#
4# This file is part of translate.
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, see <http://www.gnu.org/licenses/>.
18
19"""A translation memory server using tmdb for storage, communicates with
20clients using JSON over HTTP.
21"""
22
23import json
24import logging
25from argparse import ArgumentParser
26from io import BytesIO
27from urllib import parse
28
29from translate.misc import selector, wsgi
30from translate.storage import base, tmdb
31
32
33class TMServer:
34    """A RESTful JSON TM server."""
35
36    def __init__(
37        self,
38        tmdbfile,
39        tmfiles,
40        max_candidates=3,
41        min_similarity=75,
42        max_length=1000,
43        prefix="",
44        source_lang=None,
45        target_lang=None,
46    ):
47        if not isinstance(tmdbfile, str):
48            import sys
49
50            tmdbfile = tmdbfile.decode(sys.getfilesystemencoding())
51
52        self.tmdb = tmdb.TMDB(tmdbfile, max_candidates, min_similarity, max_length)
53
54        if tmfiles:
55            self._load_files(tmfiles, source_lang, target_lang)
56
57        # initialize url dispatcher
58        self.rest = selector.Selector(prefix=prefix)
59        self.rest.add(
60            "/{slang}/{tlang}/unit/{uid:any}",
61            GET=self.translate_unit,
62            POST=self.update_unit,
63            PUT=self.add_unit,
64            DELETE=self.forget_unit,
65        )
66
67        self.rest.add(
68            "/{slang}/{tlang}/store/{sid:any}",
69            GET=self.get_store_stats,
70            PUT=self.upload_store,
71            POST=self.add_store,
72            DELETE=self.forget_store,
73        )
74
75    def _load_files(self, tmfiles, source_lang, target_lang):
76        from translate.storage import factory
77
78        if isinstance(tmfiles, list):
79            [
80                self.tmdb.add_store(factory.getobject(tmfile), source_lang, target_lang)
81                for tmfile in tmfiles
82            ]
83        elif tmfiles:
84            self.tmdb.add_store(factory.getobject(tmfiles), source_lang, target_lang)
85
86    @selector.opliant
87    def translate_unit(self, environ, start_response, uid, slang, tlang):
88        start_response("200 OK", [("Content-type", "text/plain")])
89        candidates = self.tmdb.translate_unit(uid, slang, tlang)
90        logging.debug("candidates: %s", str(candidates))
91        response = json.dumps(candidates, indent=4).encode("utf-8")
92        params = parse.parse_qs(environ.get("QUERY_STRING", ""))
93        try:
94            callback = params.get("callback", [])[0]
95            response = f"{callback}({response})"
96        except IndexError:
97            pass
98        return [response]
99
100    @selector.opliant
101    def add_unit(self, environ, start_response, uid, slang, tlang):
102        start_response("200 OK", [("Content-type", "text/plain")])
103        # uid = unicode(urllib.unquote_plus(uid), "utf-8")
104        data = json.loads(environ["wsgi.input"].read(int(environ["CONTENT_LENGTH"])))
105        unit = base.TranslationUnit(data["source"])
106        unit.target = data["target"]
107        self.tmdb.add_unit(unit, slang, tlang)
108        return [""]
109
110    @selector.opliant
111    def update_unit(self, environ, start_response, uid, slang, tlang):
112        start_response("200 OK", [("Content-type", "text/plain")])
113        # uid = unicode(urllib.unquote_plus(uid), "utf-8")
114        data = json.loads(environ["wsgi.input"].read(int(environ["CONTENT_LENGTH"])))
115        unit = base.TranslationUnit(data["source"])
116        unit.target = data["target"]
117        self.tmdb.add_unit(unit, slang, tlang)
118        return [""]
119
120    @selector.opliant
121    def forget_unit(self, environ, start_response, uid):
122        # FIXME: implement me
123        start_response("200 OK", [("Content-type", "text/plain")])
124        # uid = unicode(urllib.unquote_plus(uid), "utf-8")
125
126        return ["FIXME"]
127
128    @selector.opliant
129    def get_store_stats(self, environ, start_response, sid):
130        # FIXME: implement me
131        start_response("200 OK", [("Content-type", "text/plain")])
132        # sid = unicode(urllib.unquote_plus(sid), "utf-8")
133
134        return ["FIXME"]
135
136    @selector.opliant
137    def upload_store(self, environ, start_response, sid, slang, tlang):
138        """add units from uploaded file to tmdb"""
139        from translate.storage import factory
140
141        start_response("200 OK", [("Content-type", "text/plain")])
142        data = BytesIO(environ["wsgi.input"].read(int(environ["CONTENT_LENGTH"])))
143        data.name = sid
144        store = factory.getobject(data)
145        count = self.tmdb.add_store(store, slang, tlang)
146        response = "added %d units from %s" % (count, sid)
147        return [response]
148
149    @selector.opliant
150    def add_store(self, environ, start_response, sid, slang, tlang):
151        """Add unit from POST data to tmdb."""
152        start_response("200 OK", [("Content-type", "text/plain")])
153        units = json.loads(environ["wsgi.input"].read(int(environ["CONTENT_LENGTH"])))
154        count = self.tmdb.add_list(units, slang, tlang)
155        response = "added %d units from %s" % (count, sid)
156        return [response]
157
158    @selector.opliant
159    def forget_store(self, environ, start_response, sid):
160        # FIXME: implement me
161        start_response("200 OK", [("Content-type", "text/plain")])
162        # sid = unicode(urllib.unquote_plus(sid), "utf-8")
163
164        return ["FIXME"]
165
166
167def main():
168    parser = ArgumentParser()
169    parser.add_argument(
170        "-d",
171        "--tmdb",
172        dest="tmdbfile",
173        default=":memory:",
174        help="translation memory database file",
175    )
176    parser.add_argument(
177        "-f",
178        "--import-translation-file",
179        dest="tmfiles",
180        action="append",
181        help="translation file to import into the database",
182    )
183    parser.add_argument(
184        "-t",
185        "--import-target-lang",
186        dest="target_lang",
187        help="target language of translation files",
188    )
189    parser.add_argument(
190        "-s",
191        "--import-source-lang",
192        dest="source_lang",
193        help="source language of translation files",
194    )
195    parser.add_argument(
196        "-b",
197        "--bind",
198        dest="bind",
199        default="localhost",
200        help="address to bind server to (default: %(default)s)",
201    )
202    parser.add_argument(
203        "-p",
204        "--port",
205        dest="port",
206        type=int,
207        default=8888,
208        help="port to listen on (default: %(default)s)",
209    )
210    parser.add_argument(
211        "--max-candidates",
212        dest="max_candidates",
213        type=int,
214        default=3,
215        help="Maximum number of candidates",
216    )
217    parser.add_argument(
218        "--min-similarity",
219        dest="min_similarity",
220        type=int,
221        default=75,
222        help="minimum similarity",
223    )
224    parser.add_argument(
225        "--max-length",
226        dest="max_length",
227        type=int,
228        default=1000,
229        help="Maxmimum string length",
230    )
231    parser.add_argument(
232        "--debug",
233        action="store_true",
234        dest="debug",
235        default=False,
236        help="enable debugging features",
237    )
238
239    args = parser.parse_args()
240
241    # setup debugging
242    format = "%(asctime)s %(levelname)s %(message)s"
243    level = args.debug and logging.DEBUG or logging.WARNING
244    if args.debug:
245        format = "%(levelname)7s %(module)s.%(funcName)s:%(lineno)d: %(message)s"
246
247    logging.basicConfig(level=level, format=format)
248
249    application = TMServer(
250        args.tmdbfile,
251        args.tmfiles,
252        max_candidates=args.max_candidates,
253        min_similarity=args.min_similarity,
254        max_length=args.max_length,
255        prefix="/tmserver",
256        source_lang=args.source_lang,
257        target_lang=args.target_lang,
258    )
259    wsgi.launch_server(args.bind, args.port, application.rest)
260
261
262if __name__ == "__main__":
263    main()
264