Files
FuelBoard Contributor bd3d3862ec Fix API base URL, rate-limit paging, background refresh; verify live against official API
- Base URL corrected to https://www.fuel-finder.service.gov.uk (the
  api.fuelfinder.service.gov.uk host in the portal's examples is stale —
  it does not resolve; www.fuel-finder... resolves to CloudFront and is
  what the live spec uses). Verified: /health now reports source=api,
  8,010 stations, 0 failures.
- Fuel Finder is strictly sequential (30 rpm per client, 429 on overlap):
  added FUEL_API_BATCH_SLEEP (default 4.0s) between pages ≈ 15 rpm.
- Stale-cache refresh moved off the request path into a background
  thread — a multi-minute rate-limited sync no longer blows the app's
  5s timeout; stale data is served while it catches up.
- Multiple diesel variants (B7_STANDARD, B7_PREMIUM, B10) now map to
  DIESEL as the MINIMUM price, so a premium price can't inflate the
  cheapest-diesel reference (CSV relay took first-wins; min is safer).
- Added B7_PREMIUM (underscore) to the fuel map — official key.
- scripts/run.sh now prefers the project venv (bare python3 missed
  uvicorn when run outside an activated env).
- Tests updated for min-diesel + B7_PREMIUM; 12/12 pass.
2026-08-12 15:50:08 +01:00

103 lines
4.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FuelBoard Relay API
Keyless proxy for the **official UK Fuel Finder API** (the OAuth-protected
GOV.UK service). Sibling of `fuelboard-relay` (which reads the public CSV
mirror): this one talks to the real API, joins the station-info and fuel-price
endpoints on `node_id`, and normalises to the **same output shape** so the
FuelBoard iOS app and widget work against either relay unchanged.
Holds the GOV.UK OAuth 2.0 client credentials server-side so the app never
embeds a secret (secrets inside an IPA are extractable by anyone).
## Architecture
```
FuelBoard app / widget
│ GET http://192.168.1.131:8789/api/v1/stations?fuel=e10&lat=..&lng=..
FuelBoard Relay API (this repo, runs on the Mac Mini)
│ POST /api/v1/oauth/generate_access_token (token cached ~1h)
│ GET /api/v1/pfs?batch-number=N (station info, 500/page)
│ GET /api/v1/pfs/fuel-prices?batch-number=N (prices, 500/page)
UK Fuel Finder API (official gov.uk open data)
```
- Token refreshed automatically before expiry (60s buffer); reused across the
two batched loops (docs: reuse tokens until near expiry).
- Both endpoints paginated 500 records/page via `batch-number` until a short
page is returned; pages are spaced `FUEL_API_BATCH_SLEEP` (default 4s) apart
to stay inside the documented 30 rpm sequential-only rate limit.
- Prices arrive as **decimal strings in pence** (`"0120.0000"` = 120.0p); some
stations report pounds (values < 2.0 are multiplied by 100). Out-of-band
values are dropped (50500p sane band) so a pounds-denominated column can't
poison the nationwide cheapest reference.
- Fuel types mapped: `E5`/`E10` → same, `B7`/`B7_STANDARD`/`B7S`/`B7P`/
`B7_PREMIUM`/`B10``DIESEL`. Others (`HVO`, `SDV`, …) are ignored — the
app tracks exactly E5/E10/DIESEL.
- Station snapshot cached `FUEL_CACHE_TTL` (default 300s); a stale cache is
refreshed in the background on the next request and stale data is served in
the meantime, so a multi-minute sync never blocks the app's 5s timeout.
- Base URL is `https://www.fuel-finder.service.gov.uk` (NOT
`api.fuelfinder...` — that host doesn't resolve). Overridable per-endpoint
via env.
- No credentials are exposed to clients — the app calls the relay keyless.
## Setup
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install pytest # only needed for the test suite
cp .env.example .env # then fill in FUEL_API_CLIENT_ID / FUEL_API_CLIENT_SECRET
./scripts/run.sh # serves on FUEL_RELAY_PORT (default 8789)
```
Without credentials, `FUEL_SOURCE=auto` falls back to the public hourly CSV
mirror (same parser as `fuelboard-relay`), so the full relay→app path is
testable end-to-end before GOV.UK One Login creds exist. `/health` reports the
active `source` (`csv` vs `api`).
## Endpoints
| Endpoint | Description |
|---|---|
| `GET /health` | Cache status, active source, last sync, failure counters |
| `GET /api/v1/stations` | All stations with prices (keyless) |
| `GET /api/v1/stations?fuel=e10&lat=53.72&lng=-1.85&radius=10&limit=10` | Filter by fuel, radius around a point, sorted nearest-first |
| `GET /api/v1/stations?fuel=diesel` (no lat/lng) | Sorted cheapest-first |
Fuel grades: `e10`, `e5`, `diesel`. Output contract identical to
`fuelboard-relay` — the app can switch relays by changing the base URL only.
## Getting credentials
1. Sign in at <https://developer.fuel-finder.service.gov.uk/get-started-ifr/onelogin> with GOV.UK One Login.
2. Register an application → get `client_id` + `client_secret`.
3. Put them in `.env` (never in git, never in the app).
## Fair use notes (from the Fuel Finder developer guideline)
- Refresh at least every 5 minutes — we do (cron-style refresh on request + TTL cache).
- Serve data unmodified — this relay only filters/sorts, never alters prices.
- Keep timestamps — `price_updated` is passed through untouched.
- Rate limits: ~100 rpm documented — batched loops issue ~2 requests per page
(info + prices), comfortably inside the limit on a 5-minute cadence.
## Tests
```bash
python -m pytest tests/ -q
```
Covers price-string parsing (pence, pounds, out-of-band), the node_id join,
fuel-type mapping, and CSV-fallback parsing — against the documented API
shapes, no network required.
## Switching the app
Point `Shared/FuelPriceProvider.swift`'s `RelayFuelProvider.baseURL` at
`http://192.168.1.131:8789` (API relay) instead of `:8788` (CSV relay). No
other app change needed — the station JSON is identical in shape.