1
0
mirror of https://github.com/gsi-upm/senpy synced 2025-09-17 12:02:21 +00:00

Compare commits

..

6 Commits

Author SHA1 Message Date
J. Fernando Sánchez
748d1a00bd Fix bug in POST 2018-10-30 16:35:17 +01:00
J. Fernando Sánchez
a82e4ed440 Fix bug in py3.5 2018-10-30 16:14:06 +01:00
J. Fernando Sánchez
c939b095de Fix POST. Closes senpy/senpy#56 2018-10-30 15:15:37 +01: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
13 changed files with 163 additions and 90 deletions

View File

@@ -11,7 +11,7 @@ class Async(AnalysisPlugin):
'''An example of an asynchronous module''' '''An example of an asynchronous module'''
author = '@balkian' author = '@balkian'
version = '0.2' version = '0.2'
async = True sync = False
def _do_async(self, num_processes): def _do_async(self, num_processes):
pool = multiprocessing.Pool(processes=num_processes) pool = multiprocessing.Pool(processes=num_processes)

View File

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

View File

@@ -69,7 +69,7 @@ def encoded_url(url=None, base=None):
if request.method == 'GET': if request.method == 'GET':
url = request.full_path[1:] # Remove the first slash url = request.full_path[1:] # Remove the first slash
else: else:
hash(frozenset(request.form.params().items())) hash(frozenset(tuple(request.parameters.items())))
code = 'hash:{}'.format(hash) code = 'hash:{}'.format(hash)
code = code or base64.urlsafe_b64encode(url.encode()).decode() code = code or base64.urlsafe_b64encode(url.encode()).decode()
@@ -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

@@ -24,8 +24,12 @@ 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)
if method == 'POST':
response = requests.post(url=url, data=params)
else:
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())
except Exception as ex: except Exception as ex:

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) or getattr(plugin, 'sync', False):
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) or not getattr(plugin, 'sync', False):
self._deactivate(plugin) self._deactivate(plugin)
else: else:
th = Thread(target=partial(self._deactivate, plugin)) th = Thread(target=partial(self._deactivate, plugin))

View File

@@ -1,5 +1,7 @@
import logging import logging
from pkg_resources import parse_version, get_distribution, DistributionNotFound
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MSG = 'GSITK is not (properly) installed.' MSG = 'GSITK is not (properly) installed.'
@@ -12,12 +14,18 @@ def raise_exception(*args, **kwargs):
try: try:
gsitk_distro = get_distribution("gsitk")
GSITK_VERSION = parse_version(gsitk_distro.version)
GSITK_AVAILABLE = GSITK_VERSION > parse_version("0.1.9.1") # Earlier versions have a bug
except DistributionNotFound:
GSITK_AVAILABLE = False
GSITK_VERSION = ()
if GSITK_AVAILABLE:
from gsitk.datasets.datasets import DatasetManager from gsitk.datasets.datasets import DatasetManager
from gsitk.evaluation.evaluation import Evaluation as Eval from gsitk.evaluation.evaluation import Evaluation as Eval
from sklearn.pipeline import Pipeline from sklearn.pipeline import Pipeline
GSITK_AVAILABLE = True
modules = locals() modules = locals()
except ImportError: else:
logger.warn(IMPORTMSG) logger.warning(IMPORTMSG)
GSITK_AVAILABLE = False
DatasetManager = Eval = Pipeline = raise_exception DatasetManager = Eval = Pipeline = raise_exception

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

@@ -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

@@ -67,10 +67,24 @@ class BlueprintsTest(TestCase):
logging.debug("Got response: %s", js) logging.debug("Got response: %s", js)
assert "@context" in js assert "@context" in js
assert "entries" in js assert "entries" in js
assert len(js['analysis']) == 1
def test_analysis_post(self):
"""
The results for a POST request should be the same as for a GET request.
"""
resp = self.client.post("/api/", data={'i': 'My aloha mohame',
'with_parameters': True})
self.assertCode(resp, 200)
js = parse_resp(resp)
logging.debug("Got response: %s", js)
assert "@context" in js
assert "entries" in js
assert len(js['analysis']) == 1
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 +109,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,28 @@ 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 + '/', def test_client_post(self):
method='GET', endpoint = 'http://dummy/'
params={'input': 'hello', client = Client(endpoint)
'algorithm': 'NONEXISTENT'}) with patch_requests('http://dummy/', Results()):
resp = client.analyse('hello')
assert isinstance(resp, Results)
with patch_requests('http://dummy/', Error('Nothing'), method='POST'):
try:
client.analyse(input='hello', method='POST', algorithm='NONEXISTENT')
raise Exception('Exceptions should be raised. This is not golang')
except Error:
pass
def test_plugins(self): def test_plugins(self):
endpoint = 'http://dummy/' endpoint = 'http://dummy/'
@@ -37,11 +43,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

@@ -1,15 +1,15 @@
#!/bin/env python #!/bin/env python
import os import os
import sys
import pickle import pickle
import shutil import shutil
import tempfile import tempfile
from unittest import TestCase from unittest import TestCase, skipIf
from senpy.models import Results, Entry, EmotionSet, Emotion, Plugins from senpy.models import Results, Entry, EmotionSet, Emotion, Plugins
from senpy import plugins from senpy import plugins
from senpy.plugins.conversion.emotion.centroids import CentroidConversion from senpy.plugins.conversion.emotion.centroids import CentroidConversion
from senpy.gsitk_compat import GSITK_AVAILABLE
import pandas as pd import pandas as pd
@@ -346,13 +346,15 @@ class PluginsTest(TestCase):
smart_metrics = results[0].metrics[0] smart_metrics = results[0].metrics[0]
assert abs(smart_metrics['accuracy'] - 1) < 0.01 assert abs(smart_metrics['accuracy'] - 1) < 0.01
@skipIf(not GSITK_AVAILABLE, "GSITK is not available")
def test_evaluation(self): def test_evaluation(self):
if sys.version_info < (3, 0): self._test_evaluation()
@skipIf(GSITK_AVAILABLE, "GSITK is available")
def test_evaluation_unavailable(self):
with self.assertRaises(Exception) as context: with self.assertRaises(Exception) as context:
self._test_evaluation() self._test_evaluation()
self.assertTrue('GSITK ' in str(context.exception)) self.assertTrue('GSITK ' in str(context.exception))
else:
self._test_evaluation()
def make_mini_test(fpath): def make_mini_test(fpath):

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'