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.
This commit is contained in:
Hemna 2021-02-18 16:31:52 -05:00
parent 9f66774541
commit e6cafeb3d2
5 changed files with 87 additions and 3 deletions

View File

@ -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

View File

@ -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."""

45
aprsd/plugins/stock.py Normal file
View File

@ -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()

View File

@ -15,3 +15,4 @@ opencage
flask
flask-classful
flask-httpauth
yfinance

View File

@ -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