diff --git a/.github/workflows/reconcile.yml b/.github/workflows/reconcile.yml new file mode 100644 index 0000000..b036668 --- /dev/null +++ b/.github/workflows/reconcile.yml @@ -0,0 +1,78 @@ +name: Reconcile + +# Manually triggered pin reconciliation workflow. A maintainer runs this +# workflow; it reads each catalogued benchmark's public release tags (via +# `git ls-remote`) and opens a PR bumping any pin whose latest release tag +# differs from what is committed. +on: + workflow_dispatch: + inputs: + benchmark: + description: "Bump only this benchmark (leave blank to check all)." + required: false + type: string + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + reconcile: + name: reconcile catalog pins + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: | + **/pyproject.toml + **/uv.lock + + - name: Create venv + run: uv venv --python 3.11 + + - name: Install from uv.lock (dev extras) + run: uv sync --locked --extra dev + + - name: Run reconcile + id: reconcile + env: + NAME: ${{ inputs.benchmark }} + run: | + if [ -n "$NAME" ]; then + uv run python scripts/reconcile.py --benchmark "$NAME" + else + uv run python scripts/reconcile.py + fi + + - name: Open bump PR + if: steps.reconcile.outputs.bumped == 'true' + env: + GH_TOKEN: ${{ github.token }} + NAME: ${{ steps.reconcile.outputs.name }} + TITLE: ${{ steps.reconcile.outputs.title }} + BODY: ${{ steps.reconcile.outputs.body }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + BRANCH="reconcile/$NAME" + git checkout -b "$BRANCH" + git add src/worldevals/register/ + git commit -m "$TITLE" + git push -u origin "$BRANCH" + gh pr create --title "$TITLE" --body "$BODY" --base main --head "$BRANCH" + + - name: Open failure issue + if: steps.reconcile.outputs.failed == 'true' + env: + GH_TOKEN: ${{ github.token }} + NAME: ${{ steps.reconcile.outputs.name }} + REASON: ${{ steps.reconcile.outputs.reason }} + run: | + gh issue create \ + --title "reconcile: failed to extract $NAME" \ + --body "Reconcile could not extract task keys for **$NAME**: $REASON" diff --git a/pyproject.toml b/pyproject.toml index 87d0f4c..08345a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", "Typing :: Typed", ] -dependencies = ["inspect-robots>=0.3"] +dependencies = ["inspect-robots>=0.3", "pyyaml>=6.0", "tomli>=2.0; python_version < '3.11'"] [project.optional-dependencies] dev = [ @@ -31,6 +31,8 @@ dev = [ "ruff>=0.6", "mypy>=1.11", "pre-commit>=3.5", + "tomli>=2.0", + "types-PyYAML>=6.0", ] docs = [ "mkdocs>=1.6", diff --git a/scripts/gen_task_keys.py b/scripts/gen_task_keys.py new file mode 100644 index 0000000..8db5627 --- /dev/null +++ b/scripts/gen_task_keys.py @@ -0,0 +1,27 @@ +"""Regenerate register//task_keys.yaml from each benchmark's pinned source. + +Reads benchmark.yaml, fetches the pinned commit's pyproject.toml, and writes the +extracted inspect_robots.tasks entry-point names. +""" + +from __future__ import annotations + +import yaml + +from worldevals._extract import fetch_task_keys +from worldevals.catalog import _REGISTER_DIR + + +def main() -> int: + for benchmark_yaml in sorted(_REGISTER_DIR.glob("*/benchmark.yaml")): + meta = yaml.safe_load(benchmark_yaml.read_text()) + src = meta["source"] + keys = fetch_task_keys(src["repository_url"], src["repository_commit"]) + out = benchmark_yaml.parent / "task_keys.yaml" + out.write_text(yaml.safe_dump({"task_keys": list(keys)}, sort_keys=False)) + print(f"{meta['name']}: {len(keys)} task keys") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/reconcile.py b/scripts/reconcile.py new file mode 100644 index 0000000..6f87efd --- /dev/null +++ b/scripts/reconcile.py @@ -0,0 +1,139 @@ +"""Reconcile catalog pins against upstream releases. + +For each benchmark (or a single named one), resolve the latest semver tag via +``git ls-remote --tags``, extract task keys at that SHA, and decide whether to +bump. When bumping, rewrite the register/ YAML files and emit step outputs for +the GitHub Actions workflow to open a PR or an issue on failure. +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys + +import yaml + +from worldevals._extract import ExtractError, fetch_task_keys +from worldevals._reconcile import decide_bump, format_pr_body, select_latest_tag +from worldevals.catalog import _REGISTER_DIR, _load_catalog + + +def _write_github_output(pairs: dict[str, str]) -> None: + """Append key=value pairs to $GITHUB_OUTPUT; print if unset.""" + output_file = os.environ.get("GITHUB_OUTPUT") + if output_file: + with open(output_file, "a") as fh: + for key, value in pairs.items(): + if "\n" in value: + # Multi-line: use heredoc form + fh.write(f"{key}< tuple[str, str] | None: + """Run git ls-remote --tags and pick the highest semver tag.""" + result = subprocess.run( + ["git", "ls-remote", "--tags", repository_url], + capture_output=True, + text=True, + timeout=60, + ) + if result.returncode != 0: + return None + lines = result.stdout.strip().splitlines() + return select_latest_tag(lines) + + +def main(argv: list[str] | None = None) -> int: + """Entry point: reconcile catalog pins against upstream releases.""" + parser = argparse.ArgumentParser(description="Reconcile catalog pins") + parser.add_argument( + "--benchmark", + type=str, + default=None, + help="Process only the named benchmark (default: all)", + ) + args = parser.parse_args(argv) + + benchmarks = _load_catalog() + if args.benchmark: + benchmarks = tuple(b for b in benchmarks if b.name == args.benchmark) + if not benchmarks: + print(f"error: no benchmark named {args.benchmark!r}", file=sys.stderr) + return 1 + + for benchmark in benchmarks: + name = benchmark.name + print(f"[{name}] checking...") + + tag_result = _resolve_latest_tag(benchmark.source.repository_url) + if tag_result is None: + print(f"[{name}] no semver tags found; skipping") + continue + + latest_tag, latest_sha = tag_result + + try: + latest_keys = fetch_task_keys(benchmark.source.repository_url, latest_sha) + except ExtractError as exc: + print(f"[{name}] extract error: {exc}", file=sys.stderr) + _write_github_output( + { + "failed": "true", + "name": name, + "reason": str(exc), + } + ) + continue + + bump = decide_bump( + name=name, + current_sha=benchmark.source.repository_commit, + current_tag=benchmark.source.tag, + current_keys=benchmark.task_keys, + latest_sha=latest_sha, + latest_tag=latest_tag, + latest_keys=latest_keys, + ) + + if bump is None: + print(f"[{name}] up to date") + continue + + # Rewrite register//benchmark.yaml (source section only) + benchmark_yaml_path = _REGISTER_DIR / name / "benchmark.yaml" + meta = yaml.safe_load(benchmark_yaml_path.read_text()) + meta["source"]["repository_commit"] = bump.new_sha + meta["source"]["tag"] = bump.new_tag + benchmark_yaml_path.write_text(yaml.safe_dump(meta, sort_keys=False)) + + # Rewrite register//task_keys.yaml + task_keys_path = _REGISTER_DIR / name / "task_keys.yaml" + task_keys_path.write_text( + yaml.safe_dump({"task_keys": list(bump.new_keys)}, sort_keys=False) + ) + + title = f"bump({name}): {bump.old_tag} to {bump.new_tag}" + body = format_pr_body(bump) + print(f"[{name}] {title}") + + _write_github_output( + { + "bumped": "true", + "name": name, + "title": title, + "body": body, + } + ) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/worldevals/_extract.py b/src/worldevals/_extract.py new file mode 100644 index 0000000..e40320e --- /dev/null +++ b/src/worldevals/_extract.py @@ -0,0 +1,78 @@ +"""Read a benchmark's task entry points from its pinned pyproject.toml. + +Fetch only pyproject.toml at a pinned commit and read the +`[project.entry-points."inspect_robots.tasks"]` table. +""" + +from __future__ import annotations + +import sys +import urllib.error +import urllib.request +from collections.abc import Callable + +if sys.version_info >= (3, 11): + import tomllib +else: # pragma: no cover - stdlib tomllib backport for 3.10 + import tomli as tomllib + +_ENTRY_POINT_GROUP = "inspect_robots.tasks" + + +class ExtractError(Exception): + """A benchmark's task keys could not be extracted.""" + + +class NoTaskEntryPointsError(ExtractError): + """The pinned pyproject.toml declares no inspect_robots.tasks entry points.""" + + +def parse_task_keys(pyproject_bytes: bytes) -> tuple[str, ...]: + """Return sorted task entry-point names from pyproject.toml bytes. + + Raises ``NoTaskEntryPointsError`` if the entry-point table is missing or + empty, and ``ExtractError`` if the TOML cannot be parsed. + """ + try: + data = tomllib.loads(pyproject_bytes.decode("utf-8")) + except (tomllib.TOMLDecodeError, UnicodeDecodeError) as exc: + raise ExtractError(f"malformed pyproject.toml: {exc}") from exc + table = data.get("project", {}).get("entry-points", {}).get(_ENTRY_POINT_GROUP, {}) + if not table: + raise NoTaskEntryPointsError( + f'no [project.entry-points."{_ENTRY_POINT_GROUP}"] table found' + ) + return tuple(sorted(table)) + + +_RAW_URL = "https://raw.githubusercontent.com/{owner}/{repo}/{sha}/pyproject.toml" + + +def parse_owner_repo(repository_url: str) -> tuple[str, str]: + """Split ``https://github.com//`` into ``(owner, repo)``.""" + prefix = "https://github.com/" + if not repository_url.startswith(prefix): + raise ExtractError(f"not a github.com URL: {repository_url}") + parts = repository_url[len(prefix) :].strip("/").split("/") + if len(parts) != 2 or not all(parts): + raise ExtractError(f"cannot parse owner/repo from: {repository_url}") + return parts[0], parts[1] + + +def _fetch_pyproject(owner: str, repo: str, sha: str) -> bytes: # pragma: no cover + url = _RAW_URL.format(owner=owner, repo=repo, sha=sha) + try: + with urllib.request.urlopen(url, timeout=30) as resp: + data: bytes = resp.read() + return data + except urllib.error.URLError as exc: + raise ExtractError(f"failed to fetch {url}: {exc}") from exc + + +_FETCH: Callable[[str, str, str], bytes] = _fetch_pyproject + + +def fetch_task_keys(repository_url: str, sha: str) -> tuple[str, ...]: + """Fetch the pinned pyproject.toml and return its sorted task entry-point names.""" + owner, repo = parse_owner_repo(repository_url) + return parse_task_keys(_FETCH(owner, repo, sha)) diff --git a/src/worldevals/_reconcile.py b/src/worldevals/_reconcile.py new file mode 100644 index 0000000..28d599f --- /dev/null +++ b/src/worldevals/_reconcile.py @@ -0,0 +1,107 @@ +"""Decide whether a benchmark's pin should be bumped, and render the PR body. + +Pure logic used by the dispatch-triggered reconcile workflow. Resolving the +latest tag and its SHA happens in the workflow (network); this module only +decides and formats, so it is fully unit-testable. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass + +_SEMVER_RE = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") + + +@dataclass +class Bump: + """A proposed pin bump for one benchmark.""" + + name: str + old_sha: str + new_sha: str + old_tag: str + new_tag: str + old_keys: tuple[str, ...] + new_keys: tuple[str, ...] + + +def decide_bump( + name: str, + current_sha: str, + current_tag: str, + current_keys: tuple[str, ...], + latest_sha: str, + latest_tag: str, + latest_keys: tuple[str, ...], +) -> Bump | None: + """Return a ``Bump`` if the latest SHA differs from the pinned one, else ``None``.""" + if current_sha == latest_sha: + return None + return Bump(name, current_sha, latest_sha, current_tag, latest_tag, current_keys, latest_keys) + + +def format_pr_body(bump: Bump) -> str: + """Render the bump as a reviewable PR body with the task-key delta.""" + added = sorted(set(bump.new_keys) - set(bump.old_keys)) + removed = sorted(set(bump.old_keys) - set(bump.new_keys)) + lines = [ + f"Bump **{bump.name}** {bump.old_tag} to {bump.new_tag}.", + "", + f"- SHA: `{bump.old_sha[:8]}` to `{bump.new_sha[:8]}`", + "", + "Task-key changes:", + *[f" + {k}" for k in added], + *[f" - {k}" for k in removed], + ] + if not added and not removed: + lines.append(" (no task-key changes)") + return "\n".join(lines) + + +def select_latest_tag(refs: list[str]) -> tuple[str, str] | None: + """Pick the highest semver tag from `git ls-remote --tags` output lines.""" + # Collect: tag -> (version_tuple, plain_sha, deref_sha | None) + tags: dict[str, tuple[tuple[int, int, int], str, str | None]] = {} + + for line in refs: + line = line.strip() + if not line: + continue + parts = line.split("\t", 1) + if len(parts) != 2: + continue + sha, ref = parts + if not ref.startswith("refs/tags/"): + continue + + is_deref = ref.endswith("^{}") + tag_name = ref[len("refs/tags/") :] + if is_deref: + tag_name = tag_name[:-3] + + match = _SEMVER_RE.match(tag_name) + if not match: + continue + + version = (int(match.group(1)), int(match.group(2)), int(match.group(3))) + + if tag_name in tags: + existing_version, existing_plain, existing_deref = tags[tag_name] + if is_deref: + tags[tag_name] = (existing_version, existing_plain, sha) + else: + tags[tag_name] = (version, sha, existing_deref) + else: + if is_deref: + tags[tag_name] = (version, "", sha) + else: + tags[tag_name] = (version, sha, None) + + if not tags: + return None + + best_tag = max(tags, key=lambda t: tags[t][0]) + _, plain_sha, deref_sha = tags[best_tag] + final_sha = deref_sha if deref_sha is not None else plain_sha + return (best_tag, final_sha) diff --git a/src/worldevals/catalog.py b/src/worldevals/catalog.py index 354cbc8..342ac71 100644 --- a/src/worldevals/catalog.py +++ b/src/worldevals/catalog.py @@ -1,16 +1,31 @@ -"""The WorldEvals catalog — the registry of physical-AI benchmark repos. +"""The WorldEvals catalog, loaded from per-benchmark YAML in ``register/``. -Each benchmark is its own repository (built on Inspect Robots) that registers its tasks -via entry points. WorldEvals indexes them so you can discover what exists and how -to install it. To add a benchmark, append a -[`Benchmark`][worldevals.catalog.Benchmark] entry here (PR). +Each benchmark is its own repository (built on Inspect Robots). Metadata lives +in ``src/worldevals/register//benchmark.yaml`` (hand-authored) with +generated task keys in a sibling ``task_keys.yaml``. ``CATALOG`` is assembled +from those files at import and is the single in-process source of truth for the +accessors below. """ from __future__ import annotations from dataclasses import dataclass +from pathlib import Path from typing import Literal +import yaml + +_REGISTER_DIR = Path(__file__).resolve().parent / "register" + + +@dataclass(frozen=True) +class Source: + """The pinned upstream source of a benchmark.""" + + repository_url: str + repository_commit: str + tag: str + @dataclass(frozen=True) class Benchmark: @@ -19,46 +34,53 @@ class Benchmark: name: str title: str description: str - repo: str - install: str + source: Source task_keys: tuple[str, ...] tags: tuple[str, ...] bimanual: bool contributors: tuple[str, ...] status: Literal["alpha", "beta", "stable"] = "alpha" - -_KITCHENBENCH_TASKS = ( - "kitchenbench/place_cutlery", - "kitchenbench/stack", - "kitchenbench/place_in_rack", - "kitchenbench/pour_pasta", - "kitchenbench/open_container", - "kitchenbench/fold_cloth", - "kitchenbench/seal_container", - "kitchenbench/handoff", - "kitchenbench/sort_cutlery", - "kitchenbench/scoop_pasta", -) - -CATALOG: tuple[Benchmark, ...] = ( - Benchmark( - name="kitchenbench", - title="KitchenBench", - description=( - "10 bimanual kitchen-manipulation tasks: pick-place, stacking, slotted " - "insertion, granular pour & tool-scoop, lid open/seal, cloth folding, a " - "two-arm handover, and a multi-instance cutlery sort." + @property + def repo(self) -> str: + """The benchmark's repository URL (from its pinned source).""" + return self.source.repository_url + + @property + def install(self) -> str: + """Derived install command: PyPI name if published, else the pinned git URL.""" + # Unpublished benchmarks install from their tagged git source; the two-step + # inspect-robots prerequisite is documented separately (see README). + return f'pip install "{self.name} @ git+{self.source.repository_url}@{self.source.tag}"' + + +def _load_benchmark(directory: Path) -> Benchmark: + meta = yaml.safe_load((directory / "benchmark.yaml").read_text()) + keys = yaml.safe_load((directory / "task_keys.yaml").read_text()) + src = meta["source"] + return Benchmark( + name=meta["name"], + title=meta["title"], + description=meta["description"].strip(), + source=Source( + repository_url=src["repository_url"], + repository_commit=src["repository_commit"], + tag=src["tag"], ), - repo="https://github.com/robocurve/kitchenbench", - install="pip install kitchenbench", # pulls in inspect-robots from PyPI - task_keys=_KITCHENBENCH_TASKS, - tags=("kitchen", "bimanual", "manipulation"), - bimanual=True, - contributors=("robocurve",), - status="alpha", - ), -) + task_keys=tuple(keys["task_keys"]), + tags=tuple(meta["tags"]), + bimanual=bool(meta["bimanual"]), + contributors=tuple(meta["contributors"]), + status=meta.get("status", "alpha"), + ) + + +def _load_catalog() -> tuple[Benchmark, ...]: + dirs = sorted(p.parent for p in _REGISTER_DIR.glob("*/benchmark.yaml")) + return tuple(_load_benchmark(d) for d in dirs) + + +CATALOG: tuple[Benchmark, ...] = _load_catalog() def catalog() -> tuple[Benchmark, ...]: diff --git a/src/worldevals/register/README.md b/src/worldevals/register/README.md new file mode 100644 index 0000000..6817fc2 --- /dev/null +++ b/src/worldevals/register/README.md @@ -0,0 +1,90 @@ +# Registration contract + +WorldEvals catalogs physical-AI benchmarks. Each benchmark is registered as a +directory under `src/worldevals/register//` containing two files: + +- `benchmark.yaml` (hand-authored): the benchmark's metadata and source pin. +- `task_keys.yaml` (CI-generated): the Inspect Robots task keys extracted from + the pinned commit. Never edit this file by hand. + + +## benchmark.yaml schema + +```yaml +name: kitchenbench +title: KitchenBench +description: > + Short description of what the benchmark evaluates. +tags: [kitchen, bimanual, manipulation] +bimanual: true +contributors: [robocurve] +status: alpha +source: + repository_url: https://github.com/robocurve/kitchenbench + repository_commit: "814e63c7b46b383de89af0a38e0a6499a6467bee" + tag: v0.3.0 +``` + +**name:** must be unique across the catalog and match the directory name. + +**source.repository_url:** a public `https://github.com/...` URL. + +**source.repository_commit:** the full 40-character SHA of the pinned commit. + +**source.tag:** the git tag corresponding to the pinned commit. + + +## How to add a benchmark + +1. Create `src/worldevals/register//benchmark.yaml` with your benchmark's + metadata and source pin (SHA + tag). +2. Generate the task keys by running `uv run python scripts/gen_task_keys.py`. + This reads your pinned commit's `pyproject.toml` and writes + `src/worldevals/register//task_keys.yaml`. Commit both files. +3. Open a pull request. + +See "Keeping the pin current" below for how the pin is updated after a new +release. + + +## Keeping the pin current + +A catalogued pin does not update itself. When a benchmark publishes a new +release, a WorldEvals maintainer bumps the pin by running the `Reconcile` +workflow (Actions tab, "Run workflow"). You do not need to add any workflow +trigger to your benchmark repo. + +The workflow reads each benchmark's public release tags (via `git ls-remote`), +and for any benchmark whose latest release tag differs from the committed pin, +it regenerates `task_keys.yaml` at the new commit and opens a bump PR. Leave +the optional `benchmark` input blank to check every catalogue entry, or set it +to bump a single benchmark. A maintainer reviews and merges the PR. + +To signal that your benchmark has a new release worth pinning, open an issue on +WorldEvals or ping a maintainer; there is no automatic push from your repo. + + +## Static pyproject.toml requirement + +A catalogued benchmark MUST declare its Inspect Robots tasks statically under +`[project.entry-points."inspect_robots.tasks"]` in a root-level +`pyproject.toml`. The task-key extractor reads this table directly from the +pinned commit (without installing the package). + +### Known limitations + +Three layouts are not supported: + +1. **Dynamic or build-generated entry points.** If entry points are produced by + a build backend plugin or code generation step, the extractor cannot see + them. +2. **Non-standard table keys.** Only + `[project.entry-points."inspect_robots.tasks"]` is recognized. Older + alternatives (such as `roboinspect.tasks`) are ignored. +3. **Non-root pyproject.toml.** Monorepo layouts where the package lives in a + subdirectory are not supported. The extractor looks only at + `pyproject.toml` in the repository root. + +The extractor fails loudly when the expected table is missing. A +non-conforming benchmark cannot be silently catalogued as taskless; CI will +surface the error. diff --git a/src/worldevals/register/kitchenbench/benchmark.yaml b/src/worldevals/register/kitchenbench/benchmark.yaml new file mode 100644 index 0000000..f693ee4 --- /dev/null +++ b/src/worldevals/register/kitchenbench/benchmark.yaml @@ -0,0 +1,14 @@ +name: kitchenbench +title: KitchenBench +description: > + 10 bimanual kitchen-manipulation tasks: pick-place, stacking, slotted + insertion, granular pour and tool-scoop, lid open/seal, cloth folding, a + two-arm handover, and a multi-instance cutlery sort. +tags: [kitchen, bimanual, manipulation] +bimanual: true +contributors: [robocurve] +status: alpha +source: + repository_url: https://github.com/robocurve/kitchenbench + repository_commit: "e781f9318648a2d65855a6b0fd138c056686d5b9" + tag: v0.5.0 diff --git a/src/worldevals/register/kitchenbench/task_keys.yaml b/src/worldevals/register/kitchenbench/task_keys.yaml new file mode 100644 index 0000000..9d72f7a --- /dev/null +++ b/src/worldevals/register/kitchenbench/task_keys.yaml @@ -0,0 +1,11 @@ +task_keys: +- kitchenbench/fold_cloth +- kitchenbench/handoff +- kitchenbench/open_container +- kitchenbench/place_cutlery +- kitchenbench/place_in_rack +- kitchenbench/pour_pasta +- kitchenbench/scoop_pasta +- kitchenbench/seal_container +- kitchenbench/sort_cutlery +- kitchenbench/stack diff --git a/uv.lock b/uv.lock index 3be186f..cc66863 100644 --- a/uv.lock +++ b/uv.lock @@ -1439,6 +1439,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" @@ -1519,6 +1528,8 @@ name = "worldevals" source = { editable = "." } dependencies = [ { name = "inspect-robots" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] [package.optional-dependencies] @@ -1528,6 +1539,8 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, + { name = "tomli" }, + { name = "types-pyyaml" }, ] docs = [ { name = "mkdocs" }, @@ -1549,6 +1562,10 @@ requires-dist = [ { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.5" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0" }, + { name = "tomli", marker = "extra == 'dev'", specifier = ">=2.0" }, + { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0" }, ] provides-extras = ["dev", "docs"]