execsql2 2.0.1__py3-none-any.whl → 2.1.2__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.py +322 -108
- execsql/config.py +134 -114
- execsql/db/access.py +89 -65
- execsql/db/base.py +97 -68
- execsql/db/dsn.py +45 -29
- execsql/db/duckdb.py +4 -5
- execsql/db/factory.py +27 -27
- execsql/db/firebird.py +30 -18
- execsql/db/mysql.py +38 -14
- execsql/db/oracle.py +58 -33
- execsql/db/postgres.py +68 -28
- execsql/db/sqlite.py +36 -27
- execsql/db/sqlserver.py +45 -30
- execsql/exceptions.py +68 -64
- execsql/exporters/__init__.py +1 -1
- execsql/exporters/base.py +42 -17
- execsql/exporters/delimited.py +60 -59
- execsql/exporters/duckdb.py +8 -12
- execsql/exporters/feather.py +32 -24
- execsql/exporters/html.py +33 -30
- execsql/exporters/json.py +18 -17
- execsql/exporters/latex.py +11 -13
- execsql/exporters/ods.py +50 -46
- execsql/exporters/parquet.py +32 -0
- execsql/exporters/pretty.py +16 -15
- execsql/exporters/raw.py +9 -11
- execsql/exporters/sqlite.py +38 -38
- execsql/exporters/templates.py +15 -72
- execsql/exporters/values.py +13 -12
- execsql/exporters/xls.py +26 -26
- execsql/exporters/xml.py +12 -12
- execsql/exporters/zip.py +0 -3
- execsql/gui/__init__.py +2 -2
- execsql/gui/console.py +0 -1
- execsql/gui/desktop.py +6 -7
- execsql/gui/tui.py +8 -14
- execsql/importers/base.py +6 -9
- execsql/importers/csv.py +10 -17
- execsql/importers/feather.py +16 -22
- execsql/importers/ods.py +3 -4
- execsql/importers/xls.py +5 -6
- execsql/metacommands/__init__.py +8 -8
- execsql/metacommands/conditions.py +41 -33
- execsql/metacommands/connect.py +113 -99
- execsql/metacommands/control.py +38 -26
- execsql/metacommands/data.py +35 -33
- execsql/metacommands/debug.py +13 -9
- execsql/metacommands/io.py +288 -229
- execsql/metacommands/prompt.py +179 -157
- execsql/metacommands/script_ext.py +11 -9
- execsql/metacommands/system.py +44 -25
- execsql/models.py +9 -16
- execsql/parser.py +10 -10
- execsql/script.py +183 -157
- execsql/state.py +170 -208
- execsql/types.py +46 -81
- execsql/utils/auth.py +114 -14
- execsql/utils/crypto.py +31 -4
- execsql/utils/datetime.py +7 -7
- execsql/utils/errors.py +34 -29
- execsql/utils/fileio.py +90 -55
- execsql/utils/gui.py +22 -23
- execsql/utils/mail.py +15 -17
- execsql/utils/numeric.py +2 -3
- execsql/utils/regex.py +9 -12
- execsql/utils/strings.py +10 -12
- execsql/utils/timer.py +0 -2
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
- execsql2-2.1.2.dist-info/METADATA +300 -0
- execsql2-2.1.2.dist-info/RECORD +96 -0
- execsql2-2.0.1.dist-info/METADATA +0 -406
- execsql2-2.0.1.dist-info/RECORD +0 -95
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/NOTICE +0 -0
execsql/exporters/templates.py
CHANGED
|
@@ -5,14 +5,16 @@ Template-based report generation for execsql.
|
|
|
5
5
|
|
|
6
6
|
Provides :class:`StrTemplateReport` (Python :class:`string.Template`
|
|
7
7
|
substitution) and :func:`report_query`, which drives the
|
|
8
|
-
``EXPORT … FORMAT str-template
|
|
9
|
-
|
|
10
|
-
processors are loaded lazily when selected.
|
|
8
|
+
``EXPORT … FORMAT str-template`` and ``FORMAT jinja`` metacommand
|
|
9
|
+
variants. The Jinja2 template processor is loaded lazily when selected.
|
|
11
10
|
"""
|
|
12
11
|
|
|
13
|
-
from typing import Any
|
|
12
|
+
from typing import Any
|
|
14
13
|
|
|
15
14
|
import execsql.state as _state
|
|
15
|
+
from execsql.exceptions import ErrInfo
|
|
16
|
+
from execsql.utils.errors import fatal_error
|
|
17
|
+
from execsql.utils.fileio import filewriter_close
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class StrTemplateReport:
|
|
@@ -39,7 +41,7 @@ class StrTemplateReport:
|
|
|
39
41
|
data_dict_rows: Any,
|
|
40
42
|
output_dest: str,
|
|
41
43
|
append: bool = False,
|
|
42
|
-
zipfile:
|
|
44
|
+
zipfile: str | None = None,
|
|
43
45
|
) -> None:
|
|
44
46
|
conf = _state.conf
|
|
45
47
|
from execsql.utils.fileio import EncodedFile
|
|
@@ -49,7 +51,7 @@ class StrTemplateReport:
|
|
|
49
51
|
ofile = _state.output
|
|
50
52
|
else:
|
|
51
53
|
if zipfile is None:
|
|
52
|
-
|
|
54
|
+
filewriter_close(output_dest)
|
|
53
55
|
if append:
|
|
54
56
|
ofile = EncodedFile(output_dest, conf.output_encoding).open("a")
|
|
55
57
|
else:
|
|
@@ -68,8 +70,8 @@ class JinjaTemplateReport:
|
|
|
68
70
|
global jinja2
|
|
69
71
|
try:
|
|
70
72
|
import jinja2
|
|
71
|
-
except:
|
|
72
|
-
|
|
73
|
+
except ImportError:
|
|
74
|
+
fatal_error(
|
|
73
75
|
"The jinja2 library is required to produce reports with the Jinja2 templating system. See http://jinja.pocoo.org/",
|
|
74
76
|
)
|
|
75
77
|
conf = _state.conf
|
|
@@ -89,7 +91,7 @@ class JinjaTemplateReport:
|
|
|
89
91
|
data_dict_rows: Any,
|
|
90
92
|
output_dest: str,
|
|
91
93
|
append: bool = False,
|
|
92
|
-
zipfile:
|
|
94
|
+
zipfile: str | None = None,
|
|
93
95
|
) -> None:
|
|
94
96
|
conf = _state.conf
|
|
95
97
|
from execsql.utils.fileio import EncodedFile
|
|
@@ -99,7 +101,7 @@ class JinjaTemplateReport:
|
|
|
99
101
|
ofile = _state.output
|
|
100
102
|
else:
|
|
101
103
|
if zipfile is None:
|
|
102
|
-
|
|
104
|
+
filewriter_close(output_dest)
|
|
103
105
|
if append:
|
|
104
106
|
ofile = EncodedFile(output_dest, conf.output_encoding).open("a")
|
|
105
107
|
else:
|
|
@@ -109,66 +111,9 @@ class JinjaTemplateReport:
|
|
|
109
111
|
try:
|
|
110
112
|
ofile.write(self.template.render(headers=headers, datatable=data_dict_rows))
|
|
111
113
|
except jinja2.TemplateSyntaxError as e:
|
|
112
|
-
raise
|
|
114
|
+
raise ErrInfo("error", other_msg=e.message + f" on template line {e.lineno}")
|
|
113
115
|
except jinja2.TemplateError as e:
|
|
114
|
-
raise
|
|
115
|
-
except:
|
|
116
|
-
raise
|
|
117
|
-
if output_dest != "stdout":
|
|
118
|
-
ofile.close()
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
class AirspeedTemplateReport:
|
|
122
|
-
# Exporting/reporting using the Airspeed templating library.
|
|
123
|
-
def __init__(self, template_file: str) -> None:
|
|
124
|
-
global airspeed
|
|
125
|
-
try:
|
|
126
|
-
import airspeed
|
|
127
|
-
except:
|
|
128
|
-
_state.fatal_error(
|
|
129
|
-
"The airspeed library is required to produce reports with the Airspeed templating system. See https://github.com/purcell/airspeed",
|
|
130
|
-
)
|
|
131
|
-
conf = _state.conf
|
|
132
|
-
self.infname = template_file
|
|
133
|
-
from execsql.utils.fileio import EncodedFile
|
|
134
|
-
|
|
135
|
-
inf = EncodedFile(template_file, conf.script_encoding)
|
|
136
|
-
self.template = airspeed.Template(inf.open("r").read())
|
|
137
|
-
|
|
138
|
-
def __repr__(self) -> str:
|
|
139
|
-
return f"StrTemplateReport({self.infname})"
|
|
140
|
-
|
|
141
|
-
def write_report(
|
|
142
|
-
self,
|
|
143
|
-
headers: Any,
|
|
144
|
-
data_dict_rows: Any,
|
|
145
|
-
output_dest: str,
|
|
146
|
-
append: bool = False,
|
|
147
|
-
zipfile: Optional[str] = None,
|
|
148
|
-
) -> None:
|
|
149
|
-
# airspeed requires an entire list to be passed, not just an iterable,
|
|
150
|
-
# so produce a list of dictionaries. This may be too big for memory if
|
|
151
|
-
# the data set is very large.
|
|
152
|
-
conf = _state.conf
|
|
153
|
-
from execsql.utils.fileio import EncodedFile
|
|
154
|
-
from execsql.exporters.zip import ZipWriter
|
|
155
|
-
|
|
156
|
-
data = [d for d in data_dict_rows]
|
|
157
|
-
if output_dest == "stdout":
|
|
158
|
-
ofile = _state.output
|
|
159
|
-
else:
|
|
160
|
-
if zipfile is None:
|
|
161
|
-
_state.filewriter_close(output_dest)
|
|
162
|
-
if append:
|
|
163
|
-
ofile = EncodedFile(output_dest, conf.output_encoding).open("a")
|
|
164
|
-
else:
|
|
165
|
-
ofile = EncodedFile(output_dest, conf.output_encoding).open("w")
|
|
166
|
-
else:
|
|
167
|
-
ofile = ZipWriter(zipfile, output_dest, append)
|
|
168
|
-
try:
|
|
169
|
-
ofile.write(self.template.merge({"headers": headers, "datatable": data}))
|
|
170
|
-
except airspeed.TemplateExecutionError as e:
|
|
171
|
-
raise _state.ErrInfo("error", other_msg=e.msg)
|
|
116
|
+
raise ErrInfo("error", other_msg=f"Jinja2 template error ({e.message})")
|
|
172
117
|
except:
|
|
173
118
|
raise
|
|
174
119
|
if output_dest != "stdout":
|
|
@@ -181,7 +126,7 @@ def report_query(
|
|
|
181
126
|
outfile: str,
|
|
182
127
|
template_file: str,
|
|
183
128
|
append: bool = False,
|
|
184
|
-
zipfile:
|
|
129
|
+
zipfile: str | None = None,
|
|
185
130
|
) -> None:
|
|
186
131
|
# Write (export) a template-based report.
|
|
187
132
|
conf = _state.conf
|
|
@@ -189,8 +134,6 @@ def report_query(
|
|
|
189
134
|
headers, ddict = db.select_rowdict(select_stmt)
|
|
190
135
|
if conf.template_processor == "jinja":
|
|
191
136
|
t = JinjaTemplateReport(template_file)
|
|
192
|
-
elif conf.template_processor == "airspeed":
|
|
193
|
-
t = AirspeedTemplateReport(template_file)
|
|
194
137
|
else:
|
|
195
138
|
t = StrTemplateReport(template_file)
|
|
196
139
|
t.write_report(headers, ddict, outfile, append, zipfile=zipfile)
|
execsql/exporters/values.py
CHANGED
|
@@ -8,28 +8,29 @@ set as a series of SQL ``INSERT INTO … VALUES (…)`` statements, suitable
|
|
|
8
8
|
for loading data into a database from a plain SQL file.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
import
|
|
12
|
-
import os
|
|
13
|
-
from typing import Any, Optional, List
|
|
11
|
+
from typing import Any
|
|
14
12
|
|
|
15
13
|
import execsql.state as _state
|
|
16
14
|
from execsql.exporters.zip import ZipWriter
|
|
15
|
+
from execsql.exceptions import ErrInfo
|
|
16
|
+
from execsql.utils.errors import exception_desc
|
|
17
|
+
from execsql.utils.fileio import filewriter_close
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def export_values(
|
|
20
21
|
outfile: str,
|
|
21
|
-
hdrs:
|
|
22
|
+
hdrs: list[str],
|
|
22
23
|
rows: Any,
|
|
23
24
|
append: bool = False,
|
|
24
|
-
desc:
|
|
25
|
-
zipfile:
|
|
25
|
+
desc: str | None = None,
|
|
26
|
+
zipfile: str | None = None,
|
|
26
27
|
) -> None:
|
|
27
28
|
conf = _state.conf
|
|
28
29
|
if outfile.lower() == "stdout":
|
|
29
30
|
f = _state.output
|
|
30
31
|
else:
|
|
31
32
|
if zipfile is None:
|
|
32
|
-
|
|
33
|
+
filewriter_close(outfile)
|
|
33
34
|
from execsql.utils.fileio import EncodedFile
|
|
34
35
|
|
|
35
36
|
ef = EncodedFile(outfile, conf.output_encoding)
|
|
@@ -64,13 +65,13 @@ def write_query_to_values(
|
|
|
64
65
|
db: Any,
|
|
65
66
|
outfile: str,
|
|
66
67
|
append: bool = False,
|
|
67
|
-
desc:
|
|
68
|
-
zipfile:
|
|
68
|
+
desc: str | None = None,
|
|
69
|
+
zipfile: str | None = None,
|
|
69
70
|
) -> None:
|
|
70
71
|
try:
|
|
71
72
|
hdrs, rows = db.select_rowsource(select_stmt)
|
|
72
|
-
except
|
|
73
|
+
except ErrInfo:
|
|
73
74
|
raise
|
|
74
|
-
except:
|
|
75
|
-
raise
|
|
75
|
+
except Exception:
|
|
76
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
|
|
76
77
|
export_values(outfile, hdrs, rows, append, desc, zipfile=zipfile)
|
execsql/exporters/xls.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
from execsql.types import DT_TimestampTZ
|
|
2
3
|
|
|
3
4
|
"""
|
|
4
5
|
XLS and XLSX spreadsheet export for execsql.
|
|
@@ -9,12 +10,11 @@ EXPORT metacommand. Requires the ``execsql2[excel]`` extras.
|
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
import datetime
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
from typing import Any, Optional, List
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
15
|
|
|
16
|
-
import execsql.state as _state
|
|
17
16
|
from execsql.exceptions import XlsFileError, XlsxFileError
|
|
17
|
+
from execsql.utils.errors import fatal_error
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class XlsFile:
|
|
@@ -32,19 +32,19 @@ class XlsFile:
|
|
|
32
32
|
try:
|
|
33
33
|
global xlrd
|
|
34
34
|
import xlrd
|
|
35
|
-
except:
|
|
36
|
-
|
|
35
|
+
except ImportError:
|
|
36
|
+
fatal_error("The xlrd library is needed to read Excel (.xls) spreadsheets.")
|
|
37
37
|
self.filename = None
|
|
38
38
|
self.encoding = None
|
|
39
39
|
self.wbk = None
|
|
40
40
|
self.datemode = 0
|
|
41
41
|
self.errlog = self.XlsLog()
|
|
42
42
|
|
|
43
|
-
def open(self, filename: str, encoding:
|
|
43
|
+
def open(self, filename: str, encoding: str | None = None, read_only: bool = False) -> None:
|
|
44
44
|
self.filename = filename
|
|
45
45
|
self.encoding = encoding
|
|
46
46
|
self.read_only = read_only
|
|
47
|
-
if
|
|
47
|
+
if Path(filename).is_file():
|
|
48
48
|
# The 'read_only' argument is not used, but is present for compatibility with XlsxFile.open().
|
|
49
49
|
self.wbk = xlrd.open_workbook(filename, logfile=self.errlog, encoding_override=self.encoding)
|
|
50
50
|
self.datemode = self.wbk.datemode
|
|
@@ -64,7 +64,7 @@ class XlsFile:
|
|
|
64
64
|
sheet_no = int(sheetname)
|
|
65
65
|
if sheet_no < 1:
|
|
66
66
|
sheet_no = None
|
|
67
|
-
except:
|
|
67
|
+
except (ValueError, TypeError):
|
|
68
68
|
sheet_no = None
|
|
69
69
|
if sheet_no is None:
|
|
70
70
|
sheet = self.wbk.sheet_by_name(sheetname)
|
|
@@ -73,10 +73,10 @@ class XlsFile:
|
|
|
73
73
|
sheet = self.wbk.sheet_by_index(max(0, sheet_no - 1))
|
|
74
74
|
return sheet
|
|
75
75
|
|
|
76
|
-
def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) ->
|
|
76
|
+
def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) -> list:
|
|
77
77
|
try:
|
|
78
78
|
sheet = self.sheet_named(sheetname)
|
|
79
|
-
except:
|
|
79
|
+
except Exception:
|
|
80
80
|
raise XlsFileError(f"There is no Excel worksheet named {sheetname} in {self.filename}.")
|
|
81
81
|
|
|
82
82
|
# Don't rely on sheet.ncols and sheet.nrows, because Excel will count columns
|
|
@@ -88,7 +88,7 @@ class XlsFile:
|
|
|
88
88
|
if columns:
|
|
89
89
|
d = [cells[c] for c in range(columns)]
|
|
90
90
|
else:
|
|
91
|
-
d =
|
|
91
|
+
d = list(cells)
|
|
92
92
|
datarow = []
|
|
93
93
|
for c in d:
|
|
94
94
|
if c.ctype == 0:
|
|
@@ -97,9 +97,9 @@ class XlsFile:
|
|
|
97
97
|
elif c.ctype == 1:
|
|
98
98
|
# This might be a timestamp with time zone that xlrd treats as a string.
|
|
99
99
|
try:
|
|
100
|
-
dt =
|
|
100
|
+
dt = DT_TimestampTZ()._from_data(c.value)
|
|
101
101
|
datarow.append(dt)
|
|
102
|
-
except:
|
|
102
|
+
except Exception:
|
|
103
103
|
datarow.append(c.value)
|
|
104
104
|
elif c.ctype == 2:
|
|
105
105
|
# float, but maybe should be int
|
|
@@ -159,19 +159,19 @@ class XlsxFile:
|
|
|
159
159
|
try:
|
|
160
160
|
global openpyxl
|
|
161
161
|
import openpyxl
|
|
162
|
-
except:
|
|
163
|
-
|
|
162
|
+
except ImportError:
|
|
163
|
+
fatal_error("The openpyxl library is needed to read Excel (.xlsx) spreadsheets.")
|
|
164
164
|
self.filename = None
|
|
165
165
|
self.encoding = None
|
|
166
166
|
self.wbk = None
|
|
167
167
|
self.read_only = False
|
|
168
168
|
self.errlog = self.XlsxLog()
|
|
169
169
|
|
|
170
|
-
def open(self, filename: str, encoding:
|
|
170
|
+
def open(self, filename: str, encoding: str | None = None, read_only: bool = False) -> None:
|
|
171
171
|
self.filename = filename
|
|
172
172
|
self.encoding = encoding
|
|
173
173
|
self.read_only = read_only
|
|
174
|
-
if
|
|
174
|
+
if Path(filename).is_file():
|
|
175
175
|
if read_only:
|
|
176
176
|
self.wbk = openpyxl.load_workbook(filename, read_only=True)
|
|
177
177
|
else:
|
|
@@ -186,7 +186,7 @@ class XlsxFile:
|
|
|
186
186
|
self.filename = None
|
|
187
187
|
self.encoding = None
|
|
188
188
|
|
|
189
|
-
def sheetnames(self) ->
|
|
189
|
+
def sheetnames(self) -> list[str]:
|
|
190
190
|
return self.wbk.sheetnames
|
|
191
191
|
|
|
192
192
|
def sheet_named(self, sheetname: Any) -> Any:
|
|
@@ -199,7 +199,7 @@ class XlsxFile:
|
|
|
199
199
|
sheet_no = int(sheetname)
|
|
200
200
|
if sheet_no < 1:
|
|
201
201
|
sheet_no = None
|
|
202
|
-
except:
|
|
202
|
+
except (ValueError, TypeError):
|
|
203
203
|
sheet_no = None
|
|
204
204
|
if sheet_no is not None:
|
|
205
205
|
# User-specified sheet numbers should be 1-based
|
|
@@ -208,10 +208,10 @@ class XlsxFile:
|
|
|
208
208
|
sheet = self.wbk[sheetname]
|
|
209
209
|
return sheet
|
|
210
210
|
|
|
211
|
-
def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) ->
|
|
211
|
+
def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) -> list:
|
|
212
212
|
try:
|
|
213
213
|
sheet = self.sheet_named(sheetname)
|
|
214
|
-
except:
|
|
214
|
+
except Exception:
|
|
215
215
|
raise XlsxFileError(f"There is no Excel worksheet named {sheetname} in {self.filename}.")
|
|
216
216
|
# Don't rely on sheet.max_column and sheet.max_row, because Excel will count columns
|
|
217
217
|
# and rows that have ever been filled, even if they are now empty. Base the column count
|
|
@@ -219,14 +219,14 @@ class XlsxFile:
|
|
|
219
219
|
# a row is entirely empty.
|
|
220
220
|
# Get the header row, skipping junk rows
|
|
221
221
|
rowsrc = sheet.iter_rows(max_row=junk_header_rows + 1, values_only=True)
|
|
222
|
-
for
|
|
222
|
+
for _hdr_row in rowsrc:
|
|
223
223
|
pass
|
|
224
224
|
# Get the number of columns
|
|
225
225
|
ncols = 0
|
|
226
|
-
for c in range(len(
|
|
227
|
-
if not
|
|
226
|
+
for c in range(len(_hdr_row)):
|
|
227
|
+
if not _hdr_row[c]:
|
|
228
228
|
break
|
|
229
|
-
ncols += 1
|
|
229
|
+
ncols += 1 # noqa: SIM113
|
|
230
230
|
# Get all the data rows
|
|
231
231
|
sheet_data = []
|
|
232
232
|
rowsrc = sheet.iter_rows(min_row=junk_header_rows + 1, values_only=True)
|
execsql/exporters/xml.py
CHANGED
|
@@ -8,13 +8,13 @@ to a well-formed XML file with one element per row and column values as
|
|
|
8
8
|
child elements or attributes.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
import
|
|
12
|
-
import os
|
|
13
|
-
import re
|
|
14
|
-
from typing import Any, Optional, List
|
|
11
|
+
from typing import Any
|
|
15
12
|
|
|
16
13
|
import execsql.state as _state
|
|
17
14
|
from execsql.exporters.zip import ZipWriter
|
|
15
|
+
from execsql.exceptions import ErrInfo
|
|
16
|
+
from execsql.utils.errors import exception_desc
|
|
17
|
+
from execsql.utils.fileio import filewriter_close
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def write_query_to_xml(
|
|
@@ -23,18 +23,18 @@ def write_query_to_xml(
|
|
|
23
23
|
db: Any,
|
|
24
24
|
outfile: str,
|
|
25
25
|
append: bool = False,
|
|
26
|
-
desc:
|
|
27
|
-
zipfile:
|
|
26
|
+
desc: str | None = None,
|
|
27
|
+
zipfile: str | None = None,
|
|
28
28
|
) -> None:
|
|
29
29
|
conf = _state.conf
|
|
30
30
|
try:
|
|
31
31
|
hdrs, rows = db.select_rowsource(select_stmt)
|
|
32
|
-
except
|
|
32
|
+
except ErrInfo:
|
|
33
33
|
raise
|
|
34
|
-
except:
|
|
35
|
-
raise
|
|
34
|
+
except Exception:
|
|
35
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
|
|
36
36
|
if zipfile is None:
|
|
37
|
-
|
|
37
|
+
filewriter_close(outfile)
|
|
38
38
|
from execsql.utils.fileio import EncodedFile
|
|
39
39
|
|
|
40
40
|
ef = EncodedFile(outfile, conf.output_encoding)
|
|
@@ -50,10 +50,10 @@ def write_query_to_xml(
|
|
|
50
50
|
if desc is not None:
|
|
51
51
|
f.write(f"<!--{desc}-->\n")
|
|
52
52
|
f.write(f"<{tablename}>\n")
|
|
53
|
-
|
|
53
|
+
str_hdrs = [str(h) for h in hdrs]
|
|
54
54
|
for row in rows:
|
|
55
55
|
f.write(" <row>\n")
|
|
56
|
-
for i, col in enumerate(
|
|
56
|
+
for i, col in enumerate(str_hdrs):
|
|
57
57
|
f.write(f" <{col}>{row[i]}</{col}>\n")
|
|
58
58
|
f.write(" </row>\n")
|
|
59
59
|
f.write(f"</{tablename}>\n")
|
execsql/exporters/zip.py
CHANGED
|
@@ -9,12 +9,9 @@ higher-level interface used by the EXPORT metacommand when the output is
|
|
|
9
9
|
directed into a ``.zip`` archive.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
import io
|
|
13
|
-
import os
|
|
14
12
|
import sys
|
|
15
13
|
import time
|
|
16
14
|
import zipfile
|
|
17
|
-
from typing import Optional
|
|
18
15
|
|
|
19
16
|
import execsql.state as _state
|
|
20
17
|
|
execsql/gui/__init__.py
CHANGED
|
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
|
|
|
22
22
|
from execsql.gui.base import GuiBackend
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def get_backend(framework: str = "tkinter") ->
|
|
25
|
+
def get_backend(framework: str = "tkinter") -> GuiBackend:
|
|
26
26
|
"""Return the best available backend for *framework*.
|
|
27
27
|
|
|
28
28
|
Falls back gracefully if the preferred backend's dependencies are missing.
|
|
@@ -50,7 +50,7 @@ def get_backend(framework: str = "tkinter") -> "GuiBackend":
|
|
|
50
50
|
return ConsoleBackend()
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def gui_manager_loop(q:
|
|
53
|
+
def gui_manager_loop(q: queue.Queue[Any], backend: GuiBackend) -> None:
|
|
54
54
|
"""GUI manager thread main loop.
|
|
55
55
|
|
|
56
56
|
Reads ``GuiSpec`` objects from *q*, dispatches each to *backend*, and
|
execsql/gui/console.py
CHANGED
|
@@ -226,7 +226,6 @@ class ConsoleBackend(GuiBackend):
|
|
|
226
226
|
message = args.get("message", "")
|
|
227
227
|
headers = args.get("headers", [])
|
|
228
228
|
rows = args.get("rows", [])
|
|
229
|
-
button_list = args.get("button_list", [("OK", 1), ("Cancel", 0)])
|
|
230
229
|
|
|
231
230
|
print(f"\n=== {title} ===", file=sys.stderr)
|
|
232
231
|
if message:
|
execsql/gui/desktop.py
CHANGED
|
@@ -10,7 +10,6 @@ Note: On macOS, Tkinter requires that dialogs run in the main thread.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
|
-
import sys
|
|
14
13
|
import threading
|
|
15
14
|
import time
|
|
16
15
|
from typing import Any
|
|
@@ -22,7 +21,7 @@ from execsql.gui.base import GuiBackend
|
|
|
22
21
|
# ---------------------------------------------------------------------------
|
|
23
22
|
try:
|
|
24
23
|
import tkinter as tk
|
|
25
|
-
from tkinter import filedialog,
|
|
24
|
+
from tkinter import filedialog, scrolledtext, ttk
|
|
26
25
|
except ImportError as _e:
|
|
27
26
|
raise ImportError(
|
|
28
27
|
"tkinter is not available on this Python installation.",
|
|
@@ -63,7 +62,7 @@ def _populate_treeview(tree: ttk.Treeview, headers: list, rows: list) -> None:
|
|
|
63
62
|
|
|
64
63
|
def _add_buttons(frame: tk.Frame, button_list: list, callback) -> None:
|
|
65
64
|
"""Add buttons from button_list = [(label, value, key?), ...] to frame."""
|
|
66
|
-
for
|
|
65
|
+
for _i, btn in enumerate(button_list):
|
|
67
66
|
label = btn[0]
|
|
68
67
|
value = btn[1]
|
|
69
68
|
b = tk.Button(frame, text=label, command=lambda v=value: callback(v), padx=8)
|
|
@@ -862,7 +861,7 @@ class ConsoleWindow:
|
|
|
862
861
|
if _state.output is not None and _state.output.write_func is self.write:
|
|
863
862
|
_state.output.reset()
|
|
864
863
|
except Exception:
|
|
865
|
-
pass
|
|
864
|
+
pass # Best-effort output reset during console teardown.
|
|
866
865
|
|
|
867
866
|
def write(self, text: str) -> None:
|
|
868
867
|
if self._text and self._text.winfo_exists():
|
|
@@ -925,7 +924,7 @@ class TkinterBackend(GuiBackend):
|
|
|
925
924
|
try:
|
|
926
925
|
self._root.destroy()
|
|
927
926
|
except Exception:
|
|
928
|
-
pass
|
|
927
|
+
pass # Tk root may already be destroyed.
|
|
929
928
|
self._root = None
|
|
930
929
|
|
|
931
930
|
def _root_or_raise(self) -> tk.Tk:
|
|
@@ -1079,7 +1078,7 @@ class _TkinterSyncQueue:
|
|
|
1079
1078
|
|
|
1080
1079
|
import queue as _stdlib_queue
|
|
1081
1080
|
|
|
1082
|
-
def __init__(self, backend:
|
|
1081
|
+
def __init__(self, backend: TkinterBackend) -> None:
|
|
1083
1082
|
self._backend = backend
|
|
1084
1083
|
|
|
1085
1084
|
# GUI types for which a None/cancelled result means the user wants to exit.
|
|
@@ -1115,7 +1114,7 @@ class _TkinterSyncQueue:
|
|
|
1115
1114
|
try:
|
|
1116
1115
|
self._backend._root.update()
|
|
1117
1116
|
except Exception:
|
|
1118
|
-
pass
|
|
1117
|
+
pass # Tk event loop may be torn down.
|
|
1119
1118
|
except Exception as exc:
|
|
1120
1119
|
result = {"error": str(exc), "button": None}
|
|
1121
1120
|
if result is not None and spec.gui_type in self._EXIT_ON_CANCEL and result.get("button") is None:
|
execsql/gui/tui.py
CHANGED
|
@@ -248,8 +248,7 @@ class DisplayScreen(_BaseDialog):
|
|
|
248
248
|
id="text_input",
|
|
249
249
|
)
|
|
250
250
|
with Horizontal(id="buttons"):
|
|
251
|
-
|
|
252
|
-
yield btn
|
|
251
|
+
yield from _button_row(button_list)
|
|
253
252
|
|
|
254
253
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
255
254
|
btn_id = event.button.id
|
|
@@ -410,8 +409,7 @@ class CompareScreen(_BaseDialog):
|
|
|
410
409
|
yield Label("Table 2")
|
|
411
410
|
yield _make_table_widget("table2", headers2, rows2)
|
|
412
411
|
with Horizontal(id="buttons"):
|
|
413
|
-
|
|
414
|
-
yield btn
|
|
412
|
+
yield from _button_row(button_list)
|
|
415
413
|
|
|
416
414
|
def on_mount(self) -> None:
|
|
417
415
|
keylist = [str(k) for k in self.args.get("keylist", [])]
|
|
@@ -515,8 +513,7 @@ class SelectRowsScreen(_BaseDialog):
|
|
|
515
513
|
yield Label("Destination")
|
|
516
514
|
yield _make_table_widget("dest_table", headers2, rows2)
|
|
517
515
|
with Horizontal(id="buttons"):
|
|
518
|
-
|
|
519
|
-
yield btn
|
|
516
|
+
yield from _button_row(button_list)
|
|
520
517
|
|
|
521
518
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
522
519
|
if event.data_table.id == "source_table":
|
|
@@ -564,8 +561,7 @@ class SelectSubScreen(_BaseDialog):
|
|
|
564
561
|
with ScrollableContainer():
|
|
565
562
|
yield _make_table_widget("sel_table", headers, rows)
|
|
566
563
|
with Horizontal(id="buttons"):
|
|
567
|
-
|
|
568
|
-
yield btn
|
|
564
|
+
yield from _button_row(button_list)
|
|
569
565
|
|
|
570
566
|
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
571
567
|
row_key = event.row_key
|
|
@@ -649,8 +645,7 @@ class MapScreen(_BaseDialog):
|
|
|
649
645
|
with ScrollableContainer():
|
|
650
646
|
yield _make_table_widget("map_table", headers, rows)
|
|
651
647
|
with Horizontal(id="buttons"):
|
|
652
|
-
|
|
653
|
-
yield btn
|
|
648
|
+
yield from _button_row(button_list)
|
|
654
649
|
|
|
655
650
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
656
651
|
btn_id = event.button.id
|
|
@@ -862,7 +857,6 @@ def _build_screen_map() -> dict:
|
|
|
862
857
|
GUI_SAVEFILE,
|
|
863
858
|
GUI_SELECTROWS,
|
|
864
859
|
GUI_SELECTSUB,
|
|
865
|
-
QUERY_CONSOLE,
|
|
866
860
|
)
|
|
867
861
|
|
|
868
862
|
return {
|
|
@@ -1125,7 +1119,7 @@ class ConsoleApp(App):
|
|
|
1125
1119
|
try:
|
|
1126
1120
|
self.query_one("#console_log", RichLog).write(text)
|
|
1127
1121
|
except Exception:
|
|
1128
|
-
pass
|
|
1122
|
+
pass # Widget may not be mounted yet.
|
|
1129
1123
|
|
|
1130
1124
|
def set_status(self, msg: str) -> None:
|
|
1131
1125
|
"""Thread-safe status bar update."""
|
|
@@ -1135,7 +1129,7 @@ class ConsoleApp(App):
|
|
|
1135
1129
|
try:
|
|
1136
1130
|
self.query_one("#status_bar", Label).update(msg)
|
|
1137
1131
|
except Exception:
|
|
1138
|
-
pass
|
|
1132
|
+
pass # Widget may not be mounted yet.
|
|
1139
1133
|
|
|
1140
1134
|
def set_progress(self, pct: float) -> None:
|
|
1141
1135
|
"""Thread-safe progress bar update (0–100)."""
|
|
@@ -1145,7 +1139,7 @@ class ConsoleApp(App):
|
|
|
1145
1139
|
try:
|
|
1146
1140
|
self.query_one("#progress_bar", ProgressBar).progress = max(0.0, min(100.0, pct))
|
|
1147
1141
|
except Exception:
|
|
1148
|
-
pass
|
|
1142
|
+
pass # Widget may not be mounted yet.
|
|
1149
1143
|
|
|
1150
1144
|
def action_request_quit(self) -> None:
|
|
1151
1145
|
if self._script_thread and self._script_thread.is_alive():
|
execsql/importers/base.py
CHANGED
|
@@ -10,27 +10,26 @@ importer sub-modules. Given a :class:`~execsql.models.DataTable` and a
|
|
|
10
10
|
information inferred during scanning.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import
|
|
14
|
-
import re
|
|
15
|
-
from typing import Any, List, Optional
|
|
13
|
+
from typing import Any
|
|
16
14
|
|
|
17
15
|
from execsql.exceptions import ErrInfo
|
|
18
16
|
from execsql.db.base import Database
|
|
19
17
|
import execsql.state as _state
|
|
18
|
+
from execsql.types import dbt_firebird
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
def import_data_table(
|
|
23
22
|
db: Database,
|
|
24
|
-
schemaname:
|
|
23
|
+
schemaname: str | None,
|
|
25
24
|
tablename: str,
|
|
26
25
|
is_new: Any,
|
|
27
|
-
hdrs:
|
|
28
|
-
data:
|
|
26
|
+
hdrs: list[str],
|
|
27
|
+
data: list[Any],
|
|
29
28
|
) -> None:
|
|
30
29
|
from execsql.utils.errors import exception_info
|
|
31
30
|
|
|
32
31
|
conf = _state.conf
|
|
33
|
-
if any(
|
|
32
|
+
if any(x is None or len(x.strip()) == 0 for x in hdrs):
|
|
34
33
|
if conf.del_empty_cols:
|
|
35
34
|
blanks = [i for i in range(len(hdrs)) if hdrs[i] is None or len(hdrs[i].strip()) == 0]
|
|
36
35
|
while len(blanks) > 0:
|
|
@@ -72,8 +71,6 @@ def import_data_table(
|
|
|
72
71
|
get_ts.tablespec = None
|
|
73
72
|
|
|
74
73
|
exec_log = _state.exec_log
|
|
75
|
-
dbt_firebird = _state.dbt_firebird
|
|
76
|
-
|
|
77
74
|
if is_new:
|
|
78
75
|
if is_new == 2:
|
|
79
76
|
tblspec = db.schema_qualified_table_name(schemaname, tablename)
|