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
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# Copy this file to .env and add your actual values

# Your OilPriceAPI key (required)
# Get your free API key at: https://oilpriceapi.com
# Get your API key at: https://oilpriceapi.com
OILPRICEAPI_KEY=your_api_key_here

# API Base URL (optional)
# Default: https://api.oilpriceapi.com
# For local development: http://localhost:5000
OILPRICEAPI_BASE_URL=https://api.oilpriceapi.com
OILPRICEAPI_BASE_URL=https://api.oilpriceapi.com
13 changes: 12 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Install verification dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e '.[dev]' pip-audit build 'jsonschema>=4.17,<4.24'
python -m pip install -e '.[dev]' pip-audit 'build==1.5.0' 'jsonschema>=4.17,<4.24'

- name: Verify release tag matches package version and protected main
env:
Expand Down Expand Up @@ -69,6 +69,17 @@ jobs:
- name: Build package
run: python -m build

- name: Validate exact built source distribution
run: |
set -euo pipefail
PACKAGE_VERSION="$(python scripts/package_version.py)"
SDIST="dist/oilpriceapi-${PACKAGE_VERSION}.tar.gz"
if [ ! -f "$SDIST" ]; then
echo "::error::Exact source distribution not found: $SDIST"
exit 1
fi
python scripts/validate_storefront_claims.py --sdist "$SDIST"

- name: Install and import the exact built wheel
run: ./scripts/clean-wheel-smoke.sh

Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ jobs:
- name: Validate public storefront claims
run: python scripts/validate_storefront_claims.py

- name: Build and validate exact source distribution
if: matrix.python-version == '3.12'
run: |
set -euo pipefail
python -m pip install 'build==1.5.0'
python -m build --sdist
PACKAGE_VERSION="$(python scripts/package_version.py)"
SDIST="dist/oilpriceapi-${PACKAGE_VERSION}.tar.gz"
if [ ! -f "$SDIST" ]; then
echo "::error::Exact source distribution not found: $SDIST"
exit 1
fi
python scripts/validate_storefront_claims.py --sdist "$SDIST"

- name: Run unit tests
run: pytest tests/ --ignore=tests/integration --ignore=tests/contract -m 'not slow' --cov=oilpriceapi --cov-report=xml -v

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to the OilPriceAPI Python SDK will be documented in this file.

## [1.12.8] - 2026-08-12

### Fixed

- Validate every customer-readable member in the exact built source
distribution, including root release/configuration files and future nested
package data, while explicitly excluding intentional test/tooling fixtures.
- Remove the unsupported universal-entitlement wording from the packaged
environment example and reject never-existent promises that attribution
headers change entitlements in authored and distributed release notes.

## [1.12.7] - 2026-08-12

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion oilpriceapi/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
Used in __init__.py, client.py, and async_client.py.
"""

__version__ = "1.12.7"
__version__ = "1.12.8"
SDK_VERSION = __version__
SDK_NAME = "oilpriceapi-python"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "oilpriceapi"
version = "1.12.7"
version = "1.12.8"
description = "Official Python SDK for source-timestamped OilPriceAPI energy data"
authors = [
{name = "OilPriceAPI", email = "support@oilpriceapi.com"}
Expand Down
197 changes: 180 additions & 17 deletions scripts/validate_storefront_claims.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import argparse
import csv
import re
from pathlib import Path
from typing import Iterable, Iterator, List, Match, Pattern, Sequence, Set, Tuple
import tarfile
from pathlib import Path, PurePosixPath
from typing import Dict, Iterable, Iterator, List, Match, Pattern, Sequence, Set, Tuple

ROOT = Path(__file__).resolve().parents[1]
CONTRACT = "https://api.oilpriceapi.com/product-facts.json"
Expand All @@ -19,7 +20,19 @@
".pyc",
".pyo",
".so",
".wasm",
}
SDIST_DEVELOPMENT_ROOTS = {".github", ".pytest_cache", ".tox", "scripts", "test", "tests"}
ACTIVE_ROOT_SURFACES = (
".env.example",
"CONTRIBUTING.md",
"EXAMPLES.md",
"MANIFEST.in",
"README.md",
"SECURITY.md",
"pyproject.toml",
)
MAX_SDIST_TEXT_BYTES = 5_000_000
_RATE_COUNT = r"\d[\d,]*"
_RATE_ACTION = r"(?:(?:api[- ]+)?(?:requests?|calls?|queries?|hits?|credits?)|reqs?\.?)"
_RATE_UNIT_SINGULAR = r"(?:second|sec|minute|min|hour|hr|day|week|month|year)"
Expand Down Expand Up @@ -141,7 +154,7 @@


def discover_public_surfaces(root: Path = ROOT) -> List[Path]:
surfaces = [root / "README.md", root / "EXAMPLES.md", root / "pyproject.toml"]
surfaces = [root / name for name in ACTIVE_ROOT_SURFACES if (root / name).is_file()]
for directory in (root / "docs", root / "oilpriceapi"):
surfaces.extend(path for path in directory.rglob("*") if _is_public_text(path))
return sorted(set(surfaces))
Expand Down Expand Up @@ -309,29 +322,170 @@ def _telemetry_reward_claims(text: str) -> List[str]:
return claims


def _text_claim_failures(surface: str, text: str) -> List[str]:
failures: List[str] = []
for label, pattern in BLOCKED:
for match in pattern.finditer(text):
failures.append(f"{surface}: {label} matched {match.group(0)!r}")
for claim in _fixed_rate_claims(text):
failures.append(f"{surface}: fixed demo rate matched {claim!r}")
failures.extend(_telemetry_claim_failures(surface, text))
return failures


def _telemetry_claim_failures(surface: str, text: str) -> List[str]:
return [
f"{surface}: telemetry quota reward matched {claim!r}"
for claim in _telemetry_reward_claims(text)
]


def _claim_failures(root: Path, surfaces: Iterable[Path]) -> List[str]:
failures: List[str] = []
for path in surfaces:
text = path.read_text(encoding="utf-8")
for label, pattern in BLOCKED:
for match in pattern.finditer(text):
failures.extend(
_text_claim_failures(
path.relative_to(root).as_posix(),
path.read_text(encoding="utf-8"),
)
)
return failures


def _safe_sdist_member_path(name: str) -> PurePosixPath:
path = PurePosixPath(name)
if path.is_absolute() or ".." in path.parts or "\\" in name:
raise ValueError(f"unsafe source-distribution member path: {name!r}")
if not path.parts:
raise ValueError(f"source-distribution member has an empty path: {name!r}")
return path


def validate_sdist(sdist: Path) -> List[str]:
"""Validate every customer-readable surface in the exact built sdist."""
sdist = sdist.resolve()
archive_suffix = ".tar.gz"
if not sdist.name.endswith(archive_suffix):
return ["source distribution filename must end in .tar.gz"]
failures: List[str] = []
text_members: Dict[str, str] = {}
package_roots: Set[str] = set()
seen_members: Set[str] = set()

try:
archive = tarfile.open(sdist, mode="r:gz")
except (OSError, tarfile.TarError) as error:
return [f"source distribution could not be opened: {error}"]

with archive:
for member in archive.getmembers():
try:
path = _safe_sdist_member_path(member.name)
except ValueError as error:
failures.append(str(error))
continue

normalized = path.as_posix()
if normalized in seen_members:
failures.append(f"source distribution contains duplicate member: {normalized}")
continue
seen_members.add(normalized)
package_roots.add(path.parts[0])

if member.issym() or member.islnk():
failures.append(f"source distribution contains a link: {normalized}")
continue
if member.isdir():
continue
if len(path.parts) < 2:
failures.append(
f"{path.relative_to(root)}: {label} matched {match.group(0)!r}"
f"source-distribution member is outside its package root: {normalized!r}"
)
for claim in _fixed_rate_claims(text):
failures.append(
f"{path.relative_to(root)}: fixed demo rate matched {claim!r}"
)
for claim in _telemetry_reward_claims(text):
failures.append(
f"{path.relative_to(root)}: telemetry quota reward matched {claim!r}"
)
continue
if not member.isfile():
failures.append(f"source distribution contains a special member: {normalized}")
continue

relative = PurePosixPath(*path.parts[1:])
if (
relative.parts[0] in SDIST_DEVELOPMENT_ROOTS
or "__pycache__" in relative.parts
):
continue
if relative.suffix.lower() in BINARY_SUFFIXES:
continue
if member.size > MAX_SDIST_TEXT_BYTES:
failures.append(f"source distribution text candidate is too large: {relative}")
continue
Comment thread
karlwaldman marked this conversation as resolved.

extracted = archive.extractfile(member)
if extracted is None:
failures.append(f"source distribution member could not be read: {relative}")
continue
contents = extracted.read(MAX_SDIST_TEXT_BYTES + 1)
if len(contents) != member.size:
failures.append(f"source distribution member size changed while reading: {relative}")
continue
if b"\x00" in contents:
continue
try:
text_members[relative.as_posix()] = contents.decode("utf-8")
except UnicodeDecodeError:
continue

expected_root = sdist.name[: -len(archive_suffix)]
if package_roots != {expected_root}:
failures.append(
"source distribution package root differs from its filename: "
f"expected {expected_root!r}, found {sorted(package_roots)!r}"
)

for surface, text in sorted(text_members.items()):
if surface == "CHANGELOG.md":
# Historical release notes can truthfully describe retired plans.
# A telemetry quota reward never existed and is forbidden in history too.
failures.extend(_telemetry_claim_failures(surface, text))
else:
failures.extend(_text_claim_failures(surface, text))

metadata = text_members.get("PKG-INFO")
version_source = text_members.get("oilpriceapi/version.py")
if metadata is None:
failures.append("source distribution must contain readable PKG-INFO")
elif CONTRACT not in metadata:
failures.append("source distribution PKG-INFO: reviewed product-facts contract is not linked")
if version_source is None:
failures.append("source distribution must contain readable oilpriceapi/version.py")
if metadata is not None and version_source is not None:
metadata_match = re.search(r"^Version: ([^\s]+)$", metadata, re.MULTILINE)
module_match = re.search(r'^__version__ = "([^"]+)"', version_source, re.MULTILINE)
package_prefix = "oilpriceapi-"
expected_version = (
expected_root[len(package_prefix) :]
if expected_root.startswith(package_prefix)
else ""
)
versions = {
metadata_match.group(1) if metadata_match else None,
module_match.group(1) if module_match else None,
expected_version,
}
if None in versions or len(versions) != 1:
failures.append("source distribution filename, metadata, and module versions differ")
return failures


def validate(root: Path = ROOT) -> List[str]:
failures = _claim_failures(root, discover_public_surfaces(root))

changelog = root / "CHANGELOG.md"
if changelog.is_file():
failures.extend(
_telemetry_claim_failures(
"CHANGELOG.md", changelog.read_text(encoding="utf-8")
)
)

readme = (root / "README.md").read_text()
if CONTRACT not in readme:
failures.append("README.md: reviewed product-facts contract is not linked")
Expand Down Expand Up @@ -374,14 +528,23 @@ def validate_package(package_root: Path) -> List[str]:

def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--package-root", type=Path)
inputs = parser.add_mutually_exclusive_group()
inputs.add_argument("--package-root", type=Path)
inputs.add_argument("--sdist", type=Path)
args = parser.parse_args()

failures = validate_package(args.package_root) if args.package_root else validate()
if args.package_root:
failures = validate_package(args.package_root)
elif args.sdist:
failures = validate_sdist(args.sdist)
else:
failures = validate()
if failures:
raise SystemExit("\n".join(failures))
if args.package_root:
print("validated exact installed Python artifact claims")
elif args.sdist:
print("validated exact Python sdist claims")
else:
print(f"validated {len(discover_public_surfaces())} public surfaces")

Expand Down
15 changes: 13 additions & 2 deletions tests/test_release_readiness.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def test_packaged_futures_examples_prefer_instrument_generic_slugs() -> None:

def test_publish_gate_audits_and_installs_the_built_wheel() -> None:
workflow = (ROOT / ".github" / "workflows" / "publish.yml").read_text()
test_workflow = (ROOT / ".github" / "workflows" / "test.yml").read_text()
smoke = (ROOT / "scripts" / "clean-wheel-smoke.sh").read_text()

assert "pip-audit" in workflow
Expand All @@ -115,6 +116,16 @@ def test_publish_gate_audits_and_installs_the_built_wheel() -> None:
assert "--package-root" in smoke
assert 'oilpriceapi-${expected_version}-py3-none-any.whl' in smoke
assert "-name '*.whl' -print -quit" not in smoke
assert "--sdist" in workflow
assert 'oilpriceapi-${PACKAGE_VERSION}.tar.gz' in workflow
assert workflow.index("Build package") < workflow.index(
"Validate exact built source distribution"
) < workflow.index("Prepare checksummed release artifact")
assert "Build and validate exact source distribution" in test_workflow
assert "python scripts/validate_storefront_claims.py --sdist" in test_workflow
assert "matrix.python-version == '3.12'" in test_workflow
assert "'build==1.5.0'" in workflow
assert "'build==1.5.0'" in test_workflow


def test_oidc_publisher_consumes_only_the_verified_artifact() -> None:
Expand Down Expand Up @@ -143,7 +154,7 @@ def test_oidc_publisher_consumes_only_the_verified_artifact() -> None:
assert action_refs
assert all(re.fullmatch(r"[0-9a-f]{40}", ref) for ref in action_refs)
assert "Verify exact public PyPI hashes" in workflow
assert workflow.count("python scripts/package_version.py") == 2
assert workflow.count("python scripts/package_version.py") == 3
assert "seq 1 24" in workflow
assert "sleep_seconds" in workflow

Expand All @@ -159,7 +170,7 @@ def test_package_version_helper_reads_the_project_version() -> None:
capture_output=True,
text=True,
)
assert result.stdout.strip() == "1.12.7"
assert result.stdout.strip() == "1.12.8"


def test_every_workflow_pins_actions_and_hardens_each_checkout_step() -> None:
Expand Down
Loading