broker-sync/broker_sync/providers/_checkpoint.py
Viktor Barzin 43d2251159 Add per-account cursor Checkpoint helper
Context
-------
Trading212's `/equity/history/orders` is cursor-paginated via a
`nextPagePath` query-param in each response. Steady-state runs must
resume where the previous run finished, or we either miss fills (if
we start from 'now') or waste the 6/min rate limit walking history
we already imported (if we start from epoch).

A shared checkpoint store must live alongside the SyncRecordStore's
dedup DB on the /data PVC so CronJob pods can see progress from the
previous invocation. One file per (provider, account_id) because:

- T212 issues one API key per wrapper — ISA + Invest share no data.
- Plain JSON files are trivial to hand-edit during backfill if a
  resume cursor gets stuck at a bad point.

This change
-----------
- broker_sync/providers/_checkpoint.py: `Checkpoint(dir, provider,
  account_id)` with `load() -> str | None` and `save(cursor)`. Writes
  `{cursor, updated_at}` to `<provider>-<account_id>.json`. Creates
  parent directory lazily on first save so the PVC only needs a
  mountpoint, not a pre-seeded layout.
- Provider-agnostic: no T212 knowledge. Will be reused for
  InvestEngine in Phase 2.
- tests/providers/test_checkpoint.py: roundtrip, filename shape,
  overwrite, per-account isolation, parent-dir creation, and a
  malformed-file fallback (returns None rather than raising) so a
  manual edit gone wrong does not brick the CronJob.

Test plan
---------
## Automated
- poetry run pytest -q  →  48 passed in 0.47s
- poetry run mypy broker_sync tests  →  Success: no issues found in 24 source files
- poetry run ruff check .  →  All checks passed!

## Manual Verification
Not applicable — pure local-filesystem helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 19:30:20 +00:00

30 lines
987 B
Python

from __future__ import annotations
import json
from datetime import UTC, datetime
from pathlib import Path
class Checkpoint:
"""Per-account cursor persistence.
File shape: `{"cursor": "...", "updated_at": "2026-04-17T12:00:00+00:00"}`
One file per (provider, account_id) at `<dir>/<provider>-<account_id>.json`.
"""
def __init__(self, directory: Path, *, provider: str, account_id: str) -> None:
self._path = directory / f"{provider}-{account_id}.json"
def load(self) -> str | None:
if not self._path.exists():
return None
raw = json.loads(self._path.read_text())
cursor = raw.get("cursor")
if not isinstance(cursor, str):
return None
return cursor
def save(self, cursor: str) -> None:
self._path.parent.mkdir(parents=True, exist_ok=True)
payload = {"cursor": cursor, "updated_at": datetime.now(UTC).isoformat()}
self._path.write_text(json.dumps(payload))