From 90b947ba7be66679940abb2f90f5c0c055abeaa8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:39:59 +0000 Subject: [PATCH 1/3] chore(deps): bump actions/download-artifact from 8.0.1 to 8.0.2 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 8.0.1 to 8.0.2. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c...9000827ccba6bdab643e8b6fd33ac0654aef8333) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: 8.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e687ee8..87e8110 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -125,7 +125,7 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 with: name: verified-pypi-package path: ${{ runner.temp }}/release-artifact @@ -168,7 +168,7 @@ jobs: contents: read steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 with: name: verified-pypi-package path: ${{ runner.temp }}/release-artifact @@ -227,7 +227,7 @@ jobs: contents: write steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 with: name: verified-pypi-package path: ${{ runner.temp }}/release-artifact From 1e881a19627e462bd452bf0c3223a9dfa4804219 Mon Sep 17 00:00:00 2001 From: karlwaldman Date: Wed, 14 Oct 2026 18:40:18 -0400 Subject: [PATCH 2/3] fix: lazy visualizer so client construction never warns (#151); remove root clutter (#157) * fix: build the visualizer lazily so client construction never warns (#151) OilPriceAPI.__init__ built PriceVisualizer eagerly, which imported matplotlib when present and emitted ImportWarning when absent, so `python -W error` could not construct a client. `viz` is now a property that builds on first access and returns None without matplotlib (unchanged contract for `if not client.viz`). Plotting methods still raise ImportError with guidance. Also removes root clutter: test_debug_params.py (an ad-hoc live-call debug script, not a test; pytest testpaths is tests/) and WEBSITE_FIXES_DEPLOYED.md (a 2025-11 website deploy note). Closes #151 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QQ7W3CkRKEnBpU5KRQUfUu * fix: client.viz raises ImportError naming the viz extra without matplotlib (#151) Adversarial review of #157 found client.viz returned None when matplotlib was missing, so client.viz.plot_* failed with a bare AttributeError. The viz property now always builds a cached PriceVisualizer lazily (no import, no warning at construction), and each plotting method raises ImportError: "matplotlib is required for visualization. Install it with: pip install 'oilpriceapi[viz]'". Adds the viz extra to pyproject. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QQ7W3CkRKEnBpU5KRQUfUu --------- Co-authored-by: Claude Opus 5.5 --- WEBSITE_FIXES_DEPLOYED.md | 231 ------------------------------- examples/visualization_demo.py | 13 +- oilpriceapi/client.py | 25 ++-- oilpriceapi/visualization.py | 24 ++-- pyproject.toml | 5 + test_debug_params.py | 45 ------ tests/test_lazy_visualization.py | 104 ++++++++++++++ 7 files changed, 146 insertions(+), 301 deletions(-) delete mode 100644 WEBSITE_FIXES_DEPLOYED.md delete mode 100644 test_debug_params.py create mode 100644 tests/test_lazy_visualization.py diff --git a/WEBSITE_FIXES_DEPLOYED.md b/WEBSITE_FIXES_DEPLOYED.md deleted file mode 100644 index a6c2bf4..0000000 --- a/WEBSITE_FIXES_DEPLOYED.md +++ /dev/null @@ -1,231 +0,0 @@ -# Python SDK Website Fixes - DEPLOYED - -**Date:** 2025-11-26 -**Deployment ID:** 0502502b-d00d-46d2-b9cf-ba5b0018b517 -**Status:** ✅ Deployed to Production - ---- - -## Changes Made - -### 1. Fixed Hero Code Example -**File:** `app/python-oil-api/page.tsx` (lines 53-66) - -**Before (WRONG):** -```python -client = OilPriceAPI(api_key="your_free_key") -prices = client.get_latest_prices() # ❌ This method doesn't exist -print(f"WTI: ${prices['WTI_USD']}/barrel") -``` - -**After (CORRECT):** -```python -client = OilPriceAPI(api_key="your_free_key") -wti = client.prices.get("WTI_USD") # ✅ Actual SDK API -brent = client.prices.get("BRENT_CRUDE_USD") -print(f"WTI: ${wti.value}/barrel") -print(f"Brent: ${brent.value}/barrel") -``` - -### 2. Fixed Installation Example -**File:** `app/python-oil-api/page.tsx` (lines 132-147) - -**Before (WRONG):** -```python -latest = client.get_latest_prices() # ❌ Wrong method -historical = client.get_historical(...) # ❌ Wrong method -``` - -**After (CORRECT):** -```python -wti = client.prices.get("WTI_USD") # ✅ Correct -historical = client.historical.get(...) # ✅ Correct -``` - -### 3. Added Async Section -**File:** `app/python-oil-api/page.tsx` (new section after line 153) - -**New content:** -```python -import asyncio -from oilpriceapi import AsyncOilPriceAPI - -async def get_prices(): - async with AsyncOilPriceAPI() as client: - # Fetch multiple commodities concurrently - prices = await asyncio.gather( - client.prices.get("BRENT_CRUDE_USD"), - client.prices.get("WTI_USD"), - client.prices.get("NATURAL_GAS_USD") - ) - for price in prices: - print(f"{price.commodity}: ${price.value}") - -asyncio.run(get_prices()) -``` - -**Benefits:** -- Shows async/await support (key selling point from Reddit post) -- Connection pooling mentioned (max 100 concurrent, 20 keepalive) -- Demonstrates concurrent requests with `asyncio.gather()` - ---- - -## Deployment Details - -**Git commit:** 8609d29 -**Commit message:** "fix: Update Python SDK code examples to match actual API" - -**Deployment command:** -```bash -doctl apps create-deployment 4cf05d8d-32ae-4cea-9e3b-2993b41bd11b -``` - -**Deployment progress:** -- Phase: PENDING_BUILD -- Progress: 0/6 -- Created: 2025-11-26 14:54:59 UTC - -**Expected completion:** ~5-7 minutes - ---- - -## Verification Checklist - -After deployment completes, verify: - -- [ ] Visit https://oilpriceapi.com/python-oil-api -- [ ] Check hero code example shows `client.prices.get()` -- [ ] Check async section is visible -- [ ] Check installation example uses correct methods -- [ ] Test code examples work (copy/paste and run) -- [ ] Clear Cloudflare cache if needed - ---- - -## Impact - -**Before fix:** -- ❌ Users copy code from website → **code fails** -- ❌ Bad first impression -- ❌ Support burden ("why don't your examples work?") -- ❌ Reddit users would downvote for misleading claims - -**After fix:** -- ✅ Users copy code → **it works** -- ✅ Good first impression -- ✅ No support burden -- ✅ Reddit users can verify claims -- ✅ Increased conversions (working code = trust) - ---- - -## What's Still TODO (Lower Priority) - -### Page: `/developers/python` -**Status:** Not fixed yet (P2) - -**Issue:** Shows manual `requests` implementation instead of SDK - -**Current code (lines 23-99):** -```python -import requests - -class OilPriceAPI: - def __init__(self, api_key): - # ... manual requests implementation -``` - -**Should show:** Actual `oilpriceapi` package usage - -**When to fix:** After Saturday Reddit post, if users report it - ---- - -## Files Changed - -1. `/home/kwaldman/code/website-clean/app/python-oil-api/page.tsx` - - Lines 53-66: Fixed hero example - - Lines 132-147: Fixed installation example - - Lines 155-200: Added async section (NEW) - ---- - -## Testing Plan - -**After deployment completes:** - -1. **Visit production page:** - ```bash - open https://oilpriceapi.com/python-oil-api - ``` - -2. **Copy first code example:** - ```bash - cd /home/kwaldman/code/sdks/python - python3 << 'EOF' - from oilpriceapi import OilPriceAPI - client = OilPriceAPI(api_key="3839c085...") - wti = client.prices.get("WTI_USD") - brent = client.prices.get("BRENT_CRUDE_USD") - print(f"WTI: ${wti.value}/barrel") - print(f"Brent: ${brent.value}/barrel") - EOF - ``` - -3. **Copy async example:** - ```bash - python3 << 'EOF' - import asyncio - from oilpriceapi import AsyncOilPriceAPI - - async def test(): - async with AsyncOilPriceAPI(api_key="3839c085...") as client: - prices = await asyncio.gather( - client.prices.get("BRENT_CRUDE_USD"), - client.prices.get("WTI_USD") - ) - for price in prices: - print(f"{price.commodity}: ${price.value}") - - asyncio.run(test()) - EOF - ``` - -4. **Both should work** ✅ - ---- - -## Monitoring - -**Check deployment status:** -```bash -doctl apps get-deployment 4cf05d8d-32ae-4cea-9e3b-2993b41bd11b 0502502b-d00d-46d2-b9cf-ba5b0018b517 -``` - -**Check production site:** -```bash -curl -I https://oilpriceapi.com/python-oil-api -``` - -**Cloudflare cache:** -If changes not visible, purge cache: -1. Go to Cloudflare dashboard -2. Caching → Purge Everything - ---- - -## Success Criteria - -✅ **Fixed BEFORE Saturday Reddit post** -✅ **Code examples match actual SDK API** -✅ **Async support prominently featured** -✅ **No false claims that can be disproven** - -**Result:** Reddit users can try the SDK and it works as advertised = good reputation + more stars/downloads. - ---- - -**Status:** ✅ DEPLOYED -**Next check:** Verify deployment completes (5-7 min) -**After verification:** Ready for Saturday Reddit post diff --git a/examples/visualization_demo.py b/examples/visualization_demo.py index 4396c37..7b4cf26 100644 --- a/examples/visualization_demo.py +++ b/examples/visualization_demo.py @@ -13,6 +13,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from oilpriceapi import OilPriceAPI +from oilpriceapi.visualization import HAS_MATPLOTLIB # Configuration API_KEY = os.getenv("OILPRICEAPI_KEY") @@ -30,8 +31,8 @@ def demo_price_series(): client = OilPriceAPI(api_key=API_KEY, base_url=BASE_URL) - if not client.viz: - print("⚠️ Matplotlib not installed. Install with: pip install matplotlib") + if not HAS_MATPLOTLIB: + print("⚠️ Matplotlib not installed. Install with: pip install 'oilpriceapi[viz]'") return # Create visualization @@ -76,7 +77,7 @@ def demo_spread_analysis(): client = OilPriceAPI(api_key=API_KEY, base_url=BASE_URL) - if not client.viz: + if not HAS_MATPLOTLIB: print("⚠️ Visualization not available") return @@ -112,7 +113,7 @@ def demo_sparklines(): client = OilPriceAPI(api_key=API_KEY, base_url=BASE_URL) - if not client.viz: + if not HAS_MATPLOTLIB: print("⚠️ Visualization not available") return @@ -146,7 +147,7 @@ def demo_small_multiples(): client = OilPriceAPI(api_key=API_KEY, base_url=BASE_URL) - if not client.viz: + if not HAS_MATPLOTLIB: print("⚠️ Visualization not available") return @@ -191,7 +192,7 @@ def main(): print(f"\n✅ Matplotlib version: {matplotlib.__version__}") except ImportError: print("\n⚠️ Matplotlib not installed!") - print(" Install with: pip install matplotlib") + print(" Install with: pip install 'oilpriceapi[viz]'") return # Run demos diff --git a/oilpriceapi/client.py b/oilpriceapi/client.py index 044a7e9..69804e2 100644 --- a/oilpriceapi/client.py +++ b/oilpriceapi/client.py @@ -221,20 +221,29 @@ def __init__( # LTL + parcel carrier fuel surcharges (#101). self.fuel_surcharge = FuelSurchargeResource(self) - # Initialize visualization (optional) - self.viz: Optional["PriceVisualizer"] - try: - from .visualization import PriceVisualizer - - self.viz = PriceVisualizer(self) - except ImportError: - self.viz = None + # Visualization is optional and built on first use of ``viz`` (#151): + # constructing a client must not import matplotlib or warn about it. + self._viz: Optional["PriceVisualizer"] = None # Initialize telemetry (opt-in, disabled by default) from .telemetry import Telemetry self._telemetry = Telemetry(enabled=enable_telemetry) + @property + def viz(self) -> "PriceVisualizer": + """Tufte-style charts, built on first access. + + Building the visualizer imports nothing optional and never warns. If + matplotlib is missing, each plotting method raises ``ImportError`` + naming the extra: ``pip install 'oilpriceapi[viz]'``. + """ + if self._viz is None: + from .visualization import PriceVisualizer + + self._viz = PriceVisualizer(self) + return self._viz + def request( self, method: str, diff --git a/oilpriceapi/visualization.py b/oilpriceapi/visualization.py index 4b919d8..be6a7e1 100644 --- a/oilpriceapi/visualization.py +++ b/oilpriceapi/visualization.py @@ -23,6 +23,13 @@ except ImportError: HAS_MATPLOTLIB = False +# Raised by every plotting method when matplotlib is not installed. It names +# the extra so the fix is one copy-paste away. +MATPLOTLIB_REQUIRED = ( + "matplotlib is required for visualization. " + "Install it with: pip install 'oilpriceapi[viz]'" +) + try: import pandas as pd HAS_PANDAS = True @@ -88,13 +95,8 @@ class PriceVisualizer: def __init__(self, client): """Initialize visualizer with OilPriceAPI client.""" self.client = client - - if not HAS_MATPLOTLIB: - warnings.warn( - "matplotlib is required for visualization. " - "Install with: pip install matplotlib", - ImportWarning - ) + # No warning here (#151): every plotting method raises ImportError + # with install guidance when matplotlib is missing. def plot_price_series( self, @@ -119,7 +121,7 @@ def plot_price_series( matplotlib Figure object """ if not HAS_MATPLOTLIB: - raise ImportError("matplotlib required for visualization") + raise ImportError(MATPLOTLIB_REQUIRED) # Fetch data history = self.client.historical.get( @@ -238,7 +240,7 @@ def plot_spread( Following Tufte's principle of showing data relationships clearly. """ if not HAS_MATPLOTLIB: - raise ImportError("matplotlib required for visualization") + raise ImportError(MATPLOTLIB_REQUIRED) # Fetch data for both commodities hist1 = self.client.historical.get( @@ -381,7 +383,7 @@ def create_sparkline( Perfect for embedding in reports or dashboards. """ if not HAS_MATPLOTLIB: - raise ImportError("matplotlib required for visualization") + raise ImportError(MATPLOTLIB_REQUIRED) # Fetch recent data end_date = datetime.now() @@ -442,7 +444,7 @@ def create_small_multiples( Allows easy comparison across multiple commodities. """ if not HAS_MATPLOTLIB: - raise ImportError("matplotlib required for visualization") + raise ImportError(MATPLOTLIB_REQUIRED) n = len(commodities) rows = (n + cols - 1) // cols diff --git a/pyproject.toml b/pyproject.toml index 833e6c9..0512f94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,11 @@ cli = [ "click>=8.0.0", "rich>=13.0.0", ] +viz = [ + "matplotlib>=3.5.0", + "numpy>=1.20.0", + "pandas>=1.5.0", +] all = [ "oilpriceapi[pandas,async,stream,cache,cli]", ] diff --git a/test_debug_params.py b/test_debug_params.py deleted file mode 100644 index db19374..0000000 --- a/test_debug_params.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -"""Debug script to see what parameters SDK sends to API.""" - -import sys -import os -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent)) - -from oilpriceapi import OilPriceAPI -from dotenv import load_dotenv - -load_dotenv() - -# Monkey patch the request method to print parameters -original_request = OilPriceAPI.request - -def debug_request(self, method, path, params=None, **kwargs): - print(f"\n🔍 DEBUG: API Request") - print(f" Method: {method}") - print(f" Path: {path}") - print(f" Params: {params}") - print(f" Kwargs: {kwargs}") - print() - return original_request(self, method, path, params, **kwargs) - -OilPriceAPI.request = debug_request - -# Now make the request -client = OilPriceAPI(api_key=os.getenv('OILPRICEAPI_KEY')) - -print("Making historical request with custom date range...") -response = client.historical.get( - commodity="WTI_USD", - start_date="2024-01-01", - end_date="2024-01-10", - interval="daily" -) - -print(f"\n✓ Got {len(response.data)} records") -if len(response.data) > 0: - print(f" First: {response.data[0].date}") - print(f" Last: {response.data[-1].date}") - -client.close() diff --git a/tests/test_lazy_visualization.py b/tests/test_lazy_visualization.py new file mode 100644 index 0000000..4297482 --- /dev/null +++ b/tests/test_lazy_visualization.py @@ -0,0 +1,104 @@ +"""#151: constructing a client must not warn about optional matplotlib.""" + +import subprocess +import sys +import warnings + +import pytest + +from oilpriceapi import OilPriceAPI + + +def test_client_construction_emits_no_warning(): + with warnings.catch_warnings(): + warnings.simplefilter("error") + client = OilPriceAPI(api_key="x") + client.close() + + +def test_import_and_construct_under_W_error(): + # The acceptance check from the issue, in a fresh interpreter so no + # earlier import has already filtered or triggered the warning. + result = subprocess.run( + [ + sys.executable, + "-W", + "error", + "-c", + "import oilpriceapi; oilpriceapi.OilPriceAPI(api_key='x').close()", + ], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + + +def test_construction_does_not_import_matplotlib(): + code = ( + "import sys, oilpriceapi; c = oilpriceapi.OilPriceAPI(api_key='x'); " + "print('matplotlib' in sys.modules); c.close()" + ) + result = subprocess.run( + [sys.executable, "-c", code], capture_output=True, text=True + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "False" + + +# Simulates an install without matplotlib: a None entry in sys.modules makes +# ``import matplotlib`` raise ImportError in a fresh interpreter. +_NO_MPL = ( + "import sys; sys.modules['matplotlib'] = None; " + "sys.modules['matplotlib.pyplot'] = None; " +) + + +@pytest.mark.parametrize( + "call", + [ + "c.viz.plot_price_series('WTI_USD')", + "c.viz.plot_spread('BRENT_CRUDE_USD', 'WTI_USD')", + "c.viz.create_sparkline([1.0, 2.0])", + "c.viz.create_small_multiples(['WTI_USD'])", + ], +) +def test_plotting_without_matplotlib_names_the_extra(call): + # Review of #157: client.viz returned None without matplotlib, so every + # documented ``client.viz.plot_*`` call died with a bare AttributeError. + code = ( + _NO_MPL + + "import oilpriceapi; c = oilpriceapi.OilPriceAPI(api_key='x')\n" + + "try:\n" + + f" {call}\n" + + "except ImportError as e:\n" + + " print('IMPORTERROR:' + str(e))\n" + + "except Exception as e:\n" + + " print('OTHER:' + type(e).__name__ + ':' + str(e))\n" + + "c.close()" + ) + result = subprocess.run( + [sys.executable, "-W", "error", "-c", code], capture_output=True, text=True + ) + assert result.returncode == 0, result.stderr + out = result.stdout.strip() + assert out.startswith("IMPORTERROR:"), out + assert "pip install 'oilpriceapi[viz]'" in out, out + + +def test_construction_without_matplotlib_is_warning_free(): + code = _NO_MPL + "import oilpriceapi; oilpriceapi.OilPriceAPI(api_key='x').close()" + result = subprocess.run( + [sys.executable, "-W", "error", "-c", code], capture_output=True, text=True + ) + assert result.returncode == 0, result.stderr + + +def test_viz_is_a_cached_visualizer(): + from oilpriceapi.visualization import PriceVisualizer + + client = OilPriceAPI(api_key="x") + try: + assert isinstance(client.viz, PriceVisualizer) + assert client.viz is client.viz + finally: + client.close() From 55775c90651a730407c11d7f06102d6c55c4fee0 Mon Sep 17 00:00:00 2001 From: Karl Waldman Date: Sat, 17 Oct 2026 17:42:36 -0400 Subject: [PATCH 3/3] test: verify the updated artifact action pin --- tests/test_release_readiness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_release_readiness.py b/tests/test_release_readiness.py index e1aa1ba..19fe36d 100644 --- a/tests/test_release_readiness.py +++ b/tests/test_release_readiness.py @@ -136,7 +136,7 @@ def test_oidc_publisher_consumes_only_the_verified_artifact() -> None: assert "Verify release tag matches package version and protected main" in workflow assert "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in workflow - assert "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" in workflow + assert "actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333" in workflow assert "pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33" in publish assert "id-token: write" in publish assert "sha256sum -c artifact.sha256" in publish