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

Merge branch 'master' 0.8.7 into patch-6

This commit is contained in:
Ian Wood 2017-05-05 15:02:15 +01:00
commit 1a9dd07f7e
16 changed files with 304 additions and 58 deletions

View File

@ -73,8 +73,8 @@ pip_test: $(addprefix pip_test-,$(PYVERSIONS))
clean: clean:
@docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true @docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
@docker images | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true @docker images | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
@docker rmi $(NAME)-dev 2>/dev/null || true @docker stop $(addprefix $(NAME)-dev,$(PYVERSIONS)) 2>/dev/null || true
@docker rm $(addprefix $(NAME)-dev,$(PYVERSIONS)) 2>/dev/null || true
git_commit: git_commit:
git commit -a git commit -a

View File

@ -0,0 +1,78 @@
{
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1",
"@type": "results",
"analysis": [
"me:SAnalysis1",
"me:SgAnalysis1",
"me:EmotionAnalysis1",
"me:NER1",
{
"@type": "analysis",
"@id": "wrong"
}
],
"entries": [
{
"@id": "http://micro.blog/status1",
"@type": [
"nif:RFC5147String",
"nif:Context"
],
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"entities": [
{
"@id": "http://micro.blog/status1#char=5,13",
"nif:beginIndex": 5,
"nif:endIndex": 13,
"nif:anchorOf": "Microsoft",
"me:references": "http://dbpedia.org/page/Microsoft",
"prov:wasGeneratedBy": "me:NER1"
},
{
"@id": "http://micro.blog/status1#char=25,37",
"nif:beginIndex": 25,
"nif:endIndex": 37,
"nif:anchorOf": "Windows Phone",
"me:references": "http://dbpedia.org/page/Windows_Phone",
"prov:wasGeneratedBy": "me:NER1"
}
],
"suggestions": [
{
"@id": "http://micro.blog/status1#char=16,77",
"nif:beginIndex": 16,
"nif:endIndex": 77,
"nif:anchorOf": "put your Windows Phone on your newest #open technology program",
"prov:wasGeneratedBy": "me:SgAnalysis1"
}
],
"sentiments": [
{
"@id": "http://micro.blog/status1#char=80,97",
"nif:beginIndex": 80,
"nif:endIndex": 97,
"nif:anchorOf": "You'll be awesome.",
"marl:hasPolarity": "marl:Positive",
"marl:polarityValue": 0.9,
"prov:wasGeneratedBy": "me:SAnalysis1"
}
],
"emotions": [
{
"@id": "http://micro.blog/status1#char=0,109",
"nif:anchorOf": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"prov:wasGeneratedBy": "me:EAnalysis1",
"onyx:hasEmotion": [
{
"onyx:hasEmotionCategory": "wna:liking"
},
{
"onyx:hasEmotionCategory": "wna:excitement"
}
]
}
]
}
]
}

View File

@ -0,0 +1,74 @@
{
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1",
"@type": "results",
"analysis": [
"me:SAnalysis1",
"me:SgAnalysis1",
"me:EmotionAnalysis1",
"me:NER1"
],
"entries": [
{
"@id": "http://micro.blog/status1",
"@type": [
"nif:RFC5147String",
"nif:Context"
],
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"entities": [
{
"@id": "http://micro.blog/status1#char=5,13",
"nif:beginIndex": 5,
"nif:endIndex": 13,
"nif:anchorOf": "Microsoft",
"me:references": "http://dbpedia.org/page/Microsoft",
"prov:wasGeneratedBy": "me:NER1"
},
{
"@id": "http://micro.blog/status1#char=25,37",
"nif:beginIndex": 25,
"nif:endIndex": 37,
"nif:anchorOf": "Windows Phone",
"me:references": "http://dbpedia.org/page/Windows_Phone",
"prov:wasGeneratedBy": "me:NER1"
}
],
"suggestions": [
{
"@id": "http://micro.blog/status1#char=16,77",
"nif:beginIndex": 16,
"nif:endIndex": 77,
"nif:anchorOf": "put your Windows Phone on your newest #open technology program",
"prov:wasGeneratedBy": "me:SgAnalysis1"
}
],
"sentiments": [
{
"@id": "http://micro.blog/status1#char=80,97",
"nif:beginIndex": 80,
"nif:endIndex": 97,
"nif:anchorOf": "You'll be awesome.",
"marl:hasPolarity": "marl:Positive",
"marl:polarityValue": 0.9,
"prov:wasGeneratedBy": "me:SAnalysis1"
}
],
"emotions": [
{
"@id": "http://micro.blog/status1#char=0,109",
"nif:anchorOf": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"prov:wasGeneratedBy": "me:EAnalysis1",
"onyx:hasEmotion": [
{
"onyx:hasEmotionCategory": "wna:liking"
},
{
"onyx:hasEmotionCategory": "wna:excitement"
}
]
}
]
}
]
}

View File

@ -1,6 +1,6 @@
Flask>=0.10.1 Flask>=0.10.1
requests>=2.4.1 requests>=2.4.1
gevent>=1.1rc4 tornado>=4.4.3
PyLD>=0.6.5 PyLD>=0.6.5
six six
future future

View File

@ -22,15 +22,16 @@ the server.
from flask import Flask from flask import Flask
from senpy.extensions import Senpy from senpy.extensions import Senpy
from gevent.wsgi import WSGIServer from tornado.wsgi import WSGIContainer
from gevent.monkey import patch_all from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
import logging import logging
import os import os
import argparse import argparse
import senpy import senpy
patch_all(thread=False)
SERVER_PORT = os.environ.get("PORT", 5000) SERVER_PORT = os.environ.get("PORT", 5000)
@ -92,9 +93,10 @@ def main():
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host, print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
args.port)) args.port))
if not app.debug: if not app.debug:
http_server = WSGIServer((args.host, args.port), app) http_server = HTTPServer(WSGIContainer(app))
http_server.listen(args.port, address=args.host)
try: try:
http_server.serve_forever() IOLoop.instance().start()
except KeyboardInterrupt: except KeyboardInterrupt:
print('Bye!') print('Bye!')
http_server.stop() http_server.stop()

View File

@ -1,6 +1,7 @@
import requests import requests
import logging import logging
from . import models from . import models
from .plugins import default_plugin_type
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -12,6 +13,10 @@ class Client(object):
def analyse(self, input, method='GET', **kwargs): def analyse(self, input, method='GET', **kwargs):
return self.request('/', method=method, input=input, **kwargs) return self.request('/', method=method, input=input, **kwargs)
def plugins(self, ptype=default_plugin_type):
resp = self.request(path='/plugins', plugin_type=ptype).plugins
return {p.name: p for p in resp}
def request(self, path=None, method='GET', **params): def request(self, path=None, method='GET', **params):
url = '{}{}'.format(self.endpoint, path) url = '{}{}'.format(self.endpoint, path)
response = requests.request(method=method, url=url, params=params) response = requests.request(method=method, url=url, params=params)

View File

@ -183,7 +183,7 @@ class Senpy(object):
return resp return resp
def _conversion_candidates(self, fromModel, toModel): def _conversion_candidates(self, fromModel, toModel):
candidates = self.filter_plugins(**{'@type': 'emotionConversionPlugin'}) candidates = self.filter_plugins(plugin_type='emotionConversionPlugin')
for name, candidate in candidates.items(): for name, candidate in candidates.items():
for pair in candidate.onyx__doesConversion: for pair in candidate.onyx__doesConversion:
logging.debug(pair) logging.debug(pair)
@ -303,6 +303,7 @@ class Senpy(object):
else: else:
th = Thread(target=act) th = Thread(target=act)
th.start() th.start()
return th
def deactivate_plugin(self, plugin_name, sync=False): def deactivate_plugin(self, plugin_name, sync=False):
try: try:
@ -327,6 +328,7 @@ class Senpy(object):
else: else:
th = Thread(target=deact) th = Thread(target=deact)
th.start() th.start()
return th
@classmethod @classmethod
def validate_info(cls, info): def validate_info(cls, info):
@ -417,33 +419,7 @@ class Senpy(object):
return self._plugin_list return self._plugin_list
def filter_plugins(self, **kwargs): def filter_plugins(self, **kwargs):
""" Filter plugins by different criteria """ return plugins.pfilter(self.plugins, **kwargs)
ptype = kwargs.pop('plugin_type', None)
logger.debug('#' * 100)
logger.debug('ptype {}'.format(ptype))
if ptype:
try:
ptype = ptype[0].upper() + ptype[1:]
pclass = getattr(plugins, ptype)
logger.debug('Class: {}'.format(pclass))
candidates = filter(lambda x: isinstance(x, pclass),
self.plugins.values())
except AttributeError:
raise Error('{} is not a valid type'.format(ptype))
else:
candidates = self.plugins.values()
logger.debug(candidates)
def matches(plug):
res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
logger.debug(
"matching {} with {}: {}".format(plug.name, kwargs, res))
return res
if kwargs:
candidates = filter(matches, candidates)
return {p.name: p for p in candidates}
@property @property
def analysis_plugins(self): def analysis_plugins(self):

View File

@ -9,6 +9,7 @@ import logging
import tempfile import tempfile
import copy import copy
from .. import models from .. import models
from ..api import API_PARAMS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -117,3 +118,40 @@ class ShelfMixin(object):
if hasattr(self, '_sh') and self._sh is not None: if hasattr(self, '_sh') and self._sh is not None:
with open(self.shelf_file, 'wb') as f: with open(self.shelf_file, 'wb') as f:
pickle.dump(self._sh, f) pickle.dump(self._sh, f)
default_plugin_type = API_PARAMS['plugin_type']['default']
def pfilter(plugins, **kwargs):
""" Filter plugins by different criteria """
if isinstance(plugins, models.Plugins):
plugins = plugins.plugins
elif isinstance(plugins, dict):
plugins = plugins.values()
ptype = kwargs.pop('plugin_type', default_plugin_type)
logger.debug('#' * 100)
logger.debug('ptype {}'.format(ptype))
if ptype:
try:
ptype = ptype[0].upper() + ptype[1:]
pclass = globals()[ptype]
logger.debug('Class: {}'.format(pclass))
candidates = filter(lambda x: isinstance(x, pclass),
plugins)
except KeyError:
raise models.Error('{} is not a valid type'.format(ptype))
else:
candidates = plugins
logger.debug(candidates)
def matches(plug):
res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
logger.debug(
"matching {} with {}: {}".format(plug.name, kwargs, res))
return res
if kwargs:
candidates = filter(matches, candidates)
return {p.name: p for p in candidates}

View File

@ -37,12 +37,12 @@
"@type": "@id", "@type": "@id",
"@container": "@set" "@container": "@set"
}, },
"plugins": {
"@container": "@list"
},
"options": { "options": {
"@container": "@set" "@container": "@set"
}, },
"plugins": {
"@container": "@set"
},
"prov:wasGeneratedBy": { "prov:wasGeneratedBy": {
"@type": "@id" "@type": "@id"
}, },

View File

@ -10,8 +10,6 @@
"items": { "items": {
"$ref": "plugin.json" "$ref": "plugin.json"
} }
},
"@type": {
} }
} }
} }

View File

@ -18,10 +18,16 @@
"type": "string" "type": "string"
}, },
"analysis": { "analysis": {
"type": "array",
"default": [], "default": [],
"type": "array",
"items": { "items": {
"$ref": "analysis.json" "anyOf": [
{
"$ref": "analysis.json"
},{
"type": "string"
}
]
} }
}, },
"entries": { "entries": {

View File

@ -0,0 +1,23 @@
from senpy.plugins import AnalysisPlugin
import multiprocessing
def _train(process_number):
return process_number
class AsyncPlugin(AnalysisPlugin):
def _do_async(self, num_processes):
pool = multiprocessing.Pool(processes=num_processes)
values = pool.map(_train, range(num_processes))
return values
def activate(self):
self.value = self._do_async(4)
def analyse_entry(self, entry, params):
values = self._do_async(2)
entry.async_values = values
yield entry

View File

@ -0,0 +1,8 @@
---
name: Async
module: asyncplugin
description: I am async
author: "@balkian"
version: '0.1'
async: true
extra_params: {}

View File

@ -4,18 +4,21 @@ try:
except ImportError: except ImportError:
from mock import patch from mock import patch
import json
from senpy.client import Client from senpy.client import Client
from senpy.models import Results, Error from senpy.models import Results, Plugins, Error
from senpy.plugins import AnalysisPlugin, default_plugin_type
class Call(dict): class Call(dict):
def __init__(self, obj): def __init__(self, obj):
self.obj = obj.jsonld() self.obj = obj.serialize()
self.status_code = 200 self.status_code = 200
self.content = self.json() self.content = self.json()
def json(self): def json(self):
return self.obj return json.loads(self.obj)
class ModelsTest(TestCase): class ModelsTest(TestCase):
@ -44,3 +47,19 @@ class ModelsTest(TestCase):
method='GET', method='GET',
params={'input': 'hello', params={'input': 'hello',
'algorithm': 'NONEXISTENT'}) 'algorithm': 'NONEXISTENT'})
def test_plugins(self):
endpoint = 'http://dummy/'
client = Client(endpoint)
plugins = Plugins()
p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'})
plugins.plugins = [p1, ]
success = Call(plugins)
with patch('requests.request', return_value=success) as patched:
response = client.plugins()
assert isinstance(response, dict)
assert len(response) == 1
assert "AnalysisP1" in response
patched.assert_called_with(
url=endpoint + '/plugins', method='GET',
params={'plugin_type': default_plugin_type})

View File

@ -167,7 +167,7 @@ class ExtensionsTest(TestCase):
assert len(senpy.plugins) > 1 assert len(senpy.plugins) > 1
def test_convert_emotions(self): def test_convert_emotions(self):
self.senpy.activate_all() self.senpy.activate_all(sync=True)
plugin = Plugin({ plugin = Plugin({
'id': 'imaginary', 'id': 'imaginary',
'onyx:usesEmotionModel': 'emoml:fsre-dimensions' 'onyx:usesEmotionModel': 'emoml:fsre-dimensions'
@ -205,3 +205,14 @@ class ExtensionsTest(TestCase):
[plugin, ], [plugin, ],
params) params)
assert len(r3.entries[0].emotions) == 1 assert len(r3.entries[0].emotions) == 1
# def test_async_plugin(self):
# """ We should accept multiprocessing plugins with async=False"""
# thread1 = self.senpy.activate_plugin("Async", sync=False)
# thread1.join(timeout=1)
# assert len(self.senpy.plugins['Async'].value) == 4
# resp = self.senpy.analyse(input='nothing', algorithm='Async')
# assert len(resp.entries[0].async_values) == 2
# self.senpy.activate_plugin("Async", sync=True)

View File

@ -109,13 +109,15 @@ class ModelsTest(TestCase):
} }
}}) }})
c = p.jsonld() c = p.jsonld()
assert "info" not in c assert '@type' in c
assert "repo" not in c assert c['@type'] == 'plugin'
assert "extra_params" in c assert 'info' not in c
logging.debug("Framed:") assert 'repo' not in c
assert 'extra_params' in c
logging.debug('Framed:')
logging.debug(c) logging.debug(c)
p.validate() p.validate()
assert "es" in c['extra_params']['none']['options'] assert 'es' in c['extra_params']['none']['options']
assert isinstance(c['extra_params']['none']['options'], list) assert isinstance(c['extra_params']['none']['options'], list)
def test_str(self): def test_str(self):
@ -158,14 +160,20 @@ class ModelsTest(TestCase):
g = rdflib.Graph().parse(data=t, format='turtle') g = rdflib.Graph().parse(data=t, format='turtle')
assert len(g) == len(triples) assert len(g) == len(triples)
def test_plugin_list(self):
"""The plugin list should be of type \"plugins\""""
plugs = Plugins()
c = plugs.jsonld()
assert '@type' in c
assert c['@type'] == 'plugins'
def test_single_plugin(self): def test_single_plugin(self):
"""A response with a single plugin should still return a list""" """A response with a single plugin should still return a list"""
plugs = Plugins() plugs = Plugins()
for i in range(10): p = Plugin({'id': str(1),
p = Plugin({'id': str(i), 'version': 0,
'version': 0, 'description': 'dummy'})
'description': 'dummy'}) plugs.plugins.append(p)
plugs.plugins.append(p)
assert isinstance(plugs.plugins, list) assert isinstance(plugs.plugins, list)
js = plugs.jsonld() js = plugs.jsonld()
assert isinstance(js['plugins'], list) assert isinstance(js['plugins'], list)