From e6cafeb3d27f656ffd0da765db9cb5be3c95c035 Mon Sep 17 00:00:00 2001 From: Hemna Date: Thu, 18 Feb 2021 16:31:52 -0500 Subject: [PATCH] Added plugin live reload and StockPlugin This patch adds 2 items. First it adds the new StockPlugin, which fetches stock quotes from yahoo finance rest API using the yfinance python module. 2nd, the web interface contains a new url /plugins, which allows aprsd to reload all of it's plugins from disk. This is useful for development where the dev is editing an existing plugin and wants to run the edited plugin without restarting aprsd itself. The /plugins url requires admin login credentials. TODO: would be nice to live reload the aprsd.yml config file, so plugin reloading can start new plugins defined in aprsd.yml between /plugins being reloaded. --- aprsd/flask.py | 10 +++++++++- aprsd/plugin.py | 14 ++++++++++++- aprsd/plugins/stock.py | 45 ++++++++++++++++++++++++++++++++++++++++++ requirements.in | 1 + requirements.txt | 20 ++++++++++++++++++- 5 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 aprsd/plugins/stock.py diff --git a/aprsd/flask.py b/aprsd/flask.py index b49c862..d6136b2 100644 --- a/aprsd/flask.py +++ b/aprsd/flask.py @@ -2,7 +2,7 @@ import json import logging import aprsd -from aprsd import messaging, stats +from aprsd import messaging, plugin, stats import flask import flask_classful from flask_httpauth import HTTPBasicAuth @@ -53,6 +53,13 @@ class APRSDFlask(flask_classful.FlaskView): return flask.render_template("messages.html", messages=json.dumps(msgs)) + @auth.login_required + def plugins(self): + pm = plugin.PluginManager() + pm.reload_plugins() + + return "reloaded" + @auth.login_required def save(self): """Save the existing queue to disk.""" @@ -86,4 +93,5 @@ def init_flask(config): flask_app.route("/stats", methods=["GET"])(server.stats) flask_app.route("/messages", methods=["GET"])(server.messages) flask_app.route("/save", methods=["GET"])(server.save) + flask_app.route("/plugins", methods=["GET"])(server.plugins) return flask_app diff --git a/aprsd/plugin.py b/aprsd/plugin.py index bd7182a..10ed29a 100644 --- a/aprsd/plugin.py +++ b/aprsd/plugin.py @@ -6,6 +6,7 @@ import inspect import logging import os import re +import threading import pluggy from thesmuggler import smuggle @@ -22,6 +23,7 @@ CORE_PLUGINS = [ "aprsd.plugins.location.LocationPlugin", "aprsd.plugins.ping.PingPlugin", "aprsd.plugins.query.QueryPlugin", + "aprsd.plugins.stock.StockPlugin", "aprsd.plugins.time.TimePlugin", "aprsd.plugins.weather.USWeatherPlugin", "aprsd.plugins.version.VersionPlugin", @@ -82,11 +84,14 @@ class PluginManager: # aprsd config dict config = None + lock = None + def __new__(cls, *args, **kwargs): """This magic turns this into a singleton.""" if cls._instance is None: cls._instance = super().__new__(cls) # Put any initialization here. + cls._instance.lock = threading.Lock() return cls._instance def __init__(self, config=None): @@ -135,6 +140,7 @@ class PluginManager: module_name, class_name = module_class_string.rsplit(".", 1) try: module = importlib.import_module(module_name) + module = importlib.reload(module) except Exception as ex: LOG.error("Failed to load Plugin '{}' : '{}'".format(module_name, ex)) return @@ -180,6 +186,11 @@ class PluginManager: except Exception as ex: LOG.exception("Couldn't load plugin '{}'".format(plugin_name), ex) + def reload_plugins(self): + with self.lock: + del self._pluggy_pm + self.setup_plugins() + def setup_plugins(self): """Create the plugin manager and register plugins.""" @@ -223,7 +234,8 @@ class PluginManager: def run(self, *args, **kwargs): """Execute all the pluguns run method.""" - return self._pluggy_pm.hook.run(*args, **kwargs) + with self.lock: + return self._pluggy_pm.hook.run(*args, **kwargs) def register(self, obj): """Register the plugin.""" diff --git a/aprsd/plugins/stock.py b/aprsd/plugins/stock.py new file mode 100644 index 0000000..4d3b20f --- /dev/null +++ b/aprsd/plugins/stock.py @@ -0,0 +1,45 @@ +import logging +import re + +from aprsd import plugin, trace +import yfinance as yf + +LOG = logging.getLogger("APRSD") + + +class StockPlugin(plugin.APRSDPluginBase): + """Stock market plugin for fetching stock quotes""" + + version = "1.0" + command_regex = "^[sS]" + command_name = "stock" + + @trace.trace + def command(self, fromcall, message, ack): + LOG.info("StockPlugin") + + a = re.search(r"^.*\s+(.*)", message) + if a is not None: + searchcall = a.group(1) + stock_symbol = searchcall.upper() + else: + reply = "No stock symbol" + return reply + + LOG.info("Fetch stock quote for '{}'".format(stock_symbol)) + + try: + stock = yf.Ticker(stock_symbol) + reply = "{} - ask: {} high: {} low: {}".format( + stock_symbol, + stock.info["ask"], + stock.info["dayHigh"], + stock.info["dayLow"], + ) + except Exception as e: + LOG.error( + "Failed to fetch stock '{}' from yahoo '{}'".format(stock_symbol, e), + ) + reply = "Failed to fetch stock '{}'".format(stock_symbol) + + return reply.rstrip() diff --git a/requirements.in b/requirements.in index b4775ee..f9e9ba7 100644 --- a/requirements.in +++ b/requirements.in @@ -15,3 +15,4 @@ opencage flask flask-classful flask-httpauth +yfinance diff --git a/requirements.txt b/requirements.txt index 5c91211..98e044c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,12 +58,22 @@ jinja2==2.11.2 # via # click-completion # flask +lxml==4.6.2 + # via yfinance markupsafe==1.1.1 # via jinja2 +multitasking==0.0.9 + # via yfinance nodeenv==1.5.0 # via pre-commit +numpy==1.20.1 + # via + # pandas + # yfinance opencage==1.2.2 # via -r requirements.in +pandas==1.2.2 + # via yfinance pbr==5.5.1 # via -r requirements.in pluggy==0.13.1 @@ -76,8 +86,12 @@ pycparser==2.20 # via cffi pyopenssl==20.0.1 # via opencage +python-dateutil==2.8.1 + # via pandas pytz==2020.5 - # via -r requirements.in + # via + # -r requirements.in + # pandas pyyaml==5.4.1 # via # -r requirements.in @@ -86,6 +100,7 @@ requests==2.25.1 # via # -r requirements.in # opencage + # yfinance shellingham==1.3.2 # via click-completion six==1.15.0 @@ -96,6 +111,7 @@ six==1.15.0 # imapclient # opencage # pyopenssl + # python-dateutil # virtualenv thesmuggler==1.0.1 # via -r requirements.in @@ -107,3 +123,5 @@ virtualenv==20.4.0 # via pre-commit werkzeug==1.0.1 # via flask +yfinance==0.1.55 + # via -r requirements.in