Skip to content

Commit 159a730

Browse files
Addressing SonarQube issues and altering logic
1 parent 7be7537 commit 159a730

File tree

4 files changed

+114
-88
lines changed

4 files changed

+114
-88
lines changed

classes/subject.py

Lines changed: 79 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,93 +1251,105 @@ def from_dataframe_row(row: pd.Series) -> "Subject":
12511251
Only fields present in the SQL query are populated.
12521252
"""
12531253

1254-
def parse_date(
1255-
val: Optional[Union[pd.Timestamp, str, datetime, date]],
1256-
) -> Optional[date]:
1257-
"""
1258-
Converts a value to a Python date object if possible.
1259-
1260-
Args:
1261-
val: The value to convert (can be pandas.Timestamp, string, datetime, date, or None).
1262-
1263-
Returns:
1264-
Optional[date]: The converted date object, or None if conversion fails.
1265-
"""
1266-
if pd.isnull(val):
1267-
return None
1268-
if isinstance(val, pd.Timestamp):
1269-
return val.to_pydatetime().date()
1270-
if isinstance(val, str):
1271-
try:
1272-
return datetime.strptime(val[:10], "%Y-%m-%d").date()
1273-
except Exception:
1274-
return None
1275-
if isinstance(val, datetime):
1276-
return val.date()
1277-
if isinstance(val, date):
1278-
return val
1279-
return None
1280-
1281-
def parse_datetime(
1282-
val: Optional[Union[pd.Timestamp, str, datetime, date]],
1283-
) -> Optional[datetime]:
1284-
"""
1285-
Converts a value to a Python datetime object if possible.
1286-
1287-
Args:
1288-
val: The value to convert (can be pandas.Timestamp, string, datetime, or None).
1289-
1290-
Returns:
1291-
Optional[datetime]: The converted datetime object, or None if conversion fails.
1292-
"""
1293-
if pd.isnull(val):
1294-
return None
1295-
if isinstance(val, pd.Timestamp):
1296-
return val.to_pydatetime()
1297-
if isinstance(val, str):
1298-
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
1299-
try:
1300-
return datetime.strptime(val[:19], fmt)
1301-
except Exception:
1302-
continue
1303-
return None
1304-
if isinstance(val, datetime):
1305-
return val
1306-
return None
1307-
13081254
field_map = {
13091255
"screening_subject_id": row.get("screening_subject_id"),
13101256
"nhs_number": row.get("subject_nhs_number"),
13111257
"surname": row.get("person_family_name"),
13121258
"forename": row.get("person_given_name"),
1313-
"datestamp": parse_datetime(row.get("datestamp")),
1259+
"datestamp": Subject.parse_datetime(row.get("datestamp")),
13141260
"screening_status_id": row.get("screening_status_id"),
13151261
"screening_status_change_reason_id": row.get("ss_reason_for_change_id"),
1316-
"screening_status_change_date": parse_date(
1262+
"screening_status_change_date": Subject.parse_date(
13171263
row.get("screening_status_change_date")
13181264
),
1319-
"screening_due_date": parse_date(row.get("screening_due_date")),
1265+
"screening_due_date": Subject.parse_date(row.get("screening_due_date")),
13201266
"screening_due_date_change_reason_id": row.get("sdd_reason_for_change_id"),
1321-
"screening_due_date_change_date": parse_date(row.get("sdd_change_date")),
1322-
"calculated_screening_due_date": parse_date(row.get("calculated_sdd")),
1323-
"surveillance_screening_due_date": parse_date(
1267+
"screening_due_date_change_date": Subject.parse_date(
1268+
row.get("sdd_change_date")
1269+
),
1270+
"calculated_screening_due_date": Subject.parse_date(
1271+
row.get("calculated_sdd")
1272+
),
1273+
"surveillance_screening_due_date": Subject.parse_date(
13241274
row.get("surveillance_screen_due_date")
13251275
),
1326-
"calculated_surveillance_due_date": parse_date(row.get("calculated_ssdd")),
1276+
"calculated_surveillance_due_date": Subject.parse_date(
1277+
row.get("calculated_ssdd")
1278+
),
13271279
"surveillance_due_date_change_reason_id": row.get(
13281280
"surveillance_sdd_rsn_change_id"
13291281
),
1330-
"surveillance_due_date_change_date": parse_date(
1282+
"surveillance_due_date_change_date": Subject.parse_date(
13311283
row.get("surveillance_sdd_change_date")
13321284
),
1333-
"lynch_due_date": parse_date(row.get("lynch_screening_due_date")),
1285+
"lynch_due_date": Subject.parse_date(row.get("lynch_screening_due_date")),
13341286
"lynch_due_date_change_reason_id": row.get(
13351287
"lynch_sdd_reason_for_change_id"
13361288
),
1337-
"lynch_due_date_change_date": parse_date(row.get("lynch_sdd_change_date")),
1338-
"calculated_lynch_due_date": parse_date(row.get("lynch_calculated_sdd")),
1339-
"date_of_birth": parse_date(row.get("date_of_birth")),
1340-
"date_of_death": parse_date(row.get("date_of_death")),
1289+
"lynch_due_date_change_date": Subject.parse_date(
1290+
row.get("lynch_sdd_change_date")
1291+
),
1292+
"calculated_lynch_due_date": Subject.parse_date(
1293+
row.get("lynch_calculated_sdd")
1294+
),
1295+
"date_of_birth": Subject.parse_date(row.get("date_of_birth")),
1296+
"date_of_death": Subject.parse_date(row.get("date_of_death")),
13411297
}
13421298

13431299
return Subject(**field_map)
1300+
1301+
@staticmethod
1302+
def parse_date(
1303+
val: Optional[Union[pd.Timestamp, str, datetime, date]],
1304+
) -> Optional[date]:
1305+
"""
1306+
Converts a value to a Python date object if possible.
1307+
1308+
Args:
1309+
val: The value to convert (can be pandas.Timestamp, string, datetime, date, or None).
1310+
1311+
Returns:
1312+
Optional[date]: The converted date object, or None if conversion fails.
1313+
"""
1314+
if pd.isnull(val):
1315+
return None
1316+
if isinstance(val, pd.Timestamp):
1317+
return val.to_pydatetime().date()
1318+
if isinstance(val, str):
1319+
try:
1320+
return datetime.strptime(val[:10], "%Y-%m-%d").date()
1321+
except Exception:
1322+
return None
1323+
if isinstance(val, datetime):
1324+
return val.date()
1325+
if isinstance(val, date):
1326+
return val
1327+
return None
1328+
1329+
@staticmethod
1330+
def parse_datetime(
1331+
val: Optional[Union[pd.Timestamp, str, datetime, date]],
1332+
) -> Optional[datetime]:
1333+
"""
1334+
Converts a value to a Python datetime object if possible.
1335+
1336+
Args:
1337+
val: The value to convert (can be pandas.Timestamp, string, datetime, or None).
1338+
1339+
Returns:
1340+
Optional[datetime]: The converted datetime object, or None if conversion fails.
1341+
"""
1342+
if pd.isnull(val):
1343+
return None
1344+
if isinstance(val, pd.Timestamp):
1345+
return val.to_pydatetime()
1346+
if isinstance(val, str):
1347+
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
1348+
try:
1349+
return datetime.strptime(val[:19], fmt)
1350+
except Exception:
1351+
continue
1352+
return None
1353+
if isinstance(val, datetime):
1354+
return val
1355+
return None

docs/utility-guides/SubjectAssertion.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ It is designed to assert that a subject with a given NHS number matches specifie
2222
## Overview
2323

2424
The `subject_assertion` function is used to verify that a subject in the database matches a set of criteria.
25-
If the subject does not match all criteria, the function will iteratively remove criteria (except NHS number) and retry, logging any criteria that caused the assertion to fail.
25+
If the subject does not match all criteria, the function will iteratively loop through each criteria (except NHS number), logging any criteria that caused the assertion to fail.
2626

2727
---
2828

@@ -36,8 +36,8 @@ If the subject does not match all criteria, the function will iteratively remove
3636
## How It Works
3737

3838
1. The function first checks if the subject with the given NHS number matches all provided criteria.
39-
2. If not, it removes one criterion at a time (except NHS number) and retries the assertion.
40-
3. This process continues until either a match is found or all criteria (except NHS number) have been removed.
39+
2. If not, it removes checks one criterion at a time and retries the assertion.
40+
3. This process continues until all criteria have been checked.
4141
4. If a match is found only after removing criteria, the failed criteria are logged.
4242
5. The function returns `True` only if all criteria match on the first attempt; otherwise, it returns `False`.
4343

tests_utils/test_subject_assertion_util.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,27 @@
33

44
pytestmark = [pytest.mark.utils_local]
55

6+
nhs_number = "9233639266"
7+
68

79
def test_subject_assertion_true():
8-
nhs_number = "9233639266"
910
criteria = {"screening status": "Inactive", "subject age": "> 28"}
1011
assert subject_assertion(nhs_number, criteria) is True
1112

1213

1314
def test_subject_assertion_false():
14-
nhs_number = "9233639266"
1515
criteria = {"screening status": "Call", "subject age": "< 28"}
1616
assert subject_assertion(nhs_number, criteria) is False
1717

1818

1919
def test_subject_assertion_false_with_some_true():
20-
nhs_number = "9233639266"
21-
criteria = {"screening status": "Inactive", "subject age": "< 28"}
20+
criteria = {
21+
"screening status": "Inactive",
22+
"subject age": "> 28",
23+
"latest episode type": "FOBT",
24+
"latest episode status": "Open",
25+
"latest episode has referral date": "Past",
26+
"latest episode has diagnosis date": "No",
27+
"latest episode diagnosis date reason": "NULL",
28+
}
2229
assert subject_assertion(nhs_number, criteria) is False

utils/subject_assertion.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ def subject_assertion(nhs_number: str, criteria: dict) -> bool:
1414
Returns:
1515
bool: True if the subject matches the provided criteria, False if it does not.
1616
"""
17-
nhs_no_criteria = {"nhs number": nhs_number}
17+
nhs_number_string = "nhs number"
18+
subject_nhs_number_string = "subject_nhs_number"
19+
nhs_no_criteria = {nhs_number_string: nhs_number}
1820
subject = Subject()
1921
user = User()
2022
builder = SubjectSelectionQueryBuilder()
@@ -29,40 +31,45 @@ def subject_assertion(nhs_number: str, criteria: dict) -> bool:
2931
subject_df = OracleDB().execute_query(query, bind_vars)
3032
subject = Subject.from_dataframe_row(subject_df.iloc[0])
3133

32-
criteria["nhs number"] = nhs_number
34+
criteria[nhs_number_string] = nhs_number
35+
36+
# Check all criteria together first
3337
query, bind_vars = builder.build_subject_selection_query(
3438
criteria=criteria,
3539
user=user,
3640
subject=subject,
3741
subjects_to_retrieve=1,
3842
)
39-
4043
df = OracleDB().execute_query(query, bind_vars)
41-
42-
if nhs_number in df["subject_nhs_number"].values:
44+
if nhs_number in df[subject_nhs_number_string].values:
4345
return True
4446

45-
# Try removing criteria one by one (except nhs number)
47+
# Check each criterion independently
4648
failed_criteria = []
47-
criteria_keys = [key for key in criteria if key != "nhs number"]
49+
criteria_keys = [key for key in criteria if key != nhs_number_string]
4850
for key in criteria_keys:
49-
reduced_criteria = {key: value for key, value in criteria.items() if key != key}
51+
single_criteria = {nhs_number_string: nhs_number, key: criteria[key]}
5052
query, bind_vars = builder.build_subject_selection_query(
51-
criteria=reduced_criteria,
53+
criteria=single_criteria,
5254
user=user,
5355
subject=subject,
5456
subjects_to_retrieve=1,
5557
)
5658
df = OracleDB().execute_query(query, bind_vars)
57-
if nhs_number in df["subject_nhs_number"].values:
58-
failed_criteria.append((key, criteria[key]))
59-
break
60-
else:
59+
if (
60+
subject_nhs_number_string not in df.columns
61+
or nhs_number not in df[subject_nhs_number_string].values
62+
):
6163
failed_criteria.append((key, criteria[key]))
6264

6365
if failed_criteria:
6466
log_message = "Subject Assertion Failed\nFailed criteria:\n" + "\n".join(
65-
[f"Key: '{key}' - Value: '{value}'" for key, value in failed_criteria]
67+
[f"{key}, {value}" for key, value in failed_criteria]
6668
)
6769
logging.error(log_message)
70+
else:
71+
logging.error(
72+
"Subject Assertion Failed: Criteria combination is invalid or conflicting."
73+
)
74+
6875
return False

0 commit comments

Comments
 (0)