execsql2 2.2.1__py3-none-any.whl → 2.4.1__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 (79) hide show
  1. execsql/cli/run.py +11 -5
  2. execsql/config.py +52 -0
  3. execsql/db/access.py +11 -3
  4. execsql/db/base.py +180 -135
  5. execsql/db/dsn.py +4 -0
  6. execsql/db/duckdb.py +4 -0
  7. execsql/db/factory.py +31 -5
  8. execsql/db/firebird.py +4 -0
  9. execsql/db/mysql.py +18 -1
  10. execsql/db/oracle.py +4 -0
  11. execsql/db/postgres.py +3 -0
  12. execsql/db/sqlite.py +3 -0
  13. execsql/db/sqlserver.py +11 -2
  14. execsql/exceptions.py +18 -0
  15. execsql/exporters/base.py +6 -0
  16. execsql/exporters/delimited.py +36 -0
  17. execsql/exporters/duckdb.py +4 -0
  18. execsql/exporters/feather.py +4 -0
  19. execsql/exporters/html.py +6 -0
  20. execsql/exporters/json.py +5 -6
  21. execsql/exporters/latex.py +4 -0
  22. execsql/exporters/ods.py +28 -7
  23. execsql/exporters/parquet.py +3 -0
  24. execsql/exporters/pretty.py +5 -0
  25. execsql/exporters/raw.py +5 -3
  26. execsql/exporters/sqlite.py +4 -0
  27. execsql/exporters/templates.py +16 -6
  28. execsql/exporters/values.py +4 -0
  29. execsql/exporters/xls.py +26 -7
  30. execsql/exporters/xml.py +3 -0
  31. execsql/exporters/zip.py +15 -0
  32. execsql/importers/base.py +5 -3
  33. execsql/importers/csv.py +7 -5
  34. execsql/importers/feather.py +6 -4
  35. execsql/importers/ods.py +2 -0
  36. execsql/importers/xls.py +2 -0
  37. execsql/metacommands/__init__.py +177 -1968
  38. execsql/metacommands/dispatch.py +2011 -0
  39. execsql/models.py +7 -0
  40. execsql/parser.py +10 -0
  41. execsql/script/__init__.py +95 -0
  42. execsql/script/control.py +162 -0
  43. execsql/{script.py → script/engine.py} +144 -406
  44. execsql/script/variables.py +281 -0
  45. execsql/types.py +29 -0
  46. execsql/utils/auth.py +2 -0
  47. execsql/utils/crypto.py +4 -6
  48. execsql/utils/datetime.py +1 -0
  49. execsql/utils/errors.py +11 -0
  50. execsql/utils/fileio.py +18 -0
  51. execsql/utils/gui.py +46 -0
  52. execsql/utils/mail.py +7 -17
  53. execsql/utils/numeric.py +2 -0
  54. execsql/utils/regex.py +9 -0
  55. execsql/utils/strings.py +16 -0
  56. execsql/utils/timer.py +2 -0
  57. execsql2-2.4.1.data/data/execsql2_extras/README.md +65 -0
  58. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/execsql.conf +1 -1
  59. {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/METADATA +8 -1
  60. execsql2-2.4.1.dist-info/RECORD +108 -0
  61. execsql2-2.2.1.data/data/execsql2_extras/READ_ME.rst +0 -127
  62. execsql2-2.2.1.dist-info/RECORD +0 -104
  63. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  64. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  65. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  66. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  67. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  68. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  69. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  70. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  71. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  72. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/script_template.sql +0 -0
  73. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  74. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  75. {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  76. {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/WHEEL +0 -0
  77. {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/entry_points.txt +0 -0
  78. {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/licenses/LICENSE.txt +0 -0
  79. {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/licenses/NOTICE +0 -0
execsql/db/base.py CHANGED
@@ -14,8 +14,10 @@ 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 contextlib
17
18
  import datetime
18
19
  import re
20
+ from abc import ABC, abstractmethod
19
21
  from decimal import Decimal
20
22
  from typing import Any
21
23
  from collections.abc import Callable, Generator, Iterator
@@ -24,6 +26,8 @@ from execsql.exceptions import ErrInfo
24
26
  from execsql.utils.errors import exception_desc
25
27
  import execsql.state as _state
26
28
 
29
+ __all__ = ["Database", "DatabasePool"]
30
+
27
31
 
28
32
  def _default_dt_cast() -> dict[type, Callable]:
29
33
  """Build the default type-cast mapping used by all database backends."""
@@ -41,13 +45,14 @@ def _default_dt_cast() -> dict[type, Callable]:
41
45
  }
42
46
 
43
47
 
44
- class Database:
48
+ class Database(ABC):
45
49
  """Abstract base class for all database connections."""
46
50
 
47
51
  _dt_cast: dict[type, Callable] | None = None
48
52
 
49
53
  @property
50
54
  def dt_cast(self) -> dict[type, Callable]:
55
+ """Return the type-cast mapping, initialising it lazily on first access."""
51
56
  if self._dt_cast is None:
52
57
  self._dt_cast = _default_dt_cast()
53
58
  return self._dt_cast
@@ -85,22 +90,38 @@ class Database:
85
90
  )
86
91
 
87
92
  def name(self) -> str:
93
+ """Return a human-readable description of this connection (DBMS + server/file)."""
88
94
  if self.server_name:
89
95
  return f"{self.type.dbms_id}(server {self.server_name}; database {self.db_name})"
90
96
  else:
91
97
  return f"{self.type.dbms_id}(file {self.db_name})"
92
98
 
99
+ @abstractmethod
93
100
  def open_db(self) -> None:
94
- from execsql.exceptions import DatabaseNotImplementedError
95
-
96
- raise DatabaseNotImplementedError(self.name(), "open_db")
101
+ """Open the underlying database connection."""
102
+ ...
97
103
 
98
104
  def cursor(self):
105
+ """Return a new cursor, opening the connection first if it has not been opened yet."""
99
106
  if self.conn is None:
100
107
  self.open_db()
101
108
  return self.conn.cursor()
102
109
 
110
+ @contextlib.contextmanager
111
+ def _cursor(self):
112
+ """Context manager that yields a cursor and closes it on exit.
113
+
114
+ Works with any DB-API 2.0 cursor regardless of whether the driver
115
+ natively supports the context manager protocol.
116
+ """
117
+ curs = self.cursor()
118
+ try:
119
+ yield curs
120
+ finally:
121
+ curs.close()
122
+
103
123
  def close(self) -> None:
124
+ """Close the database connection, logging a warning if autocommit is off."""
104
125
  if self.conn:
105
126
  if not self.autocommit:
106
127
  _state.exec_log.log_status_info(
@@ -115,24 +136,26 @@ class Database:
115
136
  return '"' + identifier.replace('"', '""') + '"'
116
137
 
117
138
  def paramsubs(self, paramcount: int) -> str:
139
+ """Return a comma-separated string of *paramcount* parameter placeholders."""
118
140
  return ",".join((self.paramstr,) * paramcount)
119
141
 
120
142
  def execute(self, sql: Any, paramlist: list | None = None) -> None:
121
- # A shortcut to self.cursor().execute() that handles encoding.
122
- # Whether or not encoding is needed depends on the DBMS.
143
+ """Execute *sql* (optionally with *paramlist*), updating ``$LAST_ROWCOUNT``.
144
+
145
+ Rolls back the current transaction and re-raises on any driver error.
146
+ """
123
147
  if type(sql) in (tuple, list):
124
148
  sql = " ".join(sql)
125
149
  try:
126
- curs = self.cursor()
127
- if paramlist is None:
128
- curs.execute(sql)
129
- else:
130
- curs.execute(sql, paramlist)
131
- try:
132
- # DuckDB does not support the 'rowcount' attribute.
133
- _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
134
- except Exception:
135
- pass # Non-critical: some drivers lack rowcount support.
150
+ with self._cursor() as curs:
151
+ if paramlist is None:
152
+ curs.execute(sql)
153
+ else:
154
+ curs.execute(sql, paramlist)
155
+ try:
156
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
157
+ except Exception:
158
+ pass # Non-critical: some drivers lack rowcount support.
136
159
  except Exception:
137
160
  try:
138
161
  self.rollback()
@@ -140,22 +163,26 @@ class Database:
140
163
  pass # Rollback is best-effort after a failed execute.
141
164
  raise
142
165
 
166
+ @abstractmethod
143
167
  def exec_cmd(self, querycommand: str) -> None:
144
- from execsql.exceptions import DatabaseNotImplementedError
145
-
146
- raise DatabaseNotImplementedError(self.name(), "exec_cmd")
168
+ """Execute a stored procedure or function by name."""
169
+ ...
147
170
 
148
171
  def autocommit_on(self) -> None:
172
+ """Enable autocommit mode so each statement is committed immediately."""
149
173
  self.autocommit = True
150
174
 
151
175
  def autocommit_off(self) -> None:
176
+ """Disable autocommit mode, grouping subsequent statements into a transaction."""
152
177
  self.autocommit = False
153
178
 
154
179
  def commit(self) -> None:
180
+ """Commit the current transaction if autocommit is enabled."""
155
181
  if self.conn and self.autocommit:
156
182
  self.conn.commit()
157
183
 
158
184
  def rollback(self) -> None:
185
+ """Roll back the current transaction; swallows errors (best-effort)."""
159
186
  if self.conn:
160
187
  try:
161
188
  self.conn.rollback()
@@ -163,6 +190,7 @@ class Database:
163
190
  pass # Best-effort; connection may already be closed.
164
191
 
165
192
  def schema_qualified_table_name(self, schema_name: str | None, table_name: str) -> str:
193
+ """Return the quoted, optionally schema-qualified form of *table_name*."""
166
194
  table_name = self.type.quoted(table_name)
167
195
  if schema_name:
168
196
  schema_name = self.type.quoted(schema_name)
@@ -170,21 +198,22 @@ class Database:
170
198
  return table_name
171
199
 
172
200
  def select_data(self, sql: str) -> tuple[list[str], list]:
173
- # Returns the results of the sql select statement.
174
- curs = self.cursor()
175
- try:
176
- curs.execute(sql)
177
- except Exception:
178
- self.rollback()
179
- raise
180
- try:
181
- _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
182
- except Exception:
183
- pass # Non-critical: some drivers lack rowcount support.
184
- rows = curs.fetchall()
185
- return [d[0] for d in curs.description], rows
201
+ """Execute *sql* and return ``(column_names, rows)`` with all rows fetched into memory."""
202
+ with self._cursor() as curs:
203
+ try:
204
+ curs.execute(sql)
205
+ except Exception:
206
+ self.rollback()
207
+ raise
208
+ try:
209
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
210
+ except Exception:
211
+ pass # Non-critical: some drivers lack rowcount support.
212
+ rows = curs.fetchall()
213
+ return [d[0] for d in curs.description], rows
186
214
 
187
215
  def select_rowsource(self, sql: str) -> tuple[list[str], Generator]:
216
+ """Execute *sql* and return ``(column_names, row_generator)`` for streaming large result sets."""
188
217
  # Return 1) a list of column names, and 2) an iterable that yields rows.
189
218
  curs = self.cursor()
190
219
  try:
@@ -219,6 +248,7 @@ class Database:
219
248
  return [d[0] for d in curs.description], decode_row()
220
249
 
221
250
  def select_rowdict(self, sql: str) -> tuple[list[str], Iterator]:
251
+ """Execute *sql* and return ``(column_names, row_iterator)`` where each row is a ``dict``."""
222
252
  # Return an iterable that yields dictionaries of row data
223
253
  curs = self.cursor()
224
254
  try:
@@ -246,35 +276,35 @@ class Database:
246
276
  return hdrs, iter(dict_row, None)
247
277
 
248
278
  def schema_exists(self, schema_name: str) -> bool:
249
- curs = self.cursor()
250
- sql = f"SELECT schema_name FROM information_schema.schemata WHERE schema_name = {self.paramstr};"
251
- curs.execute(sql, (schema_name,))
252
- rows = curs.fetchall()
253
- curs.close()
279
+ """Return ``True`` if *schema_name* exists in this database."""
280
+ with self._cursor() as curs:
281
+ sql = f"SELECT schema_name FROM information_schema.schemata WHERE schema_name = {self.paramstr};"
282
+ curs.execute(sql, (schema_name,))
283
+ rows = curs.fetchall()
254
284
  return len(rows) > 0
255
285
 
256
286
  def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
257
- curs = self.cursor()
258
- params: list = [table_name]
259
- schema_clause = ""
260
- if schema_name:
261
- schema_clause = f" and table_schema={self.paramstr}"
262
- params.append(schema_name)
263
- sql = f"select table_name from information_schema.tables where table_name = {self.paramstr}{schema_clause};"
264
- try:
265
- curs.execute(sql, params)
266
- except ErrInfo:
267
- raise
268
- except Exception as e:
269
- self.rollback()
270
- raise ErrInfo(
271
- type="db",
272
- command_text=sql,
273
- exception_msg=exception_desc(),
274
- other_msg=f"Failed test for existence of table {table_name} in {self.name()}",
275
- ) from e
276
- rows = curs.fetchall()
277
- curs.close()
287
+ """Return ``True`` if *table_name* (optionally in *schema_name*) exists."""
288
+ with self._cursor() as curs:
289
+ params: list = [table_name]
290
+ schema_clause = ""
291
+ if schema_name:
292
+ schema_clause = f" and table_schema={self.paramstr}"
293
+ params.append(schema_name)
294
+ sql = f"select table_name from information_schema.tables where table_name = {self.paramstr}{schema_clause};"
295
+ try:
296
+ curs.execute(sql, params)
297
+ except ErrInfo:
298
+ raise
299
+ except Exception as e:
300
+ self.rollback()
301
+ raise ErrInfo(
302
+ type="db",
303
+ command_text=sql,
304
+ exception_msg=exception_desc(),
305
+ other_msg=f"Failed test for existence of table {table_name} in {self.name()}",
306
+ ) from e
307
+ rows = curs.fetchall()
278
308
  return len(rows) > 0
279
309
 
280
310
  def column_exists(
@@ -283,92 +313,94 @@ class Database:
283
313
  column_name: str,
284
314
  schema_name: str | None = None,
285
315
  ) -> bool:
286
- curs = self.cursor()
287
- params: list = [table_name]
288
- schema_clause = ""
289
- if schema_name:
290
- schema_clause = f" and table_schema={self.paramstr}"
291
- params.append(schema_name)
292
- params.append(column_name)
293
- sql = (
294
- f"select column_name from information_schema.columns "
295
- f"where table_name={self.paramstr}{schema_clause} "
296
- f"and column_name={self.paramstr};"
297
- )
298
- try:
299
- curs.execute(sql, params)
300
- except ErrInfo:
301
- raise
302
- except Exception as e:
303
- self.rollback()
304
- raise ErrInfo(
305
- type="db",
306
- command_text=sql,
307
- exception_msg=exception_desc(),
308
- other_msg=f"Failed test for existence of column {column_name} in table {table_name} of {self.name()}",
309
- ) from e
310
- rows = curs.fetchall()
311
- curs.close()
316
+ """Return ``True`` if *column_name* exists in *table_name* (optionally in *schema_name*)."""
317
+ with self._cursor() as curs:
318
+ params: list = [table_name]
319
+ schema_clause = ""
320
+ if schema_name:
321
+ schema_clause = f" and table_schema={self.paramstr}"
322
+ params.append(schema_name)
323
+ params.append(column_name)
324
+ sql = (
325
+ f"select column_name from information_schema.columns "
326
+ f"where table_name={self.paramstr}{schema_clause} "
327
+ f"and column_name={self.paramstr};"
328
+ )
329
+ try:
330
+ curs.execute(sql, params)
331
+ except ErrInfo:
332
+ raise
333
+ except Exception as e:
334
+ self.rollback()
335
+ raise ErrInfo(
336
+ type="db",
337
+ command_text=sql,
338
+ exception_msg=exception_desc(),
339
+ other_msg=f"Failed test for existence of column {column_name} in table {table_name} of {self.name()}",
340
+ ) from e
341
+ rows = curs.fetchall()
312
342
  return len(rows) > 0
313
343
 
314
344
  def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
315
- curs = self.cursor()
316
- params: list = [table_name]
317
- schema_clause = ""
318
- if schema_name:
319
- schema_clause = f" and table_schema={self.paramstr}"
320
- params.append(schema_name)
321
- sql = (
322
- f"select column_name from information_schema.columns "
323
- f"where table_name={self.paramstr}{schema_clause} "
324
- f"order by ordinal_position;"
325
- )
326
- try:
327
- curs.execute(sql, params)
328
- except ErrInfo:
329
- raise
330
- except Exception as e:
331
- self.rollback()
332
- raise ErrInfo(
333
- type="db",
334
- command_text=sql,
335
- exception_msg=exception_desc(),
336
- other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
337
- ) from e
338
- rows = curs.fetchall()
339
- curs.close()
345
+ """Return the ordered list of column names for *table_name*."""
346
+ with self._cursor() as curs:
347
+ params: list = [table_name]
348
+ schema_clause = ""
349
+ if schema_name:
350
+ schema_clause = f" and table_schema={self.paramstr}"
351
+ params.append(schema_name)
352
+ sql = (
353
+ f"select column_name from information_schema.columns "
354
+ f"where table_name={self.paramstr}{schema_clause} "
355
+ f"order by ordinal_position;"
356
+ )
357
+ try:
358
+ curs.execute(sql, params)
359
+ except ErrInfo:
360
+ raise
361
+ except Exception as e:
362
+ self.rollback()
363
+ raise ErrInfo(
364
+ type="db",
365
+ command_text=sql,
366
+ exception_msg=exception_desc(),
367
+ other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
368
+ ) from e
369
+ rows = curs.fetchall()
340
370
  return [row[0] for row in rows]
341
371
 
342
372
  def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
343
- curs = self.cursor()
344
- params: list = [view_name]
345
- schema_clause = ""
346
- if schema_name:
347
- schema_clause = f" and table_schema={self.paramstr}"
348
- params.append(schema_name)
349
- sql = f"select table_name from information_schema.views where table_name = {self.paramstr}{schema_clause};"
350
- try:
351
- curs.execute(sql, params)
352
- except ErrInfo:
353
- raise
354
- except Exception as e:
355
- self.rollback()
356
- raise ErrInfo(
357
- type="db",
358
- command_text=sql,
359
- exception_msg=exception_desc(),
360
- other_msg=f"Failed test for existence of view {view_name} in {self.name()}",
361
- ) from e
362
- rows = curs.fetchall()
363
- curs.close()
373
+ """Return ``True`` if *view_name* (optionally in *schema_name*) exists."""
374
+ with self._cursor() as curs:
375
+ params: list = [view_name]
376
+ schema_clause = ""
377
+ if schema_name:
378
+ schema_clause = f" and table_schema={self.paramstr}"
379
+ params.append(schema_name)
380
+ sql = f"select table_name from information_schema.views where table_name = {self.paramstr}{schema_clause};"
381
+ try:
382
+ curs.execute(sql, params)
383
+ except ErrInfo:
384
+ raise
385
+ except Exception as e:
386
+ self.rollback()
387
+ raise ErrInfo(
388
+ type="db",
389
+ command_text=sql,
390
+ exception_msg=exception_desc(),
391
+ other_msg=f"Failed test for existence of view {view_name} in {self.name()}",
392
+ ) from e
393
+ rows = curs.fetchall()
364
394
  return len(rows) > 0
365
395
 
366
396
  def role_exists(self, rolename: str) -> bool:
397
+ """Return ``True`` if *rolename* exists; subclasses must override this."""
367
398
  from execsql.exceptions import DatabaseNotImplementedError
368
399
 
369
400
  raise DatabaseNotImplementedError(self.name(), "role_exists")
370
401
 
371
402
  def drop_table(self, tablename: str) -> None:
403
+ """Drop *tablename* if it exists; *tablename* must already be schema-qualified and quoted."""
372
404
  # The 'tablename' argument should be schema-qualified and quoted as necessary.
373
405
  self.execute(f"drop table if exists {tablename} cascade;")
374
406
  self.commit()
@@ -381,6 +413,11 @@ class Database:
381
413
  column_list: list[str],
382
414
  tablespec_src: Callable,
383
415
  ) -> None:
416
+ """Bulk-insert rows from *rowsource* into *table_name* using the columns in *column_list*.
417
+
418
+ *rowsource* must be a generator yielding lists of values in column order.
419
+ *tablespec_src* is a zero-argument callable that returns the table's type specification.
420
+ """
384
421
  # The rowsource argument must be a generator yielding a list of values for the columns of the table.
385
422
  # The column_list argument must an iterable containing column names. This may be a subset of
386
423
  # the names of columns in the rowsource.
@@ -542,6 +579,7 @@ class Database:
542
579
  csv_file_obj: Any,
543
580
  skipheader: bool,
544
581
  ) -> None:
582
+ """Import a CSV/tabular file into *table_name*; column names must be compatible."""
545
583
  # Import a text (CSV) file containing tabular data to a table. Columns must be compatible.
546
584
  if not self.table_exists(table_name, schema_name):
547
585
  raise ErrInfo(
@@ -584,12 +622,14 @@ class Database:
584
622
  column_name: str,
585
623
  file_name: str,
586
624
  ) -> None:
625
+ """Insert the raw binary content of *file_name* as a single row into *column_name* of *table_name*."""
587
626
  with open(file_name, "rb") as f:
588
627
  filedata = f.read()
589
628
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
590
629
  quoted_col = self.quote_identifier(column_name)
591
630
  sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
592
- self.cursor().execute(sql, (filedata,))
631
+ with self._cursor() as curs:
632
+ curs.execute(sql, (filedata,))
593
633
 
594
634
 
595
635
  class DatabasePool:
@@ -606,6 +646,7 @@ class DatabasePool:
606
646
  return "DatabasePool()"
607
647
 
608
648
  def add(self, db_alias: str, db_obj: Database) -> None:
649
+ """Register *db_obj* under *db_alias*, setting it as initial/current if this is the first connection."""
609
650
  db_alias = db_alias.lower()
610
651
  if db_alias == "initial" and len(self.pool) > 0:
611
652
  raise ErrInfo(
@@ -629,25 +670,27 @@ class DatabasePool:
629
670
  self.pool[db_alias] = db_obj
630
671
 
631
672
  def aliases(self) -> list[str]:
632
- # Return a list of the currently defined aliases
673
+ """Return a list of all currently registered database aliases."""
633
674
  return list(self.pool)
634
675
 
635
676
  def current(self) -> Database:
636
- # Return the current db object.
677
+ """Return the currently active ``Database`` object."""
637
678
  return self.pool[self.current_db]
638
679
 
639
680
  def current_alias(self) -> str:
640
- # Return the alias of the current db object.
681
+ """Return the alias string for the currently active database."""
641
682
  return self.current_db
642
683
 
643
684
  def initial(self) -> Database:
685
+ """Return the first ``Database`` that was added to the pool."""
644
686
  return self.pool[self.initial_db]
645
687
 
646
688
  def aliased_as(self, db_alias: str) -> Database:
689
+ """Return the ``Database`` registered under *db_alias*."""
647
690
  return self.pool[db_alias]
648
691
 
649
692
  def make_current(self, db_alias: str) -> None:
650
- # Change the current database in use.
693
+ """Set the active database to *db_alias*; raises ``ErrInfo`` if the alias is unknown."""
651
694
  db_alias = db_alias.lower()
652
695
  if db_alias not in self.pool:
653
696
  raise ErrInfo(
@@ -657,6 +700,7 @@ class DatabasePool:
657
700
  self.current_db = db_alias
658
701
 
659
702
  def disconnect(self, alias: str) -> None:
703
+ """Close and remove the connection registered under *alias* from the pool."""
660
704
  if alias == self.current_db or (alias == "initial" and "initial" in self.pool):
661
705
  raise ErrInfo(
662
706
  type="error",
@@ -667,6 +711,7 @@ class DatabasePool:
667
711
  del self.pool[alias]
668
712
 
669
713
  def closeall(self) -> None:
714
+ """Roll back and close every connection in the pool, then reset the pool to empty."""
670
715
  for alias, db in self.pool.items():
671
716
  nm = db.name()
672
717
  try:
execsql/db/dsn.py CHANGED
@@ -15,8 +15,12 @@ from execsql.utils.errors import exception_desc, fatal_error
15
15
  from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
16
16
  import execsql.state as _state
17
17
 
18
+ __all__ = ["DsnDatabase"]
19
+
18
20
 
19
21
  class DsnDatabase(Database):
22
+ """Generic ODBC adapter that connects to any data source registered as an ODBC DSN via pyodbc."""
23
+
20
24
  # There's no telling what is actually connected to a DSN, so this uses
21
25
  # generic Database methods almost exclusively. Only 'exec_cmd()' is
22
26
  # overridden, and that uses the method for SQL Server because the DAO
execsql/db/duckdb.py CHANGED
@@ -15,8 +15,12 @@ from execsql.exceptions import ErrInfo
15
15
  from execsql.utils.errors import exception_desc, fatal_error
16
16
  import execsql.state as _state
17
17
 
18
+ __all__ = ["DuckDBDatabase"]
19
+
18
20
 
19
21
  class DuckDBDatabase(Database):
22
+ """DuckDB in-process analytics adapter using the duckdb package."""
23
+
20
24
  def __init__(self, DuckDB_fn: str) -> None:
21
25
  try:
22
26
  import duckdb # noqa: F401
execsql/db/factory.py CHANGED
@@ -25,6 +25,18 @@ from execsql.db.duckdb import DuckDBDatabase
25
25
  from execsql.db.mysql import MySQLDatabase
26
26
  from execsql.db.firebird import FirebirdDatabase
27
27
 
28
+ __all__ = [
29
+ "db_Access",
30
+ "db_Dsn",
31
+ "db_DuckDB",
32
+ "db_Firebird",
33
+ "db_MySQL",
34
+ "db_Oracle",
35
+ "db_Postgres",
36
+ "db_SQLite",
37
+ "db_SqlServer",
38
+ ]
39
+
28
40
 
29
41
  def db_Access(
30
42
  Access_fn: str,
@@ -32,6 +44,7 @@ def db_Access(
32
44
  user: str | None = None,
33
45
  encoding: str | None = None,
34
46
  ) -> AccessDatabase:
47
+ """Open an MS Access database file (.mdb or .accdb) via DAO/ODBC."""
35
48
  if not Path(Access_fn).exists():
36
49
  raise ErrInfo(
37
50
  type="error",
@@ -50,6 +63,7 @@ def db_Postgres(
50
63
  new_db: bool = False,
51
64
  password: str | None = None,
52
65
  ) -> PostgresDatabase:
66
+ """Open a new PostgreSQL connection via psycopg2."""
53
67
  return PostgresDatabase(server_name, database_name, user, pw_needed, port, new_db=new_db, password=password)
54
68
 
55
69
 
@@ -58,6 +72,7 @@ def db_SQLite(
58
72
  new_db: bool = False,
59
73
  encoding: str | None = None,
60
74
  ) -> SQLiteDatabase:
75
+ """Open a SQLite database file via the standard-library sqlite3 module."""
61
76
  if new_db:
62
77
  from execsql.utils.fileio import check_dir
63
78
 
@@ -78,8 +93,10 @@ def db_SqlServer(
78
93
  pw_needed: bool = True,
79
94
  port: int | None = None,
80
95
  encoding: str | None = None,
96
+ password: str | None = None,
81
97
  ) -> SqlServerDatabase:
82
- return SqlServerDatabase(server_name, database_name, user, pw_needed, port, encoding)
98
+ """Open a Microsoft SQL Server connection via pyodbc."""
99
+ return SqlServerDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
83
100
 
84
101
 
85
102
  def db_MySQL(
@@ -89,8 +106,10 @@ def db_MySQL(
89
106
  pw_needed: bool = True,
90
107
  port: int | None = None,
91
108
  encoding: str | None = None,
109
+ password: str | None = None,
92
110
  ) -> MySQLDatabase:
93
- return MySQLDatabase(server_name, database_name, user, pw_needed, port, encoding)
111
+ """Open a MySQL or MariaDB connection via pymysql."""
112
+ return MySQLDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
94
113
 
95
114
 
96
115
  def db_DuckDB(
@@ -98,6 +117,7 @@ def db_DuckDB(
98
117
  new_db: bool = False,
99
118
  encoding: str | None = None,
100
119
  ) -> DuckDBDatabase:
120
+ """Open a DuckDB in-process analytics database file via the duckdb package."""
101
121
  if new_db:
102
122
  from execsql.utils.fileio import check_dir
103
123
 
@@ -118,8 +138,10 @@ def db_Oracle(
118
138
  pw_needed: bool = True,
119
139
  port: int | None = None,
120
140
  encoding: str | None = None,
141
+ password: str | None = None,
121
142
  ) -> OracleDatabase:
122
- return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding)
143
+ """Open an Oracle database connection via cx_Oracle (python-oracledb)."""
144
+ return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
123
145
 
124
146
 
125
147
  def db_Firebird(
@@ -129,8 +151,10 @@ def db_Firebird(
129
151
  pw_needed: bool = True,
130
152
  port: int | None = None,
131
153
  encoding: str | None = None,
154
+ password: str | None = None,
132
155
  ) -> FirebirdDatabase:
133
- return FirebirdDatabase(server_name, database_name, user, pw_needed, port, encoding)
156
+ """Open a Firebird database connection via the firebird-driver package."""
157
+ return FirebirdDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
134
158
 
135
159
 
136
160
  def db_Dsn(
@@ -138,5 +162,7 @@ def db_Dsn(
138
162
  user: str | None = None,
139
163
  pw_needed: bool = True,
140
164
  encoding: str | None = None,
165
+ password: str | None = None,
141
166
  ) -> DsnDatabase:
142
- return DsnDatabase(dsn_name=dsn_name, user_name=user, need_passwd=pw_needed, encoding=encoding)
167
+ """Open a connection to any ODBC data source registered under *dsn_name*."""
168
+ return DsnDatabase(dsn_name=dsn_name, user_name=user, need_passwd=pw_needed, encoding=encoding, password=password)
execsql/db/firebird.py CHANGED
@@ -14,8 +14,12 @@ from execsql.utils.errors import exception_desc, fatal_error
14
14
  from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
15
15
  import execsql.state as _state
16
16
 
17
+ __all__ = ["FirebirdDatabase"]
18
+
17
19
 
18
20
  class FirebirdDatabase(Database):
21
+ """Firebird adapter using the firebird-driver (fdb) package."""
22
+
19
23
  def __init__(
20
24
  self,
21
25
  server_name: str,