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.
Files changed (82) hide show
  1. execsql/api.py +21 -4
  2. execsql/cli/run.py +37 -17
  3. execsql/config.py +37 -22
  4. execsql/data/execsql.conf.template +6 -0
  5. execsql/db/access.py +6 -5
  6. execsql/db/base.py +23 -5
  7. execsql/db/dsn.py +2 -0
  8. execsql/db/duckdb.py +1 -1
  9. execsql/db/mysql.py +6 -8
  10. execsql/db/sqlite.py +1 -1
  11. execsql/exceptions.py +14 -11
  12. execsql/exporters/base.py +1 -1
  13. execsql/exporters/delimited.py +22 -16
  14. execsql/exporters/duckdb.py +6 -1
  15. execsql/exporters/feather.py +2 -2
  16. execsql/exporters/json.py +5 -3
  17. execsql/exporters/latex.py +2 -2
  18. execsql/exporters/markdown.py +1 -0
  19. execsql/exporters/ods.py +7 -3
  20. execsql/exporters/parquet.py +1 -1
  21. execsql/exporters/raw.py +8 -8
  22. execsql/exporters/sqlite.py +11 -1
  23. execsql/exporters/templates.py +2 -1
  24. execsql/exporters/xls.py +10 -10
  25. execsql/exporters/xml.py +1 -0
  26. execsql/exporters/yaml.py +1 -0
  27. execsql/exporters/zip.py +9 -7
  28. execsql/format.py +6 -6
  29. execsql/gui/console.py +3 -3
  30. execsql/gui/desktop.py +28 -10
  31. execsql/gui/tui.py +8 -6
  32. execsql/importers/base.py +14 -8
  33. execsql/importers/csv.py +8 -3
  34. execsql/importers/xls.py +1 -0
  35. execsql/metacommands/conditions.py +14 -14
  36. execsql/metacommands/connect.py +2 -2
  37. execsql/metacommands/control.py +1 -1
  38. execsql/metacommands/data.py +2 -1
  39. execsql/metacommands/io_export.py +4 -0
  40. execsql/metacommands/io_fileops.py +85 -39
  41. execsql/metacommands/io_import.py +1 -0
  42. execsql/metacommands/io_write.py +1 -1
  43. execsql/metacommands/prompt.py +24 -22
  44. execsql/metacommands/system.py +14 -1
  45. execsql/models.py +56 -17
  46. execsql/parser.py +8 -8
  47. execsql/plugins.py +1 -1
  48. execsql/script/engine.py +4 -4
  49. execsql/script/executor.py +67 -28
  50. execsql/script/parser.py +13 -12
  51. execsql/script/variables.py +4 -2
  52. execsql/state.pyi +122 -0
  53. execsql/types.py +31 -26
  54. execsql/utils/auth.py +6 -2
  55. execsql/utils/datetime.py +3 -3
  56. execsql/utils/errors.py +7 -5
  57. execsql/utils/fileio.py +77 -31
  58. execsql/utils/gui.py +15 -11
  59. execsql/utils/mail.py +9 -2
  60. execsql/utils/numeric.py +1 -1
  61. execsql/utils/regex.py +9 -9
  62. {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/METADATA +3 -3
  63. execsql2-2.22.0.dist-info/RECORD +123 -0
  64. execsql2-2.21.1.dist-info/RECORD +0 -122
  65. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/README.md +0 -0
  66. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  67. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  68. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  69. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  70. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  71. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  72. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  73. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  74. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  75. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/script_template.sql +0 -0
  76. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  77. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  78. {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  79. {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/WHEEL +0 -0
  80. {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/entry_points.txt +0 -0
  81. {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/licenses/LICENSE.txt +0 -0
  82. {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
- return f"SQL statement: \n {self.cmd.replace(chr(10), chr(10) + ' ')}"
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: object, error_msg: str) -> None:
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.value}}}); output reset to stdout.",
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:
@@ -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 to a DataTable object by 'evaluate_column_types()'
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.get, reverse=True)
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
- elements = [e if e != "" else None for e in elements]
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 elements[b]
653
- yield elements
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}"
@@ -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)
@@ -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(_state.to_json_type[c.dt[1]])
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
  )
@@ -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 WriteableZipfile
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 = WriteableZipfile(zipfile).open(outfile, append)
65
+ f = ZipWriter(zipfile, outfile, append)
66
66
  try:
67
67
  f.write("\\documentclass{article}\n")
68
68
  f.write("\\begin{document}\n")
@@ -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
@@ -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
- of = ZipWriter(zipfile, outfile, append)
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
- of.write(col)
48
+ zip_of.write(col)
49
49
  else:
50
50
  if isinstance(col, str):
51
- of.write(bytes(col, db_encoding))
51
+ zip_of.write(bytes(col, db_encoding))
52
52
  else:
53
- of.write(bytes(str(col), db_encoding))
53
+ zip_of.write(bytes(str(col), db_encoding))
54
54
  finally:
55
- of.close()
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
- of = ZipWriter(zipfile, outfile, append)
68
+ zip_of: Any = ZipWriter(zipfile, outfile, append)
69
69
  try:
70
70
  for row in rowsource:
71
71
  for col in row:
72
- of.write(base64.standard_b64decode(col))
72
+ zip_of.write(base64.standard_b64decode(col))
73
73
  finally:
74
- of.close()
74
+ zip_of.close()
@@ -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)
@@ -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=e.message + f" on template line {e.lineno}") from e
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
@@ -38,6 +38,7 @@ def write_query_to_xml(
38
38
  raise
39
39
  except Exception as e:
40
40
  raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
41
+ f: Any
41
42
  if zipfile is None:
42
43
  filewriter_close(outfile)
43
44
  from execsql.utils.fileio import EncodedFile
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.member = self.zwriter.member_file(member_fname)
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
- sql_indent = ""
386
+ line_indent = ""
387
387
  if cleaned:
388
388
  raw_cleaned = _CMT_MARKER_RE.sub("", fline)
389
- sql_indent = raw_cleaned[: len(raw_cleaned) - len(raw_cleaned.lstrip())]
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(sql_indent + orig_stripped)
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(sql_indent + cleaned)
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
- result = _format_preserving_comments(rebased, sql_indent=indent, leading_comma=leading_comma)
497
- return [target_prefix + line if line.strip() else "" for line in result]
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