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.
- valediction/__init__.py +8 -0
- valediction/convenience.py +50 -0
- valediction/data_types/__init__.py +0 -0
- valediction/data_types/data_type_helpers.py +75 -0
- valediction/data_types/data_types.py +58 -0
- valediction/data_types/type_inference.py +541 -0
- valediction/datasets/__init__.py +0 -0
- valediction/datasets/datasets.py +870 -0
- valediction/datasets/datasets_helpers.py +46 -0
- valediction/demo/DEMO - Data Dictionary.xlsx +0 -0
- valediction/demo/DEMOGRAPHICS.csv +101 -0
- valediction/demo/DIAGNOSES.csv +650 -0
- valediction/demo/LAB_TESTS.csv +1001 -0
- valediction/demo/VITALS.csv +1001 -0
- valediction/demo/__init__.py +6 -0
- valediction/demo/demo_dictionary.py +129 -0
- valediction/dictionary/__init__.py +0 -0
- valediction/dictionary/exporting.py +501 -0
- valediction/dictionary/exporting_helpers.py +371 -0
- valediction/dictionary/generation.py +357 -0
- valediction/dictionary/helpers.py +174 -0
- valediction/dictionary/importing.py +494 -0
- valediction/dictionary/integrity.py +37 -0
- valediction/dictionary/model.py +582 -0
- valediction/dictionary/template/PROJECT - Data Dictionary.xltx +0 -0
- valediction/exceptions.py +22 -0
- valediction/integrity.py +97 -0
- valediction/io/__init__.py +0 -0
- valediction/io/csv_readers.py +307 -0
- valediction/progress.py +206 -0
- valediction/support.py +72 -0
- valediction/validation/__init__.py +0 -0
- valediction/validation/helpers.py +315 -0
- valediction/validation/issues.py +280 -0
- valediction/validation/validation.py +598 -0
- valediction-1.0.0.dist-info/METADATA +15 -0
- valediction-1.0.0.dist-info/RECORD +38 -0
- 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
|
+
)
|
|
Binary file
|
|
@@ -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
|
valediction/integrity.py
ADDED
|
@@ -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
|