Skip to content

Commit cdf1562

Browse files
Adding more utils around scenario 1
1 parent 21fd80c commit cdf1562

14 files changed

+617
-134
lines changed

classes/episode_result_type.py

Lines changed: 113 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,116 @@
1+
from typing import Optional, Dict
2+
3+
14
class EpisodeResultType:
2-
"""
3-
Utility class for mapping episode result type descriptions to logical flags or valid value IDs.
4-
5-
This class provides:
6-
- Logical flags for "null", "not_null", and "any_surveillance_non_participation".
7-
- A mapping from descriptive result labels (e.g., "normal", "abnormal", "surveillance offered") to internal valid value IDs.
8-
- A method to convert descriptions to flags or IDs.
9-
10-
Methods:
11-
from_description(description: str) -> str | int:
12-
Returns the logical flag or the valid value ID for a given description.
13-
Raises ValueError if the description is not recognized.
14-
"""
15-
16-
NULL = "null"
17-
NOT_NULL = "not_null"
18-
ANY_SURVEILLANCE_NON_PARTICIPATION = "any_surveillance_non_participation"
19-
20-
_label_to_id = {
21-
"normal": 9501,
22-
"abnormal": 9502,
23-
"surveillance offered": 9503,
24-
# Add real mappings as needed
25-
}
5+
NO_RESULT = (20311, "No Result")
6+
NORMAL = (20312, "Normal (No Abnormalities Found)")
7+
LOW_RISK_ADENOMA = (20314, "Low-risk Adenoma")
8+
INTERMEDIATE_RISK_ADENOMA = (20315, "Intermediate-risk Adenoma")
9+
HIGH_RISK_ADENOMA = (20316, "High-risk Adenoma")
10+
CANCER_DETECTED = (20317, "Cancer Detected")
11+
ABNORMAL = (20313, "Abnormal")
12+
CANCER_NOT_CONFIRMED = (305001, "Cancer not confirmed")
13+
HIGH_RISK_FINDINGS = (305606, "High-risk findings")
14+
LNPCP = (305607, "LNPCP")
15+
BOWEL_SCOPE_NON_PARTICIPATION = (605002, "Bowel scope non-participation")
16+
FOBT_INADEQUATE_PARTICIPATION = (605003, "FOBt inadequate participation")
17+
DEFINITIVE_NORMAL_FOBT_OUTCOME = (605005, "Definitive normal FOBt outcome")
18+
DEFINITIVE_ABNORMAL_FOBT_OUTCOME = (605006, "Definitive abnormal FOBt outcome")
19+
HIGH_RISK_FINDINGS_SURVEILLANCE_NON_PARTICIPATION = (
20+
305619,
21+
"High-risk findings Surveillance non-participation",
22+
)
23+
LNPCP_SURVEILLANCE_NON_PARTICIPATION = (
24+
305618,
25+
"LNPCP Surveillance non-participation",
26+
)
27+
HIGH_RISK_SURVEILLANCE_NON_PARTICIPATION = (
28+
605004,
29+
"High-risk Surveillance non-participation",
30+
)
31+
INTERMEDIATE_RISK_SURVEILLANCE_NON_PARTICIPATION = (
32+
605007,
33+
"Intermediate-risk Surveillance non-participation",
34+
)
35+
LYNCH_NON_PARTICIPATION = (305688, "Lynch non-participation")
36+
ANY_SURVEILLANCE_NON_PARTICIPATION = (0, "(Any) Surveillance non-participation")
37+
NULL = (0, "Null")
38+
NOT_NULL = (0, "Not Null")
39+
40+
_all_types = [
41+
NO_RESULT,
42+
NORMAL,
43+
LOW_RISK_ADENOMA,
44+
INTERMEDIATE_RISK_ADENOMA,
45+
HIGH_RISK_ADENOMA,
46+
CANCER_DETECTED,
47+
ABNORMAL,
48+
CANCER_NOT_CONFIRMED,
49+
HIGH_RISK_FINDINGS,
50+
LNPCP,
51+
BOWEL_SCOPE_NON_PARTICIPATION,
52+
FOBT_INADEQUATE_PARTICIPATION,
53+
DEFINITIVE_NORMAL_FOBT_OUTCOME,
54+
DEFINITIVE_ABNORMAL_FOBT_OUTCOME,
55+
HIGH_RISK_FINDINGS_SURVEILLANCE_NON_PARTICIPATION,
56+
LNPCP_SURVEILLANCE_NON_PARTICIPATION,
57+
HIGH_RISK_SURVEILLANCE_NON_PARTICIPATION,
58+
INTERMEDIATE_RISK_SURVEILLANCE_NON_PARTICIPATION,
59+
LYNCH_NON_PARTICIPATION,
60+
ANY_SURVEILLANCE_NON_PARTICIPATION,
61+
NULL,
62+
NOT_NULL,
63+
]
64+
65+
_descriptions: Dict[str, "EpisodeResultType"] = {}
66+
_lowercase_descriptions: Dict[str, "EpisodeResultType"] = {}
67+
_valid_value_ids: Dict[int, "EpisodeResultType"] = {}
68+
69+
def __init__(self, valid_value_id: int, description: str):
70+
self.valid_value_id = valid_value_id
71+
self.description = description
72+
73+
@classmethod
74+
def _init_types(cls):
75+
if not cls._descriptions:
76+
for valid_value_id, description in cls._all_types:
77+
instance = cls(valid_value_id, description)
78+
cls._descriptions[description] = instance
79+
cls._lowercase_descriptions[description.lower()] = instance
80+
# Only map the first occurrence of each valid_value_id
81+
if valid_value_id not in cls._valid_value_ids:
82+
cls._valid_value_ids[valid_value_id] = instance
2683

2784
@classmethod
28-
def from_description(cls, description: str):
29-
"""
30-
Returns the logical flag or the valid value ID for a given description.
31-
32-
Args:
33-
description (str): The episode result type description.
34-
35-
Returns:
36-
str | int: The logical flag or the valid value ID.
37-
38-
Raises:
39-
ValueError: If the description is not recognized.
40-
"""
41-
key = description.strip().lower()
42-
if key in {cls.NULL, cls.NOT_NULL, cls.ANY_SURVEILLANCE_NON_PARTICIPATION}:
43-
return key
44-
if key in cls._label_to_id:
45-
return cls._label_to_id[key]
46-
raise ValueError(f"Unknown episode result type: '{description}'")
85+
def by_description(cls, description: str) -> Optional["EpisodeResultType"]:
86+
cls._init_types()
87+
return cls._descriptions.get(description)
88+
89+
@classmethod
90+
def by_description_case_insensitive(
91+
cls, description: str
92+
) -> Optional["EpisodeResultType"]:
93+
cls._init_types()
94+
return cls._lowercase_descriptions.get(description.lower())
95+
96+
@classmethod
97+
def by_valid_value_id(cls, valid_value_id: int) -> Optional["EpisodeResultType"]:
98+
cls._init_types()
99+
return cls._valid_value_ids.get(valid_value_id)
100+
101+
def get_id(self) -> int:
102+
return self.valid_value_id
103+
104+
def get_description(self) -> str:
105+
return self.description
106+
107+
def __eq__(self, other):
108+
if isinstance(other, EpisodeResultType):
109+
return (
110+
self.valid_value_id == other.valid_value_id
111+
and self.description == other.description
112+
)
113+
return False
114+
115+
def __repr__(self):
116+
return f"EpisodeResultType({self.valid_value_id}, '{self.description}')"

classes/organisation_complex.py

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,17 @@
1+
from typing import Optional
2+
3+
14
class Organisation:
25
"""
36
Class representing an organisation with id, name, and code.
47
"""
58

6-
def __init__(self, new_id: int, new_name: str, new_code: str):
9+
def __init__(
10+
self,
11+
new_id: Optional[int] = None,
12+
new_name: Optional[str] = None,
13+
new_code: Optional[str] = None,
14+
):
715
self.id = new_id
816
self.name = new_name
917
self.code = new_code
10-
11-
def get_name(self) -> str:
12-
"""Returns the organisation name"""
13-
return self.name
14-
15-
def set_name(self, name: str) -> None:
16-
"""Sets the organisation name"""
17-
self.name = name
18-
19-
def get_id(self) -> int:
20-
"""Returns the organisation id"""
21-
return self.id
22-
23-
def set_id(self, id_: int) -> None:
24-
"""Sets the organisation id"""
25-
self.id = id_
26-
27-
def get_code(self) -> str:
28-
"""Returns the organisation code"""
29-
return self.code
30-
31-
def set_code(self, code: str) -> None:
32-
"""Sets the organisation code"""
33-
self.code = code
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from dataclasses import dataclass
2+
from datetime import date
3+
from typing import Optional
4+
5+
6+
@dataclass
7+
class DatabaseTransitionParameters:
8+
"""
9+
Data class for holding parameters for a database transition.
10+
"""
11+
12+
subject_id: Optional[int] = None
13+
event_id: Optional[int] = None
14+
key_id: Optional[int] = None
15+
appointment_inst_id: Optional[int] = None
16+
kit_id: Optional[int] = None
17+
transition_id: Optional[int] = None
18+
screening_status_reason_id: Optional[int] = None
19+
episode_status_reason_id: Optional[int] = None
20+
clinical_change_reason_id: Optional[int] = None
21+
notes: Optional[str] = None
22+
cease_date: Optional[date] = None
23+
diagnostic_test_proposed_date: Optional[date] = None
24+
diagnostic_test_confirmed_date: Optional[date] = None
25+
episode_closure_reason_id: Optional[int] = None
26+
user_id: Optional[int] = None
27+
rollback_on_failure: Optional[str] = None
28+
diagnostic_test_proposed_type_id: Optional[int] = None
29+
diagnostic_test_id: Optional[int] = None
30+
surgery_date: Optional[date] = None
31+
recall_interval: Optional[int] = None
32+
discharge_id: Optional[int] = None
33+
recall_calculation_method_id: Optional[int] = None
34+
recall_test_id: Optional[int] = None
35+
comms_initial_event_id: Optional[int] = None
36+
ecomm_lett_batch_record_id: Optional[int] = None
37+
ecomm_batch_letter_id: Optional[int] = None
38+
intended_extend_id: Optional[int] = None
39+
surgery_result_id: Optional[int] = None
40+
referral_type_id: Optional[int] = None
41+
additional_data_id1: Optional[int] = None
42+
additional_data_date1: Optional[date] = None
43+
44+
def to_params_list(self) -> list:
45+
"""
46+
Returns the parameters as a list in the order expected by the stored procedure.
47+
"""
48+
return [
49+
self.subject_id, # p_screening_subject_id
50+
self.event_id, # p_event_id
51+
self.key_id, # p_key_id
52+
self.appointment_inst_id, # p_appointment_inst_id
53+
self.kit_id, # p_kitid
54+
self.transition_id, # p_event_transition_id
55+
self.screening_status_reason_id, # p_ss_reason_for_change_id
56+
self.episode_status_reason_id, # p_episode_status_reason_id
57+
self.clinical_change_reason_id, # p_clinical_rsn_closure_id
58+
self.notes, # p_notes
59+
self.cease_date, # p_ceased_confirm_recd_date
60+
self.diagnostic_test_proposed_date, # p_external_test_proposed_date
61+
self.diagnostic_test_confirmed_date, # p_external_test_date
62+
self.episode_closure_reason_id, # p_uncnfrmd_rsn_epis_close_id
63+
self.user_id, # p_pio_id
64+
self.rollback_on_failure, # p_rollback_on_failure
65+
self.diagnostic_test_proposed_type_id, # p_proposed_type_id
66+
self.diagnostic_test_id, # p_ext_test_id
67+
self.surgery_date, # p_surgery_date
68+
self.recall_interval, # p_alternative_scr_interval
69+
self.discharge_id, # p_pd_id
70+
self.recall_calculation_method_id, # p_recall_calculation_method_id
71+
self.recall_test_id, # p_recall_ext_test_id
72+
self.comms_initial_event_id, # p_comms_initial_event_id
73+
self.ecomm_lett_batch_record_id, # p_ecomm_lett_batch_record_id
74+
self.ecomm_batch_letter_id, # p_ecomm_batch_letter_id
75+
self.intended_extend_id, # p_intended_extent_id
76+
self.surgery_result_id, # p_surgery_result_id
77+
self.referral_type_id, # p_screening_referral_type_id
78+
self.additional_data_id1, # p_additional_data_id_1
79+
self.additional_data_date1, # p_additional_data_date_1
80+
None, # po_error_cur (OUT)
81+
None, # po_data_cur (OUT)
82+
]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from classes.repositories.database_transition_parameters import (
2+
DatabaseTransitionParameters,
3+
)
4+
import logging
5+
from utils.oracle.oracle import OracleDB
6+
import oracledb
7+
8+
9+
class GeneralRepository:
10+
"""
11+
Repository for general database operations.
12+
"""
13+
14+
def __init__(self):
15+
self.oracle_db = OracleDB()
16+
17+
def run_database_transition(
18+
self, database_transition_parameters: DatabaseTransitionParameters
19+
) -> None:
20+
"""
21+
Executes the PKG_EPISODE.p_set_episode_next_status stored procedure with the provided parameters.
22+
"""
23+
logging.info(
24+
f"Running database transition with transition_id = {database_transition_parameters.transition_id}"
25+
)
26+
conn = self.oracle_db.connect_to_db()
27+
try:
28+
cursor = conn.cursor()
29+
procedure_name = "PKG_EPISODE.p_set_episode_next_status"
30+
params = database_transition_parameters.to_params_list()
31+
params[-2] = cursor.var(oracledb.CURSOR) # po_error_cur
32+
params[-1] = cursor.var(oracledb.CURSOR) # po_data_cur
33+
34+
cursor.callproc(procedure_name, params)
35+
36+
# Fetch output from the cursors
37+
error_cursor = params[-2].getvalue()
38+
error_rows = error_cursor.fetchall() if error_cursor is not None else []
39+
40+
success = any(
41+
"The action was performed successfully" in str(row)
42+
for row in error_rows
43+
)
44+
45+
assert success, f"Error when executing database transition: {error_rows}"
46+
47+
conn.commit()
48+
logging.info("Database transition executed successfully.")
49+
except Exception as e:
50+
raise oracledb.DatabaseError(f"Error executing database transition: {e}")
51+
finally:
52+
self.oracle_db.disconnect_from_db(conn)

classes/repositories/subject_repository.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ def process_pi_subject(self, pio_id: int, pi_subject: PISubject) -> Optional[int
8282
out_params = [int, int, str] # contact_id, error_id, error_text
8383

8484
result = self.oracle_db.execute_stored_procedure(
85-
procedure, in_params=in_params, out_params=out_params, conn=conn
85+
procedure,
86+
in_params=in_params,
87+
out_params=out_params,
88+
conn=conn,
8689
)
8790

8891
new_contact_id = result.get(3)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import logging
2+
from typing import List, Optional, Any
3+
from utils.oracle.oracle import OracleDB
4+
from classes.user_role_type import UserRoleType
5+
6+
7+
class UserRepository:
8+
"""
9+
Repository class handling database access for users.
10+
"""
11+
12+
def __init__(self):
13+
self.oracle_db = OracleDB()
14+
15+
def get_pio_id_for_role(self, role: "UserRoleType") -> Optional[int]:
16+
"""
17+
Get the PIO ID for the role.
18+
"""
19+
logging.info(f"Getting PIO ID for role: {role.user_code}")
20+
sql = """
21+
SELECT
22+
pio.pio_id
23+
FROM person_in_org pio
24+
INNER JOIN person prs ON prs.prs_id = pio.prs_id
25+
INNER JOIN org ON org.org_id = pio.org_id
26+
WHERE prs.oe_user_code = :user_code
27+
AND org.org_code = :org_code
28+
AND pio.role_id = :role_id
29+
AND pio.is_bcss_user = 1
30+
AND TRUNC(SYSDATE) BETWEEN TRUNC(pio.start_date) AND NVL(pio.end_date, SYSDATE)
31+
"""
32+
params = {
33+
"user_code": role.user_code,
34+
"org_code": role.org_code,
35+
"role_id": role.role_id,
36+
}
37+
df = self.oracle_db.execute_query(sql, params)
38+
if not df.empty:
39+
pio_id = int(df["pio_id"].iloc[0])
40+
return pio_id
41+
return None

0 commit comments

Comments
 (0)