From 685b40858d14c59db9a5eeb84d504b9fa57d8386 Mon Sep 17 00:00:00 2001 From: Rakesh Date: Fri, 2 Jan 2026 18:14:51 -0500 Subject: [PATCH 1/4] Fix domain_is_custom operation for TIG standards and refactor domain custom check logic --- .../models/library_metadata_container.py | 7 +++ .../operations/domain_is_custom.py | 3 +- .../services/cdisc_library_service.py | 11 ++-- .../utilities/rule_processor.py | 53 ++++++++++++++----- 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/cdisc_rules_engine/models/library_metadata_container.py b/cdisc_rules_engine/models/library_metadata_container.py index e031e9b30..57b3198db 100644 --- a/cdisc_rules_engine/models/library_metadata_container.py +++ b/cdisc_rules_engine/models/library_metadata_container.py @@ -139,3 +139,10 @@ def build_ct_terms(self, ct_package_type: str, versions: str | Iterable[str]): ct_terms["term_value"].append(term["submissionValue"]) ct_terms["term_pref_term"].append(term.get("preferredTerm")) return ct_terms + + def is_domain_custom(self, domain: str) -> bool: + standard_data = self._standard_metadata or {} + domains = standard_data.get("domains", set()) + if not isinstance(domains, (set, list, tuple)): + domains = set() + return domain not in domains diff --git a/cdisc_rules_engine/operations/domain_is_custom.py b/cdisc_rules_engine/operations/domain_is_custom.py index 3b9f86986..722f15919 100644 --- a/cdisc_rules_engine/operations/domain_is_custom.py +++ b/cdisc_rules_engine/operations/domain_is_custom.py @@ -8,5 +8,4 @@ def _execute_operation(self): given domain is in standard domains. If no -> the domain is custom. """ - standard_data: dict = self.library_metadata.standard_metadata - return self.params.domain not in standard_data.get("domains", {}) + return self.library_metadata.is_domain_custom(self.params.domain) diff --git a/cdisc_rules_engine/services/cdisc_library_service.py b/cdisc_rules_engine/services/cdisc_library_service.py index a9dfb5acd..270eb315c 100644 --- a/cdisc_rules_engine/services/cdisc_library_service.py +++ b/cdisc_rules_engine/services/cdisc_library_service.py @@ -290,8 +290,7 @@ def get_standard_details( domains: Set[str] = self._extract_domain_names_from_tabulation_standard( standard_data ) - if domains: - standard_data["domains"] = domains + standard_data["domains"] = domains return standard_data def get_model_details(self, standard_details: dict) -> Optional[dict]: @@ -679,5 +678,11 @@ def _extract_domain_names_from_tabulation_standard( domain_names: Set[str] = set() for cls in standard_data.get("classes", []): for dataset in cls.get("datasets", []): - domain_names.add(dataset.get("name")) + domain_name = dataset.get("name") + if domain_name: + domain_names.add(domain_name) + for dataset in standard_data.get("datasets", []): + domain_name = dataset.get("name") + if domain_name: + domain_names.add(domain_name) return domain_names diff --git a/cdisc_rules_engine/utilities/rule_processor.py b/cdisc_rules_engine/utilities/rule_processor.py index c90ac7310..7a2009252 100644 --- a/cdisc_rules_engine/utilities/rule_processor.py +++ b/cdisc_rules_engine/utilities/rule_processor.py @@ -268,6 +268,42 @@ def rule_applies_to_class( is_excluded = True return is_included and not is_excluded + def _is_custom_domain(self, domain: str) -> bool: + return self.library_metadata.is_domain_custom(domain) + + def _get_allowed_domains_for_use_cases( + self, use_cases: List[str], substandard: str + ) -> set: + allowed_domains = set() + for use_case in use_cases: + if use_case in USE_CASE_DOMAINS[substandard]: + allowed_domains.update(USE_CASE_DOMAINS[substandard][use_case]) + return allowed_domains + + def _get_domain_to_check(self, dataset_metadata: SDTMDatasetMetadata) -> str: + if dataset_metadata.is_supp and dataset_metadata.rdomain: + return dataset_metadata.rdomain + return dataset_metadata.domain + + def _check_adam_domain( + self, domain: str, substandard: str, use_cases: List[str] + ) -> bool: + if substandard == "ADAM" and domain.startswith("AD"): + return "ANALYSIS" in use_cases + return False + + def _check_domain_in_use_case( + self, domain: str, use_cases: List[str], substandard: str + ) -> bool: + allowed_domains = self._get_allowed_domains_for_use_cases( + use_cases, substandard + ) + if domain in allowed_domains: + return True + if self._is_custom_domain(domain): + return True + return False + def rule_applies_to_use_case( self, dataset_metadata: SDTMDatasetMetadata, @@ -285,21 +321,10 @@ def rule_applies_to_use_case( if substandard not in USE_CASE_DOMAINS: return False - domain_to_check = dataset_metadata.domain - if dataset_metadata.is_supp and dataset_metadata.rdomain: - domain_to_check = dataset_metadata.rdomain - - # Handle ADaM datasets with AD prefix - if substandard == "ADAM" and domain_to_check.startswith("AD"): - return "ANALYSIS" in use_cases - - allowed_domains = set() - for use_case in use_cases: - if use_case in USE_CASE_DOMAINS[substandard]: - allowed_domains.update(USE_CASE_DOMAINS[substandard][use_case]) - if domain_to_check in allowed_domains: + domain_to_check = self._get_domain_to_check(dataset_metadata) + if self._check_adam_domain(domain_to_check, substandard, use_cases): return True - return False + return self._check_domain_in_use_case(domain_to_check, use_cases, substandard) @classmethod def rule_applies_to_entity( From 8679d4261fec93afc3a5e480b53c5a3958c11bd3 Mon Sep 17 00:00:00 2001 From: Rakesh Date: Fri, 2 Jan 2026 20:12:38 -0500 Subject: [PATCH 2/4] Add unit tests for domain_is_custom operation --- .../test_operations/test_domain_is_custom.py | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_operations/test_domain_is_custom.py b/tests/unit/test_operations/test_domain_is_custom.py index fb4f06d3d..60b6e38e6 100644 --- a/tests/unit/test_operations/test_domain_is_custom.py +++ b/tests/unit/test_operations/test_domain_is_custom.py @@ -75,6 +75,46 @@ "3-4", True, ), + ( + PandasDataset.from_dict( + { + "STUDYID": [ + "TEST_STUDY", + "TEST_STUDY", + "TEST_STUDY", + ], + "AETERM": [ + "test", + "test", + "test", + ], + } + ), + "AE", + "tig", + "1-0", + False, + ), + ( + PandasDataset.from_dict( + { + "STUDYID": [ + "TEST_STUDY", + "TEST_STUDY", + "TEST_STUDY", + ], + "XYTERM": [ + "test", + "test", + "test", + ], + } + ), + "XY", + "tig", + "1-0", + True, + ), ], ) def test_domain_is_custom( @@ -85,9 +125,14 @@ def test_domain_is_custom( standard_version: str, expected: bool, ): - standard_metadata = { - "domains": {"AE"}, - } + if standard == "tig": + standard_metadata = { + "domains": {"AE", "DM", "CM", "LB", "VS", "EX", "DS", "MH"}, + } + else: + standard_metadata = { + "domains": {"AE"}, + } operation_params.dataframe = dataframe operation_params.domain = domain operation_params.standard = standard From 742d754463e611d8ca5276da1332deac53efceec Mon Sep 17 00:00:00 2001 From: Rakesh Date: Fri, 2 Jan 2026 20:25:29 -0500 Subject: [PATCH 3/4] Fix AttributeError when library_metadata is None in _is_custom_domain --- cdisc_rules_engine/utilities/rule_processor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cdisc_rules_engine/utilities/rule_processor.py b/cdisc_rules_engine/utilities/rule_processor.py index 7a2009252..caafeaa71 100644 --- a/cdisc_rules_engine/utilities/rule_processor.py +++ b/cdisc_rules_engine/utilities/rule_processor.py @@ -269,6 +269,8 @@ def rule_applies_to_class( return is_included and not is_excluded def _is_custom_domain(self, domain: str) -> bool: + if self.library_metadata is None: + return False return self.library_metadata.is_domain_custom(domain) def _get_allowed_domains_for_use_cases( From c83aeec939f94f142cc2c7871b40f83b2d7ced5f Mon Sep 17 00:00:00 2001 From: Rakesh Date: Wed, 7 Jan 2026 11:49:44 -0500 Subject: [PATCH 4/4] Add DaskDataset test cases for domain_is_custom operation --- .../test_operations/test_domain_is_custom.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/unit/test_operations/test_domain_is_custom.py b/tests/unit/test_operations/test_domain_is_custom.py index 60b6e38e6..c6a163a6c 100644 --- a/tests/unit/test_operations/test_domain_is_custom.py +++ b/tests/unit/test_operations/test_domain_is_custom.py @@ -75,6 +75,26 @@ "3-4", True, ), + ( + DaskDataset.from_dict( + { + "STUDYID": [ + "TEST_STUDY", + "TEST_STUDY", + "TEST_STUDY", + ], + "BCTERM": [ + "test", + "test", + "test", + ], + } + ), + "BC", + "sdtmig", + "3-4", + True, + ), ( PandasDataset.from_dict( { @@ -95,6 +115,26 @@ "1-0", False, ), + ( + DaskDataset.from_dict( + { + "STUDYID": [ + "TEST_STUDY", + "TEST_STUDY", + "TEST_STUDY", + ], + "AETERM": [ + "test", + "test", + "test", + ], + } + ), + "AE", + "tig", + "1-0", + False, + ), ( PandasDataset.from_dict( { @@ -115,6 +155,26 @@ "1-0", True, ), + ( + DaskDataset.from_dict( + { + "STUDYID": [ + "TEST_STUDY", + "TEST_STUDY", + "TEST_STUDY", + ], + "XYTERM": [ + "test", + "test", + "test", + ], + } + ), + "XY", + "tig", + "1-0", + True, + ), ], ) def test_domain_is_custom(