execsql2 2.2.1__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 (78) hide show
  1. execsql/config.py +52 -0
  2. execsql/db/access.py +11 -3
  3. execsql/db/base.py +180 -135
  4. execsql/db/dsn.py +4 -0
  5. execsql/db/duckdb.py +4 -0
  6. execsql/db/factory.py +21 -0
  7. execsql/db/firebird.py +4 -0
  8. execsql/db/mysql.py +4 -0
  9. execsql/db/oracle.py +4 -0
  10. execsql/db/postgres.py +3 -0
  11. execsql/db/sqlite.py +3 -0
  12. execsql/db/sqlserver.py +11 -2
  13. execsql/exceptions.py +18 -0
  14. execsql/exporters/base.py +6 -0
  15. execsql/exporters/delimited.py +36 -0
  16. execsql/exporters/duckdb.py +4 -0
  17. execsql/exporters/feather.py +4 -0
  18. execsql/exporters/html.py +6 -0
  19. execsql/exporters/json.py +5 -6
  20. execsql/exporters/latex.py +4 -0
  21. execsql/exporters/ods.py +28 -7
  22. execsql/exporters/parquet.py +3 -0
  23. execsql/exporters/pretty.py +5 -0
  24. execsql/exporters/raw.py +5 -3
  25. execsql/exporters/sqlite.py +4 -0
  26. execsql/exporters/templates.py +16 -6
  27. execsql/exporters/values.py +4 -0
  28. execsql/exporters/xls.py +26 -7
  29. execsql/exporters/xml.py +3 -0
  30. execsql/exporters/zip.py +15 -0
  31. execsql/importers/base.py +2 -0
  32. execsql/importers/csv.py +2 -0
  33. execsql/importers/feather.py +2 -0
  34. execsql/importers/ods.py +2 -0
  35. execsql/importers/xls.py +2 -0
  36. execsql/metacommands/__init__.py +177 -1968
  37. execsql/metacommands/dispatch.py +2011 -0
  38. execsql/models.py +7 -0
  39. execsql/parser.py +10 -0
  40. execsql/script/__init__.py +95 -0
  41. execsql/script/control.py +162 -0
  42. execsql/{script.py → script/engine.py} +144 -406
  43. execsql/script/variables.py +281 -0
  44. execsql/types.py +29 -0
  45. execsql/utils/auth.py +2 -0
  46. execsql/utils/crypto.py +4 -6
  47. execsql/utils/datetime.py +1 -0
  48. execsql/utils/errors.py +11 -0
  49. execsql/utils/fileio.py +18 -0
  50. execsql/utils/gui.py +46 -0
  51. execsql/utils/mail.py +7 -17
  52. execsql/utils/numeric.py +2 -0
  53. execsql/utils/regex.py +9 -0
  54. execsql/utils/strings.py +16 -0
  55. execsql/utils/timer.py +2 -0
  56. execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
  57. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
  58. {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/METADATA +8 -1
  59. execsql2-2.4.0.dist-info/RECORD +108 -0
  60. execsql2-2.2.1.data/data/execsql2_extras/READ_ME.rst +0 -127
  61. execsql2-2.2.1.dist-info/RECORD +0 -104
  62. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  63. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  64. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  65. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  66. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  67. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  68. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  69. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  70. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  71. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
  72. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  73. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  74. {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  75. {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
  76. {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
  77. {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
  78. {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
execsql/db/oracle.py CHANGED
@@ -16,8 +16,12 @@ from execsql.utils.errors import exception_desc, fatal_error
16
16
  from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
17
17
  import execsql.state as _state
18
18
 
19
+ __all__ = ["OracleDatabase"]
20
+
19
21
 
20
22
  class OracleDatabase(Database):
23
+ """Oracle adapter using the cx_Oracle (python-oracledb) driver."""
24
+
21
25
  def __init__(
22
26
  self,
23
27
  server_name: str,
execsql/db/postgres.py CHANGED
@@ -19,11 +19,14 @@ from execsql.utils.auth import clear_stored_password, get_password, password_fro
19
19
  from execsql.utils.strings import encodings_match
20
20
  import execsql.state as _state
21
21
 
22
+ __all__ = ["PostgresDatabase"]
22
23
 
23
24
  DEFAULT_CONNECT_TIMEOUT = 30 # seconds
24
25
 
25
26
 
26
27
  class PostgresDatabase(Database):
28
+ """PostgreSQL adapter using psycopg2, with schema support, server-side COPY, and keyring auth."""
29
+
27
30
  def __init__(
28
31
  self,
29
32
  server_name: str,
execsql/db/sqlite.py CHANGED
@@ -18,11 +18,14 @@ from execsql.exceptions import ErrInfo
18
18
  from execsql.utils.errors import exception_desc, fatal_error
19
19
  import execsql.state as _state
20
20
 
21
+ __all__ = ["SQLiteDatabase"]
21
22
 
22
23
  DEFAULT_CONNECT_TIMEOUT = 30 # seconds
23
24
 
24
25
 
25
26
  class SQLiteDatabase(Database):
27
+ """SQLite adapter using the Python standard-library sqlite3 module."""
28
+
26
29
  def __init__(self, SQLite_fn: str, timeout: float = DEFAULT_CONNECT_TIMEOUT) -> None:
27
30
  try:
28
31
  import sqlite3 # noqa: F401
execsql/db/sqlserver.py CHANGED
@@ -7,6 +7,7 @@ Implements :class:`SqlServerDatabase`, which connects to Microsoft SQL
7
7
  Server via ``pyodbc``. Corresponds to ``-t s`` on the CLI.
8
8
  """
9
9
 
10
+ import re
10
11
 
11
12
  from execsql.db.base import Database
12
13
  from execsql.exceptions import ErrInfo
@@ -14,8 +15,12 @@ from execsql.utils.errors import fatal_error
14
15
  from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
15
16
  import execsql.state as _state
16
17
 
18
+ __all__ = ["SqlServerDatabase"]
19
+
17
20
 
18
21
  class SqlServerDatabase(Database):
22
+ """Microsoft SQL Server adapter using pyodbc, trying drivers from newest to oldest."""
23
+
19
24
  def __init__(
20
25
  self,
21
26
  server_name: str,
@@ -96,9 +101,13 @@ class SqlServerDatabase(Database):
96
101
  try:
97
102
  self.conn = pyodbc.connect(connstr)
98
103
  except Exception:
99
- _state.exec_log.log_status_info(f"Could not connect using: {connstr}")
104
+ _state.exec_log.log_status_info(
105
+ f"Could not connect using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
106
+ )
100
107
  else:
101
- _state.exec_log.log_status_info(f"Connected using: {connstr}")
108
+ _state.exec_log.log_status_info(
109
+ f"Connected using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
110
+ )
102
111
  return True
103
112
  return False
104
113
 
execsql/exceptions.py CHANGED
@@ -21,6 +21,24 @@ from a single location. Notable exceptions:
21
21
  - :class:`CondParserError` / :class:`NumericParserError` — parser failures.
22
22
  """
23
23
 
24
+ __all__ = [
25
+ "ExecSqlError",
26
+ "ConfigError",
27
+ "ExecSqlTimeoutError",
28
+ "ErrInfo",
29
+ "DataTypeError",
30
+ "DbTypeError",
31
+ "ColumnError",
32
+ "DataTableError",
33
+ "DatabaseNotImplementedError",
34
+ "OdsFileError",
35
+ "XlsFileError",
36
+ "XlsxFileError",
37
+ "ConsoleUIError",
38
+ "CondParserError",
39
+ "NumericParserError",
40
+ ]
41
+
24
42
 
25
43
  class ExecSqlError(Exception):
26
44
  """Base class for simple single-message execsql exceptions.
execsql/exporters/base.py CHANGED
@@ -22,6 +22,8 @@ from execsql.script import current_script_line
22
22
  from execsql.utils.errors import file_size_date
23
23
  from execsql.utils.gui import ConsoleUIError
24
24
 
25
+ __all__ = ["WriteSpec", "ExportRecord", "ExportMetadata"]
26
+
25
27
 
26
28
  class ExportRecord:
27
29
  """Records the details of a single EXPORT operation for metadata tracking.
@@ -88,15 +90,18 @@ class ExportMetadata:
88
90
  self.recordlist: list[ExportRecord] = []
89
91
 
90
92
  def add(self, exp_record: ExportRecord) -> None:
93
+ """Append an export record to the collection."""
91
94
  self.recordlist.append(exp_record)
92
95
 
93
96
  def get(self):
97
+ """Return column headers and all not-yet-exported records, marking them exported."""
94
98
  recs = [er.record for er in self.recordlist if not er.exported]
95
99
  for er in self.recordlist:
96
100
  er.exported = True
97
101
  return self.colhdrs, recs
98
102
 
99
103
  def get_all(self):
104
+ """Return column headers and every record regardless of prior export state."""
100
105
  recs = [er.record for er in self.recordlist]
101
106
  for er in self.recordlist:
102
107
  er.exported = True
@@ -130,6 +135,7 @@ class WriteSpec:
130
135
  self.written = False
131
136
 
132
137
  def write(self) -> None:
138
+ """Execute the deferred write, expanding substitution variables first."""
133
139
  # Writes the message per the specifications given to '__init__()'. Substitution
134
140
  # variables are processed.
135
141
  # Inputs: no inputs.
@@ -29,9 +29,14 @@ from execsql.utils.errors import exception_desc
29
29
  from execsql.utils.fileio import filewriter_close
30
30
  from execsql.utils.strings import clean_words, fold_words
31
31
 
32
+ __all__ = ["LineDelimiter", "CsvFile", "CsvWriter", "DelimitedWriter", "write_delimited_file"]
33
+
32
34
 
33
35
  class LineDelimiter:
36
+ """Encapsulates delimiter, quote character, and escape rules for a single line format."""
37
+
34
38
  def __init__(self, delim: str | None, quote: str | None, escchar: str | None) -> None:
39
+ """Initialise the line-format constants for a given delimiter/quote pair."""
35
40
  self.delimiter = delim
36
41
  self.joinchar = delim if delim else ""
37
42
  self.quotechar = quote
@@ -44,6 +49,7 @@ class LineDelimiter:
44
49
  self.quotedquote = None
45
50
 
46
51
  def delimited(self, datarow: Any, add_newline: bool = True) -> str:
52
+ """Format a sequence of values as a single delimited text line."""
47
53
  conf = _state.conf
48
54
  if self.quotechar:
49
55
  d_row = []
@@ -79,22 +85,30 @@ class LineDelimiter:
79
85
 
80
86
 
81
87
  class DelimitedWriter:
88
+ """Low-level writer that formats data rows as delimited text and sends them to an open file object."""
89
+
82
90
  def __init__(self, outfile: Any, delim: str | None, quote: str | None, escchar: str | None) -> None:
91
+ """Wrap an open file object with a :class:`LineDelimiter` for row-by-row writing."""
83
92
  self.outfile = outfile
84
93
  self.line_delimiter = LineDelimiter(delim, quote, escchar)
85
94
 
86
95
  def write(self, text_str: str) -> None:
96
+ """Write a raw string directly to the underlying file object."""
87
97
  self.outfile.write(text_str)
88
98
 
89
99
  def writerow(self, datarow: Any) -> None:
100
+ """Format one data row as delimited text and write it to the file."""
90
101
  self.outfile.write(self.line_delimiter.delimited(datarow))
91
102
 
92
103
  def writerows(self, datarows: Any) -> None:
104
+ """Write each row in an iterable of data rows to the file."""
93
105
  for row in datarows:
94
106
  self.writerow(row)
95
107
 
96
108
 
97
109
  class CsvWriter:
110
+ """Opens a named file (or stdout) and exposes a row-oriented delimited-text writing API."""
111
+
98
112
  def __init__(
99
113
  self,
100
114
  filename: str,
@@ -113,21 +127,32 @@ class CsvWriter:
113
127
  self.dwriter = DelimitedWriter(self.output, delim, quote, escchar)
114
128
 
115
129
  def write(self, text_str: str) -> None:
130
+ """Write a raw string to the output file."""
116
131
  self.dwriter.write(text_str)
117
132
 
118
133
  def writerow(self, datarow: Any) -> None:
134
+ """Format and write a single data row."""
119
135
  self.dwriter.writerow(datarow)
120
136
 
121
137
  def writerows(self, datarows: Any) -> None:
138
+ """Format and write each row in an iterable."""
122
139
  self.dwriter.writerows(datarows)
123
140
 
124
141
  def close(self) -> None:
142
+ """Close the underlying output file."""
125
143
  self.output.close()
126
144
  self.output = None
127
145
 
128
146
 
129
147
  class CsvFile(EncodedFile):
148
+ """Full delimited-file reader/writer with automatic format diagnosis.
149
+
150
+ Supports custom delimiters, quoting, encoding, junk-header skipping,
151
+ column-type inference, and ZIP output via :class:`CsvWriter`.
152
+ """
153
+
130
154
  def __init__(self, csvfname: str, file_encoding: str, junk_header_lines: int = 0) -> None:
155
+ """Open a CSV file path for later reading or writing."""
131
156
  super().__init__(csvfname, file_encoding)
132
157
  self.csvfname = csvfname
133
158
  self.junk_header_lines = junk_header_lines
@@ -143,6 +168,7 @@ class CsvFile(EncodedFile):
143
168
  return f"CsvFile({self.csvfname!r}, {self.encoding!r})"
144
169
 
145
170
  def openclean(self, mode: str) -> Any:
171
+ """Return an opened file object with the configured number of junk header lines skipped."""
146
172
  # Returns an opened file object with junk headers stripped.
147
173
  f = self.open(mode)
148
174
  for _ in range(self.junk_header_lines):
@@ -150,6 +176,7 @@ class CsvFile(EncodedFile):
150
176
  return f
151
177
 
152
178
  def lineformat(self, delimiter: str | None, quotechar: str | None, escapechar: str | None) -> None:
179
+ """Explicitly set the delimiter, quote character, and escape character."""
153
180
  # Specifies the format of a line.
154
181
  self.delimiter = delimiter
155
182
  self.quotechar = quotechar
@@ -413,6 +440,7 @@ class CsvFile(EncodedFile):
413
440
  )
414
441
 
415
442
  def evaluate_line_format(self) -> None:
443
+ """Scan the file to auto-detect the delimiter, quote character, and escape character."""
416
444
  # Scans the file to determine the delimiter, quote character, and escapechar.
417
445
  if not self.lineformat_set:
418
446
  f = self.openclean("rt")
@@ -426,6 +454,7 @@ class CsvFile(EncodedFile):
426
454
  self.parse_errors.append(f"{errmsg} in position {pos_no}")
427
455
 
428
456
  def read_and_parse_line(self, f: Any) -> list:
457
+ """Read and parse one line from an open file, returning a list of field values."""
429
458
  # Returns a list of line elements, parsed according to the established delimiter and quotechar.
430
459
  elements = []
431
460
  eat_multiple_delims = self.delimiter == " "
@@ -571,6 +600,7 @@ class CsvFile(EncodedFile):
571
600
  return elements
572
601
 
573
602
  def reader(self) -> Any:
603
+ """Yield parsed rows from the file as lists of field values."""
574
604
  conf = _state.conf
575
605
  self.evaluate_line_format()
576
606
  f = self.openclean("rt")
@@ -597,6 +627,7 @@ class CsvFile(EncodedFile):
597
627
  f.close()
598
628
 
599
629
  def writer(self, append: bool = False) -> CsvWriter:
630
+ """Return a :class:`CsvWriter` configured with this file's format settings."""
600
631
  return CsvWriter(self.filename, self.encoding, self.delimiter, self.quotechar, self.escapechar, append)
601
632
 
602
633
  def _colhdrs(self, inf: Any) -> list[str]:
@@ -640,17 +671,20 @@ class CsvFile(EncodedFile):
640
671
  return colnames
641
672
 
642
673
  def column_headers(self) -> list[str]:
674
+ """Return the first row of the file as a list of column header strings."""
643
675
  if not self.lineformat_set:
644
676
  self.evaluate_line_format()
645
677
  inf = self.reader()
646
678
  return self._colhdrs(inf)
647
679
 
648
680
  def data_table_def(self) -> Any:
681
+ """Return a :class:`DataTable` describing the file's columns and types."""
649
682
  if self.table_data is None:
650
683
  self.evaluate_column_types()
651
684
  return self.table_data
652
685
 
653
686
  def evaluate_column_types(self) -> None:
687
+ """Scan the file and populate ``table_data`` with inferred column types."""
654
688
  if not self.lineformat_set:
655
689
  self.evaluate_line_format()
656
690
  inf = self.reader()
@@ -658,6 +692,7 @@ class CsvFile(EncodedFile):
658
692
  self.table_data = DataTable(colnames, inf)
659
693
 
660
694
  def create_table(self, database_type: Any, schemaname: str | None, tablename: str, pretty: bool = False) -> str:
695
+ """Generate a CREATE TABLE SQL statement for this file's inferred schema."""
661
696
  return self.table_data.create_table(database_type, schemaname, tablename, pretty)
662
697
 
663
698
 
@@ -670,6 +705,7 @@ def write_delimited_file(
670
705
  append: bool = False,
671
706
  zipfile: str | None = None,
672
707
  ) -> None:
708
+ """Write a query result set to a CSV, TSV, or other delimited text file."""
673
709
  delim = None
674
710
  quote = None
675
711
  escchar = None
@@ -15,6 +15,8 @@ from typing import Any
15
15
  from execsql.exceptions import ErrInfo
16
16
  from execsql.types import dbt_duckdb
17
17
 
18
+ __all__ = ["export_duckdb", "write_query_to_duckdb"]
19
+
18
20
 
19
21
  def export_duckdb(
20
22
  outfile: str,
@@ -23,6 +25,7 @@ def export_duckdb(
23
25
  append: bool,
24
26
  tablename: str,
25
27
  ) -> None:
28
+ """Write pre-fetched rows to a named table in a DuckDB database file."""
26
29
  try:
27
30
  import duckdb
28
31
  except Exception:
@@ -79,6 +82,7 @@ def write_query_to_duckdb(
79
82
  append: bool,
80
83
  tablename: str,
81
84
  ) -> None:
85
+ """Execute a SELECT and write the result set to a named table in a DuckDB database."""
82
86
  from execsql.utils.errors import exception_desc
83
87
 
84
88
  try:
@@ -18,8 +18,11 @@ from execsql.types import DT_Boolean, DT_Date, DT_Timestamp, DT_TimestampTZ
18
18
  from execsql.utils.errors import exception_desc
19
19
  from execsql.utils.fileio import filewriter_close
20
20
 
21
+ __all__ = ["write_query_to_feather", "write_query_to_hdf5"]
22
+
21
23
 
22
24
  def write_query_to_feather(outfile: str, headers: list[str], rows: Any) -> None:
25
+ """Write a row source as an Apache Arrow Feather v2 file using polars."""
23
26
  try:
24
27
  import polars as pl
25
28
  except ImportError as e:
@@ -45,6 +48,7 @@ def write_query_to_hdf5(
45
48
  append: bool = False,
46
49
  desc: str | None = None,
47
50
  ) -> None:
51
+ """Execute a SELECT and write the result set to an HDF5 file using the tables library."""
48
52
  try:
49
53
  import tables
50
54
  except ImportError as e:
execsql/exporters/html.py CHANGED
@@ -25,6 +25,8 @@ from execsql.script import current_script_line
25
25
  from execsql.utils.errors import exception_desc
26
26
  from execsql.utils.fileio import filewriter_close
27
27
 
28
+ __all__ = ["export_html", "export_cgi_html", "write_query_to_html", "write_query_to_cgi_html"]
29
+
28
30
 
29
31
  def export_html(
30
32
  outfile: str,
@@ -35,6 +37,7 @@ def export_html(
35
37
  desc: str | None = None,
36
38
  zipfile: str | None = None,
37
39
  ) -> None:
40
+ """Write a complete HTML document containing a data table to a file or ZIP archive."""
38
41
  conf = _state.conf
39
42
 
40
43
  def write_table(f):
@@ -164,6 +167,7 @@ def export_cgi_html(
164
167
  desc: str | None = None,
165
168
  zipfile: str | None = None,
166
169
  ) -> None:
170
+ """Write a CGI-style HTML fragment (Content-Type header + table) to a file or ZIP archive."""
167
171
  conf = _state.conf
168
172
 
169
173
  def write_table(f):
@@ -223,6 +227,7 @@ def write_query_to_html(
223
227
  desc: str | None = None,
224
228
  zipfile: str | None = None,
225
229
  ) -> None:
230
+ """Execute a SELECT and write the result set as a standalone HTML document."""
226
231
  try:
227
232
  hdrs, rows = db.select_rowsource(select_stmt)
228
233
  except ErrInfo:
@@ -240,6 +245,7 @@ def write_query_to_cgi_html(
240
245
  desc: str | None = None,
241
246
  zipfile: str | None = None,
242
247
  ) -> None:
248
+ """Execute a SELECT and write the result set as a CGI-style HTML fragment."""
243
249
  try:
244
250
  hdrs, rows = db.select_rowsource(select_stmt)
245
251
  except ErrInfo:
execsql/exporters/json.py CHANGED
@@ -8,6 +8,7 @@ Provides :func:`write_query_to_json` (standard JSON array of objects) and
8
8
  both of which serialize a query result set to a file or stream.
9
9
  """
10
10
 
11
+ import json
11
12
  from typing import Any
12
13
 
13
14
  import execsql.state as _state
@@ -17,6 +18,8 @@ from execsql.models import DataTable
17
18
  from execsql.utils.errors import exception_desc
18
19
  from execsql.utils.fileio import filewriter_close
19
20
 
21
+ __all__ = ["write_query_to_json", "write_query_to_json_ts"]
22
+
20
23
 
21
24
  def write_query_to_json(
22
25
  select_stmt: str,
@@ -26,9 +29,7 @@ def write_query_to_json(
26
29
  desc: str | None = None,
27
30
  zipfile: str | None = None,
28
31
  ) -> None:
29
- global json
30
- import json
31
-
32
+ """Execute a SELECT and write the result set as a JSON array of objects."""
32
33
  conf = _state.conf
33
34
  try:
34
35
  hdrs, rows = db.select_rowsource(select_stmt)
@@ -75,9 +76,7 @@ def write_query_to_json_ts(
75
76
  desc: str | None = None,
76
77
  zipfile: str | None = None,
77
78
  ) -> None:
78
- global json
79
- import json
80
-
79
+ """Execute a SELECT and write the result set as a JSON object with a top-level field-type schema."""
81
80
  conf = _state.conf
82
81
  try:
83
82
  hdrs, rows = db.select_rowsource(select_stmt)
@@ -17,6 +17,8 @@ from execsql.exceptions import ErrInfo
17
17
  from execsql.exporters.zip import WriteableZipfile
18
18
  import execsql.state as _state
19
19
 
20
+ __all__ = ["export_latex", "write_query_to_latex"]
21
+
20
22
 
21
23
  def export_latex(
22
24
  outfile: str,
@@ -27,6 +29,7 @@ def export_latex(
27
29
  desc: str | None = None,
28
30
  zipfile: Any | None = None,
29
31
  ) -> None:
32
+ """Write pre-fetched rows as a LaTeX tabular environment to a file or ZIP archive."""
30
33
  from execsql.utils.fileio import EncodedFile
31
34
 
32
35
  def write_table(f: Any) -> None:
@@ -120,6 +123,7 @@ def write_query_to_latex(
120
123
  desc: str | None = None,
121
124
  zipfile: Any | None = None,
122
125
  ) -> None:
126
+ """Execute a SELECT and write the result set as a LaTeX tabular table."""
123
127
  from execsql.utils.errors import exception_desc
124
128
 
125
129
  try:
execsql/exporters/ods.py CHANGED
@@ -23,20 +23,25 @@ from execsql.utils.errors import exception_desc, fatal_error
23
23
  from execsql.utils.fileio import filewriter_close
24
24
  from execsql.utils.strings import unquoted
25
25
 
26
+ __all__ = ["OdsFile", "export_ods", "write_query_to_ods", "write_queries_to_ods"]
27
+
26
28
 
27
29
  class OdsFile:
30
+ """Wrapper around the ``odfpy`` library for reading and writing OpenDocument Spreadsheet files."""
31
+
28
32
  def __repr__(self) -> str:
29
33
  return "OdsFile()"
30
34
 
31
35
  def __init__(self) -> None:
36
+ """Import odfpy and initialise the workbook state."""
32
37
  global of
33
38
  try:
34
- import of as of
35
- import of.opendocument
36
- import of.table
37
- import of.text
38
- import of.number
39
- import of.style
39
+ import odf as of # noqa: F401 — submodule imports below register on the `of` alias
40
+ import odf.opendocument # noqa: F401
41
+ import odf.table # noqa: F401
42
+ import odf.text # noqa: F401
43
+ import odf.number # noqa: F401
44
+ import odf.style # noqa: F401
40
45
  except ImportError:
41
46
  fatal_error("The odfpy library is needed to create OpenDocument spreadsheets.")
42
47
  self.filename = None
@@ -44,6 +49,7 @@ class OdsFile:
44
49
  self.cell_style_names = []
45
50
 
46
51
  def open(self, filename: str) -> None:
52
+ """Open an existing ODS file or create a new one at the given path."""
47
53
  self.filename = filename
48
54
  if Path(filename).is_file():
49
55
  self.wbk = of.opendocument.load(filename)
@@ -61,6 +67,7 @@ class OdsFile:
61
67
  self.wbk = of.opendocument.OpenDocumentSpreadsheet()
62
68
 
63
69
  def define_body_style(self) -> None:
70
+ """Register the ``body`` cell style in the workbook if not already defined."""
64
71
  st_name = "body"
65
72
  if st_name not in self.cell_style_names:
66
73
  body_style = of.style.Style(name=st_name, family="table-cell")
@@ -69,6 +76,7 @@ class OdsFile:
69
76
  self.cell_style_names.append(st_name)
70
77
 
71
78
  def define_header_style(self) -> None:
79
+ """Register the ``header`` cell style (bottom-bordered) in the workbook if not already defined."""
72
80
  st_name = "header"
73
81
  if st_name not in self.cell_style_names:
74
82
  header_style = of.style.Style(name=st_name, family="table-cell")
@@ -84,6 +92,7 @@ class OdsFile:
84
92
  self.cell_style_names.append(st_name)
85
93
 
86
94
  def define_iso_datetime_style(self) -> None:
95
+ """Register an ISO-8601 datetime number style in the workbook if not already defined."""
87
96
  st_name = "iso_datetime"
88
97
  if st_name not in self.cell_style_names:
89
98
  dt_style = of.number.DateStyle(name="iso-datetime")
@@ -110,6 +119,7 @@ class OdsFile:
110
119
  self.cell_style_names.append(st_name)
111
120
 
112
121
  def define_iso_date_style(self) -> None:
122
+ """Register an ISO-8601 date number style in the workbook if not already defined."""
113
123
  st_name = "iso_date"
114
124
  if st_name not in self.cell_style_names:
115
125
  dt_style = of.number.DateStyle(name="iso-date")
@@ -125,10 +135,12 @@ class OdsFile:
125
135
  self.cell_style_names.append(st_name)
126
136
 
127
137
  def sheetnames(self) -> list[str]:
138
+ """Return a list of worksheet names in the open workbook."""
128
139
  # Returns a list of the worksheet names in the specified ODS spreadsheet.
129
140
  return [sheet.getAttribute("name") for sheet in self.wbk.spreadsheet.getElementsByType(of.table.Table)]
130
141
 
131
142
  def sheet_named(self, sheetname: Any) -> Any:
143
+ """Return the sheet matching a name or 1-based integer index, or ``None`` if not found."""
132
144
  # Return the sheet with the matching name. If the name is actually an integer,
133
145
  # return that sheet number.
134
146
  if isinstance(sheetname, int):
@@ -153,6 +165,7 @@ class OdsFile:
153
165
  return None
154
166
 
155
167
  def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) -> list:
168
+ """Return all row data from the named sheet, optionally skipping leading junk rows."""
156
169
  sheet = self.sheet_named(sheetname)
157
170
  if not sheet:
158
171
  raise OdsFileError(f"There is no sheet named {sheetname}")
@@ -195,10 +208,12 @@ class OdsFile:
195
208
  return [row_data(r) for r in rows]
196
209
 
197
210
  def new_sheet(self, sheetname: str) -> Any:
211
+ """Create and return a detached sheet object that can later be added to the workbook."""
198
212
  # Returns a sheet (a named Table) that has not yet been added to the workbook
199
213
  return of.table.Table(name=sheetname)
200
214
 
201
215
  def add_row_to_sheet(self, datarow: Any, of_table: Any, header: bool = False) -> None:
216
+ """Append a data row to an ODS table, applying header or body cell styles as appropriate."""
202
217
  if header:
203
218
  self.define_header_style()
204
219
  style_name = "header"
@@ -211,7 +226,7 @@ class OdsFile:
211
226
  if isinstance(item, bool):
212
227
  # Booleans must be evaluated before numbers.
213
228
  tc = of.table.TableCell(valuetype="boolean", value=1 if item else 0, stylename=style_name)
214
- elif isinstance(item, (float, int)):
229
+ elif isinstance(item, float | int):
215
230
  tc = of.table.TableCell(valuetype="float", value=item, stylename=style_name)
216
231
  elif isinstance(item, datetime.datetime):
217
232
  self.define_iso_datetime_style()
@@ -247,15 +262,18 @@ class OdsFile:
247
262
  tr.addElement(tc)
248
263
 
249
264
  def add_sheet(self, of_table: Any) -> None:
265
+ """Attach a prepared sheet object to the workbook's spreadsheet element."""
250
266
  self.wbk.spreadsheet.addElement(of_table)
251
267
 
252
268
  def save_close(self) -> None:
269
+ """Serialise the workbook to disk and release all resources."""
253
270
  with open(self.filename, "wb") as ofile:
254
271
  self.wbk.write(ofile)
255
272
  self.filename = None
256
273
  self.wbk = None
257
274
 
258
275
  def close(self) -> None:
276
+ """Release the workbook reference without saving."""
259
277
  self.filename = None
260
278
  self.wbk = None
261
279
 
@@ -269,6 +287,7 @@ def export_ods(
269
287
  sheetname: str | None = None,
270
288
  desc: str | None = None,
271
289
  ) -> None:
290
+ """Write a single-sheet ODS file from pre-fetched column headers and rows."""
272
291
  # If not given, determine the worksheet name to use. The pattern is "Sheetx", where x is
273
292
  # the first integer for which there is not already a sheet name.
274
293
  if append and Path(outfile).is_file():
@@ -338,6 +357,7 @@ def write_query_to_ods(
338
357
  sheetname: str | None = None,
339
358
  desc: str | None = None,
340
359
  ) -> None:
360
+ """Execute a SELECT and write the result set as a single-sheet ODS spreadsheet."""
341
361
  try:
342
362
  hdrs, rows = db.select_rowsource(select_stmt)
343
363
  except ErrInfo:
@@ -355,6 +375,7 @@ def write_queries_to_ods(
355
375
  tee: bool = False,
356
376
  desc: str | None = None,
357
377
  ) -> None:
378
+ """Write multiple tables/queries to separate sheets in a single ODS workbook."""
358
379
  from execsql.exporters.pretty import prettyprint_query
359
380
  from execsql.exporters.base import ExportRecord
360
381
 
@@ -13,8 +13,11 @@ from execsql.exceptions import ErrInfo
13
13
  from execsql.utils.errors import exception_desc
14
14
  from execsql.utils.fileio import filewriter_close
15
15
 
16
+ __all__ = ["write_query_to_parquet"]
17
+
16
18
 
17
19
  def write_query_to_parquet(outfile: str, headers: list[str], rows: Any) -> None:
20
+ """Write a row source as an Apache Parquet file using polars."""
18
21
  try:
19
22
  import polars as pl
20
23
  except ImportError as e:
@@ -16,6 +16,8 @@ from execsql.exceptions import ErrInfo
16
16
  from execsql.utils.errors import exception_desc
17
17
  from execsql.utils.fileio import filewriter_close
18
18
 
19
+ __all__ = ["prettyprint_query", "prettyprint_rowset"]
20
+
19
21
 
20
22
  def prettyprint_rowset(
21
23
  colhdrs: list[str],
@@ -26,6 +28,8 @@ def prettyprint_rowset(
26
28
  desc: str | None = None,
27
29
  zipfile: str | None = None,
28
30
  ) -> None:
31
+ """Format a pre-fetched result set as a fixed-width human-readable text table and write it."""
32
+
29
33
  # Adapted from the pp() function by Aaron Watters,
30
34
  # posted to gadfly-rdbms@egroups.com 1999-01-18.
31
35
  def as_ucode(s):
@@ -97,6 +101,7 @@ def prettyprint_query(
97
101
  desc: str | None = None,
98
102
  zipfile: str | None = None,
99
103
  ) -> None:
104
+ """Execute a SELECT and write the result set as a column-aligned text table."""
100
105
  _state.status.sql_error = False
101
106
  names, rows = db.select_data(select_stmt)
102
107
  prettyprint_rowset(names, rows, outfile, append, and_val, desc, zipfile=zipfile)