Skip to content

fix(diesel): unwrap the API envelope before building DieselPrice (#110) - #114

Merged
karlwaldman merged 1 commit into
mainfrom
fix/110-diesel-envelope
Sep 13, 2026
Merged

karlwaldman merged 1 commit into
mainfrom
fix/110-diesel-envelope

Conversation

@karlwaldman

Copy link
Copy Markdown
Member

Closes #110.

Confirmed live, not already fixed

Re-verified against production on 2026-09-13 with the smoke-test key (GET only; no credential inlined anywhere).

Before:

RAW KEYS: ['status', 'data']   DATA KEYS: ['regional_average', 'sources', 'upgrade', 'location']
REGIONAL: {"price": 8.136, "currency": "USD", "unit": "gallon", "region": "california",
           "granularity": "state", "source": "aaa", "updated_at": "2026-09-13T14:57:19Z", "cached": true}
SDK FAILS: ValidationError
7 validation errors for DieselPrice
state
  Field required [type=missing, input_value={'regional_average': {'pr...e', 'state_code': 'CA'}}, input_type=dict]
price
  Field required [type=missing, ...]
...

The raw client.request() form returns 200 with the same call, so this is an SDK envelope bug, not an API change.

Root cause

resources/diesel.py looked for regional_average at the top level, missed it (it is nested under data), fell through to the "data" in response branch, and handed the whole data object — regional_average / sources / upgrade / location — to DieselPrice(**...).

There is a second half the issue did not name: even reading data["regional_average"] is not enough. That record carries region ("california"), not the state field DieselPrice requires. So the fix has two parts:

  1. Unwrap data first, then take regional_average (a small _payload() helper, used by get_price and get_stations alike).
  2. Fill state from the envelope's location.state_code, falling back to the state code the caller passed.

get_stations had the same shape bug — DieselStationsResponse(**response) now unpacks _payload(response).

Red

Test written first, against unmodified origin/main:

$ .venv/bin/python -m pytest tests/unit/test_diesel_envelope.py --no-cov -q
E       unit
E         Field required [type=missing, input_value={'regional_average': {'pr...e', 'state_code': 'CA'}}, input_type=dict]
E       granularity
E         Field required [type=missing, ...]
E       source
E         Field required [type=missing, ...]
E       updated_at
E         Field required [type=missing, ...]

oilpriceapi/resources/diesel.py:96: ValidationError
=========================== short test summary info ============================
FAILED tests/unit/test_diesel_envelope.py::test_get_price_parses_the_live_production_envelope
FAILED tests/unit/test_diesel_envelope.py::test_get_price_fills_state_from_the_envelope_location
FAILED tests/unit/test_diesel_envelope.py::test_get_price_falls_back_to_the_requested_state_when_location_is_absent
FAILED tests/unit/test_diesel_envelope.py::test_get_stations_parses_the_data_envelope
FAILED tests/unit/test_diesel_envelope.py::test_to_dataframe_state_works_on_the_live_envelope
========================= 5 failed, 3 passed in 0.36s ==========================

The fixture in the new test file is the response body captured live from production, verbatim.

Green

$ .venv/bin/python -m pytest tests/unit/test_diesel_envelope.py tests/unit/test_diesel_resource.py --no-cov -q
============================== 31 passed in 0.47s ==============================

And against production, same script, after the fix:

SDK OK: state='CA' price=8.136 currency='USD' unit='gallon' granularity='state'
        source='aaa' updated_at=2026-09-13 14:57:19+00:00 cached=True

That matches the raw client.request() payload exactly.

get_stations is verified by signature and fixtures only — it is a POST, so it was not called against production.

Pre-existing failures unchanged

tests/unit on clean origin/main: 109 failed, 453 passed (mostly test_streaming.py).
tests/unit on this branch: 109 failed, 461 passed — same 109, +8 new passing.

(Both runs with pandas installed, so the DataFrame tests execute rather than skip. Without pandas the baseline reads 109 failed / 429 passed / 13 skipped.)

Compatibility

The older top-level {"regional_average": {...}} shape and a flat record both still parse, so every existing fixture in test_diesel_resource.py is untouched and still green. The unwrap only fires when response["data"] is a dict.

Follow-up

The diesel docs page carries a warning added because of this bug. Once this ships to PyPI, that warning can come out.

🤖 Generated with Claude Code

https://claude.ai/code/session_015ao5paex73xXvuM424Libo

client.diesel.get_price() raised pydantic ValidationError with 7 errors on
every call. GET /v1/diesel-prices?state=CA returns

    {"status": "success",
     "data": {"regional_average": {...}, "sources": {...},
              "upgrade": {...}, "location": {"type": "state", "state_code": "CA"}}}

The resource looked for `regional_average` at the TOP level, missed it because
it is nested under `data`, fell through to the `"data" in response` branch and
handed the whole `data` object to DieselPrice(**...).

Two fixes:

  * unwrap `data` first, then take `regional_average` — for both get_price()
    and get_stations()
  * `regional_average` carries `region` ("california"), not the `state` the
    model requires, so fill `state` from the envelope's location.state_code,
    falling back to the state the caller asked for

Verified live against production on 2026-09-13 with the smoke-test key:
client.diesel.get_price("CA") now returns state='CA' price=8.136 currency='USD'
unit='gallon' granularity='state' source='aaa' cached=True, matching the raw
client.request() form exactly.

get_stations() is a POST, so it is verified by signature and fixtures only —
not called against production.

The older top-level and flat shapes still parse, so the existing
test_diesel_resource.py fixtures are untouched and still green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: e8a144cb-8d99-4053-9fb0-a927ad51af57


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@karlwaldman
karlwaldman merged commit b7017b7 into main Sep 13, 2026
7 checks passed
@karlwaldman
karlwaldman deleted the fix/110-diesel-envelope branch September 13, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P1] client.diesel.* raises pydantic ValidationError on v1.13.0 — envelope unpacked one level too high

1 participant