execsql2 2.15.8__py3-none-any.whl → 2.15.11__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/__init__.py +4 -3
- execsql/cli/__init__.py +17 -0
- execsql/cli/run.py +4 -1
- execsql/config.py +20 -4
- execsql/db/base.py +4 -1
- execsql/debug/repl.py +27 -10
- execsql/gui/tui.py +59 -2
- execsql/metacommands/conditions.py +20 -2
- execsql/utils/gui.py +139 -17
- {execsql2-2.15.8.dist-info → execsql2-2.15.11.dist-info}/METADATA +2 -1
- {execsql2-2.15.8.dist-info → execsql2-2.15.11.dist-info}/RECORD +30 -30
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.15.11.dist-info}/WHEEL +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.15.11.dist-info}/entry_points.txt +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.15.11.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.15.11.dist-info}/licenses/NOTICE +0 -0
execsql/__init__.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
2
|
execsql — a maintained fork of the execsql SQL scripting tool.
|
|
3
3
|
|
|
4
|
-
This package provides the ``
|
|
5
|
-
``
|
|
6
|
-
version; all public
|
|
4
|
+
This package provides the ``execsql`` CLI command (distributed as the
|
|
5
|
+
``execsql2`` package on PyPI) and the ``execsql`` importable module.
|
|
6
|
+
The top-level package exposes only the package version; all public
|
|
7
|
+
functionality lives in sub-modules.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
execsql/cli/__init__.py
CHANGED
|
@@ -283,6 +283,16 @@ def main(
|
|
|
283
283
|
"--profile-limit",
|
|
284
284
|
help="Number of top statements to show in the --profile timing summary (default: 20).",
|
|
285
285
|
),
|
|
286
|
+
config_file: str | None = typer.Option(
|
|
287
|
+
None,
|
|
288
|
+
"--config",
|
|
289
|
+
metavar="FILE",
|
|
290
|
+
help=(
|
|
291
|
+
"Path to an execsql configuration file. "
|
|
292
|
+
"Loaded after the implicit search paths so its values take precedence. "
|
|
293
|
+
"The file may chain additional configs via its [cyan][config][/cyan] section."
|
|
294
|
+
),
|
|
295
|
+
),
|
|
286
296
|
debug: bool = typer.Option(
|
|
287
297
|
False,
|
|
288
298
|
"--debug",
|
|
@@ -376,6 +386,12 @@ def main(
|
|
|
376
386
|
webbrowser.open("https://execsql2.readthedocs.io/en/latest/", new=2, autoraise=True)
|
|
377
387
|
raise typer.Exit()
|
|
378
388
|
|
|
389
|
+
if config_file and not Path(config_file).is_file():
|
|
390
|
+
_err_console.print(
|
|
391
|
+
f"[bold red]Error:[/bold red] Config file [cyan]{config_file!r}[/cyan] does not exist.",
|
|
392
|
+
)
|
|
393
|
+
raise typer.Exit(code=2)
|
|
394
|
+
|
|
379
395
|
positional = args or []
|
|
380
396
|
if command is not None:
|
|
381
397
|
script_name = None # inline mode — no script file
|
|
@@ -458,6 +474,7 @@ def main(
|
|
|
458
474
|
ping=ping,
|
|
459
475
|
lint=lint,
|
|
460
476
|
debug=debug,
|
|
477
|
+
config_file=config_file,
|
|
461
478
|
)
|
|
462
479
|
|
|
463
480
|
|
execsql/cli/run.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import Any
|
|
|
12
12
|
import datetime
|
|
13
13
|
import getpass
|
|
14
14
|
import os
|
|
15
|
+
import platform
|
|
15
16
|
import sys
|
|
16
17
|
import traceback
|
|
17
18
|
from pathlib import Path
|
|
@@ -219,6 +220,7 @@ def _run(
|
|
|
219
220
|
ping: bool = False,
|
|
220
221
|
lint: bool = False,
|
|
221
222
|
debug: bool = False,
|
|
223
|
+
config_file: str | None = None,
|
|
222
224
|
) -> None:
|
|
223
225
|
"""Initialise state, connect to the database, load the script, and run it.
|
|
224
226
|
|
|
@@ -276,13 +278,14 @@ def _run(
|
|
|
276
278
|
elif osys.startswith("win"):
|
|
277
279
|
osys = "windows"
|
|
278
280
|
_state.subvars.add_substitution("$OS", osys)
|
|
281
|
+
_state.subvars.add_substitution("$HOSTNAME", platform.node())
|
|
279
282
|
_state.subvars.add_substitution("$PYTHON_EXECUTABLE", sys.executable)
|
|
280
283
|
|
|
281
284
|
# ------------------------------------------------------------------
|
|
282
285
|
# Read configuration file
|
|
283
286
|
# ------------------------------------------------------------------
|
|
284
287
|
script_path = str(Path(script_name).resolve().parent) if script_name else os.getcwd()
|
|
285
|
-
_state.conf = ConfigData(script_path, _state.subvars)
|
|
288
|
+
_state.conf = ConfigData(script_path, _state.subvars, config_file=config_file)
|
|
286
289
|
conf = _state.conf
|
|
287
290
|
|
|
288
291
|
# ------------------------------------------------------------------
|
execsql/config.py
CHANGED
|
@@ -198,7 +198,13 @@ class ConfigData:
|
|
|
198
198
|
raise ConfigError(f"Invalid {key}: {val}; must be >= {min_val}.")
|
|
199
199
|
setattr(self, attr, val)
|
|
200
200
|
|
|
201
|
-
def __init__(
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
script_path: str,
|
|
204
|
+
variable_pool: object,
|
|
205
|
+
*,
|
|
206
|
+
config_file: str | None = None,
|
|
207
|
+
) -> None:
|
|
202
208
|
"""Load and merge all discoverable execsql.conf files for the given script path.
|
|
203
209
|
|
|
204
210
|
Args:
|
|
@@ -207,6 +213,10 @@ class ConfigData:
|
|
|
207
213
|
variable_pool: Substitution-variable registry used to expand
|
|
208
214
|
``config_file`` path values and to populate ``[variables]``
|
|
209
215
|
sections.
|
|
216
|
+
config_file: Optional explicit config file path (from ``--config``).
|
|
217
|
+
Loaded after the implicit search paths so its values take
|
|
218
|
+
precedence over system, user, script, and working-directory
|
|
219
|
+
config files.
|
|
210
220
|
"""
|
|
211
221
|
self.db_type = "a"
|
|
212
222
|
self.server = None
|
|
@@ -290,9 +300,15 @@ class ConfigData:
|
|
|
290
300
|
config_files = [sys_config_file, user_config_file, script_config_file, startdir_config_file]
|
|
291
301
|
else:
|
|
292
302
|
config_files = [sys_config_file, user_config_file, startdir_config_file]
|
|
303
|
+
if config_file:
|
|
304
|
+
config_files.append(str(Path(config_file).resolve()))
|
|
305
|
+
from collections import deque
|
|
306
|
+
|
|
293
307
|
_MAX_CONFIG_CHAIN = 20 # Guard against circular config_file references.
|
|
308
|
+
config_queue: deque[str] = deque(config_files)
|
|
294
309
|
self.files_read: list = []
|
|
295
|
-
|
|
310
|
+
while config_queue:
|
|
311
|
+
configfile = config_queue.popleft()
|
|
296
312
|
if len(self.files_read) >= _MAX_CONFIG_CHAIN:
|
|
297
313
|
break
|
|
298
314
|
if configfile not in self.files_read and Path(configfile).is_file():
|
|
@@ -425,7 +441,7 @@ class ConfigData:
|
|
|
425
441
|
conffile = str(Path(conffile) / self.config_file_name)
|
|
426
442
|
if Path(conffile).is_file():
|
|
427
443
|
# Silently ignore a non-existent file, for cross-OS compatibility.
|
|
428
|
-
|
|
444
|
+
config_queue.appendleft(conffile)
|
|
429
445
|
# OS-specific additional config files.
|
|
430
446
|
_os_config_key: str | None = None
|
|
431
447
|
if sys.platform == "linux" and cp.has_option(self._CONFIG_SECTION, "linux_config_file"):
|
|
@@ -445,7 +461,7 @@ class ConfigData:
|
|
|
445
461
|
if not Path(conffile).is_file():
|
|
446
462
|
conffile = str(Path(conffile) / self.config_file_name)
|
|
447
463
|
if Path(conffile).is_file():
|
|
448
|
-
|
|
464
|
+
config_queue.appendleft(conffile)
|
|
449
465
|
self._get_bool(cp, self._CONFIG_SECTION, "user_logfile", "user_logfile")
|
|
450
466
|
# dao_flush_delay_secs has a specific error message — keep inline
|
|
451
467
|
if cp.has_option(self._CONFIG_SECTION, "dao_flush_delay_secs"):
|
execsql/db/base.py
CHANGED
execsql/debug/repl.py
CHANGED
|
@@ -45,23 +45,39 @@ _YELLOW = "\033[33m"
|
|
|
45
45
|
_CYAN = "\033[36m"
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
_color_cache: bool | None = None
|
|
49
|
+
|
|
50
|
+
|
|
48
51
|
def _use_color() -> bool:
|
|
49
52
|
"""Return True if the output stream supports ANSI color.
|
|
50
53
|
|
|
51
54
|
Checks ``NO_COLOR`` and ``EXECSQL_NO_COLOR`` environment variables first
|
|
52
55
|
(either set → color off). Then tests whether the active output stream
|
|
53
56
|
reports itself as a TTY.
|
|
57
|
+
|
|
58
|
+
The result is cached after the first call; call ``_reset_color_cache()``
|
|
59
|
+
to force re-evaluation (e.g. when entering the REPL).
|
|
54
60
|
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
global _color_cache # noqa: PLW0603
|
|
62
|
+
if _color_cache is not None:
|
|
63
|
+
return _color_cache
|
|
64
|
+
if os.environ.get("NO_COLOR") is not None or os.environ.get("EXECSQL_NO_COLOR") is not None:
|
|
65
|
+
_color_cache = False
|
|
66
|
+
else:
|
|
67
|
+
output = _state.output
|
|
68
|
+
if output is not None and hasattr(output, "isatty"):
|
|
69
|
+
_color_cache = output.isatty()
|
|
70
|
+
else:
|
|
71
|
+
# WriteHooks (the default _state.output) has no isatty — fall through
|
|
72
|
+
# to check the underlying stream it would write to.
|
|
73
|
+
_color_cache = sys.stdout.isatty()
|
|
74
|
+
return _color_cache
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _reset_color_cache() -> None:
|
|
78
|
+
"""Clear the cached color decision so it is re-evaluated on next use."""
|
|
79
|
+
global _color_cache # noqa: PLW0603
|
|
80
|
+
_color_cache = None
|
|
65
81
|
|
|
66
82
|
|
|
67
83
|
def _c(code: str, text: str) -> str:
|
|
@@ -169,6 +185,7 @@ def _debug_repl(*, step: bool = False) -> None:
|
|
|
169
185
|
step: When ``True``, the entry banner says "Step" instead of
|
|
170
186
|
"Breakpoint" to indicate the REPL was re-entered via step mode.
|
|
171
187
|
"""
|
|
188
|
+
_reset_color_cache()
|
|
172
189
|
try:
|
|
173
190
|
import readline as _readline # noqa: F401 — side-effect: enables history/arrow keys
|
|
174
191
|
except ImportError:
|
execsql/gui/tui.py
CHANGED
|
@@ -244,7 +244,31 @@ class MsgScreen(_BaseDialog):
|
|
|
244
244
|
|
|
245
245
|
|
|
246
246
|
class PauseScreen(_BaseDialog):
|
|
247
|
-
"""Pause dialog with optional countdown and Continue/Cancel buttons."""
|
|
247
|
+
"""Pause dialog with optional countdown progress bar and Continue/Cancel buttons."""
|
|
248
|
+
|
|
249
|
+
DEFAULT_CSS = (
|
|
250
|
+
_BaseDialog.DEFAULT_CSS
|
|
251
|
+
+ """
|
|
252
|
+
#countdown-container {
|
|
253
|
+
height: auto;
|
|
254
|
+
margin: 1 0;
|
|
255
|
+
}
|
|
256
|
+
#countdown-container ProgressBar {
|
|
257
|
+
width: 1fr;
|
|
258
|
+
}
|
|
259
|
+
#countdown-container Bar {
|
|
260
|
+
width: 1fr;
|
|
261
|
+
&> .bar--bar {
|
|
262
|
+
background: $primary 30%;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
#countdown-container .countdown-label {
|
|
266
|
+
text-align: center;
|
|
267
|
+
color: $text-muted;
|
|
268
|
+
width: 1fr;
|
|
269
|
+
}
|
|
270
|
+
"""
|
|
271
|
+
)
|
|
248
272
|
|
|
249
273
|
BINDINGS = [
|
|
250
274
|
*_BaseDialog.BINDINGS,
|
|
@@ -254,17 +278,50 @@ class PauseScreen(_BaseDialog):
|
|
|
254
278
|
def compose(self) -> ComposeResult:
|
|
255
279
|
title = self.args.get("title", "Pause")
|
|
256
280
|
message = self.args.get("message", "")
|
|
281
|
+
countdown = self.args.get("countdown")
|
|
257
282
|
with Container(id="dialog"):
|
|
258
283
|
yield Label(title, id="title")
|
|
259
284
|
yield Static(message, id="message")
|
|
285
|
+
if countdown is not None:
|
|
286
|
+
with Vertical(id="countdown-container"):
|
|
287
|
+
yield ProgressBar(
|
|
288
|
+
total=float(countdown),
|
|
289
|
+
show_percentage=False,
|
|
290
|
+
show_eta=False,
|
|
291
|
+
id="countdown-bar",
|
|
292
|
+
)
|
|
293
|
+
yield Static("", classes="countdown-label")
|
|
260
294
|
with Horizontal(id="buttons"):
|
|
261
295
|
yield Button("Cancel", id="btn_cancel_exit", variant="warning")
|
|
262
296
|
yield Button("Continue", id="btn_continue", variant="primary")
|
|
263
297
|
|
|
264
298
|
def on_mount(self) -> None:
|
|
299
|
+
import time
|
|
300
|
+
|
|
265
301
|
countdown = self.args.get("countdown")
|
|
266
302
|
if countdown is not None:
|
|
267
|
-
self.
|
|
303
|
+
self._countdown_total = float(countdown)
|
|
304
|
+
self._countdown_start = time.time()
|
|
305
|
+
self._tick_interval = self.set_interval(0.2, self._tick)
|
|
306
|
+
|
|
307
|
+
def _tick(self) -> None:
|
|
308
|
+
import time
|
|
309
|
+
|
|
310
|
+
elapsed = time.time() - self._countdown_start
|
|
311
|
+
remaining = max(0.0, self._countdown_total - elapsed)
|
|
312
|
+
progress = min(self._countdown_total, elapsed)
|
|
313
|
+
bar = self.query_one("#countdown-bar", ProgressBar)
|
|
314
|
+
bar.update(progress=progress)
|
|
315
|
+
label = self.query_one(".countdown-label", Static)
|
|
316
|
+
if remaining >= 60:
|
|
317
|
+
mins = int(remaining) // 60
|
|
318
|
+
secs = int(remaining) % 60
|
|
319
|
+
label.update(f"{mins}m {secs:02d}s remaining")
|
|
320
|
+
else:
|
|
321
|
+
label.update(f"{remaining:.0f}s remaining")
|
|
322
|
+
if remaining <= 0:
|
|
323
|
+
self._tick_interval.stop()
|
|
324
|
+
self._auto_continue()
|
|
268
325
|
|
|
269
326
|
def _auto_continue(self) -> None:
|
|
270
327
|
self._result = {"quit": False}
|
|
@@ -30,6 +30,24 @@ from execsql.utils.gui import gui_console_isrunning
|
|
|
30
30
|
from execsql.utils.strings import unquoted
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _quote_table_name(name: str) -> str:
|
|
34
|
+
"""Quote a potentially schema-qualified table name for safe SQL interpolation.
|
|
35
|
+
|
|
36
|
+
Splits on ``.`` and quotes each component with standard SQL double-quoting
|
|
37
|
+
(embedded double-quotes are escaped to ``""``).
|
|
38
|
+
|
|
39
|
+
Examples::
|
|
40
|
+
|
|
41
|
+
>>> _quote_table_name("books")
|
|
42
|
+
'"books"'
|
|
43
|
+
>>> _quote_table_name("staging.books")
|
|
44
|
+
'"staging"."books"'
|
|
45
|
+
>>> _quote_table_name('my"table')
|
|
46
|
+
'"my""table"'
|
|
47
|
+
"""
|
|
48
|
+
return ".".join('"' + part.replace('"', '""') + '"' for part in name.split("."))
|
|
49
|
+
|
|
50
|
+
|
|
33
51
|
def xf_contains(**kwargs: Any) -> bool:
|
|
34
52
|
s1 = kwargs["string1"]
|
|
35
53
|
s2 = kwargs["string2"]
|
|
@@ -59,7 +77,7 @@ def xf_endswith(**kwargs: Any) -> bool:
|
|
|
59
77
|
|
|
60
78
|
def xf_hasrows(**kwargs: Any) -> bool:
|
|
61
79
|
queryname = kwargs["queryname"]
|
|
62
|
-
sql = f"select count(*) from {queryname};"
|
|
80
|
+
sql = f"select count(*) from {_quote_table_name(queryname)};"
|
|
63
81
|
try:
|
|
64
82
|
hdrs, rec = _state.dbs.current().select_data(sql)
|
|
65
83
|
except ErrInfo:
|
|
@@ -84,7 +102,7 @@ def _row_count(queryname: str, sql_context: str, metacommandline: str) -> int:
|
|
|
84
102
|
Raises:
|
|
85
103
|
ErrInfo: If the query fails or the result is not numeric.
|
|
86
104
|
"""
|
|
87
|
-
sql = f"select count(*) from {queryname};"
|
|
105
|
+
sql = f"select count(*) from {_quote_table_name(queryname)};"
|
|
88
106
|
try:
|
|
89
107
|
_hdrs, rec = _state.dbs.current().select_data(sql)
|
|
90
108
|
except ErrInfo:
|
execsql/utils/gui.py
CHANGED
|
@@ -544,30 +544,98 @@ def get_yn_win(prompt: str) -> bool:
|
|
|
544
544
|
return get_yn(prompt)
|
|
545
545
|
|
|
546
546
|
|
|
547
|
+
def _clear_progress_line() -> None:
|
|
548
|
+
"""Erase the SIGALRM progress bar line and move to the next line."""
|
|
549
|
+
# \r returns to column 0, the spaces overwrite the bar, then \n advances.
|
|
550
|
+
sys.stdout.write("\r" + " " * 50 + "\r\n")
|
|
551
|
+
sys.stdout.flush()
|
|
552
|
+
|
|
553
|
+
|
|
547
554
|
def pause(
|
|
548
555
|
text: str,
|
|
549
556
|
action: str | None = None,
|
|
550
557
|
countdown: float | None = None,
|
|
551
558
|
timeunit: str | None = None,
|
|
552
559
|
) -> int:
|
|
553
|
-
"""Display a pause message and wait for the user.
|
|
560
|
+
"""Display a pause message and wait for the user (POSIX).
|
|
554
561
|
|
|
555
|
-
Returns 0 (user continued), 1 (user quit), or 2 (timed out).
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
562
|
+
Returns 0 (user continued), 1 (user quit/ESC), or 2 (timed out).
|
|
563
|
+
|
|
564
|
+
Uses raw-mode terminal reading so that single keypresses (Enter, Esc)
|
|
565
|
+
are detected without requiring the user to press Enter first. When
|
|
566
|
+
*countdown* is set, a SIGALRM-based progress bar is displayed via
|
|
567
|
+
:class:`~execsql.utils.timer.TimerHandler`.
|
|
560
568
|
|
|
569
|
+
Note: *countdown* is expected in **seconds** — the caller (``x_pause``)
|
|
570
|
+
has already converted minutes to seconds, so *timeunit* is not used here.
|
|
571
|
+
"""
|
|
561
572
|
print(f"\n{text}", file=sys.stderr)
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
573
|
+
|
|
574
|
+
# When stdin is not a real TTY (pytest, piped input, etc.) or the
|
|
575
|
+
# platform lacks POSIX terminal APIs (Windows), fall back to simple
|
|
576
|
+
# blocking behaviour.
|
|
577
|
+
if sys.platform == "win32" or not hasattr(sys.stdin, "fileno") or not sys.stdin.isatty():
|
|
578
|
+
if countdown is not None and action is not None:
|
|
579
|
+
import time
|
|
580
|
+
|
|
581
|
+
time.sleep(float(countdown))
|
|
582
|
+
return 2
|
|
583
|
+
try:
|
|
584
|
+
input("Press Enter to continue...")
|
|
585
|
+
except EOFError:
|
|
586
|
+
pass
|
|
587
|
+
return 0
|
|
588
|
+
|
|
589
|
+
# POSIX-only imports — guarded by the isatty() check above so this
|
|
590
|
+
# function can still be called safely on Windows (falls back above).
|
|
591
|
+
import signal
|
|
592
|
+
import termios
|
|
593
|
+
import tty
|
|
594
|
+
|
|
595
|
+
from execsql.exceptions import ExecSqlTimeoutError
|
|
596
|
+
from execsql.utils.timer import TimerHandler
|
|
597
|
+
|
|
598
|
+
if countdown is None or action is None:
|
|
599
|
+
sys.stderr.write("Press Enter to continue, Esc to quit...\n")
|
|
600
|
+
sys.stderr.flush()
|
|
601
|
+
|
|
602
|
+
fd = sys.stdin.fileno()
|
|
603
|
+
old_settings = termios.tcgetattr(fd)
|
|
604
|
+
|
|
605
|
+
timer_handler: TimerHandler | None = None
|
|
606
|
+
old_alarm = signal.SIG_DFL
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
tty.setraw(fd)
|
|
610
|
+
|
|
611
|
+
if countdown is not None and action is not None:
|
|
612
|
+
seconds = float(countdown)
|
|
613
|
+
timer_handler = TimerHandler(seconds)
|
|
614
|
+
old_alarm = signal.signal(signal.SIGALRM, timer_handler.alarm_handler)
|
|
615
|
+
signal.setitimer(signal.ITIMER_REAL, 0.01, 0.01)
|
|
616
|
+
|
|
617
|
+
has_bar = countdown is not None and action is not None
|
|
618
|
+
|
|
619
|
+
while True:
|
|
620
|
+
try:
|
|
621
|
+
ch = sys.stdin.read(1)
|
|
622
|
+
except ExecSqlTimeoutError:
|
|
623
|
+
# TimerHandler raised — countdown expired.
|
|
624
|
+
_clear_progress_line()
|
|
625
|
+
return 2 # timed out
|
|
626
|
+
if ch in ("\r", "\n"):
|
|
627
|
+
if has_bar:
|
|
628
|
+
_clear_progress_line()
|
|
629
|
+
return 0 # user continued
|
|
630
|
+
if ch == "\x1b":
|
|
631
|
+
if has_bar:
|
|
632
|
+
_clear_progress_line()
|
|
633
|
+
return 1 # user quit (ESC)
|
|
634
|
+
finally:
|
|
635
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
636
|
+
if timer_handler is not None:
|
|
637
|
+
signal.setitimer(signal.ITIMER_REAL, 0)
|
|
638
|
+
signal.signal(signal.SIGALRM, old_alarm)
|
|
571
639
|
|
|
572
640
|
|
|
573
641
|
def pause_win(
|
|
@@ -576,5 +644,59 @@ def pause_win(
|
|
|
576
644
|
countdown: float | None = None,
|
|
577
645
|
timeunit: str | None = None,
|
|
578
646
|
) -> int:
|
|
579
|
-
"""
|
|
580
|
-
|
|
647
|
+
"""Display a pause message and wait for the user (Windows).
|
|
648
|
+
|
|
649
|
+
Returns 0 (user continued), 1 (user quit/ESC), or 2 (timed out).
|
|
650
|
+
|
|
651
|
+
Uses ``msvcrt.kbhit()``/``msvcrt.getwch()`` for non-blocking single-char
|
|
652
|
+
reads with a polling loop that renders a text-mode progress bar when
|
|
653
|
+
*countdown* is set. Falls back to :func:`pause` on non-Windows platforms.
|
|
654
|
+
|
|
655
|
+
Note: *countdown* is expected in **seconds** — the caller (``x_pause``)
|
|
656
|
+
has already converted minutes to seconds, so *timeunit* is not used here.
|
|
657
|
+
"""
|
|
658
|
+
try:
|
|
659
|
+
import msvcrt
|
|
660
|
+
except ImportError:
|
|
661
|
+
# Not on Windows — delegate to the POSIX implementation.
|
|
662
|
+
return pause(text, action=action, countdown=countdown, timeunit=timeunit)
|
|
663
|
+
|
|
664
|
+
import time
|
|
665
|
+
|
|
666
|
+
print(f"\n{text}", file=sys.stderr)
|
|
667
|
+
|
|
668
|
+
if countdown is not None and action is not None:
|
|
669
|
+
seconds = float(countdown)
|
|
670
|
+
start = time.time()
|
|
671
|
+
bar_len = 30
|
|
672
|
+
while True:
|
|
673
|
+
elapsed = time.time() - start
|
|
674
|
+
remaining = max(0.0, seconds - elapsed)
|
|
675
|
+
if remaining <= 0:
|
|
676
|
+
_clear_progress_line()
|
|
677
|
+
return 2 # timed out
|
|
678
|
+
bar_left = int(round(bar_len * remaining / seconds, 0))
|
|
679
|
+
sys.stdout.write(
|
|
680
|
+
"{:8.1f} |{}{}|\r".format(remaining, "+" * bar_left, "-" * (bar_len - bar_left)),
|
|
681
|
+
)
|
|
682
|
+
sys.stdout.flush()
|
|
683
|
+
if msvcrt.kbhit():
|
|
684
|
+
ch = msvcrt.getwch()
|
|
685
|
+
if ch in ("\r", "\n"):
|
|
686
|
+
_clear_progress_line()
|
|
687
|
+
return 0
|
|
688
|
+
if ch == "\x1b":
|
|
689
|
+
_clear_progress_line()
|
|
690
|
+
return 1
|
|
691
|
+
time.sleep(0.01)
|
|
692
|
+
else:
|
|
693
|
+
sys.stderr.write("Press Enter to continue, Esc to quit...\n")
|
|
694
|
+
sys.stderr.flush()
|
|
695
|
+
while True:
|
|
696
|
+
if msvcrt.kbhit():
|
|
697
|
+
ch = msvcrt.getwch()
|
|
698
|
+
if ch in ("\r", "\n"):
|
|
699
|
+
return 0
|
|
700
|
+
if ch == "\x1b":
|
|
701
|
+
return 1
|
|
702
|
+
time.sleep(0.01)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.15.
|
|
3
|
+
Version: 2.15.11
|
|
4
4
|
Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
|
|
5
5
|
Project-URL: Homepage, https://execsql2.readthedocs.io
|
|
6
6
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
@@ -240,6 +240,7 @@ execsql script.sql # read connection from config file
|
|
|
240
240
|
| `--ping` | Test database connectivity and exit |
|
|
241
241
|
| `--profile` | Show per-statement timing summary after execution |
|
|
242
242
|
| `--progress` | Show a progress bar for long-running IMPORT operations |
|
|
243
|
+
| `--config FILE` | Load an explicit config file (highest priority after CLI args) |
|
|
243
244
|
| `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
|
|
244
245
|
| `--dump-keywords` | Print metacommand keywords as JSON and exit |
|
|
245
246
|
| `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
execsql/__init__.py,sha256=
|
|
1
|
+
execsql/__init__.py,sha256=zinIcvuXFile66yk4JKvD1NO3QqxR5MoSHXBHeRMRC0,515
|
|
2
2
|
execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
|
|
3
|
-
execsql/config.py,sha256=
|
|
3
|
+
execsql/config.py,sha256=YkLEtyLfjfihEMmAXM9Bkz2h_AkCh7CroWwlAqtmR2M,29083
|
|
4
4
|
execsql/exceptions.py,sha256=j8hykBiof9H3Za9hwLIbDcVB2Xn65ODXXplp1jkvdgM,8453
|
|
5
5
|
execsql/format.py,sha256=-6iknDddqbkapMo4NKmT5LAynDLqMW5kHgDWRg0KSws,11990
|
|
6
6
|
execsql/models.py,sha256=xXhMfycetIBGLhrEWwz2Witn3k7rdgMok73HSdTLSCw,13445
|
|
@@ -8,14 +8,14 @@ execsql/parser.py,sha256=P3ea8k7T_XLMrbhpFNZXwytdShrY302MKnhosqza1lo,15493
|
|
|
8
8
|
execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
execsql/state.py,sha256=gX7By2R9Ytxbgy3QFG69DJa8ebO-0NSpYNpCYCrnO1w,15258
|
|
10
10
|
execsql/types.py,sha256=5K3aTuWQZHftz5slFODwqxlcvvt6lROISUnvUtRUazs,31799
|
|
11
|
-
execsql/cli/__init__.py,sha256=
|
|
11
|
+
execsql/cli/__init__.py,sha256=jYw5J88e_vOthmEyzsNxxzkRf7xc7CphLixqBrUjr_c,17115
|
|
12
12
|
execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
|
|
13
13
|
execsql/cli/help.py,sha256=Sn_TgSJiQeBx-xZH0fuP5OvR_wasSTumjWF9UHfIX5k,5414
|
|
14
14
|
execsql/cli/lint.py,sha256=mVZu3Knp13-yQZkofiaeDmxLTWP7DkbaZoDeHPsfAEQ,24094
|
|
15
|
-
execsql/cli/run.py,sha256=
|
|
15
|
+
execsql/cli/run.py,sha256=HXOkqCI5Qyve8_c3OeHuB2RylmgS0iF5gUSPl-Zn5do,30824
|
|
16
16
|
execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
|
|
17
17
|
execsql/db/access.py,sha256=v0GKN4yE30r-A1gcv224jZhj0JmmRTmGSuz_KH11iY4,18119
|
|
18
|
-
execsql/db/base.py,sha256
|
|
18
|
+
execsql/db/base.py,sha256=-el1s7YrxHkdy0lFx86BKZ8mBXWD-1EvE44e5ZhD5KI,31322
|
|
19
19
|
execsql/db/dsn.py,sha256=sMXHOCVPZMMq52ReF6AywmWfyZjkQPmOIDXiIe_PGD4,5400
|
|
20
20
|
execsql/db/duckdb.py,sha256=pkmjD89gkbl0sHfcGpowx4X7PGTEpg-CoTpnzHR0Cu0,3174
|
|
21
21
|
execsql/db/factory.py,sha256=YR1m_c2Hhj_GXVGGkWoSEPZBpgNu_c4FxRnbp-xV3rs,5230
|
|
@@ -26,7 +26,7 @@ execsql/db/postgres.py,sha256=Oq881tnKgOzO0nbRmzyM_Ym8twu7QlPsRjXpHz20wpk,21012
|
|
|
26
26
|
execsql/db/sqlite.py,sha256=0aPomjNsyXww6lY039GuZufTp2T4Eyw8wdJjmXebNsE,10378
|
|
27
27
|
execsql/db/sqlserver.py,sha256=XbhprA1URZYPmSIMHEAGNdCbxZBs3yy5Kb6IvDxbACU,7597
|
|
28
28
|
execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
|
|
29
|
-
execsql/debug/repl.py,sha256=
|
|
29
|
+
execsql/debug/repl.py,sha256=ngUVuQjey7TilSMumMqOouUhzx3Zx7W5rKCqZ5XoLnY,17227
|
|
30
30
|
execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
|
|
31
31
|
execsql/exporters/base.py,sha256=W9USFyk_2eztjJ51X6CJh7-chE1i3cSx-STOtbHXCNI,6373
|
|
32
32
|
execsql/exporters/delimited.py,sha256=URvEQo1IRF_tfdVHL3uBwEonihC-XfDm0f1argQPf4M,32088
|
|
@@ -53,7 +53,7 @@ execsql/gui/__init__.py,sha256=oCb-cyhLZzVpWJ4WU5HbqEDBrV-lm0ytEwxemrOZyqs,2048
|
|
|
53
53
|
execsql/gui/base.py,sha256=15wCaSLvSb21hToCczru46ghmd56ryMpEOOZ0R_dE3U,12740
|
|
54
54
|
execsql/gui/console.py,sha256=pCBUcFGjlKXMkMjztbmt9glP3me9jAKAgQxnmUE38-0,19396
|
|
55
55
|
execsql/gui/desktop.py,sha256=Cz4Epr2H4tXMbjGVda0VcVYjEmtW6S394GBSCdsfMJw,54708
|
|
56
|
-
execsql/gui/tui.py,sha256=
|
|
56
|
+
execsql/gui/tui.py,sha256=INBCuW6iEEV15P_JrHeviI1JlNCXfoiyPBU-IC5MaLY,65200
|
|
57
57
|
execsql/importers/__init__.py,sha256=dDsxSVeQYXBvm9yGqN3QswyGbLWTwt08pvUuRJgZhl4,289
|
|
58
58
|
execsql/importers/base.py,sha256=FGVz3ntN6xHL99rQixlQj3tAf570K_oU83UtbYE1lJg,4124
|
|
59
59
|
execsql/importers/csv.py,sha256=GUVRP294vHlOlF8XNecPEzatUBOOFIqnrEV9cBQkiv0,4849
|
|
@@ -62,7 +62,7 @@ execsql/importers/json.py,sha256=Z7QJZQ9fyqaxFxjfqcfZoaoW2GSZt-DMqW5LZiEfyNs,468
|
|
|
62
62
|
execsql/importers/ods.py,sha256=MJsdsjropzCvxAA3DDZfAL_AnmZ4yij7DnrjGyDJqHQ,2843
|
|
63
63
|
execsql/importers/xls.py,sha256=e0Zfe47ZiCpA1Ae3XDJ1ko3sCiH3-8U6XLKi6NvD0jQ,3683
|
|
64
64
|
execsql/metacommands/__init__.py,sha256=3Kz-VasFS9B-C-UdHOjr3RMXjheMeYHe6qYBwp5e7wE,11434
|
|
65
|
-
execsql/metacommands/conditions.py,sha256=
|
|
65
|
+
execsql/metacommands/conditions.py,sha256=B6fBumkqoPO4wcQbw_ypYITaSnzPemAA1g5GrNLNfD0,29204
|
|
66
66
|
execsql/metacommands/connect.py,sha256=U17ByjVmwrLN5DB7Ea9lTLyFJr8On2c9LfPO1Wc4l4U,14890
|
|
67
67
|
execsql/metacommands/control.py,sha256=PAZFK1ck5SDSm5QdFV1ctif3KpEiyYWIXdDceRWgQ6k,8513
|
|
68
68
|
execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,11370
|
|
@@ -87,30 +87,30 @@ execsql/utils/crypto.py,sha256=2OnBWwn9bCBGc1ZkyRv16TvhottoCNYtXqgbE3mG3Sg,2960
|
|
|
87
87
|
execsql/utils/datetime.py,sha256=rMCXAbvj6bxKCYzC97vrludO6PU5DYQ39buZ0smDC5A,3573
|
|
88
88
|
execsql/utils/errors.py,sha256=cIYU7mKgjrRy5D2D4aDFHsH__lToz4Nv3fLQ62R1V2M,7786
|
|
89
89
|
execsql/utils/fileio.py,sha256=3HDm6QFGeXwFMmCsbea4H5Ub6u2Jrx-oNsWA3Q5aytQ,23860
|
|
90
|
-
execsql/utils/gui.py,sha256=
|
|
90
|
+
execsql/utils/gui.py,sha256=h_7V2zYfp0l6C3Ft8QtUJAPgxCUrFkgIqwimz5s1bbQ,22821
|
|
91
91
|
execsql/utils/mail.py,sha256=Sd7vWj-dz3w0XDSFU9PM8gmy41pojk-Vsgbfven2DMk,5786
|
|
92
92
|
execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
|
|
93
93
|
execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
|
|
94
94
|
execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
|
|
95
95
|
execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
|
|
96
|
-
execsql2-2.15.
|
|
97
|
-
execsql2-2.15.
|
|
98
|
-
execsql2-2.15.
|
|
99
|
-
execsql2-2.15.
|
|
100
|
-
execsql2-2.15.
|
|
101
|
-
execsql2-2.15.
|
|
102
|
-
execsql2-2.15.
|
|
103
|
-
execsql2-2.15.
|
|
104
|
-
execsql2-2.15.
|
|
105
|
-
execsql2-2.15.
|
|
106
|
-
execsql2-2.15.
|
|
107
|
-
execsql2-2.15.
|
|
108
|
-
execsql2-2.15.
|
|
109
|
-
execsql2-2.15.
|
|
110
|
-
execsql2-2.15.
|
|
111
|
-
execsql2-2.15.
|
|
112
|
-
execsql2-2.15.
|
|
113
|
-
execsql2-2.15.
|
|
114
|
-
execsql2-2.15.
|
|
115
|
-
execsql2-2.15.
|
|
116
|
-
execsql2-2.15.
|
|
96
|
+
execsql2-2.15.11.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
|
|
97
|
+
execsql2-2.15.11.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
98
|
+
execsql2-2.15.11.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
|
|
99
|
+
execsql2-2.15.11.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
|
|
100
|
+
execsql2-2.15.11.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
101
|
+
execsql2-2.15.11.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
|
|
102
|
+
execsql2-2.15.11.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
|
|
103
|
+
execsql2-2.15.11.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
|
|
104
|
+
execsql2-2.15.11.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
|
|
105
|
+
execsql2-2.15.11.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
|
|
106
|
+
execsql2-2.15.11.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
|
|
107
|
+
execsql2-2.15.11.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
|
|
108
|
+
execsql2-2.15.11.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
|
|
109
|
+
execsql2-2.15.11.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
|
|
110
|
+
execsql2-2.15.11.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
|
|
111
|
+
execsql2-2.15.11.dist-info/METADATA,sha256=ClzgnoVBb7i4U2HC5k3bAycug19JGF6loGfMHDrzXJA,18221
|
|
112
|
+
execsql2-2.15.11.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
113
|
+
execsql2-2.15.11.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
114
|
+
execsql2-2.15.11.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
115
|
+
execsql2-2.15.11.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
|
|
116
|
+
execsql2-2.15.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{execsql2-2.15.8.data → execsql2-2.15.11.data}/data/execsql2_extras/example_config_prompt.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|