Skip to content

Commit 3a5b2bd

Browse files
authored
feat: added functionality to allow error messages in business rules t… (#8)
* feat: added functionality to allow error messages in business rules to be templated * docs: fix linting and formatting * refactor: Removed additional templating step for business rule messages as now handled elsewhere Also tweaked how reference_data existance being checked for as truthy evaluation False where expected to be True * docs: Bump version to 0.1.0 to reflect still in dev. Amended readme to reflect
1 parent 3bfccde commit 3a5b2bd

16 files changed

Lines changed: 77 additions & 70 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Additionally, if you'd like to contribute a new backend implementation into the
2121

2222
## Installation and usage
2323

24-
The DVE is a Python package and can be installed using `pip`. As of release v1.0.0 we currently only supports Python 3.7, with Spark version 3.2.1 and DuckDB version of 1.1.0. We are currently working on upgrading the DVE to work on Python 3.11+ and this will be made available asap with version 2.0.0 release.
24+
The DVE is a Python package and can be installed using `pip`. As of release v0.1.0 we currently only supports Python 3.7, with Spark version 3.2.1 and DuckDB version of 1.1.0. We are currently working on upgrading the DVE to work on Python 3.11+ and this will be made available asap with version 1.0.0 release.
2525

2626
In addition to a working Python 3.7+ installation you will need OpenJDK 11 installed if you're planning to use the Spark backend implementation.
2727

@@ -30,7 +30,7 @@ Python dependencies are listed in `pyproject.toml`.
3030
To install the DVE package you can simply install using a package manager such as [pip](https://pypi.org/project/pip/).
3131

3232
```
33-
pip install git+https://github.com/NHSDigital/data-validation-engine.git@v1.0.0
33+
pip install git+https://github.com/NHSDigital/data-validation-engine.git@v0.1.0
3434
```
3535

3636
Once you have installed the DVE you are ready to use it. For guidance on how to create your dischema json document (configuration), please read the [documentation](./docs/).
@@ -48,8 +48,8 @@ If you have feature request then please follow the same process whilst using the
4848
Below is a list of features that we would like to implement or have been requested.
4949
| Feature | Release Version | Released? |
5050
| ------- | --------------- | --------- |
51-
| Open source release | 1.0.0 | Yes |
52-
| Uplift to Python 3.11 | 2.0.0 | No |
51+
| Open source release | 0.1.0 | Yes |
52+
| Uplift to Python 3.11 | 1.0.0 | No |
5353
| Upgrade to Pydantic 2.0 | Not yet confirmed | No |
5454
| Create a more user friendly interface for building and modifying dischema files | Not yet confirmed | No |
5555

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "nhs_dve"
3-
version = "1.1.0"
3+
version = "0.1.0"
44
description = "`nhs data validation engine` is a framework used to validate data"
55
authors = ["NHS England <england.contactus@nhs.net>"]
66
readme = "README.md"

src/dve/core_engine/backends/base/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def __init__(
7676
raise ValueError(f"Entity name cannot start with 'refdata_', got {entity_name!r}")
7777
self.entities[entity_name] = entity
7878

79-
self.reference_data = reference_data or {}
79+
self.reference_data = reference_data if reference_data is not None else {}
8080
"""The reference data mapping."""
8181

8282
@staticmethod

src/dve/core_engine/backends/implementations/duckdb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
DuckDBCSVReader,
88
DuckDBCSVRepeatingHeaderReader,
99
DuckDBXMLStreamReader,
10-
PolarsToDuckDBCSVReader
10+
PolarsToDuckDBCSVReader,
1111
)
1212
from .reference_data import DuckDBRefDataLoader
1313
from .rules import DuckDBStepImplementations

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class DuckDBCSVRepeatingHeaderReader(PolarsToDuckDBCSVReader):
128128
| ---------- | ---------- | ---------- |
129129
| shop1 | clothes | 2025-01-01 |
130130
"""
131+
131132
@read_function(DuckDBPyRelation)
132133
def read_to_relation( # pylint: disable=unused-argument
133134
self, resource: URI, entity_name: EntityName, schema: Type[BaseModel]

src/dve/core_engine/backends/implementations/duckdb/rules.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from dve.core_engine.constants import ROWID_COLUMN_NAME
5353
from dve.core_engine.functions import implementations as functions
5454
from dve.core_engine.message import FeedbackMessage
55+
from dve.core_engine.templating import template_object
5556
from dve.core_engine.type_hints import Messages
5657

5758

@@ -510,12 +511,14 @@ def notify(self, entities: DuckDBEntities, *, config: Notification) -> Messages:
510511
matched = matched.select(StarExpression(exclude=config.excluded_columns))
511512

512513
for record in matched.df().to_dict(orient="records"):
514+
# NOTE: only templates using values directly accessible in record - nothing nested
515+
# more complex extraction done in reporting module
513516
messages.append(
514517
FeedbackMessage(
515518
entity=config.reporting.reporting_entity_override or config.entity_name,
516519
record=record, # type: ignore
517520
error_location=config.reporting.legacy_location,
518-
error_message=config.reporting.message,
521+
error_message=template_object(config.reporting.message, record), # type: ignore
519522
failure_type=config.reporting.legacy_error_type,
520523
error_type=config.reporting.legacy_error_type,
521524
error_code=config.reporting.code,

src/dve/core_engine/backends/implementations/spark/rules.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from dve.core_engine.constants import ROWID_COLUMN_NAME
4646
from dve.core_engine.functions import implementations as functions
4747
from dve.core_engine.message import FeedbackMessage
48+
from dve.core_engine.templating import template_object
4849
from dve.core_engine.type_hints import Messages
4950

5051

@@ -406,11 +407,15 @@ def notify(self, entities: SparkEntities, *, config: Notification) -> Messages:
406407

407408
for record in matched.toLocalIterator():
408409
messages.append(
410+
# NOTE: only templates using values directly accessible in record - nothing nested
411+
# more complex extraction done in reporting module
409412
FeedbackMessage(
410413
entity=config.reporting.reporting_entity_override or config.entity_name,
411414
record=record.asDict(recursive=True),
412415
error_location=config.reporting.legacy_location,
413-
error_message=config.reporting.message,
416+
error_message=template_object(
417+
config.reporting.message, record.asDict(recursive=True)
418+
),
414419
failure_type=config.reporting.legacy_error_type,
415420
error_type=config.reporting.legacy_error_type,
416421
error_code=config.reporting.code,

src/dve/core_engine/backends/metadata/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pydantic import BaseModel
1111

1212
from dve.core_engine.backends.metadata.contract import DataContractMetadata, ReaderConfig
13-
from dve.core_engine.backends.metadata.reporting import ReportingConfig, UntemplatedReportingConfig
13+
from dve.core_engine.backends.metadata.reporting import LegacyReportingConfig, ReportingConfig
1414
from dve.core_engine.backends.metadata.rules import (
1515
AbstractStep,
1616
Aggregation,

src/dve/core_engine/backends/metadata/reporting.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class BaseReportingConfig(BaseModel):
2727
2828
"""
2929

30-
UNTEMPLATED_FIELDS: ClassVar[Set[str]] = set()
30+
UNTEMPLATED_FIELDS: ClassVar[Set[str]] = {"message"}
3131
"""Fields that should not be templated."""
3232

3333
emit: Optional[str] = None
@@ -117,15 +117,13 @@ def template(
117117
else:
118118
variables = local_variables
119119
templated = template_object(self.dict(exclude=self.UNTEMPLATED_FIELDS), variables, "jinja")
120+
templated.update(self.dict(include=self.UNTEMPLATED_FIELDS))
120121
return type_(**templated)
121122

122123

123124
class ReportingConfig(BaseReportingConfig):
124125
"""A base model defining the 'final' reporting config for a message."""
125126

126-
UNTEMPLATED_FIELDS: ClassVar[Set[str]] = {"message"}
127-
"""Fields that should not be templated."""
128-
129127
emit: ErrorEmitValue = "record_failure"
130128
category: ErrorCategory = "Bad value"
131129

@@ -246,7 +244,7 @@ def get_location_value(
246244
return self.get_location_selector()(record)
247245

248246

249-
class UntemplatedReportingConfig(BaseReportingConfig):
247+
class LegacyReportingConfig(BaseReportingConfig):
250248
"""An untemplated reporting config. This _must_ be templated prior to use.
251249
252250
This class also enables the conversion of deprecated fields to their
@@ -356,7 +354,8 @@ def template(
356354
else:
357355
variables = local_variables
358356

359-
templated = template_object(self.dict(), variables, "jinja")
357+
templated = template_object(self.dict(exclude=self.UNTEMPLATED_FIELDS), variables, "jinja")
358+
templated.update(self.dict(include=self.UNTEMPLATED_FIELDS))
360359
error_location = templated.pop("legacy_location")
361360
reporting_field = templated.pop("legacy_reporting_field")
362361
if templated.get("location") is None:

src/dve/core_engine/backends/metadata/rules.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from typing_extensions import Literal
2121

2222
from dve.core_engine.backends.base.reference_data import ReferenceConfigUnion
23-
from dve.core_engine.backends.metadata.reporting import ReportingConfig, UntemplatedReportingConfig
23+
from dve.core_engine.backends.metadata.reporting import LegacyReportingConfig, ReportingConfig
2424
from dve.core_engine.templating import template_object
2525
from dve.core_engine.type_hints import (
2626
Alias,
@@ -234,7 +234,7 @@ class DeferredFilter(AbstractStep):
234234
removed from the source entity if the reporting level is a record-level error.
235235
236236
"""
237-
reporting: Union[ReportingConfig, UntemplatedReportingConfig]
237+
reporting: Union[ReportingConfig, LegacyReportingConfig]
238238
"""The reporting information for the filter."""
239239

240240
def template(

0 commit comments

Comments
 (0)