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.
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/PKG-INFO +2 -2
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/pyproject.toml +2 -2
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/cli.py +97 -26
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_session_cli.py +26 -12
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/.gitignore +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/LICENSE +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/NOTICE +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/README.md +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/__init__.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/__main__.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/config.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/conftest.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/fake_client.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_cli.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_config.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_discover.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_effort.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_encrypted_run.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_encryption_cli.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_interrupt.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_models.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_passthrough.py +0 -0
- {generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/tests/test_robustness.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
900
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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(
|
|
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 "
|
|
76
|
-
assert "
|
|
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
|
-
|
|
86
|
-
assert
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/__init__.py
RENAMED
|
File without changes
|
{generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/__main__.py
RENAMED
|
File without changes
|
{generic_ml_cache_cli-0.6.0 → generic_ml_cache_cli-0.7.0}/src/generic_ml_cache_cli/config.py
RENAMED
|
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
|