transcript-viewer 0.5.0__tar.gz → 0.6.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.
- transcript_viewer-0.5.0/README.md → transcript_viewer-0.6.0/PKG-INFO +31 -5
- transcript_viewer-0.5.0/PKG-INFO → transcript_viewer-0.6.0/README.md +18 -16
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/pyproject.toml +8 -15
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/src/transcript_viewer/cli.py +4 -3
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/src/transcript_viewer/corpus.py +36 -7
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/src/transcript_viewer/fetch.py +4 -1
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/src/transcript_viewer/page.html +21 -3
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/src/transcript_viewer/viewer.py +8 -7
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/page.test.js +27 -8
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_corpus.py +63 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_readme.py +24 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/uv.lock +54 -5
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/.coverage +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/.github/workflows/publish.yml +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/.github/workflows/test.yml +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/.gitignore +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/src/transcript_viewer/__init__.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/src/transcript_viewer/ai.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/src/transcript_viewer/config.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/src/transcript_viewer/library.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/src/transcript_viewer/store.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_ai.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_cli.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_config.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_fetch.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_library.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_page.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_store.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_stream.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_style.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_tidiness.py +0 -0
- {transcript_viewer-0.5.0 → transcript_viewer-0.6.0}/tests/test_viewer.py +0 -0
|
@@ -1,10 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: transcript-viewer
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Browse agent transcripts in a local, dependency-free web viewer.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: atif-make>=0.5.0
|
|
8
|
+
Provides-Extra: ai
|
|
9
|
+
Requires-Dist: anthropic>=0.40; extra == 'ai'
|
|
10
|
+
Provides-Extra: parquet
|
|
11
|
+
Requires-Dist: atif-make[parquet]>=0.5.0; extra == 'parquet'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
1
14
|
# transcript-viewer
|
|
2
15
|
|
|
3
|
-
Browse
|
|
4
|
-
|
|
16
|
+
Browse agent transcripts in a local web viewer — what Claude Code, Codex and
|
|
17
|
+
Copilot write as they work.
|
|
5
18
|
|
|
6
|
-
|
|
7
|
-
|
|
19
|
+
Logs are converted on the way in to
|
|
20
|
+
[ATIF v1.7](https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md)
|
|
21
|
+
by [`atif-make`](https://github.com/jammastergirish/atif-make), which this package depends on and which does
|
|
22
|
+
all of the reading. What is added here is only the browser interface, so a
|
|
23
|
+
format this cannot open is a parser missing from `atif-make` rather than
|
|
24
|
+
anything to change in the viewer.
|
|
8
25
|
|
|
9
26
|
## Install
|
|
10
27
|
|
|
@@ -137,6 +154,14 @@ Files are deleted only where they are the viewer's own copy under
|
|
|
137
154
|
`~/.transcript-viewer/opened/`; a session found on this machine, or downloaded into a folder
|
|
138
155
|
of yours, keeps its file.
|
|
139
156
|
|
|
157
|
+
Some datasets ship as a single file holding many runs — ATBench publishes a
|
|
158
|
+
thousand agent trajectories as one JSON array, METR's as Parquet shards
|
|
159
|
+
(install with `uv tool install "transcript-viewer[parquet]"` to read those). Those are opened rather than
|
|
160
|
+
listed: the file is split into a transcript apiece and each is indexed on its
|
|
161
|
+
own, so a download of one file becomes a thousand sessions you can read. The
|
|
162
|
+
pieces are kept beside what they came from, so removing the folder removes them
|
|
163
|
+
too.
|
|
164
|
+
|
|
140
165
|
This tool used to be called `atif-view` and kept all of that under `~/.atif`.
|
|
141
166
|
If that directory is still there, the first run moves it to
|
|
142
167
|
`~/.transcript-viewer` — index, annotations and stored keys together — and says
|
|
@@ -368,7 +393,7 @@ get.
|
|
|
368
393
|
```
|
|
369
394
|
src/transcript_viewer/
|
|
370
395
|
page.html the whole front end — 2,232 lines of HTML, CSS and JavaScript
|
|
371
|
-
viewer.py the HTTP server:
|
|
396
|
+
viewer.py the HTTP server: sixteen endpoints over the page and the library
|
|
372
397
|
corpus.py the index — what is on this machine, and where it came from
|
|
373
398
|
library.py what you decide about a session: title, tags, stars, summaries
|
|
374
399
|
fetch.py bringing transcripts in from Hugging Face, GitHub, S3 or a link
|
|
@@ -376,6 +401,7 @@ src/transcript_viewer/
|
|
|
376
401
|
config.py settings and tokens
|
|
377
402
|
store.py small JSON files under ~/.transcript-viewer, written so a crash cannot truncate them
|
|
378
403
|
cli.py the command line
|
|
404
|
+
page.html the whole interface: one page, no build step, no dependencies
|
|
379
405
|
```
|
|
380
406
|
|
|
381
407
|
The page is a file rather than a string inside `viewer.py`, which is where it
|
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: transcript-viewer
|
|
3
|
-
Version: 0.5.0
|
|
4
|
-
Summary: Browse agent transcripts in a local, dependency-free web viewer.
|
|
5
|
-
License: MIT
|
|
6
|
-
Requires-Python: >=3.12
|
|
7
|
-
Requires-Dist: atif-make>=0.2.0
|
|
8
|
-
Provides-Extra: ai
|
|
9
|
-
Requires-Dist: anthropic>=0.40; extra == 'ai'
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
|
|
12
1
|
# transcript-viewer
|
|
13
2
|
|
|
14
|
-
Browse
|
|
15
|
-
|
|
3
|
+
Browse agent transcripts in a local web viewer — what Claude Code, Codex and
|
|
4
|
+
Copilot write as they work.
|
|
16
5
|
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
Logs are converted on the way in to
|
|
7
|
+
[ATIF v1.7](https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md)
|
|
8
|
+
by [`atif-make`](https://github.com/jammastergirish/atif-make), which this package depends on and which does
|
|
9
|
+
all of the reading. What is added here is only the browser interface, so a
|
|
10
|
+
format this cannot open is a parser missing from `atif-make` rather than
|
|
11
|
+
anything to change in the viewer.
|
|
19
12
|
|
|
20
13
|
## Install
|
|
21
14
|
|
|
@@ -148,6 +141,14 @@ Files are deleted only where they are the viewer's own copy under
|
|
|
148
141
|
`~/.transcript-viewer/opened/`; a session found on this machine, or downloaded into a folder
|
|
149
142
|
of yours, keeps its file.
|
|
150
143
|
|
|
144
|
+
Some datasets ship as a single file holding many runs — ATBench publishes a
|
|
145
|
+
thousand agent trajectories as one JSON array, METR's as Parquet shards
|
|
146
|
+
(install with `uv tool install "transcript-viewer[parquet]"` to read those). Those are opened rather than
|
|
147
|
+
listed: the file is split into a transcript apiece and each is indexed on its
|
|
148
|
+
own, so a download of one file becomes a thousand sessions you can read. The
|
|
149
|
+
pieces are kept beside what they came from, so removing the folder removes them
|
|
150
|
+
too.
|
|
151
|
+
|
|
151
152
|
This tool used to be called `atif-view` and kept all of that under `~/.atif`.
|
|
152
153
|
If that directory is still there, the first run moves it to
|
|
153
154
|
`~/.transcript-viewer` — index, annotations and stored keys together — and says
|
|
@@ -379,7 +380,7 @@ get.
|
|
|
379
380
|
```
|
|
380
381
|
src/transcript_viewer/
|
|
381
382
|
page.html the whole front end — 2,232 lines of HTML, CSS and JavaScript
|
|
382
|
-
viewer.py the HTTP server:
|
|
383
|
+
viewer.py the HTTP server: sixteen endpoints over the page and the library
|
|
383
384
|
corpus.py the index — what is on this machine, and where it came from
|
|
384
385
|
library.py what you decide about a session: title, tags, stars, summaries
|
|
385
386
|
fetch.py bringing transcripts in from Hugging Face, GitHub, S3 or a link
|
|
@@ -387,6 +388,7 @@ src/transcript_viewer/
|
|
|
387
388
|
config.py settings and tokens
|
|
388
389
|
store.py small JSON files under ~/.transcript-viewer, written so a crash cannot truncate them
|
|
389
390
|
cli.py the command line
|
|
391
|
+
page.html the whole interface: one page, no build step, no dependencies
|
|
390
392
|
```
|
|
391
393
|
|
|
392
394
|
The page is a file rather than a string inside `viewer.py`, which is where it
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "transcript-viewer"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Browse agent transcripts in a local, dependency-free web viewer."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
license = { text = "MIT" }
|
|
8
8
|
# The only dependency is the converter. The viewer itself is stdlib http.server
|
|
9
9
|
# plus one self-contained HTML page.
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
10
|
+
# The floor is not politeness: this viewer imports atif_make.container, which
|
|
11
|
+
# is how a benchmark published as one JSON array of a thousand runs becomes a
|
|
12
|
+
# thousand sessions. An older atif-make fails to import at all.
|
|
13
|
+
dependencies = ["atif-make>=0.5.0"]
|
|
13
14
|
|
|
14
15
|
[project.optional-dependencies]
|
|
15
16
|
# Claude-backed explanations are opt-in, so the default install stays
|
|
16
17
|
# dependency-free: pip install "transcript-viewer[ai]"
|
|
18
|
+
# Datasets published as Parquet — METR's transcripts are the one so far. The
|
|
19
|
+
# reader lives in atif-make, which only needs it to split a shard.
|
|
20
|
+
parquet = ["atif-make[parquet]>=0.5.0"]
|
|
17
21
|
ai = ["anthropic>=0.40"]
|
|
18
22
|
|
|
19
23
|
[project.scripts]
|
|
@@ -22,17 +26,6 @@ transcript-viewer = "transcript_viewer.cli:main"
|
|
|
22
26
|
[dependency-groups]
|
|
23
27
|
dev = ["pytest>=8.0"]
|
|
24
28
|
|
|
25
|
-
# The two packages move together, so track atif-make's `main` rather than a
|
|
26
|
-
# release. Once atif-make is on PyPI you can delete this block entirely: the
|
|
27
|
-
# `dependencies` entry above already resolves from the index.
|
|
28
|
-
#
|
|
29
|
-
# uv still records an exact commit in uv.lock, which is what keeps a given
|
|
30
|
-
# checkout reproducible. To pick up newer atif-make commits, re-resolve:
|
|
31
|
-
#
|
|
32
|
-
# uv lock --upgrade-package atif-make
|
|
33
|
-
#
|
|
34
|
-
[tool.uv.sources]
|
|
35
|
-
atif-make = { git = "https://github.com/jammastergirish/atif-make", branch = "main" }
|
|
36
29
|
|
|
37
30
|
[build-system]
|
|
38
31
|
requires = ["hatchling"]
|
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from . import corpus, store
|
|
10
|
-
from atif_make.
|
|
10
|
+
from atif_make.container import is_container
|
|
11
11
|
|
|
12
12
|
from .viewer import serve
|
|
13
13
|
|
|
@@ -39,10 +39,11 @@ def cmd_view(args: argparse.Namespace) -> int:
|
|
|
39
39
|
if not path.exists():
|
|
40
40
|
print(f"transcript-viewer: no such path: {path}", file=sys.stderr)
|
|
41
41
|
return 2
|
|
42
|
-
# A directory or
|
|
42
|
+
# A directory or a container holds many sessions; a plain file
|
|
43
|
+
# holds one.
|
|
43
44
|
entries = (
|
|
44
45
|
corpus.scan([path])
|
|
45
|
-
if path.is_dir() or
|
|
46
|
+
if path.is_dir() or is_container(path)
|
|
46
47
|
else _single_entry(path)
|
|
47
48
|
)
|
|
48
49
|
# An explicit path that holds nothing is a mistake worth reporting; an
|
|
@@ -16,7 +16,7 @@ from dataclasses import fields as dataclass_fields
|
|
|
16
16
|
from datetime import datetime, timezone
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
-
from atif_make.
|
|
19
|
+
from atif_make.container import is_container, open_container
|
|
20
20
|
|
|
21
21
|
from . import store
|
|
22
22
|
from atif_make.convert import AGENTS
|
|
@@ -32,6 +32,13 @@ INDEX_PATH = store.ROOT / "index.json"
|
|
|
32
32
|
# whoever asks, so a plain rescan classifies them correctly on its own.
|
|
33
33
|
OPENED_ROOT = store.ROOT / "opened"
|
|
34
34
|
|
|
35
|
+
# Where a collection's pieces are kept. A benchmark published as one JSON array
|
|
36
|
+
# becomes a transcript per file, and those files are what the index records, so
|
|
37
|
+
# they need somewhere that outlives the process that made them. Keyed by the
|
|
38
|
+
# source's content, so re-indexing the same download reuses the split and a
|
|
39
|
+
# changed file gets a new one.
|
|
40
|
+
SPLIT_ROOT = store.ROOT / "split"
|
|
41
|
+
|
|
35
42
|
# A first line shorter than this is structural rather than identifying — a
|
|
36
43
|
# pretty-printed JSON document opens with a bare "{".
|
|
37
44
|
|
|
@@ -251,6 +258,20 @@ def merge(existing: list[Entry], new: list[Entry]) -> list[Entry]:
|
|
|
251
258
|
return sorted(out, key=lambda e: e.modified, reverse=True)
|
|
252
259
|
|
|
253
260
|
|
|
261
|
+
def _split_home(path: Path) -> Path:
|
|
262
|
+
"""Where this file's pieces belong, if it turns out to hold many.
|
|
263
|
+
|
|
264
|
+
A download is unpacked beside itself on arrival, so prefer that: the pieces
|
|
265
|
+
are already there, they sit with the thing they came from, and they go when
|
|
266
|
+
the folder does. Anything else — a file being scanned where writing beside
|
|
267
|
+
it would be rude — goes under the viewer's own directory instead.
|
|
268
|
+
"""
|
|
269
|
+
beside = path.with_suffix("")
|
|
270
|
+
if beside.is_dir() and any(beside.iterdir()):
|
|
271
|
+
return beside
|
|
272
|
+
return SPLIT_ROOT / content_key(path)[:16]
|
|
273
|
+
|
|
274
|
+
|
|
254
275
|
def scan(roots: list[Path] | None = None, origin: str = "scanned") -> list[Entry]:
|
|
255
276
|
"""Find every convertible log under ``roots``. A root may be a single file."""
|
|
256
277
|
entries: list[Entry] = []
|
|
@@ -260,10 +281,11 @@ def scan(roots: list[Path] | None = None, origin: str = "scanned") -> list[Entry
|
|
|
260
281
|
if not root.exists():
|
|
261
282
|
continue
|
|
262
283
|
if root.is_file():
|
|
263
|
-
# A zip
|
|
264
|
-
|
|
284
|
+
# A zip, a tarball, or a benchmark published as one JSON array of a
|
|
285
|
+
# thousand runs: a container of logs, not a log.
|
|
286
|
+
if is_container(root):
|
|
265
287
|
try:
|
|
266
|
-
root =
|
|
288
|
+
root = open_container(root, _split_home(root))
|
|
267
289
|
except (ValueError, OSError):
|
|
268
290
|
continue
|
|
269
291
|
else:
|
|
@@ -275,11 +297,11 @@ def scan(roots: list[Path] | None = None, origin: str = "scanned") -> list[Entry
|
|
|
275
297
|
# bucket of agent runs is mostly zips — so look inside them too. Each is
|
|
276
298
|
# unpacked once and its contents scanned in place.
|
|
277
299
|
roots_here = [root]
|
|
278
|
-
for
|
|
279
|
-
if not
|
|
300
|
+
for item in sorted(root.rglob("*")):
|
|
301
|
+
if not item.is_file() or not is_container(item):
|
|
280
302
|
continue
|
|
281
303
|
try:
|
|
282
|
-
roots_here.append(
|
|
304
|
+
roots_here.append(open_container(item, _split_home(item)))
|
|
283
305
|
except (ValueError, OSError):
|
|
284
306
|
continue
|
|
285
307
|
|
|
@@ -289,11 +311,18 @@ def scan(roots: list[Path] | None = None, origin: str = "scanned") -> list[Entry
|
|
|
289
311
|
sorted(where.rglob("*.jsonl"))
|
|
290
312
|
+ sorted(where.rglob("*.har"))
|
|
291
313
|
+ sorted(where.rglob("*.json"))
|
|
314
|
+
# Not a log, but a container of them, opened above.
|
|
315
|
+
+ sorted(where.rglob("*.parquet"))
|
|
292
316
|
)
|
|
293
317
|
for path in candidates:
|
|
294
318
|
resolved = path.resolve()
|
|
295
319
|
if resolved in seen:
|
|
296
320
|
continue
|
|
321
|
+
# A container was opened above and its contents are already in this
|
|
322
|
+
# list. Indexing it as well would offer a session that cannot be
|
|
323
|
+
# opened, since it holds a thousand transcripts rather than one.
|
|
324
|
+
if is_container(path):
|
|
325
|
+
continue
|
|
297
326
|
# Subagent traces are reached through their parent, not indexed alone.
|
|
298
327
|
if path.parent.name == "subagents":
|
|
299
328
|
continue
|
|
@@ -59,8 +59,11 @@ HOSTS = {
|
|
|
59
59
|
# session than loose JSONL, so excluding archives made S3 useless for exactly
|
|
60
60
|
# the case it was added for.
|
|
61
61
|
LOGS = (".jsonl", ".json", ".har")
|
|
62
|
+
# A published dataset is a container of runs rather than a log, but it is still
|
|
63
|
+
# a thing worth fetching: METR's transcripts come as Parquet shards.
|
|
64
|
+
DATASETS = (".parquet",)
|
|
62
65
|
ARCHIVES = (".zip", ".tgz", ".tar", ".tar.gz", ".tar.bz2", ".tar.xz", ".gz")
|
|
63
|
-
SUFFIXES = LOGS + ARCHIVES
|
|
66
|
+
SUFFIXES = LOGS + DATASETS + ARCHIVES
|
|
64
67
|
|
|
65
68
|
# Downloads land beside where the viewer was launched, so they are visible and
|
|
66
69
|
# usable by other tools rather than buried in a dot-directory.
|
|
@@ -1328,17 +1328,35 @@ const clearSecret=name=>settings({name,clear:true});
|
|
|
1328
1328
|
files, and nobody should find that out by pressing a button once. */
|
|
1329
1329
|
let PLANNED=null;
|
|
1330
1330
|
|
|
1331
|
+
/* Places that really do hold transcripts, checked rather than guessed: each was
|
|
1332
|
+
fetched and converted before it was listed. Gated ones are marked, since a
|
|
1333
|
+
token you have not accepted terms for fails at the door rather than here. */
|
|
1331
1334
|
const EXAMPLES=[
|
|
1335
|
+
{name:"ATBench",
|
|
1336
|
+
url:"https://huggingface.co/datasets/AI45Research/ATBench/tree/main/ATBench",
|
|
1337
|
+
note:"1,000 labelled agent trajectories"},
|
|
1338
|
+
{name:"ATBench-500",
|
|
1339
|
+
url:"https://huggingface.co/datasets/AI45Research/ATBench/tree/main/ATBench500",
|
|
1340
|
+
note:"the smaller config, 500 trajectories"},
|
|
1332
1341
|
{name:"SLEIGHT-Bench",
|
|
1333
|
-
url:"https://huggingface.co/datasets/sleightbench/SLEIGHT-Bench/tree/main/attacks"
|
|
1334
|
-
|
|
1342
|
+
url:"https://huggingface.co/datasets/sleightbench/SLEIGHT-Bench/tree/main/attacks",
|
|
1343
|
+
note:"gated — accept the terms on Hugging Face first"},
|
|
1344
|
+
{name:"METR MALT",
|
|
1345
|
+
url:"https://huggingface.co/datasets/metr-evals/malt-transcripts-public/tree/main/data",
|
|
1346
|
+
note:"gated — accept the terms first; shards are large, so pick one"},
|
|
1347
|
+
{name:"METR MALT (vague CoT)",
|
|
1348
|
+
url:"https://huggingface.co/datasets/metr-evals/malt-transcripts-public/tree/main/vague_cot",
|
|
1349
|
+
note:"the same runs with the chain of thought made vaguer"},
|
|
1350
|
+
{name:"Redwood agent transcripts",
|
|
1351
|
+
url:"s3://rr-agent-transcripts",
|
|
1352
|
+
note:"needs an AWS profile set in Settings"},
|
|
1335
1353
|
];
|
|
1336
1354
|
|
|
1337
1355
|
function drawExamples(){
|
|
1338
1356
|
const host=document.getElementById("egs");
|
|
1339
1357
|
if(!host)return;
|
|
1340
1358
|
host.innerHTML=`<span>Try:</span>`+EXAMPLES.map((e,i)=>
|
|
1341
|
-
`<button onclick="useExample(${i})" title="${esc(e.url)}">${esc(e.name)}</button>`
|
|
1359
|
+
`<button onclick="useExample(${i})" title="${esc(e.note?e.note+" — "+e.url:e.url)}">${esc(e.name)}</button>`
|
|
1342
1360
|
).join("");
|
|
1343
1361
|
}
|
|
1344
1362
|
|
|
@@ -26,7 +26,7 @@ from urllib.parse import parse_qs, unquote, urlparse
|
|
|
26
26
|
|
|
27
27
|
from atif_make.atif import ContentPart, Trajectory
|
|
28
28
|
from . import corpus
|
|
29
|
-
from atif_make.
|
|
29
|
+
from atif_make.container import is_container, open_container
|
|
30
30
|
from atif_make.convert import convert
|
|
31
31
|
from transcript_viewer.corpus import Entry, scan
|
|
32
32
|
|
|
@@ -560,12 +560,13 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
560
560
|
home = corpus.OPENED_ROOT / corpus.content_key(staged)
|
|
561
561
|
try:
|
|
562
562
|
home.mkdir(parents=True, exist_ok=True)
|
|
563
|
-
if
|
|
563
|
+
if is_container(staged):
|
|
564
564
|
# Keep what is inside, not the container: the logs are what get
|
|
565
565
|
# indexed, and unpacking here keeps their paths stable and any
|
|
566
|
-
# sibling images resolvable. Storing the zip
|
|
567
|
-
#
|
|
568
|
-
|
|
566
|
+
# sibling images resolvable. Storing the zip — or a benchmark's
|
|
567
|
+
# thousand-run JSON array — would mean opening it again on every
|
|
568
|
+
# start.
|
|
569
|
+
unpacked = open_container(staged)
|
|
569
570
|
for item in unpacked.iterdir():
|
|
570
571
|
shutil.move(str(item), home / item.name)
|
|
571
572
|
target = home
|
|
@@ -662,10 +663,10 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
662
663
|
# names the archive happens to use internally.
|
|
663
664
|
unpacked_from: dict[Path, Path] = {}
|
|
664
665
|
for item in sorted(home.rglob("*")):
|
|
665
|
-
if not item.is_file() or not
|
|
666
|
+
if not item.is_file() or not is_container(item):
|
|
666
667
|
continue
|
|
667
668
|
try:
|
|
668
|
-
unpacked =
|
|
669
|
+
unpacked = open_container(item)
|
|
669
670
|
except (ValueError, OSError):
|
|
670
671
|
continue
|
|
671
672
|
beside = item.with_suffix("")
|
|
@@ -1165,20 +1165,39 @@ test("the examples fill the field and look straight away", async () => {
|
|
|
1165
1165
|
globalThis.__calls.push(url);
|
|
1166
1166
|
return {ok:true,json:async()=>({nodes:[]})};
|
|
1167
1167
|
};
|
|
1168
|
-
|
|
1169
|
-
|
|
1168
|
+
const i = EXAMPLES.findIndex(e=>e.url.startsWith("s3://"));
|
|
1169
|
+
globalThis.__url = EXAMPLES[i].url;
|
|
1170
|
+
return useExample(i);`);
|
|
1171
|
+
assert.strictEqual(fields.urlin.value, globalThis.__url);
|
|
1170
1172
|
assert.ok(
|
|
1171
1173
|
calls.includes("/api/browse"),
|
|
1172
1174
|
"it filled the field but did not look",
|
|
1173
1175
|
);
|
|
1174
1176
|
});
|
|
1175
1177
|
|
|
1176
|
-
test("
|
|
1177
|
-
const
|
|
1178
|
-
assert.
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1178
|
+
test("every example names a place the fetcher can actually reach", () => {
|
|
1179
|
+
const egs = run(`return EXAMPLES;`);
|
|
1180
|
+
assert.ok(egs.length >= 2, "there should be more than one thing to try");
|
|
1181
|
+
for (const e of egs) {
|
|
1182
|
+
assert.ok(e.name, "an example without a name is a blank button");
|
|
1183
|
+
assert.match(
|
|
1184
|
+
e.url,
|
|
1185
|
+
/^(https:\/\/(huggingface\.co|github\.com)\/|s3:\/\/)/,
|
|
1186
|
+
`${e.name}: not a scheme the fetcher handles`,
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
// Both kinds are worth offering: one needs no credentials, one does.
|
|
1190
|
+
assert.ok(egs.some(e => e.url.startsWith("https://huggingface.co/")));
|
|
1191
|
+
assert.ok(egs.some(e => e.url.startsWith("s3://")));
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
test("an example that needs credentials says so", () => {
|
|
1195
|
+
const egs = run(`return EXAMPLES;`);
|
|
1196
|
+
for (const e of egs) {
|
|
1197
|
+
if (e.url.startsWith("s3://") || /sleightbench/.test(e.url)) {
|
|
1198
|
+
assert.ok(e.note, `${e.name} needs something first and does not say so`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1182
1201
|
});
|
|
1183
1202
|
|
|
1184
1203
|
test("a selection is measured rather than guessed at", async () => {
|
|
@@ -433,3 +433,66 @@ def test_only_claude_code_is_asked_for_a_name(tmp_path):
|
|
|
433
433
|
entry = corpus.describe(log)
|
|
434
434
|
assert entry.format.startswith("codex")
|
|
435
435
|
assert entry.session_title is None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _atbench_rows(n: int) -> list[dict]:
|
|
439
|
+
"""Rows shaped like the real dataset, enough to be recognised as one."""
|
|
440
|
+
return [
|
|
441
|
+
{
|
|
442
|
+
"id": i,
|
|
443
|
+
"label": i % 2,
|
|
444
|
+
"risk_source": "indirect_prompt_injection",
|
|
445
|
+
"failure_mode": "unauthorized_information_disclosure",
|
|
446
|
+
"tool_used": [{"name": "f", "description": "d", "parameters": {}}],
|
|
447
|
+
"contents": [[
|
|
448
|
+
{"role": "user", "content": f"do thing {i}"},
|
|
449
|
+
{"role": "agent", "thought": "", "action": 'Complete{"response": "done"}'},
|
|
450
|
+
]],
|
|
451
|
+
}
|
|
452
|
+
for i in range(1, n + 1)
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def test_a_file_of_many_transcripts_indexes_as_many(tmp_path):
|
|
457
|
+
"""A benchmark published as one JSON array is a container, not a session."""
|
|
458
|
+
source = tmp_path / "test.json"
|
|
459
|
+
source.write_text(json.dumps(_atbench_rows(5)))
|
|
460
|
+
|
|
461
|
+
entries = corpus.scan([source])
|
|
462
|
+
assert len(entries) == 5
|
|
463
|
+
assert {e.format for e in entries} == {"atbench"}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def test_the_container_itself_is_not_offered_as_a_session(tmp_path):
|
|
467
|
+
"""It cannot be opened — it holds five transcripts, not one."""
|
|
468
|
+
source = tmp_path / "test.json"
|
|
469
|
+
source.write_text(json.dumps(_atbench_rows(5)))
|
|
470
|
+
|
|
471
|
+
entries = corpus.scan([tmp_path])
|
|
472
|
+
assert len(entries) == 5
|
|
473
|
+
assert str(source) not in {e.path for e in entries}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def test_the_pieces_keep_the_same_paths_across_scans(tmp_path):
|
|
477
|
+
"""Paths are recorded in the index, so a scan must not move them."""
|
|
478
|
+
source = tmp_path / "test.json"
|
|
479
|
+
source.write_text(json.dumps(_atbench_rows(4)))
|
|
480
|
+
|
|
481
|
+
first = {e.path for e in corpus.scan([source])}
|
|
482
|
+
second = {e.path for e in corpus.scan([source])}
|
|
483
|
+
assert first == second
|
|
484
|
+
assert all(Path(p).exists() for p in first)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_pieces_unpacked_beside_a_download_are_used_as_they_are(tmp_path):
|
|
488
|
+
"""A fetch unpacks beside the file; scanning must not split it again."""
|
|
489
|
+
source = tmp_path / "test.json"
|
|
490
|
+
source.write_text(json.dumps(_atbench_rows(3)))
|
|
491
|
+
beside = tmp_path / "test"
|
|
492
|
+
beside.mkdir()
|
|
493
|
+
for i, row in enumerate(_atbench_rows(3), start=1):
|
|
494
|
+
(beside / f"test-{i:05d}-{i}.json").write_text(json.dumps([row]))
|
|
495
|
+
|
|
496
|
+
entries = corpus.scan([tmp_path])
|
|
497
|
+
assert len(entries) == 3
|
|
498
|
+
assert all(str(beside) in e.path for e in entries)
|
|
@@ -9,6 +9,7 @@ Deliberately shallow: presence of a marker, not a parse. A test that tried to
|
|
|
9
9
|
verify the prose itself would be a worse copy of the code.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import re
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
import pytest
|
|
@@ -138,3 +139,26 @@ def test_every_measured_number_is_labelled_as_measured():
|
|
|
138
139
|
"""Figures in the README came from running something, not from a guess."""
|
|
139
140
|
for figure in ["3.8 ms", "58 are sent entire", "202 of 202"]:
|
|
140
141
|
assert figure in README, f"a measured figure went missing: {figure}"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_the_endpoint_count_is_the_real_one():
|
|
145
|
+
"""A number in prose drifts silently; count it instead of trusting it."""
|
|
146
|
+
words = {
|
|
147
|
+
12: "twelve", 13: "thirteen", 14: "fourteen", 15: "fifteen",
|
|
148
|
+
16: "sixteen", 17: "seventeen", 18: "eighteen",
|
|
149
|
+
}
|
|
150
|
+
source = Path("src/transcript_viewer/viewer.py").read_text()
|
|
151
|
+
actual = len(set(re.findall(r'"(/api/[a-z_-]+)"', source)))
|
|
152
|
+
readme = Path("README.md").read_text()
|
|
153
|
+
assert f"{words[actual]} endpoints" in readme, (
|
|
154
|
+
f"the README does not say {words[actual]} endpoints, but there are {actual}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_every_module_appears_in_the_map():
|
|
159
|
+
"""A file nobody documented is a file nobody knows to look at."""
|
|
160
|
+
readme = Path("README.md").read_text()
|
|
161
|
+
for path in sorted(Path("src/transcript_viewer").iterdir()):
|
|
162
|
+
if path.name.startswith("_") or path.suffix not in {".py", ".html"}:
|
|
163
|
+
continue
|
|
164
|
+
assert f" {path.name}" in readme, f"{path.name} is missing from the module map"
|
|
@@ -44,8 +44,17 @@ wheels = [
|
|
|
44
44
|
|
|
45
45
|
[[package]]
|
|
46
46
|
name = "atif-make"
|
|
47
|
-
version = "0.
|
|
48
|
-
source = {
|
|
47
|
+
version = "0.5.0"
|
|
48
|
+
source = { registry = "https://pypi.org/simple" }
|
|
49
|
+
sdist = { url = "https://files.pythonhosted.org/packages/60/2c/9e5d1b1ba278fde5135b8066487e029dd0620161b6a220f5cb2a118a0333/atif_make-0.5.0.tar.gz", hash = "sha256:d4c3122a63b553099f0843d5d1c666136b0bccb1c11e8ebd2b4f9c4a9200cb5e", size = 215842, upload-time = "2026-08-23T05:15:04.439Z" }
|
|
50
|
+
wheels = [
|
|
51
|
+
{ url = "https://files.pythonhosted.org/packages/00/47/f9a09d10b3d53600c43fa1b22c1d01cb731e628b3572c2e40109de195cfb/atif_make-0.5.0-py3-none-any.whl", hash = "sha256:b52fac5489f5cbc34f947f5d8b21b51a6a04276420d908a96f938e1bf7443791", size = 49769, upload-time = "2026-08-23T05:15:03.265Z" },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[package.optional-dependencies]
|
|
55
|
+
parquet = [
|
|
56
|
+
{ name = "pyarrow" },
|
|
57
|
+
]
|
|
49
58
|
|
|
50
59
|
[[package]]
|
|
51
60
|
name = "colorama"
|
|
@@ -217,6 +226,42 @@ wheels = [
|
|
|
217
226
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
218
227
|
]
|
|
219
228
|
|
|
229
|
+
[[package]]
|
|
230
|
+
name = "pyarrow"
|
|
231
|
+
version = "25.0.1"
|
|
232
|
+
source = { registry = "https://pypi.org/simple" }
|
|
233
|
+
sdist = { url = "https://files.pythonhosted.org/packages/3d/e3/27f57f80141379d60defe6703eb50a707325706f07fedfd1312c7a751995/pyarrow-25.0.1.tar.gz", hash = "sha256:9150a83248bfed9813ea3c3af74c3856c1984d444aa28e58bf7733b9750ddf6a", size = 1201653, upload-time = "2026-08-10T12:40:53.904Z" }
|
|
234
|
+
wheels = [
|
|
235
|
+
{ url = "https://files.pythonhosted.org/packages/a6/e2/9ab15b88cbfac28e16419ce5439ec29234c5172cb8259301b4ba639bdec0/pyarrow-25.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:df961f2e7ae9cf496459259d798652c70625f6c080650d6952f8c04053c58ee9", size = 35861559, upload-time = "2026-08-10T12:38:02.567Z" },
|
|
236
|
+
{ url = "https://files.pythonhosted.org/packages/58/79/a0036dbe1eabe1f73127427342f1d99982584c4a2cde2651d6c93499c6f6/pyarrow-25.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:cc4aa407fde9fc660be3939e49ea31f50f3e9fec17c0ec63159f7711edd3efc9", size = 37628383, upload-time = "2026-08-10T12:38:09.083Z" },
|
|
237
|
+
{ url = "https://files.pythonhosted.org/packages/13/49/d93a57d375f4bf0cf82913dd6bb54acafde83dd993be2282c81ac5616cad/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4340f0ba6c1d2e13f21658de1d7c662ca2545018568d0030a1e9afca159d87e3", size = 46820190, upload-time = "2026-08-10T12:38:15.458Z" },
|
|
238
|
+
{ url = "https://files.pythonhosted.org/packages/60/c9/711ca85d79f1ec98f29a5eae2b051e25b4ecec5de3e3c0e2d5c5dcb15664/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5389cdf79447ed1515c9e31620e6e1e2302249564d603f2ad727d4f6d313e4c3", size = 50102437, upload-time = "2026-08-10T12:38:22.487Z" },
|
|
239
|
+
{ url = "https://files.pythonhosted.org/packages/80/53/8fb8359ff17cfb6263a1cf3ebf7caec9fe197de118719e84fcb1d0618026/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d51592cb7561e87877c506113e7adbf1342ab579e6c21f0ef44b8ba41cb74c80", size = 49942424, upload-time = "2026-08-10T12:38:28.755Z" },
|
|
240
|
+
{ url = "https://files.pythonhosted.org/packages/e8/83/4e5ae02a9341571b18a6fca380ac7a58ce6ddae7ab3c060208c0a1e79f02/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6109c94d8b9f3b17a041daca16cacb2f651ad8f1ef70a4232c2c0f37a23da2a8", size = 53144206, upload-time = "2026-08-10T12:38:34.862Z" },
|
|
241
|
+
{ url = "https://files.pythonhosted.org/packages/65/ee/197cbf47e49f83e6ebeb946a5259a48a638dea27ac774db42fe78022179d/pyarrow-25.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8858d7bfc22e3f51529aeaa4077225029724623e4595dc9eff8c793935c34140", size = 27953934, upload-time = "2026-08-10T12:38:39.808Z" },
|
|
242
|
+
{ url = "https://files.pythonhosted.org/packages/cc/8d/8f271a7a034c834910ec925d56fa4b29733b1380f5289419f5aaa3b02777/pyarrow-25.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c7c534ec03c358a76ea3e505e74c1b6aef290af90c444dfd092dbfe23e755b85", size = 35855328, upload-time = "2026-08-10T12:38:45.489Z" },
|
|
243
|
+
{ url = "https://files.pythonhosted.org/packages/d2/cd/5bac242f4e841b9971d5eb94fdfe2577e2b70be983e27401e72055786037/pyarrow-25.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:dda9470024204d7bbf2042b47c6e8a0e47a3eeb8e34405882dfaea6577e0c153", size = 37622415, upload-time = "2026-08-10T12:38:51.107Z" },
|
|
244
|
+
{ url = "https://files.pythonhosted.org/packages/63/1f/96d03b4e1506524f7087adb0fd6b2f69f0c9c7aaff1ec36d8030082e15a5/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:44a9120ce5bd81936b8ab9a88076e3fd47c2c6838e0e43630fed83626aca81d9", size = 46813813, upload-time = "2026-08-10T12:38:57.773Z" },
|
|
245
|
+
{ url = "https://files.pythonhosted.org/packages/98/d6/33a411115b61dbfc16ad6ad73e71730f6fea654ee3667673bc53ab0e2fe7/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0befcf816e45a1af33ac775a9970b749e4868a230c7372f0ae5e932bee27039f", size = 50104452, upload-time = "2026-08-10T12:39:04.579Z" },
|
|
246
|
+
{ url = "https://files.pythonhosted.org/packages/33/ae/b1b97c9ca87f9f9ddbb5230c798df94eccce61bd79b9b45458c69a478588/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f89685964f46e4216103c75483aac0c0692a5f72212d7ca835adba5ede56ce3", size = 49951343, upload-time = "2026-08-10T12:39:11.8Z" },
|
|
247
|
+
{ url = "https://files.pythonhosted.org/packages/98/9e/a112df5cfd5a68cb1d9fc31cfe38c28d5aec9f10865ce37ecef2e4450873/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6943e2fe7954d29d84de45d29d34c8dc36ce96570e67d89aa9976e650a4a9138", size = 53144784, upload-time = "2026-08-10T12:39:20.503Z" },
|
|
248
|
+
{ url = "https://files.pythonhosted.org/packages/31/24/97e8bd98f1e3b07e2ba08bcdff690674fbe16d69a7d2712cc3884665e615/pyarrow-25.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:31e49a7888fcdf3a835da33ae777f6bb9a866334e5a789282fc26dcf426f7f15", size = 27870159, upload-time = "2026-08-10T12:39:26.161Z" },
|
|
249
|
+
{ url = "https://files.pythonhosted.org/packages/36/4c/b525824ad3094076919273cd97db61fb3d78252dee76fa3b8dc8f76774aa/pyarrow-25.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bf0b672390cdcb640d7288f96b826d71ff4e9abb254a86c89890baf51a29cee6", size = 35885255, upload-time = "2026-08-10T12:39:32.366Z" },
|
|
250
|
+
{ url = "https://files.pythonhosted.org/packages/08/62/448bb0e940de41aec31d1a956e63ad9c54afdf122a103cc3ab20c2a3ce33/pyarrow-25.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:38a9a4b4b9613380e200641891495a56c3d5a98a092db4a870af9975e220471d", size = 37644461, upload-time = "2026-08-10T12:39:38.142Z" },
|
|
251
|
+
{ url = "https://files.pythonhosted.org/packages/6e/9a/13587e38bd4806fd218f50fd13b8903fab60588a699ff0c406372e5b4043/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b726ad7e7b669be982b0c71c07fe4b037d654354130da79a7902a669e93a66b", size = 46877146, upload-time = "2026-08-10T12:39:43.722Z" },
|
|
252
|
+
{ url = "https://files.pythonhosted.org/packages/8d/61/1c5d1229fa21da4cff5365e41e57177aaac57c563c727f35419b8513d1c1/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:9171748cdf796972d85a4b60157c279913e242992e350c90c7450182a9838b2a", size = 50131616, upload-time = "2026-08-10T12:39:49.304Z" },
|
|
253
|
+
{ url = "https://files.pythonhosted.org/packages/43/20/291e1d65cc0b09aa19f03cf25cf51a2f5fa94b5db315178f2d254ed5cad4/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b7a296aac7a71fa0886c08e155ddb6c636a50013f801f6178daafa0f9e726188", size = 50008879, upload-time = "2026-08-10T12:39:56.891Z" },
|
|
254
|
+
{ url = "https://files.pythonhosted.org/packages/8b/7c/1b7c9ec28e76576337e4f97b31141c9a181b89b6d1d6221e9d8205621a58/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0fe7c8b6c03969b49c8c66182e4a18e3819ab92d07cfab5d8370c531b9369ef0", size = 53170864, upload-time = "2026-08-10T12:40:04.918Z" },
|
|
255
|
+
{ url = "https://files.pythonhosted.org/packages/b7/75/f3d789dc06011a765d14d86bda799cf72ac1d715b6a6edecaa0d73d95062/pyarrow-25.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:f729cfdbd36fd99d543b67a914d2de044c84ebe45be8b34902b299b608c15c8f", size = 28620729, upload-time = "2026-08-10T12:40:51.41Z" },
|
|
256
|
+
{ url = "https://files.pythonhosted.org/packages/fc/05/647a8ee6f7c2662feb6921315617bc04dcd6034763fb61b1199720bf6162/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:59a2de54c0cbd954da861eee4d1d330f8e909c45b53455baef696380f2c55033", size = 36130288, upload-time = "2026-08-10T12:40:11.014Z" },
|
|
257
|
+
{ url = "https://files.pythonhosted.org/packages/93/f8/c9ee997554d7bea94520667dd1933f109ac1da3ee3556d2b49381e023484/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:35935cd5de130aa5cf4dea052a63e6bf2e17006c35c3a468194242b9b2bf5956", size = 37762187, upload-time = "2026-08-10T12:40:16.592Z" },
|
|
258
|
+
{ url = "https://files.pythonhosted.org/packages/a2/08/a28c01c7fe9e96e8233ce2d13df1d402f4f999f848f51d2daacd6bb4c036/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f3831aaa25c67a99f99dc8b05873cb9d64560390372e2aa197ce9dd4a3f06a44", size = 46888003, upload-time = "2026-08-10T12:40:23.242Z" },
|
|
259
|
+
{ url = "https://files.pythonhosted.org/packages/1b/b9/58612e977d28dc58c878448866838369ee8da2f1e7cc8ed2c84b952aafee/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a1fdfc6659b6b19022f2e50627fb5cf7156a66c46bf4299379955cbe742382a", size = 50079036, upload-time = "2026-08-10T12:40:29.169Z" },
|
|
260
|
+
{ url = "https://files.pythonhosted.org/packages/72/13/66e1402dcc860e1dc2760b1e0292c9a569b62b3bccab69def1b3e907d006/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:169d3429d5be7c752125890620f75a60776d38b0035eddae939651640822332e", size = 50040226, upload-time = "2026-08-10T12:40:35.186Z" },
|
|
261
|
+
{ url = "https://files.pythonhosted.org/packages/78/10/3f1a5497a7ef732ab0f03ecca3e66d89d9c0f57fdc61b4794c456b781f01/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:119297a6dc197e45d9c6d4415f7814a67ffa36c180d26f68c154c58067ae782d", size = 53149035, upload-time = "2026-08-10T12:40:41.454Z" },
|
|
262
|
+
{ url = "https://files.pythonhosted.org/packages/93/c0/37d4a7e8e2f7a6076283673d5298018ca26478b934c6ee369e10505ab32c/pyarrow-25.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4288f27577352d608ca08553b0865e4a9b3aa14820c5d95b53337218d609835b", size = 28753071, upload-time = "2026-08-10T12:40:46.623Z" },
|
|
263
|
+
]
|
|
264
|
+
|
|
220
265
|
[[package]]
|
|
221
266
|
name = "pydantic"
|
|
222
267
|
version = "2.13.4"
|
|
@@ -343,7 +388,7 @@ wheels = [
|
|
|
343
388
|
|
|
344
389
|
[[package]]
|
|
345
390
|
name = "transcript-viewer"
|
|
346
|
-
version = "0.
|
|
391
|
+
version = "0.6.0"
|
|
347
392
|
source = { editable = "." }
|
|
348
393
|
dependencies = [
|
|
349
394
|
{ name = "atif-make" },
|
|
@@ -353,6 +398,9 @@ dependencies = [
|
|
|
353
398
|
ai = [
|
|
354
399
|
{ name = "anthropic" },
|
|
355
400
|
]
|
|
401
|
+
parquet = [
|
|
402
|
+
{ name = "atif-make", extra = ["parquet"] },
|
|
403
|
+
]
|
|
356
404
|
|
|
357
405
|
[package.dev-dependencies]
|
|
358
406
|
dev = [
|
|
@@ -362,9 +410,10 @@ dev = [
|
|
|
362
410
|
[package.metadata]
|
|
363
411
|
requires-dist = [
|
|
364
412
|
{ name = "anthropic", marker = "extra == 'ai'", specifier = ">=0.40" },
|
|
365
|
-
{ name = "atif-make",
|
|
413
|
+
{ name = "atif-make", specifier = ">=0.5.0" },
|
|
414
|
+
{ name = "atif-make", extras = ["parquet"], marker = "extra == 'parquet'", specifier = ">=0.5.0" },
|
|
366
415
|
]
|
|
367
|
-
provides-extras = ["ai"]
|
|
416
|
+
provides-extras = ["parquet", "ai"]
|
|
368
417
|
|
|
369
418
|
[package.metadata.requires-dev]
|
|
370
419
|
dev = [{ name = "pytest", specifier = ">=8.0" }]
|
|
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
|