Skip to content

[P2][bug] normalize_futures_slug raises bare ValueError, outside OilPriceAPIError - #111 made that path far more common #122

Description

@karlwaldman

Related to #111 (6e1a66c) and issue #112. Verified by execution against main on 2026-09-13 — confirmed live.

What is wrong

oilpriceapi/resources/_futures_slug.py:148 raises a builtin ValueError, which is not in the SDK's exception hierarchy:

raise ValueError(
    f"Unknown futures contract/slug {contract!r}. "
    f"Pass a slug ({valid}) or a contract code ({codes})."
)

Every FuturesResource method calls normalize_futures_slug before issuing a request (futures.py:58, 94, 129, 159, 212, 257), so this escapes straight out of the public API.

Repro

from oilpriceapi.resources.futures import FuturesResource
from oilpriceapi.exceptions import OilPriceAPIError

class FakeClient:
    def request(self, **kw): return {}

for m in ["latest", "curve", "intraday"]:
    try:
        getattr(FuturesResource(FakeClient()), m)("WTI_MIDLAND_USD")
    except OilPriceAPIError as e:
        print(m, "caught:", type(e).__name__)
    except Exception as e:
        print(m, "ESCAPES OilPriceAPIError ->", type(e).__name__)

Measured:

latest:   ESCAPES OilPriceAPIError -> ValueError
curve:    ESCAPES OilPriceAPIError -> ValueError
intraday: ESCAPES OilPriceAPIError -> ValueError

Why it matters

The ValueError predates #111, but #111 changed 18 live catalog codes from "resolved to the wrong instrument" to "raises" (LNG_NW_EUROPE_EUR, WTI_MIDLAND_USD, TTF_NL_EUR, LNG_JKM_USD, NATURAL_GAS_USD, BRENT_CRUDE_USD, GASOIL_USD, … all verified raising). That is the right call — but it means user code shaped like:

try:
    curve = client.futures.curve(code)
except OilPriceAPIError:
    fall_back()

goes from "silently wrong answer" to "uncaught crash". #111's own comment argues "a refusal is recoverable, a wrong instrument's curve is not" — it is only recoverable if the refusal is catchable through the documented base class.

Suggested fix

Raise oilpriceapi.exceptions.ValidationError (already a subclass of OilPriceAPIError) instead, with field="contract" and value=contract. Keep the "unknown slug" text as the message — but note #117: ValidationError.__str__ currently discards a supplied message whenever field is set, so that needs fixing first or the helpful list of valid slugs will not be printed.

ValueError should be retained as a base if backward compatibility matters — class FuturesContractError(ValidationError, ValueError) catches both old and new call sites.

Residual, same file (low)

_futures_slug.py:140 still strips trailing digits unconditionally, after the new month-suffix guard has already decided not to split:

WTI2026 -> wti     BRENT1 -> brent     LNG1 -> lng-jkm     CL0 -> wti

Same "guess rather than refuse" shape #111 removed from the separator split. Low impact — no live catalog code matches — but worth closing while the file is open.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority: mediumShould be fixed eventually

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions