execsql2 2.35.0__py3-none-any.whl → 2.37.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 (38) hide show
  1. execsql/api.py +16 -8
  2. execsql/cli/lint.py +54 -6
  3. execsql/cli/run.py +5 -10
  4. execsql/data/execsql.conf.template +10 -7
  5. execsql/db/base.py +12 -2
  6. execsql/db/duckdb.py +8 -1
  7. execsql/debug/repl.py +2 -2
  8. execsql/metacommands/connect.py +10 -9
  9. execsql/metacommands/io_fileops.py +6 -3
  10. execsql/metacommands/io_write.py +3 -1
  11. execsql/metacommands/script_ext.py +13 -10
  12. execsql/metacommands/system.py +13 -6
  13. execsql/script/ast.py +21 -0
  14. execsql/script/control.py +63 -3
  15. execsql/script/executor.py +369 -219
  16. execsql/script/parser.py +102 -52
  17. execsql/state.py +23 -0
  18. execsql/utils/errors.py +22 -4
  19. execsql/utils/sigterm.py +154 -0
  20. {execsql2-2.35.0.dist-info → execsql2-2.37.0.dist-info}/METADATA +2 -2
  21. {execsql2-2.35.0.dist-info → execsql2-2.37.0.dist-info}/RECORD +38 -37
  22. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/README.md +0 -0
  23. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  24. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  25. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  26. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  27. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  28. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  29. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  30. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  31. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/script_template.sql +0 -0
  32. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  33. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  34. {execsql2-2.35.0.data → execsql2-2.37.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  35. {execsql2-2.35.0.dist-info → execsql2-2.37.0.dist-info}/WHEEL +0 -0
  36. {execsql2-2.35.0.dist-info → execsql2-2.37.0.dist-info}/entry_points.txt +0 -0
  37. {execsql2-2.35.0.dist-info → execsql2-2.37.0.dist-info}/licenses/LICENSE.txt +0 -0
  38. {execsql2-2.35.0.dist-info → execsql2-2.37.0.dist-info}/licenses/NOTICE +0 -0
execsql/script/parser.py CHANGED
@@ -25,7 +25,8 @@ Usage::
25
25
  from __future__ import annotations
26
26
 
27
27
  import re
28
- from collections.abc import Iterable
28
+ from collections.abc import Callable, Iterable
29
+ from dataclasses import replace
29
30
  from pathlib import Path
30
31
  from typing import cast
31
32
 
@@ -96,7 +97,7 @@ _INCLUDE_RX = re.compile(
96
97
  )
97
98
 
98
99
 
99
- def _strip_quotes(s: str) -> str:
100
+ def strip_quotes(s: str) -> str:
100
101
  """Strip a matching pair of surrounding quotes from *s*."""
101
102
  if len(s) >= 2 and s[0] in ('"', "'") and s[-1] == s[0]:
102
103
  return s[1:-1]
@@ -126,6 +127,94 @@ _EXEC_SCRIPT_RX = re.compile(
126
127
  re.I,
127
128
  )
128
129
 
130
+
131
+ def directive_node(command: str, span: SourceSpan) -> IncludeDirective | None:
132
+ """The :class:`IncludeDirective` for an INCLUDE or EXECUTE SCRIPT *command*, or ``None`` for any other.
133
+
134
+ *command* is the metacommand without ``-- !x!``.
135
+ """
136
+ m = _INCLUDE_RX.match(command)
137
+ if m:
138
+ return IncludeDirective(
139
+ span=span,
140
+ target=strip_quotes(m.group("target").strip()),
141
+ if_exists=m.group("exists") is not None,
142
+ command=command.strip(),
143
+ )
144
+ m = _EXEC_SCRIPT_RX.match(command)
145
+ if m:
146
+ return IncludeDirective(
147
+ span=span,
148
+ target=m.group("script_id"),
149
+ is_execute_script=True,
150
+ if_exists=m.group("exists") is not None,
151
+ arguments=m.group("argexp"),
152
+ loop_type=m.group("looptype").upper() if m.group("looptype") else None,
153
+ loop_condition=m.group("loopcond"),
154
+ command=command.strip(),
155
+ )
156
+ return None
157
+
158
+
159
+ def substitute_directive(node: IncludeDirective, substitute: Callable[[str], str]) -> IncludeDirective:
160
+ """*node* with *substitute* applied once to each part that is substituted when it runs.
161
+
162
+ Those parts are INCLUDE's file and EXECUTE SCRIPT's script name and
163
+ arguments. For a node parsed from a metacommand, ``command`` becomes
164
+ the metacommand with them substituted. The metacommand is not
165
+ parsed again, so a value with a line break in it stays one value (an
166
+ unquoted argument still ends at a comma or a closing parenthesis, as it
167
+ always has). A ``WHILE`` / ``UNTIL`` condition is left as written: it
168
+ is substituted on each pass of the loop.
169
+ """
170
+ m = (_EXEC_SCRIPT_RX if node.is_execute_script else _INCLUDE_RX).match(node.command)
171
+ if m is None:
172
+ # Built without the text of a metacommand.
173
+ target = substitute(node.target)
174
+ return replace(
175
+ node,
176
+ target=target if node.is_execute_script else strip_quotes(target.strip()),
177
+ arguments=substitute(node.arguments) if node.arguments is not None else None,
178
+ )
179
+ pieces: list[str] = []
180
+ values: dict[str, str] = {}
181
+ last = 0
182
+ for group in ("script_id", "argexp") if node.is_execute_script else ("target",):
183
+ if m.group(group) is None:
184
+ continue
185
+ start, end = m.span(group)
186
+ values[group] = substitute(node.command[start:end])
187
+ pieces += [node.command[last:start], values[group]]
188
+ last = end
189
+ pieces.append(node.command[last:])
190
+ command = "".join(pieces)
191
+ if node.is_execute_script:
192
+ return replace(node, target=values["script_id"], arguments=values.get("argexp"), command=command)
193
+ return replace(node, target=strip_quotes(values["target"].strip()), command=command)
194
+
195
+
196
+ def metacommand_node(command: str, span: SourceSpan) -> IfBlock | IncludeDirective | MetaCommandStatement:
197
+ """The node for a metacommand that opens or closes no block, however it is reached.
198
+
199
+ The same on a line of its own, inside an inline ``IF (…) { … }`` and
200
+ when added with ``EXTEND SCRIPT … WITH METACOMMAND``: an inline ``IF``
201
+ is an :class:`IfBlock`, INCLUDE and EXECUTE SCRIPT an
202
+ :class:`IncludeDirective`, and any other a :class:`MetaCommandStatement`
203
+ for the dispatch table. A metacommand whose text a substitution
204
+ variable supplies (``-- !x! !!cmd!!``) is classified here as written,
205
+ before substitution, so it always goes to the dispatch table (#113).
206
+ """
207
+ m = _IF_INLINE_RX.match(command)
208
+ if m:
209
+ return IfBlock(
210
+ span=span,
211
+ condition=m.group("cond").strip(),
212
+ body=[metacommand_node(m.group("cmd").strip(), span)],
213
+ command=command.strip(),
214
+ )
215
+ return directive_node(command, span) or MetaCommandStatement(span=span, command=command)
216
+
217
+
129
218
  # A parameter default value is either a double-quoted string, a single-quoted
130
219
  # string (both may contain spaces, commas, and other special characters), or
131
220
  # a bare run of non-whitespace characters. Unquoted values must not begin
@@ -197,7 +286,7 @@ def _parse_param_defs(
197
286
  )
198
287
  name, default = m.group(1), m.group(2)
199
288
  if default is not None:
200
- default = _strip_quotes(default)
289
+ default = strip_quotes(default)
201
290
  if seen_optional is None:
202
291
  seen_optional = name
203
292
  elif seen_optional is not None:
@@ -512,7 +601,7 @@ def _parse_lines(lines: Iterable[str], source_name: str, errors: list[ErrInfo] |
512
601
  in_sql_block = True
513
602
  block_stack.append(
514
603
  _BlockFrame(
515
- SqlBlock(span=SourceSpan(source_name, file_lineno)),
604
+ SqlBlock(span=SourceSpan(source_name, file_lineno), command=cmd_text),
516
605
  kind="sqlblock",
517
606
  start_line=file_lineno,
518
607
  source=source_name,
@@ -576,6 +665,7 @@ def _parse_lines(lines: Iterable[str], source_name: str, errors: list[ErrInfo] |
576
665
  span=SourceSpan(source_name, file_lineno),
577
666
  name=name,
578
667
  param_defs=param_defs,
668
+ command=cmd_text,
579
669
  ),
580
670
  kind="script",
581
671
  start_line=file_lineno,
@@ -630,6 +720,7 @@ def _parse_lines(lines: Iterable[str], source_name: str, errors: list[ErrInfo] |
630
720
  IfBlock(
631
721
  span=SourceSpan(source_name, file_lineno),
632
722
  condition=m.group("cond").strip(),
723
+ command=cmd_text,
633
724
  ),
634
725
  kind="if",
635
726
  start_line=file_lineno,
@@ -639,18 +730,8 @@ def _parse_lines(lines: Iterable[str], source_name: str, errors: list[ErrInfo] |
639
730
  continue
640
731
 
641
732
  # -- IF (inline form): IF (cond) { cmd } --
642
- m = _IF_INLINE_RX.match(cmd_text)
643
- if m:
644
- inner = MetaCommandStatement(
645
- span=SourceSpan(source_name, file_lineno),
646
- command=m.group("cmd").strip(),
647
- )
648
- if_node = IfBlock(
649
- span=SourceSpan(source_name, file_lineno),
650
- condition=m.group("cond").strip(),
651
- body=[inner],
652
- )
653
- _current_body().append(if_node)
733
+ if _IF_INLINE_RX.match(cmd_text):
734
+ _current_body().append(metacommand_node(cmd_text, SourceSpan(source_name, file_lineno)))
654
735
  continue
655
736
 
656
737
  # -- ELSEIF --
@@ -673,6 +754,7 @@ def _parse_lines(lines: Iterable[str], source_name: str, errors: list[ErrInfo] |
673
754
  ElseIfClause(
674
755
  condition=m.group("cond").strip(),
675
756
  span=SourceSpan(source_name, file_lineno),
757
+ command=cmd_text,
676
758
  ),
677
759
  )
678
760
  continue
@@ -767,6 +849,7 @@ def _parse_lines(lines: Iterable[str], source_name: str, errors: list[ErrInfo] |
767
849
  span=SourceSpan(source_name, file_lineno),
768
850
  loop_type=m.group("looptype").upper(),
769
851
  condition=m.group("loopcond").strip(),
852
+ command=cmd_text,
770
853
  ),
771
854
  kind="loop",
772
855
  start_line=file_lineno,
@@ -793,7 +876,7 @@ def _parse_lines(lines: Iterable[str], source_name: str, errors: list[ErrInfo] |
793
876
  if m:
794
877
  block_stack.append(
795
878
  _BlockFrame(
796
- BatchBlock(span=SourceSpan(source_name, file_lineno)),
879
+ BatchBlock(span=SourceSpan(source_name, file_lineno), command=cmd_text),
797
880
  kind="batch",
798
881
  start_line=file_lineno,
799
882
  source=source_name,
@@ -814,41 +897,8 @@ def _parse_lines(lines: Iterable[str], source_name: str, errors: list[ErrInfo] |
814
897
  _close_block(closed, file_lineno)
815
898
  continue
816
899
 
817
- # -- INCLUDE --
818
- m = _INCLUDE_RX.match(cmd_text)
819
- if m:
820
- _current_body().append(
821
- IncludeDirective(
822
- span=SourceSpan(source_name, file_lineno),
823
- target=_strip_quotes(m.group("target").strip()),
824
- if_exists=m.group("exists") is not None,
825
- ),
826
- )
827
- continue
828
-
829
- # -- EXECUTE SCRIPT / RUN SCRIPT --
830
- m = _EXEC_SCRIPT_RX.match(cmd_text)
831
- if m:
832
- _current_body().append(
833
- IncludeDirective(
834
- span=SourceSpan(source_name, file_lineno),
835
- target=m.group("script_id"),
836
- is_execute_script=True,
837
- if_exists=m.group("exists") is not None,
838
- arguments=m.group("argexp"),
839
- loop_type=m.group("looptype").upper() if m.group("looptype") else None,
840
- loop_condition=m.group("loopcond"),
841
- ),
842
- )
843
- continue
844
-
845
- # -- All other metacommands (flat) --
846
- _current_body().append(
847
- MetaCommandStatement(
848
- span=SourceSpan(source_name, file_lineno),
849
- command=cmd_text,
850
- ),
851
- )
900
+ # -- INCLUDE, EXECUTE SCRIPT and all other metacommands (flat) --
901
+ _current_body().append(metacommand_node(cmd_text, SourceSpan(source_name, file_lineno)))
852
902
  continue
853
903
 
854
904
  # --- SQL line ---
execsql/state.py CHANGED
@@ -467,6 +467,29 @@ class _StateModule(types.ModuleType):
467
467
  return sorted(set(super().__dir__()) | _CONTEXT_ATTRS)
468
468
 
469
469
 
470
+ def _context_property(name: str) -> property:
471
+ """A property on :class:`_StateModule` that reads *name* from the active context.
472
+
473
+ Without it every ``_state.conf`` read failed the module's own lookup and
474
+ reached :meth:`_StateModule.__getattr__` through an ``AttributeError``.
475
+ Building that error is cheap in a script, but on Python 3.13 it checks
476
+ whether the module shadows a standard-library one, which takes ~12 µs
477
+ when ``sys.path[0]`` is ``''`` (``python -c``, stdin, the REPL,
478
+ notebooks). Writes and deletes still go through ``__setattr__`` and
479
+ ``__delattr__``.
480
+ """
481
+
482
+ def get(self: _StateModule) -> Any:
483
+ return getattr(_get_ctx(), name)
484
+
485
+ return property(get)
486
+
487
+
488
+ for _name in _CONTEXT_ATTRS:
489
+ setattr(_StateModule, _name, _context_property(_name))
490
+ del _name
491
+
492
+
470
493
  # ---------------------------------------------------------------------------
471
494
  # Utility functions
472
495
  # ---------------------------------------------------------------------------
execsql/utils/errors.py CHANGED
@@ -121,7 +121,8 @@ def stamp_errinfo(errinfo: ErrInfo) -> ErrInfo:
121
121
  errinfo.script_file = lc.source
122
122
  errinfo.script_line_no = lc.line_no
123
123
  if errinfo.cmd is None:
124
- errinfo.cmd = lc.command.commandline() if hasattr(lc.command, "commandline") else None
124
+ # The metacommand as written, without "-- !x!", as upstream reported it.
125
+ errinfo.cmd = getattr(lc.command, "statement", None)
125
126
  errinfo.cmdtype = lc.command_type
126
127
  return errinfo
127
128
 
@@ -129,10 +130,14 @@ def stamp_errinfo(errinfo: ErrInfo) -> ErrInfo:
129
130
  def _run_deferred_script(spec: Any) -> None:
130
131
  """Execute a deferred ScriptExecSpec (ON ERROR_HALT / ON CANCEL_HALT EXECUTE SCRIPT).
131
132
 
132
- Executes the target script natively through the AST executor.
133
+ Executes the target script natively through the AST executor. Its
134
+ arguments are substituted now, when the halt fires, so a deferred
135
+ variable (``!{$ERROR_MESSAGE}!``) holds the error that fired it.
133
136
  """
134
137
  from execsql.script.ast import IncludeDirective, SourceSpan
135
- from execsql.script.executor import _execute_script_native
138
+ from execsql.script.engine import substitute_vars
139
+ from execsql.script.executor import _execute_script_native, _script_args, _stack_localvars
140
+ from execsql.script.parser import substitute_directive
136
141
  from execsql.state import get_context
137
142
 
138
143
  script_id = spec.script_id.lower() if hasattr(spec, "script_id") else None
@@ -145,8 +150,16 @@ def _run_deferred_script(spec: Any) -> None:
145
150
  arguments=spec.arg_exp,
146
151
  loop_type=spec.looptype,
147
152
  loop_condition=spec.loopcond,
153
+ command=(
154
+ f"EXECUTE SCRIPT {spec.script_id}"
155
+ + (f" WITH ARGUMENTS ({spec.arg_exp})" if spec.arg_exp else "")
156
+ + (f" {spec.looptype} ({spec.loopcond})" if spec.looptype else "")
157
+ ),
148
158
  )
149
- _execute_script_native(ctx, node, ctx.ast_scripts[script_id])
159
+ localvars = _stack_localvars(ctx)
160
+ node = substitute_directive(node, lambda text: substitute_vars(text, localvars, ctx=ctx))
161
+ script_block = ctx.ast_scripts[script_id]
162
+ _execute_script_native(ctx, node, script_block, _script_args(node, script_block))
150
163
  else:
151
164
  raise ErrInfo(
152
165
  type="error",
@@ -199,6 +212,11 @@ def exit_now(exit_status: int, errinfo: ErrInfo | None, logmsg: str | None = Non
199
212
  except Exception:
200
213
  if _state.exec_log is not None:
201
214
  _state.exec_log.log_status_error("Failed to write the ON CANCEL_HALT WRITE message.")
215
+ # A halt inside BEGIN BATCH rolls the batch back: after the error is
216
+ # shown, in case a lost connection makes the rollback wait, and before
217
+ # the ON ERROR_HALT / ON CANCEL_HALT script, whose own SQL then commits.
218
+ if _state.status is not None:
219
+ _state.status.batch.abandon_all()
202
220
  # Defer import to avoid circular dependencies
203
221
  from execsql.utils.gui import gui_console_isrunning, gui_console_wait_user, gui_console_off
204
222
 
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ SIGTERM handling for the ``execsql`` command.
5
+
6
+ :func:`terminate` is the handler the console entry point installs: it kills
7
+ the commands ``SYSTEM_CMD`` is waiting on, then ends the run through
8
+ ``SystemExit`` (exit status 143), so exit handlers run and the log is
9
+ written. :func:`command` starts such a command; :func:`sigterm_held` holds
10
+ a SIGTERM back for the few lines where raising would leave something
11
+ behind, and acts on it as the block ends.
12
+
13
+ Python runs signal handlers on the main thread only, between bytecodes. A
14
+ hold therefore counts only on the main thread: a SIGTERM raised there in the
15
+ middle of ``Popen()`` (which forks the command before it returns) would
16
+ leave the command running with nothing to stop it. The Textual console runs
17
+ the script on another thread, which the handler never interrupts; that
18
+ thread stops at its next command instead (see :func:`command`). It is a
19
+ daemon thread, which may never run again once the main thread exits, so the
20
+ handler waits for a command that thread is starting and kills it itself.
21
+
22
+ The library API (``execsql.run()``) does not install the handler, so there
23
+ :func:`sigterm_held` holds nothing back and :func:`command` only stops its
24
+ command when the block is left.
25
+ """
26
+
27
+ import subprocess
28
+ import threading
29
+ from collections.abc import Iterator
30
+ from contextlib import contextmanager
31
+ from typing import Any
32
+
33
+ #: Depth of nested :func:`sigterm_held` blocks on the main thread.
34
+ _held = 0
35
+ #: The signal number of a SIGTERM that arrived while held.
36
+ _pending: int | None = None
37
+ #: The signal number of the SIGTERM that ended the run.
38
+ _terminated: int | None = None
39
+ #: Commands started by :func:`command` and not yet stopped or finished.
40
+ _commands: list[subprocess.Popen] = []
41
+ #: Held while a command is started and added to ``_commands``. Reentrant: a
42
+ #: second SIGTERM can land while the handler holds it.
43
+ _starting = threading.RLock()
44
+ #: How long the handler waits for a command that is starting (seconds).
45
+ _START_WAIT = 5.0
46
+
47
+
48
+ def terminate(signum: int, _frame: Any) -> None:
49
+ """Kill the running commands, record a ``terminated`` exit and raise ``SystemExit(128 + signum)``.
50
+
51
+ Inside a :func:`sigterm_held` block the signal is only noted; the block's
52
+ end acts on it.
53
+ """
54
+ global _pending
55
+ if _held:
56
+ _pending = signum
57
+ return
58
+ _exit(signum)
59
+
60
+
61
+ def _exit(signum: int) -> None:
62
+ global _pending, _terminated
63
+ import execsql.state as _state
64
+
65
+ _pending = None
66
+ # Set first, so a thread about to start a command sees it and starts none.
67
+ _terminated = signum
68
+ # Wait for a command another thread is starting to be listed: that thread
69
+ # (a daemon) may never run again once this one exits. Past the wait, a
70
+ # command still starting is left to that thread.
71
+ listed = _starting.acquire(timeout=_START_WAIT)
72
+ try:
73
+ for proc in list(_commands):
74
+ # Kill only: the handler may have interrupted Popen.wait() on this
75
+ # command, which holds a lock a second wait() would block on
76
+ # forever. The command() block reaps it as it is left.
77
+ _kill(proc)
78
+ finally:
79
+ if listed:
80
+ _starting.release()
81
+ if _state.exec_log is not None:
82
+ _state.exec_log.log_exit_terminated()
83
+ raise SystemExit(128 + signum)
84
+
85
+
86
+ def _kill(proc: subprocess.Popen) -> bool:
87
+ """Kill *proc* unless it has finished; False if the OS refuses (a command running as another user)."""
88
+ try:
89
+ proc.kill()
90
+ except OSError:
91
+ return False
92
+ return True
93
+
94
+
95
+ def _end_if_terminated() -> None:
96
+ """Raise the run's ``SystemExit`` once a SIGTERM has ended it (for a thread the handler does not interrupt)."""
97
+ if _terminated is not None:
98
+ raise SystemExit(128 + _terminated)
99
+
100
+
101
+ @contextmanager
102
+ def sigterm_held() -> Iterator[None]:
103
+ """Hold back a SIGTERM until the block ends, then end the run with it.
104
+
105
+ Keep the block short: a run asked to stop waits for it. An exception
106
+ the block raises gives way to the held SIGTERM. Off the main thread it
107
+ holds nothing, since the handler never interrupts that thread.
108
+ """
109
+ global _held
110
+ if threading.current_thread() is not threading.main_thread():
111
+ yield
112
+ return
113
+ _held += 1
114
+ try:
115
+ yield
116
+ finally:
117
+ _held -= 1
118
+ if not _held and _pending is not None:
119
+ _exit(_pending)
120
+
121
+
122
+ @contextmanager
123
+ def command(args: list[str]) -> Iterator[subprocess.Popen]:
124
+ """Start *args* as a command that is stopped when the block ends or the run is terminated.
125
+
126
+ Leaving the block, however it is left (the command finished, a timeout,
127
+ Ctrl-C, a SIGTERM), kills the command if it is still running and reaps
128
+ it. A SIGTERM at any point after the command starts kills it before the
129
+ run's ``SystemExit`` is raised. On a thread other than the main one,
130
+ which the handler does not interrupt, the block raises that
131
+ ``SystemExit`` itself once the SIGTERM has ended the run, and no command
132
+ starts after it.
133
+ """
134
+ proc = None
135
+ try:
136
+ with sigterm_held(), _starting:
137
+ _end_if_terminated()
138
+ proc = subprocess.Popen(args)
139
+ _commands.append(proc)
140
+ # The handler killed the command if it ended the run while it started.
141
+ _end_if_terminated()
142
+ yield proc
143
+ finally:
144
+ if proc is not None:
145
+ with sigterm_held():
146
+ # Killed before it is unlisted, so the handler kills it if this
147
+ # thread never gets that far.
148
+ killed = _kill(proc)
149
+ # Ctrl-C can land between Popen() returning and the append.
150
+ if proc in _commands:
151
+ _commands.remove(proc)
152
+ if killed:
153
+ proc.wait()
154
+ _end_if_terminated()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.35.0
3
+ Version: 2.37.0
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: Homepage, https://execsql2.readthedocs.io/en/stable/
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -441,7 +441,7 @@ execsql format --no-sql --in-place scripts/
441
441
  ```yaml
442
442
  repos:
443
443
  - repo: https://github.com/geocoug/execsql
444
- rev: v2.35.0
444
+ rev: v2.37.0
445
445
  hooks:
446
446
  - id: execsql-format
447
447
  - id: execsql-lint
@@ -1,6 +1,6 @@
1
1
  execsql/__init__.py,sha256=ZeVnpEWu8M2bn3qLmVM72NhWRueUGNbsX8UAPTermo0,901
2
2
  execsql/__main__.py,sha256=Nnh7_QgjYTTzd1Kog2thl7wzNpoxfnZ3YaurSOMzE_U,270
3
- execsql/api.py,sha256=WrlmuLKYpOnNLW8l9Svzeh28Ox0zGFrjhJtYlKY7niU,26831
3
+ execsql/api.py,sha256=ElUu7NOZLbpmSTQOCP0ePeyiSYQhGAAZHMS_3p2zlU0,27349
4
4
  execsql/config.py,sha256=wTtugbLNplxgva4P8uD8jKYs8N0MywHPTvjwHTsYFDU,40528
5
5
  execsql/config_validation.py,sha256=a4ZszyJFGTNgs1jkvXE3yMCaYFtF1Uqih2ojMDVh1v8,10556
6
6
  execsql/exceptions.py,sha256=aHavdblmP7m5ZLxyyKKmPSwwbdmP962j4ShP1Gd9r64,9651
@@ -11,7 +11,7 @@ execsql/models.py,sha256=8AnxILNwFZjDX8xXvv5usgOcTDsOIwkfZJjeYbXREe8,18374
11
11
  execsql/parser.py,sha256=bw26e1lD-PZscV11VtBdqSLkjpcYfNLQC2EYfYD5UwU,16012
12
12
  execsql/plugins.py,sha256=RykMlIjTyWcnQc9ygjLErS3wdcsZSsG9QTIvg4DwfAI,13626
13
13
  execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- execsql/state.py,sha256=rXhxx58dVFjhG58HGOXm3ewloVlvpcaYnW1rij4qjkM,23478
14
+ execsql/state.py,sha256=oAtaiX9fD0y7cwO6Is9mM9MjaSUe-AmnjuYJ3g3r3kA,24294
15
15
  execsql/state.pyi,sha256=ZDHZ1DPKYu4Qs4bcJT31XNv4GOt6O4s_zH4UHCowovw,4344
16
16
  execsql/types.py,sha256=HiIhc8wqm9Trt8WYP40VU93uuBSPo1uPP3z-sMsUI1o,33719
17
17
  execsql/cli/__init__.py,sha256=FHg2lm2gt4L-XBnlVORn3qWHZ4739Ao-uwBIURfVIjg,2798
@@ -19,9 +19,9 @@ execsql/cli/application.py,sha256=tsSYE8cerJxY6y73GM_Xr1WBwc6A0_al8y969B6lPP8,12
19
19
  execsql/cli/dispatch.py,sha256=deyN2QQYvdzMSqGGa3CSMpaqdLNeMgc5Uke0mUbl764,2704
20
20
  execsql/cli/dsn.py,sha256=Jqr0CBtj6L3BY25Ej-U13xHV85phX1TFTVpjB7A-KIs,4815
21
21
  execsql/cli/help.py,sha256=bEy6ssVjyIw73JD_UPLXnCtnCifdVCKJr_cydAAVxhk,8220
22
- execsql/cli/lint.py,sha256=kQ5zOayLTOY1jib_6jME2mzSGKCsZ6FI_qr4fysLlRs,48433
22
+ execsql/cli/lint.py,sha256=5rNSVbHbuhMo5xvPaNq0tGIGl7i0xNou6Sb8ZfnLNKc,50262
23
23
  execsql/cli/options.py,sha256=Srhf1gEOB5yy_7PxkTKBv07xK8Qzw8H-z7PUUnItpwE,2916
24
- execsql/cli/run.py,sha256=bMLvStRSX9svzb8GmbLJ9QS5bsPPoxBUhBbIwi1aX_g,42960
24
+ execsql/cli/run.py,sha256=L1NdYlEKsy7E5-B67C9UqH3cJRstT9HUiAtUW6Qw828,42845
25
25
  execsql/cli/commands/__init__.py,sha256=FVe_xF7w98krP-USxqf-BIcflA1ctMYVsV8p0i6cChA,572
26
26
  execsql/cli/commands/config.py,sha256=V7CGGkhSCqeJIlAXxAYyG_xNhnsORiwqth2XWEVPzss,8213
27
27
  execsql/cli/commands/format.py,sha256=3awfNmCc6kImeyXKZo1Jt1qXbEuWk86ZllqEOY5vxzw,4456
@@ -31,13 +31,13 @@ execsql/cli/commands/listing.py,sha256=thtkJ-ILGd0SPs57oVIMkkCGpXsfS0hQxueMc2ccR
31
31
  execsql/cli/commands/lsp.py,sha256=8xIrqA4dnapdlLqbGGb53YtFTnsX2YqpLYvrrl-Z6pE,4051
32
32
  execsql/cli/commands/run.py,sha256=sGFDku66CazJrorr0mmrhUI3QDvQVSMEYlU8G1Pd5fQ,16492
33
33
  execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- execsql/data/execsql.conf.template,sha256=3-sJPrB1u3KPsuiXv7X_BcSugUmRBgqFh37GuevyfTc,13138
34
+ execsql/data/execsql.conf.template,sha256=kEUUg1x2TcJ356Zl-AvHqItAZzcG3hBBa8-pmqdSn5U,13292
35
35
  execsql/data/script_header.sql,sha256=a7WKScekNgJoxVEmMRTkWv8NsAzW1jJ4vJfGy9HVy0g,349
36
36
  execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
37
37
  execsql/db/access.py,sha256=fxiC3rtoOJzrNXNxau-w7oh1fd9tyh2_PMliBbFc164,21427
38
- execsql/db/base.py,sha256=nMaODzV2_JqJzXJCshlWRQ9ZANPGK6IcIJ4_PBtXyT0,47714
38
+ execsql/db/base.py,sha256=BoCOp0hBa50jpCkyQ6W6GK_u9pMV5D0FuHx-vWzjFZ4,48090
39
39
  execsql/db/dsn.py,sha256=Evrc6MUllJVFDn0jZB_iks-aKCsXuNF9XYjAkttqgGQ,6995
40
- execsql/db/duckdb.py,sha256=c8VdZcmZdmyH8abrxM8mUk0BGIbittp-oV6UK7lsbgg,13954
40
+ execsql/db/duckdb.py,sha256=QmIPVOVgltC-jCwQlu8LllkrQjhJ7gjjyNOXeKQNAHg,14262
41
41
  execsql/db/factory.py,sha256=Uz7Xcbff6ZZbtLM55FhHCwGdo4KKfqU_1kyjvOHn-HY,5691
42
42
  execsql/db/firebird.py,sha256=CoNnzFtOFAkp8OJByXXXclitCPAAjM23eFJGfy3C7IE,10171
43
43
  execsql/db/mysql.py,sha256=X-brnIFiQaOwVbY0uqPrC4esclhBv4sg-gHW3EVxHNw,26923
@@ -49,7 +49,7 @@ execsql/db/sqlserver.py,sha256=WIVvTp1MNKaUZn912U7ErcjSqhaLTqnZ2hfv_Gcso74,11601
49
49
  execsql/db/target_types.py,sha256=68nSyYY0mj6sgS26zeOYcFuUJA_ik08-0BeNsSM7UrE,8563
50
50
  execsql/db/tiers.py,sha256=PH2sUYwlYqcANGVKXprWQKVIlM6fpWU9q74djn8oFwc,2924
51
51
  execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
52
- execsql/debug/repl.py,sha256=jnwrqRac4krc1pES7Ni-j1Dyw3mJIcWl5LQQIX1GkRw,20753
52
+ execsql/debug/repl.py,sha256=NE3HfMCcDIlGsL_qNsKUfU_B_ZInjqGm-z89aZB2sAA,20785
53
53
  execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
54
54
  execsql/exporters/base.py,sha256=_GoO0YqwlTxrjogxHI3CxxRR64sMgIEjRogt0KCqzoY,6402
55
55
  execsql/exporters/delimited.py,sha256=N0cP_CNlb_LLCUqbHmTQsOGufWnD_ddGdeFnXPE8Erc,35606
@@ -99,7 +99,7 @@ execsql/lsp/rename.py,sha256=fYk1gDSWWhqyoIGZO5hVIHPYYdGKNjgZS0mijFDFpac,3756
99
99
  execsql/lsp/server.py,sha256=gTjk1agZbygdbcowa3jkL45WJ37sxhJ58oiLB7BM6Bw,19893
100
100
  execsql/metacommands/__init__.py,sha256=aMf-3IRjMpNFSUI-IMAcvL-kXZHq5qF6W_p3UawdQwM,13115
101
101
  execsql/metacommands/conditions.py,sha256=4DsMXr6vnfbtcKqgnxYV4UK4NmAfE_hpfgkFjlH0bjo,29844
102
- execsql/metacommands/connect.py,sha256=SPk4h9yiUgTD3w1gNOT_oYx8VShggsO357by5Hoy6OI,15344
102
+ execsql/metacommands/connect.py,sha256=rXUd78La8JeP0bqDgEPtEKPhH95RAtQGNcXSurXaqBo,15636
103
103
  execsql/metacommands/control.py,sha256=9swW6X8jXDUpu5cHiAnQVx1ahFyWxlx7mjg4dqYfX5Y,9355
104
104
  execsql/metacommands/data.py,sha256=9zxyg3_kV4jRuagUNtSlhhlvH2TiPP0gdNAaXBXdhI0,12493
105
105
  execsql/metacommands/debug.py,sha256=mKRT7uLwnw34Xl46fNDI_zH4MsPpZyihoLlonapbXuU,12981
@@ -107,51 +107,52 @@ execsql/metacommands/dispatch.py,sha256=m7p2L_HtooKfSv58MHbDmYJJ7Xe1UEpr6mnHdvAB
107
107
  execsql/metacommands/effects.py,sha256=cRSo48R4nRJzkRR3OGHLhJzofcdAaNIobaxmn1N9TGw,3813
108
108
  execsql/metacommands/io.py,sha256=vlGBje5sgnqeilooMdhJDgSRIhysHy5_7LrKtik9Xjs,3011
109
109
  execsql/metacommands/io_export.py,sha256=_1o5T3Ejqq_0N1hYwaRPgDktn8nZT8sQLMyXPRgXEec,14587
110
- execsql/metacommands/io_fileops.py,sha256=ru7zYgNV7rg0WtaDwhdYjy5iJTcecvjzR1PCdMNGG0A,13364
110
+ execsql/metacommands/io_fileops.py,sha256=VlBscDcJ9RBShwmk9Jky9MWZbM6lA24_G4oymlehK-A,13550
111
111
  execsql/metacommands/io_import.py,sha256=gJICeK1MOfG6D73I7m8zBCalvr_ZJzD-QBKOpKYeYXo,15684
112
- execsql/metacommands/io_write.py,sha256=yXDkMbQ3haDARVATVUYZprakyJHSyk1NywdKkFLpQ14,12783
112
+ execsql/metacommands/io_write.py,sha256=hKnDUB6jYQc38Y8Ou_1e8tuESnDiSf6e2iAN7FVtDSY,12889
113
113
  execsql/metacommands/prompt.py,sha256=PlXhtBYCembG_1SSCrwpQR10mGF3_jXodu4aCSHlSCM,37950
114
114
  execsql/metacommands/reference.py,sha256=9Q7DSiPEjXyD9HQkkqiToRN59TrZ8_6Gz9VVVv9WSjw,54369
115
- execsql/metacommands/script_ext.py,sha256=CsUbyq8QFPnnvfX_k7fPlYNvvRw1xLmAWER4Exo5pwc,3658
116
- execsql/metacommands/system.py,sha256=6X2qmMpGUHDzyEluEotXt0Ks9jKQTpc2x0eoYNgN12M,7997
115
+ execsql/metacommands/script_ext.py,sha256=dtEjvl_gjPeAmsGZdp1evqGuz8lErKsuajMFGuPsEbE,3917
116
+ execsql/metacommands/system.py,sha256=0noc1u-KlO6gE0KqJDScDeX0gIlG5ciaJplxOt-EDyo,8517
117
117
  execsql/metacommands/upsert.py,sha256=hcYGonmh2sdijbqwNynn1-iajMnLp7x5vbRbtfjaf8c,21461
118
118
  execsql/script/__init__.py,sha256=I7hVO6wm3iOocxRRGEcFHhjdtL1moKbUYLB-wKbjPdk,3267
119
- execsql/script/ast.py,sha256=WqCpFxhxrT2IIGvbArTp3QS0BZryRZAb7IL4ri3I6xg,20591
120
- execsql/script/control.py,sha256=WqLy-HLPqHG3vEzYpKMiIJsD7LpORjyQuUWzFzcGz4w,2327
119
+ execsql/script/ast.py,sha256=KexrVh6dE4nHCB5CbC9odiPdyWw4rnUqD8bOsYdyNXg,21417
120
+ execsql/script/control.py,sha256=-wyfeVR--Kst9hU3rmRkoQ-cPU4g23kCkod4r0vRKxE,4762
121
121
  execsql/script/engine.py,sha256=h1pUSXGqwJASLJizqzd9LBMn_U5Vuqc1UbaZk1xUinw,18468
122
- execsql/script/executor.py,sha256=s942xbokn1S8wUNqZd8zGKs-Hr-6Gd8FNaWqnTbZk28,41041
123
- execsql/script/parser.py,sha256=z73v6t6ZbPUKkRu-UOIL6q1cCRDeoRvG2aXXC8NpMvE,37162
122
+ execsql/script/executor.py,sha256=TRRpycxyGch179imtiiUiWmCl8X3uWham_bG0fnvYjE,46799
123
+ execsql/script/parser.py,sha256=wogBw31kPddNox3fz5KPS-y3He163h_T-ES2ZfZsrCI,39695
124
124
  execsql/script/variables.py,sha256=XHRnViFp0zPy-skGpYbpckkYAcczqpw1lqTVLR9eZvo,17735
125
125
  execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
126
126
  execsql/utils/auth.py,sha256=0LYKdNMhodq9R0B8WyKp69a-xlP9wOR6Ua9Lgo2aJnQ,14868
127
127
  execsql/utils/color.py,sha256=goQqhnLg8yERpFolQikA9zYSVcuAm0unKTJiWTbQm-U,634
128
128
  execsql/utils/crypto.py,sha256=KlT6lPk-v0-qQQfAMfWvT9Q6kRdCD3_5JVaLajNocKM,3051
129
129
  execsql/utils/datetime.py,sha256=9ZizCFBYut5gC4gU3Cmegfddv1HLoU5xVQWm9thy5EI,6127
130
- execsql/utils/errors.py,sha256=PtOM2XZSMxXVyHEOcCLAmkeLNjGbGuecU8TVDgCF2RE,11668
130
+ execsql/utils/errors.py,sha256=NspFIQXAcv3AWtXhcqV8K8r9kCeywW9whIj1uXiUXxc,12770
131
131
  execsql/utils/fileio.py,sha256=IqIEhun9aOnr1oBCWK2z2VAPVo95bZ54cVglLW4rV9M,48208
132
132
  execsql/utils/gui.py,sha256=1iiupP9AsLdR6ST0gcLE143o6y6YGUFlNv_pdVPxfCE,23943
133
133
  execsql/utils/mail.py,sha256=4RiKtXa_sqij_YGdYmjQ3PG0u3G9qjs-9caQwBErtBw,7278
134
134
  execsql/utils/numeric.py,sha256=_7bi5R6pHjMCkOPne_Sh5nUj-B8e9g6WFmjptFHaunA,1573
135
135
  execsql/utils/paths.py,sha256=7IIHT6vqXTfs6Ltfrzm_I2ZV4pU8IuedOLkHo6a1_K8,1175
136
136
  execsql/utils/regex.py,sha256=DGzPBl0Kt4--RbwPhoZI6Bm6N_CBUvI0DbqE4EF6qp8,3718
137
+ execsql/utils/sigterm.py,sha256=_oK-Hj8vHiKTgR04mNud7BMX90Fb1TDHb9eT7f-auAI,5842
137
138
  execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
138
139
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
139
- execsql2-2.35.0.data/data/execsql2_extras/README.md,sha256=6f5bWiBlKRn8s6GVrU423-HFrU1Jwbk2AabxraLu3AY,5398
140
- execsql2-2.35.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
141
- execsql2-2.35.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
142
- execsql2-2.35.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
143
- execsql2-2.35.0.data/data/execsql2_extras/md_compare.sql,sha256=ARYEcOlxWLxfw-KCKDeJRdCqnuCb9PVwGaHtITR2sYU,26535
144
- execsql2-2.35.0.data/data/execsql2_extras/md_glossary.sql,sha256=gu2PDTm295YvxR7cDp25Mf-xXHly0BvlMudoNOrep5Q,10931
145
- execsql2-2.35.0.data/data/execsql2_extras/md_upsert.sql,sha256=YNUTFS6yB4bZXs-wRDn6dN8jL5wIumzZyE2uo5arIos,113407
146
- execsql2-2.35.0.data/data/execsql2_extras/pg_compare.sql,sha256=WcupGxQoWtMwHVPuy42xCpP4gAV4Ttb1W-_hwh-3xb4,24529
147
- execsql2-2.35.0.data/data/execsql2_extras/pg_glossary.sql,sha256=CfQz2HZj2nMvJfYfQrAmNYdx0uRrOIWsCXwVo5gKv_k,9953
148
- execsql2-2.35.0.data/data/execsql2_extras/script_template.sql,sha256=s5i0HULxp49N2Cpd24iALleM_dBq5oLuN3Arx_4Z7-o,11140
149
- execsql2-2.35.0.data/data/execsql2_extras/ss_compare.sql,sha256=PpU-vPyqqC1faBoo5zwVZlZHYKkT79QZisWVlk4o-ss,26415
150
- execsql2-2.35.0.data/data/execsql2_extras/ss_glossary.sql,sha256=_oTiZHpadPXWrNs8VoRe9PZgX9XdK490NMVWVTVtjl4,13110
151
- execsql2-2.35.0.data/data/execsql2_extras/ss_upsert.sql,sha256=Dvst877h4KHn8ayYF9EXI_OxqLQ89wyfcnGYYT4szro,118304
152
- execsql2-2.35.0.dist-info/METADATA,sha256=w_11vJcNw1_5LNse5KT9rHS7IJDv3-Ng6dCGM7q0_Oo,26939
153
- execsql2-2.35.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
154
- execsql2-2.35.0.dist-info/entry_points.txt,sha256=jOf53-4IVGjEZeNYUOF1GGaiPNfVZcNKxP3vDFKEyMA,58
155
- execsql2-2.35.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
156
- execsql2-2.35.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
157
- execsql2-2.35.0.dist-info/RECORD,,
140
+ execsql2-2.37.0.data/data/execsql2_extras/README.md,sha256=6f5bWiBlKRn8s6GVrU423-HFrU1Jwbk2AabxraLu3AY,5398
141
+ execsql2-2.37.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
142
+ execsql2-2.37.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
143
+ execsql2-2.37.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
144
+ execsql2-2.37.0.data/data/execsql2_extras/md_compare.sql,sha256=ARYEcOlxWLxfw-KCKDeJRdCqnuCb9PVwGaHtITR2sYU,26535
145
+ execsql2-2.37.0.data/data/execsql2_extras/md_glossary.sql,sha256=gu2PDTm295YvxR7cDp25Mf-xXHly0BvlMudoNOrep5Q,10931
146
+ execsql2-2.37.0.data/data/execsql2_extras/md_upsert.sql,sha256=YNUTFS6yB4bZXs-wRDn6dN8jL5wIumzZyE2uo5arIos,113407
147
+ execsql2-2.37.0.data/data/execsql2_extras/pg_compare.sql,sha256=WcupGxQoWtMwHVPuy42xCpP4gAV4Ttb1W-_hwh-3xb4,24529
148
+ execsql2-2.37.0.data/data/execsql2_extras/pg_glossary.sql,sha256=CfQz2HZj2nMvJfYfQrAmNYdx0uRrOIWsCXwVo5gKv_k,9953
149
+ execsql2-2.37.0.data/data/execsql2_extras/script_template.sql,sha256=s5i0HULxp49N2Cpd24iALleM_dBq5oLuN3Arx_4Z7-o,11140
150
+ execsql2-2.37.0.data/data/execsql2_extras/ss_compare.sql,sha256=PpU-vPyqqC1faBoo5zwVZlZHYKkT79QZisWVlk4o-ss,26415
151
+ execsql2-2.37.0.data/data/execsql2_extras/ss_glossary.sql,sha256=_oTiZHpadPXWrNs8VoRe9PZgX9XdK490NMVWVTVtjl4,13110
152
+ execsql2-2.37.0.data/data/execsql2_extras/ss_upsert.sql,sha256=Dvst877h4KHn8ayYF9EXI_OxqLQ89wyfcnGYYT4szro,118304
153
+ execsql2-2.37.0.dist-info/METADATA,sha256=JgJFZpUBKXM92MDCnvSW34G7J9P5U0_A77eqr-fGNsI,26939
154
+ execsql2-2.37.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
155
+ execsql2-2.37.0.dist-info/entry_points.txt,sha256=jOf53-4IVGjEZeNYUOF1GGaiPNfVZcNKxP3vDFKEyMA,58
156
+ execsql2-2.37.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
157
+ execsql2-2.37.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
158
+ execsql2-2.37.0.dist-info/RECORD,,