execsql2 2.21.1__py3-none-any.whl → 2.22.0__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/api.py +21 -4
- execsql/cli/run.py +37 -17
- execsql/config.py +37 -22
- execsql/data/execsql.conf.template +6 -0
- execsql/db/access.py +6 -5
- execsql/db/base.py +23 -5
- execsql/db/dsn.py +2 -0
- execsql/db/duckdb.py +1 -1
- execsql/db/mysql.py +6 -8
- execsql/db/sqlite.py +1 -1
- execsql/exceptions.py +14 -11
- execsql/exporters/base.py +1 -1
- execsql/exporters/delimited.py +22 -16
- execsql/exporters/duckdb.py +6 -1
- execsql/exporters/feather.py +2 -2
- execsql/exporters/json.py +5 -3
- execsql/exporters/latex.py +2 -2
- execsql/exporters/markdown.py +1 -0
- execsql/exporters/ods.py +7 -3
- execsql/exporters/parquet.py +1 -1
- execsql/exporters/raw.py +8 -8
- execsql/exporters/sqlite.py +11 -1
- execsql/exporters/templates.py +2 -1
- execsql/exporters/xls.py +10 -10
- execsql/exporters/xml.py +1 -0
- execsql/exporters/yaml.py +1 -0
- execsql/exporters/zip.py +9 -7
- execsql/format.py +6 -6
- execsql/gui/console.py +3 -3
- execsql/gui/desktop.py +28 -10
- execsql/gui/tui.py +8 -6
- execsql/importers/base.py +14 -8
- execsql/importers/csv.py +8 -3
- execsql/importers/xls.py +1 -0
- execsql/metacommands/conditions.py +14 -14
- execsql/metacommands/connect.py +2 -2
- execsql/metacommands/control.py +1 -1
- execsql/metacommands/data.py +2 -1
- execsql/metacommands/io_export.py +4 -0
- execsql/metacommands/io_fileops.py +85 -39
- execsql/metacommands/io_import.py +1 -0
- execsql/metacommands/io_write.py +1 -1
- execsql/metacommands/prompt.py +24 -22
- execsql/metacommands/system.py +14 -1
- execsql/models.py +56 -17
- execsql/parser.py +8 -8
- execsql/plugins.py +1 -1
- execsql/script/engine.py +4 -4
- execsql/script/executor.py +67 -28
- execsql/script/parser.py +13 -12
- execsql/script/variables.py +4 -2
- execsql/state.pyi +122 -0
- execsql/types.py +31 -26
- execsql/utils/auth.py +6 -2
- execsql/utils/datetime.py +3 -3
- execsql/utils/errors.py +7 -5
- execsql/utils/fileio.py +77 -31
- execsql/utils/gui.py +15 -11
- execsql/utils/mail.py +9 -2
- execsql/utils/numeric.py +1 -1
- execsql/utils/regex.py +9 -9
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/METADATA +3 -3
- execsql2-2.22.0.dist-info/RECORD +123 -0
- execsql2-2.21.1.dist-info/RECORD +0 -122
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/WHEEL +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/licenses/NOTICE +0 -0
execsql/api.py
CHANGED
|
@@ -35,7 +35,7 @@ import io
|
|
|
35
35
|
import os
|
|
36
36
|
import time
|
|
37
37
|
from pathlib import Path
|
|
38
|
-
from typing import Any
|
|
38
|
+
from typing import Any, cast
|
|
39
39
|
|
|
40
40
|
from execsql.cli.dsn import _parse_connection_string
|
|
41
41
|
from execsql.config import ConfigData, StatObj, WriteHooks
|
|
@@ -170,6 +170,7 @@ class _LibraryConfig:
|
|
|
170
170
|
self.log_datavars = False
|
|
171
171
|
self.show_progress = False
|
|
172
172
|
self.max_log_size_mb = 0
|
|
173
|
+
self.system_cmd_timeout = 0.0
|
|
173
174
|
|
|
174
175
|
# Data handling
|
|
175
176
|
self.boolean_int = True
|
|
@@ -334,6 +335,8 @@ def run(
|
|
|
334
335
|
halt_on_error: bool = True,
|
|
335
336
|
new_db: bool = False,
|
|
336
337
|
allow_system_cmd: bool = True,
|
|
338
|
+
allow_rm_file: bool = True,
|
|
339
|
+
allow_serve: bool = True,
|
|
337
340
|
) -> ScriptResult:
|
|
338
341
|
"""Execute a SQL script and return the result.
|
|
339
342
|
|
|
@@ -359,6 +362,13 @@ def run(
|
|
|
359
362
|
disabled and will raise an error if encountered. Matches the
|
|
360
363
|
``--no-system-cmd`` CLI flag and the ``allow_system_cmd``
|
|
361
364
|
config option.
|
|
365
|
+
allow_rm_file: If ``False``, the RM_FILE metacommand is disabled
|
|
366
|
+
and will raise an error if encountered. Matches the
|
|
367
|
+
``--no-rm-file`` CLI flag and the ``allow_rm_file`` config
|
|
368
|
+
option.
|
|
369
|
+
allow_serve: If ``False``, the SERVE metacommand is disabled and
|
|
370
|
+
will raise an error if encountered. Matches the ``--no-serve``
|
|
371
|
+
CLI flag and the ``allow_serve`` config option.
|
|
362
372
|
|
|
363
373
|
Returns:
|
|
364
374
|
A :class:`ScriptResult` with execution outcome, timing, errors,
|
|
@@ -391,6 +401,7 @@ def run(
|
|
|
391
401
|
if script is not None:
|
|
392
402
|
tree = parse_script(str(script), encoding=encoding)
|
|
393
403
|
else:
|
|
404
|
+
assert sql is not None
|
|
394
405
|
tree = parse_string(sql, source_name="<inline>")
|
|
395
406
|
except ErrInfo as exc:
|
|
396
407
|
return ScriptResult(
|
|
@@ -408,6 +419,7 @@ def run(
|
|
|
408
419
|
|
|
409
420
|
# Configuration
|
|
410
421
|
conf_overrides: dict[str, Any] = {"script_encoding": encoding}
|
|
422
|
+
conf: Any
|
|
411
423
|
if config_file is not None:
|
|
412
424
|
# Load a real ConfigData with the explicit config file
|
|
413
425
|
from execsql.script.variables import SubVarSet
|
|
@@ -457,6 +469,8 @@ def run(
|
|
|
457
469
|
ctx.status = StatObj()
|
|
458
470
|
ctx.status.halt_on_err = halt_on_error
|
|
459
471
|
conf.allow_system_cmd = allow_system_cmd
|
|
472
|
+
conf.allow_rm_file = allow_rm_file
|
|
473
|
+
conf.allow_serve = allow_serve
|
|
460
474
|
ctx.conf = conf
|
|
461
475
|
|
|
462
476
|
# Capture output to a buffer (suppress stdout/stderr)
|
|
@@ -465,7 +479,7 @@ def run(
|
|
|
465
479
|
ctx.output = WriteHooks(stdout_buf.write, stderr_buf.write)
|
|
466
480
|
|
|
467
481
|
# No log file for library use
|
|
468
|
-
ctx.exec_log = _NoOpLogger()
|
|
482
|
+
ctx.exec_log = cast(Any, _NoOpLogger())
|
|
469
483
|
|
|
470
484
|
with active_context(ctx):
|
|
471
485
|
# Initialize singletons (CounterVars, Timer, DatabasePool, BatchLevels, etc.)
|
|
@@ -482,6 +496,8 @@ def run(
|
|
|
482
496
|
else:
|
|
483
497
|
db = connection
|
|
484
498
|
|
|
499
|
+
assert db is not None
|
|
500
|
+
assert ctx.dbs is not None
|
|
485
501
|
ctx.dbs.add("initial", db)
|
|
486
502
|
ctx.subvars.add_substitution("$CURRENT_DBMS", db.type.dbms_id)
|
|
487
503
|
ctx.subvars.add_substitution("$CURRENT_DATABASE", db.name())
|
|
@@ -530,6 +546,7 @@ def run(
|
|
|
530
546
|
# Close connection if we own it
|
|
531
547
|
if owns_connection:
|
|
532
548
|
try:
|
|
549
|
+
assert ctx.dbs is not None
|
|
533
550
|
ctx.dbs.closeall()
|
|
534
551
|
except Exception:
|
|
535
552
|
pass
|
|
@@ -572,7 +589,7 @@ def _last_source(ctx: RuntimeContext) -> str:
|
|
|
572
589
|
"""Get the source file from the last executed command."""
|
|
573
590
|
lc = ctx.last_command
|
|
574
591
|
if lc is not None and hasattr(lc, "source"):
|
|
575
|
-
return lc.source
|
|
592
|
+
return cast(str, lc.source)
|
|
576
593
|
return "<unknown>"
|
|
577
594
|
|
|
578
595
|
|
|
@@ -580,7 +597,7 @@ def _last_line(ctx: RuntimeContext) -> int | None:
|
|
|
580
597
|
"""Get the line number from the last executed command."""
|
|
581
598
|
lc = ctx.last_command
|
|
582
599
|
if lc is not None and hasattr(lc, "line_no"):
|
|
583
|
-
return lc.line_no
|
|
600
|
+
return cast(int | None, lc.line_no)
|
|
584
601
|
return None
|
|
585
602
|
|
|
586
603
|
|
execsql/cli/run.py
CHANGED
|
@@ -7,15 +7,15 @@ and drives the main execution loop. Separated from argument parsing
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import atexit
|
|
11
|
-
from typing import Any
|
|
12
10
|
import datetime
|
|
11
|
+
import atexit
|
|
13
12
|
import getpass
|
|
14
13
|
import os
|
|
15
14
|
import platform
|
|
16
15
|
import sys
|
|
17
16
|
import traceback
|
|
18
17
|
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
19
|
|
|
20
20
|
from execsql import __version__
|
|
21
21
|
from execsql.cli.dsn import _parse_connection_string
|
|
@@ -26,6 +26,9 @@ from execsql.script import SubVarSet, current_script_line, substitute_vars
|
|
|
26
26
|
from execsql.utils.fileio import FileWriter, Logger, filewriter_end
|
|
27
27
|
from execsql.utils.gui import gui_connect, gui_console_isrunning, gui_console_off, gui_console_on, gui_console_wait_user
|
|
28
28
|
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from execsql.script.ast import Script
|
|
31
|
+
|
|
29
32
|
__all__ = ["_connect_initial_db", "_ping_db", "_print_dry_run", "_print_profile", "_run"]
|
|
30
33
|
|
|
31
34
|
# Lint helper — imported lazily inside _run() to keep start-up cost low, but
|
|
@@ -38,7 +41,7 @@ from execsql.cli.lint import _print_lint_results # noqa: F401 — re-export (us
|
|
|
38
41
|
# ---------------------------------------------------------------------------
|
|
39
42
|
|
|
40
43
|
|
|
41
|
-
def _print_dry_run(tree:
|
|
44
|
+
def _print_dry_run(tree: Script | None) -> None:
|
|
42
45
|
"""Print the parsed AST for --dry-run mode.
|
|
43
46
|
|
|
44
47
|
Walks the AST tree and prints each SQL statement and metacommand with
|
|
@@ -304,7 +307,7 @@ def _load_script(
|
|
|
304
307
|
command: str | None,
|
|
305
308
|
script_name: str | None,
|
|
306
309
|
encoding: str,
|
|
307
|
-
) ->
|
|
310
|
+
) -> Script | None:
|
|
308
311
|
"""Parse the SQL script (or inline command) into an AST.
|
|
309
312
|
|
|
310
313
|
Returns the AST tree, or ``None`` if no script is provided.
|
|
@@ -316,6 +319,8 @@ def _load_script(
|
|
|
316
319
|
command.replace("\\n", "\n").replace("\\t", "\t"),
|
|
317
320
|
"<inline>",
|
|
318
321
|
)
|
|
322
|
+
if script_name is None:
|
|
323
|
+
return None
|
|
319
324
|
return parse_script(script_name, encoding=encoding)
|
|
320
325
|
|
|
321
326
|
|
|
@@ -515,7 +520,7 @@ def _setup_logging(
|
|
|
515
520
|
}
|
|
516
521
|
logger = Logger(
|
|
517
522
|
script_name or "<inline>",
|
|
518
|
-
conf.db,
|
|
523
|
+
conf.db or "",
|
|
519
524
|
conf.server,
|
|
520
525
|
opts_dict,
|
|
521
526
|
conf.user_logfile,
|
|
@@ -537,6 +542,7 @@ def _setup_logging(
|
|
|
537
542
|
for n, repl in enumerate(sub_vars):
|
|
538
543
|
var = f"$ARG_{n + 1}"
|
|
539
544
|
subvars.add_substitution(var, repl)
|
|
545
|
+
logger.add_redaction_value(repl)
|
|
540
546
|
# `-a` values are positional and opaque (no name to denylist
|
|
541
547
|
# against). High-entropy secrets (sk-live-*, AKIA*, ghp_*,
|
|
542
548
|
# JWTs) pass any substring/value heuristic, so log the
|
|
@@ -898,8 +904,9 @@ def _execute_script_textual_console(tree: Any, conf: ConfigData) -> None:
|
|
|
898
904
|
dialog_queue = _ConsoleDialogQueue()
|
|
899
905
|
_state.gui_manager_queue = dialog_queue
|
|
900
906
|
|
|
907
|
+
ctx = _state.get_context()
|
|
901
908
|
app = ConsoleApp(
|
|
902
|
-
script_runner=lambda: execute(tree),
|
|
909
|
+
script_runner=lambda: execute(tree, ctx=ctx),
|
|
903
910
|
dialog_queue=dialog_queue,
|
|
904
911
|
wait_on_exit=conf.gui_wait_on_exit,
|
|
905
912
|
)
|
|
@@ -945,6 +952,16 @@ def _execute_script_textual_console(tree: Any, conf: ConfigData) -> None:
|
|
|
945
952
|
# ---------------------------------------------------------------------------
|
|
946
953
|
|
|
947
954
|
|
|
955
|
+
def _required_connection_value(value: str | None, description: str) -> str:
|
|
956
|
+
"""Return a required connection value or stop with the standard fatal path."""
|
|
957
|
+
if value is None:
|
|
958
|
+
from execsql.utils.errors import fatal_error
|
|
959
|
+
|
|
960
|
+
fatal_error(f"Configured database requires {description}, but none is provided.")
|
|
961
|
+
raise SystemExit(1)
|
|
962
|
+
return value
|
|
963
|
+
|
|
964
|
+
|
|
948
965
|
def _connect_initial_db(conf: ConfigData):
|
|
949
966
|
"""Create and return the initial database object based on conf.db_type."""
|
|
950
967
|
from execsql.db.factory import (
|
|
@@ -964,6 +981,7 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
964
981
|
from execsql.utils.errors import fatal_error
|
|
965
982
|
|
|
966
983
|
fatal_error("Configured to run with MS-Access, but no Access file name is provided.")
|
|
984
|
+
raise SystemExit(1)
|
|
967
985
|
return db_Access(
|
|
968
986
|
conf.db_file,
|
|
969
987
|
pw_needed=conf.passwd_prompt and conf.access_username is not None,
|
|
@@ -972,8 +990,8 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
972
990
|
)
|
|
973
991
|
elif conf.db_type == "p":
|
|
974
992
|
return db_Postgres(
|
|
975
|
-
conf.server,
|
|
976
|
-
conf.db,
|
|
993
|
+
_required_connection_value(conf.server, "a server name"),
|
|
994
|
+
_required_connection_value(conf.db, "a database name"),
|
|
977
995
|
user=conf.username,
|
|
978
996
|
pw_needed=conf.passwd_prompt,
|
|
979
997
|
port=conf.port,
|
|
@@ -983,8 +1001,8 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
983
1001
|
)
|
|
984
1002
|
elif conf.db_type == "s":
|
|
985
1003
|
return db_SqlServer(
|
|
986
|
-
conf.server,
|
|
987
|
-
conf.db,
|
|
1004
|
+
_required_connection_value(conf.server, "a server name"),
|
|
1005
|
+
_required_connection_value(conf.db, "a database name"),
|
|
988
1006
|
user=conf.username,
|
|
989
1007
|
pw_needed=conf.passwd_prompt,
|
|
990
1008
|
port=conf.port,
|
|
@@ -996,11 +1014,12 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
996
1014
|
from execsql.utils.errors import fatal_error
|
|
997
1015
|
|
|
998
1016
|
fatal_error("Configured to run with SQLite, but no SQLite file name is provided.")
|
|
1017
|
+
raise SystemExit(1)
|
|
999
1018
|
return db_SQLite(conf.db_file, new_db=conf.new_db, encoding=conf.db_encoding)
|
|
1000
1019
|
elif conf.db_type == "m":
|
|
1001
1020
|
return db_MySQL(
|
|
1002
|
-
conf.server,
|
|
1003
|
-
conf.db,
|
|
1021
|
+
_required_connection_value(conf.server, "a server name"),
|
|
1022
|
+
_required_connection_value(conf.db, "a database name"),
|
|
1004
1023
|
user=conf.username,
|
|
1005
1024
|
pw_needed=conf.passwd_prompt,
|
|
1006
1025
|
port=conf.port,
|
|
@@ -1012,11 +1031,12 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
1012
1031
|
from execsql.utils.errors import fatal_error
|
|
1013
1032
|
|
|
1014
1033
|
fatal_error("Configured to run with DuckDB, but no DuckDB file name is provided.")
|
|
1034
|
+
raise SystemExit(1)
|
|
1015
1035
|
return db_DuckDB(conf.db_file, new_db=conf.new_db, encoding=conf.db_encoding)
|
|
1016
1036
|
elif conf.db_type == "o":
|
|
1017
1037
|
return db_Oracle(
|
|
1018
|
-
conf.server,
|
|
1019
|
-
conf.db,
|
|
1038
|
+
_required_connection_value(conf.server, "a server name"),
|
|
1039
|
+
_required_connection_value(conf.db, "a database name"),
|
|
1020
1040
|
user=conf.username,
|
|
1021
1041
|
pw_needed=conf.passwd_prompt,
|
|
1022
1042
|
port=conf.port,
|
|
@@ -1025,8 +1045,8 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
1025
1045
|
)
|
|
1026
1046
|
elif conf.db_type == "f":
|
|
1027
1047
|
return db_Firebird(
|
|
1028
|
-
conf.server,
|
|
1029
|
-
conf.db,
|
|
1048
|
+
_required_connection_value(conf.server, "a server name"),
|
|
1049
|
+
_required_connection_value(conf.db, "a database name"),
|
|
1030
1050
|
user=conf.username,
|
|
1031
1051
|
pw_needed=conf.passwd_prompt,
|
|
1032
1052
|
port=conf.port,
|
|
@@ -1035,7 +1055,7 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
1035
1055
|
)
|
|
1036
1056
|
elif conf.db_type == "d":
|
|
1037
1057
|
return db_Dsn(
|
|
1038
|
-
conf.db,
|
|
1058
|
+
_required_connection_value(conf.db, "a DSN name"),
|
|
1039
1059
|
user=conf.username,
|
|
1040
1060
|
pw_needed=conf.passwd_prompt,
|
|
1041
1061
|
encoding=conf.db_encoding,
|
execsql/config.py
CHANGED
|
@@ -17,8 +17,10 @@ Provides three classes:
|
|
|
17
17
|
|
|
18
18
|
import os
|
|
19
19
|
import sys
|
|
20
|
+
from collections.abc import Callable
|
|
20
21
|
from configparser import ConfigParser
|
|
21
22
|
from pathlib import Path
|
|
23
|
+
from typing import Protocol
|
|
22
24
|
|
|
23
25
|
from execsql.exceptions import ConfigError
|
|
24
26
|
from execsql.utils.crypto import Encrypt
|
|
@@ -30,6 +32,12 @@ __all__ = [
|
|
|
30
32
|
]
|
|
31
33
|
|
|
32
34
|
|
|
35
|
+
class _VariablePool(Protocol):
|
|
36
|
+
def substitute(self, strval: str) -> tuple[str, int]: ...
|
|
37
|
+
def var_name_ok(self, varname: str) -> bool: ...
|
|
38
|
+
def add_substitution(self, varname: str, strval: object) -> None: ...
|
|
39
|
+
|
|
40
|
+
|
|
33
41
|
class StatObj:
|
|
34
42
|
"""Lightweight container for runtime status flags used by the metacommand engine.
|
|
35
43
|
|
|
@@ -315,6 +323,7 @@ class ConfigData:
|
|
|
315
323
|
self._get_bool(cp, self._CONFIG_SECTION, "log_sql", "log_sql")
|
|
316
324
|
self._get_int(cp, self._CONFIG_SECTION, "max_log_size_mb", "max_log_size_mb")
|
|
317
325
|
self._get_bool(cp, self._CONFIG_SECTION, "allow_system_cmd", "allow_system_cmd")
|
|
326
|
+
self._get_float(cp, self._CONFIG_SECTION, "system_cmd_timeout", "system_cmd_timeout", min_val=0.0)
|
|
318
327
|
self._get_bool(cp, self._CONFIG_SECTION, "allow_rm_file", "allow_rm_file")
|
|
319
328
|
self._get_bool(cp, self._CONFIG_SECTION, "allow_serve", "allow_serve")
|
|
320
329
|
self._get_str(cp, self._CONFIG_SECTION, "include_root", "include_root")
|
|
@@ -344,7 +353,7 @@ class ConfigData:
|
|
|
344
353
|
def __init__(
|
|
345
354
|
self,
|
|
346
355
|
script_path: str,
|
|
347
|
-
variable_pool:
|
|
356
|
+
variable_pool: _VariablePool,
|
|
348
357
|
*,
|
|
349
358
|
config_file: str | None = None,
|
|
350
359
|
) -> None:
|
|
@@ -361,22 +370,23 @@ class ConfigData:
|
|
|
361
370
|
precedence over system, user, script, and working-directory
|
|
362
371
|
config files.
|
|
363
372
|
"""
|
|
364
|
-
self.db_type = "l"
|
|
365
|
-
self.server = None
|
|
366
|
-
self.port = None
|
|
367
|
-
self.db = None
|
|
368
|
-
self.username = None
|
|
369
|
-
self.access_username = None
|
|
373
|
+
self.db_type: str | None = "l"
|
|
374
|
+
self.server: str | None = None
|
|
375
|
+
self.port: int | None = None
|
|
376
|
+
self.db: str | None = None
|
|
377
|
+
self.username: str | None = None
|
|
378
|
+
self.access_username: str | None = None
|
|
379
|
+
self.db_password: str | None = None
|
|
370
380
|
self.passwd_prompt = True
|
|
371
381
|
self.use_keyring = True
|
|
372
|
-
self.db_file = None
|
|
382
|
+
self.db_file: str | None = None
|
|
373
383
|
self.new_db = False
|
|
374
384
|
self.user_logfile = False
|
|
375
|
-
self.db_encoding = None
|
|
385
|
+
self.db_encoding: str | None = None
|
|
376
386
|
self.script_encoding = "utf8"
|
|
377
387
|
self.output_encoding = "utf8"
|
|
378
388
|
self.import_encoding = "utf8"
|
|
379
|
-
self.enc_err_disposition = None
|
|
389
|
+
self.enc_err_disposition: str | None = None
|
|
380
390
|
self.import_common_cols_only = False
|
|
381
391
|
self.max_int = 2147483647
|
|
382
392
|
self.boolean_int = True
|
|
@@ -396,17 +406,17 @@ class ConfigData:
|
|
|
396
406
|
self.scan_lines = 100
|
|
397
407
|
self.hdf5_text_len = 1000
|
|
398
408
|
self.write_warnings = False
|
|
399
|
-
self.write_prefix = None
|
|
400
|
-
self.write_suffix = None
|
|
401
|
-
self.gui_level = 0
|
|
409
|
+
self.write_prefix: str | None = None
|
|
410
|
+
self.write_suffix: str | None = None
|
|
411
|
+
self.gui_level: int = 0
|
|
402
412
|
self.gui_framework = "tkinter"
|
|
403
413
|
self.gui_wait_on_exit = False
|
|
404
414
|
self.gui_wait_on_error_halt = False
|
|
405
415
|
self.gui_console_height = 25
|
|
406
416
|
self.gui_console_width = 100
|
|
407
417
|
self.import_buffer = 32 * 1024
|
|
408
|
-
self.css_file = None
|
|
409
|
-
self.css_styles = None
|
|
418
|
+
self.css_file: str | None = None
|
|
419
|
+
self.css_styles: str | None = None
|
|
410
420
|
self.make_export_dirs = False
|
|
411
421
|
self.outfile_open_timeout = 600
|
|
412
422
|
self.quote_all_text = False
|
|
@@ -414,7 +424,7 @@ class ConfigData:
|
|
|
414
424
|
self.import_progress_interval = 0
|
|
415
425
|
self.show_progress = False
|
|
416
426
|
self.export_row_buffer = 1000
|
|
417
|
-
self.template_processor = None
|
|
427
|
+
self.template_processor: str | None = None
|
|
418
428
|
self.tee_write_log = False
|
|
419
429
|
self.log_datavars = True
|
|
420
430
|
self.log_sql = False
|
|
@@ -432,6 +442,7 @@ class ConfigData:
|
|
|
432
442
|
self.export_output_dir: str | None = None
|
|
433
443
|
self.dao_flush_delay_secs = 5.0
|
|
434
444
|
self.allow_system_cmd = True
|
|
445
|
+
self.system_cmd_timeout = 0.0
|
|
435
446
|
# B05: path-containment controls. ``*_root`` keys, when set, force
|
|
436
447
|
# the corresponding handler to confine resolved paths under the
|
|
437
448
|
# named root and reject anything that escapes via ``..``,
|
|
@@ -618,9 +629,9 @@ class WriteHooks:
|
|
|
618
629
|
|
|
619
630
|
def __init__(
|
|
620
631
|
self,
|
|
621
|
-
standard_output_func: object = None,
|
|
622
|
-
error_output_func: object = None,
|
|
623
|
-
status_output_func: object = None,
|
|
632
|
+
standard_output_func: Callable[[str], object] | None = None,
|
|
633
|
+
error_output_func: Callable[[str], object] | None = None,
|
|
634
|
+
status_output_func: Callable[[str], object] | None = None,
|
|
624
635
|
) -> None:
|
|
625
636
|
"""Store optional hook callables; ``None`` means use the default stream.
|
|
626
637
|
|
|
@@ -648,16 +659,20 @@ class WriteHooks:
|
|
|
648
659
|
self.write_func = None
|
|
649
660
|
self.err_func = None
|
|
650
661
|
|
|
651
|
-
def redir_stdout(self, standard_output_func: object) -> None:
|
|
662
|
+
def redir_stdout(self, standard_output_func: Callable[[str], object]) -> None:
|
|
652
663
|
"""Replace the standard-output hook with the given callable."""
|
|
653
664
|
self.write_func = standard_output_func
|
|
654
665
|
|
|
655
|
-
def redir_stderr(self, error_output_func: object, tee: bool = True) -> None:
|
|
666
|
+
def redir_stderr(self, error_output_func: Callable[[str], object], tee: bool = True) -> None:
|
|
656
667
|
"""Replace the error-output hook and optionally keep tee-to-stderr behaviour."""
|
|
657
668
|
self.err_func = error_output_func
|
|
658
669
|
self.tee_stderr = tee
|
|
659
670
|
|
|
660
|
-
def redir(
|
|
671
|
+
def redir(
|
|
672
|
+
self,
|
|
673
|
+
standard_output_func: Callable[[str], object],
|
|
674
|
+
error_output_func: Callable[[str], object],
|
|
675
|
+
) -> None:
|
|
661
676
|
"""Redirect both stdout and stderr hooks in one call."""
|
|
662
677
|
self.redir_stdout(standard_output_func)
|
|
663
678
|
self.redir_stderr(error_output_func)
|
|
@@ -279,6 +279,12 @@
|
|
|
279
279
|
# Values: Yes or No. Default: Yes.
|
|
280
280
|
#allow_system_cmd=Yes
|
|
281
281
|
|
|
282
|
+
# Maximum seconds a foreground SYSTEM_CMD may run before execsql stops it.
|
|
283
|
+
# Set to 0 for no timeout. SYSTEM_CMD ... CONTINUE is detached and is not
|
|
284
|
+
# timeout-managed by this setting.
|
|
285
|
+
# Value: A non-negative number of seconds. Default: 0.
|
|
286
|
+
#system_cmd_timeout=0
|
|
287
|
+
|
|
282
288
|
# Whether to allow the RM_FILE metacommand (which deletes a file).
|
|
283
289
|
# Set to No to prevent scripts from deleting files.
|
|
284
290
|
# Also controllable via --no-rm-file CLI flag.
|
execsql/db/access.py
CHANGED
|
@@ -12,7 +12,7 @@ import datetime
|
|
|
12
12
|
import re
|
|
13
13
|
import time
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import Any
|
|
15
|
+
from typing import Any, cast
|
|
16
16
|
|
|
17
17
|
from execsql.db.base import Database
|
|
18
18
|
from execsql.exceptions import ErrInfo
|
|
@@ -69,7 +69,7 @@ class AccessDatabase(Database):
|
|
|
69
69
|
# Encoding is only applicable to Jet < 4.0: non-accdb databases.
|
|
70
70
|
self.encoding = encoding or "windows-1252"
|
|
71
71
|
self.encode_commands = True
|
|
72
|
-
self.dao_conn = None
|
|
72
|
+
self.dao_conn: Any = None
|
|
73
73
|
self.conn = None # ODBC connection
|
|
74
74
|
self.paramstr = "?"
|
|
75
75
|
self.dt_cast = dict(self.dt_cast) # Copy the lazy-initialized default before overriding.
|
|
@@ -253,6 +253,7 @@ class AccessDatabase(Database):
|
|
|
253
253
|
else:
|
|
254
254
|
self.dao_flush_check()
|
|
255
255
|
with self._cursor() as curs:
|
|
256
|
+
encoded_sql: Any
|
|
256
257
|
if self.jet4:
|
|
257
258
|
encoded_sql = str(sql)
|
|
258
259
|
else:
|
|
@@ -398,7 +399,7 @@ class AccessDatabase(Database):
|
|
|
398
399
|
tablename = self.type.quoted(tablename)
|
|
399
400
|
self.execute(f"drop table {tablename};")
|
|
400
401
|
|
|
401
|
-
def as_datetime(self, val: Any) -> datetime.datetime | None:
|
|
402
|
+
def as_datetime(self, val: Any) -> datetime.datetime | datetime.date | datetime.time | None:
|
|
402
403
|
"""Convert a value to a datetime object suitable for Access."""
|
|
403
404
|
from execsql.types import DT_Timestamp, DT_Date, DT_Time, DataTypeError
|
|
404
405
|
|
|
@@ -414,7 +415,7 @@ class AccessDatabase(Database):
|
|
|
414
415
|
v = DT_Date().from_data(val)
|
|
415
416
|
except DataTypeError:
|
|
416
417
|
# If this generates an exception, let it go up to get caught.
|
|
417
|
-
v = DT_Time().from_data(val)
|
|
418
|
+
v = cast(datetime.time, DT_Time().from_data(val))
|
|
418
419
|
n = datetime.datetime.now()
|
|
419
420
|
v = datetime.datetime(
|
|
420
421
|
n.year,
|
|
@@ -427,7 +428,7 @@ class AccessDatabase(Database):
|
|
|
427
428
|
)
|
|
428
429
|
except Exception:
|
|
429
430
|
raise
|
|
430
|
-
return v
|
|
431
|
+
return cast(datetime.datetime, v)
|
|
431
432
|
|
|
432
433
|
def int_or_bool(self, val: Any) -> int | None:
|
|
433
434
|
"""Convert a value to an integer, recognizing Access boolean values."""
|
execsql/db/base.py
CHANGED
|
@@ -67,6 +67,19 @@ class Database(ABC):
|
|
|
67
67
|
``_state.dbs.current()`` rather than constructing adapters directly.
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
|
+
type: Any
|
|
71
|
+
server_name: Any
|
|
72
|
+
db_name: Any
|
|
73
|
+
user: Any
|
|
74
|
+
need_passwd: bool | None
|
|
75
|
+
password: str | None
|
|
76
|
+
port: int | None
|
|
77
|
+
encoding: Any
|
|
78
|
+
encode_commands: bool
|
|
79
|
+
paramstr: str
|
|
80
|
+
conn: Any
|
|
81
|
+
autocommit: bool
|
|
82
|
+
|
|
70
83
|
_dt_cast: dict[type, Callable] | None = None
|
|
71
84
|
|
|
72
85
|
@property
|
|
@@ -90,7 +103,7 @@ class Database(ABC):
|
|
|
90
103
|
encoding: str | None = None,
|
|
91
104
|
) -> None:
|
|
92
105
|
"""Initialize common connection attributes for a database backend."""
|
|
93
|
-
self.type = None
|
|
106
|
+
self.type: Any = None
|
|
94
107
|
self.server_name = server_name
|
|
95
108
|
self.db_name = db_name
|
|
96
109
|
self.user = user_name
|
|
@@ -697,12 +710,14 @@ class Database(ABC):
|
|
|
697
710
|
)
|
|
698
711
|
import_cols = csv_cols
|
|
699
712
|
|
|
713
|
+
tablespec_cache: Any = None
|
|
714
|
+
|
|
700
715
|
def get_ts() -> Any:
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
716
|
+
nonlocal tablespec_cache
|
|
717
|
+
if not tablespec_cache:
|
|
718
|
+
tablespec_cache = csv_file_obj.data_table_def()
|
|
719
|
+
return tablespec_cache
|
|
704
720
|
|
|
705
|
-
get_ts.tablespec = None
|
|
706
721
|
f = csv_file_obj.reader()
|
|
707
722
|
next(f)
|
|
708
723
|
self.populate_table(schema_name, table_name, f, import_cols, get_ts)
|
|
@@ -777,14 +792,17 @@ class DatabasePool:
|
|
|
777
792
|
|
|
778
793
|
def current(self) -> Database:
|
|
779
794
|
"""Return the currently active ``Database`` object."""
|
|
795
|
+
assert self.current_db is not None
|
|
780
796
|
return self.pool[self.current_db]
|
|
781
797
|
|
|
782
798
|
def current_alias(self) -> str:
|
|
783
799
|
"""Return the alias string for the currently active database."""
|
|
800
|
+
assert self.current_db is not None
|
|
784
801
|
return self.current_db
|
|
785
802
|
|
|
786
803
|
def initial(self) -> Database:
|
|
787
804
|
"""Return the first ``Database`` that was added to the pool."""
|
|
805
|
+
assert self.initial_db is not None
|
|
788
806
|
return self.pool[self.initial_db]
|
|
789
807
|
|
|
790
808
|
def aliased_as(self, db_alias: str) -> Database:
|
execsql/db/dsn.py
CHANGED
|
@@ -86,6 +86,8 @@ class DsnDatabase(Database):
|
|
|
86
86
|
# supplied credentials cannot inject additional attributes.
|
|
87
87
|
parts = [f"DSN={_odbc_quote(self.db_name)}"]
|
|
88
88
|
if self.need_passwd:
|
|
89
|
+
assert self.user is not None
|
|
90
|
+
assert self.password is not None
|
|
89
91
|
parts.append(f"UID={_odbc_quote(self.user)}")
|
|
90
92
|
parts.append(f"PWD={_odbc_quote(self.password)}")
|
|
91
93
|
connstr = ";".join(parts) + ";"
|
execsql/db/duckdb.py
CHANGED
|
@@ -73,7 +73,7 @@ class DuckDBDatabase(Database):
|
|
|
73
73
|
self.rollback()
|
|
74
74
|
raise
|
|
75
75
|
|
|
76
|
-
def view_exists(self, view_name: str) -> bool:
|
|
76
|
+
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
77
77
|
"""Return True if the named view exists in the DuckDB database."""
|
|
78
78
|
# DuckDB information_schema has no 'views' table; views are listed in 'tables'
|
|
79
79
|
return self.table_exists(view_name)
|
execsql/db/mysql.py
CHANGED
|
@@ -8,7 +8,7 @@ servers via ``pymysql``. Corresponds to ``-t m`` on the CLI.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import re
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any, cast
|
|
12
12
|
|
|
13
13
|
from execsql.db.base import Database
|
|
14
14
|
from execsql.exceptions import ErrInfo
|
|
@@ -105,7 +105,7 @@ class MySQLDatabase(Database):
|
|
|
105
105
|
"""
|
|
106
106
|
cached = getattr(self, "_cached_lctn", None)
|
|
107
107
|
if cached is not None:
|
|
108
|
-
return cached
|
|
108
|
+
return cast(int, cached)
|
|
109
109
|
try:
|
|
110
110
|
_, rows = self.select_data("SELECT @@lower_case_table_names;")
|
|
111
111
|
value = int(rows[0][0]) if rows else 0
|
|
@@ -114,10 +114,8 @@ class MySQLDatabase(Database):
|
|
|
114
114
|
self._cached_lctn = value
|
|
115
115
|
return value
|
|
116
116
|
|
|
117
|
-
def _fold_identifier(self, name: str
|
|
117
|
+
def _fold_identifier(self, name: str) -> str:
|
|
118
118
|
"""Lowercase *name* when the server is case-insensitive (LCTN 1/2)."""
|
|
119
|
-
if name is None:
|
|
120
|
-
return None
|
|
121
119
|
return name.lower() if self._lower_case_table_names() in (1, 2) else name
|
|
122
120
|
|
|
123
121
|
# NB: schema_exists is overridden below to return False unconditionally
|
|
@@ -127,7 +125,7 @@ class MySQLDatabase(Database):
|
|
|
127
125
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
128
126
|
return super().table_exists(
|
|
129
127
|
self._fold_identifier(table_name),
|
|
130
|
-
self._fold_identifier(schema_name),
|
|
128
|
+
self._fold_identifier(schema_name) if schema_name is not None else None,
|
|
131
129
|
)
|
|
132
130
|
|
|
133
131
|
def column_exists(
|
|
@@ -139,13 +137,13 @@ class MySQLDatabase(Database):
|
|
|
139
137
|
return super().column_exists(
|
|
140
138
|
self._fold_identifier(table_name),
|
|
141
139
|
self._fold_identifier(column_name),
|
|
142
|
-
self._fold_identifier(schema_name),
|
|
140
|
+
self._fold_identifier(schema_name) if schema_name is not None else None,
|
|
143
141
|
)
|
|
144
142
|
|
|
145
143
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
146
144
|
return super().view_exists(
|
|
147
145
|
self._fold_identifier(view_name),
|
|
148
|
-
self._fold_identifier(schema_name),
|
|
146
|
+
self._fold_identifier(schema_name) if schema_name is not None else None,
|
|
149
147
|
)
|
|
150
148
|
|
|
151
149
|
def open_db(self) -> None:
|
execsql/db/sqlite.py
CHANGED
|
@@ -130,7 +130,7 @@ class SQLiteDatabase(Database):
|
|
|
130
130
|
) from e
|
|
131
131
|
return [d[0] for d in curs.description]
|
|
132
132
|
|
|
133
|
-
def view_exists(self, view_name: str) -> bool:
|
|
133
|
+
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
134
134
|
"""Return True if the named view exists in the SQLite database."""
|
|
135
135
|
# Match case-insensitively — see table_exists().
|
|
136
136
|
with self._cursor() as curs:
|