valediction 1.0.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 (38) hide show
  1. valediction/__init__.py +8 -0
  2. valediction/convenience.py +50 -0
  3. valediction/data_types/__init__.py +0 -0
  4. valediction/data_types/data_type_helpers.py +75 -0
  5. valediction/data_types/data_types.py +58 -0
  6. valediction/data_types/type_inference.py +541 -0
  7. valediction/datasets/__init__.py +0 -0
  8. valediction/datasets/datasets.py +870 -0
  9. valediction/datasets/datasets_helpers.py +46 -0
  10. valediction/demo/DEMO - Data Dictionary.xlsx +0 -0
  11. valediction/demo/DEMOGRAPHICS.csv +101 -0
  12. valediction/demo/DIAGNOSES.csv +650 -0
  13. valediction/demo/LAB_TESTS.csv +1001 -0
  14. valediction/demo/VITALS.csv +1001 -0
  15. valediction/demo/__init__.py +6 -0
  16. valediction/demo/demo_dictionary.py +129 -0
  17. valediction/dictionary/__init__.py +0 -0
  18. valediction/dictionary/exporting.py +501 -0
  19. valediction/dictionary/exporting_helpers.py +371 -0
  20. valediction/dictionary/generation.py +357 -0
  21. valediction/dictionary/helpers.py +174 -0
  22. valediction/dictionary/importing.py +494 -0
  23. valediction/dictionary/integrity.py +37 -0
  24. valediction/dictionary/model.py +582 -0
  25. valediction/dictionary/template/PROJECT - Data Dictionary.xltx +0 -0
  26. valediction/exceptions.py +22 -0
  27. valediction/integrity.py +97 -0
  28. valediction/io/__init__.py +0 -0
  29. valediction/io/csv_readers.py +307 -0
  30. valediction/progress.py +206 -0
  31. valediction/support.py +72 -0
  32. valediction/validation/__init__.py +0 -0
  33. valediction/validation/helpers.py +315 -0
  34. valediction/validation/issues.py +280 -0
  35. valediction/validation/validation.py +598 -0
  36. valediction-1.0.0.dist-info/METADATA +15 -0
  37. valediction-1.0.0.dist-info/RECORD +38 -0
  38. valediction-1.0.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,582 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from pathlib import Path
5
+
6
+ from valediction.data_types.data_types import DataType
7
+ from valediction.dictionary.helpers import (
8
+ _check_data_type,
9
+ _check_name,
10
+ _check_order,
11
+ _check_primary_key,
12
+ _normalise_name,
13
+ )
14
+ from valediction.exceptions import DataDictionaryError
15
+ from valediction.support import list_as_bullets
16
+
17
+
18
+ class Column:
19
+ """Represents a single column in a data dictionary.
20
+
21
+ Attributes:
22
+ name (str): name of the column
23
+ order (int): order of the column
24
+ data_type (DataType | str): data type of the column
25
+ length (int | None): maximum length of the column
26
+ vocabulary (str | None): code vocabulary of the column (e.g. ICD or SNOMED)
27
+ primary_key (int | None): order of the column in the table primary key (if applicable)
28
+ foreign_key (str | None): table.column identity of the foreign key (if applicable)
29
+ enumerations (dict[str | int, str | int] | None): dictionary of code: value enumerations of the column
30
+ description (str | None): description of the column
31
+ datetime_format (str | None): identified datetime format of the column
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ name: str,
37
+ order: int,
38
+ data_type: DataType | str,
39
+ length: int | None = None,
40
+ vocabulary: str | None = None,
41
+ primary_key: int | None = None,
42
+ foreign_key: str | None = None,
43
+ enumerations: dict[str | int, str | int] | None = None,
44
+ description: str | None = None,
45
+ datetime_format: str | None = None,
46
+ ):
47
+ self.name = _normalise_name(name)
48
+ self.order = int(order) if order is not None else None
49
+ self.data_type: DataType = None
50
+ self.length = int(length) if length is not None else None
51
+ self.vocabulary = vocabulary
52
+ self.primary_key = int(primary_key) if primary_key is not None else None
53
+ self.foreign_key = foreign_key
54
+ self.enumerations = enumerations or dict()
55
+ self.description = description
56
+ self.datetime_format = datetime_format
57
+ self.set_data_type(data_type)
58
+ self.check()
59
+
60
+ # Magic
61
+ def __repr__(self) -> str:
62
+ data_type = (
63
+ self.data_type.value
64
+ if hasattr(self.data_type, "value")
65
+ else str(self.data_type)
66
+ )
67
+ len_part = f"({self.length})" if self.length is not None else ""
68
+ pk_part = (
69
+ f", primary_key={self.primary_key!r}"
70
+ if self.primary_key is not None
71
+ else ""
72
+ )
73
+ datetime_format_part = (
74
+ f", datetime_format={self.datetime_format!r}"
75
+ if self.datetime_format
76
+ else ""
77
+ )
78
+ return (
79
+ f"Column(name={self.name!r}, order={self.order!r}, "
80
+ + f"data_type='{data_type}{len_part}'{pk_part}{datetime_format_part})"
81
+ )
82
+
83
+ # Helpers
84
+ def check(self) -> None:
85
+ """
86
+ Summary:
87
+ Checks a Column object for errors.
88
+
89
+ Raises:
90
+ DataDictionaryError: if any errors are found in the Column object
91
+ """
92
+ errors = []
93
+ errors.extend(_check_name(self.name, entity="column"))
94
+ errors.extend(_check_order(self.order))
95
+ errors.extend(_check_data_type(self.data_type, self.length))
96
+ errors.extend(_check_primary_key(self.primary_key, self.data_type))
97
+
98
+ if errors:
99
+ raise DataDictionaryError(
100
+ f"\nErrors in column {self.name!r}: {list_as_bullets(errors)}"
101
+ )
102
+
103
+ def set_data_type(self, data_type: DataType) -> None:
104
+ self.data_type = (
105
+ data_type if isinstance(data_type, DataType) else DataType.parse(data_type)
106
+ )
107
+
108
+
109
+ class Table(list[Column]):
110
+ """
111
+ Summary:
112
+ Represents a table in a data dictionary.
113
+
114
+ Arguments:
115
+ name (str): name of the table
116
+ description (str | None): description of the table
117
+ columns (list[Column] | None): list of columns in the table
118
+
119
+ Raises:
120
+ DataDictionaryError: if any errors are found in the Table object
121
+ """
122
+
123
+ def __init__(
124
+ self,
125
+ name: str,
126
+ description: str | None = None,
127
+ columns: list[Column] | None = None,
128
+ ):
129
+ super().__init__()
130
+ self.name = _normalise_name(name)
131
+ self.description = description
132
+ for column in columns or []:
133
+ self.add_column(column)
134
+ self.check(instantiation=False if len(self) else True)
135
+
136
+ def __repr__(self) -> str:
137
+ cols_str = (
138
+ "" if not self else f", {list_as_bullets(elements=[str(c) for c in self])}"
139
+ )
140
+ return f"Table(name={self.name!r}, description={self.description!r}{cols_str})"
141
+
142
+ def __getitem__(self, key: int | str) -> Column:
143
+ if isinstance(key, int):
144
+ return super().__getitem__(key)
145
+ target = _normalise_name(key)
146
+ found = next((c for c in self if c.name == target), None)
147
+ if not found:
148
+ raise KeyError(f"Column {key!r} not found in table {self.name!r}.")
149
+ return found
150
+
151
+ def __get(self, name: str, default: Column | None = None) -> Column | None:
152
+ target = _normalise_name(name)
153
+ return next((c for c in self if c.name == target), default)
154
+
155
+ # Getters
156
+ def index_of(self, name: str) -> int | None:
157
+ target = _normalise_name(name)
158
+ for i, c in enumerate(self):
159
+ if c.name == target:
160
+ return i
161
+ return None
162
+
163
+ def get_column(self, column: str | int) -> Column:
164
+ """
165
+ Summary:
166
+ Retrieves a column from the table by name or order.
167
+
168
+ Args:
169
+ column (str | int): name or order of the column to retrieve
170
+
171
+ Returns:
172
+ Column: the column with the specified name or order
173
+
174
+ Raises:
175
+ KeyError: if the specified column is not found in the table
176
+ """
177
+ if isinstance(column, str):
178
+ col = self.__get(column)
179
+ if col is None:
180
+ raise KeyError(f"Column {column!r} not found in table {self.name!r}.")
181
+ return col
182
+
183
+ found = next((c for c in self if c.order == column), None)
184
+ if not found:
185
+ raise KeyError(
186
+ f"Column with order {column!r} not found in table {self.name!r}."
187
+ )
188
+ return found
189
+
190
+ def get_column_names(self) -> list[str]:
191
+ """
192
+ Summary:
193
+ Retrieves a list of column names from the table.
194
+
195
+ Returns:
196
+ list[str]: a list of column names
197
+ """
198
+ return [c.name for c in self]
199
+
200
+ def get_column_orders(self) -> list[int | None]:
201
+ """
202
+ Summary:
203
+ Retrieves a list of column orders from the table.
204
+
205
+ Returns:
206
+ list[int | None]: a list of column orders
207
+ """
208
+ return [c.order for c in self]
209
+
210
+ # Checkers
211
+ def check(self, instantiation: bool = False) -> None:
212
+ """
213
+ Summary:
214
+ Checks a Table object for errors.
215
+
216
+ Arguments:
217
+ instantiation (bool): whether this is an instantiation check or not. If
218
+ not, additionally checks primary keys and orders.
219
+
220
+ Raises:
221
+ DataDictionaryError: if any errors are found in the Table object
222
+ """
223
+ errors = []
224
+ errors.extend(_check_name(name=self.name, entity="table"))
225
+
226
+ if not instantiation:
227
+ errors.extend(self.__check_primary_keys())
228
+ errors.extend(self.__check_orders())
229
+
230
+ if errors:
231
+ raise DataDictionaryError(
232
+ f"\nErrors in table {self.name!r}: {list_as_bullets(errors)}"
233
+ )
234
+
235
+ def get_primary_keys(self) -> list[str]:
236
+ """
237
+ Summary:
238
+ Retrieves a list of primary key column names from the table.
239
+
240
+ Returns:
241
+ list[str]: a list of primary key column names
242
+ """
243
+ primary_keys = []
244
+ for column in self:
245
+ if column.primary_key is not None:
246
+ primary_keys.append(column.name)
247
+ return primary_keys
248
+
249
+ def __check_primary_keys(self) -> list[str]:
250
+ errors: list[str] = []
251
+
252
+ pk_cols = [c for c in self if c.primary_key is not None]
253
+ if len(pk_cols) == 0:
254
+ errors.append(
255
+ "table has no Primary Key column(s). At least one is required"
256
+ )
257
+ return errors
258
+
259
+ groups = defaultdict(list)
260
+ for c in pk_cols:
261
+ groups[c.primary_key].append(c.name)
262
+
263
+ for ordinal, cols in groups.items():
264
+ if len(cols) > 1:
265
+ errors.append(
266
+ f"conflicting primary_key ordinal {ordinal}: used by columns {', '.join(repr(n) for n in cols)}."
267
+ )
268
+
269
+ return errors
270
+
271
+ def __check_orders(self) -> list[str]:
272
+ errors: list[str] = []
273
+ groups = defaultdict(list)
274
+ for c in self:
275
+ groups[c.order].append(c.name)
276
+
277
+ for order_val, cols in groups.items():
278
+ if len(cols) > 1:
279
+ errors.append(
280
+ f"conflicting order {order_val}: used by columns {', '.join(repr(n) for n in cols)}."
281
+ )
282
+ return errors
283
+
284
+ # Manipulation
285
+ def sort_columns(self) -> None:
286
+ """
287
+ Summary:
288
+ Sorts the columns of the table in ascending order based on their order attribute.
289
+ """
290
+ self.sort(key=lambda c: c.order)
291
+
292
+ def add_column(self, column: Column) -> None:
293
+ """
294
+ Summary:
295
+ Adds a new column to the table.
296
+
297
+ Arguments:
298
+ column: the Column object to add to the table
299
+
300
+ Raises:
301
+ DataDictionaryError: if the column already exists, or if the order value is already in use by another column.
302
+ """
303
+ if not isinstance(column, Column):
304
+ raise DataDictionaryError("Only Column objects can be added to a Table.")
305
+
306
+ if column.name in self.get_column_names():
307
+ conflict = self.get_column(column.name)
308
+ raise DataDictionaryError(
309
+ f"Column {column.name!r} already exists (order={conflict.order!r})"
310
+ )
311
+
312
+ if column.order in self.get_column_orders():
313
+ conflict = self.get_column(column.order)
314
+ raise DataDictionaryError(
315
+ f"Order {column.order!r} already exists (name={conflict.name!r})"
316
+ )
317
+
318
+ if column.primary_key is not None:
319
+ pk_conflict = next(
320
+ (c for c in self if c.primary_key == column.primary_key), None
321
+ )
322
+ if pk_conflict is not None:
323
+ raise DataDictionaryError(
324
+ f"Primary key ordinal {column.primary_key} for {column.name!r} "
325
+ f"conflicts with existing column {pk_conflict.name!r}."
326
+ )
327
+
328
+ super().append(column)
329
+ self.sort_columns()
330
+
331
+ def remove_column(self, column: str | int) -> None:
332
+ """
333
+ Summary:
334
+ Removes a column from the table.
335
+
336
+ Arguments:
337
+ column: the column string or order to remove
338
+
339
+ Raises:
340
+ DataDictionaryError: if the column does not exist
341
+ """
342
+ if isinstance(column, str):
343
+ name = self.get_column(column).name
344
+ else:
345
+ name = self.get_column(column).name # by order
346
+ remaining = [c for c in self if c.name != name]
347
+ self.clear()
348
+ super().extend(remaining)
349
+
350
+ def set_primary_keys(self, primary_keys: list[str | int]) -> None:
351
+ """
352
+ Summary:
353
+ Sets primary keys for the table.
354
+
355
+ Arguments:
356
+ primary_keys: list of column names or orders to set as primary keys
357
+
358
+ Raises:
359
+ DataDictionaryError: if primary keys were not provided (empty list)
360
+ """
361
+ if not primary_keys:
362
+ raise DataDictionaryError(
363
+ f"Primary keys for table {self.name!r} were not provided (empty list)."
364
+ )
365
+
366
+ # Clear existing PKs
367
+ for col in self:
368
+ col.primary_key = None
369
+
370
+ # Resolve and dedupe
371
+ resolved: list[Column] = []
372
+ seen: set[str] = set()
373
+ for key in primary_keys:
374
+ col = self.get_column(key)
375
+ if col.name in seen:
376
+ raise DataDictionaryError(
377
+ f"Duplicate column {col.name!r} provided for table {self.name!r}."
378
+ )
379
+ seen.add(col.name)
380
+ resolved.append(col)
381
+
382
+ # Assign ordinals 1..N
383
+ for ordinal, col in enumerate(resolved, start=1):
384
+ col.primary_key = ordinal
385
+ # Column.check() enforces PK validity for the column's data_type
386
+ col.check()
387
+
388
+ # Table-level validation (presence, unique ordinals)
389
+ self.check()
390
+
391
+
392
+ class Dictionary(list[Table]):
393
+ """A collection of tables and metadata describing a dataset, against which records
394
+ can be validated.
395
+
396
+ Attributes:
397
+ name (str | None): Name of the dataset or project
398
+ organisations (str | None): Organisations collaborating on the dataset
399
+ version (str | None): Version number of the dataset (e.g. v1.0)
400
+ version_notes (str | None): Notes about the dataset version (e.g. changes made)
401
+ inclusion_criteria (str | None): Cohort inclusion criteria
402
+ exclusion_criteria (str | None): Cohort exclusion criteria
403
+ imported (bool): Whether the dictionary has been imported from an external source (e.g. Excel)
404
+ """
405
+
406
+ def __init__(
407
+ self,
408
+ name: str | None = None,
409
+ tables: list[Table] | None = None,
410
+ organisations: str | None = None,
411
+ version: str | None = None,
412
+ version_notes: str | None = None,
413
+ inclusion_criteria: str | None = None,
414
+ exclusion_criteria: str | None = None,
415
+ imported: bool = False,
416
+ ):
417
+ super().__init__()
418
+ self.name = name
419
+ for t in tables or []:
420
+ self.add_table(t)
421
+ self.organisations = organisations
422
+ self.version = version
423
+ self.version_notes = version_notes
424
+ self.inclusion_criteria = inclusion_criteria
425
+ self.exclusion_criteria = exclusion_criteria
426
+ self.imported = imported
427
+
428
+ # Properties
429
+ @property
430
+ def table_count(self) -> int:
431
+ return len(self)
432
+
433
+ @property
434
+ def column_count(self) -> int:
435
+ return 0 if not self.table_count else sum(len(table) for table in self)
436
+
437
+ # Magic
438
+ def __repr__(self) -> str:
439
+ tables = list_as_bullets(elements=[str(t) for t in self], bullet="\n- ")
440
+ return f"Dictionary(name={self.name!r}, imported={self.imported!r}, {tables})"
441
+
442
+ def __getitem__(self, key: int | str) -> Table:
443
+ if isinstance(key, int):
444
+ return super().__getitem__(key)
445
+ target = _normalise_name(key)
446
+ found = next((t for t in self if t.name == target), None)
447
+ if not found:
448
+ raise KeyError(f"Table {key!r} not found in Dictionary.")
449
+ return found
450
+
451
+ # Getters
452
+ def __get(self, name: str, default: Table | None = None) -> Table | None:
453
+ target = _normalise_name(name)
454
+ return next((t for t in self if t.name == target), default)
455
+
456
+ def index_of(self, name: str) -> int | None:
457
+ target = _normalise_name(name)
458
+ for i, t in enumerate(self):
459
+ if t.name == target:
460
+ return i
461
+ return None
462
+
463
+ def get_table_names(self) -> list[str]:
464
+ """
465
+ Summary:
466
+ Retrieves a list of table names from the dictionary.
467
+
468
+ Returns:
469
+ list[str]: A list of table names.
470
+ """
471
+ return [t.name for t in self]
472
+
473
+ def get_table(self, table: str) -> Table:
474
+ """
475
+ Summary:
476
+ Gets a table from the dictionary by name.
477
+
478
+ Arguments:
479
+ table (str): The name of the table to be retrieved.
480
+
481
+ Returns:
482
+ Table: The retrieved table.
483
+
484
+ Raises:
485
+ KeyError: If the table is not found in the dictionary.
486
+ """
487
+ target = _normalise_name(table)
488
+ found = next((t for t in self if t.name == target), None)
489
+
490
+ if not found:
491
+ raise KeyError(f"Table {table!r} not found in Dictionary.")
492
+
493
+ return found
494
+
495
+ # Manipulation
496
+ def add_table(self, table: Table) -> None:
497
+ """
498
+ Summary:
499
+ Adds a table to the dictionary.
500
+
501
+ Arguments:
502
+ table (Table): The table to be added.
503
+
504
+ Raises:
505
+ DataDictionaryError: If the table already exists in the dictionary.
506
+ """
507
+ if not isinstance(table, Table):
508
+ raise DataDictionaryError(
509
+ "Only Table objects can be added to a Dictionary."
510
+ )
511
+ if table.name in self.get_table_names():
512
+ raise DataDictionaryError(f"Table {table.name!r} already exists.")
513
+ super().append(table)
514
+
515
+ def remove_table(self, table: str) -> None:
516
+ """
517
+ Summary:
518
+ Removes the specified table from the dictionary.
519
+
520
+ Arguments:
521
+ table (str): The name of the table to be removed.
522
+
523
+ Raises:
524
+ DataDictionaryError: If the table does not exist in the dictionary.
525
+ """
526
+ name = self.get_table(table).name
527
+ remaining = [t for t in self if t.name != name]
528
+ self.clear()
529
+ super().extend(remaining)
530
+
531
+ def set_primary_keys(self, primary_keys: dict[str, list[str | int]]) -> None:
532
+ """
533
+ Summary:
534
+ Sets the primary keys for each table in the dictionary.
535
+
536
+ Arguments:
537
+ primary_keys (dict[str, list[str | int]]): A dictionary mapping table names to column names or orders.
538
+
539
+ Raises:
540
+ DataDictionaryError: If any tables or columns have invalid names or types, or if any tables or columns have duplicate names.
541
+ """
542
+ for table_name, keys in (primary_keys or {}).items():
543
+ self.get_table(table_name).set_primary_keys(keys)
544
+
545
+ # Helpers
546
+ def check(self) -> None:
547
+ """
548
+ Summary:
549
+ Validates the integrity of the dictionary.
550
+
551
+ Raises:
552
+ DataDictionaryError: If any tables or columns have invalid names or
553
+ types, or if any tables or columns have duplicate names.
554
+ """
555
+ for table in self:
556
+ table.check()
557
+
558
+ for table in self:
559
+ for column in table:
560
+ column.check()
561
+
562
+ # Export
563
+ def export_dictionary(
564
+ self,
565
+ directory: Path | str,
566
+ filename: str | None = None,
567
+ overwrite: bool = False,
568
+ debug: bool = False,
569
+ _template_path: Path | str | None = None,
570
+ ):
571
+ from valediction.dictionary.exporting import (
572
+ export_dictionary, # Avoid Circulars
573
+ )
574
+
575
+ return export_dictionary(
576
+ dictionary=self,
577
+ directory=directory,
578
+ filename=filename,
579
+ overwrite=overwrite,
580
+ debug=debug,
581
+ _template_path=_template_path,
582
+ )
@@ -0,0 +1,22 @@
1
+ class DataDictionaryError(Exception):
2
+ def __init__(self, message: str = "A DataDictionaryError has occurred"):
3
+ super().__init__(message)
4
+ self.message = message
5
+
6
+
7
+ class DataDictionaryImportError(Exception):
8
+ def __init__(self, message: str = "A DataDictionaryImportError has occurred"):
9
+ super().__init__(message)
10
+ self.message = message
11
+
12
+
13
+ class DataDictionaryExportError(Exception):
14
+ def __init__(self, message: str = "A DataDictionaryExportError has occurred"):
15
+ super().__init__(message)
16
+ self.message = message
17
+
18
+
19
+ class DataIntegrityError(Exception):
20
+ def __init__(self, message: str = "A DataIntegrityError has occurred"):
21
+ super().__init__(message)
22
+ self.message = message
@@ -0,0 +1,97 @@
1
+ import re
2
+ from pathlib import Path
3
+ from re import Pattern
4
+
5
+ from valediction.data_types.data_types import DataType
6
+ from valediction.support import list_as_bullets
7
+
8
+ ROOT = Path(__file__).resolve().parent
9
+ DIR_DICTIONARY = ROOT / "dictionary"
10
+ TEMPLATE_DATA_DICTIONARY_PATH = (
11
+ DIR_DICTIONARY / "template" / "Project - Data Dictionary.xltx"
12
+ )
13
+
14
+
15
+ class Config:
16
+ def __init__(self):
17
+ self.template_data_dictionary_path: Path = TEMPLATE_DATA_DICTIONARY_PATH
18
+ self.max_table_name_length: int = 63
19
+ self.max_column_name_length: int = 30
20
+ self.max_primary_keys: int = 7
21
+ self.invalid_name_pattern: str | Pattern = re.compile(r"[^A-Z0-9_]")
22
+ self.null_values: list[str] = ["", "null", "none"]
23
+ self.forbidden_characters: list[str] = []
24
+ self.date_formats: dict[str, DataType] = {
25
+ "%Y-%m-%d": DataType.DATE,
26
+ "%Y/%m/%d": DataType.DATE,
27
+ "%d/%m/%Y": DataType.DATE,
28
+ "%d-%m-%Y": DataType.DATE,
29
+ "%m/%d/%Y": DataType.DATE,
30
+ "%m-%d-%Y": DataType.DATE,
31
+ "%Y-%m-%d %H:%M:%S": DataType.DATETIME,
32
+ "%Y-%m-%d %H:%M": DataType.DATETIME,
33
+ "%d/%m/%Y %H:%M:%S": DataType.DATETIME,
34
+ "%d/%m/%Y %H:%M": DataType.DATETIME,
35
+ "%m/%d/%Y %H:%M:%S": DataType.DATETIME,
36
+ "%Y-%m-%dT%H:%M:%S": DataType.DATETIME,
37
+ "%Y-%m-%dT%H:%M:%S.%f": DataType.DATETIME,
38
+ "%Y-%m-%dT%H:%M:%S%z": DataType.DATETIME,
39
+ "%Y-%m-%dT%H:%M:%S.%f%z": DataType.DATETIME,
40
+ "%Y-%m-%dT%H:%M:%SZ": DataType.DATETIME,
41
+ "%Y-%m-%dT%H:%M:%S.%fZ": DataType.DATETIME,
42
+ }
43
+ self.enforce_no_null_columns: bool = True
44
+ self.enforce_primary_keys: bool = True
45
+
46
+ def __repr__(self):
47
+ date_list = list_as_bullets(
48
+ elements=[f"{k}: {v.name} " for k, v in self.date_formats.items()],
49
+ bullet="\n - ",
50
+ )
51
+ return (
52
+ f"Config(\n"
53
+ f"Dictionary Settings:\n"
54
+ f" - template_data_dictionary_path='{self.template_data_dictionary_path}'\n"
55
+ f" - max_table_name_length={self.max_table_name_length}\n"
56
+ f" - max_column_name_length={self.max_column_name_length}\n"
57
+ f" - max_primary_keys={self.max_primary_keys}\n"
58
+ f" - invalid_name_pattern={self.invalid_name_pattern}\n"
59
+ f"Data Settings:\n"
60
+ f" - default_null_values={self.null_values}\n"
61
+ f" - forbidden_characters={self.forbidden_characters}\n"
62
+ f" - date_formats=[{date_list}\n ]\n"
63
+ ")"
64
+ )
65
+
66
+ # Context Wrapper With Reset
67
+ def __enter__(self):
68
+ global default_config
69
+ default_config = self
70
+ return self
71
+
72
+ def __exit__(self, exc_type, exc_value, traceback):
73
+ global default_config
74
+ default_config = Config()
75
+
76
+
77
+ default_config: Config = None
78
+
79
+
80
+ def get_config() -> Config:
81
+ """Gets the current `default_config` instance. Changing attributes will set them
82
+ globally.
83
+
84
+ Returns:
85
+ Config: The current default configuration.
86
+ """
87
+ global default_config
88
+ return default_config
89
+
90
+
91
+ def reset_default_config() -> None:
92
+ """Resets `default_config` settings globally to original defaults."""
93
+ global default_config
94
+ default_config = Config()
95
+
96
+
97
+ reset_default_config()
File without changes