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/access.py
CHANGED
|
@@ -9,16 +9,15 @@ Implements :class:`AccessDatabase`, which connects to ``.mdb`` and
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import datetime
|
|
12
|
-
import io
|
|
13
|
-
import os
|
|
14
12
|
import re
|
|
15
13
|
import time
|
|
16
|
-
from
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
17
16
|
|
|
18
17
|
from execsql.db.base import Database
|
|
19
18
|
from execsql.exceptions import ErrInfo
|
|
20
19
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
21
|
-
from execsql.utils.auth import get_password
|
|
20
|
+
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
22
21
|
import execsql.state as _state
|
|
23
22
|
|
|
24
23
|
|
|
@@ -41,9 +40,9 @@ class AccessDatabase(Database):
|
|
|
41
40
|
self,
|
|
42
41
|
Access_fn: str,
|
|
43
42
|
need_passwd: bool = False,
|
|
44
|
-
user_name:
|
|
45
|
-
encoding:
|
|
46
|
-
password:
|
|
43
|
+
user_name: str | None = None,
|
|
44
|
+
encoding: str | None = None,
|
|
45
|
+
password: str | None = None,
|
|
47
46
|
) -> None:
|
|
48
47
|
try:
|
|
49
48
|
import win32com.client # noqa: F401 – imported for side-effects / availability check
|
|
@@ -74,7 +73,7 @@ class AccessDatabase(Database):
|
|
|
74
73
|
self.dt_cast[datetime.datetime] = self.as_datetime
|
|
75
74
|
self.dt_cast[int] = self.int_or_bool
|
|
76
75
|
self.last_dao_time = 0.0
|
|
77
|
-
self.temp_query_names:
|
|
76
|
+
self.temp_query_names: list[str] = []
|
|
78
77
|
self.autocommit = True
|
|
79
78
|
# Create the DAO connection
|
|
80
79
|
self.open_dao()
|
|
@@ -93,23 +92,36 @@ class AccessDatabase(Database):
|
|
|
93
92
|
self.conn = None
|
|
94
93
|
if self.need_passwd and self.user and self.password is None:
|
|
95
94
|
self.password = get_password("MS-Access", self.db_name, self.user)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
95
|
+
|
|
96
|
+
def _try_odbc_drivers():
|
|
97
|
+
db_name = str(Path(self.db_name).resolve())
|
|
98
|
+
for cs, jet4flag in self.connection_strings:
|
|
99
|
+
if self.need_passwd:
|
|
100
|
+
connstr = f"{cs % db_name} Uid={self.user}; Pwd={self.password};"
|
|
101
|
+
else:
|
|
102
|
+
connstr = cs % db_name
|
|
103
|
+
try:
|
|
104
|
+
self.conn = pyodbc.connect(connstr)
|
|
105
|
+
except Exception:
|
|
106
|
+
_state.exec_log.log_status_info(f"Could not connect via ODBC using: {connstr}")
|
|
107
|
+
else:
|
|
108
|
+
_state.exec_log.log_status_info(f"Connected via ODBC using: {connstr}")
|
|
109
|
+
self.jet4 = jet4flag
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
if not _try_odbc_drivers() and password_from_keyring():
|
|
114
|
+
clear_stored_password("MS-Access", self.db_name, self.user)
|
|
115
|
+
self.password = get_password(
|
|
116
|
+
"MS-Access",
|
|
117
|
+
self.db_name,
|
|
118
|
+
self.user,
|
|
119
|
+
skip_keyring=True,
|
|
120
|
+
other_msg="(stored credential failed — enter current password)",
|
|
121
|
+
)
|
|
122
|
+
_try_odbc_drivers()
|
|
123
|
+
|
|
124
|
+
if not self.conn:
|
|
113
125
|
raise ErrInfo(
|
|
114
126
|
type="error",
|
|
115
127
|
other_msg=f"Can't open Access database {self.db_name} using ODBC",
|
|
@@ -119,31 +131,44 @@ class AccessDatabase(Database):
|
|
|
119
131
|
import win32com.client
|
|
120
132
|
|
|
121
133
|
if self.dao_conn is not None:
|
|
122
|
-
self.dao_conn.Close
|
|
134
|
+
self.dao_conn.Close()
|
|
123
135
|
self.dao_conn = None
|
|
124
136
|
if self.need_passwd and self.user and self.password is None:
|
|
125
137
|
self.password = get_password("MS-Access", self.db_name, self.user)
|
|
126
138
|
dao_engines = ("DAO.DBEngine.120", "DAO.DBEngine.36")
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
self.
|
|
133
|
-
self.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
|
|
140
|
+
def _try_dao_engines():
|
|
141
|
+
for engine in dao_engines:
|
|
142
|
+
try:
|
|
143
|
+
daoEngine = win32com.client.Dispatch(engine)
|
|
144
|
+
if self.need_passwd:
|
|
145
|
+
self.dao_conn = daoEngine.OpenDatabase(
|
|
146
|
+
self.db_name,
|
|
147
|
+
False,
|
|
148
|
+
False,
|
|
149
|
+
f"MS Access;UID={self.user};PWD={self.password};",
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
self.dao_conn = daoEngine.OpenDatabase(self.db_name)
|
|
153
|
+
except Exception:
|
|
154
|
+
_state.exec_log.log_status_info(f"Could not connect via DAO using: {engine}")
|
|
138
155
|
else:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
156
|
+
_state.exec_log.log_status_info(f"Connected via DAO using: {engine}")
|
|
157
|
+
return True
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
if not _try_dao_engines() and password_from_keyring():
|
|
161
|
+
clear_stored_password("MS-Access", self.db_name, self.user)
|
|
162
|
+
self.password = get_password(
|
|
163
|
+
"MS-Access",
|
|
164
|
+
self.db_name,
|
|
165
|
+
self.user,
|
|
166
|
+
skip_keyring=True,
|
|
167
|
+
other_msg="(stored credential failed — enter current password)",
|
|
168
|
+
)
|
|
169
|
+
_try_dao_engines()
|
|
170
|
+
|
|
171
|
+
if not self.dao_conn:
|
|
147
172
|
raise ErrInfo(
|
|
148
173
|
type="error",
|
|
149
174
|
other_msg=(
|
|
@@ -166,7 +191,7 @@ class AccessDatabase(Database):
|
|
|
166
191
|
self.dao_conn.QueryDefs.Delete(qn)
|
|
167
192
|
self.last_dao_time = time.time()
|
|
168
193
|
except Exception:
|
|
169
|
-
pass
|
|
194
|
+
pass # Best-effort cleanup of temporary DAO query defs.
|
|
170
195
|
self.dao_conn = None
|
|
171
196
|
if self.conn:
|
|
172
197
|
self.conn.close()
|
|
@@ -176,13 +201,13 @@ class AccessDatabase(Database):
|
|
|
176
201
|
if time.time() - self.last_dao_time < 5.0:
|
|
177
202
|
time.sleep(5 - (time.time() - self.last_dao_time))
|
|
178
203
|
|
|
179
|
-
def execute(self, sqlcmd: Any, paramlist:
|
|
204
|
+
def execute(self, sqlcmd: Any, paramlist: list | None = None) -> None:
|
|
180
205
|
# A shortcut to self.cursor().execute() that handles encoding and that
|
|
181
206
|
# ensures that at least 5 seconds have passed since the last DAO command,
|
|
182
207
|
# to allow Jet's read buffer to be flushed (see https://support.microsoft.com/en-us/kb/225048).
|
|
183
208
|
# This also handles the 'CREATE TEMPORARY QUERY' extension to Access.
|
|
184
209
|
# For Access, commands in a tuple (batch) are executed singly.
|
|
185
|
-
def exec1(sql: str, paramlist:
|
|
210
|
+
def exec1(sql: str, paramlist: list | None) -> None:
|
|
186
211
|
tqd = self.temp_rx.match(sql)
|
|
187
212
|
if tqd:
|
|
188
213
|
qn = tqd.group(3)
|
|
@@ -199,9 +224,8 @@ class AccessDatabase(Database):
|
|
|
199
224
|
if self.conn is not None:
|
|
200
225
|
self.conn.close()
|
|
201
226
|
self.conn = None
|
|
202
|
-
if tqd.group(1) and tqd.group(1).strip().lower()[:4] == "temp":
|
|
203
|
-
|
|
204
|
-
self.temp_query_names.append(qn)
|
|
227
|
+
if tqd.group(1) and tqd.group(1).strip().lower()[:4] == "temp" and qn not in self.temp_query_names:
|
|
228
|
+
self.temp_query_names.append(qn)
|
|
205
229
|
else:
|
|
206
230
|
self.dao_flush_check()
|
|
207
231
|
curs = self.cursor()
|
|
@@ -224,7 +248,7 @@ class AccessDatabase(Database):
|
|
|
224
248
|
def exec_cmd(self, querycommand: str) -> None:
|
|
225
249
|
self.exec_dao(querycommand)
|
|
226
250
|
|
|
227
|
-
def select_data(self, sql: str) ->
|
|
251
|
+
def select_data(self, sql: str) -> tuple[list[str], list]:
|
|
228
252
|
# Returns the results of the sql select statement.
|
|
229
253
|
# The Access driver returns data as unicode, so no decoding is necessary.
|
|
230
254
|
self.dao_flush_check()
|
|
@@ -233,7 +257,7 @@ class AccessDatabase(Database):
|
|
|
233
257
|
rows = curs.fetchall()
|
|
234
258
|
return [d[0] for d in curs.description], rows
|
|
235
259
|
|
|
236
|
-
def select_rowsource(self, sql: str) ->
|
|
260
|
+
def select_rowsource(self, sql: str) -> tuple[list[str], Any]:
|
|
237
261
|
# Return 1) a list of column names, and 2) an iterable that yields rows.
|
|
238
262
|
self.dao_flush_check()
|
|
239
263
|
curs = self.cursor()
|
|
@@ -241,7 +265,7 @@ class AccessDatabase(Database):
|
|
|
241
265
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
242
266
|
return [d[0] for d in curs.description], iter(curs.fetchone, None)
|
|
243
267
|
|
|
244
|
-
def select_rowdict(self, sql: str) ->
|
|
268
|
+
def select_rowdict(self, sql: str) -> tuple[list[str], Any]:
|
|
245
269
|
# Return an iterable that yields dictionaries of row data.
|
|
246
270
|
self.dao_flush_check()
|
|
247
271
|
curs = self.cursor()
|
|
@@ -249,11 +273,11 @@ class AccessDatabase(Database):
|
|
|
249
273
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
250
274
|
headers = [d[0] for d in curs.description]
|
|
251
275
|
|
|
252
|
-
def dict_row() ->
|
|
276
|
+
def dict_row() -> dict | None:
|
|
253
277
|
row = curs.fetchone()
|
|
254
278
|
if row:
|
|
255
279
|
if self.encoding:
|
|
256
|
-
r = [c.decode(self.encoding) if
|
|
280
|
+
r = [c.decode(self.encoding) if isinstance(c, bytes) else c for c in row]
|
|
257
281
|
else:
|
|
258
282
|
r = row
|
|
259
283
|
return dict(zip(headers, r))
|
|
@@ -262,7 +286,7 @@ class AccessDatabase(Database):
|
|
|
262
286
|
|
|
263
287
|
return headers, iter(dict_row, None)
|
|
264
288
|
|
|
265
|
-
def table_exists(self, table_name: str, schema_name:
|
|
289
|
+
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
266
290
|
self.dao_flush_check()
|
|
267
291
|
curs = self.cursor()
|
|
268
292
|
try:
|
|
@@ -284,7 +308,7 @@ class AccessDatabase(Database):
|
|
|
284
308
|
self,
|
|
285
309
|
table_name: str,
|
|
286
310
|
column_name: str,
|
|
287
|
-
schema_name:
|
|
311
|
+
schema_name: str | None = None,
|
|
288
312
|
) -> bool:
|
|
289
313
|
self.dao_flush_check()
|
|
290
314
|
curs = self.cursor()
|
|
@@ -295,13 +319,13 @@ class AccessDatabase(Database):
|
|
|
295
319
|
return False
|
|
296
320
|
return True
|
|
297
321
|
|
|
298
|
-
def table_columns(self, table_name: str, schema_name:
|
|
322
|
+
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
299
323
|
self.dao_flush_check()
|
|
300
324
|
curs = self.cursor()
|
|
301
325
|
curs.execute(f"select top 1 * from {table_name};")
|
|
302
326
|
return [d[0] for d in curs.description]
|
|
303
327
|
|
|
304
|
-
def view_exists(self, view_name: str, schema_name:
|
|
328
|
+
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
305
329
|
self.dao_flush_check()
|
|
306
330
|
curs = self.cursor()
|
|
307
331
|
try:
|
|
@@ -327,12 +351,12 @@ class AccessDatabase(Database):
|
|
|
327
351
|
tablename = self.type.quoted(tablename)
|
|
328
352
|
self.execute(f"drop table {tablename};")
|
|
329
353
|
|
|
330
|
-
def as_datetime(self, val: Any) ->
|
|
354
|
+
def as_datetime(self, val: Any) -> datetime.datetime | None:
|
|
331
355
|
from execsql.types import DT_Timestamp, DT_Date, DT_Time, DataTypeError
|
|
332
356
|
|
|
333
357
|
if val is None or (isinstance(val, _state.stringtypes) and len(val) == 0):
|
|
334
358
|
return None
|
|
335
|
-
if
|
|
359
|
+
if isinstance(val, (datetime.date, datetime.datetime, datetime.time)):
|
|
336
360
|
return val
|
|
337
361
|
else:
|
|
338
362
|
try:
|
|
@@ -357,7 +381,7 @@ class AccessDatabase(Database):
|
|
|
357
381
|
raise
|
|
358
382
|
return v
|
|
359
383
|
|
|
360
|
-
def int_or_bool(self, val: Any) ->
|
|
384
|
+
def int_or_bool(self, val: Any) -> int | None:
|
|
361
385
|
# Because Booleans are stored as integers in Access (at least, if execsql
|
|
362
386
|
# creates the table), we have to recognize Boolean values as legitimate
|
|
363
387
|
# integers.
|
|
@@ -380,14 +404,14 @@ class AccessDatabase(Database):
|
|
|
380
404
|
|
|
381
405
|
def import_entire_file(
|
|
382
406
|
self,
|
|
383
|
-
schema_name:
|
|
407
|
+
schema_name: str | None,
|
|
384
408
|
table_name: str,
|
|
385
409
|
column_name: str,
|
|
386
410
|
file_name: str,
|
|
387
411
|
) -> None:
|
|
388
412
|
import pyodbc
|
|
389
413
|
|
|
390
|
-
with
|
|
414
|
+
with open(file_name, "rb") as f:
|
|
391
415
|
filedata = f.read()
|
|
392
416
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
393
417
|
sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
|