aprsd 4.1.2__py3-none-any.whl → 4.2.1__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/__init__.py +5 -13
- aprsd/client/client.py +156 -0
- aprsd/client/drivers/__init__.py +10 -0
- aprsd/client/drivers/aprsis.py +174 -255
- aprsd/client/drivers/fake.py +59 -11
- aprsd/client/drivers/lib/__init__.py +0 -0
- aprsd/client/drivers/lib/aprslib.py +296 -0
- aprsd/client/drivers/registry.py +86 -0
- aprsd/client/drivers/tcpkiss.py +423 -0
- aprsd/client/stats.py +2 -2
- aprsd/cmds/dev.py +6 -4
- aprsd/cmds/fetch_stats.py +2 -0
- aprsd/cmds/list_plugins.py +6 -133
- aprsd/cmds/listen.py +5 -3
- aprsd/cmds/send_message.py +8 -5
- aprsd/cmds/server.py +7 -11
- aprsd/conf/common.py +7 -1
- aprsd/exception.py +7 -0
- aprsd/log/log.py +1 -1
- aprsd/main.py +0 -7
- aprsd/packets/core.py +168 -169
- aprsd/packets/log.py +69 -59
- aprsd/plugin.py +3 -2
- aprsd/plugin_utils.py +2 -2
- aprsd/plugins/weather.py +2 -2
- aprsd/stats/collector.py +5 -4
- aprsd/threads/rx.py +13 -11
- aprsd/threads/tx.py +32 -31
- aprsd/utils/keepalive_collector.py +7 -5
- aprsd/utils/package.py +176 -0
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/METADATA +48 -48
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/RECORD +37 -37
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/WHEEL +1 -1
- aprsd/client/aprsis.py +0 -183
- aprsd/client/base.py +0 -156
- aprsd/client/drivers/kiss.py +0 -144
- aprsd/client/factory.py +0 -91
- aprsd/client/fake.py +0 -49
- aprsd/client/kiss.py +0 -143
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/entry_points.txt +0 -0
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info/licenses}/AUTHORS +0 -0
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info/licenses}/LICENSE +0 -0
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,423 @@
|
|
1
|
+
"""
|
2
|
+
APRSD KISS Client Driver using native KISS implementation.
|
3
|
+
|
4
|
+
This module provides a KISS client driver for APRSD using the new
|
5
|
+
non-asyncio KISSInterface implementation.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import datetime
|
9
|
+
import logging
|
10
|
+
import select
|
11
|
+
import socket
|
12
|
+
import time
|
13
|
+
from typing import Any, Callable, Dict
|
14
|
+
|
15
|
+
import aprslib
|
16
|
+
from ax253 import frame as ax25frame
|
17
|
+
from kiss import constants as kiss_constants
|
18
|
+
from kiss import util as kissutil
|
19
|
+
from kiss.kiss import Command
|
20
|
+
from oslo_config import cfg
|
21
|
+
|
22
|
+
from aprsd import ( # noqa
|
23
|
+
client,
|
24
|
+
conf, # noqa
|
25
|
+
exception,
|
26
|
+
)
|
27
|
+
from aprsd.packets import core
|
28
|
+
|
29
|
+
CONF = cfg.CONF
|
30
|
+
LOG = logging.getLogger('APRSD')
|
31
|
+
|
32
|
+
|
33
|
+
def handle_fend(buffer: bytes, strip_df_start: bool = True) -> bytes:
|
34
|
+
"""
|
35
|
+
Handle FEND (end of frame) encountered in a KISS data stream.
|
36
|
+
|
37
|
+
:param buffer: the buffer containing the frame
|
38
|
+
:param strip_df_start: remove leading null byte (DATA_FRAME opcode)
|
39
|
+
:return: the bytes of the frame without escape characters or frame
|
40
|
+
end markers (FEND)
|
41
|
+
"""
|
42
|
+
frame = kissutil.recover_special_codes(kissutil.strip_nmea(bytes(buffer)))
|
43
|
+
if strip_df_start:
|
44
|
+
frame = kissutil.strip_df_start(frame)
|
45
|
+
LOG.warning(f'handle_fend {" ".join(f"{b:02X}" for b in bytes(frame))}')
|
46
|
+
return bytes(frame)
|
47
|
+
|
48
|
+
|
49
|
+
# class TCPKISSDriver(metaclass=trace.TraceWrapperMetaclass):
|
50
|
+
class TCPKISSDriver:
|
51
|
+
"""APRSD client driver for TCP KISS connections."""
|
52
|
+
|
53
|
+
# Class level attributes required by Client protocol
|
54
|
+
packets_received = 0
|
55
|
+
packets_sent = 0
|
56
|
+
last_packet_sent = None
|
57
|
+
last_packet_received = None
|
58
|
+
keepalive = None
|
59
|
+
client_name = None
|
60
|
+
socket = None
|
61
|
+
# timeout in seconds
|
62
|
+
select_timeout = 1
|
63
|
+
path = None
|
64
|
+
|
65
|
+
def __init__(self):
|
66
|
+
"""Initialize the KISS client.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
client_name: Name of the client instance
|
70
|
+
"""
|
71
|
+
super().__init__()
|
72
|
+
self._connected = False
|
73
|
+
self.keepalive = datetime.datetime.now()
|
74
|
+
self._running = False
|
75
|
+
# This is initialized in setup_connection()
|
76
|
+
self.socket = None
|
77
|
+
|
78
|
+
@property
|
79
|
+
def transport(self) -> str:
|
80
|
+
return client.TRANSPORT_TCPKISS
|
81
|
+
|
82
|
+
@staticmethod
|
83
|
+
def is_enabled() -> bool:
|
84
|
+
"""Check if KISS is enabled in configuration.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
bool: True if either TCP is enabled
|
88
|
+
"""
|
89
|
+
return CONF.kiss_tcp.enabled
|
90
|
+
|
91
|
+
@staticmethod
|
92
|
+
def is_configured():
|
93
|
+
# Ensure that the config vars are correctly set
|
94
|
+
if TCPKISSDriver.is_enabled():
|
95
|
+
if not CONF.kiss_tcp.host:
|
96
|
+
LOG.error('KISS TCP enabled, but no host is set.')
|
97
|
+
raise exception.MissingConfigOptionException(
|
98
|
+
'kiss_tcp.host is not set.',
|
99
|
+
)
|
100
|
+
return True
|
101
|
+
return False
|
102
|
+
|
103
|
+
@property
|
104
|
+
def is_alive(self) -> bool:
|
105
|
+
"""Check if the client is connected.
|
106
|
+
|
107
|
+
Returns:
|
108
|
+
bool: True if connected to KISS TNC, False otherwise
|
109
|
+
"""
|
110
|
+
return self._connected
|
111
|
+
|
112
|
+
def close(self):
|
113
|
+
"""Close the connection."""
|
114
|
+
self.stop()
|
115
|
+
|
116
|
+
def send(self, packet: core.Packet):
|
117
|
+
"""Send an APRS packet.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
packet: APRS packet to send (Packet or Message object)
|
121
|
+
|
122
|
+
Raises:
|
123
|
+
Exception: If not connected or send fails
|
124
|
+
"""
|
125
|
+
if not self.socket:
|
126
|
+
raise Exception('KISS interface not initialized')
|
127
|
+
|
128
|
+
payload = None
|
129
|
+
path = self.path
|
130
|
+
packet.prepare()
|
131
|
+
payload = packet.payload.encode('US-ASCII')
|
132
|
+
if packet.path:
|
133
|
+
path = packet.path
|
134
|
+
|
135
|
+
LOG.debug(
|
136
|
+
f"KISS Send '{payload}' TO '{packet.to_call}' From "
|
137
|
+
f"'{packet.from_call}' with PATH '{path}'",
|
138
|
+
)
|
139
|
+
frame = ax25frame.Frame.ui(
|
140
|
+
destination='APZ100',
|
141
|
+
# destination=packet.to_call,
|
142
|
+
source=packet.from_call,
|
143
|
+
path=path,
|
144
|
+
info=payload,
|
145
|
+
)
|
146
|
+
|
147
|
+
# now escape the frame special characters
|
148
|
+
frame_escaped = kissutil.escape_special_codes(bytes(frame))
|
149
|
+
# and finally wrap the frame in KISS protocol
|
150
|
+
command = Command.DATA_FRAME
|
151
|
+
frame_kiss = b''.join(
|
152
|
+
[kiss_constants.FEND, command.value, frame_escaped, kiss_constants.FEND]
|
153
|
+
)
|
154
|
+
self.socket.send(frame_kiss)
|
155
|
+
# Update last packet sent time
|
156
|
+
self.last_packet_sent = datetime.datetime.now()
|
157
|
+
# Increment packets sent counter
|
158
|
+
self.packets_sent += 1
|
159
|
+
|
160
|
+
def setup_connection(self):
|
161
|
+
"""Set up the KISS interface."""
|
162
|
+
if not self.is_enabled():
|
163
|
+
LOG.error('KISS is not enabled in configuration')
|
164
|
+
return
|
165
|
+
|
166
|
+
try:
|
167
|
+
# Configure for TCP KISS
|
168
|
+
if self.is_enabled():
|
169
|
+
LOG.info(
|
170
|
+
f'KISS TCP Connection to {CONF.kiss_tcp.host}:{CONF.kiss_tcp.port}'
|
171
|
+
)
|
172
|
+
self.path = CONF.kiss_tcp.path
|
173
|
+
self.connect()
|
174
|
+
if self._connected:
|
175
|
+
LOG.info('KISS interface initialized')
|
176
|
+
else:
|
177
|
+
LOG.error('Failed to connect to KISS interface')
|
178
|
+
|
179
|
+
except Exception as ex:
|
180
|
+
LOG.error('Failed to initialize KISS interface')
|
181
|
+
LOG.exception(ex)
|
182
|
+
self._connected = False
|
183
|
+
|
184
|
+
def set_filter(self, filter_text: str):
|
185
|
+
"""Set packet filter (not implemented for KISS).
|
186
|
+
|
187
|
+
Args:
|
188
|
+
filter_text: Filter specification (ignored for KISS)
|
189
|
+
"""
|
190
|
+
# KISS doesn't support filtering at the TNC level
|
191
|
+
pass
|
192
|
+
|
193
|
+
@property
|
194
|
+
def filter(self) -> str:
|
195
|
+
"""Get packet filter (not implemented for KISS).
|
196
|
+
Returns:
|
197
|
+
str: Empty string (not implemented for KISS)
|
198
|
+
"""
|
199
|
+
return ''
|
200
|
+
|
201
|
+
def login_success(self) -> bool:
|
202
|
+
"""There is no login for KISS."""
|
203
|
+
if not self._connected:
|
204
|
+
return False
|
205
|
+
return True
|
206
|
+
|
207
|
+
def login_failure(self) -> str:
|
208
|
+
"""There is no login for KISS."""
|
209
|
+
return 'Login successful'
|
210
|
+
|
211
|
+
def consumer(self, callback: Callable, raw: bool = False):
|
212
|
+
"""Start consuming frames with the given callback.
|
213
|
+
|
214
|
+
Args:
|
215
|
+
callback: Function to call with received packets
|
216
|
+
|
217
|
+
Raises:
|
218
|
+
Exception: If not connected to KISS TNC
|
219
|
+
"""
|
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
|
227
|
+
|
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)
|
236
|
+
|
237
|
+
def decode_packet(self, *args, **kwargs) -> core.Packet:
|
238
|
+
"""Decode a packet from an AX.25 frame.
|
239
|
+
|
240
|
+
Args:
|
241
|
+
frame: Received AX.25 frame
|
242
|
+
"""
|
243
|
+
frame = kwargs.get('frame')
|
244
|
+
if not frame:
|
245
|
+
LOG.warning('No frame received to decode?!?!')
|
246
|
+
return None
|
247
|
+
|
248
|
+
LOG.warning(f'FRAME: {str(frame)}')
|
249
|
+
try:
|
250
|
+
aprslib_frame = aprslib.parse(str(frame))
|
251
|
+
packet = core.factory(aprslib_frame)
|
252
|
+
if isinstance(packet, core.ThirdPartyPacket):
|
253
|
+
return packet.subpacket
|
254
|
+
else:
|
255
|
+
return packet
|
256
|
+
except Exception as e:
|
257
|
+
LOG.error(f'Error decoding packet: {e}')
|
258
|
+
return None
|
259
|
+
|
260
|
+
def stop(self):
|
261
|
+
"""Stop the KISS interface."""
|
262
|
+
self._running = False
|
263
|
+
self._connected = False
|
264
|
+
if self.socket:
|
265
|
+
try:
|
266
|
+
self.socket.close()
|
267
|
+
except Exception:
|
268
|
+
pass
|
269
|
+
|
270
|
+
def stats(self, serializable: bool = False) -> Dict[str, Any]:
|
271
|
+
"""Get client statistics.
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
Dict containing client statistics
|
275
|
+
"""
|
276
|
+
if serializable:
|
277
|
+
keepalive = self.keepalive.isoformat()
|
278
|
+
if self.last_packet_sent:
|
279
|
+
last_packet_sent = self.last_packet_sent.isoformat()
|
280
|
+
else:
|
281
|
+
last_packet_sent = 'None'
|
282
|
+
if self.last_packet_received:
|
283
|
+
last_packet_received = self.last_packet_received.isoformat()
|
284
|
+
else:
|
285
|
+
last_packet_received = 'None'
|
286
|
+
else:
|
287
|
+
keepalive = self.keepalive
|
288
|
+
last_packet_sent = self.last_packet_sent
|
289
|
+
last_packet_received = self.last_packet_received
|
290
|
+
|
291
|
+
stats = {
|
292
|
+
'client': self.__class__.__name__,
|
293
|
+
'transport': self.transport,
|
294
|
+
'connected': self._connected,
|
295
|
+
'path': self.path,
|
296
|
+
'packets_sent': self.packets_sent,
|
297
|
+
'packets_received': self.packets_received,
|
298
|
+
'last_packet_sent': last_packet_sent,
|
299
|
+
'last_packet_received': last_packet_received,
|
300
|
+
'connection_keepalive': keepalive,
|
301
|
+
'host': CONF.kiss_tcp.host,
|
302
|
+
'port': CONF.kiss_tcp.port,
|
303
|
+
}
|
304
|
+
|
305
|
+
return stats
|
306
|
+
|
307
|
+
def connect(self) -> bool:
|
308
|
+
"""Establish TCP connection to the KISS host.
|
309
|
+
|
310
|
+
Returns:
|
311
|
+
bool: True if connection successful, False otherwise
|
312
|
+
"""
|
313
|
+
try:
|
314
|
+
if self.socket:
|
315
|
+
try:
|
316
|
+
self.socket.close()
|
317
|
+
except Exception:
|
318
|
+
pass
|
319
|
+
|
320
|
+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
321
|
+
self.socket.settimeout(5.0) # 5 second timeout for connection
|
322
|
+
self.socket.connect((CONF.kiss_tcp.host, CONF.kiss_tcp.port))
|
323
|
+
self.socket.settimeout(0.1) # Reset to shorter timeout for reads
|
324
|
+
self._connected = True
|
325
|
+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
326
|
+
# MACOS doesn't have TCP_KEEPIDLE
|
327
|
+
if hasattr(socket, 'TCP_KEEPIDLE'):
|
328
|
+
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
|
329
|
+
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
|
330
|
+
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
|
331
|
+
return True
|
332
|
+
|
333
|
+
except ConnectionError as e:
|
334
|
+
LOG.error(
|
335
|
+
f'Failed to connect to {CONF.kiss_tcp.host}:{CONF.kiss_tcp.port} - {str(e)}'
|
336
|
+
)
|
337
|
+
self._connected = False
|
338
|
+
return False
|
339
|
+
|
340
|
+
except Exception as e:
|
341
|
+
LOG.error(
|
342
|
+
f'Failed to connect to {CONF.kiss_tcp.host}:{CONF.kiss_tcp.port} - {str(e)}'
|
343
|
+
)
|
344
|
+
self._connected = False
|
345
|
+
return False
|
346
|
+
|
347
|
+
def fix_raw_frame(self, raw_frame: bytes) -> bytes:
|
348
|
+
"""Fix the raw frame by recalculating the FCS."""
|
349
|
+
ax25_data = raw_frame[2:-1] # Remove KISS markers
|
350
|
+
return handle_fend(ax25_data)
|
351
|
+
|
352
|
+
def read_frame(self, blocking=False):
|
353
|
+
"""
|
354
|
+
Generator for complete lines, received from the server
|
355
|
+
"""
|
356
|
+
try:
|
357
|
+
self.socket.setblocking(0)
|
358
|
+
except OSError as e:
|
359
|
+
LOG.error(f'socket error when setblocking(0): {str(e)}')
|
360
|
+
raise aprslib.ConnectionDrop('connection dropped') from e
|
361
|
+
|
362
|
+
while self._running:
|
363
|
+
short_buf = b''
|
364
|
+
|
365
|
+
try:
|
366
|
+
readable, _, _ = select.select(
|
367
|
+
[self.socket],
|
368
|
+
[],
|
369
|
+
[],
|
370
|
+
self.select_timeout,
|
371
|
+
)
|
372
|
+
if not readable:
|
373
|
+
if not blocking:
|
374
|
+
break
|
375
|
+
else:
|
376
|
+
continue
|
377
|
+
except Exception as e:
|
378
|
+
LOG.error(f'Error in read loop: {e}')
|
379
|
+
self._connected = False
|
380
|
+
break
|
381
|
+
|
382
|
+
try:
|
383
|
+
print('reading from socket')
|
384
|
+
short_buf = self.socket.recv(1024)
|
385
|
+
print(f'short_buf: {short_buf}')
|
386
|
+
# sock.recv returns empty if the connection drops
|
387
|
+
if not short_buf:
|
388
|
+
if not blocking:
|
389
|
+
# We could just not be blocking, so empty is expected
|
390
|
+
continue
|
391
|
+
else:
|
392
|
+
self.logger.error('socket.recv(): returned empty')
|
393
|
+
raise aprslib.ConnectionDrop('connection dropped')
|
394
|
+
|
395
|
+
raw_frame = self.fix_raw_frame(short_buf)
|
396
|
+
return ax25frame.Frame.from_bytes(raw_frame)
|
397
|
+
except OSError as e:
|
398
|
+
# self.logger.error("socket error on recv(): %s" % str(e))
|
399
|
+
if 'Resource temporarily unavailable' in str(e):
|
400
|
+
if not blocking:
|
401
|
+
if len(short_buf) == 0:
|
402
|
+
break
|
403
|
+
except socket.timeout:
|
404
|
+
continue
|
405
|
+
except (KeyboardInterrupt, SystemExit):
|
406
|
+
raise
|
407
|
+
except ConnectionError:
|
408
|
+
self.close()
|
409
|
+
if not self.auto_reconnect:
|
410
|
+
raise
|
411
|
+
else:
|
412
|
+
self.connect()
|
413
|
+
continue
|
414
|
+
except StopIteration:
|
415
|
+
break
|
416
|
+
except IOError:
|
417
|
+
LOG.error('IOError')
|
418
|
+
break
|
419
|
+
except Exception as e:
|
420
|
+
LOG.error(f'Error in read loop: {e}')
|
421
|
+
self._connected = False
|
422
|
+
if not self.auto_reconnect:
|
423
|
+
break
|
aprsd/client/stats.py
CHANGED
@@ -3,7 +3,7 @@ import threading
|
|
3
3
|
import wrapt
|
4
4
|
from oslo_config import cfg
|
5
5
|
|
6
|
-
from aprsd import
|
6
|
+
from aprsd.client.client import APRSDClient
|
7
7
|
from aprsd.utils import singleton
|
8
8
|
|
9
9
|
CONF = cfg.CONF
|
@@ -15,4 +15,4 @@ class APRSClientStats:
|
|
15
15
|
|
16
16
|
@wrapt.synchronized(lock)
|
17
17
|
def stats(self, serializable=False):
|
18
|
-
return
|
18
|
+
return APRSDClient().stats(serializable=serializable)
|
aprsd/cmds/dev.py
CHANGED
@@ -4,14 +4,15 @@
|
|
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
|
-
|
12
|
+
import aprsd
|
13
|
+
from aprsd import cli_helper, conf, packets, plugin, utils
|
12
14
|
|
13
15
|
# local imports here
|
14
|
-
from aprsd.client import base
|
15
16
|
from aprsd.main import cli
|
16
17
|
from aprsd.utils import trace
|
17
18
|
|
@@ -72,6 +73,9 @@ def test_plugin(
|
|
72
73
|
):
|
73
74
|
"""Test an individual APRSD plugin given a python path."""
|
74
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()
|
75
79
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
76
80
|
|
77
81
|
if not aprs_login:
|
@@ -97,8 +101,6 @@ def test_plugin(
|
|
97
101
|
if CONF.trace_enabled:
|
98
102
|
trace.setup_tracing(['method', 'api'])
|
99
103
|
|
100
|
-
base.APRSClient()
|
101
|
-
|
102
104
|
pm = plugin.PluginManager()
|
103
105
|
if load_all:
|
104
106
|
pm.setup_plugins(load_help_plugin=CONF.load_help_plugin)
|
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)
|
aprsd/cmds/list_plugins.py
CHANGED
@@ -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 =
|
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 =
|
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)
|