1
0
mirror of https://github.com/gsi-upm/senpy synced 2025-10-21 02:38:26 +00:00

Compare commits

..

5 Commits

Author SHA1 Message Date
Daniel Suarez Souto
530982be62 Fixed some code problems 2018-09-28 11:36:53 +02:00
Daniel Suarez Souto
38b478890b Added maxSentiment plugin 2018-09-28 11:11:47 +02:00
J. Fernando Sánchez
6dd4a44924 Make algorithm part of the URI
This also includes a couple of changes URIs to pass the tests with python 3.7

Closes #50
2018-08-17 11:01:56 +02:00
J. Fernando Sánchez
4291c5eabf Fix typo in requirements 2018-07-23 19:19:05 +02:00
J. Fernando Sánchez
7c7a815d1a Add *responses* to improve mocking 2018-07-23 19:07:57 +02:00
11 changed files with 295 additions and 76 deletions

View File

@@ -12,3 +12,4 @@ rdflib-jsonld
numpy numpy
scipy scipy
scikit-learn scikit-learn
responses

View File

@@ -184,14 +184,19 @@ def basic_api(f):
return decorated_function return decorated_function
@api_blueprint.route('/', methods=['POST', 'GET']) @api_blueprint.route('/', defaults={'plugin': None}, methods=['POST', 'GET'])
@api_blueprint.route('/<path:plugin>', methods=['POST', 'GET'])
@basic_api @basic_api
def api_root(): def api_root(plugin):
if request.parameters['help']: if request.parameters['help']:
dic = dict(api.API_PARAMS, **api.NIF_PARAMS) dic = dict(api.API_PARAMS, **api.NIF_PARAMS)
response = Help(valid_parameters=dic) response = Help(valid_parameters=dic)
return response return response
req = api.parse_call(request.parameters) req = api.parse_call(request.parameters)
if plugin:
plugin = plugin.replace('+', '/')
plugin = plugin.split('/')
req.parameters['algorithm'] = plugin
return current_app.senpy.analyse(req) return current_app.senpy.analyse(req)
@@ -221,7 +226,7 @@ def plugins():
@api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET']) @api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET'])
@basic_api @basic_api
def plugin(plugin=None): def plugin(plugin):
sp = current_app.senpy sp = current_app.senpy
return sp.get_plugin(plugin) return sp.get_plugin(plugin)

View File

@@ -13,7 +13,7 @@ class Client(object):
return self.request('/', method=method, input=input, **kwargs) return self.request('/', method=method, input=input, **kwargs)
def evaluate(self, input, method='GET', **kwargs): def evaluate(self, input, method='GET', **kwargs):
return self.request('/evaluate', method = method, input=input, **kwargs) return self.request('/evaluate', method=method, input=input, **kwargs)
def plugins(self, *args, **kwargs): def plugins(self, *args, **kwargs):
resp = self.request(path='/plugins').plugins resp = self.request(path='/plugins').plugins
@@ -24,7 +24,7 @@ class Client(object):
return {d.name: d for d in resp} return {d.name: d for d 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.rstrip('/'), path)
response = requests.request(method=method, url=url, params=params) response = requests.request(method=method, url=url, params=params)
try: try:
resp = models.from_dict(response.json()) resp = models.from_dict(response.json())

View File

@@ -153,7 +153,6 @@ class Senpy(object):
yield i yield i
return return
plugin = plugins[0] plugin = plugins[0]
self._activate(plugin) # Make sure the plugin is activated
specific_params = api.parse_extra_params(req, plugin) specific_params = api.parse_extra_params(req, plugin)
req.analysis.append({'plugin': plugin, req.analysis.append({'plugin': plugin,
'parameters': specific_params}) 'parameters': specific_params})
@@ -352,7 +351,7 @@ class Senpy(object):
logger.info("Activating plugin: {}".format(plugin.name)) logger.info("Activating plugin: {}".format(plugin.name))
if sync or 'async' in plugin and not plugin.async: if sync or not getattr(plugin, 'async', True):
return self._activate(plugin) return self._activate(plugin)
else: else:
th = Thread(target=partial(self._activate, plugin)) th = Thread(target=partial(self._activate, plugin))
@@ -375,7 +374,7 @@ class Senpy(object):
self._set_active(plugin, False) self._set_active(plugin, False)
if sync or 'async' in plugin and not plugin.async: if sync or not getattr(plugin, 'async', True):
self._deactivate(plugin) self._deactivate(plugin)
else: else:
th = Thread(target=partial(self._deactivate, plugin)) th = Thread(target=partial(self._deactivate, plugin))

View File

@@ -24,6 +24,7 @@ import nltk
from .. import models, utils from .. import models, utils
from .. import api from .. import api
from .. import gsitk_compat from .. import gsitk_compat
from .. import testing
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -143,14 +144,23 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
self.log.warn('Test case failed:\n{}'.format(pprint.pformat(case))) self.log.warn('Test case failed:\n{}'.format(pprint.pformat(case)))
raise raise
def test_case(self, case): def test_case(self, case, mock=testing.MOCK_REQUESTS):
entry = models.Entry(case['entry']) entry = models.Entry(case['entry'])
given_parameters = case.get('params', case.get('parameters', {})) given_parameters = case.get('params', case.get('parameters', {}))
expected = case.get('expected', None) expected = case.get('expected', None)
should_fail = case.get('should_fail', False) should_fail = case.get('should_fail', False)
responses = case.get('responses', [])
try: try:
params = api.parse_params(given_parameters, self.extra_params) params = api.parse_params(given_parameters, self.extra_params)
res = list(self.analyse_entries([entry, ], params))
method = partial(self.analyse_entries, [entry, ], params)
if mock:
res = list(method())
else:
with testing.patch_all_requests(responses):
res = list(method())
if not isinstance(expected, list): if not isinstance(expected, list):
expected = [expected] expected = [expected]

View File

@@ -0,0 +1,187 @@
from senpy import AnalysisPlugin, easy
class maxSentiment(AnalysisPlugin):
'''Plugin to extract max emotion from a multi-empotion set'''
author = '@dsuarezsouto'
version = '0.1'
extra_params = {
'max': {
"aliases": ["maximum", "max"],
'required': True,
'options': [True, False],
"@id": 'maxSentiment',
'default': False
}
}
def analyse_entry(self, entry, params):
if not params["max"]:
yield entry
return
set_emotions= entry.emotions[0]['onyx:hasEmotion']
max_emotion =set_emotions[0]
# Extract max emotion from the set emotions (emotion with highest intensity)
for tmp_emotion in set_emotions:
if tmp_emotion['onyx:hasEmotionIntensity']>max_emotion['onyx:hasEmotionIntensity']:
max_emotion=tmp_emotion
if max_emotion['onyx:hasEmotionIntensity'] == 0:
max_emotion['onyx:hasEmotionCategory'] = "neutral"
max_emotion['onyx:hasEmotionIntensity'] = 1.0
entry.emotions[0]['onyx:hasEmotion'] = [max_emotion]
entry.emotions[0]['prov:wasGeneratedBy'] = "maxSentiment"
#print(entry)
yield entry
# Test Cases:
# 1 Normal Situation.
# 2 Case to return a Neutral Emotion.
test_cases = [{
"entry": {
"@type": "entry",
"emotions": [
{
"@id": "Emotions0",
"@type": "emotionSet",
"onyx:hasEmotion": [
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "anger",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "joy",
"onyx:hasEmotionIntensity": 0.3333333333333333
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "negative-fear",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "sadness",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "disgust",
"onyx:hasEmotionIntensity": 0
}
]
},
],
"nif:isString": "This text makes me sad.\nwhilst this text makes me happy and surprised at the same time.\nI cannot believe it!"
},
'params': {
'max': True
},
'expected' : {
"@type": "entry",
"emotions": [
{
"@id": "Emotions0",
"@type": "emotionSet",
"onyx:hasEmotion": [
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "joy",
"onyx:hasEmotionIntensity": 0.3333333333333333
}
],
"prov:wasGeneratedBy" : 'maxSentiment'
}
],
"nif:isString": "This text makes me sad.\nwhilst this text makes me happy and surprised at the same time.\nI cannot believe it!"
}
},
{
"entry": {
"@type": "entry",
"emotions": [
{
"@id": "Emotions0",
"@type": "emotionSet",
"onyx:hasEmotion": [
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "anger",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "joy",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "negative-fear",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "sadness",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "disgust",
"onyx:hasEmotionIntensity": 0
}
]
}
],
"nif:isString": "This text makes me sad.\nwhilst this text makes me happy and surprised at the same time.\nI cannot believe it!"
},
'params': {
'max': True
},
'expected' : {
"@type": "entry",
"emotions": [
{
"@id": "Emotions0",
"@type": "emotionSet",
"onyx:hasEmotion": [
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "neutral",
"onyx:hasEmotionIntensity": 1
}
],
"prov:wasGeneratedBy" : 'maxSentiment'
}
],
"nif:isString": "This text makes me sad.\nwhilst this text makes me happy and surprised at the same time.\nI cannot believe it!"
}
}]
if __name__ == '__main__':
easy()

View File

@@ -4,6 +4,8 @@ import json
from senpy.plugins import SentimentPlugin from senpy.plugins import SentimentPlugin
from senpy.models import Sentiment from senpy.models import Sentiment
ENDPOINT = 'http://www.sentiment140.com/api/bulkClassifyJson'
class Sentiment140(SentimentPlugin): class Sentiment140(SentimentPlugin):
'''Connects to the sentiment140 free API: http://sentiment140.com''' '''Connects to the sentiment140 free API: http://sentiment140.com'''
@@ -26,7 +28,7 @@ class Sentiment140(SentimentPlugin):
def analyse_entry(self, entry, params): def analyse_entry(self, entry, params):
lang = params["language"] lang = params["language"]
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson", res = requests.post(ENDPOINT,
json.dumps({ json.dumps({
"language": lang, "language": lang,
"data": [{ "data": [{
@@ -52,18 +54,6 @@ class Sentiment140(SentimentPlugin):
entry.language = lang entry.language = lang
yield entry yield entry
def test(self, *args, **kwargs):
'''
To avoid calling the sentiment140 API, we will mock the results
from requests.
'''
from senpy.testing import patch_requests
expected = {"data": [{"polarity": 4}]}
with patch_requests(expected) as (request, response):
super(Sentiment140, self).test(*args, **kwargs)
assert request.called
assert response.json.called
test_cases = [ test_cases = [
{ {
'entry': { 'entry': {
@@ -77,6 +67,9 @@ class Sentiment140(SentimentPlugin):
'marl:hasPolarity': 'marl:Positive', 'marl:hasPolarity': 'marl:Positive',
} }
] ]
} },
'responses': [{'url': ENDPOINT,
'method': 'POST',
'json': {'data': [{'polarity': 4}]}}]
} }
] ]

View File

@@ -1,36 +1,31 @@
try:
from unittest.mock import patch, MagicMock
except ImportError:
from mock import patch, MagicMock
from past.builtins import basestring from past.builtins import basestring
import os
import json import responses as requestmock
from contextlib import contextmanager
from .models import BaseModel from .models import BaseModel
@contextmanager MOCK_REQUESTS = os.environ.get('MOCK_REQUESTS', '').lower() in ['no', 'false']
def patch_requests(value, code=200):
success = MagicMock()
if isinstance(value, BaseModel): def patch_all_requests(responses):
value = value.jsonld()
if not isinstance(value, basestring): patched = requestmock.RequestsMock()
data = json.dumps(value)
for response in responses or []:
args = response.copy()
if 'json' in args and isinstance(args['json'], BaseModel):
args['json'] = args['json'].jsonld()
args['method'] = getattr(requestmock, args.get('method', 'GET'))
patched.add(**args)
return patched
def patch_requests(url, response, method='GET', status=200):
args = {'url': url, 'method': method, 'status': status}
if isinstance(response, basestring):
args['body'] = response
else: else:
data = value args['json'] = response
return patch_all_requests([args])
success.json.return_value = value
success.status_code = code
success.content = data
success.text = data
method_mocker = MagicMock()
method_mocker.return_value = success
with patch.multiple('requests', request=method_mocker,
get=method_mocker, post=method_mocker):
yield method_mocker, success
assert method_mocker.called

View File

@@ -70,7 +70,7 @@ class BlueprintsTest(TestCase):
def test_analysis_extra(self): def test_analysis_extra(self):
""" """
Extra params that have a default should Extra params that have a default should use it
""" """
resp = self.client.get("/api/?i=My aloha mohame&algo=Dummy&with_parameters=true") resp = self.client.get("/api/?i=My aloha mohame&algo=Dummy&with_parameters=true")
self.assertCode(resp, 200) self.assertCode(resp, 200)
@@ -95,6 +95,44 @@ class BlueprintsTest(TestCase):
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a") resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a")
self.assertCode(resp, 200) self.assertCode(resp, 200)
def test_analysis_url(self):
"""
The algorithm can also be specified as part of the URL
"""
self.app.config['TESTING'] = False # Errors are expected in this case
resp = self.client.get("/api/DummyRequired?i=My aloha mohame")
self.assertCode(resp, 400)
js = parse_resp(resp)
logging.debug("Got response: %s", js)
assert isinstance(js, models.Error)
resp = self.client.get("/api/DummyRequired?i=My aloha mohame&example=notvalid")
self.assertCode(resp, 400)
resp = self.client.get("/api/DummyRequired?i=My aloha mohame&example=a")
self.assertCode(resp, 200)
def test_analysis_chain(self):
"""
More than one algorithm can be specified. Plugins will then be chained
"""
resp = self.client.get("/api/Dummy?i=My aloha mohame")
js = parse_resp(resp)
assert len(js['analysis']) == 1
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'[::-1]
resp = self.client.get("/api/Dummy/Dummy?i=My aloha mohame")
# Calling dummy twice, should return the same string
self.assertCode(resp, 200)
js = parse_resp(resp)
assert len(js['analysis']) == 2
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
resp = self.client.get("/api/Dummy+Dummy?i=My aloha mohame")
# Same with pluses instead of slashes
self.assertCode(resp, 200)
js = parse_resp(resp)
assert len(js['analysis']) == 2
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
def test_error(self): def test_error(self):
""" """
The dummy plugin returns an empty response,\ The dummy plugin returns an empty response,\

View File

@@ -14,22 +14,15 @@ class ModelsTest(TestCase):
def test_client(self): def test_client(self):
endpoint = 'http://dummy/' endpoint = 'http://dummy/'
client = Client(endpoint) client = Client(endpoint)
with patch_requests(Results()) as (request, response): with patch_requests('http://dummy/', Results()):
resp = client.analyse('hello') resp = client.analyse('hello')
assert isinstance(resp, Results) assert isinstance(resp, Results)
request.assert_called_with( with patch_requests('http://dummy/', Error('Nothing')):
url=endpoint + '/', method='GET', params={'input': 'hello'})
with patch_requests(Error('Nothing')) as (request, response):
try: try:
client.analyse(input='hello', algorithm='NONEXISTENT') client.analyse(input='hello', algorithm='NONEXISTENT')
raise Exception('Exceptions should be raised. This is not golang') raise Exception('Exceptions should be raised. This is not golang')
except Error: except Error:
pass pass
request.assert_called_with(
url=endpoint + '/',
method='GET',
params={'input': 'hello',
'algorithm': 'NONEXISTENT'})
def test_plugins(self): def test_plugins(self):
endpoint = 'http://dummy/' endpoint = 'http://dummy/'
@@ -37,11 +30,8 @@ class ModelsTest(TestCase):
plugins = Plugins() plugins = Plugins()
p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'}) p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'})
plugins.plugins = [p1, ] plugins.plugins = [p1, ]
with patch_requests(plugins) as (request, response): with patch_requests('http://dummy/plugins', plugins):
response = client.plugins() response = client.plugins()
assert isinstance(response, dict) assert isinstance(response, dict)
assert len(response) == 1 assert len(response) == 1
assert "AnalysisP1" in response assert "AnalysisP1" in response
request.assert_called_with(
url=endpoint + '/plugins', method='GET',
params={})

View File

@@ -5,28 +5,29 @@ import json
from senpy.testing import patch_requests from senpy.testing import patch_requests
from senpy.models import Results from senpy.models import Results
ENDPOINT = 'http://example.com'
class TestTest(TestCase): class TestTest(TestCase):
def test_patch_text(self): def test_patch_text(self):
with patch_requests('hello'): with patch_requests(ENDPOINT, 'hello'):
r = requests.get('http://example.com') r = requests.get(ENDPOINT)
assert r.text == 'hello' assert r.text == 'hello'
assert r.content == 'hello'
def test_patch_json(self): def test_patch_json(self):
r = Results() r = Results()
with patch_requests(r): with patch_requests(ENDPOINT, r):
res = requests.get('http://example.com') res = requests.get(ENDPOINT)
assert res.content == json.dumps(r.jsonld()) assert res.text == json.dumps(r.jsonld())
js = res.json() js = res.json()
assert js assert js
assert js['@type'] == r['@type'] assert js['@type'] == r['@type']
def test_patch_dict(self): def test_patch_dict(self):
r = {'nothing': 'new'} r = {'nothing': 'new'}
with patch_requests(r): with patch_requests(ENDPOINT, r):
res = requests.get('http://example.com') res = requests.get(ENDPOINT)
assert res.content == json.dumps(r) assert res.text == json.dumps(r)
js = res.json() js = res.json()
assert js assert js
assert js['nothing'] == 'new' assert js['nothing'] == 'new'