Skip to content

Commit 8b2dd20

Browse files
authored
Scan every customer-readable source distribution surface (#92)
* Scan exact Python source distributions * Exclude known binary sdist members before text bounds
1 parent eb40146 commit 8b2dd20

9 files changed

Lines changed: 427 additions & 25 deletions

File tree

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
# Copy this file to .env and add your actual values
33

44
# Your OilPriceAPI key (required)
5-
# Get your free API key at: https://oilpriceapi.com
5+
# Get your API key at: https://oilpriceapi.com
66
OILPRICEAPI_KEY=your_api_key_here
77

88
# API Base URL (optional)
99
# Default: https://api.oilpriceapi.com
1010
# For local development: http://localhost:5000
11-
OILPRICEAPI_BASE_URL=https://api.oilpriceapi.com
11+
OILPRICEAPI_BASE_URL=https://api.oilpriceapi.com

.github/workflows/publish.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
- name: Install verification dependencies
3131
run: |
3232
python -m pip install --upgrade pip
33-
python -m pip install -e '.[dev]' pip-audit build 'jsonschema>=4.17,<4.24'
33+
python -m pip install -e '.[dev]' pip-audit 'build==1.5.0' 'jsonschema>=4.17,<4.24'
3434
3535
- name: Verify release tag matches package version and protected main
3636
env:
@@ -69,6 +69,17 @@ jobs:
6969
- name: Build package
7070
run: python -m build
7171

72+
- name: Validate exact built source distribution
73+
run: |
74+
set -euo pipefail
75+
PACKAGE_VERSION="$(python scripts/package_version.py)"
76+
SDIST="dist/oilpriceapi-${PACKAGE_VERSION}.tar.gz"
77+
if [ ! -f "$SDIST" ]; then
78+
echo "::error::Exact source distribution not found: $SDIST"
79+
exit 1
80+
fi
81+
python scripts/validate_storefront_claims.py --sdist "$SDIST"
82+
7283
- name: Install and import the exact built wheel
7384
run: ./scripts/clean-wheel-smoke.sh
7485

.github/workflows/test.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ jobs:
4343
- name: Validate public storefront claims
4444
run: python scripts/validate_storefront_claims.py
4545

46+
- name: Build and validate exact source distribution
47+
if: matrix.python-version == '3.12'
48+
run: |
49+
set -euo pipefail
50+
python -m pip install 'build==1.5.0'
51+
python -m build --sdist
52+
PACKAGE_VERSION="$(python scripts/package_version.py)"
53+
SDIST="dist/oilpriceapi-${PACKAGE_VERSION}.tar.gz"
54+
if [ ! -f "$SDIST" ]; then
55+
echo "::error::Exact source distribution not found: $SDIST"
56+
exit 1
57+
fi
58+
python scripts/validate_storefront_claims.py --sdist "$SDIST"
59+
4660
- name: Run unit tests
4761
run: pytest tests/ --ignore=tests/integration --ignore=tests/contract -m 'not slow' --cov=oilpriceapi --cov-report=xml -v
4862

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

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

5+
## [1.12.8] - 2026-08-12
6+
7+
### Fixed
8+
9+
- Validate every customer-readable member in the exact built source
10+
distribution, including root release/configuration files and future nested
11+
package data, while explicitly excluding intentional test/tooling fixtures.
12+
- Remove the unsupported universal-entitlement wording from the packaged
13+
environment example and reject never-existent promises that attribution
14+
headers change entitlements in authored and distributed release notes.
15+
516
## [1.12.7] - 2026-08-12
617

718
### Fixed

oilpriceapi/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
Used in __init__.py, client.py, and async_client.py.
66
"""
77

8-
__version__ = "1.12.7"
8+
__version__ = "1.12.8"
99
SDK_VERSION = __version__
1010
SDK_NAME = "oilpriceapi-python"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
66

77
[project]
88
name = "oilpriceapi"
9-
version = "1.12.7"
9+
version = "1.12.8"
1010
description = "Official Python SDK for source-timestamped OilPriceAPI energy data"
1111
authors = [
1212
{name = "OilPriceAPI", email = "support@oilpriceapi.com"}

scripts/validate_storefront_claims.py

Lines changed: 180 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import argparse
55
import csv
66
import re
7-
from pathlib import Path
8-
from typing import Iterable, Iterator, List, Match, Pattern, Sequence, Set, Tuple
7+
import tarfile
8+
from pathlib import Path, PurePosixPath
9+
from typing import Dict, Iterable, Iterator, List, Match, Pattern, Sequence, Set, Tuple
910

1011
ROOT = Path(__file__).resolve().parents[1]
1112
CONTRACT = "https://api.oilpriceapi.com/product-facts.json"
@@ -19,7 +20,19 @@
1920
".pyc",
2021
".pyo",
2122
".so",
23+
".wasm",
2224
}
25+
SDIST_DEVELOPMENT_ROOTS = {".github", ".pytest_cache", ".tox", "scripts", "test", "tests"}
26+
ACTIVE_ROOT_SURFACES = (
27+
".env.example",
28+
"CONTRIBUTING.md",
29+
"EXAMPLES.md",
30+
"MANIFEST.in",
31+
"README.md",
32+
"SECURITY.md",
33+
"pyproject.toml",
34+
)
35+
MAX_SDIST_TEXT_BYTES = 5_000_000
2336
_RATE_COUNT = r"\d[\d,]*"
2437
_RATE_ACTION = r"(?:(?:api[- ]+)?(?:requests?|calls?|queries?|hits?|credits?)|reqs?\.?)"
2538
_RATE_UNIT_SINGULAR = r"(?:second|sec|minute|min|hour|hr|day|week|month|year)"
@@ -141,7 +154,7 @@
141154

142155

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

311324

325+
def _text_claim_failures(surface: str, text: str) -> List[str]:
326+
failures: List[str] = []
327+
for label, pattern in BLOCKED:
328+
for match in pattern.finditer(text):
329+
failures.append(f"{surface}: {label} matched {match.group(0)!r}")
330+
for claim in _fixed_rate_claims(text):
331+
failures.append(f"{surface}: fixed demo rate matched {claim!r}")
332+
failures.extend(_telemetry_claim_failures(surface, text))
333+
return failures
334+
335+
336+
def _telemetry_claim_failures(surface: str, text: str) -> List[str]:
337+
return [
338+
f"{surface}: telemetry quota reward matched {claim!r}"
339+
for claim in _telemetry_reward_claims(text)
340+
]
341+
342+
312343
def _claim_failures(root: Path, surfaces: Iterable[Path]) -> List[str]:
313344
failures: List[str] = []
314345
for path in surfaces:
315-
text = path.read_text(encoding="utf-8")
316-
for label, pattern in BLOCKED:
317-
for match in pattern.finditer(text):
346+
failures.extend(
347+
_text_claim_failures(
348+
path.relative_to(root).as_posix(),
349+
path.read_text(encoding="utf-8"),
350+
)
351+
)
352+
return failures
353+
354+
355+
def _safe_sdist_member_path(name: str) -> PurePosixPath:
356+
path = PurePosixPath(name)
357+
if path.is_absolute() or ".." in path.parts or "\\" in name:
358+
raise ValueError(f"unsafe source-distribution member path: {name!r}")
359+
if not path.parts:
360+
raise ValueError(f"source-distribution member has an empty path: {name!r}")
361+
return path
362+
363+
364+
def validate_sdist(sdist: Path) -> List[str]:
365+
"""Validate every customer-readable surface in the exact built sdist."""
366+
sdist = sdist.resolve()
367+
archive_suffix = ".tar.gz"
368+
if not sdist.name.endswith(archive_suffix):
369+
return ["source distribution filename must end in .tar.gz"]
370+
failures: List[str] = []
371+
text_members: Dict[str, str] = {}
372+
package_roots: Set[str] = set()
373+
seen_members: Set[str] = set()
374+
375+
try:
376+
archive = tarfile.open(sdist, mode="r:gz")
377+
except (OSError, tarfile.TarError) as error:
378+
return [f"source distribution could not be opened: {error}"]
379+
380+
with archive:
381+
for member in archive.getmembers():
382+
try:
383+
path = _safe_sdist_member_path(member.name)
384+
except ValueError as error:
385+
failures.append(str(error))
386+
continue
387+
388+
normalized = path.as_posix()
389+
if normalized in seen_members:
390+
failures.append(f"source distribution contains duplicate member: {normalized}")
391+
continue
392+
seen_members.add(normalized)
393+
package_roots.add(path.parts[0])
394+
395+
if member.issym() or member.islnk():
396+
failures.append(f"source distribution contains a link: {normalized}")
397+
continue
398+
if member.isdir():
399+
continue
400+
if len(path.parts) < 2:
318401
failures.append(
319-
f"{path.relative_to(root)}: {label} matched {match.group(0)!r}"
402+
f"source-distribution member is outside its package root: {normalized!r}"
320403
)
321-
for claim in _fixed_rate_claims(text):
322-
failures.append(
323-
f"{path.relative_to(root)}: fixed demo rate matched {claim!r}"
324-
)
325-
for claim in _telemetry_reward_claims(text):
326-
failures.append(
327-
f"{path.relative_to(root)}: telemetry quota reward matched {claim!r}"
328-
)
404+
continue
405+
if not member.isfile():
406+
failures.append(f"source distribution contains a special member: {normalized}")
407+
continue
408+
409+
relative = PurePosixPath(*path.parts[1:])
410+
if (
411+
relative.parts[0] in SDIST_DEVELOPMENT_ROOTS
412+
or "__pycache__" in relative.parts
413+
):
414+
continue
415+
if relative.suffix.lower() in BINARY_SUFFIXES:
416+
continue
417+
if member.size > MAX_SDIST_TEXT_BYTES:
418+
failures.append(f"source distribution text candidate is too large: {relative}")
419+
continue
420+
421+
extracted = archive.extractfile(member)
422+
if extracted is None:
423+
failures.append(f"source distribution member could not be read: {relative}")
424+
continue
425+
contents = extracted.read(MAX_SDIST_TEXT_BYTES + 1)
426+
if len(contents) != member.size:
427+
failures.append(f"source distribution member size changed while reading: {relative}")
428+
continue
429+
if b"\x00" in contents:
430+
continue
431+
try:
432+
text_members[relative.as_posix()] = contents.decode("utf-8")
433+
except UnicodeDecodeError:
434+
continue
435+
436+
expected_root = sdist.name[: -len(archive_suffix)]
437+
if package_roots != {expected_root}:
438+
failures.append(
439+
"source distribution package root differs from its filename: "
440+
f"expected {expected_root!r}, found {sorted(package_roots)!r}"
441+
)
442+
443+
for surface, text in sorted(text_members.items()):
444+
if surface == "CHANGELOG.md":
445+
# Historical release notes can truthfully describe retired plans.
446+
# A telemetry quota reward never existed and is forbidden in history too.
447+
failures.extend(_telemetry_claim_failures(surface, text))
448+
else:
449+
failures.extend(_text_claim_failures(surface, text))
450+
451+
metadata = text_members.get("PKG-INFO")
452+
version_source = text_members.get("oilpriceapi/version.py")
453+
if metadata is None:
454+
failures.append("source distribution must contain readable PKG-INFO")
455+
elif CONTRACT not in metadata:
456+
failures.append("source distribution PKG-INFO: reviewed product-facts contract is not linked")
457+
if version_source is None:
458+
failures.append("source distribution must contain readable oilpriceapi/version.py")
459+
if metadata is not None and version_source is not None:
460+
metadata_match = re.search(r"^Version: ([^\s]+)$", metadata, re.MULTILINE)
461+
module_match = re.search(r'^__version__ = "([^"]+)"', version_source, re.MULTILINE)
462+
package_prefix = "oilpriceapi-"
463+
expected_version = (
464+
expected_root[len(package_prefix) :]
465+
if expected_root.startswith(package_prefix)
466+
else ""
467+
)
468+
versions = {
469+
metadata_match.group(1) if metadata_match else None,
470+
module_match.group(1) if module_match else None,
471+
expected_version,
472+
}
473+
if None in versions or len(versions) != 1:
474+
failures.append("source distribution filename, metadata, and module versions differ")
329475
return failures
330476

331477

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

481+
changelog = root / "CHANGELOG.md"
482+
if changelog.is_file():
483+
failures.extend(
484+
_telemetry_claim_failures(
485+
"CHANGELOG.md", changelog.read_text(encoding="utf-8")
486+
)
487+
)
488+
335489
readme = (root / "README.md").read_text()
336490
if CONTRACT not in readme:
337491
failures.append("README.md: reviewed product-facts contract is not linked")
@@ -374,14 +528,23 @@ def validate_package(package_root: Path) -> List[str]:
374528

375529
def main() -> None:
376530
parser = argparse.ArgumentParser()
377-
parser.add_argument("--package-root", type=Path)
531+
inputs = parser.add_mutually_exclusive_group()
532+
inputs.add_argument("--package-root", type=Path)
533+
inputs.add_argument("--sdist", type=Path)
378534
args = parser.parse_args()
379535

380-
failures = validate_package(args.package_root) if args.package_root else validate()
536+
if args.package_root:
537+
failures = validate_package(args.package_root)
538+
elif args.sdist:
539+
failures = validate_sdist(args.sdist)
540+
else:
541+
failures = validate()
381542
if failures:
382543
raise SystemExit("\n".join(failures))
383544
if args.package_root:
384545
print("validated exact installed Python artifact claims")
546+
elif args.sdist:
547+
print("validated exact Python sdist claims")
385548
else:
386549
print(f"validated {len(discover_public_surfaces())} public surfaces")
387550

tests/test_release_readiness.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def test_packaged_futures_examples_prefer_instrument_generic_slugs() -> None:
106106

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

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

119130

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

@@ -159,7 +170,7 @@ def test_package_version_helper_reads_the_project_version() -> None:
159170
capture_output=True,
160171
text=True,
161172
)
162-
assert result.stdout.strip() == "1.12.7"
173+
assert result.stdout.strip() == "1.12.8"
163174

164175

165176
def test_every_workflow_pins_actions_and_hardens_each_checkout_step() -> None:

0 commit comments

Comments
 (0)