generic-ml-cache-cli 0.5.0__tar.gz → 0.6.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.5.0 → generic_ml_cache_cli-0.6.0}/PKG-INFO +13 -3
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/README.md +11 -1
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/pyproject.toml +2 -2
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/src/generic_ml_cache_cli/cli.py +105 -12
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/conftest.py +7 -1
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/test_cli.py +7 -4
- generic_ml_cache_cli-0.6.0/tests/test_session_cli.py +97 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/.gitignore +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/LICENSE +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/NOTICE +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/src/generic_ml_cache_cli/__init__.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/src/generic_ml_cache_cli/__main__.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/src/generic_ml_cache_cli/config.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/fake_client.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/test_config.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/test_discover.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/test_effort.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/test_encrypted_run.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/test_encryption_cli.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/test_interrupt.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/test_models.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/test_passthrough.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/tests/test_robustness.py +0 -0
- {generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.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.6.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.6.0
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: coverage>=7; extra == 'dev'
|
|
30
30
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
@@ -33,7 +33,12 @@ Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
|
33
33
|
Requires-Dist: vulture>=2; extra == 'dev'
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
<p align="center">
|
|
37
|
+
<picture>
|
|
38
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/danielslobozian/generic-ml-cache/main/docs/images/gmlcache-lockup-dark.png">
|
|
39
|
+
<img src="https://raw.githubusercontent.com/danielslobozian/generic-ml-cache/main/docs/images/gmlcache-lockup.png" alt="gmlcache" width="300">
|
|
40
|
+
</picture>
|
|
41
|
+
</p>
|
|
37
42
|
|
|
38
43
|
#### Detached ML Execution Cache — the terminal client
|
|
39
44
|
|
|
@@ -43,6 +48,11 @@ Description-Content-Type: text/markdown
|
|
|
43
48
|
`gmlcache` runs, records, and replays detached ML workloads — record a real client (or
|
|
44
49
|
API) call once, replay it forever by its content key, offline and byte-for-byte.
|
|
45
50
|
|
|
51
|
+
> **Single-user, local — not a gateway.** gmlcache runs on your machine, as you, across the
|
|
52
|
+
> subscriptions and APIs you already hold. It is **not** a multi-user router and **not** a way
|
|
53
|
+
> to share one subscription — see
|
|
54
|
+
> [Positioning](https://github.com/danielslobozian/generic-ml-cache/blob/main/docs/design/positioning.md).
|
|
55
|
+
|
|
46
56
|
<p align="center">
|
|
47
57
|
<img src="https://raw.githubusercontent.com/danielslobozian/generic-ml-cache/main/docs/images/gmlcache-demo.gif" alt="gmlcache: a miss records the real client call; the same command again is served instantly from cache, byte-identical" width="760">
|
|
48
58
|
</p>
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/danielslobozian/generic-ml-cache/main/docs/images/gmlcache-lockup-dark.png">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/danielslobozian/generic-ml-cache/main/docs/images/gmlcache-lockup.png" alt="gmlcache" width="300">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
8
|
#### Detached ML Execution Cache — the terminal client
|
|
4
9
|
|
|
@@ -8,6 +13,11 @@
|
|
|
8
13
|
`gmlcache` runs, records, and replays detached ML workloads — record a real client (or
|
|
9
14
|
API) call once, replay it forever by its content key, offline and byte-for-byte.
|
|
10
15
|
|
|
16
|
+
> **Single-user, local — not a gateway.** gmlcache runs on your machine, as you, across the
|
|
17
|
+
> subscriptions and APIs you already hold. It is **not** a multi-user router and **not** a way
|
|
18
|
+
> to share one subscription — see
|
|
19
|
+
> [Positioning](https://github.com/danielslobozian/generic-ml-cache/blob/main/docs/design/positioning.md).
|
|
20
|
+
|
|
11
21
|
<p align="center">
|
|
12
22
|
<img src="https://raw.githubusercontent.com/danielslobozian/generic-ml-cache/main/docs/images/gmlcache-demo.gif" alt="gmlcache: a miss records the real client call; the same command again is served instantly from cache, byte-identical" width="760">
|
|
13
23
|
</p>
|
|
@@ -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.6.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.6.0", "argcomplete>=3,<4"]
|
|
29
29
|
|
|
30
30
|
[project.urls]
|
|
31
31
|
Homepage = "https://github.com/danielslobozian/generic-ml-cache"
|
|
@@ -195,6 +195,7 @@ def _cmd_run(args: argparse.Namespace) -> int:
|
|
|
195
195
|
persistence_depth=persistence_depth,
|
|
196
196
|
record_on_error=args.record_on_error,
|
|
197
197
|
tags=list(getattr(args, "tag", None) or []),
|
|
198
|
+
session_id=_resolve_session(args),
|
|
198
199
|
)
|
|
199
200
|
|
|
200
201
|
def executable_override(client: str):
|
|
@@ -877,6 +878,71 @@ def _cmd_invalidate(args: argparse.Namespace) -> int:
|
|
|
877
878
|
return 0
|
|
878
879
|
|
|
879
880
|
|
|
881
|
+
# -- sessions ---------------------------------------------------------------
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def _resolve_session(args: argparse.Namespace) -> Optional[str]:
|
|
885
|
+
"""The session id for this run: the --session flag, else GMLCACHE_SESSION. A session
|
|
886
|
+
groups a workflow's calls; it is journal metadata, never part of the cache key."""
|
|
887
|
+
flag = getattr(args, "session", None)
|
|
888
|
+
return flag if flag else (os.environ.get("GMLCACHE_SESSION") or None)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _cmd_session_start(args: argparse.Namespace) -> int:
|
|
892
|
+
import secrets
|
|
893
|
+
|
|
894
|
+
# Print only the id, so it is scriptable: SESSION=$(gmlcache session start)
|
|
895
|
+
print(secrets.token_hex(8))
|
|
896
|
+
return 0
|
|
897
|
+
|
|
898
|
+
|
|
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"}
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def _cmd_session_report(args: argparse.Namespace) -> int:
|
|
904
|
+
store_root = _store_root()
|
|
905
|
+
if store_root is None:
|
|
906
|
+
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)
|
|
911
|
+
|
|
912
|
+
if args.json:
|
|
913
|
+
import json
|
|
914
|
+
|
|
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
|
+
)
|
|
927
|
+
return 0
|
|
928
|
+
|
|
929
|
+
if invocations == 0:
|
|
930
|
+
print(f"no events recorded for session {args.session_id!r}")
|
|
931
|
+
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}")
|
|
938
|
+
return 0
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def _cmd_session(args: argparse.Namespace) -> int:
|
|
942
|
+
print("usage: gmlcache session start | gmlcache session report <id>", file=sys.stderr)
|
|
943
|
+
return 2
|
|
944
|
+
|
|
945
|
+
|
|
880
946
|
def _use_color() -> bool:
|
|
881
947
|
"""Colour only when writing to a real terminal and NO_COLOR is unset, so piped
|
|
882
948
|
or redirected output never carries escape codes (the conventional contract)."""
|
|
@@ -904,34 +970,48 @@ def _paint(text: str, *codes: str) -> str:
|
|
|
904
970
|
|
|
905
971
|
|
|
906
972
|
def render_banner(color: bool = False) -> str:
|
|
907
|
-
"""The boxed gmlcache banner
|
|
908
|
-
|
|
973
|
+
"""The boxed gmlcache banner: the cache mark (four hollow bars; the top one is
|
|
974
|
+
the accent 'hit') beside the title, version, and tagline. Width is derived from
|
|
975
|
+
the content so everything stays aligned. ``color`` adds ANSI; off yields plain."""
|
|
909
976
|
title = "gmlcache"
|
|
910
977
|
ver = __version__
|
|
911
|
-
tag = "record · replay · check ·
|
|
978
|
+
tag = "record · replay · check · sessions · encryption"
|
|
979
|
+
|
|
980
|
+
# The mark: four hollow bars -- thin walls (▏ ▕) around a double-line body (═),
|
|
981
|
+
# widths echoing the logo. The first bar is the accent ("hit"); the rest are dim.
|
|
982
|
+
bars = ["▏" + "═" * n + "▕" for n in (11, 7, 10, 5)]
|
|
983
|
+
bar_w = max(len(b) for b in bars)
|
|
912
984
|
|
|
913
985
|
if color:
|
|
914
|
-
rule = _TEAL
|
|
915
|
-
|
|
916
|
-
vers = _TEAL_BRIGHT # bright-teal version
|
|
917
|
-
sub = _GREY # dim-grey tagline
|
|
918
|
-
off = _RESET
|
|
986
|
+
rule, name, vers, sub, off = _TEAL, _BOLD, _TEAL_BRIGHT, _GREY, _RESET
|
|
987
|
+
bar_colors = [_GREEN, _GREY, _GREY, _GREY]
|
|
919
988
|
else:
|
|
920
989
|
rule = name = vers = sub = off = ""
|
|
990
|
+
bar_colors = ["", "", "", ""]
|
|
991
|
+
|
|
992
|
+
left_pad, gap = " ", " "
|
|
993
|
+
texts = ["", tag, "", ""] # the tagline sits on the second bar row
|
|
921
994
|
|
|
995
|
+
body_w = max(len(left_pad) + bar_w + len(gap) + len(t) for t in texts)
|
|
922
996
|
left_top = f"─ {title} "
|
|
923
997
|
right_top = f" {ver} ─"
|
|
924
|
-
inner = max(len(left_top) + 6 + len(right_top),
|
|
998
|
+
inner = max(len(left_top) + 6 + len(right_top), body_w + 1)
|
|
925
999
|
top_dashes = inner - len(left_top) - len(right_top)
|
|
926
|
-
pad_right = inner - 2 - len(tag)
|
|
927
1000
|
|
|
928
1001
|
top = (
|
|
929
1002
|
f"{rule}┌─ {off}{name}{title}{off}"
|
|
930
1003
|
f"{rule} {'─' * top_dashes} {off}{vers}{ver}{off}{rule} ─┐{off}"
|
|
931
1004
|
)
|
|
932
|
-
|
|
1005
|
+
rows = []
|
|
1006
|
+
for bar, bar_color, text in zip(bars, bar_colors, texts):
|
|
1007
|
+
bar_cell = f"{bar_color}{bar}{off}" + " " * (bar_w - len(bar))
|
|
1008
|
+
used = len(left_pad) + bar_w + len(gap) + len(text)
|
|
1009
|
+
rows.append(
|
|
1010
|
+
f"{rule}│{off}{left_pad}{bar_cell}{gap}{sub}{text}{off}"
|
|
1011
|
+
f"{' ' * (inner - used)}{rule}│{off}"
|
|
1012
|
+
)
|
|
933
1013
|
bot = f"{rule}└{'─' * inner}┘{off}"
|
|
934
|
-
return "\n".join([top,
|
|
1014
|
+
return "\n".join([top, *rows, bot])
|
|
935
1015
|
|
|
936
1016
|
|
|
937
1017
|
class _BannerParser(argparse.ArgumentParser):
|
|
@@ -1056,6 +1136,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1056
1136
|
run.add_argument(
|
|
1057
1137
|
"--token", help="encryption token for an encrypted store (or set GMLCACHE_TOKEN)"
|
|
1058
1138
|
)
|
|
1139
|
+
run.add_argument(
|
|
1140
|
+
"--session", help="group this run under a session id (or set GMLCACHE_SESSION)"
|
|
1141
|
+
)
|
|
1059
1142
|
run.add_argument(
|
|
1060
1143
|
"--timeout", type=float, default=None, help="seconds before the real call is killed"
|
|
1061
1144
|
)
|
|
@@ -1242,6 +1325,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1242
1325
|
invalidatep.add_argument("--yes", action="store_true", help="confirm the irreversible wipe")
|
|
1243
1326
|
invalidatep.set_defaults(func=_cmd_invalidate)
|
|
1244
1327
|
|
|
1328
|
+
session = sub.add_parser("session", help="group a workflow's runs under a session id")
|
|
1329
|
+
session_sub = session.add_subparsers(dest="session_command")
|
|
1330
|
+
session_start = session_sub.add_parser("start", help="generate a new session id and print it")
|
|
1331
|
+
session_start.set_defaults(func=_cmd_session_start)
|
|
1332
|
+
session_report = session_sub.add_parser("report", help="summarise a session's activity")
|
|
1333
|
+
session_report.add_argument("session_id", help="the session id to report on")
|
|
1334
|
+
session_report.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
1335
|
+
session_report.set_defaults(func=_cmd_session_report)
|
|
1336
|
+
session.set_defaults(func=_cmd_session)
|
|
1337
|
+
|
|
1245
1338
|
init = sub.add_parser(
|
|
1246
1339
|
"init",
|
|
1247
1340
|
help="create the config file in the default location (if absent), then show the store",
|
|
@@ -118,7 +118,13 @@ def _isolate_config(monkeypatch, tmp_path):
|
|
|
118
118
|
monkeypatch.setenv("GMLCACHE_CONFIG", str(tmp_path / "no-such-config.ini"))
|
|
119
119
|
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "xdg-data"))
|
|
120
120
|
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "localappdata"))
|
|
121
|
-
for var in (
|
|
121
|
+
for var in (
|
|
122
|
+
"GMLCACHE_MODE",
|
|
123
|
+
"GMLCACHE_PERSIST",
|
|
124
|
+
"GMLCACHE_TIMEOUT",
|
|
125
|
+
"GMLCACHE_TOKEN",
|
|
126
|
+
"GMLCACHE_SESSION",
|
|
127
|
+
):
|
|
122
128
|
monkeypatch.delenv(var, raising=False)
|
|
123
129
|
|
|
124
130
|
|
|
@@ -134,8 +134,11 @@ def test_run_rejects_retired_location_flags(tmp_path):
|
|
|
134
134
|
def test_render_banner_lines_align():
|
|
135
135
|
from generic_ml_cache_cli.cli import render_banner
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
lines = render_banner(color=False).splitlines()
|
|
138
|
+
widths = {len(line) for line in lines}
|
|
139
|
+
assert len(widths) == 1 # every box line (top, four mark rows, bottom) is one width
|
|
140
|
+
assert len(lines) == 6 # the mark adds four bar rows inside the box
|
|
141
|
+
assert "═" in render_banner(color=False) # the hollow mark renders
|
|
139
142
|
|
|
140
143
|
|
|
141
144
|
def test_render_banner_color_is_opt_in():
|
|
@@ -162,7 +165,7 @@ def test_bare_invocation_prints_help_not_an_error(capsys):
|
|
|
162
165
|
out = capsys.readouterr().out
|
|
163
166
|
assert rc == 0
|
|
164
167
|
assert "gmlcache" in out
|
|
165
|
-
assert "record · replay · check ·
|
|
168
|
+
assert "record · replay · check · sessions · encryption" in out
|
|
166
169
|
assert "usage:" in out
|
|
167
170
|
|
|
168
171
|
|
|
@@ -176,7 +179,7 @@ def test_help_flag_shows_the_banner(capsys):
|
|
|
176
179
|
with pytest.raises(SystemExit) as excinfo:
|
|
177
180
|
main(["-h"])
|
|
178
181
|
assert excinfo.value.code == 0
|
|
179
|
-
assert "record · replay · check ·
|
|
182
|
+
assert "record · replay · check · sessions · encryption" in capsys.readouterr().out
|
|
180
183
|
|
|
181
184
|
|
|
182
185
|
# --- list (grouped by client/model) ---------------------------------------
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""CLI tests for sessions: run --session / GMLCACHE_SESSION and `session start`."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import glob
|
|
8
|
+
import sqlite3
|
|
9
|
+
|
|
10
|
+
from generic_ml_cache_cli.cli import main
|
|
11
|
+
|
|
12
|
+
_RUN = ["run", "--client", "fake", "--model", "m1", "--effort", "high", "--prompt", "STDOUT hi"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _session_ids(tmp_path):
|
|
16
|
+
dbs = glob.glob(str(tmp_path / "**" / "registry.sqlite3"), recursive=True)
|
|
17
|
+
if not dbs:
|
|
18
|
+
return []
|
|
19
|
+
conn = sqlite3.connect(dbs[0])
|
|
20
|
+
try:
|
|
21
|
+
return [r[0] for r in conn.execute("SELECT session_id FROM access_events ORDER BY id")]
|
|
22
|
+
finally:
|
|
23
|
+
conn.close()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_run_with_session_flag_records_the_session_id(tmp_path, capsys):
|
|
27
|
+
assert main(_RUN + ["--session", "workflow-1"]) == 0
|
|
28
|
+
capsys.readouterr()
|
|
29
|
+
assert _session_ids(tmp_path) == ["workflow-1"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_run_reads_session_from_env(tmp_path, capsys, monkeypatch):
|
|
33
|
+
monkeypatch.setenv("GMLCACHE_SESSION", "env-session")
|
|
34
|
+
assert main(_RUN) == 0
|
|
35
|
+
capsys.readouterr()
|
|
36
|
+
assert _session_ids(tmp_path) == ["env-session"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_flag_wins_over_env(tmp_path, capsys, monkeypatch):
|
|
40
|
+
monkeypatch.setenv("GMLCACHE_SESSION", "env-session")
|
|
41
|
+
assert main(_RUN + ["--session", "flag-session"]) == 0
|
|
42
|
+
capsys.readouterr()
|
|
43
|
+
assert _session_ids(tmp_path) == ["flag-session"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_run_without_a_session_records_null(tmp_path, capsys):
|
|
47
|
+
assert main(_RUN) == 0
|
|
48
|
+
capsys.readouterr()
|
|
49
|
+
assert _session_ids(tmp_path) == [None]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_session_start_prints_a_scriptable_id(capsys):
|
|
53
|
+
assert main(["session", "start"]) == 0
|
|
54
|
+
out = capsys.readouterr().out.strip()
|
|
55
|
+
assert out and " " not in out # a single bare id, usable as $(gmlcache session start)
|
|
56
|
+
# two starts yield distinct ids
|
|
57
|
+
main(["session", "start"])
|
|
58
|
+
assert capsys.readouterr().out.strip() != out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_bare_session_shows_usage(capsys):
|
|
62
|
+
assert main(["session"]) == 2
|
|
63
|
+
assert "session start" in capsys.readouterr().err
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_session_report_rolls_up_invocations_executions_hits(capsys):
|
|
67
|
+
run = _RUN + ["--session", "wf"]
|
|
68
|
+
main(run) # miss -> record (a real execution)
|
|
69
|
+
main(run) # same input -> hit (no execution)
|
|
70
|
+
capsys.readouterr()
|
|
71
|
+
|
|
72
|
+
assert main(["session", "report", "wf"]) == 0
|
|
73
|
+
out = capsys.readouterr().out
|
|
74
|
+
assert "invocations : 2" in out
|
|
75
|
+
assert "executions : 1" in out
|
|
76
|
+
assert "hits : 1" in out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_session_report_json(capsys):
|
|
80
|
+
import json
|
|
81
|
+
|
|
82
|
+
main(_RUN + ["--session", "wf"])
|
|
83
|
+
capsys.readouterr()
|
|
84
|
+
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
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_session_report_unknown_session_is_clean(capsys):
|
|
96
|
+
assert main(["session", "report", "nope"]) == 0
|
|
97
|
+
assert "no events" in capsys.readouterr().out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/src/generic_ml_cache_cli/__init__.py
RENAMED
|
File without changes
|
{generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.0}/src/generic_ml_cache_cli/__main__.py
RENAMED
|
File without changes
|
{generic_ml_cache_cli-0.5.0 → generic_ml_cache_cli-0.6.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
|