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/html.py CHANGED
@@ -11,6 +11,7 @@ CSS styling.
11
11
 
12
12
  import datetime
13
13
  import getpass
14
+ import html as html_mod
14
15
  import os
15
16
  import sys
16
17
  import tempfile
@@ -24,6 +25,8 @@ from execsql.script import current_script_line
24
25
  from execsql.utils.errors import exception_desc
25
26
  from execsql.utils.fileio import filewriter_close
26
27
 
28
+ __all__ = ["export_html", "export_cgi_html", "write_query_to_html", "write_query_to_cgi_html"]
29
+
27
30
 
28
31
  def export_html(
29
32
  outfile: str,
@@ -34,6 +37,7 @@ def export_html(
34
37
  desc: str | None = None,
35
38
  zipfile: str | None = None,
36
39
  ) -> None:
40
+ """Write a complete HTML document containing a data table to a file or ZIP archive."""
37
41
  conf = _state.conf
38
42
 
39
43
  def write_table(f):
@@ -42,12 +46,12 @@ def export_html(
42
46
  f.write(f"<caption>{desc}</caption>\n")
43
47
  f.write("<thead><tr>")
44
48
  for h in hdrs:
45
- f.write(f"<th>{h}</th>")
49
+ f.write(f"<th>{html_mod.escape(str(h))}</th>")
46
50
  f.write("</tr></thead>\n<tbody>\n")
47
51
  for r in rows:
48
52
  f.write("<tr>")
49
53
  for v in r:
50
- f.write(f"<td>{v if v else ''}</td>")
54
+ f.write(f"<td>{html_mod.escape(str(v)) if v else ''}</td>")
51
55
  f.write("</tr>\n")
52
56
  f.write("</tbody>\n</table>\n")
53
57
 
@@ -67,42 +71,47 @@ def export_html(
67
71
  f = ef.open("wt")
68
72
  else:
69
73
  f = ZipWriter(zipfile, outfile, append)
70
- f.write('<!DOCTYPE html>\n<html>\n<head>\n<meta charset="utf-8" />\n')
71
- if querytext:
72
- descrip = f"Source: [{querytext}] with database {_state.dbs.current().name()} in script {str(Path(script).resolve())}, line {lno}"
73
- else:
74
- descrip = f"From database {_state.dbs.current().name()} in script {str(Path(script).resolve())}, line {lno}"
75
- f.write(f'<meta name="description" content="{descrip}" />\n')
76
- datecontent = datetime.datetime.now().strftime("%Y-%m-%d")
77
- f.write(f'<meta name="created" content="{datecontent}" />\n')
78
- f.write(f'<meta name="revised" content="{datecontent}" />\n')
79
- f.write(f'<meta name="author" content="{getpass.getuser()}" />\n')
80
- f.write("<title>Data Table</title>\n")
81
- if conf.css_file or conf.css_styles:
82
- if conf.css_file:
83
- f.write(f'<link rel="stylesheet" type="text/css" href="{conf.css_file}">')
84
- if conf.css_styles:
85
- f.write(f'<style type="text/css">\n{conf.css_styles}\n</style>')
86
- else:
87
- f.write('<style type="text/css">\n')
88
- f.write(
89
- 'table {font-family: "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Lucida Console", "Courier New", Courier, fixed; '
90
- + "border-top: 3px solid #814324; border-bottom: 3px solid #814324; "
91
- + "border-left: 2px solid #814324; border-right: 2px solid #814324; "
92
- + "border-collapse: collapse; }\n",
93
- )
94
- f.write("td {text-align: left; padding 0 10px; border-right: 1px dotted #814324; }\n")
95
- f.write(
96
- "th {padding: 2px 10px; text-align: center; border-bottom: 1px solid #814324; border-right: 1px dotted #814324;}\n",
97
- )
98
- f.write("tr.hdr {font-weight: bold;}\n")
99
- f.write("thead tr {border-bottom: 1px solid #814324; background-color: #F3F1E2; }\n")
100
- f.write("tbody tr { border-bottom: 1px dotted #814324; }\n")
101
- f.write("</style>")
102
- f.write("\n</head>\n<body>\n")
103
- write_table(f)
104
- f.write("</body>\n</html>\n")
105
- f.close()
74
+ try:
75
+ f.write('<!DOCTYPE html>\n<html>\n<head>\n<meta charset="utf-8" />\n')
76
+ if querytext:
77
+ descrip = f"Source: [{querytext}] with database {_state.dbs.current().name()} in script {str(Path(script).resolve())}, line {lno}"
78
+ else:
79
+ descrip = (
80
+ f"From database {_state.dbs.current().name()} in script {str(Path(script).resolve())}, line {lno}"
81
+ )
82
+ f.write(f'<meta name="description" content="{descrip}" />\n')
83
+ datecontent = datetime.datetime.now().strftime("%Y-%m-%d")
84
+ f.write(f'<meta name="created" content="{datecontent}" />\n')
85
+ f.write(f'<meta name="revised" content="{datecontent}" />\n')
86
+ f.write(f'<meta name="author" content="{getpass.getuser()}" />\n')
87
+ f.write("<title>Data Table</title>\n")
88
+ if conf.css_file or conf.css_styles:
89
+ if conf.css_file:
90
+ f.write(f'<link rel="stylesheet" type="text/css" href="{conf.css_file}">')
91
+ if conf.css_styles:
92
+ f.write(f'<style type="text/css">\n{conf.css_styles}\n</style>')
93
+ else:
94
+ f.write('<style type="text/css">\n')
95
+ f.write(
96
+ 'table {font-family: "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Lucida Console", "Courier New", Courier, fixed; '
97
+ + "border-top: 3px solid #814324; border-bottom: 3px solid #814324; "
98
+ + "border-left: 2px solid #814324; border-right: 2px solid #814324; "
99
+ + "border-collapse: collapse; }\n",
100
+ )
101
+ f.write("td {text-align: left; padding 0 10px; border-right: 1px dotted #814324; }\n")
102
+ f.write(
103
+ "th {padding: 2px 10px; text-align: center; border-bottom: 1px solid #814324; border-right: 1px dotted #814324;}\n",
104
+ )
105
+ f.write("tr.hdr {font-weight: bold;}\n")
106
+ f.write("thead tr {border-bottom: 1px solid #814324; background-color: #F3F1E2; }\n")
107
+ f.write("tbody tr { border-bottom: 1px dotted #814324; }\n")
108
+ f.write("</style>")
109
+ f.write("\n</head>\n<body>\n")
110
+ write_table(f)
111
+ f.write("</body>\n</html>\n")
112
+ finally:
113
+ if outfile.lower() != "stdout":
114
+ f.close()
106
115
  elif not zipfile and append:
107
116
  if outfile.lower() == "stdout":
108
117
  f = sys.stdout
@@ -112,8 +121,10 @@ def export_html(
112
121
 
113
122
  ef = EncodedFile(outfile, conf.output_encoding)
114
123
  f = ef.open("wt")
115
- write_table(f)
116
- f.close()
124
+ try:
125
+ write_table(f)
126
+ finally:
127
+ f.close()
117
128
  else:
118
129
  filewriter_close(outfile)
119
130
  from execsql.utils.fileio import EncodedFile
@@ -124,23 +135,25 @@ def export_html(
124
135
  os.close(tempf) # Close the fd from mkstemp; EncodedFile opens its own handle
125
136
  tf = EncodedFile(tempfname, conf.output_encoding)
126
137
  t = tf.open("wt")
127
- remainder = ""
128
- for line in f:
129
- bodypos = line.lower().find("</body>")
130
- if bodypos > -1:
131
- t.write(line[0:bodypos])
132
- t.write("\n")
133
- remainder = line[bodypos:]
134
- break
135
- else:
138
+ try:
139
+ remainder = ""
140
+ for line in f:
141
+ bodypos = line.lower().find("</body>")
142
+ if bodypos > -1:
143
+ t.write(line[0:bodypos])
144
+ t.write("\n")
145
+ remainder = line[bodypos:]
146
+ break
147
+ else:
148
+ t.write(line)
149
+ t.write("\n")
150
+ write_table(t)
151
+ t.write(remainder)
152
+ for line in f:
136
153
  t.write(line)
137
- t.write("\n")
138
- write_table(t)
139
- t.write(remainder)
140
- for line in f:
141
- t.write(line)
142
- t.close()
143
- f.close()
154
+ finally:
155
+ t.close()
156
+ f.close()
144
157
  os.unlink(outfile)
145
158
  os.rename(tempfname, outfile)
146
159
 
@@ -154,6 +167,7 @@ def export_cgi_html(
154
167
  desc: str | None = None,
155
168
  zipfile: str | None = None,
156
169
  ) -> None:
170
+ """Write a CGI-style HTML fragment (Content-Type header + table) to a file or ZIP archive."""
157
171
  conf = _state.conf
158
172
 
159
173
  def write_table(f):
@@ -162,12 +176,12 @@ def export_cgi_html(
162
176
  f.write(f"<caption>{desc}</caption>\n")
163
177
  f.write("<thead><tr>")
164
178
  for h in hdrs:
165
- f.write(f"<th>{h}</th>")
179
+ f.write(f"<th>{html_mod.escape(str(h))}</th>")
166
180
  f.write("</tr></thead>\n<tbody>\n")
167
181
  for r in rows:
168
182
  f.write("<tr>")
169
183
  for v in r:
170
- f.write(f"<td>{v if v else ''}</td>")
184
+ f.write(f"<td>{html_mod.escape(str(v)) if v else ''}</td>")
171
185
  f.write("</tr>\n")
172
186
  f.write("</tbody>\n</table>\n")
173
187
 
@@ -184,10 +198,12 @@ def export_cgi_html(
184
198
  f = ef.open("wt")
185
199
  else:
186
200
  f = ZipWriter(zipfile, outfile, append)
187
- f.write("Content-Type: text/html\n\n")
188
- write_table(f)
189
- if outfile.lower() != "stdout":
190
- f.close()
201
+ try:
202
+ f.write("Content-Type: text/html\n\n")
203
+ write_table(f)
204
+ finally:
205
+ if outfile.lower() != "stdout":
206
+ f.close()
191
207
  else:
192
208
  if outfile == "stdout":
193
209
  f = sys.stdout
@@ -196,9 +212,11 @@ def export_cgi_html(
196
212
 
197
213
  ef = EncodedFile(outfile, conf.output_encoding)
198
214
  f = ef.open("a")
199
- write_table(f)
200
- if outfile.lower() != "stdout":
201
- f.close()
215
+ try:
216
+ write_table(f)
217
+ finally:
218
+ if outfile.lower() != "stdout":
219
+ f.close()
202
220
 
203
221
 
204
222
  def write_query_to_html(
@@ -209,12 +227,13 @@ def write_query_to_html(
209
227
  desc: str | None = None,
210
228
  zipfile: str | None = None,
211
229
  ) -> None:
230
+ """Execute a SELECT and write the result set as a standalone HTML document."""
212
231
  try:
213
232
  hdrs, rows = db.select_rowsource(select_stmt)
214
233
  except ErrInfo:
215
234
  raise
216
- except Exception:
217
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
235
+ except Exception as e:
236
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
218
237
  export_html(outfile, hdrs, rows, append, select_stmt, desc, zipfile=zipfile)
219
238
 
220
239
 
@@ -226,10 +245,11 @@ def write_query_to_cgi_html(
226
245
  desc: str | None = None,
227
246
  zipfile: str | None = None,
228
247
  ) -> None:
248
+ """Execute a SELECT and write the result set as a CGI-style HTML fragment."""
229
249
  try:
230
250
  hdrs, rows = db.select_rowsource(select_stmt)
231
251
  except ErrInfo:
232
252
  raise
233
- except Exception:
234
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
253
+ except Exception as e:
254
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
235
255
  export_cgi_html(outfile, hdrs, rows, append, select_stmt, desc, zipfile=zipfile)
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,16 +29,14 @@ 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)
35
36
  except ErrInfo:
36
37
  raise
37
- except Exception:
38
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
38
+ except Exception as e:
39
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
39
40
  if zipfile is None:
40
41
  filewriter_close(outfile)
41
42
  from execsql.utils.fileio import EncodedFile
@@ -48,20 +49,22 @@ def write_query_to_json(
48
49
  f = ef.open("wt")
49
50
  else:
50
51
  f = ZipWriter(zipfile, outfile, append)
51
- f.write("[")
52
- uhdrs = [str(h) for h in hdrs]
53
- first = True
54
- for row in rows:
55
- if first:
56
- f.write("\n")
57
- else:
58
- f.write(",\n")
59
- first = False
60
- dictdata = dict(zip(uhdrs, [str(v) if isinstance(v, str) else v for v in row]))
61
- jsondata = json.dumps(dictdata, separators=(",", ":"), default=str)
62
- f.write(str(jsondata))
63
- f.write("\n]\n")
64
- f.close()
52
+ try:
53
+ f.write("[")
54
+ uhdrs = [str(h) for h in hdrs]
55
+ first = True
56
+ for row in rows:
57
+ if first:
58
+ f.write("\n")
59
+ else:
60
+ f.write(",\n")
61
+ first = False
62
+ dictdata = dict(zip(uhdrs, [str(v) if isinstance(v, str) else v for v in row]))
63
+ jsondata = json.dumps(dictdata, separators=(",", ":"), default=str)
64
+ f.write(str(jsondata))
65
+ f.write("\n]\n")
66
+ finally:
67
+ f.close()
65
68
 
66
69
 
67
70
  def write_query_to_json_ts(
@@ -73,13 +76,14 @@ def write_query_to_json_ts(
73
76
  desc: str | None = None,
74
77
  zipfile: str | None = None,
75
78
  ) -> None:
79
+ """Execute a SELECT and write the result set as a JSON object with a top-level field-type schema."""
76
80
  conf = _state.conf
77
81
  try:
78
82
  hdrs, rows = db.select_rowsource(select_stmt)
79
83
  except ErrInfo:
80
84
  raise
81
- except Exception:
82
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
85
+ except Exception as e:
86
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
83
87
  max_col_idx = len(hdrs) - 1
84
88
  if zipfile is None:
85
89
  filewriter_close(outfile)
@@ -93,27 +97,30 @@ def write_query_to_json_ts(
93
97
  f = ef.open("wt")
94
98
  else:
95
99
  f = ZipWriter(zipfile, outfile, append)
96
- f.write("{\n")
97
- if desc is not None:
98
- f.write(f' "description": "{desc}",\n')
99
- f.write(' "fields": [\n')
100
- if write_types:
101
- # Scan the data to determine data types.
102
- tbl_desc = DataTable(hdrs, rows)
103
- # Write the column descriptions to the header.
104
- # Iterate over hdrs instead of tbl_desc.cols to preserve column order.
105
- for i, h in enumerate(hdrs):
106
- qcomma = "," if i < max_col_idx else ""
107
- c = [col for col in tbl_desc.cols if col.name == h][0]
108
- f.write(
109
- f' {{\n "name": "{c.name}",\n "title": "{c.name.capitalize().replace("_", " ")}",\n "type": "{_state.to_json_type[c.dt[1]]}"\n }}{qcomma}\n',
110
- )
111
- else:
112
- # Write the column descriptions to the header.
113
- for i, h in enumerate(hdrs):
114
- qcomma = "," if i < max_col_idx else ""
115
- f.write(
116
- f' {{\n "name": "{h}",\n "title": "{h.capitalize().replace("_", " ")}"\n }}{qcomma}\n',
117
- )
118
- f.write(" ]\n}\n")
119
- f.close()
100
+ try:
101
+ f.write("{\n")
102
+ if desc is not None:
103
+ escaped_desc = json.dumps(desc)
104
+ f.write(f' "description": {escaped_desc},\n')
105
+ f.write(' "fields": [\n')
106
+ if write_types:
107
+ # Scan the data to determine data types.
108
+ tbl_desc = DataTable(hdrs, rows)
109
+ # Write the column descriptions to the header.
110
+ # Iterate over hdrs instead of tbl_desc.cols to preserve column order.
111
+ for i, h in enumerate(hdrs):
112
+ qcomma = "," if i < max_col_idx else ""
113
+ c = [col for col in tbl_desc.cols if col.name == h][0]
114
+ f.write(
115
+ f' {{\n "name": "{c.name}",\n "title": "{c.name.capitalize().replace("_", " ")}",\n "type": "{_state.to_json_type[c.dt[1]]}"\n }}{qcomma}\n',
116
+ )
117
+ else:
118
+ # Write the column descriptions to the header.
119
+ for i, h in enumerate(hdrs):
120
+ qcomma = "," if i < max_col_idx else ""
121
+ f.write(
122
+ f' {{\n "name": "{h}",\n "title": "{h.capitalize().replace("_", " ")}"\n }}{qcomma}\n',
123
+ )
124
+ f.write(" ]\n}\n")
125
+ finally:
126
+ f.close()
@@ -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:
@@ -60,12 +63,14 @@ def export_latex(
60
63
  f = ef.open("wt")
61
64
  else:
62
65
  f = WriteableZipfile(zipfile).open(outfile, append)
63
- f.write("\\documentclass{article}\n")
64
- f.write("\\begin{document}\n")
65
- write_table(f)
66
- f.write("\\end{document}\n")
67
- if outfile.lower() != "stdout":
68
- f.close()
66
+ try:
67
+ f.write("\\documentclass{article}\n")
68
+ f.write("\\begin{document}\n")
69
+ write_table(f)
70
+ f.write("\\end{document}\n")
71
+ finally:
72
+ if outfile.lower() != "stdout":
73
+ f.close()
69
74
  else:
70
75
  if outfile.lower() == "stdout" or not Path(outfile).is_file():
71
76
  if outfile.lower() == "stdout":
@@ -75,9 +80,11 @@ def export_latex(
75
80
  else:
76
81
  ef = EncodedFile(outfile, conf.output_encoding)
77
82
  f = ef.open("wt")
78
- write_table(f)
79
- if outfile.lower() != "stdout":
80
- f.close()
83
+ try:
84
+ write_table(f)
85
+ finally:
86
+ if outfile.lower() != "stdout":
87
+ f.close()
81
88
  else:
82
89
  ef = EncodedFile(outfile, conf.output_encoding)
83
90
  f = ef.open("rt")
@@ -85,23 +92,25 @@ def export_latex(
85
92
  os.close(tempf) # Close the fd from mkstemp; EncodedFile opens its own handle
86
93
  tf = EncodedFile(tempfname, conf.output_encoding)
87
94
  t = tf.open("wt")
88
- remainder = ""
89
- for line in f:
90
- bodypos = line.lower().find("\\end{document}")
91
- if bodypos > -1:
92
- t.write(line[0:bodypos])
93
- t.write("\n")
94
- remainder = line[bodypos:]
95
- break
96
- else:
95
+ try:
96
+ remainder = ""
97
+ for line in f:
98
+ bodypos = line.lower().find("\\end{document}")
99
+ if bodypos > -1:
100
+ t.write(line[0:bodypos])
101
+ t.write("\n")
102
+ remainder = line[bodypos:]
103
+ break
104
+ else:
105
+ t.write(line)
106
+ t.write("\n")
107
+ write_table(t)
108
+ t.write(remainder)
109
+ for line in f:
97
110
  t.write(line)
98
- t.write("\n")
99
- write_table(t)
100
- t.write(remainder)
101
- for line in f:
102
- t.write(line)
103
- t.close()
104
- f.close()
111
+ finally:
112
+ t.close()
113
+ f.close()
105
114
  os.unlink(outfile)
106
115
  os.rename(tempfname, outfile)
107
116
 
@@ -114,12 +123,13 @@ def write_query_to_latex(
114
123
  desc: str | None = None,
115
124
  zipfile: Any | None = None,
116
125
  ) -> None:
126
+ """Execute a SELECT and write the result set as a LaTeX tabular table."""
117
127
  from execsql.utils.errors import exception_desc
118
128
 
119
129
  try:
120
130
  hdrs, rows = db.select_rowsource(select_stmt)
121
131
  except ErrInfo:
122
132
  raise
123
- except Exception:
124
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
133
+ except Exception as e:
134
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
125
135
  export_latex(outfile, hdrs, rows, append, select_stmt, desc, zipfile=zipfile)