pluginbench 0.5.0__py3-none-any.whl
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.
- pluginbench/__init__.py +3 -0
- pluginbench/__main__.py +3 -0
- pluginbench/assessment-charts.html +790 -0
- pluginbench/assessment.py +316 -0
- pluginbench/assessment_charts.py +1225 -0
- pluginbench/assessment_metrics.py +1956 -0
- pluginbench/assessment_models.py +127 -0
- pluginbench/baselines.py +792 -0
- pluginbench/claude.py +374 -0
- pluginbench/cli.py +672 -0
- pluginbench/code_diff.py +207 -0
- pluginbench/codex.py +451 -0
- pluginbench/codex_events.py +585 -0
- pluginbench/collectors.py +591 -0
- pluginbench/compare.py +927 -0
- pluginbench/components.py +129 -0
- pluginbench/coverage_report.py +610 -0
- pluginbench/datasets.py +207 -0
- pluginbench/evaluation.example.yaml +45 -0
- pluginbench/evaluation.py +1651 -0
- pluginbench/evaluation_report.py +1004 -0
- pluginbench/fallow_parse.mjs +101 -0
- pluginbench/harbor_backend.py +2946 -0
- pluginbench/isolation.py +314 -0
- pluginbench/judge.py +683 -0
- pluginbench/language_analysis.py +915 -0
- pluginbench/ledger.py +217 -0
- pluginbench/measurement_profiles.py +616 -0
- pluginbench/native_treatment_runtime.js +221 -0
- pluginbench/native_treatments.py +155 -0
- pluginbench/observer_data/go-analysis-v1/go.mod +10 -0
- pluginbench/observer_data/go-analysis-v1/go.sum +10 -0
- pluginbench/observer_data/go-analysis-v1/main.go +64 -0
- pluginbench/observer_data/oxc-parser-0.126.0/package-lock.json +449 -0
- pluginbench/observer_data/oxc-parser-0.126.0/package.json +5 -0
- pluginbench/parallel.py +101 -0
- pluginbench/processes.py +100 -0
- pluginbench/remeasure.py +992 -0
- pluginbench/run_lock.py +227 -0
- pluginbench/run_status.py +518 -0
- pluginbench/study.py +1858 -0
- pluginbench/study_review.py +924 -0
- pluginbench/submissions.py +486 -0
- pluginbench/task_sources.py +403 -0
- pluginbench/treatments.py +388 -0
- pluginbench/trial_containers.py +94 -0
- pluginbench/vendor/ECHARTS-LICENSE +222 -0
- pluginbench/vendor/ECHARTS-NOTICE +5 -0
- pluginbench/vendor/ZRENDER-LICENSE +29 -0
- pluginbench/vendor/echarts.min.js +48 -0
- pluginbench-0.5.0.dist-info/METADATA +294 -0
- pluginbench-0.5.0.dist-info/RECORD +58 -0
- pluginbench-0.5.0.dist-info/WHEEL +4 -0
- pluginbench-0.5.0.dist-info/entry_points.txt +2 -0
- pluginbench-0.5.0.dist-info/licenses/LICENSE +201 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-LICENSE +222 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-NOTICE +5 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ZRENDER-LICENSE +29 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""Remote task references and an optional fzf picker; no model calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import errno
|
|
7
|
+
import hashlib
|
|
8
|
+
import importlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import shlex
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import tomllib
|
|
18
|
+
from collections.abc import Coroutine
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from urllib.parse import urlsplit
|
|
21
|
+
|
|
22
|
+
import yaml
|
|
23
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, TypeAdapter, model_validator
|
|
24
|
+
|
|
25
|
+
from pluginbench.harbor_backend import native_task_assets as task_fingerprint
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _CatalogMetadata(BaseModel):
|
|
29
|
+
name: str
|
|
30
|
+
version: str
|
|
31
|
+
task_ids: list[dict[str, str]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _DownloadedTask(BaseModel):
|
|
35
|
+
path: Path
|
|
36
|
+
resolved_git_commit_id: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _error_messages(error: Exception) -> str:
|
|
40
|
+
if isinstance(error, ExceptionGroup):
|
|
41
|
+
return "; ".join(_error_messages(child) for child in error.exceptions)
|
|
42
|
+
return f"{type(error).__name__}: {error}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _harbor_response(request: Coroutine[object, object, object]) -> BaseModel:
|
|
46
|
+
try:
|
|
47
|
+
response = asyncio.run(request)
|
|
48
|
+
except Exception as error:
|
|
49
|
+
raise ValueError(f"Harbor task lookup/download failed: {_error_messages(error)}") from error
|
|
50
|
+
if not isinstance(response, BaseModel):
|
|
51
|
+
raise ValueError("Harbor returned an invalid task response")
|
|
52
|
+
return response
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cache_root() -> Path:
|
|
56
|
+
return Path(os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache"))) / "pluginbench/tasks"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def github_task(url: str) -> tuple[str, str]:
|
|
60
|
+
parsed = urlsplit(url)
|
|
61
|
+
parts = parsed.path.strip("/").split("/")
|
|
62
|
+
if (
|
|
63
|
+
parsed.scheme != "https"
|
|
64
|
+
or parsed.netloc != "github.com"
|
|
65
|
+
or parsed.query
|
|
66
|
+
or parsed.fragment
|
|
67
|
+
or len(parts) != 6
|
|
68
|
+
or parts[2] != "tree"
|
|
69
|
+
or parts[4] != "tasks"
|
|
70
|
+
or any(part in {"", ".", ".."} for part in parts)
|
|
71
|
+
or any(
|
|
72
|
+
not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", parts[index]) for index in (0, 1, 3, 5)
|
|
73
|
+
)
|
|
74
|
+
):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"use a GitHub task-folder URL: https://github.com/owner/repo/tree/ref/tasks/id"
|
|
77
|
+
)
|
|
78
|
+
return parts[5], parts[3]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def dataset_catalog(
|
|
82
|
+
dataset: str, version: str | None = None
|
|
83
|
+
) -> tuple[str, str, list[dict[str, str]]]:
|
|
84
|
+
# Imported here, not at the top: importing the CLI and rebuilding reports must not load Harbor.
|
|
85
|
+
client = importlib.import_module("harbor.registry.client.package").PackageDatasetClient()
|
|
86
|
+
|
|
87
|
+
if version and "@" in dataset:
|
|
88
|
+
raise ValueError("provide the dataset version once")
|
|
89
|
+
response = _harbor_response(
|
|
90
|
+
client.get_dataset_metadata(f"{dataset}@{version}" if version else dataset)
|
|
91
|
+
)
|
|
92
|
+
metadata = _CatalogMetadata.model_validate_json(response.model_dump_json())
|
|
93
|
+
if not metadata.version or not re.fullmatch(r"sha256:[0-9a-f]{64}", metadata.version):
|
|
94
|
+
raise ValueError("Harbor dataset did not resolve to an immutable version")
|
|
95
|
+
references = []
|
|
96
|
+
for task in metadata.task_ids:
|
|
97
|
+
if (
|
|
98
|
+
set(task) != {"org", "name", "ref"}
|
|
99
|
+
or not re.fullmatch(r"sha256:[0-9a-f]{64}", task["ref"])
|
|
100
|
+
or any(
|
|
101
|
+
not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", task[key])
|
|
102
|
+
for key in ("org", "name")
|
|
103
|
+
)
|
|
104
|
+
):
|
|
105
|
+
raise ValueError("dataset contains an unsupported or unresolved task reference")
|
|
106
|
+
references.append(task)
|
|
107
|
+
return metadata.name, metadata.version, references
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def download_tasks(references: list[dict[str, str]]) -> list[tuple[Path, dict[str, str]]]:
|
|
111
|
+
destination = _snapshot_destination(references)
|
|
112
|
+
if destination.exists():
|
|
113
|
+
_validate_snapshot(destination, references)
|
|
114
|
+
return [(destination / "tasks" / task_name(ref), ref) for ref in references]
|
|
115
|
+
ids = importlib.import_module("harbor.models.task.id")
|
|
116
|
+
client = importlib.import_module("harbor.tasks.client").TaskClient()
|
|
117
|
+
|
|
118
|
+
tasks = [
|
|
119
|
+
ids.PackageTaskId.model_validate(ref) if "org" in ref else ids.GitTaskId.model_validate(ref)
|
|
120
|
+
for ref in references
|
|
121
|
+
]
|
|
122
|
+
# Harbor trusts existing cache directories without hashing them. Import fresh bytes once;
|
|
123
|
+
# later reads use PluginBench's own complete asset fingerprint and origin receipt.
|
|
124
|
+
cache_root().mkdir(parents=True, exist_ok=True)
|
|
125
|
+
with tempfile.TemporaryDirectory(prefix=".download-", dir=cache_root()) as temporary:
|
|
126
|
+
result = _harbor_response(
|
|
127
|
+
client.download_tasks(tasks, output_dir=Path(temporary), overwrite=True)
|
|
128
|
+
)
|
|
129
|
+
receipts = TypeAdapter(list[_DownloadedTask]).validate_python(
|
|
130
|
+
result.model_dump(mode="json")["results"]
|
|
131
|
+
)
|
|
132
|
+
resolved = []
|
|
133
|
+
for ref, downloaded in zip(references, receipts, strict=True):
|
|
134
|
+
if "git_url" in ref:
|
|
135
|
+
sha = downloaded.resolved_git_commit_id
|
|
136
|
+
if not sha or not re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", sha):
|
|
137
|
+
raise ValueError("Git task did not resolve to an immutable commit")
|
|
138
|
+
ref = {**ref, "git_commit_id": sha}
|
|
139
|
+
resolved.append((downloaded.path, ref))
|
|
140
|
+
destination = materialize_tasks(resolved)
|
|
141
|
+
return [(destination / "tasks" / task_name(ref), ref) for _, ref in resolved]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def task_name(reference: dict[str, str]) -> str:
|
|
145
|
+
return reference["name"] if "org" in reference else Path(reference["path"]).name
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _snapshot_destination(references: list[dict[str, str]]) -> Path:
|
|
149
|
+
identity = hashlib.sha256(json.dumps(references, sort_keys=True).encode()).hexdigest()
|
|
150
|
+
return cache_root() / "native-snapshots" / identity
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _validate_snapshot(destination: Path, references: list[dict[str, str]]) -> None:
|
|
154
|
+
if destination.is_symlink() or any(
|
|
155
|
+
(destination / name).is_symlink() for name in (".pluginbench-source.json", "tasks")
|
|
156
|
+
):
|
|
157
|
+
raise ValueError("cached task provenance cannot follow symlinks")
|
|
158
|
+
provenance = json.loads((destination / ".pluginbench-source.json").read_text())
|
|
159
|
+
if provenance["references"] != references:
|
|
160
|
+
raise ValueError("cached task references changed")
|
|
161
|
+
names = {task_name(ref) for ref in references}
|
|
162
|
+
if set(provenance["task_hashes"]) != names:
|
|
163
|
+
raise ValueError("cached task inventory changed")
|
|
164
|
+
for name in names:
|
|
165
|
+
if task_fingerprint(destination / "tasks" / name) != provenance["task_hashes"][name]:
|
|
166
|
+
raise ValueError(f"cached task assets changed: {name}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def materialize_tasks(downloads: list[tuple[Path, dict[str, str]]]) -> Path:
|
|
170
|
+
"""Publish full task assets atomically and revalidate every later cache read."""
|
|
171
|
+
references = [ref for _, ref in downloads]
|
|
172
|
+
destination = _snapshot_destination(references)
|
|
173
|
+
parent = destination.parent
|
|
174
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
if destination.is_symlink():
|
|
176
|
+
raise ValueError("task snapshot cache cannot follow symlinks")
|
|
177
|
+
if destination.exists():
|
|
178
|
+
_validate_snapshot(destination, references)
|
|
179
|
+
return destination
|
|
180
|
+
with tempfile.TemporaryDirectory(prefix=".snapshot-", dir=parent) as temporary:
|
|
181
|
+
staging = Path(temporary) / "source"
|
|
182
|
+
(staging / "tasks").mkdir(parents=True)
|
|
183
|
+
fingerprints = {}
|
|
184
|
+
for source, ref in downloads:
|
|
185
|
+
name = task_name(ref)
|
|
186
|
+
fingerprint = task_fingerprint(source)
|
|
187
|
+
importlib.import_module("harbor.models.task.task").Task(source)
|
|
188
|
+
target = staging / "tasks" / name
|
|
189
|
+
shutil.copytree(source, target, symlinks=True)
|
|
190
|
+
if task_fingerprint(target) != fingerprint or task_fingerprint(source) != fingerprint:
|
|
191
|
+
raise ValueError("task assets changed while freezing input")
|
|
192
|
+
fingerprints[name] = fingerprint
|
|
193
|
+
(staging / ".pluginbench-source.json").write_text(
|
|
194
|
+
json.dumps({"references": references, "task_hashes": fingerprints}, indent=2) + "\n"
|
|
195
|
+
)
|
|
196
|
+
try:
|
|
197
|
+
staging.rename(destination)
|
|
198
|
+
except OSError as error:
|
|
199
|
+
if error.errno not in {errno.EEXIST, errno.ENOTEMPTY}:
|
|
200
|
+
raise
|
|
201
|
+
# Another picker/run may have published the same immutable selection.
|
|
202
|
+
return materialize_tasks(downloads)
|
|
203
|
+
return destination
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class Tasks(BaseModel):
|
|
207
|
+
model_config = ConfigDict(extra="forbid")
|
|
208
|
+
source: Path | None = Field(
|
|
209
|
+
default=None,
|
|
210
|
+
description="Selects a local task directory instead of a dataset or GitHub URLs.",
|
|
211
|
+
)
|
|
212
|
+
dataset: str | None = Field(
|
|
213
|
+
default=None,
|
|
214
|
+
description="Selects a Harbor dataset instead of a local source or GitHub URLs.",
|
|
215
|
+
)
|
|
216
|
+
version: str | None = Field(
|
|
217
|
+
default=None,
|
|
218
|
+
description="Selects the dataset version, which resolves to an immutable content hash.",
|
|
219
|
+
)
|
|
220
|
+
ids: list[str] = Field(
|
|
221
|
+
default=[], description="Selects unique task IDs from the local source or dataset."
|
|
222
|
+
)
|
|
223
|
+
urls: list[str] = Field(
|
|
224
|
+
default=[],
|
|
225
|
+
description="Selects unique GitHub task-folder URLs whose refs resolve to commits.",
|
|
226
|
+
)
|
|
227
|
+
_resolved: Path | None = PrivateAttr(default=None)
|
|
228
|
+
_references: list[dict[str, str]] = PrivateAttr(default_factory=list)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def task_ids(self) -> list[str]:
|
|
232
|
+
return [github_task(url)[0] for url in self.urls] or [
|
|
233
|
+
value.split("/")[-1] for value in self.ids
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
@model_validator(mode="after")
|
|
237
|
+
def coherent(self) -> Tasks:
|
|
238
|
+
if sum((self.source is not None, self.dataset is not None, bool(self.urls))) != 1:
|
|
239
|
+
raise ValueError("choose exactly one task source: source, dataset, or urls")
|
|
240
|
+
if self.urls and self.ids or self.version is not None and self.dataset is None:
|
|
241
|
+
raise ValueError("ids require source/dataset; version requires dataset")
|
|
242
|
+
if not self.task_ids or len(set(self.task_ids)) != len(self.task_ids):
|
|
243
|
+
raise ValueError("task IDs must be nonempty and unique")
|
|
244
|
+
if any(
|
|
245
|
+
not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*(?:/[A-Za-z0-9][A-Za-z0-9_.-]*)?", value)
|
|
246
|
+
for value in self.ids
|
|
247
|
+
):
|
|
248
|
+
raise ValueError("invalid task ID")
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def resolve(self) -> Path:
|
|
252
|
+
if self.source is not None:
|
|
253
|
+
return self.source
|
|
254
|
+
if self._resolved is not None:
|
|
255
|
+
# Revalidate published source bytes, even within one process.
|
|
256
|
+
return materialize_tasks(
|
|
257
|
+
[(self._resolved / "tasks" / task_name(ref), ref) for ref in self._references]
|
|
258
|
+
)
|
|
259
|
+
references = []
|
|
260
|
+
if self.dataset is not None:
|
|
261
|
+
self.dataset, self.version, catalog = dataset_catalog(self.dataset, self.version)
|
|
262
|
+
for selected in self.ids:
|
|
263
|
+
matches = [
|
|
264
|
+
ref
|
|
265
|
+
for ref in catalog
|
|
266
|
+
if selected == f"{ref['org']}/{ref['name']}" or selected == ref["name"]
|
|
267
|
+
]
|
|
268
|
+
if len(matches) != 1:
|
|
269
|
+
raise ValueError(f"task is missing or ambiguous in dataset: {selected}")
|
|
270
|
+
references.append(matches[0])
|
|
271
|
+
else:
|
|
272
|
+
resolver = importlib.import_module(
|
|
273
|
+
"harbor.registry.client.git_repo"
|
|
274
|
+
).resolve_repo_source
|
|
275
|
+
|
|
276
|
+
for url in self.urls:
|
|
277
|
+
github_task(url)
|
|
278
|
+
repo = resolver(url)
|
|
279
|
+
references.append(
|
|
280
|
+
{
|
|
281
|
+
"git_url": repo.git_url,
|
|
282
|
+
"git_commit_id": repo.ref or "HEAD",
|
|
283
|
+
"path": repo.subdir or "",
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
downloads = download_tasks(references)
|
|
287
|
+
self._resolved = materialize_tasks(downloads)
|
|
288
|
+
self._references = [ref for _, ref in downloads]
|
|
289
|
+
if self.urls:
|
|
290
|
+
self.urls = [
|
|
291
|
+
f"{ref['git_url'].removesuffix('.git')}/tree/{ref['git_commit_id']}/{ref['path']}"
|
|
292
|
+
for ref in self._references
|
|
293
|
+
]
|
|
294
|
+
return self._resolved
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def references(self) -> list[dict[str, str]]:
|
|
298
|
+
return self._references
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def printable(text: str) -> str:
|
|
302
|
+
return "".join(char for char in text if char.isprintable() or char in "\n\t")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def preview_task(packet: Path, index: int) -> str:
|
|
306
|
+
references = json.loads(packet.read_text())
|
|
307
|
+
ref = references[index]
|
|
308
|
+
source, _ = download_tasks([ref])[0]
|
|
309
|
+
if (
|
|
310
|
+
source.is_symlink()
|
|
311
|
+
or any(file.is_symlink() for file in source.rglob("*"))
|
|
312
|
+
or any(not (source / name).is_file() for name in ("task.toml", "instruction.md"))
|
|
313
|
+
):
|
|
314
|
+
raise ValueError("task preview cannot follow symlinks")
|
|
315
|
+
definition = tomllib.loads((source / "task.toml").read_text())
|
|
316
|
+
metadata = definition.get("metadata", {})
|
|
317
|
+
name = task_name(ref)
|
|
318
|
+
return printable(
|
|
319
|
+
f"{name}\nNative Harbor task; selected measurements are checked before evaluation\n"
|
|
320
|
+
f"Language: {metadata.get('language', 'unspecified')}\n"
|
|
321
|
+
f"Repository: {metadata.get('repository_url', 'unspecified')}\n\n"
|
|
322
|
+
+ (source / "instruction.md").read_text()
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def pick_tasks(dataset: str, version: str | None = None) -> str | None:
|
|
327
|
+
fzf = shutil.which("fzf")
|
|
328
|
+
if fzf is None:
|
|
329
|
+
raise ValueError("tasks pick requires fzf; install fzf or specify tasks in your config")
|
|
330
|
+
if not sys.stderr.isatty():
|
|
331
|
+
raise ValueError("tasks pick requires an interactive terminal")
|
|
332
|
+
name, pin, references = dataset_catalog(dataset, version)
|
|
333
|
+
lines = [f"{index}\t{ref['org']}/{ref['name']}" for index, ref in enumerate(references)]
|
|
334
|
+
with tempfile.TemporaryDirectory(prefix="pluginbench-picker-") as temporary:
|
|
335
|
+
packet = Path(temporary) / "catalog.json"
|
|
336
|
+
packet.write_text(json.dumps(references))
|
|
337
|
+
preview = (
|
|
338
|
+
" ".join(
|
|
339
|
+
shlex.quote(value)
|
|
340
|
+
for value in (sys.executable, "-m", "pluginbench.task_sources", str(packet))
|
|
341
|
+
)
|
|
342
|
+
+ " {1}"
|
|
343
|
+
)
|
|
344
|
+
result = subprocess.run(
|
|
345
|
+
[
|
|
346
|
+
fzf,
|
|
347
|
+
"--multi",
|
|
348
|
+
"--layout=reverse",
|
|
349
|
+
"--delimiter=\t",
|
|
350
|
+
"--with-nth=2..",
|
|
351
|
+
"--prompt=Tasks > ",
|
|
352
|
+
"--header=Tab select | Enter YAML | Esc cancel",
|
|
353
|
+
"--preview",
|
|
354
|
+
preview,
|
|
355
|
+
"--preview-window=right,50%,wrap",
|
|
356
|
+
],
|
|
357
|
+
input="\n".join(lines),
|
|
358
|
+
stdout=subprocess.PIPE,
|
|
359
|
+
text=True,
|
|
360
|
+
check=False,
|
|
361
|
+
)
|
|
362
|
+
if result.returncode in (1, 130):
|
|
363
|
+
return None
|
|
364
|
+
if result.returncode:
|
|
365
|
+
raise ValueError(f"fzf failed with exit code {result.returncode}")
|
|
366
|
+
chosen = result.stdout.splitlines()
|
|
367
|
+
if not chosen:
|
|
368
|
+
return None
|
|
369
|
+
if len(set(chosen)) != len(chosen) or any(line not in lines for line in chosen):
|
|
370
|
+
raise ValueError("fzf returned an invalid task selection")
|
|
371
|
+
selected = [references[int(line.split("\t", 1)[0])] for line in chosen]
|
|
372
|
+
tasks = Tasks(
|
|
373
|
+
dataset=name, version=pin, ids=[f"{ref['org']}/{ref['name']}" for ref in selected]
|
|
374
|
+
)
|
|
375
|
+
# Freeze selected task assets without resolving the dataset twice.
|
|
376
|
+
materialize_tasks(download_tasks(selected))
|
|
377
|
+
return yaml.safe_dump({"tasks": tasks.model_dump(exclude={"source", "urls"})}, sort_keys=False)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def copy_yaml(block: str) -> None:
|
|
381
|
+
candidates = (
|
|
382
|
+
[("pbcopy", [])]
|
|
383
|
+
if sys.platform == "darwin"
|
|
384
|
+
else [
|
|
385
|
+
("wl-copy", []),
|
|
386
|
+
("xclip", ["-selection", "clipboard"]),
|
|
387
|
+
("xsel", ["--clipboard", "--input"]),
|
|
388
|
+
]
|
|
389
|
+
)
|
|
390
|
+
for name, arguments in candidates:
|
|
391
|
+
executable = shutil.which(name)
|
|
392
|
+
if executable:
|
|
393
|
+
subprocess.run([executable, *arguments], input=block, text=True, check=True)
|
|
394
|
+
return
|
|
395
|
+
raise ValueError("no clipboard command available; the tasks YAML was printed to stdout")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
if __name__ == "__main__":
|
|
399
|
+
try:
|
|
400
|
+
print(preview_task(Path(sys.argv[1]), int(sys.argv[2])))
|
|
401
|
+
except (OSError, ValueError, ImportError) as error:
|
|
402
|
+
print(f"Preview unavailable: {error}", file=sys.stderr)
|
|
403
|
+
raise SystemExit(2) from error
|