1
0
mirror of https://github.com/craigerl/aprsd.git synced 2025-07-31 12:52:24 -04:00

Merge pull request #59 from craigerl/web-send-msg

Send Message via admin Web interface
This commit is contained in:
Walter A. Boring IV 2021-09-01 17:44:50 -04:00 committed by GitHub
commit 266ae7f217
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 679 additions and 99 deletions

View File

@ -38,6 +38,11 @@ class Client:
if config: if config:
self.config = config self.config = config
def new(self):
obj = super().__new__(Client)
obj.config = self.config
return obj
@property @property
def client(self): def client(self):
if not self.aprs_client: if not self.aprs_client:
@ -118,15 +123,22 @@ class Aprsdis(aprslib.IS):
self.select_timeout, self.select_timeout,
) )
if not readable: if not readable:
continue if not blocking:
break
else:
continue
try: try:
short_buf = self.sock.recv(4096) short_buf = self.sock.recv(4096)
# sock.recv returns empty if the connection drops # sock.recv returns empty if the connection drops
if not short_buf: if not short_buf:
self.logger.error("socket.recv(): returned empty") if not blocking:
raise aprslib.ConnectionDrop("connection dropped") # 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: except OSError as e:
# self.logger.error("socket error on recv(): %s" % str(e)) # self.logger.error("socket error on recv(): %s" % str(e))
if "Resource temporarily unavailable" in str(e): if "Resource temporarily unavailable" in str(e):
@ -215,7 +227,7 @@ class Aprsdis(aprslib.IS):
line = b"" line = b""
while True: while True and not self.thread_stop:
try: try:
for line in self._socket_readlines(blocking): for line in self._socket_readlines(blocking):
if line[0:1] != b"#": if line[0:1] != b"#":

View File

@ -4,14 +4,22 @@ import logging
from logging import NullHandler from logging import NullHandler
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
import sys import sys
import threading
import time
import aprslib
from aprslib.exceptions import LoginError
import flask import flask
from flask import request
import flask_classful import flask_classful
from flask_httpauth import HTTPBasicAuth from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
import aprsd 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") LOG = logging.getLogger("APRSD")
@ -20,6 +28,72 @@ auth = HTTPBasicAuth()
users = None 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. # HTTPBasicAuth doesn't work on a class method.
# This has to be out here. Rely on the APRSDFlask # This has to be out here. Rely on the APRSDFlask
# class to initialize the users from the config # class to initialize the users from the config
@ -31,6 +105,172 @@ def verify_password(username, password):
return username 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): class APRSDFlask(flask_classful.FlaskView):
config = None config = None
@ -115,6 +355,23 @@ 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 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 @auth.login_required
def packets(self): def packets(self):
packet_list = packets.PacketList().get() packet_list = packets.PacketList().get()
@ -177,6 +434,59 @@ class APRSDFlask(flask_classful.FlaskView):
return json.dumps(self._stats()) 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): def setup_logging(config, flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug") flask_log = logging.getLogger("werkzeug")
@ -211,6 +521,8 @@ def setup_logging(config, flask_app, loglevel, quiet):
def init_flask(config, loglevel, quiet): def init_flask(config, loglevel, quiet):
global socketio
flask_app = flask.Flask( flask_app = flask.Flask(
"aprsd", "aprsd",
static_url_path="/static", 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("/stats", methods=["GET"])(server.stats)
flask_app.route("/messages", methods=["GET"])(server.messages) flask_app.route("/messages", methods=["GET"])(server.messages)
flask_app.route("/packets", methods=["GET"])(server.packets) 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("/save", methods=["GET"])(server.save)
flask_app.route("/plugins", methods=["GET"])(server.plugins) 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

View File

@ -513,8 +513,9 @@ def server(
if web_enabled: if web_enabled:
flask_enabled = True flask_enabled = True
app = flask.init_flask(config, loglevel, quiet) (socketio, app) = flask.init_flask(config, loglevel, quiet)
app.run( socketio.run(
app,
host=config["aprsd"]["web"]["host"], host=config["aprsd"]["web"]["host"],
port=config["aprsd"]["web"]["port"], port=config["aprsd"]["web"]["port"],
) )

View File

@ -299,7 +299,7 @@ class RawMessage(Message):
thread = SendMessageThread(message=self) thread = SendMessageThread(message=self)
thread.start() thread.start()
def send_direct(self): def send_direct(self, aprsis_client=None):
"""Send a message without a separate thread.""" """Send a message without a separate thread."""
cl = self.get_transport() cl = self.get_transport()
log_message( log_message(
@ -379,7 +379,7 @@ class TextMessage(Message):
thread = SendMessageThread(message=self) thread = SendMessageThread(message=self)
thread.start() thread.start()
def send_direct(self): def send_direct(self, aprsis_client=None):
"""Send a message without a separate thread.""" """Send a message without a separate thread."""
cl = self.get_transport() cl = self.get_transport()
log_message( log_message(
@ -391,6 +391,7 @@ class TextMessage(Message):
) )
cl.send(self) cl.send(self)
stats.APRSDStats().msgs_tx_inc() stats.APRSDStats().msgs_tx_inc()
packets.PacketList().add(self.dict())
class SendMessageThread(threads.APRSDThread): class SendMessageThread(threads.APRSDThread):
@ -498,7 +499,7 @@ class AckMessage(Message):
thread = SendAckThread(self) thread = SendAckThread(self)
thread.start() thread.start()
def send_direct(self): def send_direct(self, aprsis_client=None):
"""Send an ack message without a separate thread.""" """Send an ack message without a separate thread."""
cl = self.get_transport() cl = self.get_transport()
log_message( log_message(

View File

@ -69,6 +69,7 @@ class WatchList:
_instance = None _instance = None
callsigns = {} callsigns = {}
config = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
@ -97,7 +98,7 @@ class WatchList:
} }
def is_enabled(self): 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) return self.config["aprsd"]["watch_list"].get("enabled", False)
else: else:
return False return False

View File

@ -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 = '<div class="ui title" id="' + title_id + '"><i class="dropdown icon"></i>';
msg_html += '<div class="ui active inline loader" id="' + loader_id +'" data-content="Waiting for Ack"></div>&nbsp;';
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>&nbsp;';
msg_html += '<i class="thumbs down outline icon" id="' + reply_id + '" data-content="Waiting for Reply"></i>&nbsp;';
msg_html += '<span id="' + span_id + '">' + from_to +'</span></div>';
msg_html += '<div class="content"><p class="transition hidden"><pre id="' + pretty_id + '" class="language-json">' + json_pretty + '</p></p></div>'
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 + "&nbsp;&nbsp;&nbsp;&nbsp;" + from + " > "
if (msg.hasOwnProperty('to')) {
from_to = from_to + msg['to']
}
from_to = from_to + "&nbsp;&nbsp;-&nbsp;&nbsp;" + msg['message']
from_to += "&nbsp;&nbsp; ===> " + 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');
}

View File

@ -0,0 +1,74 @@
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-simple-websocket@1.1.4/src/jquery.simple.websocket.min.js"></script>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<script src="/static/js/send-message.js"></script>
<script language="JavaScript">
$(document).ready(function() {
init_messages();
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD {{ version }}</h1>
</div>
<div class='ui grid text container'>
<div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprsis'>NONE</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<h3 class="ui dividing header">Send Message Form</h3>
<form id="sendform" name="sendmsg" action="">
<p><label for="from_call">From Callsign:</label>
<input type="text" name="from_call" id="from" value="WB4BOR"></p>
<p><label for="from_call_password">Password:</label>
<input type="password" name="from_call_password" id='password' value="24496"></p>
<p><label for="to_call">To Callsign:</label>
<input type="text" name="to_call" id="to" value="WB4BOR-11"></p>
<p><label for="message">Message:</label>
<input type="text" name="message" id="message" value="ping"></p>
<p><label for="wait">Wait for Reply?</label>
<input type="checkbox" name="wait_reply" id="wait_reply" value="off" checked>
</p>
<input type="submit" name="submit" class="button" id="send_msg" value="Send" />
</form>
<h3 class="ui dividing header">Messages (<span id="msgs_count">0</span>)</h3>
<div class="ui styled fluid accordion" id="accordion">
<div id="msgsDiv" class="ui mini text">Messages</div>
</div>
</body>
</html>

View File

@ -1,12 +1,12 @@
flake8 flake8
isort isort
mypy mypy
pytest
pytest-cov
pep8-naming pep8-naming
Sphinx Sphinx
tox tox
twine twine
pre-commit pre-commit
pip-tools pip-tools
pytest
pytest-cov
gray gray

View File

@ -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: # To update, run:
# #
# pip-compile dev-requirements.in # pip-compile dev-requirements.in
@ -9,10 +9,8 @@ add-trailing-comma==2.1.0
alabaster==0.7.12 alabaster==0.7.12
# via sphinx # via sphinx
appdirs==1.4.4 appdirs==1.4.4
# via # via black
# black attrs==21.2.0
# virtualenv
attrs==20.3.0
# via # via
# jsonschema # jsonschema
# pytest # pytest
@ -20,17 +18,19 @@ autoflake==1.4
# via gray # via gray
babel==2.9.1 babel==2.9.1
# via sphinx # via sphinx
backports.entry-points-selectable==1.1.0
# via virtualenv
black==21.7b0 black==21.7b0
# via gray # via gray
bleach==3.3.0 bleach==4.1.0
# via readme-renderer # via readme-renderer
certifi==2020.12.5 certifi==2021.5.30
# via requests # via requests
cfgv==3.2.0 cfgv==3.3.1
# via pre-commit # via pre-commit
chardet==4.0.0 charset-normalizer==2.0.4
# via requests # via requests
click==7.1.2 click==8.0.1
# via # via
# black # black
# pip-tools # pip-tools
@ -42,9 +42,9 @@ configargparse==1.5.2
# via gray # via gray
coverage==5.5 coverage==5.5
# via pytest-cov # via pytest-cov
distlib==0.3.1 distlib==0.3.2
# via virtualenv # via virtualenv
docutils==0.16 docutils==0.17.1
# via # via
# readme-renderer # readme-renderer
# sphinx # sphinx
@ -56,22 +56,23 @@ filelock==3.0.12
# virtualenv # virtualenv
fixit==0.1.4 fixit==0.1.4
# via gray # via gray
flake8-polyfill==1.0.2 flake8==3.9.2
# via pep8-naming
flake8==3.9.1
# via # via
# -r dev-requirements.in # -r dev-requirements.in
# fixit # fixit
# flake8-polyfill # flake8-polyfill
# pep8-naming
flake8-polyfill==1.0.2
# via pep8-naming
gray==0.10.1 gray==0.10.1
# via -r dev-requirements.in # via -r dev-requirements.in
identify==2.2.4 identify==2.2.13
# via pre-commit # via pre-commit
idna==2.10 idna==3.2
# via requests # via requests
imagesize==1.2.0 imagesize==1.2.0
# via sphinx # via sphinx
importlib-metadata==4.0.1 importlib-metadata==4.7.1
# via # via
# keyring # keyring
# twine # twine
@ -79,52 +80,54 @@ importlib-resources==5.2.2
# via fixit # via fixit
iniconfig==1.1.1 iniconfig==1.1.1
# via pytest # via pytest
isort==5.8.0 isort==5.9.3
# via # via
# -r dev-requirements.in # -r dev-requirements.in
# gray # gray
jinja2==2.11.3 jinja2==3.0.1
# via sphinx # via sphinx
jsonschema==3.2.0 jsonschema==3.2.0
# via fixit # via fixit
keyring==23.0.1 keyring==23.1.0
# via twine # via twine
libcst==0.3.20 libcst==0.3.20
# via fixit # via fixit
markupsafe==1.1.1 markupsafe==2.0.1
# via jinja2 # via jinja2
mccabe==0.6.1 mccabe==0.6.1
# via flake8 # via flake8
mypy==0.910
# via -r dev-requirements.in
mypy-extensions==0.4.3 mypy-extensions==0.4.3
# via # via
# black # black
# mypy # mypy
# typing-inspect # typing-inspect
mypy==0.812
# via -r dev-requirements.in
nodeenv==1.6.0 nodeenv==1.6.0
# via pre-commit # via pre-commit
packaging==20.9 packaging==21.0
# via # via
# bleach # bleach
# pytest # pytest
# sphinx # sphinx
# tox # tox
pathspec==0.8.1 pathspec==0.9.0
# via black # via black
pep517==0.10.0 pep517==0.11.0
# via pip-tools # via pip-tools
pep8-naming==0.11.1 pep8-naming==0.12.1
# via -r dev-requirements.in # via -r dev-requirements.in
pip-tools==6.1.0 pip-tools==6.2.0
# via -r dev-requirements.in # via -r dev-requirements.in
pkginfo==1.7.0 pkginfo==1.7.1
# via twine # via twine
pluggy==0.13.1 platformdirs==2.2.0
# via virtualenv
pluggy==1.0.0
# via # via
# pytest # pytest
# tox # tox
pre-commit==2.12.1 pre-commit==2.14.0
# via -r dev-requirements.in # via -r dev-requirements.in
prettylog==0.3.0 prettylog==0.3.0
# via gray # via gray
@ -138,7 +141,7 @@ pyflakes==2.3.1
# via # via
# autoflake # autoflake
# flake8 # flake8
pygments==2.9.0 pygments==2.10.0
# via # via
# readme-renderer # readme-renderer
# sphinx # sphinx
@ -146,12 +149,12 @@ pyparsing==2.4.7
# via packaging # via packaging
pyrsistent==0.18.0 pyrsistent==0.18.0
# via jsonschema # via jsonschema
pytest-cov==2.11.1 pytest==6.2.5
# via -r dev-requirements.in
pytest==6.2.3
# via # via
# -r dev-requirements.in # -r dev-requirements.in
# pytest-cov # pytest-cov
pytest-cov==2.12.1
# via -r dev-requirements.in
pytz==2021.1 pytz==2021.1
# via babel # via babel
pyupgrade==2.24.0 pyupgrade==2.24.0
@ -163,18 +166,18 @@ pyyaml==5.4.1
# pre-commit # pre-commit
readme-renderer==29.0 readme-renderer==29.0
# via twine # via twine
regex==2021.4.4 regex==2021.8.27
# via black # via black
requests-toolbelt==0.9.1 requests==2.26.0
# via twine
requests==2.25.1
# via # via
# requests-toolbelt # requests-toolbelt
# sphinx # sphinx
# twine # twine
rfc3986==1.4.0 requests-toolbelt==0.9.1
# via twine # via twine
six==1.15.0 rfc3986==1.5.0
# via twine
six==1.16.0
# via # via
# bleach # bleach
# jsonschema # jsonschema
@ -183,19 +186,19 @@ six==1.15.0
# virtualenv # virtualenv
snowballstemmer==2.1.0 snowballstemmer==2.1.0
# via sphinx # via sphinx
sphinx==3.5.4 sphinx==4.1.2
# via -r dev-requirements.in # via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.2 sphinxcontrib-applehelp==1.0.2
# via sphinx # via sphinx
sphinxcontrib-devhelp==1.0.2 sphinxcontrib-devhelp==1.0.2
# via sphinx # via sphinx
sphinxcontrib-htmlhelp==1.0.3 sphinxcontrib-htmlhelp==2.0.0
# via sphinx # via sphinx
sphinxcontrib-jsmath==1.0.1 sphinxcontrib-jsmath==1.0.1
# via sphinx # via sphinx
sphinxcontrib-qthelp==1.0.3 sphinxcontrib-qthelp==1.0.3
# via sphinx # via sphinx
sphinxcontrib-serializinghtml==1.1.4 sphinxcontrib-serializinghtml==1.1.5
# via sphinx # via sphinx
tokenize-rt==4.1.0 tokenize-rt==4.1.0
# via # via
@ -203,20 +206,21 @@ tokenize-rt==4.1.0
# pyupgrade # pyupgrade
toml==0.10.2 toml==0.10.2
# via # via
# pep517 # mypy
# pre-commit # pre-commit
# pytest # pytest
# pytest-cov
# tox # tox
tomli==1.2.1 tomli==1.2.1
# via black # via
tox==3.23.0 # black
# pep517
tox==3.24.3
# via -r dev-requirements.in # via -r dev-requirements.in
tqdm==4.60.0 tqdm==4.62.2
# via twine # via twine
twine==3.4.1 twine==3.4.2
# via -r dev-requirements.in # via -r dev-requirements.in
typed-ast==1.4.3
# via mypy
typing-extensions==3.10.0.0 typing-extensions==3.10.0.0
# via # via
# libcst # libcst
@ -230,15 +234,17 @@ unify==0.5
# via gray # via gray
untokenize==0.1.1 untokenize==0.1.1
# via unify # via unify
urllib3==1.26.5 urllib3==1.26.6
# via requests # via requests
virtualenv==20.4.4 virtualenv==20.7.2
# via # via
# pre-commit # pre-commit
# tox # tox
webencodings==0.5.1 webencodings==0.5.1
# via bleach # via bleach
zipp==3.4.1 wheel==0.37.0
# via pip-tools
zipp==3.5.0
# via # via
# importlib-metadata # importlib-metadata
# importlib-resources # importlib-resources

View File

@ -12,10 +12,12 @@ pbr
pyyaml pyyaml
# Allowing a newer version can lead to a conflict with # Allowing a newer version can lead to a conflict with
# requests. # requests.
py3-validate-email==0.2.16 py3-validate-email
pytz pytz
requests requests
six six
thesmuggler thesmuggler
yfinance yfinance
update_checker update_checker
flask-socketio
eventlet

View File

@ -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: # To update, run:
# #
# pip-compile requirements.in # pip-compile requirements.in
@ -8,69 +8,80 @@ aioax25==0.0.10
# via -r requirements.in # via -r requirements.in
aprslib==0.6.47 aprslib==0.6.47
# via -r requirements.in # via -r requirements.in
backoff==1.10.0 backoff==1.11.1
# via opencage # via opencage
certifi==2020.12.5 bidict==0.21.2
# via python-socketio
certifi==2021.5.30
# via requests # via requests
cffi==1.14.5 cffi==1.14.6
# via cryptography # via cryptography
chardet==4.0.0 charset-normalizer==2.0.4
# via requests # via requests
click-completion==0.5.2 click==8.0.1
# via -r requirements.in
click==7.1.2
# via # via
# -r requirements.in # -r requirements.in
# click-completion # click-completion
# flask # flask
click-completion==0.5.2
# via -r requirements.in
contexter==0.1.4 contexter==0.1.4
# via signalslot # via signalslot
cryptography==3.4.7 cryptography==3.4.7
# via pyopenssl # via pyopenssl
dnspython==2.1.0 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 filelock==3.0.12
# via py3-validate-email # via py3-validate-email
flask-classful==0.14.2 flask==2.0.1
# via -r requirements.in
flask-httpauth==4.3.0
# via -r requirements.in
flask==1.1.2
# via # via
# -r requirements.in # -r requirements.in
# flask-classful # flask-classful
# flask-httpauth # 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 # via
# py3-validate-email # py3-validate-email
# requests # requests
imapclient==2.2.0 imapclient==2.2.0
# via -r requirements.in # via -r requirements.in
itsdangerous==1.1.0 itsdangerous==2.0.1
# via flask # via flask
jinja2==2.11.3 jinja2==3.0.1
# via # via
# click-completion # click-completion
# flask # flask
lxml==4.6.3 lxml==4.6.3
# via yfinance # via yfinance
markupsafe==1.1.1 markupsafe==2.0.1
# via jinja2 # via jinja2
multitasking==0.0.9 multitasking==0.0.9
# via yfinance # via yfinance
numpy==1.20.2 numpy==1.21.2
# via # via
# pandas # pandas
# yfinance # yfinance
opencage==1.2.2 opencage==2.0.0
# via -r requirements.in # via -r requirements.in
pandas==1.2.4 pandas==1.3.2
# via yfinance # via yfinance
pbr==5.6.0 pbr==5.6.0
# via -r requirements.in # via -r requirements.in
pluggy==0.13.1 pluggy==1.0.0
# via -r requirements.in # via -r requirements.in
py3-validate-email==0.2.16 py3-validate-email==1.0.1
# via -r requirements.in # via -r requirements.in
pycparser==2.20 pycparser==2.20
# via cffi # via cffi
@ -80,13 +91,17 @@ pyserial==3.5
# via aioax25 # via aioax25
python-dateutil==2.8.1 python-dateutil==2.8.1
# via pandas # via pandas
python-engineio==4.2.1
# via python-socketio
python-socketio==5.4.0
# via flask-socketio
pytz==2021.1 pytz==2021.1
# via # via
# -r requirements.in # -r requirements.in
# pandas # pandas
pyyaml==5.4.1 pyyaml==5.4.1
# via -r requirements.in # via -r requirements.in
requests==2.25.1 requests==2.26.0
# via # via
# -r requirements.in # -r requirements.in
# opencage # opencage
@ -96,12 +111,12 @@ shellingham==1.4.0
# via click-completion # via click-completion
signalslot==0.1.2 signalslot==0.1.2
# via aioax25 # via aioax25
six==1.15.0 six==1.16.0
# via # via
# -r requirements.in # -r requirements.in
# click-completion # click-completion
# eventlet
# imapclient # imapclient
# opencage
# pyopenssl # pyopenssl
# python-dateutil # python-dateutil
# signalslot # signalslot
@ -109,11 +124,11 @@ thesmuggler==1.0.1
# via -r requirements.in # via -r requirements.in
update-checker==0.18.0 update-checker==0.18.0
# via -r requirements.in # via -r requirements.in
urllib3==1.26.5 urllib3==1.26.6
# via requests # via requests
weakrefmethod==1.0.3 weakrefmethod==1.0.3
# via signalslot # via signalslot
werkzeug==1.0.1 werkzeug==2.0.0
# via flask # via flask
yfinance==0.1.59 yfinance==0.1.63
# via -r requirements.in # via -r requirements.in