execsql2 2.17.0__py3-none-any.whl → 2.17.3__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 +6 -2
- execsql/api.py +25 -6
- execsql/cli/__init__.py +5 -3
- execsql/cli/lint.py +30 -34
- execsql/cli/run.py +10 -0
- execsql/config.py +145 -92
- execsql/db/access.py +54 -40
- execsql/db/base.py +33 -6
- execsql/db/firebird.py +3 -1
- execsql/db/mysql.py +4 -3
- execsql/db/oracle.py +36 -14
- execsql/db/postgres.py +8 -6
- execsql/db/sqlite.py +5 -2
- execsql/db/sqlserver.py +8 -6
- execsql/debug/repl.py +59 -21
- execsql/exceptions.py +19 -4
- execsql/exporters/base.py +3 -2
- execsql/exporters/delimited.py +2 -3
- execsql/exporters/feather.py +3 -3
- execsql/exporters/ods.py +1 -1
- execsql/exporters/xls.py +12 -4
- execsql/exporters/xlsx.py +1 -1
- execsql/gui/desktop.py +129 -15
- execsql/importers/__init__.py +1 -1
- execsql/importers/ods.py +1 -1
- execsql/importers/xls.py +1 -1
- execsql/metacommands/__init__.py +34 -5
- execsql/metacommands/conditions.py +26 -14
- execsql/metacommands/connect.py +21 -14
- execsql/metacommands/control.py +55 -68
- execsql/metacommands/data.py +25 -9
- execsql/metacommands/debug.py +132 -77
- execsql/metacommands/io_export.py +14 -2
- execsql/metacommands/io_import.py +11 -2
- execsql/metacommands/io_write.py +113 -11
- execsql/metacommands/prompt.py +46 -32
- execsql/metacommands/script_ext.py +63 -34
- execsql/metacommands/system.py +4 -3
- execsql/script/__init__.py +28 -37
- execsql/script/ast.py +7 -7
- execsql/script/control.py +4 -101
- execsql/script/engine.py +37 -251
- execsql/script/executor.py +181 -222
- execsql/script/parser.py +1 -3
- execsql/script/variables.py +8 -3
- execsql/state.py +125 -37
- execsql/utils/errors.py +0 -2
- execsql/utils/fileio.py +47 -3
- execsql/utils/mail.py +3 -2
- execsql/utils/strings.py +5 -5
- {execsql2-2.17.0.dist-info → execsql2-2.17.3.dist-info}/METADATA +44 -38
- execsql2-2.17.3.dist-info/RECORD +124 -0
- execsql2-2.17.3.dist-info/licenses/NOTICE +11 -0
- execsql2-2.17.0.dist-info/RECORD +0 -124
- execsql2-2.17.0.dist-info/licenses/NOTICE +0 -10
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.3.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.17.0.dist-info → execsql2-2.17.3.dist-info}/WHEEL +0 -0
- {execsql2-2.17.0.dist-info → execsql2-2.17.3.dist-info}/entry_points.txt +0 -0
- {execsql2-2.17.0.dist-info → execsql2-2.17.3.dist-info}/licenses/LICENSE.txt +0 -0
execsql/__init__.py
CHANGED
|
@@ -3,8 +3,12 @@ execsql — a maintained fork of the execsql SQL scripting tool.
|
|
|
3
3
|
|
|
4
4
|
This package provides the ``execsql`` CLI command (distributed as the
|
|
5
5
|
``execsql2`` package on PyPI) and the ``execsql`` importable module.
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
The top-level package re-exports the public Python API for programmatic
|
|
8
|
+
use: :func:`run`, :class:`ScriptResult`, :class:`ScriptError`, and the
|
|
9
|
+
:class:`ExecSqlError` exception that :meth:`ScriptResult.raise_on_error`
|
|
10
|
+
raises. Internal sub-modules carry the rest of the implementation
|
|
11
|
+
(``cli``, ``db``, ``script``, ``metacommands``, etc.).
|
|
8
12
|
"""
|
|
9
13
|
|
|
10
14
|
from __future__ import annotations
|
execsql/api.py
CHANGED
|
@@ -76,8 +76,12 @@ class ScriptResult:
|
|
|
76
76
|
commands_run: Number of SQL statements and metacommands executed.
|
|
77
77
|
elapsed: Wall-clock execution time in seconds.
|
|
78
78
|
errors: List of errors encountered (empty on success).
|
|
79
|
-
variables: Final state of
|
|
80
|
-
|
|
79
|
+
variables: Final state of substitution variables, keyed by name
|
|
80
|
+
without any sigil. Includes user-defined variables (no prefix)
|
|
81
|
+
and system variables (the leading ``$`` is stripped, so
|
|
82
|
+
``$DATE_TAG`` appears as ``"DATE_TAG"``). Environment (``&``),
|
|
83
|
+
column-data (``@``), script-local (``~``), and script-parameter
|
|
84
|
+
(``#``) variables are excluded.
|
|
81
85
|
"""
|
|
82
86
|
|
|
83
87
|
success: bool
|
|
@@ -100,7 +104,20 @@ class ScriptResult:
|
|
|
100
104
|
|
|
101
105
|
|
|
102
106
|
class ExecSqlError(Exception):
|
|
103
|
-
"""Raised by :meth:`ScriptResult.raise_on_error` when a script fails.
|
|
107
|
+
"""Raised by :meth:`ScriptResult.raise_on_error` when a script fails.
|
|
108
|
+
|
|
109
|
+
Note:
|
|
110
|
+
This is a separate class from :class:`execsql.exceptions.ExecSqlError`,
|
|
111
|
+
which is the internal base of the in-process exception hierarchy
|
|
112
|
+
(``ErrInfo``, ``ConfigError``, etc.). The library API never
|
|
113
|
+
propagates those — internal exceptions are caught and converted
|
|
114
|
+
to :class:`ScriptError` entries on the returned :class:`ScriptResult`.
|
|
115
|
+
Only ``raise_on_error()`` raises this public exception.
|
|
116
|
+
|
|
117
|
+
Attributes:
|
|
118
|
+
result: The :class:`ScriptResult` that triggered the raise. The
|
|
119
|
+
full error list is available on ``result.errors``.
|
|
120
|
+
"""
|
|
104
121
|
|
|
105
122
|
def __init__(self, message: str, result: ScriptResult) -> None:
|
|
106
123
|
super().__init__(message)
|
|
@@ -338,8 +355,10 @@ def run(
|
|
|
338
355
|
error. If ``False``, capture errors and continue.
|
|
339
356
|
new_db: If ``True``, create the database if it does not exist
|
|
340
357
|
(SQLite, PostgreSQL, DuckDB).
|
|
341
|
-
allow_system_cmd: If ``False``, the SYSTEM_CMD
|
|
342
|
-
|
|
358
|
+
allow_system_cmd: If ``False``, the SYSTEM_CMD metacommand is
|
|
359
|
+
disabled and will raise an error if encountered. Matches the
|
|
360
|
+
``--no-system-cmd`` CLI flag and the ``allow_system_cmd``
|
|
361
|
+
config option.
|
|
343
362
|
|
|
344
363
|
Returns:
|
|
345
364
|
A :class:`ScriptResult` with execution outcome, timing, errors,
|
|
@@ -449,7 +468,7 @@ def run(
|
|
|
449
468
|
ctx.exec_log = _NoOpLogger()
|
|
450
469
|
|
|
451
470
|
with active_context(ctx):
|
|
452
|
-
# Initialize singletons (
|
|
471
|
+
# Initialize singletons (CounterVars, Timer, DatabasePool, BatchLevels, etc.)
|
|
453
472
|
from execsql.state import initialize
|
|
454
473
|
|
|
455
474
|
initialize(conf, DISPATCH_TABLE, CONDITIONAL_TABLE)
|
execsql/cli/__init__.py
CHANGED
|
@@ -5,9 +5,11 @@ initialisation, database connection, and script execution.
|
|
|
5
5
|
|
|
6
6
|
Submodules:
|
|
7
7
|
|
|
8
|
-
- :mod:`execsql.cli.help`
|
|
9
|
-
- :mod:`execsql.cli.dsn`
|
|
10
|
-
- :mod:`execsql.cli.run`
|
|
8
|
+
- :mod:`execsql.cli.help` — Rich-formatted help output & console objects
|
|
9
|
+
- :mod:`execsql.cli.dsn` — Connection-string (DSN URL) parser
|
|
10
|
+
- :mod:`execsql.cli.run` — Core execution logic (``_run``, ``_connect_initial_db``, ``_ping_db``, ``_print_dry_run``, ``_print_profile``)
|
|
11
|
+
- :mod:`execsql.cli.lint_ast` — AST-based ``--lint`` static analyser
|
|
12
|
+
- :mod:`execsql.cli.lint` — Lint result printing (``_print_lint_results``); the flat-CommandList ``_lint_cmdlist`` it also contains is legacy and no longer reached from the CLI
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
execsql/cli/lint.py
CHANGED
|
@@ -1,37 +1,33 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Exit-code contract
|
|
32
|
-
------------------
|
|
33
|
-
- Returns ``1`` when at least one **error**-severity issue is found.
|
|
34
|
-
- Returns ``0`` when only warnings (or nothing) are found.
|
|
1
|
+
"""Legacy flat-CommandList linter and shared result printer for execsql.
|
|
2
|
+
|
|
3
|
+
The active ``--lint`` implementation is in :mod:`execsql.cli.lint_ast`,
|
|
4
|
+
which works against the AST produced by
|
|
5
|
+
:func:`execsql.script.parser.parse_script`. The CLI reaches that
|
|
6
|
+
linter directly; the ``_lint_cmdlist`` walker in this module is
|
|
7
|
+
unreached from the CLI now and is retained for reference / potential
|
|
8
|
+
reuse only.
|
|
9
|
+
|
|
10
|
+
What's still used from this module:
|
|
11
|
+
|
|
12
|
+
- :func:`_print_lint_results` — shared Rich-formatted output for both
|
|
13
|
+
the AST linter and any code that still constructs lint issues
|
|
14
|
+
manually. Called from ``cli/__init__.py`` and re-exported via
|
|
15
|
+
``cli/run.py``.
|
|
16
|
+
- :class:`_Issue` and the ``_error()`` / ``_warning()`` constructors
|
|
17
|
+
— used by both linters.
|
|
18
|
+
|
|
19
|
+
The flat-CommandList ``_lint_cmdlist``, ``_collect_defined_vars``, and
|
|
20
|
+
``_discover_builtin_vars`` helpers below covered the same checks the
|
|
21
|
+
AST linter now performs (unmatched IF/LOOP/BATCH, undefined ``!!$VAR!!``
|
|
22
|
+
references, missing INCLUDE files, EXECUTE SCRIPT flow analysis, empty
|
|
23
|
+
script). They operate on the legacy
|
|
24
|
+
:class:`~execsql.script.engine.CommandList`; no CLI path constructs
|
|
25
|
+
that representation any more.
|
|
26
|
+
|
|
27
|
+
Exit-code contract (still honoured by the AST linter):
|
|
28
|
+
|
|
29
|
+
- ``1`` when at least one error-severity issue is found.
|
|
30
|
+
- ``0`` when only warnings (or nothing) are found.
|
|
35
31
|
"""
|
|
36
32
|
|
|
37
33
|
from __future__ import annotations
|
execsql/cli/run.py
CHANGED
|
@@ -630,6 +630,16 @@ def _run(
|
|
|
630
630
|
import execsql.utils.fileio as _fileio
|
|
631
631
|
|
|
632
632
|
if _state.filewriter is None or not _state.filewriter.is_alive():
|
|
633
|
+
# Drain stale messages from the queues so a previously-dead subprocess
|
|
634
|
+
# can't leak responses or unconsumed commands into the new one. On
|
|
635
|
+
# macOS (`spawn`) the OS pipe buffer is small enough that retained
|
|
636
|
+
# entries from a crashed writer would deadlock the next put().
|
|
637
|
+
for q in (_fileio.fw_input, _fileio.fw_output):
|
|
638
|
+
try:
|
|
639
|
+
while True:
|
|
640
|
+
q.get_nowait()
|
|
641
|
+
except Exception:
|
|
642
|
+
pass
|
|
633
643
|
_fileio.filewriter = _state.filewriter = FileWriter(
|
|
634
644
|
_fileio.fw_input,
|
|
635
645
|
_fileio.fw_output,
|
execsql/config.py
CHANGED
|
@@ -74,6 +74,17 @@ class ConfigData:
|
|
|
74
74
|
_INCLUDE_REQ_SECTION = "include_required"
|
|
75
75
|
_INCLUDE_OPT_SECTION = "include_optional"
|
|
76
76
|
|
|
77
|
+
# Schema registry: maps attribute name -> (section, ini_key, type_label).
|
|
78
|
+
# Populated by the _get_* methods on every option they read. This is the
|
|
79
|
+
# canonical source for DEBUG WRITE CONFIG / DEBUG LOG CONFIG output —
|
|
80
|
+
# adding a new option below automatically makes it appear in the dump.
|
|
81
|
+
_schema: dict[str, tuple[str, str, str]] = {}
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def _register_option(cls, section: str, key: str, attr: str, type_label: str) -> None:
|
|
85
|
+
"""Record ``attr`` in the schema registry so config introspection sees it."""
|
|
86
|
+
cls._schema[attr] = (section, key, type_label)
|
|
87
|
+
|
|
77
88
|
def _get_str(self, cp: ConfigParser, section: str, key: str, attr: str, *, required: bool = False) -> None:
|
|
78
89
|
"""Read a string option and set ``self.<attr>``.
|
|
79
90
|
|
|
@@ -84,6 +95,7 @@ class ConfigData:
|
|
|
84
95
|
attr: Attribute name to set on ``self``.
|
|
85
96
|
required: If ``True``, raise :class:`ConfigError` when the value is ``None``.
|
|
86
97
|
"""
|
|
98
|
+
self._register_option(section, key, attr, "str")
|
|
87
99
|
if cp.has_option(section, key):
|
|
88
100
|
val = cp.get(section, key)
|
|
89
101
|
if required and val is None:
|
|
@@ -110,6 +122,7 @@ class ConfigData:
|
|
|
110
122
|
choices: Tuple of permitted values.
|
|
111
123
|
lower: If ``True`` (default), lower-case the raw value before validation.
|
|
112
124
|
"""
|
|
125
|
+
self._register_option(section, key, attr, "enum")
|
|
113
126
|
if cp.has_option(section, key):
|
|
114
127
|
val = cp.get(section, key)
|
|
115
128
|
if lower:
|
|
@@ -130,6 +143,7 @@ class ConfigData:
|
|
|
130
143
|
Raises:
|
|
131
144
|
ConfigError: If the value cannot be parsed as a boolean.
|
|
132
145
|
"""
|
|
146
|
+
self._register_option(section, key, attr, "bool")
|
|
133
147
|
if cp.has_option(section, key):
|
|
134
148
|
try:
|
|
135
149
|
setattr(self, attr, cp.getboolean(section, key))
|
|
@@ -159,6 +173,7 @@ class ConfigData:
|
|
|
159
173
|
Raises:
|
|
160
174
|
ConfigError: If the value cannot be parsed as an integer.
|
|
161
175
|
"""
|
|
176
|
+
self._register_option(section, key, attr, "int")
|
|
162
177
|
if cp.has_option(section, key):
|
|
163
178
|
try:
|
|
164
179
|
val = cp.getint(section, key) * multiply
|
|
@@ -189,6 +204,7 @@ class ConfigData:
|
|
|
189
204
|
Raises:
|
|
190
205
|
ConfigError: If the value cannot be parsed as a float, or is below ``min_val``.
|
|
191
206
|
"""
|
|
207
|
+
self._register_option(section, key, attr, "float")
|
|
192
208
|
if cp.has_option(section, key):
|
|
193
209
|
try:
|
|
194
210
|
val = cp.getfloat(section, key)
|
|
@@ -198,6 +214,127 @@ class ConfigData:
|
|
|
198
214
|
raise ConfigError(f"Invalid {key}: {val}; must be >= {min_val}.")
|
|
199
215
|
setattr(self, attr, val)
|
|
200
216
|
|
|
217
|
+
def _read_known_options(self, cp: ConfigParser) -> None:
|
|
218
|
+
"""Apply every schema-registered option from ``cp`` to ``self``.
|
|
219
|
+
|
|
220
|
+
Single source of truth for which options ``ConfigData`` knows about.
|
|
221
|
+
Called once with an empty ``ConfigParser`` at the start of ``__init__``
|
|
222
|
+
so :attr:`_schema` is populated even when no config files exist on the
|
|
223
|
+
system, then again from the file-merge loop for each real config file
|
|
224
|
+
(which actually sets attribute values).
|
|
225
|
+
|
|
226
|
+
Options with special-case validation (``db_type``, ``write_prefix``,
|
|
227
|
+
``gui_level``, ``gui_framework``, ``dao_flush_delay_secs``,
|
|
228
|
+
``enc_password``, ``email_format``) stay inline in ``__init__`` to
|
|
229
|
+
preserve their exact error messages and side effects.
|
|
230
|
+
"""
|
|
231
|
+
# --- [connect] ---
|
|
232
|
+
self._get_str(cp, self._CONNECT_SECTION, "server", "server", required=True)
|
|
233
|
+
self._get_str(cp, self._CONNECT_SECTION, "db", "db", required=True)
|
|
234
|
+
self._get_int(cp, self._CONNECT_SECTION, "port", "port")
|
|
235
|
+
self._get_str(cp, self._CONNECT_SECTION, "database", "db", required=True)
|
|
236
|
+
self._get_str(cp, self._CONNECT_SECTION, "db_file", "db_file", required=True)
|
|
237
|
+
self._get_str(cp, self._CONNECT_SECTION, "username", "username", required=True)
|
|
238
|
+
self._get_str(cp, self._CONNECT_SECTION, "access_username", "access_username")
|
|
239
|
+
self._get_bool(cp, self._CONNECT_SECTION, "password_prompt", "passwd_prompt")
|
|
240
|
+
self._get_bool(cp, self._CONNECT_SECTION, "use_keyring", "use_keyring")
|
|
241
|
+
self._get_bool(cp, self._CONNECT_SECTION, "new_db", "new_db")
|
|
242
|
+
# --- [encoding] ---
|
|
243
|
+
self._get_str(cp, self._ENCODING_SECTION, "database", "db_encoding")
|
|
244
|
+
self._get_str(cp, self._ENCODING_SECTION, "script", "script_encoding", required=True)
|
|
245
|
+
self._get_str(cp, self._ENCODING_SECTION, "import", "import_encoding", required=True)
|
|
246
|
+
self._get_str(cp, self._ENCODING_SECTION, "output", "output_encoding", required=True)
|
|
247
|
+
self._get_enum(
|
|
248
|
+
cp,
|
|
249
|
+
self._ENCODING_SECTION,
|
|
250
|
+
"error_response",
|
|
251
|
+
"enc_err_disposition",
|
|
252
|
+
("ignore", "replace", "xmlcharrefreplace", "backslashreplace"),
|
|
253
|
+
)
|
|
254
|
+
# --- [input] ---
|
|
255
|
+
self._get_int(cp, self._INPUT_SECTION, "max_int", "max_int")
|
|
256
|
+
self._get_bool(cp, self._INPUT_SECTION, "boolean_int", "boolean_int")
|
|
257
|
+
self._get_bool(cp, self._INPUT_SECTION, "boolean_words", "boolean_words")
|
|
258
|
+
self._get_bool(cp, self._INPUT_SECTION, "empty_strings", "empty_strings")
|
|
259
|
+
self._get_bool(cp, self._INPUT_SECTION, "only_strings", "only_strings")
|
|
260
|
+
self._get_bool(cp, self._INPUT_SECTION, "empty_rows", "empty_rows")
|
|
261
|
+
self._get_bool(cp, self._INPUT_SECTION, "delete_empty_columns", "del_empty_cols")
|
|
262
|
+
self._get_bool(cp, self._INPUT_SECTION, "create_column_headers", "create_col_hdrs")
|
|
263
|
+
self._get_enum(
|
|
264
|
+
cp,
|
|
265
|
+
self._INPUT_SECTION,
|
|
266
|
+
"trim_column_headers",
|
|
267
|
+
"trim_col_hdrs",
|
|
268
|
+
("none", "both", "left", "right"),
|
|
269
|
+
)
|
|
270
|
+
self._get_bool(cp, self._INPUT_SECTION, "clean_column_headers", "clean_col_hdrs")
|
|
271
|
+
self._get_enum(
|
|
272
|
+
cp,
|
|
273
|
+
self._INPUT_SECTION,
|
|
274
|
+
"fold_column_headers",
|
|
275
|
+
"fold_col_hdrs",
|
|
276
|
+
("no", "lower", "upper"),
|
|
277
|
+
)
|
|
278
|
+
self._get_bool(cp, self._INPUT_SECTION, "dedup_column_headers", "dedup_col_hdrs")
|
|
279
|
+
self._get_bool(cp, self._INPUT_SECTION, "trim_strings", "trim_strings")
|
|
280
|
+
self._get_bool(cp, self._INPUT_SECTION, "replace_newlines", "replace_newlines")
|
|
281
|
+
self._get_int(cp, self._INPUT_SECTION, "import_row_buffer", "import_row_buffer")
|
|
282
|
+
self._get_int(cp, self._INPUT_SECTION, "import_progress_interval", "import_progress_interval")
|
|
283
|
+
self._get_bool(cp, self._INPUT_SECTION, "show_progress", "show_progress")
|
|
284
|
+
self._get_bool(cp, self._INPUT_SECTION, "access_use_numeric", "access_use_numeric")
|
|
285
|
+
self._get_bool(cp, self._INPUT_SECTION, "import_only_common_columns", "import_common_cols_only")
|
|
286
|
+
self._get_bool(cp, self._INPUT_SECTION, "import_common_columns_only", "import_common_cols_only")
|
|
287
|
+
self._get_int(cp, self._INPUT_SECTION, "scan_lines", "scan_lines")
|
|
288
|
+
self._get_int(cp, self._INPUT_SECTION, "import_buffer", "import_buffer", multiply=1024)
|
|
289
|
+
# --- [output] ---
|
|
290
|
+
self._get_bool(cp, self._OUTPUT_SECTION, "log_write_messages", "tee_write_log")
|
|
291
|
+
self._get_int(cp, self._OUTPUT_SECTION, "hdf5_text_len", "hdf5_text_len")
|
|
292
|
+
self._get_str(cp, self._OUTPUT_SECTION, "css_file", "css_file", required=True)
|
|
293
|
+
self._get_str(cp, self._OUTPUT_SECTION, "css_styles", "css_styles", required=True)
|
|
294
|
+
self._get_bool(cp, self._OUTPUT_SECTION, "make_export_dirs", "make_export_dirs")
|
|
295
|
+
self._get_bool(cp, self._OUTPUT_SECTION, "quote_all_text", "quote_all_text")
|
|
296
|
+
self._get_int(cp, self._OUTPUT_SECTION, "outfile_open_timeout", "outfile_open_timeout")
|
|
297
|
+
self._get_int(cp, self._OUTPUT_SECTION, "export_row_buffer", "export_row_buffer")
|
|
298
|
+
self._get_enum(
|
|
299
|
+
cp,
|
|
300
|
+
self._OUTPUT_SECTION,
|
|
301
|
+
"template_processor",
|
|
302
|
+
"template_processor",
|
|
303
|
+
("jinja",),
|
|
304
|
+
)
|
|
305
|
+
self._get_int(cp, self._OUTPUT_SECTION, "zip_buffer_mb", "zip_buffer_mb")
|
|
306
|
+
# --- [interface] ---
|
|
307
|
+
self._get_bool(cp, self._INTERFACE_SECTION, "write_warnings", "write_warnings")
|
|
308
|
+
self._get_int(cp, self._INTERFACE_SECTION, "console_height", "gui_console_height", min_val=5)
|
|
309
|
+
self._get_int(cp, self._INTERFACE_SECTION, "console_width", "gui_console_width", min_val=20)
|
|
310
|
+
self._get_bool(cp, self._INTERFACE_SECTION, "console_wait_when_done", "gui_wait_on_exit")
|
|
311
|
+
self._get_bool(cp, self._INTERFACE_SECTION, "console_wait_when_error_halt", "gui_wait_on_error_halt")
|
|
312
|
+
# --- [config] ---
|
|
313
|
+
self._get_bool(cp, self._CONFIG_SECTION, "user_logfile", "user_logfile")
|
|
314
|
+
self._get_bool(cp, self._CONFIG_SECTION, "log_datavars", "log_datavars")
|
|
315
|
+
self._get_bool(cp, self._CONFIG_SECTION, "log_sql", "log_sql")
|
|
316
|
+
self._get_int(cp, self._CONFIG_SECTION, "max_log_size_mb", "max_log_size_mb")
|
|
317
|
+
self._get_bool(cp, self._CONFIG_SECTION, "allow_system_cmd", "allow_system_cmd")
|
|
318
|
+
# --- [email] ---
|
|
319
|
+
self._get_str(cp, self._EMAIL_SECTION, "host", "smtp_host")
|
|
320
|
+
self._get_int(cp, self._EMAIL_SECTION, "port", "smtp_port")
|
|
321
|
+
self._get_str(cp, self._EMAIL_SECTION, "username", "smtp_username")
|
|
322
|
+
self._get_str(cp, self._EMAIL_SECTION, "password", "smtp_password")
|
|
323
|
+
self._get_bool(cp, self._EMAIL_SECTION, "use_ssl", "smtp_ssl")
|
|
324
|
+
self._get_bool(cp, self._EMAIL_SECTION, "use_tls", "smtp_tls")
|
|
325
|
+
self._get_str(cp, self._EMAIL_SECTION, "message_css", "email_css")
|
|
326
|
+
|
|
327
|
+
# Register options whose loading lives inline in __init__ (because they
|
|
328
|
+
# need special-case validation or side effects) so they still appear in
|
|
329
|
+
# DEBUG WRITE CONFIG.
|
|
330
|
+
self._register_option(self._CONNECT_SECTION, "db_type", "db_type", "enum")
|
|
331
|
+
self._register_option(self._INTERFACE_SECTION, "write_prefix", "write_prefix", "str")
|
|
332
|
+
self._register_option(self._INTERFACE_SECTION, "write_suffix", "write_suffix", "str")
|
|
333
|
+
self._register_option(self._INTERFACE_SECTION, "gui_level", "gui_level", "int")
|
|
334
|
+
self._register_option(self._INTERFACE_SECTION, "gui_framework", "gui_framework", "enum")
|
|
335
|
+
self._register_option(self._CONFIG_SECTION, "dao_flush_delay_secs", "dao_flush_delay_secs", "float")
|
|
336
|
+
self._register_option(self._EMAIL_SECTION, "email_format", "email_format", "enum")
|
|
337
|
+
|
|
201
338
|
def __init__(
|
|
202
339
|
self,
|
|
203
340
|
script_path: str,
|
|
@@ -309,6 +446,13 @@ class ConfigData:
|
|
|
309
446
|
_MAX_CONFIG_CHAIN = 20 # Guard against circular config_file references.
|
|
310
447
|
config_queue: deque[str] = deque(config_files)
|
|
311
448
|
self.files_read: list = []
|
|
449
|
+
# Warm the schema registry by running the option reader against an
|
|
450
|
+
# empty ConfigParser. cp.has_option() is False for everything so no
|
|
451
|
+
# attribute values change, but every _get_* call registers its
|
|
452
|
+
# (section, ini_key, attr) tuple in ConfigData._schema. This means
|
|
453
|
+
# DEBUG WRITE CONFIG / DEBUG LOG CONFIG see the full option set even
|
|
454
|
+
# when no execsql.conf files exist on the system.
|
|
455
|
+
self._read_known_options(ConfigParser())
|
|
312
456
|
while config_queue:
|
|
313
457
|
configfile = config_queue.popleft()
|
|
314
458
|
if len(self.files_read) >= _MAX_CONFIG_CHAIN:
|
|
@@ -323,82 +467,7 @@ class ConfigData:
|
|
|
323
467
|
if t not in ("a", "d", "f", "k", "l", "m", "o", "p", "s"):
|
|
324
468
|
raise ConfigError(f"Invalid database type: {t}")
|
|
325
469
|
self.db_type = t
|
|
326
|
-
self.
|
|
327
|
-
self._get_str(cp, self._CONNECT_SECTION, "db", "db", required=True)
|
|
328
|
-
self._get_int(cp, self._CONNECT_SECTION, "port", "port")
|
|
329
|
-
self._get_str(cp, self._CONNECT_SECTION, "database", "db", required=True)
|
|
330
|
-
self._get_str(cp, self._CONNECT_SECTION, "db_file", "db_file", required=True)
|
|
331
|
-
self._get_str(cp, self._CONNECT_SECTION, "username", "username", required=True)
|
|
332
|
-
self._get_str(cp, self._CONNECT_SECTION, "access_username", "access_username")
|
|
333
|
-
self._get_bool(cp, self._CONNECT_SECTION, "password_prompt", "passwd_prompt")
|
|
334
|
-
self._get_bool(cp, self._CONNECT_SECTION, "use_keyring", "use_keyring")
|
|
335
|
-
self._get_bool(cp, self._CONNECT_SECTION, "new_db", "new_db")
|
|
336
|
-
# --- [encoding] ---
|
|
337
|
-
self._get_str(cp, self._ENCODING_SECTION, "database", "db_encoding")
|
|
338
|
-
self._get_str(cp, self._ENCODING_SECTION, "script", "script_encoding", required=True)
|
|
339
|
-
self._get_str(cp, self._ENCODING_SECTION, "import", "import_encoding", required=True)
|
|
340
|
-
self._get_str(cp, self._ENCODING_SECTION, "output", "output_encoding", required=True)
|
|
341
|
-
self._get_enum(
|
|
342
|
-
cp,
|
|
343
|
-
self._ENCODING_SECTION,
|
|
344
|
-
"error_response",
|
|
345
|
-
"enc_err_disposition",
|
|
346
|
-
("ignore", "replace", "xmlcharrefreplace", "backslashreplace"),
|
|
347
|
-
)
|
|
348
|
-
# --- [input] ---
|
|
349
|
-
self._get_int(cp, self._INPUT_SECTION, "max_int", "max_int")
|
|
350
|
-
self._get_bool(cp, self._INPUT_SECTION, "boolean_int", "boolean_int")
|
|
351
|
-
self._get_bool(cp, self._INPUT_SECTION, "boolean_words", "boolean_words")
|
|
352
|
-
self._get_bool(cp, self._INPUT_SECTION, "empty_strings", "empty_strings")
|
|
353
|
-
self._get_bool(cp, self._INPUT_SECTION, "only_strings", "only_strings")
|
|
354
|
-
self._get_bool(cp, self._INPUT_SECTION, "empty_rows", "empty_rows")
|
|
355
|
-
self._get_bool(cp, self._INPUT_SECTION, "delete_empty_columns", "del_empty_cols")
|
|
356
|
-
self._get_bool(cp, self._INPUT_SECTION, "create_column_headers", "create_col_hdrs")
|
|
357
|
-
self._get_enum(
|
|
358
|
-
cp,
|
|
359
|
-
self._INPUT_SECTION,
|
|
360
|
-
"trim_column_headers",
|
|
361
|
-
"trim_col_hdrs",
|
|
362
|
-
("none", "both", "left", "right"),
|
|
363
|
-
)
|
|
364
|
-
self._get_bool(cp, self._INPUT_SECTION, "clean_column_headers", "clean_col_hdrs")
|
|
365
|
-
self._get_enum(
|
|
366
|
-
cp,
|
|
367
|
-
self._INPUT_SECTION,
|
|
368
|
-
"fold_column_headers",
|
|
369
|
-
"fold_col_hdrs",
|
|
370
|
-
("no", "lower", "upper"),
|
|
371
|
-
)
|
|
372
|
-
self._get_bool(cp, self._INPUT_SECTION, "dedup_column_headers", "dedup_col_hdrs")
|
|
373
|
-
self._get_bool(cp, self._INPUT_SECTION, "trim_strings", "trim_strings")
|
|
374
|
-
self._get_bool(cp, self._INPUT_SECTION, "replace_newlines", "replace_newlines")
|
|
375
|
-
self._get_int(cp, self._INPUT_SECTION, "import_row_buffer", "import_row_buffer")
|
|
376
|
-
self._get_int(cp, self._INPUT_SECTION, "import_progress_interval", "import_progress_interval")
|
|
377
|
-
self._get_bool(cp, self._INPUT_SECTION, "show_progress", "show_progress")
|
|
378
|
-
self._get_bool(cp, self._INPUT_SECTION, "access_use_numeric", "access_use_numeric")
|
|
379
|
-
self._get_bool(cp, self._INPUT_SECTION, "import_only_common_columns", "import_common_cols_only")
|
|
380
|
-
self._get_bool(cp, self._INPUT_SECTION, "import_common_columns_only", "import_common_cols_only")
|
|
381
|
-
self._get_int(cp, self._INPUT_SECTION, "scan_lines", "scan_lines")
|
|
382
|
-
self._get_int(cp, self._INPUT_SECTION, "import_buffer", "import_buffer", multiply=1024)
|
|
383
|
-
# --- [output] ---
|
|
384
|
-
self._get_bool(cp, self._OUTPUT_SECTION, "log_write_messages", "tee_write_log")
|
|
385
|
-
self._get_int(cp, self._OUTPUT_SECTION, "hdf5_text_len", "hdf5_text_len")
|
|
386
|
-
self._get_str(cp, self._OUTPUT_SECTION, "css_file", "css_file", required=True)
|
|
387
|
-
self._get_str(cp, self._OUTPUT_SECTION, "css_styles", "css_styles", required=True)
|
|
388
|
-
self._get_bool(cp, self._OUTPUT_SECTION, "make_export_dirs", "make_export_dirs")
|
|
389
|
-
self._get_bool(cp, self._OUTPUT_SECTION, "quote_all_text", "quote_all_text")
|
|
390
|
-
self._get_int(cp, self._OUTPUT_SECTION, "outfile_open_timeout", "outfile_open_timeout")
|
|
391
|
-
self._get_int(cp, self._OUTPUT_SECTION, "export_row_buffer", "export_row_buffer")
|
|
392
|
-
self._get_enum(
|
|
393
|
-
cp,
|
|
394
|
-
self._OUTPUT_SECTION,
|
|
395
|
-
"template_processor",
|
|
396
|
-
"template_processor",
|
|
397
|
-
("jinja",),
|
|
398
|
-
)
|
|
399
|
-
self._get_int(cp, self._OUTPUT_SECTION, "zip_buffer_mb", "zip_buffer_mb")
|
|
400
|
-
# --- [interface] ---
|
|
401
|
-
self._get_bool(cp, self._INTERFACE_SECTION, "write_warnings", "write_warnings")
|
|
470
|
+
self._read_known_options(cp)
|
|
402
471
|
# write_prefix / write_suffix have special "clear" → None handling
|
|
403
472
|
if cp.has_option(self._INTERFACE_SECTION, "write_prefix"):
|
|
404
473
|
try:
|
|
@@ -425,10 +494,6 @@ class ConfigData:
|
|
|
425
494
|
if fw not in ("tkinter", "textual"):
|
|
426
495
|
raise ConfigError("gui_framework must be 'tkinter' or 'textual'.")
|
|
427
496
|
self.gui_framework = fw
|
|
428
|
-
self._get_int(cp, self._INTERFACE_SECTION, "console_height", "gui_console_height", min_val=5)
|
|
429
|
-
self._get_int(cp, self._INTERFACE_SECTION, "console_width", "gui_console_width", min_val=20)
|
|
430
|
-
self._get_bool(cp, self._INTERFACE_SECTION, "console_wait_when_done", "gui_wait_on_exit")
|
|
431
|
-
self._get_bool(cp, self._INTERFACE_SECTION, "console_wait_when_error_halt", "gui_wait_on_error_halt")
|
|
432
497
|
# --- [config] ---
|
|
433
498
|
# config_file / OS-specific config files retain special chaining logic
|
|
434
499
|
if cp.has_option(self._CONFIG_SECTION, "config_file"):
|
|
@@ -464,7 +529,6 @@ class ConfigData:
|
|
|
464
529
|
conffile = str(Path(conffile) / self.config_file_name)
|
|
465
530
|
if Path(conffile).is_file():
|
|
466
531
|
config_queue.appendleft(conffile)
|
|
467
|
-
self._get_bool(cp, self._CONFIG_SECTION, "user_logfile", "user_logfile")
|
|
468
532
|
# dao_flush_delay_secs has a specific error message — keep inline
|
|
469
533
|
if cp.has_option(self._CONFIG_SECTION, "dao_flush_delay_secs"):
|
|
470
534
|
self.dao_flush_delay_secs = cp.getfloat(self._CONFIG_SECTION, "dao_flush_delay_secs")
|
|
@@ -472,15 +536,7 @@ class ConfigData:
|
|
|
472
536
|
raise ConfigError(
|
|
473
537
|
f"Invalid DAO flush delay: {self.dao_flush_delay_secs}; must be >= 5.0.",
|
|
474
538
|
)
|
|
475
|
-
self._get_bool(cp, self._CONFIG_SECTION, "log_datavars", "log_datavars")
|
|
476
|
-
self._get_bool(cp, self._CONFIG_SECTION, "log_sql", "log_sql")
|
|
477
|
-
self._get_int(cp, self._CONFIG_SECTION, "max_log_size_mb", "max_log_size_mb")
|
|
478
|
-
self._get_bool(cp, self._CONFIG_SECTION, "allow_system_cmd", "allow_system_cmd")
|
|
479
539
|
# --- [email] ---
|
|
480
|
-
self._get_str(cp, self._EMAIL_SECTION, "host", "smtp_host")
|
|
481
|
-
self._get_int(cp, self._EMAIL_SECTION, "port", "smtp_port")
|
|
482
|
-
self._get_str(cp, self._EMAIL_SECTION, "username", "smtp_username")
|
|
483
|
-
self._get_str(cp, self._EMAIL_SECTION, "password", "smtp_password")
|
|
484
540
|
# enc_password has special decryption logic — keep inline
|
|
485
541
|
if cp.has_option(self._EMAIL_SECTION, "enc_password"):
|
|
486
542
|
import warnings
|
|
@@ -492,15 +548,12 @@ class ConfigData:
|
|
|
492
548
|
stacklevel=1,
|
|
493
549
|
)
|
|
494
550
|
self.smtp_password = Encrypt().decrypt(cp.get(self._EMAIL_SECTION, "enc_password"))
|
|
495
|
-
self._get_bool(cp, self._EMAIL_SECTION, "use_ssl", "smtp_ssl")
|
|
496
|
-
self._get_bool(cp, self._EMAIL_SECTION, "use_tls", "smtp_tls")
|
|
497
551
|
# email_format has a specific error message — keep inline
|
|
498
552
|
if cp.has_option(self._EMAIL_SECTION, "email_format"):
|
|
499
553
|
fmt = cp.get(self._EMAIL_SECTION, "email_format").lower()
|
|
500
554
|
if fmt not in ("plain", "html"):
|
|
501
555
|
raise ConfigError(f"Invalid email format: {fmt}")
|
|
502
556
|
self.email_format = fmt
|
|
503
|
-
self._get_str(cp, self._EMAIL_SECTION, "message_css", "email_css")
|
|
504
557
|
if cp.has_section(self._VARIABLES_SECTION) and variable_pool:
|
|
505
558
|
varsect = cp.items(self._VARIABLES_SECTION)
|
|
506
559
|
for sub, repl in varsect:
|
execsql/db/access.py
CHANGED
|
@@ -109,13 +109,15 @@ class AccessDatabase(Database):
|
|
|
109
109
|
try:
|
|
110
110
|
self.conn = pyodbc.connect(connstr)
|
|
111
111
|
except Exception:
|
|
112
|
-
_state.exec_log
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
if _state.exec_log is not None:
|
|
113
|
+
_state.exec_log.log_status_info(
|
|
114
|
+
f"Could not connect via ODBC using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
115
|
+
)
|
|
115
116
|
else:
|
|
116
|
-
_state.exec_log
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
if _state.exec_log is not None:
|
|
118
|
+
_state.exec_log.log_status_info(
|
|
119
|
+
f"Connected via ODBC using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
120
|
+
)
|
|
119
121
|
self.jet4 = jet4flag
|
|
120
122
|
return True
|
|
121
123
|
return False
|
|
@@ -162,9 +164,11 @@ class AccessDatabase(Database):
|
|
|
162
164
|
else:
|
|
163
165
|
self.dao_conn = daoEngine.OpenDatabase(self.db_name)
|
|
164
166
|
except Exception:
|
|
165
|
-
_state.exec_log
|
|
167
|
+
if _state.exec_log is not None:
|
|
168
|
+
_state.exec_log.log_status_info(f"Could not connect via DAO using: {engine}")
|
|
166
169
|
else:
|
|
167
|
-
_state.exec_log
|
|
170
|
+
if _state.exec_log is not None:
|
|
171
|
+
_state.exec_log.log_status_info(f"Connected via DAO using: {engine}")
|
|
168
172
|
return True
|
|
169
173
|
return False
|
|
170
174
|
|
|
@@ -307,23 +311,31 @@ class AccessDatabase(Database):
|
|
|
307
311
|
return headers, iter(dict_row, None)
|
|
308
312
|
|
|
309
313
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
310
|
-
"""Return True if the named table exists in the Access database.
|
|
314
|
+
"""Return True if the named table exists in the Access database.
|
|
315
|
+
|
|
316
|
+
Uses ODBC's catalog function (``cursor.tables()``) instead of
|
|
317
|
+
querying ``MSysObjects`` directly. Access 2016+ refuses ad-hoc
|
|
318
|
+
queries on the system catalog with the error
|
|
319
|
+
``Record(s) cannot be read; no read permission on 'MSysObjects'``;
|
|
320
|
+
the ODBC catalog path is permission-clean and matches the table
|
|
321
|
+
view that ``CREATE TABLE`` modifies, unlike DAO ``TableDefs``
|
|
322
|
+
which caches independently.
|
|
323
|
+
"""
|
|
311
324
|
self.dao_flush_check()
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return len(rows) > 0
|
|
325
|
+
try:
|
|
326
|
+
with self._cursor() as curs:
|
|
327
|
+
rows = list(curs.tables(table=table_name, tableType="TABLE"))
|
|
328
|
+
# Access identifiers are case-insensitive; the catalog
|
|
329
|
+
# returns whatever case the table was created with.
|
|
330
|
+
lname = table_name.lower()
|
|
331
|
+
return any(r.table_name.lower() == lname for r in rows)
|
|
332
|
+
except Exception as e:
|
|
333
|
+
raise ErrInfo(
|
|
334
|
+
type="db",
|
|
335
|
+
command_text=f"ODBC SQLTables for {table_name}",
|
|
336
|
+
exception_msg=exception_desc(),
|
|
337
|
+
other_msg=f"Failure on test for existence of Access table {table_name}",
|
|
338
|
+
) from e
|
|
327
339
|
|
|
328
340
|
def column_exists(
|
|
329
341
|
self,
|
|
@@ -352,23 +364,25 @@ class AccessDatabase(Database):
|
|
|
352
364
|
return [d[0] for d in curs.description]
|
|
353
365
|
|
|
354
366
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
355
|
-
"""Return True if the named view or query exists in the Access database.
|
|
367
|
+
"""Return True if the named view or query exists in the Access database.
|
|
368
|
+
|
|
369
|
+
Uses DAO ``QueryDefs`` (Access's saved queries play the role of views)
|
|
370
|
+
rather than querying ``MSysObjects`` directly — see :meth:`table_exists`
|
|
371
|
+
for the permission rationale.
|
|
372
|
+
"""
|
|
356
373
|
self.dao_flush_check()
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
) from e
|
|
370
|
-
rows = curs.fetchall()
|
|
371
|
-
return len(rows) > 0
|
|
374
|
+
try:
|
|
375
|
+
with self._cursor() as curs:
|
|
376
|
+
rows = list(curs.tables(table=view_name, tableType="VIEW"))
|
|
377
|
+
lname = view_name.lower()
|
|
378
|
+
return any(r.table_name.lower() == lname for r in rows)
|
|
379
|
+
except Exception as e:
|
|
380
|
+
raise ErrInfo(
|
|
381
|
+
type="db",
|
|
382
|
+
command_text=f"ODBC SQLTables for view {view_name}",
|
|
383
|
+
exception_msg=exception_desc(),
|
|
384
|
+
other_msg=f"Test for existence of Access view/query {view_name}",
|
|
385
|
+
) from e
|
|
372
386
|
|
|
373
387
|
def schema_exists(self, schema_name: str) -> bool:
|
|
374
388
|
"""Return False; Access does not support schemas."""
|