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

PEP8+Better JSON-LD support

* The API has also changed, there are new parameters to send the
context as part of the headers.
* Improved tests
* PEP8 compliance (despite the line about gevent)
This commit is contained in:
J. Fernando Sánchez 2015-02-24 07:15:25 +01:00
parent d58137e8f9
commit d1006bbc92
17 changed files with 416 additions and 207 deletions

2
app.py
View File

@ -19,7 +19,7 @@ This is a helper for development. If you want to run Senpy use:
python -m senpy python -m senpy
""" """
from gevent.monkey import patch_all; patch_all() from gevent.monkey import patch_all patch_all()
import gevent import gevent
import config import config
from flask import Flask from flask import Flask

View File

@ -11,8 +11,7 @@ class Sentiment140Plugin(SentimentPlugin):
p = params.get("prefix", None) p = params.get("prefix", None)
response = Response(prefix=p) 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" polarity = "marl:Neutral"
if polarity_value > 0: if polarity_value > 0:
polarity = "marl:Positive" polarity = "marl:Positive"

View File

@ -6,6 +6,7 @@
"version": "0.1", "version": "0.1",
"extra_params": { "extra_params": {
"language": { "language": {
"@id": "lang_rand",
"aliases": ["language", "l"], "aliases": ["language", "l"],
"required": false, "required": false,
"options": ["es", "en", "auto"] "options": ["es", "en", "auto"]

View File

@ -17,7 +17,8 @@ class Sentiment140Plugin(SentimentPlugin):
p = params.get("prefix", None) p = params.get("prefix", None)
response = Response(prefix=p) 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" polarity = "marl:Neutral"
if polarity_value > 50: if polarity_value > 50:
polarity = "marl:Positive" polarity = "marl:Positive"

View File

@ -6,6 +6,7 @@
"version": "0.1", "version": "0.1",
"extra_params": { "extra_params": {
"language": { "language": {
"@id": "lang_sentiment140",
"aliases": ["language", "l"], "aliases": ["language", "l"],
"required": false, "required": false,
"options": ["es", "en", "auto"] "options": ["es", "en", "auto"]

View File

@ -19,15 +19,18 @@ Senpy is a modular sentiment analysis server. This script runs an instance of
the server. the server.
""" """
from gevent.monkey import patch_all; patch_all(thread=False)
import gevent
from flask import Flask from flask import Flask
from senpy.extensions import Senpy from senpy.extensions import Senpy
from gevent.wsgi import WSGIServer
from gevent.monkey import patch_all
import gevent
import logging import logging
import os import os
from gevent.wsgi import WSGIServer
import argparse import argparse
patch_all(thread=False)
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Run a Senpy server') parser = argparse.ArgumentParser(description='Run a Senpy server')
parser.add_argument('--level', parser.add_argument('--level',
@ -43,20 +46,20 @@ if __name__ == '__main__':
help='Run the application in debug mode') help='Run the application in debug mode')
parser.add_argument('--host', parser.add_argument('--host',
type=str, type=str,
default = "127.0.0.1", default="127.0.0.1",
help='Use 0.0.0.0 to accept requests from any host.') help='Use 0.0.0.0 to accept requests from any host.')
parser.add_argument('--port', parser.add_argument('--port',
'-p', '-p',
type=int, type=int,
default = 5000, default=5000,
help='Port to listen on.') help='Port to listen on.')
parser.add_argument('--plugins-folder', parser.add_argument('--plugins-folder',
'-f', '-f',
type=str, type=str,
default = "plugins", default="plugins",
help='Where to look for plugins.') help='Where to look for plugins.')
args = parser.parse_args() args = parser.parse_args()
logging.basicConfig(level=getattr(logging,args.level)) logging.basicConfig(level=getattr(logging, args.level))
app = Flask(__name__) app = Flask(__name__)
app.debug = args.debug app.debug = args.debug
sp = Senpy(app, args.plugins_folder) sp = Senpy(app, args.plugins_folder)

View File

@ -17,19 +17,35 @@
""" """
Blueprints for Senpy Blueprints for Senpy
""" """
from flask import Blueprint, request, current_app
from .models import Error, Response
import json import json
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from flask import Blueprint, request, jsonify, current_app
nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__) nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__)
BASIC_PARAMS = { BASIC_PARAMS = {
"algorithm": {"aliases": ["algorithm", "a", "algo"], "algorithm": {
"required": False, "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 = {} outdict = {}
wrong_params = {} wrong_params = {}
for param, options in params.iteritems(): for param, options in params.iteritems():
for alias in options["aliases"]: if param[0] != "@": # Exclude json-ld properties
if alias in indict: logger.debug("Param: %s - Options: %s", param, options)
outdict[param] = indict[alias] for alias in options["aliases"]:
if param not in outdict: if alias in indict:
if options.get("required", False) and "default" not in options: outdict[param] = indict[alias]
wrong_params[param] = params[param] 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: else:
if "default" in options: if "options" in params[param] and \
outdict[param] = options["default"] outdict[param] not in params[param]["options"]:
else: 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: if wrong_params:
message = {"status": "failed", message = Error({"status": 404,
"message": "Missing or invalid parameters", "message": "Missing or invalid parameters",
"parameters": outdict, "parameters": outdict,
"errors": {param: error for param, error in wrong_params.iteritems()} "errors": {param: error for param, error in
} wrong_params.iteritems()}
})
raise ValueError(message) raise ValueError(message)
return outdict return outdict
def basic_analysis(params): def basic_analysis(params):
response = {"@context": ["http://demos.gsi.dit.upm.es/eurosentiment/static/context.jsonld", response = {"@context":
{ [("http://demos.gsi.dit.upm.es/"
"@base": "{}#".format(request.url.encode('utf-8')) "eurosentiment/static/context.jsonld"),
} {
], "@base": "{}#".format(request.url.encode('utf-8'))
}
],
"analysis": [{"@type": "marl:SentimentAnalysis"}], "analysis": [{"@type": "marl:SentimentAnalysis"}],
"entries": [] "entries": []
} }
@ -91,19 +113,25 @@ def home():
params = get_params(request) params = get_params(request)
algo = params.get("algorithm", None) algo = params.get("algorithm", None)
specific_params = current_app.senpy.parameters(algo) 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)) params.update(get_params(request, specific_params))
response = current_app.senpy.analyse(**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: except ValueError as ex:
return jsonify(ex.message) return ex.message.flask()
except Exception as ex:
return jsonify(status="400", message=ex.message)
@nif_blueprint.route("/default") @nif_blueprint.route("/default")
def default(): def default():
return current_app.senpy.default_plugin # return current_app.senpy.default_plugin
#return plugins(action="list", plugin=current_app.senpy.default_algorithm) 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']) @nif_blueprint.route('/plugins/', methods=['POST', 'GET'])
@ -118,12 +146,15 @@ def plugins(plugin=None, action="list"):
if plugin and not plugs: if plugin and not plugs:
return "Plugin not found", 400 return "Plugin not found", 400
if action == "list": 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: if plugin:
dic = plugs[plugin].jsonable(with_params) dic = plugs[plugin]
else: else:
dic = {plug: plugs[plug].jsonable(with_params) for plug in plugs} dic = Response(
return jsonify(dic) {plug: plugs[plug].jsonld(with_params) for plug in plugs},
frame={})
return dic.flask(in_headers=in_headers)
method = "{}_plugin".format(action) method = "{}_plugin".format(action)
if(hasattr(sp, method)): if(hasattr(sp, method)):
getattr(sp, method)(plugin) getattr(sp, method)(plugin)

View File

@ -36,5 +36,7 @@
}, },
"text": { "@id": "nif:isString" }, "text": { "@id": "nif:isString" },
"wnaffect": "http://www.gsi.dit.upm.es/ontologies/wnaffect#", "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#"
} }

View File

@ -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 os
import fnmatch import fnmatch
import inspect import inspect
@ -11,15 +18,9 @@ import json
logger = logging.getLogger(__name__) 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): class Senpy(object):
""" Default Senpy extension for Flask """ """ Default Senpy extension for Flask """
def __init__(self, app=None, plugin_folder="plugins"): def __init__(self, app=None, plugin_folder="plugins"):
@ -66,32 +67,45 @@ class Senpy(object):
if "algorithm" in params: if "algorithm" in params:
algo = params["algorithm"] algo = params["algorithm"]
elif self.plugins: 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 algo in self.plugins:
if self.plugins[algo].is_activated: if self.plugins[algo].is_activated:
plug = self.plugins[algo] plug = self.plugins[algo]
resp = plug.analyse(**params) resp = plug.analyse(**params)
resp.analysis.append(plug) resp.analysis.append(plug)
logger.debug("Returning analysis result: {}".format(resp))
return resp return resp
else: else:
logger.debug("Plugin not activated: {}".format(algo)) 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: else:
logger.debug("The algorithm '{}' is not valid\nValid algorithms: {}".format(algo, self.plugins.keys())) logger.debug(("The algorithm '{}' is not valid\n"
return Error(status=400, message="The algorithm '{}' is not valid".format(algo)) "Valid algorithms: {}").format(algo,
self.plugins.keys()))
return Error(status=400,
message="The algorithm '{}' is not valid"
.format(algo))
@property @property
def default_plugin(self): def default_plugin(self):
candidates = self.filter_plugins(is_activated=True) candidates = self.filter_plugins(is_activated=True)
if len(candidates) > 0: if len(candidates) > 0:
candidate = candidates.keys()[0] candidate = candidates.values()[0]
logger.debug("Default: {}".format(candidate)) logger.debug("Default: {}".format(candidate))
return candidate return candidate
else: else:
return None return None
def parameters(self, algo): 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): def activate_all(self, sync=False):
ps = [] ps = []
@ -137,20 +151,22 @@ class Senpy(object):
def _load_plugin(root, filename): def _load_plugin(root, filename):
logger.debug("Loading plugin: {}".format(filename)) logger.debug("Loading plugin: {}".format(filename))
fpath = os.path.join(root, filename) fpath = os.path.join(root, filename)
with open(fpath,'r') as f: with open(fpath, 'r') as f:
info = json.load(f) info = json.load(f)
logger.debug("Info: {}".format(info)) logger.debug("Info: {}".format(info))
sys.path.append(root) sys.path.append(root)
module = info["module"] module = info["module"]
name = info["name"] name = info["name"]
(fp, pathname, desc) = imp.find_module(module, [root,]) (fp, pathname, desc) = imp.find_module(module, [root, ])
try: try:
tmp = imp.load_module(module, fp, pathname, desc) tmp = imp.load_module(module, fp, pathname, desc)
sys.path.remove(root) sys.path.remove(root)
candidate = None candidate = None
for _, obj in inspect.getmembers(tmp): for _, obj in inspect.getmembers(tmp):
if inspect.isclass(obj) and inspect.getmodule(obj) == 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 candidate = obj
break break
if not candidate: if not candidate:

View File

@ -2,7 +2,8 @@ import json
import os import os
from collections import defaultdict from collections import defaultdict
from pyld import jsonld from pyld import jsonld
import logging
from flask import Response as FlaskResponse
class Leaf(dict): class Leaf(dict):
@ -11,18 +12,17 @@ class Leaf(dict):
_context = {} _context = {}
def __init__(self, def __init__(self,
id=None, *args,
context=None, **kwargs):
vocab=None,
prefix=None, id = kwargs.pop("id", None)
frame=None): context = kwargs.pop("context", self._context)
super(Leaf, self).__init__() 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: if context is not None:
self.context = context self.context = context
elif self._context:
self.context = self._context
else:
self.context = {}
if frame is not None: if frame is not None:
self._frame = frame self._frame = frame
self._prefix = prefix self._prefix = prefix
@ -60,10 +60,11 @@ class Leaf(dict):
def get_id(self, id): def get_id(self, id):
""" """
This is not the most elegant solution to change the @id attribute, but it Get id, dealing with prefixes
is the quickest way to have it included in the dictionary without extra
boilerplate.
""" """
# 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: if id and self._prefix and ":" not in id:
return "{}{}".format(self._prefix, id) return "{}{}".format(self._prefix, id)
else: else:
@ -97,7 +98,7 @@ class Leaf(dict):
return context return context
def compact(self): 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): def frame(self, frame=None, options=None):
if frame is None: if frame is None:
@ -106,84 +107,100 @@ class Leaf(dict):
options = {} options = {}
return jsonld.frame(self, frame, 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: if frame is None:
frame = self._frame frame = self._frame
if options is None:
options = {}
if context is None: if context is None:
context = self._context context = self.context
return jsonld.compact(jsonld.frame(self, frame, options), context) else:
#if parameters: context = self.get_context(context)
#resp["parameters"] = self.params # For some reason, this causes errors with pyld
#elif self.extra_params: # if options is None:
#resp["extra_parameters"] = self.extra_params # options = {"expandContext": context.copy() }
#return resp 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, removeContext=None):
def to_JSON(self): return json.dumps(self.jsonld(removeContext=removeContext),
return json.dumps(self,
default=lambda o: o.__dict__, default=lambda o: o.__dict__,
sort_keys=True, indent=4) 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): class Response(Leaf):
_frame = { "@context": { _context = Leaf.get_context("{}/context.jsonld".format(
"analysis": { os.path.dirname(os.path.realpath(__file__))))
"@container": "@set", _frame = {
"@id": "prov:wasInformedBy" "@context": _context,
}, "analysis": {
"date": { "@explicit": True,
"@id": "dc:date", "maxPolarityValue": {},
"@type": "xsd:dateTime" "minPolarityValue": {},
}, "name": {},
"dc": "http://purl.org/dc/terms/", "version": {},
"dc:subject": { },
"@type": "@id" "entries": {}
},
"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": {}
} }
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: if context is None:
context = "{}/context.jsonld".format(os.path.dirname( context = self._context
os.path.realpath(__file__))) self.context = context
super(Response, self).__init__(*args, context=context, **kwargs) super(Response, self).__init__(
self.analysis = [] *args, context=context, frame=frame, **kwargs)
self.entries = [] 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): class Entry(Leaf):
_context = { _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): def __init__(self, text=None, emotion_sets=None, opinions=None, **kwargs):
super(Entry, self).__init__(**kwargs) super(Entry, self).__init__(**kwargs)
if text: if text:
@ -191,10 +208,12 @@ class Entry(Leaf):
self.emotionSets = emotion_sets if emotion_sets else [] self.emotionSets = emotion_sets if emotion_sets else []
self.opinions = opinions if opinions else [] self.opinions = opinions if opinions else []
class Opinion(Leaf): class Opinion(Leaf):
_context = { _context = {
"@vocab": "http://www.gsi.dit.upm.es/ontologies/marl/ns#" "@vocab": "http://www.gsi.dit.upm.es/ontologies/marl/ns#"
} }
def __init__(self, polarityValue=None, hasPolarity=None, *args, **kwargs): def __init__(self, polarityValue=None, hasPolarity=None, *args, **kwargs):
super(Opinion, self).__init__(*args, super(Opinion, self).__init__(*args,
**kwargs) **kwargs)
@ -206,6 +225,7 @@ class Opinion(Leaf):
class EmotionSet(Leaf): class EmotionSet(Leaf):
_context = {} _context = {}
def __init__(self, emotions=None, *args, **kwargs): def __init__(self, emotions=None, *args, **kwargs):
if not emotions: if not emotions:
emotions = [] emotions = []
@ -214,10 +234,16 @@ class EmotionSet(Leaf):
**kwargs) **kwargs)
self.emotions = emotions or [] self.emotions = emotions or []
class Emotion(Leaf): class Emotion(Leaf):
_context = {} _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): def __init__(self, *args, **kwargs):
super(Error, self).__init__(*args) super(Error, self).__init__(*args, **kwargs)
self.update(kwargs)

View File

@ -1,67 +1,89 @@
import logging import logging
import ConfigParser import ConfigParser
from .models import Leaf from .models import Response, Leaf
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
PARAMS = {"input": {"aliases": ["i", "input"], PARAMS = {
"required": True, "input": {
"help": "Input text" "@id": "input",
}, "aliases": ["i", "input"],
"informat": {"aliases": ["f", "informat"], "required": True,
"required": False, "help": "Input text"
"default": "text", },
"options": ["turtle", "text"], "informat": {
}, "@id": "informat",
"intype": {"aliases": ["intype", "t"], "aliases": ["f", "informat"],
"required": False, "required": False,
"default": "direct", "default": "text",
"options": ["direct", "url", "file"], "options": ["turtle", "text"],
}, },
"outformat": {"aliases": ["outformat", "o"], "intype": {
"default": "json-ld", "@id": "intype",
"required": False, "aliases": ["intype", "t"],
"options": ["json-ld"], "required": False,
}, "default": "direct",
"language": {"aliases": ["language", "l"], "options": ["direct", "url", "file"],
"required": False, },
"options": ["es", "en"], "outformat": {
}, "@id": "outformat",
"prefix": {"aliases": ["prefix", "p"], "aliases": ["outformat", "o"],
"required": True, "default": "json-ld",
"default": "", "required": False,
}, "options": ["json-ld"],
"urischeme": {"aliases": ["urischeme", "u"], },
"required": False, "language": {
"default": "RFC5147String", "@id": "language",
"options": "RFC5147String" "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): class SenpyPlugin(Leaf):
_context = {"@vocab": "http://www.gsi.dit.upm.es/ontologies/senpy/ns#", _context = Leaf.get_context(Response._context)
"info": None} _frame = {"@context": _context,
_frame = { "@context": _context,
"name": {}, "name": {},
"@explicit": False, "extra_params": {"@container": "@index"},
"@explicit": True,
"version": {}, "version": {},
"repo": None, "repo": None,
"info": None, "is_activated": {},
"params": None,
} }
def __init__(self, info=None): def __init__(self, info=None):
if not info: 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)) logger.debug("Initialising {}".format(info))
super(SenpyPlugin, self).__init__() super(SenpyPlugin, self).__init__()
self.name = info["name"] self.name = info["name"]
self.version = info["version"] 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()) 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.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.is_activated = False
self.info = info self._info = info
def analyse(self, *args, **kwargs): def analyse(self, *args, **kwargs):
logger.debug("Analysing with: {} {}".format(self.name, self.version)) logger.debug("Analysing with: {} {}".format(self.name, self.version))
@ -73,6 +95,12 @@ class SenpyPlugin(Leaf):
def deactivate(self): def deactivate(self):
pass 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 @property
def id(self): def id(self):
@ -80,12 +108,15 @@ class SenpyPlugin(Leaf):
class SentimentPlugin(SenpyPlugin): class SentimentPlugin(SenpyPlugin):
def __init__(self, info, *args, **kwargs): def __init__(self, info, *args, **kwargs):
super(SentimentPlugin, self).__init__(info, *args, **kwargs) super(SentimentPlugin, self).__init__(info, *args, **kwargs)
self.minPolarityValue = float(info.get("minPolarityValue", 0)) self.minPolarityValue = float(info.get("minPolarityValue", 0))
self.maxPolarityValue = float(info.get("maxPolarityValue", 1)) self.maxPolarityValue = float(info.get("maxPolarityValue", 1))
class EmotionPlugin(SenpyPlugin): class EmotionPlugin(SenpyPlugin):
def __init__(self, info, *args, **kwargs): def __init__(self, info, *args, **kwargs):
resp = super(EmotionPlugin, self).__init__(info, *args, **kwargs) resp = super(EmotionPlugin, self).__init__(info, *args, **kwargs)
self.minEmotionValue = float(info.get("minEmotionValue", 0)) self.minEmotionValue = float(info.get("minEmotionValue", 0))

View File

@ -8,7 +8,7 @@ install_reqs = parse_requirements("requirements.txt")
# e.g. ['django==1.5.1', 'mezzanine==1.4.6'] # e.g. ['django==1.5.1', 'mezzanine==1.4.6']
reqs = [str(ir.req) for ir in install_reqs] reqs = [str(ir.req) for ir in install_reqs]
VERSION = "0.3.2" VERSION = "0.4.0"
print(reqs) print(reqs)
@ -23,9 +23,10 @@ extendable, so new algorithms and sources can be used.
author='J. Fernando Sanchez', author='J. Fernando Sanchez',
author_email='balkian@gmail.com', author_email='balkian@gmail.com',
url='https://github.com/gsi-upm/senpy', # use the URL to the github repo 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), download_url='https://github.com/gsi-upm/senpy/archive/{}.tar.gz'
keywords=['eurosentiment', 'sentiment', 'emotions', 'nif'], # arbitrary keywords .format(VERSION),
keywords=['eurosentiment', 'sentiment', 'emotions', 'nif'],
classifiers=[], classifiers=[],
install_requires=reqs, install_requires=reqs,
include_package_data = True, include_package_data=True,
) )

View File

@ -9,6 +9,7 @@ from senpy.extensions import Senpy
from flask import Flask from flask import Flask
from flask.ext.testing import TestCase from flask.ext.testing import TestCase
from gevent import sleep from gevent import sleep
from itertools import product
def check_dict(indic, template): def check_dict(indic, template):
@ -16,6 +17,7 @@ def check_dict(indic, template):
class BlueprintsTest(TestCase): class BlueprintsTest(TestCase):
def create_app(self): def create_app(self):
self.app = Flask("test_extensions") self.app = Flask("test_extensions")
self.senpy = Senpy() self.senpy = Senpy()
@ -26,24 +28,31 @@ class BlueprintsTest(TestCase):
return self.app return self.app
def test_home(self): 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("/") resp = self.client.get("/")
self.assert200(resp) self.assert404(resp)
logging.debug(resp.json) logging.debug(resp.json)
assert resp.json["status"] == "failed" assert resp.json["status"] == 404
atleast = { atleast = {
"status": "failed", "status": 404,
"message": "Missing or invalid parameters", "message": "Missing or invalid parameters",
} }
assert check_dict(resp.json, atleast) assert check_dict(resp.json, atleast)
def test_analysis(self): 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") resp = self.client.get("/?i=My aloha mohame")
self.assert200(resp) self.assert200(resp)
logging.debug(resp.json) logging.debug("Got response: %s", resp.json)
assert "@context" in 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 assert "entries" in resp.json
def test_list(self): def test_list(self):
@ -52,6 +61,19 @@ class BlueprintsTest(TestCase):
self.assert200(resp) self.assert200(resp)
logging.debug(resp.json) logging.debug(resp.json)
assert "Dummy" in 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): def test_detail(self):
""" Show only one plugin""" """ Show only one plugin"""
@ -77,3 +99,16 @@ class BlueprintsTest(TestCase):
self.assert200(resp) self.assert200(resp)
assert "is_activated" in resp.json assert "is_activated" in resp.json
assert resp.json["is_activated"] == True 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)

View File

@ -1,6 +1,8 @@
from senpy.plugins import SentimentPlugin from senpy.plugins import SentimentPlugin
from senpy.models import Response from senpy.models import Response
class DummyPlugin(SentimentPlugin): class DummyPlugin(SentimentPlugin):
def analyse(self, *args, **kwargs): def analyse(self, *args, **kwargs):
return Response() return Response()

View File

@ -11,6 +11,7 @@ from flask.ext.testing import TestCase
class ExtensionsTest(TestCase): class ExtensionsTest(TestCase):
def create_app(self): def create_app(self):
self.app = Flask("test_extensions") self.app = Flask("test_extensions")
self.dir = os.path.join(os.path.dirname(__file__), "..") self.dir = os.path.join(os.path.dirname(__file__), "..")
@ -42,19 +43,30 @@ class ExtensionsTest(TestCase):
def test_disabling(self): def test_disabling(self):
""" Disabling a plugin """ """ Disabling a plugin """
self.senpy.deactivate_all(sync=True) self.senpy.deactivate_all(sync=True)
assert self.senpy.plugins["Dummy"].is_activated == False assert not self.senpy.plugins["Dummy"].is_activated
assert self.senpy.plugins["Sleep"].is_activated == False assert not self.senpy.plugins["Sleep"].is_activated
def test_default(self): def test_default(self):
""" Default plugin should be set """ """ Default plugin should be set """
assert self.senpy.default_plugin 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): def test_analyse(self):
""" Using a plugin """ """ Using a plugin """
# I was using mock until plugin started inheriting Leaf (defaultdict with # I was using mock until plugin started inheriting
# __setattr__ and __getattr__. # Leaf (defaultdict with __setattr__ and __getattr__.
r1 = self.senpy.analyse(algorithm="Dummy", input="tupni", output="tuptuo") r1 = self.senpy.analyse(
algorithm="Dummy", input="tupni", output="tuptuo")
r2 = self.senpy.analyse(input="tupni", output="tuptuo") r2 = self.senpy.analyse(input="tupni", output="tuptuo")
assert r1.analysis[0].id[:5] == "Dummy" assert r1.analysis[0].id[:5] == "Dummy"
assert r2.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) self.senpy.deactivate_plugin(plug, sync=True)
resp = self.senpy.analyse(input="tupni") resp = self.senpy.analyse(input="tupni")
logging.debug("Response: {}".format(resp)) logging.debug("Response: {}".format(resp))
assert resp["status"] == 400 assert resp["status"] == 404
def test_filtering(self): def test_filtering(self):
""" Filtering plugins """ """ Filtering plugins """
@ -70,4 +82,5 @@ class ExtensionsTest(TestCase):
assert not len(self.senpy.filter_plugins(name="notdummy")) assert not len(self.senpy.filter_plugins(name="notdummy"))
assert self.senpy.filter_plugins(name="Dummy", is_activated=True) assert self.senpy.filter_plugins(name="Dummy", is_activated=True)
self.senpy.deactivate_plugin("Dummy", sync=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))

View File

@ -8,29 +8,74 @@ except ImportError:
import json import json
import os import os
from unittest import TestCase from unittest import TestCase
from senpy.models import Response from senpy.models import Response, Entry
from senpy.plugins import SenpyPlugin from senpy.plugins import SenpyPlugin
class ModelsTest(TestCase): class ModelsTest(TestCase):
def test_response(self): 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("@context" in r)
assert(r._frame)
logging.debug("Default frame: %s", r._frame)
assert("marl" in r.context) assert("marl" in r.context)
assert("entries" in r.context)
r2 = Response(context=json.loads('{"test": "roger"}')) r2 = Response(context=json.loads('{"test": "roger"}'))
assert("test" in r2.context) assert("test" in r2.context)
r3 = Response(context=None) r3 = Response(context=None)
del r3.context del r3.context
assert("@context" not in r3) assert("@context" not in r3)
assert("entries" in r3) assert("entries" in r3)
assert("analysis" 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): def test_opinions(self):
pass pass
def test_frame_plugin(self): def test_plugins(self):
p = SenpyPlugin({"name": "dummy", "version": 0}) p = SenpyPlugin({"name": "dummy", "version": 0})
c = p.frame() c = p.jsonld()
assert "info" not in c 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): def test_frame_response(self):
pass pass

View File

@ -2,7 +2,9 @@ from senpy.plugins import SenpyPlugin
from senpy.models import Response from senpy.models import Response
from time import sleep from time import sleep
class SleepPlugin(SenpyPlugin): class SleepPlugin(SenpyPlugin):
def __init__(self, info, *args, **kwargs): def __init__(self, info, *args, **kwargs):
super(SleepPlugin, self).__init__(info, *args, **kwargs) super(SleepPlugin, self).__init__(info, *args, **kwargs)
self.timeout = int(info["timeout"]) self.timeout = int(info["timeout"])