diff --git a/app.py b/app.py index 92c596a..22eba07 100644 --- a/app.py +++ b/app.py @@ -19,7 +19,7 @@ This is a helper for development. If you want to run Senpy use: python -m senpy """ -from gevent.monkey import patch_all; patch_all() +from gevent.monkey import patch_all patch_all() import gevent import config from flask import Flask diff --git a/plugins/rand/rand.py b/plugins/rand/rand.py index 43341a8..1cda512 100644 --- a/plugins/rand/rand.py +++ b/plugins/rand/rand.py @@ -11,8 +11,7 @@ class Sentiment140Plugin(SentimentPlugin): p = params.get("prefix", None) response = Response(prefix=p) - #polarity_value = self.maxPolarityValue*int(res.json()["data"][0]["polarity"]) * 0.25 - polarity_value = max(-1, min(1, random.gauss(0.2,0.2))) + polarity_value = max(-1, min(1, random.gauss(0.2, 0.2))) polarity = "marl:Neutral" if polarity_value > 0: polarity = "marl:Positive" diff --git a/plugins/rand/rand.senpy b/plugins/rand/rand.senpy index 4125694..b7c8c57 100644 --- a/plugins/rand/rand.senpy +++ b/plugins/rand/rand.senpy @@ -6,6 +6,7 @@ "version": "0.1", "extra_params": { "language": { + "@id": "lang_rand", "aliases": ["language", "l"], "required": false, "options": ["es", "en", "auto"] diff --git a/plugins/sentiment140/sentiment140.py b/plugins/sentiment140/sentiment140.py index 237a804..99aefd8 100644 --- a/plugins/sentiment140/sentiment140.py +++ b/plugins/sentiment140/sentiment140.py @@ -17,7 +17,8 @@ class Sentiment140Plugin(SentimentPlugin): p = params.get("prefix", None) response = Response(prefix=p) - polarity_value = self.maxPolarityValue*int(res.json()["data"][0]["polarity"]) * 0.25 + polarity_value = self.maxPolarityValue*int(res.json()["data"][0] + ["polarity"]) * 0.25 polarity = "marl:Neutral" if polarity_value > 50: polarity = "marl:Positive" diff --git a/plugins/sentiment140/sentiment140.senpy b/plugins/sentiment140/sentiment140.senpy index 453c23b..976ae4a 100644 --- a/plugins/sentiment140/sentiment140.senpy +++ b/plugins/sentiment140/sentiment140.senpy @@ -6,6 +6,7 @@ "version": "0.1", "extra_params": { "language": { + "@id": "lang_sentiment140", "aliases": ["language", "l"], "required": false, "options": ["es", "en", "auto"] diff --git a/senpy/__main__.py b/senpy/__main__.py index 961d9f9..57fa456 100644 --- a/senpy/__main__.py +++ b/senpy/__main__.py @@ -19,15 +19,18 @@ Senpy is a modular sentiment analysis server. This script runs an instance of the server. """ -from gevent.monkey import patch_all; patch_all(thread=False) -import gevent + from flask import Flask from senpy.extensions import Senpy +from gevent.wsgi import WSGIServer +from gevent.monkey import patch_all +import gevent import logging import os -from gevent.wsgi import WSGIServer import argparse +patch_all(thread=False) + if __name__ == '__main__': parser = argparse.ArgumentParser(description='Run a Senpy server') parser.add_argument('--level', @@ -43,20 +46,20 @@ if __name__ == '__main__': help='Run the application in debug mode') parser.add_argument('--host', type=str, - default = "127.0.0.1", + default="127.0.0.1", help='Use 0.0.0.0 to accept requests from any host.') parser.add_argument('--port', '-p', type=int, - default = 5000, + default=5000, help='Port to listen on.') parser.add_argument('--plugins-folder', '-f', type=str, - default = "plugins", + default="plugins", help='Where to look for plugins.') args = parser.parse_args() - logging.basicConfig(level=getattr(logging,args.level)) + logging.basicConfig(level=getattr(logging, args.level)) app = Flask(__name__) app.debug = args.debug sp = Senpy(app, args.plugins_folder) diff --git a/senpy/blueprints.py b/senpy/blueprints.py index 833d4c9..a331a09 100644 --- a/senpy/blueprints.py +++ b/senpy/blueprints.py @@ -17,19 +17,35 @@ """ Blueprints for Senpy """ +from flask import Blueprint, request, current_app +from .models import Error, Response + import json import logging logger = logging.getLogger(__name__) -from flask import Blueprint, request, jsonify, current_app nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__) BASIC_PARAMS = { - "algorithm": {"aliases": ["algorithm", "a", "algo"], - "required": False, - }, + "algorithm": { + "aliases": ["algorithm", "a", "algo"], + "required": False, + }, + "inHeaders": { + "aliases": ["inHeaders", "headers"], + "required": True, + "default": "0" + } +} + +LIST_PARAMS = { + "params": { + "aliases": ["params", "with_params"], + "required": False, + "default": "0" + }, } @@ -44,34 +60,40 @@ def get_params(req, params=BASIC_PARAMS): outdict = {} wrong_params = {} for param, options in params.iteritems(): - for alias in options["aliases"]: - if alias in indict: - outdict[param] = indict[alias] - if param not in outdict: - if options.get("required", False) and "default" not in options: - wrong_params[param] = params[param] + if param[0] != "@": # Exclude json-ld properties + logger.debug("Param: %s - Options: %s", param, options) + for alias in options["aliases"]: + if alias in indict: + outdict[param] = indict[alias] + if param not in outdict: + if options.get("required", False) and "default" not in options: + wrong_params[param] = params[param] + else: + if "default" in options: + outdict[param] = options["default"] else: - if "default" in options: - outdict[param] = options["default"] - else: - if "options" in params[param] and outdict[param] not in params[param]["options"]: - wrong_params[param] = params[param] + if "options" in params[param] and \ + outdict[param] not in params[param]["options"]: + wrong_params[param] = params[param] if wrong_params: - message = {"status": "failed", - "message": "Missing or invalid parameters", - "parameters": outdict, - "errors": {param: error for param, error in wrong_params.iteritems()} - } + message = Error({"status": 404, + "message": "Missing or invalid parameters", + "parameters": outdict, + "errors": {param: error for param, error in + wrong_params.iteritems()} + }) raise ValueError(message) return outdict def basic_analysis(params): - response = {"@context": ["http://demos.gsi.dit.upm.es/eurosentiment/static/context.jsonld", - { - "@base": "{}#".format(request.url.encode('utf-8')) - } - ], + response = {"@context": + [("http://demos.gsi.dit.upm.es/" + "eurosentiment/static/context.jsonld"), + { + "@base": "{}#".format(request.url.encode('utf-8')) + } + ], "analysis": [{"@type": "marl:SentimentAnalysis"}], "entries": [] } @@ -91,19 +113,25 @@ def home(): params = get_params(request) algo = params.get("algorithm", None) specific_params = current_app.senpy.parameters(algo) + logger.debug( + "Specific params: %s", json.dumps(specific_params, indent=4)) params.update(get_params(request, specific_params)) response = current_app.senpy.analyse(**params) - return jsonify(response) + in_headers = params["inHeaders"] != "0" + return response.flask(in_headers=in_headers) except ValueError as ex: - return jsonify(ex.message) - except Exception as ex: - return jsonify(status="400", message=ex.message) + return ex.message.flask() @nif_blueprint.route("/default") def default(): - return current_app.senpy.default_plugin - #return plugins(action="list", plugin=current_app.senpy.default_algorithm) + # return current_app.senpy.default_plugin + plug = current_app.senpy.default_plugin + if plug: + return plugins(action="list", plugin=plug.name) + else: + error = Error(status=404, message="No plugins found") + return error.flask() @nif_blueprint.route('/plugins/', methods=['POST', 'GET']) @@ -118,12 +146,15 @@ def plugins(plugin=None, action="list"): if plugin and not plugs: return "Plugin not found", 400 if action == "list": - with_params = request.args.get("params", "") == "1" + with_params = get_params(request, LIST_PARAMS)["params"] == "1" + in_headers = get_params(request, BASIC_PARAMS)["inHeaders"] != "0" if plugin: - dic = plugs[plugin].jsonable(with_params) + dic = plugs[plugin] else: - dic = {plug: plugs[plug].jsonable(with_params) for plug in plugs} - return jsonify(dic) + dic = Response( + {plug: plugs[plug].jsonld(with_params) for plug in plugs}, + frame={}) + return dic.flask(in_headers=in_headers) method = "{}_plugin".format(action) if(hasattr(sp, method)): getattr(sp, method)(plugin) diff --git a/senpy/context.jsonld b/senpy/context.jsonld index 936db98..fa5f7e3 100644 --- a/senpy/context.jsonld +++ b/senpy/context.jsonld @@ -36,5 +36,7 @@ }, "text": { "@id": "nif:isString" }, "wnaffect": "http://www.gsi.dit.upm.es/ontologies/wnaffect#", - "xsd": "http://www.w3.org/2001/XMLSchema#" + "xsd": "http://www.w3.org/2001/XMLSchema#", + "senpy": "http://www.gsi.dit.upm.es/ontologies/senpy/ns#", + "@vocab": "http://www.gsi.dit.upm.es/ontologies/senpy/ns#" } diff --git a/senpy/extensions.py b/senpy/extensions.py index 2798831..03bc554 100644 --- a/senpy/extensions.py +++ b/senpy/extensions.py @@ -1,5 +1,12 @@ """ """ +from .plugins import SenpyPlugin, SentimentPlugin, EmotionPlugin +from .models import Error +from .blueprints import nif_blueprint + +from git import Repo, InvalidGitRepositoryError +from functools import partial + import os import fnmatch import inspect @@ -11,15 +18,9 @@ import json logger = logging.getLogger(__name__) -from .plugins import SenpyPlugin, SentimentPlugin, EmotionPlugin -from .models import Error - -from .blueprints import nif_blueprint -from git import Repo, InvalidGitRepositoryError -from functools import partial - class Senpy(object): + """ Default Senpy extension for Flask """ def __init__(self, app=None, plugin_folder="plugins"): @@ -66,32 +67,45 @@ class Senpy(object): if "algorithm" in params: algo = params["algorithm"] elif self.plugins: - algo = self.default_plugin + algo = self.default_plugin and self.default_plugin.name + if not algo: + return Error(status=404, + message=("No plugins found." + " Please install one.").format(algo)) if algo in self.plugins: if self.plugins[algo].is_activated: plug = self.plugins[algo] resp = plug.analyse(**params) resp.analysis.append(plug) + logger.debug("Returning analysis result: {}".format(resp)) return resp else: logger.debug("Plugin not activated: {}".format(algo)) - return Error(status=400, message="The algorithm '{}' is not activated yet".format(algo)) + return Error(status=400, + message=("The algorithm '{}'" + " is not activated yet").format(algo)) else: - logger.debug("The algorithm '{}' is not valid\nValid algorithms: {}".format(algo, self.plugins.keys())) - return Error(status=400, message="The algorithm '{}' is not valid".format(algo)) + logger.debug(("The algorithm '{}' is not valid\n" + "Valid algorithms: {}").format(algo, + self.plugins.keys())) + return Error(status=400, + message="The algorithm '{}' is not valid" + .format(algo)) @property def default_plugin(self): candidates = self.filter_plugins(is_activated=True) if len(candidates) > 0: - candidate = candidates.keys()[0] + candidate = candidates.values()[0] logger.debug("Default: {}".format(candidate)) return candidate else: return None def parameters(self, algo): - return getattr(self.plugins.get(algo or self.default_plugin), "params", {}) + return getattr(self.plugins.get(algo) or self.default_plugin, + "params", + {}) def activate_all(self, sync=False): ps = [] @@ -137,20 +151,22 @@ class Senpy(object): def _load_plugin(root, filename): logger.debug("Loading plugin: {}".format(filename)) fpath = os.path.join(root, filename) - with open(fpath,'r') as f: + with open(fpath, 'r') as f: info = json.load(f) logger.debug("Info: {}".format(info)) sys.path.append(root) module = info["module"] name = info["name"] - (fp, pathname, desc) = imp.find_module(module, [root,]) + (fp, pathname, desc) = imp.find_module(module, [root, ]) try: tmp = imp.load_module(module, fp, pathname, desc) sys.path.remove(root) candidate = None for _, obj in inspect.getmembers(tmp): if inspect.isclass(obj) and inspect.getmodule(obj) == tmp: - logger.debug("Found plugin class: {}@{}".format(obj, inspect.getmodule(obj))) + logger.debug(("Found plugin class:" + " {}@{}").format(obj, inspect.getmodule(obj)) + ) candidate = obj break if not candidate: diff --git a/senpy/models.py b/senpy/models.py index 4f43226..20517eb 100644 --- a/senpy/models.py +++ b/senpy/models.py @@ -2,7 +2,8 @@ import json import os from collections import defaultdict from pyld import jsonld - +import logging +from flask import Response as FlaskResponse class Leaf(dict): @@ -11,18 +12,17 @@ class Leaf(dict): _context = {} def __init__(self, - id=None, - context=None, - vocab=None, - prefix=None, - frame=None): - super(Leaf, self).__init__() + *args, + **kwargs): + + id = kwargs.pop("id", None) + context = kwargs.pop("context", self._context) + vocab = kwargs.pop("vocab", None) + prefix = kwargs.pop("prefix", None) + frame = kwargs.pop("frame", None) + super(Leaf, self).__init__(*args, **kwargs) if context is not None: self.context = context - elif self._context: - self.context = self._context - else: - self.context = {} if frame is not None: self._frame = frame self._prefix = prefix @@ -60,10 +60,11 @@ class Leaf(dict): def get_id(self, id): """ - This is not the most elegant solution to change the @id attribute, but it - is the quickest way to have it included in the dictionary without extra - boilerplate. + Get id, dealing with prefixes """ + # This is not the most elegant solution to change the @id attribute, + # but it is the quickest way to have it included in the dictionary + # without extra boilerplate. if id and self._prefix and ":" not in id: return "{}{}".format(self._prefix, id) else: @@ -97,7 +98,7 @@ class Leaf(dict): return context def compact(self): - return jsonld.compact(self, self.context) + return jsonld.compact(self, self.get_context(self.context)) def frame(self, frame=None, options=None): if frame is None: @@ -106,84 +107,100 @@ class Leaf(dict): options = {} return jsonld.frame(self, frame, options) - def jsonable(self, parameters=False, frame=None, options=None, context=None): + def jsonld(self, frame=None, options=None, + context=None, removeContext=None): + if removeContext is None: + removeContext = Response._context # Loop? if frame is None: frame = self._frame - if options is None: - options = {} if context is None: - context = self._context - return jsonld.compact(jsonld.frame(self, frame, options), context) - #if parameters: - #resp["parameters"] = self.params - #elif self.extra_params: - #resp["extra_parameters"] = self.extra_params - #return resp + context = self.context + else: + context = self.get_context(context) + # For some reason, this causes errors with pyld + # if options is None: + # options = {"expandContext": context.copy() } + js = self + if frame: + logging.debug("Framing: %s", json.dumps(self, indent=4)) + logging.debug("Framing with %s", json.dumps(frame, indent=4)) + js = jsonld.frame(js, frame, options) + logging.debug("Result: %s", json.dumps(js, indent=4)) + logging.debug("Compacting with %s", json.dumps(context, indent=4)) + js = jsonld.compact(js, context, options) + logging.debug("Result: %s", json.dumps(js, indent=4)) + if removeContext == context: + del js["@context"] + return js - - def to_JSON(self): - return json.dumps(self, + def to_JSON(self, removeContext=None): + return json.dumps(self.jsonld(removeContext=removeContext), default=lambda o: o.__dict__, sort_keys=True, indent=4) + def flask(self, + in_headers=False, + url="http://demos.gsi.dit.upm.es/senpy/senpy.jsonld"): + """ + Return the values and error to be used in flask + """ + js = self.jsonld() + headers = None + if in_headers: + ctx = js["@context"] + headers = { + "Link": ('<%s>;' + 'rel="http://www.w3.org/ns/json-ld#context";' + ' type="application/ld+json"' % url) + } + del js["@context"] + return FlaskResponse(json.dumps(js), + status=self.get("status", 200), + headers=headers, + mimetype="application/json") class Response(Leaf): - _frame = { "@context": { - "analysis": { - "@container": "@set", - "@id": "prov:wasInformedBy" - }, - "date": { - "@id": "dc:date", - "@type": "xsd:dateTime" - }, - "dc": "http://purl.org/dc/terms/", - "dc:subject": { - "@type": "@id" - }, - "emotions": { - "@container": "@set", - "@id": "onyx:hasEmotionSet" - }, - "entries": { - "@container": "@set", - "@id": "prov:generated" - }, - "marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#", - "nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#", - "onyx": "http://www.gsi.dit.upm.es/ontologies/onyx/ns#", - "opinions": { - "@container": "@set", - "@id": "marl:hasOpinion" - }, - "prov": "http://www.w3.org/ns/prov#", - "rdfs": "http://www.w3.org/2000/01/rdf-schema#", - "strings": { - "@container": "@set", - "@reverse": "nif:hasContext" - }, - "wnaffect": "http://www.gsi.dit.upm.es/ontologies/wnaffect#", - "xsd": "http://www.w3.org/2001/XMLSchema#" - }, - "analysis": {}, - "entries": {} + _context = Leaf.get_context("{}/context.jsonld".format( + os.path.dirname(os.path.realpath(__file__)))) + _frame = { + "@context": _context, + "analysis": { + "@explicit": True, + "maxPolarityValue": {}, + "minPolarityValue": {}, + "name": {}, + "version": {}, + }, + "entries": {} } - def __init__(self, context=None, *args, **kwargs): + def __init__(self, *args, **kwargs): + context = kwargs.pop("context", None) + frame = kwargs.pop("frame", None) if context is None: - context = "{}/context.jsonld".format(os.path.dirname( - os.path.realpath(__file__))) - super(Response, self).__init__(*args, context=context, **kwargs) - self.analysis = [] - self.entries = [] + context = self._context + self.context = context + super(Response, self).__init__( + *args, context=context, frame=frame, **kwargs) + if self._frame is not None and "entries" in self._frame: + self.analysis = [] + self.entries = [] + + def jsonld(self, frame=None, options=None, context=None, removeContext={}): + return super(Response, self).jsonld(frame, + options, + context, + removeContext) class Entry(Leaf): _context = { - "@vocab": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#" + "@vocab": ("http://persistence.uni-leipzig.org/" + "nlp2rdf/ontologies/nif-core#") } + def __init__(self, text=None, emotion_sets=None, opinions=None, **kwargs): super(Entry, self).__init__(**kwargs) if text: @@ -191,10 +208,12 @@ class Entry(Leaf): self.emotionSets = emotion_sets if emotion_sets else [] self.opinions = opinions if opinions else [] + class Opinion(Leaf): _context = { "@vocab": "http://www.gsi.dit.upm.es/ontologies/marl/ns#" } + def __init__(self, polarityValue=None, hasPolarity=None, *args, **kwargs): super(Opinion, self).__init__(*args, **kwargs) @@ -206,6 +225,7 @@ class Opinion(Leaf): class EmotionSet(Leaf): _context = {} + def __init__(self, emotions=None, *args, **kwargs): if not emotions: emotions = [] @@ -214,10 +234,16 @@ class EmotionSet(Leaf): **kwargs) self.emotions = emotions or [] + class Emotion(Leaf): _context = {} -class Error(Leaf): + +class Error(Response): + # A better pattern would be this: + # http://flask.pocoo.org/docs/0.10/patterns/apierrors/ + _frame = {} + _context = {} + def __init__(self, *args, **kwargs): - super(Error, self).__init__(*args) - self.update(kwargs) + super(Error, self).__init__(*args, **kwargs) diff --git a/senpy/plugins.py b/senpy/plugins.py index a30838a..b958237 100644 --- a/senpy/plugins.py +++ b/senpy/plugins.py @@ -1,67 +1,89 @@ + import logging import ConfigParser -from .models import Leaf +from .models import Response, Leaf logger = logging.getLogger(__name__) -PARAMS = {"input": {"aliases": ["i", "input"], - "required": True, - "help": "Input text" - }, - "informat": {"aliases": ["f", "informat"], - "required": False, - "default": "text", - "options": ["turtle", "text"], - }, - "intype": {"aliases": ["intype", "t"], - "required": False, - "default": "direct", - "options": ["direct", "url", "file"], - }, - "outformat": {"aliases": ["outformat", "o"], - "default": "json-ld", - "required": False, - "options": ["json-ld"], - }, - "language": {"aliases": ["language", "l"], - "required": False, - "options": ["es", "en"], - }, - "prefix": {"aliases": ["prefix", "p"], - "required": True, - "default": "", - }, - "urischeme": {"aliases": ["urischeme", "u"], - "required": False, - "default": "RFC5147String", - "options": "RFC5147String" - }, - } +PARAMS = { + "input": { + "@id": "input", + "aliases": ["i", "input"], + "required": True, + "help": "Input text" + }, + "informat": { + "@id": "informat", + "aliases": ["f", "informat"], + "required": False, + "default": "text", + "options": ["turtle", "text"], + }, + "intype": { + "@id": "intype", + "aliases": ["intype", "t"], + "required": False, + "default": "direct", + "options": ["direct", "url", "file"], + }, + "outformat": { + "@id": "outformat", + "aliases": ["outformat", "o"], + "default": "json-ld", + "required": False, + "options": ["json-ld"], + }, + "language": { + "@id": "language", + "aliases": ["language", "l"], + "required": False, + }, + "prefix": { + "@id": "prefix", + "aliases": ["prefix", "p"], + "required": True, + "default": "", + }, + "urischeme": { + "@id": "urischeme", + "aliases": ["urischeme", "u"], + "required": False, + "default": "RFC5147String", + "options": "RFC5147String" + }, +} class SenpyPlugin(Leaf): - _context = {"@vocab": "http://www.gsi.dit.upm.es/ontologies/senpy/ns#", - "info": None} - _frame = { "@context": _context, + _context = Leaf.get_context(Response._context) + _frame = {"@context": _context, "name": {}, - "@explicit": False, + "extra_params": {"@container": "@index"}, + "@explicit": True, "version": {}, "repo": None, - "info": None, + "is_activated": {}, + "params": None, } + def __init__(self, info=None): if not info: - raise ValueError("You need to provide configuration information for the plugin.") + raise ValueError(("You need to provide configuration" + "information for the plugin.")) logger.debug("Initialising {}".format(info)) super(SenpyPlugin, self).__init__() self.name = info["name"] self.version = info["version"] - self.id="{}_{}".format(self.name, self.version) + self.id = "{}_{}".format(self.name, self.version) self.params = info.get("params", PARAMS.copy()) + if "@id" not in self.params: + self.params["@id"] = "params_%s" % self.id self.extra_params = info.get("extra_params", {}) - self.params.update(self.extra_params) + self.params.update(self.extra_params.copy()) + if "@id" not in self.extra_params: + self.extra_params["@id"] = "extra_params_%s" % self.id self.is_activated = False - self.info = info + self._info = info def analyse(self, *args, **kwargs): logger.debug("Analysing with: {} {}".format(self.name, self.version)) @@ -73,6 +95,12 @@ class SenpyPlugin(Leaf): def deactivate(self): pass + def jsonld(self, parameters=False, *args, **kwargs): + nframe = kwargs.pop("frame", self._frame) + if parameters: + nframe = nframe.copy() + nframe["params"] = {} + return super(SenpyPlugin, self).jsonld(frame=nframe, *args, **kwargs) @property def id(self): @@ -80,12 +108,15 @@ class SenpyPlugin(Leaf): class SentimentPlugin(SenpyPlugin): + def __init__(self, info, *args, **kwargs): super(SentimentPlugin, self).__init__(info, *args, **kwargs) self.minPolarityValue = float(info.get("minPolarityValue", 0)) self.maxPolarityValue = float(info.get("maxPolarityValue", 1)) + class EmotionPlugin(SenpyPlugin): + def __init__(self, info, *args, **kwargs): resp = super(EmotionPlugin, self).__init__(info, *args, **kwargs) self.minEmotionValue = float(info.get("minEmotionValue", 0)) diff --git a/setup.py b/setup.py index c6d8d82..9bd8553 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ install_reqs = parse_requirements("requirements.txt") # e.g. ['django==1.5.1', 'mezzanine==1.4.6'] reqs = [str(ir.req) for ir in install_reqs] -VERSION = "0.3.2" +VERSION = "0.4.0" print(reqs) @@ -23,9 +23,10 @@ extendable, so new algorithms and sources can be used. author='J. Fernando Sanchez', author_email='balkian@gmail.com', url='https://github.com/gsi-upm/senpy', # use the URL to the github repo - download_url='https://github.com/gsi-upm/senpy/archive/{}.tar.gz'.format(VERSION), - keywords=['eurosentiment', 'sentiment', 'emotions', 'nif'], # arbitrary keywords + download_url='https://github.com/gsi-upm/senpy/archive/{}.tar.gz' + .format(VERSION), + keywords=['eurosentiment', 'sentiment', 'emotions', 'nif'], classifiers=[], install_requires=reqs, - include_package_data = True, + include_package_data=True, ) diff --git a/tests/blueprints_test/__init__.py b/tests/blueprints_test/__init__.py index 3dcc383..72c05d8 100644 --- a/tests/blueprints_test/__init__.py +++ b/tests/blueprints_test/__init__.py @@ -9,6 +9,7 @@ from senpy.extensions import Senpy from flask import Flask from flask.ext.testing import TestCase from gevent import sleep +from itertools import product def check_dict(indic, template): @@ -16,6 +17,7 @@ def check_dict(indic, template): class BlueprintsTest(TestCase): + def create_app(self): self.app = Flask("test_extensions") self.senpy = Senpy() @@ -26,24 +28,31 @@ class BlueprintsTest(TestCase): return self.app def test_home(self): - """ Calling with no arguments should ask the user for more arguments """ + """ + Calling with no arguments should ask the user for more arguments + """ resp = self.client.get("/") - self.assert200(resp) + self.assert404(resp) logging.debug(resp.json) - assert resp.json["status"] == "failed" + assert resp.json["status"] == 404 atleast = { - "status": "failed", + "status": 404, "message": "Missing or invalid parameters", } assert check_dict(resp.json, atleast) def test_analysis(self): - """ The dummy plugin returns an empty response, it should contain the context """ + """ + The dummy plugin returns an empty response,\ + it should contain the context + """ resp = self.client.get("/?i=My aloha mohame") self.assert200(resp) - logging.debug(resp.json) + logging.debug("Got response: %s", resp.json) assert "@context" in resp.json - assert check_dict(resp.json["@context"], {"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#"}) + assert check_dict( + resp.json["@context"], + {"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#"}) assert "entries" in resp.json def test_list(self): @@ -52,6 +61,19 @@ class BlueprintsTest(TestCase): self.assert200(resp) logging.debug(resp.json) assert "Dummy" in resp.json + assert "@context" in resp.json + + def test_headers(self): + for i, j in product(["/plugins/?nothing=", "/?i=test&"], + ["headers", "inHeaders"]): + resp = self.client.get("%s" % (i)) + assert "@context" in resp.json + resp = self.client.get("%s&%s=0" % (i, j)) + assert "@context" in resp.json + resp = self.client.get("%s&%s=1" % (i, j)) + assert "@context" not in resp.json + resp = self.client.get("%s&%s=true" % (i, j)) + assert "@context" not in resp.json def test_detail(self): """ Show only one plugin""" @@ -77,3 +99,16 @@ class BlueprintsTest(TestCase): self.assert200(resp) assert "is_activated" in resp.json assert resp.json["is_activated"] == True + + def test_default(self): + """ Show only one plugin""" + resp = self.client.get("/default") + self.assert200(resp) + logging.debug(resp.json) + assert "@id" in resp.json + assert resp.json["@id"] == "Dummy_0.1" + resp = self.client.get("/plugins/Dummy/deactivate") + self.assert200(resp) + sleep(0.5) + resp = self.client.get("/default") + self.assert404(resp) diff --git a/tests/dummy_plugin/dummy.py b/tests/dummy_plugin/dummy.py index 49cba59..60b6e1c 100644 --- a/tests/dummy_plugin/dummy.py +++ b/tests/dummy_plugin/dummy.py @@ -1,6 +1,8 @@ from senpy.plugins import SentimentPlugin from senpy.models import Response + class DummyPlugin(SentimentPlugin): + def analyse(self, *args, **kwargs): - return Response() \ No newline at end of file + return Response() diff --git a/tests/extensions_test/__init__.py b/tests/extensions_test/__init__.py index fdcd2df..de1f407 100644 --- a/tests/extensions_test/__init__.py +++ b/tests/extensions_test/__init__.py @@ -11,6 +11,7 @@ from flask.ext.testing import TestCase class ExtensionsTest(TestCase): + def create_app(self): self.app = Flask("test_extensions") self.dir = os.path.join(os.path.dirname(__file__), "..") @@ -42,19 +43,30 @@ class ExtensionsTest(TestCase): def test_disabling(self): """ Disabling a plugin """ self.senpy.deactivate_all(sync=True) - assert self.senpy.plugins["Dummy"].is_activated == False - assert self.senpy.plugins["Sleep"].is_activated == False + assert not self.senpy.plugins["Dummy"].is_activated + assert not self.senpy.plugins["Sleep"].is_activated def test_default(self): """ Default plugin should be set """ assert self.senpy.default_plugin - assert self.senpy.default_plugin == "Dummy" + assert self.senpy.default_plugin.name == "Dummy" + self.senpy.deactivate_all(sync=True) + logging.debug("Default: {}".format(self.senpy.default_plugin)) + assert self.senpy.default_plugin is None + + def test_noplugin(self): + """ Don't analyse if there isn't any plugin installed """ + self.senpy.deactivate_all(sync=True) + resp = self.senpy.analyse(input="tupni") + logging.debug("Response: {}".format(resp)) + assert resp["status"] == 404 def test_analyse(self): """ Using a plugin """ - # I was using mock until plugin started inheriting Leaf (defaultdict with - # __setattr__ and __getattr__. - r1 = self.senpy.analyse(algorithm="Dummy", input="tupni", output="tuptuo") + # I was using mock until plugin started inheriting + # Leaf (defaultdict with __setattr__ and __getattr__. + r1 = self.senpy.analyse( + algorithm="Dummy", input="tupni", output="tuptuo") r2 = self.senpy.analyse(input="tupni", output="tuptuo") assert r1.analysis[0].id[:5] == "Dummy" assert r2.analysis[0].id[:5] == "Dummy" @@ -62,7 +74,7 @@ class ExtensionsTest(TestCase): self.senpy.deactivate_plugin(plug, sync=True) resp = self.senpy.analyse(input="tupni") logging.debug("Response: {}".format(resp)) - assert resp["status"] == 400 + assert resp["status"] == 404 def test_filtering(self): """ Filtering plugins """ @@ -70,4 +82,5 @@ class ExtensionsTest(TestCase): assert not len(self.senpy.filter_plugins(name="notdummy")) assert self.senpy.filter_plugins(name="Dummy", is_activated=True) self.senpy.deactivate_plugin("Dummy", sync=True) - assert not len(self.senpy.filter_plugins(name="Dummy", is_activated=True)) + assert not len( + self.senpy.filter_plugins(name="Dummy", is_activated=True)) diff --git a/tests/models_test/__init__.py b/tests/models_test/__init__.py index d52c1b5..7f1bb03 100644 --- a/tests/models_test/__init__.py +++ b/tests/models_test/__init__.py @@ -8,29 +8,74 @@ except ImportError: import json import os from unittest import TestCase -from senpy.models import Response +from senpy.models import Response, Entry from senpy.plugins import SenpyPlugin + class ModelsTest(TestCase): + def test_response(self): - r = Response(context=os.path.normpath(os.path.join(__file__, "..", "..", "context.jsonld"))) + r = Response(context=os.path.normpath( + os.path.join(__file__, "..", "..", "context.jsonld"))) assert("@context" in r) + assert(r._frame) + logging.debug("Default frame: %s", r._frame) assert("marl" in r.context) + assert("entries" in r.context) + r2 = Response(context=json.loads('{"test": "roger"}')) assert("test" in r2.context) + r3 = Response(context=None) del r3.context assert("@context" not in r3) assert("entries" in r3) assert("analysis" in r3) + r4 = Response() + assert("@context" in r4) + assert("entries" in r4) + assert("analysis" in r4) + + dummy = SenpyPlugin({"name": "dummy", "version": 0}) + r5 = Response({"dummy": dummy}, context=None, frame=None) + logging.debug("Response 5: %s", r5) + assert("dummy" in r5) + assert(r5["dummy"].name == "dummy") + js = r5.jsonld(context={}, frame={}) + logging.debug("jsonld 5: %s", js) + assert("dummy" in js) + assert(js["dummy"].name == "dummy") + + r6 = Response() + r6.entries.append(Entry(text="Just testing")) + logging.debug("Reponse 6: %s", r6) + assert("@context" in r6) + assert("marl" in r6.context) + assert("entries" in r6.context) + js = r6.jsonld() + logging.debug("jsonld: %s", js) + assert("entries" in js) + assert("entries" in js) + assert("analysis" in js) + resp = r6.flask() + received = json.loads(resp.data) + logging.debug("Response: %s", js) + assert(received["entries"]) + assert(received["entries"][0]["text"] == "Just testing") + assert(received["entries"][0]["text"] != "Not testing") + def test_opinions(self): pass - def test_frame_plugin(self): + def test_plugins(self): p = SenpyPlugin({"name": "dummy", "version": 0}) - c = p.frame() + c = p.jsonld() assert "info" not in c + assert "repo" not in c + assert "params" not in c + logging.debug("Framed: %s", c) + assert "extra_params" in c def test_frame_response(self): pass diff --git a/tests/sleep_plugin/sleep.py b/tests/sleep_plugin/sleep.py index a848e36..50449b8 100644 --- a/tests/sleep_plugin/sleep.py +++ b/tests/sleep_plugin/sleep.py @@ -2,7 +2,9 @@ from senpy.plugins import SenpyPlugin from senpy.models import Response from time import sleep + class SleepPlugin(SenpyPlugin): + def __init__(self, info, *args, **kwargs): super(SleepPlugin, self).__init__(info, *args, **kwargs) self.timeout = int(info["timeout"])