execsql2 2.2.1__py3-none-any.whl → 2.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execsql/cli/run.py +11 -5
- execsql/config.py +52 -0
- execsql/db/access.py +11 -3
- execsql/db/base.py +180 -135
- execsql/db/dsn.py +4 -0
- execsql/db/duckdb.py +4 -0
- execsql/db/factory.py +31 -5
- execsql/db/firebird.py +4 -0
- execsql/db/mysql.py +18 -1
- execsql/db/oracle.py +4 -0
- execsql/db/postgres.py +3 -0
- execsql/db/sqlite.py +3 -0
- execsql/db/sqlserver.py +11 -2
- execsql/exceptions.py +18 -0
- execsql/exporters/base.py +6 -0
- execsql/exporters/delimited.py +36 -0
- execsql/exporters/duckdb.py +4 -0
- execsql/exporters/feather.py +4 -0
- execsql/exporters/html.py +6 -0
- execsql/exporters/json.py +5 -6
- execsql/exporters/latex.py +4 -0
- execsql/exporters/ods.py +28 -7
- execsql/exporters/parquet.py +3 -0
- execsql/exporters/pretty.py +5 -0
- execsql/exporters/raw.py +5 -3
- execsql/exporters/sqlite.py +4 -0
- execsql/exporters/templates.py +16 -6
- execsql/exporters/values.py +4 -0
- execsql/exporters/xls.py +26 -7
- execsql/exporters/xml.py +3 -0
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +5 -3
- execsql/importers/csv.py +7 -5
- execsql/importers/feather.py +6 -4
- execsql/importers/ods.py +2 -0
- execsql/importers/xls.py +2 -0
- execsql/metacommands/__init__.py +177 -1968
- execsql/metacommands/dispatch.py +2011 -0
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +144 -406
- execsql/script/variables.py +281 -0
- execsql/types.py +29 -0
- 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 +18 -0
- 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.1.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/METADATA +8 -1
- execsql2-2.4.1.dist-info/RECORD +108 -0
- execsql2-2.2.1.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.2.1.dist-info/RECORD +0 -104
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/WHEEL +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.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
|
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":
|
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
execsql/utils/regex.py
CHANGED
|
@@ -14,6 +14,15 @@ dispatch regexes at module load time:
|
|
|
14
14
|
|
|
15
15
|
import os
|
|
16
16
|
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ins_rxs",
|
|
19
|
+
"ins_quoted_rx",
|
|
20
|
+
"ins_schema_rxs",
|
|
21
|
+
"ins_table_rxs",
|
|
22
|
+
"ins_table_list_rxs",
|
|
23
|
+
"ins_fn_rxs",
|
|
24
|
+
]
|
|
25
|
+
|
|
17
26
|
|
|
18
27
|
def ins_rxs(rx_list: tuple, fragment1: object, fragment2: object) -> tuple:
|
|
19
28
|
# Returns a tuple of all strings consisting of elements of the 'rx_list' tuple
|
execsql/utils/strings.py
CHANGED
|
@@ -23,6 +23,22 @@ identifiers:
|
|
|
23
23
|
|
|
24
24
|
import re
|
|
25
25
|
|
|
26
|
+
__all__ = [
|
|
27
|
+
"clean_word",
|
|
28
|
+
"clean_words",
|
|
29
|
+
"trim_word",
|
|
30
|
+
"trim_words",
|
|
31
|
+
"fold_word",
|
|
32
|
+
"fold_words",
|
|
33
|
+
"dedup_words",
|
|
34
|
+
"is_doublequoted",
|
|
35
|
+
"unquoted",
|
|
36
|
+
"unquoted2",
|
|
37
|
+
"encodings_match",
|
|
38
|
+
"wo_quotes",
|
|
39
|
+
"get_subvarset",
|
|
40
|
+
]
|
|
41
|
+
|
|
26
42
|
|
|
27
43
|
def clean_word(word: str) -> str:
|
|
28
44
|
# Trim leading and trailing spaces and replaces all non-alphanumeric characters with an underscore.
|
execsql/utils/timer.py
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Templates for execsql
|
|
2
|
+
|
|
3
|
+
Several types of templates are provided that may be useful in conjunction with execsql. These are:
|
|
4
|
+
|
|
5
|
+
- **execsql.conf** — An annotated version of the configuration file that includes all configuration settings and notes on their usage.
|
|
6
|
+
|
|
7
|
+
- **script_template.sql** — A framework for SQL scripts that make use of several execsql features. It includes sections for custom configuration settings, custom logfile creation, and reporting of unexpected script exits (through user cancellation or errors).
|
|
8
|
+
|
|
9
|
+
- **config_settings.sqlite** and **example_config_prompt.sql** — A SQLite database containing specifications for all settings configurable with the CONFIG metacommand, in the form used by the PROMPT ENTRY_FORM metacommand, and a SQL script illustrating how this database can be used to prompt the user for some or all of the configuration settings. *execsql* version 1.63.0 or later is needed to use this script.
|
|
10
|
+
|
|
11
|
+
## Functional Scripts for execsql
|
|
12
|
+
|
|
13
|
+
These script files provide useful functionality that should be accessed by including these scripts in other SQL scripts and then calling the sub-scripts that they contain.
|
|
14
|
+
|
|
15
|
+
### Upsert Scripts
|
|
16
|
+
|
|
17
|
+
These scripts perform automated upsert operations on any tables of any database in a DBMS that supports the standard *information_schema* views. Currently that includes:
|
|
18
|
+
|
|
19
|
+
- **PostgreSQL:** `pg_upsert.sql`
|
|
20
|
+
- **MySQL/MariaDB:** `md_upsert.sql`
|
|
21
|
+
- **SQL Server:** `ss_upsert.sql`
|
|
22
|
+
|
|
23
|
+
These scripts perform the upsert operation by using standard SQL UPDATE and INSERT statements rather than DBMS-specific implementations of the SQL MERGE statement.
|
|
24
|
+
|
|
25
|
+
Features of these upsert scripts include:
|
|
26
|
+
|
|
27
|
+
- They can be applied to any table in any database without modification.
|
|
28
|
+
- They can be applied to multiple tables simultaneously, and will perform the upsert operations in top-down order to maintain referential integrity among tables.
|
|
29
|
+
- Prior to performing the upsert operation, they check for null values in the columns of each staging table that must be non-null in the corresponding base table.
|
|
30
|
+
- Prior to performing the upsert operation, they check for duplicate primary key values in the staging tables.
|
|
31
|
+
- Prior to performing the upsert operation, they check foreign keys against both base tables and any other appropriate staging tables.
|
|
32
|
+
- They will not attempt to perform the upsert operation on any table if there are any violations of the non-null checks, primary key checks, or foreign key checks.
|
|
33
|
+
- They produce a table that either a) summarizes the number of rows that violated each type of non-null and foreign-key check, or b) summarizes the number of rows updated and the number of rows inserted for each table.
|
|
34
|
+
- Optionally, they will display all the changes to be made in a GUI interface, prompting the user to approve each update and insert operation.
|
|
35
|
+
- Optionally, they will record all operations carried out in a custom log file; this log may include the SQL statements executed and the data values that were added or changed.
|
|
36
|
+
- If an execsql console is active, they will use the console's status bar and progress bar to indicate the activity underway.
|
|
37
|
+
|
|
38
|
+
Complete documentation is available at [Read The Docs (execsql-upsert)](https://execsql-upsert.readthedocs.io/en/latest/).
|
|
39
|
+
|
|
40
|
+
### Table Comparison Scripts
|
|
41
|
+
|
|
42
|
+
These scripts generate SQL that can be used to identify differences in the content of two tables with equivalent structure. These are specifically intended to be base and staging tables. Running the SQL provided by these scripts will provide different summaries of the types of changes that would be made to the base tables by upserting the staging tables.
|
|
43
|
+
|
|
44
|
+
These scripts work with any tables of any database in a DBMS that supports the standard *information_schema* views. Currently that includes:
|
|
45
|
+
|
|
46
|
+
- **PostgreSQL:** `pg_compare.sql`
|
|
47
|
+
- **MySQL/MariaDB:** `md_compare.sql`
|
|
48
|
+
- **SQL Server:** `ss_compare.sql`
|
|
49
|
+
|
|
50
|
+
Complete documentation is available at [Read The Docs (execsql-compare)](https://execsql-compare.readthedocs.io/en/latest/).
|
|
51
|
+
|
|
52
|
+
### Glossary Creation Scripts
|
|
53
|
+
|
|
54
|
+
These scripts create a glossary of column names or other terms that can be exported to accompany a data summary. These scripts work with any tables of any database in a DBMS that supports the standard *information_schema* views. Currently that includes:
|
|
55
|
+
|
|
56
|
+
- **PostgreSQL:** `pg_glossary.sql`
|
|
57
|
+
- **MySQL/MariaDB:** `md_glossary.sql`
|
|
58
|
+
- **SQL Server:** `ss_glossary.sql`
|
|
59
|
+
|
|
60
|
+
Complete documentation is available at [Read The Docs (execsql-glossary)](https://execsql-compare.readthedocs.io/en/latest/).
|
|
61
|
+
|
|
62
|
+
## Roadmap
|
|
63
|
+
|
|
64
|
+
- [ ] Integrate documentation for [execsql-compare](https://execsql-compare.readthedocs.io/en/latest/) into the main execsql2 documentation site.
|
|
65
|
+
- [ ] Integrate documentation for [execsql-upsert](https://execsql-upsert.readthedocs.io/en/latest/) into the main execsql2 documentation site.
|