embodied-first-exam 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- embodied_first_exam-0.1.0/.gitignore +32 -0
- embodied_first_exam-0.1.0/LICENSE +21 -0
- embodied_first_exam-0.1.0/PKG-INFO +27 -0
- embodied_first_exam-0.1.0/pyproject.toml +49 -0
- embodied_first_exam-0.1.0/scripts/efe-harbor.pth +1 -0
- embodied_first_exam-0.1.0/scripts/efe.py +294 -0
- embodied_first_exam-0.1.0/scripts/efe_harbor_hook.py +33 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.ipynb_checkpoints/
|
|
8
|
+
# run outputs and logs
|
|
9
|
+
/exps/
|
|
10
|
+
/jobs/
|
|
11
|
+
*.log
|
|
12
|
+
nohup.out
|
|
13
|
+
# secrets
|
|
14
|
+
.env
|
|
15
|
+
.env.*
|
|
16
|
+
!.env.example
|
|
17
|
+
# editor, OS and merge leftovers
|
|
18
|
+
.DS_Store
|
|
19
|
+
Thumbs.db
|
|
20
|
+
*.swp
|
|
21
|
+
*~
|
|
22
|
+
*.bak
|
|
23
|
+
*.bak-*
|
|
24
|
+
*.orig
|
|
25
|
+
*.rej
|
|
26
|
+
*.tmp
|
|
27
|
+
# build contexts that scripts stage (images/<name>/BUILD_CONTEXT); images/ese/context/ is the committed snapshot
|
|
28
|
+
/images/*/context/
|
|
29
|
+
!/images/ese/context/
|
|
30
|
+
images/mujoco/rcb_mj/
|
|
31
|
+
# generated (scripts/dataset_manifest.py write)
|
|
32
|
+
/tasks/dataset.toml
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kangrui Wang and Embodied First Exam contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: embodied-first-exam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Embodied First Exam on Harbor: `harbor run --core|--all|--privileged|--standard|--standard-realtime`, or `efe run`
|
|
5
|
+
Project-URL: Homepage, https://www.embodied-first-exam.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/mll-lab-nu/embodied-first-exam
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: <3.15,>=3.12
|
|
10
|
+
Requires-Dist: harbor==0.23.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Embodied First Exam
|
|
14
|
+
|
|
15
|
+
Code agents program robots in simulation: [embodied-first-exam.ai](https://www.embodied-first-exam.ai),
|
|
16
|
+
[github.com/mll-lab-nu/embodied-first-exam](https://github.com/mll-lab-nu/embodied-first-exam).
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# needs git and Docker with Compose; GPU tasks also need an NVIDIA GPU and the NVIDIA Container Toolkit
|
|
20
|
+
uv tool install embodied-first-exam --with-executables-from harbor
|
|
21
|
+
# your API keys: in the shell, or in a .env file in the working directory
|
|
22
|
+
# the core set; the tasks are cloned on first use (a clone you run from, or $EFE_ROOT, is used instead)
|
|
23
|
+
harbor run -d embodied-first-exam -a codex -m openai/gpt-6-astra
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This installs Harbor 0.23.0 with the benchmark's flags on `harbor run` and the `efe` command. The tasks are not in
|
|
27
|
+
the package: see the repository's README.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "embodied-first-exam"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Embodied First Exam on Harbor: `harbor run --core|--all|--privileged|--standard|--standard-realtime`, or `efe run`"
|
|
5
|
+
readme = { content-type = "text/markdown", text = """
|
|
6
|
+
# Embodied First Exam
|
|
7
|
+
|
|
8
|
+
Code agents program robots in simulation: [embodied-first-exam.ai](https://www.embodied-first-exam.ai),
|
|
9
|
+
[github.com/mll-lab-nu/embodied-first-exam](https://github.com/mll-lab-nu/embodied-first-exam).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# needs git and Docker with Compose; GPU tasks also need an NVIDIA GPU and the NVIDIA Container Toolkit
|
|
13
|
+
uv tool install embodied-first-exam --with-executables-from harbor
|
|
14
|
+
# your API keys: in the shell, or in a .env file in the working directory
|
|
15
|
+
# the core set; the tasks are cloned on first use (a clone you run from, or $EFE_ROOT, is used instead)
|
|
16
|
+
harbor run -d embodied-first-exam -a codex -m openai/gpt-6-astra
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This installs Harbor 0.23.0 with the benchmark's flags on `harbor run` and the `efe` command. The tasks are not in
|
|
20
|
+
the package: see the repository's README.
|
|
21
|
+
""" }
|
|
22
|
+
license = "MIT"
|
|
23
|
+
license-files = ["LICENSE"]
|
|
24
|
+
requires-python = ">=3.12,<3.15" # Harbor's dependency fastuuid has no wheel for newer Pythons yet
|
|
25
|
+
dependencies = ["harbor==0.23.0"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://www.embodied-first-exam.ai"
|
|
29
|
+
Repository = "https://github.com/mll-lab-nu/embodied-first-exam"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
efe = "efe:main"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling>=1.27"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
# The wheel: scripts/efe.py, efe_harbor_hook.py and efe-harbor.pth, nothing else (the tasks are found at run time,
|
|
39
|
+
# scripts/efe.py find_root). An editable install (`uv tool install -e .`) puts scripts/ on the path instead.
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
only-include = ["scripts/efe.py", "scripts/efe_harbor_hook.py"]
|
|
42
|
+
sources = ["scripts"]
|
|
43
|
+
dev-mode-dirs = ["scripts"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
46
|
+
"scripts/efe-harbor.pth" = "efe-harbor.pth"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
only-include = ["scripts/efe.py", "scripts/efe_harbor_hook.py", "scripts/efe-harbor.pth", "LICENSE"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import efe_harbor_hook
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Embodied First Exam runs on Harbor. `uv tool install --force -e . --with-executables-from harbor` (in a clone) or
|
|
3
|
+
`pip install embodied-first-exam` installs `harbor` (Harbor 0.23.0, plus the flags below on `harbor run`) and `efe` (the
|
|
4
|
+
same, `efe run ...`). Run from anywhere. The tasks: $EFE_ROOT, else the clone that contains the working directory, else
|
|
5
|
+
the clone of an editable install, else a shallow clone of the release tag v<version> made on first use in
|
|
6
|
+
~/.cache/embodied-first-exam/<version> (find_root).
|
|
7
|
+
|
|
8
|
+
harbor run -a codex -m openai/gpt-6-astra # core: each core task in its default mode (the default)
|
|
9
|
+
harbor run --core --privileged -a codex -m openai/gpt-6-astra # the core tasks in another mode (also --standard, --standard-realtime)
|
|
10
|
+
harbor run --all -a codex -m openai/gpt-6-astra # every task dir
|
|
11
|
+
harbor run --all --privileged -i 'robotwin_2_0_*' -a oracle # mode flags and -i / -x filter within the set
|
|
12
|
+
harbor run --benchmark roboarc -a codex -m openai/gpt-6-astra # one benchmark's core tasks
|
|
13
|
+
harbor run -d embodied-first-exam@core -a codex -m openai/gpt-6-astra # the same sets as a dataset: @core, @all
|
|
14
|
+
harbor run -p tasks/<benchmark>/<task> -a nop # one task: Harbor's own -p, untouched
|
|
15
|
+
harbor run -p tasks/<benchmark> -a nop # every task of one benchmark: Harbor's own -p, untouched
|
|
16
|
+
|
|
17
|
+
Set: --core (default) or --all. core = the tasks of docs/core_set.toml, each in its default_mode; with mode flags, each
|
|
18
|
+
core task in those modes (a task without that mode is skipped, and counted). all = every task dir; mode flags filter it.
|
|
19
|
+
Modes: any of --privileged, --standard, --standard-realtime.
|
|
20
|
+
-d embodied-first-exam[@core|@all] (also `efe`) is --core / --all (no version: core); any other -d is Harbor's.
|
|
21
|
+
--benchmark <name> keeps a benchmark: its README label or its folder under tasks/ (repeatable). With any of these flags,
|
|
22
|
+
-p <dir> (a task dir, a benchmark folder or tasks/) narrows the set to the task dirs under it, -l <n> keeps the first n of
|
|
23
|
+
them, and -c <file> is layered after the wrapper's job config. --list prints the task dirs and runs nothing. The tasks
|
|
24
|
+
are stamped in place (scripts/stamp_mtimes.py) and handed to Harbor as one dataset per benchmark folder (Harbor's
|
|
25
|
+
local datasets are not searched recursively). A
|
|
26
|
+
`harbor run` that names its own tasks (-p, -d, -t, -c) and none of these flags is Harbor's, unchanged; so is every other
|
|
27
|
+
subcommand."""
|
|
28
|
+
import fnmatch, json, os, shutil, subprocess, sys, tempfile
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
# --- the task tree (board 0209): where the tasks and their scripts/ come from -------------------------------------------
|
|
32
|
+
REPO_URL = "https://github.com/mll-lab-nu/embodied-first-exam.git"
|
|
33
|
+
_ROOT: list[Path] = []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_tree(d: Path) -> bool:
|
|
37
|
+
"""A checkout of this repository: the core set, the tasks and the scripts that read them."""
|
|
38
|
+
return (d / "docs/core_set.toml").is_file() and (d / "tasks").is_dir() and (d / "scripts/task_conventions.py").is_file()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cache_dir() -> Path:
|
|
42
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
43
|
+
try:
|
|
44
|
+
ver = version("embodied-first-exam")
|
|
45
|
+
except PackageNotFoundError:
|
|
46
|
+
ver = "main"
|
|
47
|
+
return Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "embodied-first-exam" / ver
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cached_clone(d: Path) -> Path:
|
|
51
|
+
"""`git clone --depth 1` of the tag v<version> (main when there is no such tag) into d, once; SystemExit(2) on failure."""
|
|
52
|
+
tag = "main" if d.name == "main" else f"v{d.name}"
|
|
53
|
+
try:
|
|
54
|
+
r = subprocess.run(["git", "ls-remote", "--exit-code", "--tags", REPO_URL, f"refs/tags/{tag}"], capture_output=True)
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
print("efe: the tasks need git (no clone here, no EFE_ROOT)", file=sys.stderr); raise SystemExit(2)
|
|
57
|
+
if r.returncode not in (0, 2):
|
|
58
|
+
print(f"efe: cannot reach {REPO_URL} (git ls-remote exit {r.returncode}); clone it and run from there, or set "
|
|
59
|
+
"EFE_ROOT", file=sys.stderr); raise SystemExit(2)
|
|
60
|
+
if tag != "main" and r.returncode == 2:
|
|
61
|
+
print(f"efe: {REPO_URL} has no tag {tag}: using main instead (delete {d} to clone again later)", file=sys.stderr)
|
|
62
|
+
tag = "main"
|
|
63
|
+
print(f"efe: no clone here and no EFE_ROOT: cloning the tasks ({tag}) into {d}, once", file=sys.stderr)
|
|
64
|
+
d.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
tmp = tempfile.mkdtemp(prefix=f".{d.name}-", dir=d.parent)
|
|
66
|
+
try:
|
|
67
|
+
if subprocess.run(["git", "clone", "--depth", "1", "--branch", tag, REPO_URL, tmp]).returncode:
|
|
68
|
+
print(f"efe: git clone of {tag} failed", file=sys.stderr); raise SystemExit(2)
|
|
69
|
+
if not is_tree(d): # else another run made it meanwhile
|
|
70
|
+
try:
|
|
71
|
+
os.rename(tmp, d)
|
|
72
|
+
except OSError as e:
|
|
73
|
+
print(f"efe: cannot place the clone at {d} ({e}); remove {d} and run again", file=sys.stderr)
|
|
74
|
+
raise SystemExit(2)
|
|
75
|
+
finally:
|
|
76
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
77
|
+
return d
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def find_root(clone: bool = True) -> Path | None:
|
|
81
|
+
"""The task tree: $EFE_ROOT; else the checkout that contains the working directory; else the checkout this file is in
|
|
82
|
+
(`uv tool install -e .`); else the cached clone (made here on first use when clone; None when not)."""
|
|
83
|
+
if _ROOT:
|
|
84
|
+
return _ROOT[0]
|
|
85
|
+
cwd = Path.cwd().resolve()
|
|
86
|
+
if os.environ.get("EFE_ROOT"):
|
|
87
|
+
d = Path(os.environ["EFE_ROOT"]).expanduser().resolve()
|
|
88
|
+
if not is_tree(d):
|
|
89
|
+
print(f"EFE_ROOT={d}: not a checkout of {REPO_URL}", file=sys.stderr); raise SystemExit(2)
|
|
90
|
+
else:
|
|
91
|
+
d = next((p for p in (cwd, *cwd.parents, Path(__file__).resolve().parents[1], cache_dir()) if is_tree(p)), None)
|
|
92
|
+
if d is None and clone:
|
|
93
|
+
d = cached_clone(cache_dir())
|
|
94
|
+
if d is not None:
|
|
95
|
+
_ROOT.append(d)
|
|
96
|
+
return d
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class _Scripts:
|
|
100
|
+
"""The task tree's own scripts/<name>.py (task_conventions, stamp_mtimes), imported on first use."""
|
|
101
|
+
def __init__(self, name: str):
|
|
102
|
+
self._name = name
|
|
103
|
+
|
|
104
|
+
def __getattr__(self, attr: str):
|
|
105
|
+
import importlib
|
|
106
|
+
d = str(find_root() / "scripts")
|
|
107
|
+
if d not in sys.path:
|
|
108
|
+
sys.path.insert(0, d)
|
|
109
|
+
return getattr(importlib.import_module(self._name), attr)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
tc = _Scripts("task_conventions")
|
|
113
|
+
# ------------------------------------------------------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
MODES = {"--privileged": "privileged", "--privilege": "privileged", "--standard": "standard",
|
|
116
|
+
"--standard-realtime": "standard-realtime", "--standard_realtime": "standard-realtime"}
|
|
117
|
+
DATASET = ("embodied-first-exam", "efe")
|
|
118
|
+
SOURCES = ("-p", "--path", "-d", "--dataset", "-t", "--task", "-c", "--config", "--task-git-url")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def harbor(args: list[str], app=None) -> int:
|
|
122
|
+
if app is None:
|
|
123
|
+
from harbor.cli import main as hm
|
|
124
|
+
app = getattr(hm.app, "__wrapped_app__", hm.app)
|
|
125
|
+
sys.argv = ["harbor", *args]
|
|
126
|
+
return app() or 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def bench_label(name: str) -> str | None:
|
|
130
|
+
"""A --benchmark value as its BENCHMARKS label: the label itself (any case) or its folder under tasks/."""
|
|
131
|
+
by = {**{lab.lower(): lab for lab in tc.BENCHMARK_DIRS}, **{f: lab for lab, f in tc.BENCHMARK_DIRS.items()}}
|
|
132
|
+
return by.get(name) or by.get(name.lower())
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def under(paths: list[str]) -> set[str] | None:
|
|
136
|
+
"""The task dir names under the -p paths (a task dir, a benchmark folder, tasks/; a relative path that is not one
|
|
137
|
+
here is taken in this clone, so `-p tasks/<benchmark>` works from anywhere); None: no -p given. SystemExit(2): a path
|
|
138
|
+
that is neither."""
|
|
139
|
+
if not paths:
|
|
140
|
+
return None
|
|
141
|
+
out: set[str] = set()
|
|
142
|
+
for a in paths:
|
|
143
|
+
d = Path(a).expanduser()
|
|
144
|
+
if not d.is_dir() and not d.is_absolute() and (find_root() / d).is_dir():
|
|
145
|
+
d = find_root() / d
|
|
146
|
+
if not d.is_dir():
|
|
147
|
+
print(f"-p {a}: no such directory (here or in {find_root()})", file=sys.stderr)
|
|
148
|
+
raise SystemExit(2)
|
|
149
|
+
d = d.resolve()
|
|
150
|
+
if (d / "task.toml").is_file():
|
|
151
|
+
out.add(d.name)
|
|
152
|
+
else:
|
|
153
|
+
out |= {t.parent.name for pat in ("*/task.toml", "*/*/task.toml") for t in d.glob(pat)}
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def stamp(pick: list[str]) -> bool:
|
|
158
|
+
"""Give the picked task dirs' files their content-derived mtimes in place before Harbor builds any image from them
|
|
159
|
+
(scripts/stamp_mtimes.py, issue #258: a checkout's shared mtimes let BuildKit put another task's file in an image).
|
|
160
|
+
Files already stamped are skipped. False: a file could not be stamped."""
|
|
161
|
+
stamp_mtimes = _Scripts("stamp_mtimes")
|
|
162
|
+
err = sum(stamp_mtimes.stamp_tree(str(tc.task_dir(t)))["err"] for t in pick)
|
|
163
|
+
if err:
|
|
164
|
+
print(f"stamp_mtimes: {err} file(s) could not be stamped; no image is built from an unstamped task "
|
|
165
|
+
"(docs/protocol/running.md, Building task images)", file=sys.stderr)
|
|
166
|
+
return not err
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def config_file(pick: list[str]) -> str:
|
|
170
|
+
"""A Harbor job config (-c) with the picked task dirs: one local dataset per benchmark folder, its task_names the
|
|
171
|
+
picked dirs. Harbor reads it when the job is built; the caller removes it afterwards."""
|
|
172
|
+
by: dict[str, list[str]] = {}
|
|
173
|
+
for t in pick:
|
|
174
|
+
by.setdefault(str(tc.task_dir(t).parent), []).append(t)
|
|
175
|
+
cfg = {"datasets": [{"path": d, "task_names": names} for d, names in sorted(by.items())]}
|
|
176
|
+
fd, path = tempfile.mkstemp(prefix="efe-job-", suffix=".json")
|
|
177
|
+
with os.fdopen(fd, "w") as f:
|
|
178
|
+
json.dump(cfg, f, indent=1)
|
|
179
|
+
return path
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def load_env() -> None:
|
|
183
|
+
"""The working directory's and the clone's .env (cp .env.example .env) into the environment: their non-empty keys
|
|
184
|
+
the shell does not set (no clone is made for it)."""
|
|
185
|
+
from dotenv import dotenv_values
|
|
186
|
+
root = find_root(clone=False)
|
|
187
|
+
for f in dict.fromkeys([Path.cwd() / ".env", *([root / ".env"] if root else [])]):
|
|
188
|
+
for k, v in dotenv_values(f).items() if f.is_file() else ():
|
|
189
|
+
if v and not v.startswith("#") and k not in os.environ: # `KEY= # note`: dotenv reads the note as the value
|
|
190
|
+
os.environ[k] = v
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def dataset_sets(argv: list[str]) -> list[str] | None:
|
|
194
|
+
"""argv with -d embodied-first-exam[@core|@all] turned into --core / --all; None: an unknown version."""
|
|
195
|
+
out, it = [], iter(argv)
|
|
196
|
+
for a in it:
|
|
197
|
+
v = a.split("=", 1)[1] if a.startswith("--dataset=") else next(it, "") if a in ("-d", "--dataset") else None
|
|
198
|
+
name, _, ver = (v or "").partition("@")
|
|
199
|
+
if v is None or name not in DATASET:
|
|
200
|
+
out += [a] if v is None or a.startswith("--dataset=") else [a, v]
|
|
201
|
+
elif ver in ("", "core", "all"):
|
|
202
|
+
out.append("--" + (ver or "core"))
|
|
203
|
+
else:
|
|
204
|
+
print(f"-d {v}: the versions are {name}@core and {name}@all", file=sys.stderr)
|
|
205
|
+
return None
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def run(argv: list[str], app=None) -> int:
|
|
210
|
+
"""`harbor run` with the set / mode / benchmark flags: picks the task dirs, then `harbor run -c <job config> ...`
|
|
211
|
+
(config_file: one dataset per benchmark folder)."""
|
|
212
|
+
argv = dataset_sets(argv)
|
|
213
|
+
if argv is None:
|
|
214
|
+
return 2
|
|
215
|
+
load_env()
|
|
216
|
+
ours = ("--core", "--all", "--benchmark", "--list")
|
|
217
|
+
if any(a in SOURCES or a.startswith(tuple(s + "=" for s in SOURCES if s.startswith("--"))) for a in argv):
|
|
218
|
+
if not any(a in MODES or a in ours or a.startswith("--benchmark=") for a in argv):
|
|
219
|
+
return harbor(["run", *argv], app) # the user's own task source: plain Harbor
|
|
220
|
+
which, modes, benches, inc, exc, paths, listing, rest = "core", set(), [], [], [], [], False, []
|
|
221
|
+
n_tasks = None
|
|
222
|
+
it = iter(argv)
|
|
223
|
+
for a in it:
|
|
224
|
+
if a in ("--all", "--core"):
|
|
225
|
+
which = a[2:]
|
|
226
|
+
elif a in MODES:
|
|
227
|
+
modes.add(MODES[a])
|
|
228
|
+
elif a == "--benchmark" or a.startswith("--benchmark="):
|
|
229
|
+
b = a.split("=", 1)[1] if "=" in a else next(it, "")
|
|
230
|
+
lab = bench_label(b)
|
|
231
|
+
if lab is None:
|
|
232
|
+
print(f"--benchmark {b!r}: not a benchmark (a README label or a folder under tasks/: "
|
|
233
|
+
f"{', '.join(sorted(tc.BENCHMARK_DIRS.values()))})", file=sys.stderr)
|
|
234
|
+
return 2
|
|
235
|
+
benches.append(lab)
|
|
236
|
+
elif a in ("-i", "--include-task-name") or a.startswith("--include-task-name="):
|
|
237
|
+
inc.append(a.split("=", 1)[1] if "=" in a else next(it, ""))
|
|
238
|
+
elif a in ("-x", "--exclude-task-name") or a.startswith("--exclude-task-name="):
|
|
239
|
+
exc.append(a.split("=", 1)[1] if "=" in a else next(it, ""))
|
|
240
|
+
elif a in ("-l", "--n-tasks") or a.startswith("--n-tasks="): # Harbor's -l: applied here, to the picked set
|
|
241
|
+
v = a.split("=", 1)[1] if "=" in a else next(it, "")
|
|
242
|
+
if not v.isdigit():
|
|
243
|
+
print(f"{a} {v!r}: the number of tasks to run", file=sys.stderr); return 2
|
|
244
|
+
n_tasks = int(v)
|
|
245
|
+
elif a in ("-p", "--path") or a.startswith("--path="):
|
|
246
|
+
paths.append(a.split("=", 1)[1] if "=" in a else next(it, ""))
|
|
247
|
+
elif a in ("-c", "--config"):
|
|
248
|
+
rest += [a, next(it, "")] # the user's config: layered after ours
|
|
249
|
+
elif a in SOURCES or a.startswith(tuple(s + "=" for s in SOURCES if s.startswith("--") and s != "--config")):
|
|
250
|
+
print(f"{a}: names its own tasks; it does not combine with {', '.join((*ours, *MODES))}", file=sys.stderr)
|
|
251
|
+
return 2
|
|
252
|
+
elif a == "--list":
|
|
253
|
+
listing = True
|
|
254
|
+
elif a in ("-h", "--help"):
|
|
255
|
+
print(__doc__); return harbor(["run", "--help"], app)
|
|
256
|
+
else:
|
|
257
|
+
rest.append(a)
|
|
258
|
+
tasks = [t.name for t in tc.iter_tasks()]
|
|
259
|
+
missing: dict[str, list[str]] = {}
|
|
260
|
+
pick = tasks if which == "all" else sorted(set(tc.core_tasks(modes, missing=missing)) & set(tasks))
|
|
261
|
+
for m, ts in sorted(missing.items()):
|
|
262
|
+
print(f"{len(ts)} core task(s) have no {m} mode: skipped", file=sys.stderr)
|
|
263
|
+
scope = under(paths)
|
|
264
|
+
pick = [t for t in pick if (scope is None or t in scope)
|
|
265
|
+
and (not modes or tc.mode_of(Path(t)) in modes)
|
|
266
|
+
and (not benches or tc.bench_of(Path(t)) in benches)
|
|
267
|
+
and (not inc or any(fnmatch.fnmatchcase(t, p) for p in inc))
|
|
268
|
+
and not any(fnmatch.fnmatchcase(t, p) for p in exc)]
|
|
269
|
+
pick = pick[:n_tasks] if n_tasks is not None else pick
|
|
270
|
+
if not pick:
|
|
271
|
+
print("no task matches", file=sys.stderr); return 2
|
|
272
|
+
print(f"{len(pick)} task(s) of " + which + (f", {', '.join(sorted(modes))}" if modes else "")
|
|
273
|
+
+ (f", {', '.join(sorted(set(benches)))}" if benches else ""), file=sys.stderr)
|
|
274
|
+
if listing:
|
|
275
|
+
print("\n".join(pick)); return 0
|
|
276
|
+
if not stamp(pick):
|
|
277
|
+
return 2
|
|
278
|
+
cfg = config_file(pick)
|
|
279
|
+
try:
|
|
280
|
+
return harbor(["run", "-c", cfg, *rest], app)
|
|
281
|
+
finally:
|
|
282
|
+
os.unlink(cfg)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def main() -> int:
|
|
286
|
+
"""The `efe` command: `efe run ...` = `harbor run ...` above, `efe` alone prints this help."""
|
|
287
|
+
argv = sys.argv[1:]
|
|
288
|
+
if not argv or argv[0] in ("-h", "--help"):
|
|
289
|
+
print(__doc__.replace("harbor run", "efe run")); return 0
|
|
290
|
+
return run(argv[1:]) if argv[0] == "run" else harbor(argv)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
if __name__ == "__main__":
|
|
294
|
+
sys.exit(main())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Loaded at interpreter start by efe-harbor.pth (installed with this repo: `uv tool install --force -e .`): when
|
|
2
|
+
Harbor's CLI module is imported, its `app` is wrapped so that `harbor run` takes --core / --all / --privileged /
|
|
3
|
+
--standard / --standard-realtime (scripts/efe.py). Harbor's own options and subcommands are unchanged."""
|
|
4
|
+
import importlib.abc, importlib.machinery, sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _Finder(importlib.abc.MetaPathFinder):
|
|
8
|
+
def find_spec(self, name, path, target=None):
|
|
9
|
+
if name != "harbor.cli.main":
|
|
10
|
+
return None
|
|
11
|
+
sys.meta_path.remove(self)
|
|
12
|
+
spec = importlib.machinery.PathFinder.find_spec(name, path)
|
|
13
|
+
if spec is None or spec.loader is None:
|
|
14
|
+
return spec
|
|
15
|
+
exec_module = spec.loader.exec_module
|
|
16
|
+
|
|
17
|
+
def wrapped(module):
|
|
18
|
+
exec_module(module)
|
|
19
|
+
app = module.app
|
|
20
|
+
|
|
21
|
+
def harbor_app(*a, **kw):
|
|
22
|
+
if not a and not kw and sys.argv[1:2] == ["run"]:
|
|
23
|
+
import efe
|
|
24
|
+
return efe.run(sys.argv[2:], app=app)
|
|
25
|
+
return app(*a, **kw)
|
|
26
|
+
harbor_app.__wrapped_app__ = app
|
|
27
|
+
module.app = harbor_app
|
|
28
|
+
spec.loader.exec_module = wrapped
|
|
29
|
+
return spec
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if not any(isinstance(f, _Finder) for f in sys.meta_path):
|
|
33
|
+
sys.meta_path.insert(0, _Finder())
|