1
0
mirror of https://github.com/gsi-upm/senpy synced 2024-11-22 00:02:28 +00:00

Several fixes and changes

* Added interactive debugging
* Better exception logging
* More tests for errors
* Added ONBUILD to dockerfile
  Now creating new images based on senpy's is as easy as:
  ```from senpy:<version>```. This will automatically mount the code to
  /senpy-plugins and install all dependencies
* Added /data as a VOLUME
* Added `--use-wheel` to pip install both on the image and in the
  auto-install function.
* Closes #9

Break compatibilitity:

* Removed ability to (de)activate plugins through the web
This commit is contained in:
J. Fernando Sánchez 2017-02-17 01:53:02 +01:00
parent 3311af2167
commit b4ca5f4a7c
16 changed files with 164 additions and 99 deletions

View File

@ -14,13 +14,20 @@ stages:
.test: &test_definition .test: &test_definition
variables: variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.eggs" PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
cache: cache:
paths: paths:
- .eggs/ - .eggs/
- "$CI_PROJECT_DIR/pip-cache"
- .venv
key: "$CI_PROJECT_NAME" key: "$CI_PROJECT_NAME"
stage: test stage: test
script: script:
- pip install --use-wheel -U pip setuptools virtualenv
- virtualenv .venv/$PYTHON_VERSION
- source .venv/$PYTHON_VERSION/bin/activate
- pip install --use-wheel -r requirements.txt
- pip install --use-wheel -r test-requirements.txt
- python setup.py test - python setup.py test
test-3.5: test-3.5:

View File

@ -1,9 +1,21 @@
from python:2.7 from python:2.7
RUN mkdir /cache/
ENV PIP_CACHE_DIR=/cache/
WORKDIR /usr/src/app WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/ ADD requirements.txt /usr/src/app/
RUN pip install -r requirements.txt RUN pip install --use-wheel -r requirements.txt
ADD . /usr/src/app/ ADD . /usr/src/app/
RUN pip install . RUN pip install .
ENTRYPOINT ["python", "-m", "senpy", "-f", ".", "--host", "0.0.0.0"]
VOLUME /data/
RUN mkdir /senpy-plugins/
WORKDIR /senpy-plugins/
ONBUILD ADD . /senpy-plugins/
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]

View File

@ -1,9 +1,18 @@
from python:3.4 from python:3.4-slim
WORKDIR /usr/src/app WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/ ADD requirements.txt /usr/src/app/
RUN pip install -r requirements.txt RUN pip install --use-wheel -r requirements.txt
ADD . /usr/src/app/ ADD . /usr/src/app/
RUN pip install . RUN pip install --use-wheel .
ENTRYPOINT ["python", "-m", "senpy", "-f", ".", "--host", "0.0.0.0"]
VOLUME /data/
RUN mkdir /senpy-plugins/
WORKDIR /senpy-plugins/
ONBUILD ADD . /senpy-plugins/
ONBUILD RUN python -m senpy -f /senpy-plugins
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]

View File

@ -1,9 +1,21 @@
from python:3.5 from python:3.5
RUN mkdir /cache/
ENV PIP_CACHE_DIR=/cache/
WORKDIR /usr/src/app WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/ ADD requirements.txt /usr/src/app/
RUN pip install -r requirements.txt RUN pip install --use-wheel -r requirements.txt
ADD . /usr/src/app/ ADD . /usr/src/app/
RUN pip install . RUN pip install .
ENTRYPOINT ["python", "-m", "senpy", "-f", ".", "--host", "0.0.0.0"]
VOLUME /data/
RUN mkdir /senpy-plugins/
WORKDIR /senpy-plugins/
ONBUILD ADD . /senpy-plugins/
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]

View File

@ -1,9 +1,21 @@
from python:{{PYVERSION}} from python:{{PYVERSION}}
RUN mkdir /cache/
ENV PIP_CACHE_DIR=/cache/
WORKDIR /usr/src/app WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/ ADD requirements.txt /usr/src/app/
RUN pip install -r requirements.txt RUN pip install --use-wheel -r requirements.txt
ADD . /usr/src/app/ ADD . /usr/src/app/
RUN pip install . RUN pip install .
ENTRYPOINT ["python", "-m", "senpy", "-f", ".", "--host", "0.0.0.0"]
VOLUME /data/
RUN mkdir /senpy-plugins/
WORKDIR /senpy-plugins/
ONBUILD ADD . /senpy-plugins/
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]

View File

@ -1 +1 @@
0.7.1 0.7.2-dev1

View File

@ -26,6 +26,7 @@ from gevent.wsgi import WSGIServer
from gevent.monkey import patch_all from gevent.monkey import patch_all
import logging import logging
import os import os
import sys
import argparse import argparse
import senpy import senpy
@ -34,6 +35,22 @@ patch_all(thread=False)
SERVER_PORT = os.environ.get("PORT", 5000) SERVER_PORT = os.environ.get("PORT", 5000)
def info(type, value, tb):
if hasattr(sys, 'ps1') or not sys.stderr.isatty():
# we are in interactive mode or we don't have a tty-like
# device, so we call the default hook
sys.__excepthook__(type, value, tb)
else:
import traceback
import pdb
# we are NOT in interactive mode, print the exception...
traceback.print_exception(type, value, tb)
print
# ...then start the debugger in post-mortem mode.
# pdb.pm() # deprecated
pdb.post_mortem(tb) # more "modern"
def main(): def main():
parser = argparse.ArgumentParser(description='Run a Senpy server') parser = argparse.ArgumentParser(description='Run a Senpy server')
parser.add_argument( parser.add_argument(
@ -84,6 +101,8 @@ def main():
rl.setLevel(getattr(logging, args.level)) rl.setLevel(getattr(logging, args.level))
app = Flask(__name__) app = Flask(__name__)
app.debug = args.debug app.debug = args.debug
if args.debug:
sys.excepthook = info
sp = Senpy(app, args.plugins_folder, default_plugins=args.default_plugins) sp = Senpy(app, args.plugins_folder, default_plugins=args.default_plugins)
if args.only_install: if args.only_install:
sp.install_deps() sp.install_deps()

View File

@ -102,9 +102,8 @@ def plugins():
@api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET']) @api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET'])
@api_blueprint.route('/plugins/<plugin>/<action>', methods=['POST', 'GET'])
@basic_api @basic_api
def plugin(plugin=None, action="list"): def plugin(plugin=None):
sp = current_app.senpy sp = current_app.senpy
if plugin == 'default' and sp.default_plugin: if plugin == 'default' and sp.default_plugin:
response = sp.default_plugin response = sp.default_plugin
@ -113,11 +112,4 @@ def plugin(plugin=None, action="list"):
response = sp.plugins[plugin] response = sp.plugins[plugin]
else: else:
return Error(message="Plugin not found", status=404) return Error(message="Plugin not found", status=404)
if action == "list":
return response return response
method = "{}_plugin".format(action)
if (hasattr(sp, method)):
getattr(sp, method)(plugin)
return Response(message="Ok")
else:
return Error(message="action '{}' not allowed".format(action))

View File

@ -35,11 +35,3 @@ class Client(object):
code=response.status_code, code=response.status_code,
content=response.content)) content=response.content))
raise ex raise ex
if __name__ == '__main__':
c = Client('http://senpy.cluster.gsi.dit.upm.es/api/')
resp = c.analyse('hello')
# print(resp)
print(resp.entries)
resp.validate()

View File

@ -106,8 +106,12 @@ class Senpy(object):
resp = plug.analyse(**nif_params) resp = plug.analyse(**nif_params)
resp.analysis.append(plug) resp.analysis.append(plug)
logger.debug("Returning analysis result: {}".format(resp)) logger.debug("Returning analysis result: {}".format(resp))
except Error as ex:
logger.exception('Error returning analysis result')
resp = ex
except Exception as ex: except Exception as ex:
resp = Error(message=str(ex), status=500) resp = Error(message=str(ex), status=500)
logger.exception('Error returning analysis result')
return resp return resp
@property @property
@ -120,10 +124,6 @@ class Senpy(object):
else: else:
return None return None
def parameters(self, algo):
return getattr(
self.plugins.get(algo) or self.default_plugin, "extra_params", {})
def activate_all(self, sync=False): def activate_all(self, sync=False):
ps = [] ps = []
for plug in self.plugins.keys(): for plug in self.plugins.keys():
@ -194,17 +194,6 @@ class Senpy(object):
th = Thread(target=deact) th = Thread(target=deact)
th.start() th.start()
def reload_plugin(self, name):
logger.debug("Reloading {}".format(name))
plugin = self.plugins[name]
try:
del self.plugins[name]
nplug = self._load_plugin(plugin.module, plugin.path)
self.plugins[nplug.name] = nplug
except Exception as ex:
logger.error('Error reloading {}: {}'.format(name, ex))
self.plugins[name] = plugin
@classmethod @classmethod
def validate_info(cls, info): def validate_info(cls, info):
return all(x in info for x in ('name', 'module', 'version')) return all(x in info for x in ('name', 'module', 'version'))
@ -219,6 +208,7 @@ class Senpy(object):
if requirements: if requirements:
pip_args = [] pip_args = []
pip_args.append('install') pip_args.append('install')
pip_args.append('--use-wheel')
for req in requirements: for req in requirements:
pip_args.append(req) pip_args.append(req)
logger.info('Installing requirements: ' + str(requirements)) logger.info('Installing requirements: ' + str(requirements))
@ -233,7 +223,6 @@ class Senpy(object):
name = info["name"] name = info["name"]
sys.path.append(root) sys.path.append(root)
(fp, pathname, desc) = imp.find_module(module, [root, ]) (fp, pathname, desc) = imp.find_module(module, [root, ])
try:
cls._install_deps(info) cls._install_deps(info)
tmp = imp.load_module(module, fp, pathname, desc) tmp = imp.load_module(module, fp, pathname, desc)
sys.path.remove(root) sys.path.remove(root)
@ -241,8 +230,7 @@ class Senpy(object):
for _, obj in inspect.getmembers(tmp): for _, obj in inspect.getmembers(tmp):
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp: if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
logger.debug(("Found plugin class:" logger.debug(("Found plugin class:"
" {}@{}").format(obj, inspect.getmodule( " {}@{}").format(obj, inspect.getmodule(obj)))
obj)))
candidate = obj candidate = obj
break break
if not candidate: if not candidate:
@ -250,15 +238,12 @@ class Senpy(object):
return return
module = candidate(info=info) module = candidate(info=info)
repo_path = root repo_path = root
try:
module._repo = Repo(repo_path) module._repo = Repo(repo_path)
except InvalidGitRepositoryError: except InvalidGitRepositoryError:
logger.debug("The plugin {} is not in a Git repository".format( logger.debug("The plugin {} is not in a Git repository".format(
module)) module))
module._repo = None module._repo = None
except Exception as ex:
logger.error("Exception importing {}: {}".format(module, ex))
logger.error("Trace: {}".format(traceback.format_exc()))
return None, None
return name, module return name, module
@classmethod @classmethod

View File

@ -115,7 +115,7 @@ class SenpyMixin(object):
return ser_or_down(self._plain_dict()) return ser_or_down(self._plain_dict())
def jsonld(self, with_context=True, context_uri=None): def jsonld(self, with_context=False, context_uri=None):
ser = self.serializable() ser = self.serializable()
if with_context: if with_context:
@ -230,6 +230,11 @@ def from_dict(indict):
return cls(**indict) return cls(**indict)
def from_json(injson):
indict = json.loads(injson)
return from_dict(indict)
def from_schema(name, schema_file=None, base_classes=None): def from_schema(name, schema_file=None, base_classes=None):
base_classes = base_classes or [] base_classes = base_classes or []
base_classes.append(BaseModel) base_classes.append(BaseModel)
@ -275,6 +280,15 @@ class Error(SenpyMixin, BaseException):
self._error = _ErrorModel(message=message, *args, **kwargs) self._error = _ErrorModel(message=message, *args, **kwargs)
self.message = message self.message = message
def __getitem__(self, key):
return self._error[key]
def __setitem__(self, key, value):
self._error[key] = value
def __delitem__(self, key):
del self._error[key]
def __getattr__(self, key): def __getattr__(self, key):
if key != '_error' and hasattr(self._error, key): if key != '_error' and hasattr(self._error, key):
return getattr(self._error, key) return getattr(self._error, key)

View File

@ -50,6 +50,7 @@ class SentimentPlugin(SenpyPlugin, models.SentimentPlugin):
class EmotionPlugin(SentimentPlugin, models.EmotionPlugin): class EmotionPlugin(SentimentPlugin, models.EmotionPlugin):
def __init__(self, info, *args, **kwargs): def __init__(self, info, *args, **kwargs):
super(EmotionPlugin, self).__init__(info, *args, **kwargs)
self.minEmotionValue = float(info.get("minEmotionValue", 0)) self.minEmotionValue = float(info.get("minEmotionValue", 0))
self.maxEmotionValue = float(info.get("maxEmotionValue", 0)) self.maxEmotionValue = float(info.get("maxEmotionValue", 0))
self["@type"] = "onyx:EmotionAnalysis" self["@type"] = "onyx:EmotionAnalysis"

View File

@ -13,7 +13,7 @@
"type": "list", "type": "list",
"items": {"type": "object"} "items": {"type": "object"}
}, },
"code": { "status": {
"type": "int" "type": "int"
}, },
"required": ["message"] "required": ["message"]

View File

@ -12,8 +12,6 @@ except AttributeError:
install_reqs = parse_requirements("requirements.txt") install_reqs = parse_requirements("requirements.txt")
test_reqs = parse_requirements("test-requirements.txt") test_reqs = parse_requirements("test-requirements.txt")
# reqs is a list of requirement
# e.g. ['django==1.5.1', 'mezzanine==1.4.6']
install_reqs = [str(ir.req) for ir in install_reqs] install_reqs = [str(ir.req) for ir in install_reqs]
test_reqs = [str(ir.req) for ir in test_reqs] test_reqs = [str(ir.req) for ir in test_reqs]

View File

@ -1,11 +1,10 @@
import os import os
import logging import logging
import json
from senpy.extensions import Senpy from senpy.extensions import Senpy
from senpy import models
from flask import Flask from flask import Flask
from unittest import TestCase from unittest import TestCase
from gevent import sleep
from itertools import product from itertools import product
@ -14,7 +13,7 @@ def check_dict(indic, template):
def parse_resp(resp): def parse_resp(resp):
return json.loads(resp.data.decode('utf-8')) return models.from_json(resp.data.decode('utf-8'))
class BlueprintsTest(TestCase): class BlueprintsTest(TestCase):
@ -57,6 +56,17 @@ class BlueprintsTest(TestCase):
assert "@context" in js assert "@context" in js
assert "entries" in js assert "entries" in js
def test_error(self):
"""
The dummy plugin returns an empty response,\
it should contain the context
"""
resp = self.client.get("/api/?i=My aloha mohame&algo=DOESNOTEXIST")
self.assertCode(resp, 404)
js = parse_resp(resp)
logging.debug("Got response: %s", js)
assert isinstance(js, models.Error)
def test_list(self): def test_list(self):
""" List the plugins """ """ List the plugins """
resp = self.client.get("/api/plugins/") resp = self.client.get("/api/plugins/")
@ -94,25 +104,6 @@ class BlueprintsTest(TestCase):
assert "@id" in js assert "@id" in js
assert js["@id"] == "Dummy_0.1" assert js["@id"] == "Dummy_0.1"
def test_activate(self):
""" Activate and deactivate one plugin """
resp = self.client.get("/api/plugins/Dummy/deactivate")
self.assertCode(resp, 200)
sleep(0.5)
resp = self.client.get("/api/plugins/Dummy/")
self.assertCode(resp, 200)
js = parse_resp(resp)
assert "is_activated" in js
assert not js["is_activated"]
resp = self.client.get("/api/plugins/Dummy/activate")
self.assertCode(resp, 200)
sleep(0.5)
resp = self.client.get("/api/plugins/Dummy/")
self.assertCode(resp, 200)
js = parse_resp(resp)
assert "is_activated" in js
assert js["is_activated"]
def test_default(self): def test_default(self):
""" Show only one plugin""" """ Show only one plugin"""
resp = self.client.get("/api/plugins/default/") resp = self.client.get("/api/plugins/default/")
@ -121,11 +112,6 @@ class BlueprintsTest(TestCase):
logging.debug(js) logging.debug(js)
assert "@id" in js assert "@id" in js
assert js["@id"] == "Dummy_0.1" assert js["@id"] == "Dummy_0.1"
resp = self.client.get("/api/plugins/Dummy/deactivate")
self.assertCode(resp, 200)
sleep(0.5)
resp = self.client.get("/api/plugins/default/")
self.assertCode(resp, 404)
def test_context(self): def test_context(self):
resp = self.client.get("/api/contexts/context.jsonld") resp = self.client.get("/api/contexts/context.jsonld")

View File

@ -2,6 +2,11 @@ from __future__ import print_function
import os import os
import logging import logging
try:
from unittest import mock
except ImportError:
import mock
from functools import partial from functools import partial
from senpy.extensions import Senpy from senpy.extensions import Senpy
from senpy.models import Error from senpy.models import Error
@ -13,8 +18,9 @@ class ExtensionsTest(TestCase):
def setUp(self): def setUp(self):
self.app = Flask("test_extensions") self.app = Flask("test_extensions")
self.dir = os.path.join(os.path.dirname(__file__)) self.dir = os.path.join(os.path.dirname(__file__))
self.senpy = Senpy(plugin_folder=self.dir, default_plugins=False) self.senpy = Senpy(plugin_folder=self.dir,
self.senpy.init_app(self.app) app=self.app,
default_plugins=False)
self.senpy.activate_plugin("Dummy", sync=True) self.senpy.activate_plugin("Dummy", sync=True)
def test_init(self): def test_init(self):
@ -69,7 +75,11 @@ class ExtensionsTest(TestCase):
def test_noplugin(self): def test_noplugin(self):
""" Don't analyse if there isn't any plugin installed """ """ Don't analyse if there isn't any plugin installed """
self.senpy.deactivate_all(sync=True) self.senpy.deactivate_all(sync=True)
self.assertRaises(Error, partial(self.senpy.analyse, input="tupni")) self.assertRaises(Error, partial(self.senpy.analyse,
input="tupni"))
self.assertRaises(Error, partial(self.senpy.analyse,
input="tupni",
algorithm='Dummy'))
def test_analyse(self): def test_analyse(self):
""" Using a plugin """ """ Using a plugin """
@ -81,6 +91,18 @@ class ExtensionsTest(TestCase):
assert r1.analysis[0].id[:5] == "Dummy" assert r1.analysis[0].id[:5] == "Dummy"
assert r2.analysis[0].id[:5] == "Dummy" assert r2.analysis[0].id[:5] == "Dummy"
def test_analyse_error(self):
mm = mock.MagicMock()
mm.analyse.side_effect = Error('error on analysis', status=900)
self.senpy.plugins['MOCK'] = mm
resp = self.senpy.analyse(input='nothing', algorithm='MOCK')
assert resp['message'] == 'error on analysis'
assert resp['status'] == 900
mm.analyse.side_effect = Exception('generic exception on analysis')
resp = self.senpy.analyse(input='nothing', algorithm='MOCK')
assert resp['message'] == 'generic exception on analysis'
assert resp['status'] == 500
def test_filtering(self): def test_filtering(self):
""" Filtering plugins """ """ Filtering plugins """
assert len(self.senpy.filter_plugins(name="Dummy")) > 0 assert len(self.senpy.filter_plugins(name="Dummy")) > 0
@ -90,3 +112,7 @@ class ExtensionsTest(TestCase):
assert not len( assert not len(
self.senpy.filter_plugins( self.senpy.filter_plugins(
name="Dummy", is_activated=True)) name="Dummy", is_activated=True))
def test_load_default_plugins(self):
senpy = Senpy(plugin_folder=self.dir, default_plugins=True)
assert len(senpy.plugins) > 1