execsql2 2.21.2__py3-none-any.whl → 2.22.1__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/api.py +29 -4
- execsql/cli/__init__.py +1 -1
- execsql/cli/run.py +37 -17
- execsql/config.py +40 -22
- execsql/data/execsql.conf.template +6 -0
- execsql/db/access.py +6 -5
- execsql/db/base.py +23 -5
- execsql/db/dsn.py +2 -0
- execsql/db/duckdb.py +1 -1
- execsql/db/mysql.py +6 -8
- execsql/db/sqlite.py +1 -1
- execsql/exceptions.py +14 -11
- execsql/exporters/base.py +1 -1
- execsql/exporters/delimited.py +22 -16
- execsql/exporters/duckdb.py +6 -1
- execsql/exporters/feather.py +1 -1
- execsql/exporters/json.py +5 -3
- execsql/exporters/latex.py +2 -2
- execsql/exporters/markdown.py +1 -0
- execsql/exporters/ods.py +7 -3
- execsql/exporters/raw.py +8 -8
- execsql/exporters/sqlite.py +11 -1
- execsql/exporters/templates.py +2 -1
- execsql/exporters/xls.py +10 -10
- execsql/exporters/xml.py +1 -0
- execsql/exporters/yaml.py +1 -0
- execsql/exporters/zip.py +9 -7
- execsql/format.py +6 -6
- execsql/gui/console.py +3 -3
- execsql/gui/desktop.py +28 -10
- execsql/gui/tui.py +8 -6
- execsql/importers/base.py +14 -8
- execsql/importers/csv.py +8 -3
- execsql/importers/xls.py +1 -0
- execsql/metacommands/conditions.py +14 -14
- execsql/metacommands/connect.py +2 -2
- execsql/metacommands/control.py +1 -1
- execsql/metacommands/data.py +2 -1
- execsql/metacommands/io_export.py +4 -0
- execsql/metacommands/io_fileops.py +85 -39
- execsql/metacommands/io_import.py +1 -0
- execsql/metacommands/io_write.py +1 -1
- execsql/metacommands/prompt.py +24 -22
- execsql/metacommands/system.py +14 -1
- execsql/models.py +56 -17
- execsql/parser.py +8 -8
- execsql/plugins.py +1 -1
- execsql/script/engine.py +4 -4
- execsql/script/executor.py +69 -28
- execsql/script/parser.py +13 -12
- execsql/script/variables.py +4 -2
- execsql/state.pyi +122 -0
- execsql/types.py +31 -26
- execsql/utils/auth.py +6 -2
- execsql/utils/datetime.py +3 -3
- execsql/utils/errors.py +7 -5
- execsql/utils/fileio.py +80 -32
- execsql/utils/gui.py +15 -11
- execsql/utils/mail.py +9 -2
- execsql/utils/numeric.py +1 -1
- execsql/utils/regex.py +9 -9
- {execsql2-2.21.2.dist-info → execsql2-2.22.1.dist-info}/METADATA +114 -132
- execsql2-2.22.1.dist-info/RECORD +123 -0
- execsql2-2.21.2.dist-info/RECORD +0 -122
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.21.2.dist-info → execsql2-2.22.1.dist-info}/WHEEL +0 -0
- {execsql2-2.21.2.dist-info → execsql2-2.22.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.21.2.dist-info → execsql2-2.22.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.21.2.dist-info → execsql2-2.22.1.dist-info}/licenses/NOTICE +0 -0
execsql/utils/fileio.py
CHANGED
|
@@ -27,13 +27,15 @@ import io
|
|
|
27
27
|
import multiprocessing
|
|
28
28
|
import os
|
|
29
29
|
import queue
|
|
30
|
+
import re
|
|
30
31
|
from pathlib import Path
|
|
31
32
|
import stat
|
|
32
33
|
import sys
|
|
33
34
|
import tempfile
|
|
34
35
|
import time
|
|
36
|
+
from collections.abc import Callable
|
|
35
37
|
from encodings.aliases import aliases as codec_dict
|
|
36
|
-
from typing import Any
|
|
38
|
+
from typing import Any, cast
|
|
37
39
|
|
|
38
40
|
from execsql.exceptions import ErrInfo
|
|
39
41
|
|
|
@@ -76,7 +78,9 @@ def check_dir(filename: str) -> None:
|
|
|
76
78
|
import execsql.state as _state
|
|
77
79
|
|
|
78
80
|
conf = _state.conf
|
|
79
|
-
|
|
81
|
+
# conf is None when called via the library API (e.g. a db factory
|
|
82
|
+
# function) before run() has initialized global state.
|
|
83
|
+
if conf is not None and conf.make_export_dirs:
|
|
80
84
|
make_export_dirs(filename)
|
|
81
85
|
else:
|
|
82
86
|
dn = str(Path(filename).parent)
|
|
@@ -207,11 +211,11 @@ class FileWriter(multiprocessing.Process):
|
|
|
207
211
|
self.open_timeout = open_timeout
|
|
208
212
|
self.encoding = encoding
|
|
209
213
|
self.openmode = "a"
|
|
210
|
-
self.handle = None
|
|
214
|
+
self.handle: io.TextIOWrapper | None = None
|
|
211
215
|
self.status = self.STATUS_UNOPENED
|
|
212
|
-
self.open_start_time = None
|
|
216
|
+
self.open_start_time: float | None = None
|
|
213
217
|
self.fail_message_written = False
|
|
214
|
-
self.output_queue: collections.deque = collections.deque()
|
|
218
|
+
self.output_queue: collections.deque[str] = collections.deque()
|
|
215
219
|
self.close_after_write = False
|
|
216
220
|
|
|
217
221
|
def __del__(self) -> None:
|
|
@@ -221,6 +225,7 @@ class FileWriter(multiprocessing.Process):
|
|
|
221
225
|
pass # Best-effort cleanup at interpreter shutdown.
|
|
222
226
|
|
|
223
227
|
def write_queue(self) -> None:
|
|
228
|
+
assert self.handle is not None
|
|
224
229
|
while len(self.output_queue) > 0:
|
|
225
230
|
m = self.output_queue.pop()
|
|
226
231
|
self.handle.write(m)
|
|
@@ -238,11 +243,14 @@ class FileWriter(multiprocessing.Process):
|
|
|
238
243
|
self.open_start_time = time.time()
|
|
239
244
|
if time.time() - self.open_start_time < self.open_timeout:
|
|
240
245
|
try:
|
|
241
|
-
self.handle =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
+
self.handle = cast(
|
|
247
|
+
io.TextIOWrapper,
|
|
248
|
+
open( # noqa: SIM115
|
|
249
|
+
file=self.filename,
|
|
250
|
+
mode=self.openmode,
|
|
251
|
+
encoding=self.encoding,
|
|
252
|
+
errors="backslashreplace",
|
|
253
|
+
),
|
|
246
254
|
)
|
|
247
255
|
except Exception:
|
|
248
256
|
self.status = self.STATUS_WAITING
|
|
@@ -298,10 +306,10 @@ class FileWriter(multiprocessing.Process):
|
|
|
298
306
|
self.return_msg_queue = return_msg_queue
|
|
299
307
|
self.file_encoding = file_encoding
|
|
300
308
|
self.open_timeout = open_timeout
|
|
301
|
-
self.files: dict = {}
|
|
309
|
+
self.files: dict[str, FileWriter.FileControl] = {}
|
|
302
310
|
self.active = True
|
|
303
311
|
# Functions in execvec must be in the same order as the CMD enums.
|
|
304
|
-
self.execvec = (
|
|
312
|
+
self.execvec: tuple[Callable[..., None], ...] = (
|
|
305
313
|
self.write,
|
|
306
314
|
self.close_if_open,
|
|
307
315
|
self.close_all,
|
|
@@ -424,7 +432,7 @@ def filewriter_filestatus(filename: str) -> int:
|
|
|
424
432
|
if not _writer_alive():
|
|
425
433
|
return FileWriter.FileControl.STATUS_CLOSED
|
|
426
434
|
fw_input.put((FileWriter.CMD_GET_STATUS, (filename,)))
|
|
427
|
-
return fw_output.get()
|
|
435
|
+
return cast(int, fw_output.get())
|
|
428
436
|
|
|
429
437
|
|
|
430
438
|
def filewriter_write(filename: str, message: str) -> None:
|
|
@@ -527,19 +535,23 @@ class EncodedFile:
|
|
|
527
535
|
|
|
528
536
|
if Path(filename).exists():
|
|
529
537
|
self.encoding, self.bom_length = detect_by_bom(filename, file_encoding)
|
|
530
|
-
self.fo = None
|
|
538
|
+
self.fo: io.TextIOWrapper | None = None
|
|
531
539
|
|
|
532
540
|
def open(self, mode: str = "r") -> io.TextIOWrapper:
|
|
533
541
|
import execsql.state as _state
|
|
534
542
|
|
|
535
543
|
conf = _state.conf
|
|
536
|
-
self.fo =
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
544
|
+
self.fo = cast(
|
|
545
|
+
io.TextIOWrapper,
|
|
546
|
+
open( # noqa: SIM115
|
|
547
|
+
file=self.filename,
|
|
548
|
+
mode=mode,
|
|
549
|
+
encoding=self.encoding,
|
|
550
|
+
errors=conf.enc_err_disposition,
|
|
551
|
+
newline=None,
|
|
552
|
+
),
|
|
542
553
|
)
|
|
554
|
+
assert self.fo is not None
|
|
543
555
|
return self.fo
|
|
544
556
|
|
|
545
557
|
def close(self) -> None:
|
|
@@ -557,6 +569,7 @@ class EncodedFile:
|
|
|
557
569
|
# before entering the ``with`` block.
|
|
558
570
|
if self.fo is None:
|
|
559
571
|
self.open("r")
|
|
572
|
+
assert self.fo is not None
|
|
560
573
|
return self.fo
|
|
561
574
|
|
|
562
575
|
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
@@ -565,7 +578,15 @@ class EncodedFile:
|
|
|
565
578
|
|
|
566
579
|
class Logger:
|
|
567
580
|
# A custom logger for execsql that writes several different types of messages to a log file.
|
|
568
|
-
log_file = None
|
|
581
|
+
log_file: io.TextIOWrapper | None = None
|
|
582
|
+
_COMMON_SECRET_PATTERNS = (
|
|
583
|
+
re.compile(r"(?i)(://[^/\s:@]+:)([^@\s]+)(@)"),
|
|
584
|
+
re.compile(r"(?i)\b(password|passwd|token|secret|api[_-]?key|private[_-]?key|webhook)(\s*[=:]\s*)([^\s,;]+)"),
|
|
585
|
+
re.compile(r"\bsk-(?:live|test)-[A-Za-z0-9_-]+\b"),
|
|
586
|
+
re.compile(r"\bAKIA[A-Z0-9]{12,}\b"),
|
|
587
|
+
re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}\b"),
|
|
588
|
+
re.compile(r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b"),
|
|
589
|
+
)
|
|
569
590
|
|
|
570
591
|
def __repr__(self) -> str:
|
|
571
592
|
return f"Logger({self.script_file_name!r}, {self.db_name!r}, {self.server_name!r}, {self.cmdline_options!r}, {self.log_file_name!r})"
|
|
@@ -587,6 +608,7 @@ class Logger:
|
|
|
587
608
|
self.db_name = db_name
|
|
588
609
|
self.server_name = server_name
|
|
589
610
|
self.cmdline_options = cmdline_options
|
|
611
|
+
self._redaction_values: list[str] = []
|
|
590
612
|
if log_file_name:
|
|
591
613
|
self.log_file_name = log_file_name
|
|
592
614
|
else:
|
|
@@ -630,6 +652,9 @@ class Logger:
|
|
|
630
652
|
self.run_start = _now
|
|
631
653
|
self.run_id = _now.strftime("%Y%m%d_%H%M_%S_") + f"{_now.microsecond // 1000:03d}"
|
|
632
654
|
self.user = getpass.getuser()
|
|
655
|
+
sz: int
|
|
656
|
+
dt: str
|
|
657
|
+
abs_script: str
|
|
633
658
|
if script_file_name and Path(script_file_name).is_file():
|
|
634
659
|
sz, dt = file_size_date(script_file_name)
|
|
635
660
|
abs_script = str(Path(script_file_name).resolve())
|
|
@@ -653,9 +678,9 @@ class Logger:
|
|
|
653
678
|
self.seq_no = 0
|
|
654
679
|
atexit.register(self.close)
|
|
655
680
|
self.exit_type = "unknown"
|
|
656
|
-
self.exit_scriptfile = None
|
|
657
|
-
self.exit_lno = None
|
|
658
|
-
self.exit_description = None
|
|
681
|
+
self.exit_scriptfile: str | None = None
|
|
682
|
+
self.exit_lno: int | None = None
|
|
683
|
+
self.exit_description: str | None = None
|
|
659
684
|
atexit.register(self.log_exit)
|
|
660
685
|
|
|
661
686
|
def _ts(self) -> str:
|
|
@@ -663,6 +688,29 @@ class Logger:
|
|
|
663
688
|
|
|
664
689
|
return _datetime.datetime.now().isoformat(timespec="seconds")
|
|
665
690
|
|
|
691
|
+
def add_redaction_value(self, value: str | None) -> None:
|
|
692
|
+
if value is None:
|
|
693
|
+
return
|
|
694
|
+
value = str(value)
|
|
695
|
+
if not value or value in self._redaction_values:
|
|
696
|
+
return
|
|
697
|
+
self._redaction_values.append(value)
|
|
698
|
+
|
|
699
|
+
def _redact(self, msg: str | None) -> str | None:
|
|
700
|
+
if msg is None:
|
|
701
|
+
return None
|
|
702
|
+
redacted = str(msg)
|
|
703
|
+
for value in sorted(self._redaction_values, key=len, reverse=True):
|
|
704
|
+
redacted = redacted.replace(value, "***")
|
|
705
|
+
for pattern in self._COMMON_SECRET_PATTERNS:
|
|
706
|
+
if pattern.pattern.startswith("(?i)(://"):
|
|
707
|
+
redacted = pattern.sub(r"\1***\3", redacted)
|
|
708
|
+
elif "password|passwd|token" in pattern.pattern:
|
|
709
|
+
redacted = pattern.sub(r"\1\2***", redacted)
|
|
710
|
+
else:
|
|
711
|
+
redacted = pattern.sub("***", redacted)
|
|
712
|
+
return redacted
|
|
713
|
+
|
|
666
714
|
def _rotate_if_needed(self) -> None:
|
|
667
715
|
try:
|
|
668
716
|
import execsql.state as _state
|
|
@@ -701,32 +749,32 @@ class Logger:
|
|
|
701
749
|
self.writelog(msg)
|
|
702
750
|
|
|
703
751
|
def log_status_exception(self, msg: str | None) -> None:
|
|
704
|
-
msg = None if not msg else msg.replace("\n", "")
|
|
752
|
+
msg = self._redact(None if not msg else msg.replace("\n", ""))
|
|
705
753
|
self.seq_no += 1
|
|
706
754
|
wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\texception\t{msg or ''}\n"
|
|
707
755
|
self.writelog(wmsg)
|
|
708
756
|
|
|
709
757
|
def log_status_error(self, msg: str | None) -> None:
|
|
710
|
-
msg = None if not msg else msg.replace("\n", "")
|
|
758
|
+
msg = self._redact(None if not msg else msg.replace("\n", ""))
|
|
711
759
|
self.seq_no += 1
|
|
712
760
|
wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\terror\t{msg or ''}\n"
|
|
713
761
|
self.writelog(wmsg)
|
|
714
762
|
|
|
715
763
|
def log_status_info(self, msg: str | None) -> None:
|
|
716
|
-
msg = None if not msg else msg.replace("\n", "")
|
|
764
|
+
msg = self._redact(None if not msg else msg.replace("\n", ""))
|
|
717
765
|
self.seq_no += 1
|
|
718
766
|
wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\tinfo\t{msg or ''}\n"
|
|
719
767
|
self.writelog(wmsg)
|
|
720
768
|
|
|
721
769
|
def log_status_warning(self, msg: str | None) -> None:
|
|
722
|
-
msg = None if not msg else msg.replace("\n", "")
|
|
770
|
+
msg = self._redact(None if not msg else msg.replace("\n", ""))
|
|
723
771
|
self.seq_no += 1
|
|
724
772
|
wmsg = f"status\t{self.run_id}\t{self.seq_no}\t{self._ts()}\twarning\t{msg or ''}\n"
|
|
725
773
|
self.writelog(wmsg)
|
|
726
774
|
|
|
727
775
|
def log_sql_query(self, sql: str, db_name: str, line_no: int | None = None) -> None:
|
|
728
776
|
"""Log an executed SQL statement for audit purposes."""
|
|
729
|
-
cleaned = sql.replace("\n", " ").replace("\t", " ")
|
|
777
|
+
cleaned = self._redact(sql.replace("\n", " ").replace("\t", " ")) or ""
|
|
730
778
|
if len(cleaned) > 2000:
|
|
731
779
|
cleaned = cleaned[:2000] + "..."
|
|
732
780
|
self.seq_no += 1
|
|
@@ -735,7 +783,7 @@ class Logger:
|
|
|
735
783
|
self.writelog(wmsg)
|
|
736
784
|
|
|
737
785
|
def log_user_msg(self, msg: str | None) -> None:
|
|
738
|
-
msg = None if not msg else msg.replace("\n", "")
|
|
786
|
+
msg = self._redact(None if not msg else msg.replace("\n", ""))
|
|
739
787
|
if msg != "":
|
|
740
788
|
self.seq_no += 1
|
|
741
789
|
wmsg = f"user_msg\t{self.run_id}\t{self.seq_no}\t{self._ts()}\tinfo\t{msg}\n"
|
|
@@ -760,14 +808,14 @@ class Logger:
|
|
|
760
808
|
self.exit_type = "exception"
|
|
761
809
|
self.exit_scriptfile = None
|
|
762
810
|
self.exit_lno = None
|
|
763
|
-
self.exit_description = msg.replace("\n", "")
|
|
811
|
+
self.exit_description = self._redact(msg.replace("\n", ""))
|
|
764
812
|
|
|
765
813
|
def log_exit_error(self, msg: str | None) -> None:
|
|
766
814
|
# Save values to be used by exit() function triggered on program exit
|
|
767
815
|
self.exit_type = "error"
|
|
768
816
|
self.exit_scriptfile = None
|
|
769
817
|
self.exit_lno = None
|
|
770
|
-
self.exit_description = None if not msg else msg.replace("\n", "")
|
|
818
|
+
self.exit_description = self._redact(None if not msg else msg.replace("\n", ""))
|
|
771
819
|
|
|
772
820
|
def log_exit(self) -> None:
|
|
773
821
|
import datetime as _datetime
|
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, cast
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
25
|
# Constants
|
|
@@ -221,7 +221,7 @@ def gui_console_isrunning() -> bool:
|
|
|
221
221
|
clicking the Tkinter window's close button).
|
|
222
222
|
"""
|
|
223
223
|
if _active_backend is not None:
|
|
224
|
-
return _active_backend.query_console({}).get("console_running", False)
|
|
224
|
+
return cast(bool, _active_backend.query_console({}).get("console_running", False))
|
|
225
225
|
return _console_running
|
|
226
226
|
|
|
227
227
|
|
|
@@ -245,6 +245,7 @@ def enable_gui() -> None:
|
|
|
245
245
|
# Already initialised — nothing to do.
|
|
246
246
|
if _active_backend is not None:
|
|
247
247
|
return
|
|
248
|
+
backend: Any
|
|
248
249
|
|
|
249
250
|
framework = _state.conf.gui_framework if _state.conf else "tkinter"
|
|
250
251
|
|
|
@@ -457,11 +458,12 @@ def _apply_connect_result(alias: str, result: dict) -> None:
|
|
|
457
458
|
)
|
|
458
459
|
|
|
459
460
|
db_type = result.get("db_type", "l")
|
|
460
|
-
server = result.get("server")
|
|
461
|
-
database = result.get("database")
|
|
462
|
-
db_file = result.get("db_file")
|
|
463
|
-
username = result.get("username")
|
|
461
|
+
server = str(result.get("server") or "")
|
|
462
|
+
database = str(result.get("database") or "")
|
|
463
|
+
db_file = str(result.get("db_file") or "")
|
|
464
|
+
username = str(result.get("username") or "")
|
|
464
465
|
|
|
466
|
+
db: Any
|
|
465
467
|
if db_type == "p":
|
|
466
468
|
db = db_Postgres(server, database, user=username, pw_needed=True)
|
|
467
469
|
elif db_type == "s":
|
|
@@ -613,7 +615,7 @@ def pause(
|
|
|
613
615
|
old_settings = termios.tcgetattr(fd)
|
|
614
616
|
|
|
615
617
|
timer_handler: TimerHandler | None = None
|
|
616
|
-
old_alarm = signal.SIG_DFL
|
|
618
|
+
old_alarm: Any = signal.SIG_DFL
|
|
617
619
|
|
|
618
620
|
try:
|
|
619
621
|
tty.setraw(fd)
|
|
@@ -667,6 +669,8 @@ def pause_win(
|
|
|
667
669
|
"""
|
|
668
670
|
try:
|
|
669
671
|
import msvcrt
|
|
672
|
+
|
|
673
|
+
msvcrt_mod: Any = msvcrt
|
|
670
674
|
except ImportError:
|
|
671
675
|
# Not on Windows — delegate to the POSIX implementation.
|
|
672
676
|
return pause(text, action=action, countdown=countdown, timeunit=timeunit)
|
|
@@ -690,8 +694,8 @@ def pause_win(
|
|
|
690
694
|
"{:8.1f} |{}{}|\r".format(remaining, "+" * bar_left, "-" * (bar_len - bar_left)),
|
|
691
695
|
)
|
|
692
696
|
sys.stdout.flush()
|
|
693
|
-
if
|
|
694
|
-
ch =
|
|
697
|
+
if msvcrt_mod.kbhit():
|
|
698
|
+
ch = msvcrt_mod.getwch()
|
|
695
699
|
if ch in ("\r", "\n"):
|
|
696
700
|
_clear_progress_line()
|
|
697
701
|
return 0
|
|
@@ -703,8 +707,8 @@ def pause_win(
|
|
|
703
707
|
sys.stderr.write("Press Enter to continue, Esc to quit...\n")
|
|
704
708
|
sys.stderr.flush()
|
|
705
709
|
while True:
|
|
706
|
-
if
|
|
707
|
-
ch =
|
|
710
|
+
if msvcrt_mod.kbhit():
|
|
711
|
+
ch = msvcrt_mod.getwch()
|
|
708
712
|
if ch in ("\r", "\n"):
|
|
709
713
|
return 0
|
|
710
714
|
if ch == "\x1b":
|
execsql/utils/mail.py
CHANGED
|
@@ -17,6 +17,7 @@ from email.mime.base import MIMEBase
|
|
|
17
17
|
from email.mime.multipart import MIMEMultipart
|
|
18
18
|
from email.mime.text import MIMEText
|
|
19
19
|
from pathlib import Path
|
|
20
|
+
from typing import cast
|
|
20
21
|
|
|
21
22
|
import execsql.state as _state
|
|
22
23
|
from execsql.exceptions import ErrInfo
|
|
@@ -48,6 +49,7 @@ class Mailer:
|
|
|
48
49
|
self.close()
|
|
49
50
|
|
|
50
51
|
def __init__(self) -> None:
|
|
52
|
+
self.smtpconn: smtplib.SMTP
|
|
51
53
|
conf = _state.conf
|
|
52
54
|
if conf.smtp_host is None:
|
|
53
55
|
raise ErrInfo(type="error", other_msg="Can't send email; the email host is not configured.")
|
|
@@ -138,14 +140,16 @@ class MailSpec:
|
|
|
138
140
|
self.sent = False
|
|
139
141
|
|
|
140
142
|
@staticmethod
|
|
141
|
-
def _expand(text: str) -> str:
|
|
143
|
+
def _expand(text: str | None) -> str | None:
|
|
142
144
|
"""Expand local and global substitution variables in *text*."""
|
|
145
|
+
if text is None:
|
|
146
|
+
return None
|
|
143
147
|
result = text
|
|
144
148
|
localvars = _state.current_localvars()
|
|
145
149
|
if localvars is not None:
|
|
146
150
|
result, _ = localvars.substitute_all(result)
|
|
147
151
|
result, _ = _state.subvars.substitute_all(result)
|
|
148
|
-
return result
|
|
152
|
+
return cast(str, result)
|
|
149
153
|
|
|
150
154
|
def send(self) -> None:
|
|
151
155
|
if self.repeatable or not self.sent:
|
|
@@ -153,6 +157,9 @@ class MailSpec:
|
|
|
153
157
|
send_from = self._expand(self.send_from)
|
|
154
158
|
send_to = self._expand(self.send_to)
|
|
155
159
|
subject = self._expand(self.subject)
|
|
160
|
+
assert send_from is not None
|
|
161
|
+
assert send_to is not None
|
|
162
|
+
assert subject is not None
|
|
156
163
|
msg_content = self._expand(self.msg_content)
|
|
157
164
|
content_filename = self._expand(self.content_filename)
|
|
158
165
|
attach_filename = self._expand(self.attach_filename)
|
execsql/utils/numeric.py
CHANGED
|
@@ -45,7 +45,7 @@ def leading_zero_num(dataval: Any) -> bool:
|
|
|
45
45
|
|
|
46
46
|
def as_numeric(strval: Any) -> int | float | None:
|
|
47
47
|
# Converts the given value to an int, a float, or None.
|
|
48
|
-
if
|
|
48
|
+
if isinstance(strval, int | float):
|
|
49
49
|
return strval
|
|
50
50
|
if not isinstance(strval, str):
|
|
51
51
|
strval = str(strval)
|
execsql/utils/regex.py
CHANGED
|
@@ -24,7 +24,7 @@ __all__ = [
|
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def ins_rxs(rx_list: tuple, fragment1: object, fragment2: object) -> tuple:
|
|
27
|
+
def ins_rxs(rx_list: tuple[str, ...], fragment1: object, fragment2: object) -> tuple[str, ...]:
|
|
28
28
|
# Returns a tuple of all strings consisting of elements of the 'rx_list' tuple
|
|
29
29
|
# inserted between 'fragment1' and 'fragment2'. The fragments may themselves
|
|
30
30
|
# be tuples.
|
|
@@ -34,7 +34,7 @@ def ins_rxs(rx_list: tuple, fragment1: object, fragment2: object) -> tuple:
|
|
|
34
34
|
fragment2 = ("",)
|
|
35
35
|
if not isinstance(fragment2, tuple):
|
|
36
36
|
fragment2 = (fragment2,)
|
|
37
|
-
rv = []
|
|
37
|
+
rv: list[str] = []
|
|
38
38
|
for te in rx_list:
|
|
39
39
|
for f1 in fragment1:
|
|
40
40
|
for f2 in fragment2:
|
|
@@ -42,12 +42,12 @@ def ins_rxs(rx_list: tuple, fragment1: object, fragment2: object) -> tuple:
|
|
|
42
42
|
return tuple(rv)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def ins_quoted_rx(fragment1: object, fragment2: object, rx: str) -> tuple:
|
|
45
|
+
def ins_quoted_rx(fragment1: object, fragment2: object, rx: str) -> tuple[str, ...]:
|
|
46
46
|
return ins_rxs((rx, rf'"{rx}"'), fragment1, fragment2)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def ins_schema_rxs(fragment1: object, fragment2: object, suffix: str | None = None) -> tuple:
|
|
50
|
-
schema_exprs = (
|
|
49
|
+
def ins_schema_rxs(fragment1: object, fragment2: object, suffix: str | None = None) -> tuple[str, ...]:
|
|
50
|
+
schema_exprs: tuple[str, ...] = (
|
|
51
51
|
r'"(?P<schema>[A-Za-z0-9_\- ]+)"',
|
|
52
52
|
r"(?P<schema>[A-Za-z0-9_\-]+)",
|
|
53
53
|
r"\[(?P<schema>[A-Za-z0-9_\- ]+)\]",
|
|
@@ -57,8 +57,8 @@ def ins_schema_rxs(fragment1: object, fragment2: object, suffix: str | None = No
|
|
|
57
57
|
return ins_rxs(schema_exprs, fragment1, fragment2)
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
def ins_table_rxs(fragment1: object, fragment2: object, suffix: str | None = None) -> tuple:
|
|
61
|
-
tbl_exprs = (
|
|
60
|
+
def ins_table_rxs(fragment1: object, fragment2: object, suffix: str | None = None) -> tuple[str, ...]:
|
|
61
|
+
tbl_exprs: tuple[str, ...] = (
|
|
62
62
|
r'(?:"(?P<schema>[A-Za-z0-9_\- ]+)"\.)?"(?P<table>[A-Za-z0-9_\-\# ]+)"',
|
|
63
63
|
r"(?:(?P<schema>[A-Za-z0-9_\-]+)\.)?(?P<table>[A-Za-z0-9_\-\#]+)",
|
|
64
64
|
r'(?:"(?P<schema>[A-Za-z0-9_\- ]+)"\.)?(?P<table>[A-Za-z0-9_\-\#]+)',
|
|
@@ -75,7 +75,7 @@ def ins_table_rxs(fragment1: object, fragment2: object, suffix: str | None = Non
|
|
|
75
75
|
return ins_rxs(tbl_exprs, fragment1, fragment2)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def ins_table_list_rxs(fragment1: object, fragment2: object) -> tuple:
|
|
78
|
+
def ins_table_list_rxs(fragment1: object, fragment2: object) -> tuple[str, ...]:
|
|
79
79
|
tbl_exprs = (
|
|
80
80
|
r'(?:(?P<tables>(?:"[A-Za-z0-9_\- ]+"\.)?"[A-Za-z0-9_\-\# ]+"(?:\s*,\s*(?:"[A-Za-z0-9_\- ]+"\.)?"[A-Za-z0-9_\-\# ]+")*))',
|
|
81
81
|
r"(?:(?P<tables>(?:[A-Za-z0-9_\-]+\.)?[A-Za-z0-9_\-\#]+(?:\s*,\s*(?:[A-Za-z0-9_\-]+\.)?[A-Za-z0-9_\-\#]+)*))",
|
|
@@ -83,7 +83,7 @@ def ins_table_list_rxs(fragment1: object, fragment2: object) -> tuple:
|
|
|
83
83
|
return ins_rxs(tbl_exprs, fragment1, fragment2)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def ins_fn_rxs(fragment1: object, fragment2: object, symbolicname: str = "filename") -> tuple:
|
|
86
|
+
def ins_fn_rxs(fragment1: object, fragment2: object, symbolicname: str = "filename") -> tuple[str, ...]:
|
|
87
87
|
if os.name == "posix":
|
|
88
88
|
fns = (
|
|
89
89
|
rf"(?P<{symbolicname}>[\w\.\-\\\/\'~`!@#$^&()+={{}}\[\]:;,]*[\w\.\-\\\/\'~`!@#$^&(+={{}}\[\]:;,])",
|