1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-17 17:13:49 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
hemna 4402fc6cce Fix running fortune in container
Alpine linux has a fuct version of fortune.
This patch tries to install fortune_mod that has -n option.
2025-02-24 17:49:04 -05:00
16 changed files with 376 additions and 416 deletions
-17
View File
@@ -4,22 +4,6 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [4.1.2](https://github.com/craigerl/aprsd/compare/4.1.1...4.1.2)
> 6 March 2025
- Allow passing in a custom handler to setup_logging [`d262589`](https://github.com/craigerl/aprsd/commit/d2625893134f498748859da3b1684b04d456f790)
#### [4.1.1](https://github.com/craigerl/aprsd/compare/4.1.0...4.1.1)
> 5 March 2025
- Added new config to disable logging to console [`0fa5b07`](https://github.com/craigerl/aprsd/commit/0fa5b07d4bf4bc5d5aaad1de52b78058e472fe24)
- Added threads.service [`c1c89fd`](https://github.com/craigerl/aprsd/commit/c1c89fd2c2c69c5e6c5d29a736a7b89e3d45cfe2)
- Update requirements [`2b185ee`](https://github.com/craigerl/aprsd/commit/2b185ee1b84598c832d8a5d73753cb428854b932)
- Fixed some more ruff checks [`94ba915`](https://github.com/craigerl/aprsd/commit/94ba915ed44b11eaabc885e033669d67d8c341a5)
- 4.1.1 release [`7ed8028`](https://github.com/craigerl/aprsd/commit/7ed80283071c1ccebf1e3373727608edd0a56ee9)
#### [4.1.0](https://github.com/craigerl/aprsd/compare/4.0.2...4.1.0) #### [4.1.0](https://github.com/craigerl/aprsd/compare/4.0.2...4.1.0)
> 20 February 2025 > 20 February 2025
@@ -37,7 +21,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- fix for None packet in rx thread [`d82a81a`](https://github.com/craigerl/aprsd/commit/d82a81a2c3c1a7f50177a0a6435a555daeb858aa) - fix for None packet in rx thread [`d82a81a`](https://github.com/craigerl/aprsd/commit/d82a81a2c3c1a7f50177a0a6435a555daeb858aa)
- Fix runaway KISS driver on failed connnection [`b6da0eb`](https://github.com/craigerl/aprsd/commit/b6da0ebb0d2f4d7078dbbf91d8c03715412d89ea) - Fix runaway KISS driver on failed connnection [`b6da0eb`](https://github.com/craigerl/aprsd/commit/b6da0ebb0d2f4d7078dbbf91d8c03715412d89ea)
- CONF.logging.enable_color option added [`06bdb34`](https://github.com/craigerl/aprsd/commit/06bdb34642640d91ea96e3c6e8d8b5a4b8230611) - CONF.logging.enable_color option added [`06bdb34`](https://github.com/craigerl/aprsd/commit/06bdb34642640d91ea96e3c6e8d8b5a4b8230611)
- Update Changelog for 4.1.0 release [`a3cda9f`](https://github.com/craigerl/aprsd/commit/a3cda9f37d4c9b955b523f46b2eb8cf412a84407)
#### [4.0.2](https://github.com/craigerl/aprsd/compare/4.0.1...4.0.2) #### [4.0.2](https://github.com/craigerl/aprsd/compare/4.0.1...4.0.2)
+35 -36
View File
@@ -13,35 +13,35 @@ from aprsd.utils import trace
CONF = cfg.CONF CONF = cfg.CONF
home = str(Path.home()) home = str(Path.home())
DEFAULT_CONFIG_DIR = f'{home}/.config/aprsd/' DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
DEFAULT_SAVE_FILE = f'{home}/.config/aprsd/aprsd.p' DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
DEFAULT_CONFIG_FILE = f'{home}/.config/aprsd/aprsd.conf' DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.conf"
F = t.TypeVar('F', bound=t.Callable[..., t.Any]) F = t.TypeVar("F", bound=t.Callable[..., t.Any])
common_options = [ common_options = [
click.option( click.option(
'--loglevel', "--loglevel",
default='INFO', default="INFO",
show_default=True, show_default=True,
type=click.Choice( type=click.Choice(
['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
case_sensitive=False, case_sensitive=False,
), ),
show_choices=True, show_choices=True,
help='The log level to use for aprsd.log', help="The log level to use for aprsd.log",
), ),
click.option( click.option(
'-c', "-c",
'--config', "--config",
'config_file', "config_file",
show_default=True, show_default=True,
default=DEFAULT_CONFIG_FILE, default=DEFAULT_CONFIG_FILE,
help='The aprsd config file to use for options.', help="The aprsd config file to use for options.",
), ),
click.option( click.option(
'--quiet', "--quiet",
is_flag=True, is_flag=True,
default=False, default=False,
help="Don't log to stdout", help="Don't log to stdout",
@@ -59,7 +59,7 @@ class AliasedGroup(click.Group):
""" """
def decorator(f): def decorator(f):
aliases = kwargs.pop('aliases', []) aliases = kwargs.pop("aliases", [])
cmd = click.decorators.command(*args, **kwargs)(f) cmd = click.decorators.command(*args, **kwargs)(f)
self.add_command(cmd) self.add_command(cmd)
for alias in aliases: for alias in aliases:
@@ -77,7 +77,7 @@ class AliasedGroup(click.Group):
""" """
def decorator(f): def decorator(f):
aliases = kwargs.pop('aliases', []) aliases = kwargs.pop("aliases", [])
cmd = click.decorators.group(*args, **kwargs)(f) cmd = click.decorators.group(*args, **kwargs)(f)
self.add_command(cmd) self.add_command(cmd)
for alias in aliases: for alias in aliases:
@@ -101,37 +101,36 @@ def process_standard_options(f: F) -> F:
ctx = args[0] ctx = args[0]
ctx.ensure_object(dict) ctx.ensure_object(dict)
config_file_found = True config_file_found = True
if kwargs['config_file']: if kwargs["config_file"]:
default_config_files = [kwargs['config_file']] default_config_files = [kwargs["config_file"]]
else: else:
default_config_files = None default_config_files = None
try: try:
CONF( CONF(
[], [],
project='aprsd', project="aprsd",
version=aprsd.__version__, version=aprsd.__version__,
default_config_files=default_config_files, default_config_files=default_config_files,
) )
except cfg.ConfigFilesNotFoundError: except cfg.ConfigFilesNotFoundError:
config_file_found = False config_file_found = False
ctx.obj['loglevel'] = kwargs['loglevel'] ctx.obj["loglevel"] = kwargs["loglevel"]
# ctx.obj["config_file"] = kwargs["config_file"] # ctx.obj["config_file"] = kwargs["config_file"]
ctx.obj['quiet'] = kwargs['quiet'] ctx.obj["quiet"] = kwargs["quiet"]
log.setup_logging( log.setup_logging(
ctx.obj['loglevel'], ctx.obj["loglevel"],
ctx.obj['quiet'], ctx.obj["quiet"],
) )
if CONF.trace_enabled: if CONF.trace_enabled:
trace.setup_tracing(['method', 'api']) trace.setup_tracing(["method", "api"])
if not config_file_found: if not config_file_found:
LOG = logging.getLogger('APRSD') # noqa: N806 LOG = logging.getLogger("APRSD") # noqa: N806
LOG.error("No config file found!! run 'aprsd sample-config'") LOG.error("No config file found!! run 'aprsd sample-config'")
del kwargs['loglevel'] del kwargs["loglevel"]
del kwargs['config_file'] del kwargs["config_file"]
del kwargs['quiet'] del kwargs["quiet"]
return f(*args, **kwargs) return f(*args, **kwargs)
return update_wrapper(t.cast(F, new_func), f) return update_wrapper(t.cast(F, new_func), f)
@@ -143,17 +142,17 @@ def process_standard_options_no_config(f: F) -> F:
def new_func(*args, **kwargs): def new_func(*args, **kwargs):
ctx = args[0] ctx = args[0]
ctx.ensure_object(dict) ctx.ensure_object(dict)
ctx.obj['loglevel'] = kwargs['loglevel'] ctx.obj["loglevel"] = kwargs["loglevel"]
ctx.obj['config_file'] = kwargs['config_file'] ctx.obj["config_file"] = kwargs["config_file"]
ctx.obj['quiet'] = kwargs['quiet'] ctx.obj["quiet"] = kwargs["quiet"]
log.setup_logging( log.setup_logging(
ctx.obj['loglevel'], ctx.obj["loglevel"],
ctx.obj['quiet'], ctx.obj["quiet"],
) )
del kwargs['loglevel'] del kwargs["loglevel"]
del kwargs['config_file'] del kwargs["config_file"]
del kwargs['quiet'] del kwargs["quiet"]
return f(*args, **kwargs) return f(*args, **kwargs)
return update_wrapper(t.cast(F, new_func), f) return update_wrapper(t.cast(F, new_func), f)
+11 -14
View File
@@ -9,7 +9,7 @@ from aprsd.packets import core
from aprsd.utils import keepalive_collector from aprsd.utils import keepalive_collector
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger('APRSD') LOG = logging.getLogger("APRSD")
class APRSClient: class APRSClient:
@@ -20,8 +20,8 @@ class APRSClient:
connected = False connected = False
login_status = { login_status = {
'success': False, "success": False,
'message': None, "message": None,
} }
filter = None filter = None
lock = threading.Lock() lock = threading.Lock()
@@ -59,20 +59,17 @@ class APRSClient:
@property @property
def login_success(self): def login_success(self):
return self.login_status.get('success', False) return self.login_status.get("success", False)
@property @property
def login_failure(self): def login_failure(self):
return self.login_status['message'] return self.login_status["message"]
def set_filter(self, filter): def set_filter(self, filter):
self.filter = filter self.filter = filter
if self._client: if self._client:
self._client.set_filter(filter) self._client.set_filter(filter)
def get_filter(self):
return self.filter
@property @property
def client(self): def client(self):
if not self._client: if not self._client:
@@ -83,16 +80,16 @@ class APRSClient:
try: try:
self._client = self.setup_connection() self._client = self.setup_connection()
if self.filter: if self.filter:
LOG.info('Creating APRS client filter') LOG.info("Creating APRS client filter")
self._client.set_filter(self.filter) self._client.set_filter(self.filter)
except Exception as e: except Exception as e:
LOG.error(f'Failed to create APRS client: {e}') LOG.error(f"Failed to create APRS client: {e}")
self._client = None self._client = None
raise raise
def stop(self): def stop(self):
if self._client: if self._client:
LOG.info('Stopping client connection.') LOG.info("Stopping client connection.")
self._client.stop() self._client.stop()
def send(self, packet: core.Packet) -> None: def send(self, packet: core.Packet) -> None:
@@ -106,16 +103,16 @@ class APRSClient:
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def reset(self) -> None: def reset(self) -> None:
"""Call this to force a rebuild/reconnect.""" """Call this to force a rebuild/reconnect."""
LOG.info('Resetting client connection.') LOG.info("Resetting client connection.")
if self._client: if self._client:
self._client.close() self._client.close()
del self._client del self._client
self._create_client() self._create_client()
else: else:
LOG.warning('Client not initialized, nothing to reset.') LOG.warning("Client not initialized, nothing to reset.")
# Recreate the client # Recreate the client
LOG.info(f'Creating new client {self.client}') LOG.info(f"Creating new client {self.client}")
@abc.abstractmethod @abc.abstractmethod
def setup_connection(self): def setup_connection(self):
+28 -30
View File
@@ -3,60 +3,58 @@ import sys
import time import time
import aprslib import aprslib
import click
from aprslib.exceptions import LoginError from aprslib.exceptions import LoginError
import click
from oslo_config import cfg from oslo_config import cfg
import aprsd import aprsd
import aprsd.packets # noqa : F401 from aprsd import cli_helper, packets
from aprsd import ( from aprsd import conf # noqa : F401
cli_helper,
conf, # noqa : F401
packets,
)
from aprsd.client import client_factory from aprsd.client import client_factory
from aprsd.main import cli from aprsd.main import cli
import aprsd.packets # noqa : F401
from aprsd.packets import collector from aprsd.packets import collector
from aprsd.packets import log as packet_log from aprsd.packets import log as packet_log
from aprsd.threads import tx from aprsd.threads import tx
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger('APRSD') LOG = logging.getLogger("APRSD")
@cli.command() @cli.command()
@cli_helper.add_options(cli_helper.common_options) @cli_helper.add_options(cli_helper.common_options)
@click.option( @click.option(
'--aprs-login', "--aprs-login",
envvar='APRS_LOGIN', envvar="APRS_LOGIN",
show_envvar=True, show_envvar=True,
help='What callsign to send the message from. Defaults to config entry.', help="What callsign to send the message from. Defaults to config entry.",
) )
@click.option( @click.option(
'--aprs-password', "--aprs-password",
envvar='APRS_PASSWORD', envvar="APRS_PASSWORD",
show_envvar=True, show_envvar=True,
help='the APRS-IS password for APRS_LOGIN. Defaults to config entry.', help="the APRS-IS password for APRS_LOGIN. Defaults to config entry.",
) )
@click.option( @click.option(
'--no-ack', "--no-ack",
'-n', "-n",
is_flag=True, is_flag=True,
show_default=True, show_default=True,
default=False, default=False,
help="Don't wait for an ack, just sent it to APRS-IS and bail.", help="Don't wait for an ack, just sent it to APRS-IS and bail.",
) )
@click.option( @click.option(
'--wait-response', "--wait-response",
'-w', "-w",
is_flag=True, is_flag=True,
show_default=True, show_default=True,
default=False, default=False,
help='Wait for a response to the message?', help="Wait for a response to the message?",
) )
@click.option('--raw', default=None, help='Send a raw message. Implies --no-ack') @click.option("--raw", default=None, help="Send a raw message. Implies --no-ack")
@click.argument('tocallsign', required=True) @click.argument("tocallsign", required=True)
@click.argument('command', nargs=-1, required=True) @click.argument("command", nargs=-1, required=True)
@click.pass_context @click.pass_context
@cli_helper.process_standard_options @cli_helper.process_standard_options
def send_message( def send_message(
@@ -71,11 +69,11 @@ def send_message(
): ):
"""Send a message to a callsign via APRS_IS.""" """Send a message to a callsign via APRS_IS."""
global got_ack, got_response global got_ack, got_response
quiet = ctx.obj['quiet'] quiet = ctx.obj["quiet"]
if not aprs_login: if not aprs_login:
if CONF.aprs_network.login == conf.client.DEFAULT_LOGIN: if CONF.aprs_network.login == conf.client.DEFAULT_LOGIN:
click.echo('Must set --aprs_login or APRS_LOGIN') click.echo("Must set --aprs_login or APRS_LOGIN")
ctx.exit(-1) ctx.exit(-1)
return return
else: else:
@@ -83,15 +81,15 @@ def send_message(
if not aprs_password: if not aprs_password:
if not CONF.aprs_network.password: if not CONF.aprs_network.password:
click.echo('Must set --aprs-password or APRS_PASSWORD') click.echo("Must set --aprs-password or APRS_PASSWORD")
ctx.exit(-1) ctx.exit(-1)
return return
else: else:
aprs_password = CONF.aprs_network.password aprs_password = CONF.aprs_network.password
LOG.info(f'APRSD LISTEN Started version: {aprsd.__version__}') LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}")
if type(command) is tuple: if type(command) is tuple:
command = ' '.join(command) command = " ".join(command)
if not quiet: if not quiet:
if raw: if raw:
LOG.info(f"L'{aprs_login}' R'{raw}'") LOG.info(f"L'{aprs_login}' R'{raw}'")
@@ -131,7 +129,7 @@ def send_message(
sys.exit(0) sys.exit(0)
try: try:
client_factory.create().client # noqa: B018 client_factory.create().client
except LoginError: except LoginError:
sys.exit(-1) sys.exit(-1)
@@ -142,7 +140,7 @@ def send_message(
# message # message
if raw: if raw:
tx.send( tx.send(
packets.Packet(from_call='', to_call='', raw=raw), packets.Packet(from_call="", to_call="", raw=raw),
direct=True, direct=True,
) )
sys.exit(0) sys.exit(0)
@@ -166,7 +164,7 @@ def send_message(
aprs_client = client_factory.create().client aprs_client = client_factory.create().client
aprs_client.consumer(rx_packet, raw=False) aprs_client.consumer(rx_packet, raw=False)
except aprslib.exceptions.ConnectionDrop: except aprslib.exceptions.ConnectionDrop:
LOG.error('Connection dropped, reconnecting') LOG.error("Connection dropped, reconnecting")
time.sleep(5) time.sleep(5)
# Force the deletion of the client object connected to aprs # Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client() # This will cause a reconnect, next time client.get_client()
+63 -28
View File
@@ -12,24 +12,59 @@ from aprsd.client import client_factory
from aprsd.main import cli from aprsd.main import cli
from aprsd.packets import collector as packet_collector from aprsd.packets import collector as packet_collector
from aprsd.packets import seen_list from aprsd.packets import seen_list
from aprsd.threads import keepalive, registry, rx, service, tx from aprsd.threads import aprsd as aprsd_threads
from aprsd.threads import keepalive, registry, rx, tx
from aprsd.threads import stats as stats_thread from aprsd.threads import stats as stats_thread
from aprsd.utils import singleton
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger('APRSD') LOG = logging.getLogger("APRSD")
@singleton
class ServerThreads:
"""Registry for threads that the server command runs.
This enables extensions to register a thread to run during
the server command.
"""
def __init__(self):
self.threads: list[aprsd_threads.APRSDThread] = []
def register(self, thread: aprsd_threads.APRSDThread):
if not isinstance(thread, aprsd_threads.APRSDThread):
raise TypeError(f"Thread {thread} is not an APRSDThread")
self.threads.append(thread)
def unregister(self, thread: aprsd_threads.APRSDThread):
if not isinstance(thread, aprsd_threads.APRSDThread):
raise TypeError(f"Thread {thread} is not an APRSDThread")
self.threads.remove(thread)
def start(self):
"""Start all threads in the list."""
for thread in self.threads:
thread.start()
def join(self):
"""Join all the threads in the list"""
for thread in self.threads:
thread.join()
# main() ### # main() ###
@cli.command() @cli.command()
@cli_helper.add_options(cli_helper.common_options) @cli_helper.add_options(cli_helper.common_options)
@click.option( @click.option(
'-f', "-f",
'--flush', "--flush",
'flush', "flush",
is_flag=True, is_flag=True,
show_default=True, show_default=True,
default=False, default=False,
help='Flush out all old aged messages on disk.', help="Flush out all old aged messages on disk.",
) )
@click.pass_context @click.pass_context
@cli_helper.process_standard_options @cli_helper.process_standard_options
@@ -38,37 +73,37 @@ def server(ctx, flush):
signal.signal(signal.SIGINT, aprsd_main.signal_handler) signal.signal(signal.SIGINT, aprsd_main.signal_handler)
signal.signal(signal.SIGTERM, aprsd_main.signal_handler) signal.signal(signal.SIGTERM, aprsd_main.signal_handler)
service_threads = service.ServiceThreads() server_threads = ServerThreads()
level, msg = utils._check_version() level, msg = utils._check_version()
if level: if level:
LOG.warning(msg) LOG.warning(msg)
else: else:
LOG.info(msg) LOG.info(msg)
LOG.info(f'APRSD Started version: {aprsd.__version__}') LOG.info(f"APRSD Started version: {aprsd.__version__}")
# Initialize the client factory and create # Initialize the client factory and create
# The correct client object ready for use # The correct client object ready for use
if not client_factory.is_client_enabled(): if not client_factory.is_client_enabled():
LOG.error('No Clients are enabled in config.') LOG.error("No Clients are enabled in config.")
sys.exit(-1) sys.exit(-1)
# Make sure we have 1 client transport enabled # Make sure we have 1 client transport enabled
if not client_factory.is_client_enabled(): if not client_factory.is_client_enabled():
LOG.error('No Clients are enabled in config.') LOG.error("No Clients are enabled in config.")
sys.exit(-1) sys.exit(-1)
if not client_factory.is_client_configured(): if not client_factory.is_client_configured():
LOG.error('APRS client is not properly configured in config file.') LOG.error("APRS client is not properly configured in config file.")
sys.exit(-1) sys.exit(-1)
# Creates the client object # Creates the client object
LOG.info('Creating client connection') LOG.info("Creating client connection")
aprs_client = client_factory.create() aprs_client = client_factory.create()
LOG.info(aprs_client) LOG.info(aprs_client)
if not aprs_client.login_success: if not aprs_client.login_success:
# We failed to login, will just quit! # We failed to login, will just quit!
msg = f'Login Failure: {aprs_client.login_failure}' msg = f"Login Failure: {aprs_client.login_failure}"
LOG.error(msg) LOG.error(msg)
print(msg) print(msg)
sys.exit(-1) sys.exit(-1)
@@ -79,7 +114,7 @@ def server(ctx, flush):
# We register plugins first here so we can register each # We register plugins first here so we can register each
# plugins config options, so we can dump them all in the # plugins config options, so we can dump them all in the
# log file output. # log file output.
LOG.info('Loading Plugin Manager and registering plugins') LOG.info("Loading Plugin Manager and registering plugins")
plugin_manager = plugin.PluginManager() plugin_manager = plugin.PluginManager()
plugin_manager.setup_plugins(load_help_plugin=CONF.load_help_plugin) plugin_manager.setup_plugins(load_help_plugin=CONF.load_help_plugin)
@@ -87,10 +122,10 @@ def server(ctx, flush):
CONF.log_opt_values(LOG, logging.DEBUG) CONF.log_opt_values(LOG, logging.DEBUG)
message_plugins = plugin_manager.get_message_plugins() message_plugins = plugin_manager.get_message_plugins()
watchlist_plugins = plugin_manager.get_watchlist_plugins() watchlist_plugins = plugin_manager.get_watchlist_plugins()
LOG.info('Message Plugins enabled and running:') LOG.info("Message Plugins enabled and running:")
for p in message_plugins: for p in message_plugins:
LOG.info(p) LOG.info(p)
LOG.info('Watchlist Plugins enabled and running:') LOG.info("Watchlist Plugins enabled and running:")
for p in watchlist_plugins: for p in watchlist_plugins:
LOG.info(p) LOG.info(p)
@@ -100,37 +135,37 @@ def server(ctx, flush):
# Now load the msgTrack from disk if any # Now load the msgTrack from disk if any
if flush: if flush:
LOG.debug('Flushing All packet tracking objects.') LOG.debug("Flushing All packet tracking objects.")
packet_collector.PacketCollector().flush() packet_collector.PacketCollector().flush()
else: else:
# Try and load saved MsgTrack list # Try and load saved MsgTrack list
LOG.debug('Loading saved packet tracking data.') LOG.debug("Loading saved packet tracking data.")
packet_collector.PacketCollector().load() packet_collector.PacketCollector().load()
# Now start all the main processing threads. # Now start all the main processing threads.
service_threads.register(keepalive.KeepAliveThread()) server_threads.register(keepalive.KeepAliveThread())
service_threads.register(stats_thread.APRSDStatsStoreThread()) server_threads.register(stats_thread.APRSDStatsStoreThread())
service_threads.register( server_threads.register(
rx.APRSDRXThread( rx.APRSDRXThread(
packet_queue=threads.packet_queue, packet_queue=threads.packet_queue,
), ),
) )
service_threads.register( server_threads.register(
rx.APRSDPluginProcessPacketThread( rx.APRSDPluginProcessPacketThread(
packet_queue=threads.packet_queue, packet_queue=threads.packet_queue,
), ),
) )
if CONF.enable_beacon: if CONF.enable_beacon:
LOG.info('Beacon Enabled. Starting Beacon thread.') LOG.info("Beacon Enabled. Starting Beacon thread.")
service_threads.register(tx.BeaconSendThread()) server_threads.register(tx.BeaconSendThread())
if CONF.aprs_registry.enabled: if CONF.aprs_registry.enabled:
LOG.info('Registry Enabled. Starting Registry thread.') LOG.info("Registry Enabled. Starting Registry thread.")
service_threads.register(registry.APRSRegistryThread()) server_threads.register(registry.APRSRegistryThread())
service_threads.start() server_threads.start()
service_threads.join() server_threads.join()
return 0 return 0
-5
View File
@@ -54,11 +54,6 @@ logging_opts = [
default=True, default=True,
help='Enable ANSI color codes in logging', help='Enable ANSI color codes in logging',
), ),
cfg.BoolOpt(
'enable_console_stdout',
default=True,
help='Enable logging to the console/stdout.',
),
] ]
+10 -16
View File
@@ -51,7 +51,7 @@ class InterceptHandler(logging.Handler):
# Setup the log faciility # Setup the log faciility
# to disable log to stdout, but still log to file # to disable log to stdout, but still log to file
# use the --quiet option on the cmdln # use the --quiet option on the cmdln
def setup_logging(loglevel=None, quiet=False, custom_handler=None): def setup_logging(loglevel=None, quiet=False):
if not loglevel: if not loglevel:
log_level = CONF.logging.log_level log_level = CONF.logging.log_level
else: else:
@@ -84,18 +84,15 @@ def setup_logging(loglevel=None, quiet=False, custom_handler=None):
logging.getLogger(name).handlers = [] logging.getLogger(name).handlers = []
logging.getLogger(name).propagate = name not in disable_list logging.getLogger(name).propagate = name not in disable_list
handlers = [] handlers = [
if CONF.logging.enable_console_stdout: {
handlers.append( 'sink': sys.stdout,
{ 'serialize': False,
'sink': sys.stdout, 'format': CONF.logging.logformat,
'serialize': False, 'colorize': CONF.logging.enable_color,
'format': CONF.logging.logformat, 'level': log_level,
'colorize': CONF.logging.enable_color, },
'level': log_level, ]
},
)
if CONF.logging.logfile: if CONF.logging.logfile:
handlers.append( handlers.append(
{ {
@@ -107,9 +104,6 @@ def setup_logging(loglevel=None, quiet=False, custom_handler=None):
}, },
) )
if custom_handler:
handlers.append(custom_handler)
# configure loguru # configure loguru
logger.configure(handlers=handlers) logger.configure(handlers=handlers)
logger.level('DEBUG', color='<fg #BABABA>') logger.level('DEBUG', color='<fg #BABABA>')
+22 -22
View File
@@ -39,8 +39,8 @@ from aprsd.stats import collector
# setup the global logger # setup the global logger
# log.basicConfig(level=log.DEBUG) # level=10 # log.basicConfig(level=log.DEBUG) # level=10
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger('APRSD') LOG = logging.getLogger("APRSD")
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
flask_enabled = False flask_enabled = False
@@ -68,18 +68,18 @@ def main():
# First import all the possible commands for the CLI # First import all the possible commands for the CLI
# The commands themselves live in the cmds directory # The commands themselves live in the cmds directory
load_commands() load_commands()
utils.load_entry_points('aprsd.extension') utils.load_entry_points("aprsd.extension")
cli(auto_envvar_prefix='APRSD') cli(auto_envvar_prefix="APRSD")
def signal_handler(sig, frame): def signal_handler(sig, frame):
global flask_enabled global flask_enabled
click.echo('signal_handler: called') click.echo("signal_handler: called")
threads.APRSDThreadList().stop_all() threads.APRSDThreadList().stop_all()
if 'subprocess' not in str(frame): if "subprocess" not in str(frame):
LOG.info( LOG.info(
'Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}'.format( "Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
datetime.datetime.now(), datetime.datetime.now(),
), ),
) )
@@ -91,7 +91,7 @@ def signal_handler(sig, frame):
packets.PacketList().save() packets.PacketList().save()
collector.Collector().collect() collector.Collector().collect()
except Exception as e: except Exception as e:
LOG.error(f'Failed to save data: {e}') LOG.error(f"Failed to save data: {e}")
sys.exit(0) sys.exit(0)
# signal.signal(signal.SIGTERM, sys.exit(0)) # signal.signal(signal.SIGTERM, sys.exit(0))
# sys.exit(0) # sys.exit(0)
@@ -108,9 +108,9 @@ def check_version(ctx):
"""Check this version against the latest in pypi.org.""" """Check this version against the latest in pypi.org."""
level, msg = utils._check_version() level, msg = utils._check_version()
if level: if level:
click.secho(msg, fg='yellow') click.secho(msg, fg="yellow")
else: else:
click.secho(msg, fg='green') click.secho(msg, fg="green")
@cli.command() @cli.command()
@@ -124,12 +124,12 @@ def sample_config(ctx):
if sys.version_info < (3, 10): if sys.version_info < (3, 10):
all = imp.entry_points() all = imp.entry_points()
selected = [] selected = []
if 'oslo.config.opts' in all: if "oslo.config.opts" in all:
for x in all['oslo.config.opts']: for x in all["oslo.config.opts"]:
if x.group == 'oslo.config.opts': if x.group == "oslo.config.opts":
selected.append(x) selected.append(x)
else: else:
selected = imp.entry_points(group='oslo.config.opts') selected = imp.entry_points(group="oslo.config.opts")
return selected return selected
@@ -139,23 +139,23 @@ def sample_config(ctx):
# selected = imp.entry_points(group="oslo.config.opts") # selected = imp.entry_points(group="oslo.config.opts")
selected = _get_selected_entry_points() selected = _get_selected_entry_points()
for entry in selected: for entry in selected:
if 'aprsd' in entry.name: if "aprsd" in entry.name:
args.append('--namespace') args.append("--namespace")
args.append(entry.name) args.append(entry.name)
return args return args
args = get_namespaces() args = get_namespaces()
config_version = metadata_version('oslo.config') config_version = metadata_version("oslo.config")
logging.basicConfig(level=logging.WARN) logging.basicConfig(level=logging.WARN)
conf = cfg.ConfigOpts() conf = cfg.ConfigOpts()
generator.register_cli_opts(conf) generator.register_cli_opts(conf)
try: try:
conf(args, version=config_version) conf(args, version=config_version)
except cfg.RequiredOptError as ex: except cfg.RequiredOptError:
conf.print_help() conf.print_help()
if not sys.argv[1:]: if not sys.argv[1:]:
raise SystemExit from ex raise SystemExit
raise raise
generator.generate(conf) generator.generate(conf)
return return
@@ -165,9 +165,9 @@ def sample_config(ctx):
@click.pass_context @click.pass_context
def version(ctx): def version(ctx):
"""Show the APRSD version.""" """Show the APRSD version."""
click.echo(click.style('APRSD Version : ', fg='white'), nl=False) click.echo(click.style("APRSD Version : ", fg="white"), nl=False)
click.secho(f'{aprsd.__version__}', fg='yellow', bold=True) click.secho(f"{aprsd.__version__}", fg="yellow", bold=True)
if __name__ == '__main__': if __name__ == "__main__":
main() main()
+52 -52
View File
@@ -17,24 +17,24 @@ from aprsd.packets import watch_list
# setup the global logger # setup the global logger
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger('APRSD') LOG = logging.getLogger("APRSD")
CORE_MESSAGE_PLUGINS = [ CORE_MESSAGE_PLUGINS = [
'aprsd.plugins.email.EmailPlugin', "aprsd.plugins.email.EmailPlugin",
'aprsd.plugins.fortune.FortunePlugin', "aprsd.plugins.fortune.FortunePlugin",
'aprsd.plugins.location.LocationPlugin', "aprsd.plugins.location.LocationPlugin",
'aprsd.plugins.ping.PingPlugin', "aprsd.plugins.ping.PingPlugin",
'aprsd.plugins.time.TimePlugin', "aprsd.plugins.time.TimePlugin",
'aprsd.plugins.weather.USWeatherPlugin', "aprsd.plugins.weather.USWeatherPlugin",
'aprsd.plugins.version.VersionPlugin', "aprsd.plugins.version.VersionPlugin",
] ]
CORE_NOTIFY_PLUGINS = [ CORE_NOTIFY_PLUGINS = [
'aprsd.plugins.notify.NotifySeenPlugin', "aprsd.plugins.notify.NotifySeenPlugin",
] ]
hookspec = pluggy.HookspecMarker('aprsd') hookspec = pluggy.HookspecMarker("aprsd")
hookimpl = pluggy.HookimplMarker('aprsd') hookimpl = pluggy.HookimplMarker("aprsd")
class APRSDPluginSpec: class APRSDPluginSpec:
@@ -76,14 +76,14 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
else: else:
LOG.error( LOG.error(
"Can't start thread {}:{}, Must be a child " "Can't start thread {}:{}, Must be a child "
'of aprsd.threads.APRSDThread'.format( "of aprsd.threads.APRSDThread".format(
self, self,
thread, thread,
), ),
) )
except Exception: except Exception:
LOG.error( LOG.error(
'Failed to start threads for plugin {}'.format( "Failed to start threads for plugin {}".format(
self, self,
), ),
) )
@@ -93,7 +93,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
return self.message_counter return self.message_counter
def help(self) -> str: def help(self) -> str:
return 'Help!' return "Help!"
@abc.abstractmethod @abc.abstractmethod
def setup(self): def setup(self):
@@ -147,10 +147,10 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
# make sure the timeout is set or this doesn't work # make sure the timeout is set or this doesn't work
if watch_list: if watch_list:
aprs_client = client.client_factory.create().client aprs_client = client.client_factory.create().client
filter_str = 'b/{}'.format('/'.join(watch_list)) filter_str = "b/{}".format("/".join(watch_list))
aprs_client.set_filter(filter_str) aprs_client.set_filter(filter_str)
else: else:
LOG.warning('Watch list enabled, but no callsigns set.') LOG.warning("Watch list enabled, but no callsigns set.")
@hookimpl @hookimpl
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket: def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
@@ -164,7 +164,7 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
result = self.process(packet) result = self.process(packet)
except Exception as ex: except Exception as ex:
LOG.error( LOG.error(
'Plugin {} failed to process packet {}'.format( "Plugin {} failed to process packet {}".format(
self.__class__, self.__class__,
ex, ex,
), ),
@@ -172,7 +172,7 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
if result: if result:
self.tx_inc() self.tx_inc()
else: else:
LOG.warning(f'{self.__class__} plugin is not enabled') LOG.warning(f"{self.__class__} plugin is not enabled")
return result return result
@@ -196,7 +196,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
raise NotImplementedError raise NotImplementedError
def help(self): def help(self):
return '{}: {}'.format( return "{}: {}".format(
self.command_name.lower(), self.command_name.lower(),
self.command_regex, self.command_regex,
) )
@@ -207,7 +207,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
@hookimpl @hookimpl
def filter(self, packet: packets.MessagePacket) -> str | packets.MessagePacket: def filter(self, packet: packets.MessagePacket) -> str | packets.MessagePacket:
LOG.debug(f'{self.__class__.__name__} called') LOG.debug(f"{self.__class__.__name__} called")
if not self.enabled: if not self.enabled:
result = f"{self.__class__.__name__} isn't enabled" result = f"{self.__class__.__name__} isn't enabled"
LOG.warning(result) LOG.warning(result)
@@ -215,7 +215,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
if not isinstance(packet, packets.MessagePacket): if not isinstance(packet, packets.MessagePacket):
LOG.warning( LOG.warning(
f'{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring' f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring"
) )
return packets.NULL_MESSAGE return packets.NULL_MESSAGE
@@ -237,7 +237,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
result = self.process(packet) result = self.process(packet)
except Exception as ex: except Exception as ex:
LOG.error( LOG.error(
'Plugin {} failed to process packet {}'.format( "Plugin {} failed to process packet {}".format(
self.__class__, self.__class__,
ex, ex,
), ),
@@ -254,7 +254,7 @@ class APRSFIKEYMixin:
def ensure_aprs_fi_key(self): def ensure_aprs_fi_key(self):
if not CONF.aprs_fi.apiKey: if not CONF.aprs_fi.apiKey:
LOG.error('Config aprs_fi.apiKey is not set') LOG.error("Config aprs_fi.apiKey is not set")
self.enabled = False self.enabled = False
else: else:
self.enabled = True self.enabled = True
@@ -266,25 +266,25 @@ class HelpPlugin(APRSDRegexCommandPluginBase):
This plugin is in this file to prevent a circular import. This plugin is in this file to prevent a circular import.
""" """
command_regex = '^[hH]' command_regex = "^[hH]"
command_name = 'help' command_name = "help"
def help(self): def help(self):
return 'Help: send APRS help or help <plugin>' return "Help: send APRS help or help <plugin>"
def process(self, packet: packets.MessagePacket): def process(self, packet: packets.MessagePacket):
LOG.info('HelpPlugin') LOG.info("HelpPlugin")
# fromcall = packet.get("from") # fromcall = packet.get("from")
message = packet.message_text message = packet.message_text
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
a = re.search(r'^.*\s+(.*)', message) a = re.search(r"^.*\s+(.*)", message)
command_name = None command_name = None
if a is not None: if a is not None:
command_name = a.group(1).lower() command_name = a.group(1).lower()
pm = PluginManager() pm = PluginManager()
if command_name and '?' not in command_name: if command_name and "?" not in command_name:
# user wants help for a specific plugin # user wants help for a specific plugin
reply = None reply = None
for p in pm.get_plugins(): for p in pm.get_plugins():
@@ -303,20 +303,20 @@ class HelpPlugin(APRSDRegexCommandPluginBase):
LOG.debug(p) LOG.debug(p)
if p.enabled and isinstance(p, APRSDRegexCommandPluginBase): if p.enabled and isinstance(p, APRSDRegexCommandPluginBase):
name = p.command_name.lower() name = p.command_name.lower()
if name not in list and 'help' not in name: if name not in list and "help" not in name:
list.append(name) list.append(name)
list.sort() list.sort()
reply = ' '.join(list) reply = " ".join(list)
lines = textwrap.wrap(reply, 60) lines = textwrap.wrap(reply, 60)
replies = ["Send APRS MSG of 'help' or 'help <plugin>'"] replies = ["Send APRS MSG of 'help' or 'help <plugin>'"]
for line in lines: for line in lines:
replies.append(f'plugins: {line}') replies.append(f"plugins: {line}")
for entry in replies: for entry in replies:
LOG.debug(f'{len(entry)} {entry}') LOG.debug(f"{len(entry)} {entry}")
LOG.debug(f'{replies}') LOG.debug(f"{replies}")
return replies return replies
@@ -341,17 +341,17 @@ class PluginManager:
return cls._instance return cls._instance
def _init(self): def _init(self):
self._pluggy_pm = pluggy.PluginManager('aprsd') self._pluggy_pm = pluggy.PluginManager("aprsd")
self._pluggy_pm.add_hookspecs(APRSDPluginSpec) self._pluggy_pm.add_hookspecs(APRSDPluginSpec)
# For the watchlist plugins # For the watchlist plugins
self._watchlist_pm = pluggy.PluginManager('aprsd') self._watchlist_pm = pluggy.PluginManager("aprsd")
self._watchlist_pm.add_hookspecs(APRSDPluginSpec) self._watchlist_pm.add_hookspecs(APRSDPluginSpec)
def stats(self, serializable=False) -> dict: def stats(self, serializable=False) -> dict:
"""Collect and return stats for all plugins.""" """Collect and return stats for all plugins."""
def full_name_with_qualname(obj): def full_name_with_qualname(obj):
return '{}.{}'.format( return "{}.{}".format(
obj.__class__.__module__, obj.__class__.__module__,
obj.__class__.__qualname__, obj.__class__.__qualname__,
) )
@@ -361,10 +361,10 @@ class PluginManager:
if plugins: if plugins:
for p in plugins: for p in plugins:
plugin_stats[full_name_with_qualname(p)] = { plugin_stats[full_name_with_qualname(p)] = {
'enabled': p.enabled, "enabled": p.enabled,
'rx': p.rx_count, "rx": p.rx_count,
'tx': p.tx_count, "tx": p.tx_count,
'version': p.version, "version": p.version,
} }
return plugin_stats return plugin_stats
@@ -392,19 +392,19 @@ class PluginManager:
module_name = None module_name = None
class_name = None class_name = None
try: try:
module_name, class_name = module_class_string.rsplit('.', 1) module_name, class_name = module_class_string.rsplit(".", 1)
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
# Commented out because the email thread starts in a different context # Commented out because the email thread starts in a different context
# and hence gives a different singleton for the EmailStats # and hence gives a different singleton for the EmailStats
# module = importlib.reload(module) # module = importlib.reload(module)
except Exception as ex: except Exception as ex:
if not module_name: if not module_name:
LOG.error(f'Failed to load Plugin {module_class_string}') LOG.error(f"Failed to load Plugin {module_class_string}")
else: else:
LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'") LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'")
return return
assert hasattr(module, class_name), 'class {} is not in {}'.format( assert hasattr(module, class_name), "class {} is not in {}".format(
class_name, class_name,
module_name, module_name,
) )
@@ -412,7 +412,7 @@ class PluginManager:
# class_name, module_name)) # class_name, module_name))
cls = getattr(module, class_name) cls = getattr(module, class_name)
if super_cls is not None: if super_cls is not None:
assert issubclass(cls, super_cls), 'class {} should inherit from {}'.format( assert issubclass(cls, super_cls), "class {} should inherit from {}".format(
class_name, class_name,
super_cls.__name__, super_cls.__name__,
) )
@@ -444,7 +444,7 @@ class PluginManager:
self._watchlist_pm.register(plugin_obj) self._watchlist_pm.register(plugin_obj)
else: else:
LOG.warning( LOG.warning(
f'Plugin {plugin_obj.__class__.__name__} is disabled' f"Plugin {plugin_obj.__class__.__name__} is disabled"
) )
elif isinstance(plugin_obj, APRSDRegexCommandPluginBase): elif isinstance(plugin_obj, APRSDRegexCommandPluginBase):
if plugin_obj.enabled: if plugin_obj.enabled:
@@ -458,7 +458,7 @@ class PluginManager:
self._pluggy_pm.register(plugin_obj) self._pluggy_pm.register(plugin_obj)
else: else:
LOG.warning( LOG.warning(
f'Plugin {plugin_obj.__class__.__name__} is disabled' f"Plugin {plugin_obj.__class__.__name__} is disabled"
) )
elif isinstance(plugin_obj, APRSDPluginBase): elif isinstance(plugin_obj, APRSDPluginBase):
if plugin_obj.enabled: if plugin_obj.enabled:
@@ -471,7 +471,7 @@ class PluginManager:
self._pluggy_pm.register(plugin_obj) self._pluggy_pm.register(plugin_obj)
else: else:
LOG.warning( LOG.warning(
f'Plugin {plugin_obj.__class__.__name__} is disabled' f"Plugin {plugin_obj.__class__.__name__} is disabled"
) )
except Exception as ex: except Exception as ex:
LOG.error(f"Couldn't load plugin '{plugin_name}'") LOG.error(f"Couldn't load plugin '{plugin_name}'")
@@ -485,11 +485,11 @@ class PluginManager:
def setup_plugins( def setup_plugins(
self, self,
load_help_plugin=True, load_help_plugin=True,
plugin_list=None, plugin_list=[],
): ):
"""Create the plugin manager and register plugins.""" """Create the plugin manager and register plugins."""
LOG.info('Loading APRSD Plugins') LOG.info("Loading APRSD Plugins")
# Help plugin is always enabled. # Help plugin is always enabled.
if load_help_plugin: if load_help_plugin:
_help = HelpPlugin() _help = HelpPlugin()
@@ -509,7 +509,7 @@ class PluginManager:
for p_name in CORE_MESSAGE_PLUGINS: for p_name in CORE_MESSAGE_PLUGINS:
self._load_plugin(p_name) self._load_plugin(p_name)
LOG.info('Completed Plugin Loading.') LOG.info("Completed Plugin Loading.")
def run(self, packet: packets.MessagePacket): def run(self, packet: packets.MessagePacket):
"""Execute all the plugins run method.""" """Execute all the plugins run method."""
@@ -524,7 +524,7 @@ class PluginManager:
"""Stop all threads created by all plugins.""" """Stop all threads created by all plugins."""
with self.lock: with self.lock:
for p in self.get_plugins(): for p in self.get_plugins():
if hasattr(p, 'stop_threads'): if hasattr(p, "stop_threads"):
p.stop_threads() p.stop_threads()
def register_msg(self, obj): def register_msg(self, obj):
+21 -20
View File
@@ -4,20 +4,21 @@ import logging
import requests import requests
LOG = logging.getLogger('APRSD')
LOG = logging.getLogger("APRSD")
def get_aprs_fi(api_key, callsign): def get_aprs_fi(api_key, callsign):
LOG.debug(f"Fetch aprs.fi location for '{callsign}'") LOG.debug(f"Fetch aprs.fi location for '{callsign}'")
try: try:
url = ( url = (
'http://api.aprs.fi/api/get?&what=loc&apikey={}&format=json&name={}'.format( "http://api.aprs.fi/api/get?"
api_key, callsign "&what=loc&apikey={}&format=json"
) "&name={}".format(api_key, callsign)
) )
response = requests.get(url) response = requests.get(url)
except Exception as e: except Exception:
raise Exception('Failed to get aprs.fi location') from e raise Exception("Failed to get aprs.fi location")
else: else:
response.raise_for_status() response.raise_for_status()
return json.loads(response.text) return json.loads(response.text)
@@ -25,22 +26,22 @@ def get_aprs_fi(api_key, callsign):
def get_weather_gov_for_gps(lat, lon): def get_weather_gov_for_gps(lat, lon):
# FIXME(hemna) This is currently BROKEN # FIXME(hemna) This is currently BROKEN
LOG.debug(f'Fetch station at {lat}, {lon}') LOG.debug(f"Fetch station at {lat}, {lon}")
headers = requests.utils.default_headers() headers = requests.utils.default_headers()
headers.update( headers.update(
{'User-Agent': '(aprsd, waboring@hemna.com)'}, {"User-Agent": "(aprsd, waboring@hemna.com)"},
) )
try: try:
url2 = ( url2 = (
'https://forecast.weather.gov/MapClick.php?lat=%s' "https://forecast.weather.gov/MapClick.php?lat=%s"
'&lon=%s&FcstType=json' % (lat, lon) "&lon=%s&FcstType=json" % (lat, lon)
# f"https://api.weather.gov/points/{lat},{lon}" # f"https://api.weather.gov/points/{lat},{lon}"
) )
LOG.debug(f"Fetching weather '{url2}'") LOG.debug(f"Fetching weather '{url2}'")
response = requests.get(url2, headers=headers) response = requests.get(url2, headers=headers)
except Exception as e: except Exception as e:
LOG.error(e) LOG.error(e)
raise Exception('Failed to get weather') from e raise Exception("Failed to get weather")
else: else:
response.raise_for_status() response.raise_for_status()
return json.loads(response.text) return json.loads(response.text)
@@ -49,25 +50,25 @@ def get_weather_gov_for_gps(lat, lon):
def get_weather_gov_metar(station): def get_weather_gov_metar(station):
LOG.debug(f"Fetch metar for station '{station}'") LOG.debug(f"Fetch metar for station '{station}'")
try: try:
url = 'https://api.weather.gov/stations/{}/observations/latest'.format( url = "https://api.weather.gov/stations/{}/observations/latest".format(
station, station,
) )
response = requests.get(url) response = requests.get(url)
except Exception as e: except Exception:
raise Exception('Failed to fetch metar') from e raise Exception("Failed to fetch metar")
else: else:
response.raise_for_status() response.raise_for_status()
return json.loads(response) return json.loads(response)
def fetch_openweathermap(api_key, lat, lon, units='metric', exclude=None): def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
LOG.debug(f'Fetch openweathermap for {lat}, {lon}') LOG.debug(f"Fetch openweathermap for {lat}, {lon}")
if not exclude: if not exclude:
exclude = 'minutely,hourly,daily,alerts' exclude = "minutely,hourly,daily,alerts"
try: try:
url = ( url = (
'https://api.openweathermap.org/data/2.5/onecall?' "https://api.openweathermap.org/data/2.5/onecall?"
'lat={}&lon={}&appid={}&units={}&exclude={}'.format( "lat={}&lon={}&appid={}&units={}&exclude={}".format(
lat, lat,
lon, lon,
api_key, api_key,
@@ -79,7 +80,7 @@ def fetch_openweathermap(api_key, lat, lon, units='metric', exclude=None):
response = requests.get(url) response = requests.get(url)
except Exception as e: except Exception as e:
LOG.error(e) LOG.error(e)
raise Exception('Failed to get weather') from e raise Exception("Failed to get weather")
else: else:
response.raise_for_status() response.raise_for_status()
return json.loads(response.text) return json.loads(response.text)
+109 -110
View File
@@ -9,7 +9,7 @@ from aprsd import plugin, plugin_utils
from aprsd.utils import trace from aprsd.utils import trace
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger('APRSD') LOG = logging.getLogger("APRSD")
class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin): class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
@@ -26,22 +26,22 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
""" """
# command_regex = r"^([w][x]|[w][x]\s|weather)" # command_regex = r"^([w][x]|[w][x]\s|weather)"
command_regex = r'^[wW]' command_regex = r"^[wW]"
command_name = 'USWeather' command_name = "USWeather"
short_description = 'Provide USA only weather of GPS Beacon location' short_description = "Provide USA only weather of GPS Beacon location"
def setup(self): def setup(self):
self.ensure_aprs_fi_key() self.ensure_aprs_fi_key()
@trace.trace @trace.trace
def process(self, packet): def process(self, packet):
LOG.info('Weather Plugin') LOG.info("Weather Plugin")
fromcall = packet.from_call fromcall = packet.from_call
message = packet.get('message_text', None) message = packet.get("message_text", None)
# message = packet.get("message_text", None) # message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
a = re.search(r'^.*\s+(.*)', message) a = re.search(r"^.*\s+(.*)", message)
if a is not None: if a is not None:
searchcall = a.group(1) searchcall = a.group(1)
searchcall = searchcall.upper() searchcall = searchcall.upper()
@@ -51,34 +51,34 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
try: try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex: except Exception as ex:
LOG.error(f'Failed to fetch aprs.fi data {ex}') LOG.error(f"Failed to fetch aprs.fi data {ex}")
return 'Failed to fetch aprs.fi location' return "Failed to fetch aprs.fi location"
LOG.debug(f'LocationPlugin: aprs_data = {aprs_data}') LOG.debug(f"LocationPlugin: aprs_data = {aprs_data}")
if not len(aprs_data['entries']): if not len(aprs_data["entries"]):
LOG.error("Didn't get any entries from aprs.fi") LOG.error("Didn't get any entries from aprs.fi")
return 'Failed to fetch aprs.fi location' return "Failed to fetch aprs.fi location"
lat = aprs_data['entries'][0]['lat'] lat = aprs_data["entries"][0]["lat"]
lon = aprs_data['entries'][0]['lng'] lon = aprs_data["entries"][0]["lng"]
try: try:
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon) wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
except Exception as ex: except Exception as ex:
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'") LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
return 'Unable to get weather' return "Unable to get weather"
LOG.info(f'WX data {wx_data}') LOG.info(f"WX data {wx_data}")
reply = ( reply = (
'%sF(%sF/%sF) %s. %s, %s.' "%sF(%sF/%sF) %s. %s, %s."
% ( % (
wx_data['currentobservation']['Temp'], wx_data["currentobservation"]["Temp"],
wx_data['data']['temperature'][0], wx_data["data"]["temperature"][0],
wx_data['data']['temperature'][1], wx_data["data"]["temperature"][1],
wx_data['data']['weather'][0], wx_data["data"]["weather"][0],
wx_data['time']['startPeriodName'][1], wx_data["time"]["startPeriodName"][1],
wx_data['data']['weather'][1], wx_data["data"]["weather"][1],
) )
).rstrip() ).rstrip()
LOG.debug(f"reply: '{reply}' ") LOG.debug(f"reply: '{reply}' ")
@@ -100,31 +100,31 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
""" """
command_regex = r'^([m]|[M]|[m]\s|metar)' command_regex = r"^([m]|[M]|[m]\s|metar)"
command_name = 'USMetar' command_name = "USMetar"
short_description = 'USA only METAR of GPS Beacon location' short_description = "USA only METAR of GPS Beacon location"
def setup(self): def setup(self):
self.ensure_aprs_fi_key() self.ensure_aprs_fi_key()
@trace.trace @trace.trace
def process(self, packet): def process(self, packet):
fromcall = packet.get('from') fromcall = packet.get("from")
message = packet.get('message_text', None) message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
LOG.info(f"WX Plugin '{message}'") LOG.info(f"WX Plugin '{message}'")
a = re.search(r'^.*\s+(.*)', message) a = re.search(r"^.*\s+(.*)", message)
if a is not None: if a is not None:
searchcall = a.group(1) searchcall = a.group(1)
station = searchcall.upper() station = searchcall.upper()
try: try:
resp = plugin_utils.get_weather_gov_metar(station) resp = plugin_utils.get_weather_gov_metar(station)
except Exception as e: except Exception as e:
LOG.debug(f'Weather failed with: {str(e)}') LOG.debug(f"Weather failed with: {str(e)}")
reply = 'Unable to find station METAR' reply = "Unable to find station METAR"
else: else:
station_data = json.loads(resp.text) station_data = json.loads(resp.text)
reply = station_data['properties']['rawMessage'] reply = station_data["properties"]["rawMessage"]
return reply return reply
else: else:
@@ -136,36 +136,36 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
try: try:
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall) aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
except Exception as ex: except Exception as ex:
LOG.error(f'Failed to fetch aprs.fi data {ex}') LOG.error(f"Failed to fetch aprs.fi data {ex}")
return 'Failed to fetch aprs.fi location' return "Failed to fetch aprs.fi location"
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data)) # LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
if not len(aprs_data['entries']): if not len(aprs_data["entries"]):
LOG.error('Found no entries from aprs.fi!') LOG.error("Found no entries from aprs.fi!")
return 'Failed to fetch aprs.fi location' return "Failed to fetch aprs.fi location"
lat = aprs_data['entries'][0]['lat'] lat = aprs_data["entries"][0]["lat"]
lon = aprs_data['entries'][0]['lng'] lon = aprs_data["entries"][0]["lng"]
try: try:
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon) wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
except Exception as ex: except Exception as ex:
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'") LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
return 'Unable to metar find station.' return "Unable to metar find station."
if wx_data['location']['metar']: if wx_data["location"]["metar"]:
station = wx_data['location']['metar'] station = wx_data["location"]["metar"]
try: try:
resp = plugin_utils.get_weather_gov_metar(station) resp = plugin_utils.get_weather_gov_metar(station)
except Exception as e: except Exception as e:
LOG.debug(f'Weather failed with: {str(e)}') LOG.debug(f"Weather failed with: {str(e)}")
reply = 'Failed to get Metar' reply = "Failed to get Metar"
else: else:
station_data = json.loads(resp.text) station_data = json.loads(resp.text)
reply = station_data['properties']['rawMessage'] reply = station_data["properties"]["rawMessage"]
else: else:
# Couldn't find a station # Couldn't find a station
reply = 'No Metar station found' reply = "No Metar station found"
return reply return reply
@@ -190,36 +190,35 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
""" """
# command_regex = r"^([w][x]|[w][x]\s|weather)" # command_regex = r"^([w][x]|[w][x]\s|weather)"
command_regex = r'^[wW]' command_regex = r"^[wW]"
command_name = 'OpenWeatherMap' command_name = "OpenWeatherMap"
short_description = 'OpenWeatherMap weather of GPS Beacon location' short_description = "OpenWeatherMap weather of GPS Beacon location"
def setup(self): def setup(self):
if not CONF.owm_weather_plugin.apiKey: if not CONF.owm_weather_plugin.apiKey:
LOG.error('Config.owm_weather_plugin.apiKey is not set. Disabling') LOG.error("Config.owm_weather_plugin.apiKey is not set. Disabling")
self.enabled = False self.enabled = False
else: else:
self.enabled = True self.enabled = True
def help(self): def help(self):
_help = [ _help = [
'openweathermap: Send {} to get weather from your location'.format( "openweathermap: Send {} to get weather " "from your location".format(
self.command_regex
),
'openweathermap: Send {} <callsign> to get weather from <callsign>'.format(
self.command_regex self.command_regex
), ),
"openweathermap: Send {} <callsign> to get "
"weather from <callsign>".format(self.command_regex),
] ]
return _help return _help
@trace.trace @trace.trace
def process(self, packet): def process(self, packet):
fromcall = packet.get('from_call') fromcall = packet.get("from_call")
message = packet.get('message_text', None) message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
LOG.info(f"OWMWeather Plugin '{message}'") LOG.info(f"OWMWeather Plugin '{message}'")
a = re.search(r'^.*\s+(.*)', message) a = re.search(r"^.*\s+(.*)", message)
if a is not None: if a is not None:
searchcall = a.group(1) searchcall = a.group(1)
searchcall = searchcall.upper() searchcall = searchcall.upper()
@@ -231,16 +230,16 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
try: try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex: except Exception as ex:
LOG.error(f'Failed to fetch aprs.fi data {ex}') LOG.error(f"Failed to fetch aprs.fi data {ex}")
return 'Failed to fetch location' return "Failed to fetch location"
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data)) # LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
if not len(aprs_data['entries']): if not len(aprs_data["entries"]):
LOG.error('Found no entries from aprs.fi!') LOG.error("Found no entries from aprs.fi!")
return 'Failed to fetch location' return "Failed to fetch location"
lat = aprs_data['entries'][0]['lat'] lat = aprs_data["entries"][0]["lat"]
lon = aprs_data['entries'][0]['lng'] lon = aprs_data["entries"][0]["lng"]
units = CONF.units units = CONF.units
api_key = CONF.owm_weather_plugin.apiKey api_key = CONF.owm_weather_plugin.apiKey
@@ -250,40 +249,40 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
lat, lat,
lon, lon,
units=units, units=units,
exclude='minutely,hourly', exclude="minutely,hourly",
) )
except Exception as ex: except Exception as ex:
LOG.error(f"Couldn't fetch openweathermap api '{ex}'") LOG.error(f"Couldn't fetch openweathermap api '{ex}'")
# default to UTC # default to UTC
return 'Unable to get weather' return "Unable to get weather"
if units == 'metric': if units == "metric":
degree = 'C' degree = "C"
else: else:
degree = 'F' degree = "F"
if 'wind_gust' in wx_data['current']: if "wind_gust" in wx_data["current"]:
wind = '{:.0f}@{}G{:.0f}'.format( wind = "{:.0f}@{}G{:.0f}".format(
wx_data['current']['wind_speed'], wx_data["current"]["wind_speed"],
wx_data['current']['wind_deg'], wx_data["current"]["wind_deg"],
wx_data['current']['wind_gust'], wx_data["current"]["wind_gust"],
) )
else: else:
wind = '{:.0f}@{}'.format( wind = "{:.0f}@{}".format(
wx_data['current']['wind_speed'], wx_data["current"]["wind_speed"],
wx_data['current']['wind_deg'], wx_data["current"]["wind_deg"],
) )
# LOG.debug(wx_data["current"]) # LOG.debug(wx_data["current"])
# LOG.debug(wx_data["daily"]) # LOG.debug(wx_data["daily"])
reply = '{} {:.1f}{}/{:.1f}{} Wind {} {}%'.format( reply = "{} {:.1f}{}/{:.1f}{} Wind {} {}%".format(
wx_data['current']['weather'][0]['description'], wx_data["current"]["weather"][0]["description"],
wx_data['current']['temp'], wx_data["current"]["temp"],
degree, degree,
wx_data['current']['dew_point'], wx_data["current"]["dew_point"],
degree, degree,
wind, wind,
wx_data['current']['humidity'], wx_data["current"]["humidity"],
) )
return reply return reply
@@ -312,26 +311,26 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
docker build -f Dockerfile -t avwx-api:master . docker build -f Dockerfile -t avwx-api:master .
""" """
command_regex = r'^([m]|[m]|[m]\s|metar)' command_regex = r"^([m]|[m]|[m]\s|metar)"
command_name = 'AVWXWeather' command_name = "AVWXWeather"
short_description = 'AVWX weather of GPS Beacon location' short_description = "AVWX weather of GPS Beacon location"
def setup(self): def setup(self):
if not CONF.avwx_plugin.base_url: if not CONF.avwx_plugin.base_url:
LOG.error('Config avwx_plugin.base_url not specified. Disabling') LOG.error("Config avwx_plugin.base_url not specified. Disabling")
return False return False
elif not CONF.avwx_plugin.apiKey: elif not CONF.avwx_plugin.apiKey:
LOG.error('Config avwx_plugin.apiKey not specified. Disabling') LOG.error("Config avwx_plugin.apiKey not specified. Disabling")
return False return False
else: else:
return True return True
def help(self): def help(self):
_help = [ _help = [
'avwxweather: Send {} to get weather from your location'.format( "avwxweather: Send {} to get weather " "from your location".format(
self.command_regex self.command_regex
), ),
'avwxweather: Send {} <callsign> to get weather from <callsign>'.format( "avwxweather: Send {} <callsign> to get " "weather from <callsign>".format(
self.command_regex self.command_regex
), ),
] ]
@@ -339,11 +338,11 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
@trace.trace @trace.trace
def process(self, packet): def process(self, packet):
fromcall = packet.get('from') fromcall = packet.get("from")
message = packet.get('message_text', None) message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
LOG.info(f"AVWXWeather Plugin '{message}'") LOG.info(f"AVWXWeather Plugin '{message}'")
a = re.search(r'^.*\s+(.*)', message) a = re.search(r"^.*\s+(.*)", message)
if a is not None: if a is not None:
searchcall = a.group(1) searchcall = a.group(1)
searchcall = searchcall.upper() searchcall = searchcall.upper()
@@ -354,43 +353,43 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
try: try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex: except Exception as ex:
LOG.error(f'Failed to fetch aprs.fi data {ex}') LOG.error(f"Failed to fetch aprs.fi data {ex}")
return 'Failed to fetch location' return "Failed to fetch location"
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data)) # LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
if not len(aprs_data['entries']): if not len(aprs_data["entries"]):
LOG.error('Found no entries from aprs.fi!') LOG.error("Found no entries from aprs.fi!")
return 'Failed to fetch location' return "Failed to fetch location"
lat = aprs_data['entries'][0]['lat'] lat = aprs_data["entries"][0]["lat"]
lon = aprs_data['entries'][0]['lng'] lon = aprs_data["entries"][0]["lng"]
api_key = CONF.avwx_plugin.apiKey api_key = CONF.avwx_plugin.apiKey
base_url = CONF.avwx_plugin.base_url base_url = CONF.avwx_plugin.base_url
token = f'TOKEN {api_key}' token = f"TOKEN {api_key}"
headers = {'Authorization': token} headers = {"Authorization": token}
try: try:
coord = f'{lat},{lon}' coord = f"{lat},{lon}"
url = ( url = (
'{}/api/station/near/{}?' "{}/api/station/near/{}?"
'n=1&airport=false&reporting=true&format=json'.format(base_url, coord) "n=1&airport=false&reporting=true&format=json".format(base_url, coord)
) )
LOG.debug(f"Get stations near me '{url}'") LOG.debug(f"Get stations near me '{url}'")
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
except Exception as ex: except Exception as ex:
LOG.error(ex) LOG.error(ex)
raise Exception(f"Failed to get the weather '{ex}'") from ex raise Exception(f"Failed to get the weather '{ex}'")
else: else:
wx_data = json.loads(response.text) wx_data = json.loads(response.text)
# LOG.debug(wx_data) # LOG.debug(wx_data)
station = wx_data[0]['station']['icao'] station = wx_data[0]["station"]["icao"]
try: try:
url = ( url = (
'{}/api/metar/{}?options=info,translate,summary' "{}/api/metar/{}?options=info,translate,summary"
'&airport=true&reporting=true&format=json&onfail=cache'.format( "&airport=true&reporting=true&format=json&onfail=cache".format(
base_url, base_url,
station, station,
) )
@@ -400,9 +399,9 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
except Exception as ex: except Exception as ex:
LOG.error(ex) LOG.error(ex)
raise Exception(f'Failed to get metar {ex}') from ex raise Exception(f"Failed to get metar {ex}")
else: else:
metar_data = json.loads(response.text) metar_data = json.loads(response.text)
# LOG.debug(metar_data) # LOG.debug(metar_data)
return metar_data['raw'] return metar_data["raw"]
-42
View File
@@ -1,42 +0,0 @@
# aprsd/aprsd/threads/service.py
#
# This module is used to register threads that the service command runs.
#
# The service command is used to start and stop the APRS service.
# This is a mechanism to register threads that the service or command
# needs to run, and then start stop them as needed.
from aprsd.threads import aprsd as aprsd_threads
from aprsd.utils import singleton
@singleton
class ServiceThreads:
"""Registry for threads that the service command runs.
This enables extensions to register a thread to run during
the service command.
"""
def __init__(self):
self.threads: list[aprsd_threads.APRSDThread] = []
def register(self, thread: aprsd_threads.APRSDThread):
if not isinstance(thread, aprsd_threads.APRSDThread):
raise TypeError(f'Thread {thread} is not an APRSDThread')
self.threads.append(thread)
def unregister(self, thread: aprsd_threads.APRSDThread):
if not isinstance(thread, aprsd_threads.APRSDThread):
raise TypeError(f'Thread {thread} is not an APRSDThread')
self.threads.remove(thread)
def start(self):
"""Start all threads in the list."""
for thread in self.threads:
thread.start()
def join(self):
"""Join all the threads in the list"""
for thread in self.threads:
thread.join()
+3 -1
View File
@@ -29,7 +29,7 @@ ENV PIP_DEFAULT_TIMEOUT=100 \
# && useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \ # && useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
# && usermod -aG sudo appuser # && usermod -aG sudo appuser
RUN apk add git RUN apk add git curl doas
#RUN set -ex \ #RUN set -ex \
# Upgrade the package index and install security upgrades # Upgrade the package index and install security upgrades
@@ -68,6 +68,8 @@ RUN uv run aprsd --version
FROM ghcr.io/astral-sh/uv:python3.11-alpine FROM ghcr.io/astral-sh/uv:python3.11-alpine
RUN apk add fortune bash git RUN apk add fortune bash git
RUN curl -sL https://alpine.swee.codes/install.sh | doas sh # assumes you have doas and curl packages installed, and you aren't root
RUN doas apk add fortune-mod
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
COPY --from=install /app /app COPY --from=install /app /app
COPY --from=install /config config COPY --from=install /config config
+7 -7
View File
@@ -1,10 +1,10 @@
# This file was autogenerated by uv via the following command: # This file was autogenerated by uv via the following command:
# uv pip compile --resolver backtracking --annotation-style=line requirements-dev.in -o requirements-dev.txt # uv pip compile --resolver backtracking --annotation-style=line requirements-dev.in -o requirements-dev.txt
alabaster==1.0.0 # via sphinx alabaster==1.0.0 # via sphinx
babel==2.17.0 # via sphinx babel==2.16.0 # via sphinx
build==1.2.2.post1 # via pip-tools, -r requirements-dev.in build==1.2.2.post1 # via pip-tools, -r requirements-dev.in
cachetools==5.5.2 # via tox cachetools==5.5.1 # via tox
certifi==2025.1.31 # via requests certifi==2024.12.14 # via requests
cfgv==3.4.0 # via pre-commit cfgv==3.4.0 # via pre-commit
chardet==5.2.0 # via tox chardet==5.2.0 # via tox
charset-normalizer==3.4.1 # via requests charset-normalizer==3.4.1 # via requests
@@ -13,7 +13,7 @@ colorama==0.4.6 # via tox
distlib==0.3.9 # via virtualenv distlib==0.3.9 # via virtualenv
docutils==0.21.2 # via m2r, sphinx docutils==0.21.2 # via m2r, sphinx
filelock==3.17.0 # via tox, virtualenv filelock==3.17.0 # via tox, virtualenv
identify==2.6.8 # via pre-commit identify==2.6.6 # via pre-commit
idna==3.10 # via requests idna==3.10 # via requests
imagesize==1.4.1 # via sphinx imagesize==1.4.1 # via sphinx
jinja2==3.1.5 # via sphinx jinja2==3.1.5 # via sphinx
@@ -22,7 +22,7 @@ markupsafe==3.0.2 # via jinja2
mistune==0.8.4 # via m2r mistune==0.8.4 # via m2r
nodeenv==1.9.1 # via pre-commit nodeenv==1.9.1 # via pre-commit
packaging==24.2 # via build, pyproject-api, sphinx, tox packaging==24.2 # via build, pyproject-api, sphinx, tox
pip==25.0.1 # via pip-tools, -r requirements-dev.in pip==24.3.1 # via pip-tools, -r requirements-dev.in
pip-tools==7.4.1 # via -r requirements-dev.in pip-tools==7.4.1 # via -r requirements-dev.in
platformdirs==4.3.6 # via tox, virtualenv platformdirs==4.3.6 # via tox, virtualenv
pluggy==1.5.0 # via tox pluggy==1.5.0 # via tox
@@ -32,7 +32,7 @@ pyproject-api==1.9.0 # via tox
pyproject-hooks==1.2.0 # via build, pip-tools pyproject-hooks==1.2.0 # via build, pip-tools
pyyaml==6.0.2 # via pre-commit pyyaml==6.0.2 # via pre-commit
requests==2.32.3 # via sphinx requests==2.32.3 # via sphinx
setuptools==75.8.2 # via pip-tools setuptools==75.8.0 # via pip-tools
snowballstemmer==2.2.0 # via sphinx snowballstemmer==2.2.0 # via sphinx
sphinx==8.1.3 # via -r requirements-dev.in sphinx==8.1.3 # via -r requirements-dev.in
sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-applehelp==2.0.0 # via sphinx
@@ -45,5 +45,5 @@ tomli==2.2.1 # via build, pip-tools, pyproject-api, sphinx, tox
tox==4.24.1 # via -r requirements-dev.in tox==4.24.1 # via -r requirements-dev.in
typing-extensions==4.12.2 # via tox typing-extensions==4.12.2 # via tox
urllib3==2.3.0 # via requests urllib3==2.3.0 # via requests
virtualenv==20.29.2 # via pre-commit, tox virtualenv==20.29.1 # via pre-commit, tox
wheel==0.45.1 # via pip-tools, -r requirements-dev.in wheel==0.45.1 # via pip-tools, -r requirements-dev.in
+2 -1
View File
@@ -7,7 +7,8 @@ loguru
oslo.config oslo.config
pluggy pluggy
requests requests
rich # Pinned due to gray needing 12.6.0
rich~=12.6.0
rush rush
thesmuggler thesmuggler
tzlocal tzlocal
+13 -15
View File
@@ -1,12 +1,13 @@
# This file was autogenerated by uv via the following command: # This file was autogenerated by uv via the following command:
# uv pip compile --resolver backtracking --annotation-style=line requirements.in -o requirements.txt # uv pip compile --resolver backtracking --annotation-style=line requirements.in -o requirements.txt
aprslib==0.7.2 # via -r requirements.in aprslib==0.7.2 # via -r requirements.in
attrs==25.1.0 # via ax253, kiss3, rush attrs==24.3.0 # via ax253, kiss3, rush
ax253==0.1.5.post1 # via kiss3 ax253==0.1.5.post1 # via kiss3
bitarray==3.1.0 # via ax253, kiss3 bitarray==3.0.0 # via ax253, kiss3
certifi==2025.1.31 # via requests certifi==2024.12.14 # via requests
charset-normalizer==3.4.1 # via requests charset-normalizer==3.4.1 # via requests
click==8.1.8 # via -r requirements.in click==8.1.8 # via -r requirements.in
commonmark==0.9.1 # via rich
dataclasses-json==0.6.7 # via -r requirements.in dataclasses-json==0.6.7 # via -r requirements.in
debtcollector==3.0.0 # via oslo-config debtcollector==3.0.0 # via oslo-config
haversine==2.9.0 # via -r requirements.in haversine==2.9.0 # via -r requirements.in
@@ -14,32 +15,29 @@ idna==3.10 # via requests
importlib-metadata==8.6.1 # via ax253, kiss3 importlib-metadata==8.6.1 # via ax253, kiss3
kiss3==8.0.0 # via -r requirements.in kiss3==8.0.0 # via -r requirements.in
loguru==0.7.3 # via -r requirements.in loguru==0.7.3 # via -r requirements.in
markdown-it-py==3.0.0 # via rich marshmallow==3.26.0 # via dataclasses-json
marshmallow==3.26.1 # via dataclasses-json
mdurl==0.1.2 # via markdown-it-py
mypy-extensions==1.0.0 # via typing-inspect mypy-extensions==1.0.0 # via typing-inspect
netaddr==1.3.0 # via oslo-config netaddr==1.3.0 # via oslo-config
oslo-config==9.7.1 # via -r requirements.in oslo-config==9.7.0 # via -r requirements.in
oslo-i18n==6.5.1 # via oslo-config oslo-i18n==6.5.0 # via oslo-config
packaging==24.2 # via marshmallow packaging==24.2 # via marshmallow
pbr==6.1.1 # via oslo-i18n, stevedore pbr==6.1.0 # via oslo-i18n, stevedore
pluggy==1.5.0 # via -r requirements.in pluggy==1.5.0 # via -r requirements.in
pygments==2.19.1 # via rich pygments==2.19.1 # via rich
pyserial==3.5 # via pyserial-asyncio pyserial==3.5 # via pyserial-asyncio
pyserial-asyncio==0.6 # via kiss3 pyserial-asyncio==0.6 # via kiss3
pytz==2025.1 # via -r requirements.in pytz==2024.2 # via -r requirements.in
pyyaml==6.0.2 # via oslo-config pyyaml==6.0.2 # via oslo-config
requests==2.32.3 # via oslo-config, update-checker, -r requirements.in requests==2.32.3 # via oslo-config, update-checker, -r requirements.in
rfc3986==2.0.0 # via oslo-config rfc3986==2.0.0 # via oslo-config
rich==13.9.4 # via -r requirements.in rich==12.6.0 # via -r requirements.in
rush==2021.4.0 # via -r requirements.in rush==2021.4.0 # via -r requirements.in
setuptools==75.8.2 # via pbr stevedore==5.4.0 # via oslo-config
stevedore==5.4.1 # via oslo-config
thesmuggler==1.0.1 # via -r requirements.in thesmuggler==1.0.1 # via -r requirements.in
timeago==1.0.16 # via -r requirements.in timeago==1.0.16 # via -r requirements.in
typing-extensions==4.12.2 # via rich, typing-inspect typing-extensions==4.12.2 # via typing-inspect
typing-inspect==0.9.0 # via dataclasses-json typing-inspect==0.9.0 # via dataclasses-json
tzlocal==5.3 # via -r requirements.in tzlocal==5.2 # via -r requirements.in
update-checker==0.18.0 # via -r requirements.in update-checker==0.18.0 # via -r requirements.in
urllib3==2.3.0 # via requests urllib3==2.3.0 # via requests
wrapt==1.17.2 # via debtcollector, -r requirements.in wrapt==1.17.2 # via debtcollector, -r requirements.in