execsql2 2.1.2__py3-none-any.whl → 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. execsql/cli/__init__.py +436 -0
  2. execsql/cli/dsn.py +86 -0
  3. execsql/cli/help.py +140 -0
  4. execsql/{cli.py → cli/run.py} +14 -589
  5. execsql/config.py +65 -1
  6. execsql/db/access.py +27 -15
  7. execsql/db/base.py +328 -215
  8. execsql/db/dsn.py +10 -5
  9. execsql/db/duckdb.py +6 -2
  10. execsql/db/factory.py +21 -0
  11. execsql/db/firebird.py +27 -19
  12. execsql/db/mysql.py +12 -7
  13. execsql/db/oracle.py +15 -11
  14. execsql/db/postgres.py +31 -16
  15. execsql/db/sqlite.py +15 -11
  16. execsql/db/sqlserver.py +16 -5
  17. execsql/exceptions.py +25 -7
  18. execsql/exporters/base.py +12 -1
  19. execsql/exporters/delimited.py +80 -35
  20. execsql/exporters/duckdb.py +6 -2
  21. execsql/exporters/feather.py +10 -6
  22. execsql/exporters/html.py +89 -69
  23. execsql/exporters/json.py +52 -45
  24. execsql/exporters/latex.py +37 -27
  25. execsql/exporters/ods.py +32 -11
  26. execsql/exporters/parquet.py +5 -2
  27. execsql/exporters/pretty.py +16 -9
  28. execsql/exporters/raw.py +22 -16
  29. execsql/exporters/sqlite.py +6 -2
  30. execsql/exporters/templates.py +39 -21
  31. execsql/exporters/values.py +26 -20
  32. execsql/exporters/xls.py +30 -11
  33. execsql/exporters/xml.py +31 -13
  34. execsql/exporters/zip.py +15 -0
  35. execsql/importers/base.py +6 -4
  36. execsql/importers/csv.py +8 -6
  37. execsql/importers/feather.py +6 -4
  38. execsql/importers/ods.py +6 -4
  39. execsql/importers/xls.py +6 -4
  40. execsql/metacommands/__init__.py +208 -1548
  41. execsql/metacommands/conditions.py +101 -27
  42. execsql/metacommands/control.py +8 -4
  43. execsql/metacommands/data.py +6 -6
  44. execsql/metacommands/debug.py +6 -2
  45. execsql/metacommands/dispatch.py +2011 -0
  46. execsql/metacommands/io.py +67 -1310
  47. execsql/metacommands/io_export.py +442 -0
  48. execsql/metacommands/io_fileops.py +287 -0
  49. execsql/metacommands/io_import.py +398 -0
  50. execsql/metacommands/io_write.py +248 -0
  51. execsql/metacommands/prompt.py +22 -66
  52. execsql/metacommands/system.py +7 -2
  53. execsql/models.py +7 -0
  54. execsql/parser.py +10 -0
  55. execsql/py.typed +0 -0
  56. execsql/script/__init__.py +95 -0
  57. execsql/script/control.py +162 -0
  58. execsql/{script.py → script/engine.py} +184 -402
  59. execsql/script/variables.py +281 -0
  60. execsql/types.py +49 -20
  61. execsql/utils/auth.py +2 -0
  62. execsql/utils/crypto.py +4 -6
  63. execsql/utils/datetime.py +1 -0
  64. execsql/utils/errors.py +11 -0
  65. execsql/utils/fileio.py +33 -8
  66. execsql/utils/gui.py +46 -0
  67. execsql/utils/mail.py +7 -17
  68. execsql/utils/numeric.py +2 -0
  69. execsql/utils/regex.py +9 -0
  70. execsql/utils/strings.py +16 -0
  71. execsql/utils/timer.py +2 -0
  72. execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
  73. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
  74. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/METADATA +13 -6
  75. execsql2-2.4.0.dist-info/RECORD +108 -0
  76. execsql2-2.1.2.data/data/execsql2_extras/READ_ME.rst +0 -127
  77. execsql2-2.1.2.dist-info/RECORD +0 -96
  78. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  79. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  80. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  81. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  82. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  83. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  84. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  85. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  86. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  87. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
  88. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  89. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  90. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  91. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
  92. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
  93. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
  94. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
execsql/exporters/xls.py CHANGED
@@ -16,8 +16,12 @@ from typing import Any
16
16
  from execsql.exceptions import XlsFileError, XlsxFileError
17
17
  from execsql.utils.errors import fatal_error
18
18
 
19
+ __all__ = ["XlsFile", "XlsxFile"]
20
+
19
21
 
20
22
  class XlsFile:
23
+ """Read-only wrapper around ``xlrd`` for importing legacy ``.xls`` spreadsheets."""
24
+
21
25
  def __repr__(self) -> str:
22
26
  return "XlsFile()"
23
27
 
@@ -29,9 +33,11 @@ class XlsFile:
29
33
  self.log_msgs.append(msg)
30
34
 
31
35
  def __init__(self) -> None:
36
+ """Import xlrd and initialise file state; raises a fatal error if xlrd is absent."""
32
37
  try:
33
- global xlrd
34
38
  import xlrd
39
+
40
+ self._xlrd = xlrd
35
41
  except ImportError:
36
42
  fatal_error("The xlrd library is needed to read Excel (.xls) spreadsheets.")
37
43
  self.filename = None
@@ -41,20 +47,23 @@ class XlsFile:
41
47
  self.errlog = self.XlsLog()
42
48
 
43
49
  def open(self, filename: str, encoding: str | None = None, read_only: bool = False) -> None:
50
+ """Open an existing ``.xls`` file for reading; raises XlsFileError if absent."""
44
51
  self.filename = filename
45
52
  self.encoding = encoding
46
53
  self.read_only = read_only
47
54
  if Path(filename).is_file():
48
55
  # The 'read_only' argument is not used, but is present for compatibility with XlsxFile.open().
49
- self.wbk = xlrd.open_workbook(filename, logfile=self.errlog, encoding_override=self.encoding)
56
+ self.wbk = self._xlrd.open_workbook(filename, logfile=self.errlog, encoding_override=self.encoding)
50
57
  self.datemode = self.wbk.datemode
51
58
  else:
52
59
  raise XlsFileError(f"There is no Excel file {self.filename}.")
53
60
 
54
61
  def sheetnames(self) -> Any:
62
+ """Return the list of sheet objects in the open workbook."""
55
63
  return self.wbk.sheets()
56
64
 
57
65
  def sheet_named(self, sheetname: Any) -> Any:
66
+ """Return the sheet matching a name or 1-based integer, raising XlsFileError if absent."""
58
67
  # Return the sheet with the matching name. If the name is actually an integer,
59
68
  # return that sheet number.
60
69
  if isinstance(sheetname, int):
@@ -74,10 +83,11 @@ class XlsFile:
74
83
  return sheet
75
84
 
76
85
  def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) -> list:
86
+ """Return all row data from the named sheet, optionally skipping leading junk rows."""
77
87
  try:
78
88
  sheet = self.sheet_named(sheetname)
79
- except Exception:
80
- raise XlsFileError(f"There is no Excel worksheet named {sheetname} in {self.filename}.")
89
+ except Exception as e:
90
+ raise XlsFileError(f"There is no Excel worksheet named {sheetname} in {self.filename}.") from e
81
91
 
82
92
  # Don't rely on sheet.ncols and sheet.nrows, because Excel will count columns
83
93
  # and rows that have ever been filled, even if they are now empty. Base the column count
@@ -109,7 +119,7 @@ class XlsFile:
109
119
  datarow.append(c.value)
110
120
  elif c.ctype == 3:
111
121
  # date
112
- dt = xlrd.xldate_as_tuple(c.value, self.datemode)
122
+ dt = self._xlrd.xldate_as_tuple(c.value, self.datemode)
113
123
  # Convert to time or datetime
114
124
  if not any(dt[:3]):
115
125
  # No date values
@@ -121,7 +131,7 @@ class XlsFile:
121
131
  datarow.append(bool(c.value))
122
132
  elif c.ctype == 5:
123
133
  # Error code
124
- datarow.append(xlrd.error_text_from_code(c.value))
134
+ datarow.append(self._xlrd.error_text_from_code(c.value))
125
135
  elif c.ctype == 6:
126
136
  # blank
127
137
  datarow.append(None)
@@ -145,6 +155,8 @@ class XlsFile:
145
155
 
146
156
 
147
157
  class XlsxFile:
158
+ """Read/write wrapper around ``openpyxl`` for ``.xlsx`` spreadsheets."""
159
+
148
160
  def __repr__(self) -> str:
149
161
  return "XlsxFile()"
150
162
 
@@ -156,9 +168,11 @@ class XlsxFile:
156
168
  self.log_msgs.append(msg)
157
169
 
158
170
  def __init__(self) -> None:
171
+ """Import openpyxl and initialise file state; raises a fatal error if openpyxl is absent."""
159
172
  try:
160
- global openpyxl
161
173
  import openpyxl
174
+
175
+ self._openpyxl = openpyxl
162
176
  except ImportError:
163
177
  fatal_error("The openpyxl library is needed to read Excel (.xlsx) spreadsheets.")
164
178
  self.filename = None
@@ -168,18 +182,20 @@ class XlsxFile:
168
182
  self.errlog = self.XlsxLog()
169
183
 
170
184
  def open(self, filename: str, encoding: str | None = None, read_only: bool = False) -> None:
185
+ """Open an existing ``.xlsx`` file for reading; raises XlsxFileError if absent."""
171
186
  self.filename = filename
172
187
  self.encoding = encoding
173
188
  self.read_only = read_only
174
189
  if Path(filename).is_file():
175
190
  if read_only:
176
- self.wbk = openpyxl.load_workbook(filename, read_only=True)
191
+ self.wbk = self._openpyxl.load_workbook(filename, read_only=True)
177
192
  else:
178
- self.wbk = openpyxl.load_workbook(filename)
193
+ self.wbk = self._openpyxl.load_workbook(filename)
179
194
  else:
180
195
  raise XlsxFileError(f"There is no Excel file {self.filename}.")
181
196
 
182
197
  def close(self) -> None:
198
+ """Close the open workbook and reset all state attributes."""
183
199
  if self.wbk is not None:
184
200
  self.wbk.close()
185
201
  self.wbk = None
@@ -187,9 +203,11 @@ class XlsxFile:
187
203
  self.encoding = None
188
204
 
189
205
  def sheetnames(self) -> list[str]:
206
+ """Return the list of worksheet names in the open workbook."""
190
207
  return self.wbk.sheetnames
191
208
 
192
209
  def sheet_named(self, sheetname: Any) -> Any:
210
+ """Return the sheet matching a name or 1-based integer index."""
193
211
  # Return the sheet with the matching name. If the name is actually an integer,
194
212
  # return that sheet number.
195
213
  if isinstance(sheetname, int):
@@ -209,10 +227,11 @@ class XlsxFile:
209
227
  return sheet
210
228
 
211
229
  def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) -> list:
230
+ """Return all row data from the named sheet, optionally skipping leading junk rows."""
212
231
  try:
213
232
  sheet = self.sheet_named(sheetname)
214
- except Exception:
215
- raise XlsxFileError(f"There is no Excel worksheet named {sheetname} in {self.filename}.")
233
+ except Exception as e:
234
+ raise XlsxFileError(f"There is no Excel worksheet named {sheetname} in {self.filename}.") from e
216
235
  # Don't rely on sheet.max_column and sheet.max_row, because Excel will count columns
217
236
  # and rows that have ever been filled, even if they are now empty. Base the column count
218
237
  # on the number of contiguous non-empty cells in the first row, and process the data up to nrows until
execsql/exporters/xml.py CHANGED
@@ -8,7 +8,9 @@ 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 re
11
12
  from typing import Any
13
+ from xml.sax.saxutils import escape as xml_escape
12
14
 
13
15
  import execsql.state as _state
14
16
  from execsql.exporters.zip import ZipWriter
@@ -16,6 +18,8 @@ from execsql.exceptions import ErrInfo
16
18
  from execsql.utils.errors import exception_desc
17
19
  from execsql.utils.fileio import filewriter_close
18
20
 
21
+ __all__ = ["write_query_to_xml"]
22
+
19
23
 
20
24
  def write_query_to_xml(
21
25
  select_stmt: str,
@@ -26,13 +30,14 @@ def write_query_to_xml(
26
30
  desc: str | None = None,
27
31
  zipfile: str | None = None,
28
32
  ) -> None:
33
+ """Execute a SELECT and write the result set as a well-formed XML document."""
29
34
  conf = _state.conf
30
35
  try:
31
36
  hdrs, rows = db.select_rowsource(select_stmt)
32
37
  except ErrInfo:
33
38
  raise
34
- except Exception:
35
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
39
+ except Exception as e:
40
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
36
41
  if zipfile is None:
37
42
  filewriter_close(outfile)
38
43
  from execsql.utils.fileio import EncodedFile
@@ -47,14 +52,27 @@ def write_query_to_xml(
47
52
  else:
48
53
  f = ZipWriter(zipfile, outfile, append)
49
54
  f.write(f"<?xml version='1.0' encoding='{conf.output_encoding}'?>\n")
50
- if desc is not None:
51
- f.write(f"<!--{desc}-->\n")
52
- f.write(f"<{tablename}>\n")
53
- str_hdrs = [str(h) for h in hdrs]
54
- for row in rows:
55
- f.write(" <row>\n")
56
- for i, col in enumerate(str_hdrs):
57
- f.write(f" <{col}>{row[i]}</{col}>\n")
58
- f.write(" </row>\n")
59
- f.write(f"</{tablename}>\n")
60
- f.close()
55
+
56
+ def _safe_xml_name(name: str) -> str:
57
+ """Sanitize a string for use as an XML element name."""
58
+ # Replace characters that are invalid in XML names with underscores.
59
+ s = re.sub(r"[^\w.\-]", "_", str(name))
60
+ # XML names must start with a letter or underscore, not a digit or dot.
61
+ if s and not (s[0].isalpha() or s[0] == "_"):
62
+ s = "_" + s
63
+ return s or "_"
64
+
65
+ try:
66
+ if desc is not None:
67
+ f.write(f"<!--{desc.replace('--', '- -')}-->\n")
68
+ safe_tablename = _safe_xml_name(tablename)
69
+ f.write(f"<{safe_tablename}>\n")
70
+ str_hdrs = [_safe_xml_name(h) for h in hdrs]
71
+ for row in rows:
72
+ f.write(" <row>\n")
73
+ for i, col in enumerate(str_hdrs):
74
+ f.write(f" <{col}>{xml_escape(str(row[i]) if row[i] is not None else '')}</{col}>\n")
75
+ f.write(" </row>\n")
76
+ f.write(f"</{safe_tablename}>\n")
77
+ finally:
78
+ f.close()
execsql/exporters/zip.py CHANGED
@@ -15,9 +15,14 @@ import zipfile
15
15
 
16
16
  import execsql.state as _state
17
17
 
18
+ __all__ = ["WriteableZipfile", "ZipWriter"]
19
+
18
20
 
19
21
  class WriteableZipfile:
22
+ """Thin ZipFile wrapper that accepts chunked string writes via an internal buffer."""
23
+
20
24
  def __init__(self, zipfile_name: str, append: bool = False) -> None:
25
+ """Open (or create) a ZIP archive and allocate a write buffer."""
21
26
  conf = _state.conf
22
27
  self.bufsize = conf.zip_buffer_mb * 1024 * 1000
23
28
  self.buf = memoryview(bytearray(self.bufsize))
@@ -31,6 +36,7 @@ class WriteableZipfile:
31
36
  self.close()
32
37
 
33
38
  def member_file(self, member_filename: str) -> None:
39
+ """Create a new member entry in the archive and open it for writing."""
34
40
  # Creates a ZipInfo object (file) within the zipfile and opens it for writing.
35
41
  self.current_zinfo = zipfile.ZipInfo(
36
42
  filename=member_filename,
@@ -49,6 +55,7 @@ class WriteableZipfile:
49
55
  self.current_handle = self.zf.open(self.current_zinfo, mode="w")
50
56
 
51
57
  def zip_buffer(self) -> None:
58
+ """Flush any buffered bytes to the currently open zip member file."""
52
59
  # Writes the buffer contents, if any, to the zip member file.
53
60
  if self.buflen > 0 and self.current_handle is not None:
54
61
  with self.zf._lock:
@@ -57,6 +64,7 @@ class WriteableZipfile:
57
64
  self.buflen = 0
58
65
 
59
66
  def write(self, str_data: str) -> None:
67
+ """Buffer a UTF-8-encoded string for writing to the currently open member."""
60
68
  # Writes the given text to the currently open member.
61
69
  # Convert from string to bytes.
62
70
  data = str_data.encode("utf-8")
@@ -67,26 +75,33 @@ class WriteableZipfile:
67
75
  self.buflen = self.buflen + datalen
68
76
 
69
77
  def close_member(self) -> None:
78
+ """Flush the buffer and close the currently open member file handle."""
70
79
  if self.current_handle is not None:
71
80
  self.zip_buffer()
72
81
  self.current_handle.close()
73
82
  self.current_handle = None
74
83
 
75
84
  def close(self) -> None:
85
+ """Close the open member (flushing the buffer) and finalise the ZIP archive."""
76
86
  self.close_member()
77
87
  self.zf.close()
78
88
 
79
89
 
80
90
  class ZipWriter:
91
+ """High-level write-only interface used by EXPORT metacommands to stream output into a ZIP archive."""
92
+
81
93
  def __init__(self, zip_fname: str, member_fname: str, append: bool = False) -> None:
94
+ """Open the archive at ``zip_fname`` and begin a new member file named ``member_fname``."""
82
95
  self.zip_fname = zip_fname
83
96
  self.member_fname = member_fname
84
97
  self.zwriter = WriteableZipfile(self.zip_fname, append)
85
98
  self.member = self.zwriter.member_file(member_fname)
86
99
 
87
100
  def write(self, str_data: str) -> None:
101
+ """Write a string to the current zip member."""
88
102
  self.zwriter.write(str_data)
89
103
 
90
104
  def close(self) -> None:
105
+ """Close the zip member and finalise the archive."""
91
106
  self.zwriter.close()
92
107
  self.zwriter = None
execsql/importers/base.py CHANGED
@@ -17,6 +17,8 @@ from execsql.db.base import Database
17
17
  import execsql.state as _state
18
18
  from execsql.types import dbt_firebird
19
19
 
20
+ __all__ = ["import_data_table"]
21
+
20
22
 
21
23
  def import_data_table(
22
24
  db: Database,
@@ -85,13 +87,13 @@ def import_data_table(
85
87
  # ...except for Firebird.
86
88
  if db.type == dbt_firebird:
87
89
  db.conn.commit()
88
- except Exception:
90
+ except Exception as e:
89
91
  raise ErrInfo(
90
92
  type="db",
91
93
  command_text=sql,
92
94
  exception_msg=exception_info(),
93
95
  other_msg=f"Could not create new table ({tablename}) for IMPORT metacommand",
94
- )
96
+ ) from e
95
97
  table_cols = db.table_columns(tablename, schemaname)
96
98
  if conf.import_common_cols_only:
97
99
  import_cols = [col for col in hdrs if col.lower() in [tc.lower() for tc in table_cols]]
@@ -108,5 +110,5 @@ def import_data_table(
108
110
  db.commit()
109
111
  except ErrInfo:
110
112
  raise
111
- except Exception:
112
- raise ErrInfo("db", "Call to populate_table when importing data", exception_msg=exception_info())
113
+ except Exception as e:
114
+ raise ErrInfo("db", "Call to populate_table when importing data", exception_msg=exception_info()) from e
execsql/importers/csv.py CHANGED
@@ -17,6 +17,8 @@ from execsql.db.base import Database
17
17
  import execsql.state as _state
18
18
  from execsql.types import dbt_firebird
19
19
 
20
+ __all__ = ["importfile", "importtable"]
21
+
20
22
 
21
23
  def importtable(
22
24
  db: Database,
@@ -60,13 +62,13 @@ def importtable(
60
62
  # ...except for Firebird. Execute the commit directly via the connection so it is always done.
61
63
  if db.type == dbt_firebird:
62
64
  db.conn.commit()
63
- except Exception:
65
+ except Exception as e:
64
66
  raise ErrInfo(
65
67
  type="db",
66
68
  command_text=sql,
67
69
  exception_msg=exception_info(),
68
70
  other_msg=f"Could not create new table ({tablename}) for IMPORT metacommand",
69
- )
71
+ ) from e
70
72
  else:
71
73
  if schemaname is not None:
72
74
  if not db.table_exists(tablename, schemaname):
@@ -85,13 +87,13 @@ def importtable(
85
87
  db.commit()
86
88
  except ErrInfo:
87
89
  raise
88
- except Exception:
90
+ except Exception as e:
89
91
  fq_tablename = db.schema_qualified_table_name(schemaname, tablename)
90
92
  raise ErrInfo(
91
93
  "exception",
92
94
  exception_msg=exception_info(),
93
95
  other_msg=f"Can't import tabular file ({filename}) to table ({fq_tablename})",
94
- )
96
+ ) from e
95
97
  inf.close()
96
98
 
97
99
 
@@ -121,10 +123,10 @@ def importfile(
121
123
  db.commit()
122
124
  except ErrInfo:
123
125
  raise
124
- except Exception:
126
+ except Exception as e:
125
127
  fq_tablename = db.schema_qualified_table_name(schemaname, tablename)
126
128
  raise ErrInfo(
127
129
  "exception",
128
130
  exception_msg=exception_info(),
129
131
  other_msg=f"Can't import file ({filename}) to table ({fq_tablename})",
130
- )
132
+ ) from e
@@ -14,6 +14,8 @@ from execsql.exceptions import ErrInfo
14
14
  from execsql.db.base import Database
15
15
  from execsql.importers.base import import_data_table
16
16
 
17
+ __all__ = ["import_feather", "import_parquet"]
18
+
17
19
 
18
20
  def import_feather(
19
21
  db: Database,
@@ -26,12 +28,12 @@ def import_feather(
26
28
 
27
29
  try:
28
30
  import polars as pl
29
- except Exception:
31
+ except Exception as e:
30
32
  raise ErrInfo(
31
33
  "exception",
32
34
  exception_msg=exception_info(),
33
35
  other_msg="The polars Python library must be installed to import data from the Feather format.",
34
- )
36
+ ) from e
35
37
  df = pl.read_ipc(filename)
36
38
  hdrs = df.columns
37
39
  data = [list(row) for row in df.rows()]
@@ -49,12 +51,12 @@ def import_parquet(
49
51
 
50
52
  try:
51
53
  import polars as pl
52
- except Exception:
54
+ except Exception as e:
53
55
  raise ErrInfo(
54
56
  "exception",
55
57
  exception_msg=exception_info(),
56
58
  other_msg="The polars Python library must be installed to import data from the Parquet format.",
57
- )
59
+ ) from e
58
60
  df = pl.read_parquet(filename)
59
61
  hdrs = df.columns
60
62
  data = [list(row) for row in df.rows()]
execsql/importers/ods.py CHANGED
@@ -17,6 +17,8 @@ from execsql.exporters.ods import OdsFile
17
17
  from execsql.importers.base import import_data_table
18
18
  import execsql.state as _state
19
19
 
20
+ __all__ = ["importods", "ods_data"]
21
+
20
22
 
21
23
  def ods_data(
22
24
  filename: str,
@@ -31,12 +33,12 @@ def ods_data(
31
33
  wbk = OdsFile()
32
34
  try:
33
35
  wbk.open(filename)
34
- except Exception:
35
- raise ErrInfo(type="cmd", other_msg=f"{filename} is not a valid OpenDocument spreadsheet.")
36
+ except Exception as e:
37
+ raise ErrInfo(type="cmd", other_msg=f"{filename} is not a valid OpenDocument spreadsheet.") from e
36
38
  try:
37
39
  alldata = wbk.sheet_data(sheetname, junk_header_rows)
38
- except Exception:
39
- raise ErrInfo(type="cmd", other_msg=f"{sheetname} is not a worksheet in {filename}.")
40
+ except Exception as e:
41
+ raise ErrInfo(type="cmd", other_msg=f"{sheetname} is not a worksheet in {filename}.") from e
40
42
  colhdrs = alldata[0]
41
43
  if any(x is None or len(x.strip()) == 0 for x in colhdrs):
42
44
  if conf.del_empty_cols:
execsql/importers/xls.py CHANGED
@@ -16,6 +16,8 @@ from execsql.db.base import Database
16
16
  from execsql.importers.base import import_data_table
17
17
  import execsql.state as _state
18
18
 
19
+ __all__ = ["importxls", "xls_data"]
20
+
19
21
 
20
22
  def xls_data(
21
23
  filename: str,
@@ -45,12 +47,12 @@ def xls_data(
45
47
  raise ErrInfo(type="cmd", other_msg=f"{filename} is not a recognizable Excel spreadsheet name.")
46
48
  try:
47
49
  wbk.open(filename, encoding, read_only=True)
48
- except Exception:
49
- raise ErrInfo(type="cmd", other_msg=f"{filename} is not a valid Excel spreadsheet.")
50
+ except Exception as e:
51
+ raise ErrInfo(type="cmd", other_msg=f"{filename} is not a valid Excel spreadsheet.") from e
50
52
  try:
51
53
  alldata = wbk.sheet_data(sheetname, junk_header_rows)
52
- except Exception:
53
- raise ErrInfo(type="cmd", other_msg=f"Error reading worksheet {sheetname} from {filename}.")
54
+ except Exception as e:
55
+ raise ErrInfo(type="cmd", other_msg=f"Error reading worksheet {sheetname} from {filename}.") from e
54
56
  if len(alldata) == 0:
55
57
  raise ErrInfo(type="cmd", other_msg=f"There are no data on worksheet {sheetname} of file {filename}.")
56
58
  if ext3 == "lsx":