Skip to content
Snippets Groups Projects
Commit f62a61f3 authored by Tomasz Walkowiak's avatar Tomasz Walkowiak
Browse files

initial commit

parents
No related branches found
No related tags found
No related merge requests found
[service]
tool = omwn
rabbit_host = 10.17.0.85
rabbit_user = clarin
rabbit_password = clarin123
[tool]
workers_number = 1
[logging]
port = 9987
local_log_level = INFO
[logging_levels]
__main__ = INFO
File added
from ._service import *
from ._worker import *
File added
from __future__ import (
absolute_import,
unicode_literals,
division,
print_function,
)
import io
import json
import logging
from logging.handlers import SocketHandler
from multiprocessing import Process
import os
import time
import pika
import six
from six.moves import xrange, input, configparser
from . import logserver
from ._worker import LexWorker
__all__ = 'LexService',
# This global variable would not work on Windows, but then, the logging
# configuration as it is not would not work on Windows.
_log = logging.getLogger(__name__)
class LexService(object):
"""
Represents a web service using a :class:`LexWorker` subclass connected to a
RabbitMQ queue that receives tasks from the queue.
TODO: This docstring should probably be extended with more description of
the service system.
Each service is constructed using a configuration dictionary in the
following format::
{
"service": {
"root": "/samba/requests/",
"tool": "some-tool-name",
"rabbit_host": "0.0.0.0",
"rabbit_user": "admin",
"rabbit_password": "admin"
},
"tool": {
"workers_number": 10
},
"logging": {
"port": 4040
}
}
The meaning of the options are as follows:
* ``service/root``: The root directory where input and output directories
for the service are.
* ``service/tool``: The name of the created service (will be used to
identify directories and queues).
* ``service/rabbit_host``: IP address of the host where the RabbitMQ server
is running.
* ``service/rabbit_user`` and ``service/rabbit_password``: Credentials for
interacting with the RabbitMQ server.
* ``tool/workers_number``: Number of concurrent worker processes started
by this service. If this value is absent, it will default to 1.
* ``logging/port``: Number of port on which logging server will be
listening. This port is passed to worker initialization so that it can
set its logger to sent records to a central location.
The ``logging`` section also accepts options with the same names and
semantics as arguments to :func:`logserver.configure_loggers`, to which
they're passed if present.
A wholly optional ``logging_levels`` section can be present, mapping logger
names to their desired levels (either numeric or textual). This is
interpreted only if ``logging/port`` is present and valid.
The config dictionary may contain other values as well. It will be passed
whole to the worker class during initialization.
"""
CFG_S_SERV = 'service'
CFG_S_SERV_O_ROOT = 'root'
CFG_S_SERV_O_TOOL = 'tool'
CFG_S_SERV_O_RHOST = 'rabbit_host'
CFG_S_SERV_O_RUSER = 'rabbit_user'
CFG_S_SERV_O_RPSWD = 'rabbit_password'
CFG_S_TOOL = 'tool'
CFG_S_TOOL_O_NUMW = 'workers_number'
CFG_S_LOG = 'logging'
CFG_S_LOG_O_PORT = 'port'
CFG_S_LOG_O_FILENAME = 'logfile_name'
CFG_S_LOG_O_FILESIZE = 'logfile_maxsize'
CFG_S_LOG_O_FILECOUNT = 'logfile_maxbackups'
CFG_S_LOG_O_FORMAT = 'log_format'
CFG_S_LOG_O_LEVEL = 'local_log_level'
CFG_S_LOGLEVELS = 'logging_levels'
@classmethod
def from_ini(cls, worker_class, ini_file, configure_logger=True):
"""
Create an instance of ``LexService`` using an INI file for the
configuration dictionary.
:param type worker_class: The class of the worker to be started, must
be a subclass of :class:`LexWorker`.
:param TextIO ini_file: Input stream open to an INI file which will be
read to create the config dictionary.
:return: Constructed instance.
:rtype: LexService
"""
cp = configparser.RawConfigParser()
cp.readfp(ini_file)
confdict = {
sec: {
optname: optval
for optname, optval in cp.items(sec)
}
for sec in cp.sections()
}
return cls(worker_class, confdict, configure_logger)
@classmethod
def main(cls, worker_class, ini_path='./config.ini', pause_at_exit=False):
"""
Create an instance of ``LexService`` and start it immediately (calling
the :meth:`.serve` method).
This is a convenience method for worker scripts, to use as "main" which
starts the service in one line of code.
:param type worker_class: The class of the worker to be started, must
be a subclass of :class:`LexWorker`.
:param str ini_path: Path to an INI file which will be used as service
configuration.
:param bool pause_at_exit: If ``True``, then after :meth:`.serve`
exits, the program will wait for newline on stdin. Useful for
keeping wrapper programs alive.
"""
with io.open(ini_path) as ini_ifs:
srv = cls.from_ini(worker_class, ini_ifs)
srv.serve()
if pause_at_exit:
input('[Press Enter to exit] ')
# Process method is static to not copy the self reference.
@staticmethod
def _worker_process(worker_class,
log_address,
log_levels,
rabbit_host,
rabbit_user,
rabbit_passwd,
queue_name):
logh = (SocketHandler(*log_address)
if log_address is not None
else None)
worker_class.logging_init(logh, log_levels)
wrk = worker_class()
_log.info('Initializing %r', wrk)
wrk.init()
try:
con = pika.BlockingConnection(pika.ConnectionParameters(
host=rabbit_host,
credentials=pika.PlainCredentials(rabbit_user, rabbit_passwd),
))
try:
chan = con.channel()
chan.queue_declare(queue_name)
chan.basic_qos(prefetch_count=1)
chan.basic_consume(_Consumer(wrk), queue_name)
_log.info('Starting worker %r with queue: %s', wrk, queue_name)
chan.start_consuming()
except KeyboardInterrupt:
# This is at the moment the normal way to exit the service, so
# don't throw out stack trace.
_log.info('Shutting down on user interrupt')
finally:
_log.info('Stopping worker %r with queue: %s', wrk, queue_name)
chan.stop_consuming()
con.close()
finally:
_log.info('Finalizing %r', wrk)
wrk.close()
def __init__(self, worker_class, config, configure_logger=True):
"""
:param type worker_class: The class of the worker to be started, must
be a subclass of :class:`LexWorker`.
:param dict config: Configuration dictionary. Must contain at least the
values described in this class's documentation.
:param bool configure_logger: Whether call the logger-configuring
function. It can only be called once per main process, due to
loggers' global nature. If this is ``False``, then all
``[logging]`` parameters aside from ``port`` will be ignored.
However, it should not normally be necessary to ever create more
than one instance of ``LexService`` per main process, so passing
anything beside the default value should never be needed. This
parameter has no effect if there is no ``[logging]/port`` option in
configuration.
"""
if not issubclass(worker_class, LexWorker):
raise TypeError('{!r} is not a LexWorker'.format(worker_class))
self._wrk_cls = worker_class
self._cfg = config
cfg_serv = config[self.CFG_S_SERV]
self._q_name = 'lex_' + cfg_serv[self.CFG_S_SERV_O_TOOL]
self._r_host = cfg_serv[self.CFG_S_SERV_O_RHOST]
self._r_user = cfg_serv[self.CFG_S_SERV_O_RUSER]
self._r_passwd = cfg_serv[self.CFG_S_SERV_O_RPSWD]
try:
cfg_log = config[self.CFG_S_LOG]
except KeyError:
self._log_srv = None
self._log_lvls = {}
else:
self.__configure_logger(cfg_log, configure_logger)
try:
cfg_loglv = config[self.CFG_S_LOGLEVELS]
except KeyError:
self._log_lvls = {}
else:
self._log_lvls = {name: logserver.parse_loglevel(level)
for name, level in six.iteritems(cfg_loglv)}
try:
cfg_tool = config[self.CFG_S_TOOL]
except KeyError:
self._wrk_num = 1
else:
self._wrk_num = int(cfg_tool.get(self.CFG_S_TOOL_O_NUMW, 1))
def serve(self):
"""
Start receiving service requests and pass them to worker(s).
This methods starts worker processes, request queues and a logging
thread. It blocks until ``KeyboardInterrupt`` is sent, so it should
be run from an interactive terminal.
"""
processes = list(
Process(
target=self._worker_process,
kwargs=dict(
worker_class=self._wrk_cls,
log_address=self._log_srv.socket_address
if self._log_srv is not None
else None,
log_levels=self._log_lvls,
rabbit_host=self._r_host,
rabbit_user=self._r_user,
rabbit_passwd=self._r_passwd,
queue_name=self._q_name,
),
name='worker-{}'.format(i),
)
for i in xrange(self._wrk_num)
)
_log.info('Initializing %r', self._wrk_cls)
self._wrk_cls.static_init(self._cfg)
try:
if self._log_srv is not None:
_log.info('Starting log server listening on %r',
self._log_srv.socket_address)
self._log_srv.start()
try:
print('=== Starting server, press Ctrl+C to quit ===')
for p in processes:
p.start()
try:
# Dummy join to hang till interrupt.
# The interrupt is automatically propagated to all
# subprocesses, so we just need to join on them again after
# it has been sent.
processes[0].join()
except KeyboardInterrupt:
print('=== Quitting ===')
finally:
for p in processes:
p.join()
finally:
if self._log_srv is not None:
_log.info('Log server on %r is going down',
self._log_srv.socket_address)
self._log_srv.shutdown()
finally:
_log.info('Finalizing %r', self._wrk_cls)
self._wrk_cls.static_close()
def __configure_logger(self, log_cfg, do_cfg):
logport = log_cfg.get(self.CFG_S_LOG_O_PORT)
if logport is None:
self._log_srv = None
return
self._log_srv = logserver.LogServer(int(logport))
if not do_cfg:
return
logconf_kwargs = {}
if self.CFG_S_LOG_O_FILESIZE in log_cfg:
logconf_kwargs[self.CFG_S_LOG_O_FILESIZE] = int(
log_cfg[self.CFG_S_LOG_O_FILESIZE]
)
if self.CFG_S_LOG_O_FILECOUNT in log_cfg:
logconf_kwargs[self.CFG_S_LOG_O_FILECOUNT] = int(
log_cfg[self.CFG_S_LOG_O_FILECOUNT]
)
if self.CFG_S_LOG_O_FORMAT in log_cfg:
logconf_kwargs[self.CFG_S_LOG_O_FORMAT] = (
log_cfg[self.CFG_S_LOG_O_FORMAT]
)
if self.CFG_S_LOG_O_LEVEL in log_cfg:
logconf_kwargs[self.CFG_S_LOG_O_LEVEL] = (
log_cfg[self.CFG_S_LOG_O_LEVEL]
)
if self.CFG_S_LOG_O_FILENAME in log_cfg:
logconf_kwargs[self.CFG_S_LOG_O_FILENAME] = (
log_cfg[self.CFG_S_LOG_O_FILENAME]
)
logserver.configure_loggers(**logconf_kwargs)
class _Consumer(object):
def __init__(self, worker):
self._wrk = worker
def __call__(self, chan, meth_frame, props, body):
#out_file = os.path.join(self._pth, props.correlation_id)
result = {}
try:
self._process_task(result, props, body)
except Exception as e:
_log.exception(e);
_log.exception('Unable to process task %s',
props.correlation_id)
result['error'] = six.text_type(e)
chan.basic_publish(
exchange='',
routing_key=props.reply_to,
properties=pika.BasicProperties(
correlation_id=props.correlation_id,
),
body=json.dumps(result)
)
chan.basic_ack(delivery_tag=meth_frame.delivery_tag)
_log.info('Done with task %s', props.correlation_id)
def _process_task(self, result, props, body):
# Body must be unicode for python3
if isinstance(body, bytes):
body = body.decode('utf8')
data = json.loads(body)
_log.info('Started processing task %s', props.correlation_id)
_log.info('Options passed to task: %r', data)
start_time = time.time()
result['results']=self._wrk.process(data)
duration = time.time() - start_time
_log.info('Finished processing task %s in %g',
props.correlation_id,
duration)
result['error'] = ''
result['time'] = duration
File added
from __future__ import absolute_import, unicode_literals, division
from abc import ABCMeta, abstractmethod
import logging
import six
__all__ = 'LexWorker',
@six.add_metaclass(ABCMeta)
class LexWorker(object):
"""
The abstract class that all workers should be derived from.
If defines methods which may be overridden to perform initialization of the
worker, as well as the main processing method that will be called for every
request to the worker.
"""
@classmethod
def static_init(cls, config):
"""
This initialization method is called exactly once, when the worker
class is loaded and the service is starting.
It should load and initialize resources that can be shared across
processes, so that they don't need to be loaded many times.
All variables added to this class by this method will be pickled and
sent to all started processes. Therefore, they need to be picklable.
:param dict config: The service configuration dictionary. It's passed
whole to this method in case it wants to look at some of the
parameters.
"""
pass
@classmethod
def static_close(cls):
"""
Called after all processes have stopped and the service is shutting
down. If any of shared resources allocated by :meth:`static_init` need
to be cleaned up, the subclass should override this method to do so.
"""
pass
@staticmethod
def logging_init(log_socket_handler, log_levels):
"""
Called in each subprocess before :meth:`init`.
The purpose of this method is to set up loggers used by the worker. By
default, it takes all keys from ``logging_levels`` section of the
configuration, treats them as logger names and sets their levels to the
values assigned to them. All of those loggers also have
``log_socket_handler`` added as their handler, so they can log to the
centralized logging system.
This method silently does nothing when ``log_socket_handler`` is
``None``.
Normally, there should be no reason to override this method in a
subclass, unless some very special treatment of some loggers is
required.
:param log_socket_handler: The socket handler created for the process.
This may be ``None`` if the central logger has not been set up.
:type log_socket_handler: Optional[logging.handlers.SocketHandler]
:param log_levels: Mapping of logger names to their levels. Normally
taken from ``logging_levels`` section of config dictionary, after
textual level names are resolved.
:type log_levels: Mapping[str,int]
"""
if log_socket_handler is None:
return
for name, level in six.iteritems(log_levels):
logger = logging.getLogger(name)
logger.setLevel(level)
logger.addHandler(log_socket_handler)
def init(self):
"""
Called after an instance of this class has ben constructed in the
process it will be run. It is run once for each instance
(and therefore each process).
It should load all resources that can't be pickled and shared.
"""
pass
def close(self):
"""
Called when the process is being shut down. If the worker allocates any
per-process resources that need to be cleaned up, the subclass should
override this method to do so.
"""
pass
@abstractmethod
def process(self, task_options):
"""
Called for each request made to the worker. This method performs the
task the service is constructed to do and must be overridden by
subclasses.
:param dict task_options: Dictionary containing options for the current
processing task. Subclasses should describe what options that can
handle (or require). This dictionary may contain all values that
can be JSON-encoded.
"""
pass
File added
from __future__ import absolute_import, unicode_literals, division
import logging
from logging.handlers import RotatingFileHandler
from threading import Thread
from struct import Struct
from six.moves import socketserver, cPickle
__all__ = 'LogServer', 'configure_loggers', 'parse_loglevel'
_DEFAULT_LOGFORMAT = ('%(processName)s>>> [%(asctime)s] (%(name)s:%(lineno)d) '
'%(levelname)s: %(message)s')
# Since logging module doesn't standardize name-to-level conversion, here's a
# dict with the standard levels.
_NAME2LVL = {
'CRITICAL': 50,
'ERROR': 40,
'WARNING': 30,
'INFO': 20,
'DEBUG': 10,
}
_logger = logging.getLogger(__name__)
# Main logger is configured for loggers in this library themselves. It gets the
# same handler as worker loggers.
_main_logger = logging.getLogger(__name__.split('.', 1)[0])
# The logger that handles logs the remote loggers. It remains aside loggers for
# this library, accepts everything, logs nothing on its own and only handles
# received log records.
_service_logger = logging.getLogger('<service-remote>')
_handler = None
def configure_loggers(logfile_name='service.log',
logfile_maxsize=1024**2,
logfile_maxbackups=10,
log_format=_DEFAULT_LOGFORMAT,
local_log_level='WARNING'):
"""
Configure the logger used by :class`LogServer` instances.
This function must be called before any logging servers are started (not
that there should ever be need for more than one) and cannot be called
again. This is to ensure thread safety.
:param str logfile_name: Name of the file to which logs are written.
:param int logfile_maxsize: Maximal size in bytes of a single rotating
log file. Default is 1 MiB.
:param int logfile_maxbackups: Maximal number of backup log files kept.
Default is 10.
:param str log_format: Format string for log records output by this
server. A reasonable default is provided.
:param str local_log_level: Name of the log level for loggers in *this*
library. Refer to standard documentation for possible names. This
setting does not affect loggers from workers.
"""
global _handler
if _handler is not None:
raise RuntimeError('Cannot configure logger twice')
_handler = RotatingFileHandler(logfile_name,
maxBytes=logfile_maxsize,
backupCount=logfile_maxbackups,
encoding='utf-8',
delay=True)
fmter = logging.Formatter(log_format)
_handler.setFormatter(fmter)
_main_logger.addHandler(_handler)
_main_logger.setLevel(parse_loglevel(local_log_level))
_service_logger.addHandler(_handler)
# Make sure this will accept everything. Worker loggers should do
# filtering.
_service_logger.setLevel(logging.NOTSET)
# Also log to stderr (usually will be screen). This does not require any
# fussing.
stdhandler = logging.StreamHandler()
stdhandler.setFormatter(fmter)
_main_logger.addHandler(stdhandler)
_service_logger.addHandler(stdhandler)
def parse_loglevel(log_level):
"""
Get logging level constant number from a string.
If the string is an integer literal, return it as integer. Otherwise try to
interpret the string as one of the standard level names and return value
associated with that.
:param str log_level: String naming the log level, to be parsed.
:return: Integer value of the log level, as used by ``logging`` module.
:rtype: int
:raise KeyError: When ``log_level`` is neither an integer literal nor
the name of a standard logging level.
"""
try:
lvlnum = int(log_level)
except ValueError:
lvlnum = _NAME2LVL[log_level.upper()]
return lvlnum
class LogServer(object):
"""
Creates and starts a logging server thread. This threads awaits for
``LogRecord`` pickles from a given port on localhost and logs them to a
rotating file handler.
The thread is meant to run while waiting for subprocesses to end, so it
should not impact efficiency. The thread will also spend most of its time
listening on socket.
The logging server can be told to shutdown at any time.
"""
# str cast is for python2 compatibility.
HOST = str('localhost')
SHUTDOWN_POLL_INTERVAL = 2.
def __init__(self, port):
"""
:param int port: Number of TCP port the server will be listening on.
"""
self._port = port
self._sserver = _LogSocketServer((self.HOST, port), _LogRequestHandler)
self._sthread = Thread(target=self._sserver.serve_forever,
args=(self.SHUTDOWN_POLL_INTERVAL,),
name='logging')
@property
def socket_address(self):
"""The address tuple for socket handlers to connect to this server."""
return self.HOST, self._port
def start(self):
"""
Start the logging thread and return immediately.
:raise RuntimeError: If :func:`configure_loggers` has not been
called before this method.
"""
if _handler is None:
raise RuntimeError('configure_loggers() has not been '
'called before starting')
self._sthread.start()
def shutdown(self):
"""
Shutdown the logging server and thread.
If the thread is not alive, silently do nothing.
"""
if not self._sthread.is_alive():
return
self._sserver.shutdown()
self._sthread.join()
class _LogSocketServer(socketserver.TCPServer):
# By default, socket errors go to stdout. We want them integrated
# nicely with the logging system, hence this subclass.
# This handler is called from except block in socketserver code, so
# it's safe to log exceptions.
def handle_error(self, request, client_address):
_logger.exception('Error while handling message from %r',
client_address)
class _LogRequestHandler(socketserver.StreamRequestHandler):
# This handler is based on the stdlib example:
# https://docs.python.org/2/howto/logging-cookbook.html#sending-and-receiving-logging-events-across-a-network
# But it uses a UNIX stream socket.
# According to the example, the log record length prefix is an unsigned
# long. Calculate its size more flexibly then the hard-coded 4 bytes in the
# example.
# str cast is for python2 compatibility.
__PREFIX_STRUCT = Struct(str('!L'))
def handle(self):
# Read the length prefix.
chunk = self.rfile.read(self.__PREFIX_STRUCT.size)
if len(chunk) < self.__PREFIX_STRUCT.size:
# Must be malformed, we got EOF before reading the struct.
raise RuntimeError(
'Bad length prefix in message: expected {} bytes '
'but only got {}'
.format(self.__PREFIX_STRUCT.size, len(chunk))
)
# Get the integer representing length.
loglen = self.__PREFIX_STRUCT.unpack(chunk)[0]
chunk = self.rfile.read(loglen)
if len(chunk) < loglen:
# Again with the malformed.
raise RuntimeError(
'Bad payload in message: expected {} bytes '
'but only got {}'
.format(loglen, len(chunk))
)
logdict = cPickle.loads(chunk)
logrecord = logging.makeLogRecord(logdict)
_service_logger.handle(logrecord)
File added
#!/usr/bin/python
import logging
import lex_ws
import nltk
from nltk.corpus import wordnet as wn
my_logger = logging.getLogger(__name__)
_log = logging.getLogger(__name__)
languages={"pl":"pol","en":"eng","es":"spa"};
class OMWNWorker(lex_ws.LexWorker):
@classmethod
def static_init(cls, config):
my_logger.info('Loading models...')
for lang in languages:
wn.lemmas("test", lang=languages[lang]);
my_logger.info('Loading finished.')
return
def process(self, input):
my_logger.info('Doing work!')
res={};
if "function" in input:
res=self.evaluate_function(input["function"],input)
my_logger.info('Work done!')
return res;
def evaluate_function(self, function_type, input):
response = {}
if function_type == 'list':
element=input["element"];
url="http://compling.hss.ntu.edu.sg/omw/cgi-bin/wn-gridx.cgi?";
if not "lang" in element or not (element["lang"] in languages):
return response;
if ("lemma" in element):
print str(element["lemma"].encode('utf-8'))
res=wn.lemmas(element["lemma"].encode('utf-8').decode('utf-8') , lang=languages[element["lang"]]);
if len(res)>0:
formats=["json"];
url=url+"lemma="+element["lemma"]+"&lang="+languages[element["lang"]];
response={"formats":formats,"url":url}
return response;
elif function_type == 'get':
element=input["element"];
if not "lang" in element or not (element["lang"] in languages):
return response;
if ("lemma" in element):
return self.getDatabyLemma(element["lemma"],languages[element["lang"]]);
return {};
elif function_type == 'getInfo':
response={'pl':{'name':"Inne języki",'fullName':"Open Multilingual Wordnet",
'description':'Słowosieć (z ang. wordnet) to sieć semantyczna, która odzwierciedla system leksykalny języka naturalnego. Węzłami Słowosieci są jednostki leksykalne, czyli wyrazy i ich znaczenia, różnorako połączone relacjami semantycznymi ze ściśle określonego repertuaru. Na przykład kot jest hiponimem (podklasą) zwierzęcia, pazur i łapa są w relacji meronimii (część/całość), a wchodzić i wychodzić są antonimami. Jednostka leksykalna uzyskuje znaczenie przez odniesienie do innych jednostek leksykalnych w obrębie systemu, a możemy o niej wnioskować na podstawie przypisanych jej relacji. Na przykład kota definiuje się jako rodzaj zwierzęcia, łapę jako całość, której częścią jest pazur, a czynności wchodzenia i wychodzenia jako przeciwieństwa. <br> Struktura wordnetu jest dostosowana do potrzeb automatycznej analizy tekstów. Jest to w istocie podstawowy zasób językowy, ważny w badaniach nad sztuczną inteligencją.'
+'<br><a target="_blank" href="http://compling.hss.ntu.edu.sg/omw/">więcej...</a>',
'copyright':'Utrzymanie: <a href="http://www3.ntu.edu.sg/home/fcbond/">Francis Bond</a>&lt;<a href="mailto:bond@ieee.org">bond@ieee.org</a>&gt;'
},
'en':{'name':"Other languages",'fullName':"Open Multilingual Wordnet",
'description':'Open wordnets in a variety of languages, all linked to the Princeton Wordnet of English (PWN). The goal is to make it easy to use wordnets in multiple languages. The individual wordnets have been made by many different projects and vary greatly in size and accuracy'
+'<br><a target="_blank" href="http://compling.hss.ntu.edu.sg/omw/">more...</a>',
'copyright':'<a href="http://www3.ntu.edu.sg/home/fcbond/">Francis Bond</a>&lt;<a href="mailto:bond@ieee.org">bond@ieee.org</a>&gt;'
}
};
return response;
def getDatabyLemma(self,lemma,language):
result=[];
wnlemmas=wn.lemmas(lemma, lang=language);
for wnlem in wnlemmas:
wnsynset=wnlem.synset()
trans=dict()
for lang in languages:
if (languages[lang]!=language):
trans[lang]=wnsynset.lemma_names(languages[lang])
synset={'name':wnsynset.name(),'definition':wnsynset.definition(),'offset':str(wnsynset.offset()).zfill(8) + '-' + wnsynset.pos(),'translate':trans};
result.append(synset);
return result;
if __name__ == '__main__':
lex_ws.LexService.main(OMWNWorker)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment