execsql2 2.2.1__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.
Files changed (78) hide show
  1. execsql/config.py +52 -0
  2. execsql/db/access.py +11 -3
  3. execsql/db/base.py +180 -135
  4. execsql/db/dsn.py +4 -0
  5. execsql/db/duckdb.py +4 -0
  6. execsql/db/factory.py +21 -0
  7. execsql/db/firebird.py +4 -0
  8. execsql/db/mysql.py +4 -0
  9. execsql/db/oracle.py +4 -0
  10. execsql/db/postgres.py +3 -0
  11. execsql/db/sqlite.py +3 -0
  12. execsql/db/sqlserver.py +11 -2
  13. execsql/exceptions.py +18 -0
  14. execsql/exporters/base.py +6 -0
  15. execsql/exporters/delimited.py +36 -0
  16. execsql/exporters/duckdb.py +4 -0
  17. execsql/exporters/feather.py +4 -0
  18. execsql/exporters/html.py +6 -0
  19. execsql/exporters/json.py +5 -6
  20. execsql/exporters/latex.py +4 -0
  21. execsql/exporters/ods.py +28 -7
  22. execsql/exporters/parquet.py +3 -0
  23. execsql/exporters/pretty.py +5 -0
  24. execsql/exporters/raw.py +5 -3
  25. execsql/exporters/sqlite.py +4 -0
  26. execsql/exporters/templates.py +16 -6
  27. execsql/exporters/values.py +4 -0
  28. execsql/exporters/xls.py +26 -7
  29. execsql/exporters/xml.py +3 -0
  30. execsql/exporters/zip.py +15 -0
  31. execsql/importers/base.py +2 -0
  32. execsql/importers/csv.py +2 -0
  33. execsql/importers/feather.py +2 -0
  34. execsql/importers/ods.py +2 -0
  35. execsql/importers/xls.py +2 -0
  36. execsql/metacommands/__init__.py +177 -1968
  37. execsql/metacommands/dispatch.py +2011 -0
  38. execsql/models.py +7 -0
  39. execsql/parser.py +10 -0
  40. execsql/script/__init__.py +95 -0
  41. execsql/script/control.py +162 -0
  42. execsql/{script.py → script/engine.py} +144 -406
  43. execsql/script/variables.py +281 -0
  44. execsql/types.py +29 -0
  45. execsql/utils/auth.py +2 -0
  46. execsql/utils/crypto.py +4 -6
  47. execsql/utils/datetime.py +1 -0
  48. execsql/utils/errors.py +11 -0
  49. execsql/utils/fileio.py +18 -0
  50. execsql/utils/gui.py +46 -0
  51. execsql/utils/mail.py +7 -17
  52. execsql/utils/numeric.py +2 -0
  53. execsql/utils/regex.py +9 -0
  54. execsql/utils/strings.py +16 -0
  55. execsql/utils/timer.py +2 -0
  56. execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
  57. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
  58. {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/METADATA +8 -1
  59. execsql2-2.4.0.dist-info/RECORD +108 -0
  60. execsql2-2.2.1.data/data/execsql2_extras/READ_ME.rst +0 -127
  61. execsql2-2.2.1.dist-info/RECORD +0 -104
  62. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  63. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  64. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  65. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  66. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  67. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  68. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  69. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  70. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  71. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
  72. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  73. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  74. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  75. {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
  76. {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
  77. {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
  78. {execsql2-2.2.1.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
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
@@ -16,6 +16,7 @@ import datetime
16
16
  import re
17
17
  from typing import Any
18
18
 
19
+ __all__ = ["parse_datetime", "parse_datetimetz"]
19
20
 
20
21
  dt_fmts = collections.deque(
21
22
  (
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
@@ -15,6 +15,8 @@ Provides:
15
15
  import re
16
16
  from typing import Any
17
17
 
18
+ __all__ = ["as_numeric", "leading_zero_num"]
19
+
18
20
 
19
21
  def leading_zero_num(dataval: Any) -> bool:
20
22
  # Returns True if the data value is potentially a number but has a leading zero
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
@@ -17,6 +17,8 @@ import time
17
17
 
18
18
  from execsql.exceptions import ExecSqlTimeoutError
19
19
 
20
+ __all__ = ["Timer", "TimerHandler"]
21
+
20
22
 
21
23
  class TimerHandler:
22
24
  def __init__(self, maxtime: float) -> None:
@@ -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.
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # PURPOSE
4
4
  # Configuration file for execsql.py
5
- # See http://execsql.osdn.io/configuration.html
5
+ # See https://execsql2.readthedocs.io/en/latest/configuration/
6
6
  #
7
7
  # AUTHORS
8
8
  #