Release 2026.10

← API

Cookbook: watch the source catalog

A feed is only as good as the collection behind it. Every source in the catalog reports when it last synced successfully — this check polls that field and flags any enabled source that has been quiet longer than your threshold.

The pattern

About that timestamp

last_success looks like 2026-10-05 17:26:20.101271+00 — a space between date and time, six-digit microseconds, and a two-digit UTC offset. Python's datetime.fromisoformat rejects the short offset on 3.10 and older, so the script splits the string and rebuilds a full ISO timestamp before parsing. It runs unchanged on Python 3.8+.

The script

Save as source_monitor.py and run it hourly: 0 * * * * python3 /path/to/source_monitor.py.

#!/usr/bin/env python3
"""Source-staleness monitor: check the catalog, flag quiet sources, exit nonzero.

Cron entry (top of every hour):
    0 * * * * python3 /path/to/source_monitor.py
"""
import json
import urllib.request
from datetime import datetime, timedelta, timezone

STALE_AFTER = timedelta(hours=48)   # pick your own threshold
CATALOG = "https://pubsec.pro/api/v1/sources"


def parse_last_success(ts):
    # "2026-10-05 17:26:20.101271+00" -> aware UTC datetime.
    # The two-digit offset breaks fromisoformat() on Python <= 3.10,
    # so split the string and rebuild a full ISO timestamp.
    date_part, clock, offset = ts.split(" ")
    return datetime.fromisoformat(f"{date_part}T{clock}{offset}:00").astimezone(timezone.utc)


def main():
    with urllib.request.urlopen(CATALOG) as response:
        catalog = json.load(response)

    now = datetime.now(timezone.utc)
    problems = []
    for source in catalog["data"]:
        if not source["enabled"]:
            continue                    # switched off on purpose: not an alert
        if not source["last_success"]:
            problems.append((source["source_id"], "never succeeded"))
            continue
        age = now - parse_last_success(source["last_success"])
        if age > STALE_AFTER:
            hours = age.total_seconds() / 3600
            problems.append((source["source_id"], f"last success {hours:.0f} h ago"))

    if not problems:
        print(f"OK - {catalog['meta']['count']} sources in the catalog, "
              f"no enabled source stale (threshold {STALE_AFTER})")
        return

    print(f"STALE SOURCES (threshold {STALE_AFTER}):")
    for source_id, why in problems:
        print(f"  {source_id}: {why}")
    raise SystemExit(1)                 # nonzero exit -> cron emails the output


if __name__ == "__main__":
    main()

Notes

Back to the API reference.

Citation format

pubsec/data 2026.10, https://pubsecdata.org/api/cookbook/monitor-sources