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/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# dbtk/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
DBTK - Data Benders ToolKit
|
|
4
|
+
|
|
5
|
+
A lightweight database integration toolkit that provides:
|
|
6
|
+
- Uniform interface across different databases (PostgreSQL, Oracle, MySQL, SQL Server, SQLite)
|
|
7
|
+
- Flexible cursor types returning different data structures
|
|
8
|
+
- YAML-based configuration with password encryption
|
|
9
|
+
- Writers for CSV, Excel, fixed-width, and database-to-database export
|
|
10
|
+
- Context managers for connections and transactions
|
|
11
|
+
|
|
12
|
+
Basic usage::
|
|
13
|
+
|
|
14
|
+
import dbtk
|
|
15
|
+
|
|
16
|
+
# From YAML config file
|
|
17
|
+
with dbtk.connect('prod_warehouse') as db:
|
|
18
|
+
cursor = db.cursor()
|
|
19
|
+
cursor.execute("SELECT * FROM users")
|
|
20
|
+
|
|
21
|
+
# Export results
|
|
22
|
+
dbtk.writers.to_csv(cursor, 'users.csv')
|
|
23
|
+
dbtk.writers.to_excel(cursor, 'report.xlsx')
|
|
24
|
+
|
|
25
|
+
Direct connections:
|
|
26
|
+
from dbtk.database import postgres, oracle
|
|
27
|
+
|
|
28
|
+
db = postgres(user='user', password='pass', database='db')
|
|
29
|
+
cursor = db.cursor() # Returns Record objects
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__version__ = '0.8.0'
|
|
33
|
+
__author__ = 'Scott Bailey <scottrbailey@gmail.com>'
|
|
34
|
+
|
|
35
|
+
from .database import Database
|
|
36
|
+
from .config import connect, set_config_file
|
|
37
|
+
from .cursors import Cursor
|
|
38
|
+
from .logging_utils import setup_logging, cleanup_old_logs, errors_logged
|
|
39
|
+
from . import etl
|
|
40
|
+
from . import readers
|
|
41
|
+
from . import writers
|
|
42
|
+
|
|
43
|
+
# Simple, clean exports
|
|
44
|
+
__all__ = [
|
|
45
|
+
'connect',
|
|
46
|
+
'Database',
|
|
47
|
+
'Cursor',
|
|
48
|
+
'etl',
|
|
49
|
+
'readers',
|
|
50
|
+
'writers',
|
|
51
|
+
'setup_logging',
|
|
52
|
+
'cleanup_old_logs',
|
|
53
|
+
'errors_logged'
|
|
54
|
+
]
|
dbtk/cli.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# dbtk/cli.py
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import importlib.util
|
|
5
|
+
import sys
|
|
6
|
+
from .database import _get_all_drivers
|
|
7
|
+
from . import config
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from importlib.metadata import metadata, distributions, requires
|
|
11
|
+
except ImportError:
|
|
12
|
+
# backport to older versions
|
|
13
|
+
from importlib_metadata import metadata, distributions, requires
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _name_cleanup(name):
|
|
17
|
+
"""Cleanup module names for search and display"""
|
|
18
|
+
return name.lower().replace('-', '_')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_optional_deps(extra_name='recommended'):
|
|
22
|
+
""" Get optional dependencies for dbtk """
|
|
23
|
+
reqs = requires('dbtk') or []
|
|
24
|
+
deps = []
|
|
25
|
+
# Parse requirements like: 'psycopg2-binary>=2.8; extra == "recommended"'
|
|
26
|
+
for req in reqs:
|
|
27
|
+
req = req.replace("'", '"') # quoting changed between versions
|
|
28
|
+
if f'extra == "{extra_name}"' in req:
|
|
29
|
+
# Extract just the package name and version spec
|
|
30
|
+
pkg = req.split(';')[0].strip()
|
|
31
|
+
deps.append(pkg)
|
|
32
|
+
return deps
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_installed(pkg: str) -> bool:
|
|
36
|
+
"""find_spec fails on tomli → this never does."""
|
|
37
|
+
pkg = _name_cleanup(pkg)
|
|
38
|
+
return (
|
|
39
|
+
importlib.util.find_spec(pkg) is not None
|
|
40
|
+
or pkg in sys.modules
|
|
41
|
+
or pkg in {_name_cleanup(d.name) for d in distributions()}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def checkup():
|
|
45
|
+
""" Check which optional dependencies are installed."""
|
|
46
|
+
deps = []
|
|
47
|
+
for dep in _get_optional_deps('recommended'):
|
|
48
|
+
if ';' in dep:
|
|
49
|
+
dep = dep.split(';')[0].strip()
|
|
50
|
+
if dep and not dep.startswith('#'):
|
|
51
|
+
deps.append(dep.split('>=')[0].split('==')[0].split('<')[0].strip())
|
|
52
|
+
|
|
53
|
+
installed = {_name_cleanup(d.name): d.version for d in distributions()}
|
|
54
|
+
|
|
55
|
+
print(f"{'Package':<20} {'Status':<8} {'Version'}")
|
|
56
|
+
print("-" * 40)
|
|
57
|
+
|
|
58
|
+
for dep in deps:
|
|
59
|
+
clean = dep.replace('-', '_')
|
|
60
|
+
status = "✓" if _is_installed(clean) else "✗"
|
|
61
|
+
version = installed.get(clean.lower(), '-')
|
|
62
|
+
print(f"{dep:<20} {status:<8} {version}")
|
|
63
|
+
|
|
64
|
+
print("\nDB Drivers Priority* Status Version")
|
|
65
|
+
print("-" * 56)
|
|
66
|
+
all_drivers = _get_all_drivers()
|
|
67
|
+
by_type = {}
|
|
68
|
+
|
|
69
|
+
for name, info in all_drivers.items():
|
|
70
|
+
db_type = info['database_type']
|
|
71
|
+
by_type.setdefault(db_type, []).append((info['priority'], name, info))
|
|
72
|
+
|
|
73
|
+
if _is_installed('pyodbc'):
|
|
74
|
+
import pyodbc
|
|
75
|
+
odbc_drivers = pyodbc.drivers()
|
|
76
|
+
else:
|
|
77
|
+
odbc_drivers = []
|
|
78
|
+
|
|
79
|
+
for db_type in sorted(by_type):
|
|
80
|
+
drivers = sorted(by_type[db_type], key=lambda x: x[0])
|
|
81
|
+
print(f"{db_type}") # ← bold header
|
|
82
|
+
for pri, name, info in drivers:
|
|
83
|
+
# ── 2-space indent for hierarchy
|
|
84
|
+
display_name = f" {name}" # ← indented
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# see if driver has 'module' attribute to use instead of name
|
|
88
|
+
module_name = info.get('module', name)
|
|
89
|
+
spec = importlib.util.find_spec(module_name)
|
|
90
|
+
version = installed.get(_name_cleanup(module_name), '-- ')
|
|
91
|
+
status = "✓" if spec else "✗"
|
|
92
|
+
except ModuleNotFoundError:
|
|
93
|
+
version = '-- '
|
|
94
|
+
status = "✗"
|
|
95
|
+
odbc_driver_name = info.get("odbc_driver_name")
|
|
96
|
+
if odbc_driver_name:
|
|
97
|
+
odbc_status = "✓" if odbc_driver_name in odbc_drivers else "✗"
|
|
98
|
+
note = f'({odbc_status} {odbc_driver_name})'
|
|
99
|
+
else:
|
|
100
|
+
if 'note' in info:
|
|
101
|
+
note = f'({info.get("note")})'
|
|
102
|
+
else:
|
|
103
|
+
note = ''
|
|
104
|
+
|
|
105
|
+
print(f"{display_name:<20} {pri:<9} {status:<8} {version} {note}")
|
|
106
|
+
|
|
107
|
+
print("\n* Lower priority = preferred")
|
|
108
|
+
|
|
109
|
+
print("\nConfig Health")
|
|
110
|
+
print("-" * 40)
|
|
111
|
+
for status, msg in config.diagnose_config():
|
|
112
|
+
print(f"{status} {msg}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main():
|
|
116
|
+
parser = argparse.ArgumentParser(prog='dbtk', description='DBTK command-line utilities')
|
|
117
|
+
subparsers = parser.add_subparsers(dest='command', required=True)
|
|
118
|
+
|
|
119
|
+
# checkup
|
|
120
|
+
subparsers.add_parser('checkup', help='Check for dependencies and configuration issues')
|
|
121
|
+
|
|
122
|
+
# config-setup
|
|
123
|
+
subparsers.add_parser('config-setup', help='Interactive configuration setup wizard')
|
|
124
|
+
|
|
125
|
+
# generate-key
|
|
126
|
+
subparsers.add_parser('generate-key', help='Generate encryption key')
|
|
127
|
+
|
|
128
|
+
# store-key
|
|
129
|
+
key_parser = subparsers.add_parser('store-key',
|
|
130
|
+
help='Store encryption key in system keyring (generate if not provided)')
|
|
131
|
+
key_parser.add_argument('key', nargs='?', default=None,
|
|
132
|
+
help='Encryption key to store. If omitted, a new key is generated and stored.')
|
|
133
|
+
key_parser.add_argument('--force', action='store_true',
|
|
134
|
+
help='Overwrite existing encryption key in system keyring')
|
|
135
|
+
|
|
136
|
+
# encrypt-config
|
|
137
|
+
encrypt_parser = subparsers.add_parser('encrypt-config', help='Encrypt passwords in config file')
|
|
138
|
+
encrypt_parser.add_argument('config_file', nargs='?', help='Config file path')
|
|
139
|
+
|
|
140
|
+
# encrypt-password
|
|
141
|
+
pwd_parser = subparsers.add_parser('encrypt-password', help='Encrypt a password')
|
|
142
|
+
pwd_parser.add_argument('password', nargs='?', help='Password to encrypt')
|
|
143
|
+
|
|
144
|
+
# migrate-config
|
|
145
|
+
migrate_parser = subparsers.add_parser('migrate-config', help='Migrate config to new key')
|
|
146
|
+
migrate_parser.add_argument('old_file', help='Old config file')
|
|
147
|
+
migrate_parser.add_argument('new_file', help='New config file')
|
|
148
|
+
migrate_parser.add_argument('--new-key', help='New encryption key')
|
|
149
|
+
|
|
150
|
+
args = parser.parse_args()
|
|
151
|
+
|
|
152
|
+
if args.command == 'checkup':
|
|
153
|
+
return checkup()
|
|
154
|
+
elif args.command == 'config-setup':
|
|
155
|
+
return config.setup_config()
|
|
156
|
+
elif args.command == 'generate-key':
|
|
157
|
+
return config.generate_encryption_key()
|
|
158
|
+
elif args.command == 'store-key':
|
|
159
|
+
return config.store_key(args.key)
|
|
160
|
+
elif args.command == 'encrypt-config':
|
|
161
|
+
return config.encrypt_config_file(args.config_file)
|
|
162
|
+
elif args.command == 'encrypt-password':
|
|
163
|
+
return config.encrypt_password(args.password)
|
|
164
|
+
elif args.command == 'migrate-config':
|
|
165
|
+
return config.migrate_config(args.old_file, args.new_file, args.new_key)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == '__main__':
|
|
169
|
+
main()
|