feat(fuel-surcharge): typed LTL and parcel fuel-surcharge clients (#101) - #144
Merged
Merged
Conversation
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
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 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. Comment |
…lue, 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
karlwaldman
added a commit
that referenced
this pull request
Sep 13, 2026
…ifecycle Resolves the single conflict in CHANGELOG.md by keeping both sides: the subscription lifecycle and fuel-surcharge entries under [Unreleased] Added, with the subscription fixes under Fixed. All other files auto-merged; client.fuel_surcharge and client.subscriptions remain registered on both the sync and async clients (verified by instantiating both). Full suite on the merged tree: 1716 passed / 3 failed (known live demo 429s) / 66 skipped. ruff and mypy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
karlwaldman
added a commit
that referenced
this pull request
Sep 13, 2026
…/99-spreads-indicators Conflicts resolved by keeping every side: - CHANGELOG.md: #99 spreads/indicators entry, then the #100 and #101 Added entries and the #100 Fixed entries, all under [Unreleased]. - oilpriceapi/async_client.py: spreads, indicators and fuel_surcharge all registered (subscriptions untouched). - oilpriceapi/async_resources.py: metrics_models import plus main's multi-line models import; AsyncFuelSurchargeResource kept whole, followed by AsyncSpreadsResource and AsyncIndicatorsResource. Verified on the merged tree by instantiating both clients: spreads, indicators, fuel_surcharge and subscriptions and all their methods exist (88/88). Full suite 1859 passed / 3 failed (known live demo 429s) / 68 skipped; ruff, mypy and storefront validator clean. The .gitignore fixture exception and all 30 fixtures survive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #101.
What
client.fuel_surchargeon bothOilPriceAPIandAsyncOilPriceAPI, covering all six routes inV1::FuelSurchargeController(verified onoilpriceapi-apiorigin/main,config/routes.rbscopefuel-surcharge):list()GET /v1/fuel-surchargeList[FuelSurchargeRate]latest(carrier)GET /v1/fuel-surcharge/{carrier}/latestFuelSurchargeRatehistory(carrier, page=, per_page=)GET /v1/fuel-surcharge/{carrier}/historyFuelSurchargeHistoryPageparcel_list()GET /v1/fuel-surcharge/parcelList[ParcelFuelSurchargeCarrier]parcel_latest(carrier)GET /v1/fuel-surcharge/parcel/{carrier}/latestParcelFuelSurchargeCarrierparcel_latest_rate(carrier, service_level)?service_level=FuelSurchargeRateparcel_history(carrier, service_level, page=, per_page=)GET /v1/fuel-surcharge/parcel/{carrier}/historyFuelSurchargeHistoryPageThe parcel
latestroute returns two different shapes depending on whetherservice_levelis sent, so the SDK exposes two methods with distinct return types instead of a union the caller must sniff.Sync and async share one module (
oilpriceapi/_fuel_surcharge_common.py) for path building, validation and parsing.Wire shapes typed against (production, 2026-09-13, test key)
Rate object (
rate_payloadin the controller). Every key is always emitted exceptservice_level, which is emitted only for parcel rows:diesel_bandisnullor{"min":5.57,"max":5.6}(Southeastern Freight); each bound is nullable in the DB.doe_diesel_priceisnullfor ABF, TForce and every parcel row.{"history":[rate...],"meta":{"page":2,"per_page":3,"total_count":6,"total_pages":2}}{"carrier":"ups","carrier_name":"UPS","mode":"parcel","service_levels":[rate+service_level...]}{"status":"fail","data":{...}}:error+covered_carriers+hintfedex-freight): 404 witherror+covered_carriersservice_level: 400 withavailable_service_levels{"error":{"code":"UNAUTHORIZED",...}}.Typing and fail-closed decisions
effective_dateis adatetime.date. Only aYYYY-MM-DDstring is accepted (pydantic lax mode would otherwise turn0into 1970-01-01).retrieved_atis a timezone-awaredatetime; a naive timestamp is rejected."46.32"andtrueare rejected.sourcemust be non-empty.OilPriceAPIError(code="MALFORMED_RESPONSE")withraw_bodykept; anullstaysNone.^[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?$before any request is built.page >= 1and1 <= per_page <= 100are enforced locally. The API silently clamps: verified live,per_page=500&page=0returnedmeta {"page":1,"per_page":100}. The SDK refuses rather than return a different page than the one requested.exceptions.error_from_responsenow also readscovered_carriersandavailable_service_levelsintoerror.suggestions. Before, those lists survived only inraw_body.TDD evidence
Red (tests written first;
oilpriceapihad no fuel-surcharge models or resource):The first implementation run was also red on a real gap:
parcel_history("ups", None)was not refused locally.Green:
All 147 tests run the real sync and async clients against a mocked transport (
httpx.Client.request/httpx.AsyncClient.request) and assert the path and params actually sent. They cover:Success fixtures are trimmed production captures; the 402/403/429 bodies are canonical envelopes used only to test status mapping.
Full suite, ruff, mypy, storefront:
Compared with the main baseline (1229 passed / 3 failed / 63 skipped):
/v1/democontract tests, which are untouched by this PR.Review follow-up: local refusals are ValidationError, not ValueError (
ea37a13)No code change was needed. Every local input refusal on the new methods raises the repo convention, the same way
_url._rejectdoes:validate_sluginoilpriceapi/_fuel_surcharge_common.py._positive_intin the same file.status_code=Nonebecause no request was sent.raise ValueErrorrefusals in the non-model PR code (count: 0).The four
raise ValueErroradded inoilpriceapi/models.pyare pydanticfield_validators (effective_date,retrieved_at). Pydantic requires validators to raiseValueError. They never reach the caller asValueError:_buildconverts them toOilPriceAPIError(code="MALFORMED_RESPONSE"), which the malformed-200 tests assert.No existing public method changed its exception type. All seven fuel-surcharge methods are new, so no dual-base subclass is needed.
What changed is the tests. Before, only
latest()assertedstatus_code is Nonefor a bad carrier. Now these all go through_assert_local_refusal, which checkstype(error) is ValidationError,status_code is None,is_client_error is False,field,value, and zero transport calls, on sync and async:Red-capability proof: the slug guard was temporarily changed to
raise ValueError(f"{field} is not a valid slug"), run, then restored (restored diff: 0 lines).After restore:
Against the main baseline (1229 / 3 / 63), this branch now adds 223 unit tests and 2 skipped live tests. The 3 failures are the same live
/v1/democontract tests.Live smoke: RAN, passed
Read-only. The carrier and service level come from the list responses, not hard-coded.
Docs
README.md: new "Carrier Fuel Surcharges" sectiondocs/reference/resources.mdexamples/fuel_surcharge.pyCHANGELOG.mdunder## [Unreleased]No version bump, no tag. No snippet-manifest entry:
tests/test_snippet_manifest.pyexecutes every manifest snippet against a fixture server that only serves/v1/prices/*.Premise corrections (also commented on #101)
🤖 Generated with Claude Code
https://claude.ai/code/session_015ao5paex73xXvuM424Libo