Skip to content

Commit feee666

Browse files
committed
implement: Save every run beside its zip (t46)
1 parent 898aa97 commit feee666

3 files changed

Lines changed: 97 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ the fields are route A's, and each one is flagged or is route A's word for it. A
110110
run that fails counts route A's fields in the same way, and adds a `route B failed` line
111111
naming pandoc's message; the set is route A's reading alone.
112112

113+
The run is saved beside the zip. `OUT/report.txt` holds the printed lines, `OUT/reply-a.json`
114+
route A's reply, `OUT/reply-b.json` route B's, and `OUT/flags.json` one entry per flagged
115+
field. A run with no filter, and a run whose filter failed, writes no `reply-b.json`.
116+
113117
`convert` exits 1 where a named file is not there, and where Mathpix, the model or
114118
pandoc failed, and 0 otherwise. A flagged
115119
field does not change the exit code: the zip is written whatever the flags say, and a

‎in2lambda_agent/routes.py‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import json
2525
import re
2626
import subprocess
27-
from dataclasses import dataclass, field
27+
from dataclasses import asdict, dataclass, field
2828
from pathlib import Path
2929
from typing import Any, Callable, Optional
3030

@@ -415,6 +415,9 @@ class Converted:
415415
# twice saves this reply and passes it back as `convert`'s `route_a`; a
416416
# second call to the model returns different wording.
417417
route_a: Reply_ = field(default_factory=list)
418+
# Route B's reply, the filter's two runs merged and before reconciling, and None
419+
# where no filter was given or the filter run failed.
420+
route_b: Optional[Reply_] = None
418421
tokens: int = 0
419422
# The counts of the reconciliation, zero where route B did not run.
420423
fields: int = 0
@@ -526,6 +529,12 @@ def convert(
526529
`on_stage`, where it is given, is called with a name and a message as each step
527530
finishes - `ocr`, `route A`, `route B`, `fields`, `build` - so that a caller watching
528531
a run shows each line as the step ends rather than the report at the end of it.
532+
533+
The run is saved in `out_dir` beside the zip: `reply-a.json` as route A answers,
534+
`reply-b.json` as the filter runs, and `flags.json` and `report.txt` after the build.
535+
Each file is written as soon as its content exists, so that a step that fails keeps
536+
the replies of the steps before it. Where no filter was given, or the filter run
537+
failed, no `reply-b.json` is written.
529538
"""
530539
settings = settings or load_settings()
531540
backend = backend or choose_backend(settings)
@@ -534,6 +543,10 @@ def said(stage: str, message: str) -> None:
534543
if on_stage is not None:
535544
on_stage(stage, message)
536545

546+
out_dir = Path(out_dir)
547+
# The build writes the zip here at the end of the run; the replies are written
548+
# into the same directory as each route answers, before the build makes it.
549+
out_dir.mkdir(parents=True, exist_ok=True)
537550
read = [f"{Path(d).name}: {_read_as(d, cache_dir)}" for d in (document, solutions) if d is not None]
538551
markdown, images = markdown_of(document, cache_dir, settings)
539552
solutions_md = markdown_of(solutions, cache_dir, settings)[0] if solutions else None
@@ -546,7 +559,9 @@ def said(stage: str, message: str) -> None:
546559
else:
547560
tokens = 0
548561
said("route A", "the reply given, no call made")
562+
(out_dir / "reply-a.json").write_text(json.dumps(route_a, indent=2), encoding="utf-8")
549563
reply = route_a
564+
route_b = None
550565
counts, error = (0, 0, 0, 0), None
551566
flags = [Flag(k, fields(reply)[k], "", NOT_VERBATIM) for k in not_verbatim(reply, source)]
552567
if lua is None:
@@ -561,6 +576,8 @@ def said(stage: str, message: str) -> None:
561576
error = (stderr.decode("utf-8", "replace") if stderr else str(problem)).strip()
562577
said("route B", f"failed: {error}")
563578
else:
579+
route_b = other
580+
(out_dir / "reply-b.json").write_text(json.dumps(other, indent=2), encoding="utf-8")
564581
reconciled = reconcile(reply, other, source, backend)
565582
reply, flags = reconciled.fields, reconciled.flags
566583
# The adjudication call is the document's second call, so its tokens
@@ -576,13 +593,19 @@ def said(stage: str, message: str) -> None:
576593
flags.append(Flag(k, fields(reply)[k], "", STRAY_MINUS))
577594
result = Converted(
578595
set=to_set(reply, name=name, directory=images), zip_path=None, flags=flags,
579-
reply=reply, route_a=route_a, tokens=tokens,
596+
reply=reply, route_a=route_a, route_b=route_b, tokens=tokens,
580597
fields=counts[0], agreed=counts[1], defaulted=counts[2], adjudicated=counts[3],
581598
route_b_error=error,
582599
)
583600
said("fields", result.counted())
584601
result.zip_path = build(result.set, out_dir)
585602
said("build", str(result.zip_path))
603+
(out_dir / "flags.json").write_text(
604+
json.dumps([asdict(one) for one in result.flags], indent=2), encoding="utf-8"
605+
)
606+
# The report names the zip, so it is written after the build, and holds the lines
607+
# the command prints.
608+
(out_dir / "report.txt").write_text("\n".join(result.report()) + "\n", encoding="utf-8")
586609
return result
587610

588611

‎tests/test_routes.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
import copy
10+
import dataclasses
1011
import difflib
1112
import json
1213
import os
@@ -464,6 +465,66 @@ def test_a_sheet_whose_filter_run_fails_keeps_its_route_a_reply(tmp_path):
464465
assert result.route_b_error
465466
assert result.reply == SHEET_DIRECT
466467
assert (result.fields, result.agreed, result.flags) == (0, 0, [])
468+
# Route B wrote no reply, so there is no reply-b.json; the report says why.
469+
assert not (tmp_path / "out" / "reply-b.json").exists()
470+
assert "route B failed:" in (tmp_path / "out" / "report.txt").read_text()
471+
472+
473+
# --- what a run leaves in the out directory ------------------------------------------------
474+
475+
476+
def test_a_run_leaves_its_report_and_replies_beside_the_zip(tmp_path):
477+
out = tmp_path / "out"
478+
result = routes.convert(
479+
ME2 / "questions.md",
480+
solutions=ME2 / "solutions.md",
481+
out_dir=out,
482+
backend=FakeBackend(json.dumps(REPLY)),
483+
settings=Settings(),
484+
)
485+
486+
assert json.loads((out / "reply-a.json").read_text()) == REPLY == result.route_a
487+
assert json.loads((out / "flags.json").read_text()) == [
488+
dataclasses.asdict(f) for f in result.flags
489+
]
490+
assert (out / "report.txt").read_text() == "\n".join(result.report()) + "\n"
491+
assert "route B did not run" in (out / "report.txt").read_text()
492+
# No filter ran, so route B has no reply to save.
493+
assert not (out / "reply-b.json").exists()
494+
495+
496+
@pytest.mark.skipif(shutil.which("pandoc") is None, reason="pandoc")
497+
def test_a_run_with_a_filter_leaves_route_bs_reply_too(tmp_path):
498+
out = tmp_path / "out"
499+
lua = FIXTURES / "pair-filter.lua"
500+
backend = FakeBackend(
501+
json.dumps(PAIRED_DIRECT),
502+
json.dumps([{"field": "q2.main_text", "choice": "A", "reason": "B carries the parts too"}]),
503+
)
504+
505+
result = routes.convert(
506+
FIXTURES / "paired.md",
507+
solutions=FIXTURES / "paired_solutions.md",
508+
out_dir=out,
509+
backend=backend,
510+
settings=Settings(),
511+
lua=lua,
512+
name="paired",
513+
)
514+
515+
# Route B's reply is the two filter runs merged, as convert merges them, and is
516+
# saved before reconciling changes any field.
517+
other = routes.merge(
518+
routes.run_filter(lua, FIXTURES / "paired.md"),
519+
routes.run_filter(lua, FIXTURES / "paired_solutions.md", role="solutions"),
520+
)
521+
assert result.route_b == other
522+
assert json.loads((out / "reply-b.json").read_text()) == other
523+
assert json.loads((out / "reply-a.json").read_text()) == PAIRED_DIRECT
524+
assert json.loads((out / "flags.json").read_text()) == [
525+
dataclasses.asdict(f) for f in result.flags
526+
]
527+
assert (out / "report.txt").read_text() == "\n".join(result.report()) + "\n"
467528

468529

469530
# --- the report of one document -------------------------------------------------------------
@@ -604,3 +665,10 @@ def test_the_me2_pair_converts_from_the_command_line(tmp_path, capsys):
604665
"flag q3.p1.worked_solution",
605666
]
606667
assert printed.splitlines()[-1].startswith(f"build {tmp_path / 'out'}")
668+
# The run is saved beside the zip: no filter ran, so there is no reply-b.json.
669+
out = tmp_path / "out"
670+
report = (out / "report.txt").read_text()
671+
assert report == "\n".join(printed.splitlines()[1:]) + "\n"
672+
assert json.loads((out / "reply-a.json").read_text())
673+
assert json.loads((out / "flags.json").read_text())
674+
assert not (out / "reply-b.json").exists()

0 commit comments

Comments
 (0)