execsql2 2.21.1__py3-none-any.whl → 2.22.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/api.py +21 -4
- execsql/cli/run.py +37 -17
- execsql/config.py +37 -22
- execsql/data/execsql.conf.template +6 -0
- execsql/db/access.py +6 -5
- execsql/db/base.py +23 -5
- execsql/db/dsn.py +2 -0
- execsql/db/duckdb.py +1 -1
- execsql/db/mysql.py +6 -8
- execsql/db/sqlite.py +1 -1
- execsql/exceptions.py +14 -11
- execsql/exporters/base.py +1 -1
- execsql/exporters/delimited.py +22 -16
- execsql/exporters/duckdb.py +6 -1
- execsql/exporters/feather.py +2 -2
- execsql/exporters/json.py +5 -3
- execsql/exporters/latex.py +2 -2
- execsql/exporters/markdown.py +1 -0
- execsql/exporters/ods.py +7 -3
- execsql/exporters/parquet.py +1 -1
- execsql/exporters/raw.py +8 -8
- execsql/exporters/sqlite.py +11 -1
- execsql/exporters/templates.py +2 -1
- execsql/exporters/xls.py +10 -10
- execsql/exporters/xml.py +1 -0
- execsql/exporters/yaml.py +1 -0
- execsql/exporters/zip.py +9 -7
- execsql/format.py +6 -6
- execsql/gui/console.py +3 -3
- execsql/gui/desktop.py +28 -10
- execsql/gui/tui.py +8 -6
- execsql/importers/base.py +14 -8
- execsql/importers/csv.py +8 -3
- execsql/importers/xls.py +1 -0
- execsql/metacommands/conditions.py +14 -14
- execsql/metacommands/connect.py +2 -2
- execsql/metacommands/control.py +1 -1
- execsql/metacommands/data.py +2 -1
- execsql/metacommands/io_export.py +4 -0
- execsql/metacommands/io_fileops.py +85 -39
- execsql/metacommands/io_import.py +1 -0
- execsql/metacommands/io_write.py +1 -1
- execsql/metacommands/prompt.py +24 -22
- execsql/metacommands/system.py +14 -1
- execsql/models.py +56 -17
- execsql/parser.py +8 -8
- execsql/plugins.py +1 -1
- execsql/script/engine.py +4 -4
- execsql/script/executor.py +67 -28
- execsql/script/parser.py +13 -12
- execsql/script/variables.py +4 -2
- execsql/state.pyi +122 -0
- execsql/types.py +31 -26
- execsql/utils/auth.py +6 -2
- execsql/utils/datetime.py +3 -3
- execsql/utils/errors.py +7 -5
- execsql/utils/fileio.py +77 -31
- execsql/utils/gui.py +15 -11
- execsql/utils/mail.py +9 -2
- execsql/utils/numeric.py +1 -1
- execsql/utils/regex.py +9 -9
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/METADATA +3 -3
- execsql2-2.22.0.dist-info/RECORD +123 -0
- execsql2-2.21.1.dist-info/RECORD +0 -122
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/WHEEL +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/licenses/NOTICE +0 -0
execsql/exceptions.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
3
5
|
"""
|
|
4
6
|
Custom exception hierarchy for execsql (internal use).
|
|
5
7
|
|
|
@@ -110,15 +112,15 @@ class ErrInfo(ExecSqlError):
|
|
|
110
112
|
exception_msg: str | None = None,
|
|
111
113
|
other_msg: str | None = None,
|
|
112
114
|
) -> None:
|
|
113
|
-
self.type = type
|
|
114
|
-
self.command = command_text
|
|
115
|
-
self.exception = None if not exception_msg else exception_msg.replace("\n", "\n ")
|
|
116
|
-
self.other = None if not other_msg else other_msg.replace("\n", "\n ")
|
|
117
|
-
self.script_file = None
|
|
118
|
-
self.script_line_no = None
|
|
119
|
-
self.cmd = None
|
|
120
|
-
self.cmdtype = None
|
|
121
|
-
self.error_message = None
|
|
115
|
+
self.type: str = type
|
|
116
|
+
self.command: str | None = command_text
|
|
117
|
+
self.exception: str | None = None if not exception_msg else exception_msg.replace("\n", "\n ")
|
|
118
|
+
self.other: str | None = None if not other_msg else other_msg.replace("\n", "\n ")
|
|
119
|
+
self.script_file: str | None = None
|
|
120
|
+
self.script_line_no: int | None = None
|
|
121
|
+
self.cmd: str | None = None
|
|
122
|
+
self.cmdtype: str | None = None
|
|
123
|
+
self.error_message: str | None = None
|
|
122
124
|
# Pass a concise message to Exception so str(e), e.args, and
|
|
123
125
|
# standard loggers produce useful output.
|
|
124
126
|
super().__init__(self.other or self.exception or self.type)
|
|
@@ -133,7 +135,8 @@ class ErrInfo(ExecSqlError):
|
|
|
133
135
|
if self.cmdtype == "cmd":
|
|
134
136
|
return f"Metacommand: {self.cmd}"
|
|
135
137
|
else:
|
|
136
|
-
|
|
138
|
+
cmd = "" if self.cmd is None else self.cmd
|
|
139
|
+
return f"SQL statement: \n {cmd.replace(chr(10), chr(10) + ' ')}"
|
|
137
140
|
return None
|
|
138
141
|
|
|
139
142
|
def eval_err(self) -> str:
|
|
@@ -199,7 +202,7 @@ class DataTypeError(ExecSqlError):
|
|
|
199
202
|
class DbTypeError(ExecSqlError):
|
|
200
203
|
"""Raised when a DataType has no DBMS-specific mapping for the active database."""
|
|
201
204
|
|
|
202
|
-
def __init__(self, dbms_id: str, data_type:
|
|
205
|
+
def __init__(self, dbms_id: str, data_type: Any, error_msg: str) -> None:
|
|
203
206
|
self.dbms_id = dbms_id
|
|
204
207
|
self.data_type = data_type
|
|
205
208
|
self.error_msg = error_msg or "Unspecified error"
|
execsql/exporters/base.py
CHANGED
|
@@ -166,7 +166,7 @@ class WriteSpec:
|
|
|
166
166
|
except ConsoleUIError as e:
|
|
167
167
|
_state.output.reset()
|
|
168
168
|
_state.exec_log.log_status_info(
|
|
169
|
-
f"Console UI write failed (message {{{e
|
|
169
|
+
f"Console UI write failed (message {{{e}}}); output reset to stdout.",
|
|
170
170
|
)
|
|
171
171
|
_state.output.write(msg)
|
|
172
172
|
if conf.tee_write_log:
|
execsql/exporters/delimited.py
CHANGED
|
@@ -17,7 +17,7 @@ Provides:
|
|
|
17
17
|
import copy
|
|
18
18
|
import re
|
|
19
19
|
import sys
|
|
20
|
-
from typing import Any
|
|
20
|
+
from typing import Any, cast
|
|
21
21
|
|
|
22
22
|
from execsql.utils.fileio import EncodedFile
|
|
23
23
|
from execsql.exporters.zip import ZipWriter
|
|
@@ -42,6 +42,7 @@ class LineDelimiter:
|
|
|
42
42
|
self.joinchar = delim if delim else ""
|
|
43
43
|
self.quotechar = quote
|
|
44
44
|
self.quote_all_text = _state.conf.quote_all_text if _state.conf else False
|
|
45
|
+
self.quotedquote: str | None
|
|
45
46
|
if quote:
|
|
46
47
|
if escchar:
|
|
47
48
|
self.quotedquote = escchar + quote
|
|
@@ -63,6 +64,7 @@ class LineDelimiter:
|
|
|
63
64
|
or ("\n" in e)
|
|
64
65
|
or ("\r" in e)
|
|
65
66
|
):
|
|
67
|
+
assert self.quotedquote is not None
|
|
66
68
|
d_row.append(f"{self.quotechar}{e.replace(self.quotechar, self.quotedquote)}{self.quotechar}")
|
|
67
69
|
else:
|
|
68
70
|
d_row.append(e)
|
|
@@ -159,12 +161,12 @@ class CsvFile(EncodedFile):
|
|
|
159
161
|
self.csvfname = csvfname
|
|
160
162
|
self.junk_header_lines = junk_header_lines
|
|
161
163
|
self.lineformat_set = False # Indicates whether delimiter, quotechar, and escapechar have been set
|
|
162
|
-
self.delimiter = None
|
|
163
|
-
self.quotechar = None
|
|
164
|
-
self.escapechar = None
|
|
165
|
-
self.parse_errors = []
|
|
166
|
-
self.table_data = None # Set
|
|
167
|
-
self.blank_cols = [] # Indexes of blank column headers--columns may be deleted
|
|
164
|
+
self.delimiter: str | None = None
|
|
165
|
+
self.quotechar: str | None = None
|
|
166
|
+
self.escapechar: str | None = None
|
|
167
|
+
self.parse_errors: list[str] = []
|
|
168
|
+
self.table_data: DataTable | None = None # Set by 'evaluate_column_types()'
|
|
169
|
+
self.blank_cols: list[int] = [] # Indexes of blank column headers--columns may be deleted
|
|
168
170
|
|
|
169
171
|
def __repr__(self) -> str:
|
|
170
172
|
return f"CsvFile({self.csvfname!r}, {self.encoding!r})"
|
|
@@ -193,8 +195,8 @@ class CsvFile(EncodedFile):
|
|
|
193
195
|
def __init__(self, line_text: str) -> None:
|
|
194
196
|
"""Store the raw line text and prepare delimiter-count storage."""
|
|
195
197
|
self.text = line_text
|
|
196
|
-
self.delim_counts = {}
|
|
197
|
-
self.item_errors = [] # A list of error messages.
|
|
198
|
+
self.delim_counts: dict[str, int] = {}
|
|
199
|
+
self.item_errors: list[str] = [] # A list of error messages.
|
|
198
200
|
|
|
199
201
|
def __str__(self) -> str:
|
|
200
202
|
return "; ".join(
|
|
@@ -220,14 +222,14 @@ class CsvFile(EncodedFile):
|
|
|
220
222
|
"""Return the previously counted occurrence total for the given delimiter."""
|
|
221
223
|
return self.delim_counts[delim]
|
|
222
224
|
|
|
223
|
-
def _well_quoted(self, element: str, qchar: str):
|
|
225
|
+
def _well_quoted(self, element: str, qchar: str | None):
|
|
224
226
|
# A well-quoted element has either no quotes, a quote on each end and none
|
|
225
227
|
# in the middle, or quotes on both ends and every internal quote is either
|
|
226
228
|
# doubled or escaped.
|
|
227
229
|
# Returns a tuple of three booleans; the first indicates whether the element is
|
|
228
230
|
# well-quoted, the second indicates whether the quote character is used
|
|
229
231
|
# at all, and the third indicates whether the escape character is used.
|
|
230
|
-
if qchar not in element:
|
|
232
|
+
if qchar is None or qchar not in element:
|
|
231
233
|
return (True, False, False)
|
|
232
234
|
if len(element) == 0:
|
|
233
235
|
return (True, False, False)
|
|
@@ -439,7 +441,7 @@ class CsvFile(EncodedFile):
|
|
|
439
441
|
delim_wts = {}
|
|
440
442
|
for d in delim_stats:
|
|
441
443
|
delim_wts[d] = delim_stats[d][0] ** 2 * delim_stats[d][1]
|
|
442
|
-
delim_order = sorted(delim_wts, key=delim_wts
|
|
444
|
+
delim_order = sorted(delim_wts, key=lambda d: delim_wts[d], reverse=True)
|
|
443
445
|
for d in delim_order:
|
|
444
446
|
quote_check = eval_quotes(d)
|
|
445
447
|
if quote_check[0] and quote_check[1]:
|
|
@@ -633,6 +635,7 @@ class CsvFile(EncodedFile):
|
|
|
633
635
|
|
|
634
636
|
f = self.openclean("rt")
|
|
635
637
|
try:
|
|
638
|
+
assert self.delimiter is not None
|
|
636
639
|
csv_reader = csv.reader(
|
|
637
640
|
f,
|
|
638
641
|
delimiter=self.delimiter,
|
|
@@ -644,13 +647,13 @@ class CsvFile(EncodedFile):
|
|
|
644
647
|
if len(elements) == 0:
|
|
645
648
|
break
|
|
646
649
|
# Normalize empty strings to None for parity with the slow reader.
|
|
647
|
-
|
|
650
|
+
normalized_elements = [e if e != "" else None for e in elements]
|
|
648
651
|
if conf.del_empty_cols and len(self.blank_cols) > 0:
|
|
649
652
|
blanks = copy.copy(self.blank_cols)
|
|
650
653
|
while len(blanks) > 0:
|
|
651
654
|
b = blanks.pop()
|
|
652
|
-
del
|
|
653
|
-
yield
|
|
655
|
+
del normalized_elements[b]
|
|
656
|
+
yield normalized_elements
|
|
654
657
|
finally:
|
|
655
658
|
f.close()
|
|
656
659
|
|
|
@@ -721,7 +724,7 @@ class CsvFile(EncodedFile):
|
|
|
721
724
|
colnames = fold_words(colnames, conf.fold_col_hdrs)
|
|
722
725
|
if conf.dedup_col_hdrs:
|
|
723
726
|
colnames = dedup_words(colnames)
|
|
724
|
-
return colnames
|
|
727
|
+
return cast(list[str], colnames)
|
|
725
728
|
|
|
726
729
|
def column_headers(self) -> list[str]:
|
|
727
730
|
"""Return the first row of the file as a list of column header strings."""
|
|
@@ -734,6 +737,7 @@ class CsvFile(EncodedFile):
|
|
|
734
737
|
"""Return a :class:`DataTable` describing the file's columns and types."""
|
|
735
738
|
if self.table_data is None:
|
|
736
739
|
self.evaluate_column_types()
|
|
740
|
+
assert self.table_data is not None
|
|
737
741
|
return self.table_data
|
|
738
742
|
|
|
739
743
|
def evaluate_column_types(self) -> None:
|
|
@@ -746,6 +750,7 @@ class CsvFile(EncodedFile):
|
|
|
746
750
|
|
|
747
751
|
def create_table(self, database_type: Any, schemaname: str | None, tablename: str, pretty: bool = False) -> str:
|
|
748
752
|
"""Generate a CREATE TABLE SQL statement for this file's inferred schema."""
|
|
753
|
+
assert self.table_data is not None
|
|
749
754
|
return self.table_data.create_table(database_type, schemaname, tablename, pretty)
|
|
750
755
|
|
|
751
756
|
|
|
@@ -787,6 +792,7 @@ def write_delimited_file(
|
|
|
787
792
|
quote = ""
|
|
788
793
|
escchar = None
|
|
789
794
|
line_delimiter = LineDelimiter(delim, quote, escchar)
|
|
795
|
+
ofile: Any
|
|
790
796
|
if zipfile is not None:
|
|
791
797
|
ofile = ZipWriter(zipfile, outfile, append)
|
|
792
798
|
fdesc = f"{outfile} in {zipfile}"
|
execsql/exporters/duckdb.py
CHANGED
|
@@ -8,6 +8,8 @@ to a table in a DuckDB database file. Used by ``EXPORT … FORMAT duckdb``.
|
|
|
8
8
|
Requires the ``execsql2[duckdb]`` extra.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import datetime as _datetime
|
|
12
|
+
import decimal as _decimal
|
|
11
13
|
import math
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
from typing import Any
|
|
@@ -15,6 +17,8 @@ from typing import Any
|
|
|
15
17
|
from execsql.exceptions import ErrInfo
|
|
16
18
|
from execsql.types import dbt_duckdb
|
|
17
19
|
|
|
20
|
+
_DUCKDB_SAFE = (int, float, str, bytes, bool, _decimal.Decimal, _datetime.datetime, _datetime.date, _datetime.time)
|
|
21
|
+
|
|
18
22
|
__all__ = ["export_duckdb", "write_query_to_duckdb"]
|
|
19
23
|
|
|
20
24
|
|
|
@@ -59,7 +63,8 @@ def export_duckdb(
|
|
|
59
63
|
curs.close()
|
|
60
64
|
# Construct and run the CREATE TABLE statement
|
|
61
65
|
rowdata = list(rows)
|
|
62
|
-
tablespec = DataTable(hdrs, rowdata)
|
|
66
|
+
tablespec = DataTable(hdrs, rowdata, infer_strings=False)
|
|
67
|
+
rowdata = [tuple(v if (v is None or isinstance(v, _DUCKDB_SAFE)) else str(v) for v in row) for row in rowdata]
|
|
63
68
|
sql = tablespec.create_table(dbt_duckdb, schemaname=None, tablename=tablename)
|
|
64
69
|
curs = ddb.cursor()
|
|
65
70
|
curs.execute(sql)
|
execsql/exporters/feather.py
CHANGED
|
@@ -33,7 +33,7 @@ def write_query_to_feather(outfile: str, headers: list[str], rows: Any) -> None:
|
|
|
33
33
|
) from e
|
|
34
34
|
rows_list = list(rows)
|
|
35
35
|
if rows_list:
|
|
36
|
-
df = pl.DataFrame(rows_list, schema=headers, orient="row")
|
|
36
|
+
df = pl.DataFrame(rows_list, schema=headers, orient="row", infer_schema_length=None)
|
|
37
37
|
else:
|
|
38
38
|
df = pl.DataFrame({h: [] for h in headers})
|
|
39
39
|
filewriter_close(outfile)
|
|
@@ -93,7 +93,7 @@ def write_query_to_hdf5(
|
|
|
93
93
|
raise ErrInfo("error", other_msg=f"Invalid data type for export to HDF5: {repr(datatype)}")
|
|
94
94
|
return t, do_cast
|
|
95
95
|
|
|
96
|
-
tbl_desc = DataTable(hdrs, rows)
|
|
96
|
+
tbl_desc = DataTable(hdrs, rows, infer_strings=False)
|
|
97
97
|
h5type_dict = {}
|
|
98
98
|
cast_flags = []
|
|
99
99
|
for h in hdrs:
|
execsql/exporters/json.py
CHANGED
|
@@ -14,7 +14,7 @@ from typing import Any
|
|
|
14
14
|
import execsql.state as _state
|
|
15
15
|
from execsql.exporters.zip import ZipWriter
|
|
16
16
|
from execsql.exceptions import ErrInfo
|
|
17
|
-
from execsql.models import DataTable
|
|
17
|
+
from execsql.models import DataTable, to_json_type
|
|
18
18
|
from execsql.utils.errors import exception_desc
|
|
19
19
|
from execsql.utils.fileio import filewriter_close
|
|
20
20
|
|
|
@@ -37,6 +37,7 @@ def write_query_to_json(
|
|
|
37
37
|
raise
|
|
38
38
|
except Exception as e:
|
|
39
39
|
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
40
|
+
f: Any
|
|
40
41
|
if zipfile is None:
|
|
41
42
|
filewriter_close(outfile)
|
|
42
43
|
from execsql.utils.fileio import EncodedFile
|
|
@@ -85,6 +86,7 @@ def write_query_to_json_ts(
|
|
|
85
86
|
except Exception as e:
|
|
86
87
|
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
87
88
|
max_col_idx = len(hdrs) - 1
|
|
89
|
+
f: Any
|
|
88
90
|
if zipfile is None:
|
|
89
91
|
filewriter_close(outfile)
|
|
90
92
|
from execsql.utils.fileio import EncodedFile
|
|
@@ -105,7 +107,7 @@ def write_query_to_json_ts(
|
|
|
105
107
|
f.write(' "fields": [\n')
|
|
106
108
|
if write_types:
|
|
107
109
|
# Scan the data to determine data types.
|
|
108
|
-
tbl_desc = DataTable(hdrs, rows)
|
|
110
|
+
tbl_desc = DataTable(hdrs, rows, infer_strings=False)
|
|
109
111
|
# Write the column descriptions to the header.
|
|
110
112
|
# Iterate over hdrs instead of tbl_desc.cols to preserve column order.
|
|
111
113
|
for i, h in enumerate(hdrs):
|
|
@@ -113,7 +115,7 @@ def write_query_to_json_ts(
|
|
|
113
115
|
c = [col for col in tbl_desc.cols if col.name == h][0]
|
|
114
116
|
jname = json.dumps(c.name)
|
|
115
117
|
jtitle = json.dumps(c.name.capitalize().replace("_", " "))
|
|
116
|
-
jtype = json.dumps(
|
|
118
|
+
jtype = json.dumps(to_json_type[c.dt[1]])
|
|
117
119
|
f.write(
|
|
118
120
|
f' {{\n "name": {jname},\n "title": {jtitle},\n "type": {jtype}\n }}{qcomma}\n',
|
|
119
121
|
)
|
execsql/exporters/latex.py
CHANGED
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
from execsql.exceptions import ErrInfo
|
|
17
|
-
from execsql.exporters.zip import
|
|
17
|
+
from execsql.exporters.zip import ZipWriter
|
|
18
18
|
import execsql.state as _state
|
|
19
19
|
|
|
20
20
|
__all__ = ["export_latex", "write_query_to_latex"]
|
|
@@ -62,7 +62,7 @@ def export_latex(
|
|
|
62
62
|
ef = EncodedFile(outfile, conf.output_encoding)
|
|
63
63
|
f = ef.open("wt")
|
|
64
64
|
else:
|
|
65
|
-
f =
|
|
65
|
+
f = ZipWriter(zipfile, outfile, append)
|
|
66
66
|
try:
|
|
67
67
|
f.write("\\documentclass{article}\n")
|
|
68
68
|
f.write("\\begin{document}\n")
|
execsql/exporters/markdown.py
CHANGED
|
@@ -103,6 +103,7 @@ def write_query_to_markdown(
|
|
|
103
103
|
dashes = (f" {'-' * col_widths[i]} " for i in range(len(str_hdrs)))
|
|
104
104
|
return "|" + "|".join(dashes) + "|\n"
|
|
105
105
|
|
|
106
|
+
f: Any
|
|
106
107
|
if zipfile is None:
|
|
107
108
|
filewriter_close(outfile)
|
|
108
109
|
from execsql.utils.fileio import EncodedFile
|
execsql/exporters/ods.py
CHANGED
|
@@ -25,6 +25,8 @@ from execsql.utils.strings import unquoted
|
|
|
25
25
|
|
|
26
26
|
__all__ = ["OdsFile", "export_ods", "write_query_to_ods", "write_queries_to_ods"]
|
|
27
27
|
|
|
28
|
+
of: Any = None
|
|
29
|
+
|
|
28
30
|
|
|
29
31
|
class OdsFile:
|
|
30
32
|
"""Wrapper around the ``odfpy`` library for reading and writing OpenDocument Spreadsheet files."""
|
|
@@ -55,9 +57,9 @@ class OdsFile:
|
|
|
55
57
|
import odf.style # noqa: F401
|
|
56
58
|
except ImportError:
|
|
57
59
|
fatal_error("The odfpy library is needed to create OpenDocument spreadsheets.")
|
|
58
|
-
self.filename = None
|
|
59
|
-
self.wbk = None
|
|
60
|
-
self.cell_style_names = []
|
|
60
|
+
self.filename: str | None = None
|
|
61
|
+
self.wbk: Any = None
|
|
62
|
+
self.cell_style_names: list[str] = []
|
|
61
63
|
|
|
62
64
|
def open(self, filename: str) -> None:
|
|
63
65
|
"""Open an existing ODS file or create a new one at the given path."""
|
|
@@ -278,6 +280,8 @@ class OdsFile:
|
|
|
278
280
|
|
|
279
281
|
def save_close(self) -> None:
|
|
280
282
|
"""Serialise the workbook to disk and release all resources."""
|
|
283
|
+
if self.filename is None:
|
|
284
|
+
raise OdsFileError("Cannot save an ODS file before it has been opened.")
|
|
281
285
|
with open(self.filename, "wb") as ofile:
|
|
282
286
|
self.wbk.write(ofile)
|
|
283
287
|
self.filename = None
|
execsql/exporters/parquet.py
CHANGED
|
@@ -28,7 +28,7 @@ def write_query_to_parquet(outfile: str, headers: list[str], rows: Any) -> None:
|
|
|
28
28
|
) from e
|
|
29
29
|
rows_list = list(rows)
|
|
30
30
|
if rows_list:
|
|
31
|
-
df = pl.DataFrame(rows_list, schema=headers, orient="row")
|
|
31
|
+
df = pl.DataFrame(rows_list, schema=headers, orient="row", infer_schema_length=None)
|
|
32
32
|
else:
|
|
33
33
|
df = pl.DataFrame({h: [] for h in headers})
|
|
34
34
|
filewriter_close(outfile)
|
execsql/exporters/raw.py
CHANGED
|
@@ -40,19 +40,19 @@ def write_query_raw(
|
|
|
40
40
|
else:
|
|
41
41
|
of.write(bytes(str(col), db_encoding))
|
|
42
42
|
else:
|
|
43
|
-
|
|
43
|
+
zip_of: Any = ZipWriter(zipfile, outfile, append)
|
|
44
44
|
try:
|
|
45
45
|
for row in rowsource:
|
|
46
46
|
for col in row:
|
|
47
47
|
if isinstance(col, bytearray):
|
|
48
|
-
|
|
48
|
+
zip_of.write(col)
|
|
49
49
|
else:
|
|
50
50
|
if isinstance(col, str):
|
|
51
|
-
|
|
51
|
+
zip_of.write(bytes(col, db_encoding))
|
|
52
52
|
else:
|
|
53
|
-
|
|
53
|
+
zip_of.write(bytes(str(col), db_encoding))
|
|
54
54
|
finally:
|
|
55
|
-
|
|
55
|
+
zip_of.close()
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def write_query_b64(outfile: str, rowsource: Any, append: bool = False, zipfile: str | None = None) -> None:
|
|
@@ -65,10 +65,10 @@ def write_query_b64(outfile: str, rowsource: Any, append: bool = False, zipfile:
|
|
|
65
65
|
for col in row:
|
|
66
66
|
of.write(base64.standard_b64decode(col))
|
|
67
67
|
else:
|
|
68
|
-
|
|
68
|
+
zip_of: Any = ZipWriter(zipfile, outfile, append)
|
|
69
69
|
try:
|
|
70
70
|
for row in rowsource:
|
|
71
71
|
for col in row:
|
|
72
|
-
|
|
72
|
+
zip_of.write(base64.standard_b64decode(col))
|
|
73
73
|
finally:
|
|
74
|
-
|
|
74
|
+
zip_of.close()
|
execsql/exporters/sqlite.py
CHANGED
|
@@ -8,12 +8,15 @@ to a table in an SQLite database file. Used by ``EXPORT … FORMAT sqlite``.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import math
|
|
11
|
+
from decimal import Decimal
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Any
|
|
13
14
|
|
|
14
15
|
from execsql.exceptions import ErrInfo
|
|
15
16
|
from execsql.types import dbt_sqlite
|
|
16
17
|
|
|
18
|
+
_SQLITE_SAFE = (int, float, str, bytes)
|
|
19
|
+
|
|
17
20
|
__all__ = ["export_sqlite", "write_query_to_sqlite"]
|
|
18
21
|
|
|
19
22
|
|
|
@@ -54,7 +57,14 @@ def export_sqlite(
|
|
|
54
57
|
curs.close()
|
|
55
58
|
# Construct and run the CREATE TABLE statement
|
|
56
59
|
rowdata = list(rows)
|
|
57
|
-
tablespec = DataTable(hdrs, rowdata)
|
|
60
|
+
tablespec = DataTable(hdrs, rowdata, infer_strings=False)
|
|
61
|
+
rowdata = [
|
|
62
|
+
tuple(
|
|
63
|
+
str(v) if isinstance(v, Decimal) else v if (v is None or isinstance(v, _SQLITE_SAFE)) else str(v)
|
|
64
|
+
for v in row
|
|
65
|
+
)
|
|
66
|
+
for row in rowdata
|
|
67
|
+
]
|
|
58
68
|
sql = tablespec.create_table(dbt_sqlite, schemaname=None, tablename=tablename)
|
|
59
69
|
curs = sdb.cursor()
|
|
60
70
|
curs.execute(sql)
|
execsql/exporters/templates.py
CHANGED
|
@@ -139,7 +139,7 @@ class JinjaTemplateReport:
|
|
|
139
139
|
try:
|
|
140
140
|
ofile.write(self.template.render(headers=headers, datatable=data_dict_rows))
|
|
141
141
|
except self._jinja2.TemplateSyntaxError as e:
|
|
142
|
-
raise ErrInfo("error", other_msg=
|
|
142
|
+
raise ErrInfo("error", other_msg=f"{e} on template line {e.lineno}") from e
|
|
143
143
|
except self._jinja2.TemplateError as e:
|
|
144
144
|
raise ErrInfo("error", other_msg=f"Jinja2 template error ({e.message})") from e
|
|
145
145
|
finally:
|
|
@@ -160,6 +160,7 @@ def report_query(
|
|
|
160
160
|
conf = _state.conf
|
|
161
161
|
_state.status.sql_error = False
|
|
162
162
|
headers, ddict = db.select_rowdict(select_stmt)
|
|
163
|
+
t: Any
|
|
163
164
|
if conf.template_processor == "jinja":
|
|
164
165
|
t = JinjaTemplateReport(template_file)
|
|
165
166
|
else:
|
execsql/exporters/xls.py
CHANGED
|
@@ -19,7 +19,7 @@ Both back-ends are optional dependencies; install via
|
|
|
19
19
|
|
|
20
20
|
import datetime
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
from typing import Any
|
|
22
|
+
from typing import Any, cast
|
|
23
23
|
|
|
24
24
|
from execsql.exceptions import XlsFileError, XlsxFileError
|
|
25
25
|
from execsql.utils.errors import fatal_error
|
|
@@ -38,7 +38,7 @@ class XlsFile:
|
|
|
38
38
|
|
|
39
39
|
def __init__(self) -> None:
|
|
40
40
|
"""Initialise an empty message list."""
|
|
41
|
-
self.log_msgs = []
|
|
41
|
+
self.log_msgs: list[str] = []
|
|
42
42
|
|
|
43
43
|
def write(self, msg: str) -> None:
|
|
44
44
|
"""Append a log message to the internal list."""
|
|
@@ -52,9 +52,9 @@ class XlsFile:
|
|
|
52
52
|
self._xlrd = xlrd
|
|
53
53
|
except ImportError:
|
|
54
54
|
fatal_error("The xlrd library is needed to read Excel (.xls) spreadsheets.")
|
|
55
|
-
self.filename = None
|
|
56
|
-
self.encoding = None
|
|
57
|
-
self.wbk = None
|
|
55
|
+
self.filename: str | None = None
|
|
56
|
+
self.encoding: str | None = None
|
|
57
|
+
self.wbk: Any = None
|
|
58
58
|
self.datemode = 0
|
|
59
59
|
self.errlog = self.XlsLog()
|
|
60
60
|
|
|
@@ -177,7 +177,7 @@ class XlsxFile:
|
|
|
177
177
|
|
|
178
178
|
def __init__(self) -> None:
|
|
179
179
|
"""Initialise an empty message list."""
|
|
180
|
-
self.log_msgs = []
|
|
180
|
+
self.log_msgs: list[str] = []
|
|
181
181
|
|
|
182
182
|
def write(self, msg: str) -> None:
|
|
183
183
|
"""Append a log message to the internal list."""
|
|
@@ -191,9 +191,9 @@ class XlsxFile:
|
|
|
191
191
|
self._openpyxl = openpyxl
|
|
192
192
|
except ImportError:
|
|
193
193
|
fatal_error("The openpyxl library is needed to read Excel (.xlsx) spreadsheets.")
|
|
194
|
-
self.filename = None
|
|
195
|
-
self.encoding = None
|
|
196
|
-
self.wbk = None
|
|
194
|
+
self.filename: str | None = None
|
|
195
|
+
self.encoding: str | None = None
|
|
196
|
+
self.wbk: Any = None
|
|
197
197
|
self.read_only = False
|
|
198
198
|
self.errlog = self.XlsxLog()
|
|
199
199
|
|
|
@@ -224,7 +224,7 @@ class XlsxFile:
|
|
|
224
224
|
|
|
225
225
|
def sheetnames(self) -> list[str]:
|
|
226
226
|
"""Return the list of worksheet names in the open workbook."""
|
|
227
|
-
return self.wbk.sheetnames
|
|
227
|
+
return cast(list[str], self.wbk.sheetnames)
|
|
228
228
|
|
|
229
229
|
def sheet_named(self, sheetname: Any) -> Any:
|
|
230
230
|
"""Return the sheet matching a name or 1-based integer index."""
|
execsql/exporters/xml.py
CHANGED
execsql/exporters/yaml.py
CHANGED
|
@@ -72,6 +72,7 @@ def write_query_to_yaml(
|
|
|
72
72
|
data = [dict(zip(uhdrs, row)) for row in rows]
|
|
73
73
|
yaml_text = yaml.dump(data, default_flow_style=False, allow_unicode=True)
|
|
74
74
|
|
|
75
|
+
f: Any
|
|
75
76
|
if zipfile is None:
|
|
76
77
|
filewriter_close(outfile)
|
|
77
78
|
from execsql.utils.fileio import EncodedFile
|
execsql/exporters/zip.py
CHANGED
|
@@ -12,6 +12,7 @@ directed into a ``.zip`` archive.
|
|
|
12
12
|
import sys
|
|
13
13
|
import time
|
|
14
14
|
import zipfile
|
|
15
|
+
from typing import IO, Any, Literal, cast
|
|
15
16
|
|
|
16
17
|
import execsql.state as _state
|
|
17
18
|
|
|
@@ -28,9 +29,9 @@ class WriteableZipfile:
|
|
|
28
29
|
self.buf = memoryview(bytearray(self.bufsize))
|
|
29
30
|
self.buflen = 0 # Length of buffer contents.
|
|
30
31
|
comp = zipfile.ZIP_BZIP2
|
|
31
|
-
zmode = "w" if not append else "a"
|
|
32
|
+
zmode: Literal["w", "a"] = "w" if not append else "a"
|
|
32
33
|
self.zf = zipfile.ZipFile(zipfile_name, mode=zmode, compression=comp, compresslevel=9)
|
|
33
|
-
self.current_handle = None
|
|
34
|
+
self.current_handle: IO[bytes] | None = None
|
|
34
35
|
|
|
35
36
|
def __enter__(self) -> WriteableZipfile:
|
|
36
37
|
return self
|
|
@@ -54,7 +55,7 @@ class WriteableZipfile:
|
|
|
54
55
|
)
|
|
55
56
|
self.current_zinfo.compress_type = self.zf.compression
|
|
56
57
|
if sys.version_info.major >= 3 and sys.version_info.minor >= 7:
|
|
57
|
-
self.current_zinfo._compresslevel = self.zf.compresslevel
|
|
58
|
+
self.current_zinfo._compresslevel = self.zf.compresslevel # type: ignore[attr-defined]
|
|
58
59
|
# See https://stackoverflow.com/questions/434641/how-do-i-set-permissions-attributes-on-a-file-in-a-zip-file-using-pythons-zip
|
|
59
60
|
self.current_zinfo.external_attr = 0o100755 << 16 # ?rw-rw-rw-
|
|
60
61
|
if sys.platform.startswith("win"):
|
|
@@ -62,13 +63,13 @@ class WriteableZipfile:
|
|
|
62
63
|
else:
|
|
63
64
|
self.current_zinfo.create_system = 3
|
|
64
65
|
self.current_zinfo.file_size = 0
|
|
65
|
-
self.current_handle = self.zf.open(self.current_zinfo, mode="w")
|
|
66
|
+
self.current_handle = cast(IO[bytes], self.zf.open(self.current_zinfo, mode="w"))
|
|
66
67
|
|
|
67
68
|
def zip_buffer(self) -> None:
|
|
68
69
|
"""Flush any buffered bytes to the currently open zip member file."""
|
|
69
70
|
# Writes the buffer contents, if any, to the zip member file.
|
|
70
71
|
if self.buflen > 0 and self.current_handle is not None:
|
|
71
|
-
with self.zf._lock:
|
|
72
|
+
with cast(Any, self.zf)._lock:
|
|
72
73
|
self.current_zinfo.file_size = self.current_zinfo.file_size + self.buflen
|
|
73
74
|
self.current_handle.write(self.buf[0 : self.buflen])
|
|
74
75
|
self.buflen = 0
|
|
@@ -104,8 +105,8 @@ class ZipWriter:
|
|
|
104
105
|
"""Open the archive at ``zip_fname`` and begin a new member file named ``member_fname``."""
|
|
105
106
|
self.zip_fname = zip_fname
|
|
106
107
|
self.member_fname = member_fname
|
|
107
|
-
self.zwriter = WriteableZipfile(self.zip_fname, append)
|
|
108
|
-
self.
|
|
108
|
+
self.zwriter: WriteableZipfile | None = WriteableZipfile(self.zip_fname, append)
|
|
109
|
+
self.zwriter.member_file(member_fname)
|
|
109
110
|
|
|
110
111
|
def __enter__(self) -> ZipWriter:
|
|
111
112
|
return self
|
|
@@ -116,6 +117,7 @@ class ZipWriter:
|
|
|
116
117
|
|
|
117
118
|
def write(self, str_data: str) -> None:
|
|
118
119
|
"""Write a string to the current zip member."""
|
|
120
|
+
assert self.zwriter is not None
|
|
119
121
|
self.zwriter.write(str_data)
|
|
120
122
|
|
|
121
123
|
def close(self) -> None:
|
execsql/format.py
CHANGED
|
@@ -383,22 +383,22 @@ def _format_preserving_comments(
|
|
|
383
383
|
# Strip markers to get the underlying SQL line and its indent
|
|
384
384
|
cleaned = _CMT_MARKER_RE.sub("", fline).strip()
|
|
385
385
|
# Determine indent: use the SQL line's indent from sqlglot
|
|
386
|
-
|
|
386
|
+
line_indent = ""
|
|
387
387
|
if cleaned:
|
|
388
388
|
raw_cleaned = _CMT_MARKER_RE.sub("", fline)
|
|
389
|
-
|
|
389
|
+
line_indent = raw_cleaned[: len(raw_cleaned) - len(raw_cleaned.lstrip())]
|
|
390
390
|
for m in markers_here:
|
|
391
391
|
if m in comment_store:
|
|
392
392
|
orig = comment_store[m]
|
|
393
393
|
# Re-indent the comment to match the SQL line it precedes
|
|
394
394
|
orig_stripped = orig.strip()
|
|
395
395
|
if orig_stripped:
|
|
396
|
-
result.append(
|
|
396
|
+
result.append(line_indent + orig_stripped)
|
|
397
397
|
else:
|
|
398
398
|
result.append("")
|
|
399
399
|
found_markers.add(m)
|
|
400
400
|
if cleaned:
|
|
401
|
-
result.append(
|
|
401
|
+
result.append(line_indent + cleaned)
|
|
402
402
|
else:
|
|
403
403
|
result.append(fline)
|
|
404
404
|
|
|
@@ -493,8 +493,8 @@ def format_sql_block(
|
|
|
493
493
|
# which preserves both comments AND sqlglot formatting. When all
|
|
494
494
|
# comments are between statements, the simpler segmentation works.
|
|
495
495
|
if _has_mid_statement_comments(rebased):
|
|
496
|
-
|
|
497
|
-
return [target_prefix + line if line.strip() else "" for line in
|
|
496
|
+
formatted_lines = _format_preserving_comments(rebased, sql_indent=indent, leading_comma=leading_comma)
|
|
497
|
+
return [target_prefix + line if line.strip() else "" for line in formatted_lines]
|
|
498
498
|
|
|
499
499
|
result: list[str] = []
|
|
500
500
|
seg: list[str] = []
|
execsql/gui/console.py
CHANGED
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
11
|
import time
|
|
12
|
-
from typing import Any
|
|
12
|
+
from typing import Any, cast
|
|
13
13
|
|
|
14
14
|
from execsql.gui.base import DIFF_MARKER, GuiBackend, compare_stats as _compare_stats, compute_row_diffs
|
|
15
15
|
|
|
@@ -88,9 +88,9 @@ def _prompt_buttons(button_list: list) -> int | None:
|
|
|
88
88
|
while True:
|
|
89
89
|
raw = input("Choice: ").strip()
|
|
90
90
|
if raw.lower() in choices:
|
|
91
|
-
return choices[raw.lower()]
|
|
91
|
+
return cast(int | None, choices[raw.lower()])
|
|
92
92
|
if raw in choices:
|
|
93
|
-
return choices[raw]
|
|
93
|
+
return cast(int | None, choices[raw])
|
|
94
94
|
print("Invalid choice, please try again.", file=sys.stderr)
|
|
95
95
|
|
|
96
96
|
|