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.
Files changed (90) hide show
  1. execsql/cli.py +322 -108
  2. execsql/config.py +134 -114
  3. execsql/db/access.py +89 -65
  4. execsql/db/base.py +97 -68
  5. execsql/db/dsn.py +45 -29
  6. execsql/db/duckdb.py +4 -5
  7. execsql/db/factory.py +27 -27
  8. execsql/db/firebird.py +30 -18
  9. execsql/db/mysql.py +38 -14
  10. execsql/db/oracle.py +58 -33
  11. execsql/db/postgres.py +68 -28
  12. execsql/db/sqlite.py +36 -27
  13. execsql/db/sqlserver.py +45 -30
  14. execsql/exceptions.py +68 -64
  15. execsql/exporters/__init__.py +1 -1
  16. execsql/exporters/base.py +42 -17
  17. execsql/exporters/delimited.py +60 -59
  18. execsql/exporters/duckdb.py +8 -12
  19. execsql/exporters/feather.py +32 -24
  20. execsql/exporters/html.py +33 -30
  21. execsql/exporters/json.py +18 -17
  22. execsql/exporters/latex.py +11 -13
  23. execsql/exporters/ods.py +50 -46
  24. execsql/exporters/parquet.py +32 -0
  25. execsql/exporters/pretty.py +16 -15
  26. execsql/exporters/raw.py +9 -11
  27. execsql/exporters/sqlite.py +38 -38
  28. execsql/exporters/templates.py +15 -72
  29. execsql/exporters/values.py +13 -12
  30. execsql/exporters/xls.py +26 -26
  31. execsql/exporters/xml.py +12 -12
  32. execsql/exporters/zip.py +0 -3
  33. execsql/gui/__init__.py +2 -2
  34. execsql/gui/console.py +0 -1
  35. execsql/gui/desktop.py +6 -7
  36. execsql/gui/tui.py +8 -14
  37. execsql/importers/base.py +6 -9
  38. execsql/importers/csv.py +10 -17
  39. execsql/importers/feather.py +16 -22
  40. execsql/importers/ods.py +3 -4
  41. execsql/importers/xls.py +5 -6
  42. execsql/metacommands/__init__.py +8 -8
  43. execsql/metacommands/conditions.py +41 -33
  44. execsql/metacommands/connect.py +113 -99
  45. execsql/metacommands/control.py +38 -26
  46. execsql/metacommands/data.py +35 -33
  47. execsql/metacommands/debug.py +13 -9
  48. execsql/metacommands/io.py +288 -229
  49. execsql/metacommands/prompt.py +179 -157
  50. execsql/metacommands/script_ext.py +11 -9
  51. execsql/metacommands/system.py +44 -25
  52. execsql/models.py +9 -16
  53. execsql/parser.py +10 -10
  54. execsql/script.py +183 -157
  55. execsql/state.py +170 -208
  56. execsql/types.py +46 -81
  57. execsql/utils/auth.py +114 -14
  58. execsql/utils/crypto.py +31 -4
  59. execsql/utils/datetime.py +7 -7
  60. execsql/utils/errors.py +34 -29
  61. execsql/utils/fileio.py +90 -55
  62. execsql/utils/gui.py +22 -23
  63. execsql/utils/mail.py +15 -17
  64. execsql/utils/numeric.py +2 -3
  65. execsql/utils/regex.py +9 -12
  66. execsql/utils/strings.py +10 -12
  67. execsql/utils/timer.py +0 -2
  68. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
  69. execsql2-2.1.2.dist-info/METADATA +300 -0
  70. execsql2-2.1.2.dist-info/RECORD +96 -0
  71. execsql2-2.0.1.dist-info/METADATA +0 -406
  72. execsql2-2.0.1.dist-info/RECORD +0 -95
  73. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
  74. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  75. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  76. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
  77. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
  78. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
  79. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
  80. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
  81. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  82. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  83. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
  84. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
  85. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  86. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  87. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
  88. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
  89. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
  90. {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 typing import Any, List, Optional, Tuple
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: Optional[str] = None,
45
- encoding: Optional[str] = None,
46
- password: Optional[str] = None,
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: List[str] = []
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
- connected = False
97
- db_name = os.path.abspath(self.db_name)
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
- connected = True
111
- break
112
- if not connected:
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
- connected = False
128
- for engine in dao_engines:
129
- try:
130
- daoEngine = win32com.client.Dispatch(engine)
131
- if self.need_passwd:
132
- self.dao_conn = daoEngine.OpenDatabase(
133
- self.db_name,
134
- False,
135
- False,
136
- f"MS Access;UID={self.user};PWD={self.password};",
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
- self.dao_conn = daoEngine.OpenDatabase(self.db_name)
140
- except Exception:
141
- _state.exec_log.log_status_info(f"Could not connect via DAO using: {engine}")
142
- else:
143
- _state.exec_log.log_status_info(f"Connected via DAO using: {engine}")
144
- connected = True
145
- break
146
- if not connected:
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: Optional[list] = None) -> None:
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: Optional[list]) -> None:
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
- if qn not in self.temp_query_names:
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) -> Tuple[List[str], list]:
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) -> Tuple[List[str], Any]:
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) -> Tuple[List[str], Any]:
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() -> Optional[dict]:
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 type(c) == type(b"") else c for c in row]
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: Optional[str] = None) -> bool:
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: Optional[str] = None,
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: Optional[str] = None) -> List[str]:
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: Optional[str] = None) -> bool:
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) -> Optional[datetime.datetime]:
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 type(val) == datetime.date or type(val) == datetime.datetime or type(val) == datetime.time:
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) -> Optional[int]:
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: Optional[str],
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 io.open(file_name, "rb") as f:
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)});"