Repository navigation
v1.14.0 — credential transport, write safety, and no fabricated codes
Security release. Every user on 1.13.0 or earlier should upgrade.
PyPI has been serving a version in which a caller-supplied raw path can send the customer's API key to a host that is not OilPriceAPI.
Security — raw paths can no longer change the API origin
//fixture.invalid/v1/prices resolved to a foreign host with Authorization: Token <key> still attached. Worse, //user@fixture.invalid/... made httpx read the userinfo and replace our token with Basic dXNlcjo= — a crafted path could both redirect the request and swap the credential.
A path is now rejected unless it resolves to the configured base origin, compared on (scheme, host, port). 27 unicode and ASCII probe forms were driven through the resolver: none changed the origin. follow_redirects was checked separately and does not bypass it — httpx strips Authorization cross-origin.
Fixed — the SDK no longer manufactures a match
A price response that omits code no longer comes back wearing the code you asked for. That fabrication made the consumer-side guard price.commodity == requested pass by construction, on precisely the responses where it needed to fail — disarming the standard defence against a wrong-instrument response rather than merely failing to help. An absent code is now empty, matching what get_all already did.
Three call sites carried the same fallback; all three are fixed, and the hook was removed rather than left ignored.
Fixed — write safety
- Non-idempotent writes are no longer replayed. POST was retried 3× on timeout and 3× on 503 — a duplicated write.
max_retries=0was ignored entirely and still retried three times;retry_on=[]fell back to the default list. - A write refused after a 5xx now carries
.ambiguous_write, the "did my write land?" signal. Previously only the timeout path set it. Retry-After: -30no longer produces a negative sleep.max_retries=0is honoured as one attempt with a deprecation notice rather than raising at construction —int(os.getenv("OPA_MAX_RETRIES", "0"))is exactly how callers produce it.
Fixed — data correctness
- 18 catalogue codes resolved to a different instrument.
normalize_futures_slugsplit on the first_and discarded geography and currency:LNG_NW_EUROPE_EUR→ Japan/Korea Marker,WTI_MIDLAND_USD→ Cushing WTI,WTI_SPOT_CUSHING_USD→ the futures curve. Suffix stripping now requires the discarded tail to actually look like a month or order marker. client.diesel.*no longer raisesValidationErroragainst the live envelope — the response is unwrapped, andstateis read fromdata.location.state_coderather than theregionthe model could not accept.ValidationError.__str__no longer discards the message, so the origin guard's remediation text reaches logs and tracebacks.timeout=0andbase_url=""are honoured instead of silently becoming defaults.
Also in this release
Fixes for three regressions introduced earlier the same day by the changes above, all found by an expert review after they merged green: a forbidden SPACE in query paths that broke the SDK's own demo.py, retries stripped from read-shaped POSTs (diesel.get_stations, webhooks.test, alerts.test), and a bare ValueError escaping the SDK exception hierarchy.
Every fix applies identically to the sync and async clients, pinned by parity tests.
Full diff: v1.13.0...v1.14.0