Skip to content

v1.16.0

Latest

Choose a tag to compare

@karlwaldman karlwaldman released this 13 Sep 21:19
868322c

Added

  • Typed client.spreads and client.indicators resources (#99), sync and
    async.
    They cover the server-calculated /v1/spreads/* routes: crack,
    crack_historical, crack_all, gasoil_crack, basis,
    basis_historical, basis_all, curve_structure, curve_structure_all,
    margin, margin_historical, margin_all, physical_premium,
    physical_premium_historical and physical_premium_all. They also cover the
    /v1/indicators/* routes: fuel_switching, fuel_switching_historical,
    price_context, storage_analytics, storage_analytics_all,
    annotations, annotations_batch, cftc_positioning,
    cftc_positioning_historical and cftc_positioning_all.
    • Each method returns a pydantic model from the new
      oilpriceapi.metrics_models module. The models are typed from production
      responses captured on 2026-09-13.
    • Timestamps parse to timezone-aware datetime and calendar dates to date.
      Units, full-precision values and nulls are kept exactly as sent.
    • A key the server always emits is required. A 200 that drops it, changes its
      type, or breaks the envelope raises
      OilPriceAPIError(code="MALFORMED_RESPONSE") with the raw body. It is never
      defaulted.
    • History responses expose the server-applied period and, for crack
      spreads, the coverage actually returned.
    • Blank selectors, invalid dates, start_date after end_date, and more than
      20 codes for annotations_batch are refused before any request is sent,
      with ValidationError (an OilPriceAPIError) carrying field, value
      and status_code=None.
      The API would otherwise return a default window, or silently annotate only
      the first 20 codes.
    • /v1/indicators/congressional-trades is deliberately not exposed. It has
      never returned data in production, so there is no response shape to type.
  • Subscription lifecycle: get, update, pause, resume (#100). Sync
    and async, against GET/PATCH /v1/subscriptions/{id} and
    POST /v1/subscriptions/{id}/pause|resume. Each returns a typed
    Subscription with the server's timestamps and nulls as sent. update()
    sends only the fields you pass (name, codes, interval,
    deliver_webhook, status). Ids and update payloads are validated before
    any request is built; an invalid one raises ValidationError with
    status_code=None, field naming the argument, and nothing sent. Unknown ids raise DataNotFoundError; a refused update (interval
    below the plan minimum, webhook delivery the plan lacks) raises
    ValidationError with the server's details. update, pause and
    resume are writes and are sent once, like create: after an ambiguous
    timeout or 5xx the error carries ambiguous_write=True and get() tells
    you whether the change landed.
  • Typed LTL and parcel fuel-surcharge clients (#101). client.fuel_surcharge
    on both OilPriceAPI and AsyncOilPriceAPI covers all six
    /v1/fuel-surcharge routes: list(), latest(carrier),
    history(carrier, page=, per_page=), parcel_list(),
    parcel_latest(carrier), parcel_latest_rate(carrier, service_level) and
    parcel_history(carrier, service_level, page=, per_page=). Responses are
    FuelSurchargeRate, FuelSurchargeHistoryPage (with the server's
    meta) and ParcelFuelSurchargeCarrier models typed from production
    payloads captured on 2026-09-13: effective_date is a date,
    retrieved_at a timezone-aware datetime, and source, nullable
    doe_diesel_price and diesel_band are kept as sent. A success body
    missing a field the API always sends raises
    OilPriceAPIError(code="MALFORMED_RESPONSE") instead of defaulting it.
    Carrier slugs, service levels and pagination are validated before any
    request; out-of-range page/per_page are refused because the API clamps
    them silently.
  • Fuel-surcharge 400/404 bodies populate error.suggestions. The
    covered_carriers and available_service_levels lists the API returns with
    an unknown carrier or a missing service level are now surfaced the same way
    commodity suggestions are.

Fixed

  • SubscriptionEvent is typed from the event the API sends (#149). It
    declared type, code, payload and created_at, which
    GET /v1/subscriptions/events has never sent, so they read None on every
    real event. Meanwhile id, observed_at, snapshot, deltas, source and
    tool_name were untyped extras. The model now declares:
    • required id, seq, watch_id, observed_at (a timezone-aware
      datetime), snapshot and deltas;
    • optional source and tool_name.
      snapshot maps each code to the new SubscriptionEventSnapshot (price,
      currency, optional change_24h_pct and as_of). deltas maps each code
      to the new SubscriptionEventDelta (price_change, optional pct_change).
      An event missing a required field raises
      OilPriceAPIError(code="MALFORMED_RESPONSE").
  • error.code is no longer set to a human sentence (#145). For fail
    envelopes, {"status": "fail", "data": {"error": ...}}, the SDK copied
    data.error into error.code / error.machine_code whatever it held. Every
    fuel-surcharge 400/404 puts the sentence itself there, for example
    "Unknown carrier 'nope'. Covered carriers: ...", so code-based branching
    saw a different string on every request. data.error is now the code only
    when it is a snake-case token: upper-snake like VALIDATION_ERROR or
    INTERVAL_FLOOR, or lower-snake like invalid_code or no_price_data. A
    sentence stays in error.message and error.code is None. A canonical
    nested error object still takes precedence, and API-key redaction is
    unchanged.
  • subscriptions.list() and subscriptions.events() no longer report a
    malformed success as "nothing there" (#142), sync and async.
    A 200 without
    a data.subscriptions list returned [], and one without data.events /
    data.cursor returned an empty page with cursor=None. Fed back as
    events(since=page.cursor), that None dropped since, and the API reads a
    missing since as 0, so the poller replayed the account's whole event
    history. Both now raise OilPriceAPIError(code="MALFORMED_RESPONSE") with the
    raw body when the collection is missing or mistyped, a record is invalid,
    cursor is not a non-negative integer, has_more is not a boolean, or the
    cursor is behind since or behind an event in the page. A genuinely empty
    list or page is still an empty success, and page.cursor is now always an
    int.
  • subscriptions.events(since=...) refuses a cursor the API would read as
    0.
    The API parses since with to_i, so "abc", "" and -1 replay
    every event and 1.5 becomes 1 (verified against production on
    2026-09-13). Anything but a non-negative int or None now raises
    ValidationError(field="since", status_code=None) before a request is sent.
  • subscriptions.create() no longer turns a malformed success into a
    half-built record.
    It fell back to treating the whole data object as the
    subscription when data.subscription was missing, and leaked a raw pydantic
    or TypeError when the record was null or a list. It now raises
    OilPriceAPIError(code="MALFORMED_RESPONSE"), the same as the new lifecycle
    methods.
  • subscriptions.delete() validates the id before sending. An id such as
    "abc/pause" or "" previously produced a request to a different route; it
    now raises ValidationError(field="subscription_id", status_code=None).
  • A bad subscription interval is now an SDK refusal as well as a
    ValueError.
    subscriptions.create(interval=...), normalize_interval and
    build_create_body raise the new SubscriptionIntervalError, a subclass of
    both ValidationError and ValueError (like FuturesContractError), so
    except OilPriceAPIError catches it and existing except ValueError code
    keeps working. It carries field="interval", the rejected value, and
    status_code=None.
  • Subscription.codes is required. A record with no codes used to
    default to [], reading as a watch on nothing; the API always sends it, so a
    missing value now fails validation instead of being invented.

Deprecated

  • SubscriptionEvent.type, .code, .payload and .created_at are
    deprecated and will be removed in 2.0.0 (#149).
    The events API never sends
    any of them. They are no longer pydantic fields and do not appear in
    model_dump(). Each is now a property that emits a DeprecationWarning on
    access:

    • type returns None and has no equivalent, because every event is an
      interval snapshot.
    • code returns None. An event covers every watched code; use
      list(event.snapshot).
    • payload returns None. Use event.snapshot and event.deltas.
    • created_at returns observed_at, the event's timestamp, where it used to
      return None.

    Parsing, polling and serializing events emit no warning.