nabla-cli 0.6.0__tar.gz → 0.6.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.
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/PKG-INFO +1 -1
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/__init__.py +1 -1
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/branding.py +21 -14
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/cli.py +7 -1
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/PKG-INFO +1 -1
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/pyproject.toml +1 -1
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_cli_ux.py +118 -109
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/README.md +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/__main__.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/config.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/contract.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/induce.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/profile.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/prompt_recon.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/recorder.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/samplegen.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/scanner.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/schema_resolver.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/schemas/site_profile.schema.json +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/term.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/ui.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/SOURCES.txt +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/dependency_links.txt +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/entry_points.txt +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/requires.txt +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/top_level.txt +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/setup.cfg +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_config.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_e2e_profile.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_induce.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_prompt_recon.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_recorder.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_samplegen.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_scanner.py +0 -0
- {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_schema_resolver.py +0 -0
|
@@ -41,18 +41,21 @@ def banner_lines() -> list[tuple[str, str, str]]:
|
|
|
41
41
|
return [(logo, text, style) for logo, (text, style) in zip(_LOGO, taglines)]
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def _compose(
|
|
44
|
+
def _compose(traced: set[tuple[int, int]] | None = None) -> Text:
|
|
45
|
+
"""The full banner. With ``traced`` given, every glyph is still present
|
|
46
|
+
(no layout shift, ever) — untraced stroke cells just render quieter, and
|
|
47
|
+
the traced set brightens as the animation sweeps through."""
|
|
45
48
|
frame = Text()
|
|
46
49
|
for r, (logo_line, tagline, tag_style) in enumerate(banner_lines()):
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if
|
|
50
|
+
frame.append(" ")
|
|
51
|
+
for c, ch in enumerate(f"{logo_line:<14}"):
|
|
52
|
+
if ch == " ":
|
|
53
|
+
frame.append(" ")
|
|
54
|
+
elif traced is None or (r, c) in traced:
|
|
55
|
+
frame.append(ch, style=_BRAND_STYLE)
|
|
56
|
+
else:
|
|
57
|
+
frame.append(ch, style="dim cyan")
|
|
58
|
+
if tagline:
|
|
56
59
|
frame.append(tagline, style=tag_style)
|
|
57
60
|
frame.append("\n")
|
|
58
61
|
return frame
|
|
@@ -83,11 +86,15 @@ def print_banner(console: Console | None = None, animate: bool = False) -> None:
|
|
|
83
86
|
|
|
84
87
|
from rich.live import Live
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
# Full-size first frame: the complete banner is on screen before a
|
|
90
|
+
# single animation tick, so the layout never shifts — the sweep only
|
|
91
|
+
# changes brightness along the stroke.
|
|
92
|
+
with Live(_compose(traced=set()), console=console,
|
|
93
|
+
refresh_per_second=30, transient=False) as live:
|
|
94
|
+
time.sleep(0.12)
|
|
95
|
+
for traced in _stroke_frames():
|
|
96
|
+
live.update(_compose(traced=traced))
|
|
89
97
|
time.sleep(0.09)
|
|
90
|
-
time.sleep(0.18)
|
|
91
98
|
live.update(_compose())
|
|
92
99
|
else:
|
|
93
100
|
console.print(_compose(), end="")
|
|
@@ -194,7 +194,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
194
194
|
"build": _cmd_profile,
|
|
195
195
|
"profile": _cmd_profile, # legacy alias
|
|
196
196
|
}
|
|
197
|
-
|
|
197
|
+
try:
|
|
198
|
+
return commands[args.command](args)
|
|
199
|
+
except KeyboardInterrupt:
|
|
200
|
+
# One quiet line, not a traceback — Ctrl+C is a decision, not an error.
|
|
201
|
+
print()
|
|
202
|
+
_errcon.print("cancelled", style="yellow")
|
|
203
|
+
return 130
|
|
198
204
|
|
|
199
205
|
|
|
200
206
|
def _clipboard_text() -> str:
|
|
@@ -348,112 +348,121 @@ def test_style_is_plain_when_not_a_tty(capsys):
|
|
|
348
348
|
from nabla.term import style
|
|
349
349
|
|
|
350
350
|
assert style("hello", "red") == "hello" # captured stdout is not a tty
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
# ---------------------------------------------------------------------------
|
|
354
|
-
# Multi-site capture / build
|
|
355
|
-
# ---------------------------------------------------------------------------
|
|
356
|
-
|
|
357
|
-
_SECOND_SITE_SRC = _SINGLE_SITE_SRC.replace("extract_receipt", "extract_menu").replace(
|
|
358
|
-
"RECEIPT_SCHEMA", "MENU_SCHEMA").replace('"merchant"', '"dish"')
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def _make_two_site_repo(tmp_path):
|
|
362
|
-
repo = tmp_path / "two_site_repo"
|
|
363
|
-
repo.mkdir()
|
|
364
|
-
(repo / "receipts.py").write_text(_SINGLE_SITE_SRC, encoding="utf-8")
|
|
365
|
-
(repo / "menus.py").write_text(_SECOND_SITE_SRC, encoding="utf-8")
|
|
366
|
-
return repo
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
def test_multi_capture_writes_file_per_site(tmp_path, monkeypatch, capsys):
|
|
370
|
-
monkeypatch.setenv("NABLA_FAKE_UPSTREAM", "1")
|
|
371
|
-
repo = _make_two_site_repo(tmp_path)
|
|
372
|
-
monkeypatch.chdir(tmp_path)
|
|
373
|
-
monkeypatch.setattr(
|
|
374
|
-
"nabla.samplegen.generate_samples",
|
|
375
|
-
lambda template, schema, n, **k: [
|
|
376
|
-
{"input_slots": {"ocr_text": f"item {i}"}} for i in range(n)
|
|
377
|
-
],
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
rc = main(["capture", str(repo), "--num-samples", "3"])
|
|
381
|
-
|
|
382
|
-
assert rc == 0
|
|
383
|
-
for symbol in ("extract_receipt", "extract_menu"):
|
|
384
|
-
captured = tmp_path / f"{symbol}.captured.jsonl"
|
|
385
|
-
assert captured.exists(), symbol
|
|
386
|
-
lines = [json.loads(x) for x in captured.read_text(encoding="utf-8").splitlines()]
|
|
387
|
-
assert len(lines) == 3
|
|
388
|
-
assert all(l["_source_kind"] == "generated_samples" for l in lines)
|
|
389
|
-
assert (tmp_path / f"{symbol}.inputs.jsonl").exists()
|
|
390
|
-
out = capsys.readouterr().out
|
|
391
|
-
assert "next: nabla build" in out
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
def test_multi_capture_explicit_source_still_requires_site(tmp_path, monkeypatch, capsys):
|
|
395
|
-
repo = _make_two_site_repo(tmp_path)
|
|
396
|
-
monkeypatch.chdir(tmp_path)
|
|
397
|
-
samples = tmp_path / "s.jsonl"
|
|
398
|
-
samples.write_text(json.dumps({"input_slots": {"ocr_text": "x"}}) + "\n", encoding="utf-8")
|
|
399
|
-
|
|
400
|
-
rc = main(["capture", str(repo), "--inputs", str(samples)])
|
|
401
|
-
|
|
402
|
-
assert rc == 2
|
|
403
|
-
assert "--site is required" in capsys.readouterr().err
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
def test_multi_build_builds_every_captured_site(tmp_path, monkeypatch, capsys):
|
|
407
|
-
from nabla.contract import GoalBlock
|
|
408
|
-
|
|
409
|
-
repo = _make_two_site_repo(tmp_path)
|
|
410
|
-
monkeypatch.chdir(tmp_path)
|
|
411
|
-
pair = {"input_slots": {"ocr_text": "r"}, "prompt": "r",
|
|
412
|
-
"completion": "{\"merchant\": \"A\", \"total\": 1}",
|
|
413
|
-
"tokens": {"in": 5, "out": 5}, "latency_ms": 10,
|
|
414
|
-
"_source_kind": "samples"}
|
|
415
|
-
for symbol in ("extract_receipt", "extract_menu"):
|
|
416
|
-
(tmp_path / f"{symbol}.captured.jsonl").write_text(
|
|
417
|
-
json.dumps(pair) + "\n", encoding="utf-8")
|
|
418
|
-
monkeypatch.setattr(
|
|
419
|
-
"nabla.induce.induce",
|
|
420
|
-
lambda template, schema, examples, **k: GoalBlock(
|
|
421
|
-
description="Extracts fields.",
|
|
422
|
-
constraints=["null when absent"],
|
|
423
|
-
difficulty_dimensions=[{"name": "d", "levels": ["a", "b"], "evidence": "e"}],
|
|
424
|
-
),
|
|
425
|
-
)
|
|
426
|
-
|
|
427
|
-
rc = main(["build", str(repo)])
|
|
428
|
-
|
|
429
|
-
assert rc == 0
|
|
430
|
-
assert (tmp_path / "extract_receipt.profile.json").exists()
|
|
431
|
-
assert (tmp_path / "extract_menu.profile.json").exists()
|
|
432
|
-
out = capsys.readouterr().out
|
|
433
|
-
assert "built 2 profile(s)" in out
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def test_multi_build_no_captures_exits_2(tmp_path, monkeypatch, capsys):
|
|
437
|
-
repo = _make_two_site_repo(tmp_path)
|
|
438
|
-
monkeypatch.chdir(tmp_path)
|
|
439
|
-
|
|
440
|
-
rc = main(["build", str(repo)])
|
|
441
|
-
|
|
442
|
-
assert rc == 2
|
|
443
|
-
assert "nabla capture" in capsys.readouterr().err
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
def test_multi_select_noninteractive_fallback(monkeypatch, capsys):
|
|
447
|
-
from rich.console import Console
|
|
448
|
-
|
|
449
|
-
from nabla.ui import multi_select
|
|
450
|
-
|
|
451
|
-
console = Console(width=100)
|
|
452
|
-
monkeypatch.setattr("builtins.input", lambda *a: "1,3")
|
|
453
|
-
picked = multi_select(console, "Call sites",
|
|
454
|
-
[("a", ""), ("b", ""), ("c", "")])
|
|
455
|
-
assert picked == [0, 2]
|
|
456
|
-
|
|
457
|
-
monkeypatch.setattr("builtins.input", lambda *a: "")
|
|
458
|
-
assert multi_select(console, "Call sites",
|
|
459
|
-
[("a", ""), ("b", "")]) == [0, 1]
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
# Multi-site capture / build
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
_SECOND_SITE_SRC = _SINGLE_SITE_SRC.replace("extract_receipt", "extract_menu").replace(
|
|
358
|
+
"RECEIPT_SCHEMA", "MENU_SCHEMA").replace('"merchant"', '"dish"')
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _make_two_site_repo(tmp_path):
|
|
362
|
+
repo = tmp_path / "two_site_repo"
|
|
363
|
+
repo.mkdir()
|
|
364
|
+
(repo / "receipts.py").write_text(_SINGLE_SITE_SRC, encoding="utf-8")
|
|
365
|
+
(repo / "menus.py").write_text(_SECOND_SITE_SRC, encoding="utf-8")
|
|
366
|
+
return repo
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def test_multi_capture_writes_file_per_site(tmp_path, monkeypatch, capsys):
|
|
370
|
+
monkeypatch.setenv("NABLA_FAKE_UPSTREAM", "1")
|
|
371
|
+
repo = _make_two_site_repo(tmp_path)
|
|
372
|
+
monkeypatch.chdir(tmp_path)
|
|
373
|
+
monkeypatch.setattr(
|
|
374
|
+
"nabla.samplegen.generate_samples",
|
|
375
|
+
lambda template, schema, n, **k: [
|
|
376
|
+
{"input_slots": {"ocr_text": f"item {i}"}} for i in range(n)
|
|
377
|
+
],
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
rc = main(["capture", str(repo), "--num-samples", "3"])
|
|
381
|
+
|
|
382
|
+
assert rc == 0
|
|
383
|
+
for symbol in ("extract_receipt", "extract_menu"):
|
|
384
|
+
captured = tmp_path / f"{symbol}.captured.jsonl"
|
|
385
|
+
assert captured.exists(), symbol
|
|
386
|
+
lines = [json.loads(x) for x in captured.read_text(encoding="utf-8").splitlines()]
|
|
387
|
+
assert len(lines) == 3
|
|
388
|
+
assert all(l["_source_kind"] == "generated_samples" for l in lines)
|
|
389
|
+
assert (tmp_path / f"{symbol}.inputs.jsonl").exists()
|
|
390
|
+
out = capsys.readouterr().out
|
|
391
|
+
assert "next: nabla build" in out
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_multi_capture_explicit_source_still_requires_site(tmp_path, monkeypatch, capsys):
|
|
395
|
+
repo = _make_two_site_repo(tmp_path)
|
|
396
|
+
monkeypatch.chdir(tmp_path)
|
|
397
|
+
samples = tmp_path / "s.jsonl"
|
|
398
|
+
samples.write_text(json.dumps({"input_slots": {"ocr_text": "x"}}) + "\n", encoding="utf-8")
|
|
399
|
+
|
|
400
|
+
rc = main(["capture", str(repo), "--inputs", str(samples)])
|
|
401
|
+
|
|
402
|
+
assert rc == 2
|
|
403
|
+
assert "--site is required" in capsys.readouterr().err
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def test_multi_build_builds_every_captured_site(tmp_path, monkeypatch, capsys):
|
|
407
|
+
from nabla.contract import GoalBlock
|
|
408
|
+
|
|
409
|
+
repo = _make_two_site_repo(tmp_path)
|
|
410
|
+
monkeypatch.chdir(tmp_path)
|
|
411
|
+
pair = {"input_slots": {"ocr_text": "r"}, "prompt": "r",
|
|
412
|
+
"completion": "{\"merchant\": \"A\", \"total\": 1}",
|
|
413
|
+
"tokens": {"in": 5, "out": 5}, "latency_ms": 10,
|
|
414
|
+
"_source_kind": "samples"}
|
|
415
|
+
for symbol in ("extract_receipt", "extract_menu"):
|
|
416
|
+
(tmp_path / f"{symbol}.captured.jsonl").write_text(
|
|
417
|
+
json.dumps(pair) + "\n", encoding="utf-8")
|
|
418
|
+
monkeypatch.setattr(
|
|
419
|
+
"nabla.induce.induce",
|
|
420
|
+
lambda template, schema, examples, **k: GoalBlock(
|
|
421
|
+
description="Extracts fields.",
|
|
422
|
+
constraints=["null when absent"],
|
|
423
|
+
difficulty_dimensions=[{"name": "d", "levels": ["a", "b"], "evidence": "e"}],
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
rc = main(["build", str(repo)])
|
|
428
|
+
|
|
429
|
+
assert rc == 0
|
|
430
|
+
assert (tmp_path / "extract_receipt.profile.json").exists()
|
|
431
|
+
assert (tmp_path / "extract_menu.profile.json").exists()
|
|
432
|
+
out = capsys.readouterr().out
|
|
433
|
+
assert "built 2 profile(s)" in out
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def test_multi_build_no_captures_exits_2(tmp_path, monkeypatch, capsys):
|
|
437
|
+
repo = _make_two_site_repo(tmp_path)
|
|
438
|
+
monkeypatch.chdir(tmp_path)
|
|
439
|
+
|
|
440
|
+
rc = main(["build", str(repo)])
|
|
441
|
+
|
|
442
|
+
assert rc == 2
|
|
443
|
+
assert "nabla capture" in capsys.readouterr().err
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def test_multi_select_noninteractive_fallback(monkeypatch, capsys):
|
|
447
|
+
from rich.console import Console
|
|
448
|
+
|
|
449
|
+
from nabla.ui import multi_select
|
|
450
|
+
|
|
451
|
+
console = Console(width=100)
|
|
452
|
+
monkeypatch.setattr("builtins.input", lambda *a: "1,3")
|
|
453
|
+
picked = multi_select(console, "Call sites",
|
|
454
|
+
[("a", ""), ("b", ""), ("c", "")])
|
|
455
|
+
assert picked == [0, 2]
|
|
456
|
+
|
|
457
|
+
monkeypatch.setattr("builtins.input", lambda *a: "")
|
|
458
|
+
assert multi_select(console, "Call sites",
|
|
459
|
+
[("a", ""), ("b", "")]) == [0, 1]
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_ctrl_c_is_one_quiet_line(monkeypatch, capsys):
|
|
463
|
+
monkeypatch.setattr("nabla.cli._cmd_init", lambda args: (_ for _ in ()).throw(KeyboardInterrupt()))
|
|
464
|
+
rc = main(["init"])
|
|
465
|
+
assert rc == 130
|
|
466
|
+
captured = capsys.readouterr()
|
|
467
|
+
assert "cancelled" in captured.err
|
|
468
|
+
assert "Traceback" not in captured.err + captured.out
|
|
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
|