execsql2 2.21.2__py3-none-any.whl → 2.22.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 (81) hide show
  1. execsql/api.py +21 -4
  2. execsql/cli/run.py +37 -17
  3. execsql/config.py +37 -22
  4. execsql/data/execsql.conf.template +6 -0
  5. execsql/db/access.py +6 -5
  6. execsql/db/base.py +23 -5
  7. execsql/db/dsn.py +2 -0
  8. execsql/db/duckdb.py +1 -1
  9. execsql/db/mysql.py +6 -8
  10. execsql/db/sqlite.py +1 -1
  11. execsql/exceptions.py +14 -11
  12. execsql/exporters/base.py +1 -1
  13. execsql/exporters/delimited.py +22 -16
  14. execsql/exporters/duckdb.py +6 -1
  15. execsql/exporters/feather.py +1 -1
  16. execsql/exporters/json.py +5 -3
  17. execsql/exporters/latex.py +2 -2
  18. execsql/exporters/markdown.py +1 -0
  19. execsql/exporters/ods.py +7 -3
  20. execsql/exporters/raw.py +8 -8
  21. execsql/exporters/sqlite.py +11 -1
  22. execsql/exporters/templates.py +2 -1
  23. execsql/exporters/xls.py +10 -10
  24. execsql/exporters/xml.py +1 -0
  25. execsql/exporters/yaml.py +1 -0
  26. execsql/exporters/zip.py +9 -7
  27. execsql/format.py +6 -6
  28. execsql/gui/console.py +3 -3
  29. execsql/gui/desktop.py +28 -10
  30. execsql/gui/tui.py +8 -6
  31. execsql/importers/base.py +14 -8
  32. execsql/importers/csv.py +8 -3
  33. execsql/importers/xls.py +1 -0
  34. execsql/metacommands/conditions.py +14 -14
  35. execsql/metacommands/connect.py +2 -2
  36. execsql/metacommands/control.py +1 -1
  37. execsql/metacommands/data.py +2 -1
  38. execsql/metacommands/io_export.py +4 -0
  39. execsql/metacommands/io_fileops.py +85 -39
  40. execsql/metacommands/io_import.py +1 -0
  41. execsql/metacommands/io_write.py +1 -1
  42. execsql/metacommands/prompt.py +24 -22
  43. execsql/metacommands/system.py +14 -1
  44. execsql/models.py +56 -17
  45. execsql/parser.py +8 -8
  46. execsql/plugins.py +1 -1
  47. execsql/script/engine.py +4 -4
  48. execsql/script/executor.py +67 -28
  49. execsql/script/parser.py +13 -12
  50. execsql/script/variables.py +4 -2
  51. execsql/state.pyi +122 -0
  52. execsql/types.py +31 -26
  53. execsql/utils/auth.py +6 -2
  54. execsql/utils/datetime.py +3 -3
  55. execsql/utils/errors.py +7 -5
  56. execsql/utils/fileio.py +77 -31
  57. execsql/utils/gui.py +15 -11
  58. execsql/utils/mail.py +9 -2
  59. execsql/utils/numeric.py +1 -1
  60. execsql/utils/regex.py +9 -9
  61. {execsql2-2.21.2.dist-info → execsql2-2.22.0.dist-info}/METADATA +3 -3
  62. execsql2-2.22.0.dist-info/RECORD +123 -0
  63. execsql2-2.21.2.dist-info/RECORD +0 -122
  64. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/README.md +0 -0
  65. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  66. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  67. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  68. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  69. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  70. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  71. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  72. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  73. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  74. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/script_template.sql +0 -0
  75. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  76. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  77. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  78. {execsql2-2.21.2.dist-info → execsql2-2.22.0.dist-info}/WHEEL +0 -0
  79. {execsql2-2.21.2.dist-info → execsql2-2.22.0.dist-info}/entry_points.txt +0 -0
  80. {execsql2-2.21.2.dist-info → execsql2-2.22.0.dist-info}/licenses/LICENSE.txt +0 -0
  81. {execsql2-2.21.2.dist-info → execsql2-2.22.0.dist-info}/licenses/NOTICE +0 -0
execsql/utils/fileio.py CHANGED
@@ -27,13 +27,15 @@ import io
27
27
  import multiprocessing
28
28
  import os
29
29
  import queue
30
+ import re
30
31
  from pathlib import Path
31
32
  import stat
32
33
  import sys
33
34
  import tempfile
34
35
  import time
36
+ from collections.abc import Callable
35
37
  from encodings.aliases import aliases as codec_dict
36
- from typing import Any
38
+ from typing import Any, cast
37
39
 
38
40
  from execsql.exceptions import ErrInfo
39
41
 
@@ -207,11 +209,11 @@ class FileWriter(multiprocessing.Process):
207
209
  self.open_timeout = open_timeout
208
210
  self.encoding = encoding
209
211
  self.openmode = "a"
210
- self.handle = None
212
+ self.handle: io.TextIOWrapper | None = None
211
213
  self.status = self.STATUS_UNOPENED
212
- self.open_start_time = None
214
+ self.open_start_time: float | None = None
213
215
  self.fail_message_written = False
214
- self.output_queue: collections.deque = collections.deque()
216
+ self.output_queue: collections.deque[str] = collections.deque()
215
217
  self.close_after_write = False
216
218
 
217
219
  def __del__(self) -> None:
@@ -221,6 +223,7 @@ class FileWriter(multiprocessing.Process):
221
223
  pass # Best-effort cleanup at interpreter shutdown.
222
224
 
223
225
  def write_queue(self) -> None:
226
+ assert self.handle is not None
224
227
  while len(self.output_queue) > 0:
225
228
  m = self.output_queue.pop()
226
229
  self.handle.write(m)
@@ -238,11 +241,14 @@ class FileWriter(multiprocessing.Process):
238
241
  self.open_start_time = time.time()
239
242
  if time.time() - self.open_start_time < self.open_timeout:
240
243
  try:
241
- self.handle = open( # noqa: SIM115
242
- file=self.filename,
243
- mode=self.openmode,
244
- encoding=self.encoding,
245
- errors="backslashreplace",
244
+ self.handle = cast(
245
+ io.TextIOWrapper,
246
+ open( # noqa: SIM115
247
+ file=self.filename,
248
+ mode=self.openmode,
249
+ encoding=self.encoding,
250
+ errors="backslashreplace",
251
+ ),
246
252
  )
247
253
  except Exception:
248
254
  self.status = self.STATUS_WAITING
@@ -298,10 +304,10 @@ class FileWriter(multiprocessing.Process):
298
304
  self.return_msg_queue = return_msg_queue
299
305
  self.file_encoding = file_encoding
300
306
  self.open_timeout = open_timeout
301
- self.files: dict = {}
307
+ self.files: dict[str, FileWriter.FileControl] = {}
302
308
  self.active = True
303
309
  # Functions in execvec must be in the same order as the CMD enums.
304
- self.execvec = (
310
+ self.execvec: tuple[Callable[..., None], ...] = (
305
311
  self.write,
306
312
  self.close_if_open,
307
313
  self.close_all,
@@ -424,7 +430,7 @@ def filewriter_filestatus(filename: str) -> int:
424
430
  if not _writer_alive():
425
431
  return FileWriter.FileControl.STATUS_CLOSED
426
432
  fw_input.put((FileWriter.CMD_GET_STATUS, (filename,)))
427
- return fw_output.get()
433
+ return cast(int, fw_output.get())
428
434
 
429
435
 
430
436
  def filewriter_write(filename: str, message: str) -> None:
@@ -527,19 +533,23 @@ class EncodedFile:
527
533
 
528
534
  if Path(filename).exists():
529
535
  self.encoding, self.bom_length = detect_by_bom(filename, file_encoding)
530
- self.fo = None
536
+ self.fo: io.TextIOWrapper | None = None
531
537
 
532
538
  def open(self, mode: str = "r") -> io.TextIOWrapper:
533
539
  import execsql.state as _state
534
540
 
535
541
  conf = _state.conf
536
- self.fo = open( # noqa: SIM115
537
- file=self.filename,
538
- mode=mode,
539
- encoding=self.encoding,
540
- errors=conf.enc_err_disposition,
541
- newline=None,
542
+ self.fo = cast(
543
+ io.TextIOWrapper,
544
+ open( # noqa: SIM115
545
+ file=self.filename,
546
+ mode=mode,
547
+ encoding=self.encoding,
548
+ errors=conf.enc_err_disposition,
549
+ newline=None,
550
+ ),
542
551
  )
552
+ assert self.fo is not None
543
553
  return self.fo
544
554
 
545
555
  def close(self) -> None:
@@ -557,6 +567,7 @@ class EncodedFile:
557
567
  # before entering the ``with`` block.
558
568
  if self.fo is None:
559
569
  self.open("r")
570
+ assert self.fo is not None
560
571
  return self.fo
561
572
 
562
573
  def __exit__(self, exc_type, exc_val, exc_tb) -> None:
@@ -565,7 +576,15 @@ class EncodedFile:
565
576
 
566
577
  class Logger:
567
578
  # A custom logger for execsql that writes several different types of messages to a log file.
568
- log_file = None
579
+ log_file: io.TextIOWrapper | None = None
580
+ _COMMON_SECRET_PATTERNS = (
581
+ re.compile(r"(?i)(://[^/\s:@]+:)([^@\s]+)(@)"),
582
+ re.compile(r"(?i)\b(password|passwd|token|secret|api[_-]?key|private[_-]?key|webhook)(\s*[=:]\s*)([^\s,;]+)"),
583
+ re.compile(r"\bsk-(?:live|test)-[A-Za-z0-9_-]+\b"),
584
+ re.compile(r"\bAKIA[A-Z0-9]{12,}\b"),
585
+ re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}\b"),
586
+ re.compile(r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b"),
587
+ )
569
588
 
570
589
  def __repr__(self) -> str:
571
590
  return f"Logger({self.script_file_name!r}, {self.db_name!r}, {self.server_name!r}, {self.cmdline_options!r}, {self.log_file_name!r})"
@@ -587,6 +606,7 @@ class Logger:
587
606
  self.db_name = db_name
588
607
  self.server_name = server_name
589
608
  self.cmdline_options = cmdline_options
609
+ self._redaction_values: list[str] = []
590
610
  if log_file_name:
591
611
  self.log_file_name = log_file_name
592
612
  else:
@@ -630,6 +650,9 @@ class Logger:
630
650
  self.run_start = _now
631
651
  self.run_id = _now.strftime("%Y%m%d_%H%M_%S_") + f"{_now.microsecond // 1000:03d}"
632
652
  self.user = getpass.getuser()
653
+ sz: int
654
+ dt: str
655
+ abs_script: str
633
656
  if script_file_name and Path(script_file_name).is_file():
634
657
  sz, dt = file_size_date(script_file_name)
635
658
  abs_script = str(Path(script_file_name).resolve())
@@ -653,9 +676,9 @@ class Logger:
653
676
  self.seq_no = 0
654
677
  atexit.register(self.close)
655
678
  self.exit_type = "unknown"
656
- self.exit_scriptfile = None
657
- self.exit_lno = None
658
- self.exit_description = None
679
+ self.exit_scriptfile: str | None = None
680
+ self.exit_lno: int | None = None
681
+ self.exit_description: str | None = None
659
682
  atexit.register(self.log_exit)
660
683
 
661
684
  def _ts(self) -> str:
@@ -663,6 +686,29 @@ class Logger:
663
686
 
664
687
  return _datetime.datetime.now().isoformat(timespec="seconds")
665
688
 
689
+ def add_redaction_value(self, value: str | None) -> None:
690
+ if value is None:
691
+ return
692
+ value = str(value)
693
+ if not value or value in self._redaction_values:
694
+ return
695
+ self._redaction_values.append(value)
696
+
697
+ def _redact(self, msg: str | None) -> str | None:
698
+ if msg is None:
699
+ return None
700
+ redacted = str(msg)
701
+ for value in sorted(self._redaction_values, key=len, reverse=True):
702
+ redacted = redacted.replace(value, "***")
703
+ for pattern in self._COMMON_SECRET_PATTERNS:
704
+ if pattern.pattern.startswith("(?i)(://"):
705
+ redacted = pattern.sub(r"\1***\3", redacted)
706
+ elif "password|passwd|token" in pattern.pattern:
707
+ redacted = pattern.sub(r"\1\2***", redacted)
708
+ else:
709
+ redacted = pattern.sub("***", redacted)
710
+ return redacted
711
+
666
712
  def _rotate_if_needed(self) -> None:
667
713
  try:
668
714
  import execsql.state as _state
@@ -701,32 +747,32 @@ class Logger:
701
747
  self.writelog(msg)
702
748
 
703
749
  def log_status_exception(self, msg: str | None) -> None:
704
- msg = None if not msg else msg.replace("\n", "")
750
+ msg = self._redact(None if not msg else msg.replace("\n", ""))
705
751
  self.seq_no += 1
706
752
  wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\texception\t{msg or ''}\n"
707
753
  self.writelog(wmsg)
708
754
 
709
755
  def log_status_error(self, msg: str | None) -> None:
710
- msg = None if not msg else msg.replace("\n", "")
756
+ msg = self._redact(None if not msg else msg.replace("\n", ""))
711
757
  self.seq_no += 1
712
758
  wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\terror\t{msg or ''}\n"
713
759
  self.writelog(wmsg)
714
760
 
715
761
  def log_status_info(self, msg: str | None) -> None:
716
- msg = None if not msg else msg.replace("\n", "")
762
+ msg = self._redact(None if not msg else msg.replace("\n", ""))
717
763
  self.seq_no += 1
718
764
  wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\tinfo\t{msg or ''}\n"
719
765
  self.writelog(wmsg)
720
766
 
721
767
  def log_status_warning(self, msg: str | None) -> None:
722
- msg = None if not msg else msg.replace("\n", "")
768
+ msg = self._redact(None if not msg else msg.replace("\n", ""))
723
769
  self.seq_no += 1
724
770
  wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\twarning\t{msg or ''}\n"
725
771
  self.writelog(wmsg)
726
772
 
727
773
  def log_sql_query(self, sql: str, db_name: str, line_no: int | None = None) -> None:
728
774
  """Log an executed SQL statement for audit purposes."""
729
- cleaned = sql.replace("\n", " ").replace("\t", " ")
775
+ cleaned = self._redact(sql.replace("\n", " ").replace("\t", " ")) or ""
730
776
  if len(cleaned) > 2000:
731
777
  cleaned = cleaned[:2000] + "..."
732
778
  self.seq_no += 1
@@ -735,7 +781,7 @@ class Logger:
735
781
  self.writelog(wmsg)
736
782
 
737
783
  def log_user_msg(self, msg: str | None) -> None:
738
- msg = None if not msg else msg.replace("\n", "")
784
+ msg = self._redact(None if not msg else msg.replace("\n", ""))
739
785
  if msg != "":
740
786
  self.seq_no += 1
741
787
  wmsg = f"user_msg\t{self.run_id}\t{self.seq_no}\t{self._ts()}\tinfo\t{msg}\n"
@@ -760,14 +806,14 @@ class Logger:
760
806
  self.exit_type = "exception"
761
807
  self.exit_scriptfile = None
762
808
  self.exit_lno = None
763
- self.exit_description = msg.replace("\n", "")
809
+ self.exit_description = self._redact(msg.replace("\n", ""))
764
810
 
765
811
  def log_exit_error(self, msg: str | None) -> None:
766
812
  # Save values to be used by exit() function triggered on program exit
767
813
  self.exit_type = "error"
768
814
  self.exit_scriptfile = None
769
815
  self.exit_lno = None
770
- self.exit_description = None if not msg else msg.replace("\n", "")
816
+ self.exit_description = self._redact(None if not msg else msg.replace("\n", ""))
771
817
 
772
818
  def log_exit(self) -> None:
773
819
  import datetime as _datetime
execsql/utils/gui.py CHANGED
@@ -19,7 +19,7 @@ their bodies to avoid circular imports.
19
19
  from __future__ import annotations
20
20
 
21
21
  import sys
22
- from typing import Any
22
+ from typing import Any, cast
23
23
 
24
24
  __all__ = [
25
25
  # Constants
@@ -221,7 +221,7 @@ def gui_console_isrunning() -> bool:
221
221
  clicking the Tkinter window's close button).
222
222
  """
223
223
  if _active_backend is not None:
224
- return _active_backend.query_console({}).get("console_running", False)
224
+ return cast(bool, _active_backend.query_console({}).get("console_running", False))
225
225
  return _console_running
226
226
 
227
227
 
@@ -245,6 +245,7 @@ def enable_gui() -> None:
245
245
  # Already initialised — nothing to do.
246
246
  if _active_backend is not None:
247
247
  return
248
+ backend: Any
248
249
 
249
250
  framework = _state.conf.gui_framework if _state.conf else "tkinter"
250
251
 
@@ -457,11 +458,12 @@ def _apply_connect_result(alias: str, result: dict) -> None:
457
458
  )
458
459
 
459
460
  db_type = result.get("db_type", "l")
460
- server = result.get("server")
461
- database = result.get("database")
462
- db_file = result.get("db_file")
463
- username = result.get("username")
461
+ server = str(result.get("server") or "")
462
+ database = str(result.get("database") or "")
463
+ db_file = str(result.get("db_file") or "")
464
+ username = str(result.get("username") or "")
464
465
 
466
+ db: Any
465
467
  if db_type == "p":
466
468
  db = db_Postgres(server, database, user=username, pw_needed=True)
467
469
  elif db_type == "s":
@@ -613,7 +615,7 @@ def pause(
613
615
  old_settings = termios.tcgetattr(fd)
614
616
 
615
617
  timer_handler: TimerHandler | None = None
616
- old_alarm = signal.SIG_DFL
618
+ old_alarm: Any = signal.SIG_DFL
617
619
 
618
620
  try:
619
621
  tty.setraw(fd)
@@ -667,6 +669,8 @@ def pause_win(
667
669
  """
668
670
  try:
669
671
  import msvcrt
672
+
673
+ msvcrt_mod: Any = msvcrt
670
674
  except ImportError:
671
675
  # Not on Windows — delegate to the POSIX implementation.
672
676
  return pause(text, action=action, countdown=countdown, timeunit=timeunit)
@@ -690,8 +694,8 @@ def pause_win(
690
694
  "{:8.1f} |{}{}|\r".format(remaining, "+" * bar_left, "-" * (bar_len - bar_left)),
691
695
  )
692
696
  sys.stdout.flush()
693
- if msvcrt.kbhit():
694
- ch = msvcrt.getwch()
697
+ if msvcrt_mod.kbhit():
698
+ ch = msvcrt_mod.getwch()
695
699
  if ch in ("\r", "\n"):
696
700
  _clear_progress_line()
697
701
  return 0
@@ -703,8 +707,8 @@ def pause_win(
703
707
  sys.stderr.write("Press Enter to continue, Esc to quit...\n")
704
708
  sys.stderr.flush()
705
709
  while True:
706
- if msvcrt.kbhit():
707
- ch = msvcrt.getwch()
710
+ if msvcrt_mod.kbhit():
711
+ ch = msvcrt_mod.getwch()
708
712
  if ch in ("\r", "\n"):
709
713
  return 0
710
714
  if ch == "\x1b":
execsql/utils/mail.py CHANGED
@@ -17,6 +17,7 @@ from email.mime.base import MIMEBase
17
17
  from email.mime.multipart import MIMEMultipart
18
18
  from email.mime.text import MIMEText
19
19
  from pathlib import Path
20
+ from typing import cast
20
21
 
21
22
  import execsql.state as _state
22
23
  from execsql.exceptions import ErrInfo
@@ -48,6 +49,7 @@ class Mailer:
48
49
  self.close()
49
50
 
50
51
  def __init__(self) -> None:
52
+ self.smtpconn: smtplib.SMTP
51
53
  conf = _state.conf
52
54
  if conf.smtp_host is None:
53
55
  raise ErrInfo(type="error", other_msg="Can't send email; the email host is not configured.")
@@ -138,14 +140,16 @@ class MailSpec:
138
140
  self.sent = False
139
141
 
140
142
  @staticmethod
141
- def _expand(text: str) -> str:
143
+ def _expand(text: str | None) -> str | None:
142
144
  """Expand local and global substitution variables in *text*."""
145
+ if text is None:
146
+ return None
143
147
  result = text
144
148
  localvars = _state.current_localvars()
145
149
  if localvars is not None:
146
150
  result, _ = localvars.substitute_all(result)
147
151
  result, _ = _state.subvars.substitute_all(result)
148
- return result
152
+ return cast(str, result)
149
153
 
150
154
  def send(self) -> None:
151
155
  if self.repeatable or not self.sent:
@@ -153,6 +157,9 @@ class MailSpec:
153
157
  send_from = self._expand(self.send_from)
154
158
  send_to = self._expand(self.send_to)
155
159
  subject = self._expand(self.subject)
160
+ assert send_from is not None
161
+ assert send_to is not None
162
+ assert subject is not None
156
163
  msg_content = self._expand(self.msg_content)
157
164
  content_filename = self._expand(self.content_filename)
158
165
  attach_filename = self._expand(self.attach_filename)
execsql/utils/numeric.py CHANGED
@@ -45,7 +45,7 @@ def leading_zero_num(dataval: Any) -> bool:
45
45
 
46
46
  def as_numeric(strval: Any) -> int | float | None:
47
47
  # Converts the given value to an int, a float, or None.
48
- if type(strval) in (int, float):
48
+ if isinstance(strval, int | float):
49
49
  return strval
50
50
  if not isinstance(strval, str):
51
51
  strval = str(strval)
execsql/utils/regex.py CHANGED
@@ -24,7 +24,7 @@ __all__ = [
24
24
  ]
25
25
 
26
26
 
27
- def ins_rxs(rx_list: tuple, fragment1: object, fragment2: object) -> tuple:
27
+ def ins_rxs(rx_list: tuple[str, ...], fragment1: object, fragment2: object) -> tuple[str, ...]:
28
28
  # Returns a tuple of all strings consisting of elements of the 'rx_list' tuple
29
29
  # inserted between 'fragment1' and 'fragment2'. The fragments may themselves
30
30
  # be tuples.
@@ -34,7 +34,7 @@ def ins_rxs(rx_list: tuple, fragment1: object, fragment2: object) -> tuple:
34
34
  fragment2 = ("",)
35
35
  if not isinstance(fragment2, tuple):
36
36
  fragment2 = (fragment2,)
37
- rv = []
37
+ rv: list[str] = []
38
38
  for te in rx_list:
39
39
  for f1 in fragment1:
40
40
  for f2 in fragment2:
@@ -42,12 +42,12 @@ def ins_rxs(rx_list: tuple, fragment1: object, fragment2: object) -> tuple:
42
42
  return tuple(rv)
43
43
 
44
44
 
45
- def ins_quoted_rx(fragment1: object, fragment2: object, rx: str) -> tuple:
45
+ def ins_quoted_rx(fragment1: object, fragment2: object, rx: str) -> tuple[str, ...]:
46
46
  return ins_rxs((rx, rf'"{rx}"'), fragment1, fragment2)
47
47
 
48
48
 
49
- def ins_schema_rxs(fragment1: object, fragment2: object, suffix: str | None = None) -> tuple:
50
- schema_exprs = (
49
+ def ins_schema_rxs(fragment1: object, fragment2: object, suffix: str | None = None) -> tuple[str, ...]:
50
+ schema_exprs: tuple[str, ...] = (
51
51
  r'"(?P<schema>[A-Za-z0-9_\- ]+)"',
52
52
  r"(?P<schema>[A-Za-z0-9_\-]+)",
53
53
  r"\[(?P<schema>[A-Za-z0-9_\- ]+)\]",
@@ -57,8 +57,8 @@ def ins_schema_rxs(fragment1: object, fragment2: object, suffix: str | None = No
57
57
  return ins_rxs(schema_exprs, fragment1, fragment2)
58
58
 
59
59
 
60
- def ins_table_rxs(fragment1: object, fragment2: object, suffix: str | None = None) -> tuple:
61
- tbl_exprs = (
60
+ def ins_table_rxs(fragment1: object, fragment2: object, suffix: str | None = None) -> tuple[str, ...]:
61
+ tbl_exprs: tuple[str, ...] = (
62
62
  r'(?:"(?P<schema>[A-Za-z0-9_\- ]+)"\.)?"(?P<table>[A-Za-z0-9_\-\# ]+)"',
63
63
  r"(?:(?P<schema>[A-Za-z0-9_\-]+)\.)?(?P<table>[A-Za-z0-9_\-\#]+)",
64
64
  r'(?:"(?P<schema>[A-Za-z0-9_\- ]+)"\.)?(?P<table>[A-Za-z0-9_\-\#]+)',
@@ -75,7 +75,7 @@ def ins_table_rxs(fragment1: object, fragment2: object, suffix: str | None = Non
75
75
  return ins_rxs(tbl_exprs, fragment1, fragment2)
76
76
 
77
77
 
78
- def ins_table_list_rxs(fragment1: object, fragment2: object) -> tuple:
78
+ def ins_table_list_rxs(fragment1: object, fragment2: object) -> tuple[str, ...]:
79
79
  tbl_exprs = (
80
80
  r'(?:(?P<tables>(?:"[A-Za-z0-9_\- ]+"\.)?"[A-Za-z0-9_\-\# ]+"(?:\s*,\s*(?:"[A-Za-z0-9_\- ]+"\.)?"[A-Za-z0-9_\-\# ]+")*))',
81
81
  r"(?:(?P<tables>(?:[A-Za-z0-9_\-]+\.)?[A-Za-z0-9_\-\#]+(?:\s*,\s*(?:[A-Za-z0-9_\-]+\.)?[A-Za-z0-9_\-\#]+)*))",
@@ -83,7 +83,7 @@ def ins_table_list_rxs(fragment1: object, fragment2: object) -> tuple:
83
83
  return ins_rxs(tbl_exprs, fragment1, fragment2)
84
84
 
85
85
 
86
- def ins_fn_rxs(fragment1: object, fragment2: object, symbolicname: str = "filename") -> tuple:
86
+ def ins_fn_rxs(fragment1: object, fragment2: object, symbolicname: str = "filename") -> tuple[str, ...]:
87
87
  if os.name == "posix":
88
88
  fns = (
89
89
  rf"(?P<{symbolicname}>[\w\.\-\\\/\'~`!@#$^&()+={{}}\[\]:;,]*[\w\.\-\\\/\'~`!@#$^&(+={{}}\[\]:;,])",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.21.2
3
+ Version: 2.22.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
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -173,7 +173,7 @@ Optional extras install database drivers and feature bundles:
173
173
 
174
174
  ```bash
175
175
  # Database drivers
176
- pip install execsql2[postgres] # PostgreSQL (psycopg2-binary)
176
+ pip install execsql2[postgres] # PostgreSQL (psycopg3 / psycopg[binary])
177
177
  pip install execsql2[mysql] # MySQL / MariaDB (pymysql)
178
178
  pip install execsql2[mssql] # SQL Server (pyodbc)
179
179
  pip install execsql2[duckdb] # DuckDB
@@ -403,7 +403,7 @@ execsql-format --no-sql --in-place scripts/
403
403
  ```yaml
404
404
  repos:
405
405
  - repo: https://github.com/geocoug/execsql
406
- rev: v2.21.2
406
+ rev: v2.22.0
407
407
  hooks:
408
408
  - id: execsql-format
409
409
  ```
@@ -0,0 +1,123 @@
1
+ execsql/__init__.py,sha256=ZeVnpEWu8M2bn3qLmVM72NhWRueUGNbsX8UAPTermo0,901
2
+ execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
3
+ execsql/api.py,sha256=v_M46MkVcuGPkIG6xp5VD4XNfwMg7HG45OKxdAwrAEs,21732
4
+ execsql/config.py,sha256=gxv7SDjiVrpR05rU_rSjCBd_PP3RmwsyhOyPWOqfAGQ,34123
5
+ execsql/exceptions.py,sha256=GlphAiR8iZu4UR4U50WgSqEUMd9_8AeQJ_hhPcJOeQU,9381
6
+ execsql/format.py,sha256=ON8Jy5BSen4UI4SGY5cHKRwsvR0ZoLhH0BdblMwydYE,33670
7
+ execsql/models.py,sha256=-DWTVrPNwM-n1h3e3rXMoBv9-wpWVdZahyy4RB7fRRE,15502
8
+ execsql/parser.py,sha256=aOxZ7Mzgvk7G4-nvy9Bgz7xlaZm6oo81cffDeBk_LCk,15597
9
+ execsql/plugins.py,sha256=l82VWa5NpvOQWD6vcO-WKP-vtn0wp9__wWDFAbwxZg0,12838
10
+ execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ execsql/state.py,sha256=T6UoXXxAkUP-4KKQpfFAlI3WMzm2xUi3LSplJDuRLY0,21965
12
+ execsql/state.pyi,sha256=ES5xdoId-ALCGnLIQvQIGBZljZRB6aOOEMRLKXjO9hA,3770
13
+ execsql/types.py,sha256=eKMF2kb5LHrEwKsiFpy6z_fEH05yC0Cpsg98Ea4SpVA,32231
14
+ execsql/cli/__init__.py,sha256=aJknKKIGxYCXpny0cyHXfqJsJ95dBtlEXXhPASFG8GQ,23114
15
+ execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
16
+ execsql/cli/help.py,sha256=ThwdZuMIfLPxLAPpGWwXFY_UfyWvYOCjdlBNK20Vzd8,5718
17
+ execsql/cli/lint.py,sha256=YqKzFNUhyb_Th69hYgKk1ZZVjCsZfJMIiUGqp06JwNs,17236
18
+ execsql/cli/run.py,sha256=Yb-Ri3cds0st1hUn4_w-PHMphTsArHhC4fx919SW9gU,39390
19
+ execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ execsql/data/execsql.conf.template,sha256=w_rJ1mx3FOWi_7qWLGcxsf3HTR5nSMsbKAzlfcJh_tM,10959
21
+ execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
22
+ execsql/db/access.py,sha256=h2zAMr3mQrm6vFlOYaN7iaG19K7gitXDuonllelqbcE,19539
23
+ execsql/db/base.py,sha256=0K6mSCqFcheg_T91VOwEnagqqP_eLL2dP5VN4yDz36Q,35414
24
+ execsql/db/dsn.py,sha256=P17FwrjEkmhYiPCWZSRJuHYIQu_BCkumiXLkwZEMb4o,6334
25
+ execsql/db/duckdb.py,sha256=YxtjWSzkbJEzZcfoYKfsA9QRrRL08A31GYzkolJiIw0,3229
26
+ execsql/db/factory.py,sha256=C_cyi3GjNx0opuwc6maFFeCSkjfOfoUjuf68YT4VAHE,5377
27
+ execsql/db/firebird.py,sha256=cV5wUzQHoz7yugHdNxAn8IW7fL3v1riIsGLykCZhZwA,9480
28
+ execsql/db/mysql.py,sha256=hv_OqoJBVIaCh5e2lMTVrgBTNljAaVnxwlAoQYff8c4,18026
29
+ execsql/db/oracle.py,sha256=1_odb5xmlm8vjdJdQXz7SHm9dzIbZ5sxP_IxTklH3kA,12018
30
+ execsql/db/postgres.py,sha256=LIZgx4T_UPZvOk_mn0l832ShNKgzculdOrVrFO7Rj8I,22045
31
+ execsql/db/sqlite.py,sha256=mx56IDwLKAXdep--4C8j15qK22JkJjworSJkfnx1ZFw,11551
32
+ execsql/db/sqlserver.py,sha256=j2ViLoBWzizgaL0u6V4iHfjinrlJ4rpBD3XZiKKdwSU,8968
33
+ execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
34
+ execsql/debug/repl.py,sha256=JObeoEXh15qyk4Q3WQCCqMcBtojlIcu_Xg-ZRDZJi5Q,25491
35
+ execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
36
+ execsql/exporters/base.py,sha256=_GoO0YqwlTxrjogxHI3CxxRR64sMgIEjRogt0KCqzoY,6402
37
+ execsql/exporters/delimited.py,sha256=vd9Q74TzeeWSPU03EboqqCBOWwOOeFVL544sPXINI0w,32755
38
+ execsql/exporters/duckdb.py,sha256=912TMdYaSVD-kb-_oebTBHQp8FvYMbWTDEDD80w5Z50,3455
39
+ execsql/exporters/feather.py,sha256=tah48aep0cQRikZcmADXUPTzFddgDUT78Ndw_l0Hsj4,4178
40
+ execsql/exporters/html.py,sha256=BPTGYODiC5_5zaQsVkZ9QVAl67yfCWFTsjK0D-QOJkM,9717
41
+ execsql/exporters/json.py,sha256=wWGT6zjvIdWlO5FmXbaEw1xOhy23f1YY6bm5uQD_NS0,4393
42
+ execsql/exporters/latex.py,sha256=TiHH9EO4eF2AgdUyjpbHNZhvKhIJl8Ew6NoO1z0YbB0,4484
43
+ execsql/exporters/markdown.py,sha256=MOErBwkMHM552JE0QuNkCRyHpxTEI3Pw7o9bZOe3RE8,4447
44
+ execsql/exporters/ods.py,sha256=oenUeZuC34Urlh7rI-PXIWOc-cuKMfUU4HfYj4xzvkQ,19521
45
+ execsql/exporters/parquet.py,sha256=mbCo2Wlit1lkXmyo5EyLd_ATIHSICS49bouiBT2kdAs,1121
46
+ execsql/exporters/pretty.py,sha256=tG7Yr4lwFMs3a0Jrsq6qsNNkE8Q7sMePr61SS5QMaTs,3447
47
+ execsql/exporters/protocol.py,sha256=BxATgz0xKHbB2FpZBeNg7wZfIiCohhD1awlr3JCec0c,4372
48
+ execsql/exporters/raw.py,sha256=9o3BMt7UYmnjbP8OEzmM2kNKfwwERi2yDcuNfUoEfuQ,2530
49
+ execsql/exporters/sqlite.py,sha256=QjP_W90S-hecNqp2A-Vy58DD8MZitME1sTq4lqIS43w,3401
50
+ execsql/exporters/templates.py,sha256=wXvN6HwOHbro6t306Dq7WcnxwGdJMF1veXry9hwuI3k,6016
51
+ execsql/exporters/values.py,sha256=HIyud31aux_dbCphfKHEGeZB9fkIPE5PoGXQz817XIE,2520
52
+ execsql/exporters/xls.py,sha256=iZ_N7hneyfaDKbO1XdYv6doJCFPqJWEJql5rduoNyRU,11197
53
+ execsql/exporters/xlsx.py,sha256=0f-Gdp8DyB3muUdH6tyiGFKGx9UymZ4fL3ja6H0sBjI,11853
54
+ execsql/exporters/xml.py,sha256=t1PSr3i8xvTCWzd9jaxxAy5NcRpl6sqjnZALafKCqpM,2575
55
+ execsql/exporters/yaml.py,sha256=R61Rs_0rd5G3bg344L4hC84GM79IfYXaDO2tkF-BluY,3121
56
+ execsql/exporters/zip.py,sha256=341z4Z4MPtEkXJEtfiMSiuJigERh5_f8RDbNMBbt8KM,5078
57
+ execsql/gui/__init__.py,sha256=oCb-cyhLZzVpWJ4WU5HbqEDBrV-lm0ytEwxemrOZyqs,2048
58
+ execsql/gui/base.py,sha256=15wCaSLvSb21hToCczru46ghmd56ryMpEOOZ0R_dE3U,12740
59
+ execsql/gui/console.py,sha256=hgFnxPP7lLolKgmKCBTSMXIt4uU2L4C32XcA82ytr2U,19438
60
+ execsql/gui/desktop.py,sha256=922OdzjV4OGNDIesOxKOmlsrNp1aJ8St8cVwED0H2sE,59521
61
+ execsql/gui/tui.py,sha256=yXKBqbbJDNC6a25sUKpvy3BR10U2LhLSuGAxkw6ADcQ,65358
62
+ execsql/importers/__init__.py,sha256=zZwdQxMaValCNqUrVdvaA7XPU3J8NmqVJ4uolNWY2iU,299
63
+ execsql/importers/base.py,sha256=3Iq_Sr4cCXKHpjjJYkhZX6uhsIIRAdmvAvTdC4mFlWo,4326
64
+ execsql/importers/csv.py,sha256=Kec1VCycRdfstNoxIXsEsSFqQprZp_edpNiMZm43eTI,4914
65
+ execsql/importers/feather.py,sha256=g2B69d2uv9vmnXcmjFyTVsMP40LYEzFYkhk3gD26mGw,1900
66
+ execsql/importers/json.py,sha256=fL47h09Zzx4Qw3TTQRQJbdPdD3qVMiU-kK9C7j1IQzg,5534
67
+ execsql/importers/ods.py,sha256=VsxIkr7opBamB0PbSFmMWt7G11w8lLUH1k_kRwz28zw,2847
68
+ execsql/importers/xls.py,sha256=D7Ul42gIRYZZC5QxD35ft_wmqQv2f1jNMSbnqlzV0ZA,3851
69
+ execsql/metacommands/__init__.py,sha256=H_D1Z5kN6zfYtxatejQhQhQV1tlVZ7rcdFZ_9uhXu2Q,13010
70
+ execsql/metacommands/conditions.py,sha256=2bfdVV_LCl91quG4SRLur9Jk60NsIOStHlBCeJJ-Djc,30068
71
+ execsql/metacommands/connect.py,sha256=SPk4h9yiUgTD3w1gNOT_oYx8VShggsO357by5Hoy6OI,15344
72
+ execsql/metacommands/control.py,sha256=EAURAsBjEZm3YwOJG0Gr3oI2H5_8p3Cu8-AguhBD_ds,8007
73
+ execsql/metacommands/data.py,sha256=Rp18lHUKSTkwTFRjOQ7bDgZd42LNLVoTS3Zc_bcNEWY,12199
74
+ execsql/metacommands/debug.py,sha256=MeVXAob8ItEg2QzuSUkKDaQCEABnH6u0XcAwJzw36CE,13015
75
+ execsql/metacommands/dispatch.py,sha256=t7x0xWHJA0PeCrYf7jYeSMJgf0yxcZ_xh-_YAtBPHLw,87192
76
+ execsql/metacommands/io.py,sha256=vlGBje5sgnqeilooMdhJDgSRIhysHy5_7LrKtik9Xjs,3011
77
+ execsql/metacommands/io_export.py,sha256=YamxQwURRjZRpCG2z8W3QurncCmEmAyLFqDkxtHo5dY,14434
78
+ execsql/metacommands/io_fileops.py,sha256=l1ZaTMzvsXa88zaQp-scWvUy4TMmN4WVVhr4V6-b-tE,11645
79
+ execsql/metacommands/io_import.py,sha256=bIFQMTEW9Rc5FB0VAR-04eve7FL0g0gBKtdoGR_jXrI,15719
80
+ execsql/metacommands/io_write.py,sha256=yXDkMbQ3haDARVATVUYZprakyJHSyk1NywdKkFLpQ14,12783
81
+ execsql/metacommands/prompt.py,sha256=7pPURC11qBFcKEI7IdhrttbiBKTc1txjH59QN3dvFyQ,37865
82
+ execsql/metacommands/script_ext.py,sha256=CsUbyq8QFPnnvfX_k7fPlYNvvRw1xLmAWER4Exo5pwc,3658
83
+ execsql/metacommands/system.py,sha256=6X2qmMpGUHDzyEluEotXt0Ks9jKQTpc2x0eoYNgN12M,7997
84
+ execsql/metacommands/upsert.py,sha256=wzoMpM8g49pEvU9GkHZ62fPvqV3w1UIUfxVA7HAsS_o,20317
85
+ execsql/script/__init__.py,sha256=eGJPBDWj42aaId2lX_quSrqoKrvGwGElIrGDNCyoV1Y,3547
86
+ execsql/script/ast.py,sha256=TQ4_7Lfw1F8_k6ycdvMZdzwNafrZiljSrthVRWUsuIk,20585
87
+ execsql/script/control.py,sha256=WqLy-HLPqHG3vEzYpKMiIJsD7LpORjyQuUWzFzcGz4w,2327
88
+ execsql/script/engine.py,sha256=KbSB5aXkuDlazh3rRP4vRym_bVTDKmChLQ_OFf3-N8Q,21016
89
+ execsql/script/executor.py,sha256=64dYw8QuNcHgvnJX1PwTK01UvDgOOYcI1IKceBL4PO4,36948
90
+ execsql/script/parser.py,sha256=VhFTJK8kODNRWgIDc3LjYjA3_SMPFATkpjVl82dYnrM,35179
91
+ execsql/script/variables.py,sha256=_hklqCeqPbdOT646Yg3ZcpmPke38_8bukMG3eZMTKZw,16384
92
+ execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
93
+ execsql/utils/auth.py,sha256=8Wixkx5nayEBG7RT4WTUQpCiElIKS3yOTV_6_-hHTRs,8889
94
+ execsql/utils/crypto.py,sha256=KlT6lPk-v0-qQQfAMfWvT9Q6kRdCD3_5JVaLajNocKM,3051
95
+ execsql/utils/datetime.py,sha256=8BrD24KWeMoouJZiFaXK4HF7plc-O_xrzIw12qOEyp8,3629
96
+ execsql/utils/errors.py,sha256=bDWooUmGdN8xqUt6csq466wPqKPLHHSi7U4hsmrFZxA,8577
97
+ execsql/utils/fileio.py,sha256=q1d1121GRilGSIpEF7Srgft7oF_latVUEghNdkAZbsw,33171
98
+ execsql/utils/gui.py,sha256=T22ThhkedzQz9kBidxZwIArt1umIvrgrSUwsPWAwvug,23548
99
+ execsql/utils/mail.py,sha256=SXpTBDx0WIPl2NSjkjmnoChHYVEMyZhIKU4QiJFFZNY,6011
100
+ execsql/utils/numeric.py,sha256=_7bi5R6pHjMCkOPne_Sh5nUj-B8e9g6WFmjptFHaunA,1573
101
+ execsql/utils/regex.py,sha256=B3mSBhRRx_DpUnX_t9IoYd05KvWAebiLMCZqQAmEwcw,3885
102
+ execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
103
+ execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
104
+ execsql2-2.22.0.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
105
+ execsql2-2.22.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
106
+ execsql2-2.22.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
107
+ execsql2-2.22.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
108
+ execsql2-2.22.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
109
+ execsql2-2.22.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
110
+ execsql2-2.22.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
111
+ execsql2-2.22.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
112
+ execsql2-2.22.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
113
+ execsql2-2.22.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
114
+ execsql2-2.22.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
115
+ execsql2-2.22.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
116
+ execsql2-2.22.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
117
+ execsql2-2.22.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
118
+ execsql2-2.22.0.dist-info/METADATA,sha256=aYtTGfrjV6YMqEipLcyiyWymmhwBnx1rF6DGcY87AB8,22623
119
+ execsql2-2.22.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
120
+ execsql2-2.22.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
121
+ execsql2-2.22.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
122
+ execsql2-2.22.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
123
+ execsql2-2.22.0.dist-info/RECORD,,