- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Move the code into one module per concern (config, feed, html_lines, processing, eastern_time, sending, state, cli). apcug_forward.py stays as a thin entry point so the cron command and usage are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
| apcug_workshops | ||
| doc | ||
| tests | ||
| .gitignore | ||
| apcug_forward.py | ||
| config.example.toml | ||
| config.local.toml | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
APCUG Workshops Forwarder
Polls APCUG's Wednesday Workshop Mailchimp RSS feed and forwards new announcements by email (in production, to NYACC's Groups.io group). See doc/requirements.md for the full requirements.
Every item in the feed is forwarded, whether it's a Wednesday Workshop or another APCUG announcement such as a Saturday Safari. Before forwarding, each message is cleaned up:
- APCUG's preamble paragraph starting with "Thank you for forwarding this..." is removed.
- NYACC's own introduction (
preamble_replacement) is inserted at the very top of the message, just before the "View this email in your browser" link (preamble_anchor). If that link is missing, it goes at the top of the body. - Footer lines about the recipient's subscription are removed: "This email was
sent to", "why did I get this? / unsubscribe" (Wednesday Workshop template),
and "Want to change how you receive these emails? / You can update your
preferences or unsubscribe" (Saturday Safari template). APCUG's copyright and
mailing address lines are kept. The list is
footer_remove_prefixesin the config. - Unfilled Mailchimp placeholders such as
?e=[UNIQID]are stripped from link URLs (e.g. "View this email in your browser").
Setup
uv sync
cp config.example.toml config.toml # then edit: addresses and FastMail app password
config.local.toml is preconfigured for testing against the local Mailpit
(localhost:1025, no auth).
Usage
uv run apcug_forward.py [--conf PATH] cron
uv run apcug_forward.py [--conf PATH] [--back N] latest
| Option | Meaning |
|---|---|
cron |
Forward every feed item not seen before, oldest first. |
latest |
Unconditionally forward the newest item. |
--back N |
With latest, forward the item N places older (1 = second-newest). |
--conf PATH |
Configuration file (default config.toml in the project root). |
--send-mode smtp|file |
Override the config's send_mode for this run. |
-v |
Debug logging. |
Exit status is 0 on success (including "nothing new"), 1 if fetching or sending failed, 2 for usage or configuration errors. In cron mode the script prints nothing unless it forwards something or hits an error, so cron only mails you when there is news.
Cron
The feed is cached by Mailchimp for 15 minutes, so polling hourly is plenty:
0 * * * * cd /home/brendan/projects/apcug-workshops && /home/brendan/.local/bin/uv run --frozen apcug_forward.py cron
Files
| Path | Contents |
|---|---|
apcug_forward.py |
Entry point; the code lives in apcug_workshops/. |
apcug_workshops/ |
config (TOML loading), feed (fetch and parse RSS), html_lines (HTML as visual lines), processing (message cleanup), eastern_time (time-zone annotation), sending (build and deliver email), state (seen items), cli (arguments, locking, cron/latest). |
cache/ |
Cached copy of the feed. Its modification time is sent as If-Modified-Since; on a 304 Not Modified the cached copy is used. |
state/ |
state_file from the config: JSON record of every item seen, and a lock file that prevents overlapping runs. |
log/ |
In file send mode, each message is written here as an .eml file instead of being sent. |
First run
When the state file doesn't exist yet, every item currently in the feed is
recorded as already seen without being forwarded, so the first cron run doesn't
flood the group with old announcements. To send the current announcement right
away, run latest once after that.
Each config should use its own state_file (the local test config uses
state/seen-local.json) so test runs don't mark items as seen for production.
Development
uv run ruff check . && uv run ruff format --check .
uv run pytest