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.
Files changed (50) hide show
  1. dbtk/__init__.py +54 -0
  2. dbtk/cli.py +169 -0
  3. dbtk/config.py +1194 -0
  4. dbtk/cursors.py +566 -0
  5. dbtk/database.py +959 -0
  6. dbtk/dbtk_sample.yml +90 -0
  7. dbtk/defaults.py +29 -0
  8. dbtk/etl/__init__.py +61 -0
  9. dbtk/etl/base_surge.py +257 -0
  10. dbtk/etl/bulk_surge.py +783 -0
  11. dbtk/etl/config_generators.py +458 -0
  12. dbtk/etl/data_surge.py +294 -0
  13. dbtk/etl/managers.py +734 -0
  14. dbtk/etl/table.py +1215 -0
  15. dbtk/etl/transforms/__init__.py +107 -0
  16. dbtk/etl/transforms/address.py +560 -0
  17. dbtk/etl/transforms/core.py +594 -0
  18. dbtk/etl/transforms/database.py +506 -0
  19. dbtk/etl/transforms/datetime.py +497 -0
  20. dbtk/etl/transforms/email.py +72 -0
  21. dbtk/etl/transforms/phone.py +670 -0
  22. dbtk/formats/__init__.py +18 -0
  23. dbtk/formats/edi.py +182 -0
  24. dbtk/logging_utils.py +310 -0
  25. dbtk/readers/__init__.py +26 -0
  26. dbtk/readers/base.py +644 -0
  27. dbtk/readers/csv.py +195 -0
  28. dbtk/readers/data_frame.py +119 -0
  29. dbtk/readers/excel.py +260 -0
  30. dbtk/readers/fixed_width.py +359 -0
  31. dbtk/readers/json.py +323 -0
  32. dbtk/readers/utils.py +394 -0
  33. dbtk/readers/xml.py +260 -0
  34. dbtk/record.py +710 -0
  35. dbtk/utils.py +537 -0
  36. dbtk/writers/__init__.py +46 -0
  37. dbtk/writers/base.py +718 -0
  38. dbtk/writers/csv.py +107 -0
  39. dbtk/writers/database.py +158 -0
  40. dbtk/writers/excel.py +1086 -0
  41. dbtk/writers/fixed_width.py +290 -0
  42. dbtk/writers/json.py +156 -0
  43. dbtk/writers/utils.py +41 -0
  44. dbtk/writers/xml.py +387 -0
  45. dbtk-0.8.0.dist-info/METADATA +305 -0
  46. dbtk-0.8.0.dist-info/RECORD +50 -0
  47. dbtk-0.8.0.dist-info/WHEEL +5 -0
  48. dbtk-0.8.0.dist-info/entry_points.txt +2 -0
  49. dbtk-0.8.0.dist-info/licenses/LICENSE.txt +7 -0
  50. dbtk-0.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,458 @@
1
+ # dbtk/etl/config_generators.py
2
+ """
3
+ Generate column definitions for Table class from database schema.
4
+
5
+ Extracts table metadata and formats it as Python dictionary code
6
+ that can be copied into Table() constructor calls.
7
+ """
8
+
9
+ from typing import Dict, Any
10
+
11
+
12
+ def column_defs_from_db(cursor, table_name: str, add_comments: bool = False) -> str:
13
+ """
14
+ Generate column definitions from database table schema.
15
+
16
+ Inspects the database table structure and returns a Python dictionary
17
+ string representation of column configurations ready to use with the
18
+ Table class.
19
+
20
+ Args:
21
+ cursor: Database cursor (any cursor type)
22
+ table_name: Name of table to analyze (supports schema.table format)
23
+ add_comments: Include table/column comments from database metadata
24
+
25
+ Returns:
26
+ String containing Python dict of column definitions
27
+
28
+ Example:
29
+ >>> print(column_defs_from_db(cursor, 'users'))
30
+ {
31
+ 'id': {'field': 'id', 'primary_key': True},
32
+ 'name': {'field': 'name', 'nullable': False},
33
+ 'email': {'field': 'email'},
34
+ 'created_at': {'db_fn': 'CURRENT_TIMESTAMP'}
35
+ }
36
+
37
+ >>> # Copy output into your code:
38
+ >>> table = Table('users', columns={
39
+ ... 'id': {'field': 'id', 'primary_key': True},
40
+ ... 'name': {'field': 'name', 'nullable': False},
41
+ ... 'email': {'field': 'email'},
42
+ ... 'created_at': {'db_fn': 'CURRENT_TIMESTAMP'}
43
+ ... }, cursor=cursor)
44
+ """
45
+ db_type = cursor.connection.database_type
46
+
47
+ # Dispatch to database-specific metadata extractor
48
+ if db_type == 'oracle':
49
+ metadata = _get_oracle_metadata(cursor, table_name, add_comments)
50
+ elif db_type == 'postgres':
51
+ metadata = _get_postgres_metadata(cursor, table_name, add_comments)
52
+ elif db_type == 'mysql':
53
+ metadata = _get_mysql_metadata(cursor, table_name, add_comments)
54
+ elif db_type in ('sqlserver', 'mssql'):
55
+ metadata = _get_sqlserver_metadata(cursor, table_name, add_comments)
56
+ else:
57
+ raise ValueError(f"Column generation not supported for database type: {db_type}")
58
+
59
+ # Format metadata dict as Python code string
60
+ return _format_columns_dict(metadata, add_comments)
61
+
62
+
63
+ def _format_columns_dict(metadata: Dict[str, Any], add_comments: bool) -> str:
64
+ """
65
+ Format metadata dictionary as Python code string.
66
+
67
+ Args:
68
+ metadata: Dict with 'columns', 'table_comment', 'column_comments' keys
69
+ add_comments: Whether to include comments in output
70
+
71
+ Returns:
72
+ Formatted Python dictionary string
73
+ """
74
+ lines = []
75
+
76
+ # Add table comment if present
77
+ if add_comments and metadata.get('table_comment'):
78
+ lines.append(f"# {metadata['table_comment']}")
79
+
80
+ lines.append("{")
81
+
82
+ for col_name, col_def in metadata['columns'].items():
83
+ # Add column comment if present
84
+ if add_comments and col_name in metadata.get('column_comments', {}):
85
+ comment = metadata['column_comments'][col_name]
86
+ lines.append(f" # {comment}")
87
+
88
+ # Build column definition
89
+ parts = []
90
+ for key, value in col_def.items():
91
+ if isinstance(value, str):
92
+ # String values need quotes
93
+ parts.append(f"'{key}': '{value}'")
94
+ elif isinstance(value, bool):
95
+ # Boolean values
96
+ parts.append(f"'{key}': {value}")
97
+ else:
98
+ # Numbers or other types
99
+ parts.append(f"'{key}': {value}")
100
+
101
+ col_str = "{" + ", ".join(parts) + "}"
102
+ lines.append(f" '{col_name}': {col_str},")
103
+
104
+ lines.append("}")
105
+
106
+ return "\n".join(lines)
107
+
108
+
109
+ def _get_oracle_metadata(cursor, table_name: str, add_comments: bool = False) -> Dict[str, Any]:
110
+ """Extract table and column metadata from Oracle database."""
111
+ table_name = table_name.upper()
112
+ tab_info = table_name.split('.')
113
+ schema_name = None
114
+ if len(tab_info) == 2:
115
+ schema_name = tab_info[0]
116
+ table_name = tab_info[1]
117
+
118
+ # Get table comment if requested
119
+ table_comment = None
120
+ if add_comments:
121
+ cmt_query = '''SELECT cmt.comments FROM all_tab_comments cmt
122
+ WHERE cmt.table_name = :table_name AND cmt.owner = COALESCE(:schema_name, cmt.owner)'''
123
+ cursor.execute(cmt_query, {'table_name': table_name, 'schema_name': schema_name})
124
+ row = cursor.fetchone()
125
+ if row and row[0]:
126
+ table_comment = row[0]
127
+
128
+ # Query column information
129
+ col_query = '''
130
+ SELECT LOWER(atc.column_name) column_name, atc.data_type, atc.nullable,
131
+ CASE WHEN pkc.position IS NOT NULL THEN 'Y' ELSE 'N' END key_column,
132
+ cc.comments
133
+ FROM all_tab_cols atc
134
+ LEFT JOIN all_constraints pk ON atc.owner = pk.owner
135
+ AND atc.table_name = pk.table_name
136
+ AND pk.constraint_type = 'P'
137
+ LEFT JOIN all_col_comments cc ON atc.owner = cc.owner
138
+ AND atc.table_name = cc.table_name
139
+ AND atc.column_name = cc.column_name
140
+ LEFT JOIN all_cons_columns pkc ON atc.owner = pkc.owner
141
+ AND atc.table_name = pkc.table_name
142
+ AND atc.column_name = pkc.column_name
143
+ AND pk.constraint_name = pkc.constraint_name
144
+ WHERE atc.table_name = :table_name
145
+ AND atc.owner = COALESCE(:schema_name, atc.owner)
146
+ AND atc.virtual_column = 'NO'
147
+ ORDER BY atc.column_id
148
+ '''
149
+
150
+ cursor.execute(col_query, {'table_name': table_name, 'schema_name': schema_name})
151
+
152
+ columns = {}
153
+ column_comments = {}
154
+
155
+ for row in cursor:
156
+ col_name = row[0]
157
+ data_type = row[1]
158
+ is_nullable = row[2]
159
+ is_key = row[3]
160
+ comment = row[4]
161
+
162
+ # Store comment if present
163
+ if add_comments and comment:
164
+ column_comments[col_name] = comment
165
+
166
+ # Build column config
167
+ col_config = {'field': col_name}
168
+
169
+ # Add transform function based on data type
170
+ if data_type == 'DATE':
171
+ col_config['fn'] = 'parse_datetime' # Oracle DATE includes time
172
+ elif data_type in ('TIMESTAMP', 'TIMESTAMP WITH TIME ZONE', 'TIMESTAMP WITH LOCAL TIME ZONE'):
173
+ col_config['fn'] = 'parse_timestamp'
174
+
175
+ # Add primary key or nullable constraint
176
+ if is_key == 'Y':
177
+ col_config['primary_key'] = True
178
+ elif is_nullable == 'N':
179
+ col_config['nullable'] = False
180
+
181
+ columns[col_name] = col_config
182
+
183
+ return {
184
+ 'name': table_name,
185
+ 'columns': columns,
186
+ 'table_comment': table_comment,
187
+ 'column_comments': column_comments
188
+ }
189
+
190
+
191
+ def _get_postgres_metadata(cursor, table_name: str, add_comments: bool = False) -> Dict[str, Any]:
192
+ """Extract table and column metadata from PostgreSQL database."""
193
+ tab_info = table_name.lower().split('.')
194
+ schema = None
195
+ if len(tab_info) == 2:
196
+ schema = tab_info[0]
197
+ table_name = tab_info[1]
198
+
199
+ # Get table comment if requested
200
+ table_comment = None
201
+ if add_comments:
202
+ cmt_query = '''
203
+ SELECT obj_description(c.oid) as comments
204
+ FROM pg_class c
205
+ JOIN pg_namespace n ON n.oid = c.relnamespace
206
+ WHERE c.relname = %(table_name)s
207
+ AND n.nspname = COALESCE(%(schema)s, n.nspname)
208
+ '''
209
+ cursor.execute(cmt_query, {'table_name': table_name, 'schema': schema})
210
+ row = cursor.fetchone()
211
+ if row and row[0]:
212
+ table_comment = row[0]
213
+
214
+ # Query column information
215
+ col_query = '''
216
+ SELECT
217
+ c.column_name,
218
+ c.data_type,
219
+ c.is_nullable,
220
+ CASE WHEN kcu.column_name IS NOT NULL THEN 'Y' ELSE 'N' END as key_column,
221
+ COALESCE(col_description(pgc.oid, c.ordinal_position), '') as comments
222
+ FROM information_schema.columns c
223
+ LEFT JOIN information_schema.table_constraints tc
224
+ ON c.table_name = tc.table_name
225
+ AND tc.constraint_type = 'PRIMARY KEY'
226
+ LEFT JOIN information_schema.key_column_usage kcu
227
+ ON c.column_name = kcu.column_name
228
+ AND c.table_name = kcu.table_name
229
+ AND tc.constraint_name = kcu.constraint_name
230
+ LEFT JOIN pg_class pgc ON pgc.relname = c.table_name
231
+ WHERE c.table_name = %(table_name)s
232
+ AND c.table_schema = COALESCE(%(schema)s::varchar, c.table_schema)
233
+ ORDER BY c.ordinal_position
234
+ '''
235
+
236
+ cursor.execute(col_query, {'table_name': table_name, 'schema': schema})
237
+
238
+ columns = {}
239
+ column_comments = {}
240
+
241
+ for row in cursor:
242
+ col_name = row[0]
243
+ data_type = row[1]
244
+ is_nullable = row[2]
245
+ is_key = row[3]
246
+ comment = row[4]
247
+
248
+ # Store comment if present
249
+ if add_comments and comment:
250
+ column_comments[col_name] = comment
251
+
252
+ # Handle special timestamp columns (Rails/Django pattern)
253
+ if col_name.endswith('_at') and data_type in ('timestamp', 'timestamptz', 'timestamp without time zone', 'timestamp with time zone'):
254
+ columns[col_name] = {'db_fn': 'CURRENT_TIMESTAMP'}
255
+ continue
256
+
257
+ # Build column config
258
+ col_config = {'field': col_name}
259
+
260
+ # Add transform function based on data type
261
+ if data_type == 'date':
262
+ col_config['fn'] = 'parse_date'
263
+ elif data_type in ('timestamp', 'timestamp without time zone'):
264
+ col_config['fn'] = 'parse_datetime'
265
+ elif data_type in ('timestamptz', 'timestamp with time zone'):
266
+ col_config['fn'] = 'parse_timestamp'
267
+ elif data_type in ('time', 'time without time zone', 'time with time zone'):
268
+ col_config['fn'] = 'parse_time'
269
+
270
+ # Add primary key or nullable constraint
271
+ if is_key == 'Y':
272
+ col_config['primary_key'] = True
273
+ elif is_nullable == 'NO':
274
+ col_config['nullable'] = False
275
+
276
+ columns[col_name] = col_config
277
+
278
+ return {
279
+ 'name': table_name,
280
+ 'columns': columns,
281
+ 'table_comment': table_comment,
282
+ 'column_comments': column_comments
283
+ }
284
+
285
+
286
+ def _get_mysql_metadata(cursor, table_name: str, add_comments: bool = False) -> Dict[str, Any]:
287
+ """Extract table and column metadata from MySQL database."""
288
+ # Get table comment if requested
289
+ table_comment = None
290
+ if add_comments:
291
+ cmt_query = '''
292
+ SELECT table_comment
293
+ FROM information_schema.tables
294
+ WHERE table_name = %s AND table_schema = DATABASE()
295
+ '''
296
+ cursor.execute(cmt_query, (table_name,))
297
+ row = cursor.fetchone()
298
+ if row and row[0]:
299
+ table_comment = row[0]
300
+
301
+ # Query column information
302
+ col_query = '''
303
+ SELECT
304
+ c.column_name,
305
+ c.data_type,
306
+ c.is_nullable,
307
+ CASE WHEN c.column_key = 'PRI' THEN 'Y' ELSE 'N' END as key_column,
308
+ COALESCE(c.column_comment, '') as comments
309
+ FROM information_schema.columns c
310
+ WHERE c.table_name = %s
311
+ AND c.table_schema = DATABASE()
312
+ ORDER BY c.ordinal_position
313
+ '''
314
+
315
+ cursor.execute(col_query, (table_name,))
316
+
317
+ columns = {}
318
+ column_comments = {}
319
+
320
+ for row in cursor:
321
+ col_name = row[0]
322
+ data_type = row[1]
323
+ is_nullable = row[2]
324
+ is_key = row[3]
325
+ comment = row[4]
326
+
327
+ # Store comment if present
328
+ if add_comments and comment:
329
+ column_comments[col_name] = comment
330
+
331
+ # Handle special timestamp columns (Laravel/Rails pattern)
332
+ if col_name in ('created_at', 'updated_at') and data_type in ('datetime', 'timestamp'):
333
+ columns[col_name] = {'db_fn': 'CURRENT_TIMESTAMP'}
334
+ continue
335
+
336
+ # Build column config
337
+ col_config = {'field': col_name}
338
+
339
+ # Add transform function based on data type
340
+ if data_type == 'date':
341
+ col_config['fn'] = 'parse_date'
342
+ elif data_type in ('datetime', 'timestamp'):
343
+ col_config['fn'] = 'parse_datetime'
344
+ elif data_type == 'time':
345
+ col_config['fn'] = 'parse_time'
346
+
347
+ # Add primary key or nullable constraint
348
+ if is_key == 'Y':
349
+ col_config['primary_key'] = True
350
+ elif is_nullable == 'NO':
351
+ col_config['nullable'] = False
352
+
353
+ columns[col_name] = col_config
354
+
355
+ return {
356
+ 'name': table_name,
357
+ 'columns': columns,
358
+ 'table_comment': table_comment,
359
+ 'column_comments': column_comments
360
+ }
361
+
362
+
363
+ def _get_sqlserver_metadata(cursor, table_name: str, add_comments: bool = False) -> Dict[str, Any]:
364
+ """Extract table and column metadata from SQL Server database."""
365
+ # Get table comment if requested
366
+ table_comment = None
367
+ if add_comments:
368
+ cmt_query = '''
369
+ SELECT ep.value as comments
370
+ FROM sys.tables t
371
+ LEFT JOIN sys.extended_properties ep
372
+ ON ep.major_id = t.object_id
373
+ AND ep.minor_id = 0
374
+ AND ep.name = 'MS_Description'
375
+ WHERE t.name = ?
376
+ '''
377
+ cursor.execute(cmt_query, (table_name,))
378
+ row = cursor.fetchone()
379
+ if row and row[0]:
380
+ table_comment = row[0]
381
+
382
+ # Query column information
383
+ col_query = '''
384
+ SELECT
385
+ c.column_name,
386
+ c.data_type,
387
+ c.is_nullable,
388
+ CASE WHEN pk.column_name IS NOT NULL THEN 'Y' ELSE 'N' END as key_column,
389
+ COALESCE(ep.value, '') as comments
390
+ FROM information_schema.columns c
391
+ LEFT JOIN (
392
+ SELECT kcu.column_name, kcu.table_name
393
+ FROM information_schema.table_constraints tc
394
+ JOIN information_schema.key_column_usage kcu
395
+ ON tc.constraint_name = kcu.constraint_name
396
+ WHERE tc.constraint_type = 'PRIMARY KEY'
397
+ ) pk ON c.column_name = pk.column_name AND c.table_name = pk.table_name
398
+ LEFT JOIN sys.tables t ON t.name = c.table_name
399
+ LEFT JOIN sys.columns sc ON sc.object_id = t.object_id AND sc.name = c.column_name
400
+ LEFT JOIN sys.extended_properties ep
401
+ ON ep.major_id = t.object_id
402
+ AND ep.minor_id = sc.column_id
403
+ AND ep.name = 'MS_Description'
404
+ WHERE c.table_name = ?
405
+ ORDER BY c.ordinal_position
406
+ '''
407
+
408
+ cursor.execute(col_query, (table_name,))
409
+
410
+ columns = {}
411
+ column_comments = {}
412
+
413
+ for row in cursor:
414
+ col_name = row[0]
415
+ data_type = row[1]
416
+ is_nullable = row[2]
417
+ is_key = row[3]
418
+ comment = row[4]
419
+
420
+ # Store comment if present
421
+ if add_comments and comment:
422
+ column_comments[col_name] = comment
423
+
424
+ # Handle special timestamp columns (SQL Server pattern)
425
+ if col_name in ('CreatedDate', 'ModifiedDate') and data_type in ('datetime', 'datetime2'):
426
+ columns[col_name] = {
427
+ 'value': 'GETDATE()',
428
+ 'db_fn': 'GETDATE()'
429
+ }
430
+ continue
431
+
432
+ # Build column config
433
+ col_config = {'field': col_name}
434
+
435
+ # Add transform function based on data type
436
+ if data_type == 'date':
437
+ col_config['fn'] = 'parse_date'
438
+ elif data_type in ('datetime', 'datetime2', 'smalldatetime'):
439
+ col_config['fn'] = 'parse_datetime'
440
+ elif data_type == 'datetimeoffset':
441
+ col_config['fn'] = 'parse_timestamp'
442
+ elif data_type == 'time':
443
+ col_config['fn'] = 'parse_time'
444
+
445
+ # Add primary key or nullable constraint
446
+ if is_key == 'Y':
447
+ col_config['primary_key'] = True
448
+ elif is_nullable == 'NO':
449
+ col_config['nullable'] = False
450
+
451
+ columns[col_name] = col_config
452
+
453
+ return {
454
+ 'name': table_name,
455
+ 'columns': columns,
456
+ 'table_comment': table_comment,
457
+ 'column_comments': column_comments
458
+ }