mirror of
https://github.com/craigerl/aprsd.git
synced 2025-06-24 21:15:18 -04:00
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:
parent
9f66774541
commit
e6cafeb3d2
@ -2,7 +2,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import messaging, stats
|
from aprsd import messaging, plugin, stats
|
||||||
import flask
|
import flask
|
||||||
import flask_classful
|
import flask_classful
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
@ -53,6 +53,13 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
|
|
||||||
return flask.render_template("messages.html", messages=json.dumps(msgs))
|
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
|
@auth.login_required
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save the existing queue to disk."""
|
"""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("/stats", methods=["GET"])(server.stats)
|
||||||
flask_app.route("/messages", methods=["GET"])(server.messages)
|
flask_app.route("/messages", methods=["GET"])(server.messages)
|
||||||
flask_app.route("/save", methods=["GET"])(server.save)
|
flask_app.route("/save", methods=["GET"])(server.save)
|
||||||
|
flask_app.route("/plugins", methods=["GET"])(server.plugins)
|
||||||
return flask_app
|
return flask_app
|
||||||
|
@ -6,6 +6,7 @@ import inspect
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
|
|
||||||
import pluggy
|
import pluggy
|
||||||
from thesmuggler import smuggle
|
from thesmuggler import smuggle
|
||||||
@ -22,6 +23,7 @@ CORE_PLUGINS = [
|
|||||||
"aprsd.plugins.location.LocationPlugin",
|
"aprsd.plugins.location.LocationPlugin",
|
||||||
"aprsd.plugins.ping.PingPlugin",
|
"aprsd.plugins.ping.PingPlugin",
|
||||||
"aprsd.plugins.query.QueryPlugin",
|
"aprsd.plugins.query.QueryPlugin",
|
||||||
|
"aprsd.plugins.stock.StockPlugin",
|
||||||
"aprsd.plugins.time.TimePlugin",
|
"aprsd.plugins.time.TimePlugin",
|
||||||
"aprsd.plugins.weather.USWeatherPlugin",
|
"aprsd.plugins.weather.USWeatherPlugin",
|
||||||
"aprsd.plugins.version.VersionPlugin",
|
"aprsd.plugins.version.VersionPlugin",
|
||||||
@ -82,11 +84,14 @@ class PluginManager:
|
|||||||
# aprsd config dict
|
# aprsd config dict
|
||||||
config = None
|
config = None
|
||||||
|
|
||||||
|
lock = None
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
"""This magic turns this into a singleton."""
|
"""This magic turns this into a singleton."""
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
# Put any initialization here.
|
# Put any initialization here.
|
||||||
|
cls._instance.lock = threading.Lock()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self, config=None):
|
def __init__(self, config=None):
|
||||||
@ -135,6 +140,7 @@ class PluginManager:
|
|||||||
module_name, class_name = module_class_string.rsplit(".", 1)
|
module_name, class_name = module_class_string.rsplit(".", 1)
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
|
module = importlib.reload(module)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error("Failed to load Plugin '{}' : '{}'".format(module_name, ex))
|
LOG.error("Failed to load Plugin '{}' : '{}'".format(module_name, ex))
|
||||||
return
|
return
|
||||||
@ -180,6 +186,11 @@ class PluginManager:
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.exception("Couldn't load plugin '{}'".format(plugin_name), 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):
|
def setup_plugins(self):
|
||||||
"""Create the plugin manager and register plugins."""
|
"""Create the plugin manager and register plugins."""
|
||||||
|
|
||||||
@ -223,7 +234,8 @@ class PluginManager:
|
|||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
"""Execute all the pluguns run method."""
|
"""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):
|
def register(self, obj):
|
||||||
"""Register the plugin."""
|
"""Register the plugin."""
|
||||||
|
45
aprsd/plugins/stock.py
Normal file
45
aprsd/plugins/stock.py
Normal 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()
|
@ -15,3 +15,4 @@ opencage
|
|||||||
flask
|
flask
|
||||||
flask-classful
|
flask-classful
|
||||||
flask-httpauth
|
flask-httpauth
|
||||||
|
yfinance
|
||||||
|
@ -58,12 +58,22 @@ jinja2==2.11.2
|
|||||||
# via
|
# via
|
||||||
# click-completion
|
# click-completion
|
||||||
# flask
|
# flask
|
||||||
|
lxml==4.6.2
|
||||||
|
# via yfinance
|
||||||
markupsafe==1.1.1
|
markupsafe==1.1.1
|
||||||
# via jinja2
|
# via jinja2
|
||||||
|
multitasking==0.0.9
|
||||||
|
# via yfinance
|
||||||
nodeenv==1.5.0
|
nodeenv==1.5.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
|
numpy==1.20.1
|
||||||
|
# via
|
||||||
|
# pandas
|
||||||
|
# yfinance
|
||||||
opencage==1.2.2
|
opencage==1.2.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
pandas==1.2.2
|
||||||
|
# via yfinance
|
||||||
pbr==5.5.1
|
pbr==5.5.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pluggy==0.13.1
|
pluggy==0.13.1
|
||||||
@ -76,8 +86,12 @@ pycparser==2.20
|
|||||||
# via cffi
|
# via cffi
|
||||||
pyopenssl==20.0.1
|
pyopenssl==20.0.1
|
||||||
# via opencage
|
# via opencage
|
||||||
|
python-dateutil==2.8.1
|
||||||
|
# via pandas
|
||||||
pytz==2020.5
|
pytz==2020.5
|
||||||
# via -r requirements.in
|
# via
|
||||||
|
# -r requirements.in
|
||||||
|
# pandas
|
||||||
pyyaml==5.4.1
|
pyyaml==5.4.1
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
@ -86,6 +100,7 @@ requests==2.25.1
|
|||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# opencage
|
# opencage
|
||||||
|
# yfinance
|
||||||
shellingham==1.3.2
|
shellingham==1.3.2
|
||||||
# via click-completion
|
# via click-completion
|
||||||
six==1.15.0
|
six==1.15.0
|
||||||
@ -96,6 +111,7 @@ six==1.15.0
|
|||||||
# imapclient
|
# imapclient
|
||||||
# opencage
|
# opencage
|
||||||
# pyopenssl
|
# pyopenssl
|
||||||
|
# python-dateutil
|
||||||
# virtualenv
|
# virtualenv
|
||||||
thesmuggler==1.0.1
|
thesmuggler==1.0.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
@ -107,3 +123,5 @@ virtualenv==20.4.0
|
|||||||
# via pre-commit
|
# via pre-commit
|
||||||
werkzeug==1.0.1
|
werkzeug==1.0.1
|
||||||
# via flask
|
# via flask
|
||||||
|
yfinance==0.1.55
|
||||||
|
# via -r requirements.in
|
||||||
|
Loading…
x
Reference in New Issue
Block a user