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
3 changes: 0 additions & 3 deletions cdisc_rules_engine/constants/domains.py
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
SUPPLEMENTARY_DOMAINS = ("SUPP", "SQ")
AP_DOMAIN: str = "AP"
APFA_DOMAIN: str = "APFA"
APRELSUB_DOMAIN: str = "APRELSUB"
1 change: 1 addition & 0 deletions cdisc_rules_engine/constants/rule_constants.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
ALL_KEYWORD: str = "ALL"
SPLIT_DATASETS_KEYWORD = "SPLIT DATASETS"
58 changes: 31 additions & 27 deletions cdisc_rules_engine/models/sdtm_dataset_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,28 @@ class SDTMDatasetMetadata(DatasetMetadata):

"""
Examples
| name | unsplit_name | is_supp | domain | wildcard_replacement | rdomain | is_ap | ap_suffix | domain_is_custom | related_domain | related_domain_is_custom |
| -------- | ------------ | ------- | ------ | -------------------- | ------- | ----- | --------- | ---------------- | -------------- | ------------------------ |
| QS | QS | False | QS | QS | None | False | | False | | |
| QSX | QS | False | QS | QS | None | False | | False | | |
| QSXX | QS | False | QS | QS | None | False | | False | | |
| SUPPQS | SUPPQS | True | None | | QS | False | | False | QS | |
| SUPPQSX | SUPPQS | True | None | | QS | False | | False | QS | |
| SUPPQSXX | SUPPQS | True | None | | QS | False | | False | QS | |
| APQS | APQS | False | APQS | QS | None | True | QS | False | QS | |
| APQSX | APQS | False | APQS | QS | None | True | QS | False | QS | |
| APQSXX | APQS | False | APQS | QS | None | True | QS | False | QS | |
| SQAPQS | SQAPQS | True | None | | APQS | True | | False | QS | |
| SQAPQSX | SQAPQS | True | None | | APQS | True | | False | QS | |
| SQAPQSXX | SQAPQS | True | None | | APQS | True | | False | | |
| RELREC | RELREC | False | None | | None | False | | False | | |
| XX | XX | False | XX | XX | None | False | | True | | |
| SUPPXX | SUPPXX | True | None | | XX | False | | False | XX | True |
| APXX | APXX | False | APXX | XX | None | True | XX | False | XX | True |
| SQAPXX | SQAPXX | True | None | | APXX | True | | False | XX | True |
| FA | FA | False | FA | FA | None | False | | False | | |
| name | unsplit_name | is_supp | is_split | domain | wildcard_replacement | rdomain | is_ap | ap_suffix | domain_is_custom | related_domain | related_domain_is_custom |
| --------- | ------------ | ------- | -------- | ------ | --------------------- | ------- | ----- | --------- | ---------------- | --------------- | ------------------------- |
| QS | QS | False | False | QS | QS | None | False | | False | | |
| QSX | QS | False | True | QS | QS | None | False | | False | | |
| QSXX | QS | False | True | QS | QS | None | False | | False | | |
| SUPPQS | SUPPQS | True | False | | | QS | False | | False | QS | |
| SUPPQSX | SUPPQS | True | True | | | QS | False | | False | QS | |
| SUPPQSXX | SUPPQS | True | True | | | QS | False | | False | QS | |
| APQS | APQS | False | False | QS | QS | None | True | QS | False | QS | |
| APQSX | APQS | False | True | QS | QS | None | True | QS | False | QS | |
| APQSXX | APQS | False | True | QS | QS | None | True | QS | False | QS | |
| SQAPQS | SQAPQS | True | False | | | APQS | False | | False | QS | |
| SQAPQSX | SQAPQS | True | True | | | APQS | False | | False | QS | |
| SQAPQSXX | SQAPQS | True | True | | | APQS | False | | False | | |
| RELREC | RELREC | False | False | | | None | False | | False | | |
| XX | XX | False | False | XX | XX | None | False | | True | | |
| SUPPXX | SUPPXX | True | False | | | XX | False | | False | XX | True |
| APXX | APXX | False | False | XX | XX | None | True | XX | False | XX | True |
| SQAPXX | SQAPXX | True | False | | | APXX | False | | False | XX | True |
| FA | FA | False | False | FA | FA | None | False | | False | | |
| APRELSUB | APRELSUB | False | False | | RELSUB | None | True | RELSUB | False | | |
| APRELSPEC | APRELSPEC | False | False | | RELSPEC | None | True | RELSPEC | False | | |
""" # noqa: E501 W291

@property
Expand Down Expand Up @@ -72,24 +74,20 @@ def is_ap(self) -> bool:
"""
Returns true if APID variable exists in first_record for non-supp datasets,
or if the dataset name starts with AP for non-supp datasets,
or if rdomain starts with AP for supp datasets.
"""
if self.is_supp:
return (
isinstance(self.rdomain, str)
and len(self.rdomain) == 4
and self.rdomain.startswith("AP")
)
return False
first_record = self.first_record or {}
if "APID" in first_record:
return True

return self.name.startswith("AP")

@property
def ap_suffix(self) -> str:
"""
Returns the 2-character suffix (characters 3-4) from AP domains.
Falls back to the dataset name when there's no DOMAIN variable
(e.g. APRELSUB, APRELSPEC)
Returns empty string if not an AP domain or for supp datasets.
"""
if not self.is_ap:
Expand All @@ -98,4 +96,10 @@ def ap_suffix(self) -> str:
return ""
if isinstance(self.domain, str) and len(self.domain) >= 4:
return self.domain[2::]
if (
isinstance(self.name, str)
and len(self.name) >= 4
and self.name.upper().startswith("AP")
):
return self.name[2::]
return ""
4 changes: 3 additions & 1 deletion cdisc_rules_engine/operations/domain_is_custom.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from cdisc_rules_engine.operations.base_operation import BaseOperation
from cdisc_rules_engine.utilities.sdtm_utilities import is_custom_domain
from cdisc_rules_engine.utilities.utils import get_dataset_standard_check_name


class DomainIsCustom(BaseOperation):
Expand All @@ -9,4 +10,5 @@ def _execute_operation(self):
given domain is in standard domains.
If no -> the domain is custom.
"""
return is_custom_domain(self.library_metadata, self.params.domain)
check_name = get_dataset_standard_check_name(self.params.dataframe_metadata)
return is_custom_domain(self.library_metadata, check_name)
58 changes: 28 additions & 30 deletions cdisc_rules_engine/services/data_services/base_data_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
from cdisc_rules_engine.services.data_readers import DataReaderFactory
from cdisc_rules_engine.utilities.utils import (
get_dataset_cache_key_from_path,
search_in_list,
replace_nan_values_in_df,
)
from cdisc_rules_engine.utilities.sdtm_utilities import (
Expand Down Expand Up @@ -242,47 +241,46 @@ def handle_custom_domains(
dataset: DatasetInterface,
dataset_metadata: SDTMDatasetMetadata,
):
if self._contains_topic_variable(dataset, dataset_metadata.domain, "TERM"):
class_name = self._detect_class_from_topic_variable(
dataset, dataset_metadata.domain
)
if class_name:
return class_name
if dataset_metadata.is_ap:
return self._get_associated_persons_inherit_class(dataset, dataset_metadata)
return None

def _detect_class_from_topic_variable(
self, dataset: DatasetInterface, domain_key: str
) -> Optional[str]:
if self._contains_topic_variable(dataset, domain_key, "TERM"):
return EVENTS
if self._contains_topic_variable(dataset, dataset_metadata.domain, "TRT"):
if self._contains_topic_variable(dataset, domain_key, "TRT"):
return INTERVENTIONS
if self._contains_topic_variable(dataset, dataset_metadata.domain, "QNAM"):
if self._contains_topic_variable(dataset, domain_key, "QNAM"):
return RELATIONSHIP
if self._contains_topic_variable(dataset, dataset_metadata.domain, "TESTCD"):
if self._contains_topic_variable(dataset, dataset_metadata.domain, "OBJ"):
if self._contains_topic_variable(dataset, domain_key, "TESTCD"):
if self._contains_topic_variable(dataset, domain_key, "OBJ"):
return FINDINGS_ABOUT
return FINDINGS
if dataset_metadata.is_ap:
return self._get_associated_persons_inherit_class(dataset_metadata)
return None

def _get_associated_persons_inherit_class(
self,
dataset: DatasetInterface,
dataset_metadata: SDTMDatasetMetadata,
):
"""
Check with inherit class AP-- belongs to.
"""
) -> Optional[str]:
ap_suffix = dataset_metadata.ap_suffix
if not ap_suffix:
return None
datasets = self.get_datasets()
if len(datasets) > 1:
new_dataset_metadata: SDTMDatasetMetadata = search_in_list(
datasets, lambda item: item.domain == ap_suffix
)
if new_dataset_metadata:
if new_dataset_metadata.is_ap:
raise ValueError("Nested Associated Persons domain reference")
new_dataset = self.get_dataset(dataset_name=new_dataset_metadata.name)
else:
raise ValueError("Filename for domain doesn't exist")
return self.get_dataset_class(
new_dataset,
new_dataset_metadata,
if self.library_metadata:
class_data, _ = get_class_and_dataset_metadata(
self.library_metadata,
ap_suffix,
)
else:
return None
name = (class_data or {}).get("name")
if name:
return convert_library_class_name_to_ct_class(name)

return self._detect_class_from_topic_variable(dataset, ap_suffix)

def _contains_topic_variable(
self,
Expand Down
104 changes: 23 additions & 81 deletions cdisc_rules_engine/utilities/rule_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@
FINDINGS_ABOUT,
FINDINGS,
)
from cdisc_rules_engine.constants.domains import (
AP_DOMAIN,
APFA_DOMAIN,
SUPPLEMENTARY_DOMAINS,
)
from cdisc_rules_engine.constants.rule_constants import ALL_KEYWORD
from cdisc_rules_engine.constants.use_cases import USE_CASE_DOMAINS
from cdisc_rules_engine.interfaces import ConditionInterface
Expand Down Expand Up @@ -60,55 +55,45 @@ def __init__(
self.cache = cache
self.library_metadata = library_metadata

DATASET_PROPERTY_KEYWORDS: dict = {
"SUPP--": lambda md: bool(md.is_supp),
"SQ--": lambda md: bool(md.is_supp),
"AP--": lambda md: bool(md.is_ap),
"SPLIT DATASETS": lambda md: bool(md.is_split),
"AP SPLIT DATASETS": lambda md: (
bool(md.is_ap) and bool(md.is_split) and not bool(md.is_supp)
),
}

@classmethod
def _matched_property_keyword(
cls, dataset_metadata: SDTMDatasetMetadata, domains_to_check: List[str]
) -> bool:
for keyword, predicate in cls.DATASET_PROPERTY_KEYWORDS.items():
if keyword in domains_to_check and predicate(dataset_metadata):
return True
return False

@classmethod
def rule_applies_to_domain(
cls, dataset_metadata: SDTMDatasetMetadata, rule: dict
) -> bool:
"""
Check that rule is applicable to dataset domain
"""
domains = rule.get("domains") or {}
include_split_datasets: bool = domains.get("include_split_datasets")

included_domains = domains.get("Include", [])
excluded_domains = domains.get("Exclude", [])

is_included = cls._is_domain_name_included(
dataset_metadata, included_domains, include_split_datasets
)
is_included = cls._is_domain_name_included(dataset_metadata, included_domains)
is_excluded = cls._is_domain_name_excluded(dataset_metadata, excluded_domains)

# additional check for split domains based on the flag
is_excluded, is_included = cls._handle_split_domains(
dataset_metadata.is_split,
include_split_datasets,
is_excluded,
is_included,
)

return is_included and not is_excluded

@classmethod
def _is_domain_name_included(
cls,
dataset_metadata: SDTMDatasetMetadata,
included_domains: List[str],
include_split_datasets: bool,
) -> bool:
"""
If included domains aren't specified
and include_split_datasets is True,
and it is not a split dataset
-> domain is not included
If included domains are specified,
and the domain is not in the list of included domains,
and domain doesn't match with AP / APFA / APRELSUB / SUPP / SQ naming pattern
-> domain is not included.
In other cases domain is included
"""
if not included_domains:
if include_split_datasets is True and not dataset_metadata.is_split:
return False
return True

if (
Expand All @@ -117,7 +102,7 @@ def _is_domain_name_included(
or ALL_KEYWORD in included_domains
):
return True
if cls._domain_matched_ap_or_supp(dataset_metadata, included_domains):
if cls._matched_property_keyword(dataset_metadata, included_domains):
return True
return False

Expand All @@ -128,7 +113,6 @@ def _is_domain_name_excluded(
"""
If excluded domains are specified,
and the domain is in the list of excluded domains,
or domain name match with AP / APFA / APRELSUB / SUPP / SQ naming pattern
domain is excluded.

In other cases domain is not excluded.
Expand All @@ -143,49 +127,7 @@ def _is_domain_name_excluded(
or ALL_KEYWORD in excluded_domains
):
return True
if cls._domain_matched_ap_or_supp(dataset_metadata, excluded_domains):
return True
return False

@classmethod
def _handle_split_domains(
cls,
is_split_domain: bool,
include_split_datasets: bool,
is_excluded: bool,
is_included: bool,
) -> Tuple[bool, bool]:
"""
HANDLING SPLIT DOMAINS

If include_split_datasets is True -
add split domains to the list of included domains.
If no included domains specified, only validate split domains

If include_split_datasets is False - Exclude split domains
If include_split_datasets is None - Do nothing
"""
if include_split_datasets is True and is_split_domain and not is_excluded:
is_included = True
if include_split_datasets is False and is_split_domain:
is_excluded = True
return is_excluded, is_included

@classmethod
def _domain_matched_ap_or_supp(
cls, dataset_metadata: SDTMDatasetMetadata, domains_to_check: List[str]
) -> bool:
"""
Check that domain name match with only
AP / APFA / APRELSUB / SUPP / SQ naming pattern
"""
domains_to_check = set(domains_to_check)
supp_domains = {f"{domain}--" for domain in SUPPLEMENTARY_DOMAINS}
ap_domains = {f"{AP_DOMAIN}--", f"{APFA_DOMAIN}--"}

if dataset_metadata.is_supp and (domains_to_check & supp_domains):
return True
if dataset_metadata.is_ap and (domains_to_check & ap_domains):
if cls._matched_property_keyword(dataset_metadata, excluded_domains):
return True
return False

Expand Down Expand Up @@ -214,7 +156,7 @@ def rule_applies_to_data_structure(
def rule_applies_to_class(
self,
rule,
dataset_metadata: SDTMDatasetMetadata,
dataset_metadata: "SDTMDatasetMetadata",
):
"""
If included classes are specified and the class
Expand Down
13 changes: 13 additions & 0 deletions cdisc_rules_engine/utilities/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from cdisc_rules_engine.models.base_validation_entity import BaseValidationEntity
from cdisc_rules_engine.check_operators.helpers import is_valid_date
from cdisc_rules_engine.constants.adam_products import ADAM_PRODUCTS
from cdisc_rules_engine.models.sdtm_dataset_metadata import SDTMDatasetMetadata


def convert_dataclass_to_superclass[T](instance: object, superclass: type[T]) -> T:
Expand Down Expand Up @@ -414,3 +415,15 @@ def custom_str_conversion(x):
elif isinstance(x, float):
return f"{x:.0f}" if x.is_integer() else str(x).strip()
return x


def get_dataset_standard_check_name(
dataset_metadata: Optional[SDTMDatasetMetadata],
) -> Optional[str]:
if dataset_metadata is None:
return None
if dataset_metadata.is_supp:
return "SUPPQUAL"
if dataset_metadata.is_ap:
return dataset_metadata.ap_suffix or None
return dataset_metadata.domain or dataset_metadata.name
Loading
Loading