execsql2 2.1.2__py3-none-any.whl → 2.4.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/cli/__init__.py +436 -0
- execsql/cli/dsn.py +86 -0
- execsql/cli/help.py +140 -0
- execsql/{cli.py → cli/run.py} +14 -589
- execsql/config.py +65 -1
- execsql/db/access.py +27 -15
- execsql/db/base.py +328 -215
- execsql/db/dsn.py +10 -5
- execsql/db/duckdb.py +6 -2
- execsql/db/factory.py +21 -0
- execsql/db/firebird.py +27 -19
- execsql/db/mysql.py +12 -7
- execsql/db/oracle.py +15 -11
- execsql/db/postgres.py +31 -16
- execsql/db/sqlite.py +15 -11
- execsql/db/sqlserver.py +16 -5
- execsql/exceptions.py +25 -7
- execsql/exporters/base.py +12 -1
- execsql/exporters/delimited.py +80 -35
- execsql/exporters/duckdb.py +6 -2
- execsql/exporters/feather.py +10 -6
- execsql/exporters/html.py +89 -69
- execsql/exporters/json.py +52 -45
- execsql/exporters/latex.py +37 -27
- execsql/exporters/ods.py +32 -11
- execsql/exporters/parquet.py +5 -2
- execsql/exporters/pretty.py +16 -9
- execsql/exporters/raw.py +22 -16
- execsql/exporters/sqlite.py +6 -2
- execsql/exporters/templates.py +39 -21
- execsql/exporters/values.py +26 -20
- execsql/exporters/xls.py +30 -11
- execsql/exporters/xml.py +31 -13
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +6 -4
- execsql/importers/csv.py +8 -6
- execsql/importers/feather.py +6 -4
- execsql/importers/ods.py +6 -4
- execsql/importers/xls.py +6 -4
- execsql/metacommands/__init__.py +208 -1548
- execsql/metacommands/conditions.py +101 -27
- execsql/metacommands/control.py +8 -4
- execsql/metacommands/data.py +6 -6
- execsql/metacommands/debug.py +6 -2
- execsql/metacommands/dispatch.py +2011 -0
- execsql/metacommands/io.py +67 -1310
- execsql/metacommands/io_export.py +442 -0
- execsql/metacommands/io_fileops.py +287 -0
- execsql/metacommands/io_import.py +398 -0
- execsql/metacommands/io_write.py +248 -0
- execsql/metacommands/prompt.py +22 -66
- execsql/metacommands/system.py +7 -2
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/py.typed +0 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +184 -402
- execsql/script/variables.py +281 -0
- execsql/types.py +49 -20
- execsql/utils/auth.py +2 -0
- execsql/utils/crypto.py +4 -6
- execsql/utils/datetime.py +1 -0
- execsql/utils/errors.py +11 -0
- execsql/utils/fileio.py +33 -8
- execsql/utils/gui.py +46 -0
- execsql/utils/mail.py +7 -17
- execsql/utils/numeric.py +2 -0
- execsql/utils/regex.py +9 -0
- execsql/utils/strings.py +16 -0
- execsql/utils/timer.py +2 -0
- execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/METADATA +13 -6
- execsql2-2.4.0.dist-info/RECORD +108 -0
- execsql2-2.1.2.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.1.2.dist-info/RECORD +0 -96
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Variable containers for execsql script execution.
|
|
4
|
+
|
|
5
|
+
Classes:
|
|
6
|
+
- :class:`CounterVars` — named auto-incrementing integer counters.
|
|
7
|
+
- :class:`SubVarSet` — global ``!!$VAR!!`` substitution-variable store.
|
|
8
|
+
- :class:`LocalSubVarSet` — per-script ``~``-prefixed local variable overlay.
|
|
9
|
+
- :class:`ScriptArgSubVarSet` — per-script ``#``-prefixed argument overlay.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from execsql.exceptions import ErrInfo
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# CounterVars
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CounterVars:
|
|
25
|
+
"""Named auto-incrementing integer counters referenced as ``!!$COUNTER_N!!``."""
|
|
26
|
+
|
|
27
|
+
# A dictionary of dynamically created named counter variables.
|
|
28
|
+
_COUNTER_RX = re.compile(r"!!\$(COUNTER_\d+)!!", re.I)
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self.counters: dict[str, int] = {}
|
|
32
|
+
|
|
33
|
+
def _ctrid(self, ctr_no: int) -> str:
|
|
34
|
+
return f"counter_{ctr_no}"
|
|
35
|
+
|
|
36
|
+
def set_counter(self, ctr_no: int, ctr_val: int) -> None:
|
|
37
|
+
self.counters[self._ctrid(ctr_no)] = ctr_val
|
|
38
|
+
|
|
39
|
+
def remove_counter(self, ctr_no: int) -> None:
|
|
40
|
+
ctr_id = self._ctrid(ctr_no)
|
|
41
|
+
if ctr_id in self.counters:
|
|
42
|
+
del self.counters[ctr_id]
|
|
43
|
+
|
|
44
|
+
def remove_all_counters(self) -> None:
|
|
45
|
+
self.counters = {}
|
|
46
|
+
|
|
47
|
+
def substitute(self, command_str: str) -> tuple:
|
|
48
|
+
# Substitutes any counter variable references with the counter value and
|
|
49
|
+
# returns the modified command string and a flag indicating replacements.
|
|
50
|
+
m = self._COUNTER_RX.search(command_str, re.I)
|
|
51
|
+
if m:
|
|
52
|
+
ctr_id = m.group(1).lower()
|
|
53
|
+
if ctr_id not in self.counters:
|
|
54
|
+
self.counters[ctr_id] = 0
|
|
55
|
+
new_count = self.counters[ctr_id] + 1
|
|
56
|
+
self.counters[ctr_id] = new_count
|
|
57
|
+
return command_str.replace("!!$" + m.group(1) + "!!", str(new_count)), True
|
|
58
|
+
return command_str, False
|
|
59
|
+
|
|
60
|
+
def substitute_all(self, any_text: str) -> tuple:
|
|
61
|
+
subbed = True
|
|
62
|
+
any_subbed = False
|
|
63
|
+
while subbed:
|
|
64
|
+
any_text, subbed = self.substitute(any_text)
|
|
65
|
+
if subbed:
|
|
66
|
+
any_subbed = True
|
|
67
|
+
return any_text, any_subbed
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# SubVarSet / LocalSubVarSet / ScriptArgSubVarSet
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SubVarSet:
|
|
76
|
+
"""Pool of ``!!$VAR!!``-style substitution variables.
|
|
77
|
+
|
|
78
|
+
Variable names are stored in lowercase. Supports ``$``, ``&``, and ``@``
|
|
79
|
+
prefixes by default. Use :meth:`add_substitution` / :meth:`remove_substitution`
|
|
80
|
+
to manage entries, and :meth:`substitute_all` to expand a string.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
# A pool of substitution variables. Each variable consists of a name and
|
|
84
|
+
# a (string) value. All variable names are stored as lowercase text.
|
|
85
|
+
# Internally uses a dict for O(1) lookups; the ``substitutions`` property
|
|
86
|
+
# exposes the data as a list of ``(name, value)`` tuples for backward
|
|
87
|
+
# compatibility with external code.
|
|
88
|
+
def __init__(self) -> None:
|
|
89
|
+
self._subs_dict: dict[str, Any] = {}
|
|
90
|
+
self._compiled_patterns: dict[str, tuple] = {}
|
|
91
|
+
self.prefix_list: list[str] = ["$", "&", "@"]
|
|
92
|
+
# Don't construct/compile on init because deepcopy() can't handle compiled regexes.
|
|
93
|
+
self.var_rx = None
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def substitutions(self) -> list[tuple]:
|
|
97
|
+
"""Backward-compatible view of substitutions as a list of (name, value) tuples."""
|
|
98
|
+
return list(self._subs_dict.items())
|
|
99
|
+
|
|
100
|
+
@substitutions.setter
|
|
101
|
+
def substitutions(self, value: Any) -> None:
|
|
102
|
+
"""Accept a list of (name, value) tuples or a dict and populate the internal dict."""
|
|
103
|
+
if isinstance(value, dict):
|
|
104
|
+
self._subs_dict = dict(value)
|
|
105
|
+
else:
|
|
106
|
+
self._subs_dict = dict(value)
|
|
107
|
+
self._rebuild_all_patterns()
|
|
108
|
+
|
|
109
|
+
def _compile_patterns_for(self, varname: str) -> tuple:
|
|
110
|
+
"""Compile and return the three regex patterns (plain, single-quoted, double-quoted) for *varname*."""
|
|
111
|
+
match_escaped = "\\" + varname if varname[0] == "$" else varname
|
|
112
|
+
pat = re.compile(f"!!{match_escaped}!!", re.I)
|
|
113
|
+
patq = re.compile(f"!'!{match_escaped}!'!", re.I)
|
|
114
|
+
patdq = re.compile(f'!"!{match_escaped}!"!', re.I)
|
|
115
|
+
return (pat, patq, patdq)
|
|
116
|
+
|
|
117
|
+
def _rebuild_all_patterns(self) -> None:
|
|
118
|
+
"""Rebuild compiled patterns for every variable currently stored."""
|
|
119
|
+
self._compiled_patterns = {}
|
|
120
|
+
for varname in self._subs_dict:
|
|
121
|
+
self._compiled_patterns[varname] = self._compile_patterns_for(varname)
|
|
122
|
+
|
|
123
|
+
def compile_var_rx(self) -> None:
|
|
124
|
+
"""Compile the variable-name validation regex from the current prefix list."""
|
|
125
|
+
self.var_rx_str = r"^[" + "".join(self.prefix_list) + r"]?\w+$"
|
|
126
|
+
self.var_rx = re.compile(self.var_rx_str, re.I)
|
|
127
|
+
|
|
128
|
+
def var_name_ok(self, varname: str) -> bool:
|
|
129
|
+
if self.var_rx is None:
|
|
130
|
+
self.compile_var_rx()
|
|
131
|
+
return self.var_rx.match(varname) is not None
|
|
132
|
+
|
|
133
|
+
def check_var_name(self, varname: str) -> None:
|
|
134
|
+
if not self.var_name_ok(varname.lower()):
|
|
135
|
+
raise ErrInfo("error", other_msg=f"Invalid variable name ({varname}) in this context.")
|
|
136
|
+
|
|
137
|
+
def remove_substitution(self, template_str: str) -> None:
|
|
138
|
+
"""Remove the variable named *template_str* from the substitution pool."""
|
|
139
|
+
self.check_var_name(template_str)
|
|
140
|
+
old_sub = template_str.lower()
|
|
141
|
+
self._subs_dict.pop(old_sub, None)
|
|
142
|
+
self._compiled_patterns.pop(old_sub, None)
|
|
143
|
+
|
|
144
|
+
def add_substitution(self, varname: str, repl_str: Any) -> None:
|
|
145
|
+
"""Add or overwrite a substitution variable, compiling its match patterns."""
|
|
146
|
+
self.check_var_name(varname)
|
|
147
|
+
varname = varname.lower()
|
|
148
|
+
self._subs_dict[varname] = repl_str
|
|
149
|
+
self._compiled_patterns[varname] = self._compile_patterns_for(varname)
|
|
150
|
+
|
|
151
|
+
def append_substitution(self, varname: str, repl_str: str) -> None:
|
|
152
|
+
self.check_var_name(varname)
|
|
153
|
+
varname = varname.lower()
|
|
154
|
+
if varname in self._subs_dict:
|
|
155
|
+
self.add_substitution(varname, f"{self._subs_dict[varname]}\n{repl_str}")
|
|
156
|
+
else:
|
|
157
|
+
self.add_substitution(varname, repl_str)
|
|
158
|
+
|
|
159
|
+
def varvalue(self, varname: str) -> str | None:
|
|
160
|
+
"""Return the value of *varname*, or ``None`` if it is not defined."""
|
|
161
|
+
self.check_var_name(varname)
|
|
162
|
+
return self._subs_dict.get(varname.lower())
|
|
163
|
+
|
|
164
|
+
def increment_by(self, varname: str, numeric_increment: Any) -> None:
|
|
165
|
+
self.check_var_name(varname)
|
|
166
|
+
varvalue = self.varvalue(varname)
|
|
167
|
+
if varvalue is None:
|
|
168
|
+
varvalue = "0"
|
|
169
|
+
self.add_substitution(varname, varvalue)
|
|
170
|
+
# Import as_numeric lazily to avoid circular dependency
|
|
171
|
+
from execsql.utils.numeric import as_numeric
|
|
172
|
+
|
|
173
|
+
numvalue = as_numeric(varvalue)
|
|
174
|
+
numinc = as_numeric(numeric_increment)
|
|
175
|
+
if numvalue is None or numinc is None:
|
|
176
|
+
newval = f"{varvalue}+{numeric_increment}"
|
|
177
|
+
else:
|
|
178
|
+
newval = str(numvalue + numinc)
|
|
179
|
+
self.add_substitution(varname, newval)
|
|
180
|
+
|
|
181
|
+
def sub_exists(self, template_str: str) -> bool:
|
|
182
|
+
"""Return True if the variable named *template_str* is defined."""
|
|
183
|
+
self.check_var_name(template_str)
|
|
184
|
+
return template_str.lower() in self._subs_dict
|
|
185
|
+
|
|
186
|
+
def merge(self, other_subvars: SubVarSet | None) -> SubVarSet:
|
|
187
|
+
"""Return a new SubVarSet with this object's variables merged with other_subvars."""
|
|
188
|
+
if other_subvars is not None:
|
|
189
|
+
newsubs = SubVarSet()
|
|
190
|
+
newsubs._subs_dict = dict(self._subs_dict)
|
|
191
|
+
newsubs._compiled_patterns = dict(self._compiled_patterns)
|
|
192
|
+
newsubs.prefix_list = list(set(self.prefix_list + other_subvars.prefix_list))
|
|
193
|
+
newsubs.compile_var_rx()
|
|
194
|
+
for varname, value in other_subvars._subs_dict.items():
|
|
195
|
+
newsubs.add_substitution(varname, value)
|
|
196
|
+
return newsubs
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
# Combined regex matching any variable token in one pass.
|
|
200
|
+
# Group 1: quote marker — empty for plain !!var!!, ' for !'!var!'!, " for !"!var!"!
|
|
201
|
+
# Group 2: variable name (with optional prefix character)
|
|
202
|
+
_TOKEN_RX = re.compile(
|
|
203
|
+
r"!(?P<q>['\"]?)!(?P<varname>[$&@~#]?\w+)!(?P=q)!",
|
|
204
|
+
re.I,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def substitute(self, command_str: str) -> tuple:
|
|
208
|
+
"""Replace the first matching variable token in *command_str*.
|
|
209
|
+
|
|
210
|
+
Uses a single combined regex to find any ``!!var!!``, ``!'!var!'!``, or
|
|
211
|
+
``!"!var!"!`` token in one pass, then looks up the variable name in the
|
|
212
|
+
dict. This is O(1) per call instead of O(V) where V is the number of
|
|
213
|
+
defined variables.
|
|
214
|
+
|
|
215
|
+
Returns ``(modified_string, True)`` if a substitution was made, or
|
|
216
|
+
``(original_string, False)`` if no variable pattern matched.
|
|
217
|
+
"""
|
|
218
|
+
if not isinstance(command_str, str):
|
|
219
|
+
return command_str, False
|
|
220
|
+
m = self._TOKEN_RX.search(command_str)
|
|
221
|
+
while m:
|
|
222
|
+
varname = m.group("varname").lower()
|
|
223
|
+
if varname in self._subs_dict:
|
|
224
|
+
sub = self._subs_dict[varname]
|
|
225
|
+
if sub is None:
|
|
226
|
+
sub = ""
|
|
227
|
+
sub = str(sub)
|
|
228
|
+
if os.name != "posix":
|
|
229
|
+
sub = sub.replace("\\", "\\\\")
|
|
230
|
+
quote = m.group("q")
|
|
231
|
+
if quote == "'":
|
|
232
|
+
sub = sub.replace("'", "''")
|
|
233
|
+
elif quote == '"':
|
|
234
|
+
sub = '"' + sub + '"'
|
|
235
|
+
return command_str[: m.start()] + sub + command_str[m.end() :], True
|
|
236
|
+
# Token found but variable not defined — skip it and keep searching.
|
|
237
|
+
m = self._TOKEN_RX.search(command_str, m.end())
|
|
238
|
+
return command_str, False
|
|
239
|
+
|
|
240
|
+
def substitute_all(self, any_text: str) -> tuple:
|
|
241
|
+
"""Repeatedly apply :meth:`substitute` until no more substitutions remain.
|
|
242
|
+
|
|
243
|
+
Returns ``(fully_expanded_string, any_substitution_made)``.
|
|
244
|
+
"""
|
|
245
|
+
subbed = True
|
|
246
|
+
any_subbed = False
|
|
247
|
+
while subbed:
|
|
248
|
+
any_text, subbed = self.substitute(any_text)
|
|
249
|
+
if subbed:
|
|
250
|
+
any_subbed = True
|
|
251
|
+
return any_text, any_subbed
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class LocalSubVarSet(SubVarSet):
|
|
255
|
+
"""Substitution-variable pool restricted to ``~``-prefixed local variables."""
|
|
256
|
+
|
|
257
|
+
# A pool of local substitution variables.
|
|
258
|
+
# Only '~' is allowed as a prefix and MUST be present.
|
|
259
|
+
def __init__(self) -> None:
|
|
260
|
+
SubVarSet.__init__(self)
|
|
261
|
+
self.prefix_list = ["~"]
|
|
262
|
+
|
|
263
|
+
def compile_var_rx(self) -> None:
|
|
264
|
+
# Prefix is required, not optional.
|
|
265
|
+
self.var_rx_str = r"^[" + "".join(self.prefix_list) + r"]\w+$"
|
|
266
|
+
self.var_rx = re.compile(self.var_rx_str, re.I)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ScriptArgSubVarSet(SubVarSet):
|
|
270
|
+
"""Substitution-variable pool restricted to ``#``-prefixed script arguments."""
|
|
271
|
+
|
|
272
|
+
# A pool of script argument names.
|
|
273
|
+
# Only '#' is allowed as a prefix and MUST be present.
|
|
274
|
+
def __init__(self) -> None:
|
|
275
|
+
SubVarSet.__init__(self)
|
|
276
|
+
self.prefix_list = ["#"]
|
|
277
|
+
|
|
278
|
+
def compile_var_rx(self) -> None:
|
|
279
|
+
# Prefix is required, not optional.
|
|
280
|
+
self.var_rx_str = r"^[" + "".join(self.prefix_list) + r"]\w+$"
|
|
281
|
+
self.var_rx = re.compile(self.var_rx_str, re.I)
|
execsql/types.py
CHANGED
|
@@ -39,6 +39,35 @@ from decimal import Decimal
|
|
|
39
39
|
from execsql.exceptions import DataTypeError, DbTypeError
|
|
40
40
|
from execsql.utils.numeric import leading_zero_num
|
|
41
41
|
|
|
42
|
+
__all__ = [
|
|
43
|
+
"DataType",
|
|
44
|
+
"Tz",
|
|
45
|
+
"DT_TimestampTZ",
|
|
46
|
+
"DT_Timestamp",
|
|
47
|
+
"DT_Date",
|
|
48
|
+
"DT_Time",
|
|
49
|
+
"DT_Time_Oracle",
|
|
50
|
+
"DT_Boolean",
|
|
51
|
+
"DT_Integer",
|
|
52
|
+
"DT_Long",
|
|
53
|
+
"DT_Float",
|
|
54
|
+
"DT_Decimal",
|
|
55
|
+
"DT_Character",
|
|
56
|
+
"DT_Varchar",
|
|
57
|
+
"DT_Text",
|
|
58
|
+
"DT_Binary",
|
|
59
|
+
"DbType",
|
|
60
|
+
"dbt_postgres",
|
|
61
|
+
"dbt_sqlite",
|
|
62
|
+
"dbt_duckdb",
|
|
63
|
+
"dbt_sqlserver",
|
|
64
|
+
"dbt_access",
|
|
65
|
+
"dbt_dsn",
|
|
66
|
+
"dbt_mysql",
|
|
67
|
+
"dbt_firebird",
|
|
68
|
+
"dbt_oracle",
|
|
69
|
+
]
|
|
70
|
+
|
|
42
71
|
|
|
43
72
|
class DataType:
|
|
44
73
|
data_type_name = None
|
|
@@ -88,8 +117,8 @@ class DataType:
|
|
|
88
117
|
return data
|
|
89
118
|
try:
|
|
90
119
|
i = self.data_type(data)
|
|
91
|
-
except Exception:
|
|
92
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
raise DataTypeError(self.data_type_name, self._CONV_ERR % data) from e
|
|
93
122
|
return i
|
|
94
123
|
|
|
95
124
|
|
|
@@ -354,8 +383,8 @@ class DT_Integer(DataType):
|
|
|
354
383
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
355
384
|
try:
|
|
356
385
|
i = int(data)
|
|
357
|
-
except Exception:
|
|
358
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
386
|
+
except Exception as e:
|
|
387
|
+
raise DataTypeError(self.data_type_name, self._CONV_ERR % data) from e
|
|
359
388
|
if leading_zero_num(data):
|
|
360
389
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
361
390
|
return i
|
|
@@ -390,8 +419,8 @@ class DT_Long(DataType):
|
|
|
390
419
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
391
420
|
try:
|
|
392
421
|
i = int(data)
|
|
393
|
-
except Exception:
|
|
394
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
422
|
+
except Exception as e:
|
|
423
|
+
raise DataTypeError(self.data_type_name, self._CONV_ERR % data) from e
|
|
395
424
|
return i
|
|
396
425
|
|
|
397
426
|
|
|
@@ -428,8 +457,8 @@ class DT_Float(DataType):
|
|
|
428
457
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
429
458
|
try:
|
|
430
459
|
i = float(data)
|
|
431
|
-
except Exception:
|
|
432
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
460
|
+
except Exception as e:
|
|
461
|
+
raise DataTypeError(self.data_type_name, self._CONV_ERR % data) from e
|
|
433
462
|
return i
|
|
434
463
|
|
|
435
464
|
|
|
@@ -464,8 +493,8 @@ class DT_Decimal(DataType):
|
|
|
464
493
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
465
494
|
try:
|
|
466
495
|
dec = Decimal(data)
|
|
467
|
-
except Exception:
|
|
468
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
496
|
+
except Exception as e:
|
|
497
|
+
raise DataTypeError(self.data_type_name, self._CONV_ERR % data) from e
|
|
469
498
|
self.set_scale_prec(dec)
|
|
470
499
|
return dec
|
|
471
500
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
@@ -493,8 +522,8 @@ class DT_Character(DataType):
|
|
|
493
522
|
if not isinstance(data, str):
|
|
494
523
|
try:
|
|
495
524
|
data = data_type(data)
|
|
496
|
-
except ValueError:
|
|
497
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
525
|
+
except ValueError as e:
|
|
526
|
+
raise DataTypeError(self.data_type_name, self._CONV_ERR % data) from e
|
|
498
527
|
if len(data) > 255:
|
|
499
528
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
500
529
|
return data
|
|
@@ -521,8 +550,8 @@ class DT_Varchar(DataType):
|
|
|
521
550
|
if isinstance(data, str):
|
|
522
551
|
try:
|
|
523
552
|
data = data_type(data)
|
|
524
|
-
except ValueError:
|
|
525
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
553
|
+
except ValueError as e:
|
|
554
|
+
raise DataTypeError(self.data_type_name, self._CONV_ERR % data) from e
|
|
526
555
|
if len(data) > 255:
|
|
527
556
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
528
557
|
return data
|
|
@@ -546,8 +575,8 @@ class DT_Text(DataType):
|
|
|
546
575
|
if not isinstance(data, str):
|
|
547
576
|
try:
|
|
548
577
|
data = data_type(data)
|
|
549
|
-
except ValueError:
|
|
550
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
578
|
+
except ValueError as e:
|
|
579
|
+
raise DataTypeError(self.data_type_name, self._CONV_ERR % data) from e
|
|
551
580
|
return data
|
|
552
581
|
|
|
553
582
|
|
|
@@ -605,12 +634,12 @@ class DbType:
|
|
|
605
634
|
# A convenience function to simplify access to data type names.
|
|
606
635
|
try:
|
|
607
636
|
return self.dialect[data_type][0]
|
|
608
|
-
except Exception:
|
|
637
|
+
except Exception as e:
|
|
609
638
|
raise DbTypeError(
|
|
610
639
|
self.dbms_id,
|
|
611
640
|
data_type,
|
|
612
641
|
f"{self.dbms_id} DBMS type has no specification for data type {data_type.data_type_name}",
|
|
613
|
-
)
|
|
642
|
+
) from e
|
|
614
643
|
|
|
615
644
|
def quoted(self, dbms_object: str) -> str:
|
|
616
645
|
if re.search(r"\W", dbms_object):
|
|
@@ -638,12 +667,12 @@ class DbType:
|
|
|
638
667
|
data_type = self.spec_type(data_type)
|
|
639
668
|
try:
|
|
640
669
|
dts = self.dialect[data_type]
|
|
641
|
-
except Exception:
|
|
670
|
+
except Exception as e:
|
|
642
671
|
raise DbTypeError(
|
|
643
672
|
self.dbms_id,
|
|
644
673
|
data_type,
|
|
645
674
|
f"{self.dbms_id} DBMS type has no specification for data type {data_type.data_type_name}",
|
|
646
|
-
)
|
|
675
|
+
) from e
|
|
647
676
|
if max_len and max_len > 0 and dts[1]:
|
|
648
677
|
spec = f"{self.quoted(column_name)} {dts[0]}({max_len})"
|
|
649
678
|
elif data_type.precspec and precision and scale:
|
execsql/utils/auth.py
CHANGED
|
@@ -29,6 +29,8 @@ import getpass
|
|
|
29
29
|
|
|
30
30
|
import execsql.state as _state
|
|
31
31
|
|
|
32
|
+
__all__ = ["get_password", "clear_stored_password", "password_from_keyring"]
|
|
33
|
+
|
|
32
34
|
# Tracks whether the most recent get_password() call returned a keyring-stored value.
|
|
33
35
|
_last_from_keyring: bool = False
|
|
34
36
|
|
execsql/utils/crypto.py
CHANGED
|
@@ -21,9 +21,13 @@ that use XOR against a fixed key table followed by base64 encoding.
|
|
|
21
21
|
The monolith (line 2301) called this "SIMPLE ENCRYPTION".
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
import base64
|
|
25
|
+
import itertools
|
|
24
26
|
import random
|
|
25
27
|
import uuid
|
|
26
28
|
|
|
29
|
+
__all__ = ["Encrypt"]
|
|
30
|
+
|
|
27
31
|
|
|
28
32
|
class Encrypt:
|
|
29
33
|
"""Reversible XOR-based obfuscation for configuration file passwords.
|
|
@@ -55,12 +59,6 @@ class Encrypt:
|
|
|
55
59
|
def __repr__(self) -> str:
|
|
56
60
|
return "Encrypt()"
|
|
57
61
|
|
|
58
|
-
def __init__(self) -> None:
|
|
59
|
-
global itertools
|
|
60
|
-
global base64
|
|
61
|
-
import itertools
|
|
62
|
-
import base64
|
|
63
|
-
|
|
64
62
|
def xor(self, text: str, enckey: str) -> str:
|
|
65
63
|
return "".join(chr(ord(t) ^ ord(k)) for t, k in zip(text, itertools.cycle(enckey)))
|
|
66
64
|
|
execsql/utils/datetime.py
CHANGED
execsql/utils/errors.py
CHANGED
|
@@ -25,6 +25,17 @@ from typing import Any
|
|
|
25
25
|
import execsql.state as _state
|
|
26
26
|
from execsql.exceptions import ErrInfo
|
|
27
27
|
|
|
28
|
+
__all__ = [
|
|
29
|
+
"exception_info",
|
|
30
|
+
"exception_desc",
|
|
31
|
+
"exit_now",
|
|
32
|
+
"fatal_error",
|
|
33
|
+
"write_warning",
|
|
34
|
+
"file_size_date",
|
|
35
|
+
"chainfuncs",
|
|
36
|
+
"as_none",
|
|
37
|
+
]
|
|
38
|
+
|
|
28
39
|
|
|
29
40
|
def exception_info() -> tuple:
|
|
30
41
|
# Returns the exception type, value, source file name, source line number, and source line text.
|
execsql/utils/fileio.py
CHANGED
|
@@ -37,6 +37,24 @@ from typing import Any
|
|
|
37
37
|
|
|
38
38
|
from execsql.exceptions import ErrInfo
|
|
39
39
|
|
|
40
|
+
__all__ = [
|
|
41
|
+
"make_export_dirs",
|
|
42
|
+
"check_dir",
|
|
43
|
+
"FileWriter",
|
|
44
|
+
"EncodedFile",
|
|
45
|
+
"Logger",
|
|
46
|
+
"TempFileMgr",
|
|
47
|
+
"list_encodings",
|
|
48
|
+
"filewriter_filestatus",
|
|
49
|
+
"filewriter_write",
|
|
50
|
+
"filewriter_open_as_new",
|
|
51
|
+
"filewriter_close",
|
|
52
|
+
"filewriter_close_all_after_write",
|
|
53
|
+
"filewriter_closeall",
|
|
54
|
+
"filewriter_shutdown",
|
|
55
|
+
"filewriter_end",
|
|
56
|
+
]
|
|
57
|
+
|
|
40
58
|
|
|
41
59
|
def make_export_dirs(outfile: str) -> None:
|
|
42
60
|
if outfile.lower() != "stdout":
|
|
@@ -49,8 +67,8 @@ def make_export_dirs(outfile: str) -> None:
|
|
|
49
67
|
except OSError as e:
|
|
50
68
|
if e.errno != errno.EEXIST:
|
|
51
69
|
raise ErrInfo("exception", exception_msg=emsg) from e
|
|
52
|
-
except Exception:
|
|
53
|
-
raise ErrInfo("exception", exception_msg=emsg)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise ErrInfo("exception", exception_msg=emsg) from e
|
|
54
72
|
|
|
55
73
|
|
|
56
74
|
def check_dir(filename: str) -> None:
|
|
@@ -265,14 +283,11 @@ class FileWriter(multiprocessing.Process):
|
|
|
265
283
|
# is a command and the second is a tuple of arguments for the function indicated
|
|
266
284
|
# by that command.
|
|
267
285
|
while self.active:
|
|
268
|
-
command = None
|
|
269
|
-
argtuple = None
|
|
270
286
|
try:
|
|
271
|
-
command, argtuple = self.input_queue.
|
|
287
|
+
command, argtuple = self.input_queue.get(timeout=0.1)
|
|
272
288
|
except queue.Empty:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
self.execvec[command](*argtuple)
|
|
289
|
+
continue
|
|
290
|
+
self.execvec[command](*argtuple)
|
|
276
291
|
|
|
277
292
|
|
|
278
293
|
# Subprocess objects for asynchronous writing to text files.
|
|
@@ -539,6 +554,16 @@ class Logger:
|
|
|
539
554
|
wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\twarning\t{msg or ''}\n"
|
|
540
555
|
self.writelog(wmsg)
|
|
541
556
|
|
|
557
|
+
def log_sql_query(self, sql: str, db_name: str, line_no: int | None = None) -> None:
|
|
558
|
+
"""Log an executed SQL statement for audit purposes."""
|
|
559
|
+
cleaned = sql.replace("\n", " ").replace("\t", " ")
|
|
560
|
+
if len(cleaned) > 2000:
|
|
561
|
+
cleaned = cleaned[:2000] + "..."
|
|
562
|
+
self.seq_no += 1
|
|
563
|
+
lno = line_no if line_no is not None else 0
|
|
564
|
+
wmsg = f"sql\t{self.run_id}\t{self.seq_no}\t{self._ts()}\t{db_name}\t{lno}\t{cleaned}\n"
|
|
565
|
+
self.writelog(wmsg)
|
|
566
|
+
|
|
542
567
|
def log_user_msg(self, msg: str | None) -> None:
|
|
543
568
|
msg = None if not msg else msg.replace("\n", "")
|
|
544
569
|
if msg != "":
|
execsql/utils/gui.py
CHANGED
|
@@ -21,6 +21,52 @@ from __future__ import annotations
|
|
|
21
21
|
import sys
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Constants
|
|
26
|
+
"GUI_HALT",
|
|
27
|
+
"GUI_MSG",
|
|
28
|
+
"GUI_PAUSE",
|
|
29
|
+
"GUI_DISPLAY",
|
|
30
|
+
"GUI_ENTRY",
|
|
31
|
+
"GUI_COMPARE",
|
|
32
|
+
"GUI_SELECTROWS",
|
|
33
|
+
"GUI_SELECTSUB",
|
|
34
|
+
"GUI_ACTION",
|
|
35
|
+
"GUI_MAP",
|
|
36
|
+
"GUI_OPENFILE",
|
|
37
|
+
"GUI_SAVEFILE",
|
|
38
|
+
"GUI_DIRECTORY",
|
|
39
|
+
"QUERY_CONSOLE",
|
|
40
|
+
"GUI_CREDENTIALS",
|
|
41
|
+
"GUI_CONNECT",
|
|
42
|
+
# Data-carrier classes
|
|
43
|
+
"GuiSpec",
|
|
44
|
+
"ConsoleUIError",
|
|
45
|
+
"ActionSpec",
|
|
46
|
+
"EntrySpec",
|
|
47
|
+
# Console lifecycle
|
|
48
|
+
"gui_console_isrunning",
|
|
49
|
+
"enable_gui",
|
|
50
|
+
"gui_console_on",
|
|
51
|
+
"gui_console_off",
|
|
52
|
+
"gui_console_hide",
|
|
53
|
+
"gui_console_show",
|
|
54
|
+
"gui_console_progress",
|
|
55
|
+
"gui_console_save",
|
|
56
|
+
"gui_console_status",
|
|
57
|
+
"gui_console_wait_user",
|
|
58
|
+
"gui_console_width",
|
|
59
|
+
"gui_console_height",
|
|
60
|
+
# Database connection GUI
|
|
61
|
+
"gui_connect",
|
|
62
|
+
"gui_credentials",
|
|
63
|
+
# Interactive prompts
|
|
64
|
+
"get_yn",
|
|
65
|
+
"get_yn_win",
|
|
66
|
+
"pause",
|
|
67
|
+
"pause_win",
|
|
68
|
+
]
|
|
69
|
+
|
|
24
70
|
# ---------------------------------------------------------------------------
|
|
25
71
|
# GUI command constants — used to identify request types in the GUI queue.
|
|
26
72
|
# ---------------------------------------------------------------------------
|
execsql/utils/mail.py
CHANGED
|
@@ -11,11 +11,18 @@ metacommand and the halt/cancel email-notification hooks.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import re
|
|
14
|
+
import smtplib
|
|
15
|
+
from email import encoders
|
|
16
|
+
from email.mime.base import MIMEBase
|
|
17
|
+
from email.mime.multipart import MIMEMultipart
|
|
18
|
+
from email.mime.text import MIMEText
|
|
14
19
|
from pathlib import Path
|
|
15
20
|
|
|
16
21
|
import execsql.state as _state
|
|
17
22
|
from execsql.exceptions import ErrInfo
|
|
18
23
|
|
|
24
|
+
__all__ = ["MailSpec", "Mailer"]
|
|
25
|
+
|
|
19
26
|
|
|
20
27
|
class Mailer:
|
|
21
28
|
def __repr__(self) -> str:
|
|
@@ -26,17 +33,6 @@ class Mailer:
|
|
|
26
33
|
self.smtpconn.quit()
|
|
27
34
|
|
|
28
35
|
def __init__(self) -> None:
|
|
29
|
-
global smtplib
|
|
30
|
-
global MIMEMultipart
|
|
31
|
-
global MIMEText
|
|
32
|
-
global MIMEBase
|
|
33
|
-
global encoders
|
|
34
|
-
import smtplib
|
|
35
|
-
from email.mime.multipart import MIMEMultipart
|
|
36
|
-
from email.mime.text import MIMEText
|
|
37
|
-
from email.mime.base import MIMEBase
|
|
38
|
-
from email import encoders
|
|
39
|
-
|
|
40
36
|
conf = _state.conf
|
|
41
37
|
if conf.smtp_host is None:
|
|
42
38
|
raise ErrInfo(type="error", other_msg="Can't send email; the email host is not configured.")
|
|
@@ -69,12 +65,6 @@ class Mailer:
|
|
|
69
65
|
content_filename: str | None = None,
|
|
70
66
|
attach_filename: str | None = None,
|
|
71
67
|
) -> None:
|
|
72
|
-
global smtplib
|
|
73
|
-
global MIMEMultipart
|
|
74
|
-
global MIMEText
|
|
75
|
-
global MIMEBase
|
|
76
|
-
global encoders
|
|
77
|
-
|
|
78
68
|
conf = _state.conf
|
|
79
69
|
if conf.email_format == "html":
|
|
80
70
|
msg = MIMEMultipart("alternative")
|
execsql/utils/numeric.py
CHANGED