From e1ff40fc544009b19b3d7df771013a32714457e4 Mon Sep 17 00:00:00 2001 From: Lincoln Puzey Date: Fri, 20 Sep 2024 14:41:26 +0800 Subject: [PATCH 1/2] BDRSPS-811 Add mutual exclusive check --- abis_mapping/plugins/__init__.py | 1 + abis_mapping/plugins/mutual_exclusion.py | 47 ++++++++++++++++ tests/plugins/test_mutual_exclusion.py | 70 ++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 abis_mapping/plugins/mutual_exclusion.py create mode 100644 tests/plugins/test_mutual_exclusion.py diff --git a/abis_mapping/plugins/__init__.py b/abis_mapping/plugins/__init__.py index 6f54afa2..cd9acc60 100644 --- a/abis_mapping/plugins/__init__.py +++ b/abis_mapping/plugins/__init__.py @@ -7,6 +7,7 @@ from . import empty from . import list from . import logical_or +from . import mutual_exclusion from . import mutual_inclusion from . import sites_geometry from . import tabular diff --git a/abis_mapping/plugins/mutual_exclusion.py b/abis_mapping/plugins/mutual_exclusion.py new file mode 100644 index 00000000..1b8e0e05 --- /dev/null +++ b/abis_mapping/plugins/mutual_exclusion.py @@ -0,0 +1,47 @@ +"""Provides extra frictionless mutual exclusion checks for the package""" + + +# Third-Party +import frictionless +import frictionless.errors +import attrs + +# Typing +from typing import Iterator + + +@attrs.define(kw_only=True, repr=False) +class MutuallyExclusive(frictionless.Check): + """Checks that mutually exclusive columns are not provided together.""" + + # Check Attributes + type = "mutually-exclusive" + Errors = [frictionless.errors.RowConstraintError] + + # Attributes specific to this check + field_names: list[str] + + def validate_row(self, row: frictionless.Row) -> Iterator[frictionless.Error]: + """Called to validate the given row (on every row). + + Args: + row (frictionless.Row): The row to check the mutual exclusivity of. + + Yields: + frictionless.Error: For when the mutual exclusion is violated. + """ + # Retrieve Field Names for cells with values + fields_provided = [f for f in self.field_names if row[f] not in (None, "")] + + # Check Mutual Exclusivity + # If 2 or more of the mutually exclusive fields are provided, that's an error. + if len(fields_provided) >= 2: + # Yield Error + yield frictionless.errors.RowConstraintError.from_row( + row=row, + note=( + f"The columns {self.field_names} are mutually exclusive and must " + f"not be provided together " + f"(columns {fields_provided} were provided together)" + ), + ) diff --git a/tests/plugins/test_mutual_exclusion.py b/tests/plugins/test_mutual_exclusion.py new file mode 100644 index 00000000..f9383505 --- /dev/null +++ b/tests/plugins/test_mutual_exclusion.py @@ -0,0 +1,70 @@ +"""Provides Unit Tests for the `abis_mapping.plugins.mutual_exclusion` module""" + + +# Third-Party +import frictionless + +# Local +from abis_mapping import plugins + + +def test_checks_mutually_exclusive_valid() -> None: + """Tests the MutuallyExclusive Checker""" + # Construct Fake Resource + resource = frictionless.Resource( + source=[ + # valid: neither field provided + {"C0": "R1", "C1": "", "C2": None}, + # valid: only one field provided + {"C0": "R4", "C1": "", "C2": 1}, + {"C0": "R5", "C1": "", "C2": 0}, + {"C0": "R6", "C1": "A", "C2": None}, + ], + ) + + # Validate + report: frictionless.Report = resource.validate( + checklist=frictionless.Checklist( + checks=[ + plugins.mutual_exclusion.MutuallyExclusive( + field_names=["C1", "C2"], + ), + ], + ), + ) + + # Check + assert report.valid + + +def test_checks_mutually_exclusive_not_valid() -> None: + """Tests the MutuallyExclusive Checker""" + # Construct Fake Resource + resource = frictionless.Resource( + source=[ + # not valid: both fields provided + {"C0": "R1", "C1": "A", "C2": 1}, + {"C0": "R2", "C1": "A", "C2": 0}, + ], + ) + + # Validate + report: frictionless.Report = resource.validate( + checklist=frictionless.Checklist( + checks=[ + plugins.mutual_exclusion.MutuallyExclusive( + field_names=["C1", "C2"], + ), + ], + ), + ) + + # Check + assert not report.valid + assert len(report.tasks[0].errors) == 2 + for error in report.tasks[0].errors: + assert error.type == 'row-constraint' + assert error.note == ( + "The columns ['C1', 'C2'] are mutually exclusive and must not be " + "provided together (columns ['C1', 'C2'] were provided together)" + ) From 99e13ca51d3d03e53f80cb9236bcb9e65ebe44bf Mon Sep 17 00:00:00 2001 From: Lincoln Puzey Date: Fri, 20 Sep 2024 14:42:04 +0800 Subject: [PATCH 2/2] BDRSPS-811 Add mutual exclusive check to siteID and siteVisitID for survey occurrence V2 --- abis_mapping/templates/survey_occurrence_data_v2/mapping.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/abis_mapping/templates/survey_occurrence_data_v2/mapping.py b/abis_mapping/templates/survey_occurrence_data_v2/mapping.py index a8898f9d..0ba4981a 100644 --- a/abis_mapping/templates/survey_occurrence_data_v2/mapping.py +++ b/abis_mapping/templates/survey_occurrence_data_v2/mapping.py @@ -120,6 +120,9 @@ def apply_validation( plugins.mutual_inclusion.MutuallyInclusive( field_names=["sensitivityCategory", "sensitivityAuthority"], ), + plugins.mutual_exclusion.MutuallyExclusive( + field_names=["siteID", "siteVisitID"], + ) ], )