|
4 | 4 | import argparse |
5 | 5 | import csv |
6 | 6 | 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 |
9 | 10 |
|
10 | 11 | ROOT = Path(__file__).resolve().parents[1] |
11 | 12 | CONTRACT = "https://api.oilpriceapi.com/product-facts.json" |
|
19 | 20 | ".pyc", |
20 | 21 | ".pyo", |
21 | 22 | ".so", |
| 23 | + ".wasm", |
22 | 24 | } |
| 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 |
23 | 36 | _RATE_COUNT = r"\d[\d,]*" |
24 | 37 | _RATE_ACTION = r"(?:(?:api[- ]+)?(?:requests?|calls?|queries?|hits?|credits?)|reqs?\.?)" |
25 | 38 | _RATE_UNIT_SINGULAR = r"(?:second|sec|minute|min|hour|hr|day|week|month|year)" |
|
141 | 154 |
|
142 | 155 |
|
143 | 156 | 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()] |
145 | 158 | for directory in (root / "docs", root / "oilpriceapi"): |
146 | 159 | surfaces.extend(path for path in directory.rglob("*") if _is_public_text(path)) |
147 | 160 | return sorted(set(surfaces)) |
@@ -309,29 +322,170 @@ def _telemetry_reward_claims(text: str) -> List[str]: |
309 | 322 | return claims |
310 | 323 |
|
311 | 324 |
|
| 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 | + |
312 | 343 | def _claim_failures(root: Path, surfaces: Iterable[Path]) -> List[str]: |
313 | 344 | failures: List[str] = [] |
314 | 345 | 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: |
318 | 401 | 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}" |
320 | 403 | ) |
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") |
329 | 475 | return failures |
330 | 476 |
|
331 | 477 |
|
332 | 478 | def validate(root: Path = ROOT) -> List[str]: |
333 | 479 | failures = _claim_failures(root, discover_public_surfaces(root)) |
334 | 480 |
|
| 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 | + |
335 | 489 | readme = (root / "README.md").read_text() |
336 | 490 | if CONTRACT not in readme: |
337 | 491 | failures.append("README.md: reviewed product-facts contract is not linked") |
@@ -374,14 +528,23 @@ def validate_package(package_root: Path) -> List[str]: |
374 | 528 |
|
375 | 529 | def main() -> None: |
376 | 530 | 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) |
378 | 534 | args = parser.parse_args() |
379 | 535 |
|
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() |
381 | 542 | if failures: |
382 | 543 | raise SystemExit("\n".join(failures)) |
383 | 544 | if args.package_root: |
384 | 545 | print("validated exact installed Python artifact claims") |
| 546 | + elif args.sdist: |
| 547 | + print("validated exact Python sdist claims") |
385 | 548 | else: |
386 | 549 | print(f"validated {len(discover_public_surfaces())} public surfaces") |
387 | 550 |
|
|
0 commit comments