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

Compare commits

...

32 Commits

Author SHA1 Message Date
hemna b7791eb4fa Changelog 2021-12-07 15:05:34 -05:00
hemna 440c8d54ad Tightened up the packet logging 2021-12-07 15:00:38 -05:00
hemna bcc1b4e309 Merge pull request #75 from craigerl/unittests
Unittests
2021-12-07 13:37:02 -05:00
hemna 8ea00e9888 Added unit tests for USWeatherPlugin, USMetarPlugin 2021-12-07 13:31:58 -05:00
hemna 5d6ac5cf31 Added test_location to test LocationPlugin 2021-12-07 12:38:12 -05:00
hemna e0e75149a9 Updated pytest output
This patch changes tox.ini to update the output for the unit test
runs.
2021-12-07 11:57:01 -05:00
hemna a5184fb98c Added py39 to tox for tests 2021-12-07 11:35:18 -05:00
hemna 0ad791bdd9 Added NotifyPlugin unit tests and more
This patch restructures the unit tests for plugins.
This also adds unit tests for the NotifyPlugin
2021-12-07 11:25:14 -05:00
hemna 96cc07d15f Small cleanup on packet logging
This patch reduces some of the leading whitespace
to the message/packet logging to the log file.
2021-12-06 14:35:49 -05:00
hemna d3dd08714b Reduced the APRSIS connection reset to 2 minutes
The time in which the KeepAlive Thread would reset the APRS-IS
socket connection used to be 5 minutes.   This patch changes
that to 2 minutes.
2021-12-06 14:34:22 -05:00
hemna 055835cb3c Fixed the NotifyPlugin
The watchlist notify plugin is supposed to send an APRS message
to the configured callsign.  This patch makes sure that the
message is sent to the notify_callsign
2021-12-06 14:11:34 -05:00
hemna ff8bf02e26 Merge pull request #74 from craigerl/rich_logging
Rich logging
2021-12-03 09:32:25 -05:00
hemna b5b286e75c Fixed some pep8 errors 2021-12-03 09:10:33 -05:00
hemna 1233137caf Add tracing for dev command
This patch enables tracing output in the log for the dev
test-plugin command
2021-12-03 08:53:08 -05:00
hemna 1d5f76defc Added python rich library based logging.
The python rich library is extensive and has a really nice
log format that is easier to read and has built in formatting
and coloring of the log output.

To enable rich logging add rich_logging: True in the config file.
2021-12-03 08:05:03 -05:00
hemna 950c62f49b Merge pull request #73 from emresaglam/loglevel
Added LOG_LEVEL env variable for the docker
2021-12-03 08:02:15 -05:00
Emre Saglam 7aaa002a0e Added LOG_LEVEL env variable for the docker 2021-12-02 17:08:41 -08:00
hemna e27887db1a Update requirements to use aprslib 0.7.0
aprslib 0.7.0 has a few aprs packet parsing fixes.

https://github.com/rossengeorgiev/aprs-python/pull/66

Support for the 'more recent' reply/ack msg format from 1999
2021-11-28 10:47:12 -05:00
hemna 5e50792e80 fixed the failure during loading for objectstore
This patch fixes a silent failure of loading data from the objectstore
2021-11-13 15:07:28 -05:00
hemna deec249c45 updated docker build 2021-11-13 10:01:38 -05:00
hemna ade3c49e93 Updated Changelog 2021-11-13 10:00:40 -05:00
hemna 6fb610582d Fixed dev command missing initialization
This patch fixes a few issues when running test-plugin command.
It was missing some initialization of the stats and packets classes.
2021-11-13 09:56:19 -05:00
hemna bda2ef00dd Fix admin logging tab 2021-11-12 12:17:45 -05:00
hemna 446484e631 Added new list-plugins command
This patch adds the new list-plugins command that shows the
list of built in plugins for APRSD.
2021-11-12 11:36:22 -05:00
hemna a8a6b1aa07 Don't require check-version command to have a config
This patch removes the need for check-version to have a
config file.
2021-11-12 10:23:27 -05:00
hemna 8842fb1b44 Healthcheck command doesn't need the aprsd.yml config
This patch updates the healthcheck command to not require
the aprsd.yml config file to exist.   The healthcheck
calls a running aprsd, collects the stats to determine if it's
healthy.
2021-11-10 11:52:51 -05:00
hemna 152132b0ed Fix test failures 2021-11-10 11:51:21 -05:00
hemna 7787dc1be4 Removed requirement for aprs.fi key
This removed the requirement of running APRSD for specifying
the aprs.fi key in the config file.  The plugins that need the
key have been updated to set enabled = False when the key is missing.
2021-11-10 11:01:10 -05:00
hemna 10e34d8634 Updated Changelog 2021-11-09 15:06:40 -05:00
hemna 9469410929 Removed stock plugin. 2021-11-09 15:02:54 -05:00
hemna 998bc32c27 Merge pull request #72 from craigerl/remove_stock_plugin
Removed the stock plugin
2021-11-09 14:59:03 -05:00
hemna 88db485eb4 Removed the stock plugin
This patch removed the built in stock plugin from APRSD.
This helps clean up the requirement tree from the yfinance
python module that pulled in a lot of other requirements.

The stock plugin is it's own separate repo and module now.

https://github.com/hemna/aprsd-stock-plugin

https://pypi.org/project/aprsd-stock-plugin/
2021-11-09 14:53:20 -05:00
43 changed files with 1164 additions and 375 deletions
+52
View File
@@ -1,9 +1,61 @@
CHANGES
=======
v2.5.6
------
* Tightened up the packet logging
* Added unit tests for USWeatherPlugin, USMetarPlugin
* Added test\_location to test LocationPlugin
* Updated pytest output
* Added py39 to tox for tests
* Added NotifyPlugin unit tests and more
* Small cleanup on packet logging
* Reduced the APRSIS connection reset to 2 minutes
* Fixed the NotifyPlugin
* Fixed some pep8 errors
* Add tracing for dev command
* Added python rich library based logging
* Added LOG\_LEVEL env variable for the docker
v2.5.5
------
* Update requirements to use aprslib 0.7.0
* fixed the failure during loading for objectstore
* updated docker build
v2.5.4
------
* Updated Changelog
* Fixed dev command missing initialization
v2.5.3
------
* Fix admin logging tab
v2.5.2
------
* Added new list-plugins command
* Don't require check-version command to have a config
* Healthcheck command doesn't need the aprsd.yml config
* Fix test failures
* Removed requirement for aprs.fi key
* Updated Changelog
v2.5.1
------
* Removed stock plugin
* Removed the stock plugin
v2.5.0
------
* Updated for v2.5.0
* Updated Dockerfile's and build script for docker
* Cleaned up some verbose output & colorized output
* Reworked all the common arguments
+3 -2
View File
@@ -67,7 +67,8 @@ def main():
# First import all the possible commands for the CLI
# The commands themselves live in the cmds directory
from .cmds import ( # noqa
completion, dev, healthcheck, listen, send_message, server,
completion, dev, healthcheck, list_plugins, listen, send_message,
server,
)
cli()
@@ -97,7 +98,7 @@ def signal_handler(sig, frame):
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.pass_context
@cli_helper.process_standard_options
@cli_helper.process_standard_options_no_config
def check_version(ctx):
"""Check this version against the latest in pypi.org."""
level, msg = utils._check_version()
+21
View File
@@ -65,3 +65,24 @@ def process_standard_options(f: F) -> F:
return f(*args, **kwargs)
return update_wrapper(t.cast(F, new_func), f)
def process_standard_options_no_config(f: F) -> F:
"""Use this as a decorator when config isn't needed."""
def new_func(*args, **kwargs):
ctx = args[0]
ctx.ensure_object(dict)
ctx.obj["loglevel"] = kwargs["loglevel"]
ctx.obj["config_file"] = kwargs["config_file"]
ctx.obj["quiet"] = kwargs["quiet"]
log.setup_logging_no_config(
ctx.obj["loglevel"],
ctx.obj["quiet"],
)
del kwargs["loglevel"]
del kwargs["config_file"]
del kwargs["quiet"]
return f(*args, **kwargs)
return update_wrapper(t.cast(F, new_func), f)
+9 -1
View File
@@ -8,7 +8,7 @@ import logging
import click
# local imports here
from aprsd import cli_helper, client, plugin
from aprsd import cli_helper, client, messaging, packets, plugin, stats, trace
from ..aprsd import cli
@@ -79,7 +79,15 @@ def test_plugin(
if type(message) is tuple:
message = " ".join(message)
if config["aprsd"].get("trace", False):
trace.setup_tracing(["method", "api"])
client.Client(config)
stats.APRSDStats(config)
messaging.MsgTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
pm = plugin.PluginManager(config)
if load_all:
+1 -2
View File
@@ -40,10 +40,9 @@ LOG = logging.getLogger("APRSD")
help="How long to wait for healtcheck url to come back",
)
@click.pass_context
@cli_helper.process_standard_options
@cli_helper.process_standard_options_no_config
def healthcheck(ctx, health_url, timeout):
"""Check the health of the running aprsd server."""
ctx.obj["config"]
LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}")
try:
+59
View File
@@ -0,0 +1,59 @@
import inspect
import logging
from textwrap import indent
import click
from tabulate import tabulate
from aprsd import cli_helper, plugin
from aprsd.plugins import (
email, fortune, location, notify, ping, query, time, version, weather,
)
from ..aprsd import cli
LOG = logging.getLogger("APRSD")
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.pass_context
@cli_helper.process_standard_options_no_config
def list_plugins(ctx):
"""List the built in plugins available to APRSD."""
modules = [email, fortune, location, notify, ping, query, time, version, weather]
plugins = []
for module in modules:
entries = inspect.getmembers(module, inspect.isclass)
for entry in entries:
cls = entry[1]
if issubclass(cls, plugin.APRSDPluginBase):
info = {
"name": cls.__qualname__,
"path": f"{cls.__module__}.{cls.__qualname__}",
"version": cls.version,
"docstring": cls.__doc__,
"short_desc": cls.short_description,
}
if issubclass(cls, plugin.APRSDRegexCommandPluginBase):
info["command_regex"] = cls.command_regex
info["type"] = "RegexCommand"
if issubclass(cls, plugin.APRSDWatchListPluginBase):
info["type"] = "WatchList"
plugins.append(info)
lines = []
headers = ("Plugin Name", "Plugin Path", "Type", "Info")
for entry in plugins:
lines.append(
(entry["name"], entry["path"], entry["type"], entry["short_desc"]),
)
click.echo(indent(tabulate(lines, headers, disable_numparse=True), " "))
+1 -6
View File
@@ -79,6 +79,7 @@ DEFAULT_CONFIG_DICT = {
"logformat": DEFAULT_LOG_FORMAT,
"dateformat": DEFAULT_DATE_FORMAT,
"save_location": DEFAULT_CONFIG_DIR,
"rich_logging": False,
"trace": False,
"enabled_plugins": CORE_MESSAGE_PLUGINS,
"units": "imperial",
@@ -327,12 +328,6 @@ def parse_config(config_file):
"ham.callsign",
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
)
check_option(
config,
["services", "aprs.fi", "apiKey"],
default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"],
)
check_option(
config,
"aprs.login",
+2 -2
View File
@@ -19,7 +19,7 @@ from werkzeug.security import check_password_hash, generate_password_hash
import aprsd
from aprsd import client
from aprsd import config as aprsd_config
from aprsd import messaging, packets, plugin, stats, threads, utils
from aprsd import log, messaging, packets, plugin, stats, threads, utils
from aprsd.clients import aprsis
@@ -500,7 +500,7 @@ class LogMonitorThread(threads.APRSDThread):
def loop(self):
global socketio
try:
record = threads.logging_queue.get(block=True, timeout=5)
record = log.logging_queue.get(block=True, timeout=5)
json_record = self.json_record(record)
socketio.emit(
"log_entry", json_record,
+34 -2
View File
@@ -5,6 +5,7 @@ import queue
import sys
from aprsd import config as aprsd_config
from aprsd.logging import logging as aprsd_logging
LOG = logging.getLogger("APRSD")
@@ -17,10 +18,24 @@ logging_queue = queue.Queue()
def setup_logging(config, loglevel, quiet):
log_level = aprsd_config.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
if config["aprsd"].get("rich_logging", False):
log_format = "%(message)s"
else:
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
log_file = config["aprsd"].get("logfile", None)
rich_logging = False
if config["aprsd"].get("rich_logging", False):
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=15,
rich_tracebacks=True, omit_repeated_times=False,
)
rh.setFormatter(log_formatter)
LOG.addHandler(rh)
rich_logging = True
if log_file:
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
else:
@@ -45,9 +60,26 @@ def setup_logging(config, loglevel, quiet):
qh.setFormatter(q_log_formatter)
LOG.addHandler(qh)
if not quiet:
if not quiet and not rich_logging:
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(log_formatter)
LOG.addHandler(sh)
if imap_logger:
imap_logger.addHandler(sh)
def setup_logging_no_config(loglevel, quiet):
log_level = aprsd_config.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
log_format = aprsd_config.DEFAULT_LOG_FORMAT
date_format = aprsd_config.DEFAULT_DATE_FORMAT
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = NullHandler()
fh.setFormatter(log_formatter)
LOG.addHandler(fh)
if not quiet:
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(log_formatter)
LOG.addHandler(sh)
View File
+160
View File
@@ -0,0 +1,160 @@
from datetime import datetime
from logging import LogRecord
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Union
from rich._log_render import LogRender
from rich.logging import RichHandler
from rich.text import Text, TextType
from rich.traceback import Traceback
if TYPE_CHECKING:
from rich.console import Console, ConsoleRenderable, RenderableType
from rich.table import Table
from aprsd import utils
FormatTimeCallable = Callable[[datetime], Text]
class APRSDRichLogRender(LogRender):
def __init__(
self, *args,
show_thread: bool = False,
thread_width: Optional[int] = 10,
**kwargs,
):
super().__init__(*args, **kwargs)
self.show_thread = show_thread
self.thread_width = thread_width
def __call__(
self,
console: "Console",
renderables: Iterable["ConsoleRenderable"],
log_time: Optional[datetime] = None,
time_format: Optional[Union[str, FormatTimeCallable]] = None,
level: TextType = "",
path: Optional[str] = None,
line_no: Optional[int] = None,
link_path: Optional[str] = None,
thread_name: Optional[str] = None,
) -> "Table":
from rich.containers import Renderables
from rich.table import Table
output = Table.grid(padding=(0, 1))
output.expand = True
if self.show_time:
output.add_column(style="log.time")
if self.show_thread:
rgb = str(utils.rgb_from_name(thread_name)).replace(" ", "")
output.add_column(style=f"rgb{rgb}", width=self.thread_width)
if self.show_level:
output.add_column(style="log.level", width=self.level_width)
output.add_column(ratio=1, style="log.message", overflow="fold")
if self.show_path and path:
output.add_column(style="log.path")
row: List["RenderableType"] = []
if self.show_time:
log_time = log_time or console.get_datetime()
time_format = time_format or self.time_format
if callable(time_format):
log_time_display = time_format(log_time)
else:
log_time_display = Text(log_time.strftime(time_format))
if log_time_display == self._last_time and self.omit_repeated_times:
row.append(Text(" " * len(log_time_display)))
else:
row.append(log_time_display)
self._last_time = log_time_display
if self.show_thread:
row.append(thread_name)
if self.show_level:
row.append(level)
row.append(Renderables(renderables))
if self.show_path and path:
path_text = Text()
path_text.append(
path, style=f"link file://{link_path}" if link_path else "",
)
if line_no:
path_text.append(":")
path_text.append(
f"{line_no}",
style=f"link file://{link_path}#{line_no}" if link_path else "",
)
row.append(path_text)
output.add_row(*row)
return output
class APRSDRichHandler(RichHandler):
"""APRSD's extension of rich's RichHandler to show threads.
show_thread (bool, optional): Show the name of the thread in log entry. Defaults to False.
thread_width (int, optional): The number of characters to show for thread name. Defaults to 10.
"""
def __init__(
self, *args,
show_thread: bool = True,
thread_width: Optional[int] = 10,
**kwargs,
):
super().__init__(*args, **kwargs)
self.show_thread = show_thread
self.thread_width = thread_width
kwargs["show_thread"] = show_thread
kwargs["thread_width"] = thread_width
self._log_render = APRSDRichLogRender(
show_time=True,
show_level=True,
show_path=True,
omit_repeated_times=False,
level_width=None,
show_thread=show_thread,
thread_width=thread_width,
)
def render(
self, *, record: LogRecord,
traceback: Optional[Traceback],
message_renderable: "ConsoleRenderable",
) -> "ConsoleRenderable":
"""Render log for display.
Args:
record (LogRecord): logging Record.
traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
Returns:
ConsoleRenderable: Renderable to display log.
"""
path = Path(record.pathname).name
level = self.get_level_text(record)
time_format = None if self.formatter is None else self.formatter.datefmt
log_time = datetime.fromtimestamp(record.created)
thread_name = record.threadName
log_renderable = self._log_render(
self.console,
[message_renderable] if not traceback else [
message_renderable,
traceback,
],
log_time=log_time,
time_format=time_format,
level=level,
path=path,
line_no=record.lineno,
link_path=record.pathname if self.enable_link_path else None,
thread_name=thread_name,
)
return log_renderable
+11 -21
View File
@@ -542,37 +542,27 @@ def log_message(
log_list = [""]
if retry_number:
# LOG.info(" {} _______________(TX:{})".format(header, retry_number))
log_list.append(f" {header} _______________(TX:{retry_number})")
log_list.append(f"{header} _______________(TX:{retry_number})")
else:
# LOG.info(" {} _______________".format(header))
log_list.append(f" {header} _______________")
log_list.append(f"{header} _______________")
# LOG.info(" Raw : {}".format(raw))
log_list.append(f" Raw : {raw}")
log_list.append(f" Raw : {raw}")
if packet_type:
# LOG.info(" Packet : {}".format(packet_type))
log_list.append(f" Packet : {packet_type}")
log_list.append(f" Packet : {packet_type}")
if tocall:
# LOG.info(" To : {}".format(tocall))
log_list.append(f" To : {tocall}")
log_list.append(f" To : {tocall}")
if fromcall:
# LOG.info(" From : {}".format(fromcall))
log_list.append(f" From : {fromcall}")
log_list.append(f" From : {fromcall}")
if ack:
# LOG.info(" Ack : {}".format(ack))
log_list.append(f" Ack : {ack}")
log_list.append(f" Ack : {ack}")
else:
# LOG.info(" Message : {}".format(message))
log_list.append(f" Message : {message}")
log_list.append(f" Message : {message}")
if msg_num:
# LOG.info(" Msg number : {}".format(msg_num))
log_list.append(f" Msg number : {msg_num}")
log_list.append(f" Msg # : {msg_num}")
if uuid:
log_list.append(f" UUID : {uuid}")
# LOG.info(" {} _______________ Complete".format(header))
log_list.append(f" {header} _______________ Complete")
log_list.append(f" UUID : {uuid}")
log_list.append(f"{header} _______________ Complete")
LOG.info("\n".join(log_list))
+1 -1
View File
@@ -92,7 +92,7 @@ class ObjectStoreMixin:
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
)
LOG.debug(f"{self.data}")
except pickle.UnpicklingError as ex:
except (pickle.UnpicklingError, Exception) as ex:
LOG.error(f"Failed to UnPickle {self._save_filename()}")
LOG.error(ex)
self.data = {}
+16 -3
View File
@@ -12,6 +12,7 @@ import threading
import pluggy
from thesmuggler import smuggle
import aprsd
from aprsd import client, messaging, packets, threads
@@ -24,7 +25,6 @@ CORE_MESSAGE_PLUGINS = [
"aprsd.plugins.location.LocationPlugin",
"aprsd.plugins.ping.PingPlugin",
"aprsd.plugins.query.QueryPlugin",
"aprsd.plugins.stock.StockPlugin",
"aprsd.plugins.time.TimePlugin",
"aprsd.plugins.weather.USWeatherPlugin",
"aprsd.plugins.version.VersionPlugin",
@@ -52,7 +52,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
config = None
rx_count = 0
tx_count = 0
version = "1.0"
version = aprsd.__version__
# Holds the list of APRSDThreads that the plugin creates
threads = []
@@ -242,11 +242,24 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
if result:
self.tx_inc()
else:
LOG.warning(f"{self.__class__} isn't enabled.")
result = f"{self.__class__.__name__} isn't enabled"
LOG.warning(result)
return result
class APRSFIKEYMixin:
"""Mixin class to enable checking the existence of the aprs.fi apiKey."""
def ensure_aprs_fi_key(self):
try:
self.config.check_option(["services", "aprs.fi", "apiKey"])
self.enabled = True
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
self.enabled = False
class HelpPlugin(APRSDRegexCommandPluginBase):
"""Help Plugin that is always enabled.
+1 -1
View File
@@ -59,9 +59,9 @@ class EmailInfo:
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
"""Email Plugin."""
version = "1.0"
command_regex = "^-.*"
command_name = "email"
short_description = "Send and Receive email"
# message_number:time combos so we don't resend the same email in
# five mins {int:int}
+11 -7
View File
@@ -11,9 +11,18 @@ LOG = logging.getLogger("APRSD")
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
"""Fortune."""
version = "1.0"
command_regex = "^[fF]"
command_name = "fortune"
short_description = "Give me a fortune"
fortune_path = None
def setup(self):
self.fortune_path = shutil.which("fortune")
if not self.fortune_path:
self.enabled = False
else:
self.enabled = True
@trace.trace
def process(self, packet):
@@ -25,13 +34,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
reply = None
fortune_path = shutil.which("fortune")
if not fortune_path:
reply = "Fortune command not installed"
return reply
try:
cmnd = [fortune_path, "-s", "-n 60"]
cmnd = [self.fortune_path, "-s", "-n 60"]
command = " ".join(cmnd)
output = subprocess.check_output(
command,
+5 -9
View File
@@ -8,12 +8,15 @@ from aprsd import plugin, plugin_utils, trace
LOG = logging.getLogger("APRSD")
class LocationPlugin(plugin.APRSDRegexCommandPluginBase):
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
"""Location!"""
version = "1.0"
command_regex = "^[lL]"
command_name = "location"
short_description = "Where in the world is a CALLSIGN's last GPS beacon?"
def setup(self):
self.ensure_aprs_fi_key()
@trace.trace
def process(self, packet):
@@ -22,13 +25,6 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase):
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
# get last location of a callsign, get descriptive name from weather service
try:
self.config.check_option(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
api_key = self.config["services"]["aprs.fi"]["apiKey"]
# optional second argument is a callsign to search
+13 -3
View File
@@ -1,6 +1,6 @@
import logging
from aprsd import packets, plugin
from aprsd import messaging, packets, plugin
LOG = logging.getLogger("APRSD")
@@ -15,7 +15,7 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
seen was older than the configured age limit.
"""
version = "1.0"
short_description = "Notify me when a CALLSIGN is recently seen on APRS-IS"
def process(self, packet):
LOG.info("NotifySeenPlugin")
@@ -37,7 +37,16 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
packet_type = packets.get_packet_type(packet)
# we shouldn't notify the alert user that they are online.
if fromcall != notify_callsign:
return f"{fromcall} was just seen by type:'{packet_type}'"
msg = messaging.TextMessage(
self.config["aprs"]["login"],
notify_callsign,
f"{fromcall} was just seen by type:'{packet_type}'",
# We don't need to keep this around if it doesn't go thru
allow_delay=False,
)
return msg
else:
return messaging.NULL_MESSAGE
else:
LOG.debug(
"Not old enough to notify callsign '{}' : {} < {}".format(
@@ -46,3 +55,4 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
wl.max_delta(),
),
)
return messaging.NULL_MESSAGE
+1 -1
View File
@@ -10,9 +10,9 @@ LOG = logging.getLogger("APRSD")
class PingPlugin(plugin.APRSDRegexCommandPluginBase):
"""Ping."""
version = "1.0"
command_regex = "^[pP]"
command_name = "ping"
short_description = "reply with a Pong!"
@trace.trace
def process(self, packet):
+1 -1
View File
@@ -11,9 +11,9 @@ LOG = logging.getLogger("APRSD")
class QueryPlugin(plugin.APRSDRegexCommandPluginBase):
"""Query command."""
version = "1.0"
command_regex = r"^\!.*"
command_name = "query"
short_description = "APRSD Owner command to query messages in the MsgTrack"
@trace.trace
def process(self, packet):
-51
View File
@@ -1,51 +0,0 @@
import logging
import re
import yfinance as yf
from aprsd import plugin, trace
LOG = logging.getLogger("APRSD")
class StockPlugin(plugin.APRSDRegexCommandPluginBase):
"""Stock market plugin for fetching stock quotes"""
version = "1.0"
command_regex = "^[sS]"
command_name = "stock"
@trace.trace
def process(self, packet):
LOG.info("StockPlugin")
# fromcall = packet.get("from")
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
searchcall = a.group(1)
stock_symbol = searchcall.upper()
else:
reply = "No stock symbol"
return reply
LOG.info(f"Fetch stock quote for '{stock_symbol}'")
try:
stock = yf.Ticker(stock_symbol)
reply = "{} - ask: {} high: {} low: {}".format(
stock_symbol,
stock.info["ask"],
stock.info["dayHigh"],
stock.info["dayLow"],
)
except Exception as e:
LOG.error(
f"Failed to fetch stock '{stock_symbol}' from yahoo '{e}'",
)
reply = f"Failed to fetch stock '{stock_symbol}'"
return reply.rstrip()
+11 -19
View File
@@ -14,9 +14,9 @@ LOG = logging.getLogger("APRSD")
class TimePlugin(plugin.APRSDRegexCommandPluginBase):
"""Time command."""
version = "1.0"
command_regex = "^[tT]"
command_name = "time"
short_description = "What is the current local time."
def _get_local_tz(self):
return pytz.timezone(time.strftime("%Z"))
@@ -49,12 +49,15 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
return self.build_date_str(localzone)
class TimeOpenCageDataPlugin(TimePlugin):
class TimeOpenCageDataPlugin(TimePlugin, plugin.APRSFIKEYMixin):
"""geocage based timezone fetching."""
version = "1.0"
command_regex = "^[tT]"
command_name = "time"
short_description = "Current time of GPS beacon timezone. Uses OpenCage"
def setup(self):
self.ensure_aprs_fi_key()
@trace.trace
def process(self, packet):
@@ -62,13 +65,6 @@ class TimeOpenCageDataPlugin(TimePlugin):
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
# get last location of a callsign, get descriptive name from weather service
try:
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
api_key = self.config["services"]["aprs.fi"]["apiKey"]
# optional second argument is a callsign to search
@@ -115,12 +111,15 @@ class TimeOpenCageDataPlugin(TimePlugin):
return self.build_date_str(localzone)
class TimeOWMPlugin(TimePlugin):
class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin):
"""OpenWeatherMap based timezone fetching."""
version = "1.0"
command_regex = "^[tT]"
command_name = "time"
short_description = "Current time of GPS beacon's timezone. Uses OpenWeatherMap"
def setup(self):
self.ensure_aprs_fi_key()
@trace.trace
def process(self, packet):
@@ -128,13 +127,6 @@ class TimeOWMPlugin(TimePlugin):
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
# get last location of a callsign, get descriptive name from weather service
try:
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
# optional second argument is a callsign to search
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
+1 -1
View File
@@ -10,9 +10,9 @@ LOG = logging.getLogger("APRSD")
class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
"""Version of APRSD Plugin."""
version = "1.0"
command_regex = "^[vV]"
command_name = "version"
short_description = "What is the APRSD Version"
# message_number:time combos so we don't resend the same email in
# five mins {int:int}
+20 -16
View File
@@ -10,7 +10,7 @@ from aprsd import plugin, plugin_utils, trace
LOG = logging.getLogger("APRSD")
class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
"""USWeather Command
Returns a weather report for the calling weather station
@@ -23,9 +23,12 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
"weather" - returns weather near the calling callsign
"""
version = "1.0"
command_regex = "^[wW]"
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):
@@ -33,20 +36,18 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
fromcall = packet.get("from")
# message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
try:
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
api_key = self.config["services"]["aprs.fi"]["apiKey"]
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 location"
return "Failed to fetch aprs.fi location"
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"
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
@@ -71,7 +72,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
return reply
class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
"""METAR Command
This provides a METAR weather report from a station near the caller
@@ -86,9 +87,12 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
"""
version = "1.0"
command_regex = "^[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):
@@ -126,12 +130,12 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
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 location"
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 location"
return "Failed to fetch aprs.fi location"
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
@@ -178,9 +182,9 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
"""
version = "1.0"
command_regex = "^[wW]"
command_name = "OpenWeatherMap"
short_description = "OpenWeatherMap weather of GPS Beacon location"
def help(self):
_help = [
@@ -308,9 +312,9 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
docker build -f Dockerfile -t avwx-api:master .
"""
version = "1.0"
command_regex = "^[mM]"
command_name = "AVWXWeather"
short_description = "AVWX weather of GPS Beacon location"
def help(self):
_help = [
+1 -1
View File
@@ -88,7 +88,7 @@ class KeepAliveThread(APRSDThread):
tracemalloc.start()
super().__init__("KeepAlive")
self.config = config
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
self.max_delta = datetime.timedelta(**max_timeout)
def loop(self):
+11
View File
@@ -60,6 +60,17 @@ def end_substr(original, substr):
return idx
def rgb_from_name(name):
"""Create an rgb tuple from a string."""
hash = 0
for char in name:
hash = ord(char) + ((hash << 5) - hash)
red = hash & 255
green = (hash >> 8) & 255
blue = (hash >> 16) & 255
return red, green, blue
def human_size(bytes, units=None):
"""Returns a human readable string representation of bytes"""
if not units:
+7 -1
View File
@@ -13,6 +13,12 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
done
fi
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
LOG_LEVEL="DEBUG"
fi
echo "Log level is set to ${LOG_LEVEL}";
# check to see if there is a config file
APRSD_CONFIG="/config/aprsd.yml"
if [ ! -e "$APRSD_CONFIG" ]; then
@@ -20,4 +26,4 @@ if [ ! -e "$APRSD_CONFIG" ]; then
aprsd sample-config > $APRSD_CONFIG
fi
exec aprsd server -c $APRSD_CONFIG --loglevel DEBUG
exec aprsd server -c $APRSD_CONFIG --loglevel ${LOG_LEVEL}
+1 -1
View File
@@ -44,7 +44,7 @@ do
esac
done
VERSION="2.4.2"
VERSION="2.5.4"
if [ $ALL_PLATFORMS -eq 1 ]
then
+1
View File
@@ -9,3 +9,4 @@ services:
environment:
- TZ=America/New_York
- APRS_PLUGINS=aprsd-slack-plugin>=1.0.2
- LOG_LEVEL=ERROR
+3 -2
View File
@@ -1,5 +1,5 @@
aioax25>=0.0.10
aprslib
aprslib>=0.7.0
click
click-completion
flask
@@ -17,7 +17,8 @@ pytz
requests
six
thesmuggler
yfinance
update_checker
flask-socketio
eventlet
tabulate
rich
+12 -20
View File
@@ -6,7 +6,7 @@
#
aioax25==0.0.10
# via -r requirements.in
aprslib==0.6.47
aprslib==0.7.0
# via -r requirements.in
backoff==1.11.1
# via opencage
@@ -25,6 +25,10 @@ click==8.0.1
# flask
click-completion==0.5.2
# via -r requirements.in
colorama==0.4.4
# via rich
commonmark==0.9.1
# via rich
contexter==0.1.4
# via signalslot
cryptography==3.4.7
@@ -63,20 +67,10 @@ jinja2==3.0.1
# via
# click-completion
# flask
lxml==4.6.3
# via yfinance
markupsafe==2.0.1
# via jinja2
multitasking==0.0.9
# via yfinance
numpy==1.21.2
# via
# pandas
# yfinance
opencage==2.0.0
# via -r requirements.in
pandas==1.3.2
# via yfinance
pbr==5.6.0
# via -r requirements.in
pluggy==1.0.0
@@ -85,20 +79,18 @@ py3-validate-email==1.0.1
# via -r requirements.in
pycparser==2.20
# via cffi
pygments==2.10.0
# via rich
pyopenssl==20.0.1
# via opencage
pyserial==3.5
# via aioax25
python-dateutil==2.8.1
# via pandas
python-engineio==4.2.1
# via python-socketio
python-socketio==5.4.0
# via flask-socketio
pytz==2021.1
# via
# -r requirements.in
# pandas
# via -r requirements.in
pyyaml==5.4.1
# via -r requirements.in
requests==2.26.0
@@ -106,7 +98,8 @@ requests==2.26.0
# -r requirements.in
# opencage
# update-checker
# yfinance
rich==10.15.2
# via -r requirements.in
shellingham==1.4.0
# via click-completion
signalslot==0.1.2
@@ -118,8 +111,9 @@ six==1.16.0
# eventlet
# imapclient
# pyopenssl
# python-dateutil
# signalslot
tabulate==0.8.9
# via -r requirements.in
thesmuggler==1.0.1
# via -r requirements.in
update-checker==0.18.0
@@ -130,5 +124,3 @@ weakrefmethod==1.0.3
# via signalslot
werkzeug==2.0.0
# via flask
yfinance==0.1.63
# via -r requirements.in
+8 -2
View File
@@ -2,7 +2,7 @@ from aprsd import packets, plugin, threads
FAKE_MESSAGE_TEXT = "fake MeSSage"
FAKE_FROM_CALLSIGN = "KFART"
FAKE_FROM_CALLSIGN = "KFAKE"
FAKE_TO_CALLSIGN = "KMINE"
@@ -45,7 +45,7 @@ class FakeThread(threads.APRSDThread):
super().__init__("FakeThread")
def loop(self):
return True
return False
class FakeBaseThreadsPlugin(plugin.APRSDPluginBase):
@@ -71,3 +71,9 @@ class FakeRegexCommandPlugin(plugin.APRSDRegexCommandPluginBase):
def process(self, packet):
return FAKE_MESSAGE_TEXT
class FakeWatchListPlugin(plugin.APRSDWatchListPluginBase):
def process(self, packet):
return FAKE_MESSAGE_TEXT
View File
+28
View File
@@ -0,0 +1,28 @@
from unittest import mock
from aprsd.plugins import fortune as fortune_plugin
from .. import fake, test_plugin
class TestFortunePlugin(test_plugin.TestPlugin):
@mock.patch("shutil.which")
def test_fortune_fail(self, mock_which):
mock_which.return_value = None
fortune = fortune_plugin.FortunePlugin(self.config)
expected = "FortunePlugin isn't enabled"
packet = fake.fake_packet(message="fortune")
actual = fortune.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("subprocess.check_output")
@mock.patch("shutil.which")
def test_fortune_success(self, mock_which, mock_output):
mock_which.return_value = "/usr/bin/games/fortune"
mock_output.return_value = "Funny fortune"
fortune = fortune_plugin.FortunePlugin(self.config)
expected = "Funny fortune"
packet = fake.fake_packet(message="fortune")
actual = fortune.filter(packet)
self.assertEqual(expected, actual)
+89
View File
@@ -0,0 +1,89 @@
from unittest import mock
from aprsd.plugins import location as location_plugin
from .. import fake, test_plugin
class TestLocationPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.config.Config.check_option")
def test_location_not_enabled_missing_aprs_fi_key(self, mock_check):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check.side_effect = Exception
fortune = location_plugin.LocationPlugin(self.config)
expected = "LocationPlugin isn't enabled"
packet = fake.fake_packet(message="location")
actual = fortune.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
def test_location_failed_aprs_fi_location(self, mock_check):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check.side_effect = Exception
fortune = location_plugin.LocationPlugin(self.config)
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="location")
actual = fortune.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
def test_location_failed_aprs_fi_location_no_entries(self, mock_check):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check.return_value = {"entries": []}
fortune = location_plugin.LocationPlugin(self.config)
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="location")
actual = fortune.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
@mock.patch("time.time")
def test_location_unknown_gps(self, mock_time, mock_weather, mock_check_aprs):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check_aprs.return_value = {
"entries": [
{
"lat": 10,
"lng": 11,
"lasttime": 10,
},
],
}
mock_weather.side_effect = Exception
mock_time.return_value = 10
fortune = location_plugin.LocationPlugin(self.config)
expected = "KFAKE: Unknown Location 0' 10,11 0.0h ago"
packet = fake.fake_packet(message="location")
actual = fortune.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
@mock.patch("time.time")
def test_location_works(self, mock_time, mock_weather, mock_check_aprs):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check_aprs.return_value = {
"entries": [
{
"lat": 10,
"lng": 11,
"lasttime": 10,
},
],
}
expected_town = "Appomattox, VA"
wx_data = {"location": {"areaDescription": expected_town}}
mock_weather.return_value = wx_data
mock_time.return_value = 10
fortune = location_plugin.LocationPlugin(self.config)
expected = f"KFAKE: {expected_town} 0' 10,11 0.0h ago"
packet = fake.fake_packet(message="location")
actual = fortune.filter(packet)
self.assertEqual(expected, actual)
+185
View File
@@ -0,0 +1,185 @@
from unittest import mock
from aprsd import client
from aprsd import config as aprsd_config
from aprsd import messaging, packets
from aprsd.plugins import notify as notify_plugin
from .. import fake, test_plugin
DEFAULT_WATCHLIST_CALLSIGNS = [fake.FAKE_FROM_CALLSIGN]
class TestWatchListPlugin(test_plugin.TestPlugin):
def setUp(self):
self.fromcall = fake.FAKE_FROM_CALLSIGN
self.ack = 1
def _config(
self,
watchlist_enabled=True,
watchlist_alert_callsign=None,
watchlist_alert_time_seconds=None,
watchlist_packet_keep_count=None,
watchlist_callsigns=DEFAULT_WATCHLIST_CALLSIGNS,
):
_config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT)
default_wl = aprsd_config.DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]
_config["ham"]["callsign"] = self.fromcall
_config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
_config["services"]["aprs.fi"]["apiKey"] = "something"
# Set the watchlist specific config options
_config["aprsd"]["watch_list"]["enabled"] = watchlist_enabled
if not watchlist_alert_callsign:
watchlist_alert_callsign = fake.FAKE_TO_CALLSIGN
_config["aprsd"]["watch_list"]["alert_callsign"] = watchlist_alert_callsign
if not watchlist_alert_time_seconds:
watchlist_alert_time_seconds = default_wl["alert_time_seconds"]
_config["aprsd"]["watch_list"]["alert_time_seconds"] = watchlist_alert_time_seconds
if not watchlist_packet_keep_count:
watchlist_packet_keep_count = default_wl["packet_keep_count"]
_config["aprsd"]["watch_list"]["packet_keep_count"] = watchlist_packet_keep_count
_config["aprsd"]["watch_list"]["callsigns"] = watchlist_callsigns
return _config
class TestAPRSDWatchListPluginBase(TestWatchListPlugin):
def test_watchlist_not_enabled(self):
config = self._config(watchlist_enabled=False)
self.config_and_init(config=config)
plugin = fake.FakeWatchListPlugin(self.config)
packet = fake.fake_packet(
message="version",
msg_number=1,
)
actual = plugin.filter(packet)
expected = messaging.NULL_MESSAGE
self.assertEqual(expected, actual)
@mock.patch("aprsd.client.ClientFactory", autospec=True)
def test_watchlist_not_in_watchlist(self, mock_factory):
client.factory = mock_factory
config = self._config()
self.config_and_init(config=config)
plugin = fake.FakeWatchListPlugin(self.config)
packet = fake.fake_packet(
fromcall="FAKE",
message="version",
msg_number=1,
)
actual = plugin.filter(packet)
expected = messaging.NULL_MESSAGE
self.assertEqual(expected, actual)
class TestNotifySeenPlugin(TestWatchListPlugin):
def test_disabled(self):
config = self._config(watchlist_enabled=False)
self.config_and_init(config=config)
plugin = notify_plugin.NotifySeenPlugin(self.config)
packet = fake.fake_packet(
message="version",
msg_number=1,
)
actual = plugin.filter(packet)
expected = messaging.NULL_MESSAGE
self.assertEqual(expected, actual)
@mock.patch("aprsd.client.ClientFactory", autospec=True)
def test_callsign_not_in_watchlist(self, mock_factory):
client.factory = mock_factory
config = self._config(watchlist_enabled=False)
self.config_and_init(config=config)
plugin = notify_plugin.NotifySeenPlugin(self.config)
packet = fake.fake_packet(
message="version",
msg_number=1,
)
actual = plugin.filter(packet)
expected = messaging.NULL_MESSAGE
self.assertEqual(expected, actual)
@mock.patch("aprsd.client.ClientFactory", autospec=True)
@mock.patch("aprsd.packets.WatchList.is_old")
def test_callsign_in_watchlist_not_old(self, mock_is_old, mock_factory):
client.factory = mock_factory
mock_is_old.return_value = False
config = self._config(
watchlist_enabled=True,
watchlist_callsigns=["WB4BOR"],
)
self.config_and_init(config=config)
plugin = notify_plugin.NotifySeenPlugin(self.config)
packet = fake.fake_packet(
fromcall="WB4BOR",
message="ping",
msg_number=1,
)
actual = plugin.filter(packet)
expected = messaging.NULL_MESSAGE
self.assertEqual(expected, actual)
@mock.patch("aprsd.client.ClientFactory", autospec=True)
@mock.patch("aprsd.packets.WatchList.is_old")
def test_callsign_in_watchlist_old_same_alert_callsign(self, mock_is_old, mock_factory):
client.factory = mock_factory
mock_is_old.return_value = True
config = self._config(
watchlist_enabled=True,
watchlist_alert_callsign="WB4BOR",
watchlist_callsigns=["WB4BOR"],
)
self.config_and_init(config=config)
plugin = notify_plugin.NotifySeenPlugin(self.config)
packet = fake.fake_packet(
fromcall="WB4BOR",
message="ping",
msg_number=1,
)
actual = plugin.filter(packet)
expected = messaging.NULL_MESSAGE
self.assertEqual(expected, actual)
@mock.patch("aprsd.client.ClientFactory", autospec=True)
@mock.patch("aprsd.packets.WatchList.is_old")
def test_callsign_in_watchlist_old_send_alert(self, mock_is_old, mock_factory):
client.factory = mock_factory
mock_is_old.return_value = True
notify_callsign = "KFAKE"
fromcall = "WB4BOR"
config = self._config(
watchlist_enabled=True,
watchlist_alert_callsign=notify_callsign,
watchlist_callsigns=["WB4BOR"],
)
self.config_and_init(config=config)
plugin = notify_plugin.NotifySeenPlugin(self.config)
packet = fake.fake_packet(
fromcall=fromcall,
message="ping",
msg_number=1,
)
packet_type = packets.get_packet_type(packet)
actual = plugin.filter(packet)
msg = f"{fromcall} was just seen by type:'{packet_type}'"
self.assertIsInstance(actual, messaging.TextMessage)
self.assertEqual(fake.FAKE_TO_CALLSIGN, actual.fromcall)
self.assertEqual(notify_callsign, actual.tocall)
self.assertEqual(msg, actual.message)
+50
View File
@@ -0,0 +1,50 @@
from unittest import mock
from aprsd.plugins import ping as ping_plugin
from .. import fake, test_plugin
class TestPingPlugin(test_plugin.TestPlugin):
@mock.patch("time.localtime")
def test_ping(self, mock_time):
fake_time = mock.MagicMock()
h = fake_time.tm_hour = 16
m = fake_time.tm_min = 12
s = fake_time.tm_sec = 55
mock_time.return_value = fake_time
ping = ping_plugin.PingPlugin(self.config)
packet = fake.fake_packet(
message="location",
msg_number=1,
)
result = ping.filter(packet)
self.assertEqual(None, result)
def ping_str(h, m, s):
return (
"Pong! "
+ str(h).zfill(2)
+ ":"
+ str(m).zfill(2)
+ ":"
+ str(s).zfill(2)
)
packet = fake.fake_packet(
message="Ping",
msg_number=1,
)
actual = ping.filter(packet)
expected = ping_str(h, m, s)
self.assertEqual(expected, actual)
packet = fake.fake_packet(
message="ping",
msg_number=1,
)
actual = ping.filter(packet)
self.assertEqual(expected, actual)
+37
View File
@@ -0,0 +1,37 @@
from unittest import mock
from aprsd import messaging
from aprsd.plugins import query as query_plugin
from .. import fake, test_plugin
class TestQueryPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.messaging.MsgTrack.flush")
def test_query_flush(self, mock_flush):
packet = fake.fake_packet(message="!delete")
query = query_plugin.QueryPlugin(self.config)
expected = "Deleted ALL pending msgs."
actual = query.filter(packet)
mock_flush.assert_called_once()
self.assertEqual(expected, actual)
@mock.patch("aprsd.messaging.MsgTrack.restart_delayed")
def test_query_restart_delayed(self, mock_restart):
track = messaging.MsgTrack()
track.data = {}
packet = fake.fake_packet(message="!4")
query = query_plugin.QueryPlugin(self.config)
expected = "No pending msgs to resend"
actual = query.filter(packet)
mock_restart.assert_not_called()
self.assertEqual(expected, actual)
mock_restart.reset_mock()
# add a message
msg = messaging.TextMessage(self.fromcall, "testing", self.ack)
track.add(msg)
actual = query.filter(packet)
mock_restart.assert_called_once()
+50
View File
@@ -0,0 +1,50 @@
from unittest import mock
import pytz
from aprsd.fuzzyclock import fuzzy
from aprsd.plugins import time as time_plugin
from .. import fake, test_plugin
class TestTimePlugins(test_plugin.TestPlugin):
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
@mock.patch("aprsd.plugins.time.TimePlugin._get_utcnow")
def test_time(self, mock_utcnow, mock_localtz):
utcnow = pytz.datetime.datetime.utcnow()
mock_utcnow.return_value = utcnow
tz = pytz.timezone("US/Pacific")
mock_localtz.return_value = tz
gmt_t = pytz.utc.localize(utcnow)
local_t = gmt_t.astimezone(tz)
fake_time = mock.MagicMock()
h = int(local_t.strftime("%H"))
m = int(local_t.strftime("%M"))
fake_time.tm_sec = 13
time = time_plugin.TimePlugin(self.config)
packet = fake.fake_packet(
message="location",
msg_number=1,
)
actual = time.filter(packet)
self.assertEqual(None, actual)
cur_time = fuzzy(h, m, 1)
packet = fake.fake_packet(
message="time",
msg_number=1,
)
local_short_str = local_t.strftime("%H:%M %Z")
expected = "{} ({})".format(
cur_time,
local_short_str,
)
actual = time.filter(packet)
self.assertEqual(expected, actual)
+35
View File
@@ -0,0 +1,35 @@
from unittest import mock
import aprsd
from aprsd.plugins import version as version_plugin
from .. import fake, test_plugin
class TestVersionPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.plugin.PluginManager.get_plugins")
def test_version(self, mock_get_plugins):
expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00"
version = version_plugin.VersionPlugin(self.config)
packet = fake.fake_packet(
message="No",
msg_number=1,
)
actual = version.filter(packet)
self.assertEqual(None, actual)
packet = fake.fake_packet(
message="version",
msg_number=1,
)
actual = version.filter(packet)
self.assertEqual(expected, actual)
packet = fake.fake_packet(
message="Version",
msg_number=1,
)
actual = version.filter(packet)
self.assertEqual(expected, actual)
+176
View File
@@ -0,0 +1,176 @@
from unittest import mock
from aprsd.plugins import weather as weather_plugin
from .. import fake, test_plugin
class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.config.Config.check_option")
def test_not_enabled_missing_aprs_fi_key(self, mock_check):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check.side_effect = Exception
wx = weather_plugin.USWeatherPlugin(self.config)
expected = "USWeatherPlugin isn't enabled"
packet = fake.fake_packet(message="weather")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
def test_failed_aprs_fi_location(self, mock_check):
# When the aprs.fi api key isn't set, then
# the Plugin will be disabled.
mock_check.side_effect = Exception
wx = weather_plugin.USWeatherPlugin(self.config)
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="weather")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
def test_failed_aprs_fi_location_no_entries(self, mock_check):
# When the aprs.fi api key isn't set, then
# the Plugin will be disabled.
mock_check.return_value = {"entries": []}
wx = weather_plugin.USWeatherPlugin(self.config)
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="weather")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
def test_unknown_gps(self, mock_weather, mock_check_aprs):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check_aprs.return_value = {
"entries": [
{
"lat": 10,
"lng": 11,
"lasttime": 10,
},
],
}
mock_weather.side_effect = Exception
wx = weather_plugin.USWeatherPlugin(self.config)
expected = "Unable to get weather"
packet = fake.fake_packet(message="weather")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
def test_working(self, mock_weather, mock_check_aprs):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check_aprs.return_value = {
"entries": [
{
"lat": 10,
"lng": 11,
"lasttime": 10,
},
],
}
mock_weather.return_value = {
"currentobservation": {"Temp": "400"},
"data": {
"temperature": ["10", "11"],
"weather": ["test", "another"],
},
"time": {"startPeriodName": ["ignored", "sometime"]},
}
wx = weather_plugin.USWeatherPlugin(self.config)
expected = "400F(10F/11F) test. sometime, another."
packet = fake.fake_packet(message="weather")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
class TestUSMetarPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.config.Config.check_option")
def test_not_enabled_missing_aprs_fi_key(self, mock_check):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check.side_effect = Exception
wx = weather_plugin.USMetarPlugin(self.config)
expected = "USMetarPlugin isn't enabled"
packet = fake.fake_packet(message="metar")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
def test_failed_aprs_fi_location(self, mock_check):
# When the aprs.fi api key isn't set, then
# the Plugin will be disabled.
mock_check.side_effect = Exception
wx = weather_plugin.USMetarPlugin(self.config)
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="metar")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
def test_failed_aprs_fi_location_no_entries(self, mock_check):
# When the aprs.fi api key isn't set, then
# the Plugin will be disabled.
mock_check.return_value = {"entries": []}
wx = weather_plugin.USMetarPlugin(self.config)
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="metar")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_weather_gov_metar")
def test_gov_metar_fetch_fails(self, mock_metar):
mock_metar.side_effect = Exception
wx = weather_plugin.USMetarPlugin(self.config)
expected = "Unable to find station METAR"
packet = fake.fake_packet(message="metar KPAO")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_weather_gov_metar")
def test_airport_works(self, mock_metar):
class Response:
text = '{"properties": {"rawMessage": "BOGUSMETAR"}}'
mock_metar.return_value = Response()
wx = weather_plugin.USMetarPlugin(self.config)
expected = "BOGUSMETAR"
packet = fake.fake_packet(message="metar KPAO")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_weather_gov_metar")
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
def test_metar_works(self, mock_wx_for_gps, mock_check_aprs, mock_metar):
mock_wx_for_gps.return_value = {
"location": {"metar": "BOGUSMETAR"},
}
class Response:
text = '{"properties": {"rawMessage": "BOGUSMETAR"}}'
mock_check_aprs.return_value = {
"entries": [
{
"lat": 10,
"lng": 11,
"lasttime": 10,
},
],
}
mock_metar.return_value = Response()
wx = weather_plugin.USMetarPlugin(self.config)
expected = "BOGUSMETAR"
packet = fake.fake_packet(message="metar")
actual = wx.filter(packet)
self.assertEqual(expected, actual)
+27 -185
View File
@@ -1,33 +1,44 @@
import unittest
from unittest import mock
import pytz
import aprsd
from aprsd import config, messaging, packets, stats
from aprsd.fuzzyclock import fuzzy
from aprsd.plugins import fortune as fortune_plugin
from aprsd.plugins import ping as ping_plugin
from aprsd.plugins import query as query_plugin
from aprsd.plugins import time as time_plugin
from aprsd.plugins import version as version_plugin
from aprsd import config as aprsd_config
from aprsd import messaging, packets, stats
from . import fake
class TestPlugin(unittest.TestCase):
def setUp(self):
def setUp(self) -> None:
self.fromcall = fake.FAKE_FROM_CALLSIGN
self.ack = 1
self.config = config.DEFAULT_CONFIG_DICT
self.config["ham"]["callsign"] = self.fromcall
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
self.config_and_init()
def tearDown(self) -> None:
stats.APRSDStats._instance = None
packets.WatchList._instance = None
packets.SeenList._instance = None
messaging.MsgTrack._instance = None
self.config = None
def config_and_init(self, config=None):
if not config:
self.config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT)
self.config["ham"]["callsign"] = self.fromcall
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
self.config["services"]["aprs.fi"]["apiKey"] = "something"
else:
self.config = config
# Inintialize the stats object with the config
stats.APRSDStats(self.config)
packets.WatchList(config=self.config)
packets.SeenList(config=self.config)
messaging.MsgTrack(config=self.config)
class TestPluginBase(TestPlugin):
@mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process")
def test_base_plugin_no_threads(self, mock_process):
p = fake.FakeBaseNoThreadsPlugin(self.config)
@@ -51,8 +62,9 @@ class TestPlugin(unittest.TestCase):
@mock.patch.object(fake.FakeBaseThreadsPlugin, "create_threads")
def test_base_plugin_threads_created(self, mock_create):
fake.FakeBaseThreadsPlugin(self.config)
p = fake.FakeBaseThreadsPlugin(self.config)
mock_create.assert_called_once()
p.stop_threads()
def test_base_plugin_threads(self):
p = fake.FakeBaseThreadsPlugin(self.config)
@@ -122,173 +134,3 @@ class TestPlugin(unittest.TestCase):
expected = fake.FAKE_MESSAGE_TEXT
actual = p.filter(packet)
self.assertEqual(expected, actual)
class TestFortunePlugin(TestPlugin):
@mock.patch("shutil.which")
def test_fortune_fail(self, mock_which):
fortune = fortune_plugin.FortunePlugin(self.config)
mock_which.return_value = None
expected = "Fortune command not installed"
packet = fake.fake_packet(message="fortune")
actual = fortune.filter(packet)
self.assertEqual(expected, actual)
@mock.patch("subprocess.check_output")
@mock.patch("shutil.which")
def test_fortune_success(self, mock_which, mock_output):
fortune = fortune_plugin.FortunePlugin(self.config)
mock_which.return_value = "/usr/bin/games"
mock_output.return_value = "Funny fortune"
expected = "Funny fortune"
packet = fake.fake_packet(message="fortune")
actual = fortune.filter(packet)
self.assertEqual(expected, actual)
class TestQueryPlugin(TestPlugin):
@mock.patch("aprsd.messaging.MsgTrack.flush")
def test_query_flush(self, mock_flush):
packet = fake.fake_packet(message="!delete")
query = query_plugin.QueryPlugin(self.config)
expected = "Deleted ALL pending msgs."
actual = query.filter(packet)
mock_flush.assert_called_once()
self.assertEqual(expected, actual)
@mock.patch("aprsd.messaging.MsgTrack.restart_delayed")
def test_query_restart_delayed(self, mock_restart):
track = messaging.MsgTrack()
track.data = {}
packet = fake.fake_packet(message="!4")
query = query_plugin.QueryPlugin(self.config)
expected = "No pending msgs to resend"
actual = query.filter(packet)
mock_restart.assert_not_called()
self.assertEqual(expected, actual)
mock_restart.reset_mock()
# add a message
msg = messaging.TextMessage(self.fromcall, "testing", self.ack)
track.add(msg)
actual = query.filter(packet)
mock_restart.assert_called_once()
class TestTimePlugins(TestPlugin):
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
@mock.patch("aprsd.plugins.time.TimePlugin._get_utcnow")
def test_time(self, mock_utcnow, mock_localtz):
utcnow = pytz.datetime.datetime.utcnow()
mock_utcnow.return_value = utcnow
tz = pytz.timezone("US/Pacific")
mock_localtz.return_value = tz
gmt_t = pytz.utc.localize(utcnow)
local_t = gmt_t.astimezone(tz)
fake_time = mock.MagicMock()
h = int(local_t.strftime("%H"))
m = int(local_t.strftime("%M"))
fake_time.tm_sec = 13
time = time_plugin.TimePlugin(self.config)
packet = fake.fake_packet(
message="location",
msg_number=1,
)
actual = time.filter(packet)
self.assertEqual(None, actual)
cur_time = fuzzy(h, m, 1)
packet = fake.fake_packet(
message="time",
msg_number=1,
)
local_short_str = local_t.strftime("%H:%M %Z")
expected = "{} ({})".format(
cur_time,
local_short_str,
)
actual = time.filter(packet)
self.assertEqual(expected, actual)
class TestPingPlugin(TestPlugin):
@mock.patch("time.localtime")
def test_ping(self, mock_time):
fake_time = mock.MagicMock()
h = fake_time.tm_hour = 16
m = fake_time.tm_min = 12
s = fake_time.tm_sec = 55
mock_time.return_value = fake_time
ping = ping_plugin.PingPlugin(self.config)
packet = fake.fake_packet(
message="location",
msg_number=1,
)
result = ping.filter(packet)
self.assertEqual(None, result)
def ping_str(h, m, s):
return (
"Pong! "
+ str(h).zfill(2)
+ ":"
+ str(m).zfill(2)
+ ":"
+ str(s).zfill(2)
)
packet = fake.fake_packet(
message="Ping",
msg_number=1,
)
actual = ping.filter(packet)
expected = ping_str(h, m, s)
self.assertEqual(expected, actual)
packet = fake.fake_packet(
message="ping",
msg_number=1,
)
actual = ping.filter(packet)
self.assertEqual(expected, actual)
class TestVersionPlugin(TestPlugin):
@mock.patch("aprsd.plugin.PluginManager.get_plugins")
def test_version(self, mock_get_plugins):
expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00"
version = version_plugin.VersionPlugin(self.config)
packet = fake.fake_packet(
message="No",
msg_number=1,
)
actual = version.filter(packet)
self.assertEqual(None, actual)
packet = fake.fake_packet(
message="version",
msg_number=1,
)
actual = version.filter(packet)
self.assertEqual(expected, actual)
packet = fake.fake_packet(
message="Version",
msg_number=1,
)
actual = version.filter(packet)
self.assertEqual(expected, actual)
+9 -14
View File
@@ -2,7 +2,7 @@
minversion = 2.9.0
skipdist = True
skip_missing_interpreters = true
envlist = pre-commit,pep8,py{36,37,38}
envlist = pre-commit,pep8,py{36,37,38,39}
# Activate isolated build environment. tox will use a virtual environment
# to build a source distribution from the source tree. For build tools and
@@ -10,8 +10,11 @@ envlist = pre-commit,pep8,py{36,37,38}
isolated_build = true
[testenv]
setenv = _PYTEST_SETUP_SKIP_APRSD_DEP=1
coverage: _APRSD_TOX_CMD=coverage run -m pytest
description = Run unit-testing
setenv =
_PYTEST_SETUP_SKIP_APRSD_DEP=1
PYTHONDONTWRITEBYTECODE=1
PYTHONUNBUFFERED=1
usedevelop = True
install_command = pip install {opts} {packages}
extras = tests
@@ -20,18 +23,10 @@ deps = coverage: coverage
-r{toxinidir}/dev-requirements.txt
pytestmain: git+https://github.com/pytest-dev/pytest.git@main
commands =
{env:_APRSD_TOX_CMD:pytest} {posargs}
pytest -v --cov-report term-missing --cov=aprsd {posargs}
coverage: coverage report -m
coverage: coverage xml
[pytest]
minversion=2.0
testpaths = tests
#--pyargs --doctest-modules --ignore=.tox
addopts=-r a
filterwarnings =
error
[testenv:docs]
skip_install = true
deps =
@@ -78,8 +73,8 @@ exclude = .venv,.git,.tox,dist,doc,.ropeproject
python =
3.6: py36, pep8
3.7: py38, pep8
3.8: py38, pep8, type-check, docs
3.9: py39
3.8: py38, pep8
3.9: py39, pep8, type-check, docs
[testenv:fmt]
# This will reformat your code to comply with pep8