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.
- execsql/gui/base.py +52 -1
- execsql/gui/console.py +86 -9
- execsql/gui/desktop.py +261 -39
- execsql/gui/tui.py +325 -51
- execsql/metacommands/connect.py +5 -1
- execsql/metacommands/dispatch.py +49 -6
- execsql/metacommands/io_export.py +2 -2
- execsql/metacommands/prompt.py +6 -11
- {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/METADATA +1 -1
- {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/RECORD +29 -29
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.13.2.data → execsql2-2.14.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/WHEEL +0 -0
- {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.13.2.dist-info → execsql2-2.14.1.dist-info}/licenses/LICENSE.txt +0 -0
- {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 = "
|
|
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
|
-
|
|
181
|
-
|
|
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",
|