execsql2 2.5.0__py3-none-any.whl → 2.7.1__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.
Files changed (32) hide show
  1. execsql/exporters/__init__.py +3 -3
  2. execsql/exporters/delimited.py +2 -2
  3. execsql/exporters/markdown.py +126 -0
  4. execsql/exporters/xlsx.py +317 -0
  5. execsql/exporters/yaml.py +87 -0
  6. execsql/gui/tui.py +132 -0
  7. execsql/metacommands/__init__.py +203 -182
  8. execsql/metacommands/dispatch.py +11 -0
  9. execsql/metacommands/io.py +2 -0
  10. execsql/metacommands/io_export.py +75 -0
  11. execsql/state.py +261 -200
  12. {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/METADATA +5 -2
  13. {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/RECORD +32 -29
  14. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/README.md +0 -0
  15. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  16. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  17. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/execsql.conf +0 -0
  18. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  19. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  20. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  21. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  22. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  23. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  24. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  25. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/script_template.sql +0 -0
  26. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  27. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  28. {execsql2-2.5.0.data → execsql2-2.7.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  29. {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/WHEEL +0 -0
  30. {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/entry_points.txt +0 -0
  31. {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/licenses/LICENSE.txt +0 -0
  32. {execsql2-2.5.0.dist-info → execsql2-2.7.1.dist-info}/licenses/NOTICE +0 -0
execsql/gui/tui.py CHANGED
@@ -101,6 +101,10 @@ def _button_row(button_list: list) -> list[Button]:
101
101
  class _BaseDialog(ModalScreen):
102
102
  """Common infrastructure for all execsql dialog screens."""
103
103
 
104
+ BINDINGS = [
105
+ Binding("escape", "cancel", "Cancel", show=True),
106
+ ]
107
+
104
108
  DEFAULT_CSS = """
105
109
  _BaseDialog {
106
110
  align: center middle;
@@ -151,6 +155,11 @@ class _BaseDialog(ModalScreen):
151
155
  def result(self) -> dict:
152
156
  return self._result
153
157
 
158
+ def action_cancel(self) -> None:
159
+ """Dismiss the dialog as cancelled (triggered by Escape key)."""
160
+ self._result = {"button": None, "cancelled": True}
161
+ self.dismiss(self._result)
162
+
154
163
  @on(Button.Pressed, "#btn_cancel_exit")
155
164
  def _on_cancel_exit(self, event: Button.Pressed) -> None:
156
165
  """Universal Cancel handler — dismisses with cancelled=True so the sync queue can exit."""
@@ -167,6 +176,11 @@ class _BaseDialog(ModalScreen):
167
176
  class MsgScreen(_BaseDialog):
168
177
  """Simple message dialog with Continue and Cancel buttons."""
169
178
 
179
+ BINDINGS = [
180
+ *_BaseDialog.BINDINGS,
181
+ Binding("enter", "submit", "Continue", show=True),
182
+ ]
183
+
170
184
  def compose(self) -> ComposeResult:
171
185
  title = self.args.get("title", "Message")
172
186
  message = self.args.get("message", "")
@@ -176,6 +190,12 @@ class MsgScreen(_BaseDialog):
176
190
  with Horizontal(id="buttons"):
177
191
  yield Button("Cancel", id="btn_cancel_exit", variant="warning")
178
192
  yield Button("Continue", id="btn_close", variant="primary")
193
+ yield Footer()
194
+
195
+ def action_submit(self) -> None:
196
+ """Continue the dialog (triggered by Enter key)."""
197
+ self._result = {"button": 1}
198
+ self.dismiss(self._result)
179
199
 
180
200
  def on_button_pressed(self, event: Button.Pressed) -> None:
181
201
  if event.button.id == "btn_close":
@@ -191,6 +211,11 @@ class MsgScreen(_BaseDialog):
191
211
  class PauseScreen(_BaseDialog):
192
212
  """Pause dialog with optional countdown and Continue/Cancel buttons."""
193
213
 
214
+ BINDINGS = [
215
+ *_BaseDialog.BINDINGS,
216
+ Binding("enter", "submit", "Continue", show=True),
217
+ ]
218
+
194
219
  def compose(self) -> ComposeResult:
195
220
  title = self.args.get("title", "Pause")
196
221
  message = self.args.get("message", "")
@@ -200,6 +225,7 @@ class PauseScreen(_BaseDialog):
200
225
  with Horizontal(id="buttons"):
201
226
  yield Button("Cancel", id="btn_cancel_exit", variant="warning")
202
227
  yield Button("Continue", id="btn_continue", variant="primary")
228
+ yield Footer()
203
229
 
204
230
  def on_mount(self) -> None:
205
231
  countdown = self.args.get("countdown")
@@ -210,6 +236,11 @@ class PauseScreen(_BaseDialog):
210
236
  self._result = {"quit": False}
211
237
  self.dismiss(self._result)
212
238
 
239
+ def action_submit(self) -> None:
240
+ """Continue the dialog (triggered by Enter key)."""
241
+ self._result = {"quit": False}
242
+ self.dismiss(self._result)
243
+
213
244
  def on_button_pressed(self, event: Button.Pressed) -> None:
214
245
  if event.button.id == "btn_continue":
215
246
  self._result = {"quit": False}
@@ -224,6 +255,11 @@ class PauseScreen(_BaseDialog):
224
255
  class DisplayScreen(_BaseDialog):
225
256
  """Data display dialog: title, message, optional table, optional text entry, buttons."""
226
257
 
258
+ BINDINGS = [
259
+ *_BaseDialog.BINDINGS,
260
+ Binding("enter", "submit", "Submit", show=True),
261
+ ]
262
+
227
263
  def compose(self) -> ComposeResult:
228
264
  title = self.args.get("title", "")
229
265
  message = self.args.get("message", "")
@@ -251,6 +287,23 @@ class DisplayScreen(_BaseDialog):
251
287
  )
252
288
  with Horizontal(id="buttons"):
253
289
  yield from _button_row(button_list)
290
+ yield Footer()
291
+
292
+ def action_submit(self) -> None:
293
+ """Submit the first primary button value (triggered by Enter key)."""
294
+ button_list = self.args.get("button_list", [("Continue", 1)])
295
+ # Find the first non-cancel button value.
296
+ value = None
297
+ for btn in button_list:
298
+ if btn[1]:
299
+ value = btn[1]
300
+ break
301
+ if value is None:
302
+ return
303
+ text_input = self.query_one("#text_input", Input) if self.args.get("textentry") else None
304
+ return_value = text_input.value if text_input else None
305
+ self._result = {"button": value, "return_value": return_value}
306
+ self.dismiss(self._result)
254
307
 
255
308
  def on_button_pressed(self, event: Button.Pressed) -> None:
256
309
  btn_id = event.button.id
@@ -272,6 +325,11 @@ class DisplayScreen(_BaseDialog):
272
325
  class EntryFormScreen(_BaseDialog):
273
326
  """Multi-field entry form dialog."""
274
327
 
328
+ BINDINGS = [
329
+ *_BaseDialog.BINDINGS,
330
+ Binding("enter", "submit", "OK", show=True),
331
+ ]
332
+
275
333
  DEFAULT_CSS = (
276
334
  _BaseDialog.DEFAULT_CSS
277
335
  + """
@@ -337,9 +395,26 @@ class EntryFormScreen(_BaseDialog):
337
395
  with Horizontal(id="buttons"):
338
396
  yield Button("Cancel", id="btn_cancel_exit", variant="warning")
339
397
  yield Button("OK", id="btn_ok", variant="primary")
398
+ yield Footer()
340
399
 
341
400
  self._specs = specs
342
401
 
402
+ def action_submit(self) -> None:
403
+ """Submit the form (triggered by Enter key)."""
404
+ for spec in self._specs:
405
+ widget = self._inputs.get(spec.varname)
406
+ if widget is None:
407
+ continue
408
+ etype = (spec.entry_type or "text").lower()
409
+ if etype == "checkbox":
410
+ spec.value = "True" if widget.value else "False"
411
+ elif etype in ("dropdown", "select"):
412
+ spec.value = str(widget.value) if widget.value is not None else ""
413
+ else:
414
+ spec.value = widget.value
415
+ self._result = {"button": 1, "return_value": self._specs}
416
+ self.dismiss(self._result)
417
+
343
418
  def on_button_pressed(self, event: Button.Pressed) -> None:
344
419
  if event.button.id == "btn_ok":
345
420
  for spec in self._specs:
@@ -365,6 +440,11 @@ class EntryFormScreen(_BaseDialog):
365
440
  class CompareScreen(_BaseDialog):
366
441
  """Side-by-side table comparison dialog."""
367
442
 
443
+ BINDINGS = [
444
+ *_BaseDialog.BINDINGS,
445
+ Binding("enter", "submit", "Continue", show=True),
446
+ ]
447
+
368
448
  DEFAULT_CSS = (
369
449
  _BaseDialog.DEFAULT_CSS
370
450
  + """
@@ -412,6 +492,7 @@ class CompareScreen(_BaseDialog):
412
492
  yield _make_table_widget("table2", headers2, rows2)
413
493
  with Horizontal(id="buttons"):
414
494
  yield from _button_row(button_list)
495
+ yield Footer()
415
496
 
416
497
  def on_mount(self) -> None:
417
498
  keylist = [str(k) for k in self.args.get("keylist", [])]
@@ -461,6 +542,15 @@ class CompareScreen(_BaseDialog):
461
542
  finally:
462
543
  self._syncing = False
463
544
 
545
+ def action_submit(self) -> None:
546
+ """Submit the first primary button value (triggered by Enter key)."""
547
+ button_list = self.args.get("button_list", [("Continue", 1)])
548
+ for btn in button_list:
549
+ if btn[1]:
550
+ self._result = {"button": btn[1]}
551
+ self.dismiss(self._result)
552
+ return
553
+
464
554
  def on_button_pressed(self, event: Button.Pressed) -> None:
465
555
  btn_id = event.button.id
466
556
  if btn_id and btn_id.startswith("btn_") and btn_id[4:].isdigit():
@@ -478,6 +568,11 @@ class CompareScreen(_BaseDialog):
478
568
  class SelectRowsScreen(_BaseDialog):
479
569
  """Row selection dialog: pick rows from source table and add to target."""
480
570
 
571
+ BINDINGS = [
572
+ *_BaseDialog.BINDINGS,
573
+ Binding("enter", "submit", "Continue", show=True),
574
+ ]
575
+
481
576
  DEFAULT_CSS = (
482
577
  _BaseDialog.DEFAULT_CSS
483
578
  + """
@@ -516,6 +611,7 @@ class SelectRowsScreen(_BaseDialog):
516
611
  yield _make_table_widget("dest_table", headers2, rows2)
517
612
  with Horizontal(id="buttons"):
518
613
  yield from _button_row(button_list)
614
+ yield Footer()
519
615
 
520
616
  def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
521
617
  if event.data_table.id == "source_table":
@@ -525,6 +621,15 @@ class SelectRowsScreen(_BaseDialog):
525
621
  values = [src.get_cell(row_key, col) for col in src.columns]
526
622
  dest.add_row(*values)
527
623
 
624
+ def action_submit(self) -> None:
625
+ """Submit the first primary button value (triggered by Enter key)."""
626
+ button_list = self.args.get("button_list", [("Continue", 1)])
627
+ for btn in button_list:
628
+ if btn[1]:
629
+ self._result = {"button": btn[1]}
630
+ self.dismiss(self._result)
631
+ return
632
+
528
633
  def on_button_pressed(self, event: Button.Pressed) -> None:
529
634
  btn_id = event.button.id
530
635
  if btn_id and btn_id.startswith("btn_") and btn_id[4:].isdigit():
@@ -588,6 +693,10 @@ class SelectSubScreen(_BaseDialog):
588
693
  class ActionScreen(_BaseDialog):
589
694
  """Action button grid dialog."""
590
695
 
696
+ BINDINGS = [
697
+ *_BaseDialog.BINDINGS,
698
+ ]
699
+
591
700
  def compose(self) -> ComposeResult:
592
701
  title = self.args.get("title", "Actions")
593
702
  message = self.args.get("message", "")
@@ -609,6 +718,7 @@ class ActionScreen(_BaseDialog):
609
718
  yield Button(f"{spec.label} — {spec.prompt}", id=f"action_{i}", variant="primary")
610
719
  if include_continue:
611
720
  yield Button("Continue", id="action_continue", variant="default")
721
+ yield Footer()
612
722
 
613
723
  self._button_specs = button_specs
614
724
 
@@ -1045,6 +1155,7 @@ class ConsoleApp(App):
1045
1155
  self._script_thread: threading.Thread | None = None
1046
1156
  self._script_exception: BaseException | None = None
1047
1157
  self._screen_map: dict | None = None
1158
+ self._console_lines: list[str] = []
1048
1159
 
1049
1160
  def compose(self) -> ComposeResult:
1050
1161
  yield Header(show_clock=False)
@@ -1118,11 +1229,19 @@ class ConsoleApp(App):
1118
1229
  self.call_from_thread(self._append_console, text)
1119
1230
 
1120
1231
  def _append_console(self, text: str) -> None:
1232
+ self._console_lines.append(text)
1121
1233
  try:
1122
1234
  self.query_one("#console_log", RichLog).write(text)
1123
1235
  except Exception:
1124
1236
  pass # Widget may not be mounted yet.
1125
1237
 
1238
+ def save(self, outfile: str, append: bool = False) -> None:
1239
+ """Save the console text contents to *outfile*."""
1240
+ mode = "a" if append else "w"
1241
+ with open(outfile, mode, encoding="utf-8") as fh:
1242
+ for line in self._console_lines:
1243
+ fh.write(line if line.endswith("\n") else line + "\n")
1244
+
1126
1245
  def set_status(self, msg: str) -> None:
1127
1246
  """Thread-safe status bar update."""
1128
1247
  self.call_from_thread(self._update_status, msg)
@@ -1199,6 +1318,19 @@ class TextualBackend(GuiBackend):
1199
1318
  pct = (num / total * 100.0) if total else num
1200
1319
  self._console_app.set_progress(pct)
1201
1320
 
1321
+ def console_save(self, outfile: str, append: bool = False) -> None:
1322
+ """Save console text contents to *outfile*."""
1323
+ if self._console_app is not None:
1324
+ self._console_app.save(outfile, append)
1325
+
1326
+ def console_hide(self) -> None:
1327
+ """Hide the console (minimize) — Textual has no window-level hide,
1328
+ so this is a no-op in a terminal environment."""
1329
+
1330
+ def console_show(self) -> None:
1331
+ """Show the console — Textual has no window-level show,
1332
+ so this is a no-op in a terminal environment."""
1333
+
1202
1334
  def console_wait_user(self, message: str = "") -> None:
1203
1335
  # ConsoleApp exits when the script is done; nothing extra needed here.
1204
1336
  pass