mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-16 16:43:52 -04:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bf4bfd92c | |||
| af0feaf9c8 | |||
| 328c027ad3 | |||
| e15322ede3 | |||
| c7c9a92b15 | |||
| 3961e1d1ad | |||
| 8cd61a72c8 | |||
| 556554b1a7 | |||
| f3039ebfa1 | |||
| 58cb046b31 |
@@ -4,6 +4,20 @@ 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.2.1](https://github.com/craigerl/aprsd/compare/4.2.0...4.2.1)
|
||||
|
||||
> 7 October 2025
|
||||
|
||||
- Sanity check around decoding packet [`58cb046`](https://github.com/craigerl/aprsd/commit/58cb046b3131f4300255c527dee4c7291a4aeed2)
|
||||
- Added CONF.is_digipi [`f3039eb`](https://github.com/craigerl/aprsd/commit/f3039ebfa153d0fbad4a390089fce360e6148854)
|
||||
- Fixed stats issue with tcpkiss client. [`556554b`](https://github.com/craigerl/aprsd/commit/556554b1a76743989fa88877e7f694b918f770cf)
|
||||
- Fixed missing f string [`8cd61a7`](https://github.com/craigerl/aprsd/commit/8cd61a72c8f16e8651533baf2d677d3345bd3712)
|
||||
- Added ThirdPartyPacket decoding in tcpkiss driver [`3961e1d`](https://github.com/craigerl/aprsd/commit/3961e1d1adf428ff0fa66ed5fe0d6192adf164bf)
|
||||
- refactored list-plugins [`c7c9a92`](https://github.com/craigerl/aprsd/commit/c7c9a92b153ccc673275dca628d485d1df88db12)
|
||||
- Added package [`e15322e`](https://github.com/craigerl/aprsd/commit/e15322ede384d6e463c42e9496502e2c6ea0886c)
|
||||
- Some client and driver cleanup. [`328c027`](https://github.com/craigerl/aprsd/commit/328c027ad3db594f3d5622a2cab7cafb21d1cfd6)
|
||||
- Fixed some unit tests [`af0feaf`](https://github.com/craigerl/aprsd/commit/af0feaf9c81b8b9f443e58fbe7d23f1ff7ba63d2)
|
||||
|
||||
#### [4.2.0](https://github.com/craigerl/aprsd/compare/4.1.2...4.2.0)
|
||||
|
||||
> 12 August 2025
|
||||
@@ -25,6 +39,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- Fix tox failures [`74887af`](https://github.com/craigerl/aprsd/commit/74887af507755cd7ee27f13129b177f4d58ca8e0)
|
||||
- log the exception when tx fails. [`fa5d0c6`](https://github.com/craigerl/aprsd/commit/fa5d0c643ae85fc9ad3f615f9d0afc590f422283)
|
||||
- Updated requirements for 4.2.0 [`2c476d8`](https://github.com/craigerl/aprsd/commit/2c476d8a04df8cebbaa50780688e58d8b9df2bc9)
|
||||
- Updated Changelog for 4.2.0 [`b9fea98`](https://github.com/craigerl/aprsd/commit/b9fea982f977e9cf60c4695ca1892adaeee64298)
|
||||
|
||||
#### [4.1.2](https://github.com/craigerl/aprsd/compare/4.1.1...4.1.2)
|
||||
|
||||
|
||||
+31
-16
@@ -38,15 +38,16 @@ class APRSDClient:
|
||||
keepalive_collector.KeepAliveCollector().register(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, auto_connect: bool = True):
|
||||
self.auto_connect = auto_connect
|
||||
self.connected = False
|
||||
self.login_status = {
|
||||
'success': False,
|
||||
'message': None,
|
||||
}
|
||||
if not self.driver:
|
||||
self.driver = DriverRegistry().get_driver()
|
||||
self.driver.setup_connection()
|
||||
self.driver = DriverRegistry().get_driver()
|
||||
if self.auto_connect:
|
||||
self.connect()
|
||||
|
||||
def stats(self, serializable=False) -> dict:
|
||||
stats = {}
|
||||
@@ -54,17 +55,20 @@ class APRSDClient:
|
||||
stats = self.driver.stats(serializable=serializable)
|
||||
return stats
|
||||
|
||||
@property
|
||||
def is_enabled(self):
|
||||
if not self.driver:
|
||||
return False
|
||||
return self.driver.is_enabled()
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
for driver in DriverRegistry().drivers:
|
||||
if driver.is_enabled():
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_configured(self):
|
||||
if not self.driver:
|
||||
return False
|
||||
return self.driver.is_configured()
|
||||
@staticmethod
|
||||
def is_configured():
|
||||
"""Check if ANY driver is configured."""
|
||||
for driver in DriverRegistry().drivers:
|
||||
if driver.is_configured():
|
||||
return True
|
||||
return False
|
||||
|
||||
# @property
|
||||
# def is_connected(self):
|
||||
@@ -98,6 +102,11 @@ class APRSDClient:
|
||||
def is_alive(self):
|
||||
return self.driver.is_alive()
|
||||
|
||||
def connect(self):
|
||||
if not self.driver:
|
||||
self.driver = DriverRegistry().get_driver()
|
||||
self.driver.setup_connection()
|
||||
|
||||
def close(self):
|
||||
if not self.driver:
|
||||
return
|
||||
@@ -109,7 +118,8 @@ class APRSDClient:
|
||||
LOG.info('Resetting client connection.')
|
||||
if self.driver:
|
||||
self.driver.close()
|
||||
self.driver.setup_connection()
|
||||
if not self.delay_connect:
|
||||
self.driver.setup_connection()
|
||||
if self.filter:
|
||||
self.driver.set_filter(self.filter)
|
||||
else:
|
||||
@@ -138,4 +148,9 @@ class APRSDClient:
|
||||
return self.driver.consumer(callback=callback, raw=raw)
|
||||
|
||||
def decode_packet(self, *args, **kwargs) -> core.Packet:
|
||||
return self.driver.decode_packet(*args, **kwargs)
|
||||
try:
|
||||
packet = self.driver.decode_packet(*args, **kwargs)
|
||||
except Exception as e:
|
||||
LOG.error(f'Error decoding packet: {e}')
|
||||
return None
|
||||
return packet
|
||||
|
||||
@@ -26,6 +26,7 @@ class APRSISDriver:
|
||||
|
||||
_client = None
|
||||
_checks = False
|
||||
connected = False
|
||||
|
||||
def __init__(self):
|
||||
max_timeout = {'hours': 0.0, 'minutes': 2, 'seconds': 0}
|
||||
@@ -164,7 +165,7 @@ class APRSISDriver:
|
||||
return core.factory(args[0])
|
||||
|
||||
def consumer(self, callback: Callable, raw: bool = False):
|
||||
if self._client:
|
||||
if self._client and self.connected:
|
||||
try:
|
||||
self._client.consumer(
|
||||
callback,
|
||||
@@ -177,10 +178,9 @@ class APRSISDriver:
|
||||
LOG.info(e.__cause__)
|
||||
raise e
|
||||
else:
|
||||
LOG.warning('client is None, might be resetting.')
|
||||
self.connected = False
|
||||
|
||||
def stats(self, serializable=False) -> dict:
|
||||
def stats(self, serializable: bool = False) -> dict:
|
||||
stats = {}
|
||||
if self.is_configured():
|
||||
if self._client:
|
||||
|
||||
@@ -79,8 +79,8 @@ class TCPKISSDriver:
|
||||
def transport(self) -> str:
|
||||
return client.TRANSPORT_TCPKISS
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls) -> bool:
|
||||
@staticmethod
|
||||
def is_enabled() -> bool:
|
||||
"""Check if KISS is enabled in configuration.
|
||||
|
||||
Returns:
|
||||
@@ -248,7 +248,11 @@ class TCPKISSDriver:
|
||||
LOG.warning(f'FRAME: {str(frame)}')
|
||||
try:
|
||||
aprslib_frame = aprslib.parse(str(frame))
|
||||
return core.factory(aprslib_frame)
|
||||
packet = core.factory(aprslib_frame)
|
||||
if isinstance(packet, core.ThirdPartyPacket):
|
||||
return packet.subpacket
|
||||
else:
|
||||
return packet
|
||||
except Exception as e:
|
||||
LOG.error(f'Error decoding packet: {e}')
|
||||
return None
|
||||
@@ -271,8 +275,19 @@ class TCPKISSDriver:
|
||||
"""
|
||||
if serializable:
|
||||
keepalive = self.keepalive.isoformat()
|
||||
if self.last_packet_sent:
|
||||
last_packet_sent = self.last_packet_sent.isoformat()
|
||||
else:
|
||||
last_packet_sent = 'None'
|
||||
if self.last_packet_received:
|
||||
last_packet_received = self.last_packet_received.isoformat()
|
||||
else:
|
||||
last_packet_received = 'None'
|
||||
else:
|
||||
keepalive = self.keepalive
|
||||
last_packet_sent = self.last_packet_sent
|
||||
last_packet_received = self.last_packet_received
|
||||
|
||||
stats = {
|
||||
'client': self.__class__.__name__,
|
||||
'transport': self.transport,
|
||||
@@ -280,8 +295,8 @@ class TCPKISSDriver:
|
||||
'path': self.path,
|
||||
'packets_sent': self.packets_sent,
|
||||
'packets_received': self.packets_received,
|
||||
'last_packet_sent': self.last_packet_sent,
|
||||
'last_packet_received': self.last_packet_received,
|
||||
'last_packet_sent': last_packet_sent,
|
||||
'last_packet_received': last_packet_received,
|
||||
'connection_keepalive': keepalive,
|
||||
'host': CONF.kiss_tcp.host,
|
||||
'port': CONF.kiss_tcp.port,
|
||||
|
||||
+6
-1
@@ -4,11 +4,13 @@
|
||||
#
|
||||
# python included libs
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import click
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import cli_helper, conf, packets, plugin
|
||||
import aprsd
|
||||
from aprsd import cli_helper, conf, packets, plugin, utils
|
||||
|
||||
# local imports here
|
||||
from aprsd.main import cli
|
||||
@@ -71,6 +73,9 @@ def test_plugin(
|
||||
):
|
||||
"""Test an individual APRSD plugin given a python path."""
|
||||
|
||||
LOG.info(f'Python version: {sys.version}')
|
||||
LOG.info(f'APRSD DEV Started version: {aprsd.__version__}')
|
||||
utils.package.log_installed_extensions_and_plugins()
|
||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||
|
||||
if not aprs_login:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Fetch active stats from a remote running instance of aprsd admin web interface.
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import click
|
||||
import requests
|
||||
@@ -38,6 +39,7 @@ CONF = cfg.CONF
|
||||
def fetch_stats(ctx, host, port):
|
||||
"""Fetch stats from a APRSD admin web interface."""
|
||||
console = Console()
|
||||
console.print(f'Python version: {sys.version}')
|
||||
console.print(f'APRSD Fetch-Stats started version: {aprsd.__version__}')
|
||||
|
||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||
|
||||
+6
-133
@@ -1,119 +1,18 @@
|
||||
import fnmatch
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import sys
|
||||
from traceback import print_tb
|
||||
|
||||
import click
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
from thesmuggler import smuggle
|
||||
|
||||
from aprsd import cli_helper
|
||||
from aprsd import plugin as aprsd_plugin
|
||||
from aprsd.main import cli
|
||||
from aprsd.plugins import fortune, notify, ping, time, version, weather
|
||||
from aprsd.utils import package as aprsd_package
|
||||
|
||||
LOG = logging.getLogger('APRSD')
|
||||
PYPI_URL = 'https://pypi.org/search/'
|
||||
|
||||
|
||||
def onerror(name):
|
||||
print(f'Error importing module {name}')
|
||||
type, value, traceback = sys.exc_info()
|
||||
print_tb(traceback)
|
||||
|
||||
|
||||
def is_plugin(obj):
|
||||
for c in inspect.getmro(obj):
|
||||
if issubclass(c, aprsd_plugin.APRSDPluginBase):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def plugin_type(obj):
|
||||
for c in inspect.getmro(obj):
|
||||
if issubclass(c, aprsd_plugin.APRSDRegexCommandPluginBase):
|
||||
return 'RegexCommand'
|
||||
if issubclass(c, aprsd_plugin.APRSDWatchListPluginBase):
|
||||
return 'WatchList'
|
||||
if issubclass(c, aprsd_plugin.APRSDPluginBase):
|
||||
return 'APRSDPluginBase'
|
||||
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
def walk_package(package):
|
||||
return pkgutil.walk_packages(
|
||||
package.__path__,
|
||||
package.__name__ + '.',
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
|
||||
def get_module_info(package_name, module_name, module_path):
|
||||
if not os.path.exists(module_path):
|
||||
return None
|
||||
|
||||
dir_path = os.path.realpath(module_path)
|
||||
pattern = '*.py'
|
||||
|
||||
obj_list = []
|
||||
|
||||
for path, _subdirs, files in os.walk(dir_path):
|
||||
for name in files:
|
||||
if fnmatch.fnmatch(name, pattern):
|
||||
module = smuggle(f'{path}/{name}')
|
||||
for mem_name, obj in inspect.getmembers(module):
|
||||
if inspect.isclass(obj) and is_plugin(obj):
|
||||
obj_list.append(
|
||||
{
|
||||
'package': package_name,
|
||||
'name': mem_name,
|
||||
'obj': obj,
|
||||
'version': obj.version,
|
||||
'path': f'{".".join([module_name, obj.__name__])}',
|
||||
},
|
||||
)
|
||||
|
||||
return obj_list
|
||||
|
||||
|
||||
def _get_installed_aprsd_items():
|
||||
# installed plugins
|
||||
plugins = {}
|
||||
extensions = {}
|
||||
for _finder, name, ispkg in pkgutil.iter_modules():
|
||||
if ispkg and name.startswith('aprsd_'):
|
||||
module = importlib.import_module(name)
|
||||
pkgs = walk_package(module)
|
||||
for pkg in pkgs:
|
||||
pkg_info = get_module_info(
|
||||
module.__name__, pkg.name, module.__path__[0]
|
||||
)
|
||||
if 'plugin' in name:
|
||||
plugins[name] = pkg_info
|
||||
elif 'extension' in name:
|
||||
extensions[name] = pkg_info
|
||||
return plugins, extensions
|
||||
|
||||
|
||||
def get_installed_plugins():
|
||||
# installed plugins
|
||||
plugins, extensions = _get_installed_aprsd_items()
|
||||
return plugins
|
||||
|
||||
|
||||
def get_installed_extensions():
|
||||
# installed plugins
|
||||
plugins, extensions = _get_installed_aprsd_items()
|
||||
return extensions
|
||||
|
||||
|
||||
def show_built_in_plugins(console):
|
||||
@@ -157,34 +56,8 @@ def show_built_in_plugins(console):
|
||||
console.print(table)
|
||||
|
||||
|
||||
def _get_pypi_packages():
|
||||
if simple_r := requests.get(
|
||||
'https://pypi.org/simple',
|
||||
headers={'Accept': 'application/vnd.pypi.simple.v1+json'},
|
||||
):
|
||||
simple_response = simple_r.json()
|
||||
else:
|
||||
simple_response = {}
|
||||
|
||||
key = 'aprsd'
|
||||
matches = [
|
||||
p['name'] for p in simple_response['projects'] if p['name'].startswith(key)
|
||||
]
|
||||
|
||||
packages = []
|
||||
for pkg in matches:
|
||||
# Get info for first match
|
||||
if r := requests.get(
|
||||
f'https://pypi.org/pypi/{pkg}/json',
|
||||
headers={'Accept': 'application/json'},
|
||||
):
|
||||
packages.append(r.json())
|
||||
|
||||
return packages
|
||||
|
||||
|
||||
def show_pypi_plugins(installed_plugins, console):
|
||||
packages = _get_pypi_packages()
|
||||
packages = aprsd_package.get_pypi_packages()
|
||||
|
||||
title = Text.assemble(
|
||||
('Pypi.org APRSD Installable Plugin Packages\n\n', 'bold magenta'),
|
||||
@@ -225,7 +98,7 @@ def show_pypi_plugins(installed_plugins, console):
|
||||
|
||||
|
||||
def show_pypi_extensions(installed_extensions, console):
|
||||
packages = _get_pypi_packages()
|
||||
packages = aprsd_package.get_pypi_packages()
|
||||
|
||||
title = Text.assemble(
|
||||
('Pypi.org APRSD Installable Extension Packages\n\n', 'bold magenta'),
|
||||
@@ -282,7 +155,7 @@ def show_installed_plugins(installed_plugins, console):
|
||||
name.replace('_', '-'),
|
||||
plugin['name'],
|
||||
plugin['version'],
|
||||
plugin_type(plugin['obj']),
|
||||
aprsd_package.plugin_type(plugin['obj']),
|
||||
plugin['path'],
|
||||
)
|
||||
|
||||
@@ -302,7 +175,7 @@ def list_plugins(ctx):
|
||||
show_built_in_plugins(console)
|
||||
|
||||
status.update('Fetching pypi.org plugins')
|
||||
installed_plugins = get_installed_plugins()
|
||||
installed_plugins = aprsd_package.get_installed_plugins()
|
||||
show_pypi_plugins(installed_plugins, console)
|
||||
|
||||
status.update('Looking for installed APRSD plugins')
|
||||
@@ -321,5 +194,5 @@ def list_extensions(ctx):
|
||||
status.update('Fetching pypi.org APRSD Extensions')
|
||||
|
||||
status.update('Looking for installed APRSD Extensions')
|
||||
installed_extensions = get_installed_extensions()
|
||||
installed_extensions = aprsd_package.get_installed_extensions()
|
||||
show_pypi_extensions(installed_extensions, console)
|
||||
|
||||
@@ -221,7 +221,9 @@ def listen(
|
||||
# CONF.aprs_network.login = aprs_login
|
||||
# config["aprs"]["password"] = aprs_password
|
||||
|
||||
LOG.info(f'Python version: {sys.version}')
|
||||
LOG.info(f'APRSD Listen Started version: {aprsd.__version__}')
|
||||
utils.package.log_installed_extensions_and_plugins()
|
||||
|
||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||
collector.Collector()
|
||||
|
||||
@@ -13,6 +13,7 @@ from aprsd import (
|
||||
cli_helper,
|
||||
conf, # noqa : F401
|
||||
packets,
|
||||
utils,
|
||||
)
|
||||
from aprsd.client.client import APRSDClient
|
||||
from aprsd.main import cli
|
||||
@@ -89,7 +90,9 @@ def send_message(
|
||||
else:
|
||||
aprs_password = CONF.aprs_network.password
|
||||
|
||||
LOG.info(f'APRSD LISTEN Started version: {aprsd.__version__}')
|
||||
LOG.info(f'Python version: {sys.version}')
|
||||
LOG.info(f'APRSD SEND_MESSAGE Started version: {aprsd.__version__}')
|
||||
utils.package.log_installed_extensions_and_plugins()
|
||||
if type(command) is tuple:
|
||||
command = ' '.join(command)
|
||||
if not quiet:
|
||||
|
||||
@@ -40,12 +40,14 @@ def server(ctx, flush):
|
||||
|
||||
service_threads = service.ServiceThreads()
|
||||
|
||||
LOG.info(f'Python version: {sys.version}')
|
||||
LOG.info(f'APRSD Started version: {aprsd.__version__}')
|
||||
level, msg = utils._check_version()
|
||||
if level:
|
||||
LOG.warning(msg)
|
||||
else:
|
||||
LOG.info(msg)
|
||||
LOG.info(f'APRSD Started version: {aprsd.__version__}')
|
||||
utils.package.log_installed_extensions_and_plugins()
|
||||
|
||||
# Make sure we have 1 client transport enabled
|
||||
if not APRSDClient().is_enabled:
|
||||
|
||||
@@ -19,7 +19,7 @@ registry_group = cfg.OptGroup(
|
||||
aprsd_opts = [
|
||||
cfg.StrOpt(
|
||||
'callsign',
|
||||
required=True,
|
||||
default='NOCALL',
|
||||
help='Callsign to use for messages sent by APRSD',
|
||||
),
|
||||
cfg.BoolOpt(
|
||||
@@ -137,6 +137,12 @@ aprsd_opts = [
|
||||
help='Set this to False, to disable sending of ack packets. This will entirely stop'
|
||||
'APRSD from sending ack packets.',
|
||||
),
|
||||
cfg.BoolOpt(
|
||||
'is_digipi',
|
||||
default=False,
|
||||
help='Set this to True, if APRSD is running on a Digipi.'
|
||||
'This is useful for changing the behavior of APRSD to work with Digipi.',
|
||||
),
|
||||
]
|
||||
|
||||
watch_list_opts = [
|
||||
|
||||
@@ -13,3 +13,10 @@ class ConfigOptionBogusDefaultException(Exception):
|
||||
f"Config file option '{config_option}' needs to be "
|
||||
f"changed from provided default of '{default_fail}'"
|
||||
)
|
||||
|
||||
|
||||
class APRSClientNotConfiguredException(Exception):
|
||||
"""APRS client is not configured."""
|
||||
|
||||
def __init__(self):
|
||||
self.message = 'APRS client is not configured.'
|
||||
|
||||
+1
-1
@@ -275,7 +275,7 @@ class APRSDProcessPacketThread(APRSDFilterThread):
|
||||
def process_other_packet(self, packet, for_us=False):
|
||||
"""Process an APRS Packet that isn't a message or ack"""
|
||||
if not for_us:
|
||||
LOG.info("Got a packet meant for someone else '{packet.to_call}'")
|
||||
LOG.info(f"Got a packet meant for someone else '{packet.to_call}'")
|
||||
else:
|
||||
LOG.info('Got a non AckPacket/MessagePacket')
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import fnmatch
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import sys
|
||||
from traceback import print_tb
|
||||
|
||||
import requests
|
||||
from thesmuggler import smuggle
|
||||
|
||||
from aprsd import plugin as aprsd_plugin
|
||||
|
||||
LOG = logging.getLogger()
|
||||
|
||||
|
||||
def onerror(name):
|
||||
type, value, traceback = sys.exc_info()
|
||||
print_tb(traceback)
|
||||
|
||||
|
||||
def plugin_type(obj):
|
||||
for c in inspect.getmro(obj):
|
||||
if issubclass(c, aprsd_plugin.APRSDRegexCommandPluginBase):
|
||||
return 'RegexCommand'
|
||||
if issubclass(c, aprsd_plugin.APRSDWatchListPluginBase):
|
||||
return 'WatchList'
|
||||
if issubclass(c, aprsd_plugin.APRSDPluginBase):
|
||||
return 'APRSDPluginBase'
|
||||
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
def is_plugin(obj):
|
||||
for c in inspect.getmro(obj):
|
||||
if issubclass(c, aprsd_plugin.APRSDPluginBase):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def walk_package(package):
|
||||
return pkgutil.walk_packages(
|
||||
package.__path__,
|
||||
package.__name__ + '.',
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
|
||||
def get_module_info(package_name, module_name, module_path):
|
||||
if not os.path.exists(module_path):
|
||||
return None
|
||||
|
||||
dir_path = os.path.realpath(module_path)
|
||||
pattern = '*.py'
|
||||
|
||||
obj_list = []
|
||||
|
||||
for path, _subdirs, files in os.walk(dir_path):
|
||||
for name in files:
|
||||
if fnmatch.fnmatch(name, pattern):
|
||||
module = smuggle(f'{path}/{name}')
|
||||
for mem_name, obj in inspect.getmembers(module):
|
||||
if inspect.isclass(obj) and is_plugin(obj):
|
||||
obj_list.append(
|
||||
{
|
||||
'package': package_name,
|
||||
'name': mem_name,
|
||||
'obj': obj,
|
||||
'version': obj.version,
|
||||
'path': f'{".".join([module_name, obj.__name__])}',
|
||||
},
|
||||
)
|
||||
|
||||
return obj_list
|
||||
|
||||
|
||||
def is_aprsd_package(name):
|
||||
if name.startswith('aprsd_'):
|
||||
return True
|
||||
|
||||
|
||||
def is_aprsd_extension(name):
|
||||
if name.startswith('aprsd_') and 'extension' in name:
|
||||
# This is an installed package that is an extension of
|
||||
# APRSD
|
||||
return True
|
||||
else:
|
||||
# We might have an editable install of an extension
|
||||
# of APRSD.
|
||||
return '__editable__' in name and 'aprsd_' in name and 'extension' in name
|
||||
|
||||
|
||||
def get_installed_aprsd_items():
|
||||
# installed plugins
|
||||
plugins = {}
|
||||
extensions = {}
|
||||
for _finder, name, ispkg in pkgutil.iter_modules():
|
||||
if ispkg and is_aprsd_package(name):
|
||||
module = importlib.import_module(name)
|
||||
pkgs = walk_package(module)
|
||||
for pkg in pkgs:
|
||||
pkg_info = get_module_info(
|
||||
module.__name__, pkg.name, module.__path__[0]
|
||||
)
|
||||
if 'plugin' in name:
|
||||
plugins[name] = pkg_info
|
||||
elif 'extension' in name:
|
||||
mod = importlib.import_module(name)
|
||||
extensions[name] = mod
|
||||
elif is_aprsd_extension(name):
|
||||
# This isn't a package, so it could be an editable install
|
||||
module = importlib.import_module(name)
|
||||
key_name = next(iter(module.MAPPING.keys()))
|
||||
module = importlib.import_module(key_name)
|
||||
pkg_info = get_module_info(module.__name__, key_name, module.__path__[0])
|
||||
extensions[key_name] = module
|
||||
return plugins, extensions
|
||||
|
||||
|
||||
def get_installed_plugins():
|
||||
# installed plugins
|
||||
plugins, _ = get_installed_aprsd_items()
|
||||
return plugins
|
||||
|
||||
|
||||
def get_installed_extensions():
|
||||
# installed plugins
|
||||
_, extensions = get_installed_aprsd_items()
|
||||
return extensions
|
||||
|
||||
|
||||
def get_pypi_packages():
|
||||
if simple_r := requests.get(
|
||||
'https://pypi.org/simple',
|
||||
headers={'Accept': 'application/vnd.pypi.simple.v1+json'},
|
||||
):
|
||||
simple_response = simple_r.json()
|
||||
else:
|
||||
simple_response = {}
|
||||
|
||||
key = 'aprsd'
|
||||
matches = [
|
||||
p['name'] for p in simple_response['projects'] if p['name'].startswith(key)
|
||||
]
|
||||
|
||||
packages = []
|
||||
for pkg in matches:
|
||||
# Get info for first match
|
||||
if r := requests.get(
|
||||
f'https://pypi.org/pypi/{pkg}/json',
|
||||
headers={'Accept': 'application/json'},
|
||||
):
|
||||
packages.append(r.json())
|
||||
|
||||
return packages
|
||||
|
||||
|
||||
def log_installed_extensions_and_plugins():
|
||||
plugins, extensions = get_installed_aprsd_items()
|
||||
|
||||
for name in extensions:
|
||||
ext = extensions[name]
|
||||
# print(f"Extension: {ext}")
|
||||
# print(f"Extension: {ext.__dict__}")
|
||||
if hasattr(ext, '__version__'):
|
||||
version = ext.__version__
|
||||
elif hasattr(ext, 'version'):
|
||||
version = ext.version
|
||||
else:
|
||||
version = ext['version']
|
||||
LOG.info(f'Extension: {name} version: {version}')
|
||||
|
||||
for plugin in plugins:
|
||||
LOG.info(f'Plugin: {plugin} version: {plugins[plugin][0]["version"]}')
|
||||
@@ -353,6 +353,7 @@ class TestAPRSISDriver(unittest.TestCase):
|
||||
def test_consumer_success(self, mock_log):
|
||||
"""Test consumer forwards callback to client."""
|
||||
self.driver._client = self.mock_client
|
||||
self.driver.connected = True
|
||||
mock_callback = mock.MagicMock()
|
||||
|
||||
self.driver.consumer(mock_callback, raw=True)
|
||||
@@ -365,6 +366,7 @@ class TestAPRSISDriver(unittest.TestCase):
|
||||
def test_consumer_exception(self, mock_log):
|
||||
"""Test consumer handles exceptions."""
|
||||
self.driver._client = self.mock_client
|
||||
self.driver.connected = True
|
||||
mock_callback = mock.MagicMock()
|
||||
test_error = Exception('Test error')
|
||||
self.mock_client.consumer.side_effect = test_error
|
||||
@@ -381,8 +383,6 @@ class TestAPRSISDriver(unittest.TestCase):
|
||||
mock_callback = mock.MagicMock()
|
||||
|
||||
self.driver.consumer(mock_callback)
|
||||
|
||||
mock_log.warning.assert_called_once()
|
||||
self.assertFalse(self.driver.connected)
|
||||
|
||||
def test_stats_configured_with_client(self):
|
||||
|
||||
Reference in New Issue
Block a user