execsql2 2.22.8__py3-none-any.whl → 2.23.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 (48) hide show
  1. execsql/api.py +71 -0
  2. execsql/config.py +2 -0
  3. execsql/data/execsql.conf.template +6 -0
  4. execsql/db/access.py +12 -4
  5. execsql/db/base.py +15 -1
  6. execsql/db/dsn.py +12 -7
  7. execsql/db/duckdb.py +7 -8
  8. execsql/db/firebird.py +12 -7
  9. execsql/db/mysql.py +44 -10
  10. execsql/db/oracle.py +12 -7
  11. execsql/db/postgres.py +17 -9
  12. execsql/db/sqlite.py +7 -8
  13. execsql/db/sqlserver.py +7 -7
  14. execsql/db/tiers.py +79 -0
  15. execsql/exporters/delimited.py +15 -1
  16. execsql/exporters/html.py +8 -2
  17. execsql/exporters/latex.py +35 -2
  18. execsql/exporters/markdown.py +9 -3
  19. execsql/exporters/ods.py +22 -4
  20. execsql/exporters/pretty.py +13 -0
  21. execsql/exporters/templates.py +18 -1
  22. execsql/exporters/values.py +25 -4
  23. execsql/exporters/xlsx.py +7 -1
  24. execsql/state.py +5 -0
  25. execsql/state.pyi +3 -1
  26. execsql/types.py +4 -1
  27. execsql/utils/fileio.py +47 -2
  28. execsql/utils/mail.py +9 -5
  29. {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/METADATA +22 -13
  30. {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/RECORD +48 -47
  31. {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/WHEEL +1 -1
  32. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/README.md +0 -0
  33. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  34. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  35. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  36. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  37. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  38. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  39. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  40. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  41. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  42. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/script_template.sql +0 -0
  43. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  44. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  45. {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  46. {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/entry_points.txt +0 -0
  47. {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/licenses/LICENSE.txt +0 -0
  48. {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/licenses/NOTICE +0 -0
execsql/exporters/html.py CHANGED
@@ -51,7 +51,10 @@ def export_html(
51
51
  for r in rows:
52
52
  f.write("<tr>")
53
53
  for v in r:
54
- f.write(f"<td>{html_mod.escape(str(v)) if v else ''}</td>")
54
+ # `if v is not None`, not `if v`: 0, False, and an empty
55
+ # string are all falsy, and a zero measurement silently became
56
+ # an empty cell.
57
+ f.write(f"<td>{html_mod.escape(str(v)) if v is not None else ''}</td>")
55
58
  f.write("</tr>\n")
56
59
  f.write("</tbody>\n</table>\n")
57
60
 
@@ -191,7 +194,10 @@ def export_cgi_html(
191
194
  for r in rows:
192
195
  f.write("<tr>")
193
196
  for v in r:
194
- f.write(f"<td>{html_mod.escape(str(v)) if v else ''}</td>")
197
+ # `if v is not None`, not `if v`: 0, False, and an empty
198
+ # string are all falsy, and a zero measurement silently became
199
+ # an empty cell.
200
+ f.write(f"<td>{html_mod.escape(str(v)) if v is not None else ''}</td>")
195
201
  f.write("</tr>\n")
196
202
  f.write("</tbody>\n</table>\n")
197
203
 
@@ -20,6 +20,39 @@ import execsql.state as _state
20
20
  __all__ = ["export_latex", "write_query_to_latex"]
21
21
 
22
22
 
23
+ #: Every character LaTeX treats specially, with its text-mode replacement.
24
+ #: ``&`` matters most here: it is the column separator this exporter writes, so
25
+ #: an ampersand in a value silently added a column and the .tex file no longer
26
+ #: compiled. ``%`` is nearly as bad — it comments out the rest of the line.
27
+ #: str.translate applies these simultaneously, so the backslash replacement
28
+ #: cannot be re-escaped by a later rule.
29
+ _LATEX_ESCAPE = str.maketrans(
30
+ {
31
+ "\\": r"\textbackslash{}",
32
+ "&": r"\&",
33
+ "%": r"\%",
34
+ "$": r"\$",
35
+ "#": r"\#",
36
+ "_": r"\_",
37
+ "{": r"\{",
38
+ "}": r"\}",
39
+ "~": r"\textasciitilde{}",
40
+ "^": r"\textasciicircum{}",
41
+ },
42
+ )
43
+
44
+
45
+ def _latex_cell(value: Any) -> str:
46
+ """Render a value as LaTeX table-cell text.
47
+
48
+ ``None`` becomes an empty cell rather than the literal string "None",
49
+ matching every other exporter.
50
+ """
51
+ if value is None:
52
+ return ""
53
+ return str(value).translate(_LATEX_ESCAPE)
54
+
55
+
23
56
  def export_latex(
24
57
  outfile: str,
25
58
  hdrs: list[str],
@@ -39,10 +72,10 @@ def export_latex(
39
72
  f.write(f" \\caption{{{desc}}}\n")
40
73
  f.write(f" \\begin{{tabular}} {{{' l' * len(hdrs)} }}\n")
41
74
  f.write(" \\hline\n")
42
- f.write(" " + " & ".join([h.replace("_", r"\_") for h in hdrs]) + " \\\\\n")
75
+ f.write(" " + " & ".join([_latex_cell(h) for h in hdrs]) + " \\\\\n")
43
76
  f.write(" \\hline\n")
44
77
  for r in rows:
45
- f.write(" " + " & ".join([str(c).replace("_", r"\_") for c in r]) + " \\\\\n")
78
+ f.write(" " + " & ".join([_latex_cell(c) for c in r]) + " \\\\\n")
46
79
  f.write(" \\hline\n")
47
80
  f.write(" \\end{tabular}\n")
48
81
  f.write(" \\end{table}\n")
@@ -35,15 +35,21 @@ def _cell(value: Any) -> str:
35
35
 
36
36
  Args:
37
37
  value: The cell value from the result set. ``None`` is rendered as
38
- an empty string. Pipe characters are escaped so they do not
39
- break the table structure.
38
+ an empty string. Pipe characters are escaped, and newlines
39
+ become ``<br>``, so neither breaks the table structure.
40
40
 
41
41
  Returns:
42
42
  A string safe to embed between pipe characters in a GFM table row.
43
43
  """
44
44
  if value is None:
45
45
  return ""
46
- return str(value).translate(_PIPE_ESCAPE)
46
+ text = str(value).translate(_PIPE_ESCAPE)
47
+ if "\n" in text or "\r" in text:
48
+ # A GFM table row is one line: a literal newline ends the row mid-cell
49
+ # and leaves the rest of the values outside the table entirely. <br> is
50
+ # how GFM expresses a line break inside a cell.
51
+ text = text.replace("\r\n", "\n").replace("\r", "\n").replace("\n", "<br>")
52
+ return text
47
53
 
48
54
 
49
55
  def write_query_to_markdown(
execsql/exporters/ods.py CHANGED
@@ -10,6 +10,7 @@ Provides :func:`write_query_to_ods` (single-sheet export),
10
10
  """
11
11
 
12
12
  import datetime
13
+ import decimal
13
14
  import getpass
14
15
  import os
15
16
  from pathlib import Path
@@ -239,7 +240,12 @@ class OdsFile:
239
240
  if isinstance(item, bool):
240
241
  # Booleans must be evaluated before numbers.
241
242
  tc = of.table.TableCell(valuetype="boolean", value=1 if item else 0, stylename=style_name)
242
- elif isinstance(item, float | int):
243
+ elif isinstance(item, float | int | decimal.Decimal):
244
+ # Decimal belongs here: PostgreSQL, MySQL and DuckDB all return it
245
+ # for numeric/decimal columns. Without it the value fell to the
246
+ # untyped branch below and the cell was written with an
247
+ # office:value but no office:value-type, which is not valid ODF —
248
+ # a reader is free to treat the number as text or ignore it.
243
249
  tc = of.table.TableCell(valuetype="float", value=item, stylename=style_name)
244
250
  elif isinstance(item, datetime.datetime):
245
251
  self.define_iso_datetime_style()
@@ -266,12 +272,24 @@ class OdsFile:
266
272
  tc = of.table.TableCell(timevalue=timeval.strftime("PT%HH%MM%S.%fS"), stylename="iso_datetime")
267
273
  tc.addElement(of.text.P(text=timeval.strftime("%H:%M:%S.%f")))
268
274
  elif isinstance(item, str):
269
- item = item.replace("\n", " ").replace("\r", " ")
270
- tc = of.table.TableCell(valuetype="string", stringvalue=item, stylename=style_name)
275
+ # An XML parser normalises a newline inside an attribute to a
276
+ # space, so office:string-value cannot carry one. ODF's answer
277
+ # is one <text:p> per line, which is what LibreOffice writes;
278
+ # the attribute is set only for single-line values.
279
+ if "\n" in item or "\r" in item:
280
+ tc = of.table.TableCell(valuetype="string", stylename=style_name)
281
+ else:
282
+ tc = of.table.TableCell(valuetype="string", stringvalue=item, stylename=style_name)
271
283
  else:
272
284
  tc = of.table.TableCell(value=item, stylename=style_name)
273
285
  if item is not None:
274
- tc.addElement(of.text.P(text=item))
286
+ if isinstance(item, str) and ("\n" in item or "\r" in item):
287
+ # Previously these were flattened to spaces, silently changing
288
+ # the exported value.
289
+ for line in item.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
290
+ tc.addElement(of.text.P(text=line))
291
+ else:
292
+ tc.addElement(of.text.P(text=item))
275
293
  tr.addElement(tc)
276
294
 
277
295
  def add_sheet(self, of_table: Any) -> None:
@@ -36,6 +36,19 @@ def prettyprint_rowset(
36
36
  if s is None:
37
37
  return and_val
38
38
  if isinstance(s, str):
39
+ # A newline or tab inside a value destroys a fixed-width table: the
40
+ # row is split across physical lines and every column after it is
41
+ # misaligned. This is a display format, so they are shown as their
42
+ # two-character escapes — the row stays one line, the columns line
43
+ # up, and the reader can still see that a break is there.
44
+ if "\n" in s or "\r" in s or "\t" in s:
45
+ return (
46
+ s.replace("\\", "\\\\")
47
+ .replace("\r\n", "\\n")
48
+ .replace("\r", "\\r")
49
+ .replace("\n", "\\n")
50
+ .replace("\t", "\\t")
51
+ )
39
52
  return s
40
53
  if type(s) in (type(memoryview(b"")), bytes, bytearray):
41
54
  return f"Binary data ({len(s)} bytes)"
@@ -105,7 +105,24 @@ class JinjaTemplateReport:
105
105
  inf = EncodedFile(template_file, conf.script_encoding)
106
106
  fh = inf.open("r")
107
107
  try:
108
- self.template = SandboxedEnvironment().from_string(fh.read())
108
+ # Autoescape by template extension. The sandbox guards against a
109
+ # hostile *template*; nothing guarded against hostile *data*, so a
110
+ # value from the database rendered into an .html report as live
111
+ # markup — a `<script>` in a text column became a script tag.
112
+ # Keying on the extension leaves every non-markup template (csv,
113
+ # tex, txt, sql) byte-identical; only html/htm/xml change, and a
114
+ # template that genuinely wants raw markup asks for it with the
115
+ # standard `{{ value|safe }}`.
116
+ autoescape = self._jinja2.select_autoescape(
117
+ enabled_extensions=("html", "htm", "xml"),
118
+ default_for_string=False,
119
+ default=False,
120
+ )
121
+ env = SandboxedEnvironment(autoescape=autoescape)
122
+ # from_string() has no filename to inspect, so resolve the policy
123
+ # against the template's own path and pass the decision through.
124
+ env.autoescape = autoescape(template_file)
125
+ self.template = env.from_string(fh.read())
109
126
  finally:
110
127
  fh.close()
111
128
 
@@ -8,6 +8,7 @@ set as a series of SQL ``INSERT INTO … VALUES (…)`` statements, suitable
8
8
  for loading data into a database from a plain SQL file.
9
9
  """
10
10
 
11
+ import decimal
11
12
  from typing import Any
12
13
 
13
14
  import execsql.state as _state
@@ -19,6 +20,29 @@ from execsql.utils.fileio import filewriter_close
19
20
  __all__ = ["export_values", "write_query_to_values"]
20
21
 
21
22
 
23
+ def _sql_literal(value: Any) -> str:
24
+ """Render a value as a SQL literal for an INSERT ... VALUES list.
25
+
26
+ Only numbers are emitted bare. Everything else is quoted, with embedded
27
+ single quotes doubled — the SQL standard escape.
28
+
29
+ Quoting is the safe default rather than the exception. Previously anything
30
+ that was not a string went out through ``str()`` unquoted, so a date became
31
+ ``2026-09-24``, which SQL reads as arithmetic: PostgreSQL inserted 1993 and
32
+ reported no error at all. A timestamp, carrying a space, was a syntax
33
+ error instead. Any type this function has not been taught about is safer
34
+ quoted than bare.
35
+ """
36
+ if value is None:
37
+ return "NULL"
38
+ if isinstance(value, bool):
39
+ # Checked before int — bool is a subclass of it.
40
+ return "TRUE" if value else "FALSE"
41
+ if isinstance(value, int | float | decimal.Decimal):
42
+ return str(value)
43
+ return "'" + str(value).replace("'", "''") + "'"
44
+
45
+
22
46
  def export_values(
23
47
  outfile: str,
24
48
  hdrs: list[str],
@@ -54,10 +78,7 @@ def export_values(
54
78
  firstrow = False
55
79
  else:
56
80
  f.write(",\n")
57
- quoted_row = [
58
- f"'{v.replace(chr(39), chr(39) * 2)}'" if isinstance(v, str) else str(v) if v is not None else "NULL"
59
- for v in r
60
- ]
81
+ quoted_row = [_sql_literal(v) for v in r]
61
82
  f.write(f" ({', '.join(quoted_row)})")
62
83
  f.write("\n ;\n")
63
84
  finally:
execsql/exporters/xlsx.py CHANGED
@@ -9,6 +9,7 @@ Provides :func:`write_query_to_xlsx` (single-sheet export) and
9
9
  """
10
10
 
11
11
  import datetime
12
+ import decimal
12
13
  import getpass
13
14
  import os
14
15
  from pathlib import Path
@@ -52,7 +53,12 @@ def _cell_value(item: Any) -> Any:
52
53
  if isinstance(item, bool):
53
54
  # bool must be checked before int — bool is a subclass of int.
54
55
  return item
55
- if isinstance(item, int | float):
56
+ if isinstance(item, int | float | decimal.Decimal):
57
+ # Decimal is in openpyxl's NUMERIC_TYPES and becomes a real numeric
58
+ # cell. Without it, every ``numeric``/``decimal`` column — which is
59
+ # what PostgreSQL, MySQL and DuckDB return for measurements — fell
60
+ # through to str() below and arrived in Excel as text: no sums, no
61
+ # sorting, no charts.
56
62
  return item
57
63
  if isinstance(item, datetime.datetime):
58
64
  return item
execsql/state.py CHANGED
@@ -222,6 +222,7 @@ _CONTEXT_ATTRS: frozenset[str] = frozenset(
222
222
  "status",
223
223
  "output",
224
224
  "filewriter",
225
+ "tier_notices_shown",
225
226
  # Lazy singletons
226
227
  "counters",
227
228
  "timer",
@@ -275,6 +276,7 @@ class RuntimeContext:
275
276
  "status",
276
277
  "output",
277
278
  "filewriter",
279
+ "tier_notices_shown",
278
280
  # Lazy singletons
279
281
  "counters",
280
282
  "timer",
@@ -321,6 +323,9 @@ class RuntimeContext:
321
323
  self.status: StatObj | None = None
322
324
  self.output: WriteHooks | None = None
323
325
  self.filewriter: FileWriter | None = None
326
+ # DBMS names whose best-effort support notice has already been shown,
327
+ # so repeated connections to the same backend announce it only once.
328
+ self.tier_notices_shown: set[str] = set()
324
329
 
325
330
  # Lazy singletons
326
331
  self.counters: CounterVars | None = None
execsql/state.pyi CHANGED
@@ -51,7 +51,8 @@ exec_log: Logger
51
51
  subvars: SubVarSet
52
52
  status: StatObj
53
53
  output: Any
54
- filewriter: FileWriter
54
+ tier_notices_shown: set[str]
55
+ filewriter: FileWriter | None
55
56
  counters: CounterVars
56
57
  timer: Timer
57
58
  dbs: DatabasePool
@@ -84,6 +85,7 @@ class RuntimeContext:
84
85
  subvars: SubVarSet | None
85
86
  status: StatObj | None
86
87
  output: Any
88
+ tier_notices_shown: set[str]
87
89
  filewriter: FileWriter | None
88
90
  counters: CounterVars | None
89
91
  timer: Timer | None
execsql/types.py CHANGED
@@ -773,7 +773,10 @@ dbt_duckdb.name_datatype(DT_Date, "DATE")
773
773
  dbt_duckdb.name_datatype(DT_Time, "TIME")
774
774
  dbt_duckdb.name_datatype(DT_Integer, "INTEGER")
775
775
  dbt_duckdb.name_datatype(DT_Long, "BIGINT")
776
- dbt_duckdb.name_datatype(DT_Float, "REAL")
776
+ # DOUBLE, not REAL: DT_Float is IEEE double precision and a Python float
777
+ # always is one. DuckDB's REAL is single precision, so 1234.567 came back
778
+ # as 1234.5670166015625. Every other backend maps DT_Float to a double.
779
+ dbt_duckdb.name_datatype(DT_Float, "DOUBLE")
777
780
  dbt_duckdb.name_datatype(DT_Decimal, "NUMERIC")
778
781
  dbt_duckdb.name_datatype(DT_Boolean, "BOOLEAN")
779
782
  dbt_duckdb.name_datatype(DT_Character, "TEXT")
execsql/utils/fileio.py CHANGED
@@ -435,8 +435,40 @@ def filewriter_filestatus(filename: str) -> int:
435
435
  return cast(int, fw_output.get())
436
436
 
437
437
 
438
+ #: Set once the first dropped write has been reported, so a script writing
439
+ #: thousands of lines warns once rather than per line.
440
+ _drop_warned = False
441
+
442
+
443
+ def _warn_write_dropped(filename: str) -> None:
444
+ """Report, once per process, that file output is being discarded.
445
+
446
+ Silently dropping a write is not a safe default: a script's logfile simply
447
+ comes back empty and the run still reports success. The guard itself has
448
+ to stay — putting on a queue no subprocess is draining fills the OS pipe
449
+ buffer and deadlocks the caller — so the write is still dropped, but no
450
+ longer without saying so.
451
+ """
452
+ global _drop_warned
453
+ if _drop_warned:
454
+ return
455
+ _drop_warned = True
456
+ try:
457
+ from execsql.utils.errors import write_warning
458
+
459
+ write_warning(
460
+ f'no file writer is running; output to "{filename}" and any later file '
461
+ "is discarded. Start one with execsql.utils.fileio.FileWriter, or run "
462
+ "the script through the CLI or execsql.api.run().",
463
+ always=True,
464
+ )
465
+ except Exception:
466
+ pass # Never let the warning path break the caller.
467
+
468
+
438
469
  def filewriter_write(filename: str, message: str) -> None:
439
470
  if not _writer_alive():
471
+ _warn_write_dropped(filename)
440
472
  return
441
473
  fw_input.put((FileWriter.CMD_WRITE, (filename, message)))
442
474
 
@@ -445,6 +477,7 @@ def filewriter_open_as_new(filename: str) -> None:
445
477
  # FileWriter opens files in append mode ("a") by default. This ensures that it
446
478
  # will be opened in write mode ("w") instead. If the file is open, it will be closed.
447
479
  if not _writer_alive():
480
+ _warn_write_dropped(filename)
448
481
  return
449
482
  fw_input.put((FileWriter.CMD_OPEN_AS_NEW, (filename,)))
450
483
 
@@ -537,7 +570,19 @@ class EncodedFile:
537
570
  self.encoding, self.bom_length = detect_by_bom(filename, file_encoding)
538
571
  self.fo: io.TextIOWrapper | None = None
539
572
 
540
- def open(self, mode: str = "r") -> io.TextIOWrapper:
573
+ def open(self, mode: str = "r", newline: str | None = None) -> io.TextIOWrapper:
574
+ """Open the file in text mode.
575
+
576
+ Args:
577
+ mode: Any mode accepted by :func:`open`.
578
+ newline: Passed straight to :func:`open`. The default ``None``
579
+ enables universal-newline translation, which rewrites every
580
+ ``\n`` written as ``os.linesep`` — ``\r\n`` on Windows. That
581
+ is right for line terminators and wrong for data: a newline
582
+ *inside* a quoted field is rewritten too, so the value read
583
+ back is not the value exported. Writers whose output can
584
+ carry an embedded newline pass ``""`` to disable it.
585
+ """
541
586
  import execsql.state as _state
542
587
 
543
588
  conf = _state.conf
@@ -548,7 +593,7 @@ class EncodedFile:
548
593
  mode=mode,
549
594
  encoding=self.encoding,
550
595
  errors=conf.enc_err_disposition,
551
- newline=None,
596
+ newline=newline,
552
597
  ),
553
598
  )
554
599
  assert self.fo is not None
execsql/utils/mail.py CHANGED
@@ -66,15 +66,19 @@ class Mailer:
66
66
  self.smtpconn = smtplib.SMTP_SSL(conf.smtp_host, conf.smtp_port, timeout=smtp_timeout)
67
67
  else:
68
68
  self.smtpconn = smtplib.SMTP(conf.smtp_host, conf.smtp_port, timeout=smtp_timeout)
69
- self.smtpconn.ehlo_or_hello_if_needed()
69
+ self.smtpconn.ehlo_or_helo_if_needed()
70
70
  if conf.smtp_tls:
71
71
  self.smtpconn.starttls()
72
72
  self.smtpconn.ehlo(conf.smtp_host)
73
73
  if conf.smtp_username:
74
- if conf.smtp_password:
75
- self.smtpconn.login(conf.smtp_username, conf.smtp_password)
76
- else:
77
- self.smtpconn.login(conf.smtp_username)
74
+ if not conf.smtp_password:
75
+ # smtplib.login() requires a password; calling it with only a
76
+ # username raises TypeError deep in the stdlib.
77
+ raise ErrInfo(
78
+ type="error",
79
+ other_msg="Can't send email; an SMTP username is configured but no password.",
80
+ )
81
+ self.smtpconn.login(conf.smtp_username, conf.smtp_password)
78
82
 
79
83
  def sendmail(
80
84
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.22.8
3
+ Version: 2.23.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
@@ -224,17 +224,26 @@ execsql script.sql # read connection from config file
224
224
 
225
225
  ### Supported Databases
226
226
 
227
- | Flag | Database |
228
- | ---- | --------------- |
229
- | `p` | PostgreSQL |
230
- | `m` | MySQL / MariaDB |
231
- | `s` | MS SQL Server |
232
- | `l` | SQLite |
233
- | `k` | DuckDB |
234
- | `a` | MS Access |
235
- | `f` | Firebird |
236
- | `o` | Oracle |
237
- | `d` | ODBC DSN |
227
+ | Flag | Database | Support |
228
+ | ---- | --------------- | ------------------------------------------------ |
229
+ | `p` | PostgreSQL | Supported — verified against a live server in CI |
230
+ | `m` | MySQL / MariaDB | Supported — verified against a live server in CI |
231
+ | `s` | MS SQL Server | Supported — verified against a live server in CI |
232
+ | `l` | SQLite | Supported — verified against real files in CI |
233
+ | `k` | DuckDB | Supported — verified against real files in CI |
234
+ | `a` | MS Access | Best effort — not verified in CI |
235
+ | `f` | Firebird | Best effort — not verified in CI |
236
+ | `o` | Oracle | Best effort — not verified in CI |
237
+ | `d` | ODBC DSN | Best effort — not verified in CI |
238
+
239
+ **Supported** adapters run against a live server or a real database file on
240
+ every CI run; a regression blocks the release and bugs get fixed. **Best
241
+ effort** adapters are carried forward from the upstream monolith and are not
242
+ exercised anywhere in CI — the code is there and may work, but nothing proves
243
+ it still does. Issues and pull requests are welcome for them; no guarantee is
244
+ made. Opening a best-effort connection prints one informational line, which
245
+ `support_tier_notice=No` in the `[interface]` section of `execsql.conf`
246
+ silences.
238
247
 
239
248
  ### Common options
240
249
 
@@ -384,7 +393,7 @@ execsql-format --no-sql --in-place scripts/
384
393
  ```yaml
385
394
  repos:
386
395
  - repo: https://github.com/geocoug/execsql
387
- rev: v2.22.8
396
+ rev: v2.23.0
388
397
  hooks:
389
398
  - id: execsql-format
390
399
  ```
@@ -1,56 +1,57 @@
1
1
  execsql/__init__.py,sha256=ZeVnpEWu8M2bn3qLmVM72NhWRueUGNbsX8UAPTermo0,901
2
2
  execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
3
- execsql/api.py,sha256=EOtBwMnm-8fmB_KmvxyjglR6C7CjdYhCPBisJ9CFfJg,22227
4
- execsql/config.py,sha256=txKziUJLV7UPonj2sGFgkmlinDdSV50aHmR_cuCbEIo,34369
3
+ execsql/api.py,sha256=gBNEINlQSncfhO1tvc-qD1MBXTuhDVIP16LhiqfnzQI,25334
4
+ execsql/config.py,sha256=riUh3DVOoScpqF-GK2EAcycu_1Y9PhWTghDuJSSUJjs,34507
5
5
  execsql/exceptions.py,sha256=GlphAiR8iZu4UR4U50WgSqEUMd9_8AeQJ_hhPcJOeQU,9381
6
6
  execsql/format.py,sha256=OOxsOX6g7otVgL3gI6uGxa5WIh8GTvjJ8WO-_P91zXg,52886
7
7
  execsql/models.py,sha256=-DWTVrPNwM-n1h3e3rXMoBv9-wpWVdZahyy4RB7fRRE,15502
8
8
  execsql/parser.py,sha256=aOxZ7Mzgvk7G4-nvy9Bgz7xlaZm6oo81cffDeBk_LCk,15597
9
9
  execsql/plugins.py,sha256=l82VWa5NpvOQWD6vcO-WKP-vtn0wp9__wWDFAbwxZg0,12838
10
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
11
+ execsql/state.py,sha256=wu7T1fkXROiQ2LoBYLONYj-1BkDUdoYzV425aCeAwm4,22230
12
+ execsql/state.pyi,sha256=YByEn5N7r0M4vDceOoDGTgb2tRJ_gNkYWGjlLiqQorI,3839
13
+ execsql/types.py,sha256=8rAoRaWwgBnR-ZhNEfahTTbZ_D4fB4vBuGeMEUnu7vE,32454
14
14
  execsql/cli/__init__.py,sha256=WPwEIELz-9XFG3Dpf-037eeYBdjtZStxWkT1bc3tMek,23112
15
15
  execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
16
16
  execsql/cli/help.py,sha256=ThwdZuMIfLPxLAPpGWwXFY_UfyWvYOCjdlBNK20Vzd8,5718
17
17
  execsql/cli/lint.py,sha256=YqKzFNUhyb_Th69hYgKk1ZZVjCsZfJMIiUGqp06JwNs,17236
18
18
  execsql/cli/run.py,sha256=Yb-Ri3cds0st1hUn4_w-PHMphTsArHhC4fx919SW9gU,39390
19
19
  execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- execsql/data/execsql.conf.template,sha256=w_rJ1mx3FOWi_7qWLGcxsf3HTR5nSMsbKAzlfcJh_tM,10959
20
+ execsql/data/execsql.conf.template,sha256=qqH_G3SiKBepljY3Go4rDDlhe3pIM6gOgsjAnLn0arg,11244
21
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
22
+ execsql/db/access.py,sha256=tcnWNqcCzpBtYKHIdfYj7xaJiaiDIzq0JUSIGf4KrrI,19823
23
+ execsql/db/base.py,sha256=oqRQXvf7kwCdgQMJQuJ_rKOI8UAnakaNCr_ivdC88BA,36102
24
+ execsql/db/dsn.py,sha256=nP7cESX7pJdZSPzyo2nYj6972GpPVVc8ESwyYT0Z_yQ,6523
25
+ execsql/db/duckdb.py,sha256=QQkGH8FVWEF9u5cM-Mtit-lBtmnWm4zo4BMMSiiEXdM,3176
26
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
27
+ execsql/db/firebird.py,sha256=fbzlGB6DeR-S9RFLUD4tJYPN-_76tyRLBkQl5LRJTDo,9640
28
+ execsql/db/mysql.py,sha256=Mb35hJOAviuf_QGJgIL17ftF-Xr2eb78z0cFVMRn_5E,19835
29
+ execsql/db/oracle.py,sha256=gBcSJaS92VV_LrxAPY8wFCqa4ouD8TiEhUB4dRK3la0,12174
30
+ execsql/db/postgres.py,sha256=HfrWuJ5-K5le1sY9iGv_E51JHciPIdA0UvyLsTKlnO4,22568
31
+ execsql/db/sqlite.py,sha256=4NRCO75DOYmkGrFlC6k_i8Hxd4jadLDbuRBYOI6CD8E,11498
32
+ execsql/db/sqlserver.py,sha256=O4E9OD3V3Xowsjl3KMdYHNCBeETlkJS9HUzNKEKuA9A,8943
33
+ execsql/db/tiers.py,sha256=PH2sUYwlYqcANGVKXprWQKVIlM6fpWU9q74djn8oFwc,2924
33
34
  execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
34
35
  execsql/debug/repl.py,sha256=JObeoEXh15qyk4Q3WQCCqMcBtojlIcu_Xg-ZRDZJi5Q,25491
35
36
  execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
36
37
  execsql/exporters/base.py,sha256=_GoO0YqwlTxrjogxHI3CxxRR64sMgIEjRogt0KCqzoY,6402
37
- execsql/exporters/delimited.py,sha256=vd9Q74TzeeWSPU03EboqqCBOWwOOeFVL544sPXINI0w,32755
38
+ execsql/exporters/delimited.py,sha256=v47Js8ZrQfTqx0aTDAD-9nkOx4ilAPnk_dZT3vAoCTw,33632
38
39
  execsql/exporters/duckdb.py,sha256=912TMdYaSVD-kb-_oebTBHQp8FvYMbWTDEDD80w5Z50,3455
39
40
  execsql/exporters/feather.py,sha256=tah48aep0cQRikZcmADXUPTzFddgDUT78Ndw_l0Hsj4,4178
40
- execsql/exporters/html.py,sha256=BPTGYODiC5_5zaQsVkZ9QVAl67yfCWFTsjK0D-QOJkM,9717
41
+ execsql/exporters/html.py,sha256=hwf9UHs7EZ7xiP3p9ilBIsAukXcDu-KO2dcQHVh2z30,10111
41
42
  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
43
+ execsql/exporters/latex.py,sha256=bdUN3GwfENUZHgpw6bvjsjfivDgzyf-HRkYTP-eH-pM,5480
44
+ execsql/exporters/markdown.py,sha256=FuMGFxm9uZUOrdEuXT2ZZLCqmVDly7SMrFlrjP6kVEI,4827
45
+ execsql/exporters/ods.py,sha256=Ke9pfcyT6R85LWIWcMI6pzRzkKir5w_gmiptCeeuoBc,20730
45
46
  execsql/exporters/parquet.py,sha256=mbCo2Wlit1lkXmyo5EyLd_ATIHSICS49bouiBT2kdAs,1121
46
- execsql/exporters/pretty.py,sha256=tG7Yr4lwFMs3a0Jrsq6qsNNkE8Q7sMePr61SS5QMaTs,3447
47
+ execsql/exporters/pretty.py,sha256=SiJHBXIjoFsOHegXcdrtxYa6xSIPga1U0DQEy7pK4QY,4145
47
48
  execsql/exporters/protocol.py,sha256=BxATgz0xKHbB2FpZBeNg7wZfIiCohhD1awlr3JCec0c,4372
48
49
  execsql/exporters/raw.py,sha256=9o3BMt7UYmnjbP8OEzmM2kNKfwwERi2yDcuNfUoEfuQ,2530
49
50
  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
51
+ execsql/exporters/templates.py,sha256=nRFurwTb5nxoyHpx609FGIp3MDbX_mYQ3IvgswaEbyA,7054
52
+ execsql/exporters/values.py,sha256=UPxN5IprasBZLqM5jfUXQihjdTeBFTxbJslOEtmGOcY,3384
52
53
  execsql/exporters/xls.py,sha256=iZ_N7hneyfaDKbO1XdYv6doJCFPqJWEJql5rduoNyRU,11197
53
- execsql/exporters/xlsx.py,sha256=0f-Gdp8DyB3muUdH6tyiGFKGx9UymZ4fL3ja6H0sBjI,11853
54
+ execsql/exporters/xlsx.py,sha256=5pTN8OsygLjk3MADJf8O7t3bdzSul7rrVO7SVSR581c,12223
54
55
  execsql/exporters/xml.py,sha256=t1PSr3i8xvTCWzd9jaxxAy5NcRpl6sqjnZALafKCqpM,2575
55
56
  execsql/exporters/yaml.py,sha256=R61Rs_0rd5G3bg344L4hC84GM79IfYXaDO2tkF-BluY,3121
56
57
  execsql/exporters/zip.py,sha256=341z4Z4MPtEkXJEtfiMSiuJigERh5_f8RDbNMBbt8KM,5078
@@ -94,30 +95,30 @@ execsql/utils/auth.py,sha256=8Wixkx5nayEBG7RT4WTUQpCiElIKS3yOTV_6_-hHTRs,8889
94
95
  execsql/utils/crypto.py,sha256=KlT6lPk-v0-qQQfAMfWvT9Q6kRdCD3_5JVaLajNocKM,3051
95
96
  execsql/utils/datetime.py,sha256=8BrD24KWeMoouJZiFaXK4HF7plc-O_xrzIw12qOEyp8,3629
96
97
  execsql/utils/errors.py,sha256=bDWooUmGdN8xqUt6csq466wPqKPLHHSi7U4hsmrFZxA,8577
97
- execsql/utils/fileio.py,sha256=_15UIdcRZxZn2R6YP4xQn-6C9CVK2yxj1oEfxULo2_c,33329
98
+ execsql/utils/fileio.py,sha256=ONLoXkq5PV4dvNc16UMgi9-EY2Wy4mNUFxneSrX91N0,35231
98
99
  execsql/utils/gui.py,sha256=X5aw6LVoeaAdHaQkymm4ba176jga0cS2GrqxSFWTWTg,23853
99
- execsql/utils/mail.py,sha256=SXpTBDx0WIPl2NSjkjmnoChHYVEMyZhIKU4QiJFFZNY,6011
100
+ execsql/utils/mail.py,sha256=SXZac24p62qxDbIviO8VdDZTbKljIoJWgWZfxIGu5zI,6260
100
101
  execsql/utils/numeric.py,sha256=_7bi5R6pHjMCkOPne_Sh5nUj-B8e9g6WFmjptFHaunA,1573
101
102
  execsql/utils/regex.py,sha256=B3mSBhRRx_DpUnX_t9IoYd05KvWAebiLMCZqQAmEwcw,3885
102
103
  execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
103
104
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
104
- execsql2-2.22.8.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
105
- execsql2-2.22.8.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
106
- execsql2-2.22.8.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
107
- execsql2-2.22.8.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
108
- execsql2-2.22.8.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
109
- execsql2-2.22.8.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
110
- execsql2-2.22.8.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
111
- execsql2-2.22.8.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
112
- execsql2-2.22.8.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
113
- execsql2-2.22.8.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
114
- execsql2-2.22.8.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
115
- execsql2-2.22.8.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
116
- execsql2-2.22.8.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
117
- execsql2-2.22.8.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
118
- execsql2-2.22.8.dist-info/METADATA,sha256=YR0z4uOGh4ontDBb5qfx18stwbhiRyHkyZDAUwE_ST4,20382
119
- execsql2-2.22.8.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
120
- execsql2-2.22.8.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
121
- execsql2-2.22.8.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
122
- execsql2-2.22.8.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
123
- execsql2-2.22.8.dist-info/RECORD,,
105
+ execsql2-2.23.0.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
106
+ execsql2-2.23.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
107
+ execsql2-2.23.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
108
+ execsql2-2.23.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
109
+ execsql2-2.23.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
110
+ execsql2-2.23.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
111
+ execsql2-2.23.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
112
+ execsql2-2.23.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
113
+ execsql2-2.23.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
114
+ execsql2-2.23.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
115
+ execsql2-2.23.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
116
+ execsql2-2.23.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
117
+ execsql2-2.23.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
118
+ execsql2-2.23.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
119
+ execsql2-2.23.0.dist-info/METADATA,sha256=i-ekOSO01zUc7UtJwXvGJLTZtmvA78sZSbsRXUO0E20,21504
120
+ execsql2-2.23.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
121
+ execsql2-2.23.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
122
+ execsql2-2.23.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
123
+ execsql2-2.23.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
124
+ execsql2-2.23.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.3
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any