diff --git a/AGENTS.md b/AGENTS.md index a8aa43b94..7b9f1c332 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,7 @@ uv run ruff check . - **Regression tests must name the PR/commit they guard** in the docstring (e.g. `Guards the fix from PR #198 against the regression introduced by PR #193`). - **Human review before `main`.** PRs only. No force-pushes to `main`. Self-approval doesn't count. - **Trunk-based:** branch off `main`, PR back to `main`. No long-lived release branches. +- **Dependency cooldown:** `uv lock` never resolves to a release younger than 7 days. Don't hand-edit `[tool.uv] exclude-newer` — run `python tools/lock.py` to set it to `now - 7d` and re-lock; `tests/test_dep_cooldown.py` fails if the committed cutoff is younger than the window. A genuinely urgent fix gets a `[tool.uv.exclude-newer-package]` override plus a matching `[tool.benchflow.dependency-cooldown.override-rationale]` entry. - **Releases:** current mechanics live in [`docs/release.md`](./docs/release.md). Merges to `main` publish internal preview `.devN` builds after CI passes; public releases require a reviewed stable-version PR, a matching `v` tag on `main`, then a bump back to the next `.dev0`. ## Experiment guidance (when using benchflow to run batch tasks experiments) diff --git a/pyproject.toml b/pyproject.toml index 21bcc2145..f62307107 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,6 +108,10 @@ build-backend = "hatchling.build" only-include = [ "src", "tests", + # `tests/test_dep_cooldown.py` does `from tools.lock import ...`, so the + # sdist must ship `tools` too or the test suite fails to import from a + # source distribution. + "tools", "README.md", "CHANGELOG.md", "LICENSE", @@ -185,3 +189,29 @@ exclude = [ "src/benchflow/rewards/rubric_config.py", "src/benchflow/experimental/mcp/reviewer_server.py", ] + +[tool.uv] +# The per-package cooldown override (`exclude-newer-package` below) is only +# honored by uv >= 0.8.4. Pin a floor so an older uv can't silently ignore the +# override table and resolve a different (vulnerable / un-vetted) lock. +required-version = ">=0.8.4" +# Dependency cooldown (hard rule): never resolve to a release younger than ~7 +# days. `exclude-newer` caps every `uv lock` at this timestamp. Don't hand-edit +# it — run `python tools/lock.py` to set it to `now - 7d` and re-lock in one +# step (the value stays static so CI's `uv sync --locked` is deterministic; the +# helper just rolls it forward). The CI check `tests/test_dep_cooldown.py` fails +# if this date is ever within the last 7 days. +# +# Security exception: a freshly-published CVE fix (younger than the cap) is +# allowed via a per-package override in `exclude-newer-package` below, with a +# matching machine-checkable rationale in +# `[tool.benchflow.dependency-cooldown.override-rationale]`. The CI gate fails +# missing/stale rationales and stale overrides once their package cutoff ages +# past the cooldown window, so they cannot become a permanent package allowlist. +exclude-newer = "2026-07-06T00:00:00Z" + +[tool.uv.exclude-newer-package] +trl = "2026-07-10T00:00:00Z" + +[tool.benchflow.dependency-cooldown.override-rationale] +trl = "Temporary mainline compatibility override: the TRL-SFT export extra requires trl>=1.8.0, and 1.8.0 was published after the current global cooldown cutoff. Remove once the release ages past the cooldown window." diff --git a/src/benchflow/task/prompts.py b/src/benchflow/task/prompts.py index c4463d38c..129ba25a0 100644 --- a/src/benchflow/task/prompts.py +++ b/src/benchflow/task/prompts.py @@ -650,7 +650,7 @@ def _str_or_none(value: object) -> str | None: def _document_user_model_kind( model: str | None, ) -> Literal["scripted-linear", "model-linear"] | None: - if model in {None, "scripted", "deterministic"}: + if model is None or model in {"scripted", "deterministic"}: return "scripted-linear" if model.startswith( ( diff --git a/tests/test_dep_cooldown.py b/tests/test_dep_cooldown.py new file mode 100644 index 000000000..4fea27673 --- /dev/null +++ b/tests/test_dep_cooldown.py @@ -0,0 +1,106 @@ +"""Hard rule: the dependency cooldown — never resolve to a release younger than +a 7-day window. + +Two layers, both evaluated against *today*: + +1. `test_uv_exclude_newer_caps_resolution` — `[tool.uv] exclude-newer` is the + static cutoff `uv lock` applies at resolve time; it must be >= 7 days old so + the default resolution can't reach a brand-new release. Set it with + `python tools/lock.py` (computes `now - 7d` and re-locks). +2. `test_locked_packages_respect_cooldown` — the dynamic half: it reads the + `upload-time` uv bakes into `uv.lock` and fails if any *resolved* package is + younger than `now - 7d`. This tracks the current date with no stored date to + trust and no network calls. + +A genuinely urgent (e.g. security) fix younger than the window is allowed only +via `[tool.uv.exclude-newer-package]` plus a matching structured rationale in +`[tool.benchflow.dependency-cooldown.override-rationale]`, which exempts that +package from layer 2. +""" + +from __future__ import annotations + +import datetime +import tomllib +from pathlib import Path + +from tools.lock import ( + COOLDOWN_DAYS, + find_cooldown_override_rationale_issues, + find_cooldown_violations, + find_expired_cooldown_overrides, +) + +_PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml" +_LOCK = Path(__file__).resolve().parents[1] / "uv.lock" + + +def test_uv_exclude_newer_caps_resolution() -> None: + cfg = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8")) + raw = cfg.get("tool", {}).get("uv", {}).get("exclude-newer") + assert raw, ( + "[tool.uv] exclude-newer is missing from pyproject.toml — it enforces the " + f"{COOLDOWN_DAYS}-day dependency cooldown and must be set." + ) + cutoff = datetime.datetime.fromisoformat(str(raw).replace("Z", "+00:00")) + if cutoff.tzinfo is None: + cutoff = cutoff.replace(tzinfo=datetime.UTC) + age = datetime.datetime.now(datetime.UTC) - cutoff + age_days = age.total_seconds() / 86400 + assert age >= datetime.timedelta(days=COOLDOWN_DAYS), ( + f"[tool.uv] exclude-newer ({raw}) is only {age_days:.1f}d old; the dependency " + f"cooldown requires it to be >= {COOLDOWN_DAYS} days in the past. Re-lock " + "with `python tools/lock.py` when taking dependency updates." + ) + + +def test_locked_packages_respect_cooldown() -> None: + cfg = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8")) + exempt = set(cfg.get("tool", {}).get("uv", {}).get("exclude-newer-package", {})) + violations = find_cooldown_violations( + _LOCK.read_text(encoding="utf-8"), + exempt=exempt, + now=datetime.datetime.now(datetime.UTC), + ) + assert not violations, ( + f"uv.lock resolves packages younger than the {COOLDOWN_DAYS}-day cooldown " + "without a documented [tool.uv.exclude-newer-package] override:\n" + + "\n".join(f" {n} {v} (uploaded {ts:%Y-%m-%d})" for n, v, ts in violations) + + "\nRe-lock with `python tools/lock.py`, or add an override with rationale." + ) + + +def test_exclude_newer_package_overrides_have_rationales() -> None: + cfg = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8")) + tool = cfg.get("tool", {}) + uv = tool.get("uv", {}) + benchflow = tool.get("benchflow", {}) + cooldown = benchflow.get("dependency-cooldown", {}) + overrides = uv.get("exclude-newer-package", {}) + rationales = cooldown.get("override-rationale", {}) + issues = find_cooldown_override_rationale_issues(overrides, rationales) + assert not issues, ( + "[tool.uv.exclude-newer-package] overrides must carry a non-empty " + "machine-checkable rationale in " + "[tool.benchflow.dependency-cooldown.override-rationale]:\n" + + "\n".join(f" {issue}" for issue in issues) + ) + + +def test_exclude_newer_package_overrides_expire_with_cooldown() -> None: + cfg = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8")) + overrides = cfg.get("tool", {}).get("uv", {}).get("exclude-newer-package", {}) + now = datetime.datetime.now(datetime.UTC) + expired = find_expired_cooldown_overrides(overrides, now=now) + floor = now - datetime.timedelta(days=COOLDOWN_DAYS) + assert not expired, ( + "[tool.uv.exclude-newer-package] overrides are temporary security/urgent " + f"escape hatches and must be removed once their cutoff is older than the " + f"{COOLDOWN_DAYS}-day cooldown floor ({floor:%Y-%m-%dT%H:%M:%SZ}):\n" + + "\n".join( + f" {name} = {raw!r} (override cutoff {cutoff:%Y-%m-%dT%H:%M:%SZ})" + for name, raw, cutoff in expired + ) + + "\nRemove the stale override and re-lock with `python tools/lock.py`, " + "or keep an override only for a still-fresh security/urgent pin." + ) diff --git a/tests/test_lock_tool.py b/tests/test_lock_tool.py new file mode 100644 index 000000000..7c79ef20e --- /dev/null +++ b/tests/test_lock_tool.py @@ -0,0 +1,222 @@ +"""Unit tests for the dependency-cooldown re-lock helper (`tools/lock.py`). + +These cover the pure date/text transforms and the lock-freshness audit against +synthetic lock text; `uv lock` itself is not invoked. +""" + +from __future__ import annotations + +import datetime +import tomllib + +import pytest + +from tools import lock as lock_tool +from tools.lock import ( + COOLDOWN_DAYS, + LockError, + compute_cooldown_cutoff, + find_cooldown_override_rationale_issues, + find_cooldown_violations, + find_expired_cooldown_overrides, + main, + newest_upload_times, + rewrite_exclude_newer, +) + +_NOW = datetime.datetime(2026, 6, 15, 12, 0, tzinfo=datetime.UTC) + +# floor (now - 7d) = 2026-06-08T12:00Z: old-pkg is older, fresh-pkg is younger. +_SYNTH_LOCK = """ +[options] +exclude-newer = "2026-06-08T00:00:00Z" + +[[package]] +name = "old-pkg" +version = "1.0.0" +sdist = { url = "https://example/old.tar.gz", upload-time = "2026-05-01T00:00:00Z" } + +[[package]] +name = "fresh-pkg" +version = "2.0.0" +sdist = { url = "https://example/fresh.tar.gz", upload-time = "2026-06-13T00:00:00Z" } +wheels = [ + { url = "https://example/fresh.whl", upload-time = "2026-06-14T08:00:00Z" }, +] + +[[package]] +name = "git-pkg" +version = "0.1.0" +source = { git = "https://example/repo.git" } +""" + + +def test_cutoff_trails_today_by_cooldown_window_at_midnight() -> None: + now = datetime.datetime(2026, 6, 15, 9, 30, tzinfo=datetime.UTC) + assert compute_cooldown_cutoff(now) == "2026-06-08T00:00:00Z" + + +def test_cutoff_is_idempotent_within_a_day() -> None: + morning = datetime.datetime(2026, 6, 15, 0, 1, tzinfo=datetime.UTC) + night = datetime.datetime(2026, 6, 15, 23, 59, tzinfo=datetime.UTC) + assert compute_cooldown_cutoff(morning) == compute_cooldown_cutoff(night) + + +def test_cutoff_normalizes_non_utc_input() -> None: + # 2026-06-15 02:00 in UTC+9 is still 2026-06-14 17:00 UTC -> minus 7d. + tz = datetime.timezone(datetime.timedelta(hours=9)) + now = datetime.datetime(2026, 6, 15, 2, 0, tzinfo=tz) + assert compute_cooldown_cutoff(now) == "2026-06-07T00:00:00Z" + + +def test_cutoff_default_window_is_the_cooldown_constant() -> None: + now = datetime.datetime(2026, 6, 15, 12, 0, tzinfo=datetime.UTC) + expected = (now - datetime.timedelta(days=COOLDOWN_DAYS)).date() + assert compute_cooldown_cutoff(now).startswith(expected.isoformat()) + + +def test_cutoff_rejects_negative_window() -> None: + now = datetime.datetime(2026, 6, 15, tzinfo=datetime.UTC) + with pytest.raises(LockError): + compute_cooldown_cutoff(now, cooldown_days=-1) + + +def test_rewrite_replaces_only_the_exclude_newer_value() -> None: + text = ( + "[tool.uv]\n" + 'exclude-newer = "2026-06-01T00:00:00Z"\n' + "\n" + "[tool.uv.exclude-newer-package]\n" + 'litellm = "2026-06-14T00:00:00Z"\n' + ) + out = rewrite_exclude_newer(text, "2026-06-08T00:00:00Z") + assert 'exclude-newer = "2026-06-08T00:00:00Z"' in out + # The per-package override table is untouched. + assert 'litellm = "2026-06-14T00:00:00Z"' in out + + +def test_rewrite_requires_exactly_one_assignment() -> None: + with pytest.raises(LockError): + rewrite_exclude_newer("[tool.uv]\n", "2026-06-08T00:00:00Z") + + +def test_newest_upload_time_picks_latest_artifact_and_skips_sourceless() -> None: + newest = newest_upload_times(tomllib.loads(_SYNTH_LOCK)) + # The wheel (08:00) beats the sdist (00:00) for the same package. + assert newest["fresh-pkg"][1] == datetime.datetime( + 2026, 6, 14, 8, 0, tzinfo=datetime.UTC + ) + # A git-sourced package has no upload-time and is omitted entirely. + assert "git-pkg" not in newest + + +def test_find_cooldown_violations_flags_only_young_unexempted_packages() -> None: + violations = find_cooldown_violations(_SYNTH_LOCK, exempt=set(), now=_NOW) + assert [name for name, _, _ in violations] == ["fresh-pkg"] + + +def test_find_cooldown_violations_honors_exemptions() -> None: + violations = find_cooldown_violations(_SYNTH_LOCK, exempt={"fresh-pkg"}, now=_NOW) + assert violations == [] + + +def test_find_cooldown_violations_normalizes_exemption_names() -> None: + """Guards the fix from PR #788 so overrides are not spelling-sensitive.""" + violations = find_cooldown_violations(_SYNTH_LOCK, exempt={"Fresh_Pkg"}, now=_NOW) + assert violations == [] + + +def test_find_cooldown_violations_only_grows_more_lenient_over_time() -> None: + # The same lock, evaluated a month later: fresh-pkg has aged past the window. + later = _NOW + datetime.timedelta(days=30) + assert find_cooldown_violations(_SYNTH_LOCK, exempt=set(), now=later) == [] + + +def test_expired_cooldown_overrides_flags_aged_out_entries() -> None: + """Guards the PR #788 override escape hatch from becoming a package allowlist.""" + overrides = { + "old-pkg": "2026-06-08T00:00:00Z", + "fresh-pkg": "2026-06-14T00:00:00Z", + } + expired = find_expired_cooldown_overrides(overrides, now=_NOW) + assert [(name, raw) for name, raw, _ in expired] == [ + ("old-pkg", "2026-06-08T00:00:00Z") + ] + + +def test_expired_cooldown_overrides_rejects_invalid_timestamp() -> None: + """Guards the PR #788 override gate against unclear TOML diagnostics.""" + with pytest.raises( + LockError, + match=r"\[tool\.uv\.exclude-newer-package\] 'bad-pkg' must be an ISO timestamp", + ): + find_expired_cooldown_overrides({"bad-pkg": "not-a-date"}, now=_NOW) + + +def test_override_rationale_gate_accepts_normalized_matching_entry() -> None: + """Guards PR #788: cooldown overrides require machine-checkable rationale.""" + issues = find_cooldown_override_rationale_issues( + {"Fresh_Pkg": "2026-06-14T00:00:00Z"}, + {"fresh-pkg": "GHSA fix published inside the cooldown window."}, + ) + assert issues == [] + + +def test_override_rationale_gate_rejects_missing_or_empty_rationale() -> None: + """Guards PR #788 against bare package allowlist overrides.""" + issues = find_cooldown_override_rationale_issues( + { + "missing-rationale": "2026-06-14T00:00:00Z", + "empty-rationale": "2026-06-14T00:00:00Z", + }, + {"empty-rationale": " "}, + ) + assert issues == [ + "'empty-rationale' override rationale must be a non-empty string.", + "'missing-rationale' has [tool.uv.exclude-newer-package] but no " + "[tool.benchflow.dependency-cooldown.override-rationale] entry.", + ] + + +def test_override_rationale_gate_rejects_stale_rationale_entries() -> None: + """Guards PR #788 so rationale entries expire with their overrides.""" + issues = find_cooldown_override_rationale_issues( + {}, + {"old-pkg": "no longer has an override"}, + ) + assert issues == [ + "'old-pkg' has an override rationale but no matching " + "[tool.uv.exclude-newer-package] entry." + ] + + +def test_override_rationale_gate_rejects_non_table() -> None: + """Guards PR #788 against unclear pyproject policy shapes.""" + issues = find_cooldown_override_rationale_issues( + {"fresh-pkg": "2026-06-14T00:00:00Z"}, + "fresh-pkg: security fix", + ) + assert issues == [ + "[tool.benchflow.dependency-cooldown.override-rationale] must be a TOML table." + ] + + +def test_main_wraps_pyproject_read_oserror(tmp_path, capsys) -> None: + """Guards the PR #788 lock helper from leaking raw OSError tracebacks.""" + with pytest.raises(SystemExit) as exc_info: + main(["--pyproject", str(tmp_path), "--no-lock"]) + + assert exc_info.value.code == 1 + assert "Could not read" in capsys.readouterr().err + + +def test_run_uv_lock_wraps_oserror(monkeypatch, tmp_path) -> None: + """Guards the PR #788 lock helper from leaking subprocess OSError.""" + + def raise_oserror(*args, **kwargs): + raise OSError("spawn failed") + + monkeypatch.setattr(lock_tool.subprocess, "run", raise_oserror) + + with pytest.raises(LockError, match="Could not run `uv lock`"): + lock_tool._run_uv_lock(tmp_path / "pyproject.toml") diff --git a/tests/test_trace_import_cli.py b/tests/test_trace_import_cli.py index b80b0a719..efcb3956f 100644 --- a/tests/test_trace_import_cli.py +++ b/tests/test_trace_import_cli.py @@ -37,7 +37,7 @@ def test_tasks_generate_help_uses_long_options_only() -> None: def test_tasks_generate_rejects_removed_short_options( alias: str, value: str, tmp_path: Path ) -> None: - """Guards ENG-96: removed task-generation short options stay rejected.""" + """Guards ENG-96 and the Click 8.4.2 lock refresh from PR #788.""" result = CliRunner().invoke( app, [ @@ -52,7 +52,9 @@ def test_tasks_generate_rejects_removed_short_options( ) assert result.exit_code != 0 - assert f"No such option: {alias}" in click.unstyle(result.output) + output = click.unstyle(result.output) + assert "No such option" in output + assert alias in output def test_tasks_generate_dry_run_uses_generation_filters( diff --git a/tools/lock.py b/tools/lock.py new file mode 100644 index 000000000..b21c8f37e --- /dev/null +++ b/tools/lock.py @@ -0,0 +1,331 @@ +"""Refresh the uv dependency-cooldown cutoff to `now - COOLDOWN_DAYS`, then lock. + +The repo enforces a hard dependency cooldown: `uv lock` must never resolve to a +release younger than `COOLDOWN_DAYS` (guarded by `tests/test_dep_cooldown.py`). +The cutoff lives in `[tool.uv] exclude-newer` in pyproject.toml as a *static* +ISO timestamp — and it has to stay static, because CI runs `uv sync --locked` / +`uv export --locked` and a committed lock can only be deterministic against a +fixed cutoff. A literally-live "now - 7d" value in pyproject would change on +every CI run and break `--locked`. + +This helper is how that static value gets set without hand-typing a date: it +computes midnight UTC of `(today - COOLDOWN_DAYS)` and writes it into +`exclude-newer`, then re-locks. Run it whenever you intentionally take +dependency updates — the date "rolls" forward to the newest still-vetted day, +the committed pyproject date + `uv.lock` remain a fixed reviewable artifact, and +the cooldown invariant holds by construction. + + python tools/lock.py # set cutoff to now-7d, then `uv lock` + python tools/lock.py --no-lock # only rewrite the cutoff date + python tools/lock.py --check # print the cutoff it would write; change nothing + +A genuinely urgent fix younger than the cutoff stays a per-package exception in +`[tool.uv.exclude-newer-package]`; each override must have a matching rationale +in `[tool.benchflow.dependency-cooldown.override-rationale]`. +""" + +from __future__ import annotations + +import argparse +import datetime +import re +import subprocess +import sys +import tomllib +from collections.abc import Mapping, Sequence +from pathlib import Path + +# Single source of truth for the cooldown window. `tests/test_dep_cooldown.py` +# imports this so the CI gate and this writer can never drift apart. +COOLDOWN_DAYS = 7 + +# Matches the `[tool.uv] exclude-newer = ""` assignment only. The sibling +# `[tool.uv.exclude-newer-package]` is a table *header*, never a `key =` line, +# so it can't match here. +_EXCLUDE_NEWER_RE = re.compile( + r'^(?Pexclude-newer\s*=\s*)"[^"]*"', + re.MULTILINE, +) +_PACKAGE_NAME_RE = re.compile(r"[-_.]+") + + +class LockError(RuntimeError): + """Raised when the cooldown cutoff cannot be refreshed.""" + + +def compute_cooldown_cutoff( + now: datetime.datetime, cooldown_days: int = COOLDOWN_DAYS +) -> str: + """Return midnight-UTC of `(now - cooldown_days)` as a uv `exclude-newer` value. + + Truncating to midnight makes a same-day re-run idempotent (no lock churn), + and pins the cutoff to a whole day for easy review. + """ + if cooldown_days < 0: + raise LockError(f"cooldown_days must be non-negative, got {cooldown_days}.") + cutoff_date = ( + now.astimezone(datetime.UTC) - datetime.timedelta(days=cooldown_days) + ).date() + return f"{cutoff_date.isoformat()}T00:00:00Z" + + +def rewrite_exclude_newer(pyproject_text: str, cutoff: str) -> str: + """Return `pyproject_text` with `[tool.uv] exclude-newer` set to `cutoff`. + + Raises if the key is missing or appears more than once — we only ever want + to touch a single, known assignment. + """ + matches = _EXCLUDE_NEWER_RE.findall(pyproject_text) + if len(matches) != 1: + raise LockError( + 'Expected exactly one `[tool.uv] exclude-newer = "..."` line in ' + f"pyproject.toml, found {len(matches)}." + ) + return _EXCLUDE_NEWER_RE.sub(rf'\g"{cutoff}"', pyproject_text) + + +def normalize_package_name(name: str) -> str: + """Return the package-name shape uv/PyPI use for comparisons.""" + return _PACKAGE_NAME_RE.sub("-", name).lower() + + +def _parse_lock_timestamp(raw: str) -> datetime.datetime: + ts = datetime.datetime.fromisoformat(str(raw).replace("Z", "+00:00")) + if ts.tzinfo is None: + ts = ts.replace(tzinfo=datetime.UTC) + return ts + + +def _parse_override_timestamp(package: str, raw: object) -> datetime.datetime: + if not isinstance(raw, str): + raise LockError( + "[tool.uv.exclude-newer-package] " + f"{package!r} must be an ISO timestamp string, got {raw!r}." + ) + try: + return _parse_lock_timestamp(raw) + except ValueError as exc: + raise LockError( + "[tool.uv.exclude-newer-package] " + f"{package!r} must be an ISO timestamp, got {raw!r}." + ) from exc + + +def newest_upload_times( + lock: dict, +) -> dict[str, tuple[str, datetime.datetime]]: + """Map each locked package to `(version, newest sdist/wheel upload time)`. + + Packages with no registry `upload-time` (git/local/editable sources) are + skipped — they have no PyPI release subject to the cooldown. + """ + newest: dict[str, tuple[str, datetime.datetime]] = {} + for pkg in lock.get("package", []): + name = pkg.get("name") + if not name: + continue + version = str(pkg.get("version", "")) + raw_times: list[str] = [] + sdist = pkg.get("sdist") + if isinstance(sdist, dict) and sdist.get("upload-time"): + raw_times.append(sdist["upload-time"]) + for wheel in pkg.get("wheels") or []: + if isinstance(wheel, dict) and wheel.get("upload-time"): + raw_times.append(wheel["upload-time"]) + for raw in raw_times: + ts = _parse_lock_timestamp(raw) + if name not in newest or ts > newest[name][1]: + newest[name] = (version, ts) + return newest + + +def find_cooldown_violations( + lock_text: str, + exempt: set[str], + now: datetime.datetime, + cooldown_days: int = COOLDOWN_DAYS, +) -> list[tuple[str, str, datetime.datetime]]: + """Return locked packages published less than `cooldown_days` before `now`. + + This is the dynamic half of the cooldown: it reads the `upload-time` uv bakes + into `uv.lock` and compares each resolved package against `now - cooldown_days` + — so the window tracks the current date with no stored date to trust and no + network calls. `exempt` is the set of package names that carry a documented + `[tool.uv.exclude-newer-package]` override (allowed to be younger). + + Returns `(name, version, upload_time)` tuples, newest first. Never flags an + unchanged lock over time: upload times are immutable, so packages only age + past the window. + """ + floor = now.astimezone(datetime.UTC) - datetime.timedelta(days=cooldown_days) + lock = tomllib.loads(lock_text) + normalized_exempt = {normalize_package_name(name) for name in exempt} + violations = [ + (name, version, ts) + for name, (version, ts) in newest_upload_times(lock).items() + if ts > floor and normalize_package_name(name) not in normalized_exempt + ] + violations.sort(key=lambda item: item[2], reverse=True) + return violations + + +def find_expired_cooldown_overrides( + overrides: Mapping[str, object], + now: datetime.datetime, + cooldown_days: int = COOLDOWN_DAYS, +) -> list[tuple[str, str, datetime.datetime]]: + """Return package overrides whose temporary cutoff is no longer needed. + + `[tool.uv.exclude-newer-package]` is the explicit escape hatch for a package + that must resolve newer than the global cooldown cap. Once the override's + timestamp is older than the active cooldown floor, the global cap can cover + the package and the override must be removed instead of becoming a permanent + package allowlist. + """ + floor = now.astimezone(datetime.UTC) - datetime.timedelta(days=cooldown_days) + expired: list[tuple[str, str, datetime.datetime]] = [] + for package, raw_cutoff in overrides.items(): + cutoff = _parse_override_timestamp(package, raw_cutoff) + if cutoff <= floor: + expired.append((package, str(raw_cutoff), cutoff)) + expired.sort(key=lambda item: normalize_package_name(item[0])) + return expired + + +def find_cooldown_override_rationale_issues( + overrides: Mapping[str, object], + rationales: object, +) -> list[str]: + """Return policy issues for per-package overrides without rationale. + + TOML parsers intentionally discard comments, so the dependency-cooldown + escape hatch records rationale in a structured table instead of relying on + adjacent comments that CI cannot see. + """ + if rationales is None: + rationales = {} + if not isinstance(rationales, Mapping): + return [ + "[tool.benchflow.dependency-cooldown.override-rationale] " + "must be a TOML table." + ] + + normalized_overrides = {normalize_package_name(name): name for name in overrides} + normalized_rationales = { + normalize_package_name(str(name)): (str(name), value) + for name, value in rationales.items() + } + issues: list[str] = [] + + for normalized, package in sorted(normalized_overrides.items()): + rationale = normalized_rationales.get(normalized) + if rationale is None: + issues.append( + f"{package!r} has [tool.uv.exclude-newer-package] but no " + "[tool.benchflow.dependency-cooldown.override-rationale] entry." + ) + continue + rationale_name, value = rationale + if not isinstance(value, str) or not value.strip(): + issues.append( + f"{rationale_name!r} override rationale must be a non-empty string." + ) + + for normalized, (rationale_name, _) in sorted(normalized_rationales.items()): + if normalized not in normalized_overrides: + issues.append( + f"{rationale_name!r} has an override rationale but no matching " + "[tool.uv.exclude-newer-package] entry." + ) + + return issues + + +def _read_pyproject(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except OSError as exc: + raise LockError(f"Could not read {path}: {exc}") from exc + + +def _write_pyproject(path: Path, text: str) -> None: + try: + path.write_text(text, encoding="utf-8") + except OSError as exc: + raise LockError(f"Could not write {path}: {exc}") from exc + + +def _run_uv_lock(pyproject_path: Path) -> int: + cwd = pyproject_path.resolve().parent + try: + # Fixed argv, no shell — uv is resolved from PATH. + completed = subprocess.run( + ["uv", "lock"], + cwd=cwd, + check=False, + ) + except FileNotFoundError as exc: + raise LockError( + "`uv` was not found on PATH; install uv or pass --no-lock to only " + "rewrite the cutoff date." + ) from exc + except OSError as exc: + raise LockError(f"Could not run `uv lock` in {cwd}: {exc}") from exc + return completed.returncode + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--pyproject", + type=Path, + default=Path("pyproject.toml"), + help="Path to pyproject.toml (default: ./pyproject.toml).", + ) + parser.add_argument( + "--cooldown-days", + type=int, + default=COOLDOWN_DAYS, + help=f"Days the cutoff trails today (default: {COOLDOWN_DAYS}).", + ) + parser.add_argument( + "--no-lock", + action="store_true", + help="Rewrite the cutoff date only; skip `uv lock`.", + ) + parser.add_argument( + "--check", + action="store_true", + help="Print the cutoff that would be written and exit; change nothing.", + ) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + try: + cutoff = compute_cooldown_cutoff( + datetime.datetime.now(datetime.UTC), args.cooldown_days + ) + if args.check: + print(cutoff) + return 0 + + original = _read_pyproject(args.pyproject) + updated = rewrite_exclude_newer(original, cutoff) + if updated != original: + _write_pyproject(args.pyproject, updated) + print(f"Set [tool.uv] exclude-newer = {cutoff!r} in {args.pyproject}.") + else: + print(f"[tool.uv] exclude-newer already {cutoff!r}; no change.") + + if args.no_lock: + return 0 + return _run_uv_lock(args.pyproject) + except LockError as exc: + parser.exit(1, f"{exc}\n") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/uv.lock b/uv.lock index 4f6cfcfbf..985953ee8 100644 --- a/uv.lock +++ b/uv.lock @@ -10,6 +10,12 @@ resolution-markers = [ "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] +[options] +exclude-newer = "2026-07-06T00:00:00Z" + +[options.exclude-newer-package] +trl = "2026-07-10T00:00:00Z" + [[package]] name = "accelerate" version = "1.14.0" @@ -621,14 +627,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -733,51 +739,42 @@ wheels = [ [[package]] name = "cuda-toolkit" -version = "13.0.3.0" +version = "13.0.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, ] [package.optional-dependencies] -cublas = [ - { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, -] cudart = [ - { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-runtime" }, ] cufft = [ - { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufft" }, ] cufile = [ - { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufile" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-cupti" }, ] curand = [ - { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-curand" }, ] cusolver = [ - { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusolver" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvtx" }, ] [[package]] @@ -2383,11 +2380,11 @@ wheels = [ [[package]] name = "nvidia-nvjitlink" -version = "13.3.33" +version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" }, - { url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, ] [[package]] @@ -3690,11 +3687,11 @@ wheels = [ [[package]] name = "setuptools" -version = "83.0.0" +version = "81.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] [[package]] @@ -3895,41 +3892,42 @@ wheels = [ [[package]] name = "torch" -version = "2.13.0" +version = "2.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-bindings", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, - { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, { name = "filelock" }, { name = "fsspec" }, { name = "jinja2" }, { name = "networkx" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, { name = "setuptools" }, { name = "sympy" }, - { name = "triton", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "triton", marker = "sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, - { url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" }, - { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" }, - { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" }, - { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" }, - { url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" }, - { url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" }, - { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" }, - { url = "https://files.pythonhosted.org/packages/e9/09/2c10e8cd0e00fa5d23c052df6ce467eaa7182399f5e0f824f1e4ff42ccae/torch-2.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c", size = 427226309, upload-time = "2026-07-08T16:02:53.127Z" }, - { url = "https://files.pythonhosted.org/packages/76/c6/22c2102bbef14ca6a6cb4c20e42f088e49c5f812be4e160ae57502e325f9/torch-2.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2", size = 526614507, upload-time = "2026-07-08T16:02:16.441Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/69/d1/491e3a0389430946145888b0203f2b6a759ce2a61481b96a85c2da4f2ced/torch-2.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc", size = 427219769, upload-time = "2026-07-08T16:02:31.18Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1d/38006e045bf0a1fc28ef01e757c554e59e59a8770c284bc4f47b14e60441/torch-2.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92", size = 526571320, upload-time = "2026-07-08T16:01:59.348Z" }, - { url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" }, + { url = "https://files.pythonhosted.org/packages/f0/54/efb7ebca77970012b0cc21687a55d70eb2ba514b2c2b8e18d9fb1222f3be/torch-2.12.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d2dd0f2c5f7ccbddaf34cade0deaf476808368f902b9cdb7f36a2ab42301bc0e", size = 87991951, upload-time = "2026-06-17T21:07:49.309Z" }, + { url = "https://files.pythonhosted.org/packages/1e/00/4210d76ca7424981f04033ebe7e48816ab83287a62538747a58825db770c/torch-2.12.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2de4e19b88a481482c6c75291f2d6a52eda3ce51f311b29aa9b68499c830c07c", size = 426382721, upload-time = "2026-06-17T21:06:41.842Z" }, + { url = "https://files.pythonhosted.org/packages/76/1f/bc9f5a5aa569307076365f25afcebacb22e9c754b1bcfbaaa146627c7fda/torch-2.12.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:649e4ced014ba646f76f8cb9c9726735a6323eb321b7919f942790a923f90921", size = 532261322, upload-time = "2026-06-17T21:06:06.673Z" }, + { url = "https://files.pythonhosted.org/packages/9e/49/c549461daa008159d006a76a991fbc2f26fa8bac27a4030c858463dcb20f/torch-2.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e86550597877fb272ddc52db2f85b82cb601ea7bd932576a0340152cae2200b3", size = 122988095, upload-time = "2026-06-17T21:07:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4a/0300261818e1560d72cc160ac826005507e8b7ca0a35788b591436d05b4a/torch-2.12.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c75e93173c700bccd6bfcc4a9d19ce242ab6dacd1f1781483027a16239b9e650", size = 87992358, upload-time = "2026-06-17T21:07:40.299Z" }, + { url = "https://files.pythonhosted.org/packages/30/a7/874a5ca05e8f159211dca7921060f7057acc1adb26431e119fd150623efc/torch-2.12.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fcb61ccd20784b62bdd78ec84238a5cfb383b4994902e03bac95505ab360884c", size = 426386134, upload-time = "2026-06-17T21:07:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/e1/75/20bb8fe9c1ad6538cce8cd0391b51927ae5af0b17ed1eab44b8824465dc1/torch-2.12.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:f4afc8083dff08719edbea346644476e3cec0cf40ebe256be0ee5d5b7c7e8c0d", size = 532268019, upload-time = "2026-06-17T21:05:37.925Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fa/824ddb662af55b2eabc0dbb7b57c7c0b1bcd93693754a2b8509ec4d16490/torch-2.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:f92609e3b3ce72f25e2eb780d043ced2480c1a86c47c852604fc7a9108648386", size = 122987777, upload-time = "2026-06-17T21:07:09.49Z" }, + { url = "https://files.pythonhosted.org/packages/63/b7/1b49fe7086ea36839cc80abc43174c43d0ab6f676c0891c871c162f44fe3/torch-2.12.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e9b6f7d2dd66ea87a3ae620069d31335d594c06effb1a383bdd21cfe61e44ece", size = 88010025, upload-time = "2026-06-17T21:07:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/d7/06/5b44063a6545036dcc680d2d303b137d9176cfb2cc1e1863e3ef94abeb52/torch-2.12.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:7973ccd3d2cd35c74449213f7bded199bec6c6247e705cbeda7407af79703d91", size = 426392891, upload-time = "2026-06-17T21:05:52.261Z" }, + { url = "https://files.pythonhosted.org/packages/f8/dd/c9ce9a4b0eb3c5bb92d9ea56766e2c22559f0b45171149188494edcce80f/torch-2.12.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c64ac4aac16be5e296dcd912305605804b203333c690bf98c55bc09494ee92ad", size = 532272494, upload-time = "2026-06-17T21:06:22.72Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/f3a601fc1b1f663ff269bfe553654e638651939aa6563e8daa7167c33098/torch-2.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:f6dc4caf7eb4adb38a2d9f536b51db56310fdd1254e69a2d96767e1367c892b3", size = 122987254, upload-time = "2026-06-17T21:06:33.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/b8087556cf81ddd808dbeb34afb8396d7ae7a1694ab489f08b1a0004e7d0/torch-2.12.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2afbb2bdaa8a95040e733f05492ddf133c3967c9b7ce0abd218d704b6cab437d", size = 88303173, upload-time = "2026-06-17T21:05:06.603Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/fe09d1699fbed2afa10ebc692ff2b99d113f2605b6748cea633989e2789a/torch-2.12.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:97eba061fcb042fed191400b15568990073d67eaacaa6ee9b7ca01dd8b790fe9", size = 426404009, upload-time = "2026-06-17T21:04:57.557Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/0ce4f6c1962c60ded7270e0a9eb560fb615c92b89d332cf9e3dff36d5ecc/torch-2.12.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3867b861391701012adb2df93360efb88494dca245a185e3bb7624495cfe3f33", size = 532184292, upload-time = "2026-06-17T21:05:17.526Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/e384c12aba30320ca92aaaf557456cbcb26f04b4df307728bb8f019f5000/torch-2.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dd15595f8fc764cffde8c6361a3beb6ef69a028c851b1b3e70e077f615980d4e", size = 123231142, upload-time = "2026-06-17T21:05:27.061Z" }, ] [[package]]