diff --git a/app.py b/app.py index 3a90dbf..954895b 100644 --- a/app.py +++ b/app.py @@ -21,7 +21,7 @@ This class shows how to use the nif_server module to create custom services. ''' import config from flask import Flask -from senpy import Senpy +from senpy.extensions import Senpy app = Flask(__name__) @@ -30,4 +30,4 @@ sp.init_app(app) if __name__ == '__main__': app.debug = config.DEBUG - app.run() + app.run(use_reloader=False) diff --git a/senpy/__init__.py b/senpy/__init__.py index bb7e137..ef6866e 100644 --- a/senpy/__init__.py +++ b/senpy/__init__.py @@ -18,6 +18,8 @@ Sentiment analysis server in Python ''' +VERSION = "0.2.4" + import extensions import blueprints import plugins diff --git a/senpy/blueprints.py b/senpy/blueprints.py index 7e38dae..28b505b 100644 --- a/senpy/blueprints.py +++ b/senpy/blueprints.py @@ -22,43 +22,13 @@ import json nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__) -PARAMS = {"input": {"aliases": ["i", "input"], - "required": True, - "help": "Input text" - }, - "informat": {"aliases": ["f", "informat"], - "required": False, - "default": "text", - "options": ["turtle", "text"], - }, - "intype": {"aliases": ["intype", "t"], - "required": False, - "default": "direct", - "options": ["direct", "url", "file"], - }, - "outformat": {"aliases": ["outformat", "o"], - "default": "json-ld", - "required": False, - "options": ["json-ld"], - }, - "algorithm": {"aliases": ["algorithm", "a", "algo"], - "required": False, - }, - "language": {"aliases": ["language", "l"], - "required": False, - "options": ["es", "en"], - }, - "urischeme": {"aliases": ["urischeme", "u"], - "required": False, - "default": "RFC5147String", - "options": "RFC5147String" - }, - } +BASIC_PARAMS = { + "algorithm": {"aliases": ["algorithm", "a", "algo"], + "required": False, + }, +} -def get_algorithm(req): - return get_params(req, params={"algorithm": PARAMS["algorithm"]}) - -def get_params(req, params=PARAMS): +def get_params(req, params=BASIC_PARAMS): indict = None if req.method == 'POST': indict = req.form @@ -113,35 +83,44 @@ def basic_analysis(params): @nif_blueprint.route('/', methods=['POST', 'GET']) def home(entries=None): try: - algo = get_algorithm(request)["algorithm"] - specific_params = PARAMS.copy() - specific_params.update(current_app.senpy.parameters(algo)) + algo = get_params(request).get("algorithm", None) + specific_params = current_app.senpy.parameters(algo) params = get_params(request, specific_params) except ValueError as ex: return ex.message response = current_app.senpy.analyse(**params) return jsonify(response) +@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']) def plugins(plugin=None, action="list"): - print current_app.senpy.plugins.keys() + filt = {} if plugin: - plugs = {plugin:current_app.senpy.plugins[plugin]} - else: - plugs = current_app.senpy.plugins + filt["name"] = plugin + plugs = current_app.senpy.filter_plugins(**filt) + if plugin and not plugs: + return "Plugin not found", 400 if action == "list": - dic = {plug:plugs[plug].jsonable(True) for plug in plugs} + with_params = request.args.get("params", "") == "1" + dic = {plug:plugs[plug].jsonable(with_params) for plug in plugs} return jsonify(dic) - elif action == "disable": - plugs[plugin].enabled = False + if action == "disable": + current_app.senpy.disable_plugin(plugin) return "Ok" elif action == "enable": - plugs[plugin].enabled = True + current_app.senpy.enable_plugin(plugin) + return "Ok" + elif action == "reload": + current_app.senpy.reload_plugin(plugin) return "Ok" else: - return "action '{}' not allowed".format(action), 404 + return "action '{}' not allowed".format(action), 400 if __name__ == '__main__': import config diff --git a/senpy/extensions.py b/senpy/extensions.py index 0d1ee92..3cc2cb8 100644 --- a/senpy/extensions.py +++ b/senpy/extensions.py @@ -1,8 +1,10 @@ import os import sys -import importlib +import imp from flask import current_app +from collections import defaultdict +from .plugins import SentimentPlugin, EmotionPlugin try: from flask import _app_ctx_stack as stack @@ -18,7 +20,7 @@ class Senpy(object): self.app = app base_folder = os.path.join(os.path.dirname(__file__), "plugins") - self.search_folders = (folder for folder in (base_folder, plugin_folder) + self.search_folders = (folder for folder in (base_folder, plugin_folder, '/tmp/plugins') if folder and os.path.isdir(folder)) if app is not None: @@ -41,24 +43,49 @@ class Senpy(object): app.register_blueprint(nif_blueprint) def analyse(self, **params): + algo = None + print("analysing with params: {}".format(params)) if "algorithm" in params: algo = params["algorithm"] - if algo in self.plugins and self.plugins[algo].enabled: - plug = self.plugins[algo] - resp = plug.analyse(**params) - resp.analysis.append(plug.jsonable()) - return resp - return {"status": 500, "message": "No valid algorithm"} + elif self.plugins: + algo = self.default_plugin + if algo in self.plugins and self.plugins[algo].enabled: + plug = self.plugins[algo] + resp = plug.analyse(**params) + resp.analysis.append(plug.jsonable()) + return resp + else: + return {"status": 500, "message": "No valid algorithm"} + + @property + def default_plugin(self): + if self.plugins: + candidate = self.filter_plugins(enabled=True).keys()[0] + print("Default: {}".format(candidate)) + return candidate + else: + return Exception("No algorithm") def parameters(self, algo): - if algo in self.plugins: - if hasattr(self.plugins[algo], "parameters"): - return self.plugins[algo].parameters - return {} + return getattr(self.plugins.get(algo or self.default_plugin), "params", {}) + + def enable_plugin(self, plugin): + self.plugins[plugin].disable() + + def disable_plugin(self, plugin): + self.plugins[plugin].disable() + + def reload_plugin(self, plugin): + print("Reloading {}".format(plugin)) + plug = self.plugins[plugin] + nplug = self._load_plugin(plug.module, plug.path) + del self.plugins[plugin] + self.plugins[nplug.name] = nplug def _load_plugin(self, plugin, search_folder, enabled=True): sys.path.append(search_folder) - tmp = importlib.import_module(plugin).plugin + (fp, pathname, desc) = imp.find_module(plugin) + tmp = imp.load_module(plugin, fp, pathname, desc).plugin sys.path.remove(search_folder) tmp.path = search_folder try: @@ -68,19 +95,19 @@ class Senpy(object): tmp.repo = None if not hasattr(tmp, "enabled"): tmp.enabled = enabled + tmp.module = plugin 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) + plugin = self._load_plugin(item, search_folder) + plugins[plugin.name] = plugin return plugins @@ -105,6 +132,20 @@ class Senpy(object): self._plugins = self._load_plugins() return self._plugins + def filter_plugins(self, **kwargs): + def matches(plug): + res = all(getattr(plug, k, None)==v for (k,v) in kwargs.items()) + print("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)} + + def sentiment_plugins(self): + return (plugin for plugin in self.plugins if + isinstance(plugin, SentimentPlugin)) + if __name__ == '__main__': from flask import Flask app = Flask(__name__) diff --git a/senpy/plugins.py b/senpy/plugins.py index 5b04937..8fe22ac 100644 --- a/senpy/plugins.py +++ b/senpy/plugins.py @@ -1,24 +1,67 @@ +PARAMS = {"input": {"aliases": ["i", "input"], + "required": True, + "help": "Input text" + }, + "informat": {"aliases": ["f", "informat"], + "required": False, + "default": "text", + "options": ["turtle", "text"], + }, + "intype": {"aliases": ["intype", "t"], + "required": False, + "default": "direct", + "options": ["direct", "url", "file"], + }, + "outformat": {"aliases": ["outformat", "o"], + "default": "json-ld", + "required": False, + "options": ["json-ld"], + }, + "language": {"aliases": ["language", "l"], + "required": False, + "options": ["es", "en"], + }, + "urischeme": {"aliases": ["urischeme", "u"], + "required": False, + "default": "RFC5147String", + "options": "RFC5147String" + }, + } + class SenpyPlugin(object): - def __init__(self, name=None, version=None, params=None): + def __init__(self, name=None, version=None, extraparams=None, params=None): + print("Initing {}".format(name)) self.name = name self.version = version - self.params = params or [] + if params: + self.params = params + else: + self.params = PARAMS.copy() + if extraparams: + self.params.update(extraparams) + self.extraparams = extraparams or {} + self.enabled = True def analyse(self, *args, **kwargs): pass - def activate(self): - pass + def enable(self): + self.enabled = True - def deactivate(self): - pass + def disable(self): + self.enabled = False def jsonable(self, parameters=False): resp = { "@id": "{}_{}".format(self.name, self.version), + "enabled": self.enabled, } + if self.repo: + resp["repo"] = self.repo.remotes[0].url if parameters: - resp["parameters"] = self.params, + resp["parameters"] = self.params + elif self.extraparams: + resp["extra_parameters"] = self.extraparams return resp class SentimentPlugin(SenpyPlugin): diff --git a/senpy/plugins/sentiment140/__init__.py b/senpy/plugins/sentiment140/__init__.py index 5bca403..fd7fc72 100644 --- a/senpy/plugins/sentiment140/__init__.py +++ b/senpy/plugins/sentiment140/__init__.py @@ -3,20 +3,20 @@ import json import sys -print(sys.path) from senpy.plugins import SentimentPlugin from senpy.models import Response, Opinion, Entry class Sentiment140Plugin(SentimentPlugin): - parameters = { + EXTRA_PARAMS = { "language": {"aliases": ["language", "l"], "required": False, "options": ["es", "en", "auto"], } } def __init__(self, **kwargs): - super(Sentiment140Plugin, self).__init__(name="Sentiment140", - version="1.0", + super(Sentiment140Plugin, self).__init__(name="sentiment140", + version="2.0", + extraparams=self.EXTRA_PARAMS, **kwargs) def analyse(self, **params): @@ -44,5 +44,4 @@ class Sentiment140Plugin(SentimentPlugin): response.entries.append(entry) return response - plugin = Sentiment140Plugin() diff --git a/setup.py b/setup.py index e6f8cdb..84ba7b7 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ from setuptools import setup +import senpy setup( name = 'senpy', packages = ['senpy'], # this must be the same as the name above - version = '0.2.3', + version = senpy.VERSION, description = ''' A sentiment analysis server implementation. Designed to be \ extendable, so new algorithms and sources can be used.