Skip to content

Commit 25ba8fd

Browse files
committed
Accept worklist.create_test_item actions and process with Modality Emulator
Processes test worklist items immediately sending emulated DICOM files with sample images via the uploader.
1 parent f547802 commit 25ba8fd

4 files changed

Lines changed: 84 additions & 4 deletions

File tree

src/modality_emulator.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def __init__(self, mwl_storage: MWLStorage):
128128
self.mwl_storage = mwl_storage
129129
self.processed_items = set()
130130

131-
def process_worklist_items(self, ae: AE):
131+
def process_worklist_items(self, ae: AE, patient_name: str | None = None):
132132
"""
133133
Queries the MWL for items scheduled for today and sends generated DICOM files to the PACS server for each item.
134134
"""
@@ -140,7 +140,8 @@ def process_worklist_items(self, ae: AE):
140140
logger.info(f"Connected to PACS server {PACS_HOST}:{PACS_PORT} ({PACS_AET})")
141141

142142
logger.info("Querying MWL for scheduled items...")
143-
responses = mwl_assoc.send_c_find(self.c_find_dataset, query_model=ModalityWorklistInformationFind)
143+
c_find_dataset = self.c_find_dataset(patient_name=patient_name)
144+
responses = mwl_assoc.send_c_find(c_find_dataset, query_model=ModalityWorklistInformationFind)
144145
for status, ds in responses:
145146
status_code = getattr(status, "Status", SUCCESS)
146147

@@ -189,15 +190,18 @@ def process_worklist_items(self, ae: AE):
189190
mwl_assoc.release()
190191
pacs_assoc.release()
191192

192-
@property
193-
def c_find_dataset(self) -> Dataset:
193+
def c_find_dataset(self, patient_name: str | None = None) -> Dataset:
194194
date_today = datetime.date.today()
195195
ds = Dataset()
196196
sps_dataset = Dataset()
197197
sps_dataset.Modality = MODALITY
198198
sps_dataset.ScheduledProcedureStepStartDate = date_today.strftime("%Y%m%d")
199199
sps_dataset.ScheduledProcedureStepStartTime = "000000-"
200200
ds.ScheduledProcedureStepSequence = [sps_dataset]
201+
202+
if patient_name:
203+
ds.PatientName = patient_name
204+
201205
return ds
202206

203207

src/relay_listener.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@
1616

1717
from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
1818
from dotenv import load_dotenv
19+
from pynetdicom import AE
20+
from pynetdicom.sop_class import (
21+
DigitalMammographyXRayImageStorageForPresentation, # type: ignore
22+
ModalityWorklistInformationFind, # type: ignore
23+
)
1924
from websockets.asyncio.client import connect
2025
from websockets.exceptions import ConnectionClosedError
2126

2227
from environment import Environment
28+
from modality_emulator import ModalityEmulator
2329
from services.mwl.create_worklist_item import CreateWorklistItem
2430
from services.mwl.update_worklist_item_status import UpdateWorklistItemStatus
2531
from services.storage import MWLStorage
@@ -94,6 +100,10 @@ def process_action(self, payload: dict):
94100
return {"status": "echo", "payload": payload}
95101
elif action_name == "worklist.create_item":
96102
return CreateWorklistItem(self.storage).call(payload)
103+
elif action_name == "worklist.create_test_item":
104+
result = CreateWorklistItem(self.storage).call(payload)
105+
self.process_with_modality_emulator(patient_name=payload.get("patient_name"))
106+
return result
97107
elif action_name == "worklist.update_item_status":
98108
return UpdateWorklistItemStatus(self.storage).call(payload)
99109
else:
@@ -106,6 +116,14 @@ def _connect(self):
106116
compression=None,
107117
)
108118

119+
def process_with_modality_emulator(self, patient_name: str | None = None):
120+
"""Process worklist items with ModalityEmulator."""
121+
ae = AE(ae_title="ModalityEmulator")
122+
ae.add_requested_context(DigitalMammographyXRayImageStorageForPresentation)
123+
ae.add_requested_context(ModalityWorklistInformationFind)
124+
125+
ModalityEmulator(self.storage).process_worklist_items(ae, patient_name=patient_name)
126+
109127

110128
class RelayURI:
111129
def __init__(self):

tests/test_modality_emulator.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,36 @@ def test_process_worklist_items_returns_when_no_items(
187187
mwl_assoc.release.assert_called_once()
188188
pacs_assoc.release.assert_called_once()
189189

190+
@patch.object(ModalityEmulator, "c_find_dataset")
191+
def test_process_worklist_items_passes_patient_name_to_c_find_dataset(
192+
self,
193+
mock_c_find_dataset,
194+
success_status,
195+
):
196+
"""Process worklist items passes patient_name to c_find_dataset."""
197+
mwl_storage = MagicMock()
198+
emulator = ModalityEmulator(mwl_storage)
199+
200+
query_ds = Dataset()
201+
mock_c_find_dataset.return_value = query_ds
202+
203+
mwl_assoc = MagicMock()
204+
mwl_assoc.is_established = True
205+
mwl_assoc.send_c_find.return_value = [(success_status, None)]
206+
207+
pacs_assoc = MagicMock()
208+
pacs_assoc.is_established = True
209+
210+
ae = MagicMock()
211+
ae.associate.side_effect = [mwl_assoc, pacs_assoc]
212+
213+
emulator.process_worklist_items(ae, patient_name="Jane Doe")
214+
215+
mock_c_find_dataset.assert_called_once_with(patient_name="Jane Doe")
216+
mwl_storage.update_status.assert_not_called()
217+
mwl_assoc.release.assert_called_once()
218+
pacs_assoc.release.assert_called_once()
219+
190220
@patch("modality_emulator.time.sleep")
191221
def test_process_worklist_items_handles_failed_association(
192222
self,

tests/test_relay_listener.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,34 @@ def test_process_update_item_status_action(self, storage_instance, listener_payl
114114

115115
storage_instance.update_status.assert_called_once_with("ACC999999", "IN PROGRESS")
116116

117+
def test_process_create_test_item_action_triggers_modality_emulator(self, storage_instance, listener_payload):
118+
"""Process create test item action and trigger modality emulator."""
119+
subject = RelayListener(storage_instance)
120+
payload = dict(listener_payload)
121+
payload["action_type"] = "worklist.create_test_item"
122+
123+
with patch.object(subject, "process_with_modality_emulator") as mock_emulator:
124+
response = subject.process_action(payload)
125+
126+
assert response == {"action_id": "action-12345", "status": "created"}
127+
mock_emulator.assert_called_once_with(patient_name=payload.get("patient_name"))
128+
129+
storage_instance.store_worklist_item.assert_called_once_with(
130+
WorklistItem(
131+
accession_number="ACC999999",
132+
patient_id="999123456",
133+
patient_name="SMITH^JANE",
134+
patient_birth_date="19900202",
135+
patient_sex="F",
136+
scheduled_date="20240615",
137+
scheduled_time="101500",
138+
modality="MG",
139+
study_description="MAMMOGRAPHY",
140+
source_message_id="action-12345",
141+
)
142+
)
143+
144+
117145
def test_process_action_missing_keys(self, storage_instance, listener_payload):
118146
"""Process action missing keys."""
119147
subject = RelayListener(storage_instance)

0 commit comments

Comments
 (0)