Skip to content

Commit 8a061ef

Browse files
authored
fix: preserve dataframe context across pages (#73)
1 parent fa86d80 commit 8a061ef

12 files changed

Lines changed: 542 additions & 101 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Historical DataFrame helpers now accept a `per_page` value from 1 to 1000
13+
and fetch all pages automatically. The `client.prices.to_dataframe(...)`
14+
convenience path forwards the same option for date-range queries.
15+
16+
### Fixed
17+
18+
- Preserve each API record's currency and unit in current and historical
19+
DataFrames instead of labeling a missing currency as USD.
20+
- Remove exact duplicate records introduced by overlapping page boundaries,
21+
stop safely on empty pages with stale continuation metadata, and return a
22+
stable schema for empty historical DataFrames.
23+
1024
## [1.11.0] - 2026-07-19
1125

1226
### Changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,31 @@ print(
137137
Use the raw first-request pattern when downstream logic requires the exact
138138
source and timestamp-field semantics from the API response.
139139

140+
## Complete pandas DataFrames
141+
142+
Install the optional pandas support, then request a historical DataFrame:
143+
144+
```python
145+
df = client.historical.to_dataframe(
146+
commodity="BRENT_CRUDE_USD",
147+
start="2026-01-01",
148+
end="2026-06-30",
149+
per_page=500,
150+
)
151+
```
152+
153+
`to_dataframe()` fetches every page automatically. `per_page` controls the
154+
request page size, not the total result size, and must be an integer from 1 to
155+
1000. The DataFrame preserves each API row's `currency` and `unit`; a missing
156+
currency remains missing rather than being labeled USD. Exact records repeated
157+
by an overlapping page boundary are returned once, while distinct records are
158+
retained. Empty results have a stable schema with a `date` index.
159+
160+
The same `per_page` behavior applies to date-range queries through
161+
`client.prices.to_dataframe(...)`. See the
162+
[DataFrames and pagination guide](docs/DATAFRAMES.md) for the complete
163+
contract.
164+
140165
## Recovery
141166

142167
The package exposes typed errors for the customer-recoverable boundaries:

docs/DATAFRAMES.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# DataFrames and pagination
2+
3+
The pandas helpers preserve the source context returned by the API and fetch
4+
complete result sets without requiring a manual page loop.
5+
6+
Install the optional dependency:
7+
8+
```bash
9+
python -m pip install "oilpriceapi[pandas]"
10+
```
11+
12+
## Historical data
13+
14+
```python
15+
from oilpriceapi import OilPriceAPI
16+
17+
client = OilPriceAPI()
18+
df = client.historical.to_dataframe(
19+
commodity="BRENT_CRUDE_USD",
20+
start="2026-01-01",
21+
end="2026-06-30",
22+
interval="daily",
23+
per_page=500,
24+
)
25+
```
26+
27+
The historical DataFrame contract is:
28+
29+
- Every page is fetched automatically until the API reports completion.
30+
- `per_page` is an integer from 1 to 1000 and defaults to 500.
31+
- `per_page` controls each request; it does not limit the total rows returned.
32+
- `currency` and `unit` are taken from each API record. A missing currency
33+
remains null and is never converted to USD.
34+
- An exact record repeated on a later, overlapping page is emitted once.
35+
Distinct records, including records sharing a timestamp, remain intact.
36+
- An empty page terminates pagination even if stale metadata says another page
37+
exists.
38+
- The result is sorted by its `date` index. Empty results retain the columns
39+
`commodity`, `value`, `currency`, `unit`, and `type_name`.
40+
41+
The convenience resource uses the same pagination behavior:
42+
43+
```python
44+
df = client.prices.to_dataframe(
45+
commodity="EU_CARBON_EUR",
46+
start="2026-01-01",
47+
end="2026-06-30",
48+
per_page=250,
49+
)
50+
```
51+
52+
For model objects instead of pandas, use `client.historical.get_all(...)`.
53+
It accepts the same `per_page` range and automatically fetches every page.
54+
55+
## Current prices
56+
57+
Calling `client.prices.to_dataframe()` with no commodity returns all current
58+
prices and automatically follows the API's pagination headers:
59+
60+
```python
61+
df = client.prices.to_dataframe(per_page=250)
62+
```
63+
64+
Each row keeps the API-provided currency and unit. Exact records repeated on a
65+
later page are returned once.
66+
67+
## Manual page control
68+
69+
Use `client.historical.get(...)` when a single page is intentional:
70+
71+
```python
72+
page = client.historical.get(
73+
commodity="BRENT_CRUDE_USD",
74+
page=2,
75+
per_page=100,
76+
)
77+
```
78+
79+
Use `client.historical.iter_pages(...)` to process complete results one page
80+
at a time without retaining the entire response in memory.

docs/index.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,25 @@ Access years of historical price data for backtesting and analysis:
5757

5858
```python
5959
# Get historical data
60-
df = client.prices.to_dataframe(
61-
commodity="BRENT_CRUDE_USD",
62-
start="2024-01-01",
63-
end="2024-12-31",
64-
interval="daily"
65-
)
66-
67-
# Analyze trends
68-
print(df.describe())
69-
```
70-
71-
**[Learn about historical endpoints →](https://docs.oilpriceapi.com/api-reference/historical)**
60+
df = client.prices.to_dataframe(
61+
commodity="BRENT_CRUDE_USD",
62+
start="2024-01-01",
63+
end="2024-12-31",
64+
interval="daily",
65+
per_page=500
66+
)
67+
68+
# Analyze trends
69+
print(df.describe())
70+
```
71+
72+
The DataFrame helper fetches every page and preserves the `currency` and
73+
`unit` returned for each record. `per_page` may be set from 1 to 1000 and
74+
controls request size rather than total results. See
75+
**[DataFrames and pagination →](DATAFRAMES.md)** for empty-result and page
76+
boundary behavior.
77+
78+
**[Learn about historical endpoints →](https://docs.oilpriceapi.com/api-reference/historical)**
7279

7380
### Technical Analysis
7481

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ theme:
1515

1616
nav:
1717
- Home: index.md
18+
- DataFrames and Pagination: DATAFRAMES.md
1819
- Performance Guide: PERFORMANCE_GUIDE.md
1920
- API Reference:
2021
- Client: reference/client.md

oilpriceapi/_pagination.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Shared pagination validation for public SDK helpers."""
2+
3+
MIN_PAGE_SIZE = 1
4+
MAX_PAGE_SIZE = 1000
5+
6+
7+
def validate_page_size(per_page: int) -> int:
8+
"""Return a valid API page size or fail before making a request."""
9+
if (
10+
isinstance(per_page, bool)
11+
or not isinstance(per_page, int)
12+
or not MIN_PAGE_SIZE <= per_page <= MAX_PAGE_SIZE
13+
):
14+
raise ValueError(f"per_page must be an integer between {MIN_PAGE_SIZE} and {MAX_PAGE_SIZE}")
15+
return per_page

0 commit comments

Comments
 (0)