execsql2 2.10.1__py3-none-any.whl → 2.11.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.
Files changed (34) hide show
  1. execsql/cli/__init__.py +6 -0
  2. execsql/cli/lint.py +1 -1
  3. execsql/cli/run.py +8 -3
  4. execsql/exporters/xlsx.py +5 -0
  5. execsql/exporters/yaml.py +2 -0
  6. execsql/metacommands/conditions.py +1 -1
  7. execsql/metacommands/control.py +4 -10
  8. execsql/metacommands/debug.py +1 -1
  9. execsql/metacommands/debug_repl.py +106 -44
  10. execsql/script/engine.py +21 -7
  11. execsql/state.py +2 -2
  12. execsql/utils/errors.py +41 -2
  13. execsql/utils/gui.py +26 -4
  14. {execsql2-2.10.1.dist-info → execsql2-2.11.1.dist-info}/METADATA +6 -2
  15. {execsql2-2.10.1.dist-info → execsql2-2.11.1.dist-info}/RECORD +34 -34
  16. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/README.md +0 -0
  17. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  18. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  19. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/execsql.conf +0 -0
  20. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  21. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  22. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  23. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  24. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  25. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  26. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  27. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/script_template.sql +0 -0
  28. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  29. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  30. {execsql2-2.10.1.data → execsql2-2.11.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  31. {execsql2-2.10.1.dist-info → execsql2-2.11.1.dist-info}/WHEEL +0 -0
  32. {execsql2-2.10.1.dist-info → execsql2-2.11.1.dist-info}/entry_points.txt +0 -0
  33. {execsql2-2.10.1.dist-info → execsql2-2.11.1.dist-info}/licenses/LICENSE.txt +0 -0
  34. {execsql2-2.10.1.dist-info → execsql2-2.11.1.dist-info}/licenses/NOTICE +0 -0
execsql/cli/__init__.py CHANGED
@@ -278,6 +278,11 @@ def main(
278
278
  "--profile",
279
279
  help="Record per-statement execution times and print a timing summary after the script completes.",
280
280
  ),
281
+ debug: bool = typer.Option(
282
+ False,
283
+ "--debug",
284
+ help="Start in step-through debug mode. The debug REPL pauses before each statement.",
285
+ ),
281
286
  version: bool | None = typer.Option(
282
287
  None,
283
288
  "--version",
@@ -446,6 +451,7 @@ def main(
446
451
  profile=profile,
447
452
  ping=ping,
448
453
  lint=lint,
454
+ debug=debug,
449
455
  )
450
456
 
451
457
 
execsql/cli/lint.py CHANGED
@@ -194,7 +194,7 @@ def _lint_cmdlist(
194
194
  for cmd in cmdlist.cmdlist:
195
195
  src = cmd.source
196
196
  lno = cmd.line_no
197
- stmt = cmd.command.statement if cmd.command_type == "sql" else cmd.command.statement
197
+ stmt = cmd.command.statement
198
198
 
199
199
  if cmd.command_type == "sql":
200
200
  # SQL statements: check for variable references only
execsql/cli/run.py CHANGED
@@ -8,6 +8,7 @@ and drives the main execution loop. Separated from argument parsing
8
8
  from __future__ import annotations
9
9
 
10
10
  import atexit
11
+ from typing import Any
11
12
  import datetime
12
13
  import getpass
13
14
  import os
@@ -128,7 +129,7 @@ def _print_profile(profile_data: list[tuple]) -> None:
128
129
  # ---------------------------------------------------------------------------
129
130
 
130
131
 
131
- def _ping_db(db) -> None:
132
+ def _ping_db(db: Any) -> None:
132
133
  """Test connectivity for *db*, print connection details, and exit.
133
134
 
134
135
  Attempts to execute ``SELECT version()`` (or ``SELECT sqlite_version()``
@@ -156,7 +157,7 @@ def _ping_db(db) -> None:
156
157
  curs.close()
157
158
  if row and row[0]:
158
159
  version_str = str(row[0]).split("\n")[0].strip()
159
- break
160
+ break
160
161
  except Exception:
161
162
  continue
162
163
 
@@ -214,6 +215,7 @@ def _run(
214
215
  profile: bool = False,
215
216
  ping: bool = False,
216
217
  lint: bool = False,
218
+ debug: bool = False,
217
219
  ) -> None:
218
220
  """Initialise state, connect to the database, load the script, and run it.
219
221
 
@@ -547,6 +549,9 @@ def _run(
547
549
  if profile:
548
550
  _state.profile_data = []
549
551
 
552
+ if debug:
553
+ _state.step_mode = True
554
+
550
555
  _execute_script_direct(conf, profile=profile)
551
556
 
552
557
 
@@ -653,7 +658,7 @@ def _execute_script_direct(conf: ConfigData, *, profile: bool = False) -> None:
653
658
  lno = strace[0][1]
654
659
  msg = f"{Path(sys.argv[0]).name}: Uncaught exception {sys.exc_info()[0]} ({sys.exc_info()[1]}) on line {lno}"
655
660
  script, slno = current_script_line()
656
- if script is not None:
661
+ if script:
657
662
  msg += f" in script {script}, line {slno}"
658
663
  from execsql.utils.errors import exit_now
659
664
 
execsql/exporters/xlsx.py CHANGED
@@ -178,6 +178,11 @@ def write_query_to_xlsx(
178
178
  wb.save(outfile)
179
179
  wb.close()
180
180
 
181
+ if _state.export_metadata is not None:
182
+ _state.export_metadata.add(
183
+ ExportRecord(queryname=select_stmt, outfile=outfile, zipfile=None, description=desc),
184
+ )
185
+
181
186
 
182
187
  def write_queries_to_xlsx(
183
188
  table_list: str,
execsql/exporters/yaml.py CHANGED
@@ -82,6 +82,8 @@ def write_query_to_yaml(
82
82
  f = ZipWriter(zipfile, outfile, append)
83
83
 
84
84
  try:
85
+ if append:
86
+ f.write("---\n")
85
87
  f.write(yaml_text)
86
88
  finally:
87
89
  f.close()
@@ -1,5 +1,4 @@
1
1
  from __future__ import annotations
2
- from execsql.exceptions import ErrInfo
3
2
 
4
3
  """
5
4
  Conditional test handler functions for execsql.
@@ -15,6 +14,7 @@ at registration time.
15
14
  """
16
15
 
17
16
  import os
17
+ from execsql.exceptions import ErrInfo
18
18
  import time
19
19
  from pathlib import Path
20
20
  from typing import Any
@@ -1,5 +1,4 @@
1
1
  from __future__ import annotations
2
- from execsql.exceptions import ErrInfo
3
2
 
4
3
  """
5
4
  Control-flow metacommand handlers for execsql.
@@ -18,6 +17,8 @@ Implements the imperative ``x_*`` functions for script flow control:
18
17
  """
19
18
 
20
19
  import time
20
+
21
+ from execsql.exceptions import ErrInfo
21
22
  from typing import Any
22
23
 
23
24
  import execsql.state as _state
@@ -62,7 +63,8 @@ def x_assert(**kwargs: Any) -> None:
62
63
 
63
64
  result = _state.xcmd_test(condition)
64
65
  if result:
65
- _state.exec_log.log_user_msg(f"ASSERT passed: {condition}")
66
+ if _state.exec_log is not None:
67
+ _state.exec_log.log_user_msg(f"ASSERT passed: {condition}")
66
68
  else:
67
69
  raise ErrInfo(type="cmd", other_msg=message)
68
70
 
@@ -134,14 +136,6 @@ def x_loop(**kwargs: Any) -> None:
134
136
  )
135
137
 
136
138
 
137
- def endloop() -> None:
138
- if len(_state.loopcommandstack) == 0:
139
- raise ErrInfo("error", other_msg="END LOOP metacommand without a matching preceding LOOP metacommand.")
140
- _state.compiling_loop = False
141
- _state.commandliststack.append(_state.loopcommandstack[-1])
142
- _state.loopcommandstack.pop()
143
-
144
-
145
139
  def x_halt(**kwargs: Any) -> None:
146
140
  errmsg = kwargs["errmsg"]
147
141
  tee = kwargs["tee"]
@@ -74,7 +74,7 @@ def x_debug_log_subvars(**kwargs: Any) -> None:
74
74
  local = kwargs["local"]
75
75
  user = kwargs["user"]
76
76
  for s in _state.commandliststack[-1].localvars.substitutions:
77
- _state.exec_log.log_status_info(f"Substitution [{s}] = [{s}]")
77
+ _state.exec_log.log_status_info(f"Substitution [{s[0]}] = [{s[1]}]")
78
78
  if local is None:
79
79
  for s in _state.subvars.substitutions:
80
80
  if user is None or s[0][0].isalnum() or s[0][0] == "_":
@@ -13,6 +13,11 @@ The REPL allows the user to:
13
13
  - Step through the script one statement at a time.
14
14
  - Resume or abort execution.
15
15
 
16
+ All REPL commands are dot-prefixed (``.continue``, ``.vars``, ``.next``)
17
+ to avoid ambiguity with variable names and SQL. Anything not starting
18
+ with ``.`` is treated as either a variable lookup (if it matches a known
19
+ variable) or SQL (if it ends with ``;``).
20
+
16
21
  In non-interactive environments (CI, piped input, ``sys.stdin.isatty()`` is
17
22
  ``False``) the metacommand is silently skipped so automated pipelines are not
18
23
  blocked.
@@ -30,15 +35,19 @@ __all__ = ["x_breakpoint"]
30
35
  # ---------------------------------------------------------------------------
31
36
 
32
37
  _HELP_TEXT = """\
33
- execsql debug REPL commands:
34
- continue c Resume script execution
35
- abort q quit Halt the script (exit 1)
36
- vars List all substitution variables and their values
37
- $VARNAME Print a single variable's value (also &VAR, @VAR)
38
- SELECT ...; Run ad-hoc SQL against the current database
39
- next n Execute the next statement then pause again (step mode)
40
- stack Show the command-list stack (script name, line, depth)
41
- help Show this help text
38
+ execsql debug REPL — all commands start with '.'
39
+
40
+ .continue .c Resume script execution
41
+ .abort .q Halt the script (exit 1)
42
+ .vars List user, system, local, and counter variables
43
+ .vars all Include environment variables (&) in the listing
44
+ .next .n Execute the next statement then pause again (step mode)
45
+ .stack Show the command-list stack (script name, line, depth)
46
+ .help Show this help text
47
+
48
+ Everything else:
49
+ varname Print a variable's value (e.g. logfile, $ARG_1, &HOME)
50
+ SELECT ...; Run ad-hoc SQL against the current database
42
51
  """
43
52
 
44
53
 
@@ -64,7 +73,7 @@ def x_breakpoint(**kwargs: Any) -> None:
64
73
  def _debug_repl() -> None:
65
74
  """Interactive read-eval-print loop for script debugging.
66
75
 
67
- Reads commands from stdin until the user types ``continue`` or ``abort``,
76
+ Reads commands from stdin until the user types ``.continue`` or ``.abort``,
68
77
  or until EOF / KeyboardInterrupt.
69
78
  """
70
79
  try:
@@ -72,7 +81,7 @@ def _debug_repl() -> None:
72
81
  except ImportError:
73
82
  pass # readline not available on Windows; continue without it
74
83
 
75
- _write("\n[Breakpoint] Script paused. Type 'help' for commands, 'continue' to resume.\n")
84
+ _write("\n[Breakpoint] Script paused. Type '.help' for commands, '.c' to resume.\n")
76
85
 
77
86
  while True:
78
87
  try:
@@ -87,27 +96,49 @@ def _debug_repl() -> None:
87
96
  if not line:
88
97
  continue
89
98
 
90
- lower = line.lower()
99
+ # Dot-prefixed → REPL command
100
+ if line.startswith("."):
101
+ cmd = line[1:].strip().lower()
102
+ _handle_dot_command(line)
103
+ if cmd in ("continue", "c"):
104
+ return
105
+ if cmd in ("abort", "q", "quit"):
106
+ # _handle_dot_command already raised SystemExit, but guard anyway
107
+ return
108
+ if cmd in ("next", "n"):
109
+ return
110
+ continue
91
111
 
92
- if lower in ("continue", "c"):
93
- return
94
- elif lower in ("abort", "q", "quit"):
95
- raise SystemExit(1)
96
- elif lower == "help":
97
- _write(_HELP_TEXT)
98
- elif lower == "vars":
99
- _print_all_vars()
100
- elif lower == "stack":
101
- _print_stack()
102
- elif lower in ("next", "n"):
103
- _enable_step_mode()
104
- return
105
- elif line[0] in ("$", "&", "@"):
106
- _print_var(line)
107
- elif line.rstrip().endswith(";"):
112
+ # SQL (ends with semicolon)
113
+ if line.rstrip().endswith(";"):
108
114
  _run_sql(line)
109
- else:
110
- _write(f"Unknown command: {line!r}. Type 'help' for available commands.\n")
115
+ continue
116
+
117
+ # Everything else → variable lookup
118
+ _print_var(line)
119
+
120
+
121
+ def _handle_dot_command(line: str) -> None:
122
+ """Dispatch a dot-prefixed REPL command."""
123
+ # Strip the leading dot and normalize
124
+ cmd = line[1:].strip().lower()
125
+
126
+ if cmd in ("continue", "c"):
127
+ return # caller checks and returns from _debug_repl
128
+ elif cmd in ("abort", "q", "quit"):
129
+ raise SystemExit(1)
130
+ elif cmd == "help":
131
+ _write(_HELP_TEXT)
132
+ elif cmd == "vars all":
133
+ _print_all_vars(include_env=True)
134
+ elif cmd == "vars":
135
+ _print_all_vars()
136
+ elif cmd == "stack":
137
+ _print_stack()
138
+ elif cmd in ("next", "n"):
139
+ _enable_step_mode()
140
+ else:
141
+ _write(f" Unknown command: {line!r}. Type '.help' for available commands.\n")
111
142
 
112
143
 
113
144
  # ---------------------------------------------------------------------------
@@ -125,8 +156,8 @@ def _write(text: str) -> None:
125
156
  sys.stdout.flush()
126
157
 
127
158
 
128
- def _print_all_vars() -> None:
129
- """Print all substitution variables and their current values."""
159
+ def _print_all_vars(*, include_env: bool = False) -> None:
160
+ """Print substitution variables grouped by type."""
130
161
  subvars = _state.subvars
131
162
  if subvars is None:
132
163
  _write(" (no substitution variables defined)\n")
@@ -135,17 +166,52 @@ def _print_all_vars() -> None:
135
166
  if not items:
136
167
  _write(" (no substitution variables defined)\n")
137
168
  return
138
- # Compute column width for aligned output.
139
- max_name = max((len(name) for name, _ in items), default=0)
169
+
170
+ # Group by prefix.
171
+ user_vars: list[tuple[str, str]] = []
172
+ system_vars: list[tuple[str, str]] = []
173
+ counter_vars: list[tuple[str, str]] = []
174
+ local_vars: list[tuple[str, str]] = []
175
+ env_vars: list[tuple[str, str]] = []
176
+
140
177
  for name, value in sorted(items):
141
- _write(f" {name:<{max_name}} = {value!r}\n")
178
+ if name.startswith("&"):
179
+ env_vars.append((name, value))
180
+ elif name.startswith("~"):
181
+ local_vars.append((name, value))
182
+ elif name.startswith("@"):
183
+ counter_vars.append((name, value))
184
+ elif name.startswith("$"):
185
+ system_vars.append((name, value))
186
+ else:
187
+ user_vars.append((name, value))
188
+
189
+ def _print_group(label: str, group: list[tuple[str, str]]) -> None:
190
+ if not group:
191
+ return
192
+ _write(f" {label}:\n")
193
+ max_name = max(len(n) for n, _ in group)
194
+ for name, value in group:
195
+ _write(f" {name:<{max_name}} = {value}\n")
196
+
197
+ _print_group("User variables", user_vars)
198
+ _print_group("System variables ($)", system_vars)
199
+ _print_group("Local variables (~)", local_vars)
200
+ _print_group("Counter variables (@)", counter_vars)
201
+ if include_env:
202
+ _print_group("Environment variables (&)", env_vars)
203
+
204
+ if not any([user_vars, system_vars, local_vars, counter_vars]):
205
+ if env_vars:
206
+ _write(" (no script variables defined — use '.vars all' to see environment variables)\n")
207
+ else:
208
+ _write(" (no variables defined)\n")
142
209
 
143
210
 
144
211
  def _print_var(varname: str) -> None:
145
212
  """Print the value of a single substitution variable.
146
213
 
147
- Args:
148
- varname: The variable reference as typed by the user, e.g. ``$FOO``.
214
+ Tries the name as typed, then with the sigil prefix stripped.
149
215
  """
150
216
  subvars = _state.subvars
151
217
  if subvars is None:
@@ -153,14 +219,14 @@ def _print_var(varname: str) -> None:
153
219
  return
154
220
  # Try the name as typed first, then without the sigil prefix ($, &, @, #, ~).
155
221
  # SUB creates variables without a prefix (e.g., "logfile"), but users
156
- # naturally type "$logfile" at the prompt.
222
+ # may type "$logfile" at the prompt.
157
223
  value = subvars.varvalue(varname)
158
224
  if value is None and len(varname) > 1 and varname[0] in "$&@#~":
159
225
  value = subvars.varvalue(varname[1:])
160
226
  if value is None:
161
227
  _write(f" {varname}: (undefined)\n")
162
228
  else:
163
- _write(f" {varname} = {value!r}\n")
229
+ _write(f" {varname} = {value}\n")
164
230
 
165
231
 
166
232
  def _print_stack() -> None:
@@ -177,11 +243,7 @@ def _print_stack() -> None:
177
243
 
178
244
 
179
245
  def _run_sql(sql: str) -> None:
180
- """Execute ad-hoc SQL against the current database and pretty-print the results.
181
-
182
- Args:
183
- sql: A complete SQL statement ending with a semicolon.
184
- """
246
+ """Execute ad-hoc SQL against the current database and pretty-print the results."""
185
247
  dbs = _state.dbs
186
248
  if dbs is None:
187
249
  _write(" (no database connection is active)\n")
execsql/script/engine.py CHANGED
@@ -194,7 +194,7 @@ class MetaCommandList:
194
194
  patterns; each compiles into a separate :class:`MetaCommand` prepended to
195
195
  the dispatch list so that later registrations take priority.
196
196
  """
197
- if type(matching_regexes) in (tuple, list):
197
+ if isinstance(matching_regexes, (tuple, list)):
198
198
  raw_patterns = list(matching_regexes)
199
199
  regexes = [re.compile(rx, re.I) for rx in raw_patterns]
200
200
  else:
@@ -305,8 +305,14 @@ class SqlStmt:
305
305
  except Exception:
306
306
  e = ErrInfo(type="exception", exception_msg=exception_desc())
307
307
  if e:
308
+ from execsql.utils.errors import stamp_errinfo
309
+
310
+ stamp_errinfo(e)
308
311
  _state.subvars.add_substitution("$LAST_ERROR", cmd)
312
+ _state.subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
309
313
  _state.status.sql_error = True
314
+ if _state.exec_log is not None:
315
+ _state.exec_log.log_status_info(f"SQL error: {e.errmsg()}")
310
316
  if _state.status.halt_on_err:
311
317
  from execsql.utils.errors import exit_now
312
318
 
@@ -348,10 +354,18 @@ class MetacommandStmt:
348
354
  except Exception:
349
355
  e = ErrInfo(type="exception", exception_msg=exception_desc())
350
356
  if e:
357
+ from execsql.utils.errors import stamp_errinfo
358
+
359
+ stamp_errinfo(e)
351
360
  _state.status.metacommand_error = True
352
361
  _state.subvars.add_substitution("$LAST_ERROR", cmd)
362
+ _state.subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
363
+ if _state.exec_log is not None:
364
+ _state.exec_log.log_status_info(f"Metacommand error: {e.errmsg()}")
353
365
  if _state.status.halt_on_metacommand_err:
354
- raise ErrInfo(type="cmd", command_text=cmd, other_msg=errmsg)
366
+ # Re-raise the original ErrInfo so its message is preserved, not
367
+ # replaced with the generic "Unknown metacommand" text.
368
+ raise e
355
369
  if _state.if_stack.all_true():
356
370
  # but nothing applies, because we got here.
357
371
  _state.status.metacommand_error = True
@@ -489,6 +503,11 @@ class CommandList:
489
503
  _state.subvars.add_substitution("$CURRENT_SCRIPT_NAME", Path(cmditem.source).name)
490
504
  _state.subvars.add_substitution("$CURRENT_SCRIPT_LINE", str(cmditem.line_no))
491
505
  _state.subvars.add_substitution("$SCRIPT_LINE", str(cmditem.line_no))
506
+ if _state.step_mode:
507
+ _state.step_mode = False
508
+ from execsql.metacommands.debug_repl import _debug_repl
509
+
510
+ _debug_repl()
492
511
  _profiling = _state.profile_data is not None
493
512
  if _profiling:
494
513
  import time as _time
@@ -506,11 +525,6 @@ class CommandList:
506
525
  cmditem.command.commandline()[:100],
507
526
  ),
508
527
  )
509
- if _state.step_mode:
510
- _state.step_mode = False
511
- from execsql.metacommands.debug_repl import _debug_repl
512
-
513
- _debug_repl()
514
528
  self.cmdptr += 1
515
529
 
516
530
  def run_next(self) -> None:
execsql/state.py CHANGED
@@ -336,8 +336,7 @@ class _StateModule(types.ModuleType):
336
336
  # via setattr (local) or delattr (non-local). Since context
337
337
  # attrs live on _ctx, not __dict__, patch takes the delattr
338
338
  # path. We reset to the default rather than truly deleting.
339
- _defaults = RuntimeContext()
340
- setattr(self.__dict__["_ctx"], name, getattr(_defaults, name))
339
+ setattr(self.__dict__["_ctx"], name, getattr(_DEFAULT_CTX, name))
341
340
  else:
342
341
  super().__delattr__(name)
343
342
 
@@ -468,4 +467,5 @@ def initialize(
468
467
  # ---------------------------------------------------------------------------
469
468
 
470
469
  _ctx = RuntimeContext()
470
+ _DEFAULT_CTX = RuntimeContext() # Cached defaults for __delattr__ reset
471
471
  sys.modules[__name__].__class__ = _StateModule
execsql/utils/errors.py CHANGED
@@ -30,6 +30,7 @@ __all__ = [
30
30
  "exception_desc",
31
31
  "exit_now",
32
32
  "fatal_error",
33
+ "stamp_errinfo",
33
34
  "write_warning",
34
35
  "file_size_date",
35
36
  "chainfuncs",
@@ -70,9 +71,37 @@ def exception_desc() -> str:
70
71
  return f"{exc_type}: {exc_strval} in {exc_filename} on line {exc_lineno} of execsql."
71
72
 
72
73
 
74
+ def stamp_errinfo(errinfo: ErrInfo) -> ErrInfo:
75
+ """Attach script location from ``_state.last_command`` to an :class:`~execsql.exceptions.ErrInfo`.
76
+
77
+ Reads the source file name, line number, command text, and command type from
78
+ the most-recently-executed :class:`~execsql.script.engine.ScriptCmd` and
79
+ populates any ``None`` fields on *errinfo*. This ensures that error messages
80
+ include "Line N of script foo.sql" context even when the ErrInfo was originally
81
+ created deep inside a handler that had no access to execution state.
82
+
83
+ Args:
84
+ errinfo: The :class:`~execsql.exceptions.ErrInfo` to stamp.
85
+
86
+ Returns:
87
+ The same *errinfo* object, with location fields populated.
88
+ """
89
+ lc = _state.last_command
90
+ if lc is not None and errinfo.script_file is None:
91
+ errinfo.script_file = lc.source
92
+ errinfo.script_line_no = lc.line_no
93
+ if errinfo.cmd is None:
94
+ errinfo.cmd = lc.command.commandline() if hasattr(lc.command, "commandline") else None
95
+ errinfo.cmdtype = lc.command_type
96
+ return errinfo
97
+
98
+
73
99
  def exit_now(exit_status: int, errinfo: ErrInfo | None, logmsg: str | None = None) -> None:
74
100
  em = None
75
101
  if errinfo is not None:
102
+ stamp_errinfo(errinfo)
103
+ if _state.subvars is not None:
104
+ _state.subvars.add_substitution("$ERROR_MESSAGE", errinfo.errmsg())
76
105
  em = errinfo.write()
77
106
  if _state.err_halt_writespec is not None:
78
107
  try:
@@ -147,10 +176,20 @@ def fatal_error(error_msg: str | None = None) -> None:
147
176
  exit_now(1, ErrInfo("error", other_msg=error_msg))
148
177
 
149
178
 
150
- def write_warning(warning_msg: str) -> None:
179
+ def write_warning(warning_msg: str, *, always: bool = False) -> None:
180
+ """Write a non-fatal warning message to the log and optionally to stderr.
181
+
182
+ Args:
183
+ warning_msg: The warning text to emit.
184
+ always: When ``True``, always write to stderr regardless of the
185
+ ``conf.write_warnings`` setting. Use this for structural warnings
186
+ (e.g. IF-level mismatch, unsubstituted variables) that should always
187
+ be visible. When ``False`` (default), stderr output is gated by
188
+ ``conf.write_warnings``.
189
+ """
151
190
  if _state.exec_log is not None:
152
191
  _state.exec_log.log_status_warning(warning_msg)
153
- if _state.conf is not None and _state.conf.write_warnings and _state.output is not None:
192
+ if _state.output is not None and (always or (_state.conf is not None and _state.conf.write_warnings)):
154
193
  _state.output.write_err(f"**** Warning {warning_msg}")
155
194
 
156
195
 
execsql/utils/gui.py CHANGED
@@ -358,13 +358,35 @@ def gui_console_wait_user(message: str = "") -> None:
358
358
  print(message, file=sys.stderr)
359
359
 
360
360
 
361
- def gui_console_width() -> int:
362
- """Return the current console width in characters."""
361
+ def gui_console_width(width: int | None = None) -> int:
362
+ """Get or set the console width in characters.
363
+
364
+ When *width* is provided, updates the active GUI console (if any).
365
+ Always returns the current width.
366
+ """
367
+ global _console_width
368
+ if width is not None:
369
+ import execsql.state as _state
370
+
371
+ _console_width = int(width)
372
+ if _state.gui_console is not None and hasattr(_state.gui_console, "set_width"):
373
+ _state.gui_console.set_width(_console_width)
363
374
  return _console_width
364
375
 
365
376
 
366
- def gui_console_height() -> int:
367
- """Return the current console height in lines."""
377
+ def gui_console_height(height: int | None = None) -> int:
378
+ """Get or set the console height in lines.
379
+
380
+ When *height* is provided, updates the active GUI console (if any).
381
+ Always returns the current height.
382
+ """
383
+ global _console_height
384
+ if height is not None:
385
+ import execsql.state as _state
386
+
387
+ _console_height = int(height)
388
+ if _state.gui_console is not None and hasattr(_state.gui_console, "set_height"):
389
+ _state.gui_console.set_height(_console_height)
368
390
  return _console_height
369
391
 
370
392
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.10.1
3
+ Version: 2.11.1
4
4
  Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
5
5
  Project-URL: Repository, https://github.com/geocoug/execsql
6
6
  Project-URL: Issues, https://github.com/geocoug/execsql/issues
@@ -220,9 +220,13 @@ execsql script.sql # read connection from config file
220
220
  | `-v {0,1,2,3}` | GUI level (0=none, 1=password, 2=selection, 3=full) |
221
221
  | `-w` | Skip password prompt when a username is supplied |
222
222
  | `--dsn URL` | Connection string (e.g. `postgresql://user:pass@host/db`) |
223
+ | `--output-dir DIR` | Default base directory for EXPORT output files |
223
224
  | `--dry-run` | Parse the script and report commands without executing |
224
225
  | `--lint` | Static analysis: check structure and warn on issues (no DB) |
226
+ | `--ping` | Test database connectivity and exit |
227
+ | `--profile` | Show per-statement timing summary after execution |
225
228
  | `--progress` | Show a progress bar for long-running IMPORT operations |
229
+ | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
226
230
  | `--dump-keywords` | Print metacommand keywords as JSON and exit |
227
231
  | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
228
232
 
@@ -297,7 +301,7 @@ execsql-format --check scripts/
297
301
  ```yaml
298
302
  repos:
299
303
  - repo: https://github.com/geocoug/execsql
300
- rev: v2.4.4
304
+ rev: v2.11.0
301
305
  hooks:
302
306
  - id: execsql-format
303
307
  args: [--in-place]
@@ -7,13 +7,13 @@ execsql/format.py,sha256=-6iknDddqbkapMo4NKmT5LAynDLqMW5kHgDWRg0KSws,11990
7
7
  execsql/models.py,sha256=DxkGp9iWbuZDWPGmnxZp9mvEeyOwxEJNx94fxQQiLfQ,13538
8
8
  execsql/parser.py,sha256=mbNSMiAMR1NvNvFtQAZq6nxBOupMGJZXSimLWLtZeNs,15537
9
9
  execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- execsql/state.py,sha256=ovUQOr78R3LzsDUzBLL9Bq9ZdHiiFVlBvCNzilQ-K-s,15055
10
+ execsql/state.py,sha256=29b3SwG4GirED2KVQc-cCC7Z_-FsGN3fEt9xQNN-Puo,15090
11
11
  execsql/types.py,sha256=HVWb4umIB9lpxCGgqk3xy1hoGYPfN39xci5mHF0Izq4,31882
12
- execsql/cli/__init__.py,sha256=Y4lFKvKWyjtMgSLsmACHYG33DFpeAQxddIVrlKURwpg,16081
12
+ execsql/cli/__init__.py,sha256=dPKq7KOY7soD1GfBEztoRWcKuDLY5QyhzgC6PuyeyII,16270
13
13
  execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
14
14
  execsql/cli/help.py,sha256=Sn_TgSJiQeBx-xZH0fuP5OvR_wasSTumjWF9UHfIX5k,5414
15
- execsql/cli/lint.py,sha256=KluYROdjGJNUrVbO3cJym-H296zbih4no-F10HF0P4U,16165
16
- execsql/cli/run.py,sha256=i0ip8tm21Sm4EFbfBdcekETmiABzrRfeP84SqA4IF68,30158
15
+ execsql/cli/lint.py,sha256=XWuVcEsheZ8ql48VFWqICWEkAUezB2nIePX6SUiKSg8,16109
16
+ execsql/cli/run.py,sha256=eiD_5R2ZL8WzhbW78DBcoSbcibmOXcr3po_aYYSoPwg,30250
17
17
  execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
18
18
  execsql/db/access.py,sha256=L79gUnAnnM9EJ_f4k42jr7DI0qGcKtLOnJTlBC7uPm0,17879
19
19
  execsql/db/base.py,sha256=hfMFj8fXY0T1aXLvWJHqb0aU4EQUDFOc-YrS29HH8U4,30405
@@ -44,9 +44,9 @@ execsql/exporters/sqlite.py,sha256=XA0ALLvy-r6Pz1lpOFkWWbvpSP9Hm1tHHiuo_BvPVDk,2
44
44
  execsql/exporters/templates.py,sha256=T9nk7vJrlxiPGfOWGc79xqqDxK3TCYu0wXq48U02npw,5564
45
45
  execsql/exporters/values.py,sha256=HIyud31aux_dbCphfKHEGeZB9fkIPE5PoGXQz817XIE,2520
46
46
  execsql/exporters/xls.py,sha256=nPROgxL8XK2oiBVoqN2L-o0j_jynRIMokwB8NpvOBt0,10623
47
- execsql/exporters/xlsx.py,sha256=xXTFIKkvJnNOsFdnhSYEkJa4ulTrtq9tIRk6SSchqA0,11299
47
+ execsql/exporters/xlsx.py,sha256=Gm8ns_KeqSMu2DONSSJ1DcwPBEjYwpbU7frmX0g5L7c,11487
48
48
  execsql/exporters/xml.py,sha256=lqcOM8uKDoCayU42BPSLNH1_2DIHU5D3LtQItREU90c,2564
49
- execsql/exporters/yaml.py,sha256=0bwLDU3Fy00yMryBOSBSptbjV8Re6Ks-b62DObFNP4o,3062
49
+ execsql/exporters/yaml.py,sha256=1Vuc6uMDuLTkCuXCfXWKz4gLkkAVdEXkLs4gEB_67Xo,3110
50
50
  execsql/exporters/zip.py,sha256=9-hExltQorONNThiMfxPDYHqHsbTeq9zM9zmtG4oFb8,4410
51
51
  execsql/gui/__init__.py,sha256=oCb-cyhLZzVpWJ4WU5HbqEDBrV-lm0ytEwxemrOZyqs,2048
52
52
  execsql/gui/base.py,sha256=sfNRkDrf7FhIgMRUOdyZpRLS1Xk9RqNhrV0A1RP6PXU,6068
@@ -60,12 +60,12 @@ execsql/importers/feather.py,sha256=g2B69d2uv9vmnXcmjFyTVsMP40LYEzFYkhk3gD26mGw,
60
60
  execsql/importers/ods.py,sha256=MJsdsjropzCvxAA3DDZfAL_AnmZ4yij7DnrjGyDJqHQ,2843
61
61
  execsql/importers/xls.py,sha256=e0Zfe47ZiCpA1Ae3XDJ1ko3sCiH3-8U6XLKi6NvD0jQ,3683
62
62
  execsql/metacommands/__init__.py,sha256=TT1ARHgHltHqZ7qx4Y62o1h_GOPvUztZKCem-wAE560,11215
63
- execsql/metacommands/conditions.py,sha256=QUpevCHce9kbKpt6XpHkc73q3bYAIfaBin0b6eaJnYQ,29259
63
+ execsql/metacommands/conditions.py,sha256=Fzrk83-pWbFOoKahYdQW7CZjQeh3zByDUbfgpTM_bjQ,29259
64
64
  execsql/metacommands/connect.py,sha256=Nsm0D91i3RX-R2rzQQ-Br-gULaI6Uvdn9fqb7DOAVfE,14804
65
- execsql/metacommands/control.py,sha256=CBCg0ZKSR-BGejBW5cXwk6aJ9VrYBzCg9C40ofi8qi8,8776
65
+ execsql/metacommands/control.py,sha256=xNHyTrYUM042OgDarNq7HxslY7JuQs-KOKcb-fHUngM,8510
66
66
  execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,11370
67
- execsql/metacommands/debug.py,sha256=nmfQ2ijUbTQO3drnyV9EzFueGSTfMl-CddP_NlQyI14,8178
68
- execsql/metacommands/debug_repl.py,sha256=UsTup7oYsJ0bNsKKynxQz-4wplFKnIcKSNrkz6gl0VM,7530
67
+ execsql/metacommands/debug.py,sha256=pnT24dfvfOx8xFu86mO5czfVCGKbcvgBLyXnqaMWO4w,8184
68
+ execsql/metacommands/debug_repl.py,sha256=XQ09I7HI-drVjfIg4XqsPndvAmZSxpSmXVKjWjLqwE4,9785
69
69
  execsql/metacommands/dispatch.py,sha256=1Mae6yqrea6wViFLBsvVt33Zgx4xP8tnhOuB_aQC89c,84054
70
70
  execsql/metacommands/io.py,sha256=Duh60caM4go9JczbGYNMKKYpcMimwPzF6EQ_tshKxdE,2971
71
71
  execsql/metacommands/io_export.py,sha256=7lkCSnPhXy9FVau9_hT1u68NOVdG2DsWmvUh9hM1QWI,18359
@@ -77,38 +77,38 @@ execsql/metacommands/script_ext.py,sha256=TUgAldB2LSJAwZrCvDDi804hQ1d9BDQD2GDqHN
77
77
  execsql/metacommands/system.py,sha256=sUR5kLL7idTVg8WXIMdd-Kv7nkERIiaeL0beWsz8NyY,7293
78
78
  execsql/script/__init__.py,sha256=pIo0EJ7-vg67rSMbOvbri_BOUgLoGoSEUfJgxUN7ZS0,3380
79
79
  execsql/script/control.py,sha256=s-1eZdGARM6H1FwZ6VDdO_f50j7bvvRtTHesfUm9tbc,6144
80
- execsql/script/engine.py,sha256=2WcOfYEOwO7L_NQAd3vk_c2wk1VZKJYSakSl07FBUts,40390
80
+ execsql/script/engine.py,sha256=1_3qMn6a1nD4oYBBRNlwMU_YwZ4f2Om6-CjUGksJT4A,41087
81
81
  execsql/script/variables.py,sha256=MOT9XEHucpuuuHQZM5bklxGMBQcwHzwTBxd0q3aO0XY,11641
82
82
  execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
83
83
  execsql/utils/auth.py,sha256=onXzNkNZQZxGC5w7eey06sjvAIAX_Lf9g7nUJtcsel0,7009
84
84
  execsql/utils/crypto.py,sha256=2OnBWwn9bCBGc1ZkyRv16TvhottoCNYtXqgbE3mG3Sg,2960
85
85
  execsql/utils/datetime.py,sha256=V_itd5vVvUPjT86P_z_hh4mlerMDGhDzI5MwPMDBaI4,7715
86
- execsql/utils/errors.py,sha256=2Nxp_uivLbhgeFNuAPCiGQzE9VAdF4b1kcPClOpvme8,6380
86
+ execsql/utils/errors.py,sha256=YKhYD27-3timuZavc2vIrRIfHa71vzih-KVPsAKgvkU,8163
87
87
  execsql/utils/fileio.py,sha256=F6M4osE0Mb2ycTcvwwcYnhBXH1L36v6d7Oxdab6J16s,24110
88
- execsql/utils/gui.py,sha256=kCfHvqY60jdoJa1k5HlxhrmzM4HPp7uajV6nxz3_Qh4,17583
88
+ execsql/utils/gui.py,sha256=UFtwrXPNqNvZCJFCbumZ1aG2d9B-vyaJXIG83fDHteo,18409
89
89
  execsql/utils/mail.py,sha256=75A1cMnEfyP0_QMMWuSLv8hrcIjc9cGBCrttLpr2TXQ,5372
90
90
  execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
91
91
  execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
92
92
  execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
93
93
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
94
- execsql2-2.10.1.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
95
- execsql2-2.10.1.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
96
- execsql2-2.10.1.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
97
- execsql2-2.10.1.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
98
- execsql2-2.10.1.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
99
- execsql2-2.10.1.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
100
- execsql2-2.10.1.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
101
- execsql2-2.10.1.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
102
- execsql2-2.10.1.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
103
- execsql2-2.10.1.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
104
- execsql2-2.10.1.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
105
- execsql2-2.10.1.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
106
- execsql2-2.10.1.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
107
- execsql2-2.10.1.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
108
- execsql2-2.10.1.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
109
- execsql2-2.10.1.dist-info/METADATA,sha256=uRqE2iIiqGuy00GvroEk5nPDMg8PXCozmOtfi1DJr4Y,16956
110
- execsql2-2.10.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
111
- execsql2-2.10.1.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
112
- execsql2-2.10.1.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
113
- execsql2-2.10.1.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
114
- execsql2-2.10.1.dist-info/RECORD,,
94
+ execsql2-2.11.1.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
95
+ execsql2-2.11.1.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
96
+ execsql2-2.11.1.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
97
+ execsql2-2.11.1.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
98
+ execsql2-2.11.1.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
99
+ execsql2-2.11.1.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
100
+ execsql2-2.11.1.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
101
+ execsql2-2.11.1.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
102
+ execsql2-2.11.1.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
103
+ execsql2-2.11.1.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
104
+ execsql2-2.11.1.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
105
+ execsql2-2.11.1.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
106
+ execsql2-2.11.1.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
107
+ execsql2-2.11.1.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
108
+ execsql2-2.11.1.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
109
+ execsql2-2.11.1.dist-info/METADATA,sha256=cGmNeSCzZvJFmoCzDbMs_M3MjYP6YCZgQF0vNpIknVk,17381
110
+ execsql2-2.11.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
111
+ execsql2-2.11.1.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
112
+ execsql2-2.11.1.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
113
+ execsql2-2.11.1.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
114
+ execsql2-2.11.1.dist-info/RECORD,,