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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. execsql/cli.py +322 -108
  2. execsql/config.py +134 -114
  3. execsql/db/access.py +89 -65
  4. execsql/db/base.py +97 -68
  5. execsql/db/dsn.py +45 -29
  6. execsql/db/duckdb.py +4 -5
  7. execsql/db/factory.py +27 -27
  8. execsql/db/firebird.py +30 -18
  9. execsql/db/mysql.py +38 -14
  10. execsql/db/oracle.py +58 -33
  11. execsql/db/postgres.py +68 -28
  12. execsql/db/sqlite.py +36 -27
  13. execsql/db/sqlserver.py +45 -30
  14. execsql/exceptions.py +68 -64
  15. execsql/exporters/__init__.py +1 -1
  16. execsql/exporters/base.py +42 -17
  17. execsql/exporters/delimited.py +60 -59
  18. execsql/exporters/duckdb.py +8 -12
  19. execsql/exporters/feather.py +32 -24
  20. execsql/exporters/html.py +33 -30
  21. execsql/exporters/json.py +18 -17
  22. execsql/exporters/latex.py +11 -13
  23. execsql/exporters/ods.py +50 -46
  24. execsql/exporters/parquet.py +32 -0
  25. execsql/exporters/pretty.py +16 -15
  26. execsql/exporters/raw.py +9 -11
  27. execsql/exporters/sqlite.py +38 -38
  28. execsql/exporters/templates.py +15 -72
  29. execsql/exporters/values.py +13 -12
  30. execsql/exporters/xls.py +26 -26
  31. execsql/exporters/xml.py +12 -12
  32. execsql/exporters/zip.py +0 -3
  33. execsql/gui/__init__.py +2 -2
  34. execsql/gui/console.py +0 -1
  35. execsql/gui/desktop.py +6 -7
  36. execsql/gui/tui.py +8 -14
  37. execsql/importers/base.py +6 -9
  38. execsql/importers/csv.py +10 -17
  39. execsql/importers/feather.py +16 -22
  40. execsql/importers/ods.py +3 -4
  41. execsql/importers/xls.py +5 -6
  42. execsql/metacommands/__init__.py +8 -8
  43. execsql/metacommands/conditions.py +41 -33
  44. execsql/metacommands/connect.py +113 -99
  45. execsql/metacommands/control.py +38 -26
  46. execsql/metacommands/data.py +35 -33
  47. execsql/metacommands/debug.py +13 -9
  48. execsql/metacommands/io.py +288 -229
  49. execsql/metacommands/prompt.py +179 -157
  50. execsql/metacommands/script_ext.py +11 -9
  51. execsql/metacommands/system.py +44 -25
  52. execsql/models.py +9 -16
  53. execsql/parser.py +10 -10
  54. execsql/script.py +183 -157
  55. execsql/state.py +170 -208
  56. execsql/types.py +46 -81
  57. execsql/utils/auth.py +114 -14
  58. execsql/utils/crypto.py +31 -4
  59. execsql/utils/datetime.py +7 -7
  60. execsql/utils/errors.py +34 -29
  61. execsql/utils/fileio.py +90 -55
  62. execsql/utils/gui.py +22 -23
  63. execsql/utils/mail.py +15 -17
  64. execsql/utils/numeric.py +2 -3
  65. execsql/utils/regex.py +9 -12
  66. execsql/utils/strings.py +10 -12
  67. execsql/utils/timer.py +0 -2
  68. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
  69. execsql2-2.1.2.dist-info/METADATA +300 -0
  70. execsql2-2.1.2.dist-info/RECORD +96 -0
  71. execsql2-2.0.1.dist-info/METADATA +0 -406
  72. execsql2-2.0.1.dist-info/RECORD +0 -95
  73. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
  74. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  75. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  76. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
  77. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
  78. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
  79. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
  80. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
  81. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  82. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  83. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
  84. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
  85. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  86. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  87. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
  88. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
  89. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
  90. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/NOTICE +0 -0
execsql/utils/errors.py CHANGED
@@ -15,12 +15,12 @@ throughout the codebase:
15
15
  - :func:`fatal_error` — logs a fatal error and calls :func:`exit_now`.
16
16
  """
17
17
 
18
- import datetime
19
18
  import os
20
19
  import sys
21
20
  import time
22
21
  import traceback
23
- from typing import Any, Optional
22
+ from pathlib import Path
23
+ from typing import Any
24
24
 
25
25
  import execsql.state as _state
26
26
  from execsql.exceptions import ErrInfo
@@ -59,7 +59,7 @@ def exception_desc() -> str:
59
59
  return f"{exc_type}: {exc_strval} in {exc_filename} on line {exc_lineno} of execsql."
60
60
 
61
61
 
62
- def exit_now(exit_status: int, errinfo: Optional[ErrInfo], logmsg: Optional[str] = None) -> None:
62
+ def exit_now(exit_status: int, errinfo: ErrInfo | None, logmsg: str | None = None) -> None:
63
63
  em = None
64
64
  if errinfo is not None:
65
65
  em = errinfo.write()
@@ -67,18 +67,19 @@ def exit_now(exit_status: int, errinfo: Optional[ErrInfo], logmsg: Optional[str]
67
67
  try:
68
68
  _state.err_halt_writespec.write()
69
69
  except Exception:
70
- _state.exec_log.log_status_error("Failed to write the ON ERROR_HALT WRITE message.")
71
- if exit_status == 2:
72
- # User canceled
73
- if _state.cancel_halt_writespec is not None:
74
- try:
75
- _state.cancel_halt_writespec.write()
76
- except Exception:
70
+ if _state.exec_log is not None:
71
+ _state.exec_log.log_status_error("Failed to write the ON ERROR_HALT WRITE message.")
72
+ # User canceled
73
+ if exit_status == 2 and _state.cancel_halt_writespec is not None:
74
+ try:
75
+ _state.cancel_halt_writespec.write()
76
+ except Exception:
77
+ if _state.exec_log is not None:
77
78
  _state.exec_log.log_status_error("Failed to write the ON CANCEL_HALT WRITE message.")
78
79
  # Defer import to avoid circular dependencies
79
80
  from execsql.utils.gui import gui_console_isrunning, gui_console_wait_user, gui_console_off
80
81
 
81
- if gui_console_isrunning():
82
+ if gui_console_isrunning() and _state.conf is not None:
82
83
  if errinfo is not None:
83
84
  if _state.conf.gui_wait_on_error_halt:
84
85
  gui_console_wait_user("Script error; close the console window to exit execsql.")
@@ -92,31 +93,36 @@ def exit_now(exit_status: int, errinfo: Optional[ErrInfo], logmsg: Optional[str]
92
93
  try:
93
94
  _state.err_halt_email.send()
94
95
  except Exception:
95
- _state.exec_log.log_status_error("Failed to send the ON ERROR_HALT EMAIL message.")
96
+ if _state.exec_log is not None:
97
+ _state.exec_log.log_status_error("Failed to send the ON ERROR_HALT EMAIL message.")
96
98
  if errinfo is not None and _state.err_halt_exec is not None:
99
+ from execsql.script import runscripts # deferred: errors.py ↔ script.py circular dep
100
+
97
101
  errexec = _state.err_halt_exec
98
102
  _state.err_halt_exec = None
99
103
  _state.commandliststack = []
100
104
  errexec.execute()
101
- _state.runscripts()
105
+ runscripts()
102
106
  if exit_status == 2 and _state.cancel_halt_mailspec is not None:
103
107
  try:
104
108
  _state.cancel_halt_mailspec.send()
105
109
  except Exception:
106
- _state.exec_log.log_status_error("Failed to send the ON CANCEL_HALT EMAIL message.")
110
+ if _state.exec_log is not None:
111
+ _state.exec_log.log_status_error("Failed to send the ON CANCEL_HALT EMAIL message.")
107
112
  if exit_status == 2 and _state.cancel_halt_exec is not None:
113
+ from execsql.script import runscripts # deferred: errors.py ↔ script.py circular dep
114
+
108
115
  cancelexec = _state.cancel_halt_exec
109
116
  _state.cancel_halt_exec = None
110
117
  _state.commandliststack = []
111
118
  cancelexec.execute()
112
- _state.runscripts()
113
- if exit_status > 0:
114
- if _state.exec_log:
115
- if logmsg:
116
- _state.exec_log.log_exit_error(logmsg)
117
- else:
118
- if em:
119
- _state.exec_log.log_exit_error(em)
119
+ runscripts()
120
+ if exit_status > 0 and _state.exec_log:
121
+ if logmsg:
122
+ _state.exec_log.log_exit_error(logmsg)
123
+ else:
124
+ if em:
125
+ _state.exec_log.log_exit_error(em)
120
126
  if _state.exec_log is not None:
121
127
  _state.exec_log.log_status_info(f"{_state.cmds_run} commands run")
122
128
  _state.exec_log.close()
@@ -126,19 +132,20 @@ def exit_now(exit_status: int, errinfo: Optional[ErrInfo], logmsg: Optional[str]
126
132
  sys.exit(exit_status)
127
133
 
128
134
 
129
- def fatal_error(error_msg: Optional[str] = None) -> None:
135
+ def fatal_error(error_msg: str | None = None) -> None:
130
136
  exit_now(1, ErrInfo("error", other_msg=error_msg))
131
137
 
132
138
 
133
139
  def write_warning(warning_msg: str) -> None:
134
- _state.exec_log.log_status_warning(warning_msg)
135
- if _state.conf.write_warnings:
140
+ if _state.exec_log is not None:
141
+ _state.exec_log.log_status_warning(warning_msg)
142
+ if _state.conf is not None and _state.conf.write_warnings and _state.output is not None:
136
143
  _state.output.write_err(f"**** Warning {warning_msg}")
137
144
 
138
145
 
139
146
  def file_size_date(filename: str) -> tuple:
140
147
  # Returns the file size and date (as string) of the given file.
141
- s_file = os.path.abspath(filename)
148
+ s_file = str(Path(filename).resolve())
142
149
  f_stat = os.stat(s_file)
143
150
  return f_stat.st_size, time.strftime("%Y-%m-%d %H:%M", time.gmtime(f_stat.st_mtime))
144
151
 
@@ -154,8 +161,6 @@ def chainfuncs(*funcs: Any) -> Any:
154
161
 
155
162
 
156
163
  def as_none(item: Any) -> Any:
157
- if isinstance(item, str) and len(item) == 0:
158
- return None
159
- elif isinstance(item, int) and item == 0:
164
+ if isinstance(item, str) and len(item) == 0 or isinstance(item, int) and item == 0:
160
165
  return None
161
166
  return item
execsql/utils/fileio.py CHANGED
@@ -26,30 +26,29 @@ import errno
26
26
  import io
27
27
  import multiprocessing
28
28
  import os
29
- import os.path
30
29
  import queue
30
+ from pathlib import Path
31
31
  import stat
32
32
  import sys
33
33
  import tempfile
34
- import threading
35
34
  import time
36
35
  from encodings.aliases import aliases as codec_dict
37
- from typing import Any, Dict, Optional
36
+ from typing import Any
38
37
 
39
38
  from execsql.exceptions import ErrInfo
40
39
 
41
40
 
42
41
  def make_export_dirs(outfile: str) -> None:
43
42
  if outfile.lower() != "stdout":
44
- output_dir = os.path.dirname(outfile)
43
+ output_dir = str(Path(outfile).parent)
45
44
  if output_dir != "":
46
- output_dir = os.path.normpath(output_dir)
45
+ output_dir = str(Path(output_dir))
47
46
  emsg = f"Can't create, or can't access, the directory {output_dir} to use for exported data."
48
47
  try:
49
48
  os.makedirs(output_dir)
50
49
  except OSError as e:
51
50
  if e.errno != errno.EEXIST:
52
- raise ErrInfo("exception", exception_msg=emsg)
51
+ raise ErrInfo("exception", exception_msg=emsg) from e
53
52
  except Exception:
54
53
  raise ErrInfo("exception", exception_msg=emsg)
55
54
 
@@ -62,8 +61,8 @@ def check_dir(filename: str) -> None:
62
61
  if conf.make_export_dirs:
63
62
  make_export_dirs(filename)
64
63
  else:
65
- dn = os.path.dirname(filename)
66
- if dn != "" and not os.path.exists(dn):
64
+ dn = str(Path(filename).parent)
65
+ if dn != "" and not Path(dn).exists():
67
66
  raise ErrInfo(type="error", other_msg=f"The directory for file '{filename}' does not exist.")
68
67
 
69
68
 
@@ -122,7 +121,7 @@ class FileWriter(multiprocessing.Process):
122
121
  self.open_start_time = time.time()
123
122
  if time.time() - self.open_start_time < self.open_timeout:
124
123
  try:
125
- self.handle = io.open(
124
+ self.handle = open( # noqa: SIM115
126
125
  file=self.filename,
127
126
  mode=self.openmode,
128
127
  encoding=self.encoding,
@@ -130,10 +129,11 @@ class FileWriter(multiprocessing.Process):
130
129
  )
131
130
  except Exception:
132
131
  self.status = self.STATUS_WAITING
133
- self.status = self.STATUS_OPEN
134
- self.openmode = "a" # Return to default for next open command.
135
- self.open_start_time = None
136
- self.fail_message_written = False
132
+ else:
133
+ self.status = self.STATUS_OPEN
134
+ self.openmode = "a" # Return to default for next open command.
135
+ self.open_start_time = None
136
+ self.fail_message_written = False
137
137
  else:
138
138
  self.status = self.STATUS_OPENFAILURE
139
139
  if not self.fail_message_written:
@@ -164,7 +164,7 @@ class FileWriter(multiprocessing.Process):
164
164
  def write(self, content: str) -> None:
165
165
  self.output_queue.appendleft(content)
166
166
  self.try_open()
167
- if self.STATUS_OPEN:
167
+ if self.status == self.STATUS_OPEN:
168
168
  self.write_queue()
169
169
 
170
170
  def __init__(
@@ -176,7 +176,7 @@ class FileWriter(multiprocessing.Process):
176
176
  ) -> None:
177
177
  # open_timeout is the maximum time, in seconds, that opening will be retried
178
178
  # if a file cannot be initially opened for writing.
179
- super(FileWriter, self).__init__()
179
+ super().__init__()
180
180
  self.input_queue = input_queue
181
181
  self.return_msg_queue = return_msg_queue
182
182
  self.file_encoding = file_encoding
@@ -200,11 +200,11 @@ class FileWriter(multiprocessing.Process):
200
200
  self.close_all()
201
201
 
202
202
  def close_all(self) -> None:
203
- for fc in self.files.values():
203
+ for fc in getattr(self, "files", {}).values():
204
204
  fc.close()
205
205
 
206
206
  def close_if_open(self, fn: str) -> None:
207
- filename = os.path.abspath(fn)
207
+ filename = str(Path(fn).resolve())
208
208
  if filename in self.files:
209
209
  fc = self.files[filename]
210
210
  fc.close()
@@ -228,7 +228,7 @@ class FileWriter(multiprocessing.Process):
228
228
  self.return_msg_queue.put(token)
229
229
 
230
230
  def open_as_new(self, fn: str) -> None:
231
- filename = os.path.abspath(fn)
231
+ filename = str(Path(fn).resolve())
232
232
  if filename not in self.files:
233
233
  self.files[filename] = self.FileControl(
234
234
  filename,
@@ -239,14 +239,14 @@ class FileWriter(multiprocessing.Process):
239
239
  fc.open_as_new()
240
240
 
241
241
  def status(self, fn: str) -> None:
242
- filename = os.path.abspath(fn)
242
+ filename = str(Path(fn).resolve())
243
243
  if filename in self.files:
244
244
  self.return_msg_queue.put(self.files[filename].status)
245
245
  else:
246
246
  self.return_msg_queue.put(self.FileControl.STATUS_UNOPENED)
247
247
 
248
248
  def write(self, fn: str, content: str) -> None:
249
- filename = os.path.abspath(fn)
249
+ filename = str(Path(fn).resolve())
250
250
  if filename not in self.files:
251
251
  self.files[filename] = self.FileControl(
252
252
  filename,
@@ -277,7 +277,7 @@ class FileWriter(multiprocessing.Process):
277
277
 
278
278
  # Subprocess objects for asynchronous writing to text files.
279
279
  # filewriter is initialized in main(), so it can take configurable arguments.
280
- filewriter: Optional[FileWriter] = None
280
+ filewriter: FileWriter | None = None
281
281
  fw_input: multiprocessing.Queue = multiprocessing.Queue()
282
282
  fw_output: multiprocessing.Queue = multiprocessing.Queue()
283
283
 
@@ -328,7 +328,7 @@ def filewriter_end() -> None:
328
328
  filewriter_shutdown()
329
329
  filewriter.join()
330
330
  except Exception:
331
- pass
331
+ pass # Best-effort cleanup at interpreter shutdown.
332
332
 
333
333
 
334
334
  class EncodedFile:
@@ -345,7 +345,7 @@ class EncodedFile:
345
345
  def detect_by_bom(path: str, default_enc: str) -> tuple:
346
346
  # Detect whether a file starts with a BOM, and if it does, return the encoding.
347
347
  # Otherwise, return the default encoding specified.
348
- with io.open(path, "rb") as f:
348
+ with open(path, "rb") as f:
349
349
  raw = f.read(4)
350
350
  for enc, boms, bom_len in (
351
351
  ("utf-8-sig", (codecs.BOM_UTF8,), 3),
@@ -356,7 +356,7 @@ class EncodedFile:
356
356
  return enc, bom_len
357
357
  return default_enc, 0
358
358
 
359
- if os.path.exists(filename):
359
+ if Path(filename).exists():
360
360
  self.encoding, self.bom_length = detect_by_bom(filename, file_encoding)
361
361
  self.fo = None
362
362
 
@@ -364,7 +364,7 @@ class EncodedFile:
364
364
  import execsql.state as _state
365
365
 
366
366
  conf = _state.conf
367
- self.fo = io.open(
367
+ self.fo = open( # noqa: SIM115
368
368
  file=self.filename,
369
369
  mode=mode,
370
370
  encoding=self.encoding,
@@ -389,10 +389,10 @@ class Logger:
389
389
  self,
390
390
  script_file_name: str,
391
391
  db_name: str,
392
- server_name: Optional[str],
392
+ server_name: str | None,
393
393
  cmdline_options: dict,
394
394
  user_logfile: bool = False,
395
- log_file_name: Optional[str] = None,
395
+ log_file_name: str | None = None,
396
396
  ) -> None:
397
397
  import getpass
398
398
  from execsql.utils.errors import exception_desc, exit_now, file_size_date
@@ -406,10 +406,11 @@ class Logger:
406
406
  self.log_file_name = log_file_name
407
407
  else:
408
408
  if user_logfile:
409
- self.log_file_name = os.path.expanduser(r"~/execsql.log")
409
+ self.log_file_name = str(Path("~/execsql.log").expanduser())
410
410
  else:
411
- self.log_file_name = os.path.join(os.getcwd(), "execsql.log")
412
- f_exists = os.path.isfile(self.log_file_name)
411
+ self.log_file_name = str(Path(os.getcwd()) / "execsql.log")
412
+ self._rotate_if_needed()
413
+ f_exists = Path(self.log_file_name).is_file()
413
414
  if f_exists:
414
415
  try:
415
416
  os.chmod(self.log_file_name, os.stat(self.log_file_name).st_mode | stat.S_IWRITE)
@@ -432,16 +433,23 @@ class Logger:
432
433
  )
433
434
  import datetime as _datetime
434
435
 
435
- self.run_id = _datetime.datetime.now().strftime("%Y%m%d_%H%M_%S")
436
+ _now = _datetime.datetime.now()
437
+ self.run_start = _now
438
+ self.run_id = _now.strftime("%Y%m%d_%H%M_%S_") + f"{_now.microsecond // 1000:03d}"
436
439
  self.user = getpass.getuser()
437
- sz, dt = file_size_date(script_file_name)
440
+ if script_file_name and Path(script_file_name).is_file():
441
+ sz, dt = file_size_date(script_file_name)
442
+ abs_script = str(Path(script_file_name).resolve())
443
+ else:
444
+ sz, dt = 0, ""
445
+ abs_script = script_file_name or "<inline>"
438
446
  msg = "run\t{}\t{}\t{}\t{}\t{}\t{}\n".format(
439
447
  self.run_id,
440
- os.path.abspath(script_file_name),
448
+ abs_script,
441
449
  dt,
442
450
  sz,
443
451
  self.user,
444
- ", ".join([f"{k}: {cmdline_options[k]}" for k in cmdline_options.keys()]),
452
+ ", ".join([f"{k}: {cmdline_options[k]}" for k in cmdline_options]),
445
453
  )
446
454
  self.writelog(msg)
447
455
  if server_name:
@@ -451,12 +459,35 @@ class Logger:
451
459
  self.writelog(msg)
452
460
  self.seq_no = 0
453
461
  atexit.register(self.close)
454
- self.exit_type = None
462
+ self.exit_type = "unknown"
455
463
  self.exit_scriptfile = None
456
464
  self.exit_lno = None
457
465
  self.exit_description = None
458
466
  atexit.register(self.log_exit)
459
467
 
468
+ def _ts(self) -> str:
469
+ import datetime as _datetime
470
+
471
+ return _datetime.datetime.now().isoformat(timespec="seconds")
472
+
473
+ def _rotate_if_needed(self) -> None:
474
+ try:
475
+ import execsql.state as _state
476
+
477
+ max_mb = getattr(_state.conf, "max_log_size_mb", 0) if _state.conf else 0
478
+ except Exception:
479
+ max_mb = 0
480
+ if max_mb > 0 and Path(self.log_file_name).is_file():
481
+ size_mb = Path(self.log_file_name).stat().st_size / (1024 * 1024)
482
+ if size_mb >= max_mb:
483
+ rotated = self.log_file_name + ".1"
484
+ try:
485
+ if Path(rotated).exists():
486
+ os.remove(rotated)
487
+ os.rename(self.log_file_name, rotated)
488
+ except Exception:
489
+ pass # Log rotation is best-effort; file may be locked.
490
+
460
491
  def writelog(self, msg: str) -> None:
461
492
  if self.log_file is not None:
462
493
  self.log_file.write(msg)
@@ -468,61 +499,61 @@ class Logger:
468
499
 
469
500
  def log_db_connect(self, db: Any) -> None:
470
501
  self.seq_no += 1
471
- msg = f"connect\t{self.run_id}\t{self.seq_no}\t{db.name()}\n"
502
+ msg = f"connect\t{self.run_id}\t{self.seq_no}\t{self._ts()}\t{db.name()}\n"
472
503
  self.writelog(msg)
473
504
 
474
505
  def log_action_export(self, line_no: int, query_name: str, export_file_name: str) -> None:
475
506
  self.seq_no += 1
476
- msg = f"action\t{self.run_id}\t{self.seq_no}\texport\t{line_no}\tQuery {query_name} exported to {export_file_name}\n"
507
+ msg = f"action\t{self.run_id}\t{self.seq_no}\t{self._ts()}\texport\t{line_no}\tQuery {query_name} exported to {export_file_name}\n"
477
508
  self.writelog(msg)
478
509
 
479
- def log_action_prompt_quit(self, line_no: int, do_quit: bool, msg: Optional[str]) -> None:
510
+ def log_action_prompt_quit(self, line_no: int, do_quit: bool, msg: str | None) -> None:
480
511
  # 'do_quit' is Boolean: True to quit, False if not.
481
512
  msg = None if not msg else msg.replace("\n", "")
482
513
  self.seq_no += 1
483
514
  descrip = '{} after prompt "{}"'.format("Quitting" if do_quit else "Continuing", msg)
484
- wmsg = f"action\t{self.run_id}\t{self.seq_no}\tprompt_quit\t{str(line_no) or ''}\t{descrip}\n"
515
+ wmsg = f"action\t{self.run_id}\t{self.seq_no}\t{self._ts()}\tprompt_quit\t{str(line_no) or ''}\t{descrip}\n"
485
516
  self.writelog(wmsg)
486
517
 
487
- def log_status_exception(self, msg: Optional[str]) -> None:
518
+ def log_status_exception(self, msg: str | None) -> None:
488
519
  msg = None if not msg else msg.replace("\n", "")
489
520
  self.seq_no += 1
490
- wmsg = f"status\t{self.run_id}\t{self.seq_no}\texception\t{msg or ''}\n"
521
+ wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\texception\t{msg or ''}\n"
491
522
  self.writelog(wmsg)
492
523
 
493
- def log_status_error(self, msg: Optional[str]) -> None:
524
+ def log_status_error(self, msg: str | None) -> None:
494
525
  msg = None if not msg else msg.replace("\n", "")
495
526
  self.seq_no += 1
496
- wmsg = f"status\t{self.run_id}\t{self.seq_no}\terror\t{msg or ''}\n"
527
+ wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\terror\t{msg or ''}\n"
497
528
  self.writelog(wmsg)
498
529
 
499
- def log_status_info(self, msg: Optional[str]) -> None:
530
+ def log_status_info(self, msg: str | None) -> None:
500
531
  msg = None if not msg else msg.replace("\n", "")
501
532
  self.seq_no += 1
502
- wmsg = f"status\t{self.run_id}\t{self.seq_no}\tinfo\t{msg or ''}\n"
533
+ wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\tinfo\t{msg or ''}\n"
503
534
  self.writelog(wmsg)
504
535
 
505
- def log_status_warning(self, msg: Optional[str]) -> None:
536
+ def log_status_warning(self, msg: str | None) -> None:
506
537
  msg = None if not msg else msg.replace("\n", "")
507
538
  self.seq_no += 1
508
- wmsg = f"status\t{self.run_id}\t{self.seq_no}\twarning\t{msg or ''}\n"
539
+ wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\twarning\t{msg or ''}\n"
509
540
  self.writelog(wmsg)
510
541
 
511
- def log_user_msg(self, msg: Optional[str]) -> None:
542
+ def log_user_msg(self, msg: str | None) -> None:
512
543
  msg = None if not msg else msg.replace("\n", "")
513
544
  if msg != "":
514
545
  self.seq_no += 1
515
- wmsg = f"user_msg\t{self.run_id}\t{self.seq_no}\tinfo\t{msg}\n"
546
+ wmsg = f"user_msg\t{self.run_id}\t{self.seq_no}\t{self._ts()}\tinfo\t{msg}\n"
516
547
  self.writelog(wmsg)
517
548
 
518
- def log_exit_end(self, script_file_name: Optional[str] = None, line_no: Optional[int] = None) -> None:
549
+ def log_exit_end(self, script_file_name: str | None = None, line_no: int | None = None) -> None:
519
550
  # Save values to be used by exit() function triggered on program exit
520
551
  self.exit_type = "end_of_script"
521
552
  self.exit_scriptfile = script_file_name
522
553
  self.exit_lno = line_no
523
554
  self.exit_description = None
524
555
 
525
- def log_exit_halt(self, script_file_name: str, line_no: int, msg: Optional[str] = None) -> None:
556
+ def log_exit_halt(self, script_file_name: str, line_no: int, msg: str | None = None) -> None:
526
557
  # Save values to be used by exit() function triggered on program exit
527
558
  self.exit_type = "halt"
528
559
  self.exit_scriptfile = script_file_name
@@ -536,7 +567,7 @@ class Logger:
536
567
  self.exit_lno = None
537
568
  self.exit_description = msg.replace("\n", "")
538
569
 
539
- def log_exit_error(self, msg: Optional[str]) -> None:
570
+ def log_exit_error(self, msg: str | None) -> None:
540
571
  # Save values to be used by exit() function triggered on program exit
541
572
  self.exit_type = "error"
542
573
  self.exit_scriptfile = None
@@ -544,12 +575,16 @@ class Logger:
544
575
  self.exit_description = None if not msg else msg.replace("\n", "")
545
576
 
546
577
  def log_exit(self) -> None:
547
- wmsg = "exit\t{}\t{}\t{}({})\t{}\n".format(
578
+ import datetime as _datetime
579
+
580
+ elapsed = (_datetime.datetime.now() - self.run_start).total_seconds()
581
+ wmsg = "exit\t{}\t{}\t{}({})\t{}\t{:.1f}s\n".format(
548
582
  self.run_id,
549
583
  self.exit_type,
550
584
  self.exit_scriptfile or "",
551
585
  str(self.exit_lno or ""),
552
586
  self.exit_description or "",
587
+ elapsed,
553
588
  )
554
589
  self.writelog(wmsg)
555
590
 
@@ -565,13 +600,13 @@ class TempFileMgr:
565
600
 
566
601
  def new_temp_fn(self) -> str:
567
602
  # Get a file object, get its name, and throw away the object
568
- fn = tempfile.NamedTemporaryFile().name
603
+ fn = tempfile.NamedTemporaryFile().name # noqa: SIM115
569
604
  self.temp_file_names.append(fn)
570
605
  return fn
571
606
 
572
607
  def remove_all(self) -> None:
573
608
  for fn in self.temp_file_names:
574
- if os.path.exists(fn):
609
+ if Path(fn).exists():
575
610
  try:
576
611
  # This may fail if the user has it open; let it go.
577
612
  os.unlink(fn)
execsql/utils/gui.py CHANGED
@@ -19,7 +19,7 @@ their bodies to avoid circular imports.
19
19
  from __future__ import annotations
20
20
 
21
21
  import sys
22
- from typing import Any, Optional
22
+ from typing import Any
23
23
 
24
24
  # ---------------------------------------------------------------------------
25
25
  # GUI command constants — used to identify request types in the GUI queue.
@@ -125,14 +125,14 @@ class EntrySpec:
125
125
  varname: str,
126
126
  label: str,
127
127
  required: bool = False,
128
- initial_value: Optional[str] = None,
129
- default_width: Optional[int] = None,
130
- default_height: Optional[int] = None,
131
- lookup_list: Optional[list] = None,
132
- form_column: Optional[int] = None,
133
- validation_regex: Optional[str] = None,
134
- validation_key_regex: Optional[str] = None,
135
- entry_type: Optional[str] = None,
128
+ initial_value: str | None = None,
129
+ default_width: int | None = None,
130
+ default_height: int | None = None,
131
+ lookup_list: list | None = None,
132
+ form_column: int | None = None,
133
+ validation_regex: str | None = None,
134
+ validation_key_regex: str | None = None,
135
+ entry_type: str | None = None,
136
136
  ) -> None:
137
137
  self.varname = varname
138
138
  self.name = varname # alias used by prompt.py result processing
@@ -146,7 +146,7 @@ class EntrySpec:
146
146
  self.validation_regex = validation_regex
147
147
  self.validation_key_regex = validation_key_regex
148
148
  self.entry_type = entry_type
149
- self.value: Optional[str] = None # populated by the backend after user input
149
+ self.value: str | None = None # populated by the backend after user input
150
150
 
151
151
 
152
152
  # ---------------------------------------------------------------------------
@@ -283,7 +283,7 @@ def gui_console_show() -> None:
283
283
  _active_backend.console_show()
284
284
 
285
285
 
286
- def gui_console_progress(num: float, total: Optional[float] = None) -> None:
286
+ def gui_console_progress(num: float, total: float | None = None) -> None:
287
287
  """Update the progress indicator in the console."""
288
288
  if _active_backend is not None:
289
289
  _active_backend.console_progress(num, total)
@@ -330,8 +330,8 @@ def gui_console_height() -> int:
330
330
  def gui_connect(
331
331
  alias: str,
332
332
  message: str,
333
- help_url: Optional[str] = None,
334
- cmd: Optional[str] = None,
333
+ help_url: str | None = None,
334
+ cmd: str | None = None,
335
335
  ) -> None:
336
336
  """Prompt the user to select a database connection.
337
337
 
@@ -383,7 +383,6 @@ def _apply_connect_result(alias: str, result: dict) -> None:
383
383
  database = result.get("database")
384
384
  db_file = result.get("db_file")
385
385
  username = result.get("username")
386
- conf = _state.conf
387
386
 
388
387
  if db_type == "p":
389
388
  db = db_Postgres(server, database, user=username, pw_needed=True)
@@ -411,9 +410,9 @@ def _apply_connect_result(alias: str, result: dict) -> None:
411
410
 
412
411
  def gui_credentials(
413
412
  message: str = "",
414
- username: Optional[str] = None,
415
- pwtext: Optional[str] = None,
416
- cmd: Optional[str] = None,
413
+ username: str | None = None,
414
+ pwtext: str | None = None,
415
+ cmd: str | None = None,
417
416
  ) -> None:
418
417
  """Prompt the user for credentials.
419
418
 
@@ -479,9 +478,9 @@ def get_yn_win(prompt: str) -> bool:
479
478
 
480
479
  def pause(
481
480
  text: str,
482
- action: Optional[str] = None,
483
- countdown: Optional[float] = None,
484
- timeunit: Optional[str] = None,
481
+ action: str | None = None,
482
+ countdown: float | None = None,
483
+ timeunit: str | None = None,
485
484
  ) -> int:
486
485
  """Display a pause message and wait for the user.
487
486
 
@@ -505,9 +504,9 @@ def pause(
505
504
 
506
505
  def pause_win(
507
506
  text: str,
508
- action: Optional[str] = None,
509
- countdown: Optional[float] = None,
510
- timeunit: Optional[str] = None,
507
+ action: str | None = None,
508
+ countdown: float | None = None,
509
+ timeunit: str | None = None,
511
510
  ) -> int:
512
511
  """GUI pause dialog — falls back to terminal in headless mode."""
513
512
  return pause(text, action=action, countdown=countdown, timeunit=timeunit)