data-validation-engine 0.8.2__tar.gz → 0.8.4__tar.gz
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.
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/PKG-INFO +3 -2
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/pyproject.toml +6 -5
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py +37 -23
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/contract.py +18 -6
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/spark_helpers.py +55 -22
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/utilities.py +43 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/utilities.py +20 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/LICENSE +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/README.md +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/common/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/common/error_utils.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/base/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/base/auditing.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/base/backend.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/base/contract.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/base/core.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/base/reader.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/base/reference_data.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/base/rules.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/base/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/exceptions.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/auditing.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/contract.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/readers/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/readers/csv.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/readers/json.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/readers/xml.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/reference_data.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/rules.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/types.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/duckdb/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/auditing.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/backend.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/readers/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/readers/csv.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/readers/json.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/readers/xml.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/reference_data.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/rules.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/types.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/implementations/spark/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/metadata/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/metadata/contract.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/metadata/reporting.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/metadata/rules.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/readers/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/readers/csv.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/readers/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/readers/xml.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/readers/xml_linting.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/types.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/configuration/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/configuration/base.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/configuration/v1/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/configuration/v1/filters.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/configuration/v1/rule_stores/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/configuration/v1/rule_stores/models.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/configuration/v1/steps.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/constants.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/engine.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/exceptions.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/functions/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/functions/implementations.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/loggers.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/message.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/models.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/templating.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/type_hints.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/validation.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/domain_types.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/exc.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/function_library.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/function_wrapper.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/model_generator.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/models.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/exceptions.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/file_handling/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/file_handling/helpers.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/file_handling/implementations/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/file_handling/implementations/base.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/file_handling/implementations/dbfs.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/file_handling/implementations/file.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/file_handling/implementations/s3.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/file_handling/log_handler.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/file_handling/service.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/file_handling/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/type_hints.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/parser/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/pipeline/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/pipeline/duckdb_pipeline.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/pipeline/foundry_ddb_pipeline.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/pipeline/pipeline.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/pipeline/spark_pipeline.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/pipeline/utils.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/reporting/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/reporting/constants.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/reporting/error_report.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/reporting/excel_report.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: data-validation-engine
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.4
|
|
4
4
|
Summary: `nhs data validation engine` is a framework used to validate data
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -10,6 +10,7 @@ Requires-Python: >=3.10,<3.13
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
15
16
|
Classifier: Typing :: Typed
|
|
@@ -25,7 +26,7 @@ Requires-Dist: pandas (==2.3.3)
|
|
|
25
26
|
Requires-Dist: polars (==0.20.31)
|
|
26
27
|
Requires-Dist: pyarrow (==23.0.1)
|
|
27
28
|
Requires-Dist: pydantic (==1.10.19)
|
|
28
|
-
Requires-Dist: pyspark (>=3.
|
|
29
|
+
Requires-Dist: pyspark (>=3.5.0,<=3.5.5)
|
|
29
30
|
Requires-Dist: typing_extensions (==4.15.0)
|
|
30
31
|
Requires-Dist: urllib3 (==2.7.0)
|
|
31
32
|
Project-URL: Changelog, https://github.com/NHSDigital/data-validation-engine/blob/main/CHANGELOG.md
|
|
@@ -10,6 +10,7 @@ classifiers = [
|
|
|
10
10
|
"Programming Language :: Python :: 3",
|
|
11
11
|
"Programming Language :: Python :: 3.10",
|
|
12
12
|
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
13
14
|
"Operating System :: OS Independent",
|
|
14
15
|
"Topic :: Software Development :: Libraries",
|
|
15
16
|
"Typing :: Typed",
|
|
@@ -24,7 +25,7 @@ Issues = "https://github.com/NHSDigital/data-validation-engine/issues"
|
|
|
24
25
|
Changelog = "https://github.com/NHSDigital/data-validation-engine/blob/main/CHANGELOG.md"
|
|
25
26
|
|
|
26
27
|
[tool.poetry]
|
|
27
|
-
version = "0.8.
|
|
28
|
+
version = "0.8.4"
|
|
28
29
|
packages = [
|
|
29
30
|
{ include = "dve", from = "src" },
|
|
30
31
|
]
|
|
@@ -43,7 +44,7 @@ pandas = "2.3.3"
|
|
|
43
44
|
polars = "0.20.31"
|
|
44
45
|
pyarrow = "23.0.1"
|
|
45
46
|
pydantic = "1.10.19"
|
|
46
|
-
pyspark = ">=3.
|
|
47
|
+
pyspark = ">=3.5.0,<=3.5.5"
|
|
47
48
|
typing_extensions = "4.15.0"
|
|
48
49
|
urllib3 = "2.7.0" # dependency of boto3 & botocore
|
|
49
50
|
|
|
@@ -68,7 +69,7 @@ optional = true
|
|
|
68
69
|
behave = "1.3.3"
|
|
69
70
|
coverage = "7.11.0"
|
|
70
71
|
moto = {extras = ["s3"], version = "4.2.14"}
|
|
71
|
-
cryptography = "
|
|
72
|
+
cryptography = "50.0.0" # dependency of `moto`
|
|
72
73
|
requests = "2.33.0" # dependency of `moto`
|
|
73
74
|
Werkzeug = "3.1.6"
|
|
74
75
|
pytest = "9.0.3"
|
|
@@ -105,8 +106,8 @@ click = "8.2.1"
|
|
|
105
106
|
mkdocs = "1.6.1"
|
|
106
107
|
mkdocstrings = { version = "1.0.3", extras = ["python"] }
|
|
107
108
|
griffelib = "2.0.1"
|
|
108
|
-
pymdown-extensions = "
|
|
109
|
-
zensical = "0.0.
|
|
109
|
+
pymdown-extensions = "11.0.1"
|
|
110
|
+
zensical = "0.0.46"
|
|
110
111
|
|
|
111
112
|
[tool.ruff]
|
|
112
113
|
line-length = 100
|
|
@@ -21,8 +21,10 @@ from typing_extensions import Annotated, get_args, get_origin, get_type_hints
|
|
|
21
21
|
|
|
22
22
|
from dve.common.error_utils import get_feedback_errors_uri
|
|
23
23
|
from dve.core_engine.backends.base.utilities import _get_non_heterogenous_type
|
|
24
|
+
from dve.core_engine.backends.utilities import DEFAULT_ISO_FORMATS, datetime_format_to_regex
|
|
24
25
|
from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
|
|
25
26
|
from dve.core_engine.type_hints import URI, EntityName
|
|
27
|
+
from dve.metadata_parser.utilities import resilient_get
|
|
26
28
|
from dve.parser.file_handling.service import LocalFilesystemImplementation, _get_implementation
|
|
27
29
|
|
|
28
30
|
|
|
@@ -361,7 +363,7 @@ def duckdb_record_index(cls):
|
|
|
361
363
|
|
|
362
364
|
def _cast_as_ddb_type(field_expr: str, type_annotation: Any) -> str:
|
|
363
365
|
"""Cast to Duck DB type"""
|
|
364
|
-
return f"""
|
|
366
|
+
return f"""TRY_CAST({field_expr} as {get_duckdb_type_from_annotation(type_annotation)})"""
|
|
365
367
|
|
|
366
368
|
|
|
367
369
|
def _ddb_safely_quote_name(field_name: str) -> str:
|
|
@@ -378,9 +380,6 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
378
380
|
element_name: str,
|
|
379
381
|
type_annotation: Any,
|
|
380
382
|
parent_element: bool = True,
|
|
381
|
-
date_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
|
|
382
|
-
timestamp_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}((\+|\-)[0-9]{2}:[0-9]{2})?$", # pylint: disable=C0301
|
|
383
|
-
time_regex: str = r"^[0-9]{2}:[0-9]{2}:[0-9]{2}$",
|
|
384
383
|
) -> str:
|
|
385
384
|
"""Generate casting statements for duckdb relations from type annotations"""
|
|
386
385
|
type_origin = get_origin(type_annotation)
|
|
@@ -391,19 +390,23 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
391
390
|
if type_origin is Union:
|
|
392
391
|
python_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
393
392
|
return get_duckdb_cast_statement_from_annotation(
|
|
394
|
-
element_name,
|
|
393
|
+
element_name,
|
|
394
|
+
python_type,
|
|
395
|
+
parent_element,
|
|
395
396
|
)
|
|
396
397
|
|
|
397
398
|
# Type hint is e.g. `List[str]`, check to ensure non-heterogenity.
|
|
398
399
|
if type_origin is list or (isinstance(type_origin, type) and issubclass(type_origin, list)):
|
|
399
400
|
element_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
400
|
-
stmt = f"
|
|
401
|
+
stmt = f"LIST_TRANSFORM({quoted_name}, x -> {get_duckdb_cast_statement_from_annotation('x',element_type, False,)})" # pylint: disable=C0301
|
|
401
402
|
return stmt if not parent_element else _cast_as_ddb_type(stmt, type_annotation)
|
|
402
403
|
|
|
403
404
|
if type_origin is Annotated:
|
|
404
405
|
python_type, *other_args = get_args(type_annotation) # pylint: disable=unused-variable
|
|
405
406
|
return get_duckdb_cast_statement_from_annotation(
|
|
406
|
-
element_name,
|
|
407
|
+
element_name,
|
|
408
|
+
python_type,
|
|
409
|
+
parent_element,
|
|
407
410
|
) # add other expected params here
|
|
408
411
|
# Ensure that we have a concrete type at this point.
|
|
409
412
|
if not isinstance(type_annotation, type):
|
|
@@ -428,7 +431,9 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
428
431
|
continue
|
|
429
432
|
|
|
430
433
|
fields[field_name] = get_duckdb_cast_statement_from_annotation(
|
|
431
|
-
f"{element_name}.{field_name}",
|
|
434
|
+
f"{element_name}.{field_name}",
|
|
435
|
+
field_annotation,
|
|
436
|
+
False,
|
|
432
437
|
)
|
|
433
438
|
|
|
434
439
|
if not fields:
|
|
@@ -436,7 +441,7 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
436
441
|
f"No type annotations in dict/dataclass type (got {type_annotation!r})"
|
|
437
442
|
)
|
|
438
443
|
cast_exprs = ",".join([f'"{nme}":= {stmt}' for nme, stmt in fields.items()])
|
|
439
|
-
stmt = f"
|
|
444
|
+
stmt = f"STRUCT_PACK({cast_exprs})"
|
|
440
445
|
return stmt if not parent_element else _cast_as_ddb_type(stmt, type_annotation)
|
|
441
446
|
|
|
442
447
|
if type_annotation is list:
|
|
@@ -447,18 +452,27 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
447
452
|
raise ValueError(f"dict must be `typing.TypedDict` subclass, got {type_annotation!r}")
|
|
448
453
|
|
|
449
454
|
for type_ in type_annotation.mro():
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
455
|
+
if issubclass(type_, (date, time)):
|
|
456
|
+
_date_format: str = resilient_get(
|
|
457
|
+
type_, "DATE_FORMAT", "TIME_FORMAT"
|
|
458
|
+
) or DEFAULT_ISO_FORMATS.get(
|
|
459
|
+
type_, DEFAULT_ISO_FORMATS.get(datetime)
|
|
460
|
+
) # type: ignore
|
|
461
|
+
dt_cast_statement = rf"CASE WHEN REGEXP_FULL_MATCH(TRIM({quoted_name}), '{datetime_format_to_regex(_date_format)}') THEN TRY_STRPTIME(TRIM({quoted_name}), '{_date_format}') ELSE NULL END" # pylint: disable=C0301
|
|
462
|
+
|
|
463
|
+
# datetime is subclass of date, so needs to be handled first
|
|
464
|
+
if issubclass(type_, datetime):
|
|
465
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as TIMESTAMP)"
|
|
466
|
+
return stmt
|
|
467
|
+
if issubclass(type_, date):
|
|
468
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as DATE)"
|
|
469
|
+
return stmt
|
|
470
|
+
if issubclass(type_, time):
|
|
471
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as TIME)"
|
|
472
|
+
return stmt
|
|
473
|
+
else:
|
|
474
|
+
duck_type = get_duckdb_type_from_annotation(type_)
|
|
475
|
+
if duck_type:
|
|
476
|
+
stmt = f"TRIM({quoted_name})"
|
|
477
|
+
return _cast_as_ddb_type(stmt, type_) if parent_element else stmt
|
|
464
478
|
raise ValueError(f"No equivalent DuckDB type for {type_annotation!r}")
|
|
@@ -7,6 +7,7 @@ from typing import Any, Optional
|
|
|
7
7
|
from uuid import uuid4
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel
|
|
10
|
+
from pydantic.fields import ModelField
|
|
10
11
|
from pyspark.sql import DataFrame, SparkSession
|
|
11
12
|
from pyspark.sql import functions as sf
|
|
12
13
|
from pyspark.sql.functions import col, lit
|
|
@@ -26,6 +27,7 @@ from dve.core_engine.backends.exceptions import (
|
|
|
26
27
|
)
|
|
27
28
|
from dve.core_engine.backends.implementations.spark.spark_helpers import (
|
|
28
29
|
df_is_empty,
|
|
30
|
+
get_spark_cast_statement_from_annotation,
|
|
29
31
|
get_type_from_annotation,
|
|
30
32
|
spark_read_parquet,
|
|
31
33
|
spark_record_index,
|
|
@@ -86,7 +88,7 @@ class SparkDataContract(BaseDataContract[DataFrame]):
|
|
|
86
88
|
schema=get_type_from_annotation(schema),
|
|
87
89
|
)
|
|
88
90
|
|
|
89
|
-
# pylint: disable=R0915
|
|
91
|
+
# pylint: disable=R0914,R0915
|
|
90
92
|
def apply_data_contract(
|
|
91
93
|
self,
|
|
92
94
|
working_dir: URI,
|
|
@@ -102,6 +104,7 @@ class SparkDataContract(BaseDataContract[DataFrame]):
|
|
|
102
104
|
|
|
103
105
|
successful = True
|
|
104
106
|
for entity_name, record_df in entities.items():
|
|
107
|
+
entity_fields: dict[str, ModelField] = contract_metadata.schemas[entity_name].__fields__
|
|
105
108
|
spark_schema = get_type_from_annotation(contract_metadata.schemas[entity_name])
|
|
106
109
|
spark_schema.add(StructField(RECORD_INDEX_COLUMN_NAME, LongType()))
|
|
107
110
|
if df_is_empty(record_df):
|
|
@@ -144,12 +147,21 @@ class SparkDataContract(BaseDataContract[DataFrame]):
|
|
|
144
147
|
self.logger.info(f"Data contract found {msg_count} issues in {entity_name}")
|
|
145
148
|
|
|
146
149
|
try:
|
|
150
|
+
# TODO: will need to revisit in pydantic v2 bump as model field no longer available
|
|
147
151
|
record_df = record_df.select(
|
|
148
|
-
[
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
*[
|
|
153
|
+
(
|
|
154
|
+
get_spark_cast_statement_from_annotation(
|
|
155
|
+
fld, mdl_field.annotation
|
|
156
|
+
).alias(fld)
|
|
157
|
+
if fld in record_df.columns
|
|
158
|
+
else lit(None).cast(
|
|
159
|
+
get_type_from_annotation(mdl_field.annotation).alias(fld)
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
for fld, mdl_field in entity_fields.items()
|
|
163
|
+
],
|
|
164
|
+
col(RECORD_INDEX_COLUMN_NAME).cast(LongType()).alias(RECORD_INDEX_COLUMN_NAME),
|
|
153
165
|
)
|
|
154
166
|
except Exception as err: # pylint: disable=broad-except
|
|
155
167
|
successful = False
|
|
@@ -28,6 +28,7 @@ from typing_extensions import Annotated, Protocol, TypedDict, get_args, get_orig
|
|
|
28
28
|
|
|
29
29
|
from dve.common.error_utils import get_feedback_errors_uri
|
|
30
30
|
from dve.core_engine.backends.base.utilities import _get_non_heterogenous_type
|
|
31
|
+
from dve.core_engine.backends.utilities import DEFAULT_ISO_FORMATS, datetime_format_to_regex
|
|
31
32
|
from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
|
|
32
33
|
from dve.core_engine.type_hints import URI, EntityName
|
|
33
34
|
|
|
@@ -99,6 +100,21 @@ PYTHON_TYPE_TO_SPARK_TYPE: dict[type, st.DataType] = {
|
|
|
99
100
|
}
|
|
100
101
|
"""A mapping of Python types to the equivalent Spark types."""
|
|
101
102
|
|
|
103
|
+
PYTHON_TO_JAVA_DT_FORMAT_MAP: dict[str, str] = {
|
|
104
|
+
"%Y": "yyyy",
|
|
105
|
+
"%y": "yy",
|
|
106
|
+
"%m": "MM",
|
|
107
|
+
"%d": "dd",
|
|
108
|
+
"%H": "HH",
|
|
109
|
+
"%M": "mm",
|
|
110
|
+
"%S": "ss",
|
|
111
|
+
"%z": "XX",
|
|
112
|
+
"%Z": "z",
|
|
113
|
+
}
|
|
114
|
+
"""A mapping of python to java datetime component formats. Not exhaustive but will hopefully support
|
|
115
|
+
all requirements.
|
|
116
|
+
"""
|
|
117
|
+
|
|
102
118
|
|
|
103
119
|
PydanticModel = TypeVar("PydanticModel", bound=BaseModel)
|
|
104
120
|
"""An Pydantic model."""
|
|
@@ -272,6 +288,13 @@ def create_udf(function: Callable) -> Callable:
|
|
|
272
288
|
return udf(function, returnType=return_type)
|
|
273
289
|
|
|
274
290
|
|
|
291
|
+
def python_to_java_datetime_format(datetime_fmt: str) -> str:
|
|
292
|
+
"Helper to convert python datetime formats to Java datetime formats"
|
|
293
|
+
for pt, jt in PYTHON_TO_JAVA_DT_FORMAT_MAP.items():
|
|
294
|
+
datetime_fmt = datetime_fmt.replace(pt, jt)
|
|
295
|
+
return datetime_fmt.replace("T", "'T'")
|
|
296
|
+
|
|
297
|
+
|
|
275
298
|
SupportedBaseType = Union[str, int, bool, float, Decimal, dt.date, dt.datetime]
|
|
276
299
|
"""Supported base types for Spark literals."""
|
|
277
300
|
SparkLiteralType = Union[ # type: ignore
|
|
@@ -506,11 +529,7 @@ def _spark_safely_quote_name(field_name: str) -> str:
|
|
|
506
529
|
|
|
507
530
|
# pylint: disable=R0801
|
|
508
531
|
def get_spark_cast_statement_from_annotation(
|
|
509
|
-
element_name: str,
|
|
510
|
-
type_annotation: Any,
|
|
511
|
-
parent_element: bool = True,
|
|
512
|
-
date_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
|
|
513
|
-
timestamp_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}((\\+|\\-)[0-9]{2}:[0-9]{2})?$", # pylint: disable=C0301
|
|
532
|
+
element_name: str, type_annotation: Any, parent_element: bool = True
|
|
514
533
|
):
|
|
515
534
|
"""Generate casting statements for spark dataframes based on type annotations"""
|
|
516
535
|
type_origin = get_origin(type_annotation)
|
|
@@ -520,20 +539,18 @@ def get_spark_cast_statement_from_annotation(
|
|
|
520
539
|
# An `Optional` or `Union` type, check to ensure non-heterogenity.
|
|
521
540
|
if type_origin is Union:
|
|
522
541
|
python_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
523
|
-
return get_spark_cast_statement_from_annotation(
|
|
524
|
-
element_name, python_type, parent_element, date_regex, timestamp_regex
|
|
525
|
-
)
|
|
542
|
+
return get_spark_cast_statement_from_annotation(element_name, python_type, parent_element)
|
|
526
543
|
|
|
527
544
|
# Type hint is e.g. `List[str]`, check to ensure non-heterogenity.
|
|
528
545
|
if type_origin is list or (isinstance(type_origin, type) and issubclass(type_origin, list)):
|
|
529
546
|
element_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
530
|
-
stmt = f"
|
|
547
|
+
stmt = f"TRANSFORM({quoted_name}, x -> {get_spark_cast_statement_from_annotation('x',element_type, False)})" # pylint: disable=C0301
|
|
531
548
|
return stmt if not parent_element else _cast_as_spark_type(stmt, type_annotation)
|
|
532
549
|
|
|
533
550
|
if type_origin is Annotated:
|
|
534
551
|
python_type, *_ = get_args(type_annotation) # pylint: disable=unused-variable
|
|
535
552
|
return get_spark_cast_statement_from_annotation(
|
|
536
|
-
element_name, python_type, parent_element
|
|
553
|
+
element_name, python_type, parent_element
|
|
537
554
|
) # add other expected params here
|
|
538
555
|
# Ensure that we have a concrete type at this point.
|
|
539
556
|
if not isinstance(type_annotation, type):
|
|
@@ -558,7 +575,7 @@ def get_spark_cast_statement_from_annotation(
|
|
|
558
575
|
continue
|
|
559
576
|
|
|
560
577
|
fields[field_name] = get_spark_cast_statement_from_annotation(
|
|
561
|
-
f"{element_name}.{field_name}", field_annotation, False
|
|
578
|
+
f"{element_name}.{field_name}", field_annotation, False
|
|
562
579
|
)
|
|
563
580
|
|
|
564
581
|
if not fields:
|
|
@@ -566,7 +583,7 @@ def get_spark_cast_statement_from_annotation(
|
|
|
566
583
|
f"No type annotations in dict/dataclass type (got {type_annotation!r})"
|
|
567
584
|
)
|
|
568
585
|
cast_exprs = ",".join([f"{stmt} AS `{nme}`" for nme, stmt in fields.items()])
|
|
569
|
-
stmt = f"
|
|
586
|
+
stmt = f"STRUCT({cast_exprs})"
|
|
570
587
|
return stmt if not parent_element else _cast_as_spark_type(stmt, type_annotation)
|
|
571
588
|
if type_annotation is list:
|
|
572
589
|
raise ValueError(
|
|
@@ -576,15 +593,31 @@ def get_spark_cast_statement_from_annotation(
|
|
|
576
593
|
raise ValueError(f"dict must be `typing.TypedDict` subclass, got {type_annotation!r}")
|
|
577
594
|
|
|
578
595
|
for type_ in type_annotation.mro():
|
|
579
|
-
# datetime is subclass of date, so needs to be handled first
|
|
580
|
-
if issubclass(type_, dt.datetime):
|
|
581
|
-
stmt = rf"CASE WHEN REGEXP(TRIM({quoted_name}), '{timestamp_regex}') THEN TRIM({quoted_name}) ELSE NULL END" # pylint: disable=C0301
|
|
582
|
-
return _cast_as_spark_type(stmt, type_) if parent_element else stmt
|
|
583
596
|
if issubclass(type_, dt.date):
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
597
|
+
_date_format: str = getattr( # type: ignore
|
|
598
|
+
type_,
|
|
599
|
+
"DATE_FORMAT",
|
|
600
|
+
DEFAULT_ISO_FORMATS.get(type_, DEFAULT_ISO_FORMATS.get(dt.datetime)),
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# pylint: disable=C0301
|
|
604
|
+
dt_cast_statement = f"CASE WHEN REGEXP(TRIM({quoted_name}), '{datetime_format_to_regex(_date_format)}') THEN TRY_TO_TIMESTAMP(TRIM({quoted_name}), \"{python_to_java_datetime_format(_date_format)}\") ELSE NULL END" # pylint: disable=C0301
|
|
605
|
+
# datetime is subclass of date, so needs to be handled first
|
|
606
|
+
if issubclass(type_, dt.datetime):
|
|
607
|
+
return (
|
|
608
|
+
_cast_as_spark_type(dt_cast_statement, type_)
|
|
609
|
+
if parent_element
|
|
610
|
+
else dt_cast_statement
|
|
611
|
+
)
|
|
612
|
+
if issubclass(type_, dt.date):
|
|
613
|
+
return (
|
|
614
|
+
_cast_as_spark_type(dt_cast_statement, type_)
|
|
615
|
+
if parent_element
|
|
616
|
+
else dt_cast_statement
|
|
617
|
+
)
|
|
618
|
+
else:
|
|
619
|
+
spark_type = get_type_from_annotation(type_)
|
|
620
|
+
if spark_type:
|
|
621
|
+
stmt = f"TRIM({quoted_name})"
|
|
622
|
+
return _cast_as_spark_type(stmt, type_) if parent_element else stmt
|
|
590
623
|
raise ValueError(f"No equivalent Spark type for {type_annotation!r}")
|
|
@@ -23,6 +23,49 @@ if sys.version_info[:2] <= (3, 7):
|
|
|
23
23
|
else:
|
|
24
24
|
from typing import Annotated, get_args, get_origin, get_type_hints
|
|
25
25
|
|
|
26
|
+
DEFAULT_ISO_FORMATS: dict[type, str] = {
|
|
27
|
+
date: "%Y-%m-%d",
|
|
28
|
+
datetime: "%Y-%m-%dT%H:%M:%S",
|
|
29
|
+
time: "%H:%M:%S",
|
|
30
|
+
}
|
|
31
|
+
"""Mapping of default ISO formats to use when date format not supplied"""
|
|
32
|
+
|
|
33
|
+
PYTHON_DATE_FORMAT_REGEX_HELPER: dict[str, str] = {
|
|
34
|
+
"Y": r"[0-9]{4}",
|
|
35
|
+
"y": r"[0-9]{2}",
|
|
36
|
+
"m": r"[0-9]{2}",
|
|
37
|
+
"d": r"[0-9]{2}",
|
|
38
|
+
"H": r"[0-9]{2}",
|
|
39
|
+
"M": r"[0-9]{2}",
|
|
40
|
+
"S": r"[0-9]{2}",
|
|
41
|
+
"z": r"(\+|\-)?[0-9]+(\.[0-9]*)?",
|
|
42
|
+
"Z": r"[A-Z]{0,3}",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
REGEXP_NEED_ESCAPE_CHARS: tuple[str, str, str] = ("+", "-", ".")
|
|
46
|
+
"""Helper to map python date format to regexp expression. Not exhaustive, but aims to cover
|
|
47
|
+
all foreseen use cases."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def datetime_format_to_regex(format_str: str) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Helper function to convert python datetime formats to regexp string checks for casting
|
|
53
|
+
purposes.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
tokens = list(format_str.replace("%", ""))
|
|
57
|
+
|
|
58
|
+
for idx, tkn in enumerate(tokens):
|
|
59
|
+
if rpl := PYTHON_DATE_FORMAT_REGEX_HELPER.get(tkn):
|
|
60
|
+
tokens[idx] = rpl
|
|
61
|
+
elif tkn in REGEXP_NEED_ESCAPE_CHARS:
|
|
62
|
+
tokens[idx] = rf"\{tkn}"
|
|
63
|
+
else:
|
|
64
|
+
continue
|
|
65
|
+
tokens = [PYTHON_DATE_FORMAT_REGEX_HELPER.get(tkn, tkn) for tkn in tokens]
|
|
66
|
+
return "".join(["^", *tokens, "$"])
|
|
67
|
+
|
|
68
|
+
|
|
26
69
|
PYTHON_TYPE_TO_POLARS_TYPE: dict[type, PolarsType] = {
|
|
27
70
|
# issue with decimal conversion at the moment...
|
|
28
71
|
str: pl.Utf8, # type: ignore
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/utilities.py
RENAMED
|
@@ -52,3 +52,23 @@ def chain_get(
|
|
|
52
52
|
return result
|
|
53
53
|
|
|
54
54
|
raise exc.TypeNotFoundError(f"Callable or type ({item!r}) not found")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def resilient_get(item: object, *attribute_names: str) -> Any:
|
|
58
|
+
"""Given a number of attribute names, try to get attribute value
|
|
59
|
+
sequentially. Returns the first value found, and if no attributes found
|
|
60
|
+
returns None.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
item (object): The object to obtain attributes from (where possible)
|
|
64
|
+
attribute_names (str): The attribute names to search for
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Any: The first found attribute, otherwise None
|
|
68
|
+
"""
|
|
69
|
+
for attr in attribute_names:
|
|
70
|
+
try:
|
|
71
|
+
return getattr(item, attr)
|
|
72
|
+
except AttributeError:
|
|
73
|
+
continue
|
|
74
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/backends/types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/loggers.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/message.py
RENAMED
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/templating.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/type_hints.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/core_engine/validation.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/exc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/metadata_parser/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/pipeline/duckdb_pipeline.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/pipeline/spark_pipeline.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/reporting/constants.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/reporting/error_report.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.4}/src/dve/reporting/excel_report.py
RENAMED
|
File without changes
|