Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions src/dve/core_engine/backends/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,41 +233,76 @@
return f"Refdata loader does not support reading refdata from {self.file_extension} files"


class EmptyFileError(ReaderErrorMixin, ValueError):
class EmptyFileError(ReaderErrorMixin, CriticalMessageBearingError):
"""The read file was empty."""

def get_message_preamble(self) -> str:
return "File location provided is empty"

def __init__(self, *args: object, error_message: Optional[str] = None):

class MissingHeaderError(ReaderErrorMixin, ValueError):
super().__init__(
*args,
message=FeedbackMessage(
entity="Whole File",

Check failure on line 247 in src/dve/core_engine/backends/exceptions.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Whole File" 4 times.

See more on https://sonarcloud.io/project/issues?id=NHSDigital_data-validation-engine&issues=AaGnZuRZZSMmWhdNegIC&open=AaGnZuRZZSMmWhdNegIC&pullRequest=204
record=None,
failure_type="submission",
error_code="EMPTYFILE",
error_message=error_message or self.get_message_preamble(),
),
)


class MissingHeaderError(ReaderErrorMixin, CriticalMessageBearingError):
"""A header is expected, but was not parsed."""

def get_message_preamble(self) -> str:
return "No header was provided"

def __init__(self, *args: object, error_message: Optional[str] = None):
super().__init__(
*args,
message=FeedbackMessage(
entity="Whole File",
record=None,
failure_type="submission",
error_code="MISSINGHEADER",
error_message=error_message or self.get_message_preamble(),
),
)


class FieldCountMismatch(ReaderErrorMixin, ValueError):
class FieldCountMismatch(ReaderErrorMixin, CriticalMessageBearingError):
"""An error raised when the number of expected fields does not match
the number of parsed fields.

"""

def __init__(self, *args: object, n_expected_fields: int, n_actual_fields: int) -> None:
super().__init__(*args)
self.n_expected_fields = n_expected_fields
"""The number of fields that should have been in the file."""
self.n_actual_fields = n_actual_fields
"""The number of fields that were actually in the file."""

super().__init__(
*args,
message=FeedbackMessage(
entity="Whole File",
record=None,
failure_type="submission",
error_code="FIELDCOUNTMISMATCH",
error_message=self.get_message_preamble(),
),
)

def get_message_preamble(self) -> str:
return (
f"Number of fields expected ({self.n_expected_fields}) does not "
+ f"match the number in the provided data ({self.n_actual_fields})"
)


class SchemaMismatch(ReaderErrorMixin, ValueError):
class SchemaMismatch(ReaderErrorMixin, CriticalMessageBearingError):
"""An error raised when a file does not match a given schema (e.g. fields
are missing).

Expand All @@ -290,7 +325,16 @@
tuple of actual and expected field type, both as strings).

"""
super().__init__(*args)
super().__init__(
*args,
message=FeedbackMessage(
entity="Whole File",
record=None,
failure_type="submission",
error_code="SCHEMAMISMATCH",
error_message=self.get_message_preamble() + self.get_message_epilogue(),
),
)

def get_message_preamble(self) -> str:
return "Schema mismatch"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def read_to_relation( # pylint: disable=unused-argument
) -> DuckDBPyRelation:
"""Returns a relation object from the source csv"""
if get_content_length(resource) == 0:
raise EmptyFileError(f"File at {resource} is empty.")
raise EmptyFileError(error_message=f"File at {resource} is empty.")

if self.field_check:
self.perform_field_check(resource, entity_name, schema, all_model_fields)
Expand Down Expand Up @@ -155,7 +155,7 @@ def read_to_relation( # pylint: disable=unused-argument
) -> DuckDBPyRelation:
"""Returns a relation object from the source csv"""
if get_content_length(resource) == 0:
raise EmptyFileError(f"File at {resource} is empty.")
raise EmptyFileError(error_message=f"File at {resource} is empty.")

if self.field_check:
self.perform_field_check(resource, entity_name, schema, all_model_fields)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def read_to_dataframe(
) -> DataFrame:
"""Read a CSV file directly to a Spark DataFrame."""
if get_content_length(resource) == 0:
raise EmptyFileError(f"File at {resource} is empty.")
raise EmptyFileError(error_message=f"File at {resource} is empty.")

if self.field_check:
self.perform_field_check(resource, entity_name, schema, all_model_fields)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def read_to_dataframe(
) -> DataFrame:
"""Read a JSON file directly to a Spark DataFrame."""
if get_content_length(resource) == 0:
raise EmptyFileError(f"File at {resource} is empty.")
raise EmptyFileError(error_message=f"File at {resource} is empty.")

spark_schema: StructType = get_type_from_annotation(schema)
kwargs = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def read_to_dataframe(

"""
if get_content_length(resource) == 0:
raise EmptyFileError(f"File at {resource} is empty.")
raise EmptyFileError(error_message=f"File at {resource} is empty.")

if self.xsd_location:
msg = self._run_xmllint(file_uri=resource)
Expand Down
4 changes: 2 additions & 2 deletions src/dve/core_engine/backends/readers/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def _parse_field_names(self, stream: IO[str]) -> list[str]:
# can lead to null headers.
parsed_field_names = reader.fieldnames
if parsed_field_names is None: # pragma: no cover
raise MissingHeaderError("Header could not be parsed from CSV file")
raise MissingHeaderError(error_message="Header could not be parsed from CSV file")

if not all(
map(lambda string: isinstance(string, str), parsed_field_names)
Expand Down Expand Up @@ -231,7 +231,7 @@ def read_to_py_iterator(
) -> Iterator[dict[str, Any]]:
"""Reads the data to an iterator of dictionaries"""
if get_content_length(resource) == 0:
raise EmptyFileError(f"File at {resource!r} is empty")
raise EmptyFileError(error_message=f"File at {resource!r} is empty")

if self.field_check:
self.perform_field_check(resource, entity_name, schema, all_model_fields)
Expand Down
2 changes: 1 addition & 1 deletion src/dve/core_engine/backends/readers/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def read_to_py_iterator(

"""
if get_content_length(resource) == 0:
raise EmptyFileError(f"File at {resource!r} is empty")
raise EmptyFileError(error_message=f"File at {resource!r} is empty")

with open_stream(resource, "rb") as stream:
for idx, record in enumerate(self._parse_xml(stream, schema), start=1):
Expand Down
2 changes: 1 addition & 1 deletion src/dve/core_engine/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(
*args: object,
messages: Optional[list[str]] = None,
) -> None:
super().__init__(error_message, *args)
super().__init__(*args)
self.error_message = error_message
"""The error message explaining the critical processing error."""
self.messages = messages
Expand Down
Loading