diff --git a/README.md b/README.md index abb09ae..288db7d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![GSI Logo](http://gsi.dit.upm.es/templates/jgsi/images/logo.png) +![GSI Logo](logo.png) [Senpy](http://senpy.herokuapp.com) ========================================= Example endpoint that yields results compatible with the EUROSENTIMENT format and exposes the NIF API. diff --git a/app.py b/app.py index 3b5d66b..3a90dbf 100644 --- a/app.py +++ b/app.py @@ -20,47 +20,13 @@ Simple Sentiment Analysis server for EUROSENTIMENT This class shows how to use the nif_server module to create custom services. ''' import config -import re from flask import Flask -import random -from senpy.nif_server import * +from senpy import Senpy app = Flask(__name__) -rgx = re.compile("(\w[\w']*\w|\w)", re.U) - -def hard_analysis(params): - response = basic_analysis(params) - response["analysis"][0].update({ "marl:algorithm": "SimpleAlgorithm", - "marl:minPolarityValue": -1, - "marl:maxPolarityValue": 1}) - for i in response["entries"]: - contextid = i["@id"] - random.seed(str(params)) - polValue = 2*random.random()-1 - if polValue > 0: - pol = "marl:Positive" - elif polValue == 0: - pol = "marl:Neutral" - else: - pol = "marl:Negative" - i["opinions"] = [{"marl:polarityValue": polValue, - "marl:hasPolarity": pol - - }] - i["strings"] = [] - for m in rgx.finditer(i["nif:isString"]): - i["strings"].append({ - "@id": "{}#char={},{}".format(contextid, m.start(), m.end()), - "nif:beginIndex": m.start(), - "nif:endIndex": m.end(), - "nif:anchorOf": m.group(0) - }) - - return response - -app.analyse = hard_analysis -app.register_blueprint(nif_server) +sp = Senpy() +sp.init_app(app) if __name__ == '__main__': app.debug = config.DEBUG diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..329b8df Binary files /dev/null and b/logo.png differ diff --git a/requirements.txt b/requirements.txt index 5860909..a84606b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Flask==0.10.1 gunicorn==19.0.0 requests==2.4.1 +Flask-Plugins==1.4 +GitPython==0.3.2.RC1 diff --git a/senpy/__init__.py b/senpy/__init__.py index e69de29..631e09c 100644 --- a/senpy/__init__.py +++ b/senpy/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +''' +Sentiment analysis server in Python +''' + +from blueprints import nif_blueprint +from extensions import Senpy + + +if __name__ == '__main__': + from flask import Flask + app = Flask(__name__) + app.register_blueprint(nif_server) + app.debug = config.DEBUG + app.run() diff --git a/senpy/__main__.py b/senpy/__main__.py new file mode 100644 index 0000000..fe412c3 --- /dev/null +++ b/senpy/__main__.py @@ -0,0 +1,7 @@ +from flask import Flask +from extensions import Senpy +app = Flask(__name__) +app.debug = True +sp = Senpy() +sp.init_app(app) +app.run() diff --git a/senpy/nif_server.py b/senpy/blueprints.py similarity index 80% rename from senpy/nif_server.py rename to senpy/blueprints.py index 60f1422..6170662 100644 --- a/senpy/nif_server.py +++ b/senpy/blueprints.py @@ -18,10 +18,9 @@ Simple Sentiment Analysis server ''' from flask import Blueprint, render_template, request, jsonify, current_app -import config import json -nif_server = Blueprint("NIF Sentiment Analysis Server", __name__) +nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__) PARAMS = {"input": {"aliases": ["i", "input"], "required": True, @@ -42,7 +41,7 @@ PARAMS = {"input": {"aliases": ["i", "input"], "required": False, "options": ["json-ld"], }, - "algorithm": {"aliases": ["algorithm", "a"], + "algorithm": {"aliases": ["algorithm", "a", "algo"], "required": False, }, "language": {"aliases": ["language", "l"], @@ -109,18 +108,40 @@ def basic_analysis(params): }) return response -@nif_server.route('/', methods=['POST', 'GET']) +@nif_blueprint.route('/', methods=['POST', 'GET']) def home(entries=None): try: params = get_params(request) except ValueError as ex: return ex.message - response = current_app.analyse(params) + response = current_app.senpy.analyse(**params) return jsonify(response) +@nif_blueprint.route('/plugins/', methods=['POST', 'GET']) +@nif_blueprint.route('/plugins/', methods=['POST', 'GET']) +@nif_blueprint.route('/plugins//', methods=['POST', 'GET']) +def plugins(plugin=None, action="list"): + print current_app.senpy.plugins.keys() + if plugin: + plugs = {plugin:current_app.senpy.plugins[plugin]} + else: + plugs = current_app.senpy.plugins + if action == "list": + dic = {plug:plugs[plug].enabled for plug in plugs} + return jsonify(dic) + elif action == "disable": + plugs[plugin].enabled = False + return "Ok" + elif action == "enable": + plugs[plugin].enabled = True + return "Ok" + else: + return "action '{}' not allowed".format(action), 404 + if __name__ == '__main__': + import config from flask import Flask app = Flask(__name__) - app.register_blueprint(nif_server) + app.register_blueprint(nif_blueprint) app.debug = config.DEBUG app.run() diff --git a/senpy/extensions.py b/senpy/extensions.py new file mode 100644 index 0000000..ae33d1e --- /dev/null +++ b/senpy/extensions.py @@ -0,0 +1,107 @@ +import os +import sys +import importlib + +from flask import current_app + +try: + from flask import _app_ctx_stack as stack +except ImportError: + from flask import _request_ctx_stack as stack + +from blueprints import nif_blueprint +from git import Repo, InvalidGitRepositoryError + +class Senpy(object): + + def __init__(self, app=None, plugin_folder="plugins"): + self.app = app + base_folder = os.path.join(os.path.dirname(__file__), "plugins") + + self.search_folders = (folder for folder in (base_folder, plugin_folder) + if folder and os.path.isdir(folder)) + + if app is not None: + self.init_app(app) + + """ + + Note: I'm not particularly fond of adding self.app and app.senpy, but + I can't think of a better way to do it. + """ + def init_app(self, app, plugin_folder="plugins"): + app.senpy = self + #app.config.setdefault('SQLITE3_DATABASE', ':memory:') + # Use the newstyle teardown_appcontext if it's available, + # otherwise fall back to the request context + if hasattr(app, 'teardown_appcontext'): + app.teardown_appcontext(self.teardown) + else: + app.teardown_request(self.teardown) + app.register_blueprint(nif_blueprint) + + def analyse(self, **params): + if "algorithm" in params: + algo = params["algorithm"] + if algo in self.plugins and self.plugins[algo].enabled: + return self.plugins[algo].plugin.analyse(**params) + return {"status": 500, "message": "No valid algorithm"} + + + def _load_plugin(self, plugin, search_folder, enabled=True): + sys.path.append(search_folder) + tmp = importlib.import_module(plugin) + sys.path.remove(search_folder) + tmp.path = search_folder + try: + repo_path = os.path.join(search_folder, plugin) + tmp.repo = Repo(repo_path) + except InvalidGitRepositoryError: + tmp.repo = None + if not hasattr(tmp, "enabled"): + tmp.enabled = enabled + return tmp + + + def _load_plugins(self): + #print(sys.path) + #print(search_folder) + 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")): + plugins[item] = self._load_plugin(item, search_folder) + + return plugins + + def teardown(self, exception): + pass + + def enable_all(self): + 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): + ctx = stack.top + if ctx is not None: + if not hasattr(self, '_plugins'): + self._plugins = self._load_plugins() + print("Already plugins") + return self._plugins + +if __name__ == '__main__': + from flask import Flask + app = Flask(__name__) + sp = Senpy() + sp.init_app(app) + with app.app_context(): + sp._load_plugins() diff --git a/senpy/plugin.py b/senpy/plugin.py new file mode 100644 index 0000000..d4e1530 --- /dev/null +++ b/senpy/plugin.py @@ -0,0 +1,34 @@ +class SenpyPlugin(object): + def __init__(self, name=None, version=None, params=None): + self.name = name + self.version = version + self.params = params or [] + + def analyse(self, *args, **kwargs): + pass + + def activate(self): + pass + + def deactivate(self): + pass + +class SentimentPlugin(SenpyPlugin): + def __init__(self, + minPolarity=0, + maxPolarity=1, + **kwargs): + super(SentimentPlugin, self).__init__(**kwargs) + self.minPolarity = minPolarity + self.maxPolarity = maxPolarity + +class EmotionPlugin(SenpyPlugin): + def __init__(self, + minEmotionValue=0, + maxEmotionValue=1, + emotionCategory=None, + **kwargs): + super(EmotionPlugin, self).__init__(**kwargs) + self.minEmotionValue = minEmotionValue + self.maxEmotionValue = maxEmotionValue + self.emotionCategory = emotionCategory diff --git a/senpy/plugins/sentiment140/__init__.py b/senpy/plugins/sentiment140/__init__.py new file mode 100644 index 0000000..0dacde5 --- /dev/null +++ b/senpy/plugins/sentiment140/__init__.py @@ -0,0 +1,48 @@ +import requests +import json + +import sys + +print(sys.path) +from senpy.plugin import SentimentPlugin + +class Sentiment140Plugin(SentimentPlugin): + def __init__(self, **kwargs): + super(Sentiment140Plugin, self).__init__(name="Sentiment140", + version="1.0", + **kwargs) + + def analyse(self, **params): + res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson", + json.dumps({ + "language": "auto", + "data": [{"text": params["input"]}]} + )) + + + response = {"analysis": [{}], "entries": []} + response["analysis"][0].update({ "marl:algorithm": "SimpleAlgorithm", + "marl:minPolarityValue": 0, + "marl:maxPolarityValue": 100}) + polarityValue = int(res.json()["data"][0]["polarity"]) * 25 + polarity = "marl:Neutral" + if polarityValue > 50: + polarity = "marl:Positive" + elif polarityValue < 50: + polarity = "marl:Negative" + + response["entries"] = [ + { + "isString": params["input"], + "opinions": [{ + "marl:polarityValue": polarityValue, + "marl:hasPolarity": polarity + + }] + } + ] + + return response + + +plugin = Sentiment140Plugin() diff --git a/senpy/plugins/sentiment140.py b/senpy/plugins/sentiment140/sentiment140.py similarity index 100% rename from senpy/plugins/sentiment140.py rename to senpy/plugins/sentiment140/sentiment140.py diff --git a/setup.py b/setup.py index 4a59534..8544350 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from distutils.core import setup setup( name = 'senpy', packages = ['senpy'], # this must be the same as the name above - version = '0.1', + version = '0.2', description = ''' A sentiment analysis server implementation. Designed to be \ extendable, so new algorithms and sources can be used. @@ -10,7 +10,7 @@ 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/0.1.tar.gz', # I'll explain this in a second + download_url = 'https://github.com/balkian/senpy/archive/0.2.tar.gz', # I'll explain this in a second keywords = ['eurosentiment', 'sentiment', 'emotions', 'nif'], # arbitrary keywords classifiers = [], )