From c941379a5c3983568253a09a68613dcb42d68340 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 27 Aug 2021 15:10:37 -0400 Subject: [PATCH] Upgraded the send-message POC to use websockets This patch updates the send message Admi page to use websockets. It makes updates to the messages list instant. --- aprsd/client.py | 11 +- aprsd/flask.py | 162 +++++++++++++-------- aprsd/main.py | 9 +- aprsd/web/static/js/send-message.js | 193 ++++++++++++++++++-------- aprsd/web/templates/send-message.html | 55 +------- dev-requirements.in | 4 +- dev-requirements.txt | 121 +++++++--------- requirements.in | 4 +- requirements.txt | 63 +++++---- 9 files changed, 351 insertions(+), 271 deletions(-) diff --git a/aprsd/client.py b/aprsd/client.py index 7b65703..8993d92 100644 --- a/aprsd/client.py +++ b/aprsd/client.py @@ -123,7 +123,12 @@ class Aprsdis(aprslib.IS): self.select_timeout, ) if not readable: - continue + if not blocking: + #self.logger.warning("not fucking readable, not blocking, break!") + break + else: + #self.logger.warning("not fucking readable, continue") + continue try: short_buf = self.sock.recv(4096) @@ -224,7 +229,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"#": @@ -270,7 +275,9 @@ class Aprsdis(aprslib.IS): raise if not blocking: + #self.logger.error("Not blocking, bail bitch") break + #self.logger.error("Consumer exiting") def get_client(): diff --git a/aprsd/flask.py b/aprsd/flask.py index 839d42e..d5aa05f 100644 --- a/aprsd/flask.py +++ b/aprsd/flask.py @@ -13,6 +13,7 @@ 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 @@ -108,11 +109,14 @@ class SendMessageThread(threads.APRSDThread): aprsis_client = None request = None got_ack = False + got_reply = False - def __init__(self, config, info, msg): + 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"], @@ -183,7 +187,7 @@ class SendMessageThread(threads.APRSDThread): LOG.debug("Exiting") def rx_packet(self, packet): - global got_ack, got_response + global socketio # LOG.debug("Got packet back {}".format(packet)) resp = packet.get("response", None) if resp == "ack": @@ -191,10 +195,15 @@ class SendMessageThread(threads.APRSDThread): 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": + 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) @@ -209,9 +218,12 @@ class SendMessageThread(threads.APRSDThread): fromcall=fromcall, ack=msg_number, ) - got_response = True 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( @@ -223,37 +235,37 @@ class SendMessageThread(threads.APRSDThread): 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): - LOG.debug("LOOP Start") + # 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, reconnecting") - time.sleep(5) - # Force the deletion of the client object connected to aprs - # This will cause a reconnect, next time client.get_client() - # is called - del self.aprs_client - connecting = True - counter = 0; - while connecting: - try: - self.aprs_client = self.setup_connection() - connecting = False - except Exception: - LOG.error("Couldn't connect") - counter += 1 - if counter >= 3: - LOG.error("Reached reconnect limit.") - return False + LOG.error("Connection dropped.") + return False - LOG.debug("LOOP END") return True @@ -351,38 +363,7 @@ class APRSDFlask(flask_classful.FlaskView): @auth.login_required def send_message(self): LOG.debug(request) - if request.method == "POST": - info = { - "from": request.form["from_call"], - "to": request.form["to_call"], - "password": request.form["from_call_password"], - "message": request.form["message"], - "wait_reply": request.form["wait_reply"], - } - LOG.debug(info) - msg = messaging.TextMessage( - info["from"], info["to"], - info["message"], - ) - msgs = SentMessages() - msgs.add(msg) - msgs.set_status(msg.id, "Sending") - - send_message_t = SendMessageThread(self.config, info, msg) - send_message_t.start() - - - info["from"] - result = "sending" - msg_id = msg.id - result = { - "msg_id": msg_id, - "status": "sending", - } - return json.dumps(result) - else: - result = "fail" - + if request.method == "GET": return flask.render_template( "send-message.html", callsign=self.config["aprs"]["login"], @@ -451,6 +432,60 @@ 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") @@ -485,6 +520,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", @@ -498,8 +535,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", "POST"])(server.send_message) + 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..8c1fa78 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -513,10 +513,11 @@ def server( if web_enabled: flask_enabled = True - app = flask.init_flask(config, loglevel, quiet) - app.run( - host=config["aprsd"]["web"]["host"], - port=config["aprsd"]["web"]["port"], + (socketio, app) = flask.init_flask(config, loglevel, quiet) + socketio.run( + app, + host=config["aprsd"]["web"]["host"], + port=config["aprsd"]["web"]["port"], ) # If there are items in the msgTracker, then save them diff --git a/aprsd/web/static/js/send-message.js b/aprsd/web/static/js/send-message.js index 92afbd4..812cafc 100644 --- a/aprsd/web/static/js/send-message.js +++ b/aprsd/web/static/js/send-message.js @@ -1,74 +1,145 @@ msgs_list = {}; +var cleared = false; function size_dict(d){c=0; for (i in d) ++c; return c} -function update_messages(data) { - msgs_cnt = size_dict(data); - $('#msgs_count').html(msgs_cnt); +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); + }); - var msgsdiv = $("#msgsDiv"); - //nuke the contents first, then add to it. - if (size_dict(msgs_list) == 0 && size_dict(data) > 0) { - msgsdiv.html('') - } + socket.on("sent", function(msg) { + if (cleared == false) { + var msgsdiv = $("#msgsDiv"); + msgsdiv.html('') + cleared = true + } + add_msg(msg); + }); - jQuery.each(data, function(i, val) { - if ( msgs_list.hasOwnProperty(val["ts"]) == false ) { - // Store the packet - msgs_list[val["ts"]] = val; - ts_str = val["ts"].toString(); - ts = ts_str.split(".")[0]*1000; - var d = new Date(ts).toLocaleDateString("en-US") - var t = new Date(ts).toLocaleTimeString("en-US") + socket.on("ack", function(msg) { + update_msg(msg); + }); + socket.on("reply", function(msg) { + update_msg(msg); + }); - from = val['from'] - title_id = 'title_tx' - var from_to = d + " " + t + "    " + from + " > " + $("#sendform").submit(function(event) { + event.preventDefault(); - if (val.hasOwnProperty('to')) { - from_to = from_to + val['to'] - } - from_to = from_to + "  -  " + val['raw'] + var $checkboxes = $(this).find('input[type=checkbox]'); - id = ts_str.split('.')[0] - pretty_id = "pretty_" + id - loader_id = "loader_" + id - reply_id = "reply_" + id - json_pretty = Prism.highlight(JSON.stringify(val, null, '\t'), Prism.languages.json, 'json'); - msg_html = '
'; - msg_html += '
 '; - msg_html += ' ' + from_to + '
'; - msg_html += '
' + json_pretty + '

' - msgsdiv.prepend(msg_html); - } else { - // We have an existing entry - msgs_list[val["ts"]] = val; - ts_str = val["ts"].toString(); - id = ts_str.split('.')[0] - pretty_id = "pretty_" + id - loader_id = "loader_" + id - reply_id = "reply_" + id - var pretty_pre = $("#" + pretty_id); - if (val['ack'] == true) { - var loader_div = $('#' + loader_id); - loader_div.removeClass('ui active inline loader'); - loader_div.addClass('ui disabled loader'); - loader_div.attr('data-content', 'Got reply'); - } + //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); + } + }); - if (val['reply'] !== null) { - var reply_div = $('#' + reply_id); - reply_div.removeClass("thumbs down outline icon"); - reply_div.addClass('thumbs up outline icon'); - reply_div.attr('data-content', 'Got Reply'); - } + msg = {'from': $('#from').val(), + 'password': $('#password').val(), + 'to': $('#to').val(), + 'message': $('#message').val(), + 'wait_reply': $('#wait_reply').val(), + } - pretty_pre.html(''); - json_pretty = Prism.highlight(JSON.stringify(val, null, '\t'), Prism.languages.json, 'json'); - pretty_pre.html(json_pretty); - } - }); - - $('.ui.accordion').accordion('refresh'); + 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 index 1f9f354..566eaba 100644 --- a/aprsd/web/templates/send-message.html +++ b/aprsd/web/templates/send-message.html @@ -3,6 +3,9 @@ + + + @@ -17,51 +20,7 @@ @@ -87,12 +46,12 @@

Send Message Form

-

+

-

+

-

+

diff --git a/dev-requirements.in b/dev-requirements.in index e12700c..3cce931 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..eba5c04 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -9,28 +9,26 @@ add-trailing-comma==2.1.0 alabaster==0.7.12 # via sphinx appdirs==1.4.4 - # via - # black - # virtualenv -attrs==20.3.0 - # via - # jsonschema - # pytest + # via black +attrs==21.2.0 + # via jsonschema 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 @@ -40,11 +38,9 @@ colorlog==6.4.1 # via prettylog 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 @@ -58,40 +54,39 @@ 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 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 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 @@ -100,45 +95,42 @@ mypy-extensions==0.4.3 # black # mypy # typing-inspect -mypy==0.812 +mypy==0.910 # 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 - # via - # pytest - # tox -pre-commit==2.12.1 +platformdirs==2.2.0 + # via virtualenv +pluggy==1.0.0 + # via tox +pre-commit==2.14.0 # via -r dev-requirements.in prettylog==0.3.0 # via gray py==1.10.0 - # via - # pytest - # tox + # via tox pycodestyle==2.7.0 # via flake8 pyflakes==2.3.1 # via # autoflake # flake8 -pygments==2.9.0 +pygments==2.10.0 # via # readme-renderer # sphinx @@ -146,12 +138,6 @@ 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 - # via - # -r dev-requirements.in - # pytest-cov pytz==2021.1 # via babel pyupgrade==2.24.0 @@ -163,18 +149,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 +rfc3986==1.5.0 # via twine -six==1.15.0 +six==1.16.0 # via # bleach # jsonschema @@ -183,19 +169,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 +189,19 @@ tokenize-rt==4.1.0 # pyupgrade toml==0.10.2 # via - # pep517 + # mypy # pre-commit - # pytest # 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 +215,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..1e00739 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..1f0e6e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,17 +8,19 @@ 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 @@ -27,50 +29,51 @@ contexter==0.1.4 # via signalslot cryptography==3.4.7 # via pyopenssl -dnspython==2.1.0 - # via py3-validate-email -filelock==3.0.12 - # via py3-validate-email +dnspython==1.16.0 + # via eventlet +eventlet==0.31.1 + # via -r requirements.in flask-classful==0.14.2 # via -r requirements.in -flask-httpauth==4.3.0 +flask-httpauth==4.4.0 # via -r requirements.in -flask==1.1.2 +flask-socketio==5.1.1 + # via -r requirements.in +flask==2.0.1 # via # -r requirements.in # flask-classful # flask-httpauth -idna==2.10 - # via - # py3-validate-email - # requests + # flask-socketio +greenlet==1.1.1 + # via eventlet +idna==3.2 + # via 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 - # via -r requirements.in -py3-validate-email==0.2.16 +pluggy==1.0.0 # via -r requirements.in pycparser==2.20 # via cffi @@ -80,13 +83,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 @@ -100,8 +107,8 @@ six==1.15.0 # via # -r requirements.in # click-completion + # eventlet # imapclient - # opencage # pyopenssl # python-dateutil # signalslot @@ -109,11 +116,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 # via flask -yfinance==0.1.59 +yfinance==0.1.63 # via -r requirements.in