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.
Files changed (90) hide show
  1. execsql/cli.py +322 -108
  2. execsql/config.py +134 -114
  3. execsql/db/access.py +89 -65
  4. execsql/db/base.py +97 -68
  5. execsql/db/dsn.py +45 -29
  6. execsql/db/duckdb.py +4 -5
  7. execsql/db/factory.py +27 -27
  8. execsql/db/firebird.py +30 -18
  9. execsql/db/mysql.py +38 -14
  10. execsql/db/oracle.py +58 -33
  11. execsql/db/postgres.py +68 -28
  12. execsql/db/sqlite.py +36 -27
  13. execsql/db/sqlserver.py +45 -30
  14. execsql/exceptions.py +68 -64
  15. execsql/exporters/__init__.py +1 -1
  16. execsql/exporters/base.py +42 -17
  17. execsql/exporters/delimited.py +60 -59
  18. execsql/exporters/duckdb.py +8 -12
  19. execsql/exporters/feather.py +32 -24
  20. execsql/exporters/html.py +33 -30
  21. execsql/exporters/json.py +18 -17
  22. execsql/exporters/latex.py +11 -13
  23. execsql/exporters/ods.py +50 -46
  24. execsql/exporters/parquet.py +32 -0
  25. execsql/exporters/pretty.py +16 -15
  26. execsql/exporters/raw.py +9 -11
  27. execsql/exporters/sqlite.py +38 -38
  28. execsql/exporters/templates.py +15 -72
  29. execsql/exporters/values.py +13 -12
  30. execsql/exporters/xls.py +26 -26
  31. execsql/exporters/xml.py +12 -12
  32. execsql/exporters/zip.py +0 -3
  33. execsql/gui/__init__.py +2 -2
  34. execsql/gui/console.py +0 -1
  35. execsql/gui/desktop.py +6 -7
  36. execsql/gui/tui.py +8 -14
  37. execsql/importers/base.py +6 -9
  38. execsql/importers/csv.py +10 -17
  39. execsql/importers/feather.py +16 -22
  40. execsql/importers/ods.py +3 -4
  41. execsql/importers/xls.py +5 -6
  42. execsql/metacommands/__init__.py +8 -8
  43. execsql/metacommands/conditions.py +41 -33
  44. execsql/metacommands/connect.py +113 -99
  45. execsql/metacommands/control.py +38 -26
  46. execsql/metacommands/data.py +35 -33
  47. execsql/metacommands/debug.py +13 -9
  48. execsql/metacommands/io.py +288 -229
  49. execsql/metacommands/prompt.py +179 -157
  50. execsql/metacommands/script_ext.py +11 -9
  51. execsql/metacommands/system.py +44 -25
  52. execsql/models.py +9 -16
  53. execsql/parser.py +10 -10
  54. execsql/script.py +183 -157
  55. execsql/state.py +170 -208
  56. execsql/types.py +46 -81
  57. execsql/utils/auth.py +114 -14
  58. execsql/utils/crypto.py +31 -4
  59. execsql/utils/datetime.py +7 -7
  60. execsql/utils/errors.py +34 -29
  61. execsql/utils/fileio.py +90 -55
  62. execsql/utils/gui.py +22 -23
  63. execsql/utils/mail.py +15 -17
  64. execsql/utils/numeric.py +2 -3
  65. execsql/utils/regex.py +9 -12
  66. execsql/utils/strings.py +10 -12
  67. execsql/utils/timer.py +0 -2
  68. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
  69. execsql2-2.1.2.dist-info/METADATA +300 -0
  70. execsql2-2.1.2.dist-info/RECORD +96 -0
  71. execsql2-2.0.1.dist-info/METADATA +0 -406
  72. execsql2-2.0.1.dist-info/RECORD +0 -95
  73. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
  74. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  75. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  76. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
  77. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
  78. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
  79. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
  80. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
  81. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  82. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  83. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
  84. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
  85. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  86. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  87. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
  88. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
  89. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
  90. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/NOTICE +0 -0
@@ -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``, ``FORMAT jinja``, and
9
- ``FORMAT airspeed`` metacommand variants. Jinja2 and Airspeed template
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, Optional
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: Optional[str] = None,
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
- _state.filewriter_close(output_dest)
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
- _state.fatal_error(
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: Optional[str] = None,
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
- _state.filewriter_close(output_dest)
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 _state.ErrInfo("error", other_msg=e.message + f" on template line {e.lineno}")
114
+ raise ErrInfo("error", other_msg=e.message + f" on template line {e.lineno}")
113
115
  except jinja2.TemplateError as e:
114
- raise _state.ErrInfo("error", other_msg=f"Jinja2 template error ({e.message})")
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: Optional[str] = None,
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)
@@ -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 io
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: List[str],
22
+ hdrs: list[str],
22
23
  rows: Any,
23
24
  append: bool = False,
24
- desc: Optional[str] = None,
25
- zipfile: Optional[str] = None,
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
- _state.filewriter_close(outfile)
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: Optional[str] = None,
68
- zipfile: Optional[str] = None,
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 _state.ErrInfo:
73
+ except ErrInfo:
73
74
  raise
74
- except:
75
- raise _state.ErrInfo("db", select_stmt, exception_msg=_state.exception_desc())
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 os
13
- import re
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
- _state.fatal_error("The xlrd library is needed to read Excel (.xls) spreadsheets.")
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: Optional[str] = None, read_only: bool = False) -> None:
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 os.path.isfile(filename):
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) -> List:
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 = [cell for cell in cells]
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 = _state.DT_TimestampTZ()._from_data(c.value)
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
- _state.fatal_error("The openpyxl library is needed to read Excel (.xlsx) spreadsheets.")
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: Optional[str] = None, read_only: bool = False) -> None:
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 os.path.isfile(filename):
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) -> List[str]:
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) -> List:
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 hdr_row in rowsrc:
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(hdr_row)):
227
- if not hdr_row[c]:
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 io
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: Optional[str] = None,
27
- zipfile: Optional[str] = None,
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 _state.ErrInfo:
32
+ except ErrInfo:
33
33
  raise
34
- except:
35
- raise _state.ErrInfo("db", select_stmt, exception_msg=_state.exception_desc())
34
+ except Exception:
35
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
36
36
  if zipfile is None:
37
- _state.filewriter_close(outfile)
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
- uhdrs = [str(h) for h in hdrs]
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(hdrs):
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") -> "GuiBackend":
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: "queue.Queue[Any]", backend: "GuiBackend") -> None:
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, font, scrolledtext, ttk
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 i, btn in enumerate(button_list):
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: "TkinterBackend") -> None:
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
- for btn in _button_row(button_list):
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
- for btn in _button_row(button_list):
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
- for btn in _button_row(button_list):
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
- for btn in _button_row(button_list):
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
- for btn in _button_row(button_list):
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 os
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: Optional[str],
23
+ schemaname: str | None,
25
24
  tablename: str,
26
25
  is_new: Any,
27
- hdrs: List[str],
28
- data: List[Any],
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([x is None or len(x.strip()) == 0 for x in hdrs]):
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)