44from collections import deque
55import pandas as pd
66import duckdb
7- from typing import Any , Optional
7+ from typing import Any , Optional , List , Tuple , Dict
88
99from rich .console import Console
1010from rich .table import Table
@@ -39,7 +39,7 @@ def __init__(self) -> None:
3939 """Initializes the SheetQL tool."""
4040 self .console = Console ()
4141 self .db_connection : Optional [duckdb .DuckDBPyConnection ] = None
42- self .results_to_save : dict [str , pd .DataFrame ] = {}
42+ self .results_to_save : Dict [str , pd .DataFrame ] = {}
4343 self .history : deque [str ] = deque (maxlen = self .HISTORY_MAX_LEN )
4444
4545 def run_interactive (self ) -> None :
@@ -116,9 +116,9 @@ def _display_welcome(self) -> None:
116116 )
117117
118118 def _prompt_for_paths (
119- self , title : str , filetypes : list [ tuple [str , str ]], allow_multiple : bool
120- ) -> Optional [list [str ]]:
121- """Generic method to get file paths from the user via GUI or CLI ."""
119+ self , title : str , filetypes : List [ Tuple [str , str ]], allow_multiple : bool
120+ ) -> Optional [List [str ]]:
121+ """Generic method to get existing file paths from the user."""
122122 if TKINTER_AVAILABLE :
123123 root = tk .Tk ()
124124 root .withdraw ()
@@ -147,7 +147,39 @@ def _prompt_for_paths(
147147
148148 return valid_paths
149149
150- def _load_data (self , file_paths : list [str ]) -> dict [str , pd .DataFrame ]:
150+ def _prompt_for_save_path (self ) -> Optional [str ]:
151+ """Prompts the user for a new file save path."""
152+ if TKINTER_AVAILABLE :
153+ root = tk .Tk ()
154+ root .withdraw ()
155+ save_path = filedialog .asksaveasfilename (
156+ title = "Select Save Location" ,
157+ initialfile = self .DEFAULT_EXPORT_FILENAME ,
158+ defaultextension = ".xlsx" ,
159+ filetypes = [("Excel Files" , "*.xlsx" )],
160+ )
161+ root .destroy ()
162+ return save_path if save_path else None
163+
164+ self .console .print ("\n [cyan]Please enter a save path for the export.[/cyan]" )
165+ save_path_input = self .console .input (
166+ f"[bold]Save path (default: { self .DEFAULT_EXPORT_FILENAME } ): [/bold]"
167+ )
168+ if not save_path_input :
169+ save_path_input = self .DEFAULT_EXPORT_FILENAME
170+
171+ directory = os .path .dirname (save_path_input )
172+ if directory and not os .path .exists (directory ):
173+ try :
174+ os .makedirs (directory )
175+ except OSError as e :
176+ self .console .print (
177+ f"[red]Error: Could not create directory '{ directory } '. { e } [/red]"
178+ )
179+ return None
180+ return save_path_input
181+
182+ def _load_data (self , file_paths : List [str ]) -> Dict [str , pd .DataFrame ]:
151183 """Loads all supported files into pandas DataFrames."""
152184 all_dataframes = {}
153185 with self .console .status ("[bold green]Loading data files...[/bold green]" ):
@@ -196,7 +228,7 @@ def _load_data(self, file_paths: list[str]) -> dict[str, pd.DataFrame]:
196228 )
197229 return all_dataframes
198230
199- def _register_dataframes (self , dataframes : dict [str , pd .DataFrame ]) -> None :
231+ def _register_dataframes (self , dataframes : Dict [str , pd .DataFrame ]) -> None :
200232 """Registers new DataFrames as views in the existing DuckDB connection."""
201233 if not self .db_connection :
202234 return
@@ -240,7 +272,7 @@ def _handle_meta_command(self, command_str: str) -> bool:
240272 """Handles meta-commands. Returns True if the app should exit."""
241273 parts = command_str .split ()
242274 command = parts [0 ].lower ()
243- commands : dict [str , Any ] = {
275+ commands : Dict [str , Any ] = {
244276 ".exit" : lambda : True ,
245277 ".quit" : lambda : True ,
246278 ".help" : self ._show_help ,
@@ -354,14 +386,8 @@ def _export_results(self) -> None:
354386 self .console .print ("[yellow]No results are staged for export.[/yellow]" )
355387 return
356388
357- save_path = self ._prompt_for_paths (
358- title = "Select Save Location" ,
359- filetypes = [("Excel Files" , "*.xlsx" )],
360- allow_multiple = False ,
361- )
362-
363- if save_path and save_path [0 ]:
364- self ._save_to_excel (save_path [0 ])
389+ if save_path := self ._prompt_for_save_path ():
390+ self ._save_to_excel (save_path )
365391 else :
366392 self .console .print ("[yellow]Save operation cancelled.[/yellow]" )
367393
@@ -395,11 +421,14 @@ def _format_excel_sheets(self, writer: pd.ExcelWriter) -> None:
395421 cell .fill = header_fill
396422
397423 for column_cells in worksheet .columns :
398- max_length = max (
399- len (str (cell .value ))
400- for cell in column_cells
401- if cell .value is not None
402- )
424+ try :
425+ max_length = max (
426+ len (str (cell .value ))
427+ for cell in column_cells
428+ if cell .value is not None
429+ )
430+ except ValueError :
431+ max_length = 0
403432 worksheet .column_dimensions [column_cells [0 ].column_letter ].width = (
404433 max_length + 2
405434 )
@@ -437,7 +466,7 @@ def _add_new_files(self) -> None:
437466 self .console .print ("[green]✔ New files loaded and registered.[/green]" )
438467 self ._list_tables ()
439468
440- def _describe_table (self , command_parts : list [str ]) -> None :
469+ def _describe_table (self , command_parts : List [str ]) -> None :
441470 """Shows the schema for a given table."""
442471 if len (command_parts ) != 2 :
443472 self .console .print ("[red]Usage: .schema <table_name>[/red]" )
@@ -462,7 +491,7 @@ def _describe_table(self, command_parts: list[str]) -> None:
462491 except Exception as e :
463492 self .console .print (f"[bold red]❌ Error describing table: { e } [/bold red]" )
464493
465- def _rename_table (self , command_parts : list [str ]) -> None :
494+ def _rename_table (self , command_parts : List [str ]) -> None :
466495 """Renames a view in the database."""
467496 if len (command_parts ) != 3 :
468497 self .console .print (
@@ -510,7 +539,7 @@ def _handle_history_rerun(self, command_str: str) -> None:
510539 "[red]Invalid history command. Use !N where N is a number.[/red]"
511540 )
512541
513- def _run_script_interactive (self , command_parts : list [str ]) -> None :
542+ def _run_script_interactive (self , command_parts : List [str ]) -> None :
514543 """Handles the .runscript meta-command."""
515544 script_path = command_parts [1 ] if len (command_parts ) == 2 else None
516545 if not script_path :
@@ -546,7 +575,7 @@ def _run_script_interactive(self, command_parts: list[str]) -> None:
546575 except yaml .YAMLError as e :
547576 self .console .print (f"[bold red]❌ Error parsing YAML file: { e } [/bold red]" )
548577
549- def _execute_yaml_script (self , config : dict [str , Any ]) -> None :
578+ def _execute_yaml_script (self , config : Dict [str , Any ]) -> None :
550579 """Processes the actions defined in a parsed YAML script."""
551580 if "inputs" in config :
552581 self ._process_yaml_inputs (config .get ("inputs" , []))
@@ -555,7 +584,7 @@ def _execute_yaml_script(self, config: dict[str, Any]) -> None:
555584 if "export" in config :
556585 self ._process_yaml_export (config .get ("export" , {}))
557586
558- def _process_yaml_inputs (self , inputs : list [ dict [str , str ]]) -> None :
587+ def _process_yaml_inputs (self , inputs : List [ Dict [str , str ]]) -> None :
559588 """Loads and aliases data files from a YAML script's 'inputs' block."""
560589 self .console .print ("\n [bold]--- 1. Loading Input Files ---[/bold]" )
561590 if not inputs :
@@ -590,7 +619,7 @@ def _process_yaml_inputs(self, inputs: list[dict[str, str]]) -> None:
590619 self .db_connection .register (final_name , df )
591620 self .console .print ("[green]✔ All specified inputs loaded and aliased.[/green]" )
592621
593- def _process_yaml_tasks (self , tasks : list [ dict [str , str ]]) -> None :
622+ def _process_yaml_tasks (self , tasks : List [ Dict [str , str ]]) -> None :
594623 """Executes queries from a YAML script's 'tasks' block."""
595624 self .console .print ("\n [bold]--- 2. Executing Tasks ---[/bold]" )
596625 if not tasks :
@@ -613,7 +642,7 @@ def _process_yaml_tasks(self, tasks: list[dict[str, str]]) -> None:
613642 f"[bold red] ❌ Error in task '{ name } ': { e } [/bold red]"
614643 )
615644
616- def _process_yaml_export (self , export_config : dict [str , str ]) -> None :
645+ def _process_yaml_export (self , export_config : Dict [str , str ]) -> None :
617646 """Exports staged results based on a YAML script's 'export' block."""
618647 self .console .print ("\n [bold]--- 3. Exporting Results ---[/bold]" )
619648 if not export_config :
0 commit comments