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/db/sqlite.py
CHANGED
|
@@ -9,10 +9,9 @@ files via the Python standard library ``sqlite3`` module. Corresponds to
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import datetime
|
|
12
|
-
import io
|
|
13
12
|
import re
|
|
14
13
|
from decimal import Decimal
|
|
15
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
16
15
|
|
|
17
16
|
from execsql.db.base import Database
|
|
18
17
|
from execsql.exceptions import ErrInfo
|
|
@@ -20,8 +19,11 @@ from execsql.utils.errors import exception_desc, fatal_error
|
|
|
20
19
|
import execsql.state as _state
|
|
21
20
|
|
|
22
21
|
|
|
22
|
+
DEFAULT_CONNECT_TIMEOUT = 30 # seconds
|
|
23
|
+
|
|
24
|
+
|
|
23
25
|
class SQLiteDatabase(Database):
|
|
24
|
-
def __init__(self, SQLite_fn: str) -> None:
|
|
26
|
+
def __init__(self, SQLite_fn: str, timeout: float = DEFAULT_CONNECT_TIMEOUT) -> None:
|
|
25
27
|
try:
|
|
26
28
|
import sqlite3 # noqa: F401
|
|
27
29
|
except Exception:
|
|
@@ -36,6 +38,7 @@ class SQLiteDatabase(Database):
|
|
|
36
38
|
self.encoding = "UTF-8"
|
|
37
39
|
self.encode_commands = False
|
|
38
40
|
self.paramstr = "?"
|
|
41
|
+
self.timeout = timeout
|
|
39
42
|
self.conn = None
|
|
40
43
|
self.autocommit = True
|
|
41
44
|
self.open_db()
|
|
@@ -48,7 +51,7 @@ class SQLiteDatabase(Database):
|
|
|
48
51
|
|
|
49
52
|
if self.conn is None:
|
|
50
53
|
try:
|
|
51
|
-
self.conn = sqlite3.connect(self.db_name)
|
|
54
|
+
self.conn = sqlite3.connect(self.db_name, timeout=self.timeout)
|
|
52
55
|
except ErrInfo:
|
|
53
56
|
raise
|
|
54
57
|
except Exception:
|
|
@@ -72,11 +75,11 @@ class SQLiteDatabase(Database):
|
|
|
72
75
|
self.rollback()
|
|
73
76
|
raise
|
|
74
77
|
|
|
75
|
-
def table_exists(self, table_name: str, schema_name:
|
|
78
|
+
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
76
79
|
curs = self.cursor()
|
|
77
|
-
sql =
|
|
80
|
+
sql = "select name from sqlite_master where type='table' and name=?;"
|
|
78
81
|
try:
|
|
79
|
-
curs.execute(sql)
|
|
82
|
+
curs.execute(sql, (table_name,))
|
|
80
83
|
except ErrInfo:
|
|
81
84
|
raise
|
|
82
85
|
except Exception:
|
|
@@ -94,19 +97,15 @@ class SQLiteDatabase(Database):
|
|
|
94
97
|
self,
|
|
95
98
|
table_name: str,
|
|
96
99
|
column_name: str,
|
|
97
|
-
schema_name:
|
|
100
|
+
schema_name: str | None = None,
|
|
98
101
|
) -> bool:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
try:
|
|
102
|
-
curs.execute(sql)
|
|
103
|
-
except Exception:
|
|
104
|
-
return False
|
|
105
|
-
return True
|
|
102
|
+
cols = self.table_columns(table_name, schema_name)
|
|
103
|
+
return column_name in cols
|
|
106
104
|
|
|
107
|
-
def table_columns(self, table_name: str, schema_name:
|
|
105
|
+
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
108
106
|
curs = self.cursor()
|
|
109
|
-
|
|
107
|
+
quoted_tbl = self.quote_identifier(table_name)
|
|
108
|
+
sql = f"select * from {quoted_tbl} where 1=0;"
|
|
110
109
|
try:
|
|
111
110
|
curs.execute(sql)
|
|
112
111
|
except ErrInfo:
|
|
@@ -123,9 +122,9 @@ class SQLiteDatabase(Database):
|
|
|
123
122
|
|
|
124
123
|
def view_exists(self, view_name: str) -> bool:
|
|
125
124
|
curs = self.cursor()
|
|
126
|
-
sql =
|
|
125
|
+
sql = "select name from sqlite_master where type='view' and name=?;"
|
|
127
126
|
try:
|
|
128
|
-
curs.execute(sql)
|
|
127
|
+
curs.execute(sql, (view_name,))
|
|
129
128
|
except ErrInfo:
|
|
130
129
|
raise
|
|
131
130
|
except Exception:
|
|
@@ -148,10 +147,10 @@ class SQLiteDatabase(Database):
|
|
|
148
147
|
|
|
149
148
|
def populate_table(
|
|
150
149
|
self,
|
|
151
|
-
schema_name:
|
|
150
|
+
schema_name: str | None,
|
|
152
151
|
table_name: str,
|
|
153
152
|
rowsource: Any,
|
|
154
|
-
column_list:
|
|
153
|
+
column_list: list[str],
|
|
155
154
|
tablespec_src: Any,
|
|
156
155
|
) -> None:
|
|
157
156
|
# The rowsource argument must be a generator yielding a list of values for the columns of the table.
|
|
@@ -174,6 +173,7 @@ class SQLiteDatabase(Database):
|
|
|
174
173
|
paramspec = ",".join(["?" for c in columns])
|
|
175
174
|
sql = f"insert into {sq_name} ({colspec}) values ({paramspec});"
|
|
176
175
|
curs = self.cursor()
|
|
176
|
+
total_rows = 0
|
|
177
177
|
for datalineno, line in enumerate(rowsource):
|
|
178
178
|
# Skip empty rows.
|
|
179
179
|
if not (len(line) == 1 and line[0] is None):
|
|
@@ -190,16 +190,15 @@ class SQLiteDatabase(Database):
|
|
|
190
190
|
line[i] = line[i].strip()
|
|
191
191
|
if _state.conf.replace_newlines:
|
|
192
192
|
line[i] = re.sub(r"[\s\t]*[\r\n]+[\s\t]*", " ", line[i])
|
|
193
|
-
if not _state.conf.empty_strings:
|
|
194
|
-
|
|
195
|
-
line[i] = None
|
|
193
|
+
if not _state.conf.empty_strings and line[i].strip() == "":
|
|
194
|
+
line[i] = None
|
|
196
195
|
# Convert datetime, time, and Decimal values to strings.
|
|
197
196
|
for i in range(len(linedata)):
|
|
198
197
|
if type(linedata[i]) in (datetime.datetime, datetime.time, Decimal):
|
|
199
198
|
linedata[i] = str(linedata[i])
|
|
200
199
|
add_line = True
|
|
201
200
|
if not _state.conf.empty_rows:
|
|
202
|
-
add_line = not all(
|
|
201
|
+
add_line = not all(c is None for c in linedata)
|
|
203
202
|
if add_line:
|
|
204
203
|
try:
|
|
205
204
|
curs.execute(sql, linedata)
|
|
@@ -213,17 +212,27 @@ class SQLiteDatabase(Database):
|
|
|
213
212
|
exception_msg=exception_desc(),
|
|
214
213
|
other_msg=f"Can't load data into table {sq_name} from line {{{line}}}",
|
|
215
214
|
)
|
|
215
|
+
total_rows += 1
|
|
216
|
+
interval = getattr(_state.conf, "import_progress_interval", 0)
|
|
217
|
+
if _state.exec_log and interval > 0 and total_rows % interval == 0:
|
|
218
|
+
_state.exec_log.log_status_info(
|
|
219
|
+
f"IMPORT into {sq_name}: {total_rows} rows imported so far.",
|
|
220
|
+
)
|
|
221
|
+
if _state.exec_log:
|
|
222
|
+
_state.exec_log.log_status_info(
|
|
223
|
+
f"IMPORT into {sq_name} complete: {total_rows} rows imported.",
|
|
224
|
+
)
|
|
216
225
|
|
|
217
226
|
def import_entire_file(
|
|
218
227
|
self,
|
|
219
|
-
schema_name:
|
|
228
|
+
schema_name: str | None,
|
|
220
229
|
table_name: str,
|
|
221
230
|
column_name: str,
|
|
222
231
|
file_name: str,
|
|
223
232
|
) -> None:
|
|
224
233
|
import sqlite3
|
|
225
234
|
|
|
226
|
-
with
|
|
235
|
+
with open(file_name, "rb") as f:
|
|
227
236
|
filedata = f.read()
|
|
228
237
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
229
238
|
sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
|
execsql/db/sqlserver.py
CHANGED
|
@@ -7,13 +7,11 @@ Implements :class:`SqlServerDatabase`, which connects to Microsoft SQL
|
|
|
7
7
|
Server via ``pyodbc``. Corresponds to ``-t s`` on the CLI.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
import io
|
|
11
|
-
from typing import Optional
|
|
12
10
|
|
|
13
11
|
from execsql.db.base import Database
|
|
14
12
|
from execsql.exceptions import ErrInfo
|
|
15
|
-
from execsql.utils.errors import
|
|
16
|
-
from execsql.utils.auth import get_password
|
|
13
|
+
from execsql.utils.errors import fatal_error
|
|
14
|
+
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
17
15
|
import execsql.state as _state
|
|
18
16
|
|
|
19
17
|
|
|
@@ -22,11 +20,11 @@ class SqlServerDatabase(Database):
|
|
|
22
20
|
self,
|
|
23
21
|
server_name: str,
|
|
24
22
|
db_name: str,
|
|
25
|
-
user_name:
|
|
23
|
+
user_name: str | None,
|
|
26
24
|
need_passwd: bool = False,
|
|
27
|
-
port:
|
|
28
|
-
encoding:
|
|
29
|
-
password:
|
|
25
|
+
port: int | None = 1433,
|
|
26
|
+
encoding: str | None = "latin1",
|
|
27
|
+
password: str | None = None,
|
|
30
28
|
) -> None:
|
|
31
29
|
try:
|
|
32
30
|
import pyodbc # noqa: F401
|
|
@@ -76,30 +74,47 @@ class SqlServerDatabase(Database):
|
|
|
76
74
|
"SQL Native Client",
|
|
77
75
|
"SQL Server",
|
|
78
76
|
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
|
|
78
|
+
def _try_drivers():
|
|
79
|
+
for drv in ssdrivers:
|
|
80
|
+
if self.user:
|
|
81
|
+
if self.password:
|
|
82
|
+
connstr = (
|
|
83
|
+
f"DRIVER={{{drv}}};SERVER={self.server_name};MARS_Connection=Yes; "
|
|
84
|
+
f"DATABASE={self.db_name};Uid={self.user};Pwd={self.password}"
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
connstr = (
|
|
88
|
+
f"DRIVER={{{drv}}};SERVER={self.server_name};MARS_Connection=Yes; "
|
|
89
|
+
f"DATABASE={self.db_name};Uid={self.user}"
|
|
90
|
+
)
|
|
86
91
|
else:
|
|
87
92
|
connstr = (
|
|
88
93
|
f"DRIVER={{{drv}}};SERVER={self.server_name};MARS_Connection=Yes; "
|
|
89
|
-
f"DATABASE={self.db_name};
|
|
94
|
+
f"DATABASE={self.db_name};Trusted_Connection=yes"
|
|
90
95
|
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
f"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
try:
|
|
97
|
+
self.conn = pyodbc.connect(connstr)
|
|
98
|
+
except Exception:
|
|
99
|
+
_state.exec_log.log_status_info(f"Could not connect using: {connstr}")
|
|
100
|
+
else:
|
|
101
|
+
_state.exec_log.log_status_info(f"Connected using: {connstr}")
|
|
102
|
+
return True
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
if not _try_drivers() and password_from_keyring():
|
|
106
|
+
# Stored credential is stale — clear it and re-prompt.
|
|
107
|
+
clear_stored_password("SQL Server", self.db_name, self.user, self.server_name)
|
|
108
|
+
self.password = get_password(
|
|
109
|
+
"SQL Server",
|
|
110
|
+
self.db_name,
|
|
111
|
+
self.user,
|
|
112
|
+
server_name=self.server_name,
|
|
113
|
+
skip_keyring=True,
|
|
114
|
+
other_msg="(stored credential failed — enter current password)",
|
|
115
|
+
)
|
|
116
|
+
_try_drivers()
|
|
117
|
+
|
|
103
118
|
if not self.conn:
|
|
104
119
|
raise ErrInfo(
|
|
105
120
|
type="error",
|
|
@@ -147,14 +162,14 @@ class SqlServerDatabase(Database):
|
|
|
147
162
|
|
|
148
163
|
def import_entire_file(
|
|
149
164
|
self,
|
|
150
|
-
schema_name:
|
|
165
|
+
schema_name: str | None,
|
|
151
166
|
table_name: str,
|
|
152
167
|
column_name: str,
|
|
153
168
|
file_name: str,
|
|
154
169
|
) -> None:
|
|
155
170
|
import pyodbc
|
|
156
171
|
|
|
157
|
-
with
|
|
172
|
+
with open(file_name, "rb") as f:
|
|
158
173
|
filedata = f.read()
|
|
159
174
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
160
175
|
sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
|
execsql/exceptions.py
CHANGED
|
@@ -6,9 +6,11 @@ Custom exception hierarchy for execsql.
|
|
|
6
6
|
All domain-specific exceptions are defined here so that callers can import
|
|
7
7
|
from a single location. Notable exceptions:
|
|
8
8
|
|
|
9
|
+
- :class:`ExecSqlError` — common base for all single-message execsql exceptions.
|
|
9
10
|
- :class:`ConfigError` — invalid or missing ``execsql.conf`` values.
|
|
10
|
-
- :class:`ErrInfo` —
|
|
11
|
-
|
|
11
|
+
- :class:`ErrInfo` — rich exception carrying type, command text, exception
|
|
12
|
+
message, and script location; used as both a raised exception and an error
|
|
13
|
+
data carrier passed to ``exit_now()``.
|
|
12
14
|
- :class:`ExecSqlTimeoutError` — timeout during alarm-timer operations.
|
|
13
15
|
- :class:`DataTypeError` / :class:`DbTypeError` — type-system failures.
|
|
14
16
|
- :class:`ColumnError` / :class:`DataTableError` — data-model failures.
|
|
@@ -20,26 +22,60 @@ from a single location. Notable exceptions:
|
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
24
|
|
|
23
|
-
class
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
class ExecSqlError(Exception):
|
|
26
|
+
"""Base class for simple single-message execsql exceptions.
|
|
27
|
+
|
|
28
|
+
Subclasses inherit a ``value`` attribute holding the original message and
|
|
29
|
+
a ``__repr__`` that uses the concrete class name, so no boilerplate is
|
|
30
|
+
needed in each subclass.
|
|
31
|
+
|
|
32
|
+
``super().__init__(errmsg)`` is called so that ``str(exc)``, ``exc.args``,
|
|
33
|
+
and standard logging all produce meaningful output.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, errmsg: str) -> None:
|
|
37
|
+
super().__init__(errmsg)
|
|
38
|
+
self.value = errmsg
|
|
26
39
|
|
|
27
40
|
def __repr__(self) -> str:
|
|
28
|
-
return f"
|
|
41
|
+
return f"{type(self).__name__}({self.value!r})"
|
|
42
|
+
|
|
29
43
|
|
|
44
|
+
class ConfigError(ExecSqlError):
|
|
45
|
+
"""Raised for invalid or missing execsql configuration values."""
|
|
30
46
|
|
|
31
|
-
class ExecSqlTimeoutError(Exception):
|
|
32
|
-
"""Renamed from TimeoutError to avoid shadowing the Python 3.3+ built-in."""
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
class ExecSqlTimeoutError(ExecSqlError):
|
|
49
|
+
"""Timeout during alarm-timer operations.
|
|
50
|
+
|
|
51
|
+
Inherits from :class:`ExecSqlError` so that generic ``except ExecSqlError``
|
|
52
|
+
handlers will catch timeouts. Accepts an optional message (defaults to
|
|
53
|
+
``"Operation timed out"``), keeping it compatible with bare
|
|
54
|
+
``raise ExecSqlTimeoutError`` usage.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, errmsg: str = "Operation timed out") -> None:
|
|
58
|
+
super().__init__(errmsg)
|
|
35
59
|
|
|
36
60
|
|
|
37
61
|
class ErrInfo(Exception):
|
|
38
|
-
"""
|
|
62
|
+
"""Rich exception and error-data carrier for execsql.
|
|
63
|
+
|
|
64
|
+
``str(e)`` returns the most informative available message (``other_msg``,
|
|
65
|
+
then ``exception_msg``, then ``type``) so that standard logging and
|
|
66
|
+
exception handlers produce useful output without requiring callers to know
|
|
67
|
+
about the execsql-specific ``eval_err()`` / ``write()`` interface.
|
|
68
|
+
|
|
69
|
+
``eval_err()`` / ``write()`` remain available for the full formatted
|
|
70
|
+
message including script location, timestamp, and command context.
|
|
71
|
+
"""
|
|
39
72
|
|
|
40
73
|
def __repr__(self) -> str:
|
|
41
74
|
return f"ErrInfo({self.type!r}, {self.command!r}, {self.exception!r}, {self.other!r})"
|
|
42
75
|
|
|
76
|
+
def __str__(self) -> str:
|
|
77
|
+
return self.other or self.exception or self.type or "ErrInfo"
|
|
78
|
+
|
|
43
79
|
def __init__(
|
|
44
80
|
self,
|
|
45
81
|
type: str,
|
|
@@ -56,6 +92,9 @@ class ErrInfo(Exception):
|
|
|
56
92
|
self.cmd = None
|
|
57
93
|
self.cmdtype = None
|
|
58
94
|
self.error_message = None
|
|
95
|
+
# Pass a concise message to Exception so str(e), e.args, and
|
|
96
|
+
# standard loggers produce useful output.
|
|
97
|
+
super().__init__(self.other or self.exception or self.type)
|
|
59
98
|
|
|
60
99
|
def script_info(self) -> str | None:
|
|
61
100
|
if self.script_line_no:
|
|
@@ -117,6 +156,7 @@ class DataTypeError(Exception):
|
|
|
117
156
|
def __init__(self, data_type_name: str, error_msg: str) -> None:
|
|
118
157
|
self.data_type_name = data_type_name or "Unspecified data type"
|
|
119
158
|
self.error_msg = error_msg or "Unspecified error"
|
|
159
|
+
super().__init__(str(self))
|
|
120
160
|
|
|
121
161
|
def __repr__(self) -> str:
|
|
122
162
|
return f"DataTypeError({self.data_type_name!r}, {self.error_msg!r})"
|
|
@@ -130,6 +170,7 @@ class DbTypeError(Exception):
|
|
|
130
170
|
self.dbms_id = dbms_id
|
|
131
171
|
self.data_type = data_type
|
|
132
172
|
self.error_msg = error_msg or "Unspecified error"
|
|
173
|
+
super().__init__(str(self))
|
|
133
174
|
|
|
134
175
|
def __repr__(self) -> str:
|
|
135
176
|
return f"DbTypeError({self.dbms_id!r}, {self.data_type!r}, {self.error_msg!r})"
|
|
@@ -141,32 +182,19 @@ class DbTypeError(Exception):
|
|
|
141
182
|
return f"{self.dbms_id} DBMS type error: {self.error_msg}"
|
|
142
183
|
|
|
143
184
|
|
|
144
|
-
class ColumnError(
|
|
145
|
-
|
|
146
|
-
self.value = errmsg
|
|
147
|
-
|
|
148
|
-
def __repr__(self) -> str:
|
|
149
|
-
return f"ColumnError({self.value!r})"
|
|
150
|
-
|
|
151
|
-
def __str__(self) -> str:
|
|
152
|
-
return repr(self.value)
|
|
185
|
+
class ColumnError(ExecSqlError):
|
|
186
|
+
"""Raised for column-level data errors."""
|
|
153
187
|
|
|
154
188
|
|
|
155
|
-
class DataTableError(
|
|
156
|
-
|
|
157
|
-
self.value = errmsg
|
|
158
|
-
|
|
159
|
-
def __repr__(self) -> str:
|
|
160
|
-
return f"DataTableError({self.value})"
|
|
161
|
-
|
|
162
|
-
def __str__(self) -> str:
|
|
163
|
-
return repr(self.value)
|
|
189
|
+
class DataTableError(ExecSqlError):
|
|
190
|
+
"""Raised for DataTable-level errors."""
|
|
164
191
|
|
|
165
192
|
|
|
166
193
|
class DatabaseNotImplementedError(Exception):
|
|
167
194
|
def __init__(self, db_name: str, method: str) -> None:
|
|
168
195
|
self.db_name = db_name
|
|
169
196
|
self.method = method
|
|
197
|
+
super().__init__(str(self))
|
|
170
198
|
|
|
171
199
|
def __repr__(self) -> str:
|
|
172
200
|
return f"DatabaseNotImplementedError({self.db_name!r}, {self.method!r})"
|
|
@@ -175,49 +203,25 @@ class DatabaseNotImplementedError(Exception):
|
|
|
175
203
|
return f"Method {self.method} is not implemented for database {self.db_name}"
|
|
176
204
|
|
|
177
205
|
|
|
178
|
-
class OdsFileError(
|
|
179
|
-
|
|
180
|
-
self.value = errmsg
|
|
181
|
-
|
|
182
|
-
def __repr__(self) -> str:
|
|
183
|
-
return f"OdsFileError({self.value!r})"
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
class XlsFileError(Exception):
|
|
187
|
-
def __init__(self, errmsg: str) -> None:
|
|
188
|
-
self.value = errmsg
|
|
189
|
-
|
|
190
|
-
def __repr__(self) -> str:
|
|
191
|
-
return f"XlsFileError({self.value!r})"
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
class XlsxFileError(Exception):
|
|
195
|
-
def __init__(self, errmsg: str) -> None:
|
|
196
|
-
self.value = errmsg
|
|
206
|
+
class OdsFileError(ExecSqlError):
|
|
207
|
+
"""Raised for ODS file I/O errors."""
|
|
197
208
|
|
|
198
|
-
def __repr__(self) -> str:
|
|
199
|
-
return f"XlsxFileError({self.value!r})"
|
|
200
209
|
|
|
210
|
+
class XlsFileError(ExecSqlError):
|
|
211
|
+
"""Raised for XLS file I/O errors."""
|
|
201
212
|
|
|
202
|
-
class ConsoleUIError(Exception):
|
|
203
|
-
def __init__(self, errmsg: str) -> None:
|
|
204
|
-
self.value = errmsg
|
|
205
213
|
|
|
206
|
-
|
|
207
|
-
|
|
214
|
+
class XlsxFileError(ExecSqlError):
|
|
215
|
+
"""Raised for XLSX file I/O errors."""
|
|
208
216
|
|
|
209
217
|
|
|
210
|
-
class
|
|
211
|
-
|
|
212
|
-
self.value = errmsg
|
|
218
|
+
class ConsoleUIError(ExecSqlError):
|
|
219
|
+
"""Raised for GUI console errors."""
|
|
213
220
|
|
|
214
|
-
def __repr__(self) -> str:
|
|
215
|
-
return f"CondParserError({self.value!r})"
|
|
216
221
|
|
|
222
|
+
class CondParserError(ExecSqlError):
|
|
223
|
+
"""Raised for conditional-expression parse errors."""
|
|
217
224
|
|
|
218
|
-
class NumericParserError(Exception):
|
|
219
|
-
def __init__(self, errmsg: str) -> None:
|
|
220
|
-
self.value = errmsg
|
|
221
225
|
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
class NumericParserError(ExecSqlError):
|
|
227
|
+
"""Raised for numeric-expression parse errors."""
|
execsql/exporters/__init__.py
CHANGED
|
@@ -10,5 +10,5 @@ importing directly from here.
|
|
|
10
10
|
|
|
11
11
|
Sub-modules: ``base``, ``delimited``, ``json``, ``xml``, ``html``,
|
|
12
12
|
``latex``, ``ods``, ``xls``, ``zip``, ``raw``, ``pretty``, ``values``,
|
|
13
|
-
``templates``, ``feather``, ``duckdb``, ``sqlite``.
|
|
13
|
+
``templates``, ``feather``, ``parquet``, ``duckdb``, ``sqlite``.
|
|
14
14
|
"""
|
execsql/exporters/base.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
"""
|
|
4
2
|
Base export infrastructure — metadata tracking and write specifications.
|
|
5
3
|
|
|
@@ -14,42 +12,63 @@ Provides:
|
|
|
14
12
|
(message text, file path, encoding) used by halt/cancel hooks.
|
|
15
13
|
"""
|
|
16
14
|
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
from
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
20
19
|
|
|
21
20
|
import execsql.state as _state
|
|
21
|
+
from execsql.script import current_script_line
|
|
22
|
+
from execsql.utils.errors import file_size_date
|
|
23
|
+
from execsql.utils.gui import ConsoleUIError
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class ExportRecord:
|
|
27
|
+
"""Records the details of a single EXPORT operation for metadata tracking.
|
|
28
|
+
|
|
29
|
+
Captures the query name, output file path, optional zip file, user
|
|
30
|
+
description, originating script location, and database connection info.
|
|
31
|
+
"""
|
|
32
|
+
|
|
25
33
|
def __init__(
|
|
26
34
|
self,
|
|
27
35
|
queryname: str,
|
|
28
36
|
outfile: str,
|
|
29
|
-
zipfile:
|
|
30
|
-
description:
|
|
37
|
+
zipfile: str | None = None,
|
|
38
|
+
description: str | None = None,
|
|
31
39
|
) -> None:
|
|
32
40
|
self.exported = False
|
|
33
41
|
# Record is a list of: table_or_query_name, filename, zipfilename, file_path, user_description, script_name,
|
|
34
42
|
# script_path, script_line_no, script_datetime, database_name, database_server, user_name.
|
|
35
43
|
if zipfile is not None:
|
|
36
|
-
fpath, zfname =
|
|
44
|
+
fpath, zfname = str(Path(zipfile).resolve().parent), Path(zipfile).resolve().name
|
|
37
45
|
fname = outfile
|
|
38
46
|
else:
|
|
39
|
-
fpath, fname =
|
|
47
|
+
fpath, fname = str(Path(outfile).resolve().parent), Path(outfile).resolve().name
|
|
40
48
|
zfname = None
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
import getpass
|
|
50
|
+
|
|
51
|
+
script, lno = current_script_line()
|
|
52
|
+
if script and Path(script).is_file():
|
|
53
|
+
spath, sname = str(Path(script).resolve().parent), Path(script).resolve().name
|
|
54
|
+
_, sdt = file_size_date(script)
|
|
55
|
+
else:
|
|
56
|
+
spath, sname = "", script or "<inline>"
|
|
57
|
+
_, sdt = 0, ""
|
|
44
58
|
db = _state.dbs.current()
|
|
45
59
|
svr = db.server_name
|
|
46
60
|
dbn = db.db_name
|
|
47
|
-
usr = db.user if db.user is not None else
|
|
61
|
+
usr = db.user if db.user is not None else getpass.getuser()
|
|
48
62
|
self.record = [queryname, fname, zfname, fpath, description, sname, spath, lno, sdt, dbn, svr, usr]
|
|
49
63
|
|
|
50
64
|
|
|
51
65
|
class ExportMetadata:
|
|
52
|
-
|
|
66
|
+
"""Collection of :class:`ExportRecord` objects; can write itself as JSON.
|
|
67
|
+
|
|
68
|
+
Accumulates export records during a script run and provides them to the
|
|
69
|
+
EXPORT METADATA metacommand for serialisation.
|
|
70
|
+
"""
|
|
71
|
+
|
|
53
72
|
colhdrs = [
|
|
54
73
|
"query",
|
|
55
74
|
"filename",
|
|
@@ -66,7 +85,7 @@ class ExportMetadata:
|
|
|
66
85
|
]
|
|
67
86
|
|
|
68
87
|
def __init__(self) -> None:
|
|
69
|
-
self.recordlist:
|
|
88
|
+
self.recordlist: list[ExportRecord] = []
|
|
70
89
|
|
|
71
90
|
def add(self, exp_record: ExportRecord) -> None:
|
|
72
91
|
self.recordlist.append(exp_record)
|
|
@@ -85,10 +104,16 @@ class ExportMetadata:
|
|
|
85
104
|
|
|
86
105
|
|
|
87
106
|
class WriteSpec:
|
|
107
|
+
"""Specification for a deferred WRITE operation used by halt/cancel hooks.
|
|
108
|
+
|
|
109
|
+
Stores a message, optional destination file, tee flag, and repeatability
|
|
110
|
+
setting. Resolved and executed later by the hook machinery.
|
|
111
|
+
"""
|
|
112
|
+
|
|
88
113
|
def __repr__(self) -> str:
|
|
89
114
|
return f"WriteSpec({self.msg}, {self.outfile}, {self.tee})"
|
|
90
115
|
|
|
91
|
-
def __init__(self, message: str, dest:
|
|
116
|
+
def __init__(self, message: str, dest: str | None = None, tee: Any = None, repeatable: bool = False) -> None:
|
|
92
117
|
# Inputs
|
|
93
118
|
# message: Text to write. May contain substitution variable references.
|
|
94
119
|
# dest: The to which the text should be written. If omitted, the message
|
|
@@ -122,7 +147,7 @@ class WriteSpec:
|
|
|
122
147
|
if (not self.outfile) or self.tee:
|
|
123
148
|
try:
|
|
124
149
|
_state.output.write(msg.encode(conf.output_encoding))
|
|
125
|
-
except
|
|
150
|
+
except ConsoleUIError as e:
|
|
126
151
|
_state.output.reset()
|
|
127
152
|
_state.exec_log.log_status_info(
|
|
128
153
|
f"Console UI write failed (message {{{e.value}}}); output reset to stdout.",
|