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/base.py CHANGED
@@ -14,11 +14,9 @@ open :class:`Database` instances and tracks which connection is currently
14
14
  active. It is the canonical ``_state.dbs`` object.
15
15
  """
16
16
 
17
- import datetime
18
- import io
19
17
  import re
20
- from decimal import Decimal
21
- from typing import Any, Callable, Dict, Generator, Iterator, List, Optional, Tuple
18
+ from typing import Any
19
+ from collections.abc import Callable, Generator, Iterator
22
20
 
23
21
  from execsql.exceptions import ErrInfo
24
22
  from execsql.utils.errors import exception_desc
@@ -28,23 +26,23 @@ import execsql.state as _state
28
26
  class Database:
29
27
  """Abstract base class for all database connections."""
30
28
 
31
- dt_cast: Dict[type, Callable] = {} # populated per-subclass or in __init__
29
+ dt_cast: dict[type, Callable] = {} # populated per-subclass or in __init__
32
30
 
33
31
  def __init__(
34
32
  self,
35
- server_name: Optional[str],
36
- db_name: Optional[str],
37
- user_name: Optional[str] = None,
38
- need_passwd: Optional[bool] = None,
39
- port: Optional[int] = None,
40
- encoding: Optional[str] = None,
33
+ server_name: str | None,
34
+ db_name: str | None,
35
+ user_name: str | None = None,
36
+ need_passwd: bool | None = None,
37
+ port: int | None = None,
38
+ encoding: str | None = None,
41
39
  ) -> None:
42
40
  self.type = None
43
41
  self.server_name = server_name
44
42
  self.db_name = db_name
45
43
  self.user = user_name
46
44
  self.need_passwd = need_passwd
47
- self.password: Optional[str] = None
45
+ self.password: str | None = None
48
46
  self.port = port
49
47
  self.encoding = encoding
50
48
  self.encode_commands = True
@@ -83,10 +81,15 @@ class Database:
83
81
  self.conn.close()
84
82
  self.conn = None
85
83
 
84
+ def quote_identifier(self, identifier: str) -> str:
85
+ """Return *identifier* wrapped in double-quotes with any embedded
86
+ double-quotes escaped (standard SQL identifier quoting)."""
87
+ return '"' + identifier.replace('"', '""') + '"'
88
+
86
89
  def paramsubs(self, paramcount: int) -> str:
87
90
  return ",".join((self.paramstr,) * paramcount)
88
91
 
89
- def execute(self, sql: Any, paramlist: Optional[list] = None) -> None:
92
+ def execute(self, sql: Any, paramlist: list | None = None) -> None:
90
93
  # A shortcut to self.cursor().execute() that handles encoding.
91
94
  # Whether or not encoding is needed depends on the DBMS.
92
95
  if type(sql) in (tuple, list):
@@ -98,16 +101,16 @@ class Database:
98
101
  else:
99
102
  curs.execute(sql, paramlist)
100
103
  try:
101
- # DuckDB does not support the 'rowcount' attribute
104
+ # DuckDB does not support the 'rowcount' attribute.
102
105
  _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
103
106
  except Exception:
104
- pass
105
- except Exception as e:
107
+ pass # Non-critical: some drivers lack rowcount support.
108
+ except Exception:
106
109
  try:
107
110
  self.rollback()
108
111
  except Exception:
109
- pass
110
- raise e
112
+ pass # Rollback is best-effort after a failed execute.
113
+ raise
111
114
 
112
115
  def exec_cmd(self, querycommand: str) -> None:
113
116
  from execsql.exceptions import DatabaseNotImplementedError
@@ -129,16 +132,16 @@ class Database:
129
132
  try:
130
133
  self.conn.rollback()
131
134
  except Exception:
132
- pass
135
+ pass # Best-effort; connection may already be closed.
133
136
 
134
- def schema_qualified_table_name(self, schema_name: Optional[str], table_name: str) -> str:
137
+ def schema_qualified_table_name(self, schema_name: str | None, table_name: str) -> str:
135
138
  table_name = self.type.quoted(table_name)
136
139
  if schema_name:
137
140
  schema_name = self.type.quoted(schema_name)
138
141
  return f"{schema_name}.{table_name}"
139
142
  return table_name
140
143
 
141
- def select_data(self, sql: str) -> Tuple[List[str], list]:
144
+ def select_data(self, sql: str) -> tuple[list[str], list]:
142
145
  # Returns the results of the sql select statement.
143
146
  curs = self.cursor()
144
147
  try:
@@ -149,18 +152,18 @@ class Database:
149
152
  try:
150
153
  _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
151
154
  except Exception:
152
- pass
155
+ pass # Non-critical: some drivers lack rowcount support.
153
156
  rows = curs.fetchall()
154
157
  return [d[0] for d in curs.description], rows
155
158
 
156
- def select_rowsource(self, sql: str) -> Tuple[List[str], Generator]:
159
+ def select_rowsource(self, sql: str) -> tuple[list[str], Generator]:
157
160
  # Return 1) a list of column names, and 2) an iterable that yields rows.
158
161
  curs = self.cursor()
159
162
  try:
160
- # DuckDB cursors have no 'arraysize' attribute
163
+ # DuckDB cursors have no 'arraysize' attribute.
161
164
  curs.arraysize = _state.conf.export_row_buffer
162
165
  except Exception:
163
- pass
166
+ pass # Non-critical: not all drivers support arraysize.
164
167
  try:
165
168
  curs.execute(sql)
166
169
  except Exception:
@@ -169,7 +172,7 @@ class Database:
169
172
  try:
170
173
  _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
171
174
  except Exception:
172
- pass
175
+ pass # Non-critical: some drivers lack rowcount support.
173
176
 
174
177
  def decode_row() -> Generator:
175
178
  while True:
@@ -180,14 +183,14 @@ class Database:
180
183
  for row in rows:
181
184
  if self.encoding:
182
185
  yield [
183
- c.decode(self.encoding, "backslashreplace") if type(c) == type(b"") else c for c in row
186
+ c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row
184
187
  ]
185
188
  else:
186
189
  yield row
187
190
 
188
191
  return [d[0] for d in curs.description], decode_row()
189
192
 
190
- def select_rowdict(self, sql: str) -> Tuple[List[str], Iterator]:
193
+ def select_rowdict(self, sql: str) -> tuple[list[str], Iterator]:
191
194
  # Return an iterable that yields dictionaries of row data
192
195
  curs = self.cursor()
193
196
  try:
@@ -198,14 +201,14 @@ class Database:
198
201
  try:
199
202
  _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
200
203
  except Exception:
201
- pass
204
+ pass # Non-critical: some drivers lack rowcount support.
202
205
  hdrs = [d[0] for d in curs.description]
203
206
 
204
- def dict_row() -> Optional[dict]:
207
+ def dict_row() -> dict | None:
205
208
  row = curs.fetchone()
206
209
  if row:
207
210
  if self.encoding:
208
- r = [c.decode(self.encoding, "backslashreplace") if type(c) == type(b"") else c for c in row]
211
+ r = [c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row]
209
212
  else:
210
213
  r = row
211
214
  return dict(zip(hdrs, r))
@@ -216,19 +219,22 @@ class Database:
216
219
 
217
220
  def schema_exists(self, schema_name: str) -> bool:
218
221
  curs = self.cursor()
219
- curs.execute(
220
- f"SELECT schema_name FROM information_schema.schemata WHERE schema_name = '{schema_name}';",
221
- )
222
+ sql = f"SELECT schema_name FROM information_schema.schemata WHERE schema_name = {self.paramstr};"
223
+ curs.execute(sql, (schema_name,))
222
224
  rows = curs.fetchall()
223
225
  curs.close()
224
226
  return len(rows) > 0
225
227
 
226
- def table_exists(self, table_name: str, schema_name: Optional[str] = None) -> bool:
228
+ def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
227
229
  curs = self.cursor()
228
- schema_clause = "" if not schema_name else f" and table_schema='{schema_name}'"
229
- sql = f"select table_name from information_schema.tables where table_name = '{table_name}'{schema_clause};"
230
+ params: list = [table_name]
231
+ schema_clause = ""
232
+ if schema_name:
233
+ schema_clause = f" and table_schema={self.paramstr}"
234
+ params.append(schema_name)
235
+ sql = f"select table_name from information_schema.tables where table_name = {self.paramstr}{schema_clause};"
230
236
  try:
231
- curs.execute(sql)
237
+ curs.execute(sql, params)
232
238
  except ErrInfo:
233
239
  raise
234
240
  except Exception:
@@ -247,17 +253,22 @@ class Database:
247
253
  self,
248
254
  table_name: str,
249
255
  column_name: str,
250
- schema_name: Optional[str] = None,
256
+ schema_name: str | None = None,
251
257
  ) -> bool:
252
258
  curs = self.cursor()
253
- schema_clause = "" if not schema_name else f" and table_schema='{schema_name}'"
259
+ params: list = [table_name]
260
+ schema_clause = ""
261
+ if schema_name:
262
+ schema_clause = f" and table_schema={self.paramstr}"
263
+ params.append(schema_name)
264
+ params.append(column_name)
254
265
  sql = (
255
266
  f"select column_name from information_schema.columns "
256
- f"where table_name='{table_name}'{schema_clause} "
257
- f"and column_name='{column_name}';"
267
+ f"where table_name={self.paramstr}{schema_clause} "
268
+ f"and column_name={self.paramstr};"
258
269
  )
259
270
  try:
260
- curs.execute(sql)
271
+ curs.execute(sql, params)
261
272
  except ErrInfo:
262
273
  raise
263
274
  except Exception:
@@ -272,16 +283,20 @@ class Database:
272
283
  curs.close()
273
284
  return len(rows) > 0
274
285
 
275
- def table_columns(self, table_name: str, schema_name: Optional[str] = None) -> List[str]:
286
+ def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
276
287
  curs = self.cursor()
277
- schema_clause = "" if not schema_name else f" and table_schema='{schema_name}'"
288
+ params: list = [table_name]
289
+ schema_clause = ""
290
+ if schema_name:
291
+ schema_clause = f" and table_schema={self.paramstr}"
292
+ params.append(schema_name)
278
293
  sql = (
279
294
  f"select column_name from information_schema.columns "
280
- f"where table_name='{table_name}'{schema_clause} "
295
+ f"where table_name={self.paramstr}{schema_clause} "
281
296
  f"order by ordinal_position;"
282
297
  )
283
298
  try:
284
- curs.execute(sql)
299
+ curs.execute(sql, params)
285
300
  except ErrInfo:
286
301
  raise
287
302
  except Exception:
@@ -296,12 +311,16 @@ class Database:
296
311
  curs.close()
297
312
  return [row[0] for row in rows]
298
313
 
299
- def view_exists(self, view_name: str, schema_name: Optional[str] = None) -> bool:
314
+ def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
300
315
  curs = self.cursor()
301
- schema_clause = "" if not schema_name else f" and table_schema='{schema_name}'"
302
- sql = f"select table_name from information_schema.views where table_name = '{view_name}'{schema_clause};"
316
+ params: list = [view_name]
317
+ schema_clause = ""
318
+ if schema_name:
319
+ schema_clause = f" and table_schema={self.paramstr}"
320
+ params.append(schema_name)
321
+ sql = f"select table_name from information_schema.views where table_name = {self.paramstr}{schema_clause};"
303
322
  try:
304
- curs.execute(sql)
323
+ curs.execute(sql, params)
305
324
  except ErrInfo:
306
325
  raise
307
326
  except Exception:
@@ -328,10 +347,10 @@ class Database:
328
347
 
329
348
  def populate_table(
330
349
  self,
331
- schema_name: Optional[str],
350
+ schema_name: str | None,
332
351
  table_name: str,
333
352
  rowsource: Any,
334
- column_list: List[str],
353
+ column_list: list[str],
335
354
  tablespec_src: Callable,
336
355
  ) -> None:
337
356
  # The rowsource argument must be a generator yielding a list of values for the columns of the table.
@@ -362,9 +381,10 @@ class Database:
362
381
  rows = iter(rowsource)
363
382
  curs = self.cursor()
364
383
  eof = False
384
+ total_rows = 0
365
385
  while True:
366
386
  b = []
367
- for j in range(_state.conf.import_row_buffer):
387
+ for _j in range(_state.conf.import_row_buffer):
368
388
  try:
369
389
  line = next(rows)
370
390
  except StopIteration:
@@ -409,20 +429,19 @@ class Database:
409
429
  " ",
410
430
  line[i],
411
431
  )
412
- if not _state.conf.empty_strings:
413
- if line[i].strip() == "":
414
- line[i] = None
432
+ if not _state.conf.empty_strings and line[i].strip() == "":
433
+ line[i] = None
415
434
  lt = [type_objs[i].from_data(val) if val is not None else None for i, val in enumerate(line)]
416
435
  lt = [type_mod_fn[i](v) if type_mod_fn[i] else v for i, v in enumerate(lt)]
417
- l = []
436
+ row = []
418
437
  for i, v in enumerate(lt):
419
438
  if incl_col[i]:
420
- l.append(v)
439
+ row.append(v)
421
440
  add_line = True
422
441
  if not _state.conf.empty_rows:
423
- add_line = not all([c is None for c in l])
442
+ add_line = not all(c is None for c in row)
424
443
  if add_line:
425
- b.append(l)
444
+ b.append(row)
426
445
  if len(b) > 0:
427
446
  try:
428
447
  curs.executemany(sql, b)
@@ -436,12 +455,22 @@ class Database:
436
455
  exception_msg=exception_desc(),
437
456
  other_msg=f"Can't load data into table {sq_name} of {self.name()} from line {{{line}}}",
438
457
  )
458
+ total_rows += len(b)
459
+ interval = _state.conf.import_progress_interval
460
+ if _state.exec_log and interval > 0 and total_rows % interval == 0:
461
+ _state.exec_log.log_status_info(
462
+ f"IMPORT into {sq_name}: {total_rows} rows imported so far.",
463
+ )
439
464
  if eof:
440
465
  break
466
+ if _state.exec_log:
467
+ _state.exec_log.log_status_info(
468
+ f"IMPORT into {sq_name} complete: {total_rows} rows imported.",
469
+ )
441
470
 
442
471
  def import_tabular_file(
443
472
  self,
444
- schema_name: Optional[str],
473
+ schema_name: str | None,
445
474
  table_name: str,
446
475
  csv_file_obj: Any,
447
476
  skipheader: bool,
@@ -483,12 +512,12 @@ class Database:
483
512
 
484
513
  def import_entire_file(
485
514
  self,
486
- schema_name: Optional[str],
515
+ schema_name: str | None,
487
516
  table_name: str,
488
517
  column_name: str,
489
518
  file_name: str,
490
519
  ) -> None:
491
- with io.open(file_name, "rb") as f:
520
+ with open(file_name, "rb") as f:
492
521
  filedata = f.read()
493
522
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
494
523
  sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
@@ -500,9 +529,9 @@ class DatabasePool:
500
529
  and with the current and initial databases identified."""
501
530
 
502
531
  def __init__(self) -> None:
503
- self.pool: Dict[str, Database] = {}
504
- self.initial_db: Optional[str] = None
505
- self.current_db: Optional[str] = None
532
+ self.pool: dict[str, Database] = {}
533
+ self.initial_db: str | None = None
534
+ self.current_db: str | None = None
506
535
  self.do_rollback: bool = True
507
536
 
508
537
  def __repr__(self) -> str:
@@ -531,7 +560,7 @@ class DatabasePool:
531
560
  self.pool[db_alias].close()
532
561
  self.pool[db_alias] = db_obj
533
562
 
534
- def aliases(self) -> List[str]:
563
+ def aliases(self) -> list[str]:
535
564
  # Return a list of the currently defined aliases
536
565
  return list(self.pool)
537
566
 
execsql/db/dsn.py CHANGED
@@ -8,13 +8,11 @@ registered as an ODBC DSN via ``pyodbc``. Corresponds to ``-t d`` on
8
8
  the CLI.
9
9
  """
10
10
 
11
- import io
12
- from typing import Optional
13
11
 
14
12
  from execsql.db.base import Database
15
13
  from execsql.exceptions import ErrInfo
16
14
  from execsql.utils.errors import exception_desc, fatal_error
17
- from execsql.utils.auth import get_password
15
+ from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
18
16
  import execsql.state as _state
19
17
 
20
18
 
@@ -28,10 +26,10 @@ class DsnDatabase(Database):
28
26
  def __init__(
29
27
  self,
30
28
  dsn_name: str,
31
- user_name: Optional[str],
29
+ user_name: str | None,
32
30
  need_passwd: bool = False,
33
- encoding: Optional[str] = None,
34
- password: Optional[str] = None,
31
+ encoding: str | None = None,
32
+ password: str | None = None,
35
33
  ) -> None:
36
34
  try:
37
35
  import pyodbc # noqa: F401
@@ -65,37 +63,55 @@ class DsnDatabase(Database):
65
63
  self.conn = None
66
64
  if self.need_passwd and self.user and self.password is None:
67
65
  self.password = get_password("DSN", self.db_name, self.user)
68
- cs = "DSN=%s;"
69
- try:
66
+
67
+ def _dsn_connect(autocommit: bool = False):
68
+ cs = "DSN=%s;"
70
69
  if self.need_passwd:
70
+ kwargs = {"autocommit": autocommit} if autocommit else {}
71
71
  self.conn = pyodbc.connect(
72
72
  f"{cs % self.db_name} Uid={self.user}; Pwd={self.password};",
73
+ **kwargs,
73
74
  )
74
75
  else:
75
- self.conn = pyodbc.connect(cs % self.db_name)
76
- except Exception:
77
- excdesc = exception_desc()
78
- if "Optional feature not implemented" in excdesc:
79
- try:
80
- if self.need_passwd:
81
- self.conn = pyodbc.connect(
82
- f"{cs % self.db_name} Uid={self.user}; Pwd={self.password};",
83
- autocommit=True,
76
+ kwargs = {"autocommit": autocommit} if autocommit else {}
77
+ self.conn = pyodbc.connect(cs % self.db_name, **kwargs)
78
+
79
+ def _try_connect():
80
+ try:
81
+ _dsn_connect()
82
+ except Exception:
83
+ excdesc = exception_desc()
84
+ if "Optional feature not implemented" in excdesc:
85
+ try:
86
+ _dsn_connect(autocommit=True)
87
+ except Exception:
88
+ raise ErrInfo(
89
+ type="exception",
90
+ exception_msg=exception_desc(),
91
+ other_msg=f"Can't open DSN database {self.db_name} using ODBC",
84
92
  )
85
- else:
86
- self.conn = pyodbc.connect(cs % self.db_name, autocommit=True)
87
- except Exception:
93
+ else:
88
94
  raise ErrInfo(
89
95
  type="exception",
90
- exception_msg=exception_desc(),
96
+ exception_msg=excdesc,
91
97
  other_msg=f"Can't open DSN database {self.db_name} using ODBC",
92
98
  )
93
- else:
94
- raise ErrInfo(
95
- type="exception",
96
- exception_msg=excdesc,
97
- other_msg=f"Can't open DSN database {self.db_name} using ODBC",
98
- )
99
+
100
+ try:
101
+ _try_connect()
102
+ except ErrInfo:
103
+ if not password_from_keyring():
104
+ raise
105
+ clear_stored_password("DSN", self.db_name, self.user)
106
+ self.password = get_password(
107
+ "DSN",
108
+ self.db_name,
109
+ self.user,
110
+ skip_keyring=True,
111
+ other_msg="(stored credential failed — enter current password)",
112
+ )
113
+ self.conn = None
114
+ _try_connect()
99
115
 
100
116
  def exec_cmd(self, querycommand: str) -> None:
101
117
  # The querycommand must be a stored procedure
@@ -110,14 +126,14 @@ class DsnDatabase(Database):
110
126
 
111
127
  def import_entire_file(
112
128
  self,
113
- schema_name: Optional[str],
129
+ schema_name: str | None,
114
130
  table_name: str,
115
131
  column_name: str,
116
132
  file_name: str,
117
133
  ) -> None:
118
134
  import pyodbc
119
135
 
120
- with io.open(file_name, "rb") as f:
136
+ with open(file_name, "rb") as f:
121
137
  filedata = f.read()
122
138
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
123
139
  sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
execsql/db/duckdb.py CHANGED
@@ -8,8 +8,7 @@ analytics databases via the ``duckdb`` package. Corresponds to ``-t k``
8
8
  on the CLI.
9
9
  """
10
10
 
11
- import os
12
- from typing import Optional
11
+ from pathlib import Path
13
12
 
14
13
  from execsql.db.base import Database
15
14
  from execsql.exceptions import ErrInfo
@@ -28,7 +27,7 @@ class DuckDBDatabase(Database):
28
27
  self.type = dbt_duckdb
29
28
  self.server_name = None
30
29
  self.db_name = DuckDB_fn
31
- self.catalog_name = os.path.splitext(DuckDB_fn)[0]
30
+ self.catalog_name = Path(DuckDB_fn).stem
32
31
  self.user = None
33
32
  self.need_passwd = False
34
33
  self.encoding = "UTF-8"
@@ -76,8 +75,8 @@ class DuckDBDatabase(Database):
76
75
  # In DuckDB, the 'schemata' view is not limited to the current database.
77
76
  curs = self.cursor()
78
77
  curs.execute(
79
- f"SELECT schema_name FROM information_schema.schemata "
80
- f"WHERE schema_name = '{schema_name}' and catalog_name = '{self.catalog_name}';",
78
+ "SELECT schema_name FROM information_schema.schemata WHERE schema_name = ? and catalog_name = ?;",
79
+ (schema_name, self.catalog_name),
81
80
  )
82
81
  rows = curs.fetchall()
83
82
  curs.close()
execsql/db/factory.py CHANGED
@@ -12,8 +12,7 @@ functions are the canonical way to open a new database connection from
12
12
  :mod:`execsql.cli` and the connection-related metacommand handlers.
13
13
  """
14
14
 
15
- import os
16
- from typing import Optional
15
+ from pathlib import Path
17
16
 
18
17
  from execsql.exceptions import ErrInfo
19
18
  from execsql.db.access import AccessDatabase
@@ -30,10 +29,10 @@ from execsql.db.firebird import FirebirdDatabase
30
29
  def db_Access(
31
30
  Access_fn: str,
32
31
  pw_needed: bool = False,
33
- user: Optional[str] = None,
34
- encoding: Optional[str] = None,
32
+ user: str | None = None,
33
+ encoding: str | None = None,
35
34
  ) -> AccessDatabase:
36
- if not os.path.exists(Access_fn):
35
+ if not Path(Access_fn).exists():
37
36
  raise ErrInfo(
38
37
  type="error",
39
38
  other_msg=f'Access database file "{Access_fn}" does not exist.',
@@ -44,26 +43,27 @@ def db_Access(
44
43
  def db_Postgres(
45
44
  server_name: str,
46
45
  database_name: str,
47
- user: Optional[str] = None,
46
+ user: str | None = None,
48
47
  pw_needed: bool = True,
49
- port: Optional[int] = None,
50
- encoding: Optional[str] = None,
48
+ port: int | None = None,
49
+ encoding: str | None = None,
51
50
  new_db: bool = False,
51
+ password: str | None = None,
52
52
  ) -> PostgresDatabase:
53
- return PostgresDatabase(server_name, database_name, user, pw_needed, port, new_db=new_db)
53
+ return PostgresDatabase(server_name, database_name, user, pw_needed, port, new_db=new_db, password=password)
54
54
 
55
55
 
56
56
  def db_SQLite(
57
57
  sqlite_fn: str,
58
58
  new_db: bool = False,
59
- encoding: Optional[str] = None,
59
+ encoding: str | None = None,
60
60
  ) -> SQLiteDatabase:
61
61
  if new_db:
62
62
  from execsql.utils.fileio import check_dir
63
63
 
64
64
  check_dir(sqlite_fn)
65
65
  else:
66
- if not os.path.exists(sqlite_fn):
66
+ if not Path(sqlite_fn).exists():
67
67
  raise ErrInfo(
68
68
  type="error",
69
69
  other_msg=f'SQLite database file "{sqlite_fn}" does not exist.',
@@ -74,10 +74,10 @@ def db_SQLite(
74
74
  def db_SqlServer(
75
75
  server_name: str,
76
76
  database_name: str,
77
- user: Optional[str] = None,
77
+ user: str | None = None,
78
78
  pw_needed: bool = True,
79
- port: Optional[int] = None,
80
- encoding: Optional[str] = None,
79
+ port: int | None = None,
80
+ encoding: str | None = None,
81
81
  ) -> SqlServerDatabase:
82
82
  return SqlServerDatabase(server_name, database_name, user, pw_needed, port, encoding)
83
83
 
@@ -85,10 +85,10 @@ def db_SqlServer(
85
85
  def db_MySQL(
86
86
  server_name: str,
87
87
  database_name: str,
88
- user: Optional[str] = None,
88
+ user: str | None = None,
89
89
  pw_needed: bool = True,
90
- port: Optional[int] = None,
91
- encoding: Optional[str] = None,
90
+ port: int | None = None,
91
+ encoding: str | None = None,
92
92
  ) -> MySQLDatabase:
93
93
  return MySQLDatabase(server_name, database_name, user, pw_needed, port, encoding)
94
94
 
@@ -96,14 +96,14 @@ def db_MySQL(
96
96
  def db_DuckDB(
97
97
  duckdb_fn: str,
98
98
  new_db: bool = False,
99
- encoding: Optional[str] = None,
99
+ encoding: str | None = None,
100
100
  ) -> DuckDBDatabase:
101
101
  if new_db:
102
102
  from execsql.utils.fileio import check_dir
103
103
 
104
104
  check_dir(duckdb_fn)
105
105
  else:
106
- if not os.path.exists(duckdb_fn):
106
+ if not Path(duckdb_fn).exists():
107
107
  raise ErrInfo(
108
108
  type="error",
109
109
  other_msg=f'DuckDB database file "{duckdb_fn}" does not exist.',
@@ -114,10 +114,10 @@ def db_DuckDB(
114
114
  def db_Oracle(
115
115
  server_name: str,
116
116
  database_name: str,
117
- user: Optional[str] = None,
117
+ user: str | None = None,
118
118
  pw_needed: bool = True,
119
- port: Optional[int] = None,
120
- encoding: Optional[str] = None,
119
+ port: int | None = None,
120
+ encoding: str | None = None,
121
121
  ) -> OracleDatabase:
122
122
  return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding)
123
123
 
@@ -125,18 +125,18 @@ def db_Oracle(
125
125
  def db_Firebird(
126
126
  server_name: str,
127
127
  database_name: str,
128
- user: Optional[str] = None,
128
+ user: str | None = None,
129
129
  pw_needed: bool = True,
130
- port: Optional[int] = None,
131
- encoding: Optional[str] = None,
130
+ port: int | None = None,
131
+ encoding: str | None = None,
132
132
  ) -> FirebirdDatabase:
133
133
  return FirebirdDatabase(server_name, database_name, user, pw_needed, port, encoding)
134
134
 
135
135
 
136
136
  def db_Dsn(
137
137
  dsn_name: str,
138
- user: Optional[str] = None,
138
+ user: str | None = None,
139
139
  pw_needed: bool = True,
140
- encoding: Optional[str] = None,
140
+ encoding: str | None = None,
141
141
  ) -> DsnDatabase:
142
142
  return DsnDatabase(dsn_name=dsn_name, user_name=user, need_passwd=pw_needed, encoding=encoding)