data-validation-engine 0.8.1__tar.gz → 0.8.2__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.2}/PKG-INFO +1 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/pyproject.toml +3 -2
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/auditing.py +1 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/reader.py +2 -9
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/rules.py +10 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/exceptions.py +1 -4
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/auditing.py +2 -4
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py +46 -2
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/readers/csv.py +2 -2
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/reference_data.py +1 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/rules.py +2 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/rules.py +2 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/spark_helpers.py +50 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/metadata/contract.py +1 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/readers/csv.py +2 -2
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/readers/utilities.py +7 -7
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/v1/__init__.py +3 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/engine.py +12 -4
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/message.py +5 -15
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/models.py +2 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/__init__.py +1 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/pipeline.py +39 -25
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/utils.py +2 -0
- data_validation_engine-0.8.2/src/dve/reporting/constants.py +34 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/reporting/error_report.py +2 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/reporting/excel_report.py +32 -9
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/LICENSE +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/README.md +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/common/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/common/error_utils.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/backend.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/contract.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/core.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/reference_data.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/contract.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/readers/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/readers/json.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/readers/xml.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/types.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/auditing.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/backend.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/contract.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/readers/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/readers/csv.py +1 -1
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/readers/json.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/readers/xml.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/reference_data.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/types.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/metadata/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/metadata/reporting.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/metadata/rules.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/readers/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/readers/xml.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/readers/xml_linting.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/types.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/base.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/v1/filters.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/v1/rule_stores/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/v1/rule_stores/models.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/v1/steps.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/constants.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/exceptions.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/functions/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/functions/implementations.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/loggers.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/templating.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/type_hints.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/validation.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/domain_types.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/exc.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/function_library.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/function_wrapper.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/model_generator.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/models.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/exceptions.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/helpers.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/implementations/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/implementations/base.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/implementations/dbfs.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/implementations/file.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/implementations/s3.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/log_handler.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/service.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/type_hints.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/utilities.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/__init__.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/duckdb_pipeline.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/foundry_ddb_pipeline.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/spark_pipeline.py +0 -0
- {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/reporting/__init__.py +0 -0
|
@@ -24,7 +24,7 @@ Issues = "https://github.com/NHSDigital/data-validation-engine/issues"
|
|
|
24
24
|
Changelog = "https://github.com/NHSDigital/data-validation-engine/blob/main/CHANGELOG.md"
|
|
25
25
|
|
|
26
26
|
[tool.poetry]
|
|
27
|
-
version = "0.8.
|
|
27
|
+
version = "0.8.2"
|
|
28
28
|
packages = [
|
|
29
29
|
{ include = "dve", from = "src" },
|
|
30
30
|
]
|
|
@@ -68,6 +68,7 @@ optional = true
|
|
|
68
68
|
behave = "1.3.3"
|
|
69
69
|
coverage = "7.11.0"
|
|
70
70
|
moto = {extras = ["s3"], version = "4.2.14"}
|
|
71
|
+
cryptography = "48.0.1" # dependency of `moto`
|
|
71
72
|
requests = "2.33.0" # dependency of `moto`
|
|
72
73
|
Werkzeug = "3.1.6"
|
|
73
74
|
pytest = "9.0.3"
|
|
@@ -78,7 +79,7 @@ xlsx2csv = "0.8.4" # polars requirement
|
|
|
78
79
|
optional = true
|
|
79
80
|
|
|
80
81
|
[tool.poetry.group.lint.dependencies]
|
|
81
|
-
black = "
|
|
82
|
+
black = "26.3.1"
|
|
82
83
|
astroid = "3.3.9"
|
|
83
84
|
isort = "5.13.2"
|
|
84
85
|
pylint = "3.3.9"
|
|
@@ -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,10 @@ 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
|
|
22
24
|
from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
|
|
23
|
-
from dve.core_engine.type_hints import URI
|
|
25
|
+
from dve.core_engine.type_hints import URI, EntityName
|
|
24
26
|
from dve.parser.file_handling.service import LocalFilesystemImplementation, _get_implementation
|
|
25
27
|
|
|
26
28
|
|
|
@@ -100,7 +102,7 @@ def table_exists(connection: DuckDBPyConnection, table_name: str) -> bool:
|
|
|
100
102
|
|
|
101
103
|
def relation_is_empty(relation: DuckDBPyRelation) -> bool:
|
|
102
104
|
"""Check if a duckdb relation is empty"""
|
|
103
|
-
if relation.limit(1).
|
|
105
|
+
if relation.limit(1).shape[0] > 0:
|
|
104
106
|
return False
|
|
105
107
|
return True
|
|
106
108
|
|
|
@@ -256,6 +258,48 @@ def duckdb_write_parquet(cls):
|
|
|
256
258
|
return cls
|
|
257
259
|
|
|
258
260
|
|
|
261
|
+
def _ddb_filter_contract_errors(
|
|
262
|
+
self,
|
|
263
|
+
working_directory: URI,
|
|
264
|
+
entity: DuckDBPyRelation,
|
|
265
|
+
entity_name: EntityName,
|
|
266
|
+
) -> DuckDBPyRelation:
|
|
267
|
+
contract_error_location = get_feedback_errors_uri(working_directory, "data_contract")
|
|
268
|
+
if not Path(contract_error_location).exists():
|
|
269
|
+
return entity
|
|
270
|
+
relevant_record_rejection_codes_rel = (
|
|
271
|
+
self._connection.read_json(
|
|
272
|
+
contract_error_location,
|
|
273
|
+
columns={
|
|
274
|
+
"RecordIndex": "INTEGER",
|
|
275
|
+
"FailureType": "STRING",
|
|
276
|
+
"Status": "STRING",
|
|
277
|
+
"Entity": "STRING",
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
.filter(
|
|
281
|
+
f"FailureType == 'record' AND Status != 'informational' AND Entity = '{entity_name}'"
|
|
282
|
+
) # pylint: disable=C0301
|
|
283
|
+
.select("RecordIndex")
|
|
284
|
+
.distinct()
|
|
285
|
+
.order("RecordIndex asc")
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if relation_is_empty(relevant_record_rejection_codes_rel):
|
|
289
|
+
return entity
|
|
290
|
+
|
|
291
|
+
filtered_entity = entity.join(
|
|
292
|
+
relevant_record_rejection_codes_rel, condition="__record_index__ == RecordIndex", how="anti"
|
|
293
|
+
)
|
|
294
|
+
return filtered_entity
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def ddb_filter_contract_errors(cls):
|
|
298
|
+
"""Class decorator to filter out records that failed casting and have record rejection scope"""
|
|
299
|
+
cls.filter_data_contract_record_rejections = _ddb_filter_contract_errors
|
|
300
|
+
return cls
|
|
301
|
+
|
|
302
|
+
|
|
259
303
|
@staticmethod # type: ignore
|
|
260
304
|
def _duckdb_get_entity_count(entity: DuckDBPyRelation) -> int:
|
|
261
305
|
"""Method to obtain entity count from a persisted parquet entity"""
|
|
@@ -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
|
|
|
@@ -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,10 @@ 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
|
|
29
31
|
from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
|
|
30
|
-
from dve.core_engine.type_hints import URI
|
|
32
|
+
from dve.core_engine.type_hints import URI, EntityName
|
|
31
33
|
|
|
32
34
|
# It would be really nice if there was a more parameterisable
|
|
33
35
|
# way of doing this.
|
|
@@ -365,6 +367,53 @@ def spark_write_parquet(cls):
|
|
|
365
367
|
return cls
|
|
366
368
|
|
|
367
369
|
|
|
370
|
+
def _spark_filter_contract_errors(
|
|
371
|
+
self,
|
|
372
|
+
working_directory: URI,
|
|
373
|
+
entity: DataFrame,
|
|
374
|
+
entity_name: EntityName,
|
|
375
|
+
) -> DataFrame:
|
|
376
|
+
contract_error_location = get_feedback_errors_uri(working_directory, "data_contract")
|
|
377
|
+
if not Path(contract_error_location).exists():
|
|
378
|
+
return entity
|
|
379
|
+
|
|
380
|
+
relevant_record_rejections_codes_df = (
|
|
381
|
+
self.spark_session.read.json(
|
|
382
|
+
path=contract_error_location,
|
|
383
|
+
schema=st.StructType(
|
|
384
|
+
[
|
|
385
|
+
st.StructField("RecordIndex", st.IntegerType()),
|
|
386
|
+
st.StructField("FailureType", st.StringType()),
|
|
387
|
+
st.StructField("Status", st.StringType()),
|
|
388
|
+
st.StructField("Entity", st.StringType()),
|
|
389
|
+
]
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
.filter(
|
|
393
|
+
(sf.col("FailureType") == sf.lit("record"))
|
|
394
|
+
& (sf.col("Status") != sf.lit("informational"))
|
|
395
|
+
& (sf.col("Entity") == sf.lit(entity_name))
|
|
396
|
+
)
|
|
397
|
+
.distinct()
|
|
398
|
+
.orderBy(sf.asc(sf.col("RecordIndex")))
|
|
399
|
+
# todo - ^^ possibly relook at join strat. Does this help? Over prescriptive?
|
|
400
|
+
)
|
|
401
|
+
if df_is_empty(relevant_record_rejections_codes_df):
|
|
402
|
+
return entity
|
|
403
|
+
filtered_entity = entity.join(
|
|
404
|
+
relevant_record_rejections_codes_df,
|
|
405
|
+
on=entity.__record_index__ == relevant_record_rejections_codes_df.RecordIndex,
|
|
406
|
+
how="anti",
|
|
407
|
+
)
|
|
408
|
+
return filtered_entity
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def spark_filter_contract_errors(cls):
|
|
412
|
+
"""Class decorator to filter out records that failed casting and have record rejection scope"""
|
|
413
|
+
cls.filter_data_contract_record_rejections = _spark_filter_contract_errors
|
|
414
|
+
return cls
|
|
415
|
+
|
|
416
|
+
|
|
368
417
|
@staticmethod # type: ignore
|
|
369
418
|
def _spark_get_entity_count(entity: DataFrame) -> int:
|
|
370
419
|
"""Method to obtain entity count from a persisted parquet entity"""
|
|
@@ -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
|
|
@@ -44,16 +44,16 @@ def raise_message_bearing_error_on_header_differences(
|
|
|
44
44
|
header or vice versa.
|
|
45
45
|
"""
|
|
46
46
|
missing, additional = check_csv_header_expected(
|
|
47
|
-
resource,
|
|
48
|
-
expected_schema,
|
|
49
|
-
all_model_fields,
|
|
50
|
-
delimiter,
|
|
51
|
-
quote_char
|
|
47
|
+
resource, expected_schema, all_model_fields, delimiter, quote_char
|
|
52
48
|
)
|
|
53
49
|
|
|
54
50
|
if missing or additional:
|
|
55
|
-
record_details_missing =
|
|
56
|
-
|
|
51
|
+
record_details_missing = (
|
|
52
|
+
f"missing fields: {', '.join(sorted(missing))};" if missing else ""
|
|
53
|
+
) # pylint: disable=C0301
|
|
54
|
+
record_details_additional = (
|
|
55
|
+
f"additional fields: {', '.join(sorted(additional))};" if additional else ""
|
|
56
|
+
) # pylint: disable=C0301
|
|
57
57
|
raise MessageBearingError(
|
|
58
58
|
"The CSV header doesn't match what is expected",
|
|
59
59
|
messages=[
|
|
@@ -281,7 +281,9 @@ class V1EngineConfig(BaseEngineConfig):
|
|
|
281
281
|
rules, local_variable_list = [], []
|
|
282
282
|
added_rules: set[RuleName] = set()
|
|
283
283
|
|
|
284
|
-
for index, complex_rule_config in enumerate(
|
|
284
|
+
for index, complex_rule_config in enumerate(
|
|
285
|
+
self.transformations.complex_rules
|
|
286
|
+
): # pylint: disable=E1101
|
|
285
287
|
rule, local_params, deps = self._resolve_business_rule(complex_rule_config)
|
|
286
288
|
missing_rules = deps - added_rules
|
|
287
289
|
if missing_rules:
|
|
@@ -170,7 +170,9 @@ class CoreEngine(BaseModel):
|
|
|
170
170
|
exc_value: Optional[Exception],
|
|
171
171
|
traceback: Optional[TracebackType],
|
|
172
172
|
) -> None:
|
|
173
|
-
self.main_log.info(
|
|
173
|
+
self.main_log.info(
|
|
174
|
+
f"Exiting pipeline context, clearing {self.cache_prefix!r}"
|
|
175
|
+
) # pylint: disable=E1101
|
|
174
176
|
cache_dir = self._cache_dir
|
|
175
177
|
self._cache_dir = None
|
|
176
178
|
|
|
@@ -198,7 +200,9 @@ class CoreEngine(BaseModel):
|
|
|
198
200
|
"""
|
|
199
201
|
output_entities = {}
|
|
200
202
|
|
|
201
|
-
self.main_log.info(
|
|
203
|
+
self.main_log.info(
|
|
204
|
+
f"Writing entities to the output location: {self.output_prefix_uri}"
|
|
205
|
+
) # pylint: disable=E1101
|
|
202
206
|
for entity_name, entity in entities.items():
|
|
203
207
|
entity = entity.drop(RECORD_INDEX_COLUMN_NAME)
|
|
204
208
|
|
|
@@ -206,9 +210,13 @@ class CoreEngine(BaseModel):
|
|
|
206
210
|
|
|
207
211
|
output_uri = joinuri(self.output_prefix_uri, entity_name)
|
|
208
212
|
if get_resource_exists(output_uri):
|
|
209
|
-
self.main_log.info(
|
|
213
|
+
self.main_log.info(
|
|
214
|
+
f"{output_uri} already exists - will be overwritten"
|
|
215
|
+
) # pylint: disable=E1101
|
|
210
216
|
|
|
211
|
-
self.main_log.info(
|
|
217
|
+
self.main_log.info(
|
|
218
|
+
f"+ Writing parquet output to {output_uri!r}"
|
|
219
|
+
) # pylint: disable=E1101
|
|
212
220
|
entity.write.mode("overwrite").parquet(output_uri)
|
|
213
221
|
spark_session = SparkSession.builder.getOrCreate()
|
|
214
222
|
output_entities[entity_name] = spark_session.read.format("parquet").load(
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/message.py
RENAMED
|
@@ -36,6 +36,8 @@ class DataContractErrorDetail(BaseModel):
|
|
|
36
36
|
"""Define custom error codes for validation issues raised during the data contract phase"""
|
|
37
37
|
|
|
38
38
|
error_code: str
|
|
39
|
+
error_level: Optional[FailureType] = "record"
|
|
40
|
+
is_informational: Optional[bool] = False
|
|
39
41
|
error_message: Optional[str] = None
|
|
40
42
|
reporting_entity: Optional[str] = None
|
|
41
43
|
|
|
@@ -247,26 +249,14 @@ class FeedbackMessage: # pylint: disable=too-many-instance-attributes
|
|
|
247
249
|
messages: Messages = []
|
|
248
250
|
for error_dict in error.errors():
|
|
249
251
|
error_type = error_dict["type"]
|
|
252
|
+
# TODO - review in pydantic v2 - how handles null vs not provided values
|
|
250
253
|
if "none.not_allowed" in error_type or "value_error.missing" in error_type:
|
|
251
254
|
category = "Blank"
|
|
252
255
|
else:
|
|
253
256
|
category = "Bad value"
|
|
254
|
-
error_code = error_type
|
|
255
|
-
if "." in error_code:
|
|
256
|
-
error_code = error_code.split(".", 1)[-1]
|
|
257
|
-
|
|
258
|
-
if error_code in INTEGRITY_ERROR_CODES:
|
|
259
|
-
failure_type: FailureType = "integrity"
|
|
260
|
-
elif error_code in SUBMISSION_ERROR_CODES:
|
|
261
|
-
failure_type = "submission"
|
|
262
|
-
else:
|
|
263
|
-
failure_type = "record"
|
|
264
257
|
|
|
265
258
|
error_field = ".".join([idx for idx in error_dict["loc"] if not isinstance(idx, int)])
|
|
266
259
|
|
|
267
|
-
is_informational = False
|
|
268
|
-
if error_code.endswith("warning"):
|
|
269
|
-
is_informational = True
|
|
270
260
|
error_detail: DataContractErrorDetail = error_details.get( # type: ignore
|
|
271
261
|
error_field, DEFAULT_ERROR_DETAIL
|
|
272
262
|
).get(category)
|
|
@@ -276,8 +266,8 @@ class FeedbackMessage: # pylint: disable=too-many-instance-attributes
|
|
|
276
266
|
entity=error_detail.reporting_entity or entity,
|
|
277
267
|
original_entity=entity,
|
|
278
268
|
record=record,
|
|
279
|
-
failure_type=
|
|
280
|
-
is_informational=is_informational,
|
|
269
|
+
failure_type=error_detail.error_level, # type: ignore
|
|
270
|
+
is_informational=error_detail.is_informational, # type: ignore
|
|
281
271
|
error_type=error_type,
|
|
282
272
|
error_location=error_dict["loc"], # type: ignore
|
|
283
273
|
error_message=error_detail.template_message(record, error_dict["loc"]),
|
|
@@ -105,6 +105,8 @@ class SubmissionStatisticsRecord(AuditRecord):
|
|
|
105
105
|
|
|
106
106
|
record_count: Optional[int]
|
|
107
107
|
"""Count of records in the submitted file"""
|
|
108
|
+
number_submission_rejections: Optional[int]
|
|
109
|
+
"""Number of submission rejections raised following validation"""
|
|
108
110
|
number_record_rejections: Optional[int]
|
|
109
111
|
"""Number of record rejections raised following validation"""
|
|
110
112
|
number_warnings: Optional[int]
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/__init__.py
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Module for parsing out the DVE config and generating models,
|
|
2
|
-
|
|
2
|
+
includes domain types for validation"""
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# pylint: disable=protected-access,too-many-instance-attributes,too-many-arguments,line-too-long
|
|
2
2
|
"""Generic Pipeline object to define how DVE should be interacted with."""
|
|
3
|
+
|
|
3
4
|
import json
|
|
4
5
|
import logging
|
|
5
6
|
import re
|
|
@@ -42,6 +43,7 @@ from dve.parser import file_handling as fh
|
|
|
42
43
|
from dve.parser.file_handling.implementations.file import LocalFilesystemImplementation
|
|
43
44
|
from dve.parser.file_handling.service import _get_implementation
|
|
44
45
|
from dve.pipeline.utils import SubmissionStatus, deadletter_file, load_config, load_reader
|
|
46
|
+
from dve.reporting.constants import ErrorReportCategories
|
|
45
47
|
from dve.reporting.error_report import ERROR_SCHEMA, calculate_aggregates
|
|
46
48
|
|
|
47
49
|
PERMISSIBLE_EXCEPTIONS: tuple[type[Exception]] = (
|
|
@@ -223,7 +225,7 @@ class BaseDVEPipeline:
|
|
|
223
225
|
submission_file_uri,
|
|
224
226
|
model_name,
|
|
225
227
|
stringify_model(model), # type: ignore
|
|
226
|
-
get_all_model_fields(models.values()) # type: ignore
|
|
228
|
+
get_all_model_fields(models.values()), # type: ignore
|
|
227
229
|
),
|
|
228
230
|
f"{out}{model_name}",
|
|
229
231
|
)
|
|
@@ -379,9 +381,6 @@ class BaseDVEPipeline:
|
|
|
379
381
|
failed.append((submission_info, submission_status))
|
|
380
382
|
else:
|
|
381
383
|
success.append((submission_info, submission_status))
|
|
382
|
-
except AttributeError as exc:
|
|
383
|
-
self._logger.error(f"File transformation raised exception: {exc}")
|
|
384
|
-
raise exc
|
|
385
384
|
except PERMISSIBLE_EXCEPTIONS as exc:
|
|
386
385
|
self._logger.warning(
|
|
387
386
|
f"File transformation raised exception: {exc}. Will be retried later."
|
|
@@ -509,9 +508,6 @@ class BaseDVEPipeline:
|
|
|
509
508
|
submission_info: SubmissionInfo
|
|
510
509
|
submission_status: SubmissionStatus
|
|
511
510
|
submission_info, submission_status = future.result()
|
|
512
|
-
except AttributeError as exc:
|
|
513
|
-
self._logger.error(f"Data Contract raised exception: {exc}")
|
|
514
|
-
raise exc
|
|
515
511
|
except PERMISSIBLE_EXCEPTIONS as exc:
|
|
516
512
|
self._logger.warning(
|
|
517
513
|
f"Data Contract raised exception: {exc}. Will be retried later."
|
|
@@ -616,8 +612,19 @@ class BaseDVEPipeline:
|
|
|
616
612
|
submission_status.processing_failed = True
|
|
617
613
|
|
|
618
614
|
for entity_name, entity in entity_manager.entities.items():
|
|
615
|
+
# Note BI filtering done within the apply_rules
|
|
616
|
+
self._logger.info(f"applying data contract filter to {entity_name}.")
|
|
617
|
+
if not entity_name.startswith("Original"):
|
|
618
|
+
filtered_entity = self._step_implementations.filter_data_contract_record_rejections(
|
|
619
|
+
working_directory,
|
|
620
|
+
entity,
|
|
621
|
+
entity_name,
|
|
622
|
+
)
|
|
623
|
+
else:
|
|
624
|
+
self._logger.info(f"Skipping {entity_name}. Marked original.")
|
|
625
|
+
filtered_entity = entity
|
|
619
626
|
projected = self._step_implementations.write_parquet( # type: ignore
|
|
620
|
-
|
|
627
|
+
filtered_entity,
|
|
621
628
|
fh.joinuri(
|
|
622
629
|
self.processed_files_path,
|
|
623
630
|
submission_info.submission_id,
|
|
@@ -630,11 +637,17 @@ class BaseDVEPipeline:
|
|
|
630
637
|
)
|
|
631
638
|
|
|
632
639
|
submission_status.number_of_records = self.get_entity_count(
|
|
633
|
-
entity=entity_manager.entities[
|
|
634
|
-
f"""Original{rules.global_variables.get(
|
|
640
|
+
entity=entity_manager.entities[f"""Original{rules.global_variables.get(
|
|
635
641
|
'entity',
|
|
636
|
-
submission_info.dataset_id)}"""
|
|
637
|
-
|
|
642
|
+
submission_info.dataset_id)}"""]
|
|
643
|
+
)
|
|
644
|
+
submission_status.number_of_records_rejected = (
|
|
645
|
+
submission_status.number_of_records
|
|
646
|
+
- self.get_entity_count(
|
|
647
|
+
entity_manager.entities[
|
|
648
|
+
rules.global_variables.get("entity", submission_info.dataset_id)
|
|
649
|
+
]
|
|
650
|
+
)
|
|
638
651
|
)
|
|
639
652
|
|
|
640
653
|
return submission_info, submission_status
|
|
@@ -682,9 +695,6 @@ class BaseDVEPipeline:
|
|
|
682
695
|
unsucessful_files.append((submission_info, submission_status)) # type: ignore
|
|
683
696
|
else:
|
|
684
697
|
successful_files.append((submission_info, submission_status)) # type: ignore
|
|
685
|
-
except AttributeError as exc:
|
|
686
|
-
self._logger.error(f"Business Rules raised exception: {exc}")
|
|
687
|
-
raise exc
|
|
688
698
|
except PERMISSIBLE_EXCEPTIONS as exc:
|
|
689
699
|
self._logger.warning(
|
|
690
700
|
f"Business Rules raised exception: {exc}. Will be retried later."
|
|
@@ -758,10 +768,12 @@ class BaseDVEPipeline:
|
|
|
758
768
|
|
|
759
769
|
df = pl.DataFrame(errors, schema={key: pl.Utf8() for key in errors[0]}) # type: ignore
|
|
760
770
|
df = df.with_columns(
|
|
761
|
-
pl.when(pl.col("Status") == pl.lit("
|
|
762
|
-
.then(pl.lit("
|
|
763
|
-
.
|
|
764
|
-
.
|
|
771
|
+
pl.when(pl.col("Status") == pl.lit("informational"))
|
|
772
|
+
.then(pl.lit("Warning"))
|
|
773
|
+
.when(pl.col("FailureType") == pl.lit("submission")) # type: ignore
|
|
774
|
+
.then(pl.lit(ErrorReportCategories.FILE_REJECTION.reporting_name)) # type: ignore
|
|
775
|
+
.otherwise(pl.lit(ErrorReportCategories.RECORD_REJECTION.reporting_name)) # type: ignore
|
|
776
|
+
.alias("error_type") # type: ignore
|
|
765
777
|
)
|
|
766
778
|
df = df.select(
|
|
767
779
|
pl.col("Entity").alias("Table"), # type: ignore
|
|
@@ -823,8 +835,13 @@ class BaseDVEPipeline:
|
|
|
823
835
|
sub_stats = SubmissionStatisticsRecord(
|
|
824
836
|
submission_id=submission_info.submission_id,
|
|
825
837
|
record_count=submission_status.number_of_records,
|
|
826
|
-
|
|
827
|
-
|
|
838
|
+
number_submission_rejections=err_types.get(
|
|
839
|
+
ErrorReportCategories.FILE_REJECTION.reporting_name, 0
|
|
840
|
+
),
|
|
841
|
+
number_record_rejections=err_types.get(
|
|
842
|
+
ErrorReportCategories.RECORD_REJECTION.reporting_name, 0
|
|
843
|
+
),
|
|
844
|
+
number_warnings=err_types.get(ErrorReportCategories.WARNING.reporting_name, 0),
|
|
828
845
|
)
|
|
829
846
|
|
|
830
847
|
summary_dict = {
|
|
@@ -835,7 +852,7 @@ class BaseDVEPipeline:
|
|
|
835
852
|
summary_items = er.SummaryItems(
|
|
836
853
|
submission_status=submission_status,
|
|
837
854
|
summary_dict=summary_dict,
|
|
838
|
-
row_headings=[
|
|
855
|
+
row_headings=[e.reporting_name for e in ErrorReportCategories],
|
|
839
856
|
)
|
|
840
857
|
|
|
841
858
|
workbook = er.ExcelFormat(
|
|
@@ -894,9 +911,6 @@ class BaseDVEPipeline:
|
|
|
894
911
|
try:
|
|
895
912
|
submission_info, submission_status, submission_stats, feedback_uri = future.result()
|
|
896
913
|
reports.append((submission_info, submission_status, submission_stats, feedback_uri))
|
|
897
|
-
except AttributeError as exc:
|
|
898
|
-
self._logger.error(f"Error reports raised exception: {exc}")
|
|
899
|
-
raise exc
|
|
900
914
|
except PERMISSIBLE_EXCEPTIONS as exc:
|
|
901
915
|
self._logger.warning(
|
|
902
916
|
f"Error reports raised exception: {exc}. Will be retried later."
|
|
@@ -86,10 +86,12 @@ class SubmissionStatus:
|
|
|
86
86
|
self,
|
|
87
87
|
validation_failed: bool = False,
|
|
88
88
|
number_of_records: Optional[int] = None,
|
|
89
|
+
number_of_records_rejected: Optional[int] = None,
|
|
89
90
|
processing_failed: bool = False,
|
|
90
91
|
):
|
|
91
92
|
self.validation_failed = validation_failed
|
|
92
93
|
self.number_of_records = number_of_records
|
|
94
|
+
self.number_of_records_rejected = number_of_records_rejected
|
|
93
95
|
self.processing_failed = processing_failed
|
|
94
96
|
|
|
95
97
|
@property
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants used within the error reports
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ErrorReportCategories(Enum):
|
|
9
|
+
"""
|
|
10
|
+
Categories available within the error report.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
FILE_REJECTION = 1, "File Rejection"
|
|
14
|
+
RECORD_REJECTION = 2, "Record Rejection"
|
|
15
|
+
WARNING = 3, "Warning"
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def reporting_name(self):
|
|
19
|
+
"""
|
|
20
|
+
The error report 'friendly' name.
|
|
21
|
+
"""
|
|
22
|
+
return self.value[1]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ErrorReportStatus: # pylint: disable=R0903
|
|
26
|
+
"""
|
|
27
|
+
Statuses available within the error report.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
PROCESSING_FAILED = "There was an issue processing the submission. Please contact support."
|
|
31
|
+
FILE_REJECTION = "File has been rejected"
|
|
32
|
+
RECORD_REJECTION = "File has been accepted with record rejections"
|
|
33
|
+
ACCEPTED_WITH_WARNING = "File has been accepted, all records accepted with warnings"
|
|
34
|
+
ACCEPTED = "File has been accepted, no issues to report"
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/reporting/error_report.py
RENAMED
|
@@ -11,6 +11,7 @@ from polars import DataFrame, LazyFrame, Utf8, col # type: ignore
|
|
|
11
11
|
from dve.common.error_utils import conditional_cast
|
|
12
12
|
from dve.core_engine.message import FeedbackMessage
|
|
13
13
|
from dve.parser.file_handling.service import open_stream
|
|
14
|
+
from dve.reporting.constants import ErrorReportCategories
|
|
14
15
|
|
|
15
16
|
ERROR_SCHEMA = {
|
|
16
17
|
"Table": Utf8(),
|
|
@@ -85,7 +86,7 @@ def create_error_dataframe(errors: deque[FeedbackMessage], key_fields):
|
|
|
85
86
|
|
|
86
87
|
df = df.with_columns( # type: ignore
|
|
87
88
|
pl.when(pl.col("Status") == pl.lit("error")) # type: ignore
|
|
88
|
-
.then(pl.lit(
|
|
89
|
+
.then(pl.lit(ErrorReportCategories.FILE_REJECTION.reporting_name)) # type: ignore
|
|
89
90
|
.otherwise(pl.lit("Warning")) # type: ignore
|
|
90
91
|
.alias("error_type")
|
|
91
92
|
)
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/reporting/excel_report.py
RENAMED
|
@@ -17,6 +17,7 @@ from polars import DataFrame
|
|
|
17
17
|
from polars.exceptions import ColumnNotFoundError
|
|
18
18
|
|
|
19
19
|
from dve.pipeline.utils import SubmissionStatus
|
|
20
|
+
from dve.reporting.constants import ErrorReportCategories, ErrorReportStatus
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
@dataclass
|
|
@@ -91,18 +92,18 @@ class SummaryItems:
|
|
|
91
92
|
def get_submission_status(self, aggregates: DataFrame) -> str:
|
|
92
93
|
"""Returns the status of the submission based on the error data"""
|
|
93
94
|
if self.submission_status.processing_failed:
|
|
94
|
-
return
|
|
95
|
-
if self.submission_status.validation_failed:
|
|
96
|
-
return "File has been rejected"
|
|
95
|
+
return ErrorReportStatus.PROCESSING_FAILED
|
|
97
96
|
if aggregates.is_empty():
|
|
98
|
-
return
|
|
97
|
+
return ErrorReportStatus.ACCEPTED
|
|
99
98
|
failures = aggregates["Type"].unique()
|
|
100
|
-
if
|
|
101
|
-
status =
|
|
102
|
-
elif
|
|
103
|
-
status =
|
|
99
|
+
if ErrorReportCategories.FILE_REJECTION.reporting_name in failures:
|
|
100
|
+
status = ErrorReportStatus.FILE_REJECTION
|
|
101
|
+
elif ErrorReportCategories.RECORD_REJECTION.reporting_name in failures:
|
|
102
|
+
status = ErrorReportStatus.RECORD_REJECTION
|
|
103
|
+
elif ErrorReportCategories.WARNING.reporting_name in failures:
|
|
104
|
+
status = ErrorReportStatus.ACCEPTED_WITH_WARNING
|
|
104
105
|
else:
|
|
105
|
-
status =
|
|
106
|
+
status = ErrorReportStatus.ACCEPTED
|
|
106
107
|
return status
|
|
107
108
|
|
|
108
109
|
def _write_table(
|
|
@@ -141,6 +142,28 @@ class SummaryItems:
|
|
|
141
142
|
for key, value in self.summary_dict.items():
|
|
142
143
|
summary.append(["", _key_renames.get(key, key), str(value)])
|
|
143
144
|
|
|
145
|
+
summary.append(
|
|
146
|
+
[
|
|
147
|
+
"",
|
|
148
|
+
"Total Number of Records Processed",
|
|
149
|
+
(
|
|
150
|
+
self.submission_status.number_of_records
|
|
151
|
+
if self.submission_status.number_of_records
|
|
152
|
+
else 0
|
|
153
|
+
), # pylint: disable=C0301
|
|
154
|
+
]
|
|
155
|
+
)
|
|
156
|
+
if status not in (
|
|
157
|
+
ErrorReportStatus.PROCESSING_FAILED,
|
|
158
|
+
ErrorReportStatus.FILE_REJECTION,
|
|
159
|
+
):
|
|
160
|
+
summary.append(
|
|
161
|
+
[
|
|
162
|
+
"",
|
|
163
|
+
"Total Number of Records Rejected",
|
|
164
|
+
self.submission_status.number_of_records_rejected,
|
|
165
|
+
]
|
|
166
|
+
)
|
|
144
167
|
summary.append(["", ""])
|
|
145
168
|
|
|
146
169
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/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
|
|
@@ -9,13 +9,13 @@ from pyspark.sql import DataFrame, SparkSession
|
|
|
9
9
|
from pyspark.sql.types import StructType
|
|
10
10
|
|
|
11
11
|
from dve.core_engine.backends.base.reader import read_function
|
|
12
|
-
from dve.core_engine.backends.readers.csv import CSVFileReader
|
|
13
12
|
from dve.core_engine.backends.exceptions import EmptyFileError
|
|
14
13
|
from dve.core_engine.backends.implementations.spark.spark_helpers import (
|
|
15
14
|
get_type_from_annotation,
|
|
16
15
|
spark_record_index,
|
|
17
16
|
spark_write_parquet,
|
|
18
17
|
)
|
|
18
|
+
from dve.core_engine.backends.readers.csv import CSVFileReader
|
|
19
19
|
from dve.core_engine.type_hints import URI, EntityName
|
|
20
20
|
from dve.parser.file_handling import get_content_length
|
|
21
21
|
|
|
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.1 → data_validation_engine-0.8.2}/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.1 → data_validation_engine-0.8.2}/src/dve/core_engine/constants.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/loggers.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/templating.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/type_hints.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/exc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/models.py
RENAMED
|
File without changes
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/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.1 → data_validation_engine-0.8.2}/src/dve/pipeline/duckdb_pipeline.py
RENAMED
|
File without changes
|
|
File without changes
|
{data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/spark_pipeline.py
RENAMED
|
File without changes
|
|
File without changes
|