1
0
mirror of https://github.com/gsi-upm/senpy synced 2025-09-16 19:42:21 +00:00

Compare commits

..

6 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
J. Fernando Sánchez
a3eb8f196c Several changes
* Add flag to run tests (and exit, or run the server)
* Add ntriples outformat
* Modify dependency installation logic to avoid installing several times
* Add encoded URLs as base/prefix
* Allow plugin activation to fail
2018-07-04 16:24:42 +02:00
11 changed files with 295 additions and 76 deletions

View File

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

View File

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

View File

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

View File

@@ -153,7 +153,6 @@ class Senpy(object):
yield i
return
plugin = plugins[0]
self._activate(plugin) # Make sure the plugin is activated
specific_params = api.parse_extra_params(req, plugin)
req.analysis.append({'plugin': plugin,
'parameters': specific_params})
@@ -352,7 +351,7 @@ class Senpy(object):
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)
else:
th = Thread(target=partial(self._activate, plugin))
@@ -375,7 +374,7 @@ class Senpy(object):
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)
else:
th = Thread(target=partial(self._deactivate, plugin))

View File

@@ -24,6 +24,7 @@ import nltk
from .. import models, utils
from .. import api
from .. import gsitk_compat
from .. import testing
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)))
raise
def test_case(self, case):
def test_case(self, case, mock=testing.MOCK_REQUESTS):
entry = models.Entry(case['entry'])
given_parameters = case.get('params', case.get('parameters', {}))
expected = case.get('expected', None)
should_fail = case.get('should_fail', False)
responses = case.get('responses', [])
try:
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):
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.models import Sentiment
ENDPOINT = 'http://www.sentiment140.com/api/bulkClassifyJson'
class Sentiment140(SentimentPlugin):
'''Connects to the sentiment140 free API: http://sentiment140.com'''
@@ -26,7 +28,7 @@ class Sentiment140(SentimentPlugin):
def analyse_entry(self, entry, params):
lang = params["language"]
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
res = requests.post(ENDPOINT,
json.dumps({
"language": lang,
"data": [{
@@ -52,18 +54,6 @@ class Sentiment140(SentimentPlugin):
entry.language = lang
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 = [
{
'entry': {
@@ -77,6 +67,9 @@ class Sentiment140(SentimentPlugin):
'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
import json
from contextlib import contextmanager
import os
import responses as requestmock
from .models import BaseModel
@contextmanager
def patch_requests(value, code=200):
success = MagicMock()
if isinstance(value, BaseModel):
value = value.jsonld()
if not isinstance(value, basestring):
data = json.dumps(value)
MOCK_REQUESTS = os.environ.get('MOCK_REQUESTS', '').lower() in ['no', 'false']
def patch_all_requests(responses):
patched = requestmock.RequestsMock()
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:
data = value
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
args['json'] = response
return patch_all_requests([args])

View File

@@ -70,7 +70,7 @@ class BlueprintsTest(TestCase):
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")
self.assertCode(resp, 200)
@@ -95,6 +95,44 @@ class BlueprintsTest(TestCase):
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a")
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):
"""
The dummy plugin returns an empty response,\

View File

@@ -14,22 +14,15 @@ class ModelsTest(TestCase):
def test_client(self):
endpoint = 'http://dummy/'
client = Client(endpoint)
with patch_requests(Results()) as (request, response):
with patch_requests('http://dummy/', Results()):
resp = client.analyse('hello')
assert isinstance(resp, Results)
request.assert_called_with(
url=endpoint + '/', method='GET', params={'input': 'hello'})
with patch_requests(Error('Nothing')) as (request, response):
with patch_requests('http://dummy/', Error('Nothing')):
try:
client.analyse(input='hello', algorithm='NONEXISTENT')
raise Exception('Exceptions should be raised. This is not golang')
except Error:
pass
request.assert_called_with(
url=endpoint + '/',
method='GET',
params={'input': 'hello',
'algorithm': 'NONEXISTENT'})
def test_plugins(self):
endpoint = 'http://dummy/'
@@ -37,11 +30,8 @@ class ModelsTest(TestCase):
plugins = Plugins()
p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'})
plugins.plugins = [p1, ]
with patch_requests(plugins) as (request, response):
with patch_requests('http://dummy/plugins', plugins):
response = client.plugins()
assert isinstance(response, dict)
assert len(response) == 1
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.models import Results
ENDPOINT = 'http://example.com'
class TestTest(TestCase):
def test_patch_text(self):
with patch_requests('hello'):
r = requests.get('http://example.com')
with patch_requests(ENDPOINT, 'hello'):
r = requests.get(ENDPOINT)
assert r.text == 'hello'
assert r.content == 'hello'
def test_patch_json(self):
r = Results()
with patch_requests(r):
res = requests.get('http://example.com')
assert res.content == json.dumps(r.jsonld())
with patch_requests(ENDPOINT, r):
res = requests.get(ENDPOINT)
assert res.text == json.dumps(r.jsonld())
js = res.json()
assert js
assert js['@type'] == r['@type']
def test_patch_dict(self):
r = {'nothing': 'new'}
with patch_requests(r):
res = requests.get('http://example.com')
assert res.content == json.dumps(r)
with patch_requests(ENDPOINT, r):
res = requests.get(ENDPOINT)
assert res.text == json.dumps(r)
js = res.json()
assert js
assert js['nothing'] == 'new'