Skip to content

Commit 5b47e04

Browse files
karlwaldmanclaude
andcommitted
fix(ei): return the named collection EI methods promise (#107)
Every Energy Intelligence method typed `List[Dict[str, Any]]` returned `response["data"]`, but the EI controllers put their records under a *named* key inside `data`. `client.ei.rig_counts.by_basin()` returned `{"report_date": ..., "basins": [...]}` where the signature and the docstring example promised the basin list, so the documented `for basin in basins: basin["count"]` iterated dict keys. Verified live against https://api.oilpriceapi.com on 2026-09-13 with a Scale-tier key: 27 methods across seven EI resources are wrong the same way, in both the sync resources and `async_resources.py`. - `unwrap_ei_collection` / `unwrap_ei_object` / `ei_data` in `oilpriceapi/resources/ei/_envelopes.py` are now the single place that knows the envelope shape. The per-method `if "data" in response: return response["data"]` is gone, and `unwrap_well_permit_search_response` keeps its name and its error message but delegates to the shared helper instead of carrying a second copy. - A success body missing the named collection raises `OilPriceAPIError(code="MALFORMED_RESPONSE")` instead of handing back the envelope. An empty collection stays an empty list. - `ei.well_permits.get()` / `ei.frac_focus.get()` return the record rather than its `{"well_permit": ...}` wrapper. - `ei.forecasts.prices()` / `.production()` are typed `Dict[str, Any]`: both return a mapping keyed by commodity / series code, never a list. - `ei.well_permits.latest()` / `ei.frac_focus.latest()` deliberately keep returning the envelope so their pagination and freshness counters stay reachable; their docstrings now say so. - Docstring examples use the field names the API actually returns. Tests drive the real client transport (respx over httpx), not a stubbed resource, because the defect lives between the HTTP body and the return value. Valid, empty, missing-collection, malformed-row, 401/403/429 and async parity are covered, plus a source-level guard that every async EI method's return expression matches its sync twin character for character. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
1 parent 7982b0b commit 5b47e04

13 files changed

Lines changed: 1587 additions & 543 deletions

‎CHANGELOG.md‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,53 @@
22

33
All notable changes to the OilPriceAPI Python SDK will be documented in this file.
44

5+
## [Unreleased]
6+
7+
### Fixed
8+
9+
- **Energy Intelligence collection methods now return the collection they
10+
promise (#107).** Every EI method typed `List[Dict[str, Any]]` returned
11+
`response["data"]` -- but the EI controllers put their records under a *named*
12+
key inside `data`. `client.ei.rig_counts.by_basin()` returned
13+
`{"report_date": ..., "basins": [...]}` where the signature and the docstring
14+
example promised the basin list, so the documented
15+
`for basin in basins: basin["count"]` iterated dict *keys*. The same defect
16+
ran through `by_state` (`states`), `historical` (`records`), OPEC
17+
`by_country`/`historical`/`top_producers`, oil-inventory
18+
`by_product`/`historical`, drilling-productivity
19+
`duc_wells`/`by_basin`/`historical`/`trends`, forecast `historical`, and
20+
every well-permit and frac-focus collection -- 27 methods, sync and async.
21+
Each now returns the named list, and a success body missing that list raises
22+
`OilPriceAPIError(code="MALFORMED_RESPONSE")` instead of handing back the
23+
envelope. An empty collection is still an empty list.
24+
- **`ei.well_permits.get()` and `ei.frac_focus.get()` return the record, not its
25+
wrapper.** Production nests these under `data.well_permit` /
26+
`data.frac_focus_disclosure`, so the documented `permit["operator"]` raised
27+
`KeyError`.
28+
- **`ei.forecasts.prices()` and `ei.forecasts.production()` are typed
29+
`Dict[str, Any]`.** Both return a mapping keyed by commodity / series code,
30+
never a list; the `List[Dict[str, Any]]` annotation was wrong from the start.
31+
No behaviour change.
32+
- **Docstring examples across the EI resources now use the field names the API
33+
actually returns** (`region`/`count`, not the invented `name`/`rig_count`),
34+
verified live on 2026-09-13.
35+
36+
### Changed
37+
38+
- The per-method `if "data" in response: return response["data"]` repeated
39+
through every EI resource is replaced by one shared helper
40+
(`oilpriceapi/resources/ei/_envelopes.py`).
41+
`unwrap_well_permit_search_response` keeps its name and its error message and
42+
now delegates to it, so there is one unwrapping implementation rather than
43+
two.
44+
45+
**Behaviour change for callers who adapted to the bug:** code reading
46+
`by_basin()["basins"]`, `well_permits.list()["well_permits"]` or
47+
`well_permits.get(id)["well_permit"]` must drop that subscript. Code following
48+
the documented signature was broken before and works now. `ei.well_permits.latest()`
49+
and `ei.frac_focus.latest()` deliberately keep returning the envelope object so
50+
their pagination and freshness counters stay reachable.
51+
552
## [1.14.0] - 2026-09-13
653

754
### Fixed
@@ -112,8 +159,6 @@ pass the value you want explicitly.
112159
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
113160
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
114161

115-
## [Unreleased]
116-
117162
## [1.12.6] - 2026-08-11
118163

119164
### Changed

0 commit comments

Comments
 (0)