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