From 5894bc66a936b42fc7254423a4dbb8698200ddde Mon Sep 17 00:00:00 2001 From: Karl Waldman Date: Tue, 11 Aug 2026 07:14:59 -0400 Subject: [PATCH 1/2] release: prepare Python SDK v1.12.0 --- .github/PRE_RELEASE_CHECKLIST.md | 244 +++++-------------------------- .github/workflows/publish.yml | 8 +- CHANGELOG.md | 22 ++- oilpriceapi/version.py | 2 +- pyproject.toml | 8 +- scripts/clean-wheel-smoke.sh | 33 +++++ tests/test_release_readiness.py | 32 ++++ 7 files changed, 121 insertions(+), 228 deletions(-) create mode 100755 scripts/clean-wheel-smoke.sh create mode 100644 tests/test_release_readiness.py diff --git a/.github/PRE_RELEASE_CHECKLIST.md b/.github/PRE_RELEASE_CHECKLIST.md index aa56df6..27ba3db 100644 --- a/.github/PRE_RELEASE_CHECKLIST.md +++ b/.github/PRE_RELEASE_CHECKLIST.md @@ -1,223 +1,47 @@ # Pre-Release Validation Checklist -This checklist ensures SDK releases meet quality standards. **Complete ALL items before publishing to PyPI.** +The authoritative release design is documented in +[`docs/RELEASE_PROCESS.md`](../docs/RELEASE_PROCESS.md). Complete every gate +below before publishing a non-prerelease GitHub Release. -## Automated Checks (Run Script) +## Automated Gate -```bash -./scripts/pre-release-validation.sh -``` - -This script runs all automated validations and reports pass/fail status. - -## Manual Checklist - -### 1. Version Management ✅ -- [ ] Version bumped in `pyproject.toml` -- [ ] Version bumped in `oilpriceapi/__init__.py` -- [ ] Version updated in `CHANGELOG.md` -- [ ] CHANGELOG has comprehensive release notes -- [ ] No `UNRELEASED` sections in CHANGELOG - -### 2. Code Quality ✅ -- [ ] All unit tests pass (`pytest tests/unit -v`) -- [ ] All integration tests pass (`pytest tests/integration -v`) -- [ ] Test coverage ≥ 80% (`pytest --cov`) -- [ ] No linting errors (`ruff check .`) -- [ ] No type errors (`mypy oilpriceapi`) -- [ ] Code formatted (`black --check .`) - -### 3. Integration Validation ✅ -- [ ] Historical endpoint tests pass (catches timeout bug) -- [ ] Performance baselines met: - - 1-week queries: <30s - - 1-month queries: <60s - - 1-year queries: <120s -- [ ] All commodities tested -- [ ] Error handling verified - -### 4. Documentation ✅ -- [ ] README.md updated with new features -- [ ] API documentation current -- [ ] Code examples work -- [ ] Migration guide included (if breaking changes) -- [ ] Docstrings updated for new/changed functions - -### 5. Build & Package ✅ -- [ ] Clean build: `rm -rf dist/ build/ *.egg-info` -- [ ] Build succeeds: `python -m build` -- [ ] Wheel created: `ls dist/*.whl` -- [ ] Source distribution created: `ls dist/*.tar.gz` -- [ ] Package installs locally: `pip install dist/*.whl` -- [ ] Imports work: `python -c "import oilpriceapi; print(oilpriceapi.__version__)"` - -### 6. Backwards Compatibility ✅ -- [ ] No breaking changes (or documented in CHANGELOG) -- [ ] Existing code samples still work -- [ ] Deprecations properly warned -- [ ] Migration guide provided (if needed) - -### 7. Security ✅ -- [ ] No hardcoded credentials -- [ ] No secrets in code or tests -- [ ] Dependencies scanned: `pip-audit` -- [ ] SECURITY.md reviewed and current - -### 8. Git & GitHub ✅ -- [ ] All changes committed -- [ ] Commit message follows convention -- [ ] Git tag created: `git tag v1.X.Y` -- [ ] Tag pushed: `git push --tags` -- [ ] No uncommitted changes - -### 9. PyPI Publishing ✅ -- [ ] Test PyPI upload works: `twine upload --repository testpypi dist/*` -- [ ] Test installation from TestPyPI -- [ ] Production PyPI upload: `twine upload dist/*` -- [ ] Verify on PyPI: https://pypi.org/project/oilpriceapi/ -- [ ] Installation works: `pip install --upgrade oilpriceapi` - -### 10. Post-Release ✅ -- [ ] GitHub release created with notes -- [ ] Documentation site updated -- [ ] Announcement prepared (if major release) -- [ ] Monitor error tracking for 24 hours -- [ ] Check PyPI download stats - -## What Would Have Caught the v1.4.1 Bug? - -The historical timeout bug (reported by idan@comity.ai) would have been caught by: - -1. ✅ **Integration Tests** (`tests/integration/test_historical_endpoints.py`) - - `test_7_day_query_uses_past_week_endpoint` - Would fail (67s timeout) - - `test_365_day_query_uses_past_year_endpoint` - Would fail (30s timeout) - -2. ✅ **Performance Baselines** (`TestHistoricalPerformanceBaselines`) - - All tests would fail with timeouts - -3. ✅ **Pre-Release Script** (`scripts/pre-release-validation.sh`) - - Integration tests would fail - - Script would prevent release - -## Automation Script - -The `pre-release-validation.sh` script automates items 1-7: +Run from a clean checkout of the release commit: ```bash -# Run full validation -./scripts/pre-release-validation.sh - -# Run with verbose output -./scripts/pre-release-validation.sh --verbose - -# Skip slow tests (for quick checks) -./scripts/pre-release-validation.sh --skip-slow +python -m pip install --upgrade pip +python -m pip install -e '.[dev]' build pip-audit +ruff check oilpriceapi/ +mypy oilpriceapi/ --ignore-missing-imports +pytest tests/ --ignore=tests/integration --ignore=tests/contract -m 'not slow' +python scripts/validate_storefront_claims.py +python scripts/generate_snippet_manifest.py --source-commit "$(git rev-parse HEAD)" --output artifacts/snippets/oilpriceapi-python-snippets-v1.json +python -m build +./scripts/clean-wheel-smoke.sh +pip-audit ``` -**Exit Codes:** -- `0` - All checks passed, ready to release -- `1` - One or more checks failed, DO NOT release - -## Emergency Release Procedure +The hosted Python 3.8-3.12 matrix, keyless and keyed live tests, canonical +production snippets, and `Scheduled SDK Synthetic` must all be green at the +same release commit. The repository test gate enforces at least 50% aggregate +coverage; increases to that threshold require a reviewed test-coverage change. -If critical bug requires immediate release: - -1. Run minimum validation: - ```bash - pytest tests/unit -v --tb=short - pytest tests/integration/test_historical_endpoints.py -v - ``` - -2. Verify the specific fix works - -3. Document in CHANGELOG as emergency release - -4. **Still run full validation after emergency release** - -## Failed Validation - What to Do - -### Tests Failed -1. Fix failing tests -2. Re-run full validation -3. Update CHANGELOG if fixes required code changes - -### Performance Regression -1. Investigate using profiling -2. Fix performance issue -3. Re-establish baseline - -### Documentation Missing -1. Update documentation -2. Add code examples -3. Test examples actually work - -### Build Failed -1. Check `pyproject.toml` for errors -2. Verify all files included in manifest -3. Test clean build: `rm -rf dist/ && python -m build` - -## Version History - -| Version | Date | Validator | Result | Notes | -|---------|------|-----------|--------|-------| -| v1.4.2 | 2025-12-16 | Manual | ✅ Pass | Fixed historical timeout bug | -| v1.4.1 | 2025-12-15 | None | ❌ Fail | Historical timeout bug shipped | - -*Note: v1.4.1 did not use this checklist, which is why the bug reached production.* - -## Integration with CI/CD - -### GitHub Actions (Recommended) - -```yaml -name: Pre-Release Validation - -on: - push: - tags: - - 'v*' - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - pip install -e ".[dev]" - pip install twine pip-audit - - - name: Run pre-release validation - env: - OILPRICEAPI_KEY: ${{ secrets.OILPRICEAPI_KEY }} - run: ./scripts/pre-release-validation.sh - - - name: Build package - if: success() - run: python -m build - - - name: Publish to PyPI - if: success() - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: twine upload dist/* -``` +## Release Metadata -## Contact +- `pyproject.toml` and `oilpriceapi/version.py` contain the same version. +- `CHANGELOG.md` has one release section for that version with customer-visible + behavior and recovery guidance. +- The version is absent from PyPI and from existing GitHub releases. +- The release tag is exactly `v`. +- The worktree is clean and the tag resolves to the reviewed main commit. -Questions about the validation process: -- GitHub Issues: https://github.com/OilpriceAPI/python-sdk/issues -- Email: support@oilpriceapi.com +## Publication And Recovery -## Related Issues +Publish through a non-prerelease GitHub Release only. The `Publish to PyPI` +workflow verifies the tag, repeats the tests and dependency audit, builds and +installs the wheel in a clean environment, attaches the snippet manifest, and +uses PyPI trusted publishing. Do not upload with Twine or a local API token. -- [#20](https://github.com/OilpriceAPI/python-sdk/issues/20) - Integration tests -- [#21](https://github.com/OilpriceAPI/python-sdk/issues/21) - Performance baselines -- [#22](https://github.com/OilpriceAPI/python-sdk/issues/22) - Pre-release validation (this document) +PyPI artifacts are immutable. If a production defect appears, stop promotion, +yank the affected version, add a failing regression test, and publish a new +patch version through the same gate. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0edce7c..59f4b3d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e '.[dev]' + pip install -e '.[dev]' pip-audit - name: Verify release tag matches package version env: @@ -42,6 +42,9 @@ jobs: - name: Run unit tests run: pytest tests/ --ignore=tests/integration --ignore=tests/contract -m 'not slow' --cov=oilpriceapi -v + - name: Audit installed dependencies + run: pip-audit + publish: name: Publish to PyPI needs: test @@ -68,6 +71,9 @@ jobs: - name: Build package run: python -m build + - name: Install and import the exact built wheel + run: ./scripts/clean-wheel-smoke.sh + - name: Build signed snippet manifest run: | python scripts/generate_snippet_manifest.py \ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fab945..367d072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.12.0] - 2026-08-11 + ### Added - Add sync and async `client.commodities.search(...)`, backed by the current @@ -24,6 +26,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Stop retrying exhausted daily, monthly, and trial quota responses. Sync and + async clients now make one request at a durable quota wall while preserving + bounded retry behavior for recoverable hourly and ambiguous 429 responses. +- Replace the demo synthetic's fixed catalogue-size assertion with an + integrity contract for the original core codes and every usable returned + row. Request, transport, and operating-system failures now fail the monitor + instead of being converted to skips. - Preserve each API record's currency and unit in current and historical DataFrames instead of labeling a missing currency as USD. - Remove exact duplicate records introduced by overlapping page boundaries, @@ -383,19 +392,6 @@ print(df[["state", "price", "updated_at"]]) --- -## [Unreleased] - -### Planned - -- CLI tool (`oilprice` command) -- WebSocket support for real-time prices -- Advanced caching with Redis -- Technical indicators (RSI, MACD, Bollinger Bands) -- More visualization styles -- Jupyter notebook widgets - ---- - ## Release Notes ### How to Upgrade diff --git a/oilpriceapi/version.py b/oilpriceapi/version.py index 6d474c0..a933c70 100644 --- a/oilpriceapi/version.py +++ b/oilpriceapi/version.py @@ -5,6 +5,6 @@ Used in __init__.py, client.py, and async_client.py. """ -__version__ = "1.11.0" +__version__ = "1.12.0" SDK_VERSION = __version__ SDK_NAME = "oilpriceapi-python" diff --git a/pyproject.toml b/pyproject.toml index 02586b4..ded7f63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "oilpriceapi" -version = "1.11.0" +version = "1.12.0" description = "Official Python SDK for source-timestamped OilPriceAPI energy data" authors = [ {name = "OilPriceAPI", email = "support@oilpriceapi.com"} @@ -13,7 +13,8 @@ maintainers = [ {name = "OilPriceAPI Team", email = "support@oilpriceapi.com"} ] readme = "README.md" -license = {file = "LICENSE"} +license = "MIT" +license-files = ["LICENSE"] keywords = [ "oil", "prices", "api", "commodities", "energy", "brent", "wti", "natural gas", "source timestamps", "finance" @@ -23,7 +24,6 @@ classifiers = [ "Intended Audience :: Developers", "Intended Audience :: Financial and Insurance Industry", "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", @@ -108,6 +108,8 @@ include = '\.pyi?$' [tool.ruff] line-length = 100 target-version = "py38" + +[tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings diff --git a/scripts/clean-wheel-smoke.sh b/scripts/clean-wheel-smoke.sh new file mode 100755 index 0000000..b99b611 --- /dev/null +++ b/scripts/clean-wheel-smoke.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +wheel="$(find "$root_dir/dist" -maxdepth 1 -name '*.whl' -print -quit)" +expected_version="$( + cd "$root_dir" + python -c 'from oilpriceapi.version import SDK_VERSION; print(SDK_VERSION)' +)" + +if [[ -z "$wheel" ]]; then + echo "no wheel found under dist/" >&2 + exit 1 +fi + +smoke_dir="$(mktemp -d)" +trap 'rm -rf "$smoke_dir"' EXIT + +python -m venv "$smoke_dir/venv" +"$smoke_dir/venv/bin/python" -m pip install --quiet "$wheel" +"$smoke_dir/venv/bin/python" -m pip check +"$smoke_dir/venv/bin/python" -c ' +import sys +from oilpriceapi import OilPriceAPI, __version__ +from oilpriceapi.resources.demo import DemoResource + +expected = sys.argv[1] +assert __version__ == expected, (__version__, expected) +assert OilPriceAPI(api_key="artifact-smoke").prices is not None +assert DemoResource().base_url.startswith("https://") +' "$expected_version" + +echo "clean wheel smoke passed for oilpriceapi $expected_version" diff --git a/tests/test_release_readiness.py b/tests/test_release_readiness.py new file mode 100644 index 0000000..a4e9baf --- /dev/null +++ b/tests/test_release_readiness.py @@ -0,0 +1,32 @@ +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def test_release_documentation_matches_the_automated_gate() -> None: + checklist = (ROOT / ".github" / "PRE_RELEASE_CHECKLIST.md").read_text() + changelog = (ROOT / "CHANGELOG.md").read_text() + + referenced_scripts = re.findall(r"(?:^|\s)(\./scripts/[A-Za-z0-9_.-]+)", checklist) + missing = [path for path in referenced_scripts if not (ROOT / path).exists()] + + assert missing == [] + assert "oilpriceapi/version.py" in checklist + assert "twine upload" not in checklist + assert changelog.count("## [Unreleased]") == 1 + + +def test_publish_gate_audits_and_installs_the_built_wheel() -> None: + workflow = (ROOT / ".github" / "workflows" / "publish.yml").read_text() + + assert "pip-audit" in workflow + assert "scripts/clean-wheel-smoke.sh" in workflow + + +def test_packaging_configuration_uses_current_metadata_forms() -> None: + project = (ROOT / "pyproject.toml").read_text() + + assert 'license = "MIT"' in project + assert "License :: OSI Approved :: MIT License" not in project + assert "[tool.ruff.lint]" in project From ebad1eeec5e888fb00ff8d51d5b445cc14353169 Mon Sep 17 00:00:00 2001 From: Karl Waldman Date: Tue, 11 Aug 2026 07:25:22 -0400 Subject: [PATCH 2/2] release: keep Python 3.8 artifact gate strict --- .github/workflows/publish.yml | 1 - EXAMPLES.md | 2 +- pyproject.toml | 8 +++++--- scripts/clean-wheel-smoke.sh | 10 ++++++++-- tests/test_release_readiness.py | 17 ++++++++++++++--- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 59f4b3d..c34cf20 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -37,7 +37,6 @@ jobs: - name: Lint source with ruff run: ruff check oilpriceapi/ - continue-on-error: true - name: Run unit tests run: pytest tests/ --ignore=tests/integration --ignore=tests/contract -m 'not slow' --cov=oilpriceapi -v diff --git a/EXAMPLES.md b/EXAMPLES.md index d217d8e..ca30a47 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -621,7 +621,7 @@ print("📊 Powered by https://oilpriceapi.com") Ready to build with these examples? -1. **[Sign up for free](https://oilpriceapi.com/auth/signup)** - Get 1,000 free requests/month +1. **[Sign up for free](https://oilpriceapi.com/auth/signup)** - Get 50 requests/day 2. **[Install the SDK](https://pypi.org/project/oilpriceapi/)** - `pip install oilpriceapi` 3. **[Read the docs](https://docs.oilpriceapi.com/sdk/python)** - Complete API reference 4. **[Choose a plan](https://oilpriceapi.com/pricing)** - Upgrade for more requests diff --git a/pyproject.toml b/pyproject.toml index ded7f63..f1ba1e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,7 @@ [build-system] -requires = ["setuptools>=61.0", "wheel"] +# Setuptools 77+ warns on the legacy license table, while the older backend +# available on supported Python 3.8 cannot parse the replacement string form. +requires = ["setuptools>=70.1,<77", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -13,8 +15,7 @@ maintainers = [ {name = "OilPriceAPI Team", email = "support@oilpriceapi.com"} ] readme = "README.md" -license = "MIT" -license-files = ["LICENSE"] +license = {file = "LICENSE"} keywords = [ "oil", "prices", "api", "commodities", "energy", "brent", "wti", "natural gas", "source timestamps", "finance" @@ -24,6 +25,7 @@ classifiers = [ "Intended Audience :: Developers", "Intended Audience :: Financial and Insurance Industry", "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", diff --git a/scripts/clean-wheel-smoke.sh b/scripts/clean-wheel-smoke.sh index b99b611..7f67447 100755 --- a/scripts/clean-wheel-smoke.sh +++ b/scripts/clean-wheel-smoke.sh @@ -27,7 +27,13 @@ from oilpriceapi.resources.demo import DemoResource expected = sys.argv[1] assert __version__ == expected, (__version__, expected) assert OilPriceAPI(api_key="artifact-smoke").prices is not None -assert DemoResource().base_url.startswith("https://") +demo = DemoResource() +assert demo.base_url.startswith("https://") +prices = demo.prices()["prices"] +brent = next((price for price in prices if price.get("code") == "BRENT_CRUDE_USD"), None) +assert brent is not None +assert isinstance(brent.get("price"), (int, float)) +assert brent.get("updated_at") ' "$expected_version" -echo "clean wheel smoke passed for oilpriceapi $expected_version" +echo "clean wheel install and production demo smoke passed for oilpriceapi $expected_version" diff --git a/tests/test_release_readiness.py b/tests/test_release_readiness.py index a4e9baf..d617ec0 100644 --- a/tests/test_release_readiness.py +++ b/tests/test_release_readiness.py @@ -17,16 +17,27 @@ def test_release_documentation_matches_the_automated_gate() -> None: assert changelog.count("## [Unreleased]") == 1 +def test_examples_use_the_canonical_free_quota() -> None: + examples = (ROOT / "EXAMPLES.md").read_text() + + assert "50 requests/day" in examples + assert not re.search(r"free requests?/(?:month|monthly)", examples, re.IGNORECASE) + + def test_publish_gate_audits_and_installs_the_built_wheel() -> None: workflow = (ROOT / ".github" / "workflows" / "publish.yml").read_text() assert "pip-audit" in workflow assert "scripts/clean-wheel-smoke.sh" in workflow + assert "continue-on-error: true" not in workflow -def test_packaging_configuration_uses_current_metadata_forms() -> None: +def test_packaging_configuration_remains_compatible_with_supported_python() -> None: project = (ROOT / "pyproject.toml").read_text() - assert 'license = "MIT"' in project - assert "License :: OSI Approved :: MIT License" not in project + # Python 3.8 resolves a setuptools release whose schema does not yet accept + # the PEP 639 string form. Keep the PEP 621 table while 3.8 is supported. + assert 'requires = ["setuptools>=70.1,<77", "wheel"]' in project + assert 'license = {file = "LICENSE"}' in project + assert 'requires-python = ">=3.8"' in project assert "[tool.ruff.lint]" in project