Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
"""

import os
import time
import pytest
from pathlib import Path
from oilpriceapi import OilPriceAPI
from oilpriceapi.exceptions import RateLimitError

try:
from dotenv import dotenv_values
Expand All @@ -27,6 +29,24 @@ def dotenv_values(_path): # type: ignore[misc]
API_KEY = env_vars.get('OILPRICEAPI_KEY')
BASE_URL = env_vars.get('OILPRICEAPI_BASE_URL', 'https://api.oilpriceapi.com')

# CI shares a single 1-request/second API key across repositories, so live
# tests can collide and get HTTP 429 (RateLimitError) through no fault of the
# code under test. To keep green code green we:
# 1. Space live calls at least MIN_CALL_SPACING_SECONDS apart, and
# 2. Treat any 429 as a pytest.skip rather than a failure.
MIN_CALL_SPACING_SECONDS = 1.1
_last_live_call_at = 0.0


def _throttle_live_calls():
"""Enforce >=1.1s spacing between live API calls (1 req/sec shared key)."""
global _last_live_call_at
now = time.monotonic()
elapsed = now - _last_live_call_at
if elapsed < MIN_CALL_SPACING_SECONDS:
time.sleep(MIN_CALL_SPACING_SECONDS - elapsed)
_last_live_call_at = time.monotonic()


@pytest.fixture(scope="session")
def api_key():
Expand All @@ -47,4 +67,27 @@ def live_client(api_key, base_url):
"""Create a client for live API testing."""
client = OilPriceAPI(api_key=api_key, base_url=base_url)
yield client
client.close()
client.close()


@pytest.fixture
def live_call():
"""Run a live API call with rate-limit resilience.

Spaces calls out (>=1.1s) and converts HTTP 429 (RateLimitError) into a
pytest.skip so the shared 1-req/sec CI key can't red-flag green code.
Any non-429 error is re-raised unchanged so real failures still surface.

Usage:
price = live_call(client.prices.get, "BRENT_CRUDE_USD")
"""
def _call(func, *args, **kwargs):
_throttle_live_calls()
try:
return func(*args, **kwargs)
except RateLimitError:
pytest.skip(
"rate-limited (shared CI key) - skipping live assertion"
)

return _call
69 changes: 44 additions & 25 deletions tests/integration/test_historical_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,21 @@
import time
from datetime import datetime, timedelta
from oilpriceapi import OilPriceAPI
from oilpriceapi.exceptions import TimeoutError
from oilpriceapi.exceptions import RateLimitError, TimeoutError


@pytest.mark.integration
class TestHistoricalEndpointSelection:
"""Test that SDK selects correct endpoints for different date ranges."""

def test_1_day_query_uses_past_day_endpoint(self, live_client):
def test_1_day_query_uses_past_day_endpoint(self, live_client, live_call):
"""Verify 1-day queries use /v1/prices/past_day endpoint."""
end_date = datetime.now()
start_date = end_date - timedelta(days=1)

start_time = time.time()
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="WTI_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d"),
Expand All @@ -41,13 +42,14 @@ def test_1_day_query_uses_past_day_endpoint(self, live_client):
# Should be fast (using optimized endpoint)
assert duration < 10, f"1-day query took {duration}s, expected <10s"

def test_7_day_query_uses_past_week_endpoint(self, live_client):
def test_7_day_query_uses_past_week_endpoint(self, live_client, live_call):
"""Verify 7-day queries use /v1/prices/past_week endpoint."""
end_date = datetime.now()
start_date = end_date - timedelta(days=7)

start_time = time.time()
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="WTI_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d"),
Expand All @@ -65,13 +67,14 @@ def test_7_day_query_uses_past_week_endpoint(self, live_client):
assert duration < 30, f"7-day query took {duration}s, expected <30s"
print(f"✓ 7-day query completed in {duration:.2f}s (optimized endpoint)")

def test_30_day_query_uses_past_month_endpoint(self, live_client):
def test_30_day_query_uses_past_month_endpoint(self, live_client, live_call):
"""Verify 30-day queries use /v1/prices/past_month endpoint."""
end_date = datetime.now()
start_date = end_date - timedelta(days=30)

start_time = time.time()
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="WTI_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d"),
Expand All @@ -89,14 +92,15 @@ def test_30_day_query_uses_past_month_endpoint(self, live_client):
assert duration < 60, f"30-day query took {duration}s, expected <60s"
print(f"✓ 30-day query completed in {duration:.2f}s (optimized endpoint)")

def test_365_day_query_uses_past_year_endpoint(self, live_client):
def test_365_day_query_uses_past_year_endpoint(self, live_client, live_call):
"""
Verify 365-day queries use /v1/prices/past_year endpoint.

This is the EXACT query that failed for Idan in v1.4.1.
"""
start_time = time.time()
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="WTI_USD",
start_date="2024-01-01",
end_date="2024-12-31",
Expand Down Expand Up @@ -129,13 +133,14 @@ def test_365_day_query_uses_past_year_endpoint(self, live_client):
class TestHistoricalTimeoutBehavior:
"""Test timeout handling for historical queries."""

def test_custom_timeout_is_respected(self, live_client):
def test_custom_timeout_is_respected(self, live_client, live_call):
"""Test that custom timeout parameter works."""
# Try a multi-year query with custom timeout
start_time = time.time()

try:
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="WTI_USD",
start_date="2020-01-01",
end_date="2024-12-31",
Expand All @@ -154,11 +159,12 @@ def test_custom_timeout_is_respected(self, live_client):
# Still assert we tried with the right timeout
assert duration >= 120, "Should have used longer timeout"

def test_timeout_scales_with_date_range(self, live_client):
def test_timeout_scales_with_date_range(self, live_client, live_call):
"""Verify timeout automatically scales for larger date ranges."""
# Small query should have short timeout
start_time = time.time()
history_week = live_client.historical.get(
history_week = live_call(
live_client.historical.get,
commodity="BRENT_CRUDE_USD",
start_date=(datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d"),
end_date=datetime.now().strftime("%Y-%m-%d"),
Expand All @@ -171,7 +177,8 @@ def test_timeout_scales_with_date_range(self, live_client):

# Large query should have longer timeout
start_time = time.time()
history_year = live_client.historical.get(
history_year = live_call(
live_client.historical.get,
commodity="BRENT_CRUDE_USD",
start_date="2024-01-01",
end_date="2024-12-31",
Expand All @@ -193,13 +200,14 @@ class TestHistoricalPerformanceBaselines:
These tests document expected response times and alert on regressions.
"""

def test_1_week_query_performance_baseline(self, live_client):
def test_1_week_query_performance_baseline(self, live_client, live_call):
"""1-week queries should complete in <30s."""
end_date = datetime.now()
start_date = end_date - timedelta(days=7)

start_time = time.time()
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="WTI_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d"),
Expand All @@ -213,13 +221,14 @@ def test_1_week_query_performance_baseline(self, live_client):
# Record baseline for monitoring
print(f"📊 Performance baseline: 1-week query = {duration:.2f}s")

def test_1_month_query_performance_baseline(self, live_client):
def test_1_month_query_performance_baseline(self, live_client, live_call):
"""1-month queries should complete in <60s."""
end_date = datetime.now()
start_date = end_date - timedelta(days=30)

start_time = time.time()
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="WTI_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d"),
Expand All @@ -232,14 +241,15 @@ def test_1_month_query_performance_baseline(self, live_client):

print(f"📊 Performance baseline: 1-month query = {duration:.2f}s")

def test_1_year_query_performance_baseline(self, live_client):
def test_1_year_query_performance_baseline(self, live_client, live_call):
"""
1-year queries should complete in <120s.

This test documents the exact scenario that failed for Idan.
"""
start_time = time.time()
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="WTI_USD",
start_date="2024-01-01",
end_date="2024-12-31",
Expand All @@ -262,9 +272,10 @@ def test_1_year_query_performance_baseline(self, live_client):
class TestHistoricalDataQuality:
"""Test data quality for historical queries."""

def test_year_query_returns_complete_data(self, live_client):
def test_year_query_returns_complete_data(self, live_client, live_call):
"""Verify 1-year query returns complete dataset."""
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="WTI_USD",
start_date="2024-01-01",
end_date="2024-12-31",
Expand All @@ -281,12 +292,13 @@ def test_year_query_returns_complete_data(self, live_client):
dates = [p.date for p in history.data]
assert dates == sorted(dates, reverse=True), "Data should be sorted by date (descending)"

def test_historical_data_matches_commodity(self, live_client):
def test_historical_data_matches_commodity(self, live_client, live_call):
"""Verify all returned data matches requested commodity."""
commodities = ["WTI_USD", "BRENT_CRUDE_USD", "NATURAL_GAS_USD"]

for commodity_code in commodities:
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity=commodity_code,
start_date=(datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d"),
end_date=datetime.now().strftime("%Y-%m-%d"),
Expand Down Expand Up @@ -322,7 +334,14 @@ def query_historical(commodity):

with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(query_historical, c) for c in commodities]
results = [f.result() for f in concurrent.futures.as_completed(futures)]
try:
results = [
f.result() for f in concurrent.futures.as_completed(futures)
]
except RateLimitError:
pytest.skip(
"rate-limited (shared CI key) - skipping live assertion"
)

assert len(results) == 3
for result in results:
Expand Down
31 changes: 17 additions & 14 deletions tests/integration/test_live_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,33 @@
class TestLiveAPIIntegration:
"""Integration tests with live API."""

def test_get_current_price(self, live_client):
def test_get_current_price(self, live_client, live_call):
"""Test getting current price from live API."""
price = live_client.prices.get("BRENT_CRUDE_USD")
price = live_call(live_client.prices.get, "BRENT_CRUDE_USD")

assert price is not None
assert price.commodity == "BRENT_CRUDE_USD"
assert price.value > 0
assert price.currency == "USD"
assert isinstance(price.timestamp, datetime)

def test_get_multiple_prices(self, live_client):
def test_get_multiple_prices(self, live_client, live_call):
"""Test getting multiple prices."""
commodities = ["BRENT_CRUDE_USD", "WTI_USD", "NATURAL_GAS_USD"]
prices = live_client.prices.get_multiple(commodities)
prices = live_call(live_client.prices.get_multiple, commodities)

assert len(prices) >= 1 # At least some should succeed
for price in prices:
assert price.value > 0
assert price.commodity in commodities

def test_get_historical_data(self, live_client):
def test_get_historical_data(self, live_client, live_call):
"""Test getting historical data."""
end_date = datetime.now()
start_date = end_date - timedelta(days=7)

history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="BRENT_CRUDE_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d"),
Expand All @@ -55,10 +56,10 @@ def test_get_historical_data(self, live_client):
assert price.value > 0
assert price.commodity == "BRENT_CRUDE_USD"

def test_invalid_commodity(self, live_client):
def test_invalid_commodity(self, live_client, live_call):
"""Test handling of invalid commodity."""
with pytest.raises(DataNotFoundError):
live_client.prices.get("TOTALLY_INVALID_COMMODITY_XYZ")
live_call(live_client.prices.get, "TOTALLY_INVALID_COMMODITY_XYZ")

@pytest.mark.skip(reason="API currently doesn't validate API keys for read operations")
def test_invalid_api_key(self):
Expand All @@ -70,21 +71,22 @@ def test_invalid_api_key(self):
with pytest.raises(AuthenticationError):
bad_client.prices.get("BRENT_CRUDE_USD")

def test_context_manager(self, api_key):
def test_context_manager(self, api_key, live_call):
"""Test client works as context manager."""
with OilPriceAPI(api_key=api_key) as client:
price = client.prices.get("BRENT_CRUDE_USD")
price = live_call(client.prices.get, "BRENT_CRUDE_USD")
assert price is not None


@pytest.mark.slow
class TestLiveAPIPerformance:
"""Performance tests (marked slow)."""

def test_pagination_performance(self, live_client):
def test_pagination_performance(self, live_client, live_call):
"""Test pagination doesn't cause issues."""
# Get a reasonable amount of data
history = live_client.historical.get(
history = live_call(
live_client.historical.get,
commodity="BRENT_CRUDE_USD",
start_date="2024-01-01",
end_date="2024-01-31",
Expand All @@ -97,10 +99,11 @@ def test_pagination_performance(self, live_client):
not os.getenv("RUN_EXPENSIVE_TESTS"),
reason="Expensive test - set RUN_EXPENSIVE_TESTS=1 to run"
)
def test_get_all_historical_large_dataset(self, live_client):
def test_get_all_historical_large_dataset(self, live_client, live_call):
"""Test get_all with large dataset (expensive)."""
# This could use many API calls
all_data = live_client.historical.get_all(
all_data = live_call(
live_client.historical.get_all,
commodity="BRENT_CRUDE_USD",
start_date="2024-01-01",
end_date="2024-02-01",
Expand Down
Loading