generic-ml-cache-cli 0.6.0__tar.gz → 0.7.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.
Files changed (24) hide show
  1. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/PKG-INFO +2 -2
  2. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/pyproject.toml +2 -2
  3. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/cli.py +97 -26
  4. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_session_cli.py +26 -12
  5. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/.gitignore +0 -0
  6. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/LICENSE +0 -0
  7. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/NOTICE +0 -0
  8. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/README.md +0 -0
  9. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/__init__.py +0 -0
  10. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/__main__.py +0 -0
  11. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/config.py +0 -0
  12. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/conftest.py +0 -0
  13. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/fake_client.py +0 -0
  14. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_cli.py +0 -0
  15. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_config.py +0 -0
  16. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_discover.py +0 -0
  17. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_effort.py +0 -0
  18. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_encrypted_run.py +0 -0
  19. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_encryption_cli.py +0 -0
  20. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_interrupt.py +0 -0
  21. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_models.py +0 -0
  22. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_passthrough.py +0 -0
  23. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_robustness.py +0 -0
  24. {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_stdin_delivery.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: generic-ml-cache-cli
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Terminal UI for generic-ml-cache: the gmlcache command. A thin inbound driver over generic-ml-cache-core -- reads config, provides the data source, maps commands onto the core library.
5
5
  Project-URL: Homepage, https://github.com/danielslobozian/generic-ml-cache
6
6
  Project-URL: Repository, https://github.com/danielslobozian/generic-ml-cache
@@ -24,7 +24,7 @@ Classifier: Programming Language :: Python :: 3.13
24
24
  Classifier: Topic :: Utilities
25
25
  Requires-Python: >=3.9
26
26
  Requires-Dist: argcomplete<4,>=3
27
- Requires-Dist: generic-ml-cache-core>=0.6.0
27
+ Requires-Dist: generic-ml-cache-core>=0.7.0
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: coverage>=7; extra == 'dev'
30
30
  Requires-Dist: pytest-cov; extra == 'dev'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "generic-ml-cache-cli"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "Terminal UI for generic-ml-cache: the gmlcache command. A thin inbound driver over generic-ml-cache-core -- reads config, provides the data source, maps commands onto the core library."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -25,7 +25,7 @@ classifiers = [
25
25
  "Programming Language :: Python :: 3.13",
26
26
  "Topic :: Utilities",
27
27
  ]
28
- dependencies = ["generic-ml-cache-core>=0.6.0", "argcomplete>=3,<4"]
28
+ dependencies = ["generic-ml-cache-core>=0.7.0", "argcomplete>=3,<4"]
29
29
 
30
30
  [project.urls]
31
31
  Homepage = "https://github.com/danielslobozian/generic-ml-cache"
@@ -45,6 +45,7 @@ from generic_ml_cache_core.application.domain.model.run.cache_mode import CacheM
45
45
  from generic_ml_cache_core.application.domain.model.run.persistence_depth import PersistenceDepth
46
46
  from generic_ml_cache_core.application.domain.model.execution.execution_state import ExecutionState
47
47
  from generic_ml_cache_core.application.domain.model.execution.ml_execution import MlExecution
48
+ from generic_ml_cache_core.application.usecase.session_report import build_session_report
48
49
  from generic_ml_cache_core.application.port.inbound.run_managed_local_execution_command import (
49
50
  RunManagedLocalExecutionCommand,
50
51
  )
@@ -896,45 +897,115 @@ def _cmd_session_start(args: argparse.Namespace) -> int:
896
897
  return 0
897
898
 
898
899
 
899
- #: events where a real client call ran (vs. HIT, which replayed, or an offline MISS).
900
- _EXECUTED_EVENTS = {"record", "run", "would_hit", "would_miss"}
900
+ _TOKEN_BLOCKS = " ▏▎▍▌▋▊▉█"
901
+
902
+
903
+ def _activity_bar(value: int, maxval: int, width: int = 10) -> str:
904
+ if maxval <= 0:
905
+ return " " * width
906
+ filled = value / maxval * width
907
+ full = int(filled)
908
+ bar = "█" * full + (_TOKEN_BLOCKS[int((filled - full) * 8)] if full < width else "")
909
+ return (bar + " " * width)[:width]
910
+
911
+
912
+ def _comma(n: int) -> str:
913
+ return f"{n:,}"
914
+
915
+
916
+ def _render_session_report(report) -> str:
917
+ lines = [f"session : {report.session_id}"]
918
+ if report.span_start:
919
+ span = (
920
+ report.span_start
921
+ if report.day_count == 1
922
+ else f"{report.span_start} → {report.span_end}"
923
+ )
924
+ plural = "" if report.day_count == 1 else "s"
925
+ lines.append(f"span : {span} ({report.day_count} day{plural})")
926
+ lines.append(
927
+ f"invocations : {report.invocations} "
928
+ f"executions : {report.executions} hits : {report.hits}"
929
+ )
930
+ if report.unknown_usage:
931
+ lines.append(f"unknown : {report.unknown_usage} execution(s) reported no usage")
932
+ if report.by_model:
933
+ lines.append("")
934
+ lines.append("by provider / model:")
935
+ for m in report.by_model:
936
+ lines.append(
937
+ f" {m.client + ' / ' + m.model:<16} spent {_comma(m.spent_tokens):>9} tok"
938
+ f" (in {_comma(m.spent_input):>8} · out {_comma(m.spent_output):>7})"
939
+ f" saved {_comma(m.saved_tokens):>9} tok {m.executions} exec · {m.hits} hit"
940
+ )
941
+ if report.by_day:
942
+ lines.append("")
943
+ lines.append("by day (activity):")
944
+ maxinv = max(d.invocations for d in report.by_day)
945
+ for d in report.by_day:
946
+ lines.append(
947
+ f" {d.day} {_activity_bar(d.invocations, maxinv)} {d.invocations:>3} calls"
948
+ f" ({d.executions} exec · {d.hits} hit)"
949
+ )
950
+ return "\n".join(lines)
951
+
952
+
953
+ def _session_report_json(report) -> dict:
954
+ return {
955
+ "session": report.session_id,
956
+ "invocations": report.invocations,
957
+ "executions": report.executions,
958
+ "hits": report.hits,
959
+ "unknown_usage": report.unknown_usage,
960
+ "span": {"start": report.span_start, "end": report.span_end, "days": report.day_count},
961
+ "by_model": [
962
+ {
963
+ "client": m.client,
964
+ "model": m.model,
965
+ "spent_input": m.spent_input,
966
+ "spent_output": m.spent_output,
967
+ "spent_tokens": m.spent_tokens,
968
+ "saved_tokens": m.saved_tokens,
969
+ "executions": m.executions,
970
+ "hits": m.hits,
971
+ }
972
+ for m in report.by_model
973
+ ],
974
+ "by_day": [
975
+ {
976
+ "day": d.day,
977
+ "invocations": d.invocations,
978
+ "executions": d.executions,
979
+ "hits": d.hits,
980
+ }
981
+ for d in report.by_day
982
+ ],
983
+ }
901
984
 
902
985
 
903
986
  def _cmd_session_report(args: argparse.Namespace) -> int:
904
987
  store_root = _store_root()
905
988
  if store_root is None:
906
989
  return 4
907
- counts = build_use_cases(store_root).metrics.session_event_counts(args.session_id)
908
- invocations = sum(counts.values())
909
- executions = sum(n for event, n in counts.items() if event in _EXECUTED_EVENTS)
910
- hits = counts.get("hit", 0)
990
+ wired = build_use_cases(store_root)
991
+ events = wired.metrics.session_events(args.session_id)
992
+ # Join each event's execution to its token usage (the current execution per key).
993
+ usage_by_key = {}
994
+ for key in {e.execution_key for e in events if e.execution_key}:
995
+ execution = wired.repository.find_current(key)
996
+ if execution is not None:
997
+ usage_by_key[key] = execution.token_usage
998
+ report = build_session_report(args.session_id, events, usage_by_key)
911
999
 
912
1000
  if args.json:
913
1001
  import json
914
1002
 
915
- print(
916
- json.dumps(
917
- {
918
- "session": args.session_id,
919
- "invocations": invocations,
920
- "executions": executions,
921
- "hits": hits,
922
- "events": counts,
923
- },
924
- indent=2,
925
- )
926
- )
1003
+ print(json.dumps(_session_report_json(report), indent=2))
927
1004
  return 0
928
-
929
- if invocations == 0:
1005
+ if report.invocations == 0:
930
1006
  print(f"no events recorded for session {args.session_id!r}")
931
1007
  return 0
932
- print(f"session : {args.session_id}")
933
- print(f"invocations : {invocations}")
934
- print(f"executions : {executions} (real client calls)")
935
- print(f"hits : {hits} (served from cache)")
936
- breakdown = ", ".join(f"{event}={counts[event]}" for event in sorted(counts))
937
- print(f"events : {breakdown}")
1008
+ print(_render_session_report(report))
938
1009
  return 0
939
1010
 
940
1011
 
@@ -71,25 +71,39 @@ def test_session_report_rolls_up_invocations_executions_hits(capsys):
71
71
 
72
72
  assert main(["session", "report", "wf"]) == 0
73
73
  out = capsys.readouterr().out
74
- assert "invocations : 2" in out
75
- assert "executions : 1" in out
76
- assert "hits : 1" in out
74
+ assert "invocations : 2 executions : 1 hits : 1" in out
75
+ assert "by provider / model:" in out and "fake / m1" in out
76
+ assert "by day (activity):" in out
77
+ # no dollars anywhere in the render (cost is a client-specific advisory estimate)
78
+ assert "$" not in out and "cost" not in out.lower()
77
79
 
78
80
 
79
81
  def test_session_report_json(capsys):
80
82
  import json
81
83
 
82
- main(_RUN + ["--session", "wf"])
84
+ main(_RUN + ["--session", "wf"]) # one record (the fake client reports no usage)
83
85
  capsys.readouterr()
84
86
  assert main(["session", "report", "wf", "--json"]) == 0
85
- data = json.loads(capsys.readouterr().out)
86
- assert data == {
87
- "session": "wf",
88
- "invocations": 1,
89
- "executions": 1,
90
- "hits": 0,
91
- "events": {"record": 1},
92
- }
87
+ out = capsys.readouterr().out
88
+ assert "cost" not in out.lower() and "usd" not in out.lower() and "$" not in out
89
+ data = json.loads(out)
90
+ assert data["session"] == "wf"
91
+ assert (data["invocations"], data["executions"], data["hits"]) == (1, 1, 0)
92
+ assert data["unknown_usage"] == 1
93
+ assert data["span"]["days"] == 1
94
+ assert data["by_model"] == [
95
+ {
96
+ "client": "fake",
97
+ "model": "m1",
98
+ "spent_input": 0,
99
+ "spent_output": 0,
100
+ "spent_tokens": 0,
101
+ "saved_tokens": 0,
102
+ "executions": 1,
103
+ "hits": 0,
104
+ }
105
+ ]
106
+ assert len(data["by_day"]) == 1 and data["by_day"][0]["invocations"] == 1
93
107
 
94
108
 
95
109
  def test_session_report_unknown_session_is_clean(capsys):