Skip to content

Commit 8bd6b50

Browse files
karlwaldmanclaude
andauthored
feat(fuel-surcharge): typed LTL and parcel fuel-surcharge clients (#101) (#144)
* feat(fuel-surcharge): typed LTL and parcel fuel-surcharge clients (#101) Add client.fuel_surcharge on OilPriceAPI and AsyncOilPriceAPI covering all six /v1/fuel-surcharge routes, with FuelSurchargeRate, FuelSurchargeHistoryPage and ParcelFuelSurchargeCarrier models typed from production payloads captured 2026-09-13. - effective_date is a date, retrieved_at a tz-aware datetime; source, nullable doe_diesel_price and diesel_band are preserved as sent. - A success body missing a field the API always sends raises OilPriceAPIError(code="MALFORMED_RESPONSE"); nothing is defaulted. - Carrier slugs, service levels and pagination are validated before the request; out-of-range page/per_page are refused because the API clamps them silently (verified live: per_page=500&page=0 -> meta page 1/100). - covered_carriers / available_service_levels from 400/404 bodies populate error.suggestions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo * test(fuel-surcharge): pin local refusals to ValidationError(field, value, status_code=None) (#101) Review follow-up on #144. The carrier, service_level, page and per_page guards already raised ValidationError with status_code=None (the _url._reject convention); the tests only asserted that for carrier on latest(). Every method that takes a carrier or service level, and both history routes' pagination, now assert the exact type, status_code None, is_client_error False, field, value and zero transport calls, on sync and async. Red-capability: swapping the slug guard to a raw ValueError fails 86 of 86 selected refusal tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 4bd2900 commit 8bd6b50

14 files changed

Lines changed: 1706 additions & 2 deletions

‎CHANGELOG.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,29 @@ All notable changes to the OilPriceAPI Python SDK will be documented in this fil
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- **Typed LTL and parcel fuel-surcharge clients (#101).** `client.fuel_surcharge`
10+
on both `OilPriceAPI` and `AsyncOilPriceAPI` covers all six
11+
`/v1/fuel-surcharge` routes: `list()`, `latest(carrier)`,
12+
`history(carrier, page=, per_page=)`, `parcel_list()`,
13+
`parcel_latest(carrier)`, `parcel_latest_rate(carrier, service_level)` and
14+
`parcel_history(carrier, service_level, page=, per_page=)`. Responses are
15+
`FuelSurchargeRate`, `FuelSurchargeHistoryPage` (with the server's
16+
`meta`) and `ParcelFuelSurchargeCarrier` models typed from production
17+
payloads captured on 2026-09-13: `effective_date` is a `date`,
18+
`retrieved_at` a timezone-aware `datetime`, and `source`, nullable
19+
`doe_diesel_price` and `diesel_band` are kept as sent. A success body
20+
missing a field the API always sends raises
21+
`OilPriceAPIError(code="MALFORMED_RESPONSE")` instead of defaulting it.
22+
Carrier slugs, service levels and pagination are validated before any
23+
request; out-of-range `page`/`per_page` are refused because the API clamps
24+
them silently.
25+
- **Fuel-surcharge 400/404 bodies populate `error.suggestions`.** The
26+
`covered_carriers` and `available_service_levels` lists the API returns with
27+
an unknown carrier or a missing service level are now surfaced the same way
28+
commodity suggestions are.
29+
730
## [1.15.0] - 2026-09-13
831

932
### Fixed

‎README.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,33 @@ An empty permit search or production history is a valid data state. Do not
186186
infer broader well-level coverage from the presence of permit data or an SDK
187187
helper; dataset and account availability come from the current API response.
188188

189+
## Carrier Fuel Surcharges
190+
191+
Weekly fuel surcharges for LTL carriers and, per service level, for parcel
192+
carriers. Each rate keeps the carrier's `effective_date` and the `source` URL
193+
and `retrieved_at` time it was retrieved from; a null the API sends (for
194+
example `doe_diesel_price` on parcel rates) stays `None`.
195+
196+
```python
197+
import os
198+
199+
from oilpriceapi import OilPriceAPI
200+
201+
with OilPriceAPI(api_key=os.environ["OILPRICEAPI_KEY"]) as client:
202+
odfl = client.fuel_surcharge.latest("odfl")
203+
history = client.fuel_surcharge.history("odfl", per_page=10)
204+
ups_ground = client.fuel_surcharge.parcel_latest_rate("ups", "ground")
205+
206+
print(odfl.surcharge_percent, odfl.effective_date, odfl.source)
207+
print(history.meta.total_count, [row.effective_date for row in history.history])
208+
print(ups_ground.surcharge_percent, ups_ground.service_level)
209+
```
210+
211+
An unknown or uncovered carrier raises `DataNotFoundError` with the covered
212+
carriers in `error.suggestions`. `page` must be 1 or more and `per_page` 1 to
213+
100; the SDK refuses other values rather than letting the API clamp them.
214+
See [`examples/fuel_surcharge.py`](examples/fuel_surcharge.py).
215+
189216
## Complete pandas DataFrames
190217

191218
Install the optional pandas support, then request a historical DataFrame:

‎docs/reference/resources.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252

5353
::: oilpriceapi.resources.drilling.DrillingIntelligenceResource
5454

55+
## Fuel Surcharges
56+
57+
::: oilpriceapi.resources.fuel_surcharge.FuelSurchargeResource
58+
5559
## Well Production (Beta)
5660

5761
::: oilpriceapi.resources.well_production.WellProductionResource

‎examples/fuel_surcharge.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Carrier fuel surcharges: LTL and parcel (#101).
2+
3+
Usage:
4+
OILPRICEAPI_KEY=... python examples/fuel_surcharge.py
5+
6+
Prints the latest LTL surcharge per carrier, one carrier's recent weekly
7+
history, and the latest parcel surcharge per service level. Every row shows the
8+
carrier's effective date and where the value was retrieved from.
9+
"""
10+
11+
import os
12+
13+
from oilpriceapi import OilPriceAPI
14+
from oilpriceapi.exceptions import DataNotFoundError
15+
16+
17+
def main() -> None:
18+
with OilPriceAPI(api_key=os.environ["OILPRICEAPI_KEY"]) as client:
19+
print("LTL carriers")
20+
rates = client.fuel_surcharge.list()
21+
for rate in rates:
22+
print(
23+
f" {rate.carrier:<22} {rate.surcharge_percent:>6.2f}% "
24+
f"effective {rate.effective_date} retrieved {rate.retrieved_at:%Y-%m-%d}"
25+
)
26+
27+
if rates:
28+
carrier = rates[0].carrier
29+
page = client.fuel_surcharge.history(carrier, per_page=4)
30+
print(f"\n{carrier} history ({page.meta.total_count} weeks on record)")
31+
for row in page.history:
32+
diesel = "n/a" if row.doe_diesel_price is None else f"${row.doe_diesel_price:.3f}"
33+
print(f" {row.effective_date} {row.surcharge_percent:.2f}% DOE diesel {diesel}")
34+
print(f" source: {page.history[0].source}" if page.history else " no rows")
35+
36+
print("\nParcel carriers")
37+
for parcel in client.fuel_surcharge.parcel_list():
38+
for rate in parcel.service_levels:
39+
print(
40+
f" {parcel.carrier:<6} {rate.service_level:<26} "
41+
f"{rate.surcharge_percent:>6.2f}% effective {rate.effective_date}"
42+
)
43+
44+
try:
45+
client.fuel_surcharge.latest("fedex-freight")
46+
except DataNotFoundError as error:
47+
print(f"\nNot covered: {error.message}")
48+
print(f"Covered carriers: {', '.join(error.suggestions)}")
49+
50+
51+
if __name__ == "__main__":
52+
main()

‎oilpriceapi/__init__.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,14 @@
3333
DieselPrice,
3434
DieselStation,
3535
DieselStationsResponse,
36+
FuelSurchargeDieselBand,
37+
FuelSurchargeHistoryMeta,
38+
FuelSurchargeHistoryPage,
39+
FuelSurchargeRate,
3640
MarketBrief,
3741
MarketBriefCommodity,
3842
MarketBriefForecast,
43+
ParcelFuelSurchargeCarrier,
3944
PriceAlert,
4045
Subscription,
4146
SubscriptionEvent,
@@ -76,6 +81,11 @@
7681
"MarketBrief",
7782
"MarketBriefCommodity",
7883
"MarketBriefForecast",
84+
"FuelSurchargeRate",
85+
"FuelSurchargeDieselBand",
86+
"FuelSurchargeHistoryMeta",
87+
"FuelSurchargeHistoryPage",
88+
"ParcelFuelSurchargeCarrier",
7989
"Subscription",
8090
"SubscriptionEvent",
8191
"SubscriptionEventsPage",

0 commit comments

Comments
 (0)