generic-ml-cache-cli 0.8.0__tar.gz → 0.9.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.8.0 → generic_ml_cache_cli-0.9.0}/PKG-INFO +3 -2
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/README.md +1 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/pyproject.toml +2 -2
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/src/generic_ml_cache_cli/cli.py +166 -65
- generic_ml_cache_cli-0.9.0/tests/test_alias.py +118 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/.gitignore +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/LICENSE +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/NOTICE +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/src/generic_ml_cache_cli/__init__.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/src/generic_ml_cache_cli/__main__.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/src/generic_ml_cache_cli/async_jobs.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/src/generic_ml_cache_cli/config.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/conftest.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/fake_client.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_async_jobs.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_cli.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_config.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_discover.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_effort.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_encrypted_run.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_encryption_cli.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_interrupt.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_models.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_passthrough.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_robustness.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/tests/test_session_cli.py +0 -0
- {generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.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.9.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.9.0
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: coverage>=7; extra == 'dev'
|
|
30
30
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
@@ -78,6 +78,7 @@ This installs the `gmlcache` command and pulls in the engine,
|
|
|
78
78
|
gmlcache run --client claude --model sonnet --prompt "…" # record on a miss, replay on a hit
|
|
79
79
|
gmlcache check --client claude --model sonnet --prompt "…" # forecast: is this exact call cached?
|
|
80
80
|
gmlcache run --client claude --model sonnet --prompt "…" --detach # run detached → prints an execution id
|
|
81
|
+
gmlcache alias claude -- -p "…" --model sonnet # thin wrapper: cache a raw native call
|
|
81
82
|
gmlcache execution watch <id> # follow a detached run's live progress
|
|
82
83
|
gmlcache session report <id> # token usage by provider/model for a workflow
|
|
83
84
|
gmlcache encrypt # encrypt the whole store at rest
|
|
@@ -43,6 +43,7 @@ This installs the `gmlcache` command and pulls in the engine,
|
|
|
43
43
|
gmlcache run --client claude --model sonnet --prompt "…" # record on a miss, replay on a hit
|
|
44
44
|
gmlcache check --client claude --model sonnet --prompt "…" # forecast: is this exact call cached?
|
|
45
45
|
gmlcache run --client claude --model sonnet --prompt "…" --detach # run detached → prints an execution id
|
|
46
|
+
gmlcache alias claude -- -p "…" --model sonnet # thin wrapper: cache a raw native call
|
|
46
47
|
gmlcache execution watch <id> # follow a detached run's live progress
|
|
47
48
|
gmlcache session report <id> # token usage by provider/model for a workflow
|
|
48
49
|
gmlcache encrypt # encrypt the whole store at rest
|
|
@@ -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.9.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.9.0", "argcomplete>=3,<4"]
|
|
29
29
|
|
|
30
30
|
[project.urls]
|
|
31
31
|
Homepage = "https://github.com/danielslobozian/generic-ml-cache"
|
|
@@ -53,6 +53,9 @@ from generic_ml_cache_cli import async_jobs
|
|
|
53
53
|
from generic_ml_cache_core.application.port.inbound.run_managed_local_execution_command import (
|
|
54
54
|
RunManagedLocalExecutionCommand,
|
|
55
55
|
)
|
|
56
|
+
from generic_ml_cache_core.application.port.inbound.run_passthrough_execution_command import (
|
|
57
|
+
RunPassthroughExecutionCommand,
|
|
58
|
+
)
|
|
56
59
|
from generic_ml_cache_core.application.port.out.base import ClientAdapter
|
|
57
60
|
from generic_ml_cache_core.common.errors import (
|
|
58
61
|
CacheError,
|
|
@@ -185,13 +188,7 @@ def _resolve_managed_run(args: argparse.Namespace):
|
|
|
185
188
|
settings = config.resolve_settings(
|
|
186
189
|
file_cfg, mode_flag=args.mode, persist_flag=args.persist, timeout_flag=args.timeout
|
|
187
190
|
)
|
|
188
|
-
|
|
189
|
-
if args.offline:
|
|
190
|
-
cache_mode = CacheMode.OFFLINE
|
|
191
|
-
elif args.force:
|
|
192
|
-
cache_mode = CacheMode.REFRESH
|
|
193
|
-
else:
|
|
194
|
-
cache_mode = CacheMode(str(settings["mode"][0]))
|
|
191
|
+
cache_mode = _resolve_cache_mode(args, settings)
|
|
195
192
|
|
|
196
193
|
spec = {
|
|
197
194
|
"client": args.client,
|
|
@@ -242,31 +239,29 @@ def _spec_executable_override(spec: dict):
|
|
|
242
239
|
return lambda client: executable
|
|
243
240
|
|
|
244
241
|
|
|
245
|
-
def
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
242
|
+
def _resolve_cache_mode(args: argparse.Namespace, settings: dict) -> CacheMode:
|
|
243
|
+
"""The cache mode for a run: --offline / --force are explicit flags and win over
|
|
244
|
+
the resolved (config/env/default) mode. Shared by managed `run` and `alias`."""
|
|
245
|
+
if args.offline:
|
|
246
|
+
return CacheMode.OFFLINE
|
|
247
|
+
if args.force:
|
|
248
|
+
return CacheMode.REFRESH
|
|
249
|
+
return CacheMode(str(settings["mode"][0]))
|
|
251
250
|
|
|
252
|
-
if getattr(args, "detach", False):
|
|
253
|
-
return _submit_detached(spec, store_root, token)
|
|
254
251
|
|
|
255
|
-
|
|
252
|
+
def _run_cached_execution(execute):
|
|
253
|
+
"""Run a wired ``execute()`` call, translating the failure modes shared by every
|
|
254
|
+
cached command into ``(None, exit_code)``; on success returns ``(execution, None)``.
|
|
255
|
+
|
|
256
|
+
Centralising the ladder keeps `run` and `alias` byte-identical on errors and means
|
|
257
|
+
the rarely-hit branches (interrupt, timeout) are covered once, by the `run` tests."""
|
|
256
258
|
try:
|
|
257
|
-
|
|
258
|
-
store_root,
|
|
259
|
-
_spec_executable_override(spec),
|
|
260
|
-
spec["timeout"],
|
|
261
|
-
encryption_token=token,
|
|
262
|
-
stream_path=getattr(args, "stream", None),
|
|
263
|
-
)
|
|
264
|
-
execution = wired.run_managed.execute(command)
|
|
259
|
+
return execute(), None
|
|
265
260
|
except RunInterrupted as exc:
|
|
266
261
|
# A requested stop, not a failure: nothing was recorded. Exit 130 is the
|
|
267
262
|
# conventional "terminated by Ctrl-C".
|
|
268
263
|
print(f"gmlc: {exc}", file=sys.stderr)
|
|
269
|
-
return 130
|
|
264
|
+
return None, 130
|
|
270
265
|
except subprocess.TimeoutExpired as exc:
|
|
271
266
|
# The real call ran past --timeout and was killed before any record. Exit
|
|
272
267
|
# 124 is the timeout(1) convention, distinct from miss (3) and error (4).
|
|
@@ -274,28 +269,102 @@ def _cmd_run(args: argparse.Namespace) -> int:
|
|
|
274
269
|
f"gmlc: real call exceeded the {exc.timeout}s timeout and was killed; nothing recorded",
|
|
275
270
|
file=sys.stderr,
|
|
276
271
|
)
|
|
277
|
-
return 124
|
|
272
|
+
return None, 124
|
|
278
273
|
except CacheMiss as exc:
|
|
279
274
|
print(f"gmlc: {exc}", file=sys.stderr)
|
|
280
|
-
return 3
|
|
275
|
+
return None, 3
|
|
281
276
|
except (EncryptionTokenRequired, WrongEncryptionToken) as exc:
|
|
282
277
|
print(f"gmlc: {exc} (set --token or GMLCACHE_TOKEN)", file=sys.stderr)
|
|
283
|
-
return 4
|
|
278
|
+
return None, 4
|
|
284
279
|
except CacheError as exc:
|
|
280
|
+
print(f"gmlc: {exc}", file=sys.stderr)
|
|
281
|
+
return None, 4
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _relay_execution(execution: MlExecution) -> int:
|
|
285
|
+
"""Reproduce a (live or replayed) call's stdout, stderr and exit code exactly --
|
|
286
|
+
the quiet-mode fidelity contract shared by `run` and `alias`."""
|
|
287
|
+
sys.stdout.write(_artifact_text(execution, ArtifactType.STDOUT))
|
|
288
|
+
sys.stdout.flush()
|
|
289
|
+
sys.stderr.write(_artifact_text(execution, ArtifactType.STDERR))
|
|
290
|
+
sys.stderr.flush()
|
|
291
|
+
return _run_exit_code(execution)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
295
|
+
try:
|
|
296
|
+
spec, store_root, token = _resolve_managed_run(args)
|
|
297
|
+
except ConfigError as exc:
|
|
285
298
|
print(f"gmlc: {exc}", file=sys.stderr)
|
|
286
299
|
return 4
|
|
287
300
|
|
|
301
|
+
if getattr(args, "detach", False):
|
|
302
|
+
return _submit_detached(spec, store_root, token)
|
|
303
|
+
|
|
304
|
+
command = _command_from_spec(spec)
|
|
305
|
+
execution, error = _run_cached_execution(
|
|
306
|
+
lambda: build_use_cases(
|
|
307
|
+
store_root,
|
|
308
|
+
_spec_executable_override(spec),
|
|
309
|
+
spec["timeout"],
|
|
310
|
+
encryption_token=token,
|
|
311
|
+
stream_path=getattr(args, "stream", None),
|
|
312
|
+
).run_managed.execute(command)
|
|
313
|
+
)
|
|
314
|
+
if error is not None:
|
|
315
|
+
return error
|
|
316
|
+
|
|
288
317
|
# Materialise captured files into the cwd, exactly as the real client would.
|
|
289
318
|
_apply_output_files(execution, Path.cwd())
|
|
290
319
|
|
|
291
320
|
if getattr(args, "json", False):
|
|
292
321
|
return _print_run_json(execution, command)
|
|
293
322
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
323
|
+
return _relay_execution(execution)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _cmd_alias(args: argparse.Namespace) -> int:
|
|
327
|
+
"""`alias`: the thin native-client wrapper. Everything after the client is an
|
|
328
|
+
opaque native-argument tail, forwarded verbatim and keyed (by fingerprint) as
|
|
329
|
+
the cache identity. No isolation and no file capture -- a replay reproduces the
|
|
330
|
+
native call's stdout/stderr/exit, exactly as the default `run` does in quiet mode.
|
|
331
|
+
gmlcache's own options precede the client; the tail belongs to the native client."""
|
|
332
|
+
native_args = list(getattr(args, "native_args", None) or [])
|
|
333
|
+
# Accept an explicit `--` before the tail (`alias claude -- -p hi`) so a tail
|
|
334
|
+
# that opens with a dash never has to fight argparse; it is not a native arg.
|
|
335
|
+
if native_args and native_args[0] == "--":
|
|
336
|
+
native_args = native_args[1:]
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
file_cfg = config.load()
|
|
340
|
+
settings = config.resolve_settings(
|
|
341
|
+
file_cfg, mode_flag=args.mode, persist_flag=args.persist, timeout_flag=args.timeout
|
|
342
|
+
)
|
|
343
|
+
executable = config.executable_for(file_cfg, args.client, flag=args.executable)
|
|
344
|
+
except ConfigError as exc:
|
|
345
|
+
print(f"gmlc: {exc}", file=sys.stderr)
|
|
346
|
+
return 4
|
|
347
|
+
|
|
348
|
+
command = RunPassthroughExecutionCommand(
|
|
349
|
+
client=args.client,
|
|
350
|
+
native_args=native_args,
|
|
351
|
+
cache_mode=_resolve_cache_mode(args, settings),
|
|
352
|
+
persistence_depth=PersistenceDepth(str(settings["persist"][0])),
|
|
353
|
+
record_on_error=bool(args.record_on_error),
|
|
354
|
+
session_id=_resolve_session(args),
|
|
355
|
+
)
|
|
356
|
+
store_root = Path(str(settings["store"][0]))
|
|
357
|
+
execution, error = _run_cached_execution(
|
|
358
|
+
lambda: build_use_cases(
|
|
359
|
+
store_root,
|
|
360
|
+
lambda _client: executable,
|
|
361
|
+
settings["timeout"][0],
|
|
362
|
+
encryption_token=_resolve_token(args),
|
|
363
|
+
).run_passthrough.execute(command)
|
|
364
|
+
)
|
|
365
|
+
if error is not None:
|
|
366
|
+
return error
|
|
367
|
+
return _relay_execution(execution)
|
|
299
368
|
|
|
300
369
|
|
|
301
370
|
def _submit_detached(spec: dict, store_root: Path, token: Optional[str]) -> int:
|
|
@@ -1506,6 +1575,44 @@ class _BannerParser(argparse.ArgumentParser):
|
|
|
1506
1575
|
return render_banner(_use_color()) + "\n\n" + super().format_help()
|
|
1507
1576
|
|
|
1508
1577
|
|
|
1578
|
+
def _add_shared_run_options(parser: argparse.ArgumentParser) -> None:
|
|
1579
|
+
"""Add the run-resolution options shared by `run` and `alias` (mode, persistence,
|
|
1580
|
+
record policy, the executable seam, encryption token, session, timeout). Both
|
|
1581
|
+
commands resolve a cached call the same way, so they share this surface verbatim."""
|
|
1582
|
+
parser.add_argument(
|
|
1583
|
+
"--mode",
|
|
1584
|
+
choices=[m.value for m in CacheMode],
|
|
1585
|
+
default=None,
|
|
1586
|
+
help="resolution mode (default: cache, or config/env)",
|
|
1587
|
+
)
|
|
1588
|
+
parser.add_argument(
|
|
1589
|
+
"--persist",
|
|
1590
|
+
choices=[d.value for d in PersistenceDepth],
|
|
1591
|
+
default=None,
|
|
1592
|
+
help=(
|
|
1593
|
+
"how much to keep: meter (usage only, never replays), cache (+output, "
|
|
1594
|
+
"the default), or dataset (+input) (default: cache, or config/env)"
|
|
1595
|
+
),
|
|
1596
|
+
)
|
|
1597
|
+
parser.add_argument("--offline", action="store_true", help="shortcut for --mode offline")
|
|
1598
|
+
parser.add_argument("--force", action="store_true", help="shortcut for --mode refresh")
|
|
1599
|
+
parser.add_argument(
|
|
1600
|
+
"--record-on-error",
|
|
1601
|
+
action="store_true",
|
|
1602
|
+
help="also cache a call that fails (non-zero exit); default is to store only successes",
|
|
1603
|
+
)
|
|
1604
|
+
parser.add_argument("--executable", help="override the client executable (the seam)")
|
|
1605
|
+
parser.add_argument(
|
|
1606
|
+
"--token", help="encryption token for an encrypted store (or set GMLCACHE_TOKEN)"
|
|
1607
|
+
)
|
|
1608
|
+
parser.add_argument(
|
|
1609
|
+
"--session", help="group this run under a session id (or set GMLCACHE_SESSION)"
|
|
1610
|
+
)
|
|
1611
|
+
parser.add_argument(
|
|
1612
|
+
"--timeout", type=float, default=None, help="seconds before the real call is killed"
|
|
1613
|
+
)
|
|
1614
|
+
|
|
1615
|
+
|
|
1509
1616
|
def build_parser() -> argparse.ArgumentParser:
|
|
1510
1617
|
parser = _BannerParser(
|
|
1511
1618
|
prog="gmlcache",
|
|
@@ -1596,38 +1703,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1596
1703
|
"as the workflow engine reading usage. Files are still written to the cwd."
|
|
1597
1704
|
),
|
|
1598
1705
|
)
|
|
1599
|
-
run
|
|
1600
|
-
"--mode",
|
|
1601
|
-
choices=[m.value for m in CacheMode],
|
|
1602
|
-
default=None,
|
|
1603
|
-
help="resolution mode (default: cache, or config/env)",
|
|
1604
|
-
)
|
|
1605
|
-
run.add_argument(
|
|
1606
|
-
"--persist",
|
|
1607
|
-
choices=[d.value for d in PersistenceDepth],
|
|
1608
|
-
default=None,
|
|
1609
|
-
help=(
|
|
1610
|
-
"how much to keep: meter (usage only, never replays), cache (+output, "
|
|
1611
|
-
"the default), or dataset (+input) (default: cache, or config/env)"
|
|
1612
|
-
),
|
|
1613
|
-
)
|
|
1614
|
-
run.add_argument("--offline", action="store_true", help="shortcut for --mode offline")
|
|
1615
|
-
run.add_argument("--force", action="store_true", help="shortcut for --mode refresh")
|
|
1616
|
-
run.add_argument(
|
|
1617
|
-
"--record-on-error",
|
|
1618
|
-
action="store_true",
|
|
1619
|
-
help="also cache a call that fails (non-zero exit); default is to store only successes",
|
|
1620
|
-
)
|
|
1621
|
-
run.add_argument("--executable", help="override the client executable (the seam)")
|
|
1622
|
-
run.add_argument(
|
|
1623
|
-
"--token", help="encryption token for an encrypted store (or set GMLCACHE_TOKEN)"
|
|
1624
|
-
)
|
|
1625
|
-
run.add_argument(
|
|
1626
|
-
"--session", help="group this run under a session id (or set GMLCACHE_SESSION)"
|
|
1627
|
-
)
|
|
1628
|
-
run.add_argument(
|
|
1629
|
-
"--timeout", type=float, default=None, help="seconds before the real call is killed"
|
|
1630
|
-
)
|
|
1706
|
+
_add_shared_run_options(run)
|
|
1631
1707
|
run.add_argument(
|
|
1632
1708
|
"-v",
|
|
1633
1709
|
"--verbose",
|
|
@@ -1656,6 +1732,31 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1656
1732
|
)
|
|
1657
1733
|
run.set_defaults(func=_cmd_run)
|
|
1658
1734
|
|
|
1735
|
+
aliasp = sub.add_parser(
|
|
1736
|
+
"alias",
|
|
1737
|
+
help=(
|
|
1738
|
+
"thin native-client wrapper: cache a raw native invocation -- everything "
|
|
1739
|
+
"after the client is passed to it verbatim and is the cache identity"
|
|
1740
|
+
),
|
|
1741
|
+
description=(
|
|
1742
|
+
"Run a client through the cache as a thin wrapper. gmlcache's own options "
|
|
1743
|
+
"(below) come BEFORE the client; everything after the client is forwarded to "
|
|
1744
|
+
"it verbatim and keyed (by fingerprint) as the cache identity. No options are "
|
|
1745
|
+
"modelled or auto-completed. A replay reproduces the native call's stdout, "
|
|
1746
|
+
"stderr and exit; generated files are written by the live call only (no capture). "
|
|
1747
|
+
"Drop-in: alias claude='gmlcache alias claude'."
|
|
1748
|
+
),
|
|
1749
|
+
)
|
|
1750
|
+
_add_shared_run_options(aliasp)
|
|
1751
|
+
aliasp.add_argument("client", choices=registered_names(), help="the native client to wrap")
|
|
1752
|
+
aliasp.add_argument(
|
|
1753
|
+
"native_args",
|
|
1754
|
+
nargs=argparse.REMAINDER,
|
|
1755
|
+
metavar="-- NATIVE_ARGS",
|
|
1756
|
+
help="the native client arguments, forwarded verbatim (this is the cache identity)",
|
|
1757
|
+
)
|
|
1758
|
+
aliasp.set_defaults(func=_cmd_alias)
|
|
1759
|
+
|
|
1659
1760
|
# Internal: no help= so it never appears as a help row; metavar hides it from the list.
|
|
1660
1761
|
worker = sub.add_parser("__worker")
|
|
1661
1762
|
worker.add_argument("store_root")
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Alias mode (`gmlcache alias <client> -- <native args>`).
|
|
4
|
+
|
|
5
|
+
The thin native-client wrapper: everything after the client is an opaque tail,
|
|
6
|
+
forwarded verbatim and keyed (by fingerprint) as the cache identity. No isolation,
|
|
7
|
+
no file capture -- a replay reproduces the native call's stdout/stderr/exit.
|
|
8
|
+
|
|
9
|
+
The ``fake`` adapter's executable is the Python interpreter, so a native tail of
|
|
10
|
+
``-c <snippet>`` runs that snippet -- a portable stand-in for a real native call.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from generic_ml_cache_core.common.errors import CacheError
|
|
16
|
+
|
|
17
|
+
from generic_ml_cache_cli.cli import _run_cached_execution, main
|
|
18
|
+
|
|
19
|
+
# `fake`'s executable is sys.executable (python), so these tails run python directly.
|
|
20
|
+
# Write raw bytes via the stdout/stderr buffer so the output is identical on every OS
|
|
21
|
+
# (text-mode Python on Windows would translate "\n" -> "\r\n"); the cache relays bytes
|
|
22
|
+
# verbatim, so this is also the faithful thing to assert.
|
|
23
|
+
_HELLO = ["-c", "import sys; sys.stdout.buffer.write(b'hello\\n')"]
|
|
24
|
+
_BYE = ["-c", "import sys; sys.stdout.buffer.write(b'bye\\n')"]
|
|
25
|
+
_FAIL = ["-c", "import sys; sys.stderr.buffer.write(b'boom\\n'); sys.exit(7)"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_alias_runs_the_native_call_and_relays_output(capsys):
|
|
29
|
+
assert main(["alias", "fake", *_HELLO]) == 0
|
|
30
|
+
assert capsys.readouterr().out == "hello\n"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_alias_replays_an_identical_tail_from_cache(capsys):
|
|
34
|
+
# First call records; a non-deterministic source proves the second is a replay.
|
|
35
|
+
tail = ["-c", "import os; print(os.getpid())"]
|
|
36
|
+
assert main(["alias", "fake", *tail]) == 0
|
|
37
|
+
first = capsys.readouterr().out
|
|
38
|
+
assert main(["alias", "fake", *tail]) == 0
|
|
39
|
+
second = capsys.readouterr().out
|
|
40
|
+
assert first == second # a fresh run would print a different pid
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_alias_keys_on_the_raw_tail(capsys):
|
|
44
|
+
assert main(["alias", "fake", *_HELLO]) == 0
|
|
45
|
+
assert capsys.readouterr().out == "hello\n"
|
|
46
|
+
# A different tail is a different identity -> its own call, its own output.
|
|
47
|
+
assert main(["alias", "fake", *_BYE]) == 0
|
|
48
|
+
assert capsys.readouterr().out == "bye\n"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_alias_offline_miss_is_exit_3(capsys):
|
|
52
|
+
rc = main(["alias", "--offline", "fake", "-c", "print('never recorded')"])
|
|
53
|
+
assert rc == 3
|
|
54
|
+
assert "offline miss" in capsys.readouterr().err
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_alias_force_refreshes_a_recorded_call(capsys):
|
|
58
|
+
tail = ["-c", "import os; print(os.getpid())"]
|
|
59
|
+
assert main(["alias", "fake", *tail]) == 0
|
|
60
|
+
recorded = capsys.readouterr().out
|
|
61
|
+
assert main(["alias", "--force", "fake", *tail]) == 0
|
|
62
|
+
refreshed = capsys.readouterr().out
|
|
63
|
+
assert recorded != refreshed # --force re-ran the native call
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_alias_relays_a_native_failure_verbatim(capsys):
|
|
67
|
+
# The native exit code and stderr surface exactly; nothing is rewritten.
|
|
68
|
+
assert main(["alias", "fake", *_FAIL]) == 7
|
|
69
|
+
assert capsys.readouterr().err == "boom\n"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_alias_never_serves_a_recorded_failure_as_a_hit(capsys):
|
|
73
|
+
# --record-on-error keeps the failed call as history, but the cache never serves
|
|
74
|
+
# a failure as a hit -- a later offline call still misses (exit 3).
|
|
75
|
+
assert main(["alias", "--record-on-error", "fake", *_FAIL]) == 7
|
|
76
|
+
capsys.readouterr()
|
|
77
|
+
assert main(["alias", "--offline", "fake", *_FAIL]) == 3
|
|
78
|
+
assert "offline miss" in capsys.readouterr().err
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_alias_accepts_an_explicit_double_dash_separator(capsys):
|
|
82
|
+
# `alias fake -- <tail>` strips the separator; identity matches the bare form.
|
|
83
|
+
assert main(["alias", "fake", "--", *_HELLO]) == 0
|
|
84
|
+
assert capsys.readouterr().out == "hello\n"
|
|
85
|
+
# Same call without the separator hits the cache (offline proves it).
|
|
86
|
+
assert main(["alias", "--offline", "fake", *_HELLO]) == 0
|
|
87
|
+
assert capsys.readouterr().out == "hello\n"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_alias_does_not_interpret_native_flags_as_its_own(capsys):
|
|
91
|
+
# `--offline` AFTER the client is native (here, ignored by python -c), not a
|
|
92
|
+
# gmlcache flag: the call runs and records rather than failing as an offline miss.
|
|
93
|
+
assert (
|
|
94
|
+
main(["alias", "fake", "-c", "import sys; sys.stdout.buffer.write(b'ok\\n')", "--offline"])
|
|
95
|
+
== 0
|
|
96
|
+
)
|
|
97
|
+
assert capsys.readouterr().out == "ok\n"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_alias_strips_a_redundant_double_dash_from_the_tail(capsys):
|
|
101
|
+
# argparse's REMAINDER already eats the first `--`; a second literal `--` (a tail
|
|
102
|
+
# that genuinely begins with `--`) is stripped by alias itself, so it is forwarded
|
|
103
|
+
# cleanly. `python -- -c <snippet>` would fail, so a clean `ok` proves the strip.
|
|
104
|
+
rc = main(["alias", "fake", "--", "--", "-c", "import sys; sys.stdout.buffer.write(b'ok\\n')"])
|
|
105
|
+
assert rc == 0
|
|
106
|
+
assert capsys.readouterr().out == "ok\n"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_run_cached_execution_maps_a_cache_error_to_exit_4(capsys):
|
|
110
|
+
# The shared ladder's catch-all: any CacheError that is not a more specific
|
|
111
|
+
# subclass becomes a clean exit 4, never an uncaught traceback.
|
|
112
|
+
def boom():
|
|
113
|
+
raise CacheError("something went wrong")
|
|
114
|
+
|
|
115
|
+
execution, code = _run_cached_execution(boom)
|
|
116
|
+
assert execution is None
|
|
117
|
+
assert code == 4
|
|
118
|
+
assert "something went wrong" in capsys.readouterr().err
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/src/generic_ml_cache_cli/__init__.py
RENAMED
|
File without changes
|
{generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/src/generic_ml_cache_cli/__main__.py
RENAMED
|
File without changes
|
{generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.0}/src/generic_ml_cache_cli/async_jobs.py
RENAMED
|
File without changes
|
{generic_ml_cache_cli-0.8.0 → generic_ml_cache_cli-0.9.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
|
|
File without changes
|
|
File without changes
|