signalpilot-ai-internal 0.4.10__py3-none-any.whl → 0.7.6__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.
- signalpilot_ai_internal/_version.py +1 -1
- signalpilot_ai_internal/cache_service.py +152 -1
- signalpilot_ai_internal/file_scanner_service.py +1395 -0
- signalpilot_ai_internal/handlers.py +478 -2
- signalpilot_ai_internal/html_export_template/README.md +23 -0
- signalpilot_ai_internal/html_export_template/conf.json +12 -0
- signalpilot_ai_internal/html_export_template/index.html.j2 +140 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/package.json +3 -2
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/package.json.orig +2 -1
- signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/490.b4ccb9601c8112407c5d.js +1 -0
- signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.dc49867fafb03ea2ba4d.js +1 -0
- signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/839.ed04fa601a43e8dd24d1.js +1 -0
- signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/898.4e9edb7f224152c1dcb4.js +2 -0
- signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.ee8951353b00c13b8070.js +1 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/third-party-licenses.json +6 -6
- {signalpilot_ai_internal-0.4.10.dist-info → signalpilot_ai_internal-0.7.6.dist-info}/METADATA +3 -1
- signalpilot_ai_internal-0.7.6.dist-info/RECORD +49 -0
- signalpilot_ai_internal-0.4.10.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/104.04e170724f369fcbaf19.js +0 -2
- signalpilot_ai_internal-0.4.10.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/104.04e170724f369fcbaf19.js.LICENSE.txt +0 -24
- signalpilot_ai_internal-0.4.10.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/188.8de71ad111d5f3bd39c8.js +0 -1
- signalpilot_ai_internal-0.4.10.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/280.35d8c8b68815702a5238.js +0 -2
- signalpilot_ai_internal-0.4.10.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/606.90aaaae46b73dc3c08fb.js +0 -1
- signalpilot_ai_internal-0.4.10.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/839.460965ed98350008ee29.js +0 -1
- signalpilot_ai_internal-0.4.10.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.1b0f690a6fcd55d60a1c.js +0 -1
- signalpilot_ai_internal-0.4.10.dist-info/RECORD +0 -47
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/etc/jupyter/jupyter_server_config.d/signalpilot_ai.json +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/install.json +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/plugin.json +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.e2dadf63dc64d7b5f1ee.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.328403b5545f268b95c6.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.726e1da31a50868cb297.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.72484b768a04f89bd3dd.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.dbec4c2dc12e7b050dcc.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.fa432bdb7fb6b1c95ad6.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.37e271d7a80336daabe2.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.9b4f05a99f5003f82094.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.73c7a9290b7d35a8b9c1.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.b58fc0093d080b8ee61c.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js.LICENSE.txt +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.9720593ee20b768da3ca.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.8e6edc9a965bdd578ca7.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/742.91e7b516c8699eea3373.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.3aa564fc148b37d1d719.js +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/888.34054db17bcf6e87ec95.js +0 -0
- /signalpilot_ai_internal-0.4.10.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/280.35d8c8b68815702a5238.js.LICENSE.txt → /signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/898.4e9edb7f224152c1dcb4.js.LICENSE.txt +0 -0
- {signalpilot_ai_internal-0.4.10.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/style.js +0 -0
- {signalpilot_ai_internal-0.4.10.dist-info → signalpilot_ai_internal-0.7.6.dist-info}/WHEEL +0 -0
- {signalpilot_ai_internal-0.4.10.dist-info → signalpilot_ai_internal-0.7.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,6 +12,7 @@ from .cache_service import get_cache_service
|
|
|
12
12
|
from .cache_handlers import ChatHistoriesHandler, AppValuesHandler, CacheInfoHandler
|
|
13
13
|
from .unified_database_schema_service import UnifiedDatabaseSchemaHandler, UnifiedDatabaseQueryHandler
|
|
14
14
|
from .snowflake_schema_service import SnowflakeSchemaHandler, SnowflakeQueryHandler
|
|
15
|
+
from .file_scanner_service import get_file_scanner_service
|
|
15
16
|
from .schema_search_service import SchemaSearchHandler
|
|
16
17
|
|
|
17
18
|
|
|
@@ -258,6 +259,447 @@ class ReadAllFilesHandler(APIHandler):
|
|
|
258
259
|
return '\n'.join(lines)
|
|
259
260
|
|
|
260
261
|
|
|
262
|
+
class SelectFolderHandler(APIHandler):
|
|
263
|
+
"""Handler to open a native folder picker and return the selected absolute path"""
|
|
264
|
+
|
|
265
|
+
# Class-level flag to prevent multiple dialogs
|
|
266
|
+
_dialog_open = False
|
|
267
|
+
|
|
268
|
+
@tornado.web.authenticated
|
|
269
|
+
def get(self):
|
|
270
|
+
# Check if a dialog is already open
|
|
271
|
+
if SelectFolderHandler._dialog_open:
|
|
272
|
+
self.set_status(409) # Conflict status
|
|
273
|
+
self.finish(json.dumps({
|
|
274
|
+
"error": "A folder selection dialog is already open"
|
|
275
|
+
}))
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
import tkinter as tk
|
|
280
|
+
from tkinter import filedialog
|
|
281
|
+
import threading
|
|
282
|
+
import time
|
|
283
|
+
|
|
284
|
+
# Set flag to prevent multiple dialogs
|
|
285
|
+
SelectFolderHandler._dialog_open = True
|
|
286
|
+
|
|
287
|
+
# Create a fresh tkinter instance
|
|
288
|
+
root = tk.Tk()
|
|
289
|
+
|
|
290
|
+
# Position the root window in the center of the screen BEFORE withdrawing
|
|
291
|
+
try:
|
|
292
|
+
# Get screen dimensions
|
|
293
|
+
screen_width = root.winfo_screenwidth()
|
|
294
|
+
screen_height = root.winfo_screenheight()
|
|
295
|
+
|
|
296
|
+
# Calculate center position
|
|
297
|
+
x = (screen_width // 2) - 200 # Dialog is roughly 400px wide
|
|
298
|
+
y = (screen_height // 2) - 150 # Dialog is roughly 300px tall
|
|
299
|
+
|
|
300
|
+
# Set window position and make it visible briefly for positioning
|
|
301
|
+
root.geometry(f"400x300+{x}+{y}")
|
|
302
|
+
root.update_idletasks()
|
|
303
|
+
|
|
304
|
+
# Now withdraw the window
|
|
305
|
+
root.withdraw()
|
|
306
|
+
|
|
307
|
+
# Enhanced topmost settings for better visibility
|
|
308
|
+
root.attributes('-topmost', True)
|
|
309
|
+
root.lift()
|
|
310
|
+
root.focus_force()
|
|
311
|
+
|
|
312
|
+
# Final positioning update
|
|
313
|
+
root.update_idletasks()
|
|
314
|
+
except Exception:
|
|
315
|
+
# Fallback: just withdraw if positioning fails
|
|
316
|
+
root.withdraw()
|
|
317
|
+
|
|
318
|
+
folder = None
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
# Show the dialog with proper positioning
|
|
322
|
+
folder = filedialog.askdirectory(
|
|
323
|
+
parent=root,
|
|
324
|
+
title="Select Folder",
|
|
325
|
+
initialdir=os.path.expanduser("~") # Start in user's home directory
|
|
326
|
+
)
|
|
327
|
+
except Exception as e:
|
|
328
|
+
raise e
|
|
329
|
+
finally:
|
|
330
|
+
# Comprehensive cleanup
|
|
331
|
+
try:
|
|
332
|
+
# Force close and destroy all tkinter components
|
|
333
|
+
root.quit()
|
|
334
|
+
root.destroy()
|
|
335
|
+
|
|
336
|
+
# Additional cleanup for macOS - ensure complete destruction
|
|
337
|
+
try:
|
|
338
|
+
root.update_idletasks()
|
|
339
|
+
root.update()
|
|
340
|
+
# Force garbage collection of tkinter objects
|
|
341
|
+
import gc
|
|
342
|
+
gc.collect()
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
finally:
|
|
349
|
+
# Reset flag and add small delay to ensure cleanup
|
|
350
|
+
SelectFolderHandler._dialog_open = False
|
|
351
|
+
time.sleep(0.1) # Small delay to ensure cleanup completes
|
|
352
|
+
|
|
353
|
+
# Normalize and return absolute path or null
|
|
354
|
+
if folder:
|
|
355
|
+
folder_path = os.path.abspath(folder)
|
|
356
|
+
self.finish(json.dumps({"path": folder_path}))
|
|
357
|
+
else:
|
|
358
|
+
self.finish(json.dumps({"path": None}))
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
# Reset flag on error
|
|
362
|
+
SelectFolderHandler._dialog_open = False
|
|
363
|
+
self.set_status(400)
|
|
364
|
+
self.finish(json.dumps({
|
|
365
|
+
"error": str(e)
|
|
366
|
+
}))
|
|
367
|
+
|
|
368
|
+
class FileScanHandler(APIHandler):
|
|
369
|
+
"""Handler for scanning directories for files"""
|
|
370
|
+
|
|
371
|
+
@tornado.web.authenticated
|
|
372
|
+
async def post(self):
|
|
373
|
+
try:
|
|
374
|
+
data = json.loads(self.request.body.decode('utf-8'))
|
|
375
|
+
paths = data.get('paths', [])
|
|
376
|
+
|
|
377
|
+
if not paths:
|
|
378
|
+
self.set_status(400)
|
|
379
|
+
self.finish(json.dumps({
|
|
380
|
+
"error": "No paths provided"
|
|
381
|
+
}))
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
file_scanner = get_file_scanner_service()
|
|
385
|
+
# Pass the current working directory as the workspace root for relative path calculation
|
|
386
|
+
result = await file_scanner.scan_directories(paths, workspace_root=os.getcwd())
|
|
387
|
+
|
|
388
|
+
# Update scanned directories tracking
|
|
389
|
+
scanned_dirs = file_scanner.get_scanned_directories()
|
|
390
|
+
current_dirs = scanned_dirs.get('directories', [])
|
|
391
|
+
|
|
392
|
+
# Update directory metadata
|
|
393
|
+
for new_dir in result['scanned_directories']:
|
|
394
|
+
# Check if directory already exists
|
|
395
|
+
existing_dir = None
|
|
396
|
+
for existing in current_dirs:
|
|
397
|
+
if existing['path'] == new_dir['path']:
|
|
398
|
+
existing_dir = existing
|
|
399
|
+
break
|
|
400
|
+
|
|
401
|
+
if existing_dir:
|
|
402
|
+
# Update existing directory
|
|
403
|
+
existing_dir['file_count'] = new_dir['file_count']
|
|
404
|
+
existing_dir['scanned_at'] = new_dir['scanned_at']
|
|
405
|
+
else:
|
|
406
|
+
# Add new directory
|
|
407
|
+
current_dirs.append(new_dir)
|
|
408
|
+
|
|
409
|
+
# Save updated directories list
|
|
410
|
+
file_scanner.update_scanned_directories(current_dirs)
|
|
411
|
+
|
|
412
|
+
self.finish(json.dumps(result))
|
|
413
|
+
|
|
414
|
+
except Exception as e:
|
|
415
|
+
self.set_status(500)
|
|
416
|
+
self.finish(json.dumps({
|
|
417
|
+
"error": str(e)
|
|
418
|
+
}))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class ScannedDirectoriesHandler(APIHandler):
|
|
422
|
+
"""Handler for getting scanned directories list"""
|
|
423
|
+
|
|
424
|
+
@tornado.web.authenticated
|
|
425
|
+
def get(self):
|
|
426
|
+
try:
|
|
427
|
+
file_scanner = get_file_scanner_service()
|
|
428
|
+
result = file_scanner.get_scanned_directories()
|
|
429
|
+
|
|
430
|
+
self.finish(json.dumps(result))
|
|
431
|
+
|
|
432
|
+
except Exception as e:
|
|
433
|
+
self.set_status(500)
|
|
434
|
+
self.finish(json.dumps({
|
|
435
|
+
"error": str(e)
|
|
436
|
+
}))
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class WorkDirHandler(APIHandler):
|
|
440
|
+
"""Handler for returning current working directory"""
|
|
441
|
+
|
|
442
|
+
@tornado.web.authenticated
|
|
443
|
+
def get(self):
|
|
444
|
+
try:
|
|
445
|
+
self.finish(json.dumps({"workdir": os.getcwd()}))
|
|
446
|
+
except Exception as e:
|
|
447
|
+
self.set_status(500)
|
|
448
|
+
self.finish(json.dumps({
|
|
449
|
+
"error": str(e)
|
|
450
|
+
}))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class TerminalExecuteHandler(APIHandler):
|
|
454
|
+
"""Handler for executing terminal commands"""
|
|
455
|
+
|
|
456
|
+
@tornado.web.authenticated
|
|
457
|
+
def post(self):
|
|
458
|
+
try:
|
|
459
|
+
data = json.loads(self.request.body.decode('utf-8'))
|
|
460
|
+
command = data.get('command')
|
|
461
|
+
|
|
462
|
+
if not command:
|
|
463
|
+
self.set_status(400)
|
|
464
|
+
self.finish(json.dumps({
|
|
465
|
+
"error": "No command provided"
|
|
466
|
+
}))
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
import subprocess
|
|
470
|
+
result = subprocess.run(
|
|
471
|
+
command,
|
|
472
|
+
shell=True,
|
|
473
|
+
cwd=os.getcwd(),
|
|
474
|
+
capture_output=True,
|
|
475
|
+
text=True,
|
|
476
|
+
timeout=300
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def truncate_output(output: str, max_lines: int = 50) -> str:
|
|
480
|
+
if not output:
|
|
481
|
+
return output
|
|
482
|
+
lines = output.splitlines()
|
|
483
|
+
if len(lines) <= max_lines * 2:
|
|
484
|
+
return output
|
|
485
|
+
first_lines = lines[:max_lines]
|
|
486
|
+
last_lines = lines[-max_lines:]
|
|
487
|
+
truncated_count = len(lines) - (max_lines * 2)
|
|
488
|
+
return '\n'.join(first_lines + [f'\n... {truncated_count} lines truncated ...\n'] + last_lines)
|
|
489
|
+
|
|
490
|
+
self.finish(json.dumps({
|
|
491
|
+
"stdout": truncate_output(result.stdout),
|
|
492
|
+
"stderr": truncate_output(result.stderr),
|
|
493
|
+
"exit_code": result.returncode
|
|
494
|
+
}))
|
|
495
|
+
|
|
496
|
+
except subprocess.TimeoutExpired:
|
|
497
|
+
self.set_status(408)
|
|
498
|
+
self.finish(json.dumps({
|
|
499
|
+
"error": "Command timed out after 300 seconds"
|
|
500
|
+
}))
|
|
501
|
+
except Exception as e:
|
|
502
|
+
self.set_status(500)
|
|
503
|
+
self.finish(json.dumps({
|
|
504
|
+
"error": str(e)
|
|
505
|
+
}))
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class DeleteScannedDirectoryHandler(APIHandler):
|
|
509
|
+
"""Handler for deleting a scanned directory"""
|
|
510
|
+
|
|
511
|
+
@tornado.web.authenticated
|
|
512
|
+
def post(self):
|
|
513
|
+
try:
|
|
514
|
+
data = json.loads(self.request.body.decode('utf-8'))
|
|
515
|
+
directory_path = data.get('path')
|
|
516
|
+
|
|
517
|
+
if not directory_path:
|
|
518
|
+
self.set_status(400)
|
|
519
|
+
self.finish(json.dumps({
|
|
520
|
+
"error": "No directory path provided"
|
|
521
|
+
}))
|
|
522
|
+
return
|
|
523
|
+
|
|
524
|
+
file_scanner = get_file_scanner_service()
|
|
525
|
+
|
|
526
|
+
# Get current scanned directories
|
|
527
|
+
current_directories = file_scanner.get_scanned_directories()
|
|
528
|
+
directories = current_directories.get('directories', [])
|
|
529
|
+
|
|
530
|
+
# Filter out the directory to be deleted
|
|
531
|
+
filtered_directories = [
|
|
532
|
+
dir_info for dir_info in directories
|
|
533
|
+
if dir_info.get('path') != directory_path
|
|
534
|
+
]
|
|
535
|
+
|
|
536
|
+
# Check if directory was actually found and removed
|
|
537
|
+
if len(filtered_directories) == len(directories):
|
|
538
|
+
self.set_status(404)
|
|
539
|
+
self.finish(json.dumps({
|
|
540
|
+
"error": f"Directory '{directory_path}' not found in scanned directories"
|
|
541
|
+
}))
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
# Update the scanned directories list
|
|
545
|
+
success = file_scanner.update_scanned_directories(filtered_directories)
|
|
546
|
+
|
|
547
|
+
if success:
|
|
548
|
+
self.finish(json.dumps({
|
|
549
|
+
"success": True,
|
|
550
|
+
"message": f"Directory '{directory_path}' removed from scanning"
|
|
551
|
+
}))
|
|
552
|
+
else:
|
|
553
|
+
self.set_status(500)
|
|
554
|
+
self.finish(json.dumps({
|
|
555
|
+
"error": "Failed to update scanned directories"
|
|
556
|
+
}))
|
|
557
|
+
|
|
558
|
+
except Exception as e:
|
|
559
|
+
self.set_status(500)
|
|
560
|
+
self.finish(json.dumps({
|
|
561
|
+
"error": str(e)
|
|
562
|
+
}))
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
class NotebookCellsHandler(APIHandler):
|
|
566
|
+
"""Handler for reading specific cell ranges from notebooks"""
|
|
567
|
+
|
|
568
|
+
@tornado.web.authenticated
|
|
569
|
+
def post(self):
|
|
570
|
+
try:
|
|
571
|
+
data = json.loads(self.request.body.decode('utf-8'))
|
|
572
|
+
file_path = data.get('file_path')
|
|
573
|
+
start_cell = data.get('start_cell', 0)
|
|
574
|
+
end_cell = data.get('end_cell', 5)
|
|
575
|
+
|
|
576
|
+
if not file_path:
|
|
577
|
+
self.set_status(400)
|
|
578
|
+
self.finish(json.dumps({
|
|
579
|
+
"error": "No file_path provided"
|
|
580
|
+
}))
|
|
581
|
+
return
|
|
582
|
+
|
|
583
|
+
file_scanner = get_file_scanner_service()
|
|
584
|
+
result = file_scanner.extract_schema(file_path, start_cell=start_cell, end_cell=end_cell)
|
|
585
|
+
|
|
586
|
+
self.finish(json.dumps(result))
|
|
587
|
+
|
|
588
|
+
except Exception as e:
|
|
589
|
+
self.set_status(500)
|
|
590
|
+
self.finish(json.dumps({
|
|
591
|
+
"error": str(e)
|
|
592
|
+
}))
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class NotebookToHTMLHandler(APIHandler):
|
|
596
|
+
"""Handler for converting notebooks to HTML using nbconvert"""
|
|
597
|
+
|
|
598
|
+
@tornado.web.authenticated
|
|
599
|
+
def post(self):
|
|
600
|
+
try:
|
|
601
|
+
# Parse request body
|
|
602
|
+
try:
|
|
603
|
+
data = json.loads(self.request.body.decode('utf-8'))
|
|
604
|
+
except json.JSONDecodeError:
|
|
605
|
+
self.set_status(400)
|
|
606
|
+
self.finish(json.dumps({
|
|
607
|
+
"error": "Invalid JSON in request body"
|
|
608
|
+
}))
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
# Get notebook path
|
|
612
|
+
notebook_path = data.get('notebook_path')
|
|
613
|
+
if not notebook_path:
|
|
614
|
+
self.set_status(400)
|
|
615
|
+
self.finish(json.dumps({
|
|
616
|
+
"error": "No notebook path provided"
|
|
617
|
+
}))
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
# Get export options with defaults
|
|
621
|
+
include_input = data.get('include_input', True)
|
|
622
|
+
include_output = data.get('include_output', True)
|
|
623
|
+
include_images = data.get('include_images', True)
|
|
624
|
+
|
|
625
|
+
# Convert to nbconvert options
|
|
626
|
+
exclude_input = not include_input
|
|
627
|
+
exclude_output = not include_output
|
|
628
|
+
|
|
629
|
+
try:
|
|
630
|
+
# Import nbconvert and nbformat
|
|
631
|
+
from nbconvert import HTMLExporter
|
|
632
|
+
from nbconvert.preprocessors import ExtractOutputPreprocessor
|
|
633
|
+
from nbformat import read
|
|
634
|
+
|
|
635
|
+
# Read the notebook file using nbformat
|
|
636
|
+
notebook_file_path = Path(notebook_path)
|
|
637
|
+
if not notebook_file_path.exists():
|
|
638
|
+
self.set_status(404)
|
|
639
|
+
self.finish(json.dumps({
|
|
640
|
+
"error": f"Notebook file not found: {notebook_path}"
|
|
641
|
+
}))
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
# Load notebook using nbformat.read (more robust)
|
|
645
|
+
with open(notebook_file_path, 'r', encoding='utf-8') as f:
|
|
646
|
+
notebook_content = read(f, as_version=4)
|
|
647
|
+
|
|
648
|
+
# Get the custom template path (relative to this file)
|
|
649
|
+
template_dir = Path(__file__).parent / 'html_export_template'
|
|
650
|
+
template_path = str(template_dir.resolve())
|
|
651
|
+
|
|
652
|
+
# Configure the HTML exporter with custom template
|
|
653
|
+
exporter = HTMLExporter(template_name=template_path)
|
|
654
|
+
|
|
655
|
+
# Set export options
|
|
656
|
+
exporter.exclude_input = exclude_input
|
|
657
|
+
exporter.exclude_output = exclude_output
|
|
658
|
+
exporter.exclude_input_prompt = True
|
|
659
|
+
exporter.exclude_output_prompt = True
|
|
660
|
+
|
|
661
|
+
# Configure image handling
|
|
662
|
+
if not include_images:
|
|
663
|
+
# Add preprocessor to exclude images
|
|
664
|
+
exporter.register_preprocessor(ExtractOutputPreprocessor(), enabled=True)
|
|
665
|
+
|
|
666
|
+
# Convert notebook to HTML - nbconvert handles everything
|
|
667
|
+
html_content, _ = exporter.from_notebook_node(notebook_content)
|
|
668
|
+
|
|
669
|
+
# Get absolute path for the notebook
|
|
670
|
+
workspace_notebook_path = str(notebook_file_path.absolute())
|
|
671
|
+
|
|
672
|
+
# Return the HTML content
|
|
673
|
+
self.finish(json.dumps({
|
|
674
|
+
"success": True,
|
|
675
|
+
"html_content": html_content,
|
|
676
|
+
"notebook_path": notebook_path,
|
|
677
|
+
"workspace_notebook_path": workspace_notebook_path,
|
|
678
|
+
"export_options": {
|
|
679
|
+
"include_input": include_input,
|
|
680
|
+
"include_output": include_output,
|
|
681
|
+
"include_images": include_images
|
|
682
|
+
}
|
|
683
|
+
}))
|
|
684
|
+
|
|
685
|
+
except ImportError as e:
|
|
686
|
+
self.set_status(500)
|
|
687
|
+
self.finish(json.dumps({
|
|
688
|
+
"error": f"Required packages not installed: {str(e)}. Please install them with: pip install nbconvert nbformat"
|
|
689
|
+
}))
|
|
690
|
+
except Exception as e:
|
|
691
|
+
self.set_status(500)
|
|
692
|
+
self.finish(json.dumps({
|
|
693
|
+
"error": f"Failed to convert notebook to HTML: {str(e)}"
|
|
694
|
+
}))
|
|
695
|
+
|
|
696
|
+
except Exception as e:
|
|
697
|
+
self.set_status(500)
|
|
698
|
+
self.finish(json.dumps({
|
|
699
|
+
"error": str(e)
|
|
700
|
+
}))
|
|
701
|
+
|
|
702
|
+
|
|
261
703
|
def setup_handlers(web_app):
|
|
262
704
|
host_pattern = ".*$"
|
|
263
705
|
base_url = web_app.settings["base_url"]
|
|
@@ -268,6 +710,16 @@ def setup_handlers(web_app):
|
|
|
268
710
|
# Read all files endpoint
|
|
269
711
|
read_all_files_route = url_path_join(base_url, "signalpilot-ai-internal", "read-all-files")
|
|
270
712
|
|
|
713
|
+
# File scanning endpoints
|
|
714
|
+
file_scan_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "scan")
|
|
715
|
+
scanned_directories_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "directories")
|
|
716
|
+
select_folder_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "select-folder")
|
|
717
|
+
workdir_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "workdir")
|
|
718
|
+
delete_scanned_directory_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "directories", "delete")
|
|
719
|
+
|
|
720
|
+
# Notebook endpoints
|
|
721
|
+
notebook_cells_route = url_path_join(base_url, "signalpilot-ai-internal", "notebook", "cells")
|
|
722
|
+
|
|
271
723
|
# Cache service endpoints
|
|
272
724
|
chat_histories_route = url_path_join(base_url, "signalpilot-ai-internal", "cache", "chat-histories")
|
|
273
725
|
chat_history_route = url_path_join(base_url, "signalpilot-ai-internal", "cache", "chat-histories", "([^/]+)")
|
|
@@ -290,13 +742,32 @@ def setup_handlers(web_app):
|
|
|
290
742
|
snowflake_schema_route = url_path_join(base_url, "signalpilot-ai-internal", "snowflake", "schema")
|
|
291
743
|
snowflake_query_route = url_path_join(base_url, "signalpilot-ai-internal", "snowflake", "query")
|
|
292
744
|
|
|
745
|
+
# Notebook HTML export endpoint
|
|
746
|
+
notebook_html_route = url_path_join(base_url, "signalpilot-ai-internal", "notebook", "to-html")
|
|
747
|
+
|
|
748
|
+
# Terminal endpoint
|
|
749
|
+
terminal_execute_route = url_path_join(base_url, "signalpilot-ai-internal", "terminal", "execute")
|
|
750
|
+
|
|
293
751
|
handlers = [
|
|
294
752
|
# Original endpoint
|
|
295
753
|
(hello_route, HelloWorldHandler),
|
|
296
|
-
|
|
754
|
+
|
|
297
755
|
# Read all files endpoint
|
|
298
756
|
(read_all_files_route, ReadAllFilesHandler),
|
|
299
|
-
|
|
757
|
+
|
|
758
|
+
# File scanning endpoints
|
|
759
|
+
(file_scan_route, FileScanHandler),
|
|
760
|
+
(scanned_directories_route, ScannedDirectoriesHandler),
|
|
761
|
+
(select_folder_route, SelectFolderHandler),
|
|
762
|
+
(workdir_route, WorkDirHandler),
|
|
763
|
+
(delete_scanned_directory_route, DeleteScannedDirectoryHandler),
|
|
764
|
+
|
|
765
|
+
# Terminal endpoint
|
|
766
|
+
(terminal_execute_route, TerminalExecuteHandler),
|
|
767
|
+
|
|
768
|
+
# Notebook endpoints
|
|
769
|
+
(notebook_cells_route, NotebookCellsHandler),
|
|
770
|
+
|
|
300
771
|
# Chat histories endpoints
|
|
301
772
|
(chat_histories_route, ChatHistoriesHandler),
|
|
302
773
|
(chat_history_route, ChatHistoriesHandler),
|
|
@@ -320,6 +791,9 @@ def setup_handlers(web_app):
|
|
|
320
791
|
# Snowflake service endpoints
|
|
321
792
|
(snowflake_schema_route, SnowflakeSchemaHandler),
|
|
322
793
|
(snowflake_query_route, SnowflakeQueryHandler),
|
|
794
|
+
|
|
795
|
+
# Notebook HTML export endpoint
|
|
796
|
+
(notebook_html_route, NotebookToHTMLHandler),
|
|
323
797
|
]
|
|
324
798
|
|
|
325
799
|
web_app.add_handlers(host_pattern, handlers)
|
|
@@ -347,3 +821,5 @@ def setup_handlers(web_app):
|
|
|
347
821
|
print(f" - MySQL Query: {mysql_query_route}")
|
|
348
822
|
print(f" - Snowflake Schema: {snowflake_schema_route}")
|
|
349
823
|
print(f" - Snowflake Query: {snowflake_query_route}")
|
|
824
|
+
print(f" - Notebook Cells: {notebook_cells_route}")
|
|
825
|
+
print(f" - Notebook to HTML: {notebook_html_route}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# HTML Export Template with Code Toggle
|
|
2
|
+
|
|
3
|
+
This is a custom Jupyter NBConvert HTML template that adds a toggle to hide/show each code cell.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
The handler uses this template automatically. You can modify the notebook cells to control their visibility:
|
|
8
|
+
|
|
9
|
+
- By default, all code cells are hidden with a "Show Code" button
|
|
10
|
+
- To show a code cell by default, tag the cell with 'code_shown' in its metadata
|
|
11
|
+
|
|
12
|
+
## Template Files
|
|
13
|
+
|
|
14
|
+
- `index.html.j2` - The main template that extends lab/index.html.j2
|
|
15
|
+
- `conf.json` - Configuration for the template
|
|
16
|
+
- `README.md` - This file
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- Clean presentation with toggleable code cells
|
|
21
|
+
- Uses JQuery for smooth transitions
|
|
22
|
+
- Removes input/output prompts for cleaner appearance
|
|
23
|
+
- Responsive and modern styling
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
{% extends 'lab/index.html.j2' %}
|
|
2
|
+
|
|
3
|
+
{% block html_head %}
|
|
4
|
+
{{ super() }}
|
|
5
|
+
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
|
|
6
|
+
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
|
|
7
|
+
crossorigin="anonymous"></script>
|
|
8
|
+
|
|
9
|
+
<script>
|
|
10
|
+
$(document).ready(function() {
|
|
11
|
+
// Add Show All / Hide All buttons above first code cell (if not already added)
|
|
12
|
+
if ($('.html-toggle-all-buttons').length === 0) {
|
|
13
|
+
// Only add buttons if there is at least one code_shower present
|
|
14
|
+
if ($('.code_shower').length === 0) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
var $btnContainer = $('<div class="html-toggle-all-buttons"></div>');
|
|
18
|
+
var $showAllBtn = $('<button type="button" class="html-toggle-btn">Show All Code</button>');
|
|
19
|
+
var $hideAllBtn = $('<button type="button" class="html-toggle-btn">Hide All Code</button>');
|
|
20
|
+
$btnContainer.append($showAllBtn).append($hideAllBtn);
|
|
21
|
+
|
|
22
|
+
// Insert at the top of the <body>
|
|
23
|
+
$('body').prepend($btnContainer);
|
|
24
|
+
|
|
25
|
+
// "Show All" callback
|
|
26
|
+
$showAllBtn.on('click', function() {
|
|
27
|
+
$('.code_shower').each(function() {
|
|
28
|
+
var header = $(this);
|
|
29
|
+
var codecell = header.next();
|
|
30
|
+
// Only toggle if it's hidden
|
|
31
|
+
if (codecell.is(':hidden')) {
|
|
32
|
+
// Update styles and text as necessary
|
|
33
|
+
var wrapper = codecell.parent();
|
|
34
|
+
wrapper.css('display', 'flex');
|
|
35
|
+
wrapper.css('flex-direction', 'column');
|
|
36
|
+
header.css('font-family', 'var(--jp-content-font-family)');
|
|
37
|
+
header.text("Hide Code Cell");
|
|
38
|
+
header.css("border-radius", "2px 2px 0px 0px");
|
|
39
|
+
codecell.slideDown(0);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// "Hide All" callback
|
|
45
|
+
$hideAllBtn.on('click', function() {
|
|
46
|
+
$('.code_shower').each(function() {
|
|
47
|
+
var header = $(this);
|
|
48
|
+
var codecell = header.next();
|
|
49
|
+
// Only toggle if it's visible
|
|
50
|
+
if (codecell.is(':visible')) {
|
|
51
|
+
var wrapper = codecell.parent();
|
|
52
|
+
wrapper.css('display', 'flex');
|
|
53
|
+
wrapper.css('flex-direction', 'column');
|
|
54
|
+
header.css('font-family', 'var(--jp-content-font-family)');
|
|
55
|
+
header.text("Show Code Cell");
|
|
56
|
+
header.css("border-radius", "2px 2px 2px 2px");
|
|
57
|
+
codecell.slideUp(0);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$('.code_shower').on('click', function() {
|
|
64
|
+
var header = $(this);
|
|
65
|
+
var codecell = $(this).next();
|
|
66
|
+
|
|
67
|
+
// Ensure the codecell's parent is a flex column wrapper
|
|
68
|
+
var wrapper = codecell.parent();
|
|
69
|
+
wrapper.css('display', 'flex');
|
|
70
|
+
wrapper.css('flex-direction', 'column');
|
|
71
|
+
|
|
72
|
+
// Ensure header uses the content font family
|
|
73
|
+
header.css('font-family', 'var(--jp-content-font-family)');
|
|
74
|
+
|
|
75
|
+
codecell.slideToggle(0, function() {
|
|
76
|
+
if (codecell.is(':hidden')) {
|
|
77
|
+
header.text("Show Code Cell");
|
|
78
|
+
header.css("border-radius", "2px 2px 2px 2px");
|
|
79
|
+
} else {
|
|
80
|
+
header.text("Hide Code Cell");
|
|
81
|
+
header.css("border-radius", "2px 2px 0px 0px");
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
$('.hidden_default').next().hide();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<style>
|
|
91
|
+
div.input {
|
|
92
|
+
flex-direction: column !important;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
div.input_area {
|
|
96
|
+
border-radius: 0px 0px 2px 2px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
div.code_shower {
|
|
100
|
+
background: lightgray;
|
|
101
|
+
padding: 5px 10px;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
border-radius: 2px 2px 0px 0px;
|
|
104
|
+
font-family: var(--jp-content-font-family);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Container and button styles for Show/Hide All controls */
|
|
108
|
+
.html-toggle-all-buttons {
|
|
109
|
+
display: flex;
|
|
110
|
+
justify-content: end;
|
|
111
|
+
gap: 8px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.html-toggle-all-buttons .html-toggle-btn {
|
|
115
|
+
background: lightgray;
|
|
116
|
+
padding: 5px 10px;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
border-radius: 2px;
|
|
119
|
+
border: none;
|
|
120
|
+
font-family: var(--jp-content-font-family);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
</style>
|
|
124
|
+
{% endblock html_head %}
|
|
125
|
+
|
|
126
|
+
{% block input %}
|
|
127
|
+
{% if 'code_shown' in cell['metadata'].get('tags', []) %}
|
|
128
|
+
<div class="code_shower">Hide Code</div>
|
|
129
|
+
{% else %}
|
|
130
|
+
<div class="code_shower hidden_default">Show Code</div>
|
|
131
|
+
{% endif %}
|
|
132
|
+
|
|
133
|
+
{{ super() }}
|
|
134
|
+
{% endblock input %}
|
|
135
|
+
|
|
136
|
+
{% block output_prompt %}
|
|
137
|
+
{% endblock output_prompt %}
|
|
138
|
+
|
|
139
|
+
{% block in_prompt %}
|
|
140
|
+
{% endblock in_prompt %}
|