data-validation-engine 0.8.1__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.1 → data_validation_engine-0.8.3}/PKG-INFO +3 -2
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/pyproject.toml +6 -4
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/auditing.py +1 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/reader.py +2 -9
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/rules.py +10 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/exceptions.py +1 -4
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/auditing.py +2 -4
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py +68 -15
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/csv.py +2 -2
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/reference_data.py +1 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/rules.py +2 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/contract.py +18 -6
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/rules.py +2 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/spark_helpers.py +98 -18
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/contract.py +1 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/csv.py +2 -2
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/utilities.py +7 -7
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/utilities.py +43 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/__init__.py +3 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/engine.py +12 -4
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/message.py +5 -15
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/models.py +2 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/__init__.py +1 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/pipeline.py +39 -25
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/utils.py +2 -0
- data_validation_engine-0.8.3/src/dve/reporting/constants.py +34 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/reporting/error_report.py +2 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/reporting/excel_report.py +32 -9
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/LICENSE +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/README.md +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/common/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/common/error_utils.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/backend.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/contract.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/core.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/reference_data.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/contract.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/json.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/xml.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/types.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/auditing.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/backend.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/csv.py +1 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/json.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/xml.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/reference_data.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/types.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/reporting.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/rules.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/xml.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/xml_linting.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/types.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/base.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/filters.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/rule_stores/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/rule_stores/models.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/steps.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/constants.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/exceptions.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/functions/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/functions/implementations.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/loggers.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/templating.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/type_hints.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/validation.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/domain_types.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/exc.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/function_library.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/function_wrapper.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/model_generator.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/models.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/exceptions.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/helpers.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/base.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/dbfs.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/file.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/s3.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/log_handler.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/service.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/type_hints.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/duckdb_pipeline.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/foundry_ddb_pipeline.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/spark_pipeline.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/reporting/__init__.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
|
|
|
@@ -68,6 +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"}
|
|
72
|
+
cryptography = "48.0.1" # dependency of `moto`
|
|
71
73
|
requests = "2.33.0" # dependency of `moto`
|
|
72
74
|
Werkzeug = "3.1.6"
|
|
73
75
|
pytest = "9.0.3"
|
|
@@ -78,7 +80,7 @@ xlsx2csv = "0.8.4" # polars requirement
|
|
|
78
80
|
optional = true
|
|
79
81
|
|
|
80
82
|
[tool.poetry.group.lint.dependencies]
|
|
81
|
-
black = "
|
|
83
|
+
black = "26.3.1"
|
|
82
84
|
astroid = "3.3.9"
|
|
83
85
|
isort = "5.13.2"
|
|
84
86
|
pylint = "3.3.9"
|
|
@@ -105,7 +107,7 @@ mkdocs = "1.6.1"
|
|
|
105
107
|
mkdocstrings = { version = "1.0.3", extras = ["python"] }
|
|
106
108
|
griffelib = "2.0.1"
|
|
107
109
|
pymdown-extensions = "10.21.3"
|
|
108
|
-
zensical = "0.0.
|
|
110
|
+
zensical = "0.0.46"
|
|
109
111
|
|
|
110
112
|
[tool.ruff]
|
|
111
113
|
line-length = 100
|
|
@@ -119,10 +119,7 @@ class BaseFileReader(ABC):
|
|
|
119
119
|
"""
|
|
120
120
|
if entity_name == Iterator[dict[str, Any]]:
|
|
121
121
|
return self.read_to_py_iterator(
|
|
122
|
-
resource,
|
|
123
|
-
entity_name,
|
|
124
|
-
schema, # type: ignore
|
|
125
|
-
all_model_fields
|
|
122
|
+
resource, entity_name, schema, all_model_fields # type: ignore
|
|
126
123
|
)
|
|
127
124
|
|
|
128
125
|
self.raise_if_not_sensible_file(resource, entity_name)
|
|
@@ -133,11 +130,7 @@ class BaseFileReader(ABC):
|
|
|
133
130
|
raise ReaderLacksEntityTypeSupport(entity_type=entity_type) from err
|
|
134
131
|
|
|
135
132
|
return reader_func(
|
|
136
|
-
self,
|
|
137
|
-
resource,
|
|
138
|
-
entity_name,
|
|
139
|
-
schema,
|
|
140
|
-
all_model_fields=all_model_fields # type: ignore
|
|
133
|
+
self, resource, entity_name, schema, all_model_fields=all_model_fields # type: ignore
|
|
141
134
|
)
|
|
142
135
|
|
|
143
136
|
def add_record_index(self, entity: EntityType, **kwargs) -> EntityType:
|
|
@@ -681,3 +681,13 @@ class BaseStepImplementations(Generic[EntityType], ABC): # pylint: disable=too-
|
|
|
681
681
|
def write_parquet(self, entity: EntityType, target_location: URI, **kwargs) -> URI:
|
|
682
682
|
"""Method to write parquet files"""
|
|
683
683
|
raise NotImplementedError()
|
|
684
|
+
|
|
685
|
+
def filter_data_contract_record_rejections(
|
|
686
|
+
self,
|
|
687
|
+
working_directory: URI,
|
|
688
|
+
entity: EntityType,
|
|
689
|
+
entity_name: EntityName,
|
|
690
|
+
**kwargs,
|
|
691
|
+
):
|
|
692
|
+
"""Method to filter out record rejection errors from the data contract for a given entity"""
|
|
693
|
+
raise NotImplementedError()
|
|
@@ -37,10 +37,7 @@ class UnableToParseCSVError(MessageBearingError):
|
|
|
37
37
|
"""An error raised when unable to parse a CSV file"""
|
|
38
38
|
|
|
39
39
|
def __init__(
|
|
40
|
-
self,
|
|
41
|
-
entity_name: str,
|
|
42
|
-
field_check_error_message: str,
|
|
43
|
-
field_check_error_code: str
|
|
40
|
+
self, entity_name: str, field_check_error_message: str, field_check_error_code: str
|
|
44
41
|
):
|
|
45
42
|
super().__init__(
|
|
46
43
|
messages=[
|
|
@@ -119,10 +119,8 @@ class DDBAuditor(BaseAuditor[DuckDBPyRelation]):
|
|
|
119
119
|
schema=self.polars_schema,
|
|
120
120
|
)
|
|
121
121
|
|
|
122
|
-
self._connection.sql(
|
|
123
|
-
|
|
124
|
-
SELECT {', '.join(self.polars_schema)} from data_pl_df"""
|
|
125
|
-
)
|
|
122
|
+
self._connection.sql(f"""INSERT INTO {self._name} ({', '.join(self.polars_schema)})
|
|
123
|
+
SELECT {', '.join(self.polars_schema)} from data_pl_df""")
|
|
126
124
|
|
|
127
125
|
def retrieve_records(
|
|
128
126
|
self,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# ignore: type[attr-defined]
|
|
3
3
|
|
|
4
4
|
"""Helper objects for duckdb data contract implementation"""
|
|
5
|
+
|
|
5
6
|
from collections.abc import Generator, Iterator
|
|
6
7
|
from dataclasses import is_dataclass
|
|
7
8
|
from datetime import date, datetime, time
|
|
@@ -18,9 +19,11 @@ from pandas import DataFrame
|
|
|
18
19
|
from pydantic import BaseModel
|
|
19
20
|
from typing_extensions import Annotated, get_args, get_origin, get_type_hints
|
|
20
21
|
|
|
22
|
+
from dve.common.error_utils import get_feedback_errors_uri
|
|
21
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
|
|
22
25
|
from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
|
|
23
|
-
from dve.core_engine.type_hints import URI
|
|
26
|
+
from dve.core_engine.type_hints import URI, EntityName
|
|
24
27
|
from dve.parser.file_handling.service import LocalFilesystemImplementation, _get_implementation
|
|
25
28
|
|
|
26
29
|
|
|
@@ -100,7 +103,7 @@ def table_exists(connection: DuckDBPyConnection, table_name: str) -> bool:
|
|
|
100
103
|
|
|
101
104
|
def relation_is_empty(relation: DuckDBPyRelation) -> bool:
|
|
102
105
|
"""Check if a duckdb relation is empty"""
|
|
103
|
-
if relation.limit(1).
|
|
106
|
+
if relation.limit(1).shape[0] > 0:
|
|
104
107
|
return False
|
|
105
108
|
return True
|
|
106
109
|
|
|
@@ -256,6 +259,48 @@ def duckdb_write_parquet(cls):
|
|
|
256
259
|
return cls
|
|
257
260
|
|
|
258
261
|
|
|
262
|
+
def _ddb_filter_contract_errors(
|
|
263
|
+
self,
|
|
264
|
+
working_directory: URI,
|
|
265
|
+
entity: DuckDBPyRelation,
|
|
266
|
+
entity_name: EntityName,
|
|
267
|
+
) -> DuckDBPyRelation:
|
|
268
|
+
contract_error_location = get_feedback_errors_uri(working_directory, "data_contract")
|
|
269
|
+
if not Path(contract_error_location).exists():
|
|
270
|
+
return entity
|
|
271
|
+
relevant_record_rejection_codes_rel = (
|
|
272
|
+
self._connection.read_json(
|
|
273
|
+
contract_error_location,
|
|
274
|
+
columns={
|
|
275
|
+
"RecordIndex": "INTEGER",
|
|
276
|
+
"FailureType": "STRING",
|
|
277
|
+
"Status": "STRING",
|
|
278
|
+
"Entity": "STRING",
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
.filter(
|
|
282
|
+
f"FailureType == 'record' AND Status != 'informational' AND Entity = '{entity_name}'"
|
|
283
|
+
) # pylint: disable=C0301
|
|
284
|
+
.select("RecordIndex")
|
|
285
|
+
.distinct()
|
|
286
|
+
.order("RecordIndex asc")
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if relation_is_empty(relevant_record_rejection_codes_rel):
|
|
290
|
+
return entity
|
|
291
|
+
|
|
292
|
+
filtered_entity = entity.join(
|
|
293
|
+
relevant_record_rejection_codes_rel, condition="__record_index__ == RecordIndex", how="anti"
|
|
294
|
+
)
|
|
295
|
+
return filtered_entity
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def ddb_filter_contract_errors(cls):
|
|
299
|
+
"""Class decorator to filter out records that failed casting and have record rejection scope"""
|
|
300
|
+
cls.filter_data_contract_record_rejections = _ddb_filter_contract_errors
|
|
301
|
+
return cls
|
|
302
|
+
|
|
303
|
+
|
|
259
304
|
@staticmethod # type: ignore
|
|
260
305
|
def _duckdb_get_entity_count(entity: DuckDBPyRelation) -> int:
|
|
261
306
|
"""Method to obtain entity count from a persisted parquet entity"""
|
|
@@ -317,7 +362,7 @@ def duckdb_record_index(cls):
|
|
|
317
362
|
|
|
318
363
|
def _cast_as_ddb_type(field_expr: str, type_annotation: Any) -> str:
|
|
319
364
|
"""Cast to Duck DB type"""
|
|
320
|
-
return f"""
|
|
365
|
+
return f"""TRY_CAST({field_expr} as {get_duckdb_type_from_annotation(type_annotation)})"""
|
|
321
366
|
|
|
322
367
|
|
|
323
368
|
def _ddb_safely_quote_name(field_name: str) -> str:
|
|
@@ -334,9 +379,6 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
334
379
|
element_name: str,
|
|
335
380
|
type_annotation: Any,
|
|
336
381
|
parent_element: bool = True,
|
|
337
|
-
date_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
|
|
338
|
-
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
|
|
339
|
-
time_regex: str = r"^[0-9]{2}:[0-9]{2}:[0-9]{2}$",
|
|
340
382
|
) -> str:
|
|
341
383
|
"""Generate casting statements for duckdb relations from type annotations"""
|
|
342
384
|
type_origin = get_origin(type_annotation)
|
|
@@ -347,19 +389,23 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
347
389
|
if type_origin is Union:
|
|
348
390
|
python_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
349
391
|
return get_duckdb_cast_statement_from_annotation(
|
|
350
|
-
element_name,
|
|
392
|
+
element_name,
|
|
393
|
+
python_type,
|
|
394
|
+
parent_element,
|
|
351
395
|
)
|
|
352
396
|
|
|
353
397
|
# Type hint is e.g. `List[str]`, check to ensure non-heterogenity.
|
|
354
398
|
if type_origin is list or (isinstance(type_origin, type) and issubclass(type_origin, list)):
|
|
355
399
|
element_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
356
|
-
stmt = f"
|
|
400
|
+
stmt = f"LIST_TRANSFORM({quoted_name}, x -> {get_duckdb_cast_statement_from_annotation('x',element_type, False,)})" # pylint: disable=C0301
|
|
357
401
|
return stmt if not parent_element else _cast_as_ddb_type(stmt, type_annotation)
|
|
358
402
|
|
|
359
403
|
if type_origin is Annotated:
|
|
360
404
|
python_type, *other_args = get_args(type_annotation) # pylint: disable=unused-variable
|
|
361
405
|
return get_duckdb_cast_statement_from_annotation(
|
|
362
|
-
element_name,
|
|
406
|
+
element_name,
|
|
407
|
+
python_type,
|
|
408
|
+
parent_element,
|
|
363
409
|
) # add other expected params here
|
|
364
410
|
# Ensure that we have a concrete type at this point.
|
|
365
411
|
if not isinstance(type_annotation, type):
|
|
@@ -384,7 +430,9 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
384
430
|
continue
|
|
385
431
|
|
|
386
432
|
fields[field_name] = get_duckdb_cast_statement_from_annotation(
|
|
387
|
-
f"{element_name}.{field_name}",
|
|
433
|
+
f"{element_name}.{field_name}",
|
|
434
|
+
field_annotation,
|
|
435
|
+
False,
|
|
388
436
|
)
|
|
389
437
|
|
|
390
438
|
if not fields:
|
|
@@ -392,7 +440,7 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
392
440
|
f"No type annotations in dict/dataclass type (got {type_annotation!r})"
|
|
393
441
|
)
|
|
394
442
|
cast_exprs = ",".join([f'"{nme}":= {stmt}' for nme, stmt in fields.items()])
|
|
395
|
-
stmt = f"
|
|
443
|
+
stmt = f"STRUCT_PACK({cast_exprs})"
|
|
396
444
|
return stmt if not parent_element else _cast_as_ddb_type(stmt, type_annotation)
|
|
397
445
|
|
|
398
446
|
if type_annotation is list:
|
|
@@ -403,18 +451,23 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
403
451
|
raise ValueError(f"dict must be `typing.TypedDict` subclass, got {type_annotation!r}")
|
|
404
452
|
|
|
405
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
|
+
|
|
406
459
|
# datetime is subclass of date, so needs to be handled first
|
|
407
460
|
if issubclass(type_, datetime):
|
|
408
|
-
stmt = rf"
|
|
461
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as TIMESTAMP)"
|
|
409
462
|
return stmt
|
|
410
463
|
if issubclass(type_, date):
|
|
411
|
-
stmt = rf"
|
|
464
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as DATE)"
|
|
412
465
|
return stmt
|
|
413
466
|
if issubclass(type_, time):
|
|
414
|
-
stmt = rf"
|
|
467
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as TIME)"
|
|
415
468
|
return stmt
|
|
416
469
|
duck_type = get_duckdb_type_from_annotation(type_)
|
|
417
470
|
if duck_type:
|
|
418
|
-
stmt = f"
|
|
471
|
+
stmt = f"TRIM({quoted_name})"
|
|
419
472
|
return _cast_as_ddb_type(stmt, type_) if parent_element else stmt
|
|
420
473
|
raise ValueError(f"No equivalent DuckDB type for {type_annotation!r}")
|
|
@@ -71,7 +71,7 @@ class DuckDBCSVReader(CSVFileReader):
|
|
|
71
71
|
quote_char=quotechar,
|
|
72
72
|
field_check=field_check,
|
|
73
73
|
field_check_error_code=field_check_error_code,
|
|
74
|
-
field_check_error_message=field_check_error_message
|
|
74
|
+
field_check_error_message=field_check_error_message,
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
def read_to_py_iterator(
|
|
@@ -254,7 +254,7 @@ class DuckDBCSVRepeatingHeaderReader(PolarsToDuckDBCSVReader):
|
|
|
254
254
|
resource=resource,
|
|
255
255
|
entity_name=entity_name,
|
|
256
256
|
schema=schema,
|
|
257
|
-
all_model_fields=all_model_fields
|
|
257
|
+
all_model_fields=all_model_fields,
|
|
258
258
|
)
|
|
259
259
|
entity = entity.select(StarExpression(exclude=[RECORD_INDEX_COLUMN_NAME])).distinct()
|
|
260
260
|
no_records = entity.shape[0]
|
|
@@ -43,4 +43,4 @@ class DuckDBRefDataLoader(BaseRefDataLoader[DuckDBPyRelation]):
|
|
|
43
43
|
@mark_refdata_file_extension("arrow")
|
|
44
44
|
def load_arrow_file(self, uri: str) -> DuckDBPyRelation:
|
|
45
45
|
"""Load an arrow ipc file into a duckdb relation"""
|
|
46
|
-
return self.connection.from_arrow(ipc.open_stream(uri).read_all()) # type:ignore
|
|
46
|
+
return self.connection.from_arrow(ipc.open_stream(uri).read_all()) # type: ignore
|
|
@@ -22,6 +22,7 @@ from dve.core_engine.backends.base.rules import (
|
|
|
22
22
|
from dve.core_engine.backends.exceptions import ConstraintError
|
|
23
23
|
from dve.core_engine.backends.implementations.duckdb.duckdb_helpers import (
|
|
24
24
|
DDBStruct,
|
|
25
|
+
ddb_filter_contract_errors,
|
|
25
26
|
duckdb_read_parquet,
|
|
26
27
|
duckdb_record_index,
|
|
27
28
|
duckdb_rel_to_dictionaries,
|
|
@@ -61,6 +62,7 @@ from dve.core_engine.type_hints import Messages
|
|
|
61
62
|
@duckdb_record_index
|
|
62
63
|
@duckdb_write_parquet
|
|
63
64
|
@duckdb_read_parquet
|
|
65
|
+
@ddb_filter_contract_errors
|
|
64
66
|
class DuckDBStepImplementations(BaseStepImplementations[DuckDBPyRelation]):
|
|
65
67
|
"""An implementation of transformation steps in duckdb."""
|
|
66
68
|
|
|
@@ -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
|
|
@@ -14,6 +14,7 @@ from dve.core_engine.backends.implementations.spark.spark_helpers import (
|
|
|
14
14
|
create_udf,
|
|
15
15
|
get_all_registered_udfs,
|
|
16
16
|
object_to_spark_literal,
|
|
17
|
+
spark_filter_contract_errors,
|
|
17
18
|
spark_read_parquet,
|
|
18
19
|
spark_record_index,
|
|
19
20
|
spark_write_parquet,
|
|
@@ -53,6 +54,7 @@ from dve.core_engine.type_hints import Messages
|
|
|
53
54
|
@spark_record_index
|
|
54
55
|
@spark_write_parquet
|
|
55
56
|
@spark_read_parquet
|
|
57
|
+
@spark_filter_contract_errors
|
|
56
58
|
class SparkStepImplementations(BaseStepImplementations[DataFrame]):
|
|
57
59
|
"""An implementation of transformation steps in Apache Spark."""
|
|
58
60
|
|
|
@@ -12,6 +12,7 @@ from collections.abc import Callable, Generator, Iterator
|
|
|
12
12
|
from dataclasses import dataclass, is_dataclass
|
|
13
13
|
from decimal import Decimal
|
|
14
14
|
from functools import wraps
|
|
15
|
+
from pathlib import Path
|
|
15
16
|
from typing import Any, ClassVar, Optional, TypeVar, Union, overload
|
|
16
17
|
|
|
17
18
|
from delta.exceptions import ConcurrentAppendException, DeltaConcurrentModificationException
|
|
@@ -25,9 +26,11 @@ from pyspark.sql.functions import lit, udf
|
|
|
25
26
|
from pyspark.sql.types import LongType, StructField, StructType
|
|
26
27
|
from typing_extensions import Annotated, Protocol, TypedDict, get_args, get_origin, get_type_hints
|
|
27
28
|
|
|
29
|
+
from dve.common.error_utils import get_feedback_errors_uri
|
|
28
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
|
|
29
32
|
from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
|
|
30
|
-
from dve.core_engine.type_hints import URI
|
|
33
|
+
from dve.core_engine.type_hints import URI, EntityName
|
|
31
34
|
|
|
32
35
|
# It would be really nice if there was a more parameterisable
|
|
33
36
|
# way of doing this.
|
|
@@ -97,6 +100,21 @@ PYTHON_TYPE_TO_SPARK_TYPE: dict[type, st.DataType] = {
|
|
|
97
100
|
}
|
|
98
101
|
"""A mapping of Python types to the equivalent Spark types."""
|
|
99
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
|
+
|
|
100
118
|
|
|
101
119
|
PydanticModel = TypeVar("PydanticModel", bound=BaseModel)
|
|
102
120
|
"""An Pydantic model."""
|
|
@@ -270,6 +288,13 @@ def create_udf(function: Callable) -> Callable:
|
|
|
270
288
|
return udf(function, returnType=return_type)
|
|
271
289
|
|
|
272
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
|
+
|
|
273
298
|
SupportedBaseType = Union[str, int, bool, float, Decimal, dt.date, dt.datetime]
|
|
274
299
|
"""Supported base types for Spark literals."""
|
|
275
300
|
SparkLiteralType = Union[ # type: ignore
|
|
@@ -365,6 +390,53 @@ def spark_write_parquet(cls):
|
|
|
365
390
|
return cls
|
|
366
391
|
|
|
367
392
|
|
|
393
|
+
def _spark_filter_contract_errors(
|
|
394
|
+
self,
|
|
395
|
+
working_directory: URI,
|
|
396
|
+
entity: DataFrame,
|
|
397
|
+
entity_name: EntityName,
|
|
398
|
+
) -> DataFrame:
|
|
399
|
+
contract_error_location = get_feedback_errors_uri(working_directory, "data_contract")
|
|
400
|
+
if not Path(contract_error_location).exists():
|
|
401
|
+
return entity
|
|
402
|
+
|
|
403
|
+
relevant_record_rejections_codes_df = (
|
|
404
|
+
self.spark_session.read.json(
|
|
405
|
+
path=contract_error_location,
|
|
406
|
+
schema=st.StructType(
|
|
407
|
+
[
|
|
408
|
+
st.StructField("RecordIndex", st.IntegerType()),
|
|
409
|
+
st.StructField("FailureType", st.StringType()),
|
|
410
|
+
st.StructField("Status", st.StringType()),
|
|
411
|
+
st.StructField("Entity", st.StringType()),
|
|
412
|
+
]
|
|
413
|
+
),
|
|
414
|
+
)
|
|
415
|
+
.filter(
|
|
416
|
+
(sf.col("FailureType") == sf.lit("record"))
|
|
417
|
+
& (sf.col("Status") != sf.lit("informational"))
|
|
418
|
+
& (sf.col("Entity") == sf.lit(entity_name))
|
|
419
|
+
)
|
|
420
|
+
.distinct()
|
|
421
|
+
.orderBy(sf.asc(sf.col("RecordIndex")))
|
|
422
|
+
# todo - ^^ possibly relook at join strat. Does this help? Over prescriptive?
|
|
423
|
+
)
|
|
424
|
+
if df_is_empty(relevant_record_rejections_codes_df):
|
|
425
|
+
return entity
|
|
426
|
+
filtered_entity = entity.join(
|
|
427
|
+
relevant_record_rejections_codes_df,
|
|
428
|
+
on=entity.__record_index__ == relevant_record_rejections_codes_df.RecordIndex,
|
|
429
|
+
how="anti",
|
|
430
|
+
)
|
|
431
|
+
return filtered_entity
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def spark_filter_contract_errors(cls):
|
|
435
|
+
"""Class decorator to filter out records that failed casting and have record rejection scope"""
|
|
436
|
+
cls.filter_data_contract_record_rejections = _spark_filter_contract_errors
|
|
437
|
+
return cls
|
|
438
|
+
|
|
439
|
+
|
|
368
440
|
@staticmethod # type: ignore
|
|
369
441
|
def _spark_get_entity_count(entity: DataFrame) -> int:
|
|
370
442
|
"""Method to obtain entity count from a persisted parquet entity"""
|
|
@@ -457,11 +529,7 @@ def _spark_safely_quote_name(field_name: str) -> str:
|
|
|
457
529
|
|
|
458
530
|
# pylint: disable=R0801
|
|
459
531
|
def get_spark_cast_statement_from_annotation(
|
|
460
|
-
element_name: str,
|
|
461
|
-
type_annotation: Any,
|
|
462
|
-
parent_element: bool = True,
|
|
463
|
-
date_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
|
|
464
|
-
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
|
|
465
533
|
):
|
|
466
534
|
"""Generate casting statements for spark dataframes based on type annotations"""
|
|
467
535
|
type_origin = get_origin(type_annotation)
|
|
@@ -471,20 +539,18 @@ def get_spark_cast_statement_from_annotation(
|
|
|
471
539
|
# An `Optional` or `Union` type, check to ensure non-heterogenity.
|
|
472
540
|
if type_origin is Union:
|
|
473
541
|
python_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
474
|
-
return get_spark_cast_statement_from_annotation(
|
|
475
|
-
element_name, python_type, parent_element, date_regex, timestamp_regex
|
|
476
|
-
)
|
|
542
|
+
return get_spark_cast_statement_from_annotation(element_name, python_type, parent_element)
|
|
477
543
|
|
|
478
544
|
# Type hint is e.g. `List[str]`, check to ensure non-heterogenity.
|
|
479
545
|
if type_origin is list or (isinstance(type_origin, type) and issubclass(type_origin, list)):
|
|
480
546
|
element_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
481
|
-
stmt = f"
|
|
547
|
+
stmt = f"TRANSFORM({quoted_name}, x -> {get_spark_cast_statement_from_annotation('x',element_type, False)})" # pylint: disable=C0301
|
|
482
548
|
return stmt if not parent_element else _cast_as_spark_type(stmt, type_annotation)
|
|
483
549
|
|
|
484
550
|
if type_origin is Annotated:
|
|
485
551
|
python_type, *_ = get_args(type_annotation) # pylint: disable=unused-variable
|
|
486
552
|
return get_spark_cast_statement_from_annotation(
|
|
487
|
-
element_name, python_type, parent_element
|
|
553
|
+
element_name, python_type, parent_element
|
|
488
554
|
) # add other expected params here
|
|
489
555
|
# Ensure that we have a concrete type at this point.
|
|
490
556
|
if not isinstance(type_annotation, type):
|
|
@@ -509,7 +575,7 @@ def get_spark_cast_statement_from_annotation(
|
|
|
509
575
|
continue
|
|
510
576
|
|
|
511
577
|
fields[field_name] = get_spark_cast_statement_from_annotation(
|
|
512
|
-
f"{element_name}.{field_name}", field_annotation, False
|
|
578
|
+
f"{element_name}.{field_name}", field_annotation, False
|
|
513
579
|
)
|
|
514
580
|
|
|
515
581
|
if not fields:
|
|
@@ -517,7 +583,7 @@ def get_spark_cast_statement_from_annotation(
|
|
|
517
583
|
f"No type annotations in dict/dataclass type (got {type_annotation!r})"
|
|
518
584
|
)
|
|
519
585
|
cast_exprs = ",".join([f"{stmt} AS `{nme}`" for nme, stmt in fields.items()])
|
|
520
|
-
stmt = f"
|
|
586
|
+
stmt = f"STRUCT({cast_exprs})"
|
|
521
587
|
return stmt if not parent_element else _cast_as_spark_type(stmt, type_annotation)
|
|
522
588
|
if type_annotation is list:
|
|
523
589
|
raise ValueError(
|
|
@@ -527,15 +593,29 @@ def get_spark_cast_statement_from_annotation(
|
|
|
527
593
|
raise ValueError(f"dict must be `typing.TypedDict` subclass, got {type_annotation!r}")
|
|
528
594
|
|
|
529
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
|
|
530
604
|
# datetime is subclass of date, so needs to be handled first
|
|
531
605
|
if issubclass(type_, dt.datetime):
|
|
532
|
-
|
|
533
|
-
|
|
606
|
+
return (
|
|
607
|
+
_cast_as_spark_type(dt_cast_statement, type_)
|
|
608
|
+
if parent_element
|
|
609
|
+
else dt_cast_statement
|
|
610
|
+
)
|
|
534
611
|
if issubclass(type_, dt.date):
|
|
535
|
-
|
|
536
|
-
|
|
612
|
+
return (
|
|
613
|
+
_cast_as_spark_type(dt_cast_statement, type_)
|
|
614
|
+
if parent_element
|
|
615
|
+
else dt_cast_statement
|
|
616
|
+
)
|
|
537
617
|
spark_type = get_type_from_annotation(type_)
|
|
538
618
|
if spark_type:
|
|
539
|
-
stmt = f"
|
|
619
|
+
stmt = f"TRIM({quoted_name})"
|
|
540
620
|
return _cast_as_spark_type(stmt, type_) if parent_element else stmt
|
|
541
621
|
raise ValueError(f"No equivalent Spark type for {type_annotation!r}")
|
|
@@ -44,7 +44,7 @@ class DataContractMetadata(BaseModel, frozen=True, arbitrary_types_allowed=True)
|
|
|
44
44
|
"""The per-entity schemas, as pydantic models."""
|
|
45
45
|
if not self._schemas:
|
|
46
46
|
for entity_name, validator in self.validators.items():
|
|
47
|
-
self._schemas[entity_name] = validator.model
|
|
47
|
+
self._schemas[entity_name] = validator.model # type: ignore # pylint: disable=E1137
|
|
48
48
|
return self._schemas.copy() # pylint: disable=E1101
|
|
49
49
|
|
|
50
50
|
@root_validator(allow_reuse=True)
|
|
@@ -16,11 +16,11 @@ from dve.core_engine.backends.exceptions import (
|
|
|
16
16
|
MissingHeaderError,
|
|
17
17
|
)
|
|
18
18
|
from dve.core_engine.backends.readers.utilities import (
|
|
19
|
-
|
|
19
|
+
get_all_model_fields,
|
|
20
|
+
raise_message_bearing_error_on_header_differences,
|
|
20
21
|
)
|
|
21
22
|
from dve.core_engine.backends.utilities import get_polars_type_from_annotation, stringify_model
|
|
22
23
|
from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
|
|
23
|
-
from dve.core_engine.backends.readers.utilities import get_all_model_fields
|
|
24
24
|
from dve.core_engine.type_hints import EntityName
|
|
25
25
|
from dve.parser.file_handling import get_content_length, open_stream
|
|
26
26
|
from dve.parser.file_handling.implementations.file import file_uri_to_local_path
|