signalpilot-ai-internal 0.5.1__py3-none-any.whl → 0.6.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.
Potentially problematic release.
This version of signalpilot-ai-internal might be problematic. Click here for more details.
- signalpilot_ai_internal/_version.py +1 -1
- signalpilot_ai_internal/cache_service.py +152 -1
- signalpilot_ai_internal/file_scanner_service.py +1252 -0
- signalpilot_ai_internal/handlers.py +262 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/package.json +2 -2
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/package.json.orig +1 -1
- signalpilot_ai_internal-0.6.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/188.1ace26ac1a5e246783bb.js +1 -0
- signalpilot_ai_internal-0.6.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/839.7d9a99d0566aa6743c69.js +1 -0
- signalpilot_ai_internal-0.6.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/898.4e9edb7f224152c1dcb4.js +2 -0
- signalpilot_ai_internal-0.6.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.410a42566793b732952f.js +1 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/third-party-licenses.json +0 -6
- {signalpilot_ai_internal-0.5.1.dist-info → signalpilot_ai_internal-0.6.0.dist-info}/METADATA +3 -1
- signalpilot_ai_internal-0.6.0.dist-info/RECORD +46 -0
- signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/104.04e170724f369fcbaf19.js +0 -2
- signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/104.04e170724f369fcbaf19.js.LICENSE.txt +0 -24
- signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/188.e781cc4c87f2dbf290ec.js +0 -1
- signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/606.90aaaae46b73dc3c08fb.js +0 -1
- signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/839.7ea0c8f6af45369912f3.js +0 -1
- signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/898.5251a593584dd5d131d5.js +0 -2
- signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.59ffb91489066223094b.js +0 -1
- signalpilot_ai_internal-0.5.1.dist-info/RECORD +0 -48
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/etc/jupyter/jupyter_server_config.d/signalpilot_ai.json +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/install.json +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/plugin.json +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.e2dadf63dc64d7b5f1ee.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.328403b5545f268b95c6.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.726e1da31a50868cb297.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.72484b768a04f89bd3dd.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.dbec4c2dc12e7b050dcc.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.fa432bdb7fb6b1c95ad6.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.37e271d7a80336daabe2.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.9b4f05a99f5003f82094.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.73c7a9290b7d35a8b9c1.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.b58fc0093d080b8ee61c.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js.LICENSE.txt +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.9720593ee20b768da3ca.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.8e6edc9a965bdd578ca7.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.dc49867fafb03ea2ba4d.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/742.91e7b516c8699eea3373.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.3aa564fc148b37d1d719.js +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/888.34054db17bcf6e87ec95.js +0 -0
- /signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/898.5251a593584dd5d131d5.js.LICENSE.txt → /signalpilot_ai_internal-0.6.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/898.4e9edb7f224152c1dcb4.js.LICENSE.txt +0 -0
- {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.6.0.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/style.js +0 -0
- {signalpilot_ai_internal-0.5.1.dist-info → signalpilot_ai_internal-0.6.0.dist-info}/WHEEL +0 -0
- {signalpilot_ai_internal-0.5.1.dist-info → signalpilot_ai_internal-0.6.0.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,253 @@ 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 DeleteScannedDirectoryHandler(APIHandler):
|
|
454
|
+
"""Handler for deleting a scanned directory"""
|
|
455
|
+
|
|
456
|
+
@tornado.web.authenticated
|
|
457
|
+
def post(self):
|
|
458
|
+
try:
|
|
459
|
+
data = json.loads(self.request.body.decode('utf-8'))
|
|
460
|
+
directory_path = data.get('path')
|
|
461
|
+
|
|
462
|
+
if not directory_path:
|
|
463
|
+
self.set_status(400)
|
|
464
|
+
self.finish(json.dumps({
|
|
465
|
+
"error": "No directory path provided"
|
|
466
|
+
}))
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
file_scanner = get_file_scanner_service()
|
|
470
|
+
|
|
471
|
+
# Get current scanned directories
|
|
472
|
+
current_directories = file_scanner.get_scanned_directories()
|
|
473
|
+
directories = current_directories.get('directories', [])
|
|
474
|
+
|
|
475
|
+
# Filter out the directory to be deleted
|
|
476
|
+
filtered_directories = [
|
|
477
|
+
dir_info for dir_info in directories
|
|
478
|
+
if dir_info.get('path') != directory_path
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
# Check if directory was actually found and removed
|
|
482
|
+
if len(filtered_directories) == len(directories):
|
|
483
|
+
self.set_status(404)
|
|
484
|
+
self.finish(json.dumps({
|
|
485
|
+
"error": f"Directory '{directory_path}' not found in scanned directories"
|
|
486
|
+
}))
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
# Update the scanned directories list
|
|
490
|
+
success = file_scanner.update_scanned_directories(filtered_directories)
|
|
491
|
+
|
|
492
|
+
if success:
|
|
493
|
+
self.finish(json.dumps({
|
|
494
|
+
"success": True,
|
|
495
|
+
"message": f"Directory '{directory_path}' removed from scanning"
|
|
496
|
+
}))
|
|
497
|
+
else:
|
|
498
|
+
self.set_status(500)
|
|
499
|
+
self.finish(json.dumps({
|
|
500
|
+
"error": "Failed to update scanned directories"
|
|
501
|
+
}))
|
|
502
|
+
|
|
503
|
+
except Exception as e:
|
|
504
|
+
self.set_status(500)
|
|
505
|
+
self.finish(json.dumps({
|
|
506
|
+
"error": str(e)
|
|
507
|
+
}))
|
|
508
|
+
|
|
261
509
|
def setup_handlers(web_app):
|
|
262
510
|
host_pattern = ".*$"
|
|
263
511
|
base_url = web_app.settings["base_url"]
|
|
@@ -268,6 +516,13 @@ def setup_handlers(web_app):
|
|
|
268
516
|
# Read all files endpoint
|
|
269
517
|
read_all_files_route = url_path_join(base_url, "signalpilot-ai-internal", "read-all-files")
|
|
270
518
|
|
|
519
|
+
# File scanning endpoints
|
|
520
|
+
file_scan_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "scan")
|
|
521
|
+
scanned_directories_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "directories")
|
|
522
|
+
select_folder_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "select-folder")
|
|
523
|
+
workdir_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "workdir")
|
|
524
|
+
delete_scanned_directory_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "directories", "delete")
|
|
525
|
+
|
|
271
526
|
# Cache service endpoints
|
|
272
527
|
chat_histories_route = url_path_join(base_url, "signalpilot-ai-internal", "cache", "chat-histories")
|
|
273
528
|
chat_history_route = url_path_join(base_url, "signalpilot-ai-internal", "cache", "chat-histories", "([^/]+)")
|
|
@@ -297,6 +552,13 @@ def setup_handlers(web_app):
|
|
|
297
552
|
# Read all files endpoint
|
|
298
553
|
(read_all_files_route, ReadAllFilesHandler),
|
|
299
554
|
|
|
555
|
+
# File scanning endpoints
|
|
556
|
+
(file_scan_route, FileScanHandler),
|
|
557
|
+
(scanned_directories_route, ScannedDirectoriesHandler),
|
|
558
|
+
(select_folder_route, SelectFolderHandler),
|
|
559
|
+
(workdir_route, WorkDirHandler),
|
|
560
|
+
(delete_scanned_directory_route, DeleteScannedDirectoryHandler),
|
|
561
|
+
|
|
300
562
|
# Chat histories endpoints
|
|
301
563
|
(chat_histories_route, ChatHistoriesHandler),
|
|
302
564
|
(chat_history_route, ChatHistoriesHandler),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalpilot-ai-internal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "SignalPilot Agent - Your Jupyter Notebook Assistant",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"outputDir": "signalpilot_ai_internal/labextension",
|
|
135
135
|
"schemaDir": "schema",
|
|
136
136
|
"_build": {
|
|
137
|
-
"load": "static/remoteEntry.
|
|
137
|
+
"load": "static/remoteEntry.410a42566793b732952f.js",
|
|
138
138
|
"extension": "./extension",
|
|
139
139
|
"style": "./style"
|
|
140
140
|
}
|