execsql2 2.2.1__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/config.py +52 -0
- execsql/db/access.py +11 -3
- execsql/db/base.py +180 -135
- execsql/db/dsn.py +4 -0
- execsql/db/duckdb.py +4 -0
- execsql/db/factory.py +21 -0
- execsql/db/firebird.py +4 -0
- execsql/db/mysql.py +4 -0
- execsql/db/oracle.py +4 -0
- execsql/db/postgres.py +3 -0
- execsql/db/sqlite.py +3 -0
- execsql/db/sqlserver.py +11 -2
- execsql/exceptions.py +18 -0
- execsql/exporters/base.py +6 -0
- execsql/exporters/delimited.py +36 -0
- execsql/exporters/duckdb.py +4 -0
- execsql/exporters/feather.py +4 -0
- execsql/exporters/html.py +6 -0
- execsql/exporters/json.py +5 -6
- execsql/exporters/latex.py +4 -0
- execsql/exporters/ods.py +28 -7
- execsql/exporters/parquet.py +3 -0
- execsql/exporters/pretty.py +5 -0
- execsql/exporters/raw.py +5 -3
- execsql/exporters/sqlite.py +4 -0
- execsql/exporters/templates.py +16 -6
- execsql/exporters/values.py +4 -0
- execsql/exporters/xls.py +26 -7
- execsql/exporters/xml.py +3 -0
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +2 -0
- execsql/importers/csv.py +2 -0
- execsql/importers/feather.py +2 -0
- execsql/importers/ods.py +2 -0
- execsql/importers/xls.py +2 -0
- execsql/metacommands/__init__.py +177 -1968
- execsql/metacommands/dispatch.py +2011 -0
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +144 -406
- execsql/script/variables.py +281 -0
- execsql/types.py +29 -0
- 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 +18 -0
- 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.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/METADATA +8 -1
- execsql2-2.4.0.dist-info/RECORD +108 -0
- execsql2-2.2.1.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.2.1.dist-info/RECORD +0 -104
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
execsql/db/oracle.py
CHANGED
|
@@ -16,8 +16,12 @@ from execsql.utils.errors import exception_desc, fatal_error
|
|
|
16
16
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
17
17
|
import execsql.state as _state
|
|
18
18
|
|
|
19
|
+
__all__ = ["OracleDatabase"]
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class OracleDatabase(Database):
|
|
23
|
+
"""Oracle adapter using the cx_Oracle (python-oracledb) driver."""
|
|
24
|
+
|
|
21
25
|
def __init__(
|
|
22
26
|
self,
|
|
23
27
|
server_name: str,
|
execsql/db/postgres.py
CHANGED
|
@@ -19,11 +19,14 @@ from execsql.utils.auth import clear_stored_password, get_password, password_fro
|
|
|
19
19
|
from execsql.utils.strings import encodings_match
|
|
20
20
|
import execsql.state as _state
|
|
21
21
|
|
|
22
|
+
__all__ = ["PostgresDatabase"]
|
|
22
23
|
|
|
23
24
|
DEFAULT_CONNECT_TIMEOUT = 30 # seconds
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class PostgresDatabase(Database):
|
|
28
|
+
"""PostgreSQL adapter using psycopg2, with schema support, server-side COPY, and keyring auth."""
|
|
29
|
+
|
|
27
30
|
def __init__(
|
|
28
31
|
self,
|
|
29
32
|
server_name: str,
|
execsql/db/sqlite.py
CHANGED
|
@@ -18,11 +18,14 @@ from execsql.exceptions import ErrInfo
|
|
|
18
18
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
19
19
|
import execsql.state as _state
|
|
20
20
|
|
|
21
|
+
__all__ = ["SQLiteDatabase"]
|
|
21
22
|
|
|
22
23
|
DEFAULT_CONNECT_TIMEOUT = 30 # seconds
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class SQLiteDatabase(Database):
|
|
27
|
+
"""SQLite adapter using the Python standard-library sqlite3 module."""
|
|
28
|
+
|
|
26
29
|
def __init__(self, SQLite_fn: str, timeout: float = DEFAULT_CONNECT_TIMEOUT) -> None:
|
|
27
30
|
try:
|
|
28
31
|
import sqlite3 # noqa: F401
|
execsql/db/sqlserver.py
CHANGED
|
@@ -7,6 +7,7 @@ Implements :class:`SqlServerDatabase`, which connects to Microsoft SQL
|
|
|
7
7
|
Server via ``pyodbc``. Corresponds to ``-t s`` on the CLI.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import re
|
|
10
11
|
|
|
11
12
|
from execsql.db.base import Database
|
|
12
13
|
from execsql.exceptions import ErrInfo
|
|
@@ -14,8 +15,12 @@ from execsql.utils.errors import fatal_error
|
|
|
14
15
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
15
16
|
import execsql.state as _state
|
|
16
17
|
|
|
18
|
+
__all__ = ["SqlServerDatabase"]
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
class SqlServerDatabase(Database):
|
|
22
|
+
"""Microsoft SQL Server adapter using pyodbc, trying drivers from newest to oldest."""
|
|
23
|
+
|
|
19
24
|
def __init__(
|
|
20
25
|
self,
|
|
21
26
|
server_name: str,
|
|
@@ -96,9 +101,13 @@ class SqlServerDatabase(Database):
|
|
|
96
101
|
try:
|
|
97
102
|
self.conn = pyodbc.connect(connstr)
|
|
98
103
|
except Exception:
|
|
99
|
-
_state.exec_log.log_status_info(
|
|
104
|
+
_state.exec_log.log_status_info(
|
|
105
|
+
f"Could not connect using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
106
|
+
)
|
|
100
107
|
else:
|
|
101
|
-
_state.exec_log.log_status_info(
|
|
108
|
+
_state.exec_log.log_status_info(
|
|
109
|
+
f"Connected using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
110
|
+
)
|
|
102
111
|
return True
|
|
103
112
|
return False
|
|
104
113
|
|
execsql/exceptions.py
CHANGED
|
@@ -21,6 +21,24 @@ from a single location. Notable exceptions:
|
|
|
21
21
|
- :class:`CondParserError` / :class:`NumericParserError` — parser failures.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ExecSqlError",
|
|
26
|
+
"ConfigError",
|
|
27
|
+
"ExecSqlTimeoutError",
|
|
28
|
+
"ErrInfo",
|
|
29
|
+
"DataTypeError",
|
|
30
|
+
"DbTypeError",
|
|
31
|
+
"ColumnError",
|
|
32
|
+
"DataTableError",
|
|
33
|
+
"DatabaseNotImplementedError",
|
|
34
|
+
"OdsFileError",
|
|
35
|
+
"XlsFileError",
|
|
36
|
+
"XlsxFileError",
|
|
37
|
+
"ConsoleUIError",
|
|
38
|
+
"CondParserError",
|
|
39
|
+
"NumericParserError",
|
|
40
|
+
]
|
|
41
|
+
|
|
24
42
|
|
|
25
43
|
class ExecSqlError(Exception):
|
|
26
44
|
"""Base class for simple single-message execsql exceptions.
|
execsql/exporters/base.py
CHANGED
|
@@ -22,6 +22,8 @@ from execsql.script import current_script_line
|
|
|
22
22
|
from execsql.utils.errors import file_size_date
|
|
23
23
|
from execsql.utils.gui import ConsoleUIError
|
|
24
24
|
|
|
25
|
+
__all__ = ["WriteSpec", "ExportRecord", "ExportMetadata"]
|
|
26
|
+
|
|
25
27
|
|
|
26
28
|
class ExportRecord:
|
|
27
29
|
"""Records the details of a single EXPORT operation for metadata tracking.
|
|
@@ -88,15 +90,18 @@ class ExportMetadata:
|
|
|
88
90
|
self.recordlist: list[ExportRecord] = []
|
|
89
91
|
|
|
90
92
|
def add(self, exp_record: ExportRecord) -> None:
|
|
93
|
+
"""Append an export record to the collection."""
|
|
91
94
|
self.recordlist.append(exp_record)
|
|
92
95
|
|
|
93
96
|
def get(self):
|
|
97
|
+
"""Return column headers and all not-yet-exported records, marking them exported."""
|
|
94
98
|
recs = [er.record for er in self.recordlist if not er.exported]
|
|
95
99
|
for er in self.recordlist:
|
|
96
100
|
er.exported = True
|
|
97
101
|
return self.colhdrs, recs
|
|
98
102
|
|
|
99
103
|
def get_all(self):
|
|
104
|
+
"""Return column headers and every record regardless of prior export state."""
|
|
100
105
|
recs = [er.record for er in self.recordlist]
|
|
101
106
|
for er in self.recordlist:
|
|
102
107
|
er.exported = True
|
|
@@ -130,6 +135,7 @@ class WriteSpec:
|
|
|
130
135
|
self.written = False
|
|
131
136
|
|
|
132
137
|
def write(self) -> None:
|
|
138
|
+
"""Execute the deferred write, expanding substitution variables first."""
|
|
133
139
|
# Writes the message per the specifications given to '__init__()'. Substitution
|
|
134
140
|
# variables are processed.
|
|
135
141
|
# Inputs: no inputs.
|
execsql/exporters/delimited.py
CHANGED
|
@@ -29,9 +29,14 @@ from execsql.utils.errors import exception_desc
|
|
|
29
29
|
from execsql.utils.fileio import filewriter_close
|
|
30
30
|
from execsql.utils.strings import clean_words, fold_words
|
|
31
31
|
|
|
32
|
+
__all__ = ["LineDelimiter", "CsvFile", "CsvWriter", "DelimitedWriter", "write_delimited_file"]
|
|
33
|
+
|
|
32
34
|
|
|
33
35
|
class LineDelimiter:
|
|
36
|
+
"""Encapsulates delimiter, quote character, and escape rules for a single line format."""
|
|
37
|
+
|
|
34
38
|
def __init__(self, delim: str | None, quote: str | None, escchar: str | None) -> None:
|
|
39
|
+
"""Initialise the line-format constants for a given delimiter/quote pair."""
|
|
35
40
|
self.delimiter = delim
|
|
36
41
|
self.joinchar = delim if delim else ""
|
|
37
42
|
self.quotechar = quote
|
|
@@ -44,6 +49,7 @@ class LineDelimiter:
|
|
|
44
49
|
self.quotedquote = None
|
|
45
50
|
|
|
46
51
|
def delimited(self, datarow: Any, add_newline: bool = True) -> str:
|
|
52
|
+
"""Format a sequence of values as a single delimited text line."""
|
|
47
53
|
conf = _state.conf
|
|
48
54
|
if self.quotechar:
|
|
49
55
|
d_row = []
|
|
@@ -79,22 +85,30 @@ class LineDelimiter:
|
|
|
79
85
|
|
|
80
86
|
|
|
81
87
|
class DelimitedWriter:
|
|
88
|
+
"""Low-level writer that formats data rows as delimited text and sends them to an open file object."""
|
|
89
|
+
|
|
82
90
|
def __init__(self, outfile: Any, delim: str | None, quote: str | None, escchar: str | None) -> None:
|
|
91
|
+
"""Wrap an open file object with a :class:`LineDelimiter` for row-by-row writing."""
|
|
83
92
|
self.outfile = outfile
|
|
84
93
|
self.line_delimiter = LineDelimiter(delim, quote, escchar)
|
|
85
94
|
|
|
86
95
|
def write(self, text_str: str) -> None:
|
|
96
|
+
"""Write a raw string directly to the underlying file object."""
|
|
87
97
|
self.outfile.write(text_str)
|
|
88
98
|
|
|
89
99
|
def writerow(self, datarow: Any) -> None:
|
|
100
|
+
"""Format one data row as delimited text and write it to the file."""
|
|
90
101
|
self.outfile.write(self.line_delimiter.delimited(datarow))
|
|
91
102
|
|
|
92
103
|
def writerows(self, datarows: Any) -> None:
|
|
104
|
+
"""Write each row in an iterable of data rows to the file."""
|
|
93
105
|
for row in datarows:
|
|
94
106
|
self.writerow(row)
|
|
95
107
|
|
|
96
108
|
|
|
97
109
|
class CsvWriter:
|
|
110
|
+
"""Opens a named file (or stdout) and exposes a row-oriented delimited-text writing API."""
|
|
111
|
+
|
|
98
112
|
def __init__(
|
|
99
113
|
self,
|
|
100
114
|
filename: str,
|
|
@@ -113,21 +127,32 @@ class CsvWriter:
|
|
|
113
127
|
self.dwriter = DelimitedWriter(self.output, delim, quote, escchar)
|
|
114
128
|
|
|
115
129
|
def write(self, text_str: str) -> None:
|
|
130
|
+
"""Write a raw string to the output file."""
|
|
116
131
|
self.dwriter.write(text_str)
|
|
117
132
|
|
|
118
133
|
def writerow(self, datarow: Any) -> None:
|
|
134
|
+
"""Format and write a single data row."""
|
|
119
135
|
self.dwriter.writerow(datarow)
|
|
120
136
|
|
|
121
137
|
def writerows(self, datarows: Any) -> None:
|
|
138
|
+
"""Format and write each row in an iterable."""
|
|
122
139
|
self.dwriter.writerows(datarows)
|
|
123
140
|
|
|
124
141
|
def close(self) -> None:
|
|
142
|
+
"""Close the underlying output file."""
|
|
125
143
|
self.output.close()
|
|
126
144
|
self.output = None
|
|
127
145
|
|
|
128
146
|
|
|
129
147
|
class CsvFile(EncodedFile):
|
|
148
|
+
"""Full delimited-file reader/writer with automatic format diagnosis.
|
|
149
|
+
|
|
150
|
+
Supports custom delimiters, quoting, encoding, junk-header skipping,
|
|
151
|
+
column-type inference, and ZIP output via :class:`CsvWriter`.
|
|
152
|
+
"""
|
|
153
|
+
|
|
130
154
|
def __init__(self, csvfname: str, file_encoding: str, junk_header_lines: int = 0) -> None:
|
|
155
|
+
"""Open a CSV file path for later reading or writing."""
|
|
131
156
|
super().__init__(csvfname, file_encoding)
|
|
132
157
|
self.csvfname = csvfname
|
|
133
158
|
self.junk_header_lines = junk_header_lines
|
|
@@ -143,6 +168,7 @@ class CsvFile(EncodedFile):
|
|
|
143
168
|
return f"CsvFile({self.csvfname!r}, {self.encoding!r})"
|
|
144
169
|
|
|
145
170
|
def openclean(self, mode: str) -> Any:
|
|
171
|
+
"""Return an opened file object with the configured number of junk header lines skipped."""
|
|
146
172
|
# Returns an opened file object with junk headers stripped.
|
|
147
173
|
f = self.open(mode)
|
|
148
174
|
for _ in range(self.junk_header_lines):
|
|
@@ -150,6 +176,7 @@ class CsvFile(EncodedFile):
|
|
|
150
176
|
return f
|
|
151
177
|
|
|
152
178
|
def lineformat(self, delimiter: str | None, quotechar: str | None, escapechar: str | None) -> None:
|
|
179
|
+
"""Explicitly set the delimiter, quote character, and escape character."""
|
|
153
180
|
# Specifies the format of a line.
|
|
154
181
|
self.delimiter = delimiter
|
|
155
182
|
self.quotechar = quotechar
|
|
@@ -413,6 +440,7 @@ class CsvFile(EncodedFile):
|
|
|
413
440
|
)
|
|
414
441
|
|
|
415
442
|
def evaluate_line_format(self) -> None:
|
|
443
|
+
"""Scan the file to auto-detect the delimiter, quote character, and escape character."""
|
|
416
444
|
# Scans the file to determine the delimiter, quote character, and escapechar.
|
|
417
445
|
if not self.lineformat_set:
|
|
418
446
|
f = self.openclean("rt")
|
|
@@ -426,6 +454,7 @@ class CsvFile(EncodedFile):
|
|
|
426
454
|
self.parse_errors.append(f"{errmsg} in position {pos_no}")
|
|
427
455
|
|
|
428
456
|
def read_and_parse_line(self, f: Any) -> list:
|
|
457
|
+
"""Read and parse one line from an open file, returning a list of field values."""
|
|
429
458
|
# Returns a list of line elements, parsed according to the established delimiter and quotechar.
|
|
430
459
|
elements = []
|
|
431
460
|
eat_multiple_delims = self.delimiter == " "
|
|
@@ -571,6 +600,7 @@ class CsvFile(EncodedFile):
|
|
|
571
600
|
return elements
|
|
572
601
|
|
|
573
602
|
def reader(self) -> Any:
|
|
603
|
+
"""Yield parsed rows from the file as lists of field values."""
|
|
574
604
|
conf = _state.conf
|
|
575
605
|
self.evaluate_line_format()
|
|
576
606
|
f = self.openclean("rt")
|
|
@@ -597,6 +627,7 @@ class CsvFile(EncodedFile):
|
|
|
597
627
|
f.close()
|
|
598
628
|
|
|
599
629
|
def writer(self, append: bool = False) -> CsvWriter:
|
|
630
|
+
"""Return a :class:`CsvWriter` configured with this file's format settings."""
|
|
600
631
|
return CsvWriter(self.filename, self.encoding, self.delimiter, self.quotechar, self.escapechar, append)
|
|
601
632
|
|
|
602
633
|
def _colhdrs(self, inf: Any) -> list[str]:
|
|
@@ -640,17 +671,20 @@ class CsvFile(EncodedFile):
|
|
|
640
671
|
return colnames
|
|
641
672
|
|
|
642
673
|
def column_headers(self) -> list[str]:
|
|
674
|
+
"""Return the first row of the file as a list of column header strings."""
|
|
643
675
|
if not self.lineformat_set:
|
|
644
676
|
self.evaluate_line_format()
|
|
645
677
|
inf = self.reader()
|
|
646
678
|
return self._colhdrs(inf)
|
|
647
679
|
|
|
648
680
|
def data_table_def(self) -> Any:
|
|
681
|
+
"""Return a :class:`DataTable` describing the file's columns and types."""
|
|
649
682
|
if self.table_data is None:
|
|
650
683
|
self.evaluate_column_types()
|
|
651
684
|
return self.table_data
|
|
652
685
|
|
|
653
686
|
def evaluate_column_types(self) -> None:
|
|
687
|
+
"""Scan the file and populate ``table_data`` with inferred column types."""
|
|
654
688
|
if not self.lineformat_set:
|
|
655
689
|
self.evaluate_line_format()
|
|
656
690
|
inf = self.reader()
|
|
@@ -658,6 +692,7 @@ class CsvFile(EncodedFile):
|
|
|
658
692
|
self.table_data = DataTable(colnames, inf)
|
|
659
693
|
|
|
660
694
|
def create_table(self, database_type: Any, schemaname: str | None, tablename: str, pretty: bool = False) -> str:
|
|
695
|
+
"""Generate a CREATE TABLE SQL statement for this file's inferred schema."""
|
|
661
696
|
return self.table_data.create_table(database_type, schemaname, tablename, pretty)
|
|
662
697
|
|
|
663
698
|
|
|
@@ -670,6 +705,7 @@ def write_delimited_file(
|
|
|
670
705
|
append: bool = False,
|
|
671
706
|
zipfile: str | None = None,
|
|
672
707
|
) -> None:
|
|
708
|
+
"""Write a query result set to a CSV, TSV, or other delimited text file."""
|
|
673
709
|
delim = None
|
|
674
710
|
quote = None
|
|
675
711
|
escchar = None
|
execsql/exporters/duckdb.py
CHANGED
|
@@ -15,6 +15,8 @@ from typing import Any
|
|
|
15
15
|
from execsql.exceptions import ErrInfo
|
|
16
16
|
from execsql.types import dbt_duckdb
|
|
17
17
|
|
|
18
|
+
__all__ = ["export_duckdb", "write_query_to_duckdb"]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
def export_duckdb(
|
|
20
22
|
outfile: str,
|
|
@@ -23,6 +25,7 @@ def export_duckdb(
|
|
|
23
25
|
append: bool,
|
|
24
26
|
tablename: str,
|
|
25
27
|
) -> None:
|
|
28
|
+
"""Write pre-fetched rows to a named table in a DuckDB database file."""
|
|
26
29
|
try:
|
|
27
30
|
import duckdb
|
|
28
31
|
except Exception:
|
|
@@ -79,6 +82,7 @@ def write_query_to_duckdb(
|
|
|
79
82
|
append: bool,
|
|
80
83
|
tablename: str,
|
|
81
84
|
) -> None:
|
|
85
|
+
"""Execute a SELECT and write the result set to a named table in a DuckDB database."""
|
|
82
86
|
from execsql.utils.errors import exception_desc
|
|
83
87
|
|
|
84
88
|
try:
|
execsql/exporters/feather.py
CHANGED
|
@@ -18,8 +18,11 @@ from execsql.types import DT_Boolean, DT_Date, DT_Timestamp, DT_TimestampTZ
|
|
|
18
18
|
from execsql.utils.errors import exception_desc
|
|
19
19
|
from execsql.utils.fileio import filewriter_close
|
|
20
20
|
|
|
21
|
+
__all__ = ["write_query_to_feather", "write_query_to_hdf5"]
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
def write_query_to_feather(outfile: str, headers: list[str], rows: Any) -> None:
|
|
25
|
+
"""Write a row source as an Apache Arrow Feather v2 file using polars."""
|
|
23
26
|
try:
|
|
24
27
|
import polars as pl
|
|
25
28
|
except ImportError as e:
|
|
@@ -45,6 +48,7 @@ def write_query_to_hdf5(
|
|
|
45
48
|
append: bool = False,
|
|
46
49
|
desc: str | None = None,
|
|
47
50
|
) -> None:
|
|
51
|
+
"""Execute a SELECT and write the result set to an HDF5 file using the tables library."""
|
|
48
52
|
try:
|
|
49
53
|
import tables
|
|
50
54
|
except ImportError as e:
|
execsql/exporters/html.py
CHANGED
|
@@ -25,6 +25,8 @@ from execsql.script import current_script_line
|
|
|
25
25
|
from execsql.utils.errors import exception_desc
|
|
26
26
|
from execsql.utils.fileio import filewriter_close
|
|
27
27
|
|
|
28
|
+
__all__ = ["export_html", "export_cgi_html", "write_query_to_html", "write_query_to_cgi_html"]
|
|
29
|
+
|
|
28
30
|
|
|
29
31
|
def export_html(
|
|
30
32
|
outfile: str,
|
|
@@ -35,6 +37,7 @@ def export_html(
|
|
|
35
37
|
desc: str | None = None,
|
|
36
38
|
zipfile: str | None = None,
|
|
37
39
|
) -> None:
|
|
40
|
+
"""Write a complete HTML document containing a data table to a file or ZIP archive."""
|
|
38
41
|
conf = _state.conf
|
|
39
42
|
|
|
40
43
|
def write_table(f):
|
|
@@ -164,6 +167,7 @@ def export_cgi_html(
|
|
|
164
167
|
desc: str | None = None,
|
|
165
168
|
zipfile: str | None = None,
|
|
166
169
|
) -> None:
|
|
170
|
+
"""Write a CGI-style HTML fragment (Content-Type header + table) to a file or ZIP archive."""
|
|
167
171
|
conf = _state.conf
|
|
168
172
|
|
|
169
173
|
def write_table(f):
|
|
@@ -223,6 +227,7 @@ def write_query_to_html(
|
|
|
223
227
|
desc: str | None = None,
|
|
224
228
|
zipfile: str | None = None,
|
|
225
229
|
) -> None:
|
|
230
|
+
"""Execute a SELECT and write the result set as a standalone HTML document."""
|
|
226
231
|
try:
|
|
227
232
|
hdrs, rows = db.select_rowsource(select_stmt)
|
|
228
233
|
except ErrInfo:
|
|
@@ -240,6 +245,7 @@ def write_query_to_cgi_html(
|
|
|
240
245
|
desc: str | None = None,
|
|
241
246
|
zipfile: str | None = None,
|
|
242
247
|
) -> None:
|
|
248
|
+
"""Execute a SELECT and write the result set as a CGI-style HTML fragment."""
|
|
243
249
|
try:
|
|
244
250
|
hdrs, rows = db.select_rowsource(select_stmt)
|
|
245
251
|
except ErrInfo:
|
execsql/exporters/json.py
CHANGED
|
@@ -8,6 +8,7 @@ Provides :func:`write_query_to_json` (standard JSON array of objects) and
|
|
|
8
8
|
both of which serialize a query result set to a file or stream.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import json
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
13
14
|
import execsql.state as _state
|
|
@@ -17,6 +18,8 @@ from execsql.models import DataTable
|
|
|
17
18
|
from execsql.utils.errors import exception_desc
|
|
18
19
|
from execsql.utils.fileio import filewriter_close
|
|
19
20
|
|
|
21
|
+
__all__ = ["write_query_to_json", "write_query_to_json_ts"]
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
def write_query_to_json(
|
|
22
25
|
select_stmt: str,
|
|
@@ -26,9 +29,7 @@ def write_query_to_json(
|
|
|
26
29
|
desc: str | None = None,
|
|
27
30
|
zipfile: str | None = None,
|
|
28
31
|
) -> None:
|
|
29
|
-
|
|
30
|
-
import json
|
|
31
|
-
|
|
32
|
+
"""Execute a SELECT and write the result set as a JSON array of objects."""
|
|
32
33
|
conf = _state.conf
|
|
33
34
|
try:
|
|
34
35
|
hdrs, rows = db.select_rowsource(select_stmt)
|
|
@@ -75,9 +76,7 @@ def write_query_to_json_ts(
|
|
|
75
76
|
desc: str | None = None,
|
|
76
77
|
zipfile: str | None = None,
|
|
77
78
|
) -> None:
|
|
78
|
-
|
|
79
|
-
import json
|
|
80
|
-
|
|
79
|
+
"""Execute a SELECT and write the result set as a JSON object with a top-level field-type schema."""
|
|
81
80
|
conf = _state.conf
|
|
82
81
|
try:
|
|
83
82
|
hdrs, rows = db.select_rowsource(select_stmt)
|
execsql/exporters/latex.py
CHANGED
|
@@ -17,6 +17,8 @@ from execsql.exceptions import ErrInfo
|
|
|
17
17
|
from execsql.exporters.zip import WriteableZipfile
|
|
18
18
|
import execsql.state as _state
|
|
19
19
|
|
|
20
|
+
__all__ = ["export_latex", "write_query_to_latex"]
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
def export_latex(
|
|
22
24
|
outfile: str,
|
|
@@ -27,6 +29,7 @@ def export_latex(
|
|
|
27
29
|
desc: str | None = None,
|
|
28
30
|
zipfile: Any | None = None,
|
|
29
31
|
) -> None:
|
|
32
|
+
"""Write pre-fetched rows as a LaTeX tabular environment to a file or ZIP archive."""
|
|
30
33
|
from execsql.utils.fileio import EncodedFile
|
|
31
34
|
|
|
32
35
|
def write_table(f: Any) -> None:
|
|
@@ -120,6 +123,7 @@ def write_query_to_latex(
|
|
|
120
123
|
desc: str | None = None,
|
|
121
124
|
zipfile: Any | None = None,
|
|
122
125
|
) -> None:
|
|
126
|
+
"""Execute a SELECT and write the result set as a LaTeX tabular table."""
|
|
123
127
|
from execsql.utils.errors import exception_desc
|
|
124
128
|
|
|
125
129
|
try:
|
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,6 +357,7 @@ 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:
|
|
@@ -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
|
|
execsql/exporters/parquet.py
CHANGED
|
@@ -13,8 +13,11 @@ 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
23
|
except ImportError as e:
|
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):
|
|
@@ -97,6 +101,7 @@ def prettyprint_query(
|
|
|
97
101
|
desc: str | None = None,
|
|
98
102
|
zipfile: str | None = None,
|
|
99
103
|
) -> None:
|
|
104
|
+
"""Execute a SELECT and write the result set as a column-aligned text table."""
|
|
100
105
|
_state.status.sql_error = False
|
|
101
106
|
names, rows = db.select_data(select_stmt)
|
|
102
107
|
prettyprint_rowset(names, rows, outfile, append, and_val, desc, zipfile=zipfile)
|