diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml
index 70e5c93..a3838cf 100644
--- a/.github/workflows/release_build.yml
+++ b/.github/workflows/release_build.yml
@@ -39,7 +39,7 @@ jobs:
uses: docker/build-push-action@v3
with:
context: "{{defaultContext}}:docker"
- platforms: linux/amd64
+ platforms: linux/amd64,linux/arm64
file: ./Dockerfile
build-args: |
VERSION=${{ inputs.aprsd_version }}
diff --git a/aprsd/client.py b/aprsd/client.py
index 9c9522d..f110236 100644
--- a/aprsd/client.py
+++ b/aprsd/client.py
@@ -62,6 +62,8 @@ class Client:
"""Call this to force a rebuild/reconnect."""
if self._client:
del self._client
+ else:
+ LOG.warning("Client not initialized, nothing to reset.")
@abc.abstractmethod
def setup_connection(self):
@@ -152,9 +154,10 @@ class APRSISClient(Client):
except LoginError as e:
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
connected = False
- raise e
+ time.sleep(backoff)
except Exception as e:
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
+ connected = False
time.sleep(backoff)
backoff = backoff * 2
continue
diff --git a/aprsd/clients/aprsis.py b/aprsd/clients/aprsis.py
index 5ba53b1..aa1a9ba 100644
--- a/aprsd/clients/aprsis.py
+++ b/aprsd/clients/aprsis.py
@@ -112,23 +112,23 @@ class Aprsdis(aprslib.IS):
self._sendall(login_str)
self.sock.settimeout(5)
test = self.sock.recv(len(login_str) + 100)
+ self.logger.debug("Server: '%s'", test)
if is_py3:
test = test.decode("latin-1")
test = test.rstrip()
- self.logger.debug("Server: %s", test)
+ self.logger.debug("Server: '%s'", test)
- a, b, callsign, status, e = test.split(" ", 4)
+ if not test:
+ raise LoginError(f"Server Response Empty: '{test}'")
+
+ _, _, callsign, status, e = test.split(" ", 4)
s = e.split(",")
if len(s):
server_string = s[0].replace("server ", "")
else:
server_string = e.replace("server ", "")
- self.logger.info(f"Connected to {server_string}")
- self.server_string = server_string
- stats.APRSDStats().set_aprsis_server(server_string)
-
if callsign == "":
raise LoginError("Server responded with empty callsign???")
if callsign != self.callsign:
@@ -141,6 +141,10 @@ class Aprsdis(aprslib.IS):
else:
self.logger.info("Login successful")
+ self.logger.info(f"Connected to {server_string}")
+ self.server_string = server_string
+ stats.APRSDStats().set_aprsis_server(server_string)
+
except LoginError as e:
self.logger.error(str(e))
self.close()
@@ -148,6 +152,7 @@ class Aprsdis(aprslib.IS):
except Exception as e:
self.close()
self.logger.error(f"Failed to login '{e}'")
+ self.logger.exception(e)
raise LoginError("Failed to login")
def consumer(self, callback, blocking=True, immortal=False, raw=False):
diff --git a/aprsd/cmds/webchat.py b/aprsd/cmds/webchat.py
index c0e719d..fbdc87d 100644
--- a/aprsd/cmds/webchat.py
+++ b/aprsd/cmds/webchat.py
@@ -12,7 +12,6 @@ import click
import flask
from flask import request
from flask.logging import default_handler
-import flask_classful
from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from oslo_config import cfg
@@ -31,9 +30,16 @@ from aprsd.utils import objectstore, trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth()
-users = None
+users = {}
socketio = None
+flask_app = flask.Flask(
+ "aprsd",
+ static_url_path="/static",
+ static_folder="web/chat/static",
+ template_folder="web/chat/templates",
+)
+
def signal_handler(sig, frame):
@@ -174,106 +180,108 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
)
-class WebChatFlask(flask_classful.FlaskView):
+def set_config():
+ global users
- def set_config(self):
- global users
- self.users = {}
- user = CONF.admin.user
- self.users[user] = generate_password_hash(CONF.admin.password)
- users = self.users
- def _get_transport(self, stats):
- if CONF.aprs_network.enabled:
- transport = "aprs-is"
- aprs_connection = (
- "APRS-IS Server: "
- "{}".format(stats["stats"]["aprs-is"]["server"])
- )
- else:
- # We might be connected to a KISS socket?
- if client.KISSClient.is_enabled():
- transport = client.KISSClient.transport()
- if transport == client.TRANSPORT_TCPKISS:
- aprs_connection = (
- "TCPKISS://{}:{}".format(
- CONF.kiss_tcp.host,
- CONF.kiss_tcp.port,
- )
- )
- elif transport == client.TRANSPORT_SERIALKISS:
- # for pep8 violation
- aprs_connection = (
- "SerialKISS://{}@{} baud".format(
- CONF.kiss_serial.device,
- CONF.kiss_serial.baudrate,
- ),
- )
-
- return transport, aprs_connection
-
- @auth.login_required
- def index(self):
- ua_str = request.headers.get("User-Agent")
- # this takes about 2 seconds :(
- user_agent = ua_parse(ua_str)
- LOG.debug(f"Is mobile? {user_agent.is_mobile}")
- stats = self._stats()
-
- if user_agent.is_mobile:
- html_template = "mobile.html"
- else:
- html_template = "index.html"
-
- # For development
- # html_template = "mobile.html"
-
- LOG.debug(f"Template {html_template}")
-
- transport, aprs_connection = self._get_transport(stats)
- LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
-
- stats["transport"] = transport
- stats["aprs_connection"] = aprs_connection
- LOG.debug(f"initial stats = {stats}")
-
- return flask.render_template(
- html_template,
- initial_stats=stats,
- aprs_connection=aprs_connection,
- callsign=CONF.callsign,
- version=aprsd.__version__,
+def _get_transport(stats):
+ if CONF.aprs_network.enabled:
+ transport = "aprs-is"
+ aprs_connection = (
+ "APRS-IS Server: "
+ "{}".format(stats["stats"]["aprs-is"]["server"])
)
+ else:
+ # We might be connected to a KISS socket?
+ if client.KISSClient.is_enabled():
+ transport = client.KISSClient.transport()
+ if transport == client.TRANSPORT_TCPKISS:
+ aprs_connection = (
+ "TCPKISS://{}:{}".format(
+ CONF.kiss_tcp.host,
+ CONF.kiss_tcp.port,
+ )
+ )
+ elif transport == client.TRANSPORT_SERIALKISS:
+ # for pep8 violation
+ aprs_connection = (
+ "SerialKISS://{}@{} baud".format(
+ CONF.kiss_serial.device,
+ CONF.kiss_serial.baudrate,
+ ),
+ )
- @auth.login_required
- def send_message_status(self):
- LOG.debug(request)
- msgs = SentMessages()
- info = msgs.get_all()
- return json.dumps(info)
+ return transport, aprs_connection
- def _stats(self):
- stats_obj = stats.APRSDStats()
- now = datetime.datetime.now()
- time_format = "%m-%d-%Y %H:%M:%S"
- stats_dict = stats_obj.stats()
- # Webchat doesnt need these
- del stats_dict["aprsd"]["watch_list"]
- del stats_dict["aprsd"]["seen_list"]
- # del stats_dict["email"]
- # del stats_dict["plugins"]
- # del stats_dict["messages"]
+@auth.login_required
+@flask_app.route("/")
+def index():
+ ua_str = request.headers.get("User-Agent")
+ # this takes about 2 seconds :(
+ user_agent = ua_parse(ua_str)
+ LOG.debug(f"Is mobile? {user_agent.is_mobile}")
+ stats = _stats()
- result = {
- "time": now.strftime(time_format),
- "stats": stats_dict,
- }
+ if user_agent.is_mobile:
+ html_template = "mobile.html"
+ else:
+ html_template = "index.html"
- return result
+ # For development
+ # html_template = "mobile.html"
- def stats(self):
- return json.dumps(self._stats())
+ LOG.debug(f"Template {html_template}")
+
+ transport, aprs_connection = _get_transport(stats)
+ LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
+
+ stats["transport"] = transport
+ stats["aprs_connection"] = aprs_connection
+ LOG.debug(f"initial stats = {stats}")
+
+ return flask.render_template(
+ html_template,
+ initial_stats=stats,
+ aprs_connection=aprs_connection,
+ callsign=CONF.callsign,
+ version=aprsd.__version__,
+ )
+
+
+@auth.login_required
+@flask_app.route("//send-message-status")
+def send_message_status():
+ LOG.debug(request)
+ msgs = SentMessages()
+ info = msgs.get_all()
+ return json.dumps(info)
+
+
+def _stats():
+ stats_obj = stats.APRSDStats()
+ now = datetime.datetime.now()
+
+ time_format = "%m-%d-%Y %H:%M:%S"
+ stats_dict = stats_obj.stats()
+ # Webchat doesnt need these
+ del stats_dict["aprsd"]["watch_list"]
+ del stats_dict["aprsd"]["seen_list"]
+ # del stats_dict["email"]
+ # del stats_dict["plugins"]
+ # del stats_dict["messages"]
+
+ result = {
+ "time": now.strftime(time_format),
+ "stats": stats_dict,
+ }
+
+ return result
+
+
+@flask_app.route("/stats")
+def get_stats():
+ return json.dumps(_stats())
class SendMessageNamespace(Namespace):
@@ -377,21 +385,9 @@ def setup_logging(flask_app, loglevel, quiet):
@trace.trace
def init_flask(loglevel, quiet):
- global socketio
+ global socketio, flask_app
- flask_app = flask.Flask(
- "aprsd",
- static_url_path="/static",
- static_folder="web/chat/static",
- template_folder="web/chat/templates",
- )
setup_logging(flask_app, loglevel, quiet)
- server = WebChatFlask()
- server.set_config()
- flask_app.route("/", methods=["GET"])(server.index)
- flask_app.route("/stats", methods=["GET"])(server.stats)
- # flask_app.route("/send-message", methods=["GET"])(server.send_message)
- flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
socketio = SocketIO(
flask_app, logger=False, engineio_logger=False,
@@ -407,7 +403,7 @@ def init_flask(loglevel, quiet):
"/sendmsg",
),
)
- return socketio, flask_app
+ return socketio
# main() ###
@@ -448,6 +444,8 @@ def webchat(ctx, flush, port):
LOG.info(f"APRSD Started version: {aprsd.__version__}")
CONF.log_opt_values(LOG, logging.DEBUG)
+ user = CONF.admin.user
+ users[user] = generate_password_hash(CONF.admin.password)
# Initialize the client factory and create
# The correct client object ready for use
@@ -466,7 +464,7 @@ def webchat(ctx, flush, port):
packets.WatchList()
packets.SeenList()
- (socketio, app) = init_flask(loglevel, quiet)
+ socketio = init_flask(loglevel, quiet)
rx_thread = rx.APRSDPluginRXThread(
packet_queue=threads.packet_queue,
)
@@ -482,7 +480,7 @@ def webchat(ctx, flush, port):
keepalive.start()
LOG.info("Start socketio.run()")
socketio.run(
- app,
+ flask_app,
ssl_context="adhoc",
host=CONF.admin.web_ip,
port=port,
diff --git a/aprsd/threads/aprsd.py b/aprsd/threads/aprsd.py
index a6f7446..51d7960 100644
--- a/aprsd/threads/aprsd.py
+++ b/aprsd/threads/aprsd.py
@@ -1,4 +1,5 @@
import abc
+import datetime
import logging
import threading
@@ -50,6 +51,7 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
super().__init__(name=name)
self.thread_stop = False
APRSDThreadList().add(self)
+ self._last_loop = datetime.datetime.now()
def _should_quit(self):
""" see if we have a quit message from the global queue."""
@@ -70,10 +72,15 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
out = f"Thread <{self.__class__.__name__}({self.name}) Alive? {self.is_alive()}>"
return out
+ def loop_age(self):
+ """How old is the last loop call?"""
+ return datetime.datetime.now() - self._last_loop
+
def run(self):
LOG.debug("Starting")
while not self._should_quit():
can_loop = self.loop()
+ self._last_loop = datetime.datetime.now()
if not can_loop:
self.stop()
self._cleanup()
diff --git a/aprsd/threads/keep_alive.py b/aprsd/threads/keep_alive.py
index 0aae05c..9ea3282 100644
--- a/aprsd/threads/keep_alive.py
+++ b/aprsd/threads/keep_alive.py
@@ -68,8 +68,13 @@ class KeepAliveThread(APRSDThread):
thread_info = {}
for thread in thread_list.threads_list:
alive = thread.is_alive()
- thread_out.append(f"{thread.__class__.__name__}:{alive}")
- thread_info[thread.__class__.__name__] = alive
+ age = thread.loop_age()
+ key = thread.__class__.__name__
+ thread_out.append(f"{key}:{alive}:{age}")
+ if key not in thread_info:
+ thread_info[key] = {}
+ thread_info[key]["alive"] = alive
+ thread_info[key]["age"] = age
if not alive:
LOG.error(f"Thread {thread}")
LOG.info(",".join(thread_out))
diff --git a/aprsd/wsgi.py b/aprsd/wsgi.py
index c5278f5..17ffbe3 100644
--- a/aprsd/wsgi.py
+++ b/aprsd/wsgi.py
@@ -1,12 +1,337 @@
+import datetime
+import json
import logging
+from logging.handlers import RotatingFileHandler
+import time
+import flask
+from flask import Flask
+from flask.logging import default_handler
+from flask_httpauth import HTTPBasicAuth
+from flask_socketio import Namespace, SocketIO
from oslo_config import cfg
+from werkzeug.security import check_password_hash
-from aprsd import admin_web
-from aprsd import conf # noqa
+import aprsd
+from aprsd import cli_helper, client, conf, packets, plugin, threads
+from aprsd.log import rich as aprsd_logging
+from aprsd.rpc import client as aprsd_rpc_client
CONF = cfg.CONF
-LOG = logging.getLogger("APRSD")
-app = None
-app = admin_web.create_app()
+LOG = logging.getLogger("gunicorn.access")
+
+auth = HTTPBasicAuth()
+users = {}
+app = Flask(
+ "aprsd",
+ static_url_path="/static",
+ static_folder="web/admin/static",
+ template_folder="web/admin/templates",
+)
+socket_io = SocketIO(app)
+
+
+# HTTPBasicAuth doesn't work on a class method.
+# This has to be out here. Rely on the APRSDFlask
+# class to initialize the users from the config
+@auth.verify_password
+def verify_password(username, password):
+ global users
+
+ if username in users and check_password_hash(users.get(username), password):
+ return username
+
+
+def _stats():
+ track = aprsd_rpc_client.RPCClient().get_packet_track()
+ now = datetime.datetime.now()
+
+ time_format = "%m-%d-%Y %H:%M:%S"
+
+ stats_dict = aprsd_rpc_client.RPCClient().get_stats_dict()
+ if not stats_dict:
+ stats_dict = {
+ "aprsd": {},
+ "aprs-is": {"server": ""},
+ "messages": {
+ "sent": 0,
+ "received": 0,
+ },
+ "email": {
+ "sent": 0,
+ "received": 0,
+ },
+ "seen_list": {
+ "sent": 0,
+ "received": 0,
+ },
+ }
+
+ # Convert the watch_list entries to age
+ wl = aprsd_rpc_client.RPCClient().get_watch_list()
+ new_list = {}
+ if wl:
+ for call in wl.get_all():
+ # call_date = datetime.datetime.strptime(
+ # str(wl.last_seen(call)),
+ # "%Y-%m-%d %H:%M:%S.%f",
+ # )
+
+ # We have to convert the RingBuffer to a real list
+ # so that json.dumps works.
+ # pkts = []
+ # for pkt in wl.get(call)["packets"].get():
+ # pkts.append(pkt)
+
+ new_list[call] = {
+ "last": wl.age(call),
+ # "packets": pkts
+ }
+
+ stats_dict["aprsd"]["watch_list"] = new_list
+ packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
+ rx = tx = 0
+ if packet_list:
+ rx = packet_list.total_rx()
+ tx = packet_list.total_tx()
+ stats_dict["packets"] = {
+ "sent": tx,
+ "received": rx,
+ }
+ if track:
+ size_tracker = len(track)
+ else:
+ size_tracker = 0
+
+ result = {
+ "time": now.strftime(time_format),
+ "size_tracker": size_tracker,
+ "stats": stats_dict,
+ }
+
+ return result
+
+
+@app.route("/stats")
+def stats():
+ LOG.debug("/stats called")
+ return json.dumps(_stats())
+
+
+@auth.login_required
+@app.route("/")
+def index():
+ stats = _stats()
+ LOG.debug(stats)
+ wl = aprsd_rpc_client.RPCClient().get_watch_list()
+ if wl and wl.is_enabled():
+ watch_count = len(wl)
+ watch_age = wl.max_delta()
+ else:
+ watch_count = 0
+ watch_age = 0
+
+ sl = aprsd_rpc_client.RPCClient().get_seen_list()
+ if sl:
+ seen_count = len(sl)
+ else:
+ seen_count = 0
+
+ pm = plugin.PluginManager()
+ plugins = pm.get_plugins()
+ plugin_count = len(plugins)
+
+ if CONF.aprs_network.enabled:
+ transport = "aprs-is"
+ aprs_connection = (
+ "APRS-IS Server: "
+ "{}".format(stats["stats"]["aprs-is"]["server"])
+ )
+ else:
+ # We might be connected to a KISS socket?
+ if client.KISSClient.kiss_enabled():
+ transport = client.KISSClient.transport()
+ if transport == client.TRANSPORT_TCPKISS:
+ aprs_connection = (
+ "TCPKISS://{}:{}".format(
+ CONF.kiss_tcp.host,
+ CONF.kiss_tcp.port,
+ )
+ )
+ elif transport == client.TRANSPORT_SERIALKISS:
+ aprs_connection = (
+ "SerialKISS://{}@{} baud".format(
+ CONF.kiss_serial.device,
+ CONF.kiss_serial.baudrate,
+ )
+ )
+
+ stats["transport"] = transport
+ stats["aprs_connection"] = aprs_connection
+ entries = conf.conf_to_dict()
+
+ return flask.render_template(
+ "index.html",
+ initial_stats=stats,
+ aprs_connection=aprs_connection,
+ callsign=CONF.callsign,
+ version=aprsd.__version__,
+ config_json=json.dumps(
+ entries, indent=4,
+ sort_keys=True, default=str,
+ ),
+ watch_count=watch_count,
+ watch_age=watch_age,
+ seen_count=seen_count,
+ plugin_count=plugin_count,
+ )
+
+
+@auth.login_required
+def messages():
+ track = packets.PacketTrack()
+ msgs = []
+ for id in track:
+ LOG.info(track[id].dict())
+ msgs.append(track[id].dict())
+
+ return flask.render_template("messages.html", messages=json.dumps(msgs))
+
+
+@auth.login_required
+@app.route("/packets")
+def get_packets():
+ LOG.debug("/packets called")
+ packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
+ if packet_list:
+ packets = packet_list.get()
+ tmp_list = []
+ for pkt in packets:
+ tmp_list.append(pkt.json)
+
+ return json.dumps(tmp_list)
+ else:
+ return json.dumps([])
+
+
+@auth.login_required
+@app.route("/plugins")
+def plugins():
+ LOG.debug("/plugins called")
+ pm = plugin.PluginManager()
+ pm.reload_plugins()
+
+ return "reloaded"
+
+
+@auth.login_required
+@app.route("/save")
+def save():
+ """Save the existing queue to disk."""
+ track = packets.PacketTrack()
+ track.save()
+ return json.dumps({"messages": "saved"})
+
+
+class LogUpdateThread(threads.APRSDThread):
+
+ def __init__(self):
+ super().__init__("LogUpdate")
+
+ def loop(self):
+ global socket_io
+
+ if socket_io:
+ log_entries = aprsd_rpc_client.RPCClient().get_log_entries()
+
+ if log_entries:
+ for entry in log_entries:
+ socket_io.emit(
+ "log_entry", entry,
+ namespace="/logs",
+ )
+
+ time.sleep(5)
+ return True
+
+
+class LoggingNamespace(Namespace):
+ log_thread = None
+
+ def on_connect(self):
+ global socket_io
+ socket_io.emit(
+ "connected", {"data": "/logs Connected"},
+ namespace="/logs",
+ )
+ self.log_thread = LogUpdateThread()
+ self.log_thread.start()
+
+ def on_disconnect(self):
+ LOG.debug("LOG Disconnected")
+ if self.log_thread:
+ self.log_thread.stop()
+
+
+def setup_logging(flask_app, loglevel):
+ flask_log = logging.getLogger("werkzeug")
+ flask_app.logger.removeHandler(default_handler)
+ flask_log.removeHandler(default_handler)
+ LOG.handlers = []
+
+ log_level = conf.log.LOG_LEVELS[loglevel]
+ LOG.setLevel(log_level)
+ date_format = CONF.logging.date_format
+ flask_app.logger.disabled = True
+ gunicorn_err = logging.getLogger("gunicorn.error")
+
+ if CONF.logging.rich_logging:
+ log_format = "%(message)s"
+ log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
+ rh = aprsd_logging.APRSDRichHandler(
+ show_thread=True, thread_width=15,
+ rich_tracebacks=True, omit_repeated_times=False,
+ )
+ rh.setFormatter(log_formatter)
+ LOG.addHandler(rh)
+
+ log_file = CONF.logging.logfile
+
+ if log_file:
+ log_format = CONF.logging.logformat
+ log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
+ fh = RotatingFileHandler(
+ log_file, maxBytes=(10248576 * 5),
+ backupCount=4,
+ )
+ fh.setFormatter(log_formatter)
+ LOG.addHandler(fh)
+
+ gunicorn_err.handlers = LOG.handlers
+
+
+def init_app(config_file=None, log_level=None):
+ default_config_file = cli_helper.DEFAULT_CONFIG_FILE
+ if not config_file:
+ config_file = default_config_file
+
+ CONF(
+ [], project="aprsd", version=aprsd.__version__,
+ default_config_files=[config_file],
+ )
+
+ if not log_level:
+ log_level = CONF.logging.log_level
+
+ return log_level
+
+
+print(f"APP {__name__}")
+if __name__ == "__main__":
+ socket_io.run(app)
+
+if __name__ == "aprsd.wsgi":
+ log_level = init_app(config_file="~/.config/aprsd/aprsd.conf", log_level="DEBUG")
+ socket_io.on_namespace(LoggingNamespace("/logs"))
+ setup_logging(app, log_level)
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 0f485d9..9dd9368 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,5 +1,5 @@
#
-# This file is autogenerated by pip-compile with Python 3.8
+# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --annotation-style=line dev-requirements.in
@@ -31,13 +31,12 @@ gray==0.13.0 # via -r dev-requirements.in
identify==2.5.24 # via pre-commit
idna==3.4 # via requests
imagesize==1.4.1 # via sphinx
-importlib-metadata==6.8.0 # via sphinx
-importlib-resources==6.0.0 # via fixit, jsonschema, jsonschema-specifications
+importlib-resources==6.0.0 # via fixit
iniconfig==2.0.0 # via pytest
isort==5.12.0 # via -r dev-requirements.in, gray
jinja2==3.1.2 # via sphinx
-jsonschema==4.18.3 # via fixit
-jsonschema-specifications==2023.6.1 # via jsonschema
+jsonschema==4.18.4 # via fixit
+jsonschema-specifications==2023.7.1 # via jsonschema
libcst==1.0.1 # via fixit
markupsafe==2.1.3 # via jinja2
mccabe==0.7.0 # via flake8
@@ -48,7 +47,6 @@ packaging==23.1 # via black, build, pyproject-api, pytest, sphinx, tox
pathspec==0.11.1 # via black
pep8-naming==0.13.3 # via -r dev-requirements.in
pip-tools==7.0.0 # via -r dev-requirements.in
-pkgutil-resolve-name==1.3.10 # via jsonschema
platformdirs==3.9.1 # via black, tox, virtualenv
pluggy==1.2.0 # via pytest, tox
pre-commit==3.3.3 # via -r dev-requirements.in
@@ -59,13 +57,12 @@ pyproject-api==1.5.3 # via tox
pyproject-hooks==1.0.0 # via build
pytest==7.4.0 # via -r dev-requirements.in, pytest-cov
pytest-cov==4.1.0 # via -r dev-requirements.in
-pytz==2023.3 # via babel
pyupgrade==3.9.0 # via gray
-pyyaml==6.0 # via fixit, libcst, pre-commit
-referencing==0.29.1 # via jsonschema, jsonschema-specifications
+pyyaml==6.0.1 # via fixit, libcst, pre-commit
+referencing==0.30.0 # via jsonschema, jsonschema-specifications
requests==2.31.0 # via sphinx
rich==12.6.0 # via gray
-rpds-py==0.8.11 # via jsonschema, referencing
+rpds-py==0.9.2 # via jsonschema, referencing
snowballstemmer==2.2.0 # via sphinx
sphinx==7.0.1 # via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.4 # via sphinx
@@ -78,14 +75,13 @@ tokenize-rt==5.1.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake
tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
tox==4.6.4 # via -r dev-requirements.in
-typing-extensions==4.7.1 # via black, libcst, mypy, rich, typing-inspect
+typing-extensions==4.7.1 # via libcst, mypy, typing-inspect
typing-inspect==0.9.0 # via libcst
unify==0.5 # via gray
untokenize==0.1.1 # via unify
urllib3==2.0.3 # via requests
virtualenv==20.24.0 # via pre-commit, tox
wheel==0.40.0 # via pip-tools
-zipp==3.16.2 # via importlib-metadata, importlib-resources
# The following packages are considered to be unsafe in a requirements file:
# pip
diff --git a/docker/bin/admin.sh b/docker/bin/admin.sh
index 3fb5235..7709d5d 100755
--- a/docker/bin/admin.sh
+++ b/docker/bin/admin.sh
@@ -27,5 +27,6 @@ if [ ! -e "$APRSD_CONFIG" ]; then
fi
export COLUMNS=200
-exec gunicorn -b :8000 --workers 4 "aprsd.admin_web:create_app(config_file='$APRSD_CONFIG', log_level='$LOG_LEVEL')"
+#exec gunicorn -b :8000 --workers 4 "aprsd.admin_web:create_app(config_file='$APRSD_CONFIG', log_level='$LOG_LEVEL')"
+exec gunicorn -b :8000 --workers 4 "aprsd.wsgi:app"
#exec aprsd listen -c $APRSD_CONFIG --loglevel ${LOG_LEVEL} ${APRSD_LOAD_PLUGINS} ${APRSD_LISTEN_FILTER}
diff --git a/requirements.in b/requirements.in
index 766f757..dc69a76 100644
--- a/requirements.in
+++ b/requirements.in
@@ -2,9 +2,8 @@ aprslib>=0.7.0
click
click-params
click-completion
-flask==2.1.2
-werkzeug==2.1.2
-flask-classful
+flask
+werkzeug
flask-httpauth
imapclient
pluggy
diff --git a/requirements.txt b/requirements.txt
index c6200f1..4ab36c4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
#
-# This file is autogenerated by pip-compile with Python 3.8
+# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --annotation-style=line requirements.in
@@ -11,6 +11,7 @@ ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.12.2 # via -r requirements.in
bidict==0.22.1 # via python-socketio
bitarray==2.7.6 # via ax253, kiss3
+blinker==1.6.2 # via flask
certifi==2023.5.7 # via httpcore, requests
cffi==1.15.1 # via cryptography
charset-normalizer==3.2.0 # via requests
@@ -26,8 +27,7 @@ decorator==5.1.1 # via validators
dnspython==2.4.0 # via eventlet
eventlet==0.33.3 # via -r requirements.in
exceptiongroup==1.1.2 # via anyio
-flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio
-flask-classful==0.14.2 # via -r requirements.in
+flask==2.3.2 # via -r requirements.in, flask-httpauth, flask-socketio
flask-httpauth==4.8.0 # via -r requirements.in
flask-socketio==5.3.4 # via -r requirements.in
geographiclib==2.0 # via geopy
@@ -37,11 +37,11 @@ h11==0.14.0 # via httpcore
httpcore==0.17.3 # via dnspython
idna==3.4 # via anyio, requests
imapclient==2.3.1 # via -r requirements.in
-importlib-metadata==6.8.0 # via ax253, flask, kiss3
+importlib-metadata==6.8.0 # via ax253, kiss3
itsdangerous==2.1.2 # via flask
jinja2==3.1.2 # via click-completion, flask
kiss3==8.0.0 # via -r requirements.in
-markupsafe==2.1.3 # via jinja2
+markupsafe==2.1.3 # via jinja2, werkzeug
netaddr==0.8.0 # via oslo-config
oslo-config==9.1.1 # via -r requirements.in
oslo-i18n==6.0.0 # via oslo-config
@@ -56,7 +56,7 @@ pyserial-asyncio==0.6 # via kiss3
python-engineio==4.5.1 # via python-socketio
python-socketio==5.8.0 # via flask-socketio
pytz==2023.3 # via -r requirements.in
-pyyaml==6.0 # via -r requirements.in, oslo-config
+pyyaml==6.0.1 # via -r requirements.in, oslo-config
requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
rfc3986==2.0.0 # via oslo-config
rich==12.6.0 # via -r requirements.in
@@ -69,12 +69,11 @@ soupsieve==2.4.1 # via beautifulsoup4
stevedore==5.1.0 # via oslo-config
tabulate==0.9.0 # via -r requirements.in
thesmuggler==1.0.1 # via -r requirements.in
-typing-extensions==4.7.1 # via rich
ua-parser==0.18.0 # via user-agents
update-checker==0.18.0 # via -r requirements.in
urllib3==2.0.3 # via requests
user-agents==2.2.0 # via -r requirements.in
validators==0.20.0 # via click-params
-werkzeug==2.1.2 # via -r requirements.in, flask
+werkzeug==2.3.6 # via -r requirements.in, flask
wrapt==1.15.0 # via -r requirements.in, debtcollector
zipp==3.16.2 # via importlib-metadata
diff --git a/tests/cmds/test_webchat.py b/tests/cmds/test_webchat.py
index 3a0103d..53deb0d 100644
--- a/tests/cmds/test_webchat.py
+++ b/tests/cmds/test_webchat.py
@@ -39,9 +39,9 @@ class TestSendMessageCommand(unittest.TestCase):
CliRunner()
self.config_and_init()
- socketio, flask_app = webchat.init_flask("DEBUG", False)
+ socketio = webchat.init_flask("DEBUG", False)
self.assertIsInstance(socketio, flask_socketio.SocketIO)
- self.assertIsInstance(flask_app, flask.Flask)
+ self.assertIsInstance(webchat.flask_app, flask.Flask)
@mock.patch("aprsd.packets.tracker.PacketTrack.remove")
@mock.patch("aprsd.cmds.webchat.socketio")