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.
Files changed (35) hide show
  1. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/PKG-INFO +1 -1
  2. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/__init__.py +1 -1
  3. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/branding.py +21 -14
  4. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/cli.py +7 -1
  5. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/PKG-INFO +1 -1
  6. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/pyproject.toml +1 -1
  7. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_cli_ux.py +118 -109
  8. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/README.md +0 -0
  9. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/__main__.py +0 -0
  10. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/config.py +0 -0
  11. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/contract.py +0 -0
  12. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/induce.py +0 -0
  13. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/profile.py +0 -0
  14. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/prompt_recon.py +0 -0
  15. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/recorder.py +0 -0
  16. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/samplegen.py +0 -0
  17. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/scanner.py +0 -0
  18. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/schema_resolver.py +0 -0
  19. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/schemas/site_profile.schema.json +0 -0
  20. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/term.py +0 -0
  21. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla/ui.py +0 -0
  22. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/SOURCES.txt +0 -0
  23. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/dependency_links.txt +0 -0
  24. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/entry_points.txt +0 -0
  25. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/requires.txt +0 -0
  26. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/nabla_cli.egg-info/top_level.txt +0 -0
  27. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/setup.cfg +0 -0
  28. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_config.py +0 -0
  29. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_e2e_profile.py +0 -0
  30. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_induce.py +0 -0
  31. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_prompt_recon.py +0 -0
  32. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_recorder.py +0 -0
  33. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_samplegen.py +0 -0
  34. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_scanner.py +0 -0
  35. {nabla_cli-0.6.0 → nabla_cli-0.6.2}/tests/test_schema_resolver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nabla-cli
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Call-site harvester: scan OpenAI call sites, record real traffic, emit site profiles
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -1,3 +1,3 @@
1
1
  """nabla — call-site harvester CLI (pre-Phase-0)."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.6.2"
@@ -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(mask: set[tuple[int, int]] | None = None, with_taglines: bool = True) -> Text:
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
- if mask is None:
48
- segment = logo_line
49
- else:
50
- segment = "".join(
51
- ch if ch == " " or (r, c) in mask else " "
52
- for c, ch in enumerate(logo_line)
53
- )
54
- frame.append(f" {segment:<14}", style=_BRAND_STYLE)
55
- if with_taglines and tagline:
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
- with Live(console=console, refresh_per_second=30, transient=False) as live:
87
- for mask in _stroke_frames():
88
- live.update(_compose(mask=mask, with_taglines=False))
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
- return commands[args.command](args)
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nabla-cli
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Call-site harvester: scan OpenAI call sites, record real traffic, emit site profiles
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nabla-cli"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "Call-site harvester: scan OpenAI call sites, record real traffic, emit site profiles"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -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