broker-sync/broker_sync/providers/base.py
Viktor Barzin f306dc9605 Add Provider protocol and normaliser
Context
-------
Every broker connector needs a uniform shape so the orchestrator can
fan out without knowing provider-specific details. Normalisation (GBP
conversion) lives outside providers on purpose — keeping providers
native-currency-emitters means we can re-normalise historical activity
when HMRC rates land without re-fetching from the broker.

This change
-----------
- providers/base.py: Provider Protocol with `accounts()` and async
  `fetch(since, before)` iterator. No abstract base class — duck-typed
  Protocol so each concrete provider stays independent.
- normaliser.py: takes a native Activity + FxCache, returns a copy
  with amount_gbp/fx_rate_gbp/fx_rate_source filled in. Two modes:
  qty*price for BUY/SELL, amount for DIVIDEND/DEPOSIT/etc.
- Namespace packages for providers/, providers/parsers/, sinks/ so
  future modules slot in cleanly.

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

## Manual Verification
Not applicable at this layer.
2026-04-17 19:20:12 +00:00

32 lines
834 B
Python

from __future__ import annotations
from collections.abc import AsyncIterator
from datetime import datetime
from typing import Protocol
from broker_sync.models import Account, Activity
class Provider(Protocol):
"""Broker connector surface.
Each provider implementation is responsible for fetching raw broker
data, turning it into canonical `Activity` rows (with `account_id`
matching an `Account.id` from `accounts()`), and yielding them.
GBP conversion is performed by the shared normaliser, not here —
providers emit native currency and the caller converts.
"""
name: str
def accounts(self) -> list[Account]:
...
def fetch(
self,
*,
since: datetime | None = None,
before: datetime | None = None,
) -> AsyncIterator[Activity]:
...