Skip to content

Latest commit

 

History

History
808 lines (631 loc) · 37.1 KB

File metadata and controls

808 lines (631 loc) · 37.1 KB

Changelog

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

[Unreleased]

[1.16.0] - 2026-09-13

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.

[1.15.0] - 2026-09-13

Fixed

  • Energy Intelligence collection methods now return the collection they promise (#107). Every EI 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. The same defect ran through by_state (states), historical (records), OPEC by_country/historical/top_producers, oil-inventory by_product/historical, drilling-productivity duc_wells/by_basin/historical/trends, forecast historical, and every well-permit and frac-focus collection -- 27 methods, sync and async. Each now returns the named list, and a success body missing that list raises OilPriceAPIError(code="MALFORMED_RESPONSE") instead of handing back the envelope. An empty collection is still an empty list.
  • ei.well_permits.get() and ei.frac_focus.get() return the record, not its wrapper. Production nests these under data.well_permit / data.frac_focus_disclosure, so the documented permit["operator"] raised KeyError.
  • ei.forecasts.prices() and ei.forecasts.production() are typed Dict[str, Any]. Both return a mapping keyed by commodity / series code, never a list; the List[Dict[str, Any]] annotation was wrong from the start. No behaviour change.
  • Docstring examples across the EI resources now use the field names the API actually returns (region/count, not the invented name/rig_count), verified live on 2026-09-13.
  • The ActionCable handshake is bounded, and a failed setup no longer leaks the socket (#108). open_timeout was passed to the WebSocket upgrade and nothing else: the waits for welcome and confirm_subscription that follow had no deadline at all, so a socket that upgraded and then went quiet hung the caller indefinitely, and cancelling out of that hang left the upgraded socket open. Connect, welcome and confirm_subscription are now one bounded setup lifecycle governed by a new setup_timeout (defaulting to open_timeout, so the timeout you already configure does cover protocol setup), and every socket the stream allocates is closed on any failure, timeout or cancellation -- including a __aenter__ that raises, where __aexit__ never runs.
  • A failed reconnect consumes the reconnect budget instead of escaping on the first attempt (#108). An OSError raised while reconnecting inside the ConnectionClosed handler propagated straight out of the iterator, so a stream configured with max_reconnect_attempts=10 gave up after one. Transient failures now spend the configured consecutive-attempt budget with backoff and end in ConnectionError: Stream lost after N reconnect attempts; a permanent refusal stops immediately with the new StreamAuthError (a ConnectionError subclass, so existing handlers are unaffected) rather than retrying a rejected key ten times.
  • close() retires the stream. It is idempotent, closes the socket under a bounded teardown timeout, and prevents any subsequent reconnect; connect() on a closed stream raises instead of quietly opening a new socket. Reconnects now close the socket they are replacing.

Changed

  • The per-method if "data" in response: return response["data"] repeated through every EI resource is replaced by one shared helper (oilpriceapi/resources/ei/_envelopes.py). unwrap_well_permit_search_response keeps its name and its error message and now delegates to it, so there is one unwrapping implementation rather than two.

Behaviour change for callers who adapted to the bug: code reading by_basin()["basins"], well_permits.list()["well_permits"] or well_permits.get(id)["well_permit"] must drop that subscript. Code following the documented signature was broken before and works now. ei.well_permits.latest() and ei.frac_focus.latest() deliberately keep returning the envelope object so their pagination and freshness counters stay reachable.

[1.14.0] - 2026-09-13

Fixed

  • A price response that omits code no longer comes back wearing the code you asked for (#127). _to_price filled an absent code with the requested commodity, so price.commodity == requested -- the one consumer-side check that catches being served a different instrument (#112, mcp-server#90) -- passed by construction on exactly the rows where it needed to fail. An absent code now reads as absent (""), matching what prices.get_all() has always done. Sync and async, every price-read path. Behaviour change: code that read price.commodity on a response without code now sees "" instead of the requested code; use the code you passed in if you need it echoed back.
  • timeout=0 is honoured instead of silently becoming 30. The constructor used timeout or self.DEFAULT_TIMEOUT, so an explicit zero -- a real httpx timeout meaning "fail immediately", and what float(os.getenv("OPA_TIMEOUT", "0")) produces -- was discarded. The caller got a 30-second timeout and a hang that is very hard to attribute back to the constructor. Same defect, and the same line, as the or defaults fixed in 1.13.x for max_retries and retry_on. Both clients.
  • base_url="" no longer points the client at production. It now raises ConfigurationError. Whatever an empty string meant, silently talking to the live API -- and pinning the request-origin guard to an origin the caller did not choose -- was the worst available answer.

Changed

  • max_retries=0 is accepted again, with a DeprecationWarning, and means one attempt. It was briefly rejected with ConfigurationError at client construction, which takes a process down at startup for a value that constructed fine in 1.13.0. max_retries counts total attempts, not retries after the first, so 0 now resolves to 1 -- one attempt, no retries -- and the warning says so. It does not go back to silently meaning 3. 0 will be refused in the next major version; pass max_retries=1 to say "no retries" explicitly.
  • An integral float max_retries (3.0) is coerced with a DeprecationWarning instead of raising. That is what a JSON or YAML config round-trip produces for an integer.
  • Still refused, because no coercion is obviously right: a negative max_retries, a non-integral float (2.5), a string, and bool (True would silently mean one attempt).

Added

  • timeout is validated: a negative or non-numeric value raises ConfigurationError instead of being passed down to httpx unvalidated.

Upgrading

Nothing that worked in 1.13.0 raises in 1.14.0. Two silent behaviours change: timeout=0 now means zero rather than 30, and max_retries=0 now means one attempt rather than three. If you were relying on either of those defaults, pass the value you want explicitly.

[1.13.0] - 2026-08-23

Fixed

  • get_multiple() now batches — up to 20 codes per request instead of one request per code. The REST API accepts 20 commodity codes in a single request that counts once against quota, so the method whose purpose is fetching several prices previously cost up to 20x more quota than writing the call by hand. Through it the free plan was 50 code-reads a day; through the raw API it is 1,000.
  • The same fix is applied to AsyncPricesResource.get_multiple(), which was worse: asyncio.gather fanned out one request per code concurrently, which could also trip the 60-per-60-second rate limit on a long list. Chunks are still gathered concurrently, so 25 codes cost 2 requests rather than 25.

Changed

  • Your quota consumption will drop. This is in your favour and requires no code change, but it is a behaviour change: a get_multiple() call that previously consumed N requests now consumes ceil(N / 20).
  • The per-code failure contract is unchanged. Because the API rejects the whole request when any code in it is unknown, a failed batch is retried per code — for that chunk only — so return_failures=True still reports exactly which code was at fault.

Documentation

  • Polling examples now default to an interval that fits the free plan (30 minutes), with a plan/interval table and the measured update cadence of the underlying data. Nothing we publish moves faster than about every 2.5 minutes, so a shorter timer returns the same number.

[1.12.8] - 2026-08-12

Fixed

  • Validate every customer-readable member in the exact built source distribution, including root release/configuration files and future nested package data, while explicitly excluding intentional test/tooling fixtures.
  • Remove the unsupported universal-entitlement wording from the packaged environment example and reject never-existent promises that attribution headers change entitlements in authored and distributed release notes.

[1.12.7] - 2026-08-12

Fixed

  • Removed a nonexistent request-limit bonus claim from sync and async usage-attribution header comments.
  • Added red-first recursive authored and installed-wheel claim coverage so telemetry or application metadata cannot be presented as changing account entitlements.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[1.12.6] - 2026-08-11

Changed

  • Route Brent, WTI, gasoil, and EU carbon futures through the API's instrument-generic paths in sync and async clients. Existing venue-slug and contract-code inputs remain compatible and normalize to those same paths.

[1.12.5] - 2026-08-11

Added

  • Document a coverage-gated permit-to-production workflow and package discovery keywords for well permits, drilling data, and well production.

Fixed

  • Accept live well-permit filters without a legacy free-form query and unwrap the production { well_permits, meta } search response in sync and async clients while retaining positional-query compatibility.
  • Require the PyPI publisher to verify the complete checksummed artifact set, share one package-version parser, scan both workflow filename extensions, and allow bounded public-index propagation before release completion.

[1.12.4] - 2026-08-11

Fixed

  • Reject common fixed request and API-call rate spellings, including prefix, suffix, and hyphenated daily/hourly/minute forms, and direct examples to the reviewed live product facts.

[1.12.3] - 2026-08-11

Fixed

  • Match hyphenated free-tier wording and universal catalog claims in every readable wheel surface, and replace the remaining packaged docstrings with current-account and runtime-response terminology.

[1.12.2] - 2026-08-11

Fixed

  • Remove stale fixed plan-price, monthly allowance, cadence, uptime, and generic real-time claims from documentation and packaged docstrings.
  • Recursively validate authored docs and package source, then scan the exact installed wheel and PyPI metadata during the release smoke test.

[1.12.1] - 2026-08-11

Fixed

  • Read release metadata without importing the uninstalled source package, so the trusted publisher can validate the exact wheel from a build-only clean environment before PyPI upload.

[1.12.0] - 2026-08-11

Added

  • Add sync and async client.commodities.search(...), backed by the current API catalog rather than a bundled commodity-code list.
  • Expose bounded, credential-redacted suggestions and invalid_codes from nested invalid-code error responses.

Changed

  • Date-bearing resources now reject malformed or impossible YYYY-MM-DD strings locally while leaving well-formed range semantics to the API.
  • Historical DataFrame helpers now accept a per_page value from 1 to 1000 and fetch all pages automatically. The client.prices.to_dataframe(...) convenience path forwards the same option for date-range queries.

Fixed

  • Stop retrying exhausted daily, monthly, and trial quota responses. Sync and async clients now make one request at a durable quota wall while preserving bounded retry behavior for recoverable hourly and ambiguous 429 responses.
  • Replace the demo synthetic's fixed catalogue-size assertion with an integrity contract for the original core codes and every usable returned row. Request, transport, and operating-system failures now fail the monitor instead of being converted to skips.
  • Preserve each API record's currency and unit in current and historical DataFrames instead of labeling a missing currency as USD.
  • Remove exact duplicate records introduced by overlapping page boundaries, stop safely on empty pages with stale continuation metadata, and return a stable schema for empty historical DataFrames.

[1.11.0] - 2026-07-19

Changed

  • Replaced the PyPI storefront with reviewed source-timestamped wording and removed unsupported fixed catalog, traffic, cadence, and entitlement claims.
  • Made the canonical first-request snippet fail closed unless symbol, numeric value, currency, unit, source, and an exact API timestamp field are present.
  • Added a storefront claim guard and corrected the history snippet's declared endpoint to match its executable request path.

Added

  • Well Production Resource (beta): client.well_production (and async mirror) covering /v1/well-production* — summary(), states(), state(), well(), top_producers(), cycle_time(), cycle_time_cohorts(). Per-well data is beta and limited to states with collected regulatory data; endpoints are gated on the Drilling Intelligence feature (403 ENTERPRISE_REQUIRED). Closes #50.

Security

  • Removed a committed API-key fallback from tests/sdk_audit_test.py; the audit script now reads OILPRICEAPI_KEY/OILPRICEAPI_TEST_KEY from the environment only and skips cleanly when unset.

[1.10.2] - 2026-07-10

Changed

  • Loosen source typing and align examples with the API's masked source labels: the response source now returns market_reporting for non-government series (government labels like EIA/opec.org are unchanged). Model source fields remain a free str (no venue enum); test fixtures no longer use venue names such as ICE. See oilpriceapi-api#4175.

[1.10.1] - 2026-07-03

Changed

  • docs: registry storefront README — hero, "What can you get?" commodity table, and cross-SDK toolbox table so the PyPI page matches the other OilPriceAPI SDKs. No code changes.

[1.10.0] - 2026-07-03

Added

  • Analysis Resource (Technical Indicators): client.analysis with with_indicators(df, indicators=[...]) DataFrame helper and direct methods sma(), ema(), rsi(), macd(), bollinger_bands(), atr(). Pure pandas/numpy implementation, no new dependencies. Closes #3.

[1.5.0] - 2026-02-11

Added

  • Commodities Resource: client.commodities.list(), get(code), categories() for commodity catalog discovery
  • Futures Resource: client.futures.latest(), historical(), ohlc(), intraday(), spreads(), curve(), continuous() for futures contract data
  • Storage Resource: client.storage.all(), cushing(), spr(), regional(), history() for oil inventory levels
  • Rig Counts Resource: client.rig_counts.latest(), current(), historical(), trends(), summary() for Baker Hughes rig count data
  • Bunker Fuels Resource: client.bunker_fuels.all(), port(), compare(), spreads(), historical(), export() for marine fuel prices
  • Analytics Resource: client.analytics.performance(), statistics(), correlation(), trend(), spread(), forecast() for price analytics
  • Forecasts Resource: client.forecasts.monthly(), accuracy(), archive(), get() for EIA monthly price forecasts
  • Data Quality Resource: client.data_quality.summary(), reports(), report() for data quality monitoring
  • Drilling Intelligence Resource: client.drilling.latest(), summary(), trends(), frac_spreads(), well_permits(), duc_wells(), completions(), wells_drilled(), basin() for drilling activity data
  • Energy Intelligence Resource: client.ei with 7 sub-resources: rig_counts, oil_inventories, opec_production, drilling_productivity, forecasts, well_permits, frac_focus for comprehensive EIA data
  • Webhooks Resource: client.webhooks.create(), list(), get(), update(), delete(), test(), events() for webhook management
  • Data Sources Resource: client.data_sources.list(), get(), create(), update(), delete(), test(), logs(), health(), rotate_credentials() for data connector management
  • Enhanced Alerts: Added test(), triggers(), analytics_history() methods to existing alerts resource
  • Data Connector Support: client.get_data_connector_prices() for BYOS (Bring Your Own Subscription) prices
  • Telemetry Headers: app_url and app_name parameters for API usage attribution

Fixed

  • Diesel validation: Empty string state codes now properly rejected with ValidationError

Testing

  • 84 new unit tests added (222 total, 0 failures)
  • Test coverage improved from ~40% to 60%
  • New test files for all 13 resource modules

Breaking Changes

None - All new resources are additive. Existing code continues to work unchanged.

[1.4.3] - 2025-12-17

Fixed

  • CRITICAL: Historical Data Returns Wrong Commodity: Fixed issue where all historical queries returned BRENT_CRUDE_USD regardless of requested commodity

    • Root cause: SDK was sending commodity parameter but API expects by_code parameter
    • Impact: ALL historical queries since v1.4.0 returned incorrect data
    • Solution: Changed parameter name from commodity to by_code in historical resource
    • Reported by: Idan (idan@comity.ai)
  • Date Range Parameters Ignored: Fixed issue where start_date and end_date parameters were completely ignored

    • Root cause: API endpoints were hardcoded to return last week/month/year from current date
    • Impact: Requesting specific date ranges (e.g., Jan 2024) would return current period instead
    • Solution: API now respects start_date and end_date parameters across all historical endpoints
    • This fix was applied to the backend API simultaneously

Added

  • Strict Commodity Validation: API now validates commodity codes and returns clear error messages for invalid codes
    • Before: Silently accepted invalid codes like "oijfoijofwijewef" and returned BRENT data
    • After: Returns 400 Bad Request with list of valid codes
    • Error includes link to /v1/prices/metrics for full list of valid commodity codes

Breaking Changes

None - This is a critical bug fix. Existing code will work correctly after update.

Upgrade Priority

CRITICAL - All users of client.historical.get() should upgrade immediately. Previous versions return completely wrong data.

[1.4.2] - 2025-12-16

Fixed

  • Historical Queries Timeout Issue: Fixed 100% timeout rate on historical data requests
    • Root cause: SDK was using hardcoded /v1/prices/past_year endpoint for all date ranges
    • Solution: Implemented intelligent endpoint selection based on date range
      • 1 day range → /v1/prices/past_day endpoint
      • 7 day range → /v1/prices/past_week endpoint
      • 30 day range → /v1/prices/past_month endpoint
      • 365 day range → /v1/prices/past_year endpoint
    • Performance improvement: 7x faster for 1 week queries, 3x faster for 1 month queries

Added

  • Dynamic Timeout Management: Automatic timeout adjustment based on query size
    • 1 week queries: 30 seconds (previously 30s, but now uses optimal endpoint)
    • 1 month queries: 60 seconds
    • 1 year queries: 120 seconds (up from 30s - fixes timeout issue)
    • Custom timeout override: historical.get(..., timeout=180) for very large queries
  • Per-Request Timeout Override: Added timeout parameter to client.request() method
    • Allows fine-grained timeout control for specific requests
    • Historical resource automatically uses appropriate timeouts

Performance

  • 1 week historical queries: 67s → ~10s (7x faster via /past_week endpoint)
  • 1 month historical queries: 67s → ~20s (3x faster via /past_month endpoint)
  • 1 year historical queries: Timeout (30s) → Success (67-85s with 120s timeout)

Testing

  • Added 9 new tests for endpoint selection and timeout handling
  • All 20 existing tests pass with new changes
  • Test coverage for historical.py: 88.68% (up from ~54%)

Documentation

  • Updated historical.get() docstring with timeout parameter examples
  • Added clear examples for custom timeout usage

Breaking Changes

None - This is a backwards-compatible bug fix. Existing code will continue to work and will automatically benefit from performance improvements.

[1.4.0] - 2025-12-15

Added

  • Price Alerts: New client.alerts resource for automated price monitoring
  • Alert CRUD Operations: Complete create, read, update, delete operations
  • Webhook Notifications: HTTPS webhook support for alert triggers
  • Alert Operators: 5 comparison operators (greater_than, less_than, equals, greater_than_or_equal, less_than_or_equal)
  • Cooldown Periods: Rate limiting for alert triggers (0-1440 minutes)
  • Webhook Testing: Test webhook endpoints before creating alerts
  • DataFrame Support: alerts.to_dataframe() - Convert alerts to pandas DataFrames
  • New Pydantic models:
    • PriceAlert - Alert configuration and status
    • WebhookTestResponse - Webhook test results

Features

  • Comprehensive Validation: Input validation for all alert parameters
  • Type Safety: Full Pydantic models with datetime handling
  • Error Handling: Specific ValidationError exceptions with field details
  • Pandas Integration: Built-in DataFrame conversion for analysis
  • Documentation: Complete docstrings with examples

Supported Endpoints

Now supports 12 endpoints (up from 7):

  • GET /v1/prices/latest - Get latest commodity prices
  • GET /v1/prices - Get historical commodity prices
  • GET /v1/commodities - Get all commodities metadata
  • GET /v1/commodities/categories - Get commodity categories
  • GET /v1/commodities/{code} - Get specific commodity details
  • GET /v1/diesel-prices - Get state average diesel prices
  • POST /v1/diesel-prices/stations - Get nearby diesel stations
  • GET /v1/alerts - List all price alerts (NEW)
  • GET /v1/alerts/{id} - Get specific alert (NEW)
  • POST /v1/alerts - Create price alert (NEW)
  • PATCH /v1/alerts/{id} - Update price alert (NEW)
  • DELETE /v1/alerts/{id} - Delete price alert (NEW)

Testing

  • Added comprehensive test suite for alerts resource (22 test cases)
  • Tests cover all CRUD operations, validation, webhook testing, and DataFrame operations
  • 82% coverage of alerts functionality

Breaking Changes

None - This is a backwards-compatible feature addition.

Example Usage

from oilpriceapi import OilPriceAPI

client = OilPriceAPI()

# Create a price alert
alert = client.alerts.create(
    name="Brent High Alert",
    commodity_code="BRENT_CRUDE_USD",
    condition_operator="greater_than",
    condition_value=85.00,
    webhook_url="https://your-server.com/webhook",
    cooldown_minutes=60
)

# List all alerts
alerts = client.alerts.list()
for alert in alerts:
    print(f"{alert.name}: {alert.trigger_count} triggers")

# Update alert
client.alerts.update(alert.id, condition_value=90.00)

# Test webhook
test_result = client.alerts.test_webhook("https://your-server.com/webhook")
print(f"Webhook OK: {test_result.success}")

# Get as DataFrame
df = client.alerts.to_dataframe()

[1.3.0] - 2025-12-15

Added

  • Diesel Prices Support: New client.diesel resource for diesel price data
  • State Average Diesel Prices: diesel.get_price(state) - Get EIA state-level diesel averages (free tier)
  • Station-Level Diesel Pricing: diesel.get_stations(lat, lng, radius) - Get nearby diesel stations with current prices from Google Maps (paid tiers)
  • Diesel DataFrame Support: diesel.to_dataframe() - Convert diesel data to pandas DataFrames
  • New Pydantic models:
    • DieselPrice - State average diesel price data
    • DieselStation - Individual diesel station with pricing
    • DieselStationsResponse - Response from stations endpoint
    • DieselRegionalAverage - Regional average for comparison
    • DieselSearchArea - Search area details
    • DieselStationsMetadata - Query metadata

Features

  • Input Validation: Comprehensive validation for coordinates, state codes, and radius
  • Error Handling: Specific errors for tier restrictions (403) and rate limits (429)
  • Type Safety: Full Pydantic models for all diesel operations
  • Pandas Integration: Built-in DataFrame conversion for analysis
  • Documentation: Complete docstrings with examples

Supported Endpoints

Now supports 7 endpoints (up from 5):

  • GET /v1/prices/latest - Get latest commodity prices
  • GET /v1/prices - Get historical commodity prices
  • GET /v1/commodities - Get all commodities metadata
  • GET /v1/commodities/categories - Get commodity categories
  • GET /v1/commodities/{code} - Get specific commodity details
  • GET /v1/diesel-prices - Get state average diesel prices (NEW)
  • POST /v1/diesel-prices/stations - Get nearby diesel stations (NEW)

Testing

  • Added comprehensive test suite for diesel resource (18 test cases)
  • Tests cover input validation, error handling, and DataFrame operations
  • 100% coverage of diesel functionality

Breaking Changes

None - This is a backwards-compatible feature addition.

Example Usage

from oilpriceapi import OilPriceAPI

client = OilPriceAPI()

# State average (free tier)
ca_price = client.diesel.get_price("CA")
print(f"California diesel: ${ca_price.price:.2f}/gallon")

# Nearby stations (paid tiers)
result = client.diesel.get_stations(lat=37.7749, lng=-122.4194)
cheapest = min(result.stations, key=lambda s: s.diesel_price)
print(f"Cheapest: {cheapest.name} at {cheapest.formatted_price}")

# DataFrame analysis
df = client.diesel.to_dataframe(states=["CA", "TX", "NY", "FL"])
print(df[["state", "price", "updated_at"]])

[1.0.0] - 2025-09-29

Added

  • 🎉 Initial release of OilPriceAPI Python SDK
  • ✅ Synchronous client (OilPriceAPI)
  • ✅ Asynchronous client (AsyncOilPriceAPI)
  • ✅ Type-safe models with Pydantic
  • ✅ Current price operations (client.prices.get())
  • ✅ Historical data operations (client.historical.get())
  • ✅ Pandas DataFrame integration (to_dataframe())
  • ✅ Visualization module with Tufte-style charts
  • ✅ Automatic retry logic with exponential backoff
  • ✅ Rate limit handling
  • ✅ Comprehensive error handling
  • ✅ Context manager support (with statements)
  • ✅ Environment variable configuration
  • ✅ Full type hints for IDE autocomplete
  • ✅ Documentation and examples

Features

  • Current Prices: Get latest commodity prices
  • Historical Data: Fetch past prices with flexible date ranges
  • Multi-commodity: Support for Brent, WTI, Natural Gas, and more
  • Pagination: Automatic handling of large datasets
  • Data Export: Convert to pandas DataFrames for analysis
  • Async Support: High-performance async/await operations
  • Visualization: Built-in charting with matplotlib
  • Type Safety: Full Pydantic validation

Security

  • Environment variable-based API key management
  • No hardcoded credentials
  • HTTPS-only communication
  • Safe error messages that don't leak secrets

Documentation

  • Comprehensive README with examples
  • API reference documentation
  • Security policy (SECURITY.md)
  • Contributing guidelines (CONTRIBUTING.md)
  • Example scripts and notebooks

Supported Python Versions

  • Python 3.8+
  • Python 3.9
  • Python 3.10
  • Python 3.11
  • Python 3.12

Release Notes

How to Upgrade

# From PyPI
pip install --upgrade oilpriceapi

# From source
pip install -e ".[dev]"

Breaking Changes

None - this is the initial release.

Deprecations

None.

Migration Guide

N/A for initial release.


Links