Skip to content

Commit 6e94826

Browse files
committed
Add NSet service command
1 parent ef6e949 commit 6e94826

5 files changed

Lines changed: 199 additions & 0 deletions

File tree

src/services/dicom/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
PENDING = 0xFF00
77
INVALID_ATTRIBUTE = 0x0106
88
DUPLICATE_SOP_INSTANCE = 0x0111
9+
UNKNOWN_SOP_INSTANCE = 0x0112
910
MISSING_ATTRIBUTE = 0x0120
1011
PROCESSING_FAILURE = 0x0110

src/services/mwl/n_set.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import logging
2+
3+
from pydicom import Dataset
4+
from pynetdicom.events import Event
5+
from pynetdicom.sop_class import ModalityPerformedProcedureStep # pyright: ignore[reportAttributeAccessIssue]
6+
7+
from services.dicom import PROCESSING_FAILURE, SUCCESS, UNKNOWN_SOP_INSTANCE
8+
from services.storage import MWLStorage
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class NSet:
14+
def __init__(self, storage: MWLStorage):
15+
self.storage = storage
16+
17+
def call(self, event: Event) -> tuple[int, Dataset | None]:
18+
try:
19+
req = event.request
20+
requested_sop_instance_uid = getattr(req, "RequestedSOPInstanceUID", None)
21+
logger.info(f"MPPS N-SET: Received request for SOP Instance UID: {requested_sop_instance_uid}")
22+
23+
mod_list = event.attribute_list
24+
status = mod_list.get("PerformedProcedureStepStatus")
25+
if not status:
26+
logger.warning("MPPS N-SET: Missing PerformedProcedureStepStatus in request")
27+
return PROCESSING_FAILURE, None
28+
29+
worklist_item = self.storage.get_worklist_item_by_mpps_instance_uid(requested_sop_instance_uid)
30+
if not worklist_item:
31+
logger.warning(f"MPPS N-SET: No worklist item found for SOP Instance UID: {requested_sop_instance_uid}")
32+
return UNKNOWN_SOP_INSTANCE, None
33+
34+
accession_number = worklist_item.accession_number
35+
36+
source_message_id = self.storage.update_status(accession_number, status)
37+
if source_message_id:
38+
logger.info(f"Database updated: {accession_number} -> {status}")
39+
40+
ds = Dataset()
41+
ds.SOPClassUID = ModalityPerformedProcedureStep
42+
ds.SOPInstanceUID = requested_sop_instance_uid
43+
ds.update(mod_list)
44+
45+
logger.info("MPPS N-SET successful")
46+
return SUCCESS, ds
47+
else:
48+
logger.warning("MPPS N-SET: Failed to update database with new status")
49+
return PROCESSING_FAILURE, None
50+
except Exception as e:
51+
logger.error(f"Error in handle_set: {str(e)}", exc_info=True)
52+
return PROCESSING_FAILURE, None

src/services/storage.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,3 +521,18 @@ def mpps_instance_exists(self, mpps_instance_uid: str) -> bool:
521521
with self._get_connection() as conn:
522522
cursor = conn.execute("SELECT 1 FROM worklist_items WHERE mpps_instance_uid = ?", (mpps_instance_uid,))
523523
return cursor.fetchone() is not None
524+
525+
def get_worklist_item_by_mpps_instance_uid(self, mpps_instance_uid: str) -> Optional[WorklistItem]:
526+
"""Get a worklist item by its associated MPPS instance UID."""
527+
with self._get_connection() as conn:
528+
cursor = conn.execute(
529+
(
530+
"SELECT accession_number, modality, patient_birth_date, patient_id, "
531+
"patient_name, patient_sex, procedure_code, scheduled_date, scheduled_time, "
532+
"source_message_id, study_description, study_instance_uid, status, mpps_instance_uid "
533+
"FROM worklist_items WHERE mpps_instance_uid = ?"
534+
),
535+
(mpps_instance_uid,),
536+
)
537+
row = cursor.fetchone()
538+
return WorklistItem(**row) if row else None

tests/services/mwl/test_n_set.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
import pytest
4+
from pydicom.dataset import Dataset
5+
from pydicom.uid import generate_uid
6+
from pynetdicom.sop_class import ModalityPerformedProcedureStep # pyright: ignore[reportAttributeAccessIssue]
7+
8+
from services.dicom import (
9+
PROCESSING_FAILURE,
10+
SUCCESS,
11+
UNKNOWN_SOP_INSTANCE,
12+
)
13+
from services.mwl.n_set import NSet
14+
15+
16+
@patch(f"{NSet.__module__}.MWLStorage")
17+
class TestNSet:
18+
@pytest.fixture
19+
def event(self):
20+
event = MagicMock()
21+
event.request = MagicMock()
22+
event.attribute_list = Dataset()
23+
return event
24+
25+
@pytest.fixture
26+
def requested_sop_instance_uid(self):
27+
return generate_uid()
28+
29+
def test_missing_status_returns_processing_failure(self, mock_storage, event):
30+
event.request.RequestedSOPInstanceUID = generate_uid()
31+
32+
# No PerformedProcedureStepStatus set
33+
status, ds = NSet(mock_storage).call(event)
34+
35+
assert status == PROCESSING_FAILURE
36+
assert ds is None
37+
38+
def test_unknown_sop_instance_returns_unknown(self, mock_storage, event, requested_sop_instance_uid):
39+
event.request.RequestedSOPInstanceUID = requested_sop_instance_uid
40+
event.attribute_list.PerformedProcedureStepStatus = "IN PROGRESS"
41+
42+
mock_storage.get_worklist_item_by_mpps_instance_uid.return_value = None
43+
44+
status, ds = NSet(mock_storage).call(event)
45+
46+
assert status == UNKNOWN_SOP_INSTANCE
47+
assert ds is None
48+
mock_storage.get_worklist_item_by_mpps_instance_uid.assert_called_once_with(requested_sop_instance_uid)
49+
50+
def test_database_update_failure_returns_processing_failure(self, mock_storage, event, requested_sop_instance_uid):
51+
event.request.RequestedSOPInstanceUID = requested_sop_instance_uid
52+
event.attribute_list.PerformedProcedureStepStatus = "COMPLETED"
53+
54+
worklist_item = MagicMock()
55+
worklist_item.accession_number = "ACC123"
56+
mock_storage.get_worklist_item_by_mpps_instance_uid.return_value = worklist_item
57+
58+
mock_storage.update_status.return_value = None
59+
60+
status, ds = NSet(mock_storage).call(event)
61+
62+
assert status == PROCESSING_FAILURE
63+
assert ds is None
64+
mock_storage.update_status.assert_called_once_with("ACC123", "COMPLETED")
65+
66+
def test_successful_nset_returns_success_and_dataset(self, mock_storage, event, requested_sop_instance_uid):
67+
event.request.RequestedSOPInstanceUID = requested_sop_instance_uid
68+
event.attribute_list.PerformedProcedureStepStatus = "COMPLETED"
69+
70+
worklist_item = MagicMock()
71+
worklist_item.accession_number = "ACC123"
72+
mock_storage.get_worklist_item_by_mpps_instance_uid.return_value = worklist_item
73+
74+
mock_storage.update_status.return_value = 1001 # mock message id
75+
76+
status, ds = NSet(mock_storage).call(event)
77+
78+
assert status == SUCCESS
79+
assert isinstance(ds, Dataset)
80+
assert ds.SOPClassUID == ModalityPerformedProcedureStep
81+
assert ds.SOPInstanceUID == requested_sop_instance_uid
82+
assert ds.PerformedProcedureStepStatus == "COMPLETED"
83+
84+
mock_storage.update_status.assert_called_once_with("ACC123", "COMPLETED")
85+
86+
def test_exception_returns_processing_failure(self, mock_storage, event):
87+
event.request.RequestedSOPInstanceUID = generate_uid()
88+
event.attribute_list.PerformedProcedureStepStatus = "COMPLETED"
89+
90+
mock_storage.get_worklist_item_by_mpps_instance_uid.side_effect = Exception("DB error")
91+
92+
status, ds = NSet(mock_storage).call(event)
93+
94+
assert status == PROCESSING_FAILURE
95+
assert ds is None

tests/services/test_storage.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,3 +494,39 @@ def test_mpps_instance_not_exists(self, mock_db, tmp_dir):
494494
mock_connection.reset_mock()
495495

496496
assert subject.mpps_instance_exists(generate_uid()) is False
497+
498+
def test_get_worklist_item_by_mpps_instance_uid(self, mock_db, tmp_dir, result):
499+
mpps_instance_uid = "some-mpps-uid"
500+
mock_cursor = MagicMock()
501+
mock_cursor.fetchone.return_value = result
502+
mock_connection = MagicMock()
503+
mock_connection.execute.return_value = mock_cursor
504+
mock_db.connect.return_value = mock_connection
505+
506+
subject = MWLStorage(tmp_dir)
507+
mock_connection.reset_mock()
508+
509+
worklist_item = subject.get_worklist_item_by_mpps_instance_uid(mpps_instance_uid)
510+
511+
mock_connection.execute.assert_called_once_with(
512+
(
513+
"SELECT accession_number, modality, patient_birth_date, patient_id, "
514+
"patient_name, patient_sex, procedure_code, scheduled_date, scheduled_time, "
515+
"source_message_id, study_description, study_instance_uid, status, mpps_instance_uid "
516+
"FROM worklist_items WHERE mpps_instance_uid = ?"
517+
),
518+
(mpps_instance_uid,),
519+
)
520+
assert worklist_item == WorklistItem(**result)
521+
522+
def test_get_worklist_item_by_mpps_instance_uid_returns_none(self, mock_db, tmp_dir):
523+
mpps_instance_uid = "some-mpps-uid"
524+
mock_cursor = MagicMock()
525+
mock_cursor.fetchone.return_value = None
526+
mock_connection = MagicMock()
527+
mock_connection.execute.return_value = mock_cursor
528+
mock_db.connect.return_value = mock_connection
529+
530+
subject = MWLStorage(tmp_dir)
531+
532+
assert subject.get_worklist_item_by_mpps_instance_uid(mpps_instance_uid) is None

0 commit comments

Comments
 (0)