data-advisor 0.1.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.
data_advisor/search.py ADDED
@@ -0,0 +1,679 @@
1
+ """
2
+ search.py - Intelligent code search engine for local code banks and snippets.
3
+
4
+ Allows students and developers to quickly retrieve code templates, sensor scripts,
5
+ and machine learning pipelines during timed examinations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional, Tuple, Union
16
+
17
+ SUPPORTED_EXTENSIONS = {
18
+ ".py", ".ipynb", ".ino", ".cpp", ".c", ".h", ".hpp",
19
+ ".txt", ".md", ".sh", ".sql", ".json", ".yaml", ".yml"
20
+ }
21
+
22
+ IGNORE_DIRS = {
23
+ ".git", ".pytest_cache", ".venv", "venv", "env", "__pycache__",
24
+ "node_modules", "build", "dist", ".idea", ".vscode"
25
+ }
26
+
27
+ # Semantic query expansion dictionary for common technical terms
28
+ QUERY_ALIASES = {
29
+ "rf": ["random forest"],
30
+ "dt": ["decision tree"],
31
+ "lr": ["linear regression", "logistic regression"],
32
+ "nb": ["naive bayes"],
33
+ "knn": ["k nearest neighbors", "kneighbors"],
34
+ "kmeans": ["k means", "k-means", "clustering"],
35
+ "eda": ["exploratory", "visualization", "data analysis"],
36
+ "iss": ["international space station", "iss-now"],
37
+ "usgs": ["earthquake"],
38
+ "coap": ["aiocoap", "coap server", "coap client"],
39
+ "dht": ["dht22", "dht11", "temperature", "humidity"],
40
+ "ldr": ["light intensity", "analogread", "photoresistor"],
41
+ "pir": ["motion detection", "motion sensor"],
42
+ "pca": ["principal component", "dimensionality reduction"],
43
+ "nlp": ["tfidf", "countvectorizer", "tokenizer"],
44
+ "apriori": ["association rules", "frequent itemsets", "groceries", "lift"],
45
+ }
46
+
47
+
48
+ def safe_print(text: str) -> None:
49
+ """Print text safely across operating systems without charmap/cp1252 encode errors."""
50
+ try:
51
+ print(text)
52
+ except UnicodeEncodeError:
53
+ enc = getattr(sys.stdout, "encoding", None) or "utf-8"
54
+ try:
55
+ print(text.encode(enc, errors="replace").decode(enc))
56
+ except Exception:
57
+ print(text.encode("ascii", errors="replace").decode("ascii"))
58
+
59
+
60
+ def extract_code_from_file(file_path: Path) -> str:
61
+ """Read file content, automatically extracting code cells if it's a Jupyter Notebook."""
62
+ try:
63
+ if file_path.suffix.lower() == ".ipynb":
64
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
65
+ nb = json.load(f)
66
+ code_cells = []
67
+ for cell in nb.get("cells", []):
68
+ if cell.get("cell_type") == "code":
69
+ source = cell.get("source", [])
70
+ cell_text = "".join(source) if isinstance(source, list) else source
71
+ if cell_text.strip():
72
+ code_cells.append(cell_text)
73
+ return "\n\n# --- [Notebook Cell] ---\n".join(code_cells)
74
+ else:
75
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
76
+ return f.read()
77
+ except Exception as exc:
78
+ return f"# [Error reading {file_path.name}: {exc}]"
79
+
80
+
81
+ def extract_file_description(file_path: Path) -> str:
82
+ """Extract a concise 1-line description from file header, docstring or initial comment."""
83
+ content = extract_code_from_file(file_path)
84
+ lines = content.strip().splitlines()
85
+ for line in lines[:15]:
86
+ line_clean = line.strip().lstrip("/*#\"'").rstrip("*/\"'").strip()
87
+ if len(line_clean) > 8 and not line_clean.startswith("=") and not line_clean.lower().endswith(file_path.suffix.lower()):
88
+ return line_clean[:80]
89
+ return file_path.name
90
+
91
+
92
+ class CodeSearchEngine:
93
+ """
94
+ Dynamic code search engine that scans directories of code files,
95
+ ranks them by relevance to ANY given keyword(s), and retrieves complete files.
96
+
97
+ Dynamically scans the target directory on every search, automatically picking
98
+ up any newly pasted, modified, or removed files without requiring a restart.
99
+ """
100
+
101
+ def __init__(self, search_paths: Optional[Union[str, Path, List[Union[str, Path]]]] = None) -> None:
102
+ if search_paths is None:
103
+ self.search_paths = self._discover_default_paths()
104
+ elif isinstance(search_paths, (str, Path)):
105
+ self.search_paths = [Path(search_paths)]
106
+ else:
107
+ self.search_paths = [Path(p) for p in search_paths]
108
+
109
+ def _discover_default_paths(self) -> List[Path]:
110
+ """Auto-detect code bank directory in cwd or project root, prioritizing 'code_bank'."""
111
+ cwd = Path.cwd()
112
+ package_root = Path(__file__).resolve().parent.parent.parent
113
+
114
+ # 1. Check for 'code_bank' directory
115
+ for base in [cwd, package_root]:
116
+ cb = base / "code_bank"
117
+ if cb.exists() and cb.is_dir():
118
+ return [cb]
119
+
120
+ # 2. Check other common folder names
121
+ for candidate in ["codes", "snippets", "scripts", "examples"]:
122
+ for base in [cwd, package_root]:
123
+ p = base / candidate
124
+ if p.exists() and p.is_dir():
125
+ return [p]
126
+
127
+ # 3. Default to current working directory
128
+ return [cwd]
129
+
130
+ def collect_code_files(self) -> List[Path]:
131
+ """Recursively gather all eligible code files across configured search paths."""
132
+ files: List[Path] = []
133
+ seen = set()
134
+ for root_path in self.search_paths:
135
+ if not root_path.exists():
136
+ continue
137
+ if root_path.is_file():
138
+ if root_path.suffix.lower() in SUPPORTED_EXTENSIONS and root_path.resolve() not in seen:
139
+ files.append(root_path)
140
+ seen.add(root_path.resolve())
141
+ continue
142
+
143
+ for dirpath, dirnames, filenames in os.walk(root_path):
144
+ # Prune ignored directories
145
+ dirnames[:] = [d for d in dirnames if d not in IGNORE_DIRS and not d.startswith(".")]
146
+ for fn in sorted(filenames):
147
+ p = Path(dirpath) / fn
148
+ if p.suffix.lower() in SUPPORTED_EXTENSIONS and p.resolve() not in seen:
149
+ files.append(p)
150
+ seen.add(p.resolve())
151
+ return files
152
+
153
+ def list_all(self, print_output: bool = True) -> str:
154
+ """List all indexed code snippets with a 1-line summary."""
155
+ all_files = self.collect_code_files()
156
+ divider = "=" * 80
157
+ paths_str = ", ".join(str(p) for p in self.search_paths)
158
+
159
+ if not all_files:
160
+ msg = (
161
+ f"\n{divider}\n"
162
+ f" DATA ADVISOR CODE BANK: 0 FILES FOUND\n"
163
+ f"{divider}\n"
164
+ f"[*] Monitored Folder: {paths_str}\n"
165
+ f"[*] The folder is currently empty.\n"
166
+ f"[*] Paste your Python (.py), Jupyter Notebook (.ipynb), Arduino (.ino, .cpp),\n"
167
+ f" or text files into 'code_bank/'.\n"
168
+ f"[*] Any keywords you search for will immediately be found from your pasted files!\n"
169
+ f"{divider}\n"
170
+ )
171
+ if print_output:
172
+ safe_print(msg)
173
+ return msg
174
+
175
+ lines = [
176
+ f"\n{divider}",
177
+ f" DATA ADVISOR CODE BANK: {len(all_files)} CODE FILE(S) AVAILABLE",
178
+ divider,
179
+ f" Folder: {paths_str}",
180
+ f" {'#':<3} | {'File Name':<34} | {'Description'}",
181
+ f" {'-'*3}-|-{'-'*34}-|-{'-'*38}",
182
+ ]
183
+
184
+ for idx, fp in enumerate(all_files, 1):
185
+ desc = extract_file_description(fp)
186
+ lines.append(f" {idx:<3} | {fp.name:<34} | {desc}")
187
+
188
+ lines.append(divider)
189
+ lines.append("[*] Search any keyword : search('<keyword>') or data-advisor --search '<keyword>'")
190
+ lines.append("[*] Get complete file : get_file('<q_num or filename>') or data-advisor --get '<name>'")
191
+ lines.append("")
192
+
193
+ out_str = "\n".join(lines)
194
+ if print_output:
195
+ safe_print(out_str)
196
+ return out_str
197
+
198
+ def score_file(self, file_path: Path, query: Union[str, List[str], Tuple[str, ...]]) -> Tuple[int, str]:
199
+ """
200
+ Compute a relevance score for a file against any keyword(s).
201
+ Returns (score, file_content).
202
+ """
203
+ content = extract_code_from_file(file_path)
204
+ if not content.strip():
205
+ return 0, ""
206
+
207
+ score = 0
208
+ if isinstance(query, (list, tuple)):
209
+ q_raw = " ".join(str(k) for k in query).strip().lower()
210
+ else:
211
+ q_raw = str(query).strip().lower()
212
+
213
+ if not q_raw:
214
+ return 0, content
215
+
216
+ filename_lower = file_path.name.lower()
217
+ filename_clean = filename_lower.replace("_", " ").replace("-", " ")
218
+ content_lower = content.lower()
219
+
220
+ # Handle Question numbers (e.g. 'q1', 'q 1', 'question 1', 'q01', '1', '24')
221
+ q_num_match = re.search(r"(?:^|\b)(?:q|question)?\s*(\d+)(?:\b|$)", q_raw)
222
+ if q_num_match:
223
+ num_str = q_num_match.group(1)
224
+ try:
225
+ num_int = int(num_str)
226
+ num_2d = f"{num_int:02d}"
227
+ except ValueError:
228
+ num_int = None
229
+ num_2d = num_str
230
+
231
+ if (
232
+ f"question_{num_str}." in filename_lower
233
+ or f"question_{num_2d}." in filename_lower
234
+ or f"q{num_str}." in filename_lower
235
+ or f"q{num_2d}." in filename_lower
236
+ or filename_lower.startswith(f"{num_2d}_")
237
+ or filename_lower.startswith(f"{num_str}_")
238
+ or file_path.parent.name.lower().startswith(f"{num_2d}_")
239
+ ):
240
+ score += 500
241
+ elif (
242
+ f"question {num_str}" in content_lower
243
+ or f"question {num_2d}" in content_lower
244
+ or f"question_{num_str}" in content_lower
245
+ or f"question_{num_2d}" in content_lower
246
+ ):
247
+ score += 200
248
+
249
+ # Exact phrase match in filename
250
+ if q_raw in filename_clean or q_raw in filename_lower:
251
+ score += 250
252
+
253
+ # Extract all search tokens
254
+ raw_tokens = [tok for tok in re.split(r"[\s,;\-_+&|/]+", q_raw) if len(tok) >= 1]
255
+ if not raw_tokens:
256
+ raw_tokens = [q_raw]
257
+
258
+ # Aliases expansion for common machine learning / exam abbreviations
259
+ expanded_tokens = list(raw_tokens)
260
+ for tok in raw_tokens:
261
+ if tok in QUERY_ALIASES:
262
+ expanded_tokens.extend(QUERY_ALIASES[tok])
263
+
264
+ # Exact phrase match in content
265
+ if q_raw in content_lower:
266
+ count = content_lower.count(q_raw)
267
+ score += min(150, 60 + (count * 20))
268
+
269
+ # Check each keyword token
270
+ all_tokens_present = True
271
+ for tok in raw_tokens:
272
+ found_token = False
273
+
274
+ if tok in filename_clean or tok in file_path.parent.name.lower():
275
+ score += 70
276
+ found_token = True
277
+ if re.search(rf"\b{re.escape(tok)}\b", filename_clean):
278
+ score += 30
279
+
280
+ c = content_lower.count(tok)
281
+ if c > 0:
282
+ found_token = True
283
+ score += min(50, 15 + (c * 7))
284
+ if re.search(rf"\b{re.escape(tok)}\b", content_lower):
285
+ score += 20
286
+
287
+ if not found_token:
288
+ all_tokens_present = False
289
+
290
+ if all_tokens_present and len(raw_tokens) > 1:
291
+ score += 100
292
+
293
+ if score == 0:
294
+ for alias in expanded_tokens:
295
+ if alias in filename_clean or alias in content_lower:
296
+ score += 40
297
+
298
+ return score, content
299
+
300
+ def resolve_file(self, query: Union[str, int]) -> Optional[Path]:
301
+ """
302
+ Resolve a question number, filename, relative path, or search term
303
+ to the exact single target Path.
304
+ """
305
+ all_files = self.collect_code_files()
306
+ if not all_files:
307
+ return None
308
+
309
+ q_str = str(query).strip()
310
+
311
+ # 1. Direct path exists
312
+ direct_path = Path(q_str)
313
+ if direct_path.exists() and direct_path.is_file():
314
+ return direct_path
315
+
316
+ # 2. Check within configured search paths
317
+ for base in self.search_paths:
318
+ candidate = base / q_str
319
+ if candidate.exists() and candidate.is_file():
320
+ return candidate
321
+
322
+ # 3. Match question number e.g. 1, '1', '01', 'q1', 'q01', 'question 1', 'question_01'
323
+ m = re.search(r"(?:^|\b)(?:q|question)?\s*(\d+)(?:\b|$)", q_str.lower())
324
+ if m:
325
+ num = int(m.group(1))
326
+ num_2d = f"{num:02d}"
327
+
328
+ # Priority A: question_XX.py
329
+ for fp in all_files:
330
+ if fp.name.lower() in (f"question_{num_2d}.py", f"question_{num}.py"):
331
+ return fp
332
+
333
+ # Priority B: Folder matching number e.g. 01_dht22_serial
334
+ candidates = []
335
+ for fp in all_files:
336
+ parent_lower = fp.parent.name.lower()
337
+ if parent_lower.startswith(f"{num_2d}_") or parent_lower.startswith(f"{num}_"):
338
+ candidates.append(fp)
339
+
340
+ if candidates:
341
+ # Prefer sketch.ino or primary python script or app
342
+ for preferred in ["sketch.ino", "app.py", f"question_{num_2d}.py"]:
343
+ for c in candidates:
344
+ if c.name.lower() == preferred:
345
+ return c
346
+ for c in candidates:
347
+ if c.suffix.lower() in (".py", ".ino") and not c.name.startswith("__"):
348
+ return c
349
+ return candidates[0]
350
+
351
+ # 4. Exact filename match (case-insensitive)
352
+ for fp in all_files:
353
+ if fp.name.lower() == q_str.lower():
354
+ return fp
355
+
356
+ # 5. Filename without extension match
357
+ for fp in all_files:
358
+ if fp.stem.lower() == q_str.lower():
359
+ return fp
360
+
361
+ # 6. Fallback: highest scoring match from search engine
362
+ scored = []
363
+ for fp in all_files:
364
+ score, _ = self.score_file(fp, q_str)
365
+ if score > 0:
366
+ scored.append((score, fp))
367
+ if scored:
368
+ scored.sort(key=lambda x: x[0], reverse=True)
369
+ return scored[0][1]
370
+
371
+ return None
372
+
373
+ def get_file(
374
+ self,
375
+ query: Union[str, int],
376
+ print_output: bool = True,
377
+ line_numbers: bool = False,
378
+ ) -> str:
379
+ """
380
+ Retrieve and optionally print the COMPLETE, raw content of a file
381
+ by question number, filename, or keyword.
382
+ """
383
+ target = self.resolve_file(query)
384
+ if target is None:
385
+ msg = f"[!] Could not find any code file matching: '{query}'"
386
+ if print_output:
387
+ safe_print(msg)
388
+ return msg
389
+
390
+ raw_content = extract_code_from_file(target)
391
+
392
+ divider = "=" * 80
393
+ lines = raw_content.splitlines()
394
+ line_count = len(lines)
395
+ byte_size = target.stat().st_size if target.exists() else len(raw_content)
396
+
397
+ header = (
398
+ f"\n{divider}\n"
399
+ f" COMPLETE FILE: {target.name} ({line_count} lines, {byte_size:,} bytes)\n"
400
+ f" FILE PATH : {target.resolve()}\n"
401
+ f"{divider}\n"
402
+ )
403
+
404
+ if line_numbers:
405
+ numbered = [f"{i:4d} | {l}" for i, l in enumerate(lines, start=1)]
406
+ body = "\n".join(numbered)
407
+ else:
408
+ body = raw_content
409
+
410
+ full_display = f"{header}{body}\n{divider}\n"
411
+ if print_output:
412
+ safe_print(full_display)
413
+
414
+ return raw_content
415
+
416
+ def run_file(
417
+ self,
418
+ query: Union[str, int],
419
+ args: Optional[List[str]] = None,
420
+ timeout: Optional[int] = None,
421
+ ) -> int:
422
+ """Execute a Python file directly by question number, filename, or query."""
423
+ import subprocess
424
+
425
+ target = self.resolve_file(query)
426
+ if target is None:
427
+ safe_print(f"[!] Could not find any code file matching: '{query}'")
428
+ return 1
429
+
430
+ if target.suffix.lower() != ".py":
431
+ safe_print(f"[!] Cannot directly execute non-Python file: {target.name} (use get_file() to view code)")
432
+ return 1
433
+
434
+ cmd = [sys.executable, str(target.resolve())] + (args or [])
435
+ safe_print(f"[*] Executing {target.name} (Path: {target.resolve()})...\n")
436
+ try:
437
+ proc = subprocess.run(cmd, cwd=str(target.parent), timeout=timeout)
438
+ return proc.returncode
439
+ except Exception as err:
440
+ safe_print(f"[!] Execution failed: {err}")
441
+ return 1
442
+
443
+ def search(
444
+ self,
445
+ key: Optional[Union[str, List[str], Tuple[str, ...]]] = None,
446
+ max_results: int = 3,
447
+ print_output: bool = True,
448
+ ) -> str:
449
+ """
450
+ Search for matching files and return formatted code response.
451
+ If key is None, empty, 'list', or 'all', displays directory listing.
452
+ """
453
+ if key is None:
454
+ return self.list_all(print_output=print_output)
455
+
456
+ if isinstance(key, (list, tuple)):
457
+ if not key:
458
+ return self.list_all(print_output=print_output)
459
+ key_str = " ".join(str(k) for k in key)
460
+ else:
461
+ key_str = str(key).strip()
462
+ if not key_str or key_str.lower() in ("list", "all", "dir", "help", "--list", "-l"):
463
+ return self.list_all(print_output=print_output)
464
+
465
+ all_files = self.collect_code_files()
466
+ if not all_files:
467
+ divider = "=" * 80
468
+ paths_str = ", ".join(str(p) for p in self.search_paths)
469
+ msg = (
470
+ f"\n{divider}\n"
471
+ f" DATA ADVISOR CODE BANK: 0 FILES FOUND\n"
472
+ f"{divider}\n"
473
+ f"[*] Monitored Folder: {paths_str}\n"
474
+ f"[*] The folder is currently empty.\n"
475
+ f"[*] Paste your Python (.py), Jupyter Notebook (.ipynb), Arduino (.ino, .cpp),\n"
476
+ f" or text files into 'code_bank/'.\n"
477
+ f"{divider}\n"
478
+ )
479
+ if print_output:
480
+ safe_print(msg)
481
+ return msg
482
+
483
+ results: List[Tuple[int, Path, str]] = []
484
+ for fp in all_files:
485
+ score, content = self.score_file(fp, key_str)
486
+ if score > 0:
487
+ results.append((score, fp, content))
488
+
489
+ results.sort(key=lambda x: x[0], reverse=True)
490
+
491
+ if not results:
492
+ available = [fp.name for fp in all_files[:10]]
493
+ msg = (
494
+ f"\n[!] No matches found for keyword: '{key_str}'\n"
495
+ f"[*] Available code files in code bank ({len(all_files)} total):\n"
496
+ f" - " + "\n - ".join(available)
497
+ )
498
+ if len(all_files) > 10:
499
+ msg += f"\n ... and {len(all_files) - 10} more. Run search('list') to see all."
500
+ if print_output:
501
+ safe_print(msg)
502
+ return msg
503
+
504
+ output_blocks = []
505
+ divider = "=" * 80
506
+ thin_div = "-" * 80
507
+
508
+ top_results = results[:max_results]
509
+ header = (
510
+ f"\n{divider}\n"
511
+ f" DATA ADVISOR CODE SEARCH RESULTS FOR: '{key_str}' ({len(top_results)} match(es))\n"
512
+ f"{divider}\n"
513
+ )
514
+ output_blocks.append(header)
515
+
516
+ for rank, (score, file_path, content) in enumerate(top_results, 1):
517
+ block = [
518
+ f"\n[MATCH {rank}/{len(top_results)}] File: {file_path.name} | Relevance Score: {score}",
519
+ f"Path: {file_path.resolve()}",
520
+ thin_div,
521
+ content.strip(),
522
+ thin_div,
523
+ ]
524
+ output_blocks.append("\n".join(block))
525
+
526
+ full_output = "\n".join(output_blocks)
527
+ if print_output:
528
+ safe_print(full_output)
529
+
530
+ return full_output
531
+
532
+
533
+ # =============================================================================
534
+ # TOP-LEVEL CONVENIENCE FUNCTIONS
535
+ # =============================================================================
536
+
537
+ def search(
538
+ key: Optional[Union[str, List[str], Tuple[str, ...]]] = None,
539
+ folder: Optional[Union[str, Path, List[Union[str, Path]]]] = None,
540
+ max_results: int = 3,
541
+ print_output: bool = True,
542
+ ) -> str:
543
+ """
544
+ Search local code files and return matching code.
545
+
546
+ Parameters
547
+ ----------
548
+ key : Optional[Union[str, List[str], Tuple[str, ...]]], default=None
549
+ Search query, e.g. 'wokwi', 'linear regression', 'q1', 'esp32', ['kmeans', 'elbow'].
550
+ If None or 'list', lists all available code files in the code bank.
551
+ folder : Optional[Union[str, Path, List[Union[str, Path]]]], default=None
552
+ Directory or list of directories containing code files.
553
+ If None, automatically scans './code_bank'.
554
+ max_results : int, default=3
555
+ Maximum number of matching files to return.
556
+ print_output : bool, default=True
557
+ Whether to print the formatted response directly to the console.
558
+
559
+ Returns
560
+ -------
561
+ str
562
+ The formatted code response text.
563
+ """
564
+ engine = CodeSearchEngine(search_paths=folder)
565
+ return engine.search(key=key, max_results=max_results, print_output=print_output)
566
+
567
+
568
+ def get_file(
569
+ query: Union[str, int],
570
+ folder: Optional[Union[str, Path, List[Union[str, Path]]]] = None,
571
+ print_output: bool = True,
572
+ line_numbers: bool = False,
573
+ ) -> str:
574
+ """
575
+ Retrieve and display the COMPLETE raw file content by question number, filename, or keyword.
576
+
577
+ Parameters
578
+ ----------
579
+ query : Union[str, int]
580
+ Question number (e.g. 1, 'q1', 'question_01', 24),
581
+ filename (e.g. 'question_01.py', 'sketch.ino', 'app.py'),
582
+ or keyword / path (e.g. '04_mqtt_esp32_python/subscriber.py', 'iss').
583
+ folder : Optional[Union[str, Path, List[Union[str, Path]]]], default=None
584
+ Directory containing code files (defaults to code_bank).
585
+ print_output : bool, default=True
586
+ If True, prints the complete file content to the console.
587
+ line_numbers : bool, default=False
588
+ If True, prints line numbers alongside the code.
589
+
590
+ Returns
591
+ -------
592
+ str
593
+ The exact, complete raw content of the matching file.
594
+
595
+ Examples
596
+ --------
597
+ >>> from data_advisor import get_file
598
+ >>> get_file("q1") # Displays complete code for Question 1
599
+ >>> get_file(24) # Displays complete code for Question 24
600
+ >>> get_file("sketch.ino") # Displays ESP32 Arduino sketch
601
+ >>> code = get_file("q05", print_output=False) # Returns code as string
602
+ """
603
+ engine = CodeSearchEngine(search_paths=folder)
604
+ return engine.get_file(query=query, print_output=print_output, line_numbers=line_numbers)
605
+
606
+
607
+ def read_file(
608
+ query: Union[str, int],
609
+ folder: Optional[Union[str, Path, List[Union[str, Path]]]] = None,
610
+ ) -> str:
611
+ """
612
+ Return the COMPLETE raw file content as a string without printing to console.
613
+
614
+ Parameters
615
+ ----------
616
+ query : Union[str, int]
617
+ Question number or filename.
618
+ folder : Optional[Union[str, Path, List[Union[str, Path]]]], default=None
619
+ Folder to search within.
620
+
621
+ Returns
622
+ -------
623
+ str
624
+ Raw file content string.
625
+ """
626
+ return get_file(query=query, folder=folder, print_output=False)
627
+
628
+
629
+ def view_file(
630
+ query: Union[str, int],
631
+ folder: Optional[Union[str, Path, List[Union[str, Path]]]] = None,
632
+ line_numbers: bool = False,
633
+ ) -> str:
634
+ """
635
+ Convenience alias for get_file(query, print_output=True).
636
+ Displays the entire file completely in the console with header metadata.
637
+ """
638
+ return get_file(query=query, folder=folder, print_output=True, line_numbers=line_numbers)
639
+
640
+
641
+ def run_file(
642
+ query: Union[str, int],
643
+ folder: Optional[Union[str, Path, List[Union[str, Path]]]] = None,
644
+ args: Optional[List[str]] = None,
645
+ timeout: Optional[int] = None,
646
+ ) -> int:
647
+ """
648
+ Execute a Python file directly by question number, filename, or query.
649
+
650
+ Parameters
651
+ ----------
652
+ query : Union[str, int]
653
+ Question number (e.g. 1, 'q1', 'question_24') or filename.
654
+ folder : Optional[Union[str, Path, List[Union[str, Path]]]], default=None
655
+ Folder containing the code file.
656
+ args : Optional[List[str]], default=None
657
+ Command-line arguments to pass to the script.
658
+ timeout : Optional[int], default=None
659
+ Execution timeout in seconds.
660
+
661
+ Returns
662
+ -------
663
+ int
664
+ The process return code (0 = success).
665
+ """
666
+ engine = CodeSearchEngine(search_paths=folder)
667
+ return engine.run_file(query=query, args=args, timeout=timeout)
668
+
669
+
670
+ def list_files(
671
+ folder: Optional[Union[str, Path, List[Union[str, Path]]]] = None,
672
+ print_output: bool = True,
673
+ ) -> List[Path]:
674
+ """
675
+ List all indexed code snippets with a 1-line summary and return the list of Paths.
676
+ """
677
+ engine = CodeSearchEngine(search_paths=folder)
678
+ engine.list_all(print_output=print_output)
679
+ return engine.collect_code_files()