Initial commit

This commit is contained in:
ThePetrovich 2026-08-18 22:05:57 +08:00
commit fe5ef9132a
15 changed files with 888 additions and 0 deletions

89
README.md Normal file
View file

@ -0,0 +1,89 @@
# yksa-source-poller
The machine that polls upstreams on a schedule: a source model, a run log, the
dispatcher and the bookkeeping around one attempt. The cargo — what a fetched
record *is* and what happens to it — stays in the service.
Extracted from `tdas/yksa_tdas/sources/` and `tle/yksa_tle/sources/`, which were
the same ~500 lines with different payload types.
## Install
```
yksa-source-poller @ git+https://git.intra.yksa.space/web/yksa-source-poller.git@v0.1.0
```
Requires [yksa-django-kit](../yksa-django-kit) for `RunStatus` and `read_secret`.
It is **not** added to `INSTALLED_APPS`: every model in it is abstract, so it
ships no tables and needs no app config.
## Use
```python
# sources/models.py
class TelemetrySource(AbstractPollingSource):
SUBSCRIPTION_ID_FIELDS = {"norad": "norad_cat_id", "satnogs": "satnogs_id"}
kind = models.CharField(max_length=32, choices=SourceKind.choices)
subscription_id_kind = models.CharField(..., default=SubscriptionIdKind.SATNOGS)
tracked_satellites = models.ManyToManyField("satellites.Satellite", blank=True)
class SourceRun(AbstractSourceRun):
source = models.ForeignKey(TelemetrySource, on_delete=models.CASCADE,
related_name="runs")
```
```python
# sources/tasks.py
POLLER = Poller(
source_model=TelemetrySource,
run_model=SourceRun,
adapters=ADAPTERS,
persist=_persist_frame, # -> True when the record is new
track=_track_latest_rx, # optional: extra source fields on success
unpollable_kinds=(SourceKind.INTERNAL_PUSH,),
)
@shared_task(bind=True, max_retries=3, default_retry_delay=120)
def run_source_task(self, source_id):
try:
return run_source(POLLER, source_id)
except Exception as exc:
raise self.retry(exc=exc)
```
The Celery tasks stay in the service on purpose: `CELERY_BEAT_SCHEDULE` names
them by dotted path, and a task that moved into this package would silently stop
being scheduled.
## Why the models are abstract
The two concrete source models differ where they have to — which satellite model
the M2M points at, which identifiers the upstream accepts, and (in tdas) a
`last_rx_at` watermark. Everything else was identical. Abstract bases share the
identical part without pretending the rest is the same.
Field definitions here reproduce what the services already had, so adopting the
package needs **no migration**. Where a service's own definition differed — a
different `poll_interval_sec` default, different wording — the service overrides
the field, which Django allows for abstract inheritance. Check with
`manage.py makemigrations --check --dry-run` after wiring it up.
## What the runner guarantees
Every exit path — success, failure, unconfigured adapter, missing adapter —
writes `last_status`, updates a counter and re-arms `next_poll_at`. That is the
reason to share it: a path that forgets leaves the dispatcher either hammering
the source once a minute or ignoring it forever, and both shipped once.
`NotImplementedError` from an adapter is a *skip*, not a failure: a half-configured
source must not look like an outage.
## Tests
```sh
pip install -e ".[test]" ../yksa-django-kit
python -m pytest
```