dbtk 0.8.0__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.
- dbtk/__init__.py +54 -0
- dbtk/cli.py +169 -0
- dbtk/config.py +1194 -0
- dbtk/cursors.py +566 -0
- dbtk/database.py +959 -0
- dbtk/dbtk_sample.yml +90 -0
- dbtk/defaults.py +29 -0
- dbtk/etl/__init__.py +61 -0
- dbtk/etl/base_surge.py +257 -0
- dbtk/etl/bulk_surge.py +783 -0
- dbtk/etl/config_generators.py +458 -0
- dbtk/etl/data_surge.py +294 -0
- dbtk/etl/managers.py +734 -0
- dbtk/etl/table.py +1215 -0
- dbtk/etl/transforms/__init__.py +107 -0
- dbtk/etl/transforms/address.py +560 -0
- dbtk/etl/transforms/core.py +594 -0
- dbtk/etl/transforms/database.py +506 -0
- dbtk/etl/transforms/datetime.py +497 -0
- dbtk/etl/transforms/email.py +72 -0
- dbtk/etl/transforms/phone.py +670 -0
- dbtk/formats/__init__.py +18 -0
- dbtk/formats/edi.py +182 -0
- dbtk/logging_utils.py +310 -0
- dbtk/readers/__init__.py +26 -0
- dbtk/readers/base.py +644 -0
- dbtk/readers/csv.py +195 -0
- dbtk/readers/data_frame.py +119 -0
- dbtk/readers/excel.py +260 -0
- dbtk/readers/fixed_width.py +359 -0
- dbtk/readers/json.py +323 -0
- dbtk/readers/utils.py +394 -0
- dbtk/readers/xml.py +260 -0
- dbtk/record.py +710 -0
- dbtk/utils.py +537 -0
- dbtk/writers/__init__.py +46 -0
- dbtk/writers/base.py +718 -0
- dbtk/writers/csv.py +107 -0
- dbtk/writers/database.py +158 -0
- dbtk/writers/excel.py +1086 -0
- dbtk/writers/fixed_width.py +290 -0
- dbtk/writers/json.py +156 -0
- dbtk/writers/utils.py +41 -0
- dbtk/writers/xml.py +387 -0
- dbtk-0.8.0.dist-info/METADATA +305 -0
- dbtk-0.8.0.dist-info/RECORD +50 -0
- dbtk-0.8.0.dist-info/WHEEL +5 -0
- dbtk-0.8.0.dist-info/entry_points.txt +2 -0
- dbtk-0.8.0.dist-info/licenses/LICENSE.txt +7 -0
- dbtk-0.8.0.dist-info/top_level.txt +1 -0
dbtk/database.py
ADDED
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
# dbtk/database.py
|
|
2
|
+
"""
|
|
3
|
+
Database connection wrapper that provides a uniform interface
|
|
4
|
+
to different database adapters.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
import os
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Dict, Any, Optional, Union, Type, List
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
|
|
13
|
+
from .cursors import Cursor, ParamStyle
|
|
14
|
+
from .defaults import settings
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# users can define their own drivers in the config file
|
|
19
|
+
_user_drivers = {}
|
|
20
|
+
|
|
21
|
+
def _hide_password(kwargs):
|
|
22
|
+
"""Replace password with '********' to be printable"""
|
|
23
|
+
parms = kwargs.copy()
|
|
24
|
+
for key, val in parms.items():
|
|
25
|
+
if key in ('password', 'PWD', 'passwd'):
|
|
26
|
+
parms[key] = '********'
|
|
27
|
+
return parms
|
|
28
|
+
|
|
29
|
+
DRIVERS = {
|
|
30
|
+
# PostgreSQL Drivers
|
|
31
|
+
'psycopg2': {
|
|
32
|
+
'database_type': 'postgres',
|
|
33
|
+
'priority': 11,
|
|
34
|
+
'param_map': {'database': 'dbname'},
|
|
35
|
+
'required_params': [{'host', 'database', 'user'}],
|
|
36
|
+
'optional_params': {'port', 'password', 'sslmode', 'connect_timeout', 'application_name',
|
|
37
|
+
'client_encoding', 'options', 'sslcert', 'sslkey', 'sslrootcert'},
|
|
38
|
+
'connection_method': 'connection_string',
|
|
39
|
+
'default_port': 5432,
|
|
40
|
+
},
|
|
41
|
+
'psycopg': { # psycopg3
|
|
42
|
+
'database_type': 'postgres',
|
|
43
|
+
'priority': 12,
|
|
44
|
+
'param_map': {'database': 'dbname'},
|
|
45
|
+
'required_params': [{'host', 'database', 'user'}],
|
|
46
|
+
'optional_params': {'port', 'password', 'sslmode', 'connect_timeout', 'application_name',
|
|
47
|
+
'client_encoding', 'options', 'sslcert', 'sslkey', 'sslrootcert'},
|
|
48
|
+
'connection_method': 'connection_string',
|
|
49
|
+
'default_port': 5432,
|
|
50
|
+
'note': 'pip install psycopg[binary]'
|
|
51
|
+
},
|
|
52
|
+
'pgdb': {
|
|
53
|
+
'database_type': 'postgres',
|
|
54
|
+
'priority': 13,
|
|
55
|
+
'param_map': {'database': 'database'},
|
|
56
|
+
'required_params': [{'host', 'database', 'user'}],
|
|
57
|
+
'optional_params': {'port', 'password'},
|
|
58
|
+
'connection_method': 'kwargs',
|
|
59
|
+
'default_port': 5432,
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
# Oracle Drivers
|
|
63
|
+
'oracledb': {
|
|
64
|
+
'database_type': 'oracle',
|
|
65
|
+
'priority': 11,
|
|
66
|
+
'param_map': {'database': 'service_name'},
|
|
67
|
+
'required_params': [{'dsn', 'user'}, {'host', 'port', 'database', 'user'}],
|
|
68
|
+
'optional_params': {'password', 'mode', 'events', 'purity', 'cclass', 'tag', 'matchanytag',
|
|
69
|
+
'config_dir', 'wallet_location', 'wallet_password'},
|
|
70
|
+
'connection_method': 'dsn',
|
|
71
|
+
'default_port': 1521
|
|
72
|
+
},
|
|
73
|
+
'cx_Oracle': {
|
|
74
|
+
'database_type': 'oracle',
|
|
75
|
+
'priority': 12,
|
|
76
|
+
'param_map': {'database': 'service_name'},
|
|
77
|
+
'required_params': [{'dsn'}, {'host', 'port', 'database', 'user'}],
|
|
78
|
+
'optional_params': {'password', 'mode', 'events', 'purity', 'cclass', 'tag', 'matchanytag',
|
|
79
|
+
'encoding', 'nencoding', 'edition', 'appcontext'},
|
|
80
|
+
'connection_method': 'dsn',
|
|
81
|
+
'default_port': 1521
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
# MySQL Drivers
|
|
85
|
+
'MySQLdb': {
|
|
86
|
+
'database_type': 'mysql',
|
|
87
|
+
'priority': 11,
|
|
88
|
+
'param_map': {'database': 'db', 'password': 'passwd'},
|
|
89
|
+
'required_params': [{'host', 'database', 'user'}],
|
|
90
|
+
'optional_params': {'port', 'password', 'charset', 'use_unicode', 'sql_mode', 'read_default_file',
|
|
91
|
+
'conv', 'connect_timeout', 'compress', 'named_pipe', 'init_command',
|
|
92
|
+
'read_default_group', 'unix_socket', 'port'},
|
|
93
|
+
'connection_method': 'kwargs',
|
|
94
|
+
'default_port': 3306,
|
|
95
|
+
'note': 'pip install mysqlclient',
|
|
96
|
+
},
|
|
97
|
+
'mariadb': {
|
|
98
|
+
'database_type': 'mysql',
|
|
99
|
+
'priority': 12,
|
|
100
|
+
'param_map': {},
|
|
101
|
+
'required_params': [{'host', 'database', 'user'}],
|
|
102
|
+
'optional_params': {'port', 'password', 'unix_socket', 'ssl', 'tls_version',
|
|
103
|
+
'autocommit', 'converter', 'connect_timeout'},
|
|
104
|
+
'connection_method': 'kwargs',
|
|
105
|
+
'default_port': 3306
|
|
106
|
+
},
|
|
107
|
+
'mysql.connector': {
|
|
108
|
+
'database_type': 'mysql',
|
|
109
|
+
'priority': 13,
|
|
110
|
+
'param_map': {},
|
|
111
|
+
'required_params': [{'host', 'database', 'user'}],
|
|
112
|
+
'optional_params': {'port', 'password', 'charset', 'collation', 'autocommit', 'time_zone',
|
|
113
|
+
'sql_mode', 'use_unicode', 'get_warnings', 'raise_on_warnings',
|
|
114
|
+
'connection_timeout', 'buffered', 'raw', 'consume_results'},
|
|
115
|
+
'connection_method': 'kwargs',
|
|
116
|
+
'default_port': 3306,
|
|
117
|
+
'note': 'pip install mysql-connector-python'
|
|
118
|
+
},
|
|
119
|
+
'pymysql': {
|
|
120
|
+
'database_type': 'mysql',
|
|
121
|
+
'priority': 14,
|
|
122
|
+
'param_map': {'database': 'db', 'password': 'passwd'},
|
|
123
|
+
'required_params': [{'host', 'database', 'user'}],
|
|
124
|
+
'optional_params': {'port', 'password', 'charset', 'sql_mode', 'read_default_file',
|
|
125
|
+
'conv', 'use_unicode', 'connect_timeout', 'read_timeout', 'write_timeout',
|
|
126
|
+
'bind_address', 'unix_socket', 'autocommit'},
|
|
127
|
+
'connection_method': 'kwargs',
|
|
128
|
+
'default_port': 3306
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
# SQL Server Drivers
|
|
132
|
+
'pyodbc_sqlserver': {
|
|
133
|
+
'database_type': 'sqlserver',
|
|
134
|
+
'module': 'pyodbc',
|
|
135
|
+
'priority': 11,
|
|
136
|
+
'param_map': {'host': 'SERVER', 'database': 'DATABASE', 'user': 'UID', 'password': 'PWD'},
|
|
137
|
+
'required_params': [{'host', 'database', 'user'}, {'dsn'}],
|
|
138
|
+
'optional_params': {'password', 'port', 'driver', 'trusted_connection', 'encrypt', 'trustservercertificate'},
|
|
139
|
+
'connection_method': 'odbc_string',
|
|
140
|
+
'odbc_driver_name': 'ODBC Driver 17 for SQL Server',
|
|
141
|
+
'default_port': 1433
|
|
142
|
+
},
|
|
143
|
+
'pymssql': {
|
|
144
|
+
'database_type': 'sqlserver',
|
|
145
|
+
'priority': 12,
|
|
146
|
+
'param_map': {},
|
|
147
|
+
'required_params': [{'host', 'database', 'user'}],
|
|
148
|
+
'optional_params': {'password', 'port', 'timeout', 'login_timeout', 'charset', 'as_dict', 'appname'},
|
|
149
|
+
'connection_method': 'kwargs',
|
|
150
|
+
'default_port': 1433
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
# ODBC Drivers for other databases
|
|
154
|
+
'pyodbc_postgres': {
|
|
155
|
+
'database_type': 'postgres',
|
|
156
|
+
'module': 'pyodbc',
|
|
157
|
+
'priority': 14,
|
|
158
|
+
'param_map': {'host': 'SERVER', 'database': 'DATABASE', 'user': 'UID', 'password': 'PWD'},
|
|
159
|
+
'required_params': [{'host', 'database', 'user'}, {'dsn'}],
|
|
160
|
+
'optional_params': {'password', 'port'},
|
|
161
|
+
'connection_method': 'odbc_string',
|
|
162
|
+
'odbc_driver_name': 'PostgreSQL Unicode',
|
|
163
|
+
'default_port': 5432
|
|
164
|
+
},
|
|
165
|
+
'pyodbc_mysql': {
|
|
166
|
+
'database_type': 'mysql',
|
|
167
|
+
'module': 'pyodbc',
|
|
168
|
+
'priority': 16,
|
|
169
|
+
'param_map': {'host': 'SERVER', 'database': 'DATABASE', 'user': 'UID', 'password': 'PWD'},
|
|
170
|
+
'required_params': [{'host', 'database', 'user'}],
|
|
171
|
+
'optional_params': {'password', 'port'},
|
|
172
|
+
'connection_method': 'odbc_string',
|
|
173
|
+
'odbc_driver_name': 'MySQL ODBC 8.0 Unicode Driver',
|
|
174
|
+
'default_port': 3306
|
|
175
|
+
},
|
|
176
|
+
'pyodbc_oracle': {
|
|
177
|
+
'database_type': 'oracle',
|
|
178
|
+
'module': 'pyodbc',
|
|
179
|
+
'priority': 13,
|
|
180
|
+
'param_map': {'host': 'SERVER', 'database': 'DATABASE', 'user': 'UID', 'password': 'PWD'},
|
|
181
|
+
'required_params': [{'host', 'database', 'user'}],
|
|
182
|
+
'optional_params': {'password', 'port'},
|
|
183
|
+
'connection_method': 'odbc_string',
|
|
184
|
+
'odbc_driver_name': 'Oracle in OraClient19Home1',
|
|
185
|
+
'default_port': 1521
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
# SQLite Driver
|
|
189
|
+
'sqlite3': {
|
|
190
|
+
'database_type': 'sqlite',
|
|
191
|
+
'priority': 1,
|
|
192
|
+
'param_map': {},
|
|
193
|
+
'required_params': [{'database'}],
|
|
194
|
+
'optional_params': {'timeout', 'detect_types', 'isolation_level', 'check_same_thread',
|
|
195
|
+
'factory', 'cached_statements', 'uri'},
|
|
196
|
+
'connection_method': 'kwargs'
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def register_user_drivers(drivers_config: dict) -> None:
|
|
202
|
+
"""Register drivers from config file."""
|
|
203
|
+
global _user_drivers
|
|
204
|
+
_user_drivers.update(drivers_config)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _get_all_drivers() -> dict:
|
|
208
|
+
"""Get combined built-in and user drivers."""
|
|
209
|
+
return {**DRIVERS, **_user_drivers}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _get_drivers_for_database(db_type: str, valid_only: bool = True) -> List[str]:
|
|
213
|
+
"""
|
|
214
|
+
Gets a list of drivers available for the specified database type.
|
|
215
|
+
|
|
216
|
+
Iterates through DRIVERS to identify which ones match the specified database type.
|
|
217
|
+
Drivers can be filtered to include only those that are currently importable and available for use.
|
|
218
|
+
The result is a sorted list of driver names based on their priority.
|
|
219
|
+
|
|
220
|
+
Parameters:
|
|
221
|
+
db_type (str): The type of database for which to retrieve drivers.
|
|
222
|
+
valid_only (bool): Specifies whether to include only valid and importable
|
|
223
|
+
drivers (default is True).
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
List[str]: A sorted list of driver names available for the given database type.
|
|
227
|
+
"""
|
|
228
|
+
all_drivers = _get_all_drivers()
|
|
229
|
+
available_drivers = []
|
|
230
|
+
|
|
231
|
+
for driver_name, info in all_drivers.items():
|
|
232
|
+
if info['database_type'] == db_type:
|
|
233
|
+
if valid_only:
|
|
234
|
+
# only add if the driver is available (importable)
|
|
235
|
+
module_name = info.get('module', driver_name)
|
|
236
|
+
spec = importlib.util.find_spec(module_name)
|
|
237
|
+
if spec:
|
|
238
|
+
available_drivers.append(driver_name)
|
|
239
|
+
else:
|
|
240
|
+
available_drivers.append(driver_name)
|
|
241
|
+
|
|
242
|
+
def sort_key(driver_name):
|
|
243
|
+
priority = all_drivers[driver_name]['priority']
|
|
244
|
+
# User drivers get slight priority boost for tie-breaking
|
|
245
|
+
if driver_name in _user_drivers:
|
|
246
|
+
priority -= 0.5
|
|
247
|
+
return priority
|
|
248
|
+
available_drivers.sort(key=sort_key)
|
|
249
|
+
return available_drivers
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _get_db_type_for_driver(driver_name: str) -> str:
|
|
253
|
+
"""Get database type for a driver."""
|
|
254
|
+
return _get_all_drivers().get(driver_name, dict()).get('database_type')
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _get_params_for_database(db_type: str, driver: str = None) -> set:
|
|
258
|
+
"""Get all valid parameters for a database type from DRIVERS metadata."""
|
|
259
|
+
from .database import _get_all_drivers
|
|
260
|
+
|
|
261
|
+
all_drivers = _get_all_drivers()
|
|
262
|
+
valid_params = set()
|
|
263
|
+
|
|
264
|
+
# Collect parameters from all drivers for this database type
|
|
265
|
+
for driver_name, driver_info in all_drivers.items():
|
|
266
|
+
if driver_info['database_type'] == db_type:
|
|
267
|
+
if driver and driver_name != driver:
|
|
268
|
+
continue
|
|
269
|
+
# Add required params
|
|
270
|
+
for param_set in driver_info['required_params']:
|
|
271
|
+
valid_params.update(param_set)
|
|
272
|
+
# Add optional params
|
|
273
|
+
valid_params.update(driver_info.get('optional_params', set()))
|
|
274
|
+
|
|
275
|
+
return valid_params
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_supported_db_types() -> set:
|
|
279
|
+
"""Get all supported database types."""
|
|
280
|
+
valid_db_types = set()
|
|
281
|
+
all_drivers = _get_all_drivers()
|
|
282
|
+
for driver_name, driver_info in all_drivers.items():
|
|
283
|
+
valid_db_types.add(driver_info['database_type'])
|
|
284
|
+
return valid_db_types
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _validate_connection_params(driver_name: str, config_only: bool = False, **params) -> dict:
|
|
288
|
+
"""
|
|
289
|
+
Validate connection parameters against driver requirements.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
driver_name: Name of the database driver
|
|
293
|
+
config_only: If True, skip password validation for config storage
|
|
294
|
+
**params: Connection parameters
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Dict of validated parameters with extras removed
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
ValueError: If required parameters are missing
|
|
301
|
+
"""
|
|
302
|
+
if driver_name not in DRIVERS:
|
|
303
|
+
raise ValueError(f"Unknown driver: {driver_name}")
|
|
304
|
+
|
|
305
|
+
driver_info = DRIVERS[driver_name]
|
|
306
|
+
database_type = driver_info['database_type']
|
|
307
|
+
|
|
308
|
+
# Initialize with config-only parameters if needed
|
|
309
|
+
validated_params = {}
|
|
310
|
+
if config_only and 'encrypted_password' in params:
|
|
311
|
+
validated_params['encrypted_password'] = params['encrypted_password']
|
|
312
|
+
|
|
313
|
+
# get default port if not specified
|
|
314
|
+
if 'port' not in params:
|
|
315
|
+
default_port = driver_info.get('default_port')
|
|
316
|
+
if default_port:
|
|
317
|
+
params['port'] = default_port
|
|
318
|
+
|
|
319
|
+
# Check required parameters (any one set must be satisfied)
|
|
320
|
+
required_satisfied = False
|
|
321
|
+
for required_set in driver_info['required_params']:
|
|
322
|
+
if config_only:
|
|
323
|
+
# For config validation, ignore password requirement
|
|
324
|
+
check_set = required_set - {'password'}
|
|
325
|
+
if not check_set or check_set.issubset(params.keys()):
|
|
326
|
+
required_satisfied = True
|
|
327
|
+
break
|
|
328
|
+
else:
|
|
329
|
+
if required_set.issubset(params.keys()):
|
|
330
|
+
required_satisfied = True
|
|
331
|
+
break
|
|
332
|
+
|
|
333
|
+
if not required_satisfied:
|
|
334
|
+
msg = f"Missing required parameters. Need one of: {driver_info['required_params']}"
|
|
335
|
+
logger.error(msg)
|
|
336
|
+
logger.error(f"Current params: {params}")
|
|
337
|
+
raise ValueError(msg)
|
|
338
|
+
|
|
339
|
+
# Apply parameter mapping and filter valid params
|
|
340
|
+
param_map = driver_info.get('param_map', {})
|
|
341
|
+
|
|
342
|
+
all_valid_params = set()
|
|
343
|
+
for req_set in driver_info['required_params']:
|
|
344
|
+
all_valid_params.update(req_set)
|
|
345
|
+
all_valid_params.update(driver_info.get('optional_params', set()))
|
|
346
|
+
|
|
347
|
+
for key, value in params.items():
|
|
348
|
+
if key in all_valid_params or (config_only and key == 'encrypted_password'):
|
|
349
|
+
mapped_key = param_map.get(key, key)
|
|
350
|
+
validated_params[mapped_key] = value
|
|
351
|
+
|
|
352
|
+
return validated_params
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _get_connection_string(**kwargs) -> str:
|
|
356
|
+
""" Get connection string from keyword arguments."""
|
|
357
|
+
return " ".join([f"{key}={value}" for key, value in kwargs.items()])
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _get_odbc_string(**kwargs) -> str:
|
|
361
|
+
"""Build ODBC connection string"""
|
|
362
|
+
port = kwargs.pop('port', None)
|
|
363
|
+
if port and 'SERVER' in kwargs and '\\' not in kwargs['SERVER']:
|
|
364
|
+
kwargs['SERVER'] += f',{port}'
|
|
365
|
+
printable = ';'.join([f"{key.upper()}={value}" for key, value in _hide_password(kwargs).items()])
|
|
366
|
+
logger.debug(f'ODBC connection string: {printable}')
|
|
367
|
+
return ';'.join([f"{key.upper()}={value}" for key, value in kwargs.items()])
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _get_odbc_connection_string(**kwargs) -> str:
|
|
371
|
+
""" Get connection string for ODBC from keyword arguments."""
|
|
372
|
+
# logger.debug(f'Generating ODBC connection string from: {_hide_password(kwargs)}')
|
|
373
|
+
if 'dsn' in kwargs and kwargs['dsn']:
|
|
374
|
+
# DSN only send DSN and password if present
|
|
375
|
+
conn_str = f"DSN={kwargs['dsn']}"
|
|
376
|
+
printable_conn_str = conn_str
|
|
377
|
+
if 'PWD' in kwargs:
|
|
378
|
+
conn_str += f";PWD={kwargs['PWD']}"
|
|
379
|
+
printable_conn_str += f";PWD=******"
|
|
380
|
+
else:
|
|
381
|
+
odbc_driver_name = kwargs.pop('odbc_driver_name', None)
|
|
382
|
+
if 'port' in kwargs:
|
|
383
|
+
kwargs['SERVER'] += f',{kwargs.pop("port")}'
|
|
384
|
+
params = {key.upper(): value for key, value in kwargs.items()}
|
|
385
|
+
conn_str = ";".join([f"{key}={value}" for key, value in params.items()])
|
|
386
|
+
printable_conn_str = ";".join([f"{key}={value}" for key, value in _hide_password(params).items()])
|
|
387
|
+
if odbc_driver_name:
|
|
388
|
+
conn_str = f"DRIVER={{{odbc_driver_name}}};" + conn_str
|
|
389
|
+
printable_conn_str = f"DRIVER={{{odbc_driver_name}}};" + printable_conn_str
|
|
390
|
+
logger.debug(f"ODBC connection string: {printable_conn_str}")
|
|
391
|
+
return conn_str
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _password_prompt(prompt: str = 'Enter password: ') -> str:
|
|
395
|
+
"""
|
|
396
|
+
Prompts the user to enter a password securely without echoing it on the terminal.
|
|
397
|
+
|
|
398
|
+
This function uses the `getpass` module to securely capture user input for a password.
|
|
399
|
+
The prompt message can be customized by passing a specific string as the argument.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
prompt (str): The message to display prompting the user for input. Defaults to 'Enter password: '.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
str: The password entered by the user.
|
|
406
|
+
"""
|
|
407
|
+
import getpass
|
|
408
|
+
return getpass.getpass(prompt)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class Database:
|
|
412
|
+
"""
|
|
413
|
+
Database connection wrapper providing a uniform interface across database adapters.
|
|
414
|
+
|
|
415
|
+
The Database class wraps database-specific connection objects and provides a consistent
|
|
416
|
+
API regardless of which database driver is being used (psycopg2, oracledb, mysqlclient,
|
|
417
|
+
etc.). It handles parameter style conversions, manages cursors, and delegates attribute
|
|
418
|
+
access to the underlying connection for driver-specific functionality.
|
|
419
|
+
|
|
420
|
+
Key features:
|
|
421
|
+
|
|
422
|
+
* **Unified interface** - Same API for PostgreSQL, Oracle, MySQL, SQL Server, SQLite
|
|
423
|
+
* **Cursor factory** - Create different cursor types (Record, tuple, dict, list)
|
|
424
|
+
* **Transaction management** - Context managers for safe transactions
|
|
425
|
+
* **Attribute delegation** - Access underlying driver features when needed
|
|
426
|
+
* **Parameter style abstraction** - Automatic handling of different bind parameter formats
|
|
427
|
+
|
|
428
|
+
Attributes
|
|
429
|
+
----------
|
|
430
|
+
driver
|
|
431
|
+
The database adapter module (e.g., psycopg2, oracledb)
|
|
432
|
+
database_type : str
|
|
433
|
+
Database type: 'postgres', 'oracle', 'mysql', 'sqlserver', or 'sqlite'
|
|
434
|
+
database_name : str
|
|
435
|
+
Name of the connected database
|
|
436
|
+
placeholder : str
|
|
437
|
+
Parameter placeholder for this database's parameter style
|
|
438
|
+
|
|
439
|
+
Example
|
|
440
|
+
-------
|
|
441
|
+
::
|
|
442
|
+
|
|
443
|
+
import dbtk
|
|
444
|
+
|
|
445
|
+
# Create connection
|
|
446
|
+
db = dbtk.database.postgres(user='admin', password='secret', database='mydb')
|
|
447
|
+
|
|
448
|
+
# Or from configuration
|
|
449
|
+
db = dbtk.connect('production_db')
|
|
450
|
+
|
|
451
|
+
# Use as context manager
|
|
452
|
+
with db:
|
|
453
|
+
cursor = db.cursor()
|
|
454
|
+
cursor.execute("SELECT * FROM users WHERE status = :status", {'status': 'active'})
|
|
455
|
+
users = cursor.fetchall()
|
|
456
|
+
|
|
457
|
+
# Manual connection management
|
|
458
|
+
db = dbtk.connect('production_db')
|
|
459
|
+
cursor = db.cursor('dict') # Dictionary cursor
|
|
460
|
+
cursor.execute("SELECT * FROM orders")
|
|
461
|
+
db.commit()
|
|
462
|
+
db.close()
|
|
463
|
+
|
|
464
|
+
See Also
|
|
465
|
+
--------
|
|
466
|
+
dbtk.connect : Connect to database from configuration
|
|
467
|
+
Database.cursor : Create a cursor for executing queries
|
|
468
|
+
Database.transaction : Context manager for transactions
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
# Attributes stored locally, others delegated to _connection
|
|
472
|
+
_local_attrs = [
|
|
473
|
+
'_connection', 'database_type', 'database_name', 'driver',
|
|
474
|
+
'connection_name', 'placeholder', '_cursor_settings'
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
def __init__(self, connection, driver,
|
|
478
|
+
database_name: Optional[str] = None,
|
|
479
|
+
connection_name: Optional[str] = None,
|
|
480
|
+
cursor_settings: Optional[dict] = None):
|
|
481
|
+
"""
|
|
482
|
+
Initialize Database wrapper around an existing connection.
|
|
483
|
+
|
|
484
|
+
This is typically called by connection factory functions rather than directly.
|
|
485
|
+
Use ``dbtk.database.postgres()``, ``dbtk.connect()``, etc. instead.
|
|
486
|
+
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
489
|
+
connection
|
|
490
|
+
Underlying database connection object from the adapter
|
|
491
|
+
driver
|
|
492
|
+
Database adapter module (psycopg2, oracledb, mysqlclient, etc.)
|
|
493
|
+
database_name : str, optional
|
|
494
|
+
Name of the database. If None, attempts to extract from connection.
|
|
495
|
+
connection_name : str, optional
|
|
496
|
+
Name/alias from config file (e.g., 'imdb', 'prod_db'). None if not from config.
|
|
497
|
+
cursor_settings : dict, optional
|
|
498
|
+
Values passed to the cursor constructor. e.g. {'batch_size': 2000}
|
|
499
|
+
|
|
500
|
+
Example
|
|
501
|
+
-------
|
|
502
|
+
::
|
|
503
|
+
|
|
504
|
+
import psycopg2
|
|
505
|
+
from dbtk.database import Database
|
|
506
|
+
|
|
507
|
+
# Direct instantiation (not typical)
|
|
508
|
+
conn = psycopg2.connect(dbname='mydb', user='admin', password='secret')
|
|
509
|
+
db = Database(conn, psycopg2, 'mydb')
|
|
510
|
+
|
|
511
|
+
# Typical usage via factory functions
|
|
512
|
+
db = dbtk.database.postgres(user='admin', password='secret', database='mydb')
|
|
513
|
+
"""
|
|
514
|
+
self._connection = connection
|
|
515
|
+
self.driver = driver
|
|
516
|
+
self.connection_name = connection_name
|
|
517
|
+
|
|
518
|
+
if database_name is None:
|
|
519
|
+
database_name = (connection.get('database') or
|
|
520
|
+
connection.get('service_name') or
|
|
521
|
+
connection.get('dbname') or
|
|
522
|
+
connection.get('db'))
|
|
523
|
+
|
|
524
|
+
self.database_name = database_name
|
|
525
|
+
|
|
526
|
+
# Set parameter placeholder based on adapter style
|
|
527
|
+
paramstyle = getattr(driver, 'paramstyle', ParamStyle.DEFAULT)
|
|
528
|
+
self.placeholder = ParamStyle.get_placeholder(paramstyle)
|
|
529
|
+
|
|
530
|
+
# Determine server type from driver name
|
|
531
|
+
if driver.__name__ in DRIVERS:
|
|
532
|
+
self.database_type = DRIVERS[driver.__name__]['database_type']
|
|
533
|
+
else:
|
|
534
|
+
self.database_type = 'unknown'
|
|
535
|
+
|
|
536
|
+
logger.debug(f"Cursor Database.__init__ cursor_settings: {cursor_settings}")
|
|
537
|
+
if cursor_settings:
|
|
538
|
+
self._cursor_settings = {key: val for key, val in cursor_settings.items()
|
|
539
|
+
if key in Cursor.WRAPPER_SETTINGS}
|
|
540
|
+
else:
|
|
541
|
+
self._cursor_settings = dict()
|
|
542
|
+
|
|
543
|
+
def __getattr__(self, key: str) -> Any:
|
|
544
|
+
"""Delegate attribute access to underlying connection."""
|
|
545
|
+
if key == '__name__':
|
|
546
|
+
return self.connection_name or self.database_name or 'unknown'
|
|
547
|
+
else:
|
|
548
|
+
return getattr(self._connection, key)
|
|
549
|
+
|
|
550
|
+
def __setattr__(self, key: str, value: Any) -> None:
|
|
551
|
+
"""Set attributes locally or delegate to connection."""
|
|
552
|
+
if key in self._local_attrs:
|
|
553
|
+
self.__dict__[key] = value
|
|
554
|
+
else:
|
|
555
|
+
setattr(self._connection, key, value)
|
|
556
|
+
|
|
557
|
+
def __dir__(self) -> list:
|
|
558
|
+
"""Return available attributes."""
|
|
559
|
+
return list(set(
|
|
560
|
+
dir(self._connection) +
|
|
561
|
+
dir(self.__class__) +
|
|
562
|
+
self._local_attrs
|
|
563
|
+
))
|
|
564
|
+
|
|
565
|
+
def __str__(self) -> str:
|
|
566
|
+
"""String representation of the database connection."""
|
|
567
|
+
if self.connection_name and self.database_name:
|
|
568
|
+
return f"Database('{self.connection_name}' -> {self.database_name}:{self.database_type})"
|
|
569
|
+
elif self.database_name:
|
|
570
|
+
return f'Database({self.database_name}:{self.database_type})'
|
|
571
|
+
else:
|
|
572
|
+
return f'Database({self.database_type})'
|
|
573
|
+
|
|
574
|
+
def __repr__(self) -> str:
|
|
575
|
+
if self.database_name:
|
|
576
|
+
return f"Database('{self.database_name}', database_type='{self.database_type}')"
|
|
577
|
+
else:
|
|
578
|
+
return f"Database(database_type='{self.database_type}')"
|
|
579
|
+
|
|
580
|
+
def __enter__(self):
|
|
581
|
+
"""Context manager entry."""
|
|
582
|
+
return self
|
|
583
|
+
|
|
584
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
585
|
+
"""Context manager exit - close connection."""
|
|
586
|
+
self.close()
|
|
587
|
+
|
|
588
|
+
def cursor(self, **kwargs) -> Cursor:
|
|
589
|
+
"""
|
|
590
|
+
Create a cursor for executing database queries.
|
|
591
|
+
|
|
592
|
+
Returns a cursor that yields Record objects, providing flexible access to query results
|
|
593
|
+
through attribute, dictionary, and index notation.
|
|
594
|
+
|
|
595
|
+
Parameters
|
|
596
|
+
----------
|
|
597
|
+
**kwargs
|
|
598
|
+
Optional cursor configuration:
|
|
599
|
+
|
|
600
|
+
* ``batch_size`` (int) - Rows to process at once in bulk operations
|
|
601
|
+
* ``debug`` (bool) - Enable debug output showing queries and bind variables
|
|
602
|
+
* ``return_cursor`` (bool) - If True, execute() returns cursor for method chaining
|
|
603
|
+
|
|
604
|
+
Returns
|
|
605
|
+
-------
|
|
606
|
+
Cursor
|
|
607
|
+
Cursor instance that returns Record objects
|
|
608
|
+
|
|
609
|
+
Example
|
|
610
|
+
-------
|
|
611
|
+
::
|
|
612
|
+
|
|
613
|
+
# Create cursor - returns Records
|
|
614
|
+
cursor = db.cursor()
|
|
615
|
+
cursor.execute("SELECT id, name, email FROM users WHERE status = :status",
|
|
616
|
+
{'status': 'active'})
|
|
617
|
+
|
|
618
|
+
# Record supports multiple access patterns
|
|
619
|
+
for row in cursor:
|
|
620
|
+
print(row['name']) # Dictionary access
|
|
621
|
+
print(row.name) # Attribute access
|
|
622
|
+
print(row[1]) # Index access
|
|
623
|
+
print(row.get('phone', 'N/A')) # Safe access with default
|
|
624
|
+
|
|
625
|
+
# With configuration options
|
|
626
|
+
cursor = db.cursor(debug=True)
|
|
627
|
+
|
|
628
|
+
See Also
|
|
629
|
+
--------
|
|
630
|
+
Record : Flexible row object with dict, attribute, and index access
|
|
631
|
+
Database.transaction : Context manager for safe transactions
|
|
632
|
+
"""
|
|
633
|
+
# Only pass through allowed kwargs
|
|
634
|
+
filtered_kwargs = dict()
|
|
635
|
+
for key in Cursor.WRAPPER_SETTINGS:
|
|
636
|
+
if key in kwargs:
|
|
637
|
+
filtered_kwargs[key] = kwargs.pop(key)
|
|
638
|
+
elif key in self._cursor_settings:
|
|
639
|
+
filtered_kwargs[key] = self._cursor_settings[key]
|
|
640
|
+
return Cursor(self, **filtered_kwargs)
|
|
641
|
+
|
|
642
|
+
@contextmanager
|
|
643
|
+
def transaction(self):
|
|
644
|
+
"""
|
|
645
|
+
Context manager for safe database transactions.
|
|
646
|
+
|
|
647
|
+
Automatically commits the transaction on successful completion or rolls back
|
|
648
|
+
if an exception occurs. This ensures transactions are properly cleaned up
|
|
649
|
+
even if errors occur.
|
|
650
|
+
|
|
651
|
+
Yields
|
|
652
|
+
------
|
|
653
|
+
Database
|
|
654
|
+
This database connection instance
|
|
655
|
+
|
|
656
|
+
Raises
|
|
657
|
+
------
|
|
658
|
+
Exception
|
|
659
|
+
Re-raises any exception that occurs within the transaction block
|
|
660
|
+
after rolling back the transaction
|
|
661
|
+
|
|
662
|
+
Example
|
|
663
|
+
-------
|
|
664
|
+
::
|
|
665
|
+
|
|
666
|
+
with db.transaction():
|
|
667
|
+
cursor = db.cursor()
|
|
668
|
+
cursor.execute("INSERT INTO orders (customer, amount) VALUES (:c, :a)",
|
|
669
|
+
{'c': 'Aang', 'a': 100})
|
|
670
|
+
cursor.execute("UPDATE inventory SET stock = stock - 1 WHERE item_id = :id",
|
|
671
|
+
{'id': 42})
|
|
672
|
+
# Automatically commits on success
|
|
673
|
+
|
|
674
|
+
# If exception occurs, transaction is automatically rolled back
|
|
675
|
+
try:
|
|
676
|
+
with db.transaction():
|
|
677
|
+
cursor = db.cursor()
|
|
678
|
+
cursor.execute("INSERT INTO invalid_table ...") # Raises error
|
|
679
|
+
# Rollback happens automatically
|
|
680
|
+
except Exception as e:
|
|
681
|
+
logger.error(f"Transaction failed: {e}")
|
|
682
|
+
|
|
683
|
+
See Also
|
|
684
|
+
--------
|
|
685
|
+
Database.commit : Manually commit a transaction
|
|
686
|
+
Database.rollback : Manually roll back a transaction
|
|
687
|
+
"""
|
|
688
|
+
try:
|
|
689
|
+
yield self
|
|
690
|
+
self.commit()
|
|
691
|
+
except Exception:
|
|
692
|
+
self.rollback()
|
|
693
|
+
raise
|
|
694
|
+
|
|
695
|
+
def param_help(self) -> None:
|
|
696
|
+
"""Print help on this driver's parameter style."""
|
|
697
|
+
print(f"{self.driver.__name__}'s parameter style is \"{self.driver.paramstyle}\"")
|
|
698
|
+
print(f'"SELECT * FROM people WHERE name = {self.placeholder} AND age > {self.placeholder}", ("Smith", 30)')
|
|
699
|
+
|
|
700
|
+
if self.driver.paramstyle == ParamStyle.NAMED:
|
|
701
|
+
print(r'"SELECT * FROM people WHERE name = :name AND age > :age", {"name": "Smith", "age": 30}')
|
|
702
|
+
elif self.driver.paramstyle == ParamStyle.PYFORMAT:
|
|
703
|
+
print(r'"SELECT * FROM people WHERE name = %(name)s AND age > %(age)s", {"name": "Smith", "age": 30}')
|
|
704
|
+
|
|
705
|
+
@classmethod
|
|
706
|
+
def create(cls, db_type: str, driver: Optional[str] = None,
|
|
707
|
+
connection_name: Optional[str] = None,
|
|
708
|
+
cursor_settings: Optional[dict] = None, **kwargs) -> 'Database':
|
|
709
|
+
"""
|
|
710
|
+
Factory method to create database connections.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
db_type: Database type ('postgres', 'oracle', 'mysql', etc.)
|
|
714
|
+
driver: Specific driver to use (optional)
|
|
715
|
+
connection_name: Config file connection name/alias (optional)
|
|
716
|
+
cursor_settings: Defaults to use when creating cursors.
|
|
717
|
+
**kwargs: Connection parameters
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
Database instance
|
|
721
|
+
"""
|
|
722
|
+
db_driver = None
|
|
723
|
+
all_drivers = _get_all_drivers()
|
|
724
|
+
if driver:
|
|
725
|
+
if driver not in all_drivers:
|
|
726
|
+
raise ValueError(f"Unknown driver: {driver}")
|
|
727
|
+
if all_drivers[driver]['database_type'] != db_type:
|
|
728
|
+
raise ValueError(f"Driver '{driver}' is not compatible with database type '{db_type}'")
|
|
729
|
+
try:
|
|
730
|
+
module_name = all_drivers[driver].get('module', driver)
|
|
731
|
+
db_driver = importlib.import_module(module_name)
|
|
732
|
+
driver_name = driver
|
|
733
|
+
except ImportError:
|
|
734
|
+
logger.warning(f"Driver '{driver}' not available, falling back to default")
|
|
735
|
+
|
|
736
|
+
if db_driver is None:
|
|
737
|
+
drivers_for_db = _get_drivers_for_database(db_type)
|
|
738
|
+
for driver_name in drivers_for_db:
|
|
739
|
+
try:
|
|
740
|
+
module_name = all_drivers[driver_name].get('module', driver_name)
|
|
741
|
+
db_driver = importlib.import_module(module_name)
|
|
742
|
+
break
|
|
743
|
+
except ImportError:
|
|
744
|
+
pass
|
|
745
|
+
|
|
746
|
+
if db_driver is None:
|
|
747
|
+
raise ImportError(f"No database driver found for database type '{db_type}'")
|
|
748
|
+
|
|
749
|
+
logger.debug(f'parms before _validate: {_hide_password(kwargs)}')
|
|
750
|
+
params = _validate_connection_params(driver_name, **kwargs)
|
|
751
|
+
logger.debug(f'parms after _validate: {_hide_password(params)}')
|
|
752
|
+
if not params:
|
|
753
|
+
raise ValueError("The connection parameters were not valid.")
|
|
754
|
+
|
|
755
|
+
driver_conf = all_drivers[driver_name]
|
|
756
|
+
if driver_conf['connection_method'] == 'kwargs':
|
|
757
|
+
connection = db_driver.connect(**params)
|
|
758
|
+
elif driver_conf['connection_method'] == 'connection_string':
|
|
759
|
+
connection = db_driver.connect(_get_connection_string(**params))
|
|
760
|
+
elif driver_conf['connection_method'] == 'dsn':
|
|
761
|
+
if hasattr(db_driver, 'makedsn') and 'dsn' not in params:
|
|
762
|
+
host = params.pop('host', 'localhost')
|
|
763
|
+
port = params.pop('port', None)
|
|
764
|
+
service_name = params.pop('service_name', None)
|
|
765
|
+
params['dsn'] = db_driver.makedsn(host, port, service_name=service_name)
|
|
766
|
+
connection = db_driver.connect(**params)
|
|
767
|
+
elif driver_conf['connection_method'] == 'odbc_string':
|
|
768
|
+
cx_string = _get_odbc_string(DRIVER=driver_conf.get('odbc_driver_name', None), **params)
|
|
769
|
+
connection = db_driver.connect(cx_string)
|
|
770
|
+
else:
|
|
771
|
+
raise ValueError(f"Unknown connection method ({driver_conf['connection_method']}) for driver '{driver_name}'")
|
|
772
|
+
|
|
773
|
+
if connection:
|
|
774
|
+
db = cls(connection, db_driver, kwargs.get('database'),
|
|
775
|
+
connection_name=connection_name, cursor_settings=cursor_settings)
|
|
776
|
+
if db.database_type == 'unknown':
|
|
777
|
+
db.database_type = db_type
|
|
778
|
+
return db
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def postgres(user: str, password: Optional[str] = None, database: str = 'postgres',
|
|
782
|
+
host: str = 'localhost', port: int = 5432, driver: str = None, **kwargs) -> Database:
|
|
783
|
+
"""
|
|
784
|
+
Create a PostgreSQL database connection.
|
|
785
|
+
|
|
786
|
+
Automatically selects the best available PostgreSQL driver (psycopg2, psycopg3, or pgdb).
|
|
787
|
+
You can specify a specific driver if needed.
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
user: Database username
|
|
791
|
+
password: Database password (prompts if None)
|
|
792
|
+
database: Database name (default: 'postgres')
|
|
793
|
+
host: Server hostname or IP (default: 'localhost')
|
|
794
|
+
port: Server port (default: 5432)
|
|
795
|
+
driver: Specific driver to use ('psycopg2', 'psycopg', 'pgdb')
|
|
796
|
+
**kwargs: Additional driver-specific connection parameters
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
Database connection object with context manager support
|
|
800
|
+
|
|
801
|
+
Example
|
|
802
|
+
-------
|
|
803
|
+
::
|
|
804
|
+
>>> from dbtk.database import postgres
|
|
805
|
+
>>> with postgres(user='user', password='pass', database='mydb') as db:
|
|
806
|
+
... cursor = db.cursor()
|
|
807
|
+
... cursor.execute("SELECT * FROM users")
|
|
808
|
+
|
|
809
|
+
See Also:
|
|
810
|
+
Database.create() for more connection options
|
|
811
|
+
"""
|
|
812
|
+
return Database.create('postgres', user=user, password=password, database=database,
|
|
813
|
+
host=host, port=port, driver=driver, **kwargs)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def oracle(user: str, password: Optional[str] = None, database: str = None,
|
|
817
|
+
host: Optional[str] = None, port: int = 1521, driver: str = None, **kwargs) -> Database:
|
|
818
|
+
"""
|
|
819
|
+
Create an Oracle database connection.
|
|
820
|
+
|
|
821
|
+
Supports both DSN and connection string formats. Automatically selects
|
|
822
|
+
the best available Oracle driver (oracledb or cx_Oracle).
|
|
823
|
+
|
|
824
|
+
Args:
|
|
825
|
+
user: Database username
|
|
826
|
+
password: Database password (prompts if None)
|
|
827
|
+
database: Service name or SID
|
|
828
|
+
host: Server hostname or IP (required if not using dsn)
|
|
829
|
+
port: Server port (default: 1521)
|
|
830
|
+
driver: Specific driver to use ('oracledb', 'cx_Oracle')
|
|
831
|
+
**kwargs: Additional driver-specific parameters (dsn, mode, etc.)
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
Database connection object with context manager support
|
|
835
|
+
|
|
836
|
+
Example
|
|
837
|
+
-------
|
|
838
|
+
::
|
|
839
|
+
>>> from dbtk.database import oracle
|
|
840
|
+
>>> # Using service name
|
|
841
|
+
>>> db = oracle(user='scott', password='tiger',
|
|
842
|
+
... host='oracle.example.com', database='ORCL')
|
|
843
|
+
>>>
|
|
844
|
+
>>> # Using DSN directly
|
|
845
|
+
>>> db = oracle(user='scott', password='tiger',
|
|
846
|
+
... dsn='oracle.example.com:1521/ORCL')
|
|
847
|
+
|
|
848
|
+
See Also:
|
|
849
|
+
Database.create() for more connection options
|
|
850
|
+
"""
|
|
851
|
+
return Database.create('oracle', user=user, password=password, database=database,
|
|
852
|
+
host=host, port=port, driver=driver, **kwargs)
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def mysql(user: str, password: Optional[str] = None, database: str = 'mysql',
|
|
856
|
+
host: str = 'localhost', port: int = 3306, driver: str = None, **kwargs) -> Database:
|
|
857
|
+
"""
|
|
858
|
+
Create a MySQL/MariaDB database connection.
|
|
859
|
+
|
|
860
|
+
Automatically selects the best available MySQL driver (mysqlclient, mysql.connector,
|
|
861
|
+
pymysql, or MySQLdb).
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
user: Database username
|
|
865
|
+
password: Database password (prompts if None)
|
|
866
|
+
database: Database name (default: 'mysql')
|
|
867
|
+
host: Server hostname or IP (default: 'localhost')
|
|
868
|
+
port: Server port (default: 3306)
|
|
869
|
+
driver: Specific driver to use ('mysqlclient', 'mysql.connector', 'pymysql', 'MySQLdb')
|
|
870
|
+
**kwargs: Additional driver-specific parameters (charset, ssl, etc.)
|
|
871
|
+
|
|
872
|
+
Returns:
|
|
873
|
+
Database connection object with context manager support
|
|
874
|
+
|
|
875
|
+
Example
|
|
876
|
+
-------
|
|
877
|
+
::
|
|
878
|
+
>>> from dbtk.database import mysql
|
|
879
|
+
>>> with mysql(user='root', password='pass', database='myapp') as db:
|
|
880
|
+
... cursor = db.cursor()
|
|
881
|
+
... cursor.execute("SELECT * FROM users")
|
|
882
|
+
|
|
883
|
+
See Also:
|
|
884
|
+
Database.create() for more connection options
|
|
885
|
+
"""
|
|
886
|
+
return Database.create('mysql', user=user, password=password, database=database,
|
|
887
|
+
host=host, port=port, driver=driver, **kwargs)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def sqlserver(user: str, password: Optional[str] = None, database: str = None,
|
|
891
|
+
host: str = 'localhost', port: int = 1433, **kwargs) -> Database:
|
|
892
|
+
"""
|
|
893
|
+
Create a Microsoft SQL Server database connection.
|
|
894
|
+
|
|
895
|
+
Automatically selects the best available SQL Server driver (pyodbc or pymssql).
|
|
896
|
+
|
|
897
|
+
Args:
|
|
898
|
+
user: Database username
|
|
899
|
+
password: Database password (prompts if None)
|
|
900
|
+
database: Database name
|
|
901
|
+
host: Server hostname or IP (default: 'localhost')
|
|
902
|
+
port: Server port (default: 1433)
|
|
903
|
+
**kwargs: Additional driver-specific parameters (driver, encrypt, etc.)
|
|
904
|
+
|
|
905
|
+
Returns:
|
|
906
|
+
Database connection object with context manager support
|
|
907
|
+
|
|
908
|
+
Example
|
|
909
|
+
-------
|
|
910
|
+
::
|
|
911
|
+
>>> from dbtk.database import sqlserver
|
|
912
|
+
>>> db = sqlserver(user='sa', password='pass',
|
|
913
|
+
... database='AdventureWorks', host='sqlserver.local')
|
|
914
|
+
>>> cursor = db.cursor()
|
|
915
|
+
|
|
916
|
+
Note:
|
|
917
|
+
When using pyodbc, you may need to specify the ODBC driver:
|
|
918
|
+
sqlserver(..., driver='ODBC Driver 17 for SQL Server')
|
|
919
|
+
|
|
920
|
+
See Also:
|
|
921
|
+
Database.create() for more connection options
|
|
922
|
+
"""
|
|
923
|
+
return Database.create('sqlserver', user=user, password=password, database=database,
|
|
924
|
+
host=host, port=port, **kwargs)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def sqlite(database: str, **kwargs) -> Database:
|
|
928
|
+
"""
|
|
929
|
+
Create a SQLite database connection.
|
|
930
|
+
|
|
931
|
+
SQLite is a serverless, file-based database. Use ':memory:' for an in-memory database.
|
|
932
|
+
|
|
933
|
+
Args:
|
|
934
|
+
database: Path to database file or ':memory:' for in-memory database
|
|
935
|
+
**kwargs: Additional sqlite3.connect() parameters (timeout, isolation_level, etc.)
|
|
936
|
+
|
|
937
|
+
Returns:
|
|
938
|
+
Database connection object with context manager support
|
|
939
|
+
|
|
940
|
+
Example
|
|
941
|
+
-------
|
|
942
|
+
::
|
|
943
|
+
>>> from dbtk.database import sqlite
|
|
944
|
+
>>> # File-based database
|
|
945
|
+
>>> with sqlite('app.db') as db:
|
|
946
|
+
... cursor = db.cursor()
|
|
947
|
+
... cursor.execute("CREATE TABLE users (id INTEGER, name TEXT)")
|
|
948
|
+
>>>
|
|
949
|
+
>>> # In-memory database (useful for testing)
|
|
950
|
+
>>> db = sqlite(':memory:')
|
|
951
|
+
|
|
952
|
+
See Also:
|
|
953
|
+
Database.create() for more connection options
|
|
954
|
+
sqlite3 module documentation for additional parameters
|
|
955
|
+
"""
|
|
956
|
+
import sqlite3
|
|
957
|
+
|
|
958
|
+
connection = sqlite3.connect(database, **kwargs)
|
|
959
|
+
return Database(connection, sqlite3, os.path.basename(database))
|