data-validation-engine 0.8.1__tar.gz → 0.8.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/PKG-INFO +3 -2
  2. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/pyproject.toml +6 -4
  3. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/auditing.py +1 -0
  4. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/reader.py +2 -9
  5. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/rules.py +10 -0
  6. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/exceptions.py +1 -4
  7. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/auditing.py +2 -4
  8. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py +68 -15
  9. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/csv.py +2 -2
  10. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/reference_data.py +1 -1
  11. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/rules.py +2 -0
  12. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/contract.py +18 -6
  13. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/rules.py +2 -0
  14. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/spark_helpers.py +98 -18
  15. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/contract.py +1 -1
  16. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/csv.py +2 -2
  17. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/utilities.py +7 -7
  18. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/utilities.py +43 -0
  19. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/__init__.py +3 -1
  20. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/engine.py +12 -4
  21. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/message.py +5 -15
  22. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/models.py +2 -0
  23. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/__init__.py +1 -1
  24. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/pipeline.py +39 -25
  25. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/utils.py +2 -0
  26. data_validation_engine-0.8.3/src/dve/reporting/constants.py +34 -0
  27. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/reporting/error_report.py +2 -1
  28. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/reporting/excel_report.py +32 -9
  29. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/LICENSE +0 -0
  30. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/README.md +0 -0
  31. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/__init__.py +0 -0
  32. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/common/__init__.py +0 -0
  33. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/common/error_utils.py +0 -0
  34. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/__init__.py +0 -0
  35. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/__init__.py +0 -0
  36. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/__init__.py +0 -0
  37. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/backend.py +0 -0
  38. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/contract.py +0 -0
  39. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/core.py +0 -0
  40. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/reference_data.py +0 -0
  41. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/utilities.py +0 -0
  42. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/__init__.py +0 -0
  43. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/__init__.py +0 -0
  44. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/contract.py +0 -0
  45. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/__init__.py +0 -0
  46. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/json.py +0 -0
  47. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/xml.py +0 -0
  48. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/types.py +0 -0
  49. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/utilities.py +0 -0
  50. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/__init__.py +0 -0
  51. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/auditing.py +0 -0
  52. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/backend.py +0 -0
  53. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/__init__.py +0 -0
  54. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/csv.py +1 -1
  55. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/json.py +0 -0
  56. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/xml.py +0 -0
  57. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/reference_data.py +0 -0
  58. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/types.py +0 -0
  59. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/utilities.py +0 -0
  60. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/__init__.py +0 -0
  61. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/reporting.py +0 -0
  62. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/rules.py +0 -0
  63. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/__init__.py +0 -0
  64. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/xml.py +0 -0
  65. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/xml_linting.py +0 -0
  66. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/types.py +0 -0
  67. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/__init__.py +0 -0
  68. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/base.py +0 -0
  69. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/filters.py +0 -0
  70. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/rule_stores/__init__.py +0 -0
  71. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/rule_stores/models.py +0 -0
  72. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/steps.py +0 -0
  73. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/constants.py +0 -0
  74. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/exceptions.py +0 -0
  75. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/functions/__init__.py +0 -0
  76. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/functions/implementations.py +0 -0
  77. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/loggers.py +0 -0
  78. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/templating.py +0 -0
  79. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/type_hints.py +0 -0
  80. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/core_engine/validation.py +0 -0
  81. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/domain_types.py +0 -0
  82. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/exc.py +0 -0
  83. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/function_library.py +0 -0
  84. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/function_wrapper.py +0 -0
  85. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/model_generator.py +0 -0
  86. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/models.py +0 -0
  87. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/metadata_parser/utilities.py +0 -0
  88. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/__init__.py +0 -0
  89. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/exceptions.py +0 -0
  90. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/__init__.py +0 -0
  91. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/helpers.py +0 -0
  92. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/__init__.py +0 -0
  93. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/base.py +0 -0
  94. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/dbfs.py +0 -0
  95. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/file.py +0 -0
  96. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/s3.py +0 -0
  97. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/log_handler.py +0 -0
  98. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/service.py +0 -0
  99. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/utilities.py +0 -0
  100. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/type_hints.py +0 -0
  101. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/parser/utilities.py +0 -0
  102. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/__init__.py +0 -0
  103. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/duckdb_pipeline.py +0 -0
  104. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/foundry_ddb_pipeline.py +0 -0
  105. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/pipeline/spark_pipeline.py +0 -0
  106. {data_validation_engine-0.8.1 → data_validation_engine-0.8.3}/src/dve/reporting/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-validation-engine
3
- Version: 0.8.1
3
+ Version: 0.8.3
4
4
  Summary: `nhs data validation engine` is a framework used to validate data
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -10,6 +10,7 @@ Requires-Python: >=3.10,<3.13
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
13
14
  Classifier: Operating System :: OS Independent
14
15
  Classifier: Topic :: Software Development :: Libraries
15
16
  Classifier: Typing :: Typed
@@ -25,7 +26,7 @@ Requires-Dist: pandas (==2.3.3)
25
26
  Requires-Dist: polars (==0.20.31)
26
27
  Requires-Dist: pyarrow (==23.0.1)
27
28
  Requires-Dist: pydantic (==1.10.19)
28
- Requires-Dist: pyspark (>=3.0.0,<=3.5.2)
29
+ Requires-Dist: pyspark (>=3.5.0,<=3.5.5)
29
30
  Requires-Dist: typing_extensions (==4.15.0)
30
31
  Requires-Dist: urllib3 (==2.7.0)
31
32
  Project-URL: Changelog, https://github.com/NHSDigital/data-validation-engine/blob/main/CHANGELOG.md
@@ -10,6 +10,7 @@ classifiers = [
10
10
  "Programming Language :: Python :: 3",
11
11
  "Programming Language :: Python :: 3.10",
12
12
  "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
13
14
  "Operating System :: OS Independent",
14
15
  "Topic :: Software Development :: Libraries",
15
16
  "Typing :: Typed",
@@ -24,7 +25,7 @@ Issues = "https://github.com/NHSDigital/data-validation-engine/issues"
24
25
  Changelog = "https://github.com/NHSDigital/data-validation-engine/blob/main/CHANGELOG.md"
25
26
 
26
27
  [tool.poetry]
27
- version = "0.8.1"
28
+ version = "0.8.3"
28
29
  packages = [
29
30
  { include = "dve", from = "src" },
30
31
  ]
@@ -43,7 +44,7 @@ pandas = "2.3.3"
43
44
  polars = "0.20.31"
44
45
  pyarrow = "23.0.1"
45
46
  pydantic = "1.10.19"
46
- pyspark = ">=3.0.0,<=3.5.2"
47
+ pyspark = ">=3.5.0,<=3.5.5"
47
48
  typing_extensions = "4.15.0"
48
49
  urllib3 = "2.7.0" # dependency of boto3 & botocore
49
50
 
@@ -68,6 +69,7 @@ optional = true
68
69
  behave = "1.3.3"
69
70
  coverage = "7.11.0"
70
71
  moto = {extras = ["s3"], version = "4.2.14"}
72
+ cryptography = "48.0.1" # dependency of `moto`
71
73
  requests = "2.33.0" # dependency of `moto`
72
74
  Werkzeug = "3.1.6"
73
75
  pytest = "9.0.3"
@@ -78,7 +80,7 @@ xlsx2csv = "0.8.4" # polars requirement
78
80
  optional = true
79
81
 
80
82
  [tool.poetry.group.lint.dependencies]
81
- black = "24.3.0"
83
+ black = "26.3.1"
82
84
  astroid = "3.3.9"
83
85
  isort = "5.13.2"
84
86
  pylint = "3.3.9"
@@ -105,7 +107,7 @@ mkdocs = "1.6.1"
105
107
  mkdocstrings = { version = "1.0.3", extras = ["python"] }
106
108
  griffelib = "2.0.1"
107
109
  pymdown-extensions = "10.21.3"
108
- zensical = "0.0.31"
110
+ zensical = "0.0.46"
109
111
 
110
112
  [tool.ruff]
111
113
  line-length = 100
@@ -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,11 @@ from pandas import DataFrame
18
19
  from pydantic import BaseModel
19
20
  from typing_extensions import Annotated, get_args, get_origin, get_type_hints
20
21
 
22
+ from dve.common.error_utils import get_feedback_errors_uri
21
23
  from dve.core_engine.backends.base.utilities import _get_non_heterogenous_type
24
+ from dve.core_engine.backends.utilities import DEFAULT_ISO_FORMATS, datetime_format_to_regex
22
25
  from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
23
- from dve.core_engine.type_hints import URI
26
+ from dve.core_engine.type_hints import URI, EntityName
24
27
  from dve.parser.file_handling.service import LocalFilesystemImplementation, _get_implementation
25
28
 
26
29
 
@@ -100,7 +103,7 @@ def table_exists(connection: DuckDBPyConnection, table_name: str) -> bool:
100
103
 
101
104
  def relation_is_empty(relation: DuckDBPyRelation) -> bool:
102
105
  """Check if a duckdb relation is empty"""
103
- if relation.limit(1).count("*"):
106
+ if relation.limit(1).shape[0] > 0:
104
107
  return False
105
108
  return True
106
109
 
@@ -256,6 +259,48 @@ def duckdb_write_parquet(cls):
256
259
  return cls
257
260
 
258
261
 
262
+ def _ddb_filter_contract_errors(
263
+ self,
264
+ working_directory: URI,
265
+ entity: DuckDBPyRelation,
266
+ entity_name: EntityName,
267
+ ) -> DuckDBPyRelation:
268
+ contract_error_location = get_feedback_errors_uri(working_directory, "data_contract")
269
+ if not Path(contract_error_location).exists():
270
+ return entity
271
+ relevant_record_rejection_codes_rel = (
272
+ self._connection.read_json(
273
+ contract_error_location,
274
+ columns={
275
+ "RecordIndex": "INTEGER",
276
+ "FailureType": "STRING",
277
+ "Status": "STRING",
278
+ "Entity": "STRING",
279
+ },
280
+ )
281
+ .filter(
282
+ f"FailureType == 'record' AND Status != 'informational' AND Entity = '{entity_name}'"
283
+ ) # pylint: disable=C0301
284
+ .select("RecordIndex")
285
+ .distinct()
286
+ .order("RecordIndex asc")
287
+ )
288
+
289
+ if relation_is_empty(relevant_record_rejection_codes_rel):
290
+ return entity
291
+
292
+ filtered_entity = entity.join(
293
+ relevant_record_rejection_codes_rel, condition="__record_index__ == RecordIndex", how="anti"
294
+ )
295
+ return filtered_entity
296
+
297
+
298
+ def ddb_filter_contract_errors(cls):
299
+ """Class decorator to filter out records that failed casting and have record rejection scope"""
300
+ cls.filter_data_contract_record_rejections = _ddb_filter_contract_errors
301
+ return cls
302
+
303
+
259
304
  @staticmethod # type: ignore
260
305
  def _duckdb_get_entity_count(entity: DuckDBPyRelation) -> int:
261
306
  """Method to obtain entity count from a persisted parquet entity"""
@@ -317,7 +362,7 @@ def duckdb_record_index(cls):
317
362
 
318
363
  def _cast_as_ddb_type(field_expr: str, type_annotation: Any) -> str:
319
364
  """Cast to Duck DB type"""
320
- return f"""try_cast({field_expr} as {get_duckdb_type_from_annotation(type_annotation)})"""
365
+ return f"""TRY_CAST({field_expr} as {get_duckdb_type_from_annotation(type_annotation)})"""
321
366
 
322
367
 
323
368
  def _ddb_safely_quote_name(field_name: str) -> str:
@@ -334,9 +379,6 @@ def get_duckdb_cast_statement_from_annotation(
334
379
  element_name: str,
335
380
  type_annotation: Any,
336
381
  parent_element: bool = True,
337
- date_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
338
- timestamp_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}((\+|\-)[0-9]{2}:[0-9]{2})?$", # pylint: disable=C0301
339
- time_regex: str = r"^[0-9]{2}:[0-9]{2}:[0-9]{2}$",
340
382
  ) -> str:
341
383
  """Generate casting statements for duckdb relations from type annotations"""
342
384
  type_origin = get_origin(type_annotation)
@@ -347,19 +389,23 @@ def get_duckdb_cast_statement_from_annotation(
347
389
  if type_origin is Union:
348
390
  python_type = _get_non_heterogenous_type(get_args(type_annotation))
349
391
  return get_duckdb_cast_statement_from_annotation(
350
- element_name, python_type, parent_element, date_regex, timestamp_regex
392
+ element_name,
393
+ python_type,
394
+ parent_element,
351
395
  )
352
396
 
353
397
  # Type hint is e.g. `List[str]`, check to ensure non-heterogenity.
354
398
  if type_origin is list or (isinstance(type_origin, type) and issubclass(type_origin, list)):
355
399
  element_type = _get_non_heterogenous_type(get_args(type_annotation))
356
- stmt = f"list_transform({quoted_name}, x -> {get_duckdb_cast_statement_from_annotation('x',element_type, False, date_regex, timestamp_regex)})" # pylint: disable=C0301
400
+ stmt = f"LIST_TRANSFORM({quoted_name}, x -> {get_duckdb_cast_statement_from_annotation('x',element_type, False,)})" # pylint: disable=C0301
357
401
  return stmt if not parent_element else _cast_as_ddb_type(stmt, type_annotation)
358
402
 
359
403
  if type_origin is Annotated:
360
404
  python_type, *other_args = get_args(type_annotation) # pylint: disable=unused-variable
361
405
  return get_duckdb_cast_statement_from_annotation(
362
- element_name, python_type, parent_element, date_regex, timestamp_regex
406
+ element_name,
407
+ python_type,
408
+ parent_element,
363
409
  ) # add other expected params here
364
410
  # Ensure that we have a concrete type at this point.
365
411
  if not isinstance(type_annotation, type):
@@ -384,7 +430,9 @@ def get_duckdb_cast_statement_from_annotation(
384
430
  continue
385
431
 
386
432
  fields[field_name] = get_duckdb_cast_statement_from_annotation(
387
- f"{element_name}.{field_name}", field_annotation, False, date_regex, timestamp_regex
433
+ f"{element_name}.{field_name}",
434
+ field_annotation,
435
+ False,
388
436
  )
389
437
 
390
438
  if not fields:
@@ -392,7 +440,7 @@ def get_duckdb_cast_statement_from_annotation(
392
440
  f"No type annotations in dict/dataclass type (got {type_annotation!r})"
393
441
  )
394
442
  cast_exprs = ",".join([f'"{nme}":= {stmt}' for nme, stmt in fields.items()])
395
- stmt = f"struct_pack({cast_exprs})"
443
+ stmt = f"STRUCT_PACK({cast_exprs})"
396
444
  return stmt if not parent_element else _cast_as_ddb_type(stmt, type_annotation)
397
445
 
398
446
  if type_annotation is list:
@@ -403,18 +451,23 @@ def get_duckdb_cast_statement_from_annotation(
403
451
  raise ValueError(f"dict must be `typing.TypedDict` subclass, got {type_annotation!r}")
404
452
 
405
453
  for type_ in type_annotation.mro():
454
+ _date_format: str = getattr( # type: ignore
455
+ type_, "DATE_FORMAT", DEFAULT_ISO_FORMATS.get(type_, DEFAULT_ISO_FORMATS.get(datetime))
456
+ )
457
+ dt_cast_statement = rf"CASE WHEN REGEXP_FULL_MATCH(TRIM({quoted_name}), '{datetime_format_to_regex(_date_format)}') THEN TRY_STRPTIME(TRIM({quoted_name}), '{_date_format}') ELSE NULL END" # pylint: disable=C0301
458
+
406
459
  # datetime is subclass of date, so needs to be handled first
407
460
  if issubclass(type_, datetime):
408
- stmt = rf"CASE WHEN REGEXP_MATCHES(TRIM({quoted_name}), '{timestamp_regex}') THEN TRY_CAST(TRIM({quoted_name}) as TIMESTAMP) ELSE NULL END" # pylint: disable=C0301
461
+ stmt = rf"TRY_CAST({dt_cast_statement} as TIMESTAMP)"
409
462
  return stmt
410
463
  if issubclass(type_, date):
411
- stmt = rf"CASE WHEN REGEXP_MATCHES(TRIM({quoted_name}), '{date_regex}') THEN TRY_CAST(TRIM({quoted_name}) as DATE) ELSE NULL END" # pylint: disable=C0301
464
+ stmt = rf"TRY_CAST({dt_cast_statement} as DATE)"
412
465
  return stmt
413
466
  if issubclass(type_, time):
414
- stmt = rf"CASE WHEN REGEXP_MATCHES(TRIM({quoted_name}), '{time_regex}') THEN TRY_CAST(TRIM({quoted_name}) as TIME) ELSE NULL END" # pylint: disable=C0301
467
+ stmt = rf"TRY_CAST({dt_cast_statement} as TIME)"
415
468
  return stmt
416
469
  duck_type = get_duckdb_type_from_annotation(type_)
417
470
  if duck_type:
418
- stmt = f"trim({quoted_name})"
471
+ stmt = f"TRIM({quoted_name})"
419
472
  return _cast_as_ddb_type(stmt, type_) if parent_element else stmt
420
473
  raise ValueError(f"No equivalent DuckDB type for {type_annotation!r}")
@@ -71,7 +71,7 @@ class DuckDBCSVReader(CSVFileReader):
71
71
  quote_char=quotechar,
72
72
  field_check=field_check,
73
73
  field_check_error_code=field_check_error_code,
74
- field_check_error_message=field_check_error_message
74
+ field_check_error_message=field_check_error_message,
75
75
  )
76
76
 
77
77
  def read_to_py_iterator(
@@ -254,7 +254,7 @@ class DuckDBCSVRepeatingHeaderReader(PolarsToDuckDBCSVReader):
254
254
  resource=resource,
255
255
  entity_name=entity_name,
256
256
  schema=schema,
257
- all_model_fields=all_model_fields
257
+ all_model_fields=all_model_fields,
258
258
  )
259
259
  entity = entity.select(StarExpression(exclude=[RECORD_INDEX_COLUMN_NAME])).distinct()
260
260
  no_records = entity.shape[0]
@@ -43,4 +43,4 @@ class DuckDBRefDataLoader(BaseRefDataLoader[DuckDBPyRelation]):
43
43
  @mark_refdata_file_extension("arrow")
44
44
  def load_arrow_file(self, uri: str) -> DuckDBPyRelation:
45
45
  """Load an arrow ipc file into a duckdb relation"""
46
- return self.connection.from_arrow(ipc.open_stream(uri).read_all()) # type:ignore
46
+ return self.connection.from_arrow(ipc.open_stream(uri).read_all()) # type: ignore
@@ -22,6 +22,7 @@ from dve.core_engine.backends.base.rules import (
22
22
  from dve.core_engine.backends.exceptions import ConstraintError
23
23
  from dve.core_engine.backends.implementations.duckdb.duckdb_helpers import (
24
24
  DDBStruct,
25
+ ddb_filter_contract_errors,
25
26
  duckdb_read_parquet,
26
27
  duckdb_record_index,
27
28
  duckdb_rel_to_dictionaries,
@@ -61,6 +62,7 @@ from dve.core_engine.type_hints import Messages
61
62
  @duckdb_record_index
62
63
  @duckdb_write_parquet
63
64
  @duckdb_read_parquet
65
+ @ddb_filter_contract_errors
64
66
  class DuckDBStepImplementations(BaseStepImplementations[DuckDBPyRelation]):
65
67
  """An implementation of transformation steps in duckdb."""
66
68
 
@@ -7,6 +7,7 @@ from typing import Any, Optional
7
7
  from uuid import uuid4
8
8
 
9
9
  from pydantic import BaseModel
10
+ from pydantic.fields import ModelField
10
11
  from pyspark.sql import DataFrame, SparkSession
11
12
  from pyspark.sql import functions as sf
12
13
  from pyspark.sql.functions import col, lit
@@ -26,6 +27,7 @@ from dve.core_engine.backends.exceptions import (
26
27
  )
27
28
  from dve.core_engine.backends.implementations.spark.spark_helpers import (
28
29
  df_is_empty,
30
+ get_spark_cast_statement_from_annotation,
29
31
  get_type_from_annotation,
30
32
  spark_read_parquet,
31
33
  spark_record_index,
@@ -86,7 +88,7 @@ class SparkDataContract(BaseDataContract[DataFrame]):
86
88
  schema=get_type_from_annotation(schema),
87
89
  )
88
90
 
89
- # pylint: disable=R0915
91
+ # pylint: disable=R0914,R0915
90
92
  def apply_data_contract(
91
93
  self,
92
94
  working_dir: URI,
@@ -102,6 +104,7 @@ class SparkDataContract(BaseDataContract[DataFrame]):
102
104
 
103
105
  successful = True
104
106
  for entity_name, record_df in entities.items():
107
+ entity_fields: dict[str, ModelField] = contract_metadata.schemas[entity_name].__fields__
105
108
  spark_schema = get_type_from_annotation(contract_metadata.schemas[entity_name])
106
109
  spark_schema.add(StructField(RECORD_INDEX_COLUMN_NAME, LongType()))
107
110
  if df_is_empty(record_df):
@@ -144,12 +147,21 @@ class SparkDataContract(BaseDataContract[DataFrame]):
144
147
  self.logger.info(f"Data contract found {msg_count} issues in {entity_name}")
145
148
 
146
149
  try:
150
+ # TODO: will need to revisit in pydantic v2 bump as model field no longer available
147
151
  record_df = record_df.select(
148
- [
149
- col(column.name).cast(column.dataType)
150
- for column in spark_schema
151
- if column.name in record_df.columns
152
- ]
152
+ *[
153
+ (
154
+ get_spark_cast_statement_from_annotation(
155
+ fld, mdl_field.annotation
156
+ ).alias(fld)
157
+ if fld in record_df.columns
158
+ else lit(None).cast(
159
+ get_type_from_annotation(mdl_field.annotation).alias(fld)
160
+ )
161
+ )
162
+ for fld, mdl_field in entity_fields.items()
163
+ ],
164
+ col(RECORD_INDEX_COLUMN_NAME).cast(LongType()).alias(RECORD_INDEX_COLUMN_NAME),
153
165
  )
154
166
  except Exception as err: # pylint: disable=broad-except
155
167
  successful = False
@@ -14,6 +14,7 @@ from dve.core_engine.backends.implementations.spark.spark_helpers import (
14
14
  create_udf,
15
15
  get_all_registered_udfs,
16
16
  object_to_spark_literal,
17
+ spark_filter_contract_errors,
17
18
  spark_read_parquet,
18
19
  spark_record_index,
19
20
  spark_write_parquet,
@@ -53,6 +54,7 @@ from dve.core_engine.type_hints import Messages
53
54
  @spark_record_index
54
55
  @spark_write_parquet
55
56
  @spark_read_parquet
57
+ @spark_filter_contract_errors
56
58
  class SparkStepImplementations(BaseStepImplementations[DataFrame]):
57
59
  """An implementation of transformation steps in Apache Spark."""
58
60
 
@@ -12,6 +12,7 @@ from collections.abc import Callable, Generator, Iterator
12
12
  from dataclasses import dataclass, is_dataclass
13
13
  from decimal import Decimal
14
14
  from functools import wraps
15
+ from pathlib import Path
15
16
  from typing import Any, ClassVar, Optional, TypeVar, Union, overload
16
17
 
17
18
  from delta.exceptions import ConcurrentAppendException, DeltaConcurrentModificationException
@@ -25,9 +26,11 @@ from pyspark.sql.functions import lit, udf
25
26
  from pyspark.sql.types import LongType, StructField, StructType
26
27
  from typing_extensions import Annotated, Protocol, TypedDict, get_args, get_origin, get_type_hints
27
28
 
29
+ from dve.common.error_utils import get_feedback_errors_uri
28
30
  from dve.core_engine.backends.base.utilities import _get_non_heterogenous_type
31
+ from dve.core_engine.backends.utilities import DEFAULT_ISO_FORMATS, datetime_format_to_regex
29
32
  from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
30
- from dve.core_engine.type_hints import URI
33
+ from dve.core_engine.type_hints import URI, EntityName
31
34
 
32
35
  # It would be really nice if there was a more parameterisable
33
36
  # way of doing this.
@@ -97,6 +100,21 @@ PYTHON_TYPE_TO_SPARK_TYPE: dict[type, st.DataType] = {
97
100
  }
98
101
  """A mapping of Python types to the equivalent Spark types."""
99
102
 
103
+ PYTHON_TO_JAVA_DT_FORMAT_MAP: dict[str, str] = {
104
+ "%Y": "yyyy",
105
+ "%y": "yy",
106
+ "%m": "MM",
107
+ "%d": "dd",
108
+ "%H": "HH",
109
+ "%M": "mm",
110
+ "%S": "ss",
111
+ "%z": "XX",
112
+ "%Z": "z",
113
+ }
114
+ """A mapping of python to java datetime component formats. Not exhaustive but will hopefully support
115
+ all requirements.
116
+ """
117
+
100
118
 
101
119
  PydanticModel = TypeVar("PydanticModel", bound=BaseModel)
102
120
  """An Pydantic model."""
@@ -270,6 +288,13 @@ def create_udf(function: Callable) -> Callable:
270
288
  return udf(function, returnType=return_type)
271
289
 
272
290
 
291
+ def python_to_java_datetime_format(datetime_fmt: str) -> str:
292
+ "Helper to convert python datetime formats to Java datetime formats"
293
+ for pt, jt in PYTHON_TO_JAVA_DT_FORMAT_MAP.items():
294
+ datetime_fmt = datetime_fmt.replace(pt, jt)
295
+ return datetime_fmt.replace("T", "'T'")
296
+
297
+
273
298
  SupportedBaseType = Union[str, int, bool, float, Decimal, dt.date, dt.datetime]
274
299
  """Supported base types for Spark literals."""
275
300
  SparkLiteralType = Union[ # type: ignore
@@ -365,6 +390,53 @@ def spark_write_parquet(cls):
365
390
  return cls
366
391
 
367
392
 
393
+ def _spark_filter_contract_errors(
394
+ self,
395
+ working_directory: URI,
396
+ entity: DataFrame,
397
+ entity_name: EntityName,
398
+ ) -> DataFrame:
399
+ contract_error_location = get_feedback_errors_uri(working_directory, "data_contract")
400
+ if not Path(contract_error_location).exists():
401
+ return entity
402
+
403
+ relevant_record_rejections_codes_df = (
404
+ self.spark_session.read.json(
405
+ path=contract_error_location,
406
+ schema=st.StructType(
407
+ [
408
+ st.StructField("RecordIndex", st.IntegerType()),
409
+ st.StructField("FailureType", st.StringType()),
410
+ st.StructField("Status", st.StringType()),
411
+ st.StructField("Entity", st.StringType()),
412
+ ]
413
+ ),
414
+ )
415
+ .filter(
416
+ (sf.col("FailureType") == sf.lit("record"))
417
+ & (sf.col("Status") != sf.lit("informational"))
418
+ & (sf.col("Entity") == sf.lit(entity_name))
419
+ )
420
+ .distinct()
421
+ .orderBy(sf.asc(sf.col("RecordIndex")))
422
+ # todo - ^^ possibly relook at join strat. Does this help? Over prescriptive?
423
+ )
424
+ if df_is_empty(relevant_record_rejections_codes_df):
425
+ return entity
426
+ filtered_entity = entity.join(
427
+ relevant_record_rejections_codes_df,
428
+ on=entity.__record_index__ == relevant_record_rejections_codes_df.RecordIndex,
429
+ how="anti",
430
+ )
431
+ return filtered_entity
432
+
433
+
434
+ def spark_filter_contract_errors(cls):
435
+ """Class decorator to filter out records that failed casting and have record rejection scope"""
436
+ cls.filter_data_contract_record_rejections = _spark_filter_contract_errors
437
+ return cls
438
+
439
+
368
440
  @staticmethod # type: ignore
369
441
  def _spark_get_entity_count(entity: DataFrame) -> int:
370
442
  """Method to obtain entity count from a persisted parquet entity"""
@@ -457,11 +529,7 @@ def _spark_safely_quote_name(field_name: str) -> str:
457
529
 
458
530
  # pylint: disable=R0801
459
531
  def get_spark_cast_statement_from_annotation(
460
- element_name: str,
461
- type_annotation: Any,
462
- parent_element: bool = True,
463
- date_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
464
- timestamp_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}((\\+|\\-)[0-9]{2}:[0-9]{2})?$", # pylint: disable=C0301
532
+ element_name: str, type_annotation: Any, parent_element: bool = True
465
533
  ):
466
534
  """Generate casting statements for spark dataframes based on type annotations"""
467
535
  type_origin = get_origin(type_annotation)
@@ -471,20 +539,18 @@ def get_spark_cast_statement_from_annotation(
471
539
  # An `Optional` or `Union` type, check to ensure non-heterogenity.
472
540
  if type_origin is Union:
473
541
  python_type = _get_non_heterogenous_type(get_args(type_annotation))
474
- return get_spark_cast_statement_from_annotation(
475
- element_name, python_type, parent_element, date_regex, timestamp_regex
476
- )
542
+ return get_spark_cast_statement_from_annotation(element_name, python_type, parent_element)
477
543
 
478
544
  # Type hint is e.g. `List[str]`, check to ensure non-heterogenity.
479
545
  if type_origin is list or (isinstance(type_origin, type) and issubclass(type_origin, list)):
480
546
  element_type = _get_non_heterogenous_type(get_args(type_annotation))
481
- stmt = f"transform({quoted_name}, x -> {get_spark_cast_statement_from_annotation('x',element_type, False, date_regex, timestamp_regex)})" # pylint: disable=C0301
547
+ stmt = f"TRANSFORM({quoted_name}, x -> {get_spark_cast_statement_from_annotation('x',element_type, False)})" # pylint: disable=C0301
482
548
  return stmt if not parent_element else _cast_as_spark_type(stmt, type_annotation)
483
549
 
484
550
  if type_origin is Annotated:
485
551
  python_type, *_ = get_args(type_annotation) # pylint: disable=unused-variable
486
552
  return get_spark_cast_statement_from_annotation(
487
- element_name, python_type, parent_element, date_regex, timestamp_regex
553
+ element_name, python_type, parent_element
488
554
  ) # add other expected params here
489
555
  # Ensure that we have a concrete type at this point.
490
556
  if not isinstance(type_annotation, type):
@@ -509,7 +575,7 @@ def get_spark_cast_statement_from_annotation(
509
575
  continue
510
576
 
511
577
  fields[field_name] = get_spark_cast_statement_from_annotation(
512
- f"{element_name}.{field_name}", field_annotation, False, date_regex, timestamp_regex
578
+ f"{element_name}.{field_name}", field_annotation, False
513
579
  )
514
580
 
515
581
  if not fields:
@@ -517,7 +583,7 @@ def get_spark_cast_statement_from_annotation(
517
583
  f"No type annotations in dict/dataclass type (got {type_annotation!r})"
518
584
  )
519
585
  cast_exprs = ",".join([f"{stmt} AS `{nme}`" for nme, stmt in fields.items()])
520
- stmt = f"struct({cast_exprs})"
586
+ stmt = f"STRUCT({cast_exprs})"
521
587
  return stmt if not parent_element else _cast_as_spark_type(stmt, type_annotation)
522
588
  if type_annotation is list:
523
589
  raise ValueError(
@@ -527,15 +593,29 @@ def get_spark_cast_statement_from_annotation(
527
593
  raise ValueError(f"dict must be `typing.TypedDict` subclass, got {type_annotation!r}")
528
594
 
529
595
  for type_ in type_annotation.mro():
596
+ _date_format: str = getattr( # type: ignore
597
+ type_,
598
+ "DATE_FORMAT",
599
+ DEFAULT_ISO_FORMATS.get(type_, DEFAULT_ISO_FORMATS.get(dt.datetime)),
600
+ )
601
+
602
+ # pylint: disable=C0301
603
+ dt_cast_statement = f"CASE WHEN REGEXP(TRIM({quoted_name}), '{datetime_format_to_regex(_date_format)}') THEN TRY_TO_TIMESTAMP(TRIM({quoted_name}), \"{python_to_java_datetime_format(_date_format)}\") ELSE NULL END" # pylint: disable=C0301
530
604
  # datetime is subclass of date, so needs to be handled first
531
605
  if issubclass(type_, dt.datetime):
532
- stmt = rf"CASE WHEN REGEXP(TRIM({quoted_name}), '{timestamp_regex}') THEN TRIM({quoted_name}) ELSE NULL END" # pylint: disable=C0301
533
- return _cast_as_spark_type(stmt, type_) if parent_element else stmt
606
+ return (
607
+ _cast_as_spark_type(dt_cast_statement, type_)
608
+ if parent_element
609
+ else dt_cast_statement
610
+ )
534
611
  if issubclass(type_, dt.date):
535
- stmt = rf"CASE WHEN REGEXP(TRIM({quoted_name}), '{date_regex}') THEN TRIM({quoted_name}) ELSE NULL END" # pylint: disable=C0301
536
- return _cast_as_spark_type(stmt, type_) if parent_element else stmt
612
+ return (
613
+ _cast_as_spark_type(dt_cast_statement, type_)
614
+ if parent_element
615
+ else dt_cast_statement
616
+ )
537
617
  spark_type = get_type_from_annotation(type_)
538
618
  if spark_type:
539
- stmt = f"trim({quoted_name})"
619
+ stmt = f"TRIM({quoted_name})"
540
620
  return _cast_as_spark_type(stmt, type_) if parent_element else stmt
541
621
  raise ValueError(f"No equivalent Spark type for {type_annotation!r}")
@@ -44,7 +44,7 @@ class DataContractMetadata(BaseModel, frozen=True, arbitrary_types_allowed=True)
44
44
  """The per-entity schemas, as pydantic models."""
45
45
  if not self._schemas:
46
46
  for entity_name, validator in self.validators.items():
47
- self._schemas[entity_name] = validator.model # 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