data-validation-engine 0.8.2__tar.gz → 0.8.3__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.3}/PKG-INFO +3 -2
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/pyproject.toml +4 -3
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py +22 -13
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/contract.py +18 -6
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/spark_helpers.py +48 -17
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/utilities.py +43 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/LICENSE +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/README.md +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/common/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/common/error_utils.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/auditing.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/backend.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/contract.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/core.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/reader.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/reference_data.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/rules.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/exceptions.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/auditing.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/contract.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/csv.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/json.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/xml.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/reference_data.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/rules.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/types.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/auditing.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/backend.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/csv.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/json.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/xml.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/reference_data.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/rules.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/types.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/contract.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/reporting.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/rules.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/csv.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/xml.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/xml_linting.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/types.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/base.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/filters.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/rule_stores/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/rule_stores/models.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/steps.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/constants.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/engine.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/exceptions.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/functions/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/functions/implementations.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/loggers.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/message.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/models.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/templating.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/type_hints.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/validation.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/domain_types.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/exc.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/function_library.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/function_wrapper.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/model_generator.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/models.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/exceptions.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/helpers.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/base.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/dbfs.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/file.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/s3.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/log_handler.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/service.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/type_hints.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/utilities.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/duckdb_pipeline.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/foundry_ddb_pipeline.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/pipeline.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/spark_pipeline.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/utils.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/reporting/__init__.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/reporting/constants.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/reporting/error_report.py +0 -0
- {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/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.3
|
|
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.3"
|
|
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
|
|
|
@@ -106,7 +107,7 @@ mkdocs = "1.6.1"
|
|
|
106
107
|
mkdocstrings = { version = "1.0.3", extras = ["python"] }
|
|
107
108
|
griffelib = "2.0.1"
|
|
108
109
|
pymdown-extensions = "10.21.3"
|
|
109
|
-
zensical = "0.0.
|
|
110
|
+
zensical = "0.0.46"
|
|
110
111
|
|
|
111
112
|
[tool.ruff]
|
|
112
113
|
line-length = 100
|
|
@@ -21,6 +21,7 @@ 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
|
|
26
27
|
from dve.parser.file_handling.service import LocalFilesystemImplementation, _get_implementation
|
|
@@ -361,7 +362,7 @@ def duckdb_record_index(cls):
|
|
|
361
362
|
|
|
362
363
|
def _cast_as_ddb_type(field_expr: str, type_annotation: Any) -> str:
|
|
363
364
|
"""Cast to Duck DB type"""
|
|
364
|
-
return f"""
|
|
365
|
+
return f"""TRY_CAST({field_expr} as {get_duckdb_type_from_annotation(type_annotation)})"""
|
|
365
366
|
|
|
366
367
|
|
|
367
368
|
def _ddb_safely_quote_name(field_name: str) -> str:
|
|
@@ -378,9 +379,6 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
378
379
|
element_name: str,
|
|
379
380
|
type_annotation: Any,
|
|
380
381
|
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
382
|
) -> str:
|
|
385
383
|
"""Generate casting statements for duckdb relations from type annotations"""
|
|
386
384
|
type_origin = get_origin(type_annotation)
|
|
@@ -391,19 +389,23 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
391
389
|
if type_origin is Union:
|
|
392
390
|
python_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
393
391
|
return get_duckdb_cast_statement_from_annotation(
|
|
394
|
-
element_name,
|
|
392
|
+
element_name,
|
|
393
|
+
python_type,
|
|
394
|
+
parent_element,
|
|
395
395
|
)
|
|
396
396
|
|
|
397
397
|
# Type hint is e.g. `List[str]`, check to ensure non-heterogenity.
|
|
398
398
|
if type_origin is list or (isinstance(type_origin, type) and issubclass(type_origin, list)):
|
|
399
399
|
element_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
400
|
-
stmt = f"
|
|
400
|
+
stmt = f"LIST_TRANSFORM({quoted_name}, x -> {get_duckdb_cast_statement_from_annotation('x',element_type, False,)})" # pylint: disable=C0301
|
|
401
401
|
return stmt if not parent_element else _cast_as_ddb_type(stmt, type_annotation)
|
|
402
402
|
|
|
403
403
|
if type_origin is Annotated:
|
|
404
404
|
python_type, *other_args = get_args(type_annotation) # pylint: disable=unused-variable
|
|
405
405
|
return get_duckdb_cast_statement_from_annotation(
|
|
406
|
-
element_name,
|
|
406
|
+
element_name,
|
|
407
|
+
python_type,
|
|
408
|
+
parent_element,
|
|
407
409
|
) # add other expected params here
|
|
408
410
|
# Ensure that we have a concrete type at this point.
|
|
409
411
|
if not isinstance(type_annotation, type):
|
|
@@ -428,7 +430,9 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
428
430
|
continue
|
|
429
431
|
|
|
430
432
|
fields[field_name] = get_duckdb_cast_statement_from_annotation(
|
|
431
|
-
f"{element_name}.{field_name}",
|
|
433
|
+
f"{element_name}.{field_name}",
|
|
434
|
+
field_annotation,
|
|
435
|
+
False,
|
|
432
436
|
)
|
|
433
437
|
|
|
434
438
|
if not fields:
|
|
@@ -436,7 +440,7 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
436
440
|
f"No type annotations in dict/dataclass type (got {type_annotation!r})"
|
|
437
441
|
)
|
|
438
442
|
cast_exprs = ",".join([f'"{nme}":= {stmt}' for nme, stmt in fields.items()])
|
|
439
|
-
stmt = f"
|
|
443
|
+
stmt = f"STRUCT_PACK({cast_exprs})"
|
|
440
444
|
return stmt if not parent_element else _cast_as_ddb_type(stmt, type_annotation)
|
|
441
445
|
|
|
442
446
|
if type_annotation is list:
|
|
@@ -447,18 +451,23 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
447
451
|
raise ValueError(f"dict must be `typing.TypedDict` subclass, got {type_annotation!r}")
|
|
448
452
|
|
|
449
453
|
for type_ in type_annotation.mro():
|
|
454
|
+
_date_format: str = getattr( # type: ignore
|
|
455
|
+
type_, "DATE_FORMAT", DEFAULT_ISO_FORMATS.get(type_, DEFAULT_ISO_FORMATS.get(datetime))
|
|
456
|
+
)
|
|
457
|
+
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
|
|
458
|
+
|
|
450
459
|
# datetime is subclass of date, so needs to be handled first
|
|
451
460
|
if issubclass(type_, datetime):
|
|
452
|
-
stmt = rf"
|
|
461
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as TIMESTAMP)"
|
|
453
462
|
return stmt
|
|
454
463
|
if issubclass(type_, date):
|
|
455
|
-
stmt = rf"
|
|
464
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as DATE)"
|
|
456
465
|
return stmt
|
|
457
466
|
if issubclass(type_, time):
|
|
458
|
-
stmt = rf"
|
|
467
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as TIME)"
|
|
459
468
|
return stmt
|
|
460
469
|
duck_type = get_duckdb_type_from_annotation(type_)
|
|
461
470
|
if duck_type:
|
|
462
|
-
stmt = f"
|
|
471
|
+
stmt = f"TRIM({quoted_name})"
|
|
463
472
|
return _cast_as_ddb_type(stmt, type_) if parent_element else stmt
|
|
464
473
|
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,29 @@ 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():
|
|
596
|
+
_date_format: str = getattr( # type: ignore
|
|
597
|
+
type_,
|
|
598
|
+
"DATE_FORMAT",
|
|
599
|
+
DEFAULT_ISO_FORMATS.get(type_, DEFAULT_ISO_FORMATS.get(dt.datetime)),
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# pylint: disable=C0301
|
|
603
|
+
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
|
|
579
604
|
# datetime is subclass of date, so needs to be handled first
|
|
580
605
|
if issubclass(type_, dt.datetime):
|
|
581
|
-
|
|
582
|
-
|
|
606
|
+
return (
|
|
607
|
+
_cast_as_spark_type(dt_cast_statement, type_)
|
|
608
|
+
if parent_element
|
|
609
|
+
else dt_cast_statement
|
|
610
|
+
)
|
|
583
611
|
if issubclass(type_, dt.date):
|
|
584
|
-
|
|
585
|
-
|
|
612
|
+
return (
|
|
613
|
+
_cast_as_spark_type(dt_cast_statement, type_)
|
|
614
|
+
if parent_element
|
|
615
|
+
else dt_cast_statement
|
|
616
|
+
)
|
|
586
617
|
spark_type = get_type_from_annotation(type_)
|
|
587
618
|
if spark_type:
|
|
588
|
-
stmt = f"
|
|
619
|
+
stmt = f"TRIM({quoted_name})"
|
|
589
620
|
return _cast_as_spark_type(stmt, type_) if parent_element else stmt
|
|
590
621
|
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
|
|
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.3}/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.3}/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.3}/src/dve/core_engine/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/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.3}/src/dve/core_engine/loggers.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/message.py
RENAMED
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/templating.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/type_hints.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/validation.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/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.3}/src/dve/metadata_parser/models.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/utilities.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.3}/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.3}/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.3}/src/dve/reporting/constants.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/reporting/error_report.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/reporting/excel_report.py
RENAMED
|
File without changes
|