Skip to content

Commit 3755ecf

Browse files
authored
Merge pull request #3 from uzairmukadam/v2.0.1b
Fixed result export issue requiring an exisiting file rather than creating a new one.
2 parents ffd47e1 + af6be0a commit 3755ecf

File tree

2 files changed

+61
-33
lines changed

2 files changed

+61
-33
lines changed

sheet_ql.py

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from collections import deque
55
import pandas as pd
66
import duckdb
7-
from typing import Any, Optional
7+
from typing import Any, Optional, List, Tuple, Dict
88

99
from rich.console import Console
1010
from 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:

tests/test_sheet_ql.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,19 +149,18 @@ def test_07_load_command(self):
149149
self.assertIn("sample_json", final_tables)
150150

151151
@patch("sheet_ql.SheetQL._format_excel_sheets")
152-
@patch("sheet_ql.SheetQL._prompt_for_paths")
153-
def test_08_export_command(self, mock_prompt, mock_format):
152+
@patch("sheet_ql.SheetQL._prompt_for_save_path")
153+
def test_08_export_command(self, mock_prompt_save, mock_format):
154154
"""Test the '.export' command workflow."""
155155
self.tool.results_to_save["my_results"] = pd.DataFrame({"a": [1]})
156156
self.assertTrue(self.tool.results_to_save)
157157

158-
# Use the safe, temporary test directory
159158
save_path = os.path.join(self.test_dir, "report.xlsx")
160-
mock_prompt.return_value = [save_path]
159+
mock_prompt_save.return_value = save_path
161160

162161
self.tool._export_results()
163162

164-
mock_prompt.assert_called_once()
163+
mock_prompt_save.assert_called_once()
165164
mock_format.assert_called_once()
166165
self.assertFalse(
167166
self.tool.results_to_save,

0 commit comments

Comments
 (0)