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

Python 3 compatible

There are also some slight changes to the JSON schemas and the use of
JSON-LD.
This commit is contained in:
J. Fernando Sánchez 2016-02-19 19:24:09 +01:00
parent a79df7a3da
commit 14c9f61864
32 changed files with 621 additions and 349 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
*egg-info *egg-info
dist dist
README.html README.html
__pycache__

View File

@ -2,6 +2,10 @@ Flask>=0.10.1
gunicorn>=19.0.0 gunicorn>=19.0.0
requests>=2.4.1 requests>=2.4.1
GitPython>=0.3.2.RC1 GitPython>=0.3.2.RC1
gevent>=1.0.1 gevent>=1.1rc4
PyLD>=0.6.5 PyLD>=0.6.5
Flask-Testing>=0.4.2 Flask-Testing>=0.4.2
six
future
jsonschema
jsonref

View File

@ -17,8 +17,8 @@
""" """
Blueprints for Senpy Blueprints for Senpy
""" """
from flask import Blueprint, request, current_app, Flask, redirect, url_for, render_template from flask import Blueprint, request, current_app, render_template
from .models import Error, Response, Leaf from .models import Error, Response
from future.utils import iteritems from future.utils import iteritems
import json import json
@ -27,6 +27,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__) nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__)
demo_blueprint = Blueprint("Demo of the service. It includes an HTML+Javascript playground to test senpy", __name__)
BASIC_PARAMS = { BASIC_PARAMS = {
"algorithm": { "algorithm": {
@ -40,15 +41,6 @@ BASIC_PARAMS = {
} }
} }
LIST_PARAMS = {
"params": {
"aliases": ["params", "with_params"],
"required": False,
"default": "0"
},
}
def get_params(req, params=BASIC_PARAMS): def get_params(req, params=BASIC_PARAMS):
if req.method == 'POST': if req.method == 'POST':
indict = req.form indict = req.form
@ -76,12 +68,11 @@ def get_params(req, params=BASIC_PARAMS):
outdict[param] not in params[param]["options"]: outdict[param] not in params[param]["options"]:
wrong_params[param] = params[param] wrong_params[param] = params[param]
if wrong_params: if wrong_params:
message = Error({"status": 404, 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 errors={param: error for param, error in
iteritems(wrong_params)} iteritems(wrong_params)})
})
raise Error(message=message) raise Error(message=message)
return outdict return outdict
@ -107,12 +98,12 @@ def basic_analysis(params):
return response return response
@nif_blueprint.route('/') @demo_blueprint.route('/')
def index(): def index():
return render_template("index.html") return render_template("index.html")
@nif_blueprint.route('/api', methods=['POST', 'GET']) @nif_blueprint.route('/', methods=['POST', 'GET'])
def api(): def api():
try: try:
params = get_params(request) params = get_params(request)
@ -128,7 +119,7 @@ def api():
return ex.message.flask() return ex.message.flask()
@nif_blueprint.route("/api/default") @nif_blueprint.route("/default")
def default(): def default():
# return current_app.senpy.default_plugin # return current_app.senpy.default_plugin
plug = current_app.senpy.default_plugin plug = current_app.senpy.default_plugin
@ -139,9 +130,9 @@ def default():
return error.flask() return error.flask()
@nif_blueprint.route('/api/plugins/', methods=['POST', 'GET']) @nif_blueprint.route('/plugins/', methods=['POST', 'GET'])
@nif_blueprint.route('/api/plugins/<plugin>', methods=['POST', 'GET']) @nif_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET'])
@nif_blueprint.route('/api/plugins/<plugin>/<action>', methods=['POST', 'GET']) @nif_blueprint.route('/plugins/<plugin>/<action>', methods=['POST', 'GET'])
def plugins(plugin=None, action="list"): def plugins(plugin=None, action="list"):
filt = {} filt = {}
sp = current_app.senpy sp = current_app.senpy
@ -151,21 +142,19 @@ 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 = get_params(request, LIST_PARAMS)["params"] == "1"
in_headers = get_params(request, BASIC_PARAMS)["inHeaders"] != "0" in_headers = get_params(request, BASIC_PARAMS)["inHeaders"] != "0"
if plugin: if plugin:
dic = plugs[plugin] dic = plugs[plugin]
else: else:
dic = Response( dic = Response(
{plug: plugs[plug].jsonld(with_params) for plug in plugs}, {plug: plugs[plug].serializable() for plug in plugs})
frame={})
return dic.flask(in_headers=in_headers) 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)
return Leaf(message="Ok").flask() return Response(message="Ok").flask()
else: else:
return Error("action '{}' not allowed".format(action)).flask() return Error(message="action '{}' not allowed".format(action)).flask()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,42 +0,0 @@
{
"dc": "http://purl.org/dc/terms/",
"dc:subject": {
"@type": "@id"
},
"xsd": "http://www.w3.org/2001/XMLSchema#",
"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#",
"emotions": {
"@container": "@set",
"@id": "onyx:hasEmotionSet"
},
"opinions": {
"@container": "@set",
"@id": "marl:hasOpinion"
},
"prov": "http://www.w3.org/ns/prov#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"analysis": {
"@container": "@set",
"@id": "prov:wasInformedBy"
},
"entries": {
"@container": "@set",
"@id": "prov:generated"
},
"strings": {
"@container": "@set",
"@reverse": "nif:hasContext"
},
"date":
{
"@id": "dc:date",
"@type": "xsd:dateTime"
},
"text": { "@id": "nif:isString" },
"wnaffect": "http://www.gsi.dit.upm.es/ontologies/wnaffect#",
"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

@ -8,7 +8,7 @@ monkey.patch_all()
from .plugins import SenpyPlugin, SentimentPlugin, EmotionPlugin from .plugins import SenpyPlugin, SentimentPlugin, EmotionPlugin
from .models import Error from .models import Error
from .blueprints import nif_blueprint from .blueprints import nif_blueprint, demo_blueprint
from git import Repo, InvalidGitRepositoryError from git import Repo, InvalidGitRepositoryError
from functools import partial from functools import partial
@ -57,7 +57,8 @@ class Senpy(object):
app.teardown_appcontext(self.teardown) app.teardown_appcontext(self.teardown)
else: else:
app.teardown_request(self.teardown) app.teardown_request(self.teardown)
app.register_blueprint(nif_blueprint) app.register_blueprint(nif_blueprint, url_prefix="/api")
app.register_blueprint(demo_blueprint, url_prefix="/")
def add_folder(self, folder): def add_folder(self, folder):
logger.debug("Adding folder: %s", folder) logger.debug("Adding folder: %s", folder)

View File

@ -1,71 +1,71 @@
'''
Senpy Models.
This implementation should mirror the JSON schema definition.
For compatibility with Py3 and for easier debugging, this new version drops introspection
and adds all arguments to the models.
'''
from __future__ import print_function from __future__ import print_function
from six import string_types from six import string_types
import time
import copy
import json import json
import os import os
import logging import logging
import jsonref
import jsonschema
from collections import defaultdict
from pyld import jsonld
from flask import Response as FlaskResponse from flask import Response as FlaskResponse
class Response(object): DEFINITIONS_FILE = 'definitions.json'
CONTEXT_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld')
@property def get_schema_path(schema_file, absolute=False):
def context(self): if absolute:
if not hasattr(self, '_context'): return os.path.realpath(schema_file)
self._context = None else:
return self._context return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'schemas', schema_file)
def read_schema(schema_file, absolute=False):
schema_path = get_schema_path(schema_file, absolute)
schema_uri = 'file://{}'.format(schema_path)
return jsonref.load(open(schema_path), base_uri=schema_uri)
base_schema = read_schema(DEFINITIONS_FILE)
logging.debug(base_schema)
class Context(dict):
@staticmethod @staticmethod
def get_context(context): def load(context):
if isinstance(context, list): logging.debug('Loading context: {}'.format(context))
if not context:
return context
elif isinstance(context, list):
contexts = [] contexts = []
for c in context: for c in context:
contexts.append(Response.get_context(c)) contexts.append(Context.load(c))
return contexts return contexts
elif isinstance(context, dict): elif isinstance(context, dict):
return context return Context(context)
elif isinstance(context, string_types): elif isinstance(context, string_types):
try: try:
with open(context) as f: with open(context) as f:
return json.loads(f.read()) return Context(json.loads(f.read()))
except IOError: except IOError:
return context return context
else: else:
raise AttributeError('Please, provide a valid context') raise AttributeError('Please, provide a valid context')
def jsonld(self, frame=None, options=None, base_context = Context.load(CONTEXT_PATH)
context=None, removeContext=None):
if removeContext is None:
removeContext = Response._context # Loop?
if frame is None:
frame = self._frame
if context is None:
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, removeContext=None):
return json.dumps(self.jsonld(removeContext=removeContext), class SenpyMixin(object):
default=lambda o: o.__dict__, context = base_context
sort_keys=True, indent=4)
def flask(self, def flask(self,
in_headers=False, in_headers=False,
@ -73,46 +73,166 @@ class Response(object):
""" """
Return the values and error to be used in flask Return the values and error to be used in flask
""" """
js = self.jsonld()
headers = None headers = None
if in_headers: if in_headers:
ctx = js["@context"]
headers = { headers = {
"Link": ('<%s>;' "Link": ('<%s>;'
'rel="http://www.w3.org/ns/json-ld#context";' 'rel="http://www.w3.org/ns/json-ld#context";'
' type="application/ld+json"' % url) ' type="application/ld+json"' % url)
} }
del js["@context"] return FlaskResponse(self.to_JSON(with_context=not in_headers),
return FlaskResponse(json.dumps(js, indent=4), status=getattr(self, "status", 200),
status=self.get("status", 200),
headers=headers, headers=headers,
mimetype="application/json") mimetype="application/json")
class Entry(JSONLD):
pass def serializable(self):
def ser_or_down(item):
if hasattr(item, 'serializable'):
return item.serializable()
elif isinstance(item, dict):
temp = dict()
for kp in item:
vp = item[kp]
temp[kp] = ser_or_down(vp)
return temp
elif isinstance(item, list):
return list(ser_or_down(i) for i in item)
else:
return item
return ser_or_down(self._plain_dict())
class Sentiment(JSONLD): def jsonld(self, context=None, with_context=False):
pass ser = self.serializable()
if with_context:
ser["@context"] = self.context
return ser
class EmotionSet(JSONLD): def to_JSON(self, *args, **kwargs):
pass js = json.dumps(self.jsonld(*args, **kwargs), indent=4,
sort_keys=True)
return js
class Emotion(JSONLD): class SenpyModel(SenpyMixin, dict):
pass
schema = base_schema
class Suggestion(JSONLD): prefix = None
pass
class Error(BaseException, JSONLD):
# A better pattern would be this:
# htp://flask.pocoo.org/docs/0.10/patterns/apierrors/
_frame = {}
_context = {}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.message = kwargs.get('message', None) temp = dict(*args, **kwargs)
super(Error, self).__init__(*args)
reqs = self.schema.get('required', [])
for i in reqs:
if i not in temp:
prop = self.schema['properties'][i]
if 'default' in prop:
temp[i] = copy.deepcopy(prop['default'])
if 'context' in temp:
context = temp['context']
del temp['context']
self.__dict__['context'] = Context.load(context)
super(SenpyModel, self).__init__(temp)
def _get_key(self, key):
key = key.replace("__", ":", 1)
return key
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
def __getattr__(self, key):
try:
return self.__getitem__(self._get_key(key))
except KeyError:
raise AttributeError(key)
def __setattr__(self, key, value):
self.__setitem__(self._get_key(key), value)
def __delattr__(self, key):
self.__delitem__(self._get_key(key))
def validate(self, obj=None):
if not obj:
obj = self
if hasattr(obj, "jsonld"):
obj = obj.jsonld()
jsonschema.validate(obj, self.schema)
@classmethod
def from_base(cls, name):
subschema = base_schema[name]
return warlock.model_factory(subschema, base_class=cls)
def _plain_dict(self):
d = { k: v for (k,v) in self.items() if k[0] != "_"}
if hasattr(self, "id"):
d["@id"] = self.id
return d
@property
def id(self):
if not hasattr(self, '_id'):
self.__dict__["_id"] = '_:{}_{}'.format(type(self).__name__, time.time())
return self._id
@id.setter
def id(self, value):
self._id = value
class Response(SenpyModel):
schema = read_schema('response.json')
class Results(SenpyModel):
schema = read_schema('results.json')
def jsonld(self, context=None, with_context=True):
return super(Results, self).jsonld(context, with_context)
class Entry(SenpyModel):
schema = read_schema('entry.json')
class Sentiment(SenpyModel):
schema = read_schema('sentiment.json')
class Analysis(SenpyModel):
schema = read_schema('analysis.json')
class EmotionSet(SenpyModel):
schema = read_schema('emotionSet.json')
class Suggestion(SenpyModel):
schema = read_schema('suggestion.json')
class PluginModel(SenpyModel):
schema = read_schema('plugin.json')
class Plugins(SenpyModel):
schema = read_schema('plugins.json')
class Error(SenpyMixin, BaseException ):
def __init__(self, message, status=500, params=None, errors=None, *args, **kwargs):
self.message = message
self.status = status
self.params = params or {}
self.errors = errors or ""
def _plain_dict(self):
return self.__dict__
def __str__(self):
return str(self.jsonld())

View File

@ -5,7 +5,7 @@ import inspect
import os.path import os.path
import shelve import shelve
import logging import logging
from .models import Response, Leaf from .models import Response, PluginModel, Error
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -58,36 +58,21 @@ PARAMS = {
} }
class SenpyPlugin(Leaf): class SenpyPlugin(PluginModel):
_context = Leaf.get_context(Response._context)
_frame = {"@context": _context,
"name": {},
"extra_params": {"@container": "@index"},
"@explicit": True,
"version": {},
"repo": None,
"is_activated": {},
"params": None,
}
def __init__(self, info=None): def __init__(self, info=None):
if not info: if not info:
raise Error(message=("You need to provide configuration" raise Error(message=("You need to provide configuration"
"information for the plugin.")) "information for the plugin."))
logger.debug("Initialising {}".format(info)) logger.debug("Initialising {}".format(info))
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.params = info.get("params", PARAMS.copy()) self.params = info.get("params", PARAMS.copy())
if "@id" not in self.params: if "@id" not in self.params:
self.params["@id"] = "params_%s" % self.id self.params["@id"] = "params_%s" % self.id
self.extra_params = info.get("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
super(SenpyPlugin, self).__init__()
def get_folder(self): def get_folder(self):
return os.path.dirname(inspect.getfile(self.__class__)) return os.path.dirname(inspect.getfile(self.__class__))
@ -102,13 +87,6 @@ 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):
return "{}_{}".format(self.name, self.version) return "{}_{}".format(self.name, self.version)
@ -123,6 +101,7 @@ class SentimentPlugin(SenpyPlugin):
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))
self["@type"] = "marl:SentimentAnalysis"
class EmotionPlugin(SenpyPlugin): class EmotionPlugin(SenpyPlugin):
@ -131,6 +110,7 @@ class EmotionPlugin(SenpyPlugin):
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))
self.maxEmotionValue = float(info.get("maxEmotionValue", 0)) self.maxEmotionValue = float(info.get("maxEmotionValue", 0))
self["@type"] = "onyx:EmotionAnalysis"
class ShelfMixin(object): class ShelfMixin(object):
@ -145,6 +125,11 @@ class ShelfMixin(object):
def sh(self): def sh(self):
if os.path.isfile(self.shelf_file): if os.path.isfile(self.shelf_file):
os.remove(self.shelf_file) os.remove(self.shelf_file)
self.close()
def __del__(self):
self.close()
self.deactivate()
@property @property
def shelf_file(self): def shelf_file(self):

View File

@ -2,7 +2,7 @@ import json
import random import random
from senpy.plugins import SentimentPlugin from senpy.plugins import SentimentPlugin
from senpy.models import Response, Opinion, Entry from senpy.models import Results, Sentiment, Entry
class Sentiment140Plugin(SentimentPlugin): class Sentiment140Plugin(SentimentPlugin):
@ -10,22 +10,33 @@ class Sentiment140Plugin(SentimentPlugin):
lang = params.get("language", "auto") lang = params.get("language", "auto")
p = params.get("prefix", None) p = params.get("prefix", None)
response = Response(prefix=p) response = Results(prefix=p)
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"
elif polarity_value < 0: elif polarity_value < 0:
polarity = "marl:Negative" polarity = "marl:Negative"
entry = Entry(id="Entry0", entry = Entry({"id":":Entry0",
text=params["input"], "nif:isString": params["input"]})
prefix=p) sentiment = Sentiment({"id": ":Sentiment0",
opinion = Opinion(id="Opinion0", "marl:hasPolarity": polarity,
prefix=p, "marl:polarityValue": polarity_value})
hasPolarity=polarity, sentiment["prov:wasGeneratedBy"] = self.id
polarityValue=polarity_value) entry.sentiments = []
opinion["prov:wasGeneratedBy"] = self.id entry.sentiments.append(sentiment)
entry.opinions.append(opinion)
entry.language = lang entry.language = lang
response.entries.append(entry) response.entries.append(entry)
return response return response

View File

@ -2,7 +2,7 @@ import requests
import json import json
from senpy.plugins import SentimentPlugin from senpy.plugins import SentimentPlugin
from senpy.models import Response, Opinion, Entry from senpy.models import Results, Sentiment, Entry
class Sentiment140Plugin(SentimentPlugin): class Sentiment140Plugin(SentimentPlugin):
@ -16,7 +16,7 @@ class Sentiment140Plugin(SentimentPlugin):
) )
p = params.get("prefix", None) p = params.get("prefix", None)
response = Response(prefix=p) response = Results(prefix=p)
polarity_value = self.maxPolarityValue*int(res.json()["data"][0] polarity_value = self.maxPolarityValue*int(res.json()["data"][0]
["polarity"]) * 0.25 ["polarity"]) * 0.25
polarity = "marl:Neutral" polarity = "marl:Neutral"
@ -25,15 +25,16 @@ class Sentiment140Plugin(SentimentPlugin):
polarity = "marl:Positive" polarity = "marl:Positive"
elif polarity_value < neutral_value: elif polarity_value < neutral_value:
polarity = "marl:Negative" polarity = "marl:Negative"
entry = Entry(id="Entry0", entry = Entry(id="Entry0",
text=params["input"], nif__isString=params["input"])
prefix=p) sentiment = Sentiment(id="Sentiment0",
opinion = Opinion(id="Opinion0", prefix=p,
prefix=p, marl__hasPolarity=polarity,
hasPolarity=polarity, marl__polarityValue=polarity_value)
polarityValue=polarity_value) sentiment.prov__wasGeneratedBy = self.id
opinion["prov:wasGeneratedBy"] = self.id entry.sentiments = []
entry.opinions.append(opinion) entry.sentiments.append(sentiment)
entry.language = lang entry.language = lang
response.entries.append(entry) response.entries.append(entry)
return response return response

View File

@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Analysis"
}

View File

@ -0,0 +1,33 @@
{
"@vocab": "http://www.gsi.dit.upm.es/ontologies/senpy#",
"dc": "http://dublincore.org/2012/06/14/dcelements#",
"me": "http://www.mixedemotions-project.eu/ns/model#",
"prov": "http://www.w3.org/ns/prov#",
"nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#",
"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#",
"onyx": "http://www.gsi.dit.upm.es/ontologies/onyx#",
"wnaffect": "http://www.gsi.dit.upm.es/ontologies/wnaffect#",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"topics": {
"@id": "dc:subject"
},
"entities": {
"@id": "me:hasEntities"
},
"suggestions": {
"@id": "me:hasSuggestions"
},
"emotions": {
"@id": "onyx:hasEmotionSet"
},
"sentiments": {
"@id": "marl:hasOpinion"
},
"entries": {
"@id": "prov:used"
},
"analysis": {
"@id": "prov:wasGeneratedBy"
}
}

View File

@ -0,0 +1,161 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"Results": {
"title": "Results",
"description": "The results of an analysis",
"type": "object",
"properties": {
"@context": {
"$ref": "#/Context"
},
"@id": {
"description": "ID of the analysis",
"type": "string"
},
"analysis": {
"type": "array",
"default": [],
"items": {
"$ref": "#/Analysis"
}
},
"entries": {
"type": "array",
"default": [],
"items": {
"$ref": "#/Entry"
}
}
},
"required": ["@id", "analysis", "entries"]
},
"Context": {
"description": "JSON-LD Context",
"type": ["array", "string", "object"]
},
"Analysis": {
"description": "Senpy analysis",
"type": "object",
"properties": {
"@id": {
"type": "string"
},
"@type": {
"type": "string",
"description": "Type of the analysis. e.g. marl:SentimentAnalysis"
}
},
"required": ["@id", "@type"]
},
"Entry": {
"properties": {
"@id": {
"type": "string"
},
"@type": {
"enum": [["nif:RFC5147String", "nif:Context"]]
},
"nif:isString": {
"description": "String contained in this Context",
"type": "string"
},
"sentiments": {
"type": "array",
"items": {"$ref": "#/Sentiment" }
},
"emotions": {
"type": "array",
"items": {"$ref": "#/EmotionSet" }
},
"entities": {
"type": "array",
"items": {"$ref": "#/Entity" }
},
"topics": {
"type": "array",
"items": {"$ref": "#/Topic" }
},
"suggestions": {
"type": "array",
"items": {"$ref": "#/Suggestion" }
}
},
"required": ["@id", "nif:isString"]
},
"Sentiment": {
"properties": {
"@id": {"type": "string"},
"nif:beginIndex": {"type": "integer"},
"nif:endIndex": {"type": "integer"},
"nif:anchorOf": {
"description": "Piece of context that contains the Sentiment",
"type": "string"
},
"marl:hasPolarity": {
"enum": ["marl:Positive", "marl:Negative", "marl:Neutral"]
},
"marl:polarityValue": {
"type": "number"
},
"prov:wasGeneratedBy": {
"type": "string",
"description": "The ID of the analysis that generated this Sentiment. The full object should be included in the \"analysis\" property of the root object"
}
},
"required": ["@id", "prov:wasGeneratedBy"]
},
"EmotionSet": {
"properties": {
"@id": {"type": "string"},
"nif:beginIndex": {"type": "integer"},
"nif:endIndex": {"type": "integer"},
"nif:anchorOf": {
"description": "Piece of context that contains the Sentiment",
"type": "string"
},
"onyx:hasEmotion": {
"$ref": "#/Emotion"
},
"prov:wasGeneratedBy": {
"type": "string",
"description": "The ID of the analysis that generated this Emotion. The full object should be included in the \"analysis\" property of the root object"
}
},
"required": ["@id", "prov:wasGeneratedBy", "onyx:hasEmotion"]
},
"Emotion": {
"type": "object"
},
"Entity": {
"type": "object"
},
"Topic": {
"type": "object"
},
"Suggestion": {
"type": "object"
},
"Plugins": {
"properties": {
"plugins": {
"type": "array",
"items": {
"$ref": "#/Plugin"
}
}
}
},
"Plugin": {
"type": "object",
"required": ["@id"],
"properties": {
"@id": {
"type": "string"
}
}
},
"Response": {
"type": "object"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Emotion"
}

View File

@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/EmotionSet"
}

4
senpy/schemas/entry.json Normal file
View File

@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Entry"
}

View File

@ -0,0 +1,3 @@
{
"$ref": "definitions.json#/Plugin"
}

View File

@ -0,0 +1,3 @@
{
"$ref": "definitions.json#/Plugins"
}

View File

@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Response"
}

View File

@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Results"
}

View File

@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Sentiment"
}

View File

@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Suggestion"
}

View File

@ -1,2 +1,4 @@
[metadata] [metadata]
description-file = README.rst description-file = README.rst
[aliases]
test=pytest

View File

@ -15,7 +15,7 @@ except AttributeError:
install_reqs = [str(ir.req) for ir in install_reqs] install_reqs = [str(ir.req) for ir in install_reqs]
test_reqs = [str(ir.req) for ir in test_reqs] test_reqs = [str(ir.req) for ir in test_reqs]
VERSION = "0.4.11" VERSION = "0.5"
setup( setup(
name='senpy', name='senpy',
@ -34,7 +34,7 @@ extendable, so new algorithms and sources can be used.
classifiers=[], classifiers=[],
install_requires=install_reqs, install_requires=install_reqs,
tests_require=test_reqs, tests_require=test_reqs,
test_suite="nose.collector", setup_requires=['pytest-runner',],
include_package_data=True, include_package_data=True,
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [

View File

@ -1,3 +1,2 @@
nose pytest
mock mock
pbr

View File

@ -1,40 +0,0 @@
{
"dc": "http://purl.org/dc/terms/",
"dc:subject": {
"@type": "@id"
},
"xsd": "http://www.w3.org/2001/XMLSchema#",
"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#",
"emotions": {
"@container": "@set",
"@id": "onyx:hasEmotionSet"
},
"opinions": {
"@container": "@set",
"@id": "marl:hasOpinion"
},
"prov": "http://www.w3.org/ns/prov#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"analysis": {
"@container": "@set",
"@id": "prov:wasInformedBy"
},
"entries": {
"@container": "@set",
"@id": "prov:generated"
},
"strings": {
"@container": "@set",
"@reverse": "nif:hasContext"
},
"date":
{
"@id": "dc:date",
"@type": "xsd:dateTime"
},
"text": { "@id": "nif:isString" },
"wnaffect": "http://www.gsi.dit.upm.es/ontologies/wnaffect#",
"xsd": "http://www.w3.org/2001/XMLSchema#"
}

View File

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

View File

@ -1,81 +0,0 @@
import os
import logging
try:
import unittest.mock as mock
except ImportError:
import mock
import json
import os
from unittest import TestCase
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")))
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.decode())
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_plugins(self):
p = SenpyPlugin({"name": "dummy", "version": 0})
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

View File

@ -1,5 +1,5 @@
from senpy.plugins import SenpyPlugin from senpy.plugins import SenpyPlugin
from senpy.models import Response from senpy.models import Results
from time import sleep from time import sleep
@ -14,4 +14,4 @@ class SleepPlugin(SenpyPlugin):
def analyse(self, *args, **kwargs): def analyse(self, *args, **kwargs):
sleep(float(kwargs.get("timeout", self.timeout))) sleep(float(kwargs.get("timeout", self.timeout)))
return Response() return Results()

View File

@ -1,10 +1,6 @@
import os import os
import logging import logging
try:
import unittest.mock as mock
except ImportError:
import mock
from senpy.extensions import Senpy 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
@ -31,7 +27,7 @@ class BlueprintsTest(TestCase):
""" """
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("/api") resp = self.client.get("/api/")
self.assert404(resp) self.assert404(resp)
logging.debug(resp.json) logging.debug(resp.json)
assert resp.json["status"] == 404 assert resp.json["status"] == 404
@ -46,7 +42,7 @@ class BlueprintsTest(TestCase):
The dummy plugin returns an empty response,\ The dummy plugin returns an empty response,\
it should contain the context it should contain the context
""" """
resp = self.client.get("/api?i=My aloha mohame") resp = self.client.get("/api/?i=My aloha mohame")
self.assert200(resp) self.assert200(resp)
logging.debug("Got response: %s", resp.json) logging.debug("Got response: %s", resp.json)
assert "@context" in resp.json assert "@context" in resp.json
@ -64,7 +60,7 @@ class BlueprintsTest(TestCase):
assert "@context" in resp.json assert "@context" in resp.json
def test_headers(self): def test_headers(self):
for i, j in product(["/api/plugins/?nothing=", "/api?i=test&"], for i, j in product(["/api/plugins/?nothing=", "/api/?i=test&"],
["headers", "inHeaders"]): ["headers", "inHeaders"]):
resp = self.client.get("%s" % (i)) resp = self.client.get("%s" % (i))
assert "@context" in resp.json assert "@context" in resp.json
@ -77,7 +73,7 @@ class BlueprintsTest(TestCase):
def test_detail(self): def test_detail(self):
""" Show only one plugin""" """ Show only one plugin"""
resp = self.client.get("/api/plugins/Dummy") resp = self.client.get("/api/plugins/Dummy/")
self.assert200(resp) self.assert200(resp)
logging.debug(resp.json) logging.debug(resp.json)
assert "@id" in resp.json assert "@id" in resp.json
@ -88,14 +84,14 @@ class BlueprintsTest(TestCase):
resp = self.client.get("/api/plugins/Dummy/deactivate") resp = self.client.get("/api/plugins/Dummy/deactivate")
self.assert200(resp) self.assert200(resp)
sleep(0.5) sleep(0.5)
resp = self.client.get("/api/plugins/Dummy") resp = self.client.get("/api/plugins/Dummy/")
self.assert200(resp) self.assert200(resp)
assert "is_activated" in resp.json assert "is_activated" in resp.json
assert resp.json["is_activated"] == False assert resp.json["is_activated"] == False
resp = self.client.get("/api/plugins/Dummy/activate") resp = self.client.get("/api/plugins/Dummy/activate")
self.assert200(resp) self.assert200(resp)
sleep(0.5) sleep(0.5)
resp = self.client.get("/api/plugins/Dummy") resp = self.client.get("/api/plugins/Dummy/")
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

View File

@ -2,10 +2,6 @@ from __future__ import print_function
import os import os
import logging import logging
try:
import unittest.mock as mock
except ImportError:
import mock
from senpy.extensions import Senpy 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
@ -15,7 +11,7 @@ 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__))
self.senpy = Senpy(plugin_folder=self.dir, default_plugins=False) self.senpy = Senpy(plugin_folder=self.dir, default_plugins=False)
self.senpy.init_app(self.app) self.senpy.init_app(self.app)
self.senpy.activate_plugin("Dummy", sync=True) self.senpy.activate_plugin("Dummy", sync=True)
@ -60,7 +56,7 @@ class ExtensionsTest(TestCase):
self.senpy.deactivate_all(sync=True) self.senpy.deactivate_all(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"] == 404 assert resp.status == 404
def test_analyse(self): def test_analyse(self):
""" Using a plugin """ """ Using a plugin """
@ -75,7 +71,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"] == 404 assert resp.status == 404
def test_filtering(self): def test_filtering(self):

97
tests/test_models.py Normal file
View File

@ -0,0 +1,97 @@
import os
import logging
import jsonschema
import json
import os
from unittest import TestCase
from senpy.models import Response, Entry, Results, Sentiment, EmotionSet, Error
from senpy.plugins import SenpyPlugin
from pprint import pprint
class ModelsTest(TestCase):
def test_jsonld(self):
ctx = os.path.normpath(os.path.join(__file__, "..", "..", "..", "senpy", "schemas", "context.jsonld"))
prueba = {"@id": "test",
"analysis": [],
"entries": []}
r = Results(**prueba)
print("Response's context: ")
pprint(r.context)
j = r.jsonld(with_context=True)
print("As JSON:")
pprint(j)
assert("@context" in j)
assert("marl" in j["@context"])
assert("entries" in j["@context"])
r6 = Results(**prueba)
r6.entries.append(Entry({"@id":"ohno", "nif:isString":"Just testing"}))
logging.debug("Reponse 6: %s", r6)
assert("marl" in r6.context)
assert("entries" in r6.context)
j6 = r6.jsonld(with_context=True)
logging.debug("jsonld: %s", j6)
assert("@context" in j6)
assert("entries" in j6)
assert("analysis" in j6)
resp = r6.flask()
received = json.loads(resp.data.decode())
logging.debug("Response: %s", j6)
assert(received["entries"])
assert(received["entries"][0]["nif:isString"] == "Just testing")
assert(received["entries"][0]["nif:isString"] != "Not testing")
def test_entries(self):
e = Entry()
self.assertRaises(jsonschema.ValidationError, e.validate)
e.nif__isString = "this is a test"
e.nif__beginIndex = 0
e.nif__endIndex = 10
e.validate()
def test_sentiment(self):
s = Sentiment()
self.assertRaises(jsonschema.ValidationError, s.validate)
s.nif__anchorOf = "so much testing"
s.prov__wasGeneratedBy = ""
s.validate()
def test_emotion_set(self):
e = EmotionSet()
self.assertRaises(jsonschema.ValidationError, e.validate)
e.nif__anchorOf = "so much testing"
e.prov__wasGeneratedBy = ""
self.assertRaises(jsonschema.ValidationError, e.validate)
e.onyx__hasEmotion = {}
e.validate()
def test_results(self):
r = Results()
e = Entry()
e.nif__isString = "Results test"
r.entries.append(e)
r.id = ":test_results"
r.validate()
def test_sentiments(self):
pass
def test_plugins(self):
self.assertRaises(Error, SenpyPlugin)
p = SenpyPlugin({"name": "dummy", "version": 0})
c = p.jsonld()
assert "info" not in c
assert "repo" not in c
assert "params" in c
logging.debug("Framed:")
logging.debug(c)
p.validate()
def test_frame_response(self):
pass

View File

@ -1,17 +1,15 @@
#!/bin/env python2 #!/bin/env python
# -*- py-which-shell: "python2"; -*-
import os import os
import logging import logging
import shelve import shelve
import shutil
import tempfile
try:
import unittest.mock as mock
except ImportError:
import mock
import json import json
import os import os
from unittest import TestCase from unittest import TestCase
from senpy.models import Response, Entry from senpy.models import Results, Entry
from senpy.plugins import SenpyPlugin, ShelfMixin from senpy.plugins import SenpyPlugin, ShelfMixin
@ -27,14 +25,18 @@ class ShelfTest(ShelfMixin, SenpyPlugin):
class ModelsTest(TestCase): class ModelsTest(TestCase):
shelf_file = 'shelf_test.db'
def tearDown(self): def tearDown(self):
if os.path.exists(self.shelf_dir):
shutil.rmtree(self.shelf_dir)
if os.path.isfile(self.shelf_file): if os.path.isfile(self.shelf_file):
os.remove(self.shelf_file) os.remove(self.shelf_file)
setUp = tearDown def setUp(self):
self.shelf_dir = tempfile.mkdtemp()
self.shelf_file = os.path.join(self.shelf_dir, "shelf")
def test_shelf(self): def test_shelf(self):
''' A shelf is created and the value is stored ''' ''' A shelf is created and the value is stored '''
@ -45,11 +47,10 @@ class ModelsTest(TestCase):
assert a.shelf_file == self.shelf_file assert a.shelf_file == self.shelf_file
a.sh['a'] = 'fromA' a.sh['a'] = 'fromA'
a.test(key='a', value='fromA') a.test(key='a', value='fromA')
del(a)
assert os.path.isfile(self.shelf_file)
sh = shelve.open(self.shelf_file) sh = shelve.open(self.shelf_file)
assert sh['a'] == 'fromA' assert sh['a'] == 'fromA'