diff --git a/app.py b/app.py index 652a5e3..a82ef27 100644 --- a/app.py +++ b/app.py @@ -14,11 +14,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -''' +""" Simple Sentiment Analysis server for EUROSENTIMENT This class shows how to use the nif_server module to create custom services. -''' +""" import config from flask import Flask from senpy.extensions import Senpy diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..23ee03b --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1 @@ +mock \ No newline at end of file diff --git a/plugins-bak/prueba/__init__.py b/plugins-bak/prueba/__init__.py deleted file mode 100644 index af50e93..0000000 --- a/plugins-bak/prueba/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from senpy.plugins import SenpyPlugin - -class Prueba(SenpyPlugin): - def __init__(self, **kwargs): - super(Prueba, self).__init__(name="prueba", - version="4.0", - **kwargs) - -plugin = Prueba() diff --git a/plugins/sentiment140/__init__.py b/plugins/sentiment140/__init__.py index fd7fc72..1cf4407 100644 --- a/plugins/sentiment140/__init__.py +++ b/plugins/sentiment140/__init__.py @@ -1,11 +1,10 @@ import requests import json -import sys - from senpy.plugins import SentimentPlugin from senpy.models import Response, Opinion, Entry + class Sentiment140Plugin(SentimentPlugin): EXTRA_PARAMS = { "language": {"aliases": ["language", "l"], @@ -13,6 +12,7 @@ class Sentiment140Plugin(SentimentPlugin): "options": ["es", "en", "auto"], } } + def __init__(self, **kwargs): super(Sentiment140Plugin, self).__init__(name="sentiment140", version="2.0", @@ -22,23 +22,21 @@ class Sentiment140Plugin(SentimentPlugin): def analyse(self, **params): lang = params.get("language", "auto") res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson", - json.dumps({ - "language": lang, - "data": [{"text": params["input"]}]} - )) - + json.dumps({"language": lang, + "data": [{"text": params["input"]}] + } + ) + ) response = Response() - polarityValue = int(res.json()["data"][0]["polarity"]) * 25 + polarity_value = int(res.json()["data"][0]["polarity"]) * 25 polarity = "marl:Neutral" - if polarityValue > 50: + if polarity_value > 50: polarity = "marl:Positive" - elif polarityValue < 50: + elif polarity_value < 50: polarity = "marl:Negative" - - entry = Entry(text=params["input"]) - opinion = Opinion(polarity=polarity, polarityValue=polarityValue) + opinion = Opinion(polarity=polarity, polarity_value=polarity_value) entry.opinions.append(opinion) entry.language = lang response.entries.append(entry) diff --git a/requirements.txt b/requirements.txt index a84606b..1f91c73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ Flask==0.10.1 gunicorn==19.0.0 requests==2.4.1 -Flask-Plugins==1.4 -GitPython==0.3.2.RC1 +GitPython==0.3.2.RC1 \ No newline at end of file diff --git a/senpy/__init__.py b/senpy/__init__.py index 528ea3a..c788029 100644 --- a/senpy/__init__.py +++ b/senpy/__init__.py @@ -14,21 +14,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -''' +""" Sentiment analysis server in Python -''' +""" -VERSION = "0.2.6" +VERSION = "0.2.7" import extensions import blueprints import plugins - - -if __name__ == '__main__': - from flask import Flask - app = Flask(__name__) - sp = extensions.Senpy() - sp.init_app(app) - app.debug = config.DEBUG - app.run() diff --git a/senpy/blueprints.py b/senpy/blueprints.py index e9340a8..cafc91d 100644 --- a/senpy/blueprints.py +++ b/senpy/blueprints.py @@ -1,27 +1,28 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2014 J. Fernando Sánchez Rada - Grupo de Sistemas Inteligentes -# DIT, UPM +# Copyright 2014 J. Fernando Sánchez Rada - Grupo de Sistemas Inteligentes +# DIT, UPM # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software +# Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -''' -Simple Sentiment Analysis server -''' +""" +Blueprints for Senpy +""" import json import logging + logger = logging.getLogger(__name__) -from flask import Blueprint, render_template, request, jsonify, current_app +from flask import Blueprint, request, jsonify, current_app nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__) @@ -31,8 +32,8 @@ BASIC_PARAMS = { }, } + def get_params(req, params=BASIC_PARAMS): - indict = None if req.method == 'POST': indict = req.form elif req.method == 'GET': @@ -41,37 +42,37 @@ def get_params(req, params=BASIC_PARAMS): raise ValueError("Invalid data") outdict = {} - wrongParams = {} + wrong_params = {} for param, options in params.iteritems(): for alias in options["aliases"]: if alias in indict: outdict[param] = indict[alias] if param not in outdict: if options.get("required", False): - wrongParams[param] = params[param] + wrong_params[param] = params[param] else: if "default" in options: outdict[param] = options["default"] else: - if "options" in params[param] and \ - outdict[param] not in params[param]["options"]: - wrongParams[param] = params[param] - if wrongParams: - message = {"status": "failed", "message": "Missing or invalid parameters"} - message["parameters"] = outdict - message["errors"] = {param:error for param, error in wrongParams.iteritems()} + if "options" in params[param] and outdict[param] not in params[param]["options"]: + wrong_params[param] = params[param] + if wrong_params: + message = {"status": "failed", + "message": "Missing or invalid parameters", + "parameters": outdict, + "errors": {param: error for param, error in wrong_params.iteritems()} + } raise ValueError(json.dumps(message)) return outdict + def basic_analysis(params): response = {"@context": ["http://demos.gsi.dit.upm.es/eurosentiment/static/context.jsonld", { "@base": "{}#".format(request.url.encode('utf-8')) } ], - "analysis": [{ - "@type": "marl:SentimentAnalysis" - }], + "analysis": [{"@type": "marl:SentimentAnalysis"}], "entries": [] } if "language" in params: @@ -83,8 +84,9 @@ def basic_analysis(params): }) return response + @nif_blueprint.route('/', methods=['POST', 'GET']) -def home(entries=None): +def home(): try: algo = get_params(request).get("algorithm", None) specific_params = current_app.senpy.parameters(algo) @@ -96,11 +98,13 @@ def home(entries=None): except Exception as ex: return jsonify(status="400", message=ex.message) + @nif_blueprint.route("/default") def default(): return current_app.senpy.default_plugin #return plugins(action="list", plugin=current_app.senpy.default_algorithm) + @nif_blueprint.route('/plugins/', methods=['POST', 'GET']) @nif_blueprint.route('/plugins/', methods=['POST', 'GET']) @nif_blueprint.route('/plugins//', methods=['POST', 'GET']) @@ -113,7 +117,7 @@ def plugins(plugin=None, action="list"): return "Plugin not found", 400 if action == "list": with_params = request.args.get("params", "") == "1" - dic = {plug:plugs[plug].jsonable(with_params) for plug in plugs} + dic = {plug: plugs[plug].jsonable(with_params) for plug in plugs} return jsonify(dic) if action == "disable": current_app.senpy.disable_plugin(plugin) @@ -127,9 +131,11 @@ def plugins(plugin=None, action="list"): else: return "action '{}' not allowed".format(action), 400 + if __name__ == '__main__': import config from flask import Flask + app = Flask(__name__) app.register_blueprint(nif_blueprint) app.debug = config.DEBUG diff --git a/senpy/extensions.py b/senpy/extensions.py index b91823e..2ff544d 100644 --- a/senpy/extensions.py +++ b/senpy/extensions.py @@ -1,3 +1,5 @@ +""" +""" import os import sys import imp @@ -55,7 +57,6 @@ class Senpy(object): else: return False - def analyse(self, **params): algo = None logger.debug("analysing with params: {}".format(params)) @@ -69,12 +70,12 @@ class Senpy(object): resp.analysis.append(plug.jsonable()) return resp else: - return {"status": 400, "message": "The algorithm '{}' is not valid".format(algo) } + return {"status": 400, "message": "The algorithm '{}' is not valid".format(algo)} @property def default_plugin(self): candidates = self.filter_plugins(enabled=True) - if len(candidates)>1: + if len(candidates) > 1: candidate = candidates.keys()[0] logger.debug("Default: {}".format(candidate)) return candidate @@ -97,7 +98,8 @@ class Senpy(object): del self.plugins[plugin] self.plugins[nplug.name] = nplug - def _load_plugin(self, plugin, search_folder, enabled=True): + @staticmethod + def _load_plugin(plugin, search_folder, enabled=True): logger.debug("Loading plugins") sys.path.append(search_folder) (fp, pathname, desc) = imp.find_module(plugin) @@ -118,14 +120,14 @@ class Senpy(object): logger.debug("Exception importing {}: {}".format(plugin, ex)) return tmp - def _load_plugins(self): plugins = {} for search_folder in self._search_folders: for item in os.listdir(search_folder): if os.path.isdir(os.path.join(search_folder, item)) \ - and os.path.exists( - os.path.join(search_folder, item, "__init__.py")): + and os.path.exists(os.path.join(search_folder, + item, + "__init__.py")): plugin = self._load_plugin(item, search_folder) if plugin: plugins[plugin.name] = plugin @@ -140,12 +142,6 @@ class Senpy(object): for plugin in self.plugins: self.enable_plugin(plugin) - def enable_plugin(self, item): - self.plugins[item].enabled = True - - def disable_plugin(self, item): - self.plugins[item].enabled = False - @property def plugins(self): """ Return the plugins registered for a given application. """ @@ -157,27 +153,20 @@ class Senpy(object): def filter_plugins(self, **kwargs): """ Filter plugins by different criteria """ + def matches(plug): res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items()) logger.debug("matching {} with {}: {}".format(plug.name, kwargs, res)) return res + if not kwargs: return self.plugins else: - return {n:p for n, p in self.plugins.items() if matches(p)} + return {n: p for n, p in self.plugins.items() if matches(p)} def sentiment_plugins(self): """ Return only the sentiment plugins """ - return {p:plugin for p, plugin in self.plugins.items() if - isinstance(plugin, SentimentPlugin)} - - -if __name__ == '__main__': - from flask import Flask - app = Flask(__name__) - sp = Senpy() - sp.init_app(APP) - with APP.app_context(): - sp._load_plugins() + return {p: plugin for p, plugin in self.plugins.items() if + isinstance(plugin, SentimentPlugin)} \ No newline at end of file diff --git a/senpy/models.py b/senpy/models.py index 7dcbf0c..92d3787 100644 --- a/senpy/models.py +++ b/senpy/models.py @@ -2,6 +2,7 @@ import json import os from collections import defaultdict + class Leaf(defaultdict): def __init__(self, ofclass=list): super(Leaf, self).__init__(ofclass) @@ -15,6 +16,7 @@ class Leaf(defaultdict): def __delattr__(self, name): return super(Leaf, self).__delitem__(name) + class Response(Leaf): def __init__(self, context=None): super(Response, self).__init__() @@ -22,10 +24,10 @@ class Response(Leaf): self["entries"] = [] if context is None: context = "{}/context.jsonld".format(os.path.dirname( - os.path.realpath(__file__))) + os.path.realpath(__file__))) if isinstance(context, dict): self["@context"] = context - if isinstance(context, basestring): + if isinstance(context, str) or isinstance(context, unicode): try: with open(context) as f: self["@context"] = json.loads(f.read()) @@ -34,26 +36,28 @@ class Response(Leaf): class Entry(Leaf): - def __init__(self, text=None, emotionSets=None, opinions=None, **kwargs): + def __init__(self, text=None, emotion_sets=None, opinions=None, **kwargs): super(Entry, self).__init__(**kwargs) if text: self.text = text - if emotionSets: - self.emotionSets = emotionSets + if emotion_sets: + self.emotionSets = emotion_sets if opinions: self.opinions = opinions + class Opinion(Leaf): - def __init__(self, polarityValue=None, polarity=None, **kwargs): + def __init__(self, polarity_value=None, polarity=None, **kwargs): super(Opinion, self).__init__(**kwargs) - if polarityValue is not None: - self.polarityValue = polarityValue + if polarity_value is not None: + self.polarity_value = polarity_value if polarity is not None: self.polarity = polarity - class EmotionSet(Leaf): - def __init__(self, emotions=[], **kwargs): + def __init__(self, emotions=None, **kwargs): + if not emotions: + emotions = [] super(EmotionSet, self).__init__(**kwargs) self.emotions = emotions or [] diff --git a/senpy/plugins.py b/senpy/plugins.py index a3e2fd3..31e80e2 100644 --- a/senpy/plugins.py +++ b/senpy/plugins.py @@ -32,6 +32,7 @@ PARAMS = {"input": {"aliases": ["i", "input"], }, } + class SenpyPlugin(object): def __init__(self, name=None, version=None, extraparams=None, params=None): logger.debug("Initialising {}".format(name)) @@ -56,11 +57,11 @@ class SenpyPlugin(object): self.enabled = False def jsonable(self, parameters=False): - resp = { + resp = { "@id": "{}_{}".format(self.name, self.version), "enabled": self.enabled, } - if self.repo: + if hasattr(self, "repo") and self.repo: resp["repo"] = self.repo.remotes[0].url if parameters: resp["parameters"] = self.params @@ -68,14 +69,15 @@ class SenpyPlugin(object): resp["extra_parameters"] = self.extraparams return resp + class SentimentPlugin(SenpyPlugin): def __init__(self, - minPolarityValue=0, - maxPolarityValue=1, + min_polarity_value=0, + max_polarity_value=1, **kwargs): super(SentimentPlugin, self).__init__(**kwargs) - self.minPolarityValue = minPolarityValue - self.maxPolarityValue = maxPolarityValue + self.minPolarityValue = min_polarity_value + self.maxPolarityValue = max_polarity_value def jsonable(self, *args, **kwargs): resp = super(SentimentPlugin, self).jsonable(*args, **kwargs) @@ -83,16 +85,17 @@ class SentimentPlugin(SenpyPlugin): resp["marl:minPolarityValue"] = self.minPolarityValue return resp + class EmotionPlugin(SenpyPlugin): def __init__(self, - minEmotionValue=0, - maxEmotionValue=1, - emotionCategory=None, + min_emotion_value=0, + max_emotion_value=1, + emotion_category=None, **kwargs): super(EmotionPlugin, self).__init__(**kwargs) - self.minEmotionValue = minEmotionValue - self.maxEmotionValue = maxEmotionValue - self.emotionCategory = emotionCategory + self.minEmotionValue = min_emotion_value + self.maxEmotionValue = max_emotion_value + self.emotionCategory = emotion_category def jsonable(self, *args, **kwargs): resp = super(EmotionPlugin, self).jsonable(*args, **kwargs) diff --git a/setup.py b/setup.py index 7e90c9c..eeca594 100644 --- a/setup.py +++ b/setup.py @@ -2,17 +2,17 @@ from setuptools import setup import senpy setup( - name = 'senpy', - packages = ['senpy'], # this must be the same as the name above - version = senpy.VERSION, - description = ''' + name='senpy', + packages=['senpy'], # this must be the same as the name above + version=senpy.VERSION, + description=''' A sentiment analysis server implementation. Designed to be \ extendable, so new algorithms and sources can be used. ''', - author = 'J. Fernando Sanchez', - author_email = 'balkian@gmail.com', - url = 'https://github.com/balkian/senpy', # use the URL to the github repo - download_url = 'https://github.com/balkian/senpy/archive/{}.tar.gz'.format(senpy.VERSION), - keywords = ['eurosentiment', 'sentiment', 'emotions', 'nif'], # arbitrary keywords - classifiers = [], + author='J. Fernando Sanchez', + author_email='balkian@gmail.com', + url='https://github.com/balkian/senpy', # use the URL to the github repo + download_url='https://github.com/balkian/senpy/archive/{}.tar.gz'.format(senpy.VERSION), + keywords=['eurosentiment', 'sentiment', 'emotions', 'nif'], # arbitrary keywords + classifiers=[], ) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..8b13789 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/blueprints_test/__init__.py b/tests/blueprints_test/__init__.py index df0f1a4..3712682 100644 --- a/tests/blueprints_test/__init__.py +++ b/tests/blueprints_test/__init__.py @@ -1,5 +1,7 @@ + import os import logging + try: import unittest.mock as mock except ImportError: @@ -8,10 +10,12 @@ from senpy.extensions import Senpy from flask import Flask from flask.ext.testing import TestCase + def check_dict(indic, template): - return all(item in indic.items() for item in template.items()) + return all(item in indic.items() for item in template.items()) -class Blueprints_Test(TestCase): + +class BlueprintsTest(TestCase): def create_app(self): self.app = Flask("test_extensions") self.senpy = Senpy() @@ -20,7 +24,6 @@ class Blueprints_Test(TestCase): self.senpy.add_folder(self.dir) return self.app - def test_home(self): """ Calling with no arguments should ask the user for more arguments """ resp = self.client.get("/") diff --git a/tests/extensions_test/__init__.py b/tests/extensions_test/__init__.py index 3a6f240..25ed292 100644 --- a/tests/extensions_test/__init__.py +++ b/tests/extensions_test/__init__.py @@ -1,5 +1,6 @@ import os import logging + try: import unittest.mock as mock except ImportError: @@ -9,7 +10,7 @@ from flask import Flask from flask.ext.testing import TestCase -class Extensions_Test(TestCase): +class ExtensionsTest(TestCase): def create_app(self): self.app = Flask("test_extensions") self.senpy = Senpy() @@ -22,16 +23,16 @@ class Extensions_Test(TestCase): """ Initialising the app with the extension. """ assert hasattr(self.app, "senpy") tapp = Flask("temp app") - tsen = Senpy(tapp) + self.senpy.init_app(tapp) assert hasattr(tapp, "senpy") def test_discovery(self): """ Discovery of plugins in given folders. """ + # noinspection PyProtectedMember assert self.dir in self.senpy._search_folders print self.senpy.plugins assert "dummy" in self.senpy.plugins - def test_enabling(self): """ Enabling a plugin """ self.senpy.enable_plugin("dummy") @@ -41,7 +42,7 @@ class Extensions_Test(TestCase): """ Disabling a plugin """ self.senpy.enable_plugin("dummy") self.senpy.disable_plugin("dummy") - assert self.senpy.plugins["dummy"].enabled == False + assert not self.senpy.plugins["dummy"].enabled def test_default(self): """ Default plugin should be set """ @@ -63,7 +64,7 @@ class Extensions_Test(TestCase): def test_filtering(self): """ Filtering plugins """ - assert len(self.senpy.filter_plugins(name="dummy"))>0 + assert len(self.senpy.filter_plugins(name="dummy")) > 0 assert not len(self.senpy.filter_plugins(name="notdummy")) assert self.senpy.filter_plugins(name="dummy", enabled=True) self.senpy.disable_plugin("dummy")