diff --git a/aprsd/client.py b/aprsd/client.py index a2d5e45..dd1e248 100644 --- a/aprsd/client.py +++ b/aprsd/client.py @@ -38,6 +38,11 @@ class Client: if config: self.config = config + def new(self): + obj = super().__new__(Client) + obj.config = self.config + return obj + @property def client(self): if not self.aprs_client: @@ -118,15 +123,22 @@ class Aprsdis(aprslib.IS): self.select_timeout, ) if not readable: - continue + if not blocking: + break + else: + continue try: short_buf = self.sock.recv(4096) # sock.recv returns empty if the connection drops if not short_buf: - self.logger.error("socket.recv(): returned empty") - raise aprslib.ConnectionDrop("connection dropped") + if not blocking: + # We could just not be blocking, so empty is expected + continue + else: + self.logger.error("socket.recv(): returned empty") + raise aprslib.ConnectionDrop("connection dropped") except OSError as e: # self.logger.error("socket error on recv(): %s" % str(e)) if "Resource temporarily unavailable" in str(e): @@ -215,7 +227,7 @@ class Aprsdis(aprslib.IS): line = b"" - while True: + while True and not self.thread_stop: try: for line in self._socket_readlines(blocking): if line[0:1] != b"#": diff --git a/aprsd/flask.py b/aprsd/flask.py index e850899..bf4a81c 100644 --- a/aprsd/flask.py +++ b/aprsd/flask.py @@ -4,14 +4,22 @@ import logging from logging import NullHandler from logging.handlers import RotatingFileHandler import sys +import threading +import time +import aprslib +from aprslib.exceptions import LoginError import flask +from flask import request import flask_classful from flask_httpauth import HTTPBasicAuth +from flask_socketio import Namespace, SocketIO from werkzeug.security import check_password_hash, generate_password_hash import aprsd -from aprsd import kissclient, messaging, packets, plugin, stats, utils +from aprsd import ( + client, kissclient, messaging, packets, plugin, stats, threads, utils, +) LOG = logging.getLogger("APRSD") @@ -20,6 +28,72 @@ auth = HTTPBasicAuth() users = None +class SentMessages: + _instance = None + lock = None + + msgs = {} + + 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.lock = threading.Lock() + return cls._instance + + def add(self, msg): + with self.lock: + self.msgs[msg.id] = self._create(msg.id) + self.msgs[msg.id]["from"] = msg.fromcall + self.msgs[msg.id]["to"] = msg.tocall + self.msgs[msg.id]["message"] = msg.message.rstrip("\n") + self.msgs[msg.id]["raw"] = str(msg).rstrip("\n") + + def _create(self, id): + return { + "id": id, + "ts": time.time(), + "ack": False, + "from": None, + "to": None, + "raw": None, + "message": None, + "status": None, + "last_update": None, + "reply": None, + } + + def __len__(self): + with self.lock: + return len(self.msgs.keys()) + + def get(self, id): + with self.lock: + if id in self.msgs: + return self.msgs[id] + + def get_all(self): + with self.lock: + return self.msgs + + def set_status(self, id, status): + with self.lock: + self.msgs[id]["last_update"] = str(datetime.datetime.now()) + self.msgs[id]["status"] = status + + def ack(self, id): + """The message got an ack!""" + with self.lock: + self.msgs[id]["last_update"] = str(datetime.datetime.now()) + self.msgs[id]["ack"] = True + + def reply(self, id, packet): + """We got a packet back from the sent message.""" + with self.lock: + self.msgs[id]["reply"] = packet + + # 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 @@ -31,6 +105,172 @@ def verify_password(username, password): return username +class SendMessageThread(threads.APRSDThread): + """Thread for sending a message from web.""" + + aprsis_client = None + request = None + got_ack = False + got_reply = False + + def __init__(self, config, info, msg, namespace): + self.config = config + self.request = info + self.msg = msg + self.namespace = namespace + self.start_time = datetime.datetime.now() + msg = "({} -> {}) : {}".format( + info["from"], + info["to"], + info["message"], + ) + super().__init__(f"WEB_SEND_MSG-{msg}") + + def setup_connection(self): + user = self.request["from"] + password = self.request["password"] + host = self.config["aprs"].get("host", "rotate.aprs.net") + port = self.config["aprs"].get("port", 14580) + connected = False + backoff = 1 + while not connected: + try: + LOG.info("Creating aprslib client") + aprs_client = client.Aprsdis( + user, + passwd=password, + host=host, + port=port, + ) + # Force the logging to be the same + aprs_client.logger = LOG + aprs_client.connect() + connected = True + backoff = 1 + except LoginError as e: + LOG.error(f"Failed to login to APRS-IS Server '{e}'") + connected = False + raise e + except Exception as e: + LOG.error(f"Unable to connect to APRS-IS server. '{e}' ") + time.sleep(backoff) + backoff = backoff * 2 + continue + LOG.debug(f"Logging in to APRS-IS with user '{user}'") + return aprs_client + + def run(self): + LOG.debug("Starting") + from_call = self.request["from"] + self.request["password"] + to_call = self.request["to"] + message = self.request["message"] + LOG.info( + "From: '{}' To: '{}' Send '{}'".format( + from_call, + to_call, + message, + ), + ) + + try: + self.aprs_client = self.setup_connection() + except LoginError as e: + f"Failed to setup Connection {e}" + + self.msg.send_direct(aprsis_client=self.aprs_client) + SentMessages().set_status(self.msg.id, "Sent") + + while not self.thread_stop: + can_loop = self.loop() + if not can_loop: + self.stop() + threads.APRSDThreadList().remove(self) + LOG.debug("Exiting") + + def rx_packet(self, packet): + global socketio + # LOG.debug("Got packet back {}".format(packet)) + resp = packet.get("response", None) + if resp == "ack": + ack_num = packet.get("msgNo") + LOG.info(f"We got ack for our sent message {ack_num}") + messaging.log_packet(packet) + SentMessages().ack(self.msg.id) + socketio.emit( + "ack", SentMessages().get(self.msg.id), + namespace="/ws", + ) + stats.APRSDStats().ack_rx_inc() + self.got_ack = True + if self.request["wait_reply"] == "0" or self.got_reply: + # We aren't waiting for a reply, so we can bail + self.stop() + self.thread_stop = self.aprs_client.thread_stop = True + else: + packets.PacketList().add(packet) + stats.APRSDStats().msgs_rx_inc() + message = packet.get("message_text", None) + fromcall = packet["from"] + msg_number = packet.get("msgNo", "0") + messaging.log_message( + "Received Message", + packet["raw"], + message, + fromcall=fromcall, + ack=msg_number, + ) + SentMessages().reply(self.msg.id, packet) + SentMessages().set_status(self.msg.id, "Got Reply") + socketio.emit( + "reply", SentMessages().get(self.msg.id), + namespace="/ws", + ) + + # Send the ack back? + ack = messaging.AckMessage( + self.request["from"], + fromcall, + msg_id=msg_number, + ) + ack.send_direct() + SentMessages().set_status(self.msg.id, "Ack Sent") + + # Now we can exit, since we are done. + self.got_reply = True + if self.got_ack: + self.stop() + self.thread_stop = self.aprs_client.thread_stop = True + + def loop(self): + # we have a general time limit expecting results of + # around 120 seconds before we exit + now = datetime.datetime.now() + start_delta = str(now - self.start_time) + delta = utils.parse_delta_str(start_delta) + d = datetime.timedelta(**delta) + max_timeout = {"hours": 0.0, "minutes": 1, "seconds": 0} + max_delta = datetime.timedelta(**max_timeout) + if d > max_delta: + LOG.error("XXXXXX Haven't completed everything in 60 seconds. BAIL!") + return False + + if self.got_ack and self.got_reply: + LOG.warning("We got everything already. BAIL") + return False + + try: + # This will register a packet consumer with aprslib + # When new packets come in the consumer will process + # the packet + self.aprs_client.consumer(self.rx_packet, raw=False, blocking=False) + except aprslib.exceptions.ConnectionDrop: + LOG.error("Connection dropped.") + return False + + return True + + class APRSDFlask(flask_classful.FlaskView): config = None @@ -115,6 +355,23 @@ class APRSDFlask(flask_classful.FlaskView): return flask.render_template("messages.html", messages=json.dumps(msgs)) + @auth.login_required + def send_message_status(self): + LOG.debug(request) + msgs = SentMessages() + info = msgs.get_all() + return json.dumps(info) + + @auth.login_required + def send_message(self): + LOG.debug(request) + if request.method == "GET": + return flask.render_template( + "send-message.html", + callsign=self.config["aprs"]["login"], + version=aprsd.__version__, + ) + @auth.login_required def packets(self): packet_list = packets.PacketList().get() @@ -177,6 +434,59 @@ class APRSDFlask(flask_classful.FlaskView): return json.dumps(self._stats()) +class SendMessageNamespace(Namespace): + _config = None + got_ack = False + reply_sent = False + msg = None + request = None + + def __init__(self, namespace=None, config=None): + self._config = config + super().__init__(namespace) + + def on_connect(self): + global socketio + LOG.debug("Web socket connected") + socketio.emit( + "connected", {"data": "Lets dance"}, + namespace="/ws", + ) + + def on_disconnect(self): + LOG.debug("WS Disconnected") + + def on_send(self, data): + global socketio + LOG.debug(f"WS: on_send {data}") + self.request = data + msg = messaging.TextMessage( + data["from"], data["to"], + data["message"], + ) + self.msg = msg + msgs = SentMessages() + msgs.add(msg) + msgs.set_status(msg.id, "Sending") + socketio.emit( + "sent", SentMessages().get(self.msg.id), + namespace="/ws", + ) + + socketio.start_background_task(self._start, self._config, data, msg, self) + LOG.warning("WS: on_send: exit") + + def _start(self, config, data, msg, namespace): + msg_thread = SendMessageThread(self._config, data, msg, self) + msg_thread.start() + + def handle_message(self, data): + LOG.debug(f"WS Data {data}") + + def handle_json(self, data): + LOG.debug(f"WS json {data}") + + def setup_logging(config, flask_app, loglevel, quiet): flask_log = logging.getLogger("werkzeug") @@ -211,6 +521,8 @@ def setup_logging(config, flask_app, loglevel, quiet): def init_flask(config, loglevel, quiet): + global socketio + flask_app = flask.Flask( "aprsd", static_url_path="/static", @@ -224,6 +536,17 @@ def init_flask(config, loglevel, quiet): flask_app.route("/stats", methods=["GET"])(server.stats) flask_app.route("/messages", methods=["GET"])(server.messages) flask_app.route("/packets", methods=["GET"])(server.packets) + flask_app.route("/send-message", methods=["GET"])(server.send_message) + flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status) flask_app.route("/save", methods=["GET"])(server.save) flask_app.route("/plugins", methods=["GET"])(server.plugins) - return flask_app + + socketio = SocketIO( + flask_app, logger=False, engineio_logger=False, + async_mode="threading", + ) + # import eventlet + # eventlet.monkey_patch() + + socketio.on_namespace(SendMessageNamespace("/ws", config=config)) + return socketio, flask_app diff --git a/aprsd/main.py b/aprsd/main.py index bd9a711..30b2d84 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -513,8 +513,9 @@ def server( if web_enabled: flask_enabled = True - app = flask.init_flask(config, loglevel, quiet) - app.run( + (socketio, app) = flask.init_flask(config, loglevel, quiet) + socketio.run( + app, host=config["aprsd"]["web"]["host"], port=config["aprsd"]["web"]["port"], ) diff --git a/aprsd/messaging.py b/aprsd/messaging.py index f53ce0e..0fe54f6 100644 --- a/aprsd/messaging.py +++ b/aprsd/messaging.py @@ -299,7 +299,7 @@ class RawMessage(Message): thread = SendMessageThread(message=self) thread.start() - def send_direct(self): + def send_direct(self, aprsis_client=None): """Send a message without a separate thread.""" cl = self.get_transport() log_message( @@ -379,7 +379,7 @@ class TextMessage(Message): thread = SendMessageThread(message=self) thread.start() - def send_direct(self): + def send_direct(self, aprsis_client=None): """Send a message without a separate thread.""" cl = self.get_transport() log_message( @@ -391,6 +391,7 @@ class TextMessage(Message): ) cl.send(self) stats.APRSDStats().msgs_tx_inc() + packets.PacketList().add(self.dict()) class SendMessageThread(threads.APRSDThread): @@ -498,7 +499,7 @@ class AckMessage(Message): thread = SendAckThread(self) thread.start() - def send_direct(self): + def send_direct(self, aprsis_client=None): """Send an ack message without a separate thread.""" cl = self.get_transport() log_message( diff --git a/aprsd/packets.py b/aprsd/packets.py index ef5e3f5..8e80432 100644 --- a/aprsd/packets.py +++ b/aprsd/packets.py @@ -69,6 +69,7 @@ class WatchList: _instance = None callsigns = {} + config = None def __new__(cls, *args, **kwargs): if cls._instance is None: @@ -97,7 +98,7 @@ class WatchList: } def is_enabled(self): - if "watch_list" in self.config["aprsd"]: + if self.config and "watch_list" in self.config["aprsd"]: return self.config["aprsd"]["watch_list"].get("enabled", False) else: return False diff --git a/aprsd/web/static/js/send-message.js b/aprsd/web/static/js/send-message.js new file mode 100644 index 0000000..812cafc --- /dev/null +++ b/aprsd/web/static/js/send-message.js @@ -0,0 +1,145 @@ +msgs_list = {}; +var cleared = false; + +function size_dict(d){c=0; for (i in d) ++c; return c} + +function init_messages() { + const socket = io("/ws"); + socket.on('connect', function () { + console.log("Connected to socketio"); + }); + socket.on('connected', function(msg) { + console.log("Connected!"); + console.log(msg); + }); + + socket.on("sent", function(msg) { + if (cleared == false) { + var msgsdiv = $("#msgsDiv"); + msgsdiv.html('') + cleared = true + } + add_msg(msg); + }); + + socket.on("ack", function(msg) { + update_msg(msg); + }); + socket.on("reply", function(msg) { + update_msg(msg); + }); + + $("#sendform").submit(function(event) { + event.preventDefault(); + + var $checkboxes = $(this).find('input[type=checkbox]'); + + //loop through the checkboxes and change to hidden fields + $checkboxes.each(function() { + if ($(this)[0].checked) { + $(this).attr('type', 'hidden'); + $(this).val(1); + } else { + $(this).attr('type', 'hidden'); + $(this).val(0); + } + }); + + msg = {'from': $('#from').val(), + 'password': $('#password').val(), + 'to': $('#to').val(), + 'message': $('#message').val(), + 'wait_reply': $('#wait_reply').val(), + } + + socket.emit("send", msg); + + //loop through the checkboxes and change to hidden fields + $checkboxes.each(function() { + $(this).attr('type', 'checkbox'); + }); + }); +} + +function add_msg(msg) { + var msgsdiv = $("#msgsDiv"); + + ts_str = msg["ts"].toString(); + ts = ts_str.split(".")[0]*1000; + var d = new Date(ts).toLocaleDateString("en-US") + var t = new Date(ts).toLocaleTimeString("en-US") + + from = msg['from'] + title_id = 'title_tx' + var from_to = d + " " + t + "    " + from + " > " + + if (msg.hasOwnProperty('to')) { + from_to = from_to + msg['to'] + } + from_to = from_to + "  -  " + msg['message'] + + id = ts_str.split('.')[0] + pretty_id = "pretty_" + id + loader_id = "loader_" + id + ack_id = "ack_" + id + reply_id = "reply_" + id + span_id = "span_" + id + json_pretty = Prism.highlight(JSON.stringify(msg, null, '\t'), Prism.languages.json, 'json'); + msg_html = '
'; + msg_html += '
 '; + msg_html += ' '; + msg_html += ' '; + msg_html += '' + from_to +'
'; + msg_html += '
' + json_pretty + '

' + msgsdiv.prepend(msg_html); + $('.ui.accordion').accordion('refresh'); +} + +function update_msg(msg) { + var msgsdiv = $("#msgsDiv"); + // We have an existing entry + ts_str = msg["ts"].toString(); + id = ts_str.split('.')[0] + pretty_id = "pretty_" + id + loader_id = "loader_" + id + reply_id = "reply_" + id + ack_id = "ack_" + id + span_id = "span_" + id + + + + if (msg['ack'] == true) { + var loader_div = $('#' + loader_id); + var ack_div = $('#' + ack_id); + loader_div.removeClass('ui active inline loader'); + loader_div.addClass('ui disabled loader'); + ack_div.removeClass('thumbs up outline icon'); + ack_div.addClass('thumbs up outline icon'); + } + + if (msg['reply'] !== null) { + var reply_div = $('#' + reply_id); + reply_div.removeClass("thumbs down outline icon"); + reply_div.addClass('reply icon'); + reply_div.attr('data-content', 'Got Reply'); + + var d = new Date(ts).toLocaleDateString("en-US") + var t = new Date(ts).toLocaleTimeString("en-US") + var from_to = d + " " + t + "    " + from + " > " + + if (msg.hasOwnProperty('to')) { + from_to = from_to + msg['to'] + } + from_to = from_to + "  -  " + msg['message'] + from_to += "   ===> " + msg["reply"]["message_text"] + + var span_div = $('#' + span_id); + span_div.html(from_to); + } + + var pretty_pre = $("#" + pretty_id); + pretty_pre.html(''); + json_pretty = Prism.highlight(JSON.stringify(msg, null, '\t'), Prism.languages.json, 'json'); + pretty_pre.html(json_pretty); + $('.ui.accordion').accordion('refresh'); +} diff --git a/aprsd/web/templates/send-message.html b/aprsd/web/templates/send-message.html new file mode 100644 index 0000000..566eaba --- /dev/null +++ b/aprsd/web/templates/send-message.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

APRSD {{ version }}

+
+ +
+
+ {{ callsign }} + connected to + NONE +
+ +
+ NONE +
+
+ +

Send Message Form

+
+

+

+

+

+ +

+

+ +

+

+ +

+ +

+ + +
+ +

Messages (0)

+
+
Messages
+
+ + + + + diff --git a/dev-requirements.in b/dev-requirements.in index e12700c..e55ac9e 100644 --- a/dev-requirements.in +++ b/dev-requirements.in @@ -1,12 +1,12 @@ flake8 isort mypy -pytest -pytest-cov pep8-naming Sphinx tox twine pre-commit pip-tools +pytest +pytest-cov gray diff --git a/dev-requirements.txt b/dev-requirements.txt index e3141af..c709889 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.8 # To update, run: # # pip-compile dev-requirements.in @@ -9,10 +9,8 @@ add-trailing-comma==2.1.0 alabaster==0.7.12 # via sphinx appdirs==1.4.4 - # via - # black - # virtualenv -attrs==20.3.0 + # via black +attrs==21.2.0 # via # jsonschema # pytest @@ -20,17 +18,19 @@ autoflake==1.4 # via gray babel==2.9.1 # via sphinx +backports.entry-points-selectable==1.1.0 + # via virtualenv black==21.7b0 # via gray -bleach==3.3.0 +bleach==4.1.0 # via readme-renderer -certifi==2020.12.5 +certifi==2021.5.30 # via requests -cfgv==3.2.0 +cfgv==3.3.1 # via pre-commit -chardet==4.0.0 +charset-normalizer==2.0.4 # via requests -click==7.1.2 +click==8.0.1 # via # black # pip-tools @@ -42,9 +42,9 @@ configargparse==1.5.2 # via gray coverage==5.5 # via pytest-cov -distlib==0.3.1 +distlib==0.3.2 # via virtualenv -docutils==0.16 +docutils==0.17.1 # via # readme-renderer # sphinx @@ -56,22 +56,23 @@ filelock==3.0.12 # virtualenv fixit==0.1.4 # via gray -flake8-polyfill==1.0.2 - # via pep8-naming -flake8==3.9.1 +flake8==3.9.2 # via # -r dev-requirements.in # fixit # flake8-polyfill + # pep8-naming +flake8-polyfill==1.0.2 + # via pep8-naming gray==0.10.1 # via -r dev-requirements.in -identify==2.2.4 +identify==2.2.13 # via pre-commit -idna==2.10 +idna==3.2 # via requests imagesize==1.2.0 # via sphinx -importlib-metadata==4.0.1 +importlib-metadata==4.7.1 # via # keyring # twine @@ -79,52 +80,54 @@ importlib-resources==5.2.2 # via fixit iniconfig==1.1.1 # via pytest -isort==5.8.0 +isort==5.9.3 # via # -r dev-requirements.in # gray -jinja2==2.11.3 +jinja2==3.0.1 # via sphinx jsonschema==3.2.0 # via fixit -keyring==23.0.1 +keyring==23.1.0 # via twine libcst==0.3.20 # via fixit -markupsafe==1.1.1 +markupsafe==2.0.1 # via jinja2 mccabe==0.6.1 # via flake8 +mypy==0.910 + # via -r dev-requirements.in mypy-extensions==0.4.3 # via # black # mypy # typing-inspect -mypy==0.812 - # via -r dev-requirements.in nodeenv==1.6.0 # via pre-commit -packaging==20.9 +packaging==21.0 # via # bleach # pytest # sphinx # tox -pathspec==0.8.1 +pathspec==0.9.0 # via black -pep517==0.10.0 +pep517==0.11.0 # via pip-tools -pep8-naming==0.11.1 +pep8-naming==0.12.1 # via -r dev-requirements.in -pip-tools==6.1.0 +pip-tools==6.2.0 # via -r dev-requirements.in -pkginfo==1.7.0 +pkginfo==1.7.1 # via twine -pluggy==0.13.1 +platformdirs==2.2.0 + # via virtualenv +pluggy==1.0.0 # via # pytest # tox -pre-commit==2.12.1 +pre-commit==2.14.0 # via -r dev-requirements.in prettylog==0.3.0 # via gray @@ -138,7 +141,7 @@ pyflakes==2.3.1 # via # autoflake # flake8 -pygments==2.9.0 +pygments==2.10.0 # via # readme-renderer # sphinx @@ -146,12 +149,12 @@ pyparsing==2.4.7 # via packaging pyrsistent==0.18.0 # via jsonschema -pytest-cov==2.11.1 - # via -r dev-requirements.in -pytest==6.2.3 +pytest==6.2.5 # via # -r dev-requirements.in # pytest-cov +pytest-cov==2.12.1 + # via -r dev-requirements.in pytz==2021.1 # via babel pyupgrade==2.24.0 @@ -163,18 +166,18 @@ pyyaml==5.4.1 # pre-commit readme-renderer==29.0 # via twine -regex==2021.4.4 +regex==2021.8.27 # via black -requests-toolbelt==0.9.1 - # via twine -requests==2.25.1 +requests==2.26.0 # via # requests-toolbelt # sphinx # twine -rfc3986==1.4.0 +requests-toolbelt==0.9.1 # via twine -six==1.15.0 +rfc3986==1.5.0 + # via twine +six==1.16.0 # via # bleach # jsonschema @@ -183,19 +186,19 @@ six==1.15.0 # virtualenv snowballstemmer==2.1.0 # via sphinx -sphinx==3.5.4 +sphinx==4.1.2 # via -r dev-requirements.in sphinxcontrib-applehelp==1.0.2 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx -sphinxcontrib-htmlhelp==1.0.3 +sphinxcontrib-htmlhelp==2.0.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx -sphinxcontrib-serializinghtml==1.1.4 +sphinxcontrib-serializinghtml==1.1.5 # via sphinx tokenize-rt==4.1.0 # via @@ -203,20 +206,21 @@ tokenize-rt==4.1.0 # pyupgrade toml==0.10.2 # via - # pep517 + # mypy # pre-commit # pytest + # pytest-cov # tox tomli==1.2.1 - # via black -tox==3.23.0 + # via + # black + # pep517 +tox==3.24.3 # via -r dev-requirements.in -tqdm==4.60.0 +tqdm==4.62.2 # via twine -twine==3.4.1 +twine==3.4.2 # via -r dev-requirements.in -typed-ast==1.4.3 - # via mypy typing-extensions==3.10.0.0 # via # libcst @@ -230,15 +234,17 @@ unify==0.5 # via gray untokenize==0.1.1 # via unify -urllib3==1.26.5 +urllib3==1.26.6 # via requests -virtualenv==20.4.4 +virtualenv==20.7.2 # via # pre-commit # tox webencodings==0.5.1 # via bleach -zipp==3.4.1 +wheel==0.37.0 + # via pip-tools +zipp==3.5.0 # via # importlib-metadata # importlib-resources diff --git a/requirements.in b/requirements.in index 7703126..b55cb43 100644 --- a/requirements.in +++ b/requirements.in @@ -12,10 +12,12 @@ pbr pyyaml # Allowing a newer version can lead to a conflict with # requests. -py3-validate-email==0.2.16 +py3-validate-email pytz requests six thesmuggler yfinance update_checker +flask-socketio +eventlet diff --git a/requirements.txt b/requirements.txt index f044a69..727141a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.8 # To update, run: # # pip-compile requirements.in @@ -8,69 +8,80 @@ aioax25==0.0.10 # via -r requirements.in aprslib==0.6.47 # via -r requirements.in -backoff==1.10.0 +backoff==1.11.1 # via opencage -certifi==2020.12.5 +bidict==0.21.2 + # via python-socketio +certifi==2021.5.30 # via requests -cffi==1.14.5 +cffi==1.14.6 # via cryptography -chardet==4.0.0 +charset-normalizer==2.0.4 # via requests -click-completion==0.5.2 - # via -r requirements.in -click==7.1.2 +click==8.0.1 # via # -r requirements.in # click-completion # flask +click-completion==0.5.2 + # via -r requirements.in contexter==0.1.4 # via signalslot cryptography==3.4.7 # via pyopenssl dnspython==2.1.0 - # via py3-validate-email + # via + # eventlet + # py3-validate-email +eventlet==0.32.0 + # via -r requirements.in filelock==3.0.12 # via py3-validate-email -flask-classful==0.14.2 - # via -r requirements.in -flask-httpauth==4.3.0 - # via -r requirements.in -flask==1.1.2 +flask==2.0.1 # via # -r requirements.in # flask-classful # flask-httpauth -idna==2.10 + # flask-socketio +flask-classful==0.14.2 + # via -r requirements.in +flask-httpauth==4.4.0 + # via -r requirements.in +flask-socketio==5.1.1 + # via -r requirements.in +greenlet==1.1.1 + # via eventlet +idna==3.2 # via # py3-validate-email # requests imapclient==2.2.0 # via -r requirements.in -itsdangerous==1.1.0 +itsdangerous==2.0.1 # via flask -jinja2==2.11.3 +jinja2==3.0.1 # via # click-completion # flask lxml==4.6.3 # via yfinance -markupsafe==1.1.1 +markupsafe==2.0.1 # via jinja2 multitasking==0.0.9 # via yfinance -numpy==1.20.2 +numpy==1.21.2 # via # pandas # yfinance -opencage==1.2.2 +opencage==2.0.0 # via -r requirements.in -pandas==1.2.4 +pandas==1.3.2 # via yfinance pbr==5.6.0 # via -r requirements.in -pluggy==0.13.1 +pluggy==1.0.0 # via -r requirements.in -py3-validate-email==0.2.16 +py3-validate-email==1.0.1 # via -r requirements.in pycparser==2.20 # via cffi @@ -80,13 +91,17 @@ pyserial==3.5 # via aioax25 python-dateutil==2.8.1 # via pandas +python-engineio==4.2.1 + # via python-socketio +python-socketio==5.4.0 + # via flask-socketio pytz==2021.1 # via # -r requirements.in # pandas pyyaml==5.4.1 # via -r requirements.in -requests==2.25.1 +requests==2.26.0 # via # -r requirements.in # opencage @@ -96,12 +111,12 @@ shellingham==1.4.0 # via click-completion signalslot==0.1.2 # via aioax25 -six==1.15.0 +six==1.16.0 # via # -r requirements.in # click-completion + # eventlet # imapclient - # opencage # pyopenssl # python-dateutil # signalslot @@ -109,11 +124,11 @@ thesmuggler==1.0.1 # via -r requirements.in update-checker==0.18.0 # via -r requirements.in -urllib3==1.26.5 +urllib3==1.26.6 # via requests weakrefmethod==1.0.3 # via signalslot -werkzeug==1.0.1 +werkzeug==2.0.0 # via flask -yfinance==0.1.59 +yfinance==0.1.63 # via -r requirements.in