diff --git a/ChangeLog.md b/ChangeLog.md index fdf9d9a..01bb88c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,6 +4,22 @@ 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). +#### [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) > 20 February 2025 @@ -21,6 +37,7 @@ 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 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) +- 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) diff --git a/aprsd/cmds/send_message.py b/aprsd/cmds/send_message.py index 4288b44..904263b 100644 --- a/aprsd/cmds/send_message.py +++ b/aprsd/cmds/send_message.py @@ -3,58 +3,60 @@ import sys import time import aprslib -from aprslib.exceptions import LoginError import click +from aprslib.exceptions import LoginError from oslo_config import cfg import aprsd -from aprsd import cli_helper, packets -from aprsd import conf # noqa : F401 +import aprsd.packets # noqa : F401 +from aprsd import ( + cli_helper, + conf, # noqa : F401 + packets, +) from aprsd.client import client_factory from aprsd.main import cli -import aprsd.packets # noqa : F401 from aprsd.packets import collector from aprsd.packets import log as packet_log from aprsd.threads import tx - CONF = cfg.CONF -LOG = logging.getLogger("APRSD") +LOG = logging.getLogger('APRSD') @cli.command() @cli_helper.add_options(cli_helper.common_options) @click.option( - "--aprs-login", - envvar="APRS_LOGIN", + '--aprs-login', + envvar='APRS_LOGIN', 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( - "--aprs-password", - envvar="APRS_PASSWORD", + '--aprs-password', + envvar='APRS_PASSWORD', 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( - "--no-ack", - "-n", + '--no-ack', + '-n', is_flag=True, show_default=True, default=False, help="Don't wait for an ack, just sent it to APRS-IS and bail.", ) @click.option( - "--wait-response", - "-w", + '--wait-response', + '-w', is_flag=True, show_default=True, 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.argument("tocallsign", required=True) -@click.argument("command", nargs=-1, required=True) +@click.option('--raw', default=None, help='Send a raw message. Implies --no-ack') +@click.argument('tocallsign', required=True) +@click.argument('command', nargs=-1, required=True) @click.pass_context @cli_helper.process_standard_options def send_message( @@ -69,11 +71,11 @@ def send_message( ): """Send a message to a callsign via APRS_IS.""" global got_ack, got_response - quiet = ctx.obj["quiet"] + quiet = ctx.obj['quiet'] if not aprs_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) return else: @@ -81,15 +83,15 @@ def send_message( if not aprs_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) return else: 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: - command = " ".join(command) + command = ' '.join(command) if not quiet: if raw: LOG.info(f"L'{aprs_login}' R'{raw}'") @@ -129,7 +131,7 @@ def send_message( sys.exit(0) try: - client_factory.create().client + client_factory.create().client # noqa: B018 except LoginError: sys.exit(-1) @@ -140,7 +142,7 @@ def send_message( # message if raw: tx.send( - packets.Packet(from_call="", to_call="", raw=raw), + packets.Packet(from_call='', to_call='', raw=raw), direct=True, ) sys.exit(0) @@ -164,7 +166,7 @@ def send_message( aprs_client = client_factory.create().client aprs_client.consumer(rx_packet, raw=False) except aprslib.exceptions.ConnectionDrop: - LOG.error("Connection dropped, reconnecting") + LOG.error('Connection dropped, reconnecting') time.sleep(5) # Force the deletion of the client object connected to aprs # This will cause a reconnect, next time client.get_client() diff --git a/aprsd/cmds/server.py b/aprsd/cmds/server.py index f4ad975..058fe27 100644 --- a/aprsd/cmds/server.py +++ b/aprsd/cmds/server.py @@ -12,59 +12,24 @@ from aprsd.client import client_factory from aprsd.main import cli from aprsd.packets import collector as packet_collector from aprsd.packets import seen_list -from aprsd.threads import aprsd as aprsd_threads -from aprsd.threads import keepalive, registry, rx, tx +from aprsd.threads import keepalive, registry, rx, service, tx from aprsd.threads import stats as stats_thread -from aprsd.utils import singleton CONF = cfg.CONF -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() +LOG = logging.getLogger('APRSD') # main() ### @cli.command() @cli_helper.add_options(cli_helper.common_options) @click.option( - "-f", - "--flush", - "flush", + '-f', + '--flush', + 'flush', is_flag=True, show_default=True, default=False, - help="Flush out all old aged messages on disk.", + help='Flush out all old aged messages on disk.', ) @click.pass_context @cli_helper.process_standard_options @@ -73,37 +38,37 @@ def server(ctx, flush): signal.signal(signal.SIGINT, aprsd_main.signal_handler) signal.signal(signal.SIGTERM, aprsd_main.signal_handler) - server_threads = ServerThreads() + service_threads = service.ServiceThreads() level, msg = utils._check_version() if level: LOG.warning(msg) else: 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 # The correct client object ready for use 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) # Make sure we have 1 client transport 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) 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) # Creates the client object - LOG.info("Creating client connection") + LOG.info('Creating client connection') aprs_client = client_factory.create() LOG.info(aprs_client) if not aprs_client.login_success: # 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) print(msg) sys.exit(-1) @@ -114,7 +79,7 @@ def server(ctx, flush): # We register plugins first here so we can register each # plugins config options, so we can dump them all in the # 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.setup_plugins(load_help_plugin=CONF.load_help_plugin) @@ -122,10 +87,10 @@ def server(ctx, flush): CONF.log_opt_values(LOG, logging.DEBUG) message_plugins = plugin_manager.get_message_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: LOG.info(p) - LOG.info("Watchlist Plugins enabled and running:") + LOG.info('Watchlist Plugins enabled and running:') for p in watchlist_plugins: LOG.info(p) @@ -135,37 +100,37 @@ def server(ctx, flush): # Now load the msgTrack from disk if any if flush: - LOG.debug("Flushing All packet tracking objects.") + LOG.debug('Flushing All packet tracking objects.') packet_collector.PacketCollector().flush() else: # Try and load saved MsgTrack list - LOG.debug("Loading saved packet tracking data.") + LOG.debug('Loading saved packet tracking data.') packet_collector.PacketCollector().load() # Now start all the main processing threads. - server_threads.register(keepalive.KeepAliveThread()) - server_threads.register(stats_thread.APRSDStatsStoreThread()) - server_threads.register( + service_threads.register(keepalive.KeepAliveThread()) + service_threads.register(stats_thread.APRSDStatsStoreThread()) + service_threads.register( rx.APRSDRXThread( packet_queue=threads.packet_queue, ), ) - server_threads.register( + service_threads.register( rx.APRSDPluginProcessPacketThread( packet_queue=threads.packet_queue, ), ) if CONF.enable_beacon: - LOG.info("Beacon Enabled. Starting Beacon thread.") - server_threads.register(tx.BeaconSendThread()) + LOG.info('Beacon Enabled. Starting Beacon thread.') + service_threads.register(tx.BeaconSendThread()) if CONF.aprs_registry.enabled: - LOG.info("Registry Enabled. Starting Registry thread.") - server_threads.register(registry.APRSRegistryThread()) + LOG.info('Registry Enabled. Starting Registry thread.') + service_threads.register(registry.APRSRegistryThread()) - server_threads.start() - server_threads.join() + service_threads.start() + service_threads.join() return 0 diff --git a/aprsd/log/log.py b/aprsd/log/log.py index 95c9a10..edbd7f4 100644 --- a/aprsd/log/log.py +++ b/aprsd/log/log.py @@ -51,7 +51,7 @@ class InterceptHandler(logging.Handler): # Setup the log faciility # to disable log to stdout, but still log to file # use the --quiet option on the cmdln -def setup_logging(loglevel=None, quiet=False): +def setup_logging(loglevel=None, quiet=False, custom_handler=None): if not loglevel: log_level = CONF.logging.log_level else: @@ -107,6 +107,9 @@ def setup_logging(loglevel=None, quiet=False): }, ) + if custom_handler: + handlers.append(custom_handler) + # configure loguru logger.configure(handlers=handlers) logger.level('DEBUG', color='') diff --git a/aprsd/main.py b/aprsd/main.py index 5db0d78..2764bdf 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -39,8 +39,8 @@ from aprsd.stats import collector # setup the global logger # log.basicConfig(level=log.DEBUG) # level=10 CONF = cfg.CONF -LOG = logging.getLogger("APRSD") -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) +LOG = logging.getLogger('APRSD') +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) flask_enabled = False @@ -68,18 +68,18 @@ def main(): # First import all the possible commands for the CLI # The commands themselves live in the cmds directory load_commands() - utils.load_entry_points("aprsd.extension") - cli(auto_envvar_prefix="APRSD") + utils.load_entry_points('aprsd.extension') + cli(auto_envvar_prefix='APRSD') def signal_handler(sig, frame): global flask_enabled - click.echo("signal_handler: called") + click.echo('signal_handler: called') threads.APRSDThreadList().stop_all() - if "subprocess" not in str(frame): + if 'subprocess' not in str(frame): 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(), ), ) @@ -91,7 +91,7 @@ def signal_handler(sig, frame): packets.PacketList().save() collector.Collector().collect() except Exception as e: - LOG.error(f"Failed to save data: {e}") + LOG.error(f'Failed to save data: {e}') sys.exit(0) # signal.signal(signal.SIGTERM, sys.exit(0)) # sys.exit(0) @@ -108,9 +108,9 @@ def check_version(ctx): """Check this version against the latest in pypi.org.""" level, msg = utils._check_version() if level: - click.secho(msg, fg="yellow") + click.secho(msg, fg='yellow') else: - click.secho(msg, fg="green") + click.secho(msg, fg='green') @cli.command() @@ -124,12 +124,12 @@ def sample_config(ctx): if sys.version_info < (3, 10): all = imp.entry_points() selected = [] - if "oslo.config.opts" in all: - for x in all["oslo.config.opts"]: - if x.group == "oslo.config.opts": + if 'oslo.config.opts' in all: + for x in all['oslo.config.opts']: + if x.group == 'oslo.config.opts': selected.append(x) else: - selected = imp.entry_points(group="oslo.config.opts") + selected = imp.entry_points(group='oslo.config.opts') return selected @@ -139,23 +139,23 @@ def sample_config(ctx): # selected = imp.entry_points(group="oslo.config.opts") selected = _get_selected_entry_points() for entry in selected: - if "aprsd" in entry.name: - args.append("--namespace") + if 'aprsd' in entry.name: + args.append('--namespace') args.append(entry.name) return args args = get_namespaces() - config_version = metadata_version("oslo.config") + config_version = metadata_version('oslo.config') logging.basicConfig(level=logging.WARN) conf = cfg.ConfigOpts() generator.register_cli_opts(conf) try: conf(args, version=config_version) - except cfg.RequiredOptError: + except cfg.RequiredOptError as ex: conf.print_help() if not sys.argv[1:]: - raise SystemExit + raise SystemExit from ex raise generator.generate(conf) return @@ -165,9 +165,9 @@ def sample_config(ctx): @click.pass_context def version(ctx): """Show the APRSD version.""" - click.echo(click.style("APRSD Version : ", fg="white"), nl=False) - click.secho(f"{aprsd.__version__}", fg="yellow", bold=True) + click.echo(click.style('APRSD Version : ', fg='white'), nl=False) + click.secho(f'{aprsd.__version__}', fg='yellow', bold=True) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/aprsd/plugin.py b/aprsd/plugin.py index 94f15df..b3ede28 100644 --- a/aprsd/plugin.py +++ b/aprsd/plugin.py @@ -17,24 +17,24 @@ from aprsd.packets import watch_list # setup the global logger CONF = cfg.CONF -LOG = logging.getLogger("APRSD") +LOG = logging.getLogger('APRSD') CORE_MESSAGE_PLUGINS = [ - "aprsd.plugins.email.EmailPlugin", - "aprsd.plugins.fortune.FortunePlugin", - "aprsd.plugins.location.LocationPlugin", - "aprsd.plugins.ping.PingPlugin", - "aprsd.plugins.time.TimePlugin", - "aprsd.plugins.weather.USWeatherPlugin", - "aprsd.plugins.version.VersionPlugin", + 'aprsd.plugins.email.EmailPlugin', + 'aprsd.plugins.fortune.FortunePlugin', + 'aprsd.plugins.location.LocationPlugin', + 'aprsd.plugins.ping.PingPlugin', + 'aprsd.plugins.time.TimePlugin', + 'aprsd.plugins.weather.USWeatherPlugin', + 'aprsd.plugins.version.VersionPlugin', ] CORE_NOTIFY_PLUGINS = [ - "aprsd.plugins.notify.NotifySeenPlugin", + 'aprsd.plugins.notify.NotifySeenPlugin', ] -hookspec = pluggy.HookspecMarker("aprsd") -hookimpl = pluggy.HookimplMarker("aprsd") +hookspec = pluggy.HookspecMarker('aprsd') +hookimpl = pluggy.HookimplMarker('aprsd') class APRSDPluginSpec: @@ -76,14 +76,14 @@ class APRSDPluginBase(metaclass=abc.ABCMeta): else: LOG.error( "Can't start thread {}:{}, Must be a child " - "of aprsd.threads.APRSDThread".format( + 'of aprsd.threads.APRSDThread'.format( self, thread, ), ) except Exception: LOG.error( - "Failed to start threads for plugin {}".format( + 'Failed to start threads for plugin {}'.format( self, ), ) @@ -93,7 +93,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta): return self.message_counter def help(self) -> str: - return "Help!" + return 'Help!' @abc.abstractmethod def setup(self): @@ -147,10 +147,10 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): # make sure the timeout is set or this doesn't work if watch_list: 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) else: - LOG.warning("Watch list enabled, but no callsigns set.") + LOG.warning('Watch list enabled, but no callsigns set.') @hookimpl def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket: @@ -164,7 +164,7 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): result = self.process(packet) except Exception as ex: LOG.error( - "Plugin {} failed to process packet {}".format( + 'Plugin {} failed to process packet {}'.format( self.__class__, ex, ), @@ -172,7 +172,7 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): if result: self.tx_inc() else: - LOG.warning(f"{self.__class__} plugin is not enabled") + LOG.warning(f'{self.__class__} plugin is not enabled') return result @@ -196,7 +196,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): raise NotImplementedError def help(self): - return "{}: {}".format( + return '{}: {}'.format( self.command_name.lower(), self.command_regex, ) @@ -207,7 +207,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): @hookimpl 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: result = f"{self.__class__.__name__} isn't enabled" LOG.warning(result) @@ -215,7 +215,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): if not isinstance(packet, packets.MessagePacket): 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 @@ -237,7 +237,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): result = self.process(packet) except Exception as ex: LOG.error( - "Plugin {} failed to process packet {}".format( + 'Plugin {} failed to process packet {}'.format( self.__class__, ex, ), @@ -254,7 +254,7 @@ class APRSFIKEYMixin: def ensure_aprs_fi_key(self): 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 else: self.enabled = True @@ -266,25 +266,25 @@ class HelpPlugin(APRSDRegexCommandPluginBase): This plugin is in this file to prevent a circular import. """ - command_regex = "^[hH]" - command_name = "help" + command_regex = '^[hH]' + command_name = 'help' def help(self): - return "Help: send APRS help or help " + return 'Help: send APRS help or help ' def process(self, packet: packets.MessagePacket): - LOG.info("HelpPlugin") + LOG.info('HelpPlugin') # fromcall = packet.get("from") message = packet.message_text # ack = packet.get("msgNo", "0") - a = re.search(r"^.*\s+(.*)", message) + a = re.search(r'^.*\s+(.*)', message) command_name = None if a is not None: command_name = a.group(1).lower() 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 reply = None for p in pm.get_plugins(): @@ -303,20 +303,20 @@ class HelpPlugin(APRSDRegexCommandPluginBase): LOG.debug(p) if p.enabled and isinstance(p, APRSDRegexCommandPluginBase): 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.sort() - reply = " ".join(list) + reply = ' '.join(list) lines = textwrap.wrap(reply, 60) replies = ["Send APRS MSG of 'help' or 'help '"] for line in lines: - replies.append(f"plugins: {line}") + replies.append(f'plugins: {line}') 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 @@ -341,17 +341,17 @@ class PluginManager: return cls._instance def _init(self): - self._pluggy_pm = pluggy.PluginManager("aprsd") + self._pluggy_pm = pluggy.PluginManager('aprsd') self._pluggy_pm.add_hookspecs(APRSDPluginSpec) # For the watchlist plugins - self._watchlist_pm = pluggy.PluginManager("aprsd") + self._watchlist_pm = pluggy.PluginManager('aprsd') self._watchlist_pm.add_hookspecs(APRSDPluginSpec) def stats(self, serializable=False) -> dict: """Collect and return stats for all plugins.""" def full_name_with_qualname(obj): - return "{}.{}".format( + return '{}.{}'.format( obj.__class__.__module__, obj.__class__.__qualname__, ) @@ -361,10 +361,10 @@ class PluginManager: if plugins: for p in plugins: plugin_stats[full_name_with_qualname(p)] = { - "enabled": p.enabled, - "rx": p.rx_count, - "tx": p.tx_count, - "version": p.version, + 'enabled': p.enabled, + 'rx': p.rx_count, + 'tx': p.tx_count, + 'version': p.version, } return plugin_stats @@ -392,19 +392,19 @@ class PluginManager: module_name = None class_name = None 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) # Commented out because the email thread starts in a different context # and hence gives a different singleton for the EmailStats # module = importlib.reload(module) except Exception as ex: 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: LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'") return - assert hasattr(module, class_name), "class {} is not in {}".format( + assert hasattr(module, class_name), 'class {} is not in {}'.format( class_name, module_name, ) @@ -412,7 +412,7 @@ class PluginManager: # class_name, module_name)) cls = getattr(module, class_name) 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, super_cls.__name__, ) @@ -444,7 +444,7 @@ class PluginManager: self._watchlist_pm.register(plugin_obj) else: LOG.warning( - f"Plugin {plugin_obj.__class__.__name__} is disabled" + f'Plugin {plugin_obj.__class__.__name__} is disabled' ) elif isinstance(plugin_obj, APRSDRegexCommandPluginBase): if plugin_obj.enabled: @@ -458,7 +458,7 @@ class PluginManager: self._pluggy_pm.register(plugin_obj) else: LOG.warning( - f"Plugin {plugin_obj.__class__.__name__} is disabled" + f'Plugin {plugin_obj.__class__.__name__} is disabled' ) elif isinstance(plugin_obj, APRSDPluginBase): if plugin_obj.enabled: @@ -471,7 +471,7 @@ class PluginManager: self._pluggy_pm.register(plugin_obj) else: LOG.warning( - f"Plugin {plugin_obj.__class__.__name__} is disabled" + f'Plugin {plugin_obj.__class__.__name__} is disabled' ) except Exception as ex: LOG.error(f"Couldn't load plugin '{plugin_name}'") @@ -485,11 +485,11 @@ class PluginManager: def setup_plugins( self, load_help_plugin=True, - plugin_list=[], + plugin_list=None, ): """Create the plugin manager and register plugins.""" - LOG.info("Loading APRSD Plugins") + LOG.info('Loading APRSD Plugins') # Help plugin is always enabled. if load_help_plugin: _help = HelpPlugin() @@ -509,7 +509,7 @@ class PluginManager: for p_name in CORE_MESSAGE_PLUGINS: self._load_plugin(p_name) - LOG.info("Completed Plugin Loading.") + LOG.info('Completed Plugin Loading.') def run(self, packet: packets.MessagePacket): """Execute all the plugins run method.""" @@ -524,7 +524,7 @@ class PluginManager: """Stop all threads created by all plugins.""" with self.lock: for p in self.get_plugins(): - if hasattr(p, "stop_threads"): + if hasattr(p, 'stop_threads'): p.stop_threads() def register_msg(self, obj): diff --git a/aprsd/plugin_utils.py b/aprsd/plugin_utils.py index c225f9f..11fb29a 100644 --- a/aprsd/plugin_utils.py +++ b/aprsd/plugin_utils.py @@ -4,21 +4,20 @@ import logging import requests - -LOG = logging.getLogger("APRSD") +LOG = logging.getLogger('APRSD') def get_aprs_fi(api_key, callsign): LOG.debug(f"Fetch aprs.fi location for '{callsign}'") try: url = ( - "http://api.aprs.fi/api/get?" - "&what=loc&apikey={}&format=json" - "&name={}".format(api_key, callsign) + 'http://api.aprs.fi/api/get?&what=loc&apikey={}&format=json&name={}'.format( + api_key, callsign + ) ) response = requests.get(url) - except Exception: - raise Exception("Failed to get aprs.fi location") + except Exception as e: + raise Exception('Failed to get aprs.fi location') from e else: response.raise_for_status() return json.loads(response.text) @@ -26,22 +25,22 @@ def get_aprs_fi(api_key, callsign): def get_weather_gov_for_gps(lat, lon): # 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.update( - {"User-Agent": "(aprsd, waboring@hemna.com)"}, + {'User-Agent': '(aprsd, waboring@hemna.com)'}, ) try: url2 = ( - "https://forecast.weather.gov/MapClick.php?lat=%s" - "&lon=%s&FcstType=json" % (lat, lon) + 'https://forecast.weather.gov/MapClick.php?lat=%s' + '&lon=%s&FcstType=json' % (lat, lon) # f"https://api.weather.gov/points/{lat},{lon}" ) LOG.debug(f"Fetching weather '{url2}'") response = requests.get(url2, headers=headers) except Exception as e: LOG.error(e) - raise Exception("Failed to get weather") + raise Exception('Failed to get weather') from e else: response.raise_for_status() return json.loads(response.text) @@ -50,21 +49,21 @@ def get_weather_gov_for_gps(lat, lon): def get_weather_gov_metar(station): LOG.debug(f"Fetch metar for station '{station}'") try: - url = "https://api.weather.gov/stations/{}/observations/latest".format( + url = 'https://api.weather.gov/stations/{}/observations/latest'.format( station, ) response = requests.get(url) - except Exception: - raise Exception("Failed to fetch metar") + except Exception as e: + raise Exception('Failed to fetch metar') from e else: response.raise_for_status() return json.loads(response) -def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None): - LOG.debug(f"Fetch openweathermap for {lat}, {lon}") +def fetch_openweathermap(api_key, lat, lon, units='metric', exclude=None): + LOG.debug(f'Fetch openweathermap for {lat}, {lon}') if not exclude: - exclude = "minutely,hourly,daily,alerts" + exclude = 'minutely,hourly,daily,alerts' try: url = ( "https://api.openweathermap.org/data/3.0/onecall?" @@ -80,7 +79,7 @@ def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None): response = requests.get(url) except Exception as e: LOG.error(e) - raise Exception("Failed to get weather") + raise Exception('Failed to get weather') from e else: response.raise_for_status() return json.loads(response.text) diff --git a/aprsd/plugins/weather.py b/aprsd/plugins/weather.py index 065a933..c7981f9 100644 --- a/aprsd/plugins/weather.py +++ b/aprsd/plugins/weather.py @@ -9,7 +9,7 @@ from aprsd import plugin, plugin_utils from aprsd.utils import trace CONF = cfg.CONF -LOG = logging.getLogger("APRSD") +LOG = logging.getLogger('APRSD') 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"^[wW]" + command_regex = r'^[wW]' - command_name = "USWeather" - short_description = "Provide USA only weather of GPS Beacon location" + command_name = 'USWeather' + short_description = 'Provide USA only weather of GPS Beacon location' def setup(self): self.ensure_aprs_fi_key() @trace.trace def process(self, packet): - LOG.info("Weather Plugin") + LOG.info('Weather Plugin') fromcall = packet.from_call - message = packet.get("message_text", None) + message = packet.get('message_text', None) # message = packet.get("message_text", None) # ack = packet.get("msgNo", "0") - a = re.search(r"^.*\s+(.*)", message) + a = re.search(r'^.*\s+(.*)', message) if a is not None: searchcall = a.group(1) searchcall = searchcall.upper() @@ -51,34 +51,34 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin) try: aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) except Exception as ex: - LOG.error(f"Failed to fetch aprs.fi data {ex}") - return "Failed to fetch aprs.fi location" + LOG.error(f'Failed to fetch aprs.fi data {ex}') + return 'Failed to fetch aprs.fi location' - LOG.debug(f"LocationPlugin: aprs_data = {aprs_data}") - if not len(aprs_data["entries"]): + LOG.debug(f'LocationPlugin: aprs_data = {aprs_data}') + if not len(aprs_data['entries']): 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"] - lon = aprs_data["entries"][0]["lng"] + lat = aprs_data['entries'][0]['lat'] + lon = aprs_data['entries'][0]['lng'] try: wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon) except Exception as 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 = ( - "%sF(%sF/%sF) %s. %s, %s." + '%sF(%sF/%sF) %s. %s, %s.' % ( - wx_data["currentobservation"]["Temp"], - wx_data["data"]["temperature"][0], - wx_data["data"]["temperature"][1], - wx_data["data"]["weather"][0], - wx_data["time"]["startPeriodName"][1], - wx_data["data"]["weather"][1], + wx_data['currentobservation']['Temp'], + wx_data['data']['temperature'][0], + wx_data['data']['temperature'][1], + wx_data['data']['weather'][0], + wx_data['time']['startPeriodName'][1], + wx_data['data']['weather'][1], ) ).rstrip() LOG.debug(f"reply: '{reply}' ") @@ -100,31 +100,31 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin): """ - command_regex = r"^([m]|[M]|[m]\s|metar)" - command_name = "USMetar" - short_description = "USA only METAR of GPS Beacon location" + command_regex = r'^([m]|[M]|[m]\s|metar)' + command_name = 'USMetar' + short_description = 'USA only METAR of GPS Beacon location' def setup(self): self.ensure_aprs_fi_key() @trace.trace def process(self, packet): - fromcall = packet.get("from") - message = packet.get("message_text", None) + fromcall = packet.get('from') + message = packet.get('message_text', None) # ack = packet.get("msgNo", "0") LOG.info(f"WX Plugin '{message}'") - a = re.search(r"^.*\s+(.*)", message) + a = re.search(r'^.*\s+(.*)', message) if a is not None: searchcall = a.group(1) station = searchcall.upper() try: resp = plugin_utils.get_weather_gov_metar(station) except Exception as e: - LOG.debug(f"Weather failed with: {str(e)}") - reply = "Unable to find station METAR" + LOG.debug(f'Weather failed with: {str(e)}') + reply = 'Unable to find station METAR' else: station_data = json.loads(resp.text) - reply = station_data["properties"]["rawMessage"] + reply = station_data['properties']['rawMessage'] return reply else: @@ -136,36 +136,36 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin): try: aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall) except Exception as ex: - LOG.error(f"Failed to fetch aprs.fi data {ex}") - return "Failed to fetch aprs.fi location" + LOG.error(f'Failed to fetch aprs.fi data {ex}') + return 'Failed to fetch aprs.fi location' # LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data)) - if not len(aprs_data["entries"]): - LOG.error("Found no entries from aprs.fi!") - return "Failed to fetch aprs.fi location" + if not len(aprs_data['entries']): + LOG.error('Found no entries from aprs.fi!') + return 'Failed to fetch aprs.fi location' - lat = aprs_data["entries"][0]["lat"] - lon = aprs_data["entries"][0]["lng"] + lat = aprs_data['entries'][0]['lat'] + lon = aprs_data['entries'][0]['lng'] try: wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon) except Exception as 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"]: - station = wx_data["location"]["metar"] + if wx_data['location']['metar']: + station = wx_data['location']['metar'] try: resp = plugin_utils.get_weather_gov_metar(station) except Exception as e: - LOG.debug(f"Weather failed with: {str(e)}") - reply = "Failed to get Metar" + LOG.debug(f'Weather failed with: {str(e)}') + reply = 'Failed to get Metar' else: station_data = json.loads(resp.text) - reply = station_data["properties"]["rawMessage"] + reply = station_data['properties']['rawMessage'] else: # Couldn't find a station - reply = "No Metar station found" + reply = 'No Metar station found' return reply @@ -190,35 +190,36 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase): """ # command_regex = r"^([w][x]|[w][x]\s|weather)" - command_regex = r"^[wW]" + command_regex = r'^[wW]' - command_name = "OpenWeatherMap" - short_description = "OpenWeatherMap weather of GPS Beacon location" + command_name = 'OpenWeatherMap' + short_description = 'OpenWeatherMap weather of GPS Beacon location' def setup(self): 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 else: self.enabled = True def help(self): _help = [ - "openweathermap: Send {} to get weather " "from your location".format( + 'openweathermap: Send {} to get weather from your location'.format( + self.command_regex + ), + 'openweathermap: Send {} to get weather from '.format( self.command_regex ), - "openweathermap: Send {} to get " - "weather from ".format(self.command_regex), ] return _help @trace.trace def process(self, packet): - fromcall = packet.get("from_call") - message = packet.get("message_text", None) + fromcall = packet.get('from_call') + message = packet.get('message_text', None) # ack = packet.get("msgNo", "0") LOG.info(f"OWMWeather Plugin '{message}'") - a = re.search(r"^.*\s+(.*)", message) + a = re.search(r'^.*\s+(.*)', message) if a is not None: searchcall = a.group(1) searchcall = searchcall.upper() @@ -230,16 +231,16 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase): try: aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) except Exception as ex: - LOG.error(f"Failed to fetch aprs.fi data {ex}") - return "Failed to fetch location" + LOG.error(f'Failed to fetch aprs.fi data {ex}') + return 'Failed to fetch location' # LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data)) - if not len(aprs_data["entries"]): - LOG.error("Found no entries from aprs.fi!") - return "Failed to fetch location" + if not len(aprs_data['entries']): + LOG.error('Found no entries from aprs.fi!') + return 'Failed to fetch location' - lat = aprs_data["entries"][0]["lat"] - lon = aprs_data["entries"][0]["lng"] + lat = aprs_data['entries'][0]['lat'] + lon = aprs_data['entries'][0]['lng'] units = CONF.units api_key = CONF.owm_weather_plugin.apiKey @@ -249,40 +250,40 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase): lat, lon, units=units, - exclude="minutely,hourly", + exclude='minutely,hourly', ) except Exception as ex: LOG.error(f"Couldn't fetch openweathermap api '{ex}'") # default to UTC - return "Unable to get weather" + return 'Unable to get weather' - if units == "metric": - degree = "C" + if units == 'metric': + degree = 'C' else: - degree = "F" + degree = 'F' - if "wind_gust" in wx_data["current"]: - wind = "{:.0f}@{}G{:.0f}".format( - wx_data["current"]["wind_speed"], - wx_data["current"]["wind_deg"], - wx_data["current"]["wind_gust"], + if 'wind_gust' in wx_data['current']: + wind = '{:.0f}@{}G{:.0f}'.format( + wx_data['current']['wind_speed'], + wx_data['current']['wind_deg'], + wx_data['current']['wind_gust'], ) else: - wind = "{:.0f}@{}".format( - wx_data["current"]["wind_speed"], - wx_data["current"]["wind_deg"], + wind = '{:.0f}@{}'.format( + wx_data['current']['wind_speed'], + wx_data['current']['wind_deg'], ) # LOG.debug(wx_data["current"]) # LOG.debug(wx_data["daily"]) - reply = "{} {:.1f}{}/{:.1f}{} Wind {} {}%".format( - wx_data["current"]["weather"][0]["description"], - wx_data["current"]["temp"], + reply = '{} {:.1f}{}/{:.1f}{} Wind {} {}%'.format( + wx_data['current']['weather'][0]['description'], + wx_data['current']['temp'], degree, - wx_data["current"]["dew_point"], + wx_data['current']['dew_point'], degree, wind, - wx_data["current"]["humidity"], + wx_data['current']['humidity'], ) return reply @@ -311,26 +312,26 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase): docker build -f Dockerfile -t avwx-api:master . """ - command_regex = r"^([m]|[m]|[m]\s|metar)" - command_name = "AVWXWeather" - short_description = "AVWX weather of GPS Beacon location" + command_regex = r'^([m]|[m]|[m]\s|metar)' + command_name = 'AVWXWeather' + short_description = 'AVWX weather of GPS Beacon location' def setup(self): 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 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 else: return True def help(self): _help = [ - "avwxweather: Send {} to get weather " "from your location".format( + 'avwxweather: Send {} to get weather from your location'.format( self.command_regex ), - "avwxweather: Send {} to get " "weather from ".format( + 'avwxweather: Send {} to get weather from '.format( self.command_regex ), ] @@ -338,11 +339,11 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase): @trace.trace def process(self, packet): - fromcall = packet.get("from") - message = packet.get("message_text", None) + fromcall = packet.get('from') + message = packet.get('message_text', None) # ack = packet.get("msgNo", "0") LOG.info(f"AVWXWeather Plugin '{message}'") - a = re.search(r"^.*\s+(.*)", message) + a = re.search(r'^.*\s+(.*)', message) if a is not None: searchcall = a.group(1) searchcall = searchcall.upper() @@ -353,43 +354,43 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase): try: aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) except Exception as ex: - LOG.error(f"Failed to fetch aprs.fi data {ex}") - return "Failed to fetch location" + LOG.error(f'Failed to fetch aprs.fi data {ex}') + return 'Failed to fetch location' # LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data)) - if not len(aprs_data["entries"]): - LOG.error("Found no entries from aprs.fi!") - return "Failed to fetch location" + if not len(aprs_data['entries']): + LOG.error('Found no entries from aprs.fi!') + return 'Failed to fetch location' - lat = aprs_data["entries"][0]["lat"] - lon = aprs_data["entries"][0]["lng"] + lat = aprs_data['entries'][0]['lat'] + lon = aprs_data['entries'][0]['lng'] api_key = CONF.avwx_plugin.apiKey base_url = CONF.avwx_plugin.base_url - token = f"TOKEN {api_key}" - headers = {"Authorization": token} + token = f'TOKEN {api_key}' + headers = {'Authorization': token} try: - coord = f"{lat},{lon}" + coord = f'{lat},{lon}' url = ( - "{}/api/station/near/{}?" - "n=1&airport=false&reporting=true&format=json".format(base_url, coord) + '{}/api/station/near/{}?' + 'n=1&airport=false&reporting=true&format=json'.format(base_url, coord) ) LOG.debug(f"Get stations near me '{url}'") response = requests.get(url, headers=headers) except Exception as ex: LOG.error(ex) - raise Exception(f"Failed to get the weather '{ex}'") + raise Exception(f"Failed to get the weather '{ex}'") from ex else: wx_data = json.loads(response.text) # LOG.debug(wx_data) - station = wx_data[0]["station"]["icao"] + station = wx_data[0]['station']['icao'] try: url = ( - "{}/api/metar/{}?options=info,translate,summary" - "&airport=true&reporting=true&format=json&onfail=cache".format( + '{}/api/metar/{}?options=info,translate,summary' + '&airport=true&reporting=true&format=json&onfail=cache'.format( base_url, station, ) @@ -399,9 +400,9 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase): response = requests.get(url, headers=headers) except Exception as ex: LOG.error(ex) - raise Exception(f"Failed to get metar {ex}") + raise Exception(f'Failed to get metar {ex}') from ex else: metar_data = json.loads(response.text) # LOG.debug(metar_data) - return metar_data["raw"] + return metar_data['raw'] diff --git a/aprsd/threads/service.py b/aprsd/threads/service.py new file mode 100644 index 0000000..6971c51 --- /dev/null +++ b/aprsd/threads/service.py @@ -0,0 +1,42 @@ +# 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() diff --git a/requirements-dev.txt b/requirements-dev.txt index f06952d..7b7d586 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,10 @@ # 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 alabaster==1.0.0 # via sphinx -babel==2.16.0 # via sphinx +babel==2.17.0 # via sphinx build==1.2.2.post1 # via pip-tools, -r requirements-dev.in -cachetools==5.5.1 # via tox -certifi==2024.12.14 # via requests +cachetools==5.5.2 # via tox +certifi==2025.1.31 # via requests cfgv==3.4.0 # via pre-commit chardet==5.2.0 # via tox charset-normalizer==3.4.1 # via requests @@ -13,7 +13,7 @@ colorama==0.4.6 # via tox distlib==0.3.9 # via virtualenv docutils==0.21.2 # via m2r, sphinx filelock==3.17.0 # via tox, virtualenv -identify==2.6.6 # via pre-commit +identify==2.6.8 # via pre-commit idna==3.10 # via requests imagesize==1.4.1 # via sphinx jinja2==3.1.5 # via sphinx @@ -22,7 +22,7 @@ markupsafe==3.0.2 # via jinja2 mistune==0.8.4 # via m2r nodeenv==1.9.1 # via pre-commit packaging==24.2 # via build, pyproject-api, sphinx, tox -pip==24.3.1 # via pip-tools, -r requirements-dev.in +pip==25.0.1 # via pip-tools, -r requirements-dev.in pip-tools==7.4.1 # via -r requirements-dev.in platformdirs==4.3.6 # via tox, virtualenv 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 pyyaml==6.0.2 # via pre-commit requests==2.32.3 # via sphinx -setuptools==75.8.0 # via pip-tools +setuptools==75.8.2 # via pip-tools snowballstemmer==2.2.0 # via sphinx sphinx==8.1.3 # via -r requirements-dev.in 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 typing-extensions==4.12.2 # via tox urllib3==2.3.0 # via requests -virtualenv==20.29.1 # via pre-commit, tox +virtualenv==20.29.2 # via pre-commit, tox wheel==0.45.1 # via pip-tools, -r requirements-dev.in diff --git a/requirements.in b/requirements.in index 5c2cddf..1357ea9 100644 --- a/requirements.in +++ b/requirements.in @@ -7,8 +7,7 @@ loguru oslo.config pluggy requests -# Pinned due to gray needing 12.6.0 -rich~=12.6.0 +rich rush thesmuggler tzlocal diff --git a/requirements.txt b/requirements.txt index 9df6168..a20d464 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,12 @@ # This file was autogenerated by uv via the following command: # uv pip compile --resolver backtracking --annotation-style=line requirements.in -o requirements.txt aprslib==0.7.2 # via -r requirements.in -attrs==24.3.0 # via ax253, kiss3, rush +attrs==25.1.0 # via ax253, kiss3, rush ax253==0.1.5.post1 # via kiss3 -bitarray==3.0.0 # via ax253, kiss3 -certifi==2024.12.14 # via requests +bitarray==3.1.0 # via ax253, kiss3 +certifi==2025.1.31 # via requests charset-normalizer==3.4.1 # via requests click==8.1.8 # via -r requirements.in -commonmark==0.9.1 # via rich dataclasses-json==0.6.7 # via -r requirements.in debtcollector==3.0.0 # via oslo-config haversine==2.9.0 # via -r requirements.in @@ -15,29 +14,32 @@ idna==3.10 # via requests importlib-metadata==8.6.1 # via ax253, kiss3 kiss3==8.0.0 # via -r requirements.in loguru==0.7.3 # via -r requirements.in -marshmallow==3.26.0 # via dataclasses-json +markdown-it-py==3.0.0 # via rich +marshmallow==3.26.1 # via dataclasses-json +mdurl==0.1.2 # via markdown-it-py mypy-extensions==1.0.0 # via typing-inspect netaddr==1.3.0 # via oslo-config -oslo-config==9.7.0 # via -r requirements.in -oslo-i18n==6.5.0 # via oslo-config +oslo-config==9.7.1 # via -r requirements.in +oslo-i18n==6.5.1 # via oslo-config packaging==24.2 # via marshmallow -pbr==6.1.0 # via oslo-i18n, stevedore +pbr==6.1.1 # via oslo-i18n, stevedore pluggy==1.5.0 # via -r requirements.in pygments==2.19.1 # via rich pyserial==3.5 # via pyserial-asyncio pyserial-asyncio==0.6 # via kiss3 -pytz==2024.2 # via -r requirements.in +pytz==2025.1 # via -r requirements.in pyyaml==6.0.2 # via oslo-config requests==2.32.3 # via oslo-config, update-checker, -r requirements.in rfc3986==2.0.0 # via oslo-config -rich==12.6.0 # via -r requirements.in +rich==13.9.4 # via -r requirements.in rush==2021.4.0 # via -r requirements.in -stevedore==5.4.0 # via oslo-config +setuptools==75.8.2 # via pbr +stevedore==5.4.1 # via oslo-config thesmuggler==1.0.1 # via -r requirements.in timeago==1.0.16 # via -r requirements.in -typing-extensions==4.12.2 # via typing-inspect +typing-extensions==4.12.2 # via rich, typing-inspect typing-inspect==0.9.0 # via dataclasses-json -tzlocal==5.2 # via -r requirements.in +tzlocal==5.3 # via -r requirements.in update-checker==0.18.0 # via -r requirements.in urllib3==2.3.0 # via requests wrapt==1.17.2 # via debtcollector, -r requirements.in