generic-ml-cache-cli 0.2.0__py3-none-any.whl
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/__init__.py +28 -0
- generic_ml_cache_cli/__main__.py +10 -0
- generic_ml_cache_cli/cli.py +869 -0
- generic_ml_cache_cli/config.py +347 -0
- generic_ml_cache_cli-0.2.0.dist-info/METADATA +96 -0
- generic_ml_cache_cli-0.2.0.dist-info/RECORD +10 -0
- generic_ml_cache_cli-0.2.0.dist-info/WHEEL +4 -0
- generic_ml_cache_cli-0.2.0.dist-info/entry_points.txt +2 -0
- generic_ml_cache_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
- generic_ml_cache_cli-0.2.0.dist-info/licenses/NOTICE +8 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Optional configuration: defaults for ``run``, discovered from one INI file.
|
|
4
|
+
|
|
5
|
+
Three rules keep this predictable:
|
|
6
|
+
|
|
7
|
+
* **Opt-in.** :func:`load` reads the file only if it already exists and *never*
|
|
8
|
+
writes it -- the cache works with no file present. :func:`write_default_config`
|
|
9
|
+
(the ``gmlcache init`` command) writes one on explicit request, never on
|
|
10
|
+
install or first run.
|
|
11
|
+
* **Overridable, with explicit precedence.** For ``mode`` and ``timeout`` the
|
|
12
|
+
winner is, in order: a CLI flag, an environment variable, the config file, the
|
|
13
|
+
built-in default. The ``store`` location is the exception -- config file or
|
|
14
|
+
built-in default only, with **no flag and no environment** -- because where the
|
|
15
|
+
stored executions live is the cache's own concern, not a per-call knob.
|
|
16
|
+
* **Zero dependencies.** The format is INI (stdlib :mod:`configparser`) and the
|
|
17
|
+
per-user location is resolved inline, so nothing beyond the standard library is
|
|
18
|
+
needed on any supported Python.
|
|
19
|
+
|
|
20
|
+
Location (override everything with ``GMLCACHE_CONFIG=/path/to/file``):
|
|
21
|
+
|
|
22
|
+
* Windows -- ``%APPDATA%\\generic-ml-cache\\config.ini``
|
|
23
|
+
* otherwise -- ``$XDG_CONFIG_HOME/generic-ml-cache/config.ini`` (or
|
|
24
|
+
``~/.config/generic-ml-cache/config.ini``)
|
|
25
|
+
|
|
26
|
+
File shape::
|
|
27
|
+
|
|
28
|
+
[defaults]
|
|
29
|
+
mode = cache
|
|
30
|
+
# store defaults to the per-user data dir (XDG data home); set a path to change it
|
|
31
|
+
store = /path/to/store
|
|
32
|
+
timeout = 120
|
|
33
|
+
trust_scan = false
|
|
34
|
+
|
|
35
|
+
[executables]
|
|
36
|
+
claude = /opt/claude/bin/claude
|
|
37
|
+
codex = /usr/local/bin/codex
|
|
38
|
+
|
|
39
|
+
The optional ``[executables]`` section maps a client name to the path (or bare
|
|
40
|
+
command) used to launch it, supplying a persistent default for the per-call
|
|
41
|
+
``--executable`` seam. It is for installs that are not on ``PATH`` or for pinning
|
|
42
|
+
one of several builds; it never changes *which* client/model runs. Precedence per
|
|
43
|
+
client is ``--executable`` flag > ``[executables]`` config > the adapter's own
|
|
44
|
+
``PATH`` lookup. Unknown client keys are kept, not rejected (the adapter registry
|
|
45
|
+
is extensible), and a path is not validated at load -- a wrong path surfaces a
|
|
46
|
+
clear error only if and when that client is actually launched.
|
|
47
|
+
|
|
48
|
+
``trust_scan`` (boolean, default ``false``) governs whether an *allow-path* call
|
|
49
|
+
may be cached. Allow-path folders cannot be fingerprinted, so by default such a
|
|
50
|
+
call is passthrough (always fresh, never stored). Setting ``trust_scan = true``
|
|
51
|
+
asserts that the scanned folders are stable and lets these calls be cached like
|
|
52
|
+
any other -- on the ordinary key (the prompt already names the folder), with the
|
|
53
|
+
folders themselves never entering the key or the stored record. It is deliberately a
|
|
54
|
+
config/environment setting, not a per-call flag, because it trades soundness for
|
|
55
|
+
reuse and should be a considered, standing choice. Precedence is the usual
|
|
56
|
+
environment (``GMLCACHE_TRUST_SCAN``) > config file > built-in default.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import configparser
|
|
62
|
+
import os
|
|
63
|
+
import re
|
|
64
|
+
from dataclasses import dataclass, field
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
from typing import Dict, Optional, Tuple
|
|
67
|
+
|
|
68
|
+
from generic_ml_cache_core.application.domain.model.run.cache_mode import CacheMode
|
|
69
|
+
from generic_ml_cache_core.common.errors import ConfigError
|
|
70
|
+
|
|
71
|
+
CONFIG_ENV = "GMLCACHE_CONFIG"
|
|
72
|
+
APP_DIR = "generic-ml-cache"
|
|
73
|
+
CONFIG_NAME = "config.ini"
|
|
74
|
+
SECTION = "defaults"
|
|
75
|
+
EXECUTABLES_SECTION = "executables"
|
|
76
|
+
|
|
77
|
+
#: built-in defaults; ``timeout`` of ``None`` means "no timeout". The store has
|
|
78
|
+
#: no static default here -- it resolves to :func:`default_store_path` (per-user
|
|
79
|
+
#: data dir) and has no flag/env layer, only the config file.
|
|
80
|
+
DEFAULTS: Dict[str, Optional[str]] = {"mode": "cache", "timeout": None}
|
|
81
|
+
|
|
82
|
+
_MODES = {m.value for m in CacheMode}
|
|
83
|
+
|
|
84
|
+
#: written by ``gmlcache init`` (and only then); ``{store}`` is filled with the
|
|
85
|
+
#: resolved per-user default so the user can see and edit where the store lives.
|
|
86
|
+
_DEFAULT_CONFIG_TEMPLATE = """\
|
|
87
|
+
# generic-ml-cache configuration.
|
|
88
|
+
#
|
|
89
|
+
# Precedence for mode/timeout: CLI flag > environment > this file > built-in
|
|
90
|
+
# default. The STORE location is set only here -- there is no flag and no
|
|
91
|
+
# environment for it, because the store is the cache's own internal structure,
|
|
92
|
+
# not a per-call knob. To run a fully isolated cache, point GMLCACHE_CONFIG at a
|
|
93
|
+
# different config file: that selects a whole separate configuration (its own
|
|
94
|
+
# store, its own settings), which is a deliberate isolated instance rather than
|
|
95
|
+
# a per-call redirect.
|
|
96
|
+
|
|
97
|
+
[defaults]
|
|
98
|
+
mode = cache
|
|
99
|
+
# Where the store lives. This is the per-user data dir by default; change freely.
|
|
100
|
+
store = {store}
|
|
101
|
+
# timeout = 120
|
|
102
|
+
trust_scan = false
|
|
103
|
+
# Optional cache size cap. Off by default = keep every execution forever. When set
|
|
104
|
+
# (e.g. 5GB / 500MB / a byte count), the cache evicts the least-recently-used
|
|
105
|
+
# executions to make room as it records new ones. Time-based ("not used in N days")
|
|
106
|
+
# eviction arrives with daemon mode.
|
|
107
|
+
# max_size = 5GB
|
|
108
|
+
|
|
109
|
+
# Optional: pin a client's executable (off-PATH installs, or a specific build).
|
|
110
|
+
# [executables]
|
|
111
|
+
# claude = /opt/claude/bin/claude
|
|
112
|
+
# codex = /usr/local/bin/codex
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def default_config_path() -> Path:
|
|
117
|
+
"""The per-user config path for this OS, ignoring the env override."""
|
|
118
|
+
if os.name == "nt":
|
|
119
|
+
base = os.environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming")
|
|
120
|
+
else:
|
|
121
|
+
base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
|
|
122
|
+
return Path(base) / APP_DIR / CONFIG_NAME
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def resolve_config_path() -> Path:
|
|
126
|
+
"""Where the config file would be read from: env override, else OS default."""
|
|
127
|
+
override = os.environ.get(CONFIG_ENV)
|
|
128
|
+
return Path(override) if override else default_config_path()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def default_data_dir() -> Path:
|
|
132
|
+
"""The per-user data directory for this OS (XDG data home / %LOCALAPPDATA%)."""
|
|
133
|
+
if os.name == "nt":
|
|
134
|
+
base = (
|
|
135
|
+
os.environ.get("LOCALAPPDATA")
|
|
136
|
+
or os.environ.get("APPDATA")
|
|
137
|
+
or str(Path.home() / "AppData" / "Local")
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
base = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
|
|
141
|
+
return Path(base) / APP_DIR
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def default_store_path() -> Path:
|
|
145
|
+
"""Where the store lives when the config does not say otherwise.
|
|
146
|
+
|
|
147
|
+
The store is the cache's own internal structure, so its default sits in the
|
|
148
|
+
per-user data directory (honoring ``XDG_DATA_HOME``), never in whatever
|
|
149
|
+
directory the cache happens to be invoked from. There is deliberately no flag
|
|
150
|
+
or environment override for the store *location*: a caller cannot redirect it
|
|
151
|
+
per call, because that would fork the cache into per-caller copies and defeat
|
|
152
|
+
the one thing a cache is for -- reuse. To run a fully isolated cache, point
|
|
153
|
+
``GMLCACHE_CONFIG`` at a different whole config file.
|
|
154
|
+
"""
|
|
155
|
+
return default_data_dir() / "store"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class FileConfig:
|
|
160
|
+
"""Settings read from the config file. ``source`` is the file actually read,
|
|
161
|
+
or ``None`` when no file was present."""
|
|
162
|
+
|
|
163
|
+
mode: Optional[str] = None
|
|
164
|
+
store: Optional[str] = None
|
|
165
|
+
timeout: Optional[float] = None
|
|
166
|
+
trust_scan: Optional[bool] = None
|
|
167
|
+
max_size: Optional[int] = None
|
|
168
|
+
executables: Dict[str, str] = field(default_factory=dict)
|
|
169
|
+
source: Optional[Path] = None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _parse_timeout(raw: str, where: str) -> float:
|
|
173
|
+
try:
|
|
174
|
+
return float(raw)
|
|
175
|
+
except ValueError as exc:
|
|
176
|
+
raise ConfigError(f"invalid timeout {raw!r} {where}; expected a number") from exc
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
_TRUE = {"true", "1", "yes", "on"}
|
|
180
|
+
_FALSE = {"false", "0", "no", "off"}
|
|
181
|
+
|
|
182
|
+
_SIZE_UNITS = {"b": 1, "kb": 1024, "mb": 1024**2, "gb": 1024**3, "tb": 1024**4}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _parse_size(raw: str, where: str) -> int:
|
|
186
|
+
"""Parse a human size (``5GB``, ``500MB``, ``1048576``) into bytes (base 1024)."""
|
|
187
|
+
text = raw.strip().lower().replace(" ", "")
|
|
188
|
+
match = re.fullmatch(r"([0-9]*\.?[0-9]+)([a-z]*)", text)
|
|
189
|
+
if not match:
|
|
190
|
+
raise ConfigError(f"invalid size {raw!r} {where}; e.g. 5GB, 500MB, or a byte count")
|
|
191
|
+
number, unit = match.group(1), match.group(2) or "b"
|
|
192
|
+
if unit not in _SIZE_UNITS:
|
|
193
|
+
raise ConfigError(
|
|
194
|
+
f"invalid size unit {unit!r} {where}; expected one of {sorted(_SIZE_UNITS)}"
|
|
195
|
+
)
|
|
196
|
+
return int(float(number) * _SIZE_UNITS[unit])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _parse_bool(raw: str, where: str) -> bool:
|
|
200
|
+
v = raw.strip().lower()
|
|
201
|
+
if v in _TRUE:
|
|
202
|
+
return True
|
|
203
|
+
if v in _FALSE:
|
|
204
|
+
return False
|
|
205
|
+
raise ConfigError(f"invalid boolean {raw!r} {where}; expected true or false")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def load(path: Optional[Path] = None) -> FileConfig:
|
|
209
|
+
"""Read the config file if it exists; a missing file yields empty defaults."""
|
|
210
|
+
p = path or resolve_config_path()
|
|
211
|
+
if not p.is_file():
|
|
212
|
+
return FileConfig()
|
|
213
|
+
|
|
214
|
+
parser = configparser.ConfigParser()
|
|
215
|
+
try:
|
|
216
|
+
parser.read(p, encoding="utf-8")
|
|
217
|
+
except configparser.Error as exc:
|
|
218
|
+
raise ConfigError(f"could not parse config at {p}: {exc}") from exc
|
|
219
|
+
|
|
220
|
+
section = parser[SECTION] if parser.has_section(SECTION) else None
|
|
221
|
+
|
|
222
|
+
def get(key: str) -> Optional[str]:
|
|
223
|
+
return section.get(key) if section is not None else None
|
|
224
|
+
|
|
225
|
+
mode = get("mode")
|
|
226
|
+
if mode is not None and mode not in _MODES:
|
|
227
|
+
raise ConfigError(f"invalid mode {mode!r} in {p}; expected one of {sorted(_MODES)}")
|
|
228
|
+
|
|
229
|
+
timeout_raw = get("timeout")
|
|
230
|
+
timeout = _parse_timeout(timeout_raw, f"in {p}") if timeout_raw else None
|
|
231
|
+
|
|
232
|
+
trust_scan_raw = get("trust_scan")
|
|
233
|
+
trust_scan = _parse_bool(trust_scan_raw, f"in {p}") if trust_scan_raw else None
|
|
234
|
+
|
|
235
|
+
max_size_raw = get("max_size")
|
|
236
|
+
max_size = _parse_size(max_size_raw, f"in {p}") if max_size_raw else None
|
|
237
|
+
|
|
238
|
+
# [executables]: client name -> path/command. Kept verbatim and leniently
|
|
239
|
+
# (unknown client keys are not an error -- the adapter registry is
|
|
240
|
+
# extensible, and a key is only ever consulted when that client is run).
|
|
241
|
+
executables = (
|
|
242
|
+
{k: v for k, v in parser[EXECUTABLES_SECTION].items()}
|
|
243
|
+
if parser.has_section(EXECUTABLES_SECTION)
|
|
244
|
+
else {}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return FileConfig(
|
|
248
|
+
mode=mode,
|
|
249
|
+
store=get("store"),
|
|
250
|
+
timeout=timeout,
|
|
251
|
+
trust_scan=trust_scan,
|
|
252
|
+
max_size=max_size,
|
|
253
|
+
executables=executables,
|
|
254
|
+
source=p,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def write_default_config(path: Optional[Path] = None) -> Tuple[Path, bool]:
|
|
259
|
+
"""Create the config file with documented defaults, if it is absent.
|
|
260
|
+
|
|
261
|
+
Returns ``(path, created)``; ``created`` is ``False`` when a file already
|
|
262
|
+
existed (it is *never* overwritten). The generated file spells out the
|
|
263
|
+
resolved default store path so the user can see -- and edit -- where the
|
|
264
|
+
store lives. This is the only path that ever writes the config: ``load``
|
|
265
|
+
still never creates it, so the cache keeps working with zero files present.
|
|
266
|
+
"""
|
|
267
|
+
p = path or resolve_config_path()
|
|
268
|
+
if p.is_file():
|
|
269
|
+
return p, False
|
|
270
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
p.write_text(_DEFAULT_CONFIG_TEMPLATE.format(store=default_store_path()), encoding="utf-8")
|
|
272
|
+
return p, True
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _pick(flag, env, file_value, default) -> Tuple[object, str]:
|
|
276
|
+
"""First non-empty of flag > env > file > default, with its provenance."""
|
|
277
|
+
if flag is not None:
|
|
278
|
+
return flag, "flag"
|
|
279
|
+
if env is not None and env != "":
|
|
280
|
+
return env, "env"
|
|
281
|
+
if file_value is not None:
|
|
282
|
+
return file_value, "config"
|
|
283
|
+
return default, "default"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def resolve_settings(
|
|
287
|
+
file_cfg: FileConfig,
|
|
288
|
+
*,
|
|
289
|
+
mode_flag: Optional[str] = None,
|
|
290
|
+
timeout_flag: Optional[float] = None,
|
|
291
|
+
) -> Dict[str, Tuple[object, str]]:
|
|
292
|
+
"""Resolve each setting to ``(value, source)`` by the documented precedence.
|
|
293
|
+
|
|
294
|
+
``source`` is one of ``flag`` / ``env`` / ``config`` / ``default`` so callers
|
|
295
|
+
(notably ``status``) can show exactly why a value is what it is. The store is
|
|
296
|
+
the exception: it has neither a flag nor an env layer (only ``config`` or
|
|
297
|
+
``default``), because its location is the cache's own, not a per-call knob.
|
|
298
|
+
"""
|
|
299
|
+
env = os.environ
|
|
300
|
+
|
|
301
|
+
mode_env = env.get("GMLCACHE_MODE")
|
|
302
|
+
if mode_env and mode_env not in _MODES:
|
|
303
|
+
raise ConfigError(
|
|
304
|
+
f"invalid mode {mode_env!r} in GMLCACHE_MODE; expected one of {sorted(_MODES)}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
timeout_env_raw = env.get("GMLCACHE_TIMEOUT")
|
|
308
|
+
timeout_env = (
|
|
309
|
+
_parse_timeout(timeout_env_raw, "in GMLCACHE_TIMEOUT") if timeout_env_raw else None
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
trust_env_raw = env.get("GMLCACHE_TRUST_SCAN")
|
|
313
|
+
trust_env = _parse_bool(trust_env_raw, "in GMLCACHE_TRUST_SCAN") if trust_env_raw else None
|
|
314
|
+
|
|
315
|
+
max_size_env_raw = env.get("GMLCACHE_MAX_SIZE")
|
|
316
|
+
max_size_env = (
|
|
317
|
+
_parse_size(max_size_env_raw, "in GMLCACHE_MAX_SIZE") if max_size_env_raw else None
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
"mode": _pick(mode_flag, mode_env, file_cfg.mode, DEFAULTS["mode"]),
|
|
322
|
+
# store: config file or built-in per-user default only. No flag, no env --
|
|
323
|
+
# a per-call store override would fork the cache and defeat reuse.
|
|
324
|
+
"store": _pick(None, None, file_cfg.store, str(default_store_path())),
|
|
325
|
+
"timeout": _pick(timeout_flag, timeout_env, file_cfg.timeout, DEFAULTS["timeout"]),
|
|
326
|
+
# trust_scan has no CLI flag -- a standing, deliberate choice only.
|
|
327
|
+
"trust_scan": _pick(None, trust_env, file_cfg.trust_scan, False),
|
|
328
|
+
# max_size: off (None) by default = keep everything. Standing policy, so
|
|
329
|
+
# config/env only, no per-call flag.
|
|
330
|
+
"max_size": _pick(None, max_size_env, file_cfg.max_size, None),
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def executable_for(
|
|
335
|
+
file_cfg: FileConfig, client: str, *, flag: Optional[str] = None
|
|
336
|
+
) -> Optional[str]:
|
|
337
|
+
"""The executable override to hand the adapter for ``client``.
|
|
338
|
+
|
|
339
|
+
Precedence is ``--executable`` flag > ``[executables]`` config entry, and
|
|
340
|
+
``None`` when neither is set -- in which case the adapter falls back to its
|
|
341
|
+
own ``PATH`` lookup. There is deliberately no environment layer here: a
|
|
342
|
+
single variable cannot name *which* client, and per-client variables would
|
|
343
|
+
be overkill for what this seam is for.
|
|
344
|
+
"""
|
|
345
|
+
if flag is not None:
|
|
346
|
+
return flag
|
|
347
|
+
return file_cfg.executables.get(client)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: generic-ml-cache-cli
|
|
3
|
+
Version: 0.2.0
|
|
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
|
+
Project-URL: Homepage, https://github.com/danielslobozian/generic-ml-cache
|
|
6
|
+
Project-URL: Repository, https://github.com/danielslobozian/generic-ml-cache
|
|
7
|
+
Project-URL: Issues, https://github.com/danielslobozian/generic-ml-cache/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/danielslobozian/generic-ml-cache/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Daniel Slobozian
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: NOTICE
|
|
13
|
+
Keywords: agent,ai,cache,claude,cli,codex,cursor,llm,replay
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: argcomplete<4,>=3
|
|
27
|
+
Requires-Dist: generic-ml-cache-core>=0.2.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: coverage>=7; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
33
|
+
Requires-Dist: vulture>=2; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# gmlcache
|
|
37
|
+
|
|
38
|
+
#### Detached ML Execution Cache — the terminal client
|
|
39
|
+
|
|
40
|
+
[](https://github.com/danielslobozian/generic-ml-cache/blob/main/LICENSE)
|
|
41
|
+
[](https://github.com/danielslobozian/generic-ml-cache/blob/main/docs/ROADMAP.md)
|
|
42
|
+
|
|
43
|
+
`gmlcache` runs, records, and replays detached ML workloads — record a real client (or
|
|
44
|
+
API) call once, replay it forever by its content key, offline and byte-for-byte.
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
<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
|
+
</p>
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install generic-ml-cache-cli
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This installs the `gmlcache` command and pulls in the engine,
|
|
57
|
+
[`generic-ml-cache-core`](https://github.com/danielslobozian/generic-ml-cache/tree/main/packages/core).
|
|
58
|
+
|
|
59
|
+
## Use
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
gmlcache run --client claude --model sonnet --prompt "…" # record on a miss, replay on a hit
|
|
63
|
+
gmlcache check --client claude --model sonnet --prompt "…" # is this exact call already cached?
|
|
64
|
+
gmlcache list # stored executions, grouped by client/model
|
|
65
|
+
gmlcache stats # totals, hit counts, token usage & cost
|
|
66
|
+
gmlcache inspect <key> # pretty-print one stored execution
|
|
67
|
+
gmlcache doctor | models | status | init # environment & configuration helpers
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## What it does
|
|
71
|
+
|
|
72
|
+
- **Records** a real agentic CLI client (`claude`, `codex`, `cursor-agent`) or an API
|
|
73
|
+
call — stdout, stderr, exit code, generated files, and token usage.
|
|
74
|
+
- **Replays** an identical request instantly and offline, **byte-for-byte** — gmlcache
|
|
75
|
+
adds nothing to the client's output, so it is a transparent drop-in.
|
|
76
|
+
- **Reports** — list, group, inspect, and measure stored executions and their savings.
|
|
77
|
+
|
|
78
|
+
## Built on a reusable engine
|
|
79
|
+
|
|
80
|
+
`gmlcache` is the terminal client — one inbound driver over the engine. The whole cache
|
|
81
|
+
logic and every adapter live in
|
|
82
|
+
[`generic-ml-cache-core`](https://github.com/danielslobozian/generic-ml-cache/tree/main/packages/core),
|
|
83
|
+
a **stateless, dependency-free** library. To embed the cache in your own application
|
|
84
|
+
instead of driving it from a terminal, depend on the core and inject your own data
|
|
85
|
+
source — you never reimplement the adapters.
|
|
86
|
+
|
|
87
|
+
## Links
|
|
88
|
+
|
|
89
|
+
- **Repository & docs:** <https://github.com/danielslobozian/generic-ml-cache>
|
|
90
|
+
- **Changelog** (both packages, versioned in lockstep): [`CHANGELOG.md`](https://github.com/danielslobozian/generic-ml-cache/blob/main/CHANGELOG.md)
|
|
91
|
+
- **Security policy:** [`SECURITY.md`](https://github.com/danielslobozian/generic-ml-cache/blob/main/SECURITY.md)
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
Apache-2.0 — see [`LICENSE`](https://github.com/danielslobozian/generic-ml-cache/blob/main/LICENSE)
|
|
96
|
+
and [`NOTICE`](https://github.com/danielslobozian/generic-ml-cache/blob/main/NOTICE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
generic_ml_cache_cli/__init__.py,sha256=Mf-sU7LioXdIhW7zpz_6cXIYSJ3KpbOo8mkU9dAWxu0,990
|
|
2
|
+
generic_ml_cache_cli/__main__.py,sha256=ti3KgTXj7bJ6sLdRypX3AlW2SBSlFq-m1fhNq85Ip0U,267
|
|
3
|
+
generic_ml_cache_cli/cli.py,sha256=Wv9Q2qzg2ln7Xmu60MdbinjDXLNnsAiKcvQowv00hY8,32950
|
|
4
|
+
generic_ml_cache_cli/config.py,sha256=GjIqwBZCP_QXvHp4LOmZ7dB5d7fio2wgYz_zGswmPlc,14035
|
|
5
|
+
generic_ml_cache_cli-0.2.0.dist-info/METADATA,sha256=z87al94gK4XfvVbfXi0VwovyC-IPGNlH9usj-LHGWz4,4807
|
|
6
|
+
generic_ml_cache_cli-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
generic_ml_cache_cli-0.2.0.dist-info/entry_points.txt,sha256=zzoX6uRxj8v-dutF2P9aIG2Cgnh8RlVRUMvDk1c9RWQ,59
|
|
8
|
+
generic_ml_cache_cli-0.2.0.dist-info/licenses/LICENSE,sha256=de-gfE0q-xTYImzwC3dj3S7BxVhanf6RmIGjo_7y3aw,11357
|
|
9
|
+
generic_ml_cache_cli-0.2.0.dist-info/licenses/NOTICE,sha256=hQoAdw5YSwg3GmeY8hmy9nyOh20La-Qc6mre-TKpn5M,329
|
|
10
|
+
generic_ml_cache_cli-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or Derivative
|
|
95
|
+
Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
generic-ml-cache
|
|
2
|
+
Copyright 2026 Daniel Slobozian
|
|
3
|
+
|
|
4
|
+
This product includes software developed by Daniel Slobozian.
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
7
|
+
this software except in compliance with the License. You may obtain a copy of
|
|
8
|
+
the License at http://www.apache.org/licenses/LICENSE-2.0
|