execsql2 2.1.2__py3-none-any.whl → 2.4.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 (94) hide show
  1. execsql/cli/__init__.py +436 -0
  2. execsql/cli/dsn.py +86 -0
  3. execsql/cli/help.py +140 -0
  4. execsql/{cli.py → cli/run.py} +14 -589
  5. execsql/config.py +65 -1
  6. execsql/db/access.py +27 -15
  7. execsql/db/base.py +328 -215
  8. execsql/db/dsn.py +10 -5
  9. execsql/db/duckdb.py +6 -2
  10. execsql/db/factory.py +21 -0
  11. execsql/db/firebird.py +27 -19
  12. execsql/db/mysql.py +12 -7
  13. execsql/db/oracle.py +15 -11
  14. execsql/db/postgres.py +31 -16
  15. execsql/db/sqlite.py +15 -11
  16. execsql/db/sqlserver.py +16 -5
  17. execsql/exceptions.py +25 -7
  18. execsql/exporters/base.py +12 -1
  19. execsql/exporters/delimited.py +80 -35
  20. execsql/exporters/duckdb.py +6 -2
  21. execsql/exporters/feather.py +10 -6
  22. execsql/exporters/html.py +89 -69
  23. execsql/exporters/json.py +52 -45
  24. execsql/exporters/latex.py +37 -27
  25. execsql/exporters/ods.py +32 -11
  26. execsql/exporters/parquet.py +5 -2
  27. execsql/exporters/pretty.py +16 -9
  28. execsql/exporters/raw.py +22 -16
  29. execsql/exporters/sqlite.py +6 -2
  30. execsql/exporters/templates.py +39 -21
  31. execsql/exporters/values.py +26 -20
  32. execsql/exporters/xls.py +30 -11
  33. execsql/exporters/xml.py +31 -13
  34. execsql/exporters/zip.py +15 -0
  35. execsql/importers/base.py +6 -4
  36. execsql/importers/csv.py +8 -6
  37. execsql/importers/feather.py +6 -4
  38. execsql/importers/ods.py +6 -4
  39. execsql/importers/xls.py +6 -4
  40. execsql/metacommands/__init__.py +208 -1548
  41. execsql/metacommands/conditions.py +101 -27
  42. execsql/metacommands/control.py +8 -4
  43. execsql/metacommands/data.py +6 -6
  44. execsql/metacommands/debug.py +6 -2
  45. execsql/metacommands/dispatch.py +2011 -0
  46. execsql/metacommands/io.py +67 -1310
  47. execsql/metacommands/io_export.py +442 -0
  48. execsql/metacommands/io_fileops.py +287 -0
  49. execsql/metacommands/io_import.py +398 -0
  50. execsql/metacommands/io_write.py +248 -0
  51. execsql/metacommands/prompt.py +22 -66
  52. execsql/metacommands/system.py +7 -2
  53. execsql/models.py +7 -0
  54. execsql/parser.py +10 -0
  55. execsql/py.typed +0 -0
  56. execsql/script/__init__.py +95 -0
  57. execsql/script/control.py +162 -0
  58. execsql/{script.py → script/engine.py} +184 -402
  59. execsql/script/variables.py +281 -0
  60. execsql/types.py +49 -20
  61. execsql/utils/auth.py +2 -0
  62. execsql/utils/crypto.py +4 -6
  63. execsql/utils/datetime.py +1 -0
  64. execsql/utils/errors.py +11 -0
  65. execsql/utils/fileio.py +33 -8
  66. execsql/utils/gui.py +46 -0
  67. execsql/utils/mail.py +7 -17
  68. execsql/utils/numeric.py +2 -0
  69. execsql/utils/regex.py +9 -0
  70. execsql/utils/strings.py +16 -0
  71. execsql/utils/timer.py +2 -0
  72. execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
  73. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
  74. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/METADATA +13 -6
  75. execsql2-2.4.0.dist-info/RECORD +108 -0
  76. execsql2-2.1.2.data/data/execsql2_extras/READ_ME.rst +0 -127
  77. execsql2-2.1.2.dist-info/RECORD +0 -96
  78. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  79. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  80. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  81. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  82. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  83. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  84. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  85. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  86. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  87. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
  88. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  89. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  90. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  91. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
  92. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
  93. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
  94. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
execsql/exporters/ods.py CHANGED
@@ -23,20 +23,25 @@ from execsql.utils.errors import exception_desc, fatal_error
23
23
  from execsql.utils.fileio import filewriter_close
24
24
  from execsql.utils.strings import unquoted
25
25
 
26
+ __all__ = ["OdsFile", "export_ods", "write_query_to_ods", "write_queries_to_ods"]
27
+
26
28
 
27
29
  class OdsFile:
30
+ """Wrapper around the ``odfpy`` library for reading and writing OpenDocument Spreadsheet files."""
31
+
28
32
  def __repr__(self) -> str:
29
33
  return "OdsFile()"
30
34
 
31
35
  def __init__(self) -> None:
36
+ """Import odfpy and initialise the workbook state."""
32
37
  global of
33
38
  try:
34
- import of as of
35
- import of.opendocument
36
- import of.table
37
- import of.text
38
- import of.number
39
- import of.style
39
+ import odf as of # noqa: F401 — submodule imports below register on the `of` alias
40
+ import odf.opendocument # noqa: F401
41
+ import odf.table # noqa: F401
42
+ import odf.text # noqa: F401
43
+ import odf.number # noqa: F401
44
+ import odf.style # noqa: F401
40
45
  except ImportError:
41
46
  fatal_error("The odfpy library is needed to create OpenDocument spreadsheets.")
42
47
  self.filename = None
@@ -44,6 +49,7 @@ class OdsFile:
44
49
  self.cell_style_names = []
45
50
 
46
51
  def open(self, filename: str) -> None:
52
+ """Open an existing ODS file or create a new one at the given path."""
47
53
  self.filename = filename
48
54
  if Path(filename).is_file():
49
55
  self.wbk = of.opendocument.load(filename)
@@ -61,6 +67,7 @@ class OdsFile:
61
67
  self.wbk = of.opendocument.OpenDocumentSpreadsheet()
62
68
 
63
69
  def define_body_style(self) -> None:
70
+ """Register the ``body`` cell style in the workbook if not already defined."""
64
71
  st_name = "body"
65
72
  if st_name not in self.cell_style_names:
66
73
  body_style = of.style.Style(name=st_name, family="table-cell")
@@ -69,6 +76,7 @@ class OdsFile:
69
76
  self.cell_style_names.append(st_name)
70
77
 
71
78
  def define_header_style(self) -> None:
79
+ """Register the ``header`` cell style (bottom-bordered) in the workbook if not already defined."""
72
80
  st_name = "header"
73
81
  if st_name not in self.cell_style_names:
74
82
  header_style = of.style.Style(name=st_name, family="table-cell")
@@ -84,6 +92,7 @@ class OdsFile:
84
92
  self.cell_style_names.append(st_name)
85
93
 
86
94
  def define_iso_datetime_style(self) -> None:
95
+ """Register an ISO-8601 datetime number style in the workbook if not already defined."""
87
96
  st_name = "iso_datetime"
88
97
  if st_name not in self.cell_style_names:
89
98
  dt_style = of.number.DateStyle(name="iso-datetime")
@@ -110,6 +119,7 @@ class OdsFile:
110
119
  self.cell_style_names.append(st_name)
111
120
 
112
121
  def define_iso_date_style(self) -> None:
122
+ """Register an ISO-8601 date number style in the workbook if not already defined."""
113
123
  st_name = "iso_date"
114
124
  if st_name not in self.cell_style_names:
115
125
  dt_style = of.number.DateStyle(name="iso-date")
@@ -125,10 +135,12 @@ class OdsFile:
125
135
  self.cell_style_names.append(st_name)
126
136
 
127
137
  def sheetnames(self) -> list[str]:
138
+ """Return a list of worksheet names in the open workbook."""
128
139
  # Returns a list of the worksheet names in the specified ODS spreadsheet.
129
140
  return [sheet.getAttribute("name") for sheet in self.wbk.spreadsheet.getElementsByType(of.table.Table)]
130
141
 
131
142
  def sheet_named(self, sheetname: Any) -> Any:
143
+ """Return the sheet matching a name or 1-based integer index, or ``None`` if not found."""
132
144
  # Return the sheet with the matching name. If the name is actually an integer,
133
145
  # return that sheet number.
134
146
  if isinstance(sheetname, int):
@@ -153,6 +165,7 @@ class OdsFile:
153
165
  return None
154
166
 
155
167
  def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) -> list:
168
+ """Return all row data from the named sheet, optionally skipping leading junk rows."""
156
169
  sheet = self.sheet_named(sheetname)
157
170
  if not sheet:
158
171
  raise OdsFileError(f"There is no sheet named {sheetname}")
@@ -195,10 +208,12 @@ class OdsFile:
195
208
  return [row_data(r) for r in rows]
196
209
 
197
210
  def new_sheet(self, sheetname: str) -> Any:
211
+ """Create and return a detached sheet object that can later be added to the workbook."""
198
212
  # Returns a sheet (a named Table) that has not yet been added to the workbook
199
213
  return of.table.Table(name=sheetname)
200
214
 
201
215
  def add_row_to_sheet(self, datarow: Any, of_table: Any, header: bool = False) -> None:
216
+ """Append a data row to an ODS table, applying header or body cell styles as appropriate."""
202
217
  if header:
203
218
  self.define_header_style()
204
219
  style_name = "header"
@@ -211,7 +226,7 @@ class OdsFile:
211
226
  if isinstance(item, bool):
212
227
  # Booleans must be evaluated before numbers.
213
228
  tc = of.table.TableCell(valuetype="boolean", value=1 if item else 0, stylename=style_name)
214
- elif isinstance(item, (float, int)):
229
+ elif isinstance(item, float | int):
215
230
  tc = of.table.TableCell(valuetype="float", value=item, stylename=style_name)
216
231
  elif isinstance(item, datetime.datetime):
217
232
  self.define_iso_datetime_style()
@@ -247,15 +262,18 @@ class OdsFile:
247
262
  tr.addElement(tc)
248
263
 
249
264
  def add_sheet(self, of_table: Any) -> None:
265
+ """Attach a prepared sheet object to the workbook's spreadsheet element."""
250
266
  self.wbk.spreadsheet.addElement(of_table)
251
267
 
252
268
  def save_close(self) -> None:
269
+ """Serialise the workbook to disk and release all resources."""
253
270
  with open(self.filename, "wb") as ofile:
254
271
  self.wbk.write(ofile)
255
272
  self.filename = None
256
273
  self.wbk = None
257
274
 
258
275
  def close(self) -> None:
276
+ """Release the workbook reference without saving."""
259
277
  self.filename = None
260
278
  self.wbk = None
261
279
 
@@ -269,6 +287,7 @@ def export_ods(
269
287
  sheetname: str | None = None,
270
288
  desc: str | None = None,
271
289
  ) -> None:
290
+ """Write a single-sheet ODS file from pre-fetched column headers and rows."""
272
291
  # If not given, determine the worksheet name to use. The pattern is "Sheetx", where x is
273
292
  # the first integer for which there is not already a sheet name.
274
293
  if append and Path(outfile).is_file():
@@ -338,12 +357,13 @@ def write_query_to_ods(
338
357
  sheetname: str | None = None,
339
358
  desc: str | None = None,
340
359
  ) -> None:
360
+ """Execute a SELECT and write the result set as a single-sheet ODS spreadsheet."""
341
361
  try:
342
362
  hdrs, rows = db.select_rowsource(select_stmt)
343
363
  except ErrInfo:
344
364
  raise
345
- except Exception:
346
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
365
+ except Exception as e:
366
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
347
367
  export_ods(outfile, hdrs, rows, append, select_stmt, sheetname, desc)
348
368
 
349
369
 
@@ -355,6 +375,7 @@ def write_queries_to_ods(
355
375
  tee: bool = False,
356
376
  desc: str | None = None,
357
377
  ) -> None:
378
+ """Write multiple tables/queries to separate sheets in a single ODS workbook."""
358
379
  from execsql.exporters.pretty import prettyprint_query
359
380
  from execsql.exporters.base import ExportRecord
360
381
 
@@ -403,8 +424,8 @@ def write_queries_to_ods(
403
424
  hdrs, rows = db.select_rowsource(select_stmt)
404
425
  except ErrInfo:
405
426
  raise
406
- except Exception:
407
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
427
+ except Exception as e:
428
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
408
429
  # Add the data to a new sheet.
409
430
  tbl = wbk.new_sheet(sheet_name)
410
431
  wbk.add_row_to_sheet(hdrs, tbl, header=True)
@@ -13,16 +13,19 @@ from execsql.exceptions import ErrInfo
13
13
  from execsql.utils.errors import exception_desc
14
14
  from execsql.utils.fileio import filewriter_close
15
15
 
16
+ __all__ = ["write_query_to_parquet"]
17
+
16
18
 
17
19
  def write_query_to_parquet(outfile: str, headers: list[str], rows: Any) -> None:
20
+ """Write a row source as an Apache Parquet file using polars."""
18
21
  try:
19
22
  import polars as pl
20
- except ImportError:
23
+ except ImportError as e:
21
24
  raise ErrInfo(
22
25
  "exception",
23
26
  exception_msg=exception_desc(),
24
27
  other_msg="The polars Python package must be installed to export data to the parquet format.",
25
- )
28
+ ) from e
26
29
  rows_list = list(rows)
27
30
  if rows_list:
28
31
  df = pl.DataFrame(rows_list, schema=headers, orient="row")
@@ -16,6 +16,8 @@ from execsql.exceptions import ErrInfo
16
16
  from execsql.utils.errors import exception_desc
17
17
  from execsql.utils.fileio import filewriter_close
18
18
 
19
+ __all__ = ["prettyprint_query", "prettyprint_rowset"]
20
+
19
21
 
20
22
  def prettyprint_rowset(
21
23
  colhdrs: list[str],
@@ -26,6 +28,8 @@ def prettyprint_rowset(
26
28
  desc: str | None = None,
27
29
  zipfile: str | None = None,
28
30
  ) -> None:
31
+ """Format a pre-fetched result set as a fixed-width human-readable text table and write it."""
32
+
29
33
  # Adapted from the pp() function by Aaron Watters,
30
34
  # posted to gadfly-rdbms@egroups.com 1999-01-18.
31
35
  def as_ucode(s):
@@ -43,12 +47,12 @@ def prettyprint_rowset(
43
47
  if not isinstance(rows, list):
44
48
  try:
45
49
  rows = list(rows)
46
- except Exception:
50
+ except Exception as e:
47
51
  raise ErrInfo(
48
52
  "exception",
49
53
  exception_msg=exception_desc(),
50
54
  other_msg="Can't create a list in memory of the data to be displayed as formatted text.",
51
- )
55
+ ) from e
52
56
  rcols = range(len(colhdrs))
53
57
  rrows = range(len(rows))
54
58
  colwidths = [max(0, len(colhdrs[j]), *(len(as_ucode(rows[i][j])) for i in rrows)) for j in rcols]
@@ -76,13 +80,15 @@ def prettyprint_rowset(
76
80
  ofile = EncodedFile(output_dest, _state.conf.output_encoding).open("w")
77
81
  else:
78
82
  ofile = ZipWriter(zipfile, output_dest, append)
79
- if desc is not None:
80
- ofile.write(f"{desc}\n")
81
- for row in rows:
82
- ln = f"{margin}{row}\n"
83
- ofile.write(ln)
84
- if output_dest != "stdout":
85
- ofile.close()
83
+ try:
84
+ if desc is not None:
85
+ ofile.write(f"{desc}\n")
86
+ for row in rows:
87
+ ln = f"{margin}{row}\n"
88
+ ofile.write(ln)
89
+ finally:
90
+ if output_dest != "stdout":
91
+ ofile.close()
86
92
  return None
87
93
 
88
94
 
@@ -95,6 +101,7 @@ def prettyprint_query(
95
101
  desc: str | None = None,
96
102
  zipfile: str | None = None,
97
103
  ) -> None:
104
+ """Execute a SELECT and write the result set as a column-aligned text table."""
98
105
  _state.status.sql_error = False
99
106
  names, rows = db.select_data(select_stmt)
100
107
  prettyprint_rowset(names, rows, outfile, append, and_val, desc, zipfile=zipfile)
execsql/exporters/raw.py CHANGED
@@ -9,11 +9,14 @@ used by the ``EXPORT … FORMAT raw`` and ``FORMAT b64`` metacommand
9
9
  variants.
10
10
  """
11
11
 
12
+ import base64
12
13
  from typing import Any
13
14
 
14
15
  from execsql.exporters.zip import ZipWriter
15
16
  from execsql.utils.fileio import filewriter_close
16
17
 
18
+ __all__ = ["write_query_raw", "write_query_b64"]
19
+
17
20
 
18
21
  def write_query_raw(
19
22
  outfile: str,
@@ -22,35 +25,38 @@ def write_query_raw(
22
25
  append: bool = False,
23
26
  zipfile: str | None = None,
24
27
  ) -> None:
28
+ """Write raw binary column data from a row source directly to a file or ZIP archive."""
25
29
  if zipfile is None:
26
30
  filewriter_close(outfile)
27
31
  mode = "wb" if not append else "ab"
28
32
  of = open(outfile, mode) # noqa: SIM115
29
33
  else:
30
34
  of = ZipWriter(zipfile, outfile, append)
31
- for row in rowsource:
32
- for col in row:
33
- if isinstance(col, bytearray):
34
- of.write(col)
35
- else:
36
- if isinstance(col, str):
37
- of.write(bytes(col, db_encoding))
35
+ try:
36
+ for row in rowsource:
37
+ for col in row:
38
+ if isinstance(col, bytearray):
39
+ of.write(col)
38
40
  else:
39
- of.write(bytes(str(col), db_encoding))
40
- of.close()
41
+ if isinstance(col, str):
42
+ of.write(bytes(col, db_encoding))
43
+ else:
44
+ of.write(bytes(str(col), db_encoding))
45
+ finally:
46
+ of.close()
41
47
 
42
48
 
43
49
  def write_query_b64(outfile: str, rowsource: Any, append: bool = False, zipfile: str | None = None) -> None:
44
- global base64
45
- import base64
46
-
50
+ """Decode base64 column data from a row source and write the raw bytes to a file or ZIP archive."""
47
51
  if zipfile is None:
48
52
  filewriter_close(outfile)
49
53
  mode = "wb" if not append else "ab"
50
54
  of = open(outfile, mode) # noqa: SIM115
51
55
  else:
52
56
  of = ZipWriter(zipfile, outfile, append)
53
- for row in rowsource:
54
- for col in row:
55
- of.write(base64.standard_b64decode(col))
56
- of.close()
57
+ try:
58
+ for row in rowsource:
59
+ for col in row:
60
+ of.write(base64.standard_b64decode(col))
61
+ finally:
62
+ of.close()
@@ -14,6 +14,8 @@ from typing import Any
14
14
  from execsql.exceptions import ErrInfo
15
15
  from execsql.types import dbt_sqlite
16
16
 
17
+ __all__ = ["export_sqlite", "write_query_to_sqlite"]
18
+
17
19
 
18
20
  def export_sqlite(
19
21
  outfile: str,
@@ -22,6 +24,7 @@ def export_sqlite(
22
24
  append: bool,
23
25
  tablename: str,
24
26
  ) -> None:
27
+ """Write pre-fetched rows to a table in an SQLite database file, creating it if necessary."""
25
28
  import sqlite3
26
29
 
27
30
  from execsql.models import DataTable
@@ -71,12 +74,13 @@ def write_query_to_sqlite(
71
74
  append: bool,
72
75
  tablename: str,
73
76
  ) -> None:
77
+ """Execute a SELECT and write the result set to a named table in an SQLite database."""
74
78
  from execsql.utils.errors import exception_desc
75
79
 
76
80
  try:
77
81
  hdrs, rows = db.select_rowsource(select_stmt)
78
82
  except ErrInfo:
79
83
  raise
80
- except Exception:
81
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
84
+ except Exception as e:
85
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
82
86
  export_sqlite(outfile, hdrs, rows, append, tablename)
@@ -9,6 +9,7 @@ substitution) and :func:`report_query`, which drives the
9
9
  variants. The Jinja2 template processor is loaded lazily when selected.
10
10
  """
11
11
 
12
+ import string
12
13
  from typing import Any
13
14
 
14
15
  import execsql.state as _state
@@ -16,21 +17,26 @@ from execsql.exceptions import ErrInfo
16
17
  from execsql.utils.errors import fatal_error
17
18
  from execsql.utils.fileio import filewriter_close
18
19
 
20
+ __all__ = ["StrTemplateReport", "JinjaTemplateReport", "report_query"]
21
+
19
22
 
20
23
  class StrTemplateReport:
24
+ """Generates a report by applying Python's :class:`string.Template` to each row of a data table."""
25
+
21
26
  # Exporting/reporting using Python's default string.Template, iterated over all
22
27
  # rows of a data table.
23
28
  def __init__(self, template_file: str) -> None:
24
- global string
25
- import string
26
-
29
+ """Load and compile the template from the given file path."""
27
30
  conf = _state.conf
28
31
  self.infname = template_file
29
32
  from execsql.utils.fileio import EncodedFile
30
33
 
31
34
  inf = EncodedFile(self.infname, conf.script_encoding)
32
- self.template = string.Template(inf.open("r").read())
33
- inf.close()
35
+ fh = inf.open("r")
36
+ try:
37
+ self.template = string.Template(fh.read())
38
+ finally:
39
+ fh.close()
34
40
 
35
41
  def __repr__(self) -> str:
36
42
  return f"StrTemplateReport({self.infname})"
@@ -43,6 +49,7 @@ class StrTemplateReport:
43
49
  append: bool = False,
44
50
  zipfile: str | None = None,
45
51
  ) -> None:
52
+ """Render the template for each row in ``data_dict_rows`` and write the output."""
46
53
  conf = _state.conf
47
54
  from execsql.utils.fileio import EncodedFile
48
55
  from execsql.exporters.zip import ZipWriter
@@ -58,18 +65,25 @@ class StrTemplateReport:
58
65
  ofile = EncodedFile(output_dest, conf.output_encoding).open("w")
59
66
  else:
60
67
  ofile = ZipWriter(zipfile, output_dest, append)
61
- for dd in data_dict_rows:
62
- ofile.write(self.template.safe_substitute(dd))
63
- if output_dest != "stdout":
64
- ofile.close()
68
+ try:
69
+ for dd in data_dict_rows:
70
+ ofile.write(self.template.safe_substitute(dd))
71
+ finally:
72
+ if output_dest != "stdout":
73
+ ofile.close()
65
74
 
66
75
 
67
76
  class JinjaTemplateReport:
77
+ """Generates a report by rendering a Jinja2 template against the full data table."""
78
+
68
79
  # Exporting/reporting using the Jinja2 templating library.
69
80
  def __init__(self, template_file: str) -> None:
70
- global jinja2
81
+ """Load and compile the Jinja2 template from the given file path."""
71
82
  try:
72
83
  import jinja2
84
+ from jinja2.sandbox import SandboxedEnvironment
85
+
86
+ self._jinja2 = jinja2
73
87
  except ImportError:
74
88
  fatal_error(
75
89
  "The jinja2 library is required to produce reports with the Jinja2 templating system. See http://jinja.pocoo.org/",
@@ -79,11 +93,14 @@ class JinjaTemplateReport:
79
93
  from execsql.utils.fileio import EncodedFile
80
94
 
81
95
  inf = EncodedFile(template_file, conf.script_encoding)
82
- self.template = jinja2.Template(inf.open("r").read())
83
- inf.close()
96
+ fh = inf.open("r")
97
+ try:
98
+ self.template = SandboxedEnvironment().from_string(fh.read())
99
+ finally:
100
+ fh.close()
84
101
 
85
102
  def __repr__(self) -> str:
86
- return f"StrTemplateReport({self.infname})"
103
+ return f"JinjaTemplateReport({self.infname})"
87
104
 
88
105
  def write_report(
89
106
  self,
@@ -93,6 +110,7 @@ class JinjaTemplateReport:
93
110
  append: bool = False,
94
111
  zipfile: str | None = None,
95
112
  ) -> None:
113
+ """Render the Jinja2 template with ``headers`` and ``datatable`` context and write the output."""
96
114
  conf = _state.conf
97
115
  from execsql.utils.fileio import EncodedFile
98
116
  from execsql.exporters.zip import ZipWriter
@@ -110,14 +128,13 @@ class JinjaTemplateReport:
110
128
  ofile = ZipWriter(zipfile, output_dest, append)
111
129
  try:
112
130
  ofile.write(self.template.render(headers=headers, datatable=data_dict_rows))
113
- except jinja2.TemplateSyntaxError as e:
114
- raise ErrInfo("error", other_msg=e.message + f" on template line {e.lineno}")
115
- except jinja2.TemplateError as e:
116
- raise ErrInfo("error", other_msg=f"Jinja2 template error ({e.message})")
117
- except:
118
- raise
119
- if output_dest != "stdout":
120
- ofile.close()
131
+ except self._jinja2.TemplateSyntaxError as e:
132
+ raise ErrInfo("error", other_msg=e.message + f" on template line {e.lineno}") from e
133
+ except self._jinja2.TemplateError as e:
134
+ raise ErrInfo("error", other_msg=f"Jinja2 template error ({e.message})") from e
135
+ finally:
136
+ if output_dest != "stdout":
137
+ ofile.close()
121
138
 
122
139
 
123
140
  def report_query(
@@ -128,6 +145,7 @@ def report_query(
128
145
  append: bool = False,
129
146
  zipfile: str | None = None,
130
147
  ) -> None:
148
+ """Execute a SELECT and render the result set through a str-template or Jinja2 template file."""
131
149
  # Write (export) a template-based report.
132
150
  conf = _state.conf
133
151
  _state.status.sql_error = False
@@ -16,6 +16,8 @@ from execsql.exceptions import ErrInfo
16
16
  from execsql.utils.errors import exception_desc
17
17
  from execsql.utils.fileio import filewriter_close
18
18
 
19
+ __all__ = ["export_values", "write_query_to_values"]
20
+
19
21
 
20
22
  def export_values(
21
23
  outfile: str,
@@ -25,6 +27,7 @@ def export_values(
25
27
  desc: str | None = None,
26
28
  zipfile: str | None = None,
27
29
  ) -> None:
30
+ """Write pre-fetched rows as SQL INSERT … VALUES statements to a file or ZIP archive."""
28
31
  conf = _state.conf
29
32
  if outfile.lower() == "stdout":
30
33
  f = _state.output
@@ -40,24 +43,26 @@ def export_values(
40
43
  f = ef.open("wt")
41
44
  else:
42
45
  f = ZipWriter(zipfile, outfile, append)
43
- if desc is not None:
44
- f.write(f"-- {desc}\n")
45
- f.write(f"INSERT INTO !!target_table!!\n ({', '.join(hdrs)})\n")
46
- f.write("VALUES\n")
47
- firstrow = True
48
- for r in rows:
49
- if firstrow:
50
- firstrow = False
51
- else:
52
- f.write(",\n")
53
- quoted_row = [
54
- f"'{v.replace(chr(39), chr(39) * 2)}'" if isinstance(v, str) else str(v) if v is not None else "NULL"
55
- for v in r
56
- ]
57
- f.write(f" ({', '.join(quoted_row)})")
58
- f.write("\n ;\n")
59
- if outfile.lower() != "stdout":
60
- f.close()
46
+ try:
47
+ if desc is not None:
48
+ f.write(f"-- {desc}\n")
49
+ f.write(f"INSERT INTO !!target_table!!\n ({', '.join(hdrs)})\n")
50
+ f.write("VALUES\n")
51
+ firstrow = True
52
+ for r in rows:
53
+ if firstrow:
54
+ firstrow = False
55
+ else:
56
+ 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
+ ]
61
+ f.write(f" ({', '.join(quoted_row)})")
62
+ f.write("\n ;\n")
63
+ finally:
64
+ if outfile.lower() != "stdout":
65
+ f.close()
61
66
 
62
67
 
63
68
  def write_query_to_values(
@@ -68,10 +73,11 @@ def write_query_to_values(
68
73
  desc: str | None = None,
69
74
  zipfile: str | None = None,
70
75
  ) -> None:
76
+ """Execute a SELECT and write the result set as SQL INSERT … VALUES statements."""
71
77
  try:
72
78
  hdrs, rows = db.select_rowsource(select_stmt)
73
79
  except ErrInfo:
74
80
  raise
75
- except Exception:
76
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
81
+ except Exception as e:
82
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
77
83
  export_values(outfile, hdrs, rows, append, desc, zipfile=zipfile)