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.
Files changed (106) hide show
  1. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/PKG-INFO +1 -1
  2. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/pyproject.toml +3 -2
  3. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/auditing.py +1 -0
  4. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/reader.py +2 -9
  5. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/rules.py +10 -0
  6. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/exceptions.py +1 -4
  7. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/auditing.py +2 -4
  8. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py +46 -2
  9. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/readers/csv.py +2 -2
  10. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/reference_data.py +1 -1
  11. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/rules.py +2 -0
  12. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/rules.py +2 -0
  13. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/spark_helpers.py +50 -1
  14. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/metadata/contract.py +1 -1
  15. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/readers/csv.py +2 -2
  16. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/readers/utilities.py +7 -7
  17. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/v1/__init__.py +3 -1
  18. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/engine.py +12 -4
  19. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/message.py +5 -15
  20. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/models.py +2 -0
  21. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/__init__.py +1 -1
  22. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/pipeline.py +39 -25
  23. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/utils.py +2 -0
  24. data_validation_engine-0.8.2/src/dve/reporting/constants.py +34 -0
  25. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/reporting/error_report.py +2 -1
  26. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/reporting/excel_report.py +32 -9
  27. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/LICENSE +0 -0
  28. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/README.md +0 -0
  29. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/__init__.py +0 -0
  30. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/common/__init__.py +0 -0
  31. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/common/error_utils.py +0 -0
  32. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/__init__.py +0 -0
  33. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/__init__.py +0 -0
  34. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/__init__.py +0 -0
  35. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/backend.py +0 -0
  36. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/contract.py +0 -0
  37. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/core.py +0 -0
  38. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/reference_data.py +0 -0
  39. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/base/utilities.py +0 -0
  40. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/__init__.py +0 -0
  41. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/__init__.py +0 -0
  42. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/contract.py +0 -0
  43. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/readers/__init__.py +0 -0
  44. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/readers/json.py +0 -0
  45. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/readers/xml.py +0 -0
  46. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/types.py +0 -0
  47. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/duckdb/utilities.py +0 -0
  48. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/__init__.py +0 -0
  49. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/auditing.py +0 -0
  50. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/backend.py +0 -0
  51. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/contract.py +0 -0
  52. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/readers/__init__.py +0 -0
  53. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/readers/csv.py +1 -1
  54. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/readers/json.py +0 -0
  55. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/readers/xml.py +0 -0
  56. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/reference_data.py +0 -0
  57. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/types.py +0 -0
  58. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/implementations/spark/utilities.py +0 -0
  59. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/metadata/__init__.py +0 -0
  60. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/metadata/reporting.py +0 -0
  61. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/metadata/rules.py +0 -0
  62. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/readers/__init__.py +0 -0
  63. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/readers/xml.py +0 -0
  64. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/readers/xml_linting.py +0 -0
  65. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/types.py +0 -0
  66. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/backends/utilities.py +0 -0
  67. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/__init__.py +0 -0
  68. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/base.py +0 -0
  69. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/v1/filters.py +0 -0
  70. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/v1/rule_stores/__init__.py +0 -0
  71. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/v1/rule_stores/models.py +0 -0
  72. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/configuration/v1/steps.py +0 -0
  73. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/constants.py +0 -0
  74. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/exceptions.py +0 -0
  75. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/functions/__init__.py +0 -0
  76. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/functions/implementations.py +0 -0
  77. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/loggers.py +0 -0
  78. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/templating.py +0 -0
  79. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/type_hints.py +0 -0
  80. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/core_engine/validation.py +0 -0
  81. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/domain_types.py +0 -0
  82. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/exc.py +0 -0
  83. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/function_library.py +0 -0
  84. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/function_wrapper.py +0 -0
  85. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/model_generator.py +0 -0
  86. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/models.py +0 -0
  87. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/metadata_parser/utilities.py +0 -0
  88. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/__init__.py +0 -0
  89. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/exceptions.py +0 -0
  90. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/__init__.py +0 -0
  91. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/helpers.py +0 -0
  92. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/implementations/__init__.py +0 -0
  93. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/implementations/base.py +0 -0
  94. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/implementations/dbfs.py +0 -0
  95. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/implementations/file.py +0 -0
  96. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/implementations/s3.py +0 -0
  97. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/log_handler.py +0 -0
  98. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/service.py +0 -0
  99. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/file_handling/utilities.py +0 -0
  100. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/type_hints.py +0 -0
  101. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/parser/utilities.py +0 -0
  102. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/__init__.py +0 -0
  103. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/duckdb_pipeline.py +0 -0
  104. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/foundry_ddb_pipeline.py +0 -0
  105. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/src/dve/pipeline/spark_pipeline.py +0 -0
  106. {data_validation_engine-0.8.1 → data_validation_engine-0.8.2}/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.1
3
+ Version: 0.8.2
4
4
  Summary: `nhs data validation engine` is a framework used to validate data
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -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.1"
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 = "24.3.0"
82
+ black = "26.3.1"
82
83
  astroid = "3.3.9"
83
84
  isort = "5.13.2"
84
85
  pylint = "3.3.9"
@@ -1,5 +1,6 @@
1
1
  # pylint: disable=C0209
2
2
  """Base auditing objects and managers for use of DVE services"""
3
+
3
4
  import multiprocessing
4
5
  import operator
5
6
  import threading
@@ -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
- f"""INSERT INTO {self._name} ({', '.join(self.polars_schema)})
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).count("*"):
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 # type: ignore # pylint: disable=E1137
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
- raise_message_bearing_error_on_header_differences
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 = f"missing fields: {', '.join(sorted(missing))};" if missing else "" # pylint: disable=C0301
56
- record_details_additional = f"additional fields: {', '.join(sorted(additional))};" if additional else "" # pylint: disable=C0301
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(self.transformations.complex_rules): # pylint: disable=E1101
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(f"Exiting pipeline context, clearing {self.cache_prefix!r}") # pylint: disable=E1101
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(f"Writing entities to the output location: {self.output_prefix_uri}") # pylint: disable=E1101
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(f"{output_uri} already exists - will be overwritten") # pylint: disable=E1101
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(f"+ Writing parquet output to {output_uri!r}") # pylint: disable=E1101
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(
@@ -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=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]
@@ -1,2 +1,2 @@
1
1
  """Module for parsing out the DVE config and generating models,
2
- includes domain types for validation"""
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
- entity,
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("error")) # type: ignore
762
- .then(pl.lit("Submission Failure")) # type: ignore
763
- .otherwise(pl.lit("Warning")) # type: ignore
764
- .alias("error_type")
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
- number_record_rejections=err_types.get("Submission Failure", 0),
827
- number_warnings=err_types.get("Warning", 0),
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=["Submission Failure", "Warning"],
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"
@@ -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("Submission Failure")) # type: ignore
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
  )
@@ -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 "There was an issue processing the submission. Please contact support."
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 "File has been accepted, no issues to report"
97
+ return ErrorReportStatus.ACCEPTED
99
98
  failures = aggregates["Type"].unique()
100
- if "Submission Failure" in failures:
101
- status = "File has been rejected"
102
- elif "Warning" in failures:
103
- status = "File has been accepted, all records accepted with warnings"
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 = "File has been accepted, no issues to report"
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
 
@@ -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