trap-cli 0.0.1__tar.gz → 0.0.2__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.
- {trap_cli-0.0.1 → trap_cli-0.0.2}/PKG-INFO +1 -1
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/_version.py +2 -2
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cli/__init__.py +68 -9
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/local.py +29 -16
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/provenance.py +3 -1
- {trap_cli-0.0.1 → trap_cli-0.0.2}/.gitignore +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/LICENSE +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/README.md +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/examples/echo/task/README.md +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/pyproject.toml +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/__init__.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/auth/__init__.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/auth/client.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/auth/login.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/auth/oauth.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/auth/store.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cli/_auth.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cost/__init__.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cost/calculator.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cost/providers.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/cost/proxy.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/display/__init__.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/display/progress.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/display/submit.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/environment/__init__.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/environment/detector.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/__init__.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/base.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/remote.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/rev.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/git_ops/url.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/loader/__init__.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/loader/errors.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/loader/trap_yaml.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/loader/traptask_yaml.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/__init__.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/cost.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/environment.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/report.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/results.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/trap_yaml.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/models/traptask_yaml.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/report/__init__.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/report/base.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/report/handle.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/report/json.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/report/rich.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/__init__.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/capture.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/grader.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/judge.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/layout.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/proc.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/solution.py +0 -0
- {trap_cli-0.0.1 → trap_cli-0.0.2}/src/trap/runner/task.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trap-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: tp — non-invasive CLI testing framework for AI workflows. Submits results to trapstreet.run.
|
|
5
5
|
Project-URL: Homepage, https://trapstreet.run
|
|
6
6
|
Project-URL: Repository, https://github.com/trapstreet/trap
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
21
|
+
__version__ = version = '0.0.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -23,6 +23,8 @@ from trap.runner import TaskRunner
|
|
|
23
23
|
app = typer.Typer(help="AI prompt / agent / workflow / testing framework.")
|
|
24
24
|
app.add_typer(auth_app, name="auth")
|
|
25
25
|
console = Console()
|
|
26
|
+
# Warnings go to stderr so `-o json` stdout stays machine-parseable.
|
|
27
|
+
err_console = Console(stderr=True)
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def _die(msg: object) -> typer.Exit:
|
|
@@ -42,7 +44,7 @@ def _confirm_remote(url: str, *, trust: bool) -> None:
|
|
|
42
44
|
runs); refuses (rather than running silently) when there's no TTY to confirm at."""
|
|
43
45
|
if trust:
|
|
44
46
|
return
|
|
45
|
-
|
|
47
|
+
err_console.print(
|
|
46
48
|
"[yellow]⚠ about to download and RUN code from a remote source:[/yellow]\n"
|
|
47
49
|
f" [bold]{url}[/bold]\n"
|
|
48
50
|
" trap will execute its setup command, the solution, and any judge/grader\n"
|
|
@@ -57,6 +59,39 @@ def _confirm_remote(url: str, *, trust: bool) -> None:
|
|
|
57
59
|
raise typer.Exit(code=1)
|
|
58
60
|
|
|
59
61
|
|
|
62
|
+
def _confirm_unanchored(provenance: Provenance, *, allow: bool) -> None:
|
|
63
|
+
"""Gate run/submit when a side has no anchored git provenance.
|
|
64
|
+
|
|
65
|
+
trapstreet accepts such uploads but its leaderboard hides runs it cannot pin to
|
|
66
|
+
a commit — make the user acknowledge that up front instead of discovering it on
|
|
67
|
+
the site. The reason travels in the provenance's `issue` field, so `tp submit`
|
|
68
|
+
(which reads the saved report rather than re-probing the checkouts) names it
|
|
69
|
+
too. `allow` (--allow-unanchored / TRAP_ALLOW_UNANCHORED, for CI) keeps the
|
|
70
|
+
warning but skips the prompt; with no TTY and no `allow` it refuses.
|
|
71
|
+
"""
|
|
72
|
+
sides = {"solution": provenance.solution, "task": provenance.task}
|
|
73
|
+
missing = {name: side.issue for name, side in sides.items() if not side.repo}
|
|
74
|
+
if not missing:
|
|
75
|
+
return
|
|
76
|
+
for name, reason in missing.items():
|
|
77
|
+
suffix = f" ({reason})" if reason else ""
|
|
78
|
+
err_console.print(f"[yellow]⚠ {name} has no git provenance{suffix}[/yellow]")
|
|
79
|
+
err_console.print(
|
|
80
|
+
"[yellow] trapstreet accepts the submission, but the leaderboard hides runs that "
|
|
81
|
+
"aren't anchored to a commit on a remote — run from a clean, committed checkout "
|
|
82
|
+
"with an origin remote to make it rankable.[/yellow]"
|
|
83
|
+
)
|
|
84
|
+
if allow:
|
|
85
|
+
return
|
|
86
|
+
if not sys.stdin.isatty():
|
|
87
|
+
raise _die(
|
|
88
|
+
"unanchored provenance needs confirmation; pass --allow-unanchored "
|
|
89
|
+
"(or set TRAP_ALLOW_UNANCHORED=1) to proceed non-interactively"
|
|
90
|
+
)
|
|
91
|
+
if not typer.confirm("Continue anyway?", default=False):
|
|
92
|
+
raise typer.Exit(code=1)
|
|
93
|
+
|
|
94
|
+
|
|
60
95
|
@app.command()
|
|
61
96
|
def run(
|
|
62
97
|
task: Annotated[str | None, typer.Argument()] = None,
|
|
@@ -76,6 +111,14 @@ def run(
|
|
|
76
111
|
"(git+ URL) solution/task. Also settable via TRAP_TRUST_REMOTE=1.",
|
|
77
112
|
),
|
|
78
113
|
] = False,
|
|
114
|
+
allow_unanchored: Annotated[
|
|
115
|
+
bool,
|
|
116
|
+
typer.Option(
|
|
117
|
+
"--allow-unanchored",
|
|
118
|
+
help="Skip the confirmation when the run has no git provenance (the "
|
|
119
|
+
"leaderboard hides such runs). Also settable via TRAP_ALLOW_UNANCHORED=1.",
|
|
120
|
+
),
|
|
121
|
+
] = False,
|
|
79
122
|
tags: Annotated[list[str] | None, typer.Option("--tag", "-t")] = None,
|
|
80
123
|
output: Annotated[OutputFormat, typer.Option("--output", "-o")] = OutputFormat.rich,
|
|
81
124
|
fail_fast: Annotated[bool, typer.Option("--fail-fast")] = False,
|
|
@@ -138,6 +181,16 @@ def run(
|
|
|
138
181
|
except (GitOpsError, ConfigError, subprocess.CalledProcessError) as e:
|
|
139
182
|
raise _die(e) from None
|
|
140
183
|
|
|
184
|
+
# Record git provenance (repo + commit) of both checkouts — solution and task —
|
|
185
|
+
# so the run is reproducible; an unanchored side carries an `issue` naming why.
|
|
186
|
+
# Probed once, before any case runs: an unanchored checkout produces a report the
|
|
187
|
+
# leaderboard will hide, so the user must acknowledge (or pre-authorise) that.
|
|
188
|
+
provenance = Provenance(
|
|
189
|
+
solution=LocalRepo.provenance_of(trap_yaml_loader.trap_dir),
|
|
190
|
+
task=LocalRepo.provenance_of(traptask_yaml_loader.traptask_dir),
|
|
191
|
+
)
|
|
192
|
+
_confirm_unanchored(provenance, allow=allow_unanchored or _env_truthy("TRAP_ALLOW_UNANCHORED"))
|
|
193
|
+
|
|
141
194
|
active_cases = traptask_yaml_loader.cases_with_tags(tags or [])
|
|
142
195
|
|
|
143
196
|
started_at_local = datetime.now()
|
|
@@ -162,13 +215,6 @@ def run(
|
|
|
162
215
|
)
|
|
163
216
|
finished_at_utc = datetime.now(UTC)
|
|
164
217
|
|
|
165
|
-
# Record git provenance (repo + commit) of both checkouts — solution and task —
|
|
166
|
-
# so the run is reproducible. Each side is empty for a dirty/remote-less/local tree.
|
|
167
|
-
provenance = Provenance(
|
|
168
|
-
solution=LocalRepo.provenance_of(trap_yaml_loader.trap_dir),
|
|
169
|
-
task=LocalRepo.provenance_of(traptask_yaml_loader.traptask_dir),
|
|
170
|
-
)
|
|
171
|
-
|
|
172
218
|
# Capture the host machine environment (CPU/RAM/OS/Python) unless disabled.
|
|
173
219
|
# Detection is best-effort and must never abort a completed run.
|
|
174
220
|
environment_info = None
|
|
@@ -232,6 +278,14 @@ def submit(
|
|
|
232
278
|
] = None,
|
|
233
279
|
workspace: Annotated[Path, typer.Option("--workspace", "-w")] = Path(".trap"),
|
|
234
280
|
run: Annotated[str, typer.Option("--run", "-r", help="Which run to upload.")] = "latest",
|
|
281
|
+
allow_unanchored: Annotated[
|
|
282
|
+
bool,
|
|
283
|
+
typer.Option(
|
|
284
|
+
"--allow-unanchored",
|
|
285
|
+
help="Skip the confirmation when the run has no git provenance (the "
|
|
286
|
+
"leaderboard hides such runs). Also settable via TRAP_ALLOW_UNANCHORED=1.",
|
|
287
|
+
),
|
|
288
|
+
] = False,
|
|
235
289
|
) -> None:
|
|
236
290
|
"""Upload a report.json to trapstreet.
|
|
237
291
|
|
|
@@ -254,9 +308,14 @@ def submit(
|
|
|
254
308
|
raise _die(e) from None
|
|
255
309
|
report_handle = ReportHandle(workspace.resolve(), task_alias, run)
|
|
256
310
|
try:
|
|
257
|
-
report_handle.
|
|
311
|
+
report_data = report_handle.load()
|
|
258
312
|
except FileNotFoundError:
|
|
259
313
|
raise _die(f"no report at {report_handle.report_json_path}. Run [bold]tp run[/bold] first.") from None
|
|
314
|
+
# Repeat the unanchored-provenance gate at upload time — the report records
|
|
315
|
+
# what `tp run` saw, so the checkouts aren't re-probed here.
|
|
316
|
+
_confirm_unanchored(
|
|
317
|
+
report_data.provenance, allow=allow_unanchored or _env_truthy("TRAP_ALLOW_UNANCHORED")
|
|
318
|
+
)
|
|
260
319
|
report_path = report_handle.report_json_path
|
|
261
320
|
|
|
262
321
|
client = ApiClient(resolved_server, resolved_key)
|
|
@@ -17,14 +17,17 @@ class LocalRepo:
|
|
|
17
17
|
here, so the "open repo + read origin/commit/dirty" logic lives in one place.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
def __init__(self, repo: git.Repo) -> None:
|
|
20
|
+
def __init__(self, repo: git.Repo, path: Path) -> None:
|
|
21
21
|
self.repo = repo
|
|
22
|
+
# The path this checkout was opened at (may sit below the repo root when
|
|
23
|
+
# opened with search_parent).
|
|
24
|
+
self.path = path
|
|
22
25
|
|
|
23
26
|
@classmethod
|
|
24
27
|
def open(cls, path: Path, *, search_parent: bool = False) -> LocalRepo | None:
|
|
25
28
|
"""Open the git checkout at `path`, or None if it isn't a git repo."""
|
|
26
29
|
try:
|
|
27
|
-
return cls(git.Repo(path, search_parent_directories=search_parent))
|
|
30
|
+
return cls(git.Repo(path, search_parent_directories=search_parent), path)
|
|
28
31
|
except (git.InvalidGitRepositoryError, git.NoSuchPathError):
|
|
29
32
|
return None
|
|
30
33
|
|
|
@@ -36,25 +39,35 @@ class LocalRepo:
|
|
|
36
39
|
except AttributeError:
|
|
37
40
|
return None
|
|
38
41
|
|
|
39
|
-
def
|
|
40
|
-
"""{repo, commit}
|
|
42
|
+
def provenance_issue(self) -> str | None:
|
|
43
|
+
"""Why this checkout cannot be anchored to {repo, commit}, or None if it can.
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.trap/, .venv, …) don't count as dirty.
|
|
45
|
+
The run isn't reproducible from remote+commit alone when the repo has no
|
|
46
|
+
origin, no commit, or uncommitted tracked-file changes. Best-effort: a probe
|
|
47
|
+
failure degrades to a reason rather than raising. Untracked files (run
|
|
48
|
+
outputs under .trap/, .venv, …) don't count as dirty.
|
|
47
49
|
"""
|
|
48
50
|
try:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
if self.origin_normalised_url is None:
|
|
52
|
+
return "no origin remote"
|
|
53
|
+
if not self.repo.head.is_valid():
|
|
54
|
+
return "no commit to anchor to"
|
|
55
|
+
if self.repo.is_dirty():
|
|
56
|
+
return "uncommitted changes"
|
|
57
|
+
return None
|
|
53
58
|
except Exception:
|
|
54
|
-
return
|
|
59
|
+
return "git probe failed"
|
|
60
|
+
|
|
61
|
+
def provenance(self) -> GitProvenance:
|
|
62
|
+
"""{repo, commit} for a clean checkout with an origin, else empty with `issue`
|
|
63
|
+
naming why — we claim nothing about a run that isn't reproducible."""
|
|
64
|
+
issue = self.provenance_issue()
|
|
65
|
+
if issue is not None:
|
|
66
|
+
return GitProvenance(issue=issue)
|
|
67
|
+
return GitProvenance(repo=self.origin_normalised_url, commit=self.repo.head.commit.hexsha)
|
|
55
68
|
|
|
56
69
|
@classmethod
|
|
57
70
|
def provenance_of(cls, path: Path) -> GitProvenance:
|
|
58
|
-
"""Provenance ({repo, commit}) of the checkout at `path
|
|
71
|
+
"""Provenance ({repo, commit}) of the checkout at `path`."""
|
|
59
72
|
local_repo = cls.open(path, search_parent=True)
|
|
60
|
-
return local_repo.provenance() if local_repo else GitProvenance()
|
|
73
|
+
return local_repo.provenance() if local_repo else GitProvenance(issue="not a git repo")
|
|
@@ -5,10 +5,12 @@ from pydantic import BaseModel, Field
|
|
|
5
5
|
|
|
6
6
|
class GitProvenance(BaseModel):
|
|
7
7
|
"""Git origin of one checkout: {repo, commit}. Both None when the tree isn't a
|
|
8
|
-
clean, remote-backed git repo (see
|
|
8
|
+
clean, remote-backed git repo — `issue` then names why (see
|
|
9
|
+
LocalRepo.provenance_issue). Anchored checkouts carry no issue."""
|
|
9
10
|
|
|
10
11
|
repo: str | None = None
|
|
11
12
|
commit: str | None = None
|
|
13
|
+
issue: str | None = None
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class Provenance(BaseModel):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|