execsql2 2.13.2__py3-none-any.whl → 2.14.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 (29) hide show
  1. execsql/gui/base.py +52 -1
  2. execsql/gui/console.py +86 -9
  3. execsql/gui/desktop.py +261 -39
  4. execsql/gui/tui.py +325 -51
  5. execsql/metacommands/connect.py +5 -1
  6. execsql/metacommands/dispatch.py +49 -6
  7. execsql/metacommands/io_export.py +2 -2
  8. execsql/metacommands/prompt.py +6 -11
  9. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/METADATA +1 -1
  10. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/RECORD +29 -29
  11. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/README.md +0 -0
  12. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  13. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  14. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/execsql.conf +0 -0
  15. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  16. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  17. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  18. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  19. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  20. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  21. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  22. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/script_template.sql +0 -0
  23. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  24. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  25. {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  26. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/WHEEL +0 -0
  27. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/entry_points.txt +0 -0
  28. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/licenses/LICENSE.txt +0 -0
  29. {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/licenses/NOTICE +0 -0
execsql/gui/base.py CHANGED
@@ -8,7 +8,58 @@ from typing import TYPE_CHECKING, Any
8
8
  if TYPE_CHECKING:
9
9
  pass
10
10
 
11
- __all__ = ["GuiBackend"]
11
+ __all__ = ["GuiBackend", "compare_stats"]
12
+
13
+
14
+ def compare_stats(
15
+ headers1: list,
16
+ rows1: list,
17
+ headers2: list,
18
+ rows2: list,
19
+ keylist: list,
20
+ ) -> str:
21
+ """Return a one-line diff summary for compare dialogs.
22
+
23
+ Computes matching rows, differing rows, and rows only in one table
24
+ based on the given key columns. Returns an empty string when *keylist*
25
+ is empty (stats cannot be computed without keys).
26
+ """
27
+ if not keylist:
28
+ return ""
29
+ key_idx1 = [i for i, h in enumerate(headers1) if str(h) in keylist]
30
+ key_idx2 = [i for i, h in enumerate(headers2) if str(h) in keylist]
31
+ if not key_idx1 or not key_idx2:
32
+ return ""
33
+
34
+ def _kv(row: list | tuple, idxs: list) -> tuple:
35
+ return tuple(str(row[i]) if row[i] is not None else "" for i in idxs)
36
+
37
+ keys1 = {_kv(r, key_idx1) for r in rows1}
38
+ keys2 = {_kv(r, key_idx2) for r in rows2}
39
+ only1 = len(keys1 - keys2)
40
+ only2 = len(keys2 - keys1)
41
+ common_keys = keys1 & keys2
42
+ row_map1 = {_kv(r, key_idx1): r for r in rows1}
43
+ row_map2 = {_kv(r, key_idx2): r for r in rows2}
44
+ matching = 0
45
+ differing = 0
46
+ for k in common_keys:
47
+ r1 = [str(v) if v is not None else "" for v in row_map1[k]]
48
+ r2 = [str(v) if v is not None else "" for v in row_map2[k]]
49
+ if r1 == r2:
50
+ matching += 1
51
+ else:
52
+ differing += 1
53
+ parts: list[str] = []
54
+ if matching:
55
+ parts.append(f"{matching:,} matching")
56
+ if differing:
57
+ parts.append(f"{differing:,} differing")
58
+ if only1:
59
+ parts.append(f"{only1:,} only in Table 1")
60
+ if only2:
61
+ parts.append(f"{only2:,} only in Table 2")
62
+ return " | ".join(parts) if parts else "Tables are identical"
12
63
 
13
64
 
14
65
  class GuiBackend(ABC):
execsql/gui/console.py CHANGED
@@ -11,11 +11,23 @@ import sys
11
11
  import time
12
12
  from typing import Any
13
13
 
14
- from execsql.gui.base import GuiBackend
14
+ from execsql.gui.base import GuiBackend, compare_stats as _compare_stats
15
15
 
16
16
  __all__ = ["ConsoleBackend"]
17
17
 
18
18
 
19
+ def _print_help_url(args: dict) -> None:
20
+ """Print the help URL if present in *args*."""
21
+ url = args.get("help_url")
22
+ if url:
23
+ print(f" Help: {url}", file=sys.stderr)
24
+
25
+
26
+ def _row_count_text(n: int) -> str:
27
+ """Return a human-readable row count string, e.g. '3 rows' or '1 row'."""
28
+ return f"{n:,} row{'s' if n != 1 else ''}"
29
+
30
+
19
31
  def _print_table(headers: list, rows: list, file: Any = None) -> None:
20
32
  """Print a simple ASCII table to the given file (default stderr)."""
21
33
  if file is None:
@@ -89,6 +101,7 @@ class ConsoleBackend(GuiBackend):
89
101
  rows = args.get("rowset")
90
102
  if headers and rows:
91
103
  _print_table(headers, rows)
104
+ print(f" {_row_count_text(len(rows))}", file=sys.stderr)
92
105
  input("Press Enter to acknowledge...")
93
106
  return {"button": 1}
94
107
 
@@ -119,14 +132,14 @@ class ConsoleBackend(GuiBackend):
119
132
  textentry = args.get("textentry", False)
120
133
  hidetext = args.get("hidetext", False)
121
134
  initial = args.get("initialtext", "")
122
- free = args.get("free", False)
123
-
124
135
  if title:
125
136
  print(f"\n=== {title} ===", file=sys.stderr)
126
137
  if message:
127
138
  print(message, file=sys.stderr)
139
+ _print_help_url(args)
128
140
  if headers and rows:
129
141
  _print_table(headers, rows)
142
+ print(f" {_row_count_text(len(rows))}", file=sys.stderr)
130
143
 
131
144
  return_value = None
132
145
  if textentry:
@@ -137,9 +150,6 @@ class ConsoleBackend(GuiBackend):
137
150
  else:
138
151
  return_value = input(f"Enter value [{initial}]: ").strip() or initial
139
152
 
140
- if free:
141
- return {"button": 1, "return_value": return_value}
142
-
143
153
  btn = _prompt_buttons(button_list)
144
154
  return {"button": btn, "return_value": return_value}
145
155
 
@@ -154,15 +164,17 @@ class ConsoleBackend(GuiBackend):
154
164
  print(f"\n=== {title} ===", file=sys.stderr)
155
165
  if message:
156
166
  print(message, file=sys.stderr)
167
+ _print_help_url(args)
157
168
  if headers and rows:
158
169
  _print_table(headers, rows)
170
+ print(f" {_row_count_text(len(rows))}", file=sys.stderr)
159
171
 
160
172
  for spec in entry_specs:
161
173
  entry_type = (spec.entry_type or "text").lower()
162
174
  initial = spec.initial_value or ""
163
175
  if entry_type == "checkbox":
164
176
  raw = input(f"{spec.label} [y/n, current={initial}]: ").strip().lower()
165
- spec.value = "True" if raw in ("y", "yes", "true", "1") else "False"
177
+ spec.value = "1" if raw in ("y", "yes", "true", "1") else "0"
166
178
  elif entry_type in ("dropdown", "select") and spec.lookup_list:
167
179
  choices = spec.lookup_list
168
180
  print(f"{spec.label}:", file=sys.stderr)
@@ -177,9 +189,58 @@ class ConsoleBackend(GuiBackend):
177
189
  spec.value = raw
178
190
  break
179
191
  print("Invalid choice.", file=sys.stderr)
180
- else:
181
- raw = input(f"{spec.label} [{initial}]: ").strip()
192
+ elif entry_type == "listbox" and spec.lookup_list:
193
+ choices = spec.lookup_list
194
+ print(f"{spec.label} (enter numbers separated by commas):", file=sys.stderr)
195
+ for i, c in enumerate(choices, 1):
196
+ print(f" [{i}] {c}", file=sys.stderr)
197
+ raw = input("Selections: ").strip()
198
+ selected = []
199
+ for part in raw.split(","):
200
+ part = part.strip()
201
+ if part.isdigit() and 1 <= int(part) <= len(choices):
202
+ selected.append(choices[int(part) - 1])
203
+ spec.value = ",".join(f"'{v.replace(chr(39), chr(39) + chr(39))}'" for v in selected)
204
+ elif entry_type == "radiobuttons":
205
+ parts = (spec.label or "").split(";")
206
+ label = parts[0] if parts else spec.label
207
+ buttons = parts[1:] if len(parts) > 1 else [spec.label or "Option"]
208
+ print(f"{label}:", file=sys.stderr)
209
+ for i, b in enumerate(buttons, 1):
210
+ print(f" [{i}] {b.strip()}", file=sys.stderr)
211
+ while True:
212
+ raw = input("Choice number: ").strip()
213
+ if raw.isdigit() and 1 <= int(raw) <= len(buttons):
214
+ spec.value = raw
215
+ break
216
+ print("Invalid choice.", file=sys.stderr)
217
+ elif entry_type == "textarea":
218
+ print(f"{spec.label} (enter text, blank line to finish):", file=sys.stderr)
219
+ lines = []
220
+ while True:
221
+ line = input()
222
+ if not line:
223
+ break
224
+ lines.append(line)
225
+ spec.value = "\n".join(lines) or initial
226
+ elif entry_type in ("inputfile", "outputfile"):
227
+ raw = input(f"{spec.label} (file path) [{initial}]: ").strip()
182
228
  spec.value = raw or initial
229
+ else:
230
+ while True:
231
+ raw = input(f"{spec.label} [{initial}]: ").strip()
232
+ val = raw or initial
233
+ if spec.required and not val:
234
+ print(" (required)", file=sys.stderr)
235
+ continue
236
+ if spec.validation_regex and val:
237
+ import re as _re
238
+
239
+ if not _re.fullmatch(spec.validation_regex, val):
240
+ print(f" (must match: {spec.validation_regex})", file=sys.stderr)
241
+ continue
242
+ spec.value = val
243
+ break
183
244
 
184
245
  print("", file=sys.stderr)
185
246
  raw = input("Submit? [y/n]: ").strip().lower()
@@ -199,10 +260,18 @@ class ConsoleBackend(GuiBackend):
199
260
  print(f"\n=== {title} ===", file=sys.stderr)
200
261
  if message:
201
262
  print(message, file=sys.stderr)
263
+ _print_help_url(args)
202
264
  print("\n--- Table 1 ---", file=sys.stderr)
203
265
  _print_table(headers1, rows1)
266
+ print(f" {_row_count_text(len(rows1))}", file=sys.stderr)
204
267
  print("\n--- Table 2 ---", file=sys.stderr)
205
268
  _print_table(headers2, rows2)
269
+ print(f" {_row_count_text(len(rows2))}", file=sys.stderr)
270
+
271
+ keylist = [str(k) for k in args.get("keylist", [])]
272
+ summary = _compare_stats(headers1, rows1, headers2, rows2, keylist)
273
+ if summary:
274
+ print(f"\n {summary}", file=sys.stderr)
206
275
 
207
276
  btn = _prompt_buttons(button_list)
208
277
  return {"button": btn}
@@ -217,7 +286,9 @@ class ConsoleBackend(GuiBackend):
217
286
  print(f"\n=== {title} ===", file=sys.stderr)
218
287
  if message:
219
288
  print(message, file=sys.stderr)
289
+ _print_help_url(args)
220
290
  _print_table(headers1, rows1)
291
+ print(f" {_row_count_text(len(rows1))}", file=sys.stderr)
221
292
  print("(Row selection requires a GUI backend; displaying source data only.)", file=sys.stderr)
222
293
 
223
294
  btn = _prompt_buttons(button_list)
@@ -255,8 +326,10 @@ class ConsoleBackend(GuiBackend):
255
326
  print(f"\n=== {title} ===", file=sys.stderr)
256
327
  if message:
257
328
  print(message, file=sys.stderr)
329
+ _print_help_url(args)
258
330
  if headers and rows:
259
331
  _print_table(headers, rows)
332
+ print(f" {_row_count_text(len(rows))}", file=sys.stderr)
260
333
 
261
334
  if not button_specs:
262
335
  input("Press Enter to continue...")
@@ -293,6 +366,8 @@ class ConsoleBackend(GuiBackend):
293
366
  print(message, file=sys.stderr)
294
367
  print("(Interactive map requires a GUI backend; showing tabular data.)", file=sys.stderr)
295
368
  _print_table(headers, rows)
369
+ if rows:
370
+ print(f" {_row_count_text(len(rows))}", file=sys.stderr)
296
371
 
297
372
  if lat_col and lon_col and headers and rows:
298
373
  try:
@@ -334,6 +409,7 @@ class ConsoleBackend(GuiBackend):
334
409
  message = args.get("message", "")
335
410
  if message:
336
411
  print(message, file=sys.stderr)
412
+ _print_help_url(args)
337
413
  username = input("Username: ").strip()
338
414
  import getpass
339
415
 
@@ -344,6 +420,7 @@ class ConsoleBackend(GuiBackend):
344
420
  message = args.get("message", "")
345
421
  if message:
346
422
  print(message, file=sys.stderr)
423
+ _print_help_url(args)
347
424
  db_types = {
348
425
  "p": "PostgreSQL",
349
426
  "s": "SQL Server",