- 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.
103 lines
4.5 KiB
Markdown
103 lines
4.5 KiB
Markdown
# 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 (50–500p 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.
|