Skip to content

Commit b9c872e

Browse files
committed
feat: add facility to add additional checks at end of file transformation stage
1 parent 3eca2f1 commit b9c872e

16 files changed

Lines changed: 322 additions & 21 deletions

File tree

‎src/dve/core_engine/backends/base/reader.py‎

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from dve.core_engine.backends.exceptions import MessageBearingError, ReaderLacksEntityTypeSupport
1212
from dve.core_engine.backends.types import EntityName, EntityType
13+
from dve.core_engine.configuration.v1 import AllowedAdditionalChecks, _ReaderAdditionalChecksConfig
1314
from dve.core_engine.message import FeedbackMessage
1415
from dve.core_engine.type_hints import URI, ArbitraryFunction, WrapDecorator
1516
from dve.parser.file_handling.service import open_stream
@@ -109,29 +110,58 @@ def read_to_entity_type(
109110
entity_name: EntityName,
110111
schema: type[BaseModel],
111112
all_model_fields: Optional[set[str]] = None,
112-
) -> EntityType:
113+
additional_checks: Optional[
114+
dict[AllowedAdditionalChecks, _ReaderAdditionalChecksConfig]
115+
] = None,
116+
):
113117
"""Read to the specified entity type, if supported.
114118
115119
NOTE: Simple types should either be returned as strings (if present) or
116120
`None`. Format validation, casting, and parsing should be done in the
117121
data contract.
118122
119123
"""
120-
if entity_name == Iterator[dict[str, Any]]:
121-
return self.read_to_py_iterator(
124+
additional_checks = additional_checks or {}
125+
126+
self.raise_if_not_sensible_file(resource, entity_name)
127+
128+
if entity_type == Iterator[dict[str, Any]]:
129+
entity = self.read_to_py_iterator(
122130
resource, entity_name, schema, all_model_fields # type: ignore
123131
)
124132

125-
self.raise_if_not_sensible_file(resource, entity_name)
133+
else:
126134

127-
try:
128-
reader_func = self.__read_methods__[entity_type]
129-
except KeyError as err:
130-
raise ReaderLacksEntityTypeSupport(entity_type=entity_type) from err
135+
try:
136+
reader_func = self.__read_methods__[entity_type]
137+
except KeyError as err:
138+
raise ReaderLacksEntityTypeSupport(entity_type=entity_type) from err
131139

132-
return reader_func(
133-
self, resource, entity_name, schema, all_model_fields=all_model_fields # type: ignore
134-
)
140+
entity = reader_func(
141+
self,
142+
resource,
143+
entity_name,
144+
schema,
145+
all_model_fields=all_model_fields, # type: ignore
146+
)
147+
148+
if config := additional_checks.get("check_empty"):
149+
if self.check_entity_empty(entity):
150+
raise MessageBearingError(
151+
f"The mandatory entity {entity_name} is empty",
152+
messages=[
153+
FeedbackMessage(
154+
entity=entity_name,
155+
record=None,
156+
failure_type="submission",
157+
error_location=entity_name,
158+
error_code=config.error_code,
159+
error_message=config.error_message,
160+
)
161+
],
162+
)
163+
164+
return entity
135165

136166
def add_record_index(self, entity: EntityType, **kwargs) -> EntityType:
137167
"""Add a record index to the entity"""
@@ -141,6 +171,10 @@ def drop_record_index(self, entity: EntityType, **kwargs) -> EntityType:
141171
"""Drop a record index to the entity"""
142172
raise NotImplementedError(f"drop_record_index not implemented in {self.__class__}")
143173

174+
def check_entity_empty(self, entity: EntityType) -> bool:
175+
"""Determine if the entity supplied is empty"""
176+
raise NotImplementedError(f"check_entity_empty not implemented in {self.__class__}")
177+
144178
def write_parquet(
145179
self,
146180
entity: EntityType,

‎src/dve/core_engine/backends/base/rules.py‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,7 @@ def process_node(node: HierarchyNode):
448448
record=record, # type: ignore
449449
error_location=location,
450450
error_message=template_object(
451-
node.missing_parent_id_error_message,
452-
record
451+
node.missing_parent_id_error_message, record
453452
),
454453
failure_type="record",
455454
error_type="record",
@@ -522,10 +521,7 @@ def process_node(node: HierarchyNode) -> bool:
522521
entity=node.parent_entity,
523522
record=record, # type: ignore
524523
error_location=location,
525-
error_message=template_object(
526-
node.no_valid_records_error_message,
527-
record
528-
),
524+
error_message=template_object(node.no_valid_records_error_message, record),
529525
failure_type="record",
530526
error_type="record",
531527
error_code=node.no_valid_records_error_code,

‎src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,16 @@ def duckdb_get_entity_count(cls):
322322
return cls
323323

324324

325+
def _duckdb_check_entity_empty(self, entity: DuckDBPyRelation) -> bool: # pylint: disable=W0613
326+
return entity.shape[0] == 0
327+
328+
329+
def duckdb_check_entity_empty(cls):
330+
"""Class decorator to check whether a supplied entity is empty"""
331+
cls.check_entity_empty = _duckdb_check_entity_empty
332+
return cls
333+
334+
325335
def get_all_registered_udfs(connection: DuckDBPyConnection) -> set[str]:
326336
"""Function to supply the names of a registered functions stored in the supplied
327337
duckdb connection. Creates the temp table used to store registered functions (if not exists).

‎src/dve/core_engine/backends/implementations/duckdb/readers/csv.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
UnableToParseCSVError,
2323
)
2424
from dve.core_engine.backends.implementations.duckdb.duckdb_helpers import (
25+
duckdb_check_entity_empty,
2526
duckdb_record_index,
2627
duckdb_write_parquet,
2728
get_duckdb_type_from_annotation,
@@ -35,6 +36,7 @@
3536
from dve.parser.file_handling import get_content_length
3637

3738

39+
@duckdb_check_entity_empty
3840
@duckdb_record_index
3941
@duckdb_write_parquet
4042
class DuckDBCSVReader(CSVFileReader):

‎src/dve/core_engine/backends/implementations/duckdb/readers/json.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from dve.core_engine.backends.base.reader import BaseFileReader, read_function
1212
from dve.core_engine.backends.implementations.duckdb.duckdb_helpers import (
13+
duckdb_check_entity_empty,
1314
duckdb_record_index,
1415
duckdb_write_parquet,
1516
get_duckdb_type_from_annotation,
@@ -18,6 +19,7 @@
1819
from dve.core_engine.type_hints import URI, EntityName
1920

2021

22+
@duckdb_check_entity_empty
2123
@duckdb_record_index
2224
@duckdb_write_parquet
2325
class DuckDBJSONReader(BaseFileReader):

‎src/dve/core_engine/backends/implementations/duckdb/readers/xml.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
from dve.core_engine.backends.base.reader import read_function
1212
from dve.core_engine.backends.exceptions import MessageBearingError
13-
from dve.core_engine.backends.implementations.duckdb.duckdb_helpers import duckdb_write_parquet
13+
from dve.core_engine.backends.implementations.duckdb.duckdb_helpers import (
14+
duckdb_check_entity_empty,
15+
duckdb_write_parquet,
16+
)
1417
from dve.core_engine.backends.readers.xml import XMLStreamReader
1518
from dve.core_engine.backends.utilities import (
1619
get_polars_type_from_annotation,
@@ -20,6 +23,7 @@
2023
from dve.core_engine.type_hints import URI
2124

2225

26+
@duckdb_check_entity_empty
2327
@polars_record_index
2428
@duckdb_write_parquet
2529
class DuckDBXMLStreamReader(XMLStreamReader):

‎src/dve/core_engine/backends/implementations/spark/readers/csv.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from dve.core_engine.backends.exceptions import EmptyFileError
1313
from dve.core_engine.backends.implementations.spark.spark_helpers import (
1414
get_type_from_annotation,
15+
spark_check_entity_empty,
1516
spark_record_index,
1617
spark_write_parquet,
1718
)
@@ -20,6 +21,7 @@
2021
from dve.parser.file_handling import get_content_length
2122

2223

24+
@spark_check_entity_empty
2325
@spark_record_index
2426
@spark_write_parquet
2527
class SparkCSVReader(CSVFileReader):

‎src/dve/core_engine/backends/implementations/spark/readers/json.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
from dve.core_engine.backends.exceptions import EmptyFileError
1212
from dve.core_engine.backends.implementations.spark.spark_helpers import (
1313
get_type_from_annotation,
14+
spark_check_entity_empty,
1415
spark_record_index,
1516
spark_write_parquet,
1617
)
1718
from dve.core_engine.type_hints import URI, EntityName
1819
from dve.parser.file_handling import get_content_length
1920

2021

22+
@spark_check_entity_empty
2123
@spark_record_index
2224
@spark_write_parquet
2325
class SparkJSONReader(BaseFileReader):

‎src/dve/core_engine/backends/implementations/spark/readers/xml.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from dve.core_engine.backends.implementations.spark.spark_helpers import (
1818
df_is_empty,
1919
get_type_from_annotation,
20+
spark_check_entity_empty,
2021
spark_record_index,
2122
spark_write_parquet,
2223
)
@@ -29,6 +30,7 @@
2930
"""The mode to use when parsing XML files with Spark."""
3031

3132

33+
@spark_check_entity_empty
3234
@spark_record_index
3335
@spark_write_parquet
3436
class SparkXMLStreamReader(XMLStreamReader):
@@ -55,6 +57,7 @@ def read_to_dataframe(
5557
)
5658

5759

60+
@spark_check_entity_empty
5861
@spark_record_index
5962
@spark_write_parquet
6063
class SparkXMLReader(BasicXMLFileReader): # pylint: disable=too-many-instance-attributes

‎src/dve/core_engine/backends/implementations/spark/spark_helpers.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,16 @@ def spark_get_entity_count(cls):
456456
return cls
457457

458458

459+
def _spark_check_entity_empty(self, entity: DataFrame) -> bool: # pylint: disable=W0613
460+
return entity.count() == 0
461+
462+
463+
def spark_check_entity_empty(cls):
464+
"""Class decorator to check whether a supplied entity is empty"""
465+
cls.check_entity_empty = _spark_check_entity_empty
466+
return cls
467+
468+
459469
def get_all_registered_udfs(spark: SparkSession) -> set[str]:
460470
"""Function to supply the names of a registered functions stored in the supplied
461471
spark session.

0 commit comments

Comments
 (0)