Skip to content

Commit f17936a

Browse files
authored
Use instrument-generic futures routes in Python 1.12.6 (#90)
* fix: use canonical futures routes * test: bind canonical futures examples per client
1 parent c4e222e commit f17936a

11 files changed

Lines changed: 269 additions & 69 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.12.6] - 2026-08-11
11+
12+
### Changed
13+
14+
- Route Brent, WTI, gasoil, and EU carbon futures through the API's
15+
instrument-generic paths in sync and async clients. Existing venue-slug and
16+
contract-code inputs remain compatible and normalize to those same paths.
17+
1018
## [1.12.5] - 2026-08-11
1119

1220
### Added

oilpriceapi/async_resources.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,16 @@ async def categories(self) -> Dict[str, List[Dict[str, Any]]]:
316316
class AsyncFuturesResource:
317317
"""Async resource for futures contract operations.
318318
319-
Endpoints are keyed by *slug* (e.g. ``"ice-brent"``). Methods accept either
320-
a slug or a friendly contract code (``"BZ"``, ``"CL"``, ``"NG"``, ...),
321-
normalized via :func:`normalize_futures_slug`.
319+
Endpoints are keyed by instrument-generic slugs (e.g. ``"brent"``).
320+
Methods accept either a slug or a friendly contract code (``"BZ"``,
321+
``"CL"``, ``"NG"``, ...), normalized via
322+
:func:`normalize_futures_slug`.
323+
324+
Examples:
325+
>>> await client.futures.latest("brent")
326+
>>> await client.futures.latest("wti")
327+
>>> await client.futures.latest("gasoil")
328+
>>> await client.futures.latest("eu-carbon")
322329
"""
323330

324331
def __init__(self, client):
@@ -328,7 +335,7 @@ async def latest(self, contract: str) -> Dict[str, Any]:
328335
"""Get the latest futures curve. Accepts a slug or contract code.
329336
330337
Example:
331-
>>> await client.futures.latest("ice-brent") # or "BZ"
338+
>>> await client.futures.latest("brent") # or "BZ"
332339
"""
333340
slug = normalize_futures_slug(contract)
334341
response = await self.client.request(method="GET", path=f"/v1/futures/{slug}")
@@ -404,9 +411,9 @@ def _continuous_slug(contract: str) -> str:
404411
slug = normalize_futures_slug(contract)
405412
if slug.startswith("continuous/"):
406413
return slug
407-
if slug == "ice-brent":
414+
if slug == "brent":
408415
return "continuous/brent"
409-
if slug == "ice-wti":
416+
if slug == "wti":
410417
return "continuous/wti"
411418
raise ValueError(
412419
f"Continuous futures are only available for Brent and WTI, "

oilpriceapi/resources/_futures_slug.py

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
"""
22
Futures slug normalization.
33
4-
The OilPriceAPI futures endpoints are keyed by *slug*, not by exchange
5-
contract code. The latest-curve route is ``GET /v1/futures/{slug}`` and the
6-
sub-resources are ``/v1/futures/{slug}/curve``, ``/historical``, ``/ohlc``,
7-
``/intraday`` and ``/spread-history``. There is no ``?contract=`` route, so a
8-
caller passing a raw ticker such as ``"CL.1"`` would hit
4+
The OilPriceAPI futures endpoints are keyed by instrument-generic slugs, not by
5+
exchange contract codes. The latest-curve route is ``GET /v1/futures/{slug}``
6+
and the sub-resources are ``/v1/futures/{slug}/curve``, ``/historical``,
7+
``/ohlc``, ``/intraday`` and ``/spread-history``. There is no ``?contract=``
8+
route, so a caller passing a raw ticker such as ``"CL.1"`` would hit
99
``/v1/futures/CL.1`` and get a 404.
1010
1111
To keep the SDK friendly, callers may pass either:
1212
13-
* a canonical slug (``"ice-brent"``, ``"ice-wti"``, ``"natural-gas"``, ...), or
13+
* a canonical slug (``"brent"``, ``"wti"``, ``"natural-gas"``, ...),
14+
* a legacy venue slug (``"ice-brent"``, ``"ice-wti"``, ...), or
1415
* a familiar exchange/contract code (``"BZ"``, ``"CL"``, ``"NG"``, ...),
1516
1617
and :func:`normalize_futures_slug` resolves it to the canonical slug the API
@@ -23,38 +24,47 @@
2324

2425
from typing import Dict, Set
2526

26-
# Canonical slugs accepted by the API (latest-curve routes).
27+
# Canonical slugs emitted by the SDK for latest-curve routes.
2728
VALID_SLUGS: Set[str] = {
28-
"ice-brent",
29-
"ice-wti",
30-
"ice-gasoil",
29+
"brent",
30+
"wti",
31+
"gasoil",
3132
"natural-gas",
3233
"ttf-gas",
3334
"lng-jkm",
34-
"eua-carbon",
35+
"eu-carbon",
3536
"uk-carbon",
3637
"continuous/brent",
3738
"continuous/wti",
3839
}
3940

40-
# Friendly exchange/contract codes -> canonical slug.
41+
# Older public route names remain valid caller inputs, but the SDK emits the
42+
# instrument-generic route so new traffic does not encode a reporting venue.
43+
LEGACY_SLUG_TO_CANONICAL: Dict[str, str] = {
44+
"ice-brent": "brent",
45+
"ice-wti": "wti",
46+
"ice-gasoil": "gasoil",
47+
"eua-carbon": "eu-carbon",
48+
}
49+
50+
# Friendly exchange/contract codes -> canonical instrument slug.
4151
# Keys are matched case-insensitively against the leading contract symbol
42-
# (e.g. "CL", "CL.1", "CL1!" all resolve to ice-wti).
52+
# (e.g. "CL", "CL.1", "CL1!" all resolve to wti).
4353
CONTRACT_CODE_TO_SLUG: Dict[str, str] = {
44-
"BZ": "ice-brent", # ICE Brent
45-
"BRENT": "ice-brent",
46-
"CL": "ice-wti", # WTI (NYMEX/ICE ticker)
47-
"WTI": "ice-wti",
48-
"G": "ice-gasoil", # ICE Gas Oil
49-
"QS": "ice-gasoil", # ICE Gas Oil (alt ticker)
50-
"GASOIL": "ice-gasoil",
54+
"BZ": "brent",
55+
"BRENT": "brent",
56+
"CL": "wti",
57+
"WTI": "wti",
58+
"G": "gasoil",
59+
"QS": "gasoil",
60+
"GASOIL": "gasoil",
5161
"NG": "natural-gas", # NYMEX Henry Hub natural gas
5262
"NATGAS": "natural-gas",
5363
"TTF": "ttf-gas", # ICE TTF natural gas
5464
"JKM": "lng-jkm", # ICE/CME JKM LNG
5565
"LNG": "lng-jkm",
56-
"EUA": "eua-carbon", # ICE EUA carbon
57-
"EU_CARBON": "eua-carbon",
66+
"EUA": "eu-carbon",
67+
"EU_CARBON": "eu-carbon",
5868
"UKA": "uk-carbon", # ICE UKA (UK) carbon
5969
"UK_CARBON": "uk-carbon",
6070
}
@@ -67,10 +77,10 @@ def normalize_futures_slug(contract: str) -> str:
6777
friendly exchange/contract code (e.g. ``"BZ"``, ``"CL.1"``, ``"NG"``).
6878
6979
Args:
70-
contract: A slug (``"ice-brent"``) or a contract code (``"BZ"``).
80+
contract: A slug (``"brent"``) or a contract code (``"BZ"``).
7181
7282
Returns:
73-
The canonical slug the API expects (e.g. ``"ice-brent"``).
83+
The instrument-generic slug the API expects (e.g. ``"brent"``).
7484
7585
Raises:
7686
ValueError: If ``contract`` is empty or cannot be resolved.
@@ -85,9 +95,17 @@ def normalize_futures_slug(contract: str) -> str:
8595
if lowered in VALID_SLUGS:
8696
return lowered
8797

98+
legacy_slug = LEGACY_SLUG_TO_CANONICAL.get(lowered)
99+
if legacy_slug is not None:
100+
return legacy_slug
101+
102+
symbol = raw.upper()
103+
exact_code_slug = CONTRACT_CODE_TO_SLUG.get(symbol)
104+
if exact_code_slug is not None:
105+
return exact_code_slug
106+
88107
# Contract code form: take the leading symbol before any month/order
89108
# suffix such as ".1", "1!", "-2025-12", "_2025_12".
90-
symbol = raw.upper()
91109
for sep in (".", "!", "-", "_", " "):
92110
if sep in symbol:
93111
symbol = symbol.split(sep, 1)[0]

oilpriceapi/resources/futures.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@
33
44
Futures contract price operations.
55
6-
Endpoints are keyed by *slug* (e.g. ``"ice-brent"``), not by raw exchange
7-
contract code. The latest-curve route is ``GET /v1/futures/{slug}`` and the
8-
sub-resources are ``/{slug}/curve``, ``/historical``, ``/ohlc``, ``/intraday``
9-
and ``/spread-history``. Each method accepts either a slug or a friendly
10-
contract code (e.g. ``"BZ"``, ``"CL"``, ``"NG"``) and normalizes it via
11-
:mod:`._futures_slug`.
12-
13-
Valid slugs: ``ice-brent``, ``ice-wti``, ``ice-gasoil``, ``natural-gas``,
14-
``ttf-gas``, ``lng-jkm``, ``eua-carbon``, ``uk-carbon`` (+ continuous slugs
6+
Endpoints are keyed by instrument-generic slugs (e.g. ``"brent"``), not by raw
7+
exchange contract code. The latest-curve route is ``GET /v1/futures/{slug}``
8+
and the sub-resources are ``/{slug}/curve``, ``/historical``, ``/ohlc``,
9+
``/intraday`` and ``/spread-history``. Each method accepts either a slug or a
10+
friendly contract code (e.g. ``"BZ"``, ``"CL"``, ``"NG"``) and normalizes it
11+
via :mod:`._futures_slug`.
12+
13+
Valid slugs: ``brent``, ``wti``, ``gasoil``, ``natural-gas``,
14+
``ttf-gas``, ``lng-jkm``, ``eu-carbon``, ``uk-carbon`` (+ continuous slugs
1515
``continuous/brent`` and ``continuous/wti``).
16+
17+
Examples:
18+
>>> client.futures.latest("brent")
19+
>>> client.futures.latest("wti")
20+
>>> client.futures.latest("gasoil")
21+
>>> client.futures.latest("eu-carbon")
1622
"""
1723

1824
from datetime import date, datetime
@@ -37,14 +43,14 @@ def latest(self, contract: str) -> Dict[str, Any]:
3743
"""Get the latest futures curve for a contract family.
3844
3945
Args:
40-
contract: Futures slug (e.g. ``"ice-brent"``, ``"ice-wti"``) or a
46+
contract: Futures slug (e.g. ``"brent"``, ``"wti"``) or a
4147
friendly contract code (e.g. ``"BZ"``, ``"CL"``, ``"NG"``).
4248
4349
Returns:
4450
Latest futures curve data (front month + forward contracts)
4551
4652
Example:
47-
>>> curve = client.futures.latest("ice-brent")
53+
>>> curve = client.futures.latest("brent")
4854
>>> # Friendly code form also works:
4955
>>> curve = client.futures.latest("BZ")
5056
>>> print(curve["front_month"]["last_price"])
@@ -78,7 +84,7 @@ def historical(
7884
7985
Example:
8086
>>> history = client.futures.historical(
81-
... contract="ice-wti",
87+
... contract="wti",
8288
... start_date="2024-01-01",
8389
... end_date="2024-12-31"
8490
... )
@@ -114,7 +120,7 @@ def ohlc(self, contract: str, date: Optional[str] = None) -> Dict[str, Any]:
114120
OHLC data with open, high, low, close, and volume
115121
116122
Example:
117-
>>> ohlc = client.futures.ohlc("ice-wti")
123+
>>> ohlc = client.futures.ohlc("wti")
118124
>>> print(f"Open: ${ohlc['open']:.2f}")
119125
>>> print(f"High: ${ohlc['high']:.2f}")
120126
>>> print(f"Low: ${ohlc['low']:.2f}")
@@ -146,7 +152,7 @@ def intraday(self, contract: str) -> List[Dict[str, Any]]:
146152
List of intraday price records
147153
148154
Example:
149-
>>> intraday = client.futures.intraday("ice-wti")
155+
>>> intraday = client.futures.intraday("wti")
150156
>>> for record in intraday:
151157
... print(f"{record['time']}: ${record['price']:.2f}")
152158
"""
@@ -199,7 +205,7 @@ def curve(self, contract: str) -> List[Dict[str, Any]]:
199205
List of futures curve data points
200206
201207
Example:
202-
>>> curve = client.futures.curve("ice-wti")
208+
>>> curve = client.futures.curve("wti")
203209
>>> for point in curve:
204210
... print(f"{point['month']}: ${point['price']:.2f}")
205211
"""
@@ -251,9 +257,9 @@ def _continuous_slug(contract: str) -> str:
251257
slug = normalize_futures_slug(contract)
252258
if slug.startswith("continuous/"):
253259
return slug
254-
if slug in ("ice-brent",):
260+
if slug == "brent":
255261
return "continuous/brent"
256-
if slug in ("ice-wti",):
262+
if slug == "wti":
257263
return "continuous/wti"
258264
raise ValueError(
259265
f"Continuous futures are only available for Brent and WTI, "

oilpriceapi/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
Used in __init__.py, client.py, and async_client.py.
66
"""
77

8-
__version__ = "1.12.5"
8+
__version__ = "1.12.6"
99
SDK_VERSION = __version__
1010
SDK_NAME = "oilpriceapi-python"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
66

77
[project]
88
name = "oilpriceapi"
9-
version = "1.12.5"
9+
version = "1.12.6"
1010
description = "Official Python SDK for source-timestamped OilPriceAPI energy data"
1111
authors = [
1212
{name = "OilPriceAPI", email = "support@oilpriceapi.com"}

tests/integration/test_live_futures.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,27 +57,36 @@ def live_client():
5757

5858

5959
def test_latest_by_slug(live_client):
60-
"""futures.latest('ice-brent') returns 200 + a sane Brent price."""
61-
curve = live_client.futures.latest("ice-brent")
60+
"""futures.latest('brent') returns 200 + a sane Brent price."""
61+
curve = live_client.futures.latest("brent")
6262
assert isinstance(curve, dict)
6363
price = _front_price(curve)
6464
# Sanity range for Brent crude (USD/bbl).
6565
assert 10 < price < 500, f"Brent price out of sane range: {price}"
6666

6767

6868
def test_latest_by_contract_code(live_client):
69-
"""Friendly code 'BZ' normalizes to ice-brent and returns a sane price."""
69+
"""Friendly code 'BZ' normalizes to brent and returns a sane price."""
7070
time.sleep(RATE_LIMIT_SLEEP)
7171
curve = live_client.futures.latest("BZ")
7272
assert isinstance(curve, dict)
7373
price = _front_price(curve)
7474
assert 10 < price < 500, f"Brent (BZ) price out of sane range: {price}"
7575

7676

77+
def test_latest_by_legacy_slug(live_client):
78+
"""Legacy venue input remains compatible while using the generic route."""
79+
time.sleep(RATE_LIMIT_SLEEP)
80+
curve = live_client.futures.latest("ice-brent")
81+
assert isinstance(curve, dict)
82+
price = _front_price(curve)
83+
assert 10 < price < 500, f"Brent legacy input price out of sane range: {price}"
84+
85+
7786
def test_curve(live_client):
78-
"""futures.curve('ice-brent') returns 200 with curve data."""
87+
"""futures.curve('brent') returns 200 with curve data."""
7988
time.sleep(RATE_LIMIT_SLEEP)
80-
curve = live_client.futures.curve("ice-brent")
89+
curve = live_client.futures.curve("brent")
8190
# Curve responses may be a list of points or a dict wrapping them.
8291
assert curve is not None
8392
if isinstance(curve, dict):

tests/test_release_readiness.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,23 @@ def test_examples_defer_mutable_allowances_to_product_facts() -> None:
8787
)
8888

8989

90+
def test_packaged_futures_examples_prefer_instrument_generic_slugs() -> None:
91+
for path in (
92+
"oilpriceapi/resources/futures.py",
93+
"oilpriceapi/async_resources.py",
94+
):
95+
source = (ROOT / path).read_text()
96+
example_lines = re.findall(r"(?m)^\s*(?:>>>|\.\.\.)\s+.*$", source)
97+
examples = "\n".join(example_lines)
98+
99+
for example_line in example_lines:
100+
assert not re.search(r"[\"'](?:ice|eua)-", example_line)
101+
for canonical_slug in ("brent", "wti", "gasoil", "eu-carbon"):
102+
assert canonical_slug in examples, (
103+
f"{path} examples omit {canonical_slug}"
104+
)
105+
106+
90107
def test_publish_gate_audits_and_installs_the_built_wheel() -> None:
91108
workflow = (ROOT / ".github" / "workflows" / "publish.yml").read_text()
92109
smoke = (ROOT / "scripts" / "clean-wheel-smoke.sh").read_text()
@@ -142,7 +159,7 @@ def test_package_version_helper_reads_the_project_version() -> None:
142159
capture_output=True,
143160
text=True,
144161
)
145-
assert result.stdout.strip() == "1.12.5"
162+
assert result.stdout.strip() == "1.12.6"
146163

147164

148165
def test_every_workflow_pins_actions_and_hardens_each_checkout_step() -> None:

0 commit comments

Comments
 (0)