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.
- execsql/cli.py +322 -108
- execsql/config.py +134 -114
- execsql/db/access.py +89 -65
- execsql/db/base.py +97 -68
- execsql/db/dsn.py +45 -29
- execsql/db/duckdb.py +4 -5
- execsql/db/factory.py +27 -27
- execsql/db/firebird.py +30 -18
- execsql/db/mysql.py +38 -14
- execsql/db/oracle.py +58 -33
- execsql/db/postgres.py +68 -28
- execsql/db/sqlite.py +36 -27
- execsql/db/sqlserver.py +45 -30
- execsql/exceptions.py +68 -64
- execsql/exporters/__init__.py +1 -1
- execsql/exporters/base.py +42 -17
- execsql/exporters/delimited.py +60 -59
- execsql/exporters/duckdb.py +8 -12
- execsql/exporters/feather.py +32 -24
- execsql/exporters/html.py +33 -30
- execsql/exporters/json.py +18 -17
- execsql/exporters/latex.py +11 -13
- execsql/exporters/ods.py +50 -46
- execsql/exporters/parquet.py +32 -0
- execsql/exporters/pretty.py +16 -15
- execsql/exporters/raw.py +9 -11
- execsql/exporters/sqlite.py +38 -38
- execsql/exporters/templates.py +15 -72
- execsql/exporters/values.py +13 -12
- execsql/exporters/xls.py +26 -26
- execsql/exporters/xml.py +12 -12
- execsql/exporters/zip.py +0 -3
- execsql/gui/__init__.py +2 -2
- execsql/gui/console.py +0 -1
- execsql/gui/desktop.py +6 -7
- execsql/gui/tui.py +8 -14
- execsql/importers/base.py +6 -9
- execsql/importers/csv.py +10 -17
- execsql/importers/feather.py +16 -22
- execsql/importers/ods.py +3 -4
- execsql/importers/xls.py +5 -6
- execsql/metacommands/__init__.py +8 -8
- execsql/metacommands/conditions.py +41 -33
- execsql/metacommands/connect.py +113 -99
- execsql/metacommands/control.py +38 -26
- execsql/metacommands/data.py +35 -33
- execsql/metacommands/debug.py +13 -9
- execsql/metacommands/io.py +288 -229
- execsql/metacommands/prompt.py +179 -157
- execsql/metacommands/script_ext.py +11 -9
- execsql/metacommands/system.py +44 -25
- execsql/models.py +9 -16
- execsql/parser.py +10 -10
- execsql/script.py +183 -157
- execsql/state.py +170 -208
- execsql/types.py +46 -81
- execsql/utils/auth.py +114 -14
- execsql/utils/crypto.py +31 -4
- execsql/utils/datetime.py +7 -7
- execsql/utils/errors.py +34 -29
- execsql/utils/fileio.py +90 -55
- execsql/utils/gui.py +22 -23
- execsql/utils/mail.py +15 -17
- execsql/utils/numeric.py +2 -3
- execsql/utils/regex.py +9 -12
- execsql/utils/strings.py +10 -12
- execsql/utils/timer.py +0 -2
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
- execsql2-2.1.2.dist-info/METADATA +300 -0
- execsql2-2.1.2.dist-info/RECORD +96 -0
- execsql2-2.0.1.dist-info/METADATA +0 -406
- execsql2-2.0.1.dist-info/RECORD +0 -95
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
|
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:
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
113
|
-
if exit_status > 0:
|
|
114
|
-
if
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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:
|
|
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
|
|
135
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
43
|
+
output_dir = str(Path(outfile).parent)
|
|
45
44
|
if output_dir != "":
|
|
46
|
-
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 =
|
|
66
|
-
if dn != "" and not
|
|
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 =
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
392
|
+
server_name: str | None,
|
|
393
393
|
cmdline_options: dict,
|
|
394
394
|
user_logfile: bool = False,
|
|
395
|
-
log_file_name:
|
|
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 =
|
|
409
|
+
self.log_file_name = str(Path("~/execsql.log").expanduser())
|
|
410
410
|
else:
|
|
411
|
-
self.log_file_name =
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
129
|
-
default_width:
|
|
130
|
-
default_height:
|
|
131
|
-
lookup_list:
|
|
132
|
-
form_column:
|
|
133
|
-
validation_regex:
|
|
134
|
-
validation_key_regex:
|
|
135
|
-
entry_type:
|
|
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:
|
|
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:
|
|
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:
|
|
334
|
-
cmd:
|
|
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:
|
|
415
|
-
pwtext:
|
|
416
|
-
cmd:
|
|
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:
|
|
483
|
-
countdown:
|
|
484
|
-
timeunit:
|
|
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:
|
|
509
|
-
countdown:
|
|
510
|
-
timeunit:
|
|
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)
|