aprsd 4.2.0__py3-none-any.whl → 4.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aprsd/client/client.py CHANGED
@@ -10,14 +10,14 @@ from oslo_config import cfg
10
10
  from aprsd.client import drivers # noqa - ensure drivers are registered
11
11
  from aprsd.client.drivers.registry import DriverRegistry
12
12
  from aprsd.packets import core
13
- from aprsd.utils import keepalive_collector
13
+ from aprsd.utils import keepalive_collector, trace
14
14
 
15
15
  CONF = cfg.CONF
16
16
  LOG = logging.getLogger('APRSD')
17
17
  LOGU = logger
18
18
 
19
19
 
20
- class APRSDClient:
20
+ class APRSDClient(metaclass=trace.TraceWrapperMetaclass):
21
21
  """APRSD client class.
22
22
 
23
23
  This is a singleton class that provides a single instance of the APRSD client.
@@ -38,15 +38,18 @@ class APRSDClient:
38
38
  keepalive_collector.KeepAliveCollector().register(cls)
39
39
  return cls._instance
40
40
 
41
- def __init__(self):
41
+ def __init__(self, auto_connect: bool = True):
42
+ self.auto_connect = auto_connect
42
43
  self.connected = False
44
+ self.running = False
43
45
  self.login_status = {
44
46
  'success': False,
45
47
  'message': None,
46
48
  }
47
49
  if not self.driver:
48
50
  self.driver = DriverRegistry().get_driver()
49
- self.driver.setup_connection()
51
+ if self.auto_connect:
52
+ self.connect()
50
53
 
51
54
  def stats(self, serializable=False) -> dict:
52
55
  stats = {}
@@ -54,17 +57,20 @@ class APRSDClient:
54
57
  stats = self.driver.stats(serializable=serializable)
55
58
  return stats
56
59
 
57
- @property
58
- def is_enabled(self):
59
- if not self.driver:
60
- return False
61
- return self.driver.is_enabled()
62
-
63
- @property
64
- def is_configured(self):
65
- if not self.driver:
66
- return False
67
- return self.driver.is_configured()
60
+ @staticmethod
61
+ def is_enabled():
62
+ for driver in DriverRegistry().drivers:
63
+ if driver.is_enabled():
64
+ return True
65
+ return False
66
+
67
+ @staticmethod
68
+ def is_configured():
69
+ """Check if ANY driver is configured."""
70
+ for driver in DriverRegistry().drivers:
71
+ if driver.is_configured():
72
+ return True
73
+ return False
68
74
 
69
75
  # @property
70
76
  # def is_connected(self):
@@ -96,9 +102,20 @@ class APRSDClient:
96
102
  return self.driver.filter
97
103
 
98
104
  def is_alive(self):
99
- return self.driver.is_alive()
105
+ return self.driver.is_alive
106
+
107
+ @wrapt.synchronized(lock)
108
+ def connect(self):
109
+ if not self.driver:
110
+ self.driver = DriverRegistry().get_driver()
111
+ if not self.connected:
112
+ self.driver.setup_connection()
113
+ self.connected = self.driver.is_alive
114
+ self.running = True
100
115
 
101
116
  def close(self):
117
+ self.running = False
118
+ self.connected = False
102
119
  if not self.driver:
103
120
  return
104
121
  self.driver.close()
@@ -106,17 +123,22 @@ class APRSDClient:
106
123
  @wrapt.synchronized(lock)
107
124
  def reset(self):
108
125
  """Call this to force a rebuild/reconnect."""
109
- LOG.info('Resetting client connection.')
126
+ LOG.warning('Resetting client connection.')
110
127
  if self.driver:
111
128
  self.driver.close()
112
- self.driver.setup_connection()
129
+ if self.auto_connect:
130
+ self.driver.setup_connection()
113
131
  if self.filter:
114
132
  self.driver.set_filter(self.filter)
115
133
  else:
116
134
  LOG.warning('Client not initialized, nothing to reset.')
117
135
 
118
136
  def send(self, packet: core.Packet) -> bool:
119
- return self.driver.send(packet)
137
+ if self.running:
138
+ return self.driver.send(packet)
139
+ else:
140
+ LOG.error('Client not running, not sending packet.')
141
+ return False
120
142
 
121
143
  # For the keepalive collector
122
144
  def keepalive_check(self):
@@ -135,7 +157,13 @@ class APRSDClient:
135
157
  LOGU.opt(colors=True).info(f'<green>Client keepalive {keepalive}</green>')
136
158
 
137
159
  def consumer(self, callback: Callable, raw: bool = False):
138
- return self.driver.consumer(callback=callback, raw=raw)
160
+ if self.running:
161
+ return self.driver.consumer(callback=callback, raw=raw)
139
162
 
140
163
  def decode_packet(self, *args, **kwargs) -> core.Packet:
141
- return self.driver.decode_packet(*args, **kwargs)
164
+ try:
165
+ packet = self.driver.decode_packet(*args, **kwargs)
166
+ except Exception as e:
167
+ LOG.error(f'Error decoding packet: {e}')
168
+ return None
169
+ return packet
@@ -10,6 +10,7 @@ from oslo_config import cfg
10
10
  from aprsd import client, exception
11
11
  from aprsd.client.drivers.lib.aprslib import APRSLibClient
12
12
  from aprsd.packets import core
13
+ from aprsd.utils import singleton
13
14
 
14
15
  CONF = cfg.CONF
15
16
  LOG = logging.getLogger('APRSD')
@@ -17,6 +18,7 @@ LOGU = logger
17
18
 
18
19
 
19
20
  # class APRSISDriver(metaclass=trace.TraceWrapperMetaclass):
21
+ @singleton
20
22
  class APRSISDriver:
21
23
  """This is the APRS-IS driver for the APRSD client.
22
24
 
@@ -26,6 +28,7 @@ class APRSISDriver:
26
28
 
27
29
  _client = None
28
30
  _checks = False
31
+ connected = False
29
32
 
30
33
  def __init__(self):
31
34
  max_timeout = {'hours': 0.0, 'minutes': 2, 'seconds': 0}
@@ -77,16 +80,18 @@ class APRSISDriver:
77
80
  if self._client:
78
81
  self._client.stop()
79
82
  self._client.close()
83
+ self.connected = False
80
84
 
81
85
  def send(self, packet: core.Packet) -> bool:
82
86
  return self._client.send(packet)
83
87
 
84
88
  def setup_connection(self):
89
+ if self.connected:
90
+ return
85
91
  user = CONF.aprs_network.login
86
92
  password = CONF.aprs_network.password
87
93
  host = CONF.aprs_network.host
88
94
  port = CONF.aprs_network.port
89
- self.connected = False
90
95
  backoff = 1
91
96
  retries = 3
92
97
  retry_count = 0
@@ -96,7 +101,7 @@ class APRSISDriver:
96
101
  break
97
102
  try:
98
103
  LOG.info(
99
- f'Creating aprslib client({host}:{port}) and logging in {user}.'
104
+ f'Creating aprslib client({host}:{port}) and logging in {user}. try #{retry_count}'
100
105
  )
101
106
  self._client = APRSLibClient(
102
107
  user, passwd=password, host=host, port=port
@@ -151,7 +156,7 @@ class APRSISDriver:
151
156
  def _is_stale_connection(self):
152
157
  delta = datetime.datetime.now() - self._client.aprsd_keepalive
153
158
  if delta > self.max_delta:
154
- LOG.error(f'Connection is stale, last heard {delta} ago.')
159
+ LOG.warning(f'Connection is stale, last heard {delta} ago.')
155
160
  return True
156
161
  return False
157
162
 
@@ -164,7 +169,7 @@ class APRSISDriver:
164
169
  return core.factory(args[0])
165
170
 
166
171
  def consumer(self, callback: Callable, raw: bool = False):
167
- if self._client:
172
+ if self._client and self.connected:
168
173
  try:
169
174
  self._client.consumer(
170
175
  callback,
@@ -177,10 +182,9 @@ class APRSISDriver:
177
182
  LOG.info(e.__cause__)
178
183
  raise e
179
184
  else:
180
- LOG.warning('client is None, might be resetting.')
181
185
  self.connected = False
182
186
 
183
- def stats(self, serializable=False) -> dict:
187
+ def stats(self, serializable: bool = False) -> dict:
184
188
  stats = {}
185
189
  if self.is_configured():
186
190
  if self._client:
@@ -64,7 +64,7 @@ class APRSDFakeDriver(metaclass=trace.TraceWrapperMetaclass):
64
64
  return None
65
65
 
66
66
  @wrapt.synchronized(lock)
67
- def send(self, packet: core.Packet):
67
+ def send(self, packet: core.Packet) -> bool:
68
68
  """Send an APRS Message object."""
69
69
  LOG.info(f'Sending packet: {packet}')
70
70
  payload = None
@@ -84,6 +84,7 @@ class APRSDFakeDriver(metaclass=trace.TraceWrapperMetaclass):
84
84
  f"FAKE::Send '{payload}' TO '{packet.to_call}' From "
85
85
  f'\'{packet.from_call}\' with PATH "{self.path}"',
86
86
  )
87
+ return True
87
88
 
88
89
  def consumer(self, callback: Callable, raw: bool = False):
89
90
  LOG.debug('Start non blocking FAKE consumer')
@@ -20,6 +20,7 @@ class ClientDriver(Protocol):
20
20
  def is_configured(self) -> bool:
21
21
  pass
22
22
 
23
+ @property
23
24
  def is_alive(self) -> bool:
24
25
  pass
25
26
 
@@ -9,7 +9,6 @@ import datetime
9
9
  import logging
10
10
  import select
11
11
  import socket
12
- import time
13
12
  from typing import Any, Callable, Dict
14
13
 
15
14
  import aprslib
@@ -25,6 +24,7 @@ from aprsd import ( # noqa
25
24
  exception,
26
25
  )
27
26
  from aprsd.packets import core
27
+ from aprsd.utils import trace
28
28
 
29
29
  CONF = cfg.CONF
30
30
  LOG = logging.getLogger('APRSD')
@@ -46,10 +46,12 @@ def handle_fend(buffer: bytes, strip_df_start: bool = True) -> bytes:
46
46
  return bytes(frame)
47
47
 
48
48
 
49
- # class TCPKISSDriver(metaclass=trace.TraceWrapperMetaclass):
50
- class TCPKISSDriver:
49
+ class TCPKISSDriver(metaclass=trace.TraceWrapperMetaclass):
50
+ # class TCPKISSDriver:
51
51
  """APRSD client driver for TCP KISS connections."""
52
52
 
53
+ _instance = None
54
+
53
55
  # Class level attributes required by Client protocol
54
56
  packets_received = 0
55
57
  packets_sent = 0
@@ -62,6 +64,12 @@ class TCPKISSDriver:
62
64
  select_timeout = 1
63
65
  path = None
64
66
 
67
+ def __new__(cls, *args, **kwargs):
68
+ """This magic turns this into a singleton."""
69
+ if cls._instance is None:
70
+ cls._instance = super().__new__(cls)
71
+ return cls._instance
72
+
65
73
  def __init__(self):
66
74
  """Initialize the KISS client.
67
75
 
@@ -71,7 +79,6 @@ class TCPKISSDriver:
71
79
  super().__init__()
72
80
  self._connected = False
73
81
  self.keepalive = datetime.datetime.now()
74
- self._running = False
75
82
  # This is initialized in setup_connection()
76
83
  self.socket = None
77
84
 
@@ -79,8 +86,8 @@ class TCPKISSDriver:
79
86
  def transport(self) -> str:
80
87
  return client.TRANSPORT_TCPKISS
81
88
 
82
- @classmethod
83
- def is_enabled(cls) -> bool:
89
+ @staticmethod
90
+ def is_enabled() -> bool:
84
91
  """Check if KISS is enabled in configuration.
85
92
 
86
93
  Returns:
@@ -111,7 +118,15 @@ class TCPKISSDriver:
111
118
 
112
119
  def close(self):
113
120
  """Close the connection."""
114
- self.stop()
121
+ self._connected = False
122
+ if self.socket:
123
+ try:
124
+ self.socket.close()
125
+ except Exception as e:
126
+ LOG.error(f'close: error closing socket: {e}')
127
+ pass
128
+ else:
129
+ LOG.warning('close: socket not initialized. no reason to close.')
115
130
 
116
131
  def send(self, packet: core.Packet):
117
132
  """Send an APRS packet.
@@ -163,6 +178,10 @@ class TCPKISSDriver:
163
178
  LOG.error('KISS is not enabled in configuration')
164
179
  return
165
180
 
181
+ if self._connected:
182
+ LOG.warning('KISS interface already connected')
183
+ return
184
+
166
185
  try:
167
186
  # Configure for TCP KISS
168
187
  if self.is_enabled():
@@ -217,22 +236,18 @@ class TCPKISSDriver:
217
236
  Raises:
218
237
  Exception: If not connected to KISS TNC
219
238
  """
220
- self._running = True
221
- while self._running:
222
- # Ensure connection
223
- if not self._connected:
224
- if not self.connect():
225
- time.sleep(1)
226
- continue
239
+ # Ensure connection
240
+ if not self._connected:
241
+ return
227
242
 
228
- # Read frame
229
- frame = self.read_frame()
230
- if frame:
231
- LOG.warning(f'GOT FRAME: {frame} calling {callback}')
232
- kwargs = {
233
- 'frame': frame,
234
- }
235
- callback(**kwargs)
243
+ # Read frame
244
+ frame = self.read_frame()
245
+ if frame:
246
+ LOG.info(f'GOT FRAME: {frame} calling {callback}')
247
+ kwargs = {
248
+ 'frame': frame,
249
+ }
250
+ callback(**kwargs)
236
251
 
237
252
  def decode_packet(self, *args, **kwargs) -> core.Packet:
238
253
  """Decode a packet from an AX.25 frame.
@@ -245,24 +260,17 @@ class TCPKISSDriver:
245
260
  LOG.warning('No frame received to decode?!?!')
246
261
  return None
247
262
 
248
- LOG.warning(f'FRAME: {str(frame)}')
249
263
  try:
250
264
  aprslib_frame = aprslib.parse(str(frame))
251
- return core.factory(aprslib_frame)
265
+ packet = core.factory(aprslib_frame)
266
+ if isinstance(packet, core.ThirdPartyPacket):
267
+ return packet.subpacket
268
+ else:
269
+ return packet
252
270
  except Exception as e:
253
271
  LOG.error(f'Error decoding packet: {e}')
254
272
  return None
255
273
 
256
- def stop(self):
257
- """Stop the KISS interface."""
258
- self._running = False
259
- self._connected = False
260
- if self.socket:
261
- try:
262
- self.socket.close()
263
- except Exception:
264
- pass
265
-
266
274
  def stats(self, serializable: bool = False) -> Dict[str, Any]:
267
275
  """Get client statistics.
268
276
 
@@ -271,8 +279,19 @@ class TCPKISSDriver:
271
279
  """
272
280
  if serializable:
273
281
  keepalive = self.keepalive.isoformat()
282
+ if self.last_packet_sent:
283
+ last_packet_sent = self.last_packet_sent.isoformat()
284
+ else:
285
+ last_packet_sent = 'None'
286
+ if self.last_packet_received:
287
+ last_packet_received = self.last_packet_received.isoformat()
288
+ else:
289
+ last_packet_received = 'None'
274
290
  else:
275
291
  keepalive = self.keepalive
292
+ last_packet_sent = self.last_packet_sent
293
+ last_packet_received = self.last_packet_received
294
+
276
295
  stats = {
277
296
  'client': self.__class__.__name__,
278
297
  'transport': self.transport,
@@ -280,8 +299,8 @@ class TCPKISSDriver:
280
299
  'path': self.path,
281
300
  'packets_sent': self.packets_sent,
282
301
  'packets_received': self.packets_received,
283
- 'last_packet_sent': self.last_packet_sent,
284
- 'last_packet_received': self.last_packet_received,
302
+ 'last_packet_sent': last_packet_sent,
303
+ 'last_packet_received': last_packet_received,
285
304
  'connection_keepalive': keepalive,
286
305
  'host': CONF.kiss_tcp.host,
287
306
  'port': CONF.kiss_tcp.port,
@@ -338,13 +357,19 @@ class TCPKISSDriver:
338
357
  """
339
358
  Generator for complete lines, received from the server
340
359
  """
360
+ if not self.socket:
361
+ return None
362
+
363
+ if not self._connected:
364
+ return None
365
+
341
366
  try:
342
367
  self.socket.setblocking(0)
343
368
  except OSError as e:
344
369
  LOG.error(f'socket error when setblocking(0): {str(e)}')
345
370
  raise aprslib.ConnectionDrop('connection dropped') from e
346
371
 
347
- while self._running:
372
+ while self._connected:
348
373
  short_buf = b''
349
374
 
350
375
  try:
@@ -360,14 +385,14 @@ class TCPKISSDriver:
360
385
  else:
361
386
  continue
362
387
  except Exception as e:
388
+ # No need to log if we are not running.
389
+ # this happens when the client is stopped/closed.
363
390
  LOG.error(f'Error in read loop: {e}')
364
391
  self._connected = False
365
392
  break
366
393
 
367
394
  try:
368
- print('reading from socket')
369
395
  short_buf = self.socket.recv(1024)
370
- print(f'short_buf: {short_buf}')
371
396
  # sock.recv returns empty if the connection drops
372
397
  if not short_buf:
373
398
  if not blocking:
aprsd/cmds/dev.py CHANGED
@@ -4,11 +4,13 @@
4
4
  #
5
5
  # python included libs
6
6
  import logging
7
+ import sys
7
8
 
8
9
  import click
9
10
  from oslo_config import cfg
10
11
 
11
- from aprsd import cli_helper, conf, packets, plugin
12
+ import aprsd
13
+ from aprsd import cli_helper, conf, packets, plugin, utils
12
14
 
13
15
  # local imports here
14
16
  from aprsd.main import cli
@@ -71,6 +73,9 @@ def test_plugin(
71
73
  ):
72
74
  """Test an individual APRSD plugin given a python path."""
73
75
 
76
+ LOG.info(f'Python version: {sys.version}')
77
+ LOG.info(f'APRSD DEV Started version: {aprsd.__version__}')
78
+ utils.package.log_installed_extensions_and_plugins()
74
79
  CONF.log_opt_values(LOG, logging.DEBUG)
75
80
 
76
81
  if not aprs_login:
aprsd/cmds/fetch_stats.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # Fetch active stats from a remote running instance of aprsd admin web interface.
2
2
  import logging
3
+ import sys
3
4
 
4
5
  import click
5
6
  import requests
@@ -38,6 +39,7 @@ CONF = cfg.CONF
38
39
  def fetch_stats(ctx, host, port):
39
40
  """Fetch stats from a APRSD admin web interface."""
40
41
  console = Console()
42
+ console.print(f'Python version: {sys.version}')
41
43
  console.print(f'APRSD Fetch-Stats started version: {aprsd.__version__}')
42
44
 
43
45
  CONF.log_opt_values(LOG, logging.DEBUG)
@@ -1,119 +1,18 @@
1
- import fnmatch
2
- import importlib
3
1
  import inspect
4
2
  import logging
5
- import os
6
- import pkgutil
7
- import sys
8
- from traceback import print_tb
9
3
 
10
4
  import click
11
- import requests
12
5
  from rich.console import Console
13
6
  from rich.table import Table
14
7
  from rich.text import Text
15
- from thesmuggler import smuggle
16
8
 
17
9
  from aprsd import cli_helper
18
10
  from aprsd import plugin as aprsd_plugin
19
11
  from aprsd.main import cli
20
12
  from aprsd.plugins import fortune, notify, ping, time, version, weather
13
+ from aprsd.utils import package as aprsd_package
21
14
 
22
15
  LOG = logging.getLogger('APRSD')
23
- PYPI_URL = 'https://pypi.org/search/'
24
-
25
-
26
- def onerror(name):
27
- print(f'Error importing module {name}')
28
- type, value, traceback = sys.exc_info()
29
- print_tb(traceback)
30
-
31
-
32
- def is_plugin(obj):
33
- for c in inspect.getmro(obj):
34
- if issubclass(c, aprsd_plugin.APRSDPluginBase):
35
- return True
36
-
37
- return False
38
-
39
-
40
- def plugin_type(obj):
41
- for c in inspect.getmro(obj):
42
- if issubclass(c, aprsd_plugin.APRSDRegexCommandPluginBase):
43
- return 'RegexCommand'
44
- if issubclass(c, aprsd_plugin.APRSDWatchListPluginBase):
45
- return 'WatchList'
46
- if issubclass(c, aprsd_plugin.APRSDPluginBase):
47
- return 'APRSDPluginBase'
48
-
49
- return 'Unknown'
50
-
51
-
52
- def walk_package(package):
53
- return pkgutil.walk_packages(
54
- package.__path__,
55
- package.__name__ + '.',
56
- onerror=onerror,
57
- )
58
-
59
-
60
- def get_module_info(package_name, module_name, module_path):
61
- if not os.path.exists(module_path):
62
- return None
63
-
64
- dir_path = os.path.realpath(module_path)
65
- pattern = '*.py'
66
-
67
- obj_list = []
68
-
69
- for path, _subdirs, files in os.walk(dir_path):
70
- for name in files:
71
- if fnmatch.fnmatch(name, pattern):
72
- module = smuggle(f'{path}/{name}')
73
- for mem_name, obj in inspect.getmembers(module):
74
- if inspect.isclass(obj) and is_plugin(obj):
75
- obj_list.append(
76
- {
77
- 'package': package_name,
78
- 'name': mem_name,
79
- 'obj': obj,
80
- 'version': obj.version,
81
- 'path': f'{".".join([module_name, obj.__name__])}',
82
- },
83
- )
84
-
85
- return obj_list
86
-
87
-
88
- def _get_installed_aprsd_items():
89
- # installed plugins
90
- plugins = {}
91
- extensions = {}
92
- for _finder, name, ispkg in pkgutil.iter_modules():
93
- if ispkg and name.startswith('aprsd_'):
94
- module = importlib.import_module(name)
95
- pkgs = walk_package(module)
96
- for pkg in pkgs:
97
- pkg_info = get_module_info(
98
- module.__name__, pkg.name, module.__path__[0]
99
- )
100
- if 'plugin' in name:
101
- plugins[name] = pkg_info
102
- elif 'extension' in name:
103
- extensions[name] = pkg_info
104
- return plugins, extensions
105
-
106
-
107
- def get_installed_plugins():
108
- # installed plugins
109
- plugins, extensions = _get_installed_aprsd_items()
110
- return plugins
111
-
112
-
113
- def get_installed_extensions():
114
- # installed plugins
115
- plugins, extensions = _get_installed_aprsd_items()
116
- return extensions
117
16
 
118
17
 
119
18
  def show_built_in_plugins(console):
@@ -157,34 +56,8 @@ def show_built_in_plugins(console):
157
56
  console.print(table)
158
57
 
159
58
 
160
- def _get_pypi_packages():
161
- if simple_r := requests.get(
162
- 'https://pypi.org/simple',
163
- headers={'Accept': 'application/vnd.pypi.simple.v1+json'},
164
- ):
165
- simple_response = simple_r.json()
166
- else:
167
- simple_response = {}
168
-
169
- key = 'aprsd'
170
- matches = [
171
- p['name'] for p in simple_response['projects'] if p['name'].startswith(key)
172
- ]
173
-
174
- packages = []
175
- for pkg in matches:
176
- # Get info for first match
177
- if r := requests.get(
178
- f'https://pypi.org/pypi/{pkg}/json',
179
- headers={'Accept': 'application/json'},
180
- ):
181
- packages.append(r.json())
182
-
183
- return packages
184
-
185
-
186
59
  def show_pypi_plugins(installed_plugins, console):
187
- packages = _get_pypi_packages()
60
+ packages = aprsd_package.get_pypi_packages()
188
61
 
189
62
  title = Text.assemble(
190
63
  ('Pypi.org APRSD Installable Plugin Packages\n\n', 'bold magenta'),
@@ -225,7 +98,7 @@ def show_pypi_plugins(installed_plugins, console):
225
98
 
226
99
 
227
100
  def show_pypi_extensions(installed_extensions, console):
228
- packages = _get_pypi_packages()
101
+ packages = aprsd_package.get_pypi_packages()
229
102
 
230
103
  title = Text.assemble(
231
104
  ('Pypi.org APRSD Installable Extension Packages\n\n', 'bold magenta'),
@@ -282,7 +155,7 @@ def show_installed_plugins(installed_plugins, console):
282
155
  name.replace('_', '-'),
283
156
  plugin['name'],
284
157
  plugin['version'],
285
- plugin_type(plugin['obj']),
158
+ aprsd_package.plugin_type(plugin['obj']),
286
159
  plugin['path'],
287
160
  )
288
161
 
@@ -302,7 +175,7 @@ def list_plugins(ctx):
302
175
  show_built_in_plugins(console)
303
176
 
304
177
  status.update('Fetching pypi.org plugins')
305
- installed_plugins = get_installed_plugins()
178
+ installed_plugins = aprsd_package.get_installed_plugins()
306
179
  show_pypi_plugins(installed_plugins, console)
307
180
 
308
181
  status.update('Looking for installed APRSD plugins')
@@ -321,5 +194,5 @@ def list_extensions(ctx):
321
194
  status.update('Fetching pypi.org APRSD Extensions')
322
195
 
323
196
  status.update('Looking for installed APRSD Extensions')
324
- installed_extensions = get_installed_extensions()
197
+ installed_extensions = aprsd_package.get_installed_extensions()
325
198
  show_pypi_extensions(installed_extensions, console)
aprsd/cmds/listen.py CHANGED
@@ -221,7 +221,9 @@ def listen(
221
221
  # CONF.aprs_network.login = aprs_login
222
222
  # config["aprs"]["password"] = aprs_password
223
223
 
224
+ LOG.info(f'Python version: {sys.version}')
224
225
  LOG.info(f'APRSD Listen Started version: {aprsd.__version__}')
226
+ utils.package.log_installed_extensions_and_plugins()
225
227
 
226
228
  CONF.log_opt_values(LOG, logging.DEBUG)
227
229
  collector.Collector()
@@ -13,6 +13,7 @@ from aprsd import (
13
13
  cli_helper,
14
14
  conf, # noqa : F401
15
15
  packets,
16
+ utils,
16
17
  )
17
18
  from aprsd.client.client import APRSDClient
18
19
  from aprsd.main import cli
@@ -89,7 +90,9 @@ def send_message(
89
90
  else:
90
91
  aprs_password = CONF.aprs_network.password
91
92
 
92
- LOG.info(f'APRSD LISTEN Started version: {aprsd.__version__}')
93
+ LOG.info(f'Python version: {sys.version}')
94
+ LOG.info(f'APRSD SEND_MESSAGE Started version: {aprsd.__version__}')
95
+ utils.package.log_installed_extensions_and_plugins()
93
96
  if type(command) is tuple:
94
97
  command = ' '.join(command)
95
98
  if not quiet:
aprsd/cmds/server.py CHANGED
@@ -40,12 +40,14 @@ def server(ctx, flush):
40
40
 
41
41
  service_threads = service.ServiceThreads()
42
42
 
43
+ LOG.info(f'Python version: {sys.version}')
44
+ LOG.info(f'APRSD Started version: {aprsd.__version__}')
43
45
  level, msg = utils._check_version()
44
46
  if level:
45
47
  LOG.warning(msg)
46
48
  else:
47
49
  LOG.info(msg)
48
- LOG.info(f'APRSD Started version: {aprsd.__version__}')
50
+ utils.package.log_installed_extensions_and_plugins()
49
51
 
50
52
  # Make sure we have 1 client transport enabled
51
53
  if not APRSDClient().is_enabled:
aprsd/conf/common.py CHANGED
@@ -19,7 +19,7 @@ registry_group = cfg.OptGroup(
19
19
  aprsd_opts = [
20
20
  cfg.StrOpt(
21
21
  'callsign',
22
- required=True,
22
+ default='NOCALL',
23
23
  help='Callsign to use for messages sent by APRSD',
24
24
  ),
25
25
  cfg.BoolOpt(
@@ -137,6 +137,12 @@ aprsd_opts = [
137
137
  help='Set this to False, to disable sending of ack packets. This will entirely stop'
138
138
  'APRSD from sending ack packets.',
139
139
  ),
140
+ cfg.BoolOpt(
141
+ 'is_digipi',
142
+ default=False,
143
+ help='Set this to True, if APRSD is running on a Digipi.'
144
+ 'This is useful for changing the behavior of APRSD to work with Digipi.',
145
+ ),
140
146
  ]
141
147
 
142
148
  watch_list_opts = [
aprsd/exception.py CHANGED
@@ -13,3 +13,10 @@ class ConfigOptionBogusDefaultException(Exception):
13
13
  f"Config file option '{config_option}' needs to be "
14
14
  f"changed from provided default of '{default_fail}'"
15
15
  )
16
+
17
+
18
+ class APRSClientNotConfiguredException(Exception):
19
+ """APRS client is not configured."""
20
+
21
+ def __init__(self):
22
+ self.message = 'APRS client is not configured.'
aprsd/main.py CHANGED
@@ -72,6 +72,7 @@ def main():
72
72
 
73
73
  def signal_handler(sig, frame):
74
74
  click.echo('signal_handler: called')
75
+ collector.Collector().stop_all()
75
76
  threads.APRSDThreadList().stop_all()
76
77
  if 'subprocess' not in str(frame):
77
78
  LOG.info(
aprsd/stats/collector.py CHANGED
@@ -44,3 +44,9 @@ class Collector:
44
44
  if not isinstance(producer_name, StatsProducer):
45
45
  raise TypeError(f'Producer {producer_name} is not a StatsProducer')
46
46
  self.producers.remove(producer_name)
47
+
48
+ def stop_all(self):
49
+ """Stop and unregister all registered stats producers."""
50
+ for producer in self.producers[:]:
51
+ LOG.info(f'Stopping Stats producer {producer}')
52
+ self.unregister_producer(producer)
aprsd/threads/rx.py CHANGED
@@ -275,7 +275,7 @@ class APRSDProcessPacketThread(APRSDFilterThread):
275
275
  def process_other_packet(self, packet, for_us=False):
276
276
  """Process an APRS Packet that isn't a message or ack"""
277
277
  if not for_us:
278
- LOG.info("Got a packet meant for someone else '{packet.to_call}'")
278
+ LOG.info(f"Got a packet meant for someone else '{packet.to_call}'")
279
279
  else:
280
280
  LOG.info('Got a non AckPacket/MessagePacket')
281
281
 
aprsd/utils/package.py ADDED
@@ -0,0 +1,176 @@
1
+ import fnmatch
2
+ import importlib
3
+ import inspect
4
+ import logging
5
+ import os
6
+ import pkgutil
7
+ import sys
8
+ from traceback import print_tb
9
+
10
+ import requests
11
+ from thesmuggler import smuggle
12
+
13
+ from aprsd import plugin as aprsd_plugin
14
+
15
+ LOG = logging.getLogger()
16
+
17
+
18
+ def onerror(name):
19
+ type, value, traceback = sys.exc_info()
20
+ print_tb(traceback)
21
+
22
+
23
+ def plugin_type(obj):
24
+ for c in inspect.getmro(obj):
25
+ if issubclass(c, aprsd_plugin.APRSDRegexCommandPluginBase):
26
+ return 'RegexCommand'
27
+ if issubclass(c, aprsd_plugin.APRSDWatchListPluginBase):
28
+ return 'WatchList'
29
+ if issubclass(c, aprsd_plugin.APRSDPluginBase):
30
+ return 'APRSDPluginBase'
31
+
32
+ return 'Unknown'
33
+
34
+
35
+ def is_plugin(obj):
36
+ for c in inspect.getmro(obj):
37
+ if issubclass(c, aprsd_plugin.APRSDPluginBase):
38
+ return True
39
+
40
+ return False
41
+
42
+
43
+ def walk_package(package):
44
+ return pkgutil.walk_packages(
45
+ package.__path__,
46
+ package.__name__ + '.',
47
+ onerror=onerror,
48
+ )
49
+
50
+
51
+ def get_module_info(package_name, module_name, module_path):
52
+ if not os.path.exists(module_path):
53
+ return None
54
+
55
+ dir_path = os.path.realpath(module_path)
56
+ pattern = '*.py'
57
+
58
+ obj_list = []
59
+
60
+ for path, _subdirs, files in os.walk(dir_path):
61
+ for name in files:
62
+ if fnmatch.fnmatch(name, pattern):
63
+ module = smuggle(f'{path}/{name}')
64
+ for mem_name, obj in inspect.getmembers(module):
65
+ if inspect.isclass(obj) and is_plugin(obj):
66
+ obj_list.append(
67
+ {
68
+ 'package': package_name,
69
+ 'name': mem_name,
70
+ 'obj': obj,
71
+ 'version': obj.version,
72
+ 'path': f'{".".join([module_name, obj.__name__])}',
73
+ },
74
+ )
75
+
76
+ return obj_list
77
+
78
+
79
+ def is_aprsd_package(name):
80
+ if name.startswith('aprsd_'):
81
+ return True
82
+
83
+
84
+ def is_aprsd_extension(name):
85
+ if name.startswith('aprsd_') and 'extension' in name:
86
+ # This is an installed package that is an extension of
87
+ # APRSD
88
+ return True
89
+ else:
90
+ # We might have an editable install of an extension
91
+ # of APRSD.
92
+ return '__editable__' in name and 'aprsd_' in name and 'extension' in name
93
+
94
+
95
+ def get_installed_aprsd_items():
96
+ # installed plugins
97
+ plugins = {}
98
+ extensions = {}
99
+ for _finder, name, ispkg in pkgutil.iter_modules():
100
+ if ispkg and is_aprsd_package(name):
101
+ module = importlib.import_module(name)
102
+ pkgs = walk_package(module)
103
+ for pkg in pkgs:
104
+ pkg_info = get_module_info(
105
+ module.__name__, pkg.name, module.__path__[0]
106
+ )
107
+ if 'plugin' in name:
108
+ plugins[name] = pkg_info
109
+ elif 'extension' in name:
110
+ mod = importlib.import_module(name)
111
+ extensions[name] = mod
112
+ elif is_aprsd_extension(name):
113
+ # This isn't a package, so it could be an editable install
114
+ module = importlib.import_module(name)
115
+ key_name = next(iter(module.MAPPING.keys()))
116
+ module = importlib.import_module(key_name)
117
+ pkg_info = get_module_info(module.__name__, key_name, module.__path__[0])
118
+ extensions[key_name] = module
119
+ return plugins, extensions
120
+
121
+
122
+ def get_installed_plugins():
123
+ # installed plugins
124
+ plugins, _ = get_installed_aprsd_items()
125
+ return plugins
126
+
127
+
128
+ def get_installed_extensions():
129
+ # installed plugins
130
+ _, extensions = get_installed_aprsd_items()
131
+ return extensions
132
+
133
+
134
+ def get_pypi_packages():
135
+ if simple_r := requests.get(
136
+ 'https://pypi.org/simple',
137
+ headers={'Accept': 'application/vnd.pypi.simple.v1+json'},
138
+ ):
139
+ simple_response = simple_r.json()
140
+ else:
141
+ simple_response = {}
142
+
143
+ key = 'aprsd'
144
+ matches = [
145
+ p['name'] for p in simple_response['projects'] if p['name'].startswith(key)
146
+ ]
147
+
148
+ packages = []
149
+ for pkg in matches:
150
+ # Get info for first match
151
+ if r := requests.get(
152
+ f'https://pypi.org/pypi/{pkg}/json',
153
+ headers={'Accept': 'application/json'},
154
+ ):
155
+ packages.append(r.json())
156
+
157
+ return packages
158
+
159
+
160
+ def log_installed_extensions_and_plugins():
161
+ plugins, extensions = get_installed_aprsd_items()
162
+
163
+ for name in extensions:
164
+ ext = extensions[name]
165
+ # print(f"Extension: {ext}")
166
+ # print(f"Extension: {ext.__dict__}")
167
+ if hasattr(ext, '__version__'):
168
+ version = ext.__version__
169
+ elif hasattr(ext, 'version'):
170
+ version = ext.version
171
+ else:
172
+ version = ext['version']
173
+ LOG.info(f'Extension: {name} version: {version}')
174
+
175
+ for plugin in plugins:
176
+ LOG.info(f'Plugin: {plugin} version: {plugins[plugin][0]["version"]}')
aprsd/utils/trace.py CHANGED
@@ -5,11 +5,11 @@ import logging
5
5
  import time
6
6
  import types
7
7
 
8
- VALID_TRACE_FLAGS = {"method", "api"}
8
+ VALID_TRACE_FLAGS = {'method', 'api'}
9
9
  TRACE_API = False
10
10
  TRACE_METHOD = False
11
11
  TRACE_ENABLED = False
12
- LOG = logging.getLogger("APRSD")
12
+ LOG = logging.getLogger('APRSD')
13
13
 
14
14
 
15
15
  def trace(*dec_args, **dec_kwargs):
@@ -27,11 +27,12 @@ def trace(*dec_args, **dec_kwargs):
27
27
 
28
28
  def _decorator(f):
29
29
  func_name = f.__qualname__
30
- func_file = "/".join(f.__code__.co_filename.split("/")[-4:])
30
+ func_file = '/'.join(f.__code__.co_filename.split('/')[-4:])
31
+ func_line_number = f.__code__.co_firstlineno
31
32
 
32
33
  @functools.wraps(f)
33
34
  def trace_logging_wrapper(*args, **kwargs):
34
- filter_function = dec_kwargs.get("filter_function")
35
+ filter_function = dec_kwargs.get('filter_function')
35
36
  logger = LOG
36
37
 
37
38
  # NOTE(ameade): Don't bother going any further if DEBUG log level
@@ -40,16 +41,16 @@ def trace(*dec_args, **dec_kwargs):
40
41
  return f(*args, **kwargs)
41
42
 
42
43
  all_args = inspect.getcallargs(f, *args, **kwargs)
43
-
44
44
  pass_filter = filter_function is None or filter_function(all_args)
45
45
 
46
46
  if pass_filter:
47
47
  logger.debug(
48
- "==> %(func)s: call %(all_args)r file: %(file)s",
48
+ '==> %(func)s: call %(all_args)r file: %(file)s:%(line_number)d',
49
49
  {
50
- "func": func_name,
51
- "all_args": str(all_args),
52
- "file": func_file,
50
+ 'func': func_name,
51
+ 'all_args': str(all_args),
52
+ 'file': func_file,
53
+ 'line_number': func_line_number,
53
54
  },
54
55
  )
55
56
 
@@ -59,11 +60,11 @@ def trace(*dec_args, **dec_kwargs):
59
60
  except Exception as exc:
60
61
  total_time = int(round(time.time() * 1000)) - start_time
61
62
  logger.debug(
62
- "<== %(func)s: exception (%(time)dms) %(exc)r",
63
+ '<== %(func)s: exception (%(time)dms) %(exc)r',
63
64
  {
64
- "func": func_name,
65
- "time": total_time,
66
- "exc": exc,
65
+ 'func': func_name,
66
+ 'time': total_time,
67
+ 'exc': exc,
67
68
  },
68
69
  )
69
70
  raise
@@ -78,11 +79,11 @@ def trace(*dec_args, **dec_kwargs):
78
79
 
79
80
  if pass_filter:
80
81
  logger.debug(
81
- "<== %(func)s: return (%(time)dms) %(result)r",
82
+ '<== %(func)s: return (%(time)dms) %(result)r',
82
83
  {
83
- "func": func_name,
84
- "time": total_time,
85
- "result": mask_result,
84
+ 'func': func_name,
85
+ 'time': total_time,
86
+ 'result': mask_result,
86
87
  },
87
88
  )
88
89
  return result
@@ -174,7 +175,7 @@ def setup_tracing(trace_flags):
174
175
  except TypeError: # Handle when trace_flags is None or a test mock
175
176
  trace_flags = []
176
177
  for invalid_flag in set(trace_flags) - VALID_TRACE_FLAGS:
177
- LOG.warning("Invalid trace flag: %s", invalid_flag)
178
- TRACE_METHOD = "method" in trace_flags
179
- TRACE_API = "api" in trace_flags
178
+ LOG.warning('Invalid trace flag: %s', invalid_flag)
179
+ TRACE_METHOD = 'method' in trace_flags
180
+ TRACE_API = 'api' in trace_flags
180
181
  TRACE_ENABLED = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aprsd
3
- Version: 4.2.0
3
+ Version: 4.2.3
4
4
  Summary: APRSd is a APRS-IS server that can be used to connect to APRS-IS and send and receive APRS packets.
5
5
  Author-email: Craig Lamparter <craig@craiger.org>, "Walter A. Boring IV" <waboring@hemna.com>, Emre Saglam <emresaglam@gmail.com>, Jason Martin <jhmartin@toger.us>, John <johng42@users.noreply.github.com>, Martiros Shakhzadyan <vrzh@vrzh.net>, Zoe Moore <zoenb@mailbox.org>, ranguli <hello@joshmurphy.ca>
6
6
  Maintainer-email: Craig Lamparter <craig@craiger.org>, "Walter A. Boring IV" <waboring@hemna.com>
@@ -1,31 +1,31 @@
1
1
  aprsd/__init__.py,sha256=ci_49KK2a4GXyxcM2lFZfNAOsBfXzh0yayIGQazw56I,687
2
2
  aprsd/cli_helper.py,sha256=mKHww_cStwFBThjntFylrSnF6n9jOPHlR8r9SsrCxdY,4605
3
- aprsd/exception.py,sha256=Oi6w0ISPemX0UpLB40BYnZBT1YOU-Pnpj8yf-A0E4-E,504
4
- aprsd/main.py,sha256=sOzG4tKtm7FGxwvYWIwiPujzo5TV3F0cSkR7IWHlb8M,4719
3
+ aprsd/exception.py,sha256=bzln7sP9USQ67tz5nQh6e4KMaS8X2uhPJMNVmCkKV30,679
4
+ aprsd/main.py,sha256=JDI8VUGvi8KSG4hH125OlvdXC4xtwyakU0cB2YHpTXA,4756
5
5
  aprsd/plugin.py,sha256=pdyvKhSugFo4vRy7VflnaZ-BKozGa3GTTAHErskW3WU,17848
6
6
  aprsd/plugin_utils.py,sha256=beXQ1n_YI38rqDwswtE8NjFwFzjpaeve9W_JotX1n1A,2544
7
7
  aprsd/client/__init__.py,sha256=lh7mKBoSo2Tt82QoqlAARGm4LY6ffsn7d8x6M4B7n9g,154
8
- aprsd/client/client.py,sha256=i4KBGT_-6btH9e3X2moyGYz7OGHLHgbS8qqYMmlsH_o,3912
8
+ aprsd/client/client.py,sha256=sTZaxduDyGYtBpxpOvYAGG6D6gz6CPsivou9eKnW0tw,4898
9
9
  aprsd/client/stats.py,sha256=dUqRZao04B2TTj9TGMSygdLHa3if3pEy86eK-fK4sj4,353
10
10
  aprsd/client/drivers/__init__.py,sha256=IWSSrAEMrJQzulCIAx-N7Du0HK9sQOXIh98aJRCtfdg,421
11
- aprsd/client/drivers/aprsis.py,sha256=upUqCYDVVvOfbX43h5ncXq7CdlMrJsnO2ghbA88RKAk,6665
12
- aprsd/client/drivers/fake.py,sha256=b3q9-4O9Gtd7XnEzgZ2NzR5Zv6szxKBe07nsXJXmSHw,3249
13
- aprsd/client/drivers/registry.py,sha256=AWDWqxjOYHDz8ow4fNSSJ7JR6Tgy1axG6r0I9Yk2nJ8,2213
14
- aprsd/client/drivers/tcpkiss.py,sha256=PIiBaj0j4KDcXKRi3gUYDp10tC6qLmUlGfTbkQKPN1M,12926
11
+ aprsd/client/drivers/aprsis.py,sha256=DLtNgj1YAOiaHiALfHD6W-QF6JkzgQdmL7BmPXOa7KA,6763
12
+ aprsd/client/drivers/fake.py,sha256=iCe7zRJxL3giADSX6PMNMcasr9g4GpPNFSGhrKlgCeg,3277
13
+ aprsd/client/drivers/registry.py,sha256=vTKYTGAnVdBAyvCL51BadGFMNBsibzr9-xLPfXm_a9U,2227
14
+ aprsd/client/drivers/tcpkiss.py,sha256=begkOCkrd1g5JpuPGgY-EMiy3NcMxZaHaaWZqoCIQ5w,13817
15
15
  aprsd/client/drivers/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  aprsd/client/drivers/lib/aprslib.py,sha256=9KTEKlivGT1f1js-u-w1WNLhOs1kQxkr3COa1CI95oE,9439
17
17
  aprsd/cmds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  aprsd/cmds/completion.py,sha256=BMM8dSYUihYmj2fSsefo6LzgJZKbxHnNfRIhwVZW_U8,816
19
- aprsd/cmds/dev.py,sha256=iXjO0WBrEPE8wnuQU-7BpKOHsPLqyNZZ-67FvWiWOko,4097
20
- aprsd/cmds/fetch_stats.py,sha256=rE99ir67udprRLxOhWkZ1btgF5Vs8UX74S4ssjYi4lM,9621
19
+ aprsd/cmds/dev.py,sha256=9znqGv8qUd1hRV_-Iit8ktxFhYFIIB4rOnluhurrv28,4296
20
+ aprsd/cmds/fetch_stats.py,sha256=L8l-43GfZiWu9c9BgxpUV1PdrB8IG8z1FlsryAvPeAI,9684
21
21
  aprsd/cmds/healthcheck.py,sha256=swXb4LE1qdr3Z-417fv4LhWvscE4WUrqF9So1sWXiCU,2635
22
- aprsd/cmds/list_plugins.py,sha256=ongeK7gy_IxzLw_EfhclKMVHptyzIubV4PYxCxDMIqs,10415
23
- aprsd/cmds/listen.py,sha256=t_EOfy-qNRox6YwaeXHtynk_F5mShyDSjv2GZA9ca3I,9415
24
- aprsd/cmds/send_message.py,sha256=iJEteEWTPj3gQhSyBj8BvQr4WQzjrVbHWxaHgvIcpQM,4709
25
- aprsd/cmds/server.py,sha256=uZQawLzL5IKUPUHRleligJqlIokRwujcovhcivixY1g,4034
22
+ aprsd/cmds/list_plugins.py,sha256=PzvoyXfV26xAiTHtgSiJyWAmdPd2BEua5ta5fGuspVQ,7010
23
+ aprsd/cmds/listen.py,sha256=3M8eXIneEuM5wfskAFFYUZSfHTQveTAFSmnRZAfe9WY,9519
24
+ aprsd/cmds/send_message.py,sha256=EVJ-VayxcSNYAJ8dM8-LM9HLbsanC2mOWA6bfZwcLn4,4830
25
+ aprsd/cmds/server.py,sha256=hB3Tu0MbzEvg4XkoCFomvfMZqIitDKdIejQ6uwHDm0o,4138
26
26
  aprsd/conf/__init__.py,sha256=2ikrZahcS2EULQEJ5xErnzMVR2vcsDdMvixEPuNE-xE,1678
27
27
  aprsd/conf/client.py,sha256=9oheIUnotnbHmpKtxlw-00quW0o-3pZYtFmaim9bsjs,3013
28
- aprsd/conf/common.py,sha256=6bYna2bn0js6cq3c0h3KzbzgOWcpqeui5JEijx5rDj8,7386
28
+ aprsd/conf/common.py,sha256=l5tUw6G5CNOiFMZQPoStkpsNa3d9xr4BunzZK0FNgV8,7605
29
29
  aprsd/conf/log.py,sha256=qBoF8ptGHK7G6NPHNZCE2PBH7S-L587wFkT-9ikUtfE,1656
30
30
  aprsd/conf/opts.py,sha256=kbZELePpBDoQrKbowbWHgmLksbREARU8T8UhX9FIhq8,2725
31
31
  aprsd/conf/plugin_common.py,sha256=H7LxiJL0Sl1D_dpAmJHEHSbPxaOK938M1WCOcTpOYN8,1990
@@ -52,12 +52,12 @@ aprsd/plugins/version.py,sha256=NAPFKQ0lQqgjoDyTVy8tZcRGmdcJE_ZogRElDz27V2U,839
52
52
  aprsd/plugins/weather.py,sha256=XOspEoAumUb35TW2RYqjRV0lp6hrR_kOMIT41iEUGQQ,13399
53
53
  aprsd/stats/__init__.py,sha256=ltAtUiEvpokBEtOpq0sxpDGOLQT2evgeZSVBzDzjkSo,808
54
54
  aprsd/stats/app.py,sha256=axqMA137zKqU03yO8XI5f1QE8ajmr9YK0J9O9m4iSzo,1378
55
- aprsd/stats/collector.py,sha256=7nx_KSP_PUeuX5q8xJhsB9G_JfgQFCAvrPNzO7uK4FM,1502
55
+ aprsd/stats/collector.py,sha256=hIwJyw7P457NduqEzP0FPHEzf_HOuXepsBRmxiSCKKs,1743
56
56
  aprsd/threads/__init__.py,sha256=KPqAOhS4q995NCEWDnPCHiuu3guyuMZlyDUGx_h50z8,259
57
57
  aprsd/threads/aprsd.py,sha256=13AS7jAhcQBmTervBiADaC1Ins9C-6TrMDYjW1fQJkg,4794
58
58
  aprsd/threads/keepalive.py,sha256=E48_erNnqik5AAllMGx5gexAwPIaznCp0HCG45NltFw,3939
59
59
  aprsd/threads/registry.py,sha256=zWG4-SMBSx46NY93__Bt_F8vFrljxQvMHbi80WP5kY8,1690
60
- aprsd/threads/rx.py,sha256=u7SZ-YqINrnetusV7qFRWKVVoDMfj8-Z1bnSby3w3bI,14936
60
+ aprsd/threads/rx.py,sha256=u0W3YgU83OaZcKyw60C3B-Chb6LXYXpDv9RsXKSKHcg,14937
61
61
  aprsd/threads/service.py,sha256=fGjyr34i2-CJdBynxachNaBrBsULwmAnewLHdTujfmk,1375
62
62
  aprsd/threads/stats.py,sha256=drKqHV2WxgXhyWXGrmRHQx7oKlrJ9bwEMtCInCzulPY,884
63
63
  aprsd/threads/tx.py,sha256=spuWk4E5HvTWYNyhKB1kxqVbVvxNapzYvi9tNScTAHs,9264
@@ -67,12 +67,13 @@ aprsd/utils/fuzzyclock.py,sha256=qKV8SYZhQGOHG9biF8TeueLb6RMppspx1Zg4IOy1Z10,326
67
67
  aprsd/utils/json.py,sha256=eHoBfXGcchO4Q1MXj6uKK9YU-H8HKPJ2cThLZ1dM_vo,2578
68
68
  aprsd/utils/keepalive_collector.py,sha256=r0oKCpKPGsivOKTQkTfwcJRESqMfbNawCyqrcHoJo8M,1807
69
69
  aprsd/utils/objectstore.py,sha256=0OivUeagncWGH7eWjTwZhauf-etweTabp8Oykt0hoF4,3541
70
+ aprsd/utils/package.py,sha256=BtyIT6F5tPTFIATy5zDtYnB2k-rSFey5K2t9RPGenck,5025
70
71
  aprsd/utils/ring_buffer.py,sha256=lWWuw7lEbc2URhqAJfRLjpXBDLiK6UUWzk3j2VFnERQ,1111
71
- aprsd/utils/trace.py,sha256=lIuDZHOjvWbL2IMJ2tN_XW4Ch8oe4uQ7uLGylUByli0,5687
72
- aprsd-4.2.0.dist-info/licenses/AUTHORS,sha256=fGZhgXFMCfDPbp0hHllwmPyfiKPobLNA3sJA3BMIJVE,22
73
- aprsd-4.2.0.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
74
- aprsd-4.2.0.dist-info/METADATA,sha256=eky_cPS4yV2IcaRxoiHCOc95g4UzL8pmMbnfzI5cxpU,45583
75
- aprsd-4.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
76
- aprsd-4.2.0.dist-info/entry_points.txt,sha256=4fReoJUB-bFqOUK6eeXYYCvTdVLprL7KVH0hWQRP9eM,171
77
- aprsd-4.2.0.dist-info/top_level.txt,sha256=v1O96niUcJOTMh9aQnFRknbScJ6mMOwqurdbxeaeSjs,6
78
- aprsd-4.2.0.dist-info/RECORD,,
72
+ aprsd/utils/trace.py,sha256=3W4V14qg63x39l77H7iaCEacXd_otvz3oJKwbKqH_jM,5812
73
+ aprsd-4.2.3.dist-info/licenses/AUTHORS,sha256=fGZhgXFMCfDPbp0hHllwmPyfiKPobLNA3sJA3BMIJVE,22
74
+ aprsd-4.2.3.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
75
+ aprsd-4.2.3.dist-info/METADATA,sha256=PJmJ8nucsHVSdnJRPggmAhLIiKHqJWC0hLyEYiN6NHU,45583
76
+ aprsd-4.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
+ aprsd-4.2.3.dist-info/entry_points.txt,sha256=4fReoJUB-bFqOUK6eeXYYCvTdVLprL7KVH0hWQRP9eM,171
78
+ aprsd-4.2.3.dist-info/top_level.txt,sha256=v1O96niUcJOTMh9aQnFRknbScJ6mMOwqurdbxeaeSjs,6
79
+ aprsd-4.2.3.dist-info/RECORD,,
File without changes