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

Released v0.7

Bug-fixes and improvements:
* Closes #5
* Closes #1
* Adds Client (beta)
* Added several schemas
* Lighter string representation -> should avoid delays in the analysis
  with plugins that have 'heavy' attributes

Backwards-incompatible changes:
* Context in headers by default
* All schemas include a "@type" argument that is used for autodetection
  in the client

... And possibly many more, this is still <1.0
This commit is contained in:
J. Fernando Sánchez 2017-02-08 21:55:17 +01:00
parent fbf0384985
commit 908090f634
22 changed files with 298 additions and 113 deletions

View File

@ -1,4 +1,4 @@
from python:2.7-slim from python:2.7
WORKDIR /usr/src/app WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/ ADD requirements.txt /usr/src/app/

View File

@ -1,4 +1,4 @@
from python:3.4-slim from python:3.4
WORKDIR /usr/src/app WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/ ADD requirements.txt /usr/src/app/

View File

@ -1,4 +1,4 @@
from python:3.5-slim from python:3.5
WORKDIR /usr/src/app WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/ ADD requirements.txt /usr/src/app/

View File

@ -1,5 +0,0 @@
from python:3.4
RUN pip install pytest
ADD requirements.txt /usr/src/app/
RUN pip install -r /usr/src/app/requirements.txt

View File

@ -1,4 +1,4 @@
from python:{{PYVERSION}}-slim from python:{{PYVERSION}}
WORKDIR /usr/src/app WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/ ADD requirements.txt /usr/src/app/

View File

@ -4,6 +4,7 @@ NAME=senpy
REPO=gsiupm REPO=gsiupm
VERSION=$(shell cat $(NAME)/VERSION) VERSION=$(shell cat $(NAME)/VERSION)
TARNAME=$(NAME)-$(subst -,.,$(VERSION)).tar.gz TARNAME=$(NAME)-$(subst -,.,$(VERSION)).tar.gz
IMAGENAME=$(REPO)/$(NAME):$(VERSION)
all: build run all: build run
@ -22,27 +23,24 @@ dockerfiles: $(addprefix Dockerfile-,$(PYVERSIONS))
Dockerfile-%: Dockerfile.template Dockerfile-%: Dockerfile.template
sed "s/{{PYVERSION}}/$*/" Dockerfile.template > Dockerfile-$* sed "s/{{PYVERSION}}/$*/" Dockerfile.template > Dockerfile-$*
build: $(addprefix build-, $(PYMAIN)) quick_build: $(addprefix build-, $(PYMAIN))
buildall: $(addprefix build-, $(PYVERSIONS)) build: $(addprefix build-, $(PYVERSIONS))
build-%: Dockerfile-% build-%: Dockerfile-%
docker build -t '$(REPO)/$(NAME):$(VERSION)-python$*' -f Dockerfile-$* .; docker build -t '$(IMAGENAME)-python$*' -f Dockerfile-$* .;
build-debug-%: quick_test: $(addprefix test-,$(PYMAIN))
docker build -t '$(NAME)-debug' -f Dockerfile-debug-$* .;
test: $(addprefix test-,$(PYMAIN)) test: $(addprefix test-,$(PYVERSIONS))
testall: $(addprefix test-,$(PYVERSIONS))
debug-%: debug-%:
docker run --rm -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti $(NAME)-debug ; (docker start $(NAME)-debug && docker attach $(NAME)-debug) || docker run -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti --name $(NAME)-debug '$(IMAGENAME)-python$*'
debug: debug-$(PYMAIN) debug: debug-$(PYMAIN)
test-%: build-% test-%: build-%
docker run --rm -w /usr/src/app/ --entrypoint=/usr/local/bin/python -ti '$(REPO)/$(NAME):$(VERSION)-python$*' setup.py test --addopts "-vvv -s" ; docker run --rm -w /usr/src/app/ --entrypoint=/usr/local/bin/python -ti '$(IMAGENAME)-python$*' setup.py test --addopts "-vvv -s" ;
dist/$(TARNAME): dist/$(TARNAME):
docker run --rm -ti -v $$PWD:/usr/src/app/ -w /usr/src/app/ python:$(PYMAIN) python setup.py sdist; docker run --rm -ti -v $$PWD:/usr/src/app/ -w /usr/src/app/ python:$(PYMAIN) python setup.py sdist;
@ -55,12 +53,13 @@ pip_test-%: sdist
pip_test: $(addprefix pip_test-,$(PYVERSIONS)) pip_test: $(addprefix pip_test-,$(PYVERSIONS))
upload-%: test-% upload-%: test-%
docker push '$(REPO)/$(NAME):$(VERSION)-python$*' docker push '$(IMAGENAME)-python$*'
upload: testall $(addprefix upload-,$(PYVERSIONS)) upload: test $(addprefix upload-,$(PYVERSIONS))
docker tag '$(REPO)/$(NAME):$(VERSION)-python$(PYMAIN)' '$(REPO)/$(NAME):$(VERSION)' docker tag '$(IMAGENAME)-python$(PYMAIN)' '$(IMAGENAME)'
docker tag '$(REPO)/$(NAME):$(VERSION)-python$(PYMAIN)' '$(REPO)/$(NAME)' docker tag '$(IMAGENAME)-python$(PYMAIN)' '$(REPO)/$(NAME)'
docker push '$(REPO)/$(NAME):$(VERSION)' docker push '$(IMAGENAME)'
docker push '$(REPO)/$(NAME)'
clean: clean:
@docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[1] != "${VERSION}"){ print $$1;}}' | xargs docker rm 2>/dev/null|| true @docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[1] != "${VERSION}"){ print $$1;}}' | xargs docker rm 2>/dev/null|| true
@ -78,6 +77,6 @@ pip_upload:
pip_test: $(addprefix pip_test-,$(PYVERSIONS)) pip_test: $(addprefix pip_test-,$(PYVERSIONS))
run: build run: build
docker run --rm -p 5000:5000 -ti '$(REPO)/$(NAME):$(VERSION)-python$(PYMAIN)' docker run --rm -p 5000:5000 -ti '$(IMAGENAME)-python$(PYMAIN)'
.PHONY: test test-% build-% build test pip_test run yapf dev .PHONY: test test-% build-% build test pip_test run yapf dev

View File

@ -1 +1 @@
0.7.0-dev3 0.7.0

45
senpy/client.py Normal file
View File

@ -0,0 +1,45 @@
import requests
import logging
from . import models
logger = logging.getLogger(__name__)
class Client(object):
def __init__(self, endpoint):
self.endpoint = endpoint
def analyse(self, input, method='GET', **kwargs):
return self.request('/', method=method, input=input, **kwargs)
def request(self, path=None, method='GET', **params):
url = '{}{}'.format(self.endpoint, path)
response = requests.request(method=method,
url=url,
params=params)
try:
resp = models.from_dict(response.json())
resp.validate(resp)
return resp
except Exception as ex:
logger.error(('There seems to be a problem with the response:\n'
'\tURL: {url}\n'
'\tError: {error}\n'
'\t\n'
'#### Response:\n'
'\tCode: {code}'
'\tContent: {content}'
'\n').format(error=ex,
url=url,
code=response.status_code,
content=response.content))
raise ex
if __name__ == '__main__':
c = Client('http://senpy.cluster.gsi.dit.upm.es/api/')
resp = c.analyse('hello')
# print(resp)
print(resp.entries)
resp.validate()

View File

@ -161,7 +161,7 @@ class Senpy(object):
self._set_active_plugin(plugin_name, success) self._set_active_plugin(plugin_name, success)
except Exception as ex: except Exception as ex:
msg = "Error activating plugin {} - {} : \n\t{}".format( msg = "Error activating plugin {} - {} : \n\t{}".format(
plugin.name, ex, ex.format_exc()) plugin.name, ex, traceback.format_exc())
logger.error(msg) logger.error(msg)
raise Error(msg) raise Error(msg)
if sync: if sync:

View File

@ -12,12 +12,15 @@ import time
import copy import copy
import json import json
import os import os
import logging
import jsonref import jsonref
import jsonschema import jsonschema
from flask import Response as FlaskResponse from flask import Response as FlaskResponse
import logging
logger = logging.getLogger(__name__)
DEFINITIONS_FILE = 'definitions.json' DEFINITIONS_FILE = 'definitions.json'
CONTEXT_PATH = os.path.join( CONTEXT_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld') os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld')
@ -40,7 +43,6 @@ def read_schema(schema_file, absolute=False):
base_schema = read_schema(DEFINITIONS_FILE) base_schema = read_schema(DEFINITIONS_FILE)
logging.debug(base_schema)
class Context(dict): class Context(dict):
@ -72,7 +74,7 @@ base_context = Context.load(CONTEXT_PATH)
class SenpyMixin(object): class SenpyMixin(object):
context = base_context["@context"] context = base_context["@context"]
def flask(self, in_headers=False, headers=None, **kwargs): def flask(self, in_headers=True, headers=None, **kwargs):
""" """
Return the values and error to be used in flask. Return the values and error to be used in flask.
So far, it returns a fixed context. We should store/generate different So far, it returns a fixed context. We should store/generate different
@ -151,14 +153,16 @@ class SenpyMixin(object):
return str(self.to_JSON()) return str(self.to_JSON())
class SenpyModel(SenpyMixin, dict): class BaseModel(SenpyMixin, dict):
schema = base_schema schema = base_schema
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.id = kwargs.pop('id', '{}_{}'.format( if 'id' in kwargs:
type(self).__name__, time.time())) self.id = kwargs.pop('id')
elif kwargs.pop('_auto_id', True):
self.id = '_:{}_{}'.format(
type(self).__name__, time.time())
temp = dict(*args, **kwargs) temp = dict(*args, **kwargs)
for obj in [self.schema, ] + self.schema.get('allOf', []): for obj in [self.schema, ] + self.schema.get('allOf', []):
@ -175,7 +179,11 @@ class SenpyModel(SenpyMixin, dict):
context = temp['context'] context = temp['context']
del temp['context'] del temp['context']
self.__dict__['context'] = Context.load(context) self.__dict__['context'] = Context.load(context)
super(SenpyModel, self).__init__(temp) try:
temp['@type'] = getattr(self, '@type')
except AttributeError:
logger.warn('Creating an instance of an unknown model')
super(BaseModel, self).__init__(temp)
def _get_key(self, key): def _get_key(self, key):
key = key.replace("__", ":", 1) key = key.replace("__", ":", 1)
@ -206,73 +214,80 @@ class SenpyModel(SenpyMixin, dict):
return d return d
class Response(SenpyModel): _subtypes = {}
schema = read_schema('response.json')
class Results(SenpyModel): def register(rsubclass, rtype=None):
schema = read_schema('results.json') _subtypes[rtype or rsubclass.__name__] = rsubclass
class Entry(SenpyModel): def from_dict(indict):
schema = read_schema('entry.json') target = indict.get('@type', None)
if target and target in _subtypes:
cls = _subtypes[target]
else:
cls = BaseModel
return cls(**indict)
class Sentiment(SenpyModel): def from_schema(name, schema_file=None, base_classes=None):
schema = read_schema('sentiment.json') base_classes = base_classes or []
base_classes.append(BaseModel)
schema_file = schema_file or '{}.json'.format(name)
class_name = '{}{}'.format(i[0].upper(), i[1:])
newclass = type(class_name, tuple(base_classes), {})
setattr(newclass, '@type', name)
setattr(newclass, 'schema', read_schema(schema_file))
register(newclass, name)
return newclass
class Analysis(SenpyModel): def _add_from_schema(*args, **kwargs):
schema = read_schema('analysis.json') generatedClass = from_schema(*args, **kwargs)
globals()[generatedClass.__name__] = generatedClass
del generatedClass
class EmotionSet(SenpyModel): for i in ['response',
schema = read_schema('emotionSet.json') 'results',
'entry',
'sentiment',
'analysis',
'emotionSet',
'emotion',
'emotionModel',
'suggestion',
'plugin',
'emotionPlugin',
'sentimentPlugin',
'plugins']:
_add_from_schema(i)
_ErrorModel = from_schema('error')
class Emotion(SenpyModel):
schema = read_schema('emotion.json')
class EmotionModel(SenpyModel):
schema = read_schema('emotionModel.json')
class Suggestion(SenpyModel):
schema = read_schema('suggestion.json')
class PluginModel(SenpyModel):
schema = read_schema('plugin.json')
class EmotionPluginModel(SenpyModel):
schema = read_schema('plugin.json')
class SentimentPluginModel(SenpyModel):
schema = read_schema('plugin.json')
class Plugins(SenpyModel):
schema = read_schema('plugins.json')
class Error(SenpyMixin, BaseException): class Error(SenpyMixin, BaseException):
def __init__(self, def __init__(self,
message, message,
status=500,
params=None,
errors=None,
*args, *args,
**kwargs): **kwargs):
super(Error, self).__init__(self, message, message)
self._error = _ErrorModel(message=message, *args, **kwargs)
self.message = message self.message = message
self.status = status
self.params = params or {}
self.errors = errors or ""
def _plain_dict(self): def __getattr__(self, key):
return self.__dict__ if key != '_error' and hasattr(self._error, key):
return getattr(self._error, key)
raise AttributeError(key)
def __str__(self): def __setattr__(self, key, value):
return str(self.jsonld()) if key != '_error':
return setattr(self._error, key, value)
else:
super(Error, self).__setattr__(key, value)
def __delattr__(self, key):
delattr(self._error, key)
register(Error, 'error')

View File

@ -6,15 +6,15 @@ import os.path
import pickle import pickle
import logging import logging
import tempfile import tempfile
from .models import PluginModel, Error from . import models
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SenpyPlugin(PluginModel): class SenpyPlugin(models.Plugin):
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 models.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__(info) super(SenpyPlugin, self).__init__(info)
@ -40,7 +40,7 @@ class SenpyPlugin(PluginModel):
self.deactivate() self.deactivate()
class SentimentPlugin(SenpyPlugin): class SentimentPlugin(SenpyPlugin, models.SentimentPlugin):
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))
@ -48,7 +48,7 @@ class SentimentPlugin(SenpyPlugin):
self["@type"] = "marl:SentimentAnalysis" self["@type"] = "marl:SentimentAnalysis"
class EmotionPlugin(SenpyPlugin): class EmotionPlugin(SentimentPlugin, models.EmotionPlugin):
def __init__(self, info, *args, **kwargs): def __init__(self, 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))
@ -71,10 +71,6 @@ class ShelfMixin(object):
del self.__dict__['_sh'] del self.__dict__['_sh']
self.save() self.save()
def __del__(self):
self.save()
super(ShelfMixin, self).__del__()
@property @property
def shelf_file(self): def shelf_file(self):
if not hasattr(self, '_shelf_file') or not self._shelf_file: if not hasattr(self, '_shelf_file') or not self._shelf_file:
@ -86,8 +82,7 @@ class ShelfMixin(object):
return self._shelf_file return self._shelf_file
def save(self): def save(self):
logger.debug('closing pickle') logger.debug('saving pickle')
if hasattr(self, '_sh') and self._sh is not None: if hasattr(self, '_sh') and self._sh is not None:
with open(self.shelf_file, 'wb') as f: with open(self.shelf_file, 'wb') as f:
pickle.dump(self._sh, f) pickle.dump(self._sh, f)
del (self.__dict__['_sh'])

7
senpy/schemas/\ Normal file
View File

@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Senpy analysis",
"allOf": [{
"$ref": "atom.json"
}]
}

15
senpy/schemas/atom.json Normal file
View File

@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Base schema for all Senpy objects",
"type": "object",
"properties": {
"@id": {
"type": "string"
},
"@type": {
"type": "string",
"description": "Type of the atom. e.g., 'onyx:EmotionAnalysis', 'nif:Entry'"
}
},
"required": ["@id", "@type"]
}

View File

@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"$allOf": [
{
"$ref": "plugin.json"
},
{
"properties": {
"onyx:usesEmotionModel": {
"type": "array",
"items": {
"$ref": "emotionModel.json"
}
}
}
}
]
}

View File

@ -5,9 +5,6 @@
"@id": { "@id": {
"type": "string" "type": "string"
}, },
"@type": {
"enum": [["nif:RFC5147String", "nif:Context"]]
},
"nif:isString": { "nif:isString": {
"description": "String contained in this Context", "description": "String contained in this Context",
"type": "string" "type": "string"

23
senpy/schemas/error.json Normal file
View File

@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Base schema for all Senpy objects",
"type": "object",
"$allOf": [
{"$ref": "atom.json"},
{
"properties": {
"message": {
"type": "string"
},
"errors": {
"type": "list",
"items": {"type": "object"}
},
"code": {
"type": "int"
},
"required": ["message"]
}
}
]
}

View File

@ -4,7 +4,12 @@
"required": ["@id", "extra_params"], "required": ["@id", "extra_params"],
"properties": { "properties": {
"@id": { "@id": {
"type": "string" "type": "string",
"description": "Unique identifier for the plugin, usually comprised of the name of the plugin and the version."
},
"name": {
"type": "string",
"description": "The name of the plugin, which will be used in the algorithm detection phase"
}, },
"extra_params": { "extra_params": {
"type": "object", "type": "object",

View File

@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"$allOf": [
{
"$ref": "plugin.json"
},
{
"properties": {
"marl:minPolarityValue": {
"type": "number"
},
"marl:maxPolarityValue": {
"type": "number"
}
}
}
]
}

View File

@ -1,6 +1,11 @@
import logging import logging
from functools import partial from functools import partial
try:
from unittest.mock import patch
except ImportError:
from mock import patch
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from unittest import TestCase from unittest import TestCase
@ -11,9 +16,12 @@ from senpy.models import Error
class CLITest(TestCase): class CLITest(TestCase):
def test_basic(self): def test_basic(self):
self.assertRaises(Error, partial(main_function, [])) self.assertRaises(Error, partial(main_function, []))
res = main_function(['--input', 'test'])
assert 'entries' in res with patch('senpy.extensions.Senpy.analyse') as patched:
res = main_function(['--input', 'test', '--algo', 'rand']) main_function(['--input', 'test'])
assert 'entries' in res
assert 'analysis' in res patched.assert_called_with(input='test')
assert res['analysis'][0]['name'] == 'rand' with patch('senpy.extensions.Senpy.analyse') as patched:
main_function(['--input', 'test', '--algo', 'rand'])
patched.assert_called_with(input='test', algo='rand')

42
tests/test_client.py Normal file
View File

@ -0,0 +1,42 @@
from unittest import TestCase
try:
from unittest.mock import patch
except ImportError:
from mock import patch
from senpy.client import Client
from senpy.models import Results, Error
class Call(dict):
def __init__(self, obj):
self.obj = obj.jsonld()
def json(self):
return self.obj
class ModelsTest(TestCase):
def setUp(self):
self.host = '0.0.0.0'
self.port = 5000
def test_client(self):
endpoint = 'http://dummy/'
client = Client(endpoint)
success = Call(Results())
with patch('requests.request', return_value=success) as patched:
resp = client.analyse('hello')
assert isinstance(resp, Results)
patched.assert_called_with(url=endpoint + '/',
method='GET',
params={'input': 'hello'})
error = Call(Error('Nothing'))
with patch('requests.request', return_value=error) as patched:
resp = client.analyse(input='hello', algorithm='NONEXISTENT')
assert isinstance(resp, Error)
patched.assert_called_with(url=endpoint + '/',
method='GET',
params={'input': 'hello',
'algorithm': 'NONEXISTENT'})

View File

@ -11,7 +11,9 @@ from pprint import pprint
class ModelsTest(TestCase): class ModelsTest(TestCase):
def test_jsonld(self): def test_jsonld(self):
prueba = {"id": "test", "analysis": [], "entries": []} prueba = {"id": "test",
"analysis": [],
"entries": []}
r = Results(**prueba) r = Results(**prueba)
print("Response's context: ") print("Response's context: ")
pprint(r.context) pprint(r.context)
@ -28,11 +30,11 @@ class ModelsTest(TestCase):
assert "id" not in j assert "id" not in j
r6 = Results(**prueba) r6 = Results(**prueba)
r6.entries.append( e = Entry({
Entry({
"@id": "ohno", "@id": "ohno",
"nif:isString": "Just testing" "nif:isString": "Just testing"
})) })
r6.entries.append(e)
logging.debug("Reponse 6: %s", r6) logging.debug("Reponse 6: %s", r6)
assert ("marl" in r6.context) assert ("marl" in r6.context)
assert ("entries" in r6.context) assert ("entries" in r6.context)

View File

@ -44,7 +44,6 @@ class PluginsTest(TestCase):
a = ShelfDummyPlugin( a = ShelfDummyPlugin(
info={'name': 'default_shelve_file', info={'name': 'default_shelve_file',
'version': 'test'}) 'version': 'test'})
assert os.path.dirname(a.shelf_file) == tempfile.gettempdir()
a.activate() a.activate()
assert os.path.isfile(a.shelf_file) assert os.path.isfile(a.shelf_file)
os.remove(a.shelf_file) os.remove(a.shelf_file)