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/__init__.py +38 -0
- data_advisor/__main__.py +9 -0
- data_advisor/advisor.py +436 -0
- data_advisor/cli.py +160 -0
- data_advisor/codegen.py +515 -0
- data_advisor/rules.py +380 -0
- data_advisor/scanner.py +655 -0
- data_advisor/search.py +679 -0
- data_advisor/visualizer.py +236 -0
- data_advisor-0.1.0.dist-info/METADATA +303 -0
- data_advisor-0.1.0.dist-info/RECORD +14 -0
- data_advisor-0.1.0.dist-info/WHEEL +5 -0
- data_advisor-0.1.0.dist-info/entry_points.txt +2 -0
- data_advisor-0.1.0.dist-info/top_level.txt +1 -0
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()
|