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.
- execsql/cli/__init__.py +436 -0
- execsql/cli/dsn.py +86 -0
- execsql/cli/help.py +140 -0
- execsql/{cli.py → cli/run.py} +14 -589
- execsql/config.py +65 -1
- execsql/db/access.py +27 -15
- execsql/db/base.py +328 -215
- execsql/db/dsn.py +10 -5
- execsql/db/duckdb.py +6 -2
- execsql/db/factory.py +21 -0
- execsql/db/firebird.py +27 -19
- execsql/db/mysql.py +12 -7
- execsql/db/oracle.py +15 -11
- execsql/db/postgres.py +31 -16
- execsql/db/sqlite.py +15 -11
- execsql/db/sqlserver.py +16 -5
- execsql/exceptions.py +25 -7
- execsql/exporters/base.py +12 -1
- execsql/exporters/delimited.py +80 -35
- execsql/exporters/duckdb.py +6 -2
- execsql/exporters/feather.py +10 -6
- execsql/exporters/html.py +89 -69
- execsql/exporters/json.py +52 -45
- execsql/exporters/latex.py +37 -27
- execsql/exporters/ods.py +32 -11
- execsql/exporters/parquet.py +5 -2
- execsql/exporters/pretty.py +16 -9
- execsql/exporters/raw.py +22 -16
- execsql/exporters/sqlite.py +6 -2
- execsql/exporters/templates.py +39 -21
- execsql/exporters/values.py +26 -20
- execsql/exporters/xls.py +30 -11
- execsql/exporters/xml.py +31 -13
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +6 -4
- execsql/importers/csv.py +8 -6
- execsql/importers/feather.py +6 -4
- execsql/importers/ods.py +6 -4
- execsql/importers/xls.py +6 -4
- execsql/metacommands/__init__.py +208 -1548
- execsql/metacommands/conditions.py +101 -27
- execsql/metacommands/control.py +8 -4
- execsql/metacommands/data.py +6 -6
- execsql/metacommands/debug.py +6 -2
- execsql/metacommands/dispatch.py +2011 -0
- execsql/metacommands/io.py +67 -1310
- execsql/metacommands/io_export.py +442 -0
- execsql/metacommands/io_fileops.py +287 -0
- execsql/metacommands/io_import.py +398 -0
- execsql/metacommands/io_write.py +248 -0
- execsql/metacommands/prompt.py +22 -66
- execsql/metacommands/system.py +7 -2
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/py.typed +0 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +184 -402
- execsql/script/variables.py +281 -0
- execsql/types.py +49 -20
- execsql/utils/auth.py +2 -0
- execsql/utils/crypto.py +4 -6
- execsql/utils/datetime.py +1 -0
- execsql/utils/errors.py +11 -0
- execsql/utils/fileio.py +33 -8
- execsql/utils/gui.py +46 -0
- execsql/utils/mail.py +7 -17
- execsql/utils/numeric.py +2 -0
- execsql/utils/regex.py +9 -0
- execsql/utils/strings.py +16 -0
- execsql/utils/timer.py +2 -0
- execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/METADATA +13 -6
- execsql2-2.4.0.dist-info/RECORD +108 -0
- execsql2-2.1.2.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.1.2.dist-info/RECORD +0 -96
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
|
35
|
-
import
|
|
36
|
-
import
|
|
37
|
-
import
|
|
38
|
-
import
|
|
39
|
-
import
|
|
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,
|
|
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)
|
execsql/exporters/parquet.py
CHANGED
|
@@ -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")
|
execsql/exporters/pretty.py
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
32
|
-
for
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
for
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
try:
|
|
58
|
+
for row in rowsource:
|
|
59
|
+
for col in row:
|
|
60
|
+
of.write(base64.standard_b64decode(col))
|
|
61
|
+
finally:
|
|
62
|
+
of.close()
|
execsql/exporters/sqlite.py
CHANGED
|
@@ -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)
|
execsql/exporters/templates.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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"
|
|
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
|
|
114
|
-
raise ErrInfo("error", other_msg=e.message + f" on template line {e.lineno}")
|
|
115
|
-
except
|
|
116
|
-
raise ErrInfo("error", other_msg=f"Jinja2 template error ({e.message})")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
execsql/exporters/values.py
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
firstrow
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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)
|