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.
- execsql/cli/__init__.py +436 -0
- execsql/cli/dsn.py +86 -0
- execsql/cli/help.py +140 -0
- execsql/{cli.py → cli/run.py} +14 -589
- execsql/config.py +65 -1
- execsql/db/access.py +27 -15
- execsql/db/base.py +328 -215
- execsql/db/dsn.py +10 -5
- execsql/db/duckdb.py +6 -2
- execsql/db/factory.py +21 -0
- execsql/db/firebird.py +27 -19
- execsql/db/mysql.py +12 -7
- execsql/db/oracle.py +15 -11
- execsql/db/postgres.py +31 -16
- execsql/db/sqlite.py +15 -11
- execsql/db/sqlserver.py +16 -5
- execsql/exceptions.py +25 -7
- execsql/exporters/base.py +12 -1
- execsql/exporters/delimited.py +80 -35
- execsql/exporters/duckdb.py +6 -2
- execsql/exporters/feather.py +10 -6
- execsql/exporters/html.py +89 -69
- execsql/exporters/json.py +52 -45
- execsql/exporters/latex.py +37 -27
- execsql/exporters/ods.py +32 -11
- execsql/exporters/parquet.py +5 -2
- execsql/exporters/pretty.py +16 -9
- execsql/exporters/raw.py +22 -16
- execsql/exporters/sqlite.py +6 -2
- execsql/exporters/templates.py +39 -21
- execsql/exporters/values.py +26 -20
- execsql/exporters/xls.py +30 -11
- execsql/exporters/xml.py +31 -13
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +6 -4
- execsql/importers/csv.py +8 -6
- execsql/importers/feather.py +6 -4
- execsql/importers/ods.py +6 -4
- execsql/importers/xls.py +6 -4
- execsql/metacommands/__init__.py +208 -1548
- execsql/metacommands/conditions.py +101 -27
- execsql/metacommands/control.py +8 -4
- execsql/metacommands/data.py +6 -6
- execsql/metacommands/debug.py +6 -2
- execsql/metacommands/dispatch.py +2011 -0
- execsql/metacommands/io.py +67 -1310
- execsql/metacommands/io_export.py +442 -0
- execsql/metacommands/io_fileops.py +287 -0
- execsql/metacommands/io_import.py +398 -0
- execsql/metacommands/io_write.py +248 -0
- execsql/metacommands/prompt.py +22 -66
- execsql/metacommands/system.py +7 -2
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/py.typed +0 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +184 -402
- execsql/script/variables.py +281 -0
- execsql/types.py +49 -20
- execsql/utils/auth.py +2 -0
- execsql/utils/crypto.py +4 -6
- execsql/utils/datetime.py +1 -0
- execsql/utils/errors.py +11 -0
- execsql/utils/fileio.py +33 -8
- execsql/utils/gui.py +46 -0
- execsql/utils/mail.py +7 -17
- execsql/utils/numeric.py +2 -0
- execsql/utils/regex.py +9 -0
- execsql/utils/strings.py +16 -0
- execsql/utils/timer.py +2 -0
- execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/METADATA +13 -6
- execsql2-2.4.0.dist-info/RECORD +108 -0
- execsql2-2.1.2.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.1.2.dist-info/RECORD +0 -96
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
execsql/config.py
CHANGED
|
@@ -23,12 +23,25 @@ from pathlib import Path
|
|
|
23
23
|
from execsql.exceptions import ConfigError
|
|
24
24
|
from execsql.utils.crypto import Encrypt
|
|
25
25
|
|
|
26
|
+
__all__ = [
|
|
27
|
+
"StatObj",
|
|
28
|
+
"ConfigData",
|
|
29
|
+
"WriteHooks",
|
|
30
|
+
]
|
|
31
|
+
|
|
26
32
|
|
|
27
33
|
class StatObj:
|
|
34
|
+
"""Lightweight container for runtime status flags used by the metacommand engine.
|
|
35
|
+
|
|
36
|
+
Tracks error conditions, halt-on-error policy, dialog cancellation state,
|
|
37
|
+
and the current batch nesting level.
|
|
38
|
+
"""
|
|
39
|
+
|
|
28
40
|
# A generic object to maintain status indicators. These status
|
|
29
41
|
# indicators are primarily those used in the metacommand
|
|
30
42
|
# environment rather than for the program as a whole.
|
|
31
43
|
def __init__(self) -> None:
|
|
44
|
+
"""Initialise all status flags to their default (non-error) values."""
|
|
32
45
|
self.halt_on_err = True
|
|
33
46
|
self.sql_error = False
|
|
34
47
|
self.halt_on_metacommand_err = True
|
|
@@ -42,6 +55,13 @@ class StatObj:
|
|
|
42
55
|
|
|
43
56
|
|
|
44
57
|
class ConfigData:
|
|
58
|
+
"""Reads and merges ``execsql.conf`` INI files, exposing all options as attributes.
|
|
59
|
+
|
|
60
|
+
Searches system, user, script-directory, and working-directory locations
|
|
61
|
+
(in that order) and applies each file's settings cumulatively so that
|
|
62
|
+
later files override earlier ones.
|
|
63
|
+
"""
|
|
64
|
+
|
|
45
65
|
config_file_name = "execsql.conf"
|
|
46
66
|
_CONNECT_SECTION = "connect"
|
|
47
67
|
_ENCODING_SECTION = "encoding"
|
|
@@ -55,6 +75,15 @@ class ConfigData:
|
|
|
55
75
|
_INCLUDE_OPT_SECTION = "include_optional"
|
|
56
76
|
|
|
57
77
|
def __init__(self, script_path: str, variable_pool: object) -> None:
|
|
78
|
+
"""Load and merge all discoverable execsql.conf files for the given script path.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
script_path: Directory of the running script; used to locate a
|
|
82
|
+
script-adjacent ``execsql.conf``.
|
|
83
|
+
variable_pool: Substitution-variable registry used to expand
|
|
84
|
+
``config_file`` path values and to populate ``[variables]``
|
|
85
|
+
sections.
|
|
86
|
+
"""
|
|
58
87
|
self.db_type = "a"
|
|
59
88
|
self.server = None
|
|
60
89
|
self.port = None
|
|
@@ -106,10 +135,12 @@ class ConfigData:
|
|
|
106
135
|
self.quote_all_text = False
|
|
107
136
|
self.import_row_buffer = 1000
|
|
108
137
|
self.import_progress_interval = 0
|
|
138
|
+
self.show_progress = False
|
|
109
139
|
self.export_row_buffer = 1000
|
|
110
140
|
self.template_processor = None
|
|
111
141
|
self.tee_write_log = False
|
|
112
142
|
self.log_datavars = True
|
|
143
|
+
self.log_sql = False
|
|
113
144
|
self.max_log_size_mb = 0
|
|
114
145
|
self.smtp_host = None
|
|
115
146
|
self.smtp_port = None
|
|
@@ -143,7 +174,7 @@ class ConfigData:
|
|
|
143
174
|
cp.read(configfile)
|
|
144
175
|
if cp.has_option(self._CONNECT_SECTION, "db_type"):
|
|
145
176
|
t = cp.get(self._CONNECT_SECTION, "db_type").lower()
|
|
146
|
-
if len(t) != 1 or t not in ("a", "
|
|
177
|
+
if len(t) != 1 or t not in ("a", "d", "f", "k", "l", "m", "o", "p", "s"):
|
|
147
178
|
raise ConfigError(f"Invalid database type: {t}")
|
|
148
179
|
self.db_type = t
|
|
149
180
|
if cp.has_option(self._CONNECT_SECTION, "server"):
|
|
@@ -291,6 +322,11 @@ class ConfigData:
|
|
|
291
322
|
self.import_progress_interval = cp.getint(self._INPUT_SECTION, "import_progress_interval")
|
|
292
323
|
except Exception as e:
|
|
293
324
|
raise ConfigError("Invalid argument for import_progress_interval.") from e
|
|
325
|
+
if cp.has_option(self._INPUT_SECTION, "show_progress"):
|
|
326
|
+
try:
|
|
327
|
+
self.show_progress = cp.getboolean(self._INPUT_SECTION, "show_progress")
|
|
328
|
+
except Exception as e:
|
|
329
|
+
raise ConfigError("Invalid argument for show_progress.") from e
|
|
294
330
|
if cp.has_option(self._INPUT_SECTION, "access_use_numeric"):
|
|
295
331
|
try:
|
|
296
332
|
self.access_use_numeric = cp.getboolean(self._INPUT_SECTION, "access_use_numeric")
|
|
@@ -466,6 +502,11 @@ class ConfigData:
|
|
|
466
502
|
self.log_datavars = cp.getboolean(self._CONFIG_SECTION, "log_datavars")
|
|
467
503
|
except Exception as e:
|
|
468
504
|
raise ConfigError("Invalid argument to log_datavars setting.") from e
|
|
505
|
+
if cp.has_option(self._CONFIG_SECTION, "log_sql"):
|
|
506
|
+
try:
|
|
507
|
+
self.log_sql = cp.getboolean(self._CONFIG_SECTION, "log_sql")
|
|
508
|
+
except Exception as e:
|
|
509
|
+
raise ConfigError("Invalid argument to log_sql setting.") from e
|
|
469
510
|
if cp.has_option(self._CONFIG_SECTION, "max_log_size_mb"):
|
|
470
511
|
try:
|
|
471
512
|
self.max_log_size_mb = cp.getint(self._CONFIG_SECTION, "max_log_size_mb")
|
|
@@ -535,6 +576,12 @@ class ConfigData:
|
|
|
535
576
|
|
|
536
577
|
|
|
537
578
|
class WriteHooks:
|
|
579
|
+
"""Thin wrapper around stdout/stderr that supports GUI or test-harness redirection.
|
|
580
|
+
|
|
581
|
+
Each output hook is a callable that accepts a single string. When a hook
|
|
582
|
+
is ``None`` the default ``sys.stdout`` or ``sys.stderr`` is used.
|
|
583
|
+
"""
|
|
584
|
+
|
|
538
585
|
def __repr__(self) -> str:
|
|
539
586
|
return f"WriteHooks({self.write_func!r}, {self.err_func!r}, {self.status_func!r})"
|
|
540
587
|
|
|
@@ -544,6 +591,16 @@ class WriteHooks:
|
|
|
544
591
|
error_output_func: object = None,
|
|
545
592
|
status_output_func: object = None,
|
|
546
593
|
) -> None:
|
|
594
|
+
"""Store optional hook callables; ``None`` means use the default stream.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
standard_output_func: Callable to receive standard-output text, or
|
|
598
|
+
``None`` to use ``sys.stdout``.
|
|
599
|
+
error_output_func: Callable to receive error-output text, or
|
|
600
|
+
``None`` to use ``sys.stderr``.
|
|
601
|
+
status_output_func: Callable to receive status-line text, or
|
|
602
|
+
``None`` to suppress.
|
|
603
|
+
"""
|
|
547
604
|
# Arguments should be functions that take a single string and
|
|
548
605
|
# write it to the desired destination. Both stdout and stderr can be hooked.
|
|
549
606
|
# If a hook function is not specified, the default of stdout or stderr will
|
|
@@ -555,22 +612,27 @@ class WriteHooks:
|
|
|
555
612
|
self.tee_stderr = True
|
|
556
613
|
|
|
557
614
|
def reset(self) -> None:
|
|
615
|
+
"""Reset both output hooks to ``None``, restoring stdout/stderr behaviour."""
|
|
558
616
|
# Resets output to stdout and stderr.
|
|
559
617
|
self.write_func = None
|
|
560
618
|
self.err_func = None
|
|
561
619
|
|
|
562
620
|
def redir_stdout(self, standard_output_func: object) -> None:
|
|
621
|
+
"""Replace the standard-output hook with the given callable."""
|
|
563
622
|
self.write_func = standard_output_func
|
|
564
623
|
|
|
565
624
|
def redir_stderr(self, error_output_func: object, tee: bool = True) -> None:
|
|
625
|
+
"""Replace the error-output hook and optionally keep tee-to-stderr behaviour."""
|
|
566
626
|
self.err_func = error_output_func
|
|
567
627
|
self.tee_stderr = tee
|
|
568
628
|
|
|
569
629
|
def redir(self, standard_output_func: object, error_output_func: object) -> None:
|
|
630
|
+
"""Redirect both stdout and stderr hooks in one call."""
|
|
570
631
|
self.redir_stdout(standard_output_func)
|
|
571
632
|
self.redir_stderr(error_output_func)
|
|
572
633
|
|
|
573
634
|
def write(self, strval: str) -> None:
|
|
635
|
+
"""Write a string to the standard-output hook, or to sys.stdout if unset."""
|
|
574
636
|
if self.write_func:
|
|
575
637
|
self.write_func(strval)
|
|
576
638
|
else:
|
|
@@ -578,6 +640,7 @@ class WriteHooks:
|
|
|
578
640
|
sys.stdout.flush()
|
|
579
641
|
|
|
580
642
|
def write_err(self, strval: str) -> None:
|
|
643
|
+
"""Write an error string to the error-output hook, or to sys.stderr if unset."""
|
|
581
644
|
if strval[-1] != "\n":
|
|
582
645
|
strval += "\n"
|
|
583
646
|
if self.err_func:
|
|
@@ -590,5 +653,6 @@ class WriteHooks:
|
|
|
590
653
|
sys.stderr.flush()
|
|
591
654
|
|
|
592
655
|
def write_status(self, strval: str) -> None:
|
|
656
|
+
"""Forward a status string to the status hook if one is registered."""
|
|
593
657
|
if self.status_func:
|
|
594
658
|
self.status_func(strval)
|
execsql/db/access.py
CHANGED
|
@@ -20,8 +20,12 @@ from execsql.utils.errors import exception_desc, fatal_error
|
|
|
20
20
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
21
21
|
import execsql.state as _state
|
|
22
22
|
|
|
23
|
+
__all__ = ["AccessDatabase"]
|
|
24
|
+
|
|
23
25
|
|
|
24
26
|
class AccessDatabase(Database):
|
|
27
|
+
"""MS Access adapter connecting to .mdb/.accdb files via DAO (win32com) with pyodbc fallback."""
|
|
28
|
+
|
|
25
29
|
# Regex for the 'create temporary view' SQL extension
|
|
26
30
|
temp_rx = re.compile(
|
|
27
31
|
r"^\s*create(?:\s+or\s+replace)?(\s+temp(?:orary)?)?\s+(?:(view|query))\s+(\w+) as\s+",
|
|
@@ -68,7 +72,7 @@ class AccessDatabase(Database):
|
|
|
68
72
|
self.dao_conn = None
|
|
69
73
|
self.conn = None # ODBC connection
|
|
70
74
|
self.paramstr = "?"
|
|
71
|
-
self.dt_cast = dict(
|
|
75
|
+
self.dt_cast = dict(self.dt_cast) # Copy the lazy-initialized default before overriding.
|
|
72
76
|
self.dt_cast[datetime.date] = self.as_datetime
|
|
73
77
|
self.dt_cast[datetime.datetime] = self.as_datetime
|
|
74
78
|
self.dt_cast[int] = self.int_or_bool
|
|
@@ -103,9 +107,13 @@ class AccessDatabase(Database):
|
|
|
103
107
|
try:
|
|
104
108
|
self.conn = pyodbc.connect(connstr)
|
|
105
109
|
except Exception:
|
|
106
|
-
_state.exec_log.log_status_info(
|
|
110
|
+
_state.exec_log.log_status_info(
|
|
111
|
+
f"Could not connect via ODBC using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
112
|
+
)
|
|
107
113
|
else:
|
|
108
|
-
_state.exec_log.log_status_info(
|
|
114
|
+
_state.exec_log.log_status_info(
|
|
115
|
+
f"Connected via ODBC using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
116
|
+
)
|
|
109
117
|
self.jet4 = jet4flag
|
|
110
118
|
return True
|
|
111
119
|
return False
|
|
@@ -290,17 +298,17 @@ class AccessDatabase(Database):
|
|
|
290
298
|
self.dao_flush_check()
|
|
291
299
|
curs = self.cursor()
|
|
292
300
|
try:
|
|
293
|
-
sql =
|
|
294
|
-
curs.execute(sql)
|
|
301
|
+
sql = "select Name from MSysObjects where Name=? And Type In (1,4,6);"
|
|
302
|
+
curs.execute(sql, (table_name,))
|
|
295
303
|
except ErrInfo:
|
|
296
304
|
raise
|
|
297
|
-
except Exception:
|
|
305
|
+
except Exception as e:
|
|
298
306
|
raise ErrInfo(
|
|
299
307
|
type="db",
|
|
300
308
|
command_text=sql,
|
|
301
309
|
exception_msg=exception_desc(),
|
|
302
310
|
other_msg=f"Failure on test for existence of Access table {table_name}",
|
|
303
|
-
)
|
|
311
|
+
) from e
|
|
304
312
|
rows = curs.fetchall()
|
|
305
313
|
return len(rows) > 0
|
|
306
314
|
|
|
@@ -312,7 +320,9 @@ class AccessDatabase(Database):
|
|
|
312
320
|
) -> bool:
|
|
313
321
|
self.dao_flush_check()
|
|
314
322
|
curs = self.cursor()
|
|
315
|
-
|
|
323
|
+
quoted_col = self.quote_identifier(column_name)
|
|
324
|
+
quoted_tbl = self.quote_identifier(table_name)
|
|
325
|
+
sql = f"select top 1 {quoted_col} from {quoted_tbl};"
|
|
316
326
|
try:
|
|
317
327
|
curs.execute(sql)
|
|
318
328
|
except Exception:
|
|
@@ -322,24 +332,25 @@ class AccessDatabase(Database):
|
|
|
322
332
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
323
333
|
self.dao_flush_check()
|
|
324
334
|
curs = self.cursor()
|
|
325
|
-
|
|
335
|
+
quoted_tbl = self.quote_identifier(table_name)
|
|
336
|
+
curs.execute(f"select top 1 * from {quoted_tbl};")
|
|
326
337
|
return [d[0] for d in curs.description]
|
|
327
338
|
|
|
328
339
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
329
340
|
self.dao_flush_check()
|
|
330
341
|
curs = self.cursor()
|
|
331
342
|
try:
|
|
332
|
-
sql =
|
|
333
|
-
curs.execute(sql)
|
|
343
|
+
sql = "select Name from MSysObjects where Name=? And Type = 5;"
|
|
344
|
+
curs.execute(sql, (view_name,))
|
|
334
345
|
except ErrInfo:
|
|
335
346
|
raise
|
|
336
|
-
except Exception:
|
|
347
|
+
except Exception as e:
|
|
337
348
|
raise ErrInfo(
|
|
338
349
|
type="db",
|
|
339
350
|
command_text=sql,
|
|
340
351
|
exception_msg=exception_desc(),
|
|
341
352
|
other_msg=f"Test for existence of Access view/query {view_name}",
|
|
342
|
-
)
|
|
353
|
+
) from e
|
|
343
354
|
rows = curs.fetchall()
|
|
344
355
|
return len(rows) > 0
|
|
345
356
|
|
|
@@ -356,7 +367,7 @@ class AccessDatabase(Database):
|
|
|
356
367
|
|
|
357
368
|
if val is None or (isinstance(val, _state.stringtypes) and len(val) == 0):
|
|
358
369
|
return None
|
|
359
|
-
if isinstance(val,
|
|
370
|
+
if isinstance(val, datetime.date | datetime.datetime | datetime.time):
|
|
360
371
|
return val
|
|
361
372
|
else:
|
|
362
373
|
try:
|
|
@@ -414,5 +425,6 @@ class AccessDatabase(Database):
|
|
|
414
425
|
with open(file_name, "rb") as f:
|
|
415
426
|
filedata = f.read()
|
|
416
427
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
417
|
-
|
|
428
|
+
quoted_col = self.quote_identifier(column_name)
|
|
429
|
+
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
418
430
|
self.cursor().execute(sql, (pyodbc.Binary(filedata),))
|