Skip to content

Commit 1973bd7

Browse files
authored
Add well production resource and secret guard
Includes the well production resource work, removes exposed API tokens, and adds CI coverage to block hardcoded OilPriceAPI-shaped tokens.
1 parent 6cfa2d8 commit 1973bd7

15 files changed

Lines changed: 1140 additions & 4 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+
### Added
11+
12+
- **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.
13+
14+
### Security
15+
16+
- 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.
17+
1018
## [1.10.2] - 2026-07-10
1119

1220
### Changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,38 @@ print(f"Permian rigs: {permian['rig_count']}")
372372
completions = client.drilling.completions()
373373
```
374374

375+
### Well Production (Beta)
376+
377+
US well production data. State/national monthly aggregates come from the
378+
EIA API; per-well history and cycle-time analytics are **beta** and only
379+
cover states where regulatory data has been collected — this is not a
380+
complete US well-level production dataset. Requires a plan with the
381+
Drilling Intelligence feature (403 `ENTERPRISE_REQUIRED` otherwise).
382+
383+
```python
384+
# National overview + top producing states
385+
overview = client.well_production.summary()
386+
for state in overview["top_states"]:
387+
print(f"{state['state']}: {state['oil_bbl']:,} bbl ({state['period']})")
388+
389+
# State-level production for a month
390+
states = client.well_production.states(period="2026-04")
391+
392+
# Production history for one state
393+
tx = client.well_production.state("TX", start_date="2026-01-01")
394+
395+
# Per-well history (beta; 14-digit API number, dashes OK)
396+
well = client.well_production.well("42-285-34329-00-00")
397+
398+
# Top producing wells in a state (beta)
399+
top = client.well_production.top_producers("NM", limit=10, months=12)
400+
401+
# Permit-to-production cycle times (beta)
402+
ct = client.well_production.cycle_time(state="TX")
403+
print(f"Median cycle: {ct['cycle_time_stats']['median_days']} days")
404+
cohorts = client.well_production.cycle_time_cohorts(state="TX", group_by="quarter")
405+
```
406+
375407
### Webhooks (New in v1.5.0)
376408

377409
```python
@@ -573,6 +605,7 @@ async with AsyncOilPriceAPI() as client:
573605
-**Bunker Fuels** - Marine fuel prices across major ports
574606
-**Price Analytics** - Performance, correlations, trends, and forecasts
575607
-**Drilling Intelligence** - DUC wells, permits, completions, and basin data
608+
-**Well Production (beta)** - State/national production aggregates, per-well history, cycle times
576609
-**Webhooks** - Manage event subscriptions and notifications
577610
-**EIA Forecasts** - Official monthly price forecasts with accuracy tracking
578611
-**Energy Intelligence** - EIA data, OPEC production, drilling productivity

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+
## Well Production (Beta)
56+
57+
::: oilpriceapi.resources.well_production.WellProductionResource
58+
5559
## Webhooks
5660

5761
::: oilpriceapi.resources.webhooks.WebhooksResource

oilpriceapi/async_client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
AsyncStorageResource,
3535
AsyncSubscriptionsResource,
3636
AsyncWebhooksResource,
37+
AsyncWellProductionResource,
3738
)
3839
from .exceptions import (
3940
AuthenticationError,
@@ -149,6 +150,8 @@ def __init__(
149150
self.forecasts = AsyncForecastsResource(self)
150151
self.data_quality = AsyncDataQualityResource(self)
151152
self.drilling = AsyncDrillingIntelligenceResource(self)
153+
# US well production aggregates + per-well beta data (#50).
154+
self.well_production = AsyncWellProductionResource(self)
152155
self.ei = AsyncEnergyIntelligenceResource(self)
153156
self.webhooks = AsyncWebhooksResource(self)
154157
self.data_sources = AsyncDataSourcesResource(self)

oilpriceapi/async_resources.py

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111
from .exceptions import ValidationError
1212
from .models import DieselPrice, DieselStationsResponse, PriceAlert, Subscription, SubscriptionEvent
13-
from .resource_validators import VALID_OPERATORS, format_date
13+
from .resource_validators import VALID_OPERATORS, format_date, normalize_api_number
1414
from .resources._futures_slug import normalize_futures_slug
1515
from .resources.subscriptions import SubscriptionEventsPage
1616

@@ -773,6 +773,135 @@ async def basin(self, name: str) -> Dict[str, Any]:
773773
return response
774774

775775

776+
class AsyncWellProductionResource:
777+
"""Async resource for US well production data (beta).
778+
779+
Mirror of ``oilpriceapi.resources.well_production.WellProductionResource``.
780+
"""
781+
782+
def __init__(self, client):
783+
self.client = client
784+
785+
async def summary(self) -> Dict[str, Any]:
786+
response = await self.client.request(method="GET", path="/v1/well-production")
787+
if "data" in response:
788+
return response["data"]
789+
return response
790+
791+
async def states(self, period: Optional[str] = None, **params) -> Dict[str, Any]:
792+
if period is not None:
793+
params["period"] = period
794+
response = await self.client.request(
795+
method="GET", path="/v1/well-production/states", params=params
796+
)
797+
if "data" in response:
798+
return response["data"]
799+
return response
800+
801+
async def state(
802+
self,
803+
code: str,
804+
start_date: Optional[str] = None,
805+
end_date: Optional[str] = None,
806+
**params,
807+
) -> Dict[str, Any]:
808+
if start_date is not None:
809+
params["start_date"] = start_date
810+
if end_date is not None:
811+
params["end_date"] = end_date
812+
response = await self.client.request(
813+
method="GET", path=f"/v1/well-production/states/{code}", params=params
814+
)
815+
if "data" in response:
816+
return response["data"]
817+
return response
818+
819+
async def well(self, api_number: str) -> Dict[str, Any]:
820+
normalized = normalize_api_number(api_number)
821+
response = await self.client.request(
822+
method="GET", path=f"/v1/well-production/wells/{normalized}"
823+
)
824+
if "data" in response:
825+
return response["data"]
826+
return response
827+
828+
async def top_producers(
829+
self,
830+
state_code: str = "TX",
831+
limit: int = 20,
832+
months: Optional[int] = None,
833+
**params,
834+
) -> Dict[str, Any]:
835+
params["state_code"] = state_code
836+
params["limit"] = limit
837+
if months is not None:
838+
params["months"] = months
839+
response = await self.client.request(
840+
method="GET", path="/v1/well-production/top-producers", params=params
841+
)
842+
if "data" in response:
843+
return response["data"]
844+
return response
845+
846+
async def cycle_time(
847+
self,
848+
state: Optional[str] = None,
849+
start_date: Optional[str] = None,
850+
end_date: Optional[str] = None,
851+
operator: Optional[str] = None,
852+
formation: Optional[str] = None,
853+
lat: Optional[float] = None,
854+
lng: Optional[float] = None,
855+
radius_miles: Optional[float] = None,
856+
**params,
857+
) -> Dict[str, Any]:
858+
filters = {
859+
"state": state,
860+
"start_date": start_date,
861+
"end_date": end_date,
862+
"operator": operator,
863+
"formation": formation,
864+
"lat": lat,
865+
"lng": lng,
866+
"radius_miles": radius_miles,
867+
}
868+
params.update({k: v for k, v in filters.items() if v is not None})
869+
response = await self.client.request(
870+
method="GET", path="/v1/well-production/cycle-time", params=params
871+
)
872+
if "data" in response:
873+
return response["data"]
874+
return response
875+
876+
async def cycle_time_cohorts(
877+
self,
878+
state: Optional[str] = None,
879+
start_date: Optional[str] = None,
880+
end_date: Optional[str] = None,
881+
lat: Optional[float] = None,
882+
lng: Optional[float] = None,
883+
radius_miles: Optional[float] = None,
884+
group_by: Optional[str] = None,
885+
**params,
886+
) -> Dict[str, Any]:
887+
filters = {
888+
"state": state,
889+
"start_date": start_date,
890+
"end_date": end_date,
891+
"lat": lat,
892+
"lng": lng,
893+
"radius_miles": radius_miles,
894+
"group_by": group_by,
895+
}
896+
params.update({k: v for k, v in filters.items() if v is not None})
897+
response = await self.client.request(
898+
method="GET", path="/v1/well-production/cycle-time/cohorts", params=params
899+
)
900+
if "data" in response:
901+
return response["data"]
902+
return response
903+
904+
776905
# EI sub-resources
777906

778907
class AsyncEIRigCountsResource:

oilpriceapi/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from .resources.storage import StorageResource
5151
from .resources.subscriptions import SubscriptionsResource
5252
from .resources.webhooks import WebhooksResource
53+
from .resources.well_production import WellProductionResource
5354
from .retry import RetryStrategy
5455

5556

@@ -179,6 +180,8 @@ def __init__(
179180
self.forecasts = ForecastsResource(self)
180181
self.data_quality = DataQualityResource(self)
181182
self.drilling = DrillingIntelligenceResource(self)
183+
# US well production aggregates + per-well beta data (#50).
184+
self.well_production = WellProductionResource(self)
182185
self.ei = EnergyIntelligenceResource(self)
183186
self.webhooks = WebhooksResource(self)
184187
self.data_sources = DataSourcesResource(self)

oilpriceapi/resource_validators.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,36 @@ def format_date(date_input: Union[str, date, datetime]) -> str:
5353
return date_input.isoformat()
5454
else:
5555
raise ValueError(f"Invalid date type: {type(date_input)}")
56+
57+
58+
# ---------------------------------------------------------------------------
59+
# Well production API numbers
60+
# ---------------------------------------------------------------------------
61+
62+
API_NUMBER_LENGTH = 14
63+
64+
65+
def normalize_api_number(api_number: str) -> str:
66+
"""Normalise a well API number to the 14-digit form the API expects.
67+
68+
Strips any non-digit separators (dashes, spaces) and validates the
69+
length client-side so callers get an immediate, descriptive error
70+
instead of a 400 round-trip.
71+
72+
Args:
73+
api_number: A well API number, e.g. ``"42285343290000"`` or
74+
``"42-285-34329-00-00"``.
75+
76+
Returns:
77+
The 14-digit API number string.
78+
79+
Raises:
80+
ValueError: If the value does not contain exactly 14 digits.
81+
"""
82+
digits = "".join(ch for ch in str(api_number) if ch.isdigit())
83+
if len(digits) != API_NUMBER_LENGTH:
84+
raise ValueError(
85+
f"API number must be {API_NUMBER_LENGTH} digits, "
86+
f"got {len(digits)} from {api_number!r}"
87+
)
88+
return digits

oilpriceapi/resources/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .storage import StorageResource
2424
from .subscriptions import SubscriptionsResource
2525
from .webhooks import WebhooksResource
26+
from .well_production import WellProductionResource
2627

2728
__all__ = [
2829
"AnalysisResource",
@@ -44,4 +45,5 @@
4445
"DataSourcesResource",
4546
"DemoResource",
4647
"SubscriptionsResource",
48+
"WellProductionResource",
4749
]

0 commit comments

Comments
 (0)