lgit-cli 3.7.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.
Files changed (54) hide show
  1. lgit/__init__.py +75 -0
  2. lgit/__main__.py +8 -0
  3. lgit/analysis.py +326 -0
  4. lgit/api.py +1077 -0
  5. lgit/cache.py +338 -0
  6. lgit/changelog.py +523 -0
  7. lgit/cli.py +1104 -0
  8. lgit/compose.py +2110 -0
  9. lgit/config.py +437 -0
  10. lgit/diffing.py +384 -0
  11. lgit/errors.py +137 -0
  12. lgit/git.py +852 -0
  13. lgit/map_reduce.py +508 -0
  14. lgit/markdown_output.py +709 -0
  15. lgit/models.py +924 -0
  16. lgit/normalization.py +411 -0
  17. lgit/patch.py +784 -0
  18. lgit/profile.py +426 -0
  19. lgit/py.typed +0 -0
  20. lgit/repo.py +287 -0
  21. lgit/resources/__init__.py +1 -0
  22. lgit/resources/commit_types.json +242 -0
  23. lgit/resources/prompts/analysis/default.md +237 -0
  24. lgit/resources/prompts/analysis/markdown.md +112 -0
  25. lgit/resources/prompts/changelog/default.md +89 -0
  26. lgit/resources/prompts/changelog/markdown.md +60 -0
  27. lgit/resources/prompts/compose-bind/default.md +40 -0
  28. lgit/resources/prompts/compose-bind/markdown.md +41 -0
  29. lgit/resources/prompts/compose-intent/default.md +63 -0
  30. lgit/resources/prompts/compose-intent/markdown.md +59 -0
  31. lgit/resources/prompts/fast/default.md +46 -0
  32. lgit/resources/prompts/fast/markdown.md +51 -0
  33. lgit/resources/prompts/map/default.md +67 -0
  34. lgit/resources/prompts/map/markdown.md +63 -0
  35. lgit/resources/prompts/reduce/default.md +81 -0
  36. lgit/resources/prompts/reduce/markdown.md +68 -0
  37. lgit/resources/prompts/summary/default.md +74 -0
  38. lgit/resources/prompts/summary/markdown.md +77 -0
  39. lgit/resources/validation_data.json +1 -0
  40. lgit/rewrite.py +392 -0
  41. lgit/style.py +295 -0
  42. lgit/templates.py +385 -0
  43. lgit/testing/__init__.py +62 -0
  44. lgit/testing/compare.py +57 -0
  45. lgit/testing/fixture.py +386 -0
  46. lgit/testing/report.py +201 -0
  47. lgit/testing/runner.py +256 -0
  48. lgit/tokens.py +90 -0
  49. lgit/validation.py +545 -0
  50. lgit_cli-3.7.0.dist-info/METADATA +288 -0
  51. lgit_cli-3.7.0.dist-info/RECORD +54 -0
  52. lgit_cli-3.7.0.dist-info/WHEEL +4 -0
  53. lgit_cli-3.7.0.dist-info/entry_points.txt +2 -0
  54. lgit_cli-3.7.0.dist-info/licenses/LICENSE +21 -0
lgit/cache.py ADDED
@@ -0,0 +1,338 @@
1
+ """SQLite-backed best-effort cache for LLM responses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sqlite3
8
+ import threading
9
+ import time
10
+ from dataclasses import dataclass
11
+ from datetime import timedelta
12
+ from pathlib import Path
13
+ from typing import Any, Self
14
+
15
+ from blake3 import blake3
16
+
17
+ SCHEMA_VERSION = 3
18
+ PRUNE_DIVISOR = 64
19
+ MAX_FAILURES = 64
20
+
21
+ _GLOBAL_LOCK = threading.Lock()
22
+ _GLOBAL_INITIALIZED = False
23
+ _GLOBAL: LlmCache | None = None
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class CachedLlmResponse:
28
+ """Stored request/response payload returned for a cache hit."""
29
+
30
+ request: str
31
+ response: str
32
+ created_at: int
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class FailureRecord:
37
+ """Recorded LLM failure retained for offline diagnosis only."""
38
+
39
+ model: str
40
+ operation: str
41
+ request: str
42
+ response: str
43
+ error: str
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class CacheMaterial:
48
+ """Material that uniquely identifies a one-shot LLM request."""
49
+
50
+ operation: str
51
+ model: str
52
+ tool_name: str
53
+ tool_description: str
54
+ system_prompt: str
55
+ user_prompt: str
56
+ schema: Any
57
+ api_mode: str
58
+ markdown_output: bool
59
+
60
+
61
+ class LlmCache:
62
+ """SQLite-backed cache of parsed LLM responses."""
63
+
64
+ def __init__(self, path: str | os.PathLike[str], ttl: timedelta | int | float = 0) -> None:
65
+ self.path = Path(path)
66
+ self.ttl_secs = _ttl_seconds(ttl)
67
+ self._lock = threading.Lock()
68
+ self.path.parent.mkdir(parents=True, exist_ok=True)
69
+ self._conn = sqlite3.connect(self.path, check_same_thread=False)
70
+ self._conn.execute("PRAGMA journal_mode=WAL")
71
+ self._conn.execute("PRAGMA synchronous=NORMAL")
72
+ self._create_schema()
73
+
74
+ @classmethod
75
+ def open(cls, path: str | os.PathLike[str], ttl: timedelta | int | float = 0) -> Self:
76
+ """Open or create a cache database at ``path``."""
77
+
78
+ return cls(path, ttl)
79
+
80
+ def get_entry(self, key: str) -> CachedLlmResponse | None:
81
+ """Return the stored request/response for ``key`` or ``None`` on miss."""
82
+
83
+ try:
84
+ with self._lock:
85
+ row = self._conn.execute(
86
+ """
87
+ SELECT request, response, created_at
88
+ FROM responses
89
+ WHERE key = ? AND schema_version = ?
90
+ """,
91
+ (key, SCHEMA_VERSION),
92
+ ).fetchone()
93
+ if row is None:
94
+ return None
95
+ request, response, created_at = str(row[0]), str(row[1]), int(row[2])
96
+ if self.ttl_secs > 0 and created_at < _now_unix() - self.ttl_secs:
97
+ self._conn.execute("DELETE FROM responses WHERE key = ?", (key,))
98
+ self._conn.commit()
99
+ return None
100
+ self._conn.execute("UPDATE responses SET accessed_at = ? WHERE key = ?", (_now_unix(), key))
101
+ self._conn.commit()
102
+ return CachedLlmResponse(request=request, response=response, created_at=created_at)
103
+ except Exception:
104
+ return None
105
+
106
+ def get(self, key: str) -> str | None:
107
+ """Return the cached response payload for ``key`` if available."""
108
+
109
+ entry = self.get_entry(key)
110
+ return entry.response if entry is not None else None
111
+
112
+ def put(self, key: str, model: str, operation: str, request: str, response: str) -> None:
113
+ """Insert or replace a successful response, swallowing cache failures."""
114
+
115
+ try:
116
+ now = _now_unix()
117
+ with self._lock:
118
+ self._conn.execute(
119
+ """
120
+ INSERT OR REPLACE INTO responses
121
+ (key, schema_version, model, operation, request, response, created_at, accessed_at)
122
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
123
+ """,
124
+ (key, SCHEMA_VERSION, model, operation, request, response, now, now),
125
+ )
126
+ if self.ttl_secs > 0 and now % PRUNE_DIVISOR == 0:
127
+ self._conn.execute("DELETE FROM responses WHERE created_at < ?", (now - self.ttl_secs,))
128
+ self._conn.commit()
129
+ except Exception:
130
+ return
131
+
132
+ def put_failure(
133
+ self,
134
+ key: str,
135
+ model: str,
136
+ operation: str,
137
+ request: str,
138
+ response: str,
139
+ error: str,
140
+ ) -> None:
141
+ """Record a failed response for diagnostics without serving it as a hit."""
142
+
143
+ try:
144
+ now = _now_unix()
145
+ with self._lock:
146
+ self._conn.execute(
147
+ """
148
+ INSERT INTO failures
149
+ (schema_version, key, model, operation, request, response, error, created_at)
150
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
151
+ """,
152
+ (SCHEMA_VERSION, key, model, operation, request, response, error, now),
153
+ )
154
+ if self.ttl_secs > 0:
155
+ self._conn.execute("DELETE FROM failures WHERE created_at < ?", (now - self.ttl_secs,))
156
+ self._conn.execute(
157
+ """
158
+ DELETE FROM failures
159
+ WHERE id NOT IN (SELECT id FROM failures ORDER BY id DESC LIMIT ?)
160
+ """,
161
+ (MAX_FAILURES,),
162
+ )
163
+ self._conn.commit()
164
+ except Exception:
165
+ return
166
+
167
+ def recent_failures(self, limit: int) -> list[FailureRecord]:
168
+ """Return recent diagnostic failures, newest first."""
169
+
170
+ try:
171
+ with self._lock:
172
+ rows = self._conn.execute(
173
+ """
174
+ SELECT model, operation, request, response, error
175
+ FROM failures
176
+ ORDER BY id DESC
177
+ LIMIT ?
178
+ """,
179
+ (max(0, int(limit)),),
180
+ ).fetchall()
181
+ return [FailureRecord(*(str(value) for value in row)) for row in rows]
182
+ except Exception:
183
+ return []
184
+
185
+ def close(self) -> None:
186
+ """Close the underlying SQLite connection."""
187
+
188
+ with self._lock:
189
+ self._conn.close()
190
+
191
+ def _create_schema(self) -> None:
192
+ with self._lock:
193
+ self._conn.executescript(
194
+ """
195
+ CREATE TABLE IF NOT EXISTS responses (
196
+ key TEXT PRIMARY KEY,
197
+ schema_version INTEGER NOT NULL,
198
+ model TEXT NOT NULL,
199
+ operation TEXT NOT NULL,
200
+ request TEXT NOT NULL,
201
+ response TEXT NOT NULL,
202
+ created_at INTEGER NOT NULL,
203
+ accessed_at INTEGER NOT NULL
204
+ );
205
+ CREATE INDEX IF NOT EXISTS idx_responses_created_at ON responses(created_at);
206
+ CREATE TABLE IF NOT EXISTS failures (
207
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
208
+ schema_version INTEGER NOT NULL,
209
+ key TEXT NOT NULL,
210
+ model TEXT NOT NULL,
211
+ operation TEXT NOT NULL,
212
+ request TEXT NOT NULL,
213
+ response TEXT NOT NULL,
214
+ error TEXT NOT NULL,
215
+ created_at INTEGER NOT NULL
216
+ );
217
+ CREATE INDEX IF NOT EXISTS idx_failures_created_at ON failures(created_at);
218
+ """
219
+ )
220
+ try:
221
+ self._conn.execute("ALTER TABLE responses ADD COLUMN request TEXT NOT NULL DEFAULT ''")
222
+ except sqlite3.OperationalError as error:
223
+ if "duplicate column name" not in str(error).lower():
224
+ raise
225
+ self._conn.commit()
226
+
227
+
228
+ def init(config: object) -> None:
229
+ """Initialize the process-global cache from configuration; first call wins."""
230
+
231
+ global _GLOBAL_INITIALIZED, _GLOBAL
232
+ with _GLOBAL_LOCK:
233
+ if _GLOBAL_INITIALIZED:
234
+ return
235
+ _GLOBAL = _build_from_config(config)
236
+ _GLOBAL_INITIALIZED = True
237
+
238
+
239
+ def global_cache() -> LlmCache | None:
240
+ """Return the initialized process-global cache handle, if any."""
241
+
242
+ return _GLOBAL
243
+
244
+
245
+ def get_global() -> LlmCache | None:
246
+ """Return the initialized process-global cache handle, if any."""
247
+
248
+ return global_cache()
249
+
250
+
251
+ def global_() -> LlmCache | None:
252
+ """Return the initialized process-global cache handle, if any."""
253
+
254
+ return global_cache()
255
+
256
+
257
+ def compute_key(material: CacheMaterial) -> str:
258
+ """Compute a stable BLAKE3 cache key over request material."""
259
+
260
+ hasher = blake3()
261
+ hasher.update(b"llm-cache/v1\n")
262
+ _write_field(hasher, "operation", material.operation)
263
+ _write_field(hasher, "model", material.model)
264
+ _write_field(hasher, "api_mode", material.api_mode)
265
+ _write_field(hasher, "tool_name", material.tool_name)
266
+ _write_field(hasher, "tool_description", material.tool_description)
267
+ _write_field(hasher, "system", material.system_prompt)
268
+ _write_field(hasher, "user", material.user_prompt)
269
+ _write_field(hasher, "markdown_output", "true" if material.markdown_output else "false")
270
+ try:
271
+ schema = json.dumps(material.schema, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
272
+ except TypeError:
273
+ schema = ""
274
+ _write_field(hasher, "schema", schema)
275
+ hasher.update(b"\n")
276
+ return hasher.hexdigest()
277
+
278
+
279
+ def _build_from_config(config: object) -> LlmCache | None:
280
+ if not bool(getattr(config, "cache_enabled", True)):
281
+ return None
282
+ cache_dir = _resolve_cache_dir(config)
283
+ if cache_dir is None:
284
+ return None
285
+ ttl_days = int(getattr(config, "cache_ttl_days", 14) or 0)
286
+ try:
287
+ return LlmCache.open(cache_dir / "responses.sqlite", timedelta(days=ttl_days))
288
+ except Exception:
289
+ return None
290
+
291
+
292
+ def _resolve_cache_dir(config: object) -> Path | None:
293
+ explicit = getattr(config, "cache_dir", None)
294
+ if explicit:
295
+ return Path(explicit)
296
+ xdg = os.environ.get("XDG_CACHE_HOME")
297
+ if xdg:
298
+ return Path(xdg) / "llm-git"
299
+ home = os.environ.get("HOME") or os.environ.get("USERPROFILE")
300
+ if home:
301
+ return Path(home) / ".cache" / "llm-git"
302
+ return None
303
+
304
+
305
+ def _ttl_seconds(ttl: timedelta | int | float) -> int:
306
+ if isinstance(ttl, timedelta):
307
+ return max(0, int(ttl.total_seconds()))
308
+ return max(0, int(ttl))
309
+
310
+
311
+ def _now_unix() -> int:
312
+ return int(time.time())
313
+
314
+
315
+ def _write_field(hasher: Any, name: str, value: str) -> None:
316
+ hasher.update(name.encode())
317
+ hasher.update(b"\x00")
318
+ hasher.update(value.encode())
319
+ hasher.update(b"\n")
320
+
321
+
322
+ # ``global`` is a Python keyword, but callers using getattr(lgit.cache, "global")
323
+ # can still reach the Rust-compatible name.
324
+ globals()["global"] = global_cache
325
+ globals()["global_"] = global_
326
+
327
+ __all__ = [
328
+ "SCHEMA_VERSION",
329
+ "CacheMaterial",
330
+ "CachedLlmResponse",
331
+ "FailureRecord",
332
+ "LlmCache",
333
+ "compute_key",
334
+ "get_global",
335
+ "global_",
336
+ "global_cache",
337
+ "init",
338
+ ]