code-context-control 2.28.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 (150) hide show
  1. cli/__init__.py +1 -0
  2. cli/_hook_utils.py +99 -0
  3. cli/c3.py +6152 -0
  4. cli/commands/__init__.py +1 -0
  5. cli/commands/common.py +312 -0
  6. cli/commands/parser.py +286 -0
  7. cli/docs.html +3178 -0
  8. cli/edits.html +878 -0
  9. cli/hook_auto_snapshot.py +142 -0
  10. cli/hook_c3_signal.py +61 -0
  11. cli/hook_c3read.py +116 -0
  12. cli/hook_edit_ledger.py +213 -0
  13. cli/hook_edit_unlock.py +170 -0
  14. cli/hook_filter.py +130 -0
  15. cli/hook_ghost_files.py +238 -0
  16. cli/hook_pretool_enforce.py +334 -0
  17. cli/hook_read.py +200 -0
  18. cli/hook_session_stats.py +62 -0
  19. cli/hook_terse_advisor.py +190 -0
  20. cli/hub.html +3764 -0
  21. cli/hub_server.py +1619 -0
  22. cli/mcp_proxy.py +428 -0
  23. cli/mcp_server.py +660 -0
  24. cli/server.py +2985 -0
  25. cli/tools/__init__.py +4 -0
  26. cli/tools/_helpers.py +65 -0
  27. cli/tools/agent.py +1165 -0
  28. cli/tools/compress.py +215 -0
  29. cli/tools/delegate.py +1184 -0
  30. cli/tools/edit.py +313 -0
  31. cli/tools/edits.py +118 -0
  32. cli/tools/filter.py +285 -0
  33. cli/tools/impact.py +163 -0
  34. cli/tools/memory.py +469 -0
  35. cli/tools/read.py +224 -0
  36. cli/tools/search.py +337 -0
  37. cli/tools/session.py +95 -0
  38. cli/tools/shell.py +193 -0
  39. cli/tools/status.py +306 -0
  40. cli/tools/validate.py +310 -0
  41. cli/ui/api.js +36 -0
  42. cli/ui/app.js +207 -0
  43. cli/ui/components/chat.js +758 -0
  44. cli/ui/components/dashboard.js +689 -0
  45. cli/ui/components/edits.js +220 -0
  46. cli/ui/components/instructions.js +481 -0
  47. cli/ui/components/memory.js +626 -0
  48. cli/ui/components/sessions.js +606 -0
  49. cli/ui/components/settings.js +1404 -0
  50. cli/ui/components/sidebar.js +156 -0
  51. cli/ui/icons.js +51 -0
  52. cli/ui/shared.js +119 -0
  53. cli/ui/theme.js +22 -0
  54. cli/ui.html +168 -0
  55. cli/ui_legacy.html +6797 -0
  56. cli/ui_nano.html +503 -0
  57. code_context_control-2.28.0.dist-info/METADATA +248 -0
  58. code_context_control-2.28.0.dist-info/RECORD +150 -0
  59. code_context_control-2.28.0.dist-info/WHEEL +5 -0
  60. code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
  61. code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
  62. code_context_control-2.28.0.dist-info/top_level.txt +5 -0
  63. core/__init__.py +75 -0
  64. core/config.py +269 -0
  65. core/ide.py +188 -0
  66. oracle/__init__.py +1 -0
  67. oracle/config.py +75 -0
  68. oracle/oracle.html +3900 -0
  69. oracle/oracle_server.py +663 -0
  70. oracle/services/__init__.py +1 -0
  71. oracle/services/c3_bridge.py +210 -0
  72. oracle/services/chat_engine.py +1103 -0
  73. oracle/services/chat_store.py +155 -0
  74. oracle/services/cross_memory.py +154 -0
  75. oracle/services/federated_graph.py +463 -0
  76. oracle/services/health_checker.py +117 -0
  77. oracle/services/insight_engine.py +307 -0
  78. oracle/services/memory_reader.py +106 -0
  79. oracle/services/memory_writer.py +182 -0
  80. oracle/services/ollama_bridge.py +332 -0
  81. oracle/services/project_scanner.py +87 -0
  82. oracle/services/review_agent.py +206 -0
  83. services/__init__.py +1 -0
  84. services/activity_log.py +93 -0
  85. services/agent_base.py +124 -0
  86. services/agents.py +1529 -0
  87. services/auto_memory.py +407 -0
  88. services/bench/__init__.py +6 -0
  89. services/bench/external/__init__.py +29 -0
  90. services/bench/external/aider_polyglot.py +405 -0
  91. services/bench/external/swe_bench.py +485 -0
  92. services/benchmark_dashboard.py +596 -0
  93. services/claude_md.py +785 -0
  94. services/compressor.py +592 -0
  95. services/context_snapshot.py +356 -0
  96. services/conversation_store.py +870 -0
  97. services/doc_index.py +537 -0
  98. services/e2e_benchmark.py +2884 -0
  99. services/e2e_evaluator.py +396 -0
  100. services/e2e_tasks.py +743 -0
  101. services/edit_ledger.py +459 -0
  102. services/embedding_index.py +341 -0
  103. services/error_reporting.py +123 -0
  104. services/file_memory.py +734 -0
  105. services/hub_service.py +585 -0
  106. services/indexer.py +712 -0
  107. services/memory.py +318 -0
  108. services/memory_consolidator.py +538 -0
  109. services/memory_graph.py +382 -0
  110. services/memory_grounder.py +304 -0
  111. services/memory_scorer.py +246 -0
  112. services/metrics.py +86 -0
  113. services/notifications.py +209 -0
  114. services/ollama_client.py +201 -0
  115. services/output_filter.py +488 -0
  116. services/parser.py +1238 -0
  117. services/project_manager.py +579 -0
  118. services/protocol.py +306 -0
  119. services/proxy_state.py +152 -0
  120. services/retrieval_broker.py +129 -0
  121. services/router.py +414 -0
  122. services/runtime.py +326 -0
  123. services/session_benchmark.py +1945 -0
  124. services/session_manager.py +1026 -0
  125. services/session_preloader.py +251 -0
  126. services/text_index.py +90 -0
  127. services/tool_classifier.py +176 -0
  128. services/transcript_index.py +340 -0
  129. services/validation_cache.py +155 -0
  130. services/vector_store.py +299 -0
  131. services/version_tracker.py +271 -0
  132. services/watcher.py +192 -0
  133. tui/__init__.py +0 -0
  134. tui/backend.py +59 -0
  135. tui/main.py +145 -0
  136. tui/screens/__init__.py +1 -0
  137. tui/screens/benchmark_view.py +109 -0
  138. tui/screens/claudemd_view.py +46 -0
  139. tui/screens/compress_view.py +52 -0
  140. tui/screens/index_view.py +74 -0
  141. tui/screens/init_view.py +82 -0
  142. tui/screens/mcp_view.py +73 -0
  143. tui/screens/optimize_view.py +41 -0
  144. tui/screens/pipe_view.py +46 -0
  145. tui/screens/projects_view.py +355 -0
  146. tui/screens/search_view.py +55 -0
  147. tui/screens/session_view.py +143 -0
  148. tui/screens/stats.py +158 -0
  149. tui/screens/ui_view.py +54 -0
  150. tui/theme.tcss +335 -0
@@ -0,0 +1,459 @@
1
+ """EditLedger — persistent, git-integrated audit trail of AI edits.
2
+
3
+ Storage: .c3/edit_ledger.jsonl (append-only)
4
+
5
+ Performance: in-memory caches for version map and seq counter avoid
6
+ repeated full-file scans. Git info is captured via a single combined
7
+ command instead of 3 separate subprocesses.
8
+ """
9
+
10
+ import json
11
+ import subprocess
12
+ import sys
13
+ import threading
14
+ from collections import Counter
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+
18
+
19
+ class EditLedger:
20
+ """Tracks every AI edit with version numbering and git context."""
21
+
22
+ def __init__(self, project_path: str):
23
+ self.project_path = Path(project_path).resolve()
24
+ self.ledger_file = self.project_path / ".c3" / "edit_ledger.jsonl"
25
+ self.ledger_file.parent.mkdir(parents=True, exist_ok=True)
26
+ self._git_root = self._detect_git_root()
27
+ # In-memory caches — loaded lazily on first use, updated on writes
28
+ self._version_cache: dict[str, int] | None = None # {file: max_version}
29
+ self._total_count: int | None = None
30
+ self._seq_counter: int = 0 # monotonic within process lifetime
31
+ self._write_lock = threading.Lock()
32
+
33
+ # ── Cache management ──────────────────────────────────────────────
34
+
35
+ def _ensure_cache(self):
36
+ """Load version cache from ledger file (once per process)."""
37
+ if self._version_cache is not None:
38
+ return
39
+ self._version_cache = {}
40
+ self._total_count = 0
41
+ if not self.ledger_file.exists():
42
+ return
43
+ for line in self.ledger_file.read_text(encoding="utf-8").splitlines():
44
+ if not line.strip():
45
+ continue
46
+ try:
47
+ entry = json.loads(line)
48
+ except (json.JSONDecodeError, ValueError):
49
+ continue
50
+ if "target_id" in entry: # skip patch entries
51
+ continue
52
+ self._total_count += 1
53
+ f = entry.get("file", "")
54
+ v_str = entry.get("version", "v0")
55
+ try:
56
+ v_num = int(v_str.lstrip("v"))
57
+ self._version_cache[f] = max(self._version_cache.get(f, 0), v_num)
58
+ except (ValueError, AttributeError):
59
+ pass
60
+
61
+ # ── Public API ────────────────────────────────────────────────────
62
+
63
+ def log_edit(self, file: str, change_type: str, summary: str,
64
+ lines_changed: list = None, tags: list = None,
65
+ session_id: str = None, include_git: bool = True,
66
+ detail: dict = None) -> dict:
67
+ """Record an edit. Returns the entry dict.
68
+
69
+ Args:
70
+ include_git: if False, skips git subprocess calls (faster).
71
+ """
72
+ self._ensure_cache()
73
+ rel = file.replace("\\", "/")
74
+ now = datetime.now(timezone.utc)
75
+ self._seq_counter += 1
76
+
77
+ # Git info — single combined command when enabled
78
+ git_info = {"commit": "", "author": "", "subject": "", "dirty": False}
79
+ diff_summary = ""
80
+ if include_git and self._git_root:
81
+ git_info, diff_summary = self._git_combined(rel)
82
+
83
+ # Version from cache — O(1)
84
+ cur = self._version_cache.get(rel, 0)
85
+ new_v = cur + 1
86
+ self._version_cache[rel] = new_v
87
+
88
+ entry = {
89
+ "id": f"edit_{now.strftime('%Y%m%d_%H%M%S')}_{self._seq_counter:03d}",
90
+ "timestamp": now.isoformat(),
91
+ "session_id": session_id or "",
92
+ "file": rel,
93
+ "change_type": change_type,
94
+ "summary": summary,
95
+ "lines_changed": lines_changed,
96
+ "version": f"v{new_v}",
97
+ "git": git_info,
98
+ "diff_summary": diff_summary,
99
+ "tags": tags or [],
100
+ }
101
+ if detail:
102
+ entry["detail"] = detail
103
+ with open(self.ledger_file, "a", encoding="utf-8") as f:
104
+ f.write(json.dumps(entry) + "\n")
105
+ if self._total_count is not None:
106
+ self._total_count += 1
107
+ return entry
108
+
109
+ def get_history(self, file: str = None, limit: int = 50,
110
+ since: str = None) -> list:
111
+ """Query edits, optionally filtered by file and/or time."""
112
+ results = self._load_merged(file_filter=file, since_filter=since)
113
+ return results[-limit:]
114
+
115
+ def get_file_versions(self, file: str) -> list:
116
+ """All version entries for a specific file."""
117
+ return self.get_history(file=file, limit=10000)
118
+
119
+ def get_stats(self) -> dict:
120
+ """Summary: total edits, files edited, by change_type."""
121
+ if not self.ledger_file.exists():
122
+ return {"total": 0, "by_type": {}, "files": 0, "most_edited": []}
123
+ type_counts = Counter()
124
+ file_counts = Counter()
125
+ total = 0
126
+ with open(self.ledger_file, encoding="utf-8") as f:
127
+ for line in f:
128
+ if not line.strip():
129
+ continue
130
+ try:
131
+ entry = json.loads(line)
132
+ except json.JSONDecodeError:
133
+ continue
134
+ if "target_id" in entry: # skip patch entries
135
+ continue
136
+ total += 1
137
+ type_counts[entry.get("change_type", "unknown")] += 1
138
+ file_counts[entry.get("file", "unknown")] += 1
139
+ return {
140
+ "total": total,
141
+ "by_type": dict(type_counts),
142
+ "files": len(file_counts),
143
+ "most_edited": [
144
+ {"file": f, "count": c}
145
+ for f, c in file_counts.most_common(10)
146
+ ],
147
+ }
148
+
149
+ def get_version(self, file: str) -> str:
150
+ """Current version string for a file (e.g. 'v3')."""
151
+ self._ensure_cache()
152
+ rel = file.replace("\\", "/")
153
+ v = self._version_cache.get(rel, 0)
154
+ return f"v{v}" if v > 0 else "v0"
155
+
156
+ def tag_edit(self, edit_id: str, tag: str) -> bool:
157
+ """Add a tag to an existing edit. Rewrites the entry in-place."""
158
+ if not self.ledger_file.exists():
159
+ return False
160
+ lines = self.ledger_file.read_text(encoding="utf-8").splitlines()
161
+ found = False
162
+ new_lines = []
163
+ for line in lines:
164
+ if not line.strip():
165
+ new_lines.append(line)
166
+ continue
167
+ try:
168
+ entry = json.loads(line)
169
+ except json.JSONDecodeError:
170
+ new_lines.append(line)
171
+ continue
172
+ if entry.get("id") == edit_id:
173
+ tags = entry.get("tags", [])
174
+ if tag not in tags:
175
+ tags.append(tag)
176
+ entry["tags"] = tags
177
+ found = True
178
+ new_lines.append(json.dumps(entry))
179
+ else:
180
+ new_lines.append(line)
181
+ if found:
182
+ self.ledger_file.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
183
+ return found
184
+
185
+ # ── Private helpers ───────────────────────────────────────────────
186
+
187
+ def _tail_entries(self, limit: int) -> list:
188
+ """Read the last `limit` entries from the ledger efficiently."""
189
+ try:
190
+ raw = self.ledger_file.read_bytes()
191
+ except Exception:
192
+ return []
193
+ # Scan backwards for enough newlines
194
+ lines_found = []
195
+ pos = len(raw)
196
+ while pos > 0 and len(lines_found) < limit + 5: # extra buffer for blank lines
197
+ nl = raw.rfind(b"\n", 0, pos - 1)
198
+ chunk = raw[nl + 1:pos]
199
+ if chunk.strip():
200
+ lines_found.append(chunk)
201
+ pos = nl if nl >= 0 else 0
202
+ # Parse in forward order
203
+ results = []
204
+ for raw_line in reversed(lines_found):
205
+ try:
206
+ results.append(json.loads(raw_line))
207
+ except (json.JSONDecodeError, ValueError):
208
+ continue
209
+ return results[-limit:]
210
+
211
+ def _detect_git_root(self):
212
+ """Find git root directory."""
213
+ try:
214
+ kwargs = {}
215
+ if sys.platform == "win32":
216
+ kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
217
+ result = subprocess.run(
218
+ ["git", "rev-parse", "--show-toplevel"],
219
+ cwd=self.project_path,
220
+ capture_output=True, text=True, timeout=3,
221
+ stdin=subprocess.DEVNULL,
222
+ **kwargs,
223
+ )
224
+ if result.returncode == 0:
225
+ return Path(result.stdout.strip()).resolve()
226
+ except Exception:
227
+ pass
228
+ return None
229
+
230
+ def _git_combined(self, rel_path: str) -> tuple:
231
+ """Capture git info + diff in a single subprocess call.
232
+
233
+ Returns (git_info_dict, diff_summary_str).
234
+ """
235
+ info = {"commit": "", "author": "", "subject": "", "dirty": False}
236
+ diff_summary = ""
237
+ abs_path = (self.project_path / rel_path).resolve()
238
+ try:
239
+ git_rel = str(abs_path.relative_to(self._git_root))
240
+ except Exception:
241
+ return info, diff_summary
242
+
243
+ kwargs = {}
244
+ if sys.platform == "win32":
245
+ kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
246
+
247
+ # Single shell command: status + log + diff
248
+ # Using && chaining so we get all info in one subprocess
249
+ sep = "---C3SEP---"
250
+ cmd = (
251
+ f'git status --porcelain -- "{git_rel}" && '
252
+ f'echo {sep} && '
253
+ f'git log -1 --format="%H%x1f%an%x1f%s" -- "{git_rel}" && '
254
+ f'echo {sep} && '
255
+ f'git diff --numstat -- "{git_rel}"'
256
+ )
257
+ try:
258
+ proc = subprocess.Popen(
259
+ cmd, shell=True,
260
+ cwd=self._git_root,
261
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
262
+ stdin=subprocess.DEVNULL,
263
+ text=True, **kwargs,
264
+ )
265
+ try:
266
+ stdout, _ = proc.communicate(timeout=4)
267
+ except subprocess.TimeoutExpired:
268
+ if sys.platform == "win32":
269
+ subprocess.run(
270
+ ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
271
+ capture_output=True, **kwargs,
272
+ )
273
+ else:
274
+ proc.kill()
275
+ proc.communicate()
276
+ return info, diff_summary
277
+ parts = (stdout or "").split(sep)
278
+ # Part 0: status --porcelain output
279
+ if len(parts) > 0:
280
+ info["dirty"] = bool(parts[0].strip())
281
+ # Part 1: log output
282
+ if len(parts) > 1:
283
+ log_line = parts[1].strip()
284
+ fields = log_line.split("\x1f")
285
+ if len(fields) == 3:
286
+ info["commit"] = fields[0][:12]
287
+ info["author"] = fields[1]
288
+ info["subject"] = fields[2]
289
+ # Part 2: diff --numstat output
290
+ if len(parts) > 2:
291
+ diff_line = parts[2].strip()
292
+ if diff_line:
293
+ nums = diff_line.split("\t")
294
+ if len(nums) >= 2:
295
+ diff_summary = f"+{nums[0]} -{nums[1]}"
296
+ except Exception:
297
+ pass
298
+
299
+ return info, diff_summary
300
+
301
+ # ── Async enrichment ──────────────────────────────────────────────
302
+
303
+ def _load_merged(self, file_filter: str = None, since_filter: str = None) -> list:
304
+ """Read all base entries with any appended patches merged in.
305
+
306
+ Patch entries are identified by having a 'target_id' field.
307
+ Returns entries sorted by timestamp ascending.
308
+ """
309
+ if not self.ledger_file.exists():
310
+ return []
311
+ base: dict = {} # id → entry dict
312
+ patches: dict = {} # target_id → list of patch dicts
313
+ try:
314
+ for line in self.ledger_file.read_text(encoding="utf-8").splitlines():
315
+ if not line.strip():
316
+ continue
317
+ try:
318
+ entry = json.loads(line)
319
+ except (json.JSONDecodeError, ValueError):
320
+ continue
321
+ if "target_id" in entry:
322
+ patches.setdefault(entry["target_id"], []).append(entry)
323
+ else:
324
+ eid = entry.get("id")
325
+ if eid:
326
+ base[eid] = entry
327
+ except Exception:
328
+ return []
329
+
330
+ # Apply patches — each patch carries git enrichment and/or validation data
331
+ for target_id, patch_list in patches.items():
332
+ if target_id not in base:
333
+ continue
334
+ for patch in patch_list:
335
+ if "git" in patch:
336
+ base[target_id]["git"] = patch["git"]
337
+ base[target_id]["diff_summary"] = patch.get("diff_summary", "")
338
+ base[target_id].pop("git_pending", None)
339
+ if "valid" in patch:
340
+ base[target_id]["valid"] = patch["valid"]
341
+ if patch.get("errors"):
342
+ base[target_id]["lint_errors"] = patch["errors"]
343
+
344
+ norm_file = file_filter.replace("\\", "/") if file_filter else None
345
+ results = []
346
+ for entry in base.values():
347
+ if norm_file and entry.get("file") != norm_file:
348
+ continue
349
+ if since_filter and entry.get("timestamp", "") < since_filter:
350
+ continue
351
+ results.append(entry)
352
+ results.sort(key=lambda e: e.get("timestamp", ""))
353
+ return results
354
+
355
+ def enrich_pending(self, batch: int = 10) -> int:
356
+ """Find hook-logged entries with git_pending=True and append git patches.
357
+
358
+ Called by EditLedgerEnricherAgent on a background timer.
359
+ Returns the number of entries enriched this cycle.
360
+ """
361
+ if not self.ledger_file.exists() or not self._git_root:
362
+ return 0
363
+ pending = []
364
+ already_patched: set = set()
365
+ try:
366
+ for line in self.ledger_file.read_text(encoding="utf-8").splitlines():
367
+ if not line.strip():
368
+ continue
369
+ try:
370
+ entry = json.loads(line)
371
+ except (json.JSONDecodeError, ValueError):
372
+ continue
373
+ if "target_id" in entry and "git" in entry:
374
+ already_patched.add(entry["target_id"])
375
+ elif "target_id" not in entry and entry.get("git_pending") and entry.get("file"):
376
+ pending.append(entry)
377
+ except Exception:
378
+ return 0
379
+
380
+ to_enrich = [e for e in pending if e["id"] not in already_patched][:batch]
381
+ if not to_enrich:
382
+ return 0
383
+
384
+ patches = []
385
+ for entry in to_enrich:
386
+ git_info, diff_summary = self._git_combined(entry["file"])
387
+ patches.append({
388
+ "target_id": entry["id"],
389
+ "git": git_info,
390
+ "diff_summary": diff_summary,
391
+ "enriched_at": datetime.now(timezone.utc).isoformat(),
392
+ })
393
+
394
+ with self._write_lock:
395
+ try:
396
+ with open(self.ledger_file, "a", encoding="utf-8") as f:
397
+ for patch in patches:
398
+ f.write(json.dumps(patch) + "\n")
399
+ except Exception:
400
+ return 0
401
+ return len(patches)
402
+
403
+ def validate_pending(self, batch: int = 5, validation_cache=None) -> list:
404
+ """Find recently-edited files without validation results and validate them.
405
+
406
+ Appends validate patches to the ledger and returns result dicts.
407
+ Called by EditLedgerEnricherAgent on a background timer.
408
+ """
409
+ if not self.ledger_file.exists() or not validation_cache:
410
+ return []
411
+ pending = []
412
+ already_validated: set = set()
413
+ try:
414
+ for line in self.ledger_file.read_text(encoding="utf-8").splitlines():
415
+ if not line.strip():
416
+ continue
417
+ try:
418
+ entry = json.loads(line)
419
+ except (json.JSONDecodeError, ValueError):
420
+ continue
421
+ if "target_id" in entry and "valid" in entry:
422
+ already_validated.add(entry["target_id"])
423
+ elif "target_id" not in entry and entry.get("file"):
424
+ pending.append(entry)
425
+ except Exception:
426
+ return []
427
+
428
+ # Most recent first, skip already validated
429
+ to_validate = [e for e in reversed(pending) if e["id"] not in already_validated][:batch]
430
+ if not to_validate:
431
+ return []
432
+
433
+ results = []
434
+ patches = []
435
+ for entry in to_validate:
436
+ try:
437
+ result = validation_cache.validate_file(entry["file"])
438
+ except Exception:
439
+ result = None
440
+ if result is None:
441
+ continue
442
+ patch = {
443
+ "target_id": entry["id"],
444
+ "valid": result.get("valid", True),
445
+ "errors": result.get("errors", []),
446
+ "validated_at": datetime.now(timezone.utc).isoformat(),
447
+ }
448
+ patches.append(patch)
449
+ results.append({"id": entry["id"], "file": entry["file"], **patch})
450
+
451
+ if patches:
452
+ with self._write_lock:
453
+ try:
454
+ with open(self.ledger_file, "a", encoding="utf-8") as f:
455
+ for patch in patches:
456
+ f.write(json.dumps(patch) + "\n")
457
+ except Exception:
458
+ pass
459
+ return results