1
0
mirror of https://github.com/craigerl/aprsd.git synced 2025-06-13 20:02:26 -04:00

Compare commits

...

30 Commits

Author SHA1 Message Date
375a5e5b34 Updated README.md TOC
Updated the table of contents
2025-01-24 16:23:42 -05:00
cf4a29f0cb reduced logo size 50% 2025-01-23 08:55:06 -05:00
447451c6c9 Added plugin and extension links
This patch updates the README.md file to link to existing plugins
and extensions.
2025-01-23 08:51:23 -05:00
0ed648f8f8 Added APRSD logo 2025-01-23 08:23:38 -05:00
ba8acdc584 try making image for webchat 2025-01-22 16:59:14 -05:00
dabb48c6f6 updated healthcheck.sh 2025-01-18 21:25:47 +00:00
1054999568 added healthcheck.sh 2025-01-17 00:44:43 +00:00
044ea4cc9a Update the admin and setup.sh for container 2025-01-17 00:40:39 +00:00
24db814c82 Updated Docker for using alpine and uv 2025-01-15 18:00:12 -05:00
1cba31f0ac removed pytest from README 2025-01-15 17:55:12 -05:00
02e29405ce Added star history to readme 2025-01-15 17:52:21 -05:00
cdd297c5bb Added activity to README 2025-01-15 08:18:48 -05:00
e332d7c9d0 some cleanup 2025-01-10 17:09:42 -05:00
e4f82d6054 updated tools in pre-commit 2025-01-10 17:07:45 -05:00
8d98546055 Added .mailmap 2025-01-07 15:57:19 -05:00
3ee422b5c9 update to py 3.10 2025-01-07 10:50:22 -05:00
7702d68cf7 updated action versions 2025-01-07 10:48:12 -05:00
c8735c257a added authors.yml 2025-01-07 10:46:59 -05:00
3cd9bfa7bb updated docs rst files 2025-01-07 10:22:26 -05:00
007386505a updated plugin example 2025-01-07 10:22:01 -05:00
7f2c1d7124 updated requirements 2025-01-07 10:06:02 -05:00
fbec7168eb updated requirements-dev 2025-01-07 10:01:41 -05:00
e3a7e7fb8a updated github workflows 2025-01-07 09:35:39 -05:00
a21432fb24 removed BeautifulSoup usage 2025-01-06 17:27:39 -05:00
275e33538d Updated README.md
Added links to aprsd-admin-extension and aprsd-webchat-extension
since they are now external.
2025-01-06 17:18:15 -05:00
5274c5dc56 Some cleanup with list plugins 2025-01-06 17:14:55 -05:00
df14eb8f28 updated workflow 2025-01-06 17:14:01 -05:00
fd74405b5f Updated README.md 2025-01-06 17:13:40 -05:00
ec1adf4182 fixed list-plugins
This patch fixes the list-plugins and list-extensions.
Pypi changed their search page to require javascript, which
breaks python scripts....
2025-01-03 17:16:26 -05:00
b1a830d54e Changed README.rst -> README.md 2025-01-03 12:29:31 -05:00
36 changed files with 982 additions and 1031 deletions

15
.github/workflows/authors.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: Update Authors
on:
push:
branches:
- master
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: wow-actions/update-authors@v1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
template: '{{email}} : {{commits}}'
path: 'AUTHORS'

View File

@ -18,7 +18,6 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v8
@ -30,16 +29,16 @@ jobs:
run: |
echo "Selected Branch '${{ steps.extract_branch.outputs.branch }}'"
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to Docker HUB
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build the Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v6
with:
context: "{{defaultContext}}:docker"
platforms: linux/amd64,linux/arm64

View File

@ -7,7 +7,7 @@ on:
branches:
- "**"
tags:
- "v*.*.*"
- "*.*.*"
pull_request:
branches:
- "master"
@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
@ -35,21 +35,20 @@ jobs:
needs: tox
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v8
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to Docker HUB
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build the Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v6
with:
context: "{{defaultContext}}:docker"
platforms: linux/amd64,linux/arm64

View File

@ -9,9 +9,9 @@ jobs:
matrix:
python-version: ["3.10", "3.11"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies

4
.mailmap Normal file
View File

@ -0,0 +1,4 @@
Craig Lamparter <craig@craiger.org> <craiger@hpe.com>
Craig Lamparter <craig@craiger.org> craigerl <craig@craiger.org>
Craig Lamparter <craig@craiger.org> craigerl <craiger@hpe.com>
Walter A. Boring IV <waboring@hemna.com> Hemna <waboring@hemna.com>

View File

@ -13,12 +13,12 @@ repos:
- id: check-illegal-windows-names
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.5.0
rev: v2.7.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
rev: v0.9.0
hooks:
- id: ruff
###### Relevant part below ######
@ -26,3 +26,16 @@ repos:
args: ["check", "--select", "I", "--fix"]
###### Relevant part above ######
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.5.16
hooks:
# Compile requirements
- id: pip-compile
name: pip-compile requirements.in
args: [--resolver, backtracking, --annotation-style=line, requirements.in, -o, requirements.txt]
- id: pip-compile
name: pip-compile requirements-dev.in
args: [--resolver, backtracking, --annotation-style=line, requirements-dev.in, -o, requirements-dev.txt]
files: ^requirements-dev\.(in|txt)$

31
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,31 @@
# CONTRIBUTING
Code contributions are welcomed and appreciated. Just submit a PR!
The current build environment uses `pre-commit`, and `uv`.
### Environment setup:
```console
pip install uv
uv venv
uv pip install pip-tools
git clone git@github.com:craigerl/aprsd.git
cd aprsd
pre-commit install
# Optionally run the pre-commit scripts at any time
pre-commit run --all-files
```
### Running and testing:
From the aprstastic directory:
```console
cd aprsd
uv pip install -e .
# Running
uv run aprsd
```

454
README.md Normal file
View File

@ -0,0 +1,454 @@
# APRSD - Ham radio APRS-IS Message platform software
## KM6LYW and WB4BOR
[![pypi](https://badge.fury.io/py/aprsd.svg)](https://badge.fury.io/py/aprsd)
[![versions](https://img.shields.io/pypi/pyversions/aprsd.svg)](https://pypi.org/pypi/aprsd)
[![slack](https://img.shields.io/badge/slack-@hemna/aprsd-blue.svg?logo=slack)](https://hemna.slack.com/app_redirect?channel=C01KQSCP5RP)
![issues](https://img.shields.io/github/issues/craigerl/aprsd)
![commit](https://img.shields.io/github/last-commit/craigerl/aprsd)
[![imports](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/)
[![down](https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/aprsd)
[APRSD](http://github.com/craigerl/aprsd) is a Ham radio
[APRS](http://aprs.org) message platform built with python.
![image](./aprsd_logo.png)
# Table of Contents
1. [APRSD - Ham radio APRS-IS Message platform software](#aprsd---ham-radio-aprs-is-message-platform-software)
2. [What is APRSD](#what-is-aprsd)
3. [APRSD Plugins/Extensions](#aprsd-pluginsextensions)
4. [List of existing plugins - APRS Message processing/responders](#list-of-existing-plugins---aprs-message-processingresponders)
5. [List of existing extensions - Add new capabilities to APRSD](#list-of-existing-extensions---add-new-capabilities-to-aprsd)
6. [APRSD Overview Diagram](#aprsd-overview-diagram)
7. [Typical use case](#typical-use-case)
8. [Installation](#installation)
9. [Example usage](#example-usage)
10. [Help](#help)
11. [Commands](#commands)
12. [Configuration](#configuration)
13. [server](#server)
14. [Current list plugins](#current-list-plugins)
15. [Current list extensions](#current-list-extensions)
16. [send-message](#send-message)
17. [Development](#development)
18. [Release](#release)
19. [Building your own APRSD plugins](#building-your-own-aprsd-plugins)
20. [Overview](#overview)
21. [Docker Container](#docker-container)
22. [Building](#building)
23. [Official Build](#official-build)
24. [Development Build](#development-build)
25. [Running the container](#running-the-container)
26. [Activity](#activity)
27. [Star History](#star-history)
---
> [!WARNING]
> Legal operation of this software requires an amateur radio license and a valid call sign.
> [!NOTE]
> Star this repo to follow our progress! This code is under active development, and contributions are both welcomed and appreciated. See [CONTRIBUTING.md](<https://github.com/craigerl/aprsd/blob/master/CONTRIBUTING.md>) for details.
### What is APRSD
APRSD is a python application for interacting with the APRS network and Ham radios with KISS interfaces and
providing APRS services for HAM radio operators.
APRSD currently has 4 main commands to use.
- server - Connect to APRS and listen/respond to APRS messages
- send-message - Send a message to a callsign via APRS_IS.
- listen - Listen to packets on the APRS-IS Network based on FILTER.
- check-version - check the version of aprsd
- sample-config - generate a sample config file
- dev - helpful for testing new aprsd plugins under development
- dump-stats - output the stats of a running aprsd server command
- list-plugins - list the built in plugins, available plugins on pypi.org and installed plugins
- list-extensions - list the available extensions on pypi.org and installed extensions
Each of those commands can connect to the APRS-IS network if internet
connectivity is available. If internet is not available, then APRS can
be configured to talk to a TCP KISS TNC for radio connectivity directly.
Please [read the docs](https://aprsd.readthedocs.io) to learn more!
### APRSD Plugins/Extensions
APRSD Has the ability to add plugins and extensions. Plugins add new message filters that can look for specific messages and respond. For example, the aprsd-email-plugin adds the ability to send/recieve email to/from an APRS callsign. Extensions add new unique capabilities to APRSD itself. For example the aprsd-admin-extension adds a web interface command that shows the running status of the aprsd server command. aprsd-webchat-extension is a new web based APRS 'chat' command.
You can see the [available plugins/extensions on pypi here:](https://pypi.org/search/?q=aprsd) [https://pypi.org/search/?q=aprsd](https://pypi.org/search/?q=aprsd)
> [!NOTE]
> aprsd admin and webchat commands have been extracted into separate extensions.
* [See admin extension here](https://github.com/hemna/aprsd-admin-extension) <div id="admin logo" align="left"><img src="https://raw.githubusercontent.com/hemna/aprsd-admin-extension/refs/heads/master/screenshot.png" alt="Web Admin" width="340"/></div>
* [See webchat extension here](https://github.com/hemna/aprsd-webchat-extension) <div id="webchat logo" align="left"><img src="https://raw.githubusercontent.com/hemna/aprsd-webchat-extension/master/screenshot.png" alt="Webchat" width="340"/></div>
### List of existing plugins - APRS Message processing/responders
- [aprsd-email-plugin](https://github.com/hemna/aprsd-email-plugin) - send/receive email!
- [aprsd-location-plugin](https://github.com/hemna/aprsd-location-plugin) - get latest GPS location.
- [aprsd-locationdata-plugin](https://github.com/hemna/aprsd-locationdata-plugin) - get latest GPS location
- [aprsd-digipi-plugin](https://github.com/hemna/aprsd-digipi-plugin) - Look for digipi beacon packets
- [aprsd-w3w-plugin](https://github.com/hemna/aprsd-w3w-plugin) - get your w3w coordinates
- [aprsd-mqtt-plugin](https://github.com/hemna/aprsd-mqtt-plugin) - send aprs packets to an MQTT topic
- [aprsd-telegram-plugin](https://github.com/hemna/aprsd-telegram-plugin) - send/receive messages to telegram
- [aprsd-borat-plugin](https://github.com/hemna/aprsd-borat-plugin) - get Borat quotes
- [aprsd-wxnow-plugin](https://github.com/hemna/aprsd-wxnow-plugin) - get closest N weather station reports
- [aprsd-weewx-plugin](https://github.com/hemna/aprsd-weewx-plugin) - get weather from your weewx weather station
- [aprsd-slack-plugin](https://github.com/hemna/aprsd-slack-plugin) - send/receive messages to a slack channel
- [aprsd-sentry-plugin](https://github.com/hemna/aprsd-sentry-plugin) -
- [aprsd-repeat-plugins](https://github.com/hemna/aprsd-repeat-plugins) - plugins for the REPEAT service. Get nearest Ham radio repeaters!
- [aprsd-twitter-plugin](https://github.com/hemna/aprsd-twitter-plugin) - make tweets from your Ham Radio!
- [aprsd-timeopencage-plugin](https://github.com/hemna/aprsd-timeopencage-plugin) - Get local time for a callsign
- [aprsd-stock-plugin](https://github.com/hemna/aprsd-stock-plugin) - get stock quotes from your Ham radio
### List of existing extensions - Add new capabilities to APRSD
- [aprsd-admin-extension](https://github.com/hemna/aprsd-admin-extension) - Web Administration page for APRSD
- [aprsd-webchat-extension](https://github.com/hemna/aprsd-webchat-extension) - Web page for APRS Messaging
- [aprsd-irc-extension](https://github.com/hemna/aprsd-irc-extension) - an IRC like server command for APRS
### APRSD Overview Diagram
![image](https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true)
### Typical use case
APRSD\'s typical use case is that of providing an APRS wide service to
all HAM radio operators. For example the callsign \'REPEAT\' on the APRS
network is actually an instance of APRSD that can provide a list of HAM
repeaters in the area of the callsign that sent the message.
Ham radio operator using an APRS enabled HAM radio sends a message to
check the weather. An APRS message is sent, and then picked up by APRSD.
The APRS packet is decoded, and the message is sent through the list of
plugins for processing. For example, the WeatherPlugin picks up the
message, fetches the weather for the area around the user who sent the
request, and then responds with the weather conditions in that area.
Also includes a watch list of HAM callsigns to look out for. The watch
list can notify you when a HAM callsign in the list is seen and now
available to message on the APRS network.
### Installation
To install `aprsd`, use Pip:
`pip install aprsd`
### Example usage
`aprsd -h`
### Help
:
└─> aprsd -h
Usage: aprsd [OPTIONS] COMMAND [ARGS]...
Options:
--version Show the version and exit.
-h, --help Show this message and exit.
Commands:
check-version Check this version against the latest in pypi.org.
completion Show the shell completion code
dev Development type subcommands
fetch-stats Fetch stats from a APRSD admin web interface.
healthcheck Check the health of the running aprsd server.
list-extensions List the built in plugins available to APRSD.
list-plugins List the built in plugins available to APRSD.
listen Listen to packets on the APRS-IS Network based on FILTER.
sample-config Generate a sample Config file from aprsd and all...
send-message Send a message to a callsign via APRS_IS.
server Start the aprsd server gateway process.
version Show the APRSD version.
### Commands
### Configuration
This command outputs a sample config yml formatted block that you can
edit and use to pass in to `aprsd` with `-c`. By default aprsd looks in
`~/.config/aprsd/aprsd.yml`
`aprsd sample-config`
└─> aprsd sample-config
...
### server
This is the main server command that will listen to APRS-IS servers and
look for incomming commands to the callsign configured in the config
file
└─[$] > aprsd server --help
Usage: aprsd server [OPTIONS]
Start the aprsd server gateway process.
Options:
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
The log level to use for aprsd.log
[default: INFO]
-c, --config TEXT The aprsd config file to use for options.
[default:
/Users/i530566/.config/aprsd/aprsd.yml]
--quiet Don't log to stdout
-f, --flush Flush out all old aged messages on disk.
[default: False]
-h, --help Show this message and exit.
└─> aprsd server
Registering LogMonitorThread
2025-01-06 16:27:12.398 | MainThread | INFO | APRSD is up to date | aprsd.cmds.server:server:82
2025-01-06 16:27:12.398 | MainThread | INFO | APRSD Started version: 3.5.1.dev0+g72d068c.d20250102 | aprsd.cmds.server:server:83
2025-01-06 16:27:12.398 | MainThread | INFO | Creating client connection | aprsd.cmds.server:server:101
2025-01-06 16:27:12.398 | MainThread | INFO | Creating aprslib client(noam.aprs2.net:14580) and logging in WB4BOR-1. | aprsd.client.aprsis:setup_connection:136
2025-01-06 16:27:12.398 | MainThread | INFO | Attempting connection to noam.aprs2.net:14580 | aprslib.inet:_connect:226
2025-01-06 16:27:12.473 | MainThread | INFO | Connected to ('44.135.208.225', 14580) | aprslib.inet:_connect:233
2025-01-06 16:27:12.617 | MainThread | INFO | Login successful | aprsd.client.drivers.aprsis:_send_login:154
2025-01-06 16:27:12.618 | MainThread | INFO | Connected to T2BC | aprsd.client.drivers.aprsis:_send_login:156
2025-01-06 16:27:12.618 | MainThread | INFO | <aprsd.client.aprsis.APRSISClient object at 0x103a36480> | aprsd.cmds.server:server:103
2025-01-06 16:27:12.618 | MainThread | INFO | Loading Plugin Manager and registering plugins | aprsd.cmds.server:server:117
2025-01-06 16:27:12.619 | MainThread | INFO | Loading APRSD Plugins | aprsd.plugin:setup_plugins:492
#### Current list plugins
└─> aprsd list-plugins
🐍 APRSD Built-in Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
Pypi.org APRSD Installable Plugin Packages
Install any of the following plugins with
'pip install <Plugin Package Name>'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-assistant-plugin │ APRSd plugin for hosting the APRS Assistant chatbot │ 0.0.3 │ 2024-10-20T02:59:39 │ No │
│ │ (aprs-assistant) │ │ │ │
│ 📂 aprsd-borat-plugin │ Borat quotes for aprsd plugin │ 0.1.1.dev1 │ 2024-01-19T16:04:38 │ No │
│ 📂 aprsd-locationdata-plugin │ Fetch location information from a callsign │ 0.3.0 │ 2024-02-06T17:20:43 │ No │
│ 📂 aprsd-mqtt-plugin │ APRSD MQTT Plugin sends APRS packets to mqtt queue │ 0.2.0 │ 2023-04-17T16:01:50 │ No │
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.2.0 │ 2023-01-10T17:15:36 │ No │
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ 2022-12-02T19:07:33 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and │ 1.2.0 │ 2023-01-10T19:21:33 │ No │
│ │ responds │ │ │ │
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ 2022-12-02T18:56:19 │ Yes │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ 2022-12-02T19:07:15 │ No │
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.2.0 │ 2023-01-10T17:07:11 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.5.0 │ 2023-01-10T16:51:47 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather │ 0.3.2 │ 2023-04-20T20:16:19 │ No │
│ │ station. │ │ │ │
│ 📂 aprsd-wxnow-plugin │ APRSD Plugin for getting the closest wx reports to last │ 0.2.0 │ 2023-10-08T01:27:29 │ Yes │
│ │ beacon │ │ │ │
└──────────────────────────────┴──────────────────────────────────────────────────────────────┴────────────┴─────────────────────┴────────────┘
🐍 APRSD Installed 3rd party Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ aprsd-stock-plugin │ YahooStockQuote │ 0.1.3 │ RegexCommand │ aprsd_stock_plugin.stock.YahooStockQuote │
│ aprsd-wxnow-plugin │ WXNowPlugin │ 0.2.0 │ RegexCommand │ aprsd_wxnow_plugin.conf.opts.WXNowPlugin │
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
#### Current list extensions
└─> aprsd list-extensions
Pypi.org APRSD Installable Extension Packages
Install any of the following extensions by running
'pip install <Plugin Package Name>'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Extension Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-admin-extension │ Administration extension for the Ham radio APRSD Server │ 1.0.1 │ 2025-01-06T21:57:24 │ Yes │
│ 📂 aprsd-irc-extension │ An Extension to Ham radio APRSD Daemon to act like an irc server │ 0.0.5 │ 2024-04-09T11:28:47 │ No │
│ │ for APRS │ │ │ │
└──────────────────────────┴─────────────────────────────────────────────────────────────────────┴─────────┴─────────────────────┴────────────┘
### send-message
This command is typically used for development to send another aprsd
instance test messages
└─[$] > aprsd send-message -h
Usage: aprsd send-message [OPTIONS] TOCALLSIGN COMMAND...
Send a message to a callsign via APRS_IS.
Options:
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
The log level to use for aprsd.log
[default: INFO]
-c, --config TEXT The aprsd config file to use for options.
[default:
/Users/i530566/.config/aprsd/aprsd.yml]
--quiet Don't log to stdout
--aprs-login TEXT What callsign to send the message from.
[env var: APRS_LOGIN]
--aprs-password TEXT the APRS-IS password for APRS_LOGIN [env
var: APRS_PASSWORD]
-n, --no-ack Don't wait for an ack, just sent it to APRS-
IS and bail. [default: False]
-w, --wait-response Wait for a response to the message?
[default: False]
--raw TEXT Send a raw message. Implies --no-ack
-h, --help Show this message and exit.
### Development
- `git clone git@github.com:craigerl/aprsd.git`
- `cd aprsd`
- `make`
#### Workflow
While working aprsd, The workflow is as follows:
- Checkout a new branch to work on by running
`git checkout -b mybranch`
- Make your changes to the code
- Run Tox with the following options:
- `tox -epep8`
- `tox -efmt`
- `tox -p`
- Commit your changes. This will run the pre-commit hooks which does
checks too
`git commit`
- Once you are done with all of your commits, then push up the branch
to github with:
`git push -u origin mybranch`
- Create a pull request from your branch so github tests can run and
we can do a code review.
#### Release
To do release to pypi:
- Tag release with:
`git tag -v1.XX -m "New release"`
- Push release tag:
`git push origin master --tags`
- Do a test build and verify build is valid by running:
`make build`
- Once twine is happy, upload release to pypi:
`make upload`
#### Building your own APRSD plugins
APRSD plugins are the mechanism by which APRSD can respond to APRS
Messages. The plugins are loaded at server startup and can also be
loaded at listen startup. When a packet is received by APRSD, it is
passed to each of the plugins in the order they were registered in the
config file. The plugins can then decide what to do with the packet.
When a plugin is called, it is passed a APRSD Packet object. The plugin
can then do something with the packet and return a reply message if
desired. If a plugin does not want to reply to the packet, it can just
return None. When a plugin does return a reply message, APRSD will send
the reply message to the appropriate destination.
For example, when a \'ping\' message is received, the PingPlugin will
return a reply message of \'pong\'. When APRSD receives the \'pong\'
message, it will be sent back to the original caller of the ping
message.
APRSD plugins are simply python packages that can be installed from
pypi.org. They are installed into the aprsd virtualenv and can be
imported by APRSD at runtime. The plugins are registered in the config
file and loaded at startup of the aprsd server command or the aprsd
listen command.
#### Overview
You can build your own plugins by following the instructions in the
[Building your own APRSD plugins](#building-your-own-aprsd-plugins)
section.
Plugins are called by APRSD when packe
### Docker Container
### Building
There are 2 versions of the container Dockerfile that can be used. The
main Dockerfile, which is for building the official release container
based off of the pip install version of aprsd and the Dockerfile-dev,
which is used for building a container based off of a git branch of the
repo.
### Official Build
`docker build -t hemna6969/aprsd:latest .`
### Development Build
`docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .`
### Running the container
There is a `docker-compose.yml` file in the `docker/` directory that can
be used to run your container. To provide the container an `aprsd.conf`
configuration file, change your `docker-compose.yml` as shown below:
volumes:
- $HOME/.config/aprsd:/config
To install plugins at container start time, pass in a list of
comma-separated list of plugins on PyPI using the `APRSD_PLUGINS`
environment variable in the `docker-compose.yml` file. Note that version
constraints may also be provided. For example:
environment:
- APRSD_PLUGINS=aprsd-slack-plugin>=1.0.2,aprsd-twitter-plugin
### Activity
![Alt](https://repobeats.axiom.co/api/embed/8b96657861770a15f0b851a5eebafb34d0e0b3d3.svg "Repobeats analytics image")
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=craigerl/aprsd&type=Date)](https://star-history.com/#craigerl/aprsd&Date)

View File

@ -1,502 +0,0 @@
===============================================
APRSD - Ham radio APRS-IS Message plugin server
===============================================
KM6LYW and WB4BOR
____________________
|pypi| |pytest| |versions| |slack| |issues| |commit| |imports| |down|
`APRSD <http://github.com/craigerl/aprsd>`_ is a Ham radio `APRS <http://aprs.org>`_ message command gateway built on python.
Table of Contents
=================
1. `What is APRSD <#what-is-aprsd>`_
2. `APRSD Overview Diagram <#aprsd-overview-diagram>`_
3. `Typical Use Case <#typical-use-case>`_
4. `Installation <#installation>`_
5. `Example Usage <#example-usage>`_
6. `Help <#help>`_
7. `Commands <#commands>`_
- `Configuration <#configuration>`_
- `Server <#server>`_
- `Current List of Built-in Plugins <#current-list-of-built-in-plugins>`_
- `Pypi.org APRSD Installable Plugin Packages <#pypiorg-aprsd-installable-plugin-packages>`_
- `🐍 APRSD Installed 3rd Party Plugins <#aprsd-installed-3rd-party-plugins>`_
- `Send Message <#send-message>`_
- `Send Email (Radio to SMTP Server) <#send-email-radio-to-smtp-server>`_
- `Receive Email (IMAP Server to Radio) <#receive-email-imap-server-to-radio>`_
- `Location <#location>`_
- `Web Admin Interface <#web-admin-interface>`_
8. `Development <#development>`_
- `Building Your Own APRSD Plugins <#building-your-own-aprsd-plugins>`_
9. `Workflow <#workflow>`_
10. `Release <#release>`_
11. `Docker Container <#docker-container>`_
- `Building <#building-1>`_
- `Official Build <#official-build>`_
- `Development Build <#development-build>`_
- `Running the Container <#running-the-container>`_
What is APRSD
=============
APRSD is a python application for interacting with the APRS network and providing
APRS services for HAM radio operators.
APRSD currently has 4 main commands to use.
* server - Connect to APRS and listen/respond to APRS messages
* webchat - web based chat program over APRS
* send-message - Send a message to a callsign via APRS_IS.
* listen - Listen to packets on the APRS-IS Network based on FILTER.
Each of those commands can connect to the APRS-IS network if internet connectivity
is available. If internet is not available, then APRS can be configured to talk
to a TCP KISS TNC for radio connectivity.
Please `read the docs`_ to learn more!
APRSD Overview Diagram
======================
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
Typical use case
================
APRSD's typical use case is that of providing an APRS wide service to all HAM
radio operators. For example the callsign 'REPEAT' on the APRS network is actually
an instance of APRSD that can provide a list of HAM repeaters in the area of the
callsign that sent the message.
Ham radio operator using an APRS enabled HAM radio sends a message to check
the weather. An APRS message is sent, and then picked up by APRSD. The
APRS packet is decoded, and the message is sent through the list of plugins
for processing. For example, the WeatherPlugin picks up the message, fetches the weather
for the area around the user who sent the request, and then responds with
the weather conditions in that area. Also includes a watch list of HAM
callsigns to look out for. The watch list can notify you when a HAM callsign
in the list is seen and now available to message on the APRS network.
Installation
=============
To install ``aprsd``, use Pip:
``pip install aprsd``
Example usage
==============
``aprsd -h``
Help
====
::
└─> aprsd -h
Usage: aprsd [OPTIONS] COMMAND [ARGS]...
Options:
--version Show the version and exit.
-h, --help Show this message and exit.
Commands:
check-version Check this version against the latest in pypi.org.
completion Show the shell completion code
dev Development type subcommands
fetch-stats Fetch stats from a APRSD admin web interface.
healthcheck Check the health of the running aprsd server.
list-extensions List the built in plugins available to APRSD.
list-plugins List the built in plugins available to APRSD.
listen Listen to packets on the APRS-IS Network based on FILTER.
sample-config Generate a sample Config file from aprsd and all...
send-message Send a message to a callsign via APRS_IS.
server Start the aprsd server gateway process.
version Show the APRSD version.
webchat Web based HAM Radio chat program!
Commands
========
Configuration
=============
This command outputs a sample config yml formatted block that you can edit
and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.config/aprsd/aprsd.yml``
``aprsd sample-config``
::
└─> aprsd sample-config
...
server
======
This is the main server command that will listen to APRS-IS servers and
look for incomming commands to the callsign configured in the config file
::
└─[$] > aprsd server --help
Usage: aprsd server [OPTIONS]
Start the aprsd server gateway process.
Options:
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
The log level to use for aprsd.log
[default: INFO]
-c, --config TEXT The aprsd config file to use for options.
[default:
/Users/i530566/.config/aprsd/aprsd.yml]
--quiet Don't log to stdout
-f, --flush Flush out all old aged messages on disk.
[default: False]
-h, --help Show this message and exit.
└─> aprsd server
Load config
12/07/2021 03:16:17 PM MainThread INFO APRSD is up to date server.py:51
12/07/2021 03:16:17 PM MainThread INFO APRSD Started version: 2.5.6 server.py:52
12/07/2021 03:16:17 PM MainThread INFO Using CONFIG values: server.py:55
12/07/2021 03:16:17 PM MainThread INFO ham.callsign = WB4BOR server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.login = WB4BOR-12 server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.password = XXXXXXXXXXXXXXXXXXX server.py:58
12/07/2021 03:16:17 PM MainThread INFO aprs.host = noam.aprs2.net server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.port = 14580 server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.logfile = /tmp/aprsd.log server.py:60
Current list of built-in plugins
--------------------------------
::
└─> aprsd list-plugins
🐍 APRSD Built-in Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
Pypi.org APRSD Installable Plugin Packages
Install any of the following plugins with 'pip install <Plugin Package Name>'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
🐍 APRSD Installed 3rd party Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
send-message
============
This command is typically used for development to send another aprsd instance
test messages
::
└─[$] > aprsd send-message -h
Usage: aprsd send-message [OPTIONS] TOCALLSIGN COMMAND...
Send a message to a callsign via APRS_IS.
Options:
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
The log level to use for aprsd.log
[default: INFO]
-c, --config TEXT The aprsd config file to use for options.
[default:
/Users/i530566/.config/aprsd/aprsd.yml]
--quiet Don't log to stdout
--aprs-login TEXT What callsign to send the message from.
[env var: APRS_LOGIN]
--aprs-password TEXT the APRS-IS password for APRS_LOGIN [env
var: APRS_PASSWORD]
-n, --no-ack Don't wait for an ack, just sent it to APRS-
IS and bail. [default: False]
-w, --wait-response Wait for a response to the message?
[default: False]
--raw TEXT Send a raw message. Implies --no-ack
-h, --help Show this message and exit.
SEND EMAIL (radio to smtp server)
=================================
::
Received message______________
Raw : KM6XXX>APY400,WIDE1-1,qAO,KM6XXX-1::KM6XXX-9 :-user@host.com test new shortcuts global, radio to pc{29
From : KM6XXX
Message : -user@host.com test new shortcuts global, radio to pc
Msg number : 29
Sending Email_________________
To : user@host.com
Subject : KM6XXX
Body : test new shortcuts global, radio to pc
Sending ack __________________ Tx(3)
Raw : KM6XXX-9>APRS::KM6XXX :ack29
To : KM6XXX
Ack number : 29
RECEIVE EMAIL (imap server to radio)
====================================
::
Sending message_______________ 6(Tx3)
Raw : KM6XXX-9>APRS::KM6XXX :-somebody@gmail.com email from internet to radio{6
To : KM6XXX
Message : -somebody@gmail.com email from internet to radio
Received message______________
Raw : KM6XXX>APY400,WIDE1-1,qAO,KM6XXX-1::KM6XXX-9 :ack6
From : KM6XXX
Message : ack6
Msg number : 0
LOCATION
========
::
Received Message _______________
Raw : KM6XXX-6>APRS,TCPIP*,qAC,T2CAEAST::KM6XXX-14:location{2
From : KM6XXX-6
Message : location
Msg number : 2
Received Message _______________ Complete
Sending Message _______________
Raw : KM6XXX-14>APRS::KM6XXX-6 :KM6XXX-6: 8 Miles E Auburn CA 0' 0,-120.93584 1873.7h ago{2
To : KM6XXX-6
Message : KM6XXX-6: 8 Miles E Auburn CA 0' 0,-120.93584 1873.7h ago
Msg number : 2
Sending Message _______________ Complete
Sending ack _______________
Raw : KM6XXX-14>APRS::KM6XXX-6 :ack2
To : KM6XXX-6
Ack : 2
Sending ack _______________ Complete
AND... ping, fortune, time.....
Web Admin Interface
===================
APRSD has a web admin interface that allows you to view the status of the running APRSD server instance.
The web admin interface shows graphs of packet counts, packet types, number of threads running, the latest
packets sent and received, and the status of each of the plugins that are loaded. You can also view the logfile
and view the raw APRSD configuration file.
To start the web admin interface, You have to install gunicorn in your virtualenv that already has aprsd installed.
::
source <path to APRSD's virtualenv>/bin/activate
aprsd admin --loglevel INFO
The web admin interface will be running on port 8080 on the local machine. http://localhost:8080
Development
===========
* ``git clone git@github.com:craigerl/aprsd.git``
* ``cd aprsd``
* ``make``
Workflow
--------
While working aprsd, The workflow is as follows:
* Checkout a new branch to work on by running
``git checkout -b mybranch``
* Make your changes to the code
* Run Tox with the following options:
- ``tox -epep8``
- ``tox -efmt``
- ``tox -p``
* Commit your changes. This will run the pre-commit hooks which does checks too
``git commit``
* Once you are done with all of your commits, then push up the branch to
github with:
``git push -u origin mybranch``
* Create a pull request from your branch so github tests can run and we can do
a code review.
Release
-------
To do release to pypi:
* Tag release with:
``git tag -v1.XX -m "New release"``
* Push release tag:
``git push origin master --tags``
* Do a test build and verify build is valid by running:
``make build``
* Once twine is happy, upload release to pypi:
``make upload``
Building your own APRSD plugins
-------------------------------
APRSD plugins are the mechanism by which APRSD can respond to APRS Messages. The plugins are loaded at server startup
and can also be loaded at listen startup. When a packet is received by APRSD, it is passed to each of the plugins
in the order they were registered in the config file. The plugins can then decide what to do with the packet.
When a plugin is called, it is passed a APRSD Packet object. The plugin can then do something with the packet and
return a reply message if desired. If a plugin does not want to reply to the packet, it can just return None.
When a plugin does return a reply message, APRSD will send the reply message to the appropriate destination.
For example, when a 'ping' message is received, the PingPlugin will return a reply message of 'pong'. When APRSD
receives the 'pong' message, it will be sent back to the original caller of the ping message.
APRSD plugins are simply python packages that can be installed from pypi.org. They are installed into the
aprsd virtualenv and can be imported by APRSD at runtime. The plugins are registered in the config file and loaded
at startup of the aprsd server command or the aprsd listen command.
Overview
--------
You can build your own plugins by following the instructions in the `Building your own APRSD plugins`_ section.
Plugins are called by APRSD when packe
Docker Container
================
Building
========
There are 2 versions of the container Dockerfile that can be used.
The main Dockerfile, which is for building the official release container
based off of the pip install version of aprsd and the Dockerfile-dev,
which is used for building a container based off of a git branch of
the repo.
Official Build
==============
``docker build -t hemna6969/aprsd:latest .``
Development Build
=================
``docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .``
Running the container
=====================
There is a ``docker-compose.yml`` file in the ``docker/`` directory
that can be used to run your container. To provide the container
an ``aprsd.conf`` configuration file, change your
``docker-compose.yml`` as shown below:
::
volumes:
- $HOME/.config/aprsd:/config
To install plugins at container start time, pass in a list of
comma-separated list of plugins on PyPI using the ``APRSD_PLUGINS``
environment variable in the ``docker-compose.yml`` file. Note that
version constraints may also be provided. For example:
::
environment:
- APRSD_PLUGINS=aprsd-slack-plugin>=1.0.2,aprsd-twitter-plugin
.. badges
.. |pypi| image:: https://badge.fury.io/py/aprsd.svg
:target: https://badge.fury.io/py/aprsd
.. |pytest| image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg
:target: https://github.com/craigerl/aprsd/actions
.. |versions| image:: https://img.shields.io/pypi/pyversions/aprsd.svg
:target: https://pypi.org/pypi/aprsd
.. |slack| image:: https://img.shields.io/badge/slack-@hemna/aprsd-blue.svg?logo=slack
:target: https://hemna.slack.com/app_redirect?channel=C01KQSCP5RP
.. |imports| image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336
:target: https://timothycrosley.github.io/isort/
.. |issues| image:: https://img.shields.io/github/issues/craigerl/aprsd
.. |commit| image:: https://img.shields.io/github/last-commit/craigerl/aprsd
.. |down| image:: https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads
:target: https://pepy.tech/project/aprsd
.. links
.. _read the docs:
https://aprsd.readthedocs.io

View File

@ -11,7 +11,7 @@ from aprsd.packets import core
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
@ -24,12 +24,12 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
path = []
def __init__(self):
LOG.info("Starting APRSDFakeClient client.")
self.path = ["WIDE1-1", "WIDE2-1"]
LOG.info('Starting APRSDFakeClient client.')
self.path = ['WIDE1-1', 'WIDE2-1']
def stop(self):
self.thread_stop = True
LOG.info("Shutdown APRSDFakeClient client.")
LOG.info('Shutdown APRSDFakeClient client.')
def is_alive(self):
"""If the connection is alive or not."""
@ -38,35 +38,31 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
@wrapt.synchronized(lock)
def send(self, packet: core.Packet):
"""Send an APRS Message object."""
LOG.info(f"Sending packet: {packet}")
LOG.info(f'Sending packet: {packet}')
payload = None
if isinstance(packet, core.Packet):
packet.prepare()
payload = packet.payload.encode("US-ASCII")
if packet.path:
packet.path
else:
self.path
payload = packet.payload.encode('US-ASCII')
else:
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
msg_payload = f'{packet.raw}{{{str(packet.msgNo)}'
payload = (
":{:<9}:{}".format(
':{:<9}:{}'.format(
packet.to_call,
msg_payload,
)
).encode("US-ASCII")
).encode('US-ASCII')
LOG.debug(
f"FAKE::Send '{payload}' TO '{packet.to_call}' From "
f"'{packet.from_call}' with PATH \"{self.path}\"",
f'\'{packet.from_call}\' with PATH "{self.path}"',
)
def consumer(self, callback, blocking=False, immortal=False, raw=False):
LOG.debug("Start non blocking FAKE consumer")
LOG.debug('Start non blocking FAKE consumer')
# Generate packets here?
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
raw = 'GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW'
pkt_raw = aprslib.parse(raw)
pkt = core.factory(pkt_raw)
callback(packet=pkt)
LOG.debug(f"END blocking FAKE consumer {self}")
LOG.debug(f'END blocking FAKE consumer {self}')
time.sleep(8)

View File

@ -16,11 +16,11 @@ from aprsd.main import cli
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
LOG = logging.getLogger('APRSD')
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
@cli.group(help="Development type subcommands", context_settings=CONTEXT_SETTINGS)
@cli.group(help='Development type subcommands', context_settings=CONTEXT_SETTINGS)
@click.pass_context
def dev(ctx):
pass
@ -29,37 +29,37 @@ def dev(ctx):
@dev.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"--aprs-login",
envvar="APRS_LOGIN",
'--aprs-login',
envvar='APRS_LOGIN',
show_envvar=True,
help="What callsign to send the message from.",
help='What callsign to send the message from.',
)
@click.option(
"-p",
"--plugin",
"plugin_path",
'-p',
'--plugin',
'plugin_path',
show_default=True,
default=None,
help="The plugin to run. Ex: aprsd.plugins.ping.PingPlugin",
help='The plugin to run. Ex: aprsd.plugins.ping.PingPlugin',
)
@click.option(
"-a",
"--all",
"load_all",
'-a',
'--all',
'load_all',
show_default=True,
is_flag=True,
default=False,
help="Load all the plugins in config?",
help='Load all the plugins in config?',
)
@click.option(
"-n",
"--num",
"number",
'-n',
'--num',
'number',
show_default=True,
default=1,
help="Number of times to call the plugin",
help='Number of times to call the plugin',
)
@click.argument("message", nargs=-1, required=True)
@click.argument('message', nargs=-1, required=True)
@click.pass_context
@cli_helper.process_standard_options
def test_plugin(
@ -76,7 +76,7 @@ def test_plugin(
if not aprs_login:
if CONF.aprs_network.login == conf.client.DEFAULT_LOGIN:
click.echo("Must set --aprs_login or APRS_LOGIN")
click.echo('Must set --aprs_login or APRS_LOGIN')
ctx.exit(-1)
return
else:
@ -86,16 +86,16 @@ def test_plugin(
if not plugin_path:
click.echo(ctx.get_help())
click.echo("")
click.echo("Failed to provide -p option to test a plugin")
click.echo('')
click.echo('Failed to provide -p option to test a plugin')
ctx.exit(-1)
return
if type(message) is tuple:
message = " ".join(message)
message = ' '.join(message)
if CONF.trace_enabled:
trace.setup_tracing(["method", "api"])
trace.setup_tracing(['method', 'api'])
base.APRSClient()
@ -105,13 +105,13 @@ def test_plugin(
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase)
if not obj:
click.echo(ctx.get_help())
click.echo("")
click.echo('')
ctx.fail(f"Failed to create object from plugin path '{plugin_path}'")
ctx.exit()
# Register the plugin they wanted tested.
LOG.info(
"Testing plugin {} Version {}".format(
'Testing plugin {} Version {}'.format(
obj.__class__,
obj.version,
),
@ -126,7 +126,7 @@ def test_plugin(
)
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
for x in range(number):
for _ in range(number):
replies = pm.run(packet)
# Plugin might have threads, so lets stop them so we can exit.
# obj.stop_threads()
@ -147,17 +147,12 @@ def test_plugin(
elif isinstance(reply, packets.Packet):
# We have a message based object.
LOG.info(reply)
else:
# A plugin can return a null message flag which signals
# us that they processed the message correctly, but have
# nothing to reply with, so we avoid replying with a
# usage string
if reply is not packets.NULL_MESSAGE:
LOG.info(
packets.MessagePacket(
from_call=CONF.callsign,
to_call=fromcall,
message_text=reply,
),
)
elif reply is not packets.NULL_MESSAGE:
LOG.info(
packets.MessagePacket(
from_call=CONF.callsign,
to_call=fromcall,
message_text=reply,
),
)
pm.stop()

View File

@ -15,30 +15,30 @@ from aprsd.threads.stats import StatsStore
# setup the global logger
# log.basicConfig(level=log.DEBUG) # level=10
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
CONF = cfg.CONF
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"--host",
'--host',
type=str,
default=None,
help="IP address of the remote aprsd admin web ui fetch stats from.",
help='IP address of the remote aprsd admin web ui fetch stats from.',
)
@click.option(
"--port",
'--port',
type=int,
default=None,
help="Port of the remote aprsd web admin interface to fetch stats from.",
help='Port of the remote aprsd web admin interface to fetch stats from.',
)
@click.pass_context
@cli_helper.process_standard_options
def fetch_stats(ctx, host, port):
"""Fetch stats from a APRSD admin web interface."""
console = Console()
console.print(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
console.print(f'APRSD Fetch-Stats started version: {aprsd.__version__}')
CONF.log_opt_values(LOG, logging.DEBUG)
if not host:
@ -46,114 +46,110 @@ def fetch_stats(ctx, host, port):
if not port:
port = CONF.admin.web_port
msg = f"Fetching stats from {host}:{port}"
msg = f'Fetching stats from {host}:{port}'
console.print(msg)
with console.status(msg):
response = requests.get(f"http://{host}:{port}/stats", timeout=120)
response = requests.get(f'http://{host}:{port}/stats', timeout=120)
if not response:
console.print(
f"Failed to fetch stats from {host}:{port}?",
style="bold red",
f'Failed to fetch stats from {host}:{port}?',
style='bold red',
)
return
stats = response.json()
if not stats:
console.print(
f"Failed to fetch stats from aprsd admin ui at {host}:{port}",
style="bold red",
f'Failed to fetch stats from aprsd admin ui at {host}:{port}',
style='bold red',
)
return
aprsd_title = (
"APRSD "
f"[bold cyan]v{stats['APRSDStats']['version']}[/] "
f"Callsign [bold green]{stats['APRSDStats']['callsign']}[/] "
f"Uptime [bold yellow]{stats['APRSDStats']['uptime']}[/]"
'APRSD '
f'[bold cyan]v{stats["APRSDStats"]["version"]}[/] '
f'Callsign [bold green]{stats["APRSDStats"]["callsign"]}[/] '
f'Uptime [bold yellow]{stats["APRSDStats"]["uptime"]}[/]'
)
console.rule(f"Stats from {host}:{port}")
console.print("\n\n")
console.rule(f'Stats from {host}:{port}')
console.print('\n\n')
console.rule(aprsd_title)
# Show the connection to APRS
# It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP
if "aprs-is" in stats:
title = f"APRS-IS Connection {stats['APRSClientStats']['server_string']}"
if 'aprs-is' in stats:
title = f'APRS-IS Connection {stats["APRSClientStats"]["server_string"]}'
table = Table(title=title)
table.add_column("Key")
table.add_column("Value")
for key, value in stats["APRSClientStats"].items():
table.add_column('Key')
table.add_column('Value')
for key, value in stats['APRSClientStats'].items():
table.add_row(key, value)
console.print(table)
threads_table = Table(title="Threads")
threads_table.add_column("Name")
threads_table.add_column("Alive?")
for name, alive in stats["APRSDThreadList"].items():
threads_table = Table(title='Threads')
threads_table.add_column('Name')
threads_table.add_column('Alive?')
for name, alive in stats['APRSDThreadList'].items():
threads_table.add_row(name, str(alive))
console.print(threads_table)
packet_totals = Table(title="Packet Totals")
packet_totals.add_column("Key")
packet_totals.add_column("Value")
packet_totals.add_row("Total Received", str(stats["PacketList"]["rx"]))
packet_totals.add_row("Total Sent", str(stats["PacketList"]["tx"]))
packet_totals = Table(title='Packet Totals')
packet_totals.add_column('Key')
packet_totals.add_column('Value')
packet_totals.add_row('Total Received', str(stats['PacketList']['rx']))
packet_totals.add_row('Total Sent', str(stats['PacketList']['tx']))
console.print(packet_totals)
# Show each of the packet types
packets_table = Table(title="Packets By Type")
packets_table.add_column("Packet Type")
packets_table.add_column("TX")
packets_table.add_column("RX")
for key, value in stats["PacketList"]["packets"].items():
packets_table.add_row(key, str(value["tx"]), str(value["rx"]))
packets_table = Table(title='Packets By Type')
packets_table.add_column('Packet Type')
packets_table.add_column('TX')
packets_table.add_column('RX')
for key, value in stats['PacketList']['packets'].items():
packets_table.add_row(key, str(value['tx']), str(value['rx']))
console.print(packets_table)
if "plugins" in stats:
count = len(stats["PluginManager"])
plugins_table = Table(title=f"Plugins ({count})")
plugins_table.add_column("Plugin")
plugins_table.add_column("Enabled")
plugins_table.add_column("Version")
plugins_table.add_column("TX")
plugins_table.add_column("RX")
plugins = stats["PluginManager"]
for key, value in plugins.items():
if 'plugins' in stats:
count = len(stats['PluginManager'])
plugins_table = Table(title=f'Plugins ({count})')
plugins_table.add_column('Plugin')
plugins_table.add_column('Enabled')
plugins_table.add_column('Version')
plugins_table.add_column('TX')
plugins_table.add_column('RX')
plugins = stats['PluginManager']
for key, _ in plugins.items():
plugins_table.add_row(
key,
str(plugins[key]["enabled"]),
plugins[key]["version"],
str(plugins[key]["tx"]),
str(plugins[key]["rx"]),
str(plugins[key]['enabled']),
plugins[key]['version'],
str(plugins[key]['tx']),
str(plugins[key]['rx']),
)
console.print(plugins_table)
seen_list = stats.get("SeenList")
if seen_list:
if seen_list := stats.get('SeenList'):
count = len(seen_list)
seen_table = Table(title=f"Seen List ({count})")
seen_table.add_column("Callsign")
seen_table.add_column("Message Count")
seen_table.add_column("Last Heard")
seen_table = Table(title=f'Seen List ({count})')
seen_table.add_column('Callsign')
seen_table.add_column('Message Count')
seen_table.add_column('Last Heard')
for key, value in seen_list.items():
seen_table.add_row(key, str(value["count"]), value["last"])
seen_table.add_row(key, str(value['count']), value['last'])
console.print(seen_table)
watch_list = stats.get("WatchList")
if watch_list:
if watch_list := stats.get('WatchList'):
count = len(watch_list)
watch_table = Table(title=f"Watch List ({count})")
watch_table.add_column("Callsign")
watch_table.add_column("Last Heard")
watch_table = Table(title=f'Watch List ({count})')
watch_table.add_column('Callsign')
watch_table.add_column('Last Heard')
for key, value in watch_list.items():
watch_table.add_row(key, value["last"])
watch_table.add_row(key, value['last'])
console.print(watch_table)
@ -161,27 +157,27 @@ def fetch_stats(ctx, host, port):
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"--raw",
'--raw',
is_flag=True,
default=False,
help="Dump raw stats instead of formatted output.",
help='Dump raw stats instead of formatted output.',
)
@click.option(
"--show-section",
default=["All"],
help="Show specific sections of the stats. "
" Choices: All, APRSDStats, APRSDThreadList, APRSClientStats,"
" PacketList, SeenList, WatchList",
'--show-section',
default=['All'],
help='Show specific sections of the stats. '
' Choices: All, APRSDStats, APRSDThreadList, APRSClientStats,'
' PacketList, SeenList, WatchList',
multiple=True,
type=click.Choice(
[
"All",
"APRSDStats",
"APRSDThreadList",
"APRSClientStats",
"PacketList",
"SeenList",
"WatchList",
'All',
'APRSDStats',
'APRSDThreadList',
'APRSClientStats',
'PacketList',
'SeenList',
'WatchList',
],
case_sensitive=False,
),
@ -191,122 +187,122 @@ def fetch_stats(ctx, host, port):
def dump_stats(ctx, raw, show_section):
"""Dump the current stats from the running APRSD instance."""
console = Console()
console.print(f"APRSD Dump-Stats started version: {aprsd.__version__}")
console.print(f'APRSD Dump-Stats started version: {aprsd.__version__}')
with console.status("Dumping stats"):
with console.status('Dumping stats'):
ss = StatsStore()
ss.load()
stats = ss.data
if raw:
if "All" in show_section:
if 'All' in show_section:
console.print(stats)
return
else:
for section in show_section:
console.print(f"Dumping {section} section:")
console.print(f'Dumping {section} section:')
console.print(stats[section])
return
t = Table(title="APRSD Stats")
t.add_column("Key")
t.add_column("Value")
for key, value in stats["APRSDStats"].items():
t = Table(title='APRSD Stats')
t.add_column('Key')
t.add_column('Value')
for key, value in stats['APRSDStats'].items():
t.add_row(key, str(value))
if "All" in show_section or "APRSDStats" in show_section:
if 'All' in show_section or 'APRSDStats' in show_section:
console.print(t)
# Show the thread list
t = Table(title="Thread List")
t.add_column("Name")
t.add_column("Class")
t.add_column("Alive?")
t.add_column("Loop Count")
t.add_column("Age")
for name, value in stats["APRSDThreadList"].items():
t = Table(title='Thread List')
t.add_column('Name')
t.add_column('Class')
t.add_column('Alive?')
t.add_column('Loop Count')
t.add_column('Age')
for name, value in stats['APRSDThreadList'].items():
t.add_row(
name,
value["class"],
str(value["alive"]),
str(value["loop_count"]),
str(value["age"]),
value['class'],
str(value['alive']),
str(value['loop_count']),
str(value['age']),
)
if "All" in show_section or "APRSDThreadList" in show_section:
if 'All' in show_section or 'APRSDThreadList' in show_section:
console.print(t)
# Show the plugins
t = Table(title="Plugin List")
t.add_column("Name")
t.add_column("Enabled")
t.add_column("Version")
t.add_column("TX")
t.add_column("RX")
for name, value in stats["PluginManager"].items():
t = Table(title='Plugin List')
t.add_column('Name')
t.add_column('Enabled')
t.add_column('Version')
t.add_column('TX')
t.add_column('RX')
for name, value in stats['PluginManager'].items():
t.add_row(
name,
str(value["enabled"]),
value["version"],
str(value["tx"]),
str(value["rx"]),
str(value['enabled']),
value['version'],
str(value['tx']),
str(value['rx']),
)
if "All" in show_section or "PluginManager" in show_section:
if 'All' in show_section or 'PluginManager' in show_section:
console.print(t)
# Now show the client stats
t = Table(title="Client Stats")
t.add_column("Key")
t.add_column("Value")
for key, value in stats["APRSClientStats"].items():
t = Table(title='Client Stats')
t.add_column('Key')
t.add_column('Value')
for key, value in stats['APRSClientStats'].items():
t.add_row(key, str(value))
if "All" in show_section or "APRSClientStats" in show_section:
if 'All' in show_section or 'APRSClientStats' in show_section:
console.print(t)
# now show the packet list
packet_list = stats.get("PacketList")
t = Table(title="Packet List")
t.add_column("Key")
t.add_column("Value")
t.add_row("Total Received", str(packet_list["rx"]))
t.add_row("Total Sent", str(packet_list["tx"]))
packet_list = stats.get('PacketList')
t = Table(title='Packet List')
t.add_column('Key')
t.add_column('Value')
t.add_row('Total Received', str(packet_list['rx']))
t.add_row('Total Sent', str(packet_list['tx']))
if "All" in show_section or "PacketList" in show_section:
if 'All' in show_section or 'PacketList' in show_section:
console.print(t)
# now show the seen list
seen_list = stats.get("SeenList")
seen_list = stats.get('SeenList')
sorted_seen_list = sorted(
seen_list.items(),
)
t = Table(title="Seen List")
t.add_column("Callsign")
t.add_column("Message Count")
t.add_column("Last Heard")
t = Table(title='Seen List')
t.add_column('Callsign')
t.add_column('Message Count')
t.add_column('Last Heard')
for key, value in sorted_seen_list:
t.add_row(
key,
str(value["count"]),
str(value["last"]),
str(value['count']),
str(value['last']),
)
if "All" in show_section or "SeenList" in show_section:
if 'All' in show_section or 'SeenList' in show_section:
console.print(t)
# now show the watch list
watch_list = stats.get("WatchList")
watch_list = stats.get('WatchList')
sorted_watch_list = sorted(
watch_list.items(),
)
t = Table(title="Watch List")
t.add_column("Callsign")
t.add_column("Last Heard")
t = Table(title='Watch List')
t.add_column('Callsign')
t.add_column('Last Heard')
for key, value in sorted_watch_list:
t.add_row(
key,
str(value["last"]),
str(value['last']),
)
if "All" in show_section or "WatchList" in show_section:
if 'All' in show_section or 'WatchList' in show_section:
console.print(t)

View File

@ -13,9 +13,9 @@ from oslo_config import cfg
from rich.console import Console
import aprsd
from aprsd import (
from aprsd import ( # noqa: F401
cli_helper,
conf, # noqa
conf,
)
# local imports here

View File

@ -4,14 +4,11 @@ import inspect
import logging
import os
import pkgutil
import re
import sys
from traceback import print_tb
from urllib.parse import urljoin
import click
import requests
from bs4 import BeautifulSoup
from rich.console import Console
from rich.table import Table
from rich.text import Text
@ -22,12 +19,12 @@ from aprsd import plugin as aprsd_plugin
from aprsd.main import cli
from aprsd.plugins import fortune, notify, ping, time, version, weather
LOG = logging.getLogger("APRSD")
PYPI_URL = "https://pypi.org/search/"
LOG = logging.getLogger('APRSD')
PYPI_URL = 'https://pypi.org/search/'
def onerror(name):
print(f"Error importing module {name}")
print(f'Error importing module {name}')
type, value, traceback = sys.exc_info()
print_tb(traceback)
@ -43,19 +40,19 @@ def is_plugin(obj):
def plugin_type(obj):
for c in inspect.getmro(obj):
if issubclass(c, aprsd_plugin.APRSDRegexCommandPluginBase):
return "RegexCommand"
return 'RegexCommand'
if issubclass(c, aprsd_plugin.APRSDWatchListPluginBase):
return "WatchList"
return 'WatchList'
if issubclass(c, aprsd_plugin.APRSDPluginBase):
return "APRSDPluginBase"
return 'APRSDPluginBase'
return "Unknown"
return 'Unknown'
def walk_package(package):
return pkgutil.walk_packages(
package.__path__,
package.__name__ + ".",
package.__name__ + '.',
onerror=onerror,
)
@ -65,23 +62,23 @@ def get_module_info(package_name, module_name, module_path):
return None
dir_path = os.path.realpath(module_path)
pattern = "*.py"
pattern = '*.py'
obj_list = []
for path, _subdirs, files in os.walk(dir_path):
for name in files:
if fnmatch.fnmatch(name, pattern):
module = smuggle(f"{path}/{name}")
module = smuggle(f'{path}/{name}')
for mem_name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and is_plugin(obj):
obj_list.append(
{
"package": package_name,
"name": mem_name,
"obj": obj,
"version": obj.version,
"path": f"{'.'.join([module_name, obj.__name__])}",
'package': package_name,
'name': mem_name,
'obj': obj,
'version': obj.version,
'path': f'{".".join([module_name, obj.__name__])}',
},
)
@ -92,20 +89,18 @@ def _get_installed_aprsd_items():
# installed plugins
plugins = {}
extensions = {}
for finder, name, ispkg in pkgutil.iter_modules():
if name.startswith("aprsd_"):
print(f"Found aprsd_ module: {name}")
if ispkg:
module = importlib.import_module(name)
pkgs = walk_package(module)
for pkg in pkgs:
pkg_info = get_module_info(
module.__name__, pkg.name, module.__path__[0]
)
if "plugin" in name:
plugins[name] = pkg_info
elif "extension" in name:
extensions[name] = pkg_info
for _finder, name, ispkg in pkgutil.iter_modules():
if ispkg and name.startswith('aprsd_'):
module = importlib.import_module(name)
pkgs = walk_package(module)
for pkg in pkgs:
pkg_info = get_module_info(
module.__name__, pkg.name, module.__path__[0]
)
if 'plugin' in name:
plugins[name] = pkg_info
elif 'extension' in name:
extensions[name] = pkg_info
return plugins, extensions
@ -131,154 +126,141 @@ def show_built_in_plugins(console):
cls = entry[1]
if issubclass(cls, aprsd_plugin.APRSDPluginBase):
info = {
"name": cls.__qualname__,
"path": f"{cls.__module__}.{cls.__qualname__}",
"version": cls.version,
"docstring": cls.__doc__,
"short_desc": cls.short_description,
'name': cls.__qualname__,
'path': f'{cls.__module__}.{cls.__qualname__}',
'version': cls.version,
'docstring': cls.__doc__,
'short_desc': cls.short_description,
}
if issubclass(cls, aprsd_plugin.APRSDRegexCommandPluginBase):
info["command_regex"] = cls.command_regex
info["type"] = "RegexCommand"
info['command_regex'] = cls.command_regex
info['type'] = 'RegexCommand'
if issubclass(cls, aprsd_plugin.APRSDWatchListPluginBase):
info["type"] = "WatchList"
info['type'] = 'WatchList'
plugins.append(info)
plugins = sorted(plugins, key=lambda i: i["name"])
plugins = sorted(plugins, key=lambda i: i['name'])
table = Table(
title="[not italic]:snake:[/] [bold][magenta]APRSD Built-in Plugins [not italic]:snake:[/]",
title='[not italic]:snake:[/] [bold][magenta]APRSD Built-in Plugins [not italic]:snake:[/]',
)
table.add_column("Plugin Name", style="cyan", no_wrap=True)
table.add_column("Info", style="bold yellow")
table.add_column("Type", style="bold green")
table.add_column("Plugin Path", style="bold blue")
table.add_column('Plugin Name', style='cyan', no_wrap=True)
table.add_column('Info', style='bold yellow')
table.add_column('Type', style='bold green')
table.add_column('Plugin Path', style='bold blue')
for entry in plugins:
table.add_row(entry["name"], entry["short_desc"], entry["type"], entry["path"])
table.add_row(entry['name'], entry['short_desc'], entry['type'], entry['path'])
console.print(table)
def _get_pypi_packages():
query = "aprsd"
snippets = []
s = requests.Session()
for page in range(1, 3):
params = {"q": query, "page": page}
r = s.get(PYPI_URL, params=params)
soup = BeautifulSoup(r.text, "html.parser")
snippets += soup.select('a[class*="snippet"]')
if not hasattr(s, "start_url"):
s.start_url = r.url.rsplit("&page", maxsplit=1).pop(0)
if simple_r := requests.get(
'https://pypi.org/simple',
headers={'Accept': 'application/vnd.pypi.simple.v1+json'},
):
simple_response = simple_r.json()
else:
simple_response = {}
return snippets
key = 'aprsd'
matches = [
p['name'] for p in simple_response['projects'] if p['name'].startswith(key)
]
packages = []
for pkg in matches:
# Get info for first match
if r := requests.get(
f'https://pypi.org/pypi/{pkg}/json',
headers={'Accept': 'application/json'},
):
packages.append(r.json())
return packages
def show_pypi_plugins(installed_plugins, console):
snippets = _get_pypi_packages()
packages = _get_pypi_packages()
title = Text.assemble(
("Pypi.org APRSD Installable Plugin Packages\n\n", "bold magenta"),
("Install any of the following plugins with\n", "bold yellow"),
("'pip install ", "bold white"),
("<Plugin Package Name>'", "cyan"),
('Pypi.org APRSD Installable Plugin Packages\n\n', 'bold magenta'),
('Install any of the following plugins with\n', 'bold yellow'),
("'pip install ", 'bold white'),
("<Plugin Package Name>'", 'cyan'),
)
table = Table(title=title)
table.add_column("Plugin Package Name", style="cyan", no_wrap=True)
table.add_column("Description", style="yellow")
table.add_column("Version", style="yellow", justify="center")
table.add_column("Released", style="bold green", justify="center")
table.add_column("Installed?", style="red", justify="center")
for snippet in snippets:
link = urljoin(PYPI_URL, snippet.get("href"))
package = re.sub(
r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip()
)
version = re.sub(
r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip()
)
created = re.sub(
r"\s+", " ", snippet.select_one('span[class*="created"]').text.strip()
)
description = re.sub(
r"\s+", " ", snippet.select_one('p[class*="description"]').text.strip()
)
emoji = ":open_file_folder:"
table.add_column('Plugin Package Name', style='cyan', no_wrap=True)
table.add_column('Description', style='yellow')
table.add_column('Version', style='yellow', justify='center')
table.add_column('Released', style='bold green', justify='center')
table.add_column('Installed?', style='red', justify='center')
emoji = ':open_file_folder:'
for package in packages:
link = package['info']['package_url']
version = package['info']['version']
package_name = package['info']['name']
description = package['info']['summary']
created = package['releases'][version][0]['upload_time']
if "aprsd-" not in package or "-plugin" not in package:
if 'aprsd-' not in package_name or '-plugin' not in package_name:
continue
under = package.replace("-", "_")
if under in installed_plugins:
installed = "Yes"
else:
installed = "No"
under = package_name.replace('-', '_')
installed = 'Yes' if under in installed_plugins else 'No'
table.add_row(
f"[link={link}]{emoji}[/link] {package}",
f'[link={link}]{emoji}[/link] {package_name}',
description,
version,
created,
installed,
)
console.print("\n")
console.print('\n')
console.print(table)
def show_pypi_extensions(installed_extensions, console):
snippets = _get_pypi_packages()
packages = _get_pypi_packages()
title = Text.assemble(
("Pypi.org APRSD Installable Extension Packages\n\n", "bold magenta"),
("Install any of the following extensions by running\n", "bold yellow"),
("'pip install ", "bold white"),
("<Plugin Package Name>'", "cyan"),
('Pypi.org APRSD Installable Extension Packages\n\n', 'bold magenta'),
('Install any of the following extensions by running\n', 'bold yellow'),
("'pip install ", 'bold white'),
("<Plugin Package Name>'", 'cyan'),
)
table = Table(title=title)
table.add_column("Extension Package Name", style="cyan", no_wrap=True)
table.add_column("Description", style="yellow")
table.add_column("Version", style="yellow", justify="center")
table.add_column("Released", style="bold green", justify="center")
table.add_column("Installed?", style="red", justify="center")
for snippet in snippets:
link = urljoin(PYPI_URL, snippet.get("href"))
package = re.sub(
r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip()
)
version = re.sub(
r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip()
)
created = re.sub(
r"\s+", " ", snippet.select_one('span[class*="created"]').text.strip()
)
description = re.sub(
r"\s+", " ", snippet.select_one('p[class*="description"]').text.strip()
)
emoji = ":open_file_folder:"
table.add_column('Extension Package Name', style='cyan', no_wrap=True)
table.add_column('Description', style='yellow')
table.add_column('Version', style='yellow', justify='center')
table.add_column('Released', style='bold green', justify='center')
table.add_column('Installed?', style='red', justify='center')
emoji = ':open_file_folder:'
if "aprsd-" not in package or "-extension" not in package:
for package in packages:
link = package['info']['package_url']
version = package['info']['version']
package_name = package['info']['name']
description = package['info']['summary']
created = package['releases'][version][0]['upload_time']
if 'aprsd-' not in package_name or '-extension' not in package_name:
continue
under = package.replace("-", "_")
if under in installed_extensions:
installed = "Yes"
else:
installed = "No"
under = package_name.replace('-', '_')
installed = 'Yes' if under in installed_extensions else 'No'
table.add_row(
f"[link={link}]{emoji}[/link] {package}",
f'[link={link}]{emoji}[/link] {package_name}',
description,
version,
created,
installed,
)
console.print("\n")
console.print('\n')
console.print(table)
@ -287,24 +269,24 @@ def show_installed_plugins(installed_plugins, console):
return
table = Table(
title="[not italic]:snake:[/] [bold][magenta]APRSD Installed 3rd party Plugins [not italic]:snake:[/]",
title='[not italic]:snake:[/] [bold][magenta]APRSD Installed 3rd party Plugins [not italic]:snake:[/]',
)
table.add_column("Package Name", style=" bold white", no_wrap=True)
table.add_column("Plugin Name", style="cyan", no_wrap=True)
table.add_column("Version", style="yellow", justify="center")
table.add_column("Type", style="bold green")
table.add_column("Plugin Path", style="bold blue")
table.add_column('Package Name', style=' bold white', no_wrap=True)
table.add_column('Plugin Name', style='cyan', no_wrap=True)
table.add_column('Version', style='yellow', justify='center')
table.add_column('Type', style='bold green')
table.add_column('Plugin Path', style='bold blue')
for name in installed_plugins:
for plugin in installed_plugins[name]:
table.add_row(
name.replace("_", "-"),
plugin["name"],
plugin["version"],
plugin_type(plugin["obj"]),
plugin["path"],
name.replace('_', '-'),
plugin['name'],
plugin['version'],
plugin_type(plugin['obj']),
plugin['path'],
)
console.print("\n")
console.print('\n')
console.print(table)
@ -316,14 +298,14 @@ def list_plugins(ctx):
"""List the built in plugins available to APRSD."""
console = Console()
with console.status("Show Built-in Plugins") as status:
with console.status('Show Built-in Plugins') as status:
show_built_in_plugins(console)
status.update("Fetching pypi.org plugins")
status.update('Fetching pypi.org plugins')
installed_plugins = get_installed_plugins()
show_pypi_plugins(installed_plugins, console)
status.update("Looking for installed APRSD plugins")
status.update('Looking for installed APRSD plugins')
show_installed_plugins(installed_plugins, console)
@ -335,7 +317,9 @@ def list_extensions(ctx):
"""List the built in plugins available to APRSD."""
console = Console()
with console.status("Show APRSD Extensions") as status:
status.update("Fetching pypi.org APRSD Extensions")
with console.status('Show APRSD Extensions') as status:
status.update('Fetching pypi.org APRSD Extensions')
status.update('Looking for installed APRSD Extensions')
installed_extensions = get_installed_extensions()
show_pypi_extensions(installed_extensions, console)

View File

@ -113,6 +113,7 @@ class ListenStatsThread(APRSDThread):
stats_json = collector.Collector().collect()
stats = stats_json["PacketList"]
total_rx = stats["rx"]
packet_count = len(stats["packets"])
rx_delta = total_rx - self._last_total_rx
rate = rx_delta / 10
@ -120,7 +121,8 @@ class ListenStatsThread(APRSDThread):
LOGU.opt(colors=True).info(
f"<green>RX Rate: {rate} pps</green> "
f"<yellow>Total RX: {total_rx}</yellow> "
f"<red>RX Last 10 secs: {rx_delta}</red>",
f"<red>RX Last 10 secs: {rx_delta}</red> "
f"<white>Packets in PacketList: {packet_count}</white>",
)
self._last_total_rx = total_rx

BIN
aprsd_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -1,6 +1,6 @@
FROM python:3.11-slim AS build
FROM ghcr.io/astral-sh/uv:python3.11-alpine AS build
ARG VERSION=3.4.0
ARG VERSION=3.5.0
# pass this in as 'dev' if you want to install from github repo vs pypi
ARG INSTALL_TYPE=pypi
@ -8,7 +8,7 @@ ARG BRANCH=master
ARG BUILDX_QEMU_ENV
ENV APRSD_BRANCH=${BRANCH:-master}
ENV TZ=${TZ:-US/Eastern}
#ENV TZ=${TZ:-US/Eastern}
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
ENV APRSD_PIP_VERSION=${VERSION}
@ -23,49 +23,59 @@ ENV PIP_DEFAULT_TIMEOUT=100 \
PIP_NO_CACHE_DIR=1
RUN set -ex \
# Create a non-root user
&& addgroup --system --gid 1001 appgroup \
&& useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
&& usermod -aG sudo appuser \
#RUN set -ex \
# # Create a non-root user
# && addgroup --system --gid 1001 appgroup \
# && useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
# && usermod -aG sudo appuser
RUN apk add git
#RUN set -ex \
# Upgrade the package index and install security upgrades
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y git build-essential curl libffi-dev \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo fortune \
# && apt-get update \
# && apt-get upgrade -y \
# && apt-get install -y git build-essential curl libffi-dev \
# python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo fortune \
# Install dependencies
# Clean up
&& apt-get autoremove -y \
&& apt-get clean -y
# && apt-get autoremove -y \
# && apt-get clean -y
### Final stage
FROM build AS install
WORKDIR /app
RUN pip3 install -U pip
RUN mkdir /config
RUN chown -R appuser:appgroup /app
RUN chown -R appuser:appgroup /config
USER appuser
RUN uv venv
RUN uv pip install -U pip
RUN if [ "$INSTALL_TYPE" = "pypi" ]; then \
pip3 install aprsd==$APRSD_PIP_VERSION; \
uv pip install aprsd==$APRSD_PIP_VERSION; \
elif [ "$INSTALL_TYPE" = "github" ]; then \
git clone -b $APRSD_BRANCH https://github.com/craigerl/aprsd; \
cd /app/aprsd && pip install .; \
ls -al /app/.local/lib/python3.11/site-packages/aprsd*; \
cd /app/aprsd && uv pip install .; \
ls -al /app/.venv/lib/python3.11/site-packages/aprsd*; \
fi
RUN pip install gevent uwsgi
# RUN uv pip install gevent uwsgi
RUN echo "PATH=\$PATH:/usr/games:/app/.local/bin" >> /app/.bashrc
RUN which aprsd
RUN aprsd sample-config > /config/aprsd.conf
RUN aprsd --version
#RUN which aprsd
RUN uv run aprsd sample-config > /config/aprsd.conf
RUN uv run aprsd --version
FROM ghcr.io/astral-sh/uv:python3.11-alpine
RUN apk add fortune bash git
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
COPY --from=install /app /app
COPY --from=install /config config
ADD bin/setup.sh /app
ADD bin/admin.sh /app
ADD bin/listen.sh /app
ADD bin/healthcheck.sh /app
FROM install AS final
RUN ls -al /app
# For the web admin interface
EXPOSE 8001
@ -74,7 +84,7 @@ ENTRYPOINT ["/app/setup.sh"]
CMD ["server"]
# Set the user to run the application
USER appuser
# USER appuser
HEALTHCHECK --interval=1m --timeout=12s --start-period=30s \
CMD aprsd healthcheck --config /config/aprsd.conf --loglevel DEBUG
CMD /app/healthcheck.sh

View File

@ -1,6 +1,8 @@
#!/usr/bin/env bash
set -x
source /app/.venv/bin/activate
if [ ! -z "${APRSD_PLUGINS}" ]; then
OLDIFS=$IFS
IFS=','
@ -9,7 +11,7 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$plugin'"
pip3 install --user $plugin
uv pip install $plugin
done
fi
if [ ! -z "${APRSD_EXTENSIONS}" ]; then
@ -20,7 +22,7 @@ if [ ! -z "${APRSD_EXTENSIONS}" ]; then
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$extension'"
pip3 install --user $extension
uv pip install $extension
done
fi
@ -40,5 +42,7 @@ fi
export COLUMNS=200
#exec gunicorn -b :8000 --workers 4 "aprsd.admin_web:create_app(config_file='$APRSD_CONFIG', log_level='$LOG_LEVEL')"
# exec gunicorn -b :8000 --workers 4 "aprsd.wsgi:app"
exec uwsgi --http :8000 --gevent 1000 --http-websockets --master -w aprsd.wsgi --callable app
#exec uwsgi --http :8000 --gevent 1000 --http-websockets --master -w aprsd.wsgi --callable app
#exec aprsd listen -c $APRSD_CONFIG --loglevel ${LOG_LEVEL} ${APRSD_LOAD_PLUGINS} ${APRSD_LISTEN_FILTER}
#
uv run aprsd admin web -c $APRSD_CONFIG --loglevel ${LOG_LEVEL}

22
docker/bin/healthcheck.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
#
# This is the docker container healthcheck script
# It's assumed to be running in a working aprsd container.
set -x
source /app/.venv/bin/activate
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.conf"
if [ ! -e "$APRSD_CONFIG" ]; then
echo "'$APRSD_CONFIG' File does not exist. Creating."
aprsd sample-config > $APRSD_CONFIG
fi
uv run aprsd healthcheck --config $APRSD_CONFIG --loglevel ${LOG_LEVEL}

View File

@ -1,6 +1,8 @@
#!/usr/bin/env bash
set -x
source /app/.venv/bin/activate
if [ ! -z "${APRSD_PLUGINS}" ]; then
OLDIFS=$IFS
IFS=','
@ -9,7 +11,7 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$plugin'"
pip3 install --user $plugin
uv pip install --user $plugin
done
fi
@ -21,7 +23,7 @@ if [ ! -z "${APRSD_EXTENSIONS}" ]; then
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$extension'"
pip3 install --user $extension
uv pip install --user $extension
done
fi
@ -35,7 +37,7 @@ echo "Log level is set to ${LOG_LEVEL}";
APRSD_CONFIG="/config/aprsd.conf"
if [ ! -e "$APRSD_CONFIG" ]; then
echo "'$APRSD_CONFIG' File does not exist. Creating."
aprsd sample-config > $APRSD_CONFIG
uv run aprsd sample-config > $APRSD_CONFIG
fi
export COLUMNS=200

View File

@ -1,6 +1,8 @@
#!/usr/bin/env bash
set -x
source /app/.venv/bin/activate
# The default command
# Override the command in docker-compose.yml to change
# what command you want to run in the container
@ -18,7 +20,7 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$plugin'"
pip3 install --user $plugin
uv pip install $plugin
done
fi
@ -30,7 +32,7 @@ if [ ! -z "${APRSD_EXTENSIONS}" ]; then
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$extension'"
pip3 install --user $extension
uv pip install $extension
done
fi

View File

@ -68,14 +68,6 @@ aprsd.cmds.server module
:undoc-members:
:show-inheritance:
aprsd.cmds.webchat module
-------------------------
.. automodule:: aprsd.cmds.webchat
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -44,14 +44,6 @@ aprsd.conf.plugin\_common module
:undoc-members:
:show-inheritance:
aprsd.conf.plugin\_email module
-------------------------------
.. automodule:: aprsd.conf.plugin_email
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -4,14 +4,6 @@ aprsd.plugins package
Submodules
----------
aprsd.plugins.email module
--------------------------
.. automodule:: aprsd.plugins.email
:members:
:undoc-members:
:show-inheritance:
aprsd.plugins.fortune module
----------------------------
@ -20,14 +12,6 @@ aprsd.plugins.fortune module
:undoc-members:
:show-inheritance:
aprsd.plugins.location module
-----------------------------
.. automodule:: aprsd.plugins.location
:members:
:undoc-members:
:show-inheritance:
aprsd.plugins.notify module
---------------------------

View File

@ -16,7 +16,6 @@ Subpackages
aprsd.stats
aprsd.threads
aprsd.utils
aprsd.web
Submodules
----------
@ -45,14 +44,6 @@ aprsd.main module
:undoc-members:
:show-inheritance:
aprsd.messaging module
----------------------
.. automodule:: aprsd.messaging
:members:
:undoc-members:
:show-inheritance:
aprsd.plugin module
-------------------
@ -69,14 +60,6 @@ aprsd.plugin\_utils module
:undoc-members:
:show-inheritance:
aprsd.wsgi module
-----------------
.. automodule:: aprsd.wsgi
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -12,18 +12,10 @@ aprsd.threads.aprsd module
:undoc-members:
:show-inheritance:
aprsd.threads.keep\_alive module
--------------------------------
aprsd.threads.keepalive module
------------------------------
.. automodule:: aprsd.threads.keep_alive
:members:
:undoc-members:
:show-inheritance:
aprsd.threads.log\_monitor module
---------------------------------
.. automodule:: aprsd.threads.log_monitor
.. automodule:: aprsd.threads.keepalive
:members:
:undoc-members:
:show-inheritance:

View File

@ -28,6 +28,14 @@ aprsd.utils.json module
:undoc-members:
:show-inheritance:
aprsd.utils.keepalive\_collector module
---------------------------------------
.. automodule:: aprsd.utils.keepalive_collector
:members:
:undoc-members:
:show-inheritance:
aprsd.utils.objectstore module
------------------------------

View File

@ -1,10 +0,0 @@
aprsd.web.admin package
=======================
Module contents
---------------
.. automodule:: aprsd.web.admin
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,18 +0,0 @@
aprsd.web package
=================
Subpackages
-----------
.. toctree::
:maxdepth: 4
aprsd.web.admin
Module contents
---------------
.. automodule:: aprsd.web
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,7 +1,6 @@
import logging
from aprsd import plugin
from aprsd import packets, plugin
LOG = logging.getLogger("APRSD")
@ -14,7 +13,7 @@ class HelloPlugin(plugin.APRSDRegexCommandPluginBase):
command_regex = "^[hH]"
command_name = "hello"
def process(self, packet):
def process(self, packet: packets.MessagePacket):
LOG.info("HelloPlugin")
reply = f"Hello '{packet.from_call}'"
return reply

View File

@ -18,7 +18,7 @@ description = "APRSd is a APRS-IS server that can be used to connect to APRS-IS
# 'Programming Language' classifiers in this file, 'pip install' will check this
# and refuse to install the project if the version does not match. See
# https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
requires-python = ">=3.9"
requires-python = ">=3.10"
dynamic = ["version", "dependencies", "optional-dependencies"]
@ -173,3 +173,20 @@ skip_gitignore = true
branch = true
[tool.setuptools_scm]
[tool.ruff]
line-length = 88
select = [
"F", # pyflakes rules
"E", # pycodestyle error rules
"W", # pycodestyle warning rules
"B", # flake8-bugbear rules
# "I", # isort rules
]
ignore = [
"E501", # line-too-long
]
[tool.ruff.format]
indent-style = "space"
quote-style = "single"

View File

@ -1,40 +1,38 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --annotation-style=line requirements-dev.in
#
# This file was autogenerated by uv via the following command:
# uv pip compile --resolver backtracking --annotation-style=line requirements-dev.in -o requirements-dev.txt
alabaster==1.0.0 # via sphinx
babel==2.16.0 # via sphinx
build==1.2.2.post1 # via -r requirements-dev.in, pip-tools
build==1.2.2.post1 # via pip-tools, -r requirements-dev.in
cachetools==5.5.0 # via tox
certifi==2024.12.14 # via requests
cfgv==3.4.0 # via pre-commit
chardet==5.2.0 # via tox
charset-normalizer==3.4.0 # via requests
click==8.1.7 # via pip-tools
charset-normalizer==3.4.1 # via requests
click==8.1.8 # via pip-tools
colorama==0.4.6 # via tox
distlib==0.3.9 # via virtualenv
docutils==0.21.2 # via m2r, sphinx
filelock==3.16.1 # via tox, virtualenv
identify==2.6.3 # via pre-commit
identify==2.6.5 # via pre-commit
idna==3.10 # via requests
imagesize==1.4.1 # via sphinx
jinja2==3.1.4 # via sphinx
jinja2==3.1.5 # via sphinx
m2r==0.3.1 # via -r requirements-dev.in
markupsafe==3.0.2 # via jinja2
mistune==0.8.4 # via m2r
nodeenv==1.9.1 # via pre-commit
packaging==24.2 # via build, pyproject-api, sphinx, tox
pip==24.3.1 # via pip-tools, -r requirements-dev.in
pip-tools==7.4.1 # via -r requirements-dev.in
platformdirs==4.3.6 # via tox, virtualenv
pluggy==1.5.0 # via tox
pre-commit==4.0.1 # via -r requirements-dev.in
pygments==2.18.0 # via sphinx
pygments==2.19.1 # via sphinx
pyproject-api==1.8.0 # via tox
pyproject-hooks==1.2.0 # via build, pip-tools
pyyaml==6.0.2 # via pre-commit
requests==2.32.3 # via sphinx
setuptools==75.7.0 # via pip-tools
snowballstemmer==2.2.0 # via sphinx
sphinx==8.1.3 # via -r requirements-dev.in
sphinxcontrib-applehelp==2.0.0 # via sphinx
@ -46,10 +44,6 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx
tomli==2.2.1 # via build, pip-tools, pyproject-api, sphinx, tox
tox==4.23.2 # via -r requirements-dev.in
typing-extensions==4.12.2 # via tox
urllib3==2.2.3 # via requests
virtualenv==20.28.0 # via pre-commit, tox
wheel==0.45.1 # via -r requirements-dev.in, pip-tools
# The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools
urllib3==2.3.0 # via requests
virtualenv==20.28.1 # via pre-commit, tox
wheel==0.45.1 # via pip-tools, -r requirements-dev.in

View File

@ -1,6 +1,4 @@
aprslib>=0.7.0
# For the list-plugins pypi.org search scraping
beautifulsoup4
click
dataclasses-json
haversine

View File

@ -1,17 +1,12 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --annotation-style=line requirements.in
#
# This file was autogenerated by uv via the following command:
# uv pip compile --resolver backtracking --annotation-style=line requirements.in -o requirements.txt
aprslib==0.7.2 # via -r requirements.in
attrs==24.3.0 # via ax253, kiss3, rush
ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.12.3 # via -r requirements.in
bitarray==3.0.0 # via ax253, kiss3
certifi==2024.12.14 # via requests
charset-normalizer==3.4.0 # via requests
click==8.1.7 # via -r requirements.in
charset-normalizer==3.4.1 # via requests
click==8.1.8 # via -r requirements.in
commonmark==0.9.1 # via rich
dataclasses-json==0.6.7 # via -r requirements.in
debtcollector==3.0.0 # via oslo-config
@ -20,7 +15,7 @@ idna==3.10 # via requests
importlib-metadata==8.5.0 # via ax253, kiss3
kiss3==8.0.0 # via -r requirements.in
loguru==0.7.3 # via -r requirements.in
marshmallow==3.23.2 # via dataclasses-json
marshmallow==3.24.1 # via dataclasses-json
mypy-extensions==1.0.0 # via typing-inspect
netaddr==1.3.0 # via oslo-config
oslo-config==9.7.0 # via -r requirements.in
@ -28,16 +23,15 @@ oslo-i18n==6.5.0 # via oslo-config
packaging==24.2 # via marshmallow
pbr==6.1.0 # via oslo-i18n, stevedore
pluggy==1.5.0 # via -r requirements.in
pygments==2.18.0 # via rich
pygments==2.19.1 # via rich
pyserial==3.5 # via pyserial-asyncio
pyserial-asyncio==0.6 # via kiss3
pytz==2024.2 # via -r requirements.in
pyyaml==6.0.2 # via oslo-config
requests==2.32.3 # via -r requirements.in, oslo-config, update-checker
requests==2.32.3 # via oslo-config, update-checker, -r requirements.in
rfc3986==2.0.0 # via oslo-config
rich==12.6.0 # via -r requirements.in
rush==2021.4.0 # via -r requirements.in
soupsieve==2.6 # via beautifulsoup4
stevedore==5.4.0 # via oslo-config
thesmuggler==1.0.1 # via -r requirements.in
timeago==1.0.16 # via -r requirements.in
@ -45,6 +39,6 @@ typing-extensions==4.12.2 # via typing-inspect
typing-inspect==0.9.0 # via dataclasses-json
tzlocal==5.2 # via -r requirements.in
update-checker==0.18.0 # via -r requirements.in
urllib3==2.2.3 # via requests
wrapt==1.17.0 # via -r requirements.in, debtcollector
urllib3==2.3.0 # via requests
wrapt==1.17.0 # via debtcollector, -r requirements.in
zipp==3.21.0 # via importlib-metadata

View File

@ -2,9 +2,9 @@ from unittest import mock
from oslo_config import cfg
from aprsd import (
from aprsd import ( # noqa: F401
client,
conf, # noqa: F401
conf,
packets,
)
from aprsd.plugins import notify as notify_plugin

View File

@ -3,8 +3,8 @@ from unittest import mock
from oslo_config import cfg
from aprsd import (
conf, # noqa: F401
from aprsd import ( # noqa: F401
conf,
packets,
plugins,
)