execsql2 2.6.0__py3-none-any.whl → 2.8.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 (35) hide show
  1. execsql/cli/__init__.py +6 -0
  2. execsql/cli/run.py +95 -7
  3. execsql/exporters/__init__.py +3 -3
  4. execsql/exporters/delimited.py +2 -2
  5. execsql/exporters/markdown.py +126 -0
  6. execsql/exporters/xlsx.py +317 -0
  7. execsql/exporters/yaml.py +87 -0
  8. execsql/metacommands/__init__.py +25 -2
  9. execsql/metacommands/control.py +33 -0
  10. execsql/metacommands/dispatch.py +42 -0
  11. execsql/metacommands/io.py +2 -0
  12. execsql/metacommands/io_export.py +75 -0
  13. execsql/script/engine.py +16 -0
  14. execsql/state.py +10 -0
  15. {execsql2-2.6.0.dist-info → execsql2-2.8.0.dist-info}/METADATA +6 -2
  16. {execsql2-2.6.0.dist-info → execsql2-2.8.0.dist-info}/RECORD +35 -32
  17. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/README.md +0 -0
  18. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  19. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  20. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/execsql.conf +0 -0
  21. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  22. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  23. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  24. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  25. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  26. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  27. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  28. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/script_template.sql +0 -0
  29. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  30. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  31. {execsql2-2.6.0.data → execsql2-2.8.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  32. {execsql2-2.6.0.dist-info → execsql2-2.8.0.dist-info}/WHEEL +0 -0
  33. {execsql2-2.6.0.dist-info → execsql2-2.8.0.dist-info}/entry_points.txt +0 -0
  34. {execsql2-2.6.0.dist-info → execsql2-2.8.0.dist-info}/licenses/LICENSE.txt +0 -0
  35. {execsql2-2.6.0.dist-info → execsql2-2.8.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ YAML export for execsql.
5
+
6
+ Provides :func:`write_query_to_yaml` which serialises a query result set as a
7
+ YAML sequence of mappings (one mapping per row).
8
+
9
+ Requires the ``PyYAML`` package (``pip install PyYAML`` or
10
+ ``pip install 'execsql2[formats]'``).
11
+ """
12
+
13
+ from typing import Any
14
+
15
+ import execsql.state as _state
16
+ from execsql.exceptions import ErrInfo
17
+ from execsql.exporters.zip import ZipWriter
18
+ from execsql.utils.errors import exception_desc
19
+ from execsql.utils.fileio import filewriter_close
20
+
21
+ __all__ = ["write_query_to_yaml"]
22
+
23
+
24
+ def write_query_to_yaml(
25
+ select_stmt: str,
26
+ db: Any,
27
+ outfile: str,
28
+ append: bool = False,
29
+ desc: str | None = None,
30
+ zipfile: str | None = None,
31
+ ) -> None:
32
+ """Execute *select_stmt* and write the result set to *outfile* as YAML.
33
+
34
+ The output is a YAML sequence of mappings — one mapping per row with
35
+ column headers as keys. Python types are preserved: integers stay
36
+ integers, floats stay floats, ``None`` becomes ``null``.
37
+
38
+ Args:
39
+ select_stmt: SQL SELECT statement to execute.
40
+ db: Database connection object exposing ``select_rowsource()``.
41
+ outfile: Destination file path, or ``"stdout"``.
42
+ append: When ``True`` the YAML sequence is appended to an existing
43
+ file. Note that concatenating two bare YAML sequences in one
44
+ file produces a multi-document stream; callers are responsible
45
+ for ensuring the resulting file is valid for their use-case.
46
+ desc: Optional description string. Ignored in plain YAML output
47
+ (YAML does not have a standard metadata header), but accepted
48
+ for API consistency with other exporters.
49
+ zipfile: When provided, write *outfile* as a member of this zip
50
+ archive instead of writing to the filesystem directly.
51
+ """
52
+ try:
53
+ import yaml # type: ignore[import-untyped]
54
+ except ImportError as exc:
55
+ raise ErrInfo(
56
+ "error",
57
+ other_msg=("PyYAML is required for FORMAT YAML export. Install it with: pip install PyYAML"),
58
+ ) from exc
59
+
60
+ conf = _state.conf
61
+ try:
62
+ hdrs, rows = db.select_rowsource(select_stmt)
63
+ except ErrInfo:
64
+ raise
65
+ except Exception as e:
66
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
67
+
68
+ # Build the list of dicts in memory. YAML output is human-readable and
69
+ # typically used for small-to-medium result sets; loading into memory is
70
+ # acceptable and required by yaml.dump().
71
+ uhdrs = [str(h) for h in hdrs]
72
+ data = [dict(zip(uhdrs, row)) for row in rows]
73
+ yaml_text = yaml.dump(data, default_flow_style=False, allow_unicode=True)
74
+
75
+ if zipfile is None:
76
+ filewriter_close(outfile)
77
+ from execsql.utils.fileio import EncodedFile
78
+
79
+ ef = EncodedFile(outfile, conf.output_encoding)
80
+ f = ef.open("at" if append else "wt")
81
+ else:
82
+ f = ZipWriter(zipfile, outfile, append)
83
+
84
+ try:
85
+ f.write(yaml_text)
86
+ finally:
87
+ f.close()
@@ -36,6 +36,7 @@ from execsql.metacommands.connect import (
36
36
  x_daoflushdelay,
37
37
  )
38
38
  from execsql.metacommands.control import (
39
+ x_assert,
39
40
  x_if,
40
41
  x_if_orif,
41
42
  x_if_andif,
@@ -105,6 +106,7 @@ from execsql.metacommands.io import (
105
106
  x_export_query_with_template,
106
107
  x_export_with_template,
107
108
  x_export_ods_multiple,
109
+ x_export_xlsx_multiple,
108
110
  x_export_metadata,
109
111
  x_export_metadata_table,
110
112
  x_import,
@@ -237,6 +239,7 @@ __all__ = [
237
239
  "x_pg_vacuum",
238
240
  "x_daoflushdelay",
239
241
  # control handlers
242
+ "x_assert",
240
243
  "x_if",
241
244
  "x_if_orif",
242
245
  "x_if_andif",
@@ -303,6 +306,7 @@ __all__ = [
303
306
  "x_export_query_with_template",
304
307
  "x_export_with_template",
305
308
  "x_export_ods_multiple",
309
+ "x_export_xlsx_multiple",
306
310
  "x_export_metadata",
307
311
  "x_export_metadata_table",
308
312
  "x_import",
@@ -427,12 +431,31 @@ TEXT_FORMATS = ["TXT", "TXT-AND", "PLAIN"]
427
431
  JSON_VARIANT_FORMATS = ["JSON_TS", "JSON_TABLESCHEMA"]
428
432
 
429
433
  QUERY_EXPORT_FORMATS = (
430
- DELIMITED_FORMATS + TEXT_FORMATS + ["ODS", "JSON", "HTML", "CGI-HTML", "VALUES", "LATEX", "RAW", "B64", "FEATHER"]
434
+ DELIMITED_FORMATS
435
+ + TEXT_FORMATS
436
+ + ["ODS", "XLSX", "JSON", "HTML", "CGI-HTML", "VALUES", "LATEX", "RAW", "B64", "FEATHER", "YAML", "MARKDOWN", "MD"]
431
437
  )
432
438
  TABLE_EXPORT_FORMATS = (
433
439
  DELIMITED_FORMATS
434
440
  + TEXT_FORMATS
435
- + ["JSON", "XML", "VALUES", "HTML", "CGI-HTML", "SQLITE", "DUCKDB", "LATEX", "RAW", "B64", "FEATHER", "HDF5"]
441
+ + [
442
+ "JSON",
443
+ "XML",
444
+ "VALUES",
445
+ "HTML",
446
+ "CGI-HTML",
447
+ "SQLITE",
448
+ "DUCKDB",
449
+ "LATEX",
450
+ "RAW",
451
+ "B64",
452
+ "FEATHER",
453
+ "HDF5",
454
+ "XLSX",
455
+ "YAML",
456
+ "MARKDOWN",
457
+ "MD",
458
+ ]
436
459
  )
437
460
  SERVE_FORMATS = ["BINARY", "CSV", "TXT", "TEXT", "ODS", "JSON", "HTML", "PDF", "ZIP"]
438
461
  METADATA_FORMATS = ["CSV", "TAB", "TSV", "TABQ", "TSVQ", "TXT", "TEXT"]
@@ -34,6 +34,39 @@ from execsql.utils.fileio import EncodedFile, check_dir
34
34
  from execsql.utils.gui import GUI_HALT, GuiSpec, enable_gui, gui_console_isrunning
35
35
 
36
36
 
37
+ def x_assert(**kwargs: Any) -> None:
38
+ """Evaluate a condition and raise ErrInfo if it is false.
39
+
40
+ Syntax::
41
+
42
+ -- !x! ASSERT <condition> ["message"]
43
+ -- !x! ASSERT <condition> ['message']
44
+ -- !x! ASSERT <condition>
45
+
46
+ Args:
47
+ **kwargs: Keyword arguments injected by the dispatch table.
48
+ ``condtest`` — the condition expression string.
49
+ ``message`` — optional user-supplied failure message; may be None.
50
+
51
+ Raises:
52
+ ErrInfo: When the condition evaluates to False (or raises internally
53
+ for an unrecognized condition).
54
+ """
55
+ condition: str = kwargs["condtest"].strip()
56
+ raw_message: str | None = kwargs.get("message")
57
+ if raw_message:
58
+ # Strip surrounding quotes that the regex captured
59
+ message: str = raw_message.strip("'\"")
60
+ else:
61
+ message = f"Assertion failed: {condition}"
62
+
63
+ result = _state.xcmd_test(condition)
64
+ if result:
65
+ _state.exec_log.log_user_msg(f"ASSERT passed: {condition}")
66
+ else:
67
+ raise ErrInfo(type="cmd", other_msg=message)
68
+
69
+
37
70
  def x_if(**kwargs: Any) -> None:
38
71
  tf_value = _state.xcmd_test(kwargs["condtest"])
39
72
  if tf_value:
@@ -36,6 +36,7 @@ from execsql.metacommands.connect import (
36
36
  x_use,
37
37
  )
38
38
  from execsql.metacommands.control import (
39
+ x_assert,
39
40
  x_begin_batch,
40
41
  x_break,
41
42
  x_end_batch,
@@ -107,6 +108,7 @@ from execsql.metacommands.io import (
107
108
  x_export_metadata,
108
109
  x_export_metadata_table,
109
110
  x_export_ods_multiple,
111
+ x_export_xlsx_multiple,
110
112
  x_export_query,
111
113
  x_export_query_with_template,
112
114
  x_export_row_buffer,
@@ -395,6 +397,16 @@ def build_dispatch_table() -> MetaCommandList:
395
397
  ),
396
398
  x_export_ods_multiple,
397
399
  )
400
+ mcl.add(
401
+ ins_table_list_rxs(
402
+ r"^\s*EXPORT\s+",
403
+ ins_fn_rxs(
404
+ r"\s+(?P<tee>TEE\s+)?(?P<append>APPEND\s+)?TO\s+",
405
+ r'\s+AS\s+XLSX(?:\s+DESCRIP(?:TION)?\s+"(?P<description>[^"]*)")?\s*$',
406
+ ),
407
+ ),
408
+ x_export_xlsx_multiple,
409
+ )
398
410
 
399
411
  # ------------------------------------------------------------------
400
412
  # IMPORT_FILE
@@ -1648,6 +1660,36 @@ def build_dispatch_table() -> MetaCommandList:
1648
1660
  category="action",
1649
1661
  )
1650
1662
 
1663
+ # ------------------------------------------------------------------
1664
+ # ASSERT
1665
+ # ------------------------------------------------------------------
1666
+ # Two registrations; MetaCommandList.add() prepends, so register the
1667
+ # broader (no-message) pattern first and the more specific (with-message)
1668
+ # pattern second — the second registration wins because it is prepended
1669
+ # last and therefore tried first during dispatch.
1670
+ #
1671
+ # with-message: the trailing quoted token is captured as `message`;
1672
+ # everything between ASSERT and the message becomes `condtest`.
1673
+ # This handles conditions that themselves contain quoted strings, e.g.:
1674
+ # ASSERT $VAR = 'expected' 'wrong value'
1675
+ # The non-greedy (.+?) stops before the LAST quoted token on the line.
1676
+ #
1677
+ # no-message: full remainder after ASSERT goes into `condtest`.
1678
+ mcl.add(
1679
+ r"^\s*ASSERT\s+(?P<condtest>.+?)\s*$",
1680
+ x_assert,
1681
+ description="ASSERT",
1682
+ category="action",
1683
+ run_when_false=False,
1684
+ )
1685
+ mcl.add(
1686
+ r"^\s*ASSERT\s+(?P<condtest>.+?)\s+(?P<message>(?:\"[^\"]*\"|'[^']*'))\s*$",
1687
+ x_assert,
1688
+ description="ASSERT",
1689
+ category="action",
1690
+ run_when_false=False,
1691
+ )
1692
+
1651
1693
  # ------------------------------------------------------------------
1652
1694
  # IF / ORIF / ANDIF / ELSEIF / ELSE / ENDIF
1653
1695
  # ------------------------------------------------------------------
@@ -22,6 +22,7 @@ from execsql.metacommands.io_export import ( # noqa: F401
22
22
  x_export_metadata,
23
23
  x_export_metadata_table,
24
24
  x_export_ods_multiple,
25
+ x_export_xlsx_multiple,
25
26
  x_export_query,
26
27
  x_export_query_with_template,
27
28
  x_export_row_buffer,
@@ -78,6 +79,7 @@ __all__ = [
78
79
  "x_export_metadata",
79
80
  "x_export_metadata_table",
80
81
  "x_export_ods_multiple",
82
+ "x_export_xlsx_multiple",
81
83
  "x_export_query",
82
84
  "x_export_query_with_template",
83
85
  "x_export_row_buffer",
@@ -19,6 +19,7 @@ from execsql.exporters.html import write_query_to_cgi_html, write_query_to_html
19
19
  from execsql.exporters.json import write_query_to_json, write_query_to_json_ts
20
20
  from execsql.exporters.latex import write_query_to_latex
21
21
  from execsql.exporters.ods import write_queries_to_ods, write_query_to_ods
22
+ from execsql.exporters.xlsx import write_queries_to_xlsx, write_query_to_xlsx
22
23
  from execsql.exporters.parquet import write_query_to_parquet
23
24
  from execsql.exporters.pretty import prettyprint_query, prettyprint_rowset
24
25
  from execsql.exporters.raw import write_query_b64, write_query_raw
@@ -26,6 +27,8 @@ from execsql.exporters.sqlite import write_query_to_sqlite
26
27
  from execsql.exporters.templates import report_query
27
28
  from execsql.exporters.values import write_query_to_values
28
29
  from execsql.exporters.xml import write_query_to_xml
30
+ from execsql.exporters.markdown import write_query_to_markdown
31
+ from execsql.exporters.yaml import write_query_to_yaml
29
32
  from execsql.importers.base import import_data_table
30
33
  from execsql.script import current_script_line
31
34
  from execsql.utils.errors import exception_desc
@@ -85,6 +88,8 @@ def x_export(**kwargs: Any) -> None:
85
88
  raise ErrInfo("error", other_msg="Cannot export to the HDF5 format within a zipfile.")
86
89
  if filefmt == "ods":
87
90
  raise ErrInfo("error", other_msg="Cannot export to an ODS workbook within a zipfile.")
91
+ if filefmt == "xlsx":
92
+ raise ErrInfo("error", other_msg="Cannot export to an XLSX workbook within a zipfile.")
88
93
  notype = bool(kwargs.get("notype"))
89
94
  if zipfilename is not None:
90
95
  check_dir(zipfilename)
@@ -120,6 +125,15 @@ def x_export(**kwargs: Any) -> None:
120
125
  sheetname=queryname,
121
126
  desc=description,
122
127
  )
128
+ elif filefmt == "xlsx":
129
+ write_query_to_xlsx(
130
+ select_stmt,
131
+ _state.dbs.current(),
132
+ outfile,
133
+ append,
134
+ sheetname=queryname,
135
+ desc=description,
136
+ )
123
137
  elif filefmt == "duckdb":
124
138
  write_query_to_duckdb(select_stmt, _state.dbs.current(), outfile, append, tablename=queryname)
125
139
  elif filefmt == "sqlite":
@@ -191,6 +205,24 @@ def x_export(**kwargs: Any) -> None:
191
205
  )
192
206
  elif filefmt == "hdf5":
193
207
  write_query_to_hdf5(table, select_stmt, _state.dbs.current(), outfile, append, desc=description)
208
+ elif filefmt == "yaml":
209
+ write_query_to_yaml(
210
+ select_stmt,
211
+ _state.dbs.current(),
212
+ outfile,
213
+ append,
214
+ desc=description,
215
+ zipfile=zipfilename,
216
+ )
217
+ elif filefmt in ("markdown", "md"):
218
+ write_query_to_markdown(
219
+ select_stmt,
220
+ _state.dbs.current(),
221
+ outfile,
222
+ append,
223
+ desc=description,
224
+ zipfile=zipfilename,
225
+ )
194
226
  else:
195
227
  try:
196
228
  hdrs, rows = _state.dbs.current().select_rowsource(select_stmt)
@@ -237,6 +269,8 @@ def x_export_query(**kwargs: Any) -> None:
237
269
  raise ErrInfo("error", other_msg="Cannot export to the HDF5 format within a zipfile.")
238
270
  if filefmt == "ods":
239
271
  raise ErrInfo("error", other_msg="Cannot export to an ODS workbook within a zipfile.")
272
+ if filefmt == "xlsx":
273
+ raise ErrInfo("error", other_msg="Cannot export to an XLSX workbook within a zipfile.")
240
274
  notype = bool(kwargs.get("notype"))
241
275
  check_dir(outfile)
242
276
  if tee and outfile.lower() != "stdout":
@@ -270,6 +304,16 @@ def x_export_query(**kwargs: Any) -> None:
270
304
  sheetname=f"Query_{lno}",
271
305
  desc=description,
272
306
  )
307
+ elif filefmt == "xlsx":
308
+ script_name, lno = current_script_line()
309
+ write_query_to_xlsx(
310
+ select_stmt,
311
+ _state.dbs.current(),
312
+ outfile,
313
+ append,
314
+ sheetname=f"Query_{lno}",
315
+ desc=description,
316
+ )
273
317
  elif filefmt == "json":
274
318
  write_query_to_json(
275
319
  select_stmt,
@@ -325,6 +369,24 @@ def x_export_query(**kwargs: Any) -> None:
325
369
  desc=description,
326
370
  zipfile=zipfilename,
327
371
  )
372
+ elif filefmt == "yaml":
373
+ write_query_to_yaml(
374
+ select_stmt,
375
+ _state.dbs.current(),
376
+ outfile,
377
+ append,
378
+ desc=description,
379
+ zipfile=zipfilename,
380
+ )
381
+ elif filefmt in ("markdown", "md"):
382
+ write_query_to_markdown(
383
+ select_stmt,
384
+ _state.dbs.current(),
385
+ outfile,
386
+ append,
387
+ desc=description,
388
+ zipfile=zipfilename,
389
+ )
328
390
  else:
329
391
  try:
330
392
  hdrs, rows = _state.dbs.current().select_rowsource(select_stmt)
@@ -403,6 +465,19 @@ def x_export_ods_multiple(**kwargs: Any) -> None:
403
465
  write_queries_to_ods(table_list, _state.dbs.current(), outfile, append, tee, desc=description)
404
466
 
405
467
 
468
+ def x_export_xlsx_multiple(**kwargs: Any) -> None:
469
+ """Export multiple tables to separate worksheets in a single XLSX workbook."""
470
+ table_list = kwargs["tables"]
471
+ outfile = kwargs["filename"]
472
+ description = kwargs["description"]
473
+ tee = kwargs["tee"]
474
+ tee = bool(tee)
475
+ append = kwargs["append"]
476
+ append = append is not None
477
+ check_dir(outfile)
478
+ write_queries_to_xlsx(table_list, _state.dbs.current(), outfile, append, tee, desc=description)
479
+
480
+
406
481
  def x_export_metadata(**kwargs: Any) -> None:
407
482
  outfile = kwargs["filename"]
408
483
  append = kwargs["append"] is not None
execsql/script/engine.py CHANGED
@@ -489,7 +489,23 @@ class CommandList:
489
489
  _state.subvars.add_substitution("$CURRENT_SCRIPT_NAME", Path(cmditem.source).name)
490
490
  _state.subvars.add_substitution("$CURRENT_SCRIPT_LINE", str(cmditem.line_no))
491
491
  _state.subvars.add_substitution("$SCRIPT_LINE", str(cmditem.line_no))
492
+ _profiling = _state.profile_data is not None
493
+ if _profiling:
494
+ import time as _time
495
+
496
+ _t0 = _time.perf_counter()
492
497
  cmditem.command.run(self.localvars.merge(self.paramvals), not _state.status.batch.in_batch())
498
+ if _profiling:
499
+ _elapsed = _time.perf_counter() - _t0
500
+ _state.profile_data.append(
501
+ (
502
+ cmditem.source,
503
+ cmditem.line_no,
504
+ cmditem.command_type,
505
+ _elapsed,
506
+ cmditem.command.commandline()[:100],
507
+ ),
508
+ )
493
509
  self.cmdptr += 1
494
510
 
495
511
  def run_next(self) -> None:
execsql/state.py CHANGED
@@ -94,6 +94,8 @@ __all__ = [
94
94
  "gui_console",
95
95
  "gui_manager_queue",
96
96
  "gui_manager_thread",
97
+ # Profiling
98
+ "profile_data",
97
99
  # Version
98
100
  "primary_vno",
99
101
  "secondary_vno",
@@ -191,6 +193,8 @@ _CONTEXT_ATTRS: frozenset[str] = frozenset(
191
193
  "gui_console",
192
194
  "gui_manager_queue",
193
195
  "gui_manager_thread",
196
+ # Profiling
197
+ "profile_data",
194
198
  },
195
199
  )
196
200
 
@@ -242,6 +246,8 @@ class RuntimeContext:
242
246
  "gui_console",
243
247
  "gui_manager_queue",
244
248
  "gui_manager_thread",
249
+ # Profiling
250
+ "profile_data",
245
251
  )
246
252
 
247
253
  def __init__(self) -> None:
@@ -289,6 +295,10 @@ class RuntimeContext:
289
295
  self.gui_manager_queue: _mp.Queue | None = None
290
296
  self.gui_manager_thread: _threading.Thread | None = None
291
297
 
298
+ # Profiling — None means profiling is disabled; a list means it is enabled.
299
+ # Each entry: (source, line_no, command_type, elapsed_secs, command_text_preview)
300
+ self.profile_data: list[tuple] | None = None
301
+
292
302
 
293
303
  # ---------------------------------------------------------------------------
294
304
  # Module proxy — transparently delegates context attr access to _ctx
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.6.0
3
+ Version: 2.8.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: Repository, https://github.com/geocoug/execsql
6
6
  Project-URL: Issues, https://github.com/geocoug/execsql/issues
@@ -54,6 +54,7 @@ Requires-Dist: polars; extra == 'all'
54
54
  Requires-Dist: psycopg2-binary; extra == 'all'
55
55
  Requires-Dist: pymysql; extra == 'all'
56
56
  Requires-Dist: pyodbc; extra == 'all'
57
+ Requires-Dist: pyyaml; extra == 'all'
57
58
  Requires-Dist: tables; extra == 'all'
58
59
  Requires-Dist: xlrd; extra == 'all'
59
60
  Provides-Extra: all-db
@@ -76,6 +77,7 @@ Requires-Dist: openpyxl; extra == 'dev'
76
77
  Requires-Dist: polars; extra == 'dev'
77
78
  Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
78
79
  Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
80
+ Requires-Dist: pyyaml; extra == 'dev'
79
81
  Requires-Dist: ruff>=0.4; extra == 'dev'
80
82
  Requires-Dist: tables; extra == 'dev'
81
83
  Requires-Dist: tox-uv>=1.13.1; extra == 'dev'
@@ -91,6 +93,7 @@ Requires-Dist: jinja2; extra == 'formats'
91
93
  Requires-Dist: odfpy; extra == 'formats'
92
94
  Requires-Dist: openpyxl; extra == 'formats'
93
95
  Requires-Dist: polars; extra == 'formats'
96
+ Requires-Dist: pyyaml; extra == 'formats'
94
97
  Requires-Dist: tables; extra == 'formats'
95
98
  Requires-Dist: xlrd; extra == 'formats'
96
99
  Provides-Extra: mssql
@@ -227,9 +230,10 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
227
230
  # Features
228
231
 
229
232
  - Import data from CSV, TSV, Excel, OpenDocument, Feather, or Parquet files into a database table.
230
- - Export query results in 15+ formats including CSV, TSV, JSON, XML, HTML, LaTeX, OpenDocument, Feather, HDF5, DuckDB, SQLite, plain text, and Jinja2 templates.
233
+ - Export query results in 20+ formats including CSV, TSV, JSON, YAML, XML, HTML, Markdown, LaTeX, XLSX, OpenDocument, Feather, Parquet, HDF5, DuckDB, SQLite, plain text, and Jinja2 templates.
231
234
  - Copy data between databases, including across different DBMS types.
232
235
  - Conditionally execute SQL and metacommands using `IF`/`ELSE`/`ENDIF` based on data values, DBMS type, or user input.
236
+ - Validate data with `ASSERT` — halt the script with a clear error message if a condition is false (ideal for CI pipelines).
233
237
  - Loop over blocks of SQL and metacommands using `LOOP`/`ENDLOOP`.
234
238
  - Use substitution variables (`SUB`, `$ARG_x`, built-in variables like `$date_tag`) to parameterize scripts.
235
239
  - Include or chain scripts with `INCLUDE` and `SCRIPT`.
@@ -7,12 +7,12 @@ execsql/format.py,sha256=-6iknDddqbkapMo4NKmT5LAynDLqMW5kHgDWRg0KSws,11990
7
7
  execsql/models.py,sha256=DxkGp9iWbuZDWPGmnxZp9mvEeyOwxEJNx94fxQQiLfQ,13538
8
8
  execsql/parser.py,sha256=mbNSMiAMR1NvNvFtQAZq6nxBOupMGJZXSimLWLtZeNs,15537
9
9
  execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- execsql/state.py,sha256=Kg_cMr0DDEjFkEQ02BKO2xxeH7N03aBRj8UjvzQK-C0,14445
10
+ execsql/state.py,sha256=BodGWiLD7I3s7LFd8Mb6SHMp3I1BhVE4rYcR0UZWAoM,14799
11
11
  execsql/types.py,sha256=HVWb4umIB9lpxCGgqk3xy1hoGYPfN39xci5mHF0Izq4,31882
12
- execsql/cli/__init__.py,sha256=s4283f04Z-SS5CdwsJj6t_oHuyL5sQELsUGTh6x45NU,14908
12
+ execsql/cli/__init__.py,sha256=KewdhCBL8iRa_iPZZ7RKeRl90pGCXg3_lsMaVZph-kY,15118
13
13
  execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
14
14
  execsql/cli/help.py,sha256=Sn_TgSJiQeBx-xZH0fuP5OvR_wasSTumjWF9UHfIX5k,5414
15
- execsql/cli/run.py,sha256=seThMuXV0BmpG94lzOteoxlSjez-p-Ht4__5jjsJZAI,22829
15
+ execsql/cli/run.py,sha256=pkUIcWtzDSYfoIlM418cZdLix7NI45HKRIBxgZfFtHg,26405
16
16
  execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
17
17
  execsql/db/access.py,sha256=L79gUnAnnM9EJ_f4k42jr7DI0qGcKtLOnJTlBC7uPm0,17879
18
18
  execsql/db/base.py,sha256=hfMFj8fXY0T1aXLvWJHqb0aU4EQUDFOc-YrS29HH8U4,30405
@@ -25,14 +25,15 @@ execsql/db/oracle.py,sha256=AFVHhGlCzBuU7JgrAqeUG6e8TUUkk1Y80XVJQnGOqLM,10547
25
25
  execsql/db/postgres.py,sha256=oXR7ODzQhR3yO6q-aNa9_il_rO3SpOX9yYGsfIqHwLI,20139
26
26
  execsql/db/sqlite.py,sha256=2fD3AfckIGWN1oHcOaqQlQnbig26top1IlW-ejPHttI,10204
27
27
  execsql/db/sqlserver.py,sha256=mNwmIIxTzqXU-cOjpNpeFi568HjQHsAk8Xnn-tR6F_E,7563
28
- execsql/exporters/__init__.py,sha256=IB3GdPKdRdyp9UPWTMqqJty769-LXKgBcLbEf3Q4LuM,656
28
+ execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
29
29
  execsql/exporters/base.py,sha256=W9USFyk_2eztjJ51X6CJh7-chE1i3cSx-STOtbHXCNI,6373
30
- execsql/exporters/delimited.py,sha256=VnqDRuybQ7D9fBWf8UtqYYKTiQ-qwMJZFg_SaxNUV-k,30298
30
+ execsql/exporters/delimited.py,sha256=zMvurTRVl5W-6N8DuYtn_xILUkYLMlfflwWMfvdeaF0,30304
31
31
  execsql/exporters/duckdb.py,sha256=Wc9I5uiV4MzmVQzCX-vgVHQUL7U3ZWdOkFVFWBv5SXM,2911
32
32
  execsql/exporters/feather.py,sha256=w2qZAnewzeiRMnmPXECvkgD-6KtyxaiQwjokRT7Awrc,4167
33
33
  execsql/exporters/html.py,sha256=ISQBOr7AJ5koKlebXSvWqzEvl1nXriCRGeKmk-bzkrc,9335
34
34
  execsql/exporters/json.py,sha256=yljlRBbmvDVSTQUe0EdfdqTTRpD5sHfn7-jQ457ydvc,4139
35
35
  execsql/exporters/latex.py,sha256=w_B83_5vKPe8uYxCWGdqvxwJeq0mw5zzKYDiAb7dbN0,4503
36
+ execsql/exporters/markdown.py,sha256=_ZX3dikbtAb6qZxYeWxDZAPF0-cNKTPR7or5kTbD2ZU,4436
36
37
  execsql/exporters/ods.py,sha256=jl2qVHUeCLLv8xrkZfG3jgXbaglQ3rggCHziv7tNQOI,18876
37
38
  execsql/exporters/parquet.py,sha256=186vUTH1oVAQ0s_qayLzEQVsKKu3ijAkhYEI6tysXkg,1095
38
39
  execsql/exporters/pretty.py,sha256=9isA8f6xUz-3-JhMJimibnvtybVrT1cnoAjGnzsPEGI,3423
@@ -42,7 +43,9 @@ execsql/exporters/sqlite.py,sha256=XA0ALLvy-r6Pz1lpOFkWWbvpSP9Hm1tHHiuo_BvPVDk,2
42
43
  execsql/exporters/templates.py,sha256=T9nk7vJrlxiPGfOWGc79xqqDxK3TCYu0wXq48U02npw,5564
43
44
  execsql/exporters/values.py,sha256=HIyud31aux_dbCphfKHEGeZB9fkIPE5PoGXQz817XIE,2520
44
45
  execsql/exporters/xls.py,sha256=nPROgxL8XK2oiBVoqN2L-o0j_jynRIMokwB8NpvOBt0,10623
46
+ execsql/exporters/xlsx.py,sha256=xXTFIKkvJnNOsFdnhSYEkJa4ulTrtq9tIRk6SSchqA0,11299
45
47
  execsql/exporters/xml.py,sha256=lqcOM8uKDoCayU42BPSLNH1_2DIHU5D3LtQItREU90c,2564
48
+ execsql/exporters/yaml.py,sha256=0bwLDU3Fy00yMryBOSBSptbjV8Re6Ks-b62DObFNP4o,3062
46
49
  execsql/exporters/zip.py,sha256=9-hExltQorONNThiMfxPDYHqHsbTeq9zM9zmtG4oFb8,4410
47
50
  execsql/gui/__init__.py,sha256=oCb-cyhLZzVpWJ4WU5HbqEDBrV-lm0ytEwxemrOZyqs,2048
48
51
  execsql/gui/base.py,sha256=sfNRkDrf7FhIgMRUOdyZpRLS1Xk9RqNhrV0A1RP6PXU,6068
@@ -55,15 +58,15 @@ execsql/importers/csv.py,sha256=Mu848WNzuhVO1ade-WurPyxqGOuVNRO8UwRF3-bav_I,4845
55
58
  execsql/importers/feather.py,sha256=g2B69d2uv9vmnXcmjFyTVsMP40LYEzFYkhk3gD26mGw,1900
56
59
  execsql/importers/ods.py,sha256=MJsdsjropzCvxAA3DDZfAL_AnmZ4yij7DnrjGyDJqHQ,2843
57
60
  execsql/importers/xls.py,sha256=e0Zfe47ZiCpA1Ae3XDJ1ko3sCiH3-8U6XLKi6NvD0jQ,3683
58
- execsql/metacommands/__init__.py,sha256=pdEMN1PoJFDgx1p4TFbi4H1Vx6h_QIXcuE-23AEkwvI,10813
61
+ execsql/metacommands/__init__.py,sha256=ejuY2GFHxNh5f_Yp_GOV0EBe2vuUcly0-zBrKiR3qes,11112
59
62
  execsql/metacommands/conditions.py,sha256=u-XdeIWj9QMht9hRGhvH0XlB9V09AliAPKDBHRXc02s,24540
60
63
  execsql/metacommands/connect.py,sha256=Nsm0D91i3RX-R2rzQQ-Br-gULaI6Uvdn9fqb7DOAVfE,14804
61
- execsql/metacommands/control.py,sha256=FCIWD-ZivHRZDqMS-2k37iR05HKHsv_7UPh5zJAg4I4,7693
64
+ execsql/metacommands/control.py,sha256=CBCg0ZKSR-BGejBW5cXwk6aJ9VrYBzCg9C40ofi8qi8,8776
62
65
  execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,11370
63
66
  execsql/metacommands/debug.py,sha256=nmfQ2ijUbTQO3drnyV9EzFueGSTfMl-CddP_NlQyI14,8178
64
- execsql/metacommands/dispatch.py,sha256=th4vA2Vg4vY_fPcePRJUZ67CwUX-_j7tSckCucB3MQA,82065
65
- execsql/metacommands/io.py,sha256=Vv0NnUHiNKVnHbWJ0nsEdtN5SW042OtPCu7hrV57XyY,2913
66
- execsql/metacommands/io_export.py,sha256=kxfGnoBDdPFfncxgz6dazMIoGZEHPOO-Mg02kbpdd20,15965
67
+ execsql/metacommands/dispatch.py,sha256=I6HoBKMofRalL1Cmdsnj1jQFZSFXCgntTofFaIZWgWQ,83670
68
+ execsql/metacommands/io.py,sha256=Duh60caM4go9JczbGYNMKKYpcMimwPzF6EQ_tshKxdE,2971
69
+ execsql/metacommands/io_export.py,sha256=7lkCSnPhXy9FVau9_hT1u68NOVdG2DsWmvUh9hM1QWI,18359
67
70
  execsql/metacommands/io_fileops.py,sha256=RKqbWPTYiwiqCZYG-lpih0w1JVOY4RBFdWr3BJb_pnY,9669
68
71
  execsql/metacommands/io_import.py,sha256=wyxJJdlW07P5ZIhweejhXyyGANAvEhY5uMjKZ200Jyc,12983
69
72
  execsql/metacommands/io_write.py,sha256=NpL2aYGfBpbqmPpYsqniYltYfd_SCA1EQz3_4qSdNbo,8279
@@ -72,7 +75,7 @@ execsql/metacommands/script_ext.py,sha256=TUgAldB2LSJAwZrCvDDi804hQ1d9BDQD2GDqHN
72
75
  execsql/metacommands/system.py,sha256=sUR5kLL7idTVg8WXIMdd-Kv7nkERIiaeL0beWsz8NyY,7293
73
76
  execsql/script/__init__.py,sha256=pIo0EJ7-vg67rSMbOvbri_BOUgLoGoSEUfJgxUN7ZS0,3380
74
77
  execsql/script/control.py,sha256=s-1eZdGARM6H1FwZ6VDdO_f50j7bvvRtTHesfUm9tbc,6144
75
- execsql/script/engine.py,sha256=5WOuSbQR1vrp_SawylshzLmdHco2oEjqZBSoxRg0Ggo,39638
78
+ execsql/script/engine.py,sha256=d3iUGF_r4OQAlqKpd8pIuWGAjDlYvzYiKqi-2Ew1-Yo,40213
76
79
  execsql/script/variables.py,sha256=MOT9XEHucpuuuHQZM5bklxGMBQcwHzwTBxd0q3aO0XY,11641
77
80
  execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
78
81
  execsql/utils/auth.py,sha256=onXzNkNZQZxGC5w7eey06sjvAIAX_Lf9g7nUJtcsel0,7009
@@ -86,24 +89,24 @@ execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
86
89
  execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
87
90
  execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
88
91
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
89
- execsql2-2.6.0.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
90
- execsql2-2.6.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
91
- execsql2-2.6.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
92
- execsql2-2.6.0.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
93
- execsql2-2.6.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
94
- execsql2-2.6.0.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
95
- execsql2-2.6.0.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
96
- execsql2-2.6.0.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
97
- execsql2-2.6.0.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
98
- execsql2-2.6.0.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
99
- execsql2-2.6.0.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
100
- execsql2-2.6.0.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
101
- execsql2-2.6.0.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
102
- execsql2-2.6.0.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
103
- execsql2-2.6.0.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
104
- execsql2-2.6.0.dist-info/METADATA,sha256=NV6dRQPFMR13RsyM3my0svP-jfb6qd6sLePtERmD8S0,16573
105
- execsql2-2.6.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
106
- execsql2-2.6.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
107
- execsql2-2.6.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
108
- execsql2-2.6.0.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
109
- execsql2-2.6.0.dist-info/RECORD,,
92
+ execsql2-2.8.0.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
93
+ execsql2-2.8.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
94
+ execsql2-2.8.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
95
+ execsql2-2.8.0.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
96
+ execsql2-2.8.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
97
+ execsql2-2.8.0.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
98
+ execsql2-2.8.0.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
99
+ execsql2-2.8.0.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
100
+ execsql2-2.8.0.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
101
+ execsql2-2.8.0.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
102
+ execsql2-2.8.0.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
103
+ execsql2-2.8.0.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
104
+ execsql2-2.8.0.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
105
+ execsql2-2.8.0.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
106
+ execsql2-2.8.0.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
107
+ execsql2-2.8.0.dist-info/METADATA,sha256=fGxvlbidjAgVeGg51ZWXNPcblzQrR3UWbgfF60yGpuA,16849
108
+ execsql2-2.8.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
109
+ execsql2-2.8.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
110
+ execsql2-2.8.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
111
+ execsql2-2.8.0.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
112
+ execsql2-2.8.0.dist-info/RECORD,,