From d8cdeeaf78b023fe8c6ecfa61fc0d6e4bd670468 Mon Sep 17 00:00:00 2001 From: scaramallion Date: Sat, 4 Jul 2020 11:33:12 +1000 Subject: [PATCH] Fix selection of presentation context when using UPS (#512) --- README.rst | 2 +- docs/changelog/index.rst | 1 + docs/changelog/v1.5.1.rst | 2 +- docs/changelog/v1.5.2.rst | 16 ++++ docs/index.rst | 1 + docs/reference/sop_classes.rst | 1 + pynetdicom/_version.py | 2 +- pynetdicom/association.py | 61 ++++++++++++- pynetdicom/tests/test_assoc.py | 162 ++++++++++++++++++++++++++++++++- 9 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 docs/changelog/v1.5.2.rst diff --git a/README.rst b/README.rst index 6ef1f61214..11239f7261 100644 --- a/README.rst +++ b/README.rst @@ -145,7 +145,7 @@ peer SCP, the following DIMSE-C and -N services are available: +----------------+----------------------------------------------------------------------------------------+ | N-EVENT-REPORT | `Association.send_n_event_report(dataset, event_type, class_uid, instance_uid) `_ | +----------------+----------------------------------------------------------------------------------------+ -| N-GET | `Association.send_n_set(dataset, class_uid, instance_uid) `_ | +| N-GET | `Association.send_n_get(identifier_list, class_uid, instance_uid) `_ | +----------------+----------------------------------------------------------------------------------------+ | N-SET | `Association.send_n_set(dataset, class_uid, instance_uid) `_ | +----------------+----------------------------------------------------------------------------------------+ diff --git a/docs/changelog/index.rst b/docs/changelog/index.rst index 2a82036637..3e85255062 100644 --- a/docs/changelog/index.rst +++ b/docs/changelog/index.rst @@ -7,6 +7,7 @@ Release Notes .. toctree:: :maxdepth: 1 + v1.5.2 v1.5.1 v1.5.0 v1.4.1 diff --git a/docs/changelog/v1.5.1.rst b/docs/changelog/v1.5.1.rst index a65d2992c4..e0b703e3ba 100644 --- a/docs/changelog/v1.5.1.rst +++ b/docs/changelog/v1.5.1.rst @@ -4,6 +4,6 @@ ===== Changes -..... +....... * Switch *pydicom* dependency to >= 1.4.2 (:issue:`493`) diff --git a/docs/changelog/v1.5.2.rst b/docs/changelog/v1.5.2.rst new file mode 100644 index 0000000000..b20332dc92 --- /dev/null +++ b/docs/changelog/v1.5.2.rst @@ -0,0 +1,16 @@ +.. _v1.5.2: + +1.5.2 +===== + +Changes +....... + +* The ``Association.send_n_*()`` methods now allow the use of :class:`UPS Push + ` as the + *Requested* or *Affected SOP Class* when :class:`UPS Pull + `, :class:`Watch + `, :class:`Event + `, or :class:`Query + ` has been + accepted in a presentation context by the peer (:issue:`509`) diff --git a/docs/index.rst b/docs/index.rst index ed7adc460e..5b6e86fc8a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -119,6 +119,7 @@ Applications Release Notes ============= +* :doc:`v1.5.2 ` * :doc:`v1.5.1 ` * :doc:`v1.5.0 ` * :doc:`v1.4.1 ` diff --git a/docs/reference/sop_classes.rst b/docs/reference/sop_classes.rst index 2017e8b479..fa4a3e216b 100644 --- a/docs/reference/sop_classes.rst +++ b/docs/reference/sop_classes.rst @@ -368,6 +368,7 @@ Unified Procedure Step UnifiedProcedureStepEventSOPClass UnifiedProcedureStepPullSOPClass UnifiedProcedureStepPushSOPClass + UnifiedProcedureStepQuerySOPClass UnifiedProcedureStepWatchSOPClass Verification diff --git a/pynetdicom/_version.py b/pynetdicom/_version.py index e7c2c3b1ce..409223e91b 100644 --- a/pynetdicom/_version.py +++ b/pynetdicom/_version.py @@ -10,7 +10,7 @@ import re -__version__ = '1.5.1' +__version__ = '1.5.2' VERSION_PATTERN = r""" diff --git a/pynetdicom/association.py b/pynetdicom/association.py index ca308f73d5..b98ae0b6b9 100644 --- a/pynetdicom/association.py +++ b/pynetdicom/association.py @@ -27,7 +27,12 @@ standard_dimse_recv_handler, standard_dimse_sent_handler, standard_pdu_recv_handler, standard_pdu_sent_handler, ) -from pynetdicom.sop_class import uid_to_service_class, VerificationSOPClass +from pynetdicom.sop_class import ( + uid_to_service_class, VerificationSOPClass, + UnifiedProcedureStepPullSOPClass, UnifiedProcedureStepPushSOPClass, + UnifiedProcedureStepWatchSOPClass, UnifiedProcedureStepEventSOPClass, + UnifiedProcedureStepQuerySOPClass +) from pynetdicom.pdu_primitives import ( UserIdentityNegotiation, MaximumLengthNotification, @@ -366,6 +371,30 @@ def _get_valid_context(self, ab_syntax, tr_syntax, role=None, possible_contexts = [ cx for cx in possible_contexts if ab_syntax == cx.abstract_syntax ] + + # For UPS we can also match UPS Push to Pull/Watch/Event/Query + if ( + ab_syntax == UnifiedProcedureStepPushSOPClass + and not possible_contexts + ): + LOGGER.info( + "No exact matching context found for 'Unified Procedure Step " + "- Push SOP Class', checking accepted contexts for other UPS " + "SOP classes" + ) + ups = [ + UnifiedProcedureStepPullSOPClass, + UnifiedProcedureStepWatchSOPClass, + UnifiedProcedureStepEventSOPClass, + UnifiedProcedureStepQuerySOPClass + ] + possible_contexts.extend( + [ + cx for cx in self._accepted_cx.values() + if cx.abstract_syntax in ups + ] + ) + # Filter by role if role == 'scu': possible_contexts = [ @@ -1091,6 +1120,12 @@ def send_c_find(self, dataset, query_model, msg_id=1, priority=2): # Determine the Presentation Context we are operating under # and hence the transfer syntax to use for encoding `dataset` context = self._get_valid_context(query_model, '', 'scu') + if context.abstract_syntax != query_model: + LOGGER.info("Using Presentation Context:") + LOGGER.info(" Context ID: {}".format(context.context_id)) + LOGGER.info( + " Abstract Syntax: ={}".format(context.abstract_syntax.name) + ) # Build C-FIND request primitive # (M) Message ID @@ -2094,6 +2129,12 @@ def send_n_action(self, dataset, action_type, class_uid, instance_uid, # Determine the Presentation Context we are operating under # and hence the transfer syntax to use for encoding `dataset` context = self._get_valid_context(meta_uid or class_uid, '', 'scu') + if class_uid and context.abstract_syntax != class_uid: + LOGGER.info("Using Presentation Context:") + LOGGER.info(" Context ID: {}".format(context.context_id)) + LOGGER.info( + " Abstract Syntax: ={}".format(context.abstract_syntax.name) + ) transfer_syntax = context.transfer_syntax[0] # Build N-ACTION request primitive @@ -2652,6 +2693,12 @@ def send_n_event_report(self, dataset, event_type, class_uid, # selection negotiation, so we need to ignore the negotiate role # since the SCP will be sending requests to the SCU context = self._get_valid_context(meta_uid or class_uid, '', None) + if class_uid and context.abstract_syntax != class_uid: + LOGGER.info("Using Presentation Context:") + LOGGER.info(" Context ID: {}".format(context.context_id)) + LOGGER.info( + " Abstract Syntax: ={}".format(context.abstract_syntax.name) + ) transfer_syntax = context.transfer_syntax[0] # Build N-EVENT-REPORT request primitive @@ -2876,6 +2923,12 @@ def send_n_get(self, identifier_list, class_uid, instance_uid, msg_id=1, # Determine the Presentation Context we are operating under # and hence the transfer syntax to use for encoding `dataset` context = self._get_valid_context(meta_uid or class_uid, '', 'scu') + if class_uid and context.abstract_syntax != class_uid: + LOGGER.info("Using Presentation Context:") + LOGGER.info(" Context ID: {}".format(context.context_id)) + LOGGER.info( + " Abstract Syntax: ={}".format(context.abstract_syntax.name) + ) transfer_syntax = context.transfer_syntax[0] # Build N-GET request primitive @@ -3105,6 +3158,12 @@ def send_n_set(self, dataset, class_uid, instance_uid, msg_id=1, # Determine the Presentation Context we are operating under # and hence the transfer syntax to use for encoding `dataset` context = self._get_valid_context(meta_uid or class_uid, '', 'scu') + if class_uid and context.abstract_syntax != class_uid: + LOGGER.info("Using Presentation Context:") + LOGGER.info(" Context ID: {}".format(context.context_id)) + LOGGER.info( + " Abstract Syntax: ={}".format(context.abstract_syntax.name) + ) transfer_syntax = context.transfer_syntax[0] # Build N-SET request primitive diff --git a/pynetdicom/tests/test_assoc.py b/pynetdicom/tests/test_assoc.py index 67de6d27e7..2e98d347dc 100644 --- a/pynetdicom/tests/test_assoc.py +++ b/pynetdicom/tests/test_assoc.py @@ -50,7 +50,10 @@ PatientRootQueryRetrieveInformationModelMove, PatientStudyOnlyQueryRetrieveInformationModelMove, StudyRootQueryRetrieveInformationModelMove, - SecondaryCaptureImageStorage + SecondaryCaptureImageStorage, + UnifiedProcedureStepPullSOPClass, + UnifiedProcedureStepPushSOPClass, + UnifiedProcedureStepWatchSOPClass ) from .dummy_c_scp import ( DummyVerificationSCP, DummyStorageSCP, DummyFindSCP, DummyGetSCP, @@ -3838,12 +3841,16 @@ class TestGetValidContext(object): def setup(self): """Run prior to each test""" self.scp = None + self.ae = None def teardown(self): """Clear any active threads""" if self.scp: self.scp.abort() + if self.ae: + self.ae.shutdown() + time.sleep(0.1) for thread in threading.enumerate(): @@ -4362,6 +4369,159 @@ def test_little_big(self): self.scp.abort() + def test_ups_push_action(self, caplog): + """Test matching UPS Push to other UPS contexts.""" + def handle(event, cx): + cx.append(event.context) + return 0x0000, None + + self.ae = ae = AE() + ae.network_timeout = 5 + ae.dimse_timeout = 5 + ae.acse_timeout = 5 + ae.add_supported_context(UnifiedProcedureStepPullSOPClass) + + contexts = [] + handlers = [(evt.EVT_N_ACTION, handle, [contexts])] + scp = ae.start_server(('', 11112), block=False, evt_handlers=handlers) + + ae.add_requested_context(UnifiedProcedureStepPullSOPClass) + assoc = ae.associate('localhost', 11112) + assert assoc.is_established + + msg = ( + r"No exact matching context found for 'Unified Procedure Step " + r"- Push SOP Class', checking accepted contexts for other UPS " + r"SOP classes" + ) + ds = Dataset() + ds.TransactionUID = '1.2.3.4' + with caplog.at_level(logging.DEBUG, logger='pynetdicom'): + status, rsp = assoc.send_n_action( + ds, 1, UnifiedProcedureStepPushSOPClass, '1.2.3' + ) + assert msg in caplog.text + + assoc.release() + assert contexts[0].abstract_syntax == UnifiedProcedureStepPullSOPClass + scp.shutdown() + + def test_ups_push_get(self, caplog): + """Test matching UPS Push to other UPS contexts.""" + self.ae = ae = AE() + ae.network_timeout = 5 + ae.dimse_timeout = 5 + ae.acse_timeout = 5 + ae.add_supported_context(UnifiedProcedureStepPullSOPClass) + + scp = ae.start_server(('', 11112), block=False) + + ae.add_requested_context(UnifiedProcedureStepPullSOPClass) + assoc = ae.associate('localhost', 11112) + assert assoc.is_established + + msg = ( + r"No exact matching context found for 'Unified Procedure Step " + r"- Push SOP Class', checking accepted contexts for other UPS " + r"SOP classes" + ) + with caplog.at_level(logging.DEBUG, logger='pynetdicom'): + status, rsp = assoc.send_n_get( + [0x00100010], UnifiedProcedureStepPushSOPClass, '1.2.3' + ) + assert msg in caplog.text + + assoc.release() + scp.shutdown() + + def test_ups_push_set(self, caplog): + """Test matching UPS Push to other UPS contexts.""" + self.ae = ae = AE() + ae.network_timeout = 5 + ae.dimse_timeout = 5 + ae.acse_timeout = 5 + ae.add_supported_context(UnifiedProcedureStepPullSOPClass) + + scp = ae.start_server(('', 11112), block=False) + + ae.add_requested_context(UnifiedProcedureStepPullSOPClass) + assoc = ae.associate('localhost', 11112) + assert assoc.is_established + + msg = ( + r"No exact matching context found for 'Unified Procedure Step " + r"- Push SOP Class', checking accepted contexts for other UPS " + r"SOP classes" + ) + ds = Dataset() + ds.TransactionUID = '1.2.3.4' + with caplog.at_level(logging.DEBUG, logger='pynetdicom'): + status, rsp = assoc.send_n_set( + ds, UnifiedProcedureStepPushSOPClass, '1.2.3' + ) + assert msg in caplog.text + + assoc.release() + scp.shutdown() + + def test_ups_push_er(self, caplog): + """Test matching UPS Push to other UPS contexts.""" + self.ae = ae = AE() + ae.network_timeout = 5 + ae.dimse_timeout = 5 + ae.acse_timeout = 5 + ae.add_supported_context(UnifiedProcedureStepPullSOPClass) + + scp = ae.start_server(('', 11112), block=False) + + ae.add_requested_context(UnifiedProcedureStepPullSOPClass) + assoc = ae.associate('localhost', 11112) + assert assoc.is_established + + msg = ( + r"No exact matching context found for 'Unified Procedure Step " + r"- Push SOP Class', checking accepted contexts for other UPS " + r"SOP classes" + ) + ds = Dataset() + ds.TransactionUID = '1.2.3.4' + with caplog.at_level(logging.DEBUG, logger='pynetdicom'): + status, rsp = assoc.send_n_event_report( + ds, 1, UnifiedProcedureStepPushSOPClass, '1.2.3' + ) + assert msg in caplog.text + + assoc.release() + scp.shutdown() + + def test_ups_push_find(self, caplog): + """Test matching UPS Push to other UPS contexts.""" + self.ae = ae = AE() + ae.network_timeout = 5 + ae.dimse_timeout = 5 + ae.acse_timeout = 5 + ae.add_supported_context(UnifiedProcedureStepPullSOPClass) + + scp = ae.start_server(('', 11112), block=False) + + ae.add_requested_context(UnifiedProcedureStepPullSOPClass) + assoc = ae.associate('localhost', 11112) + assert assoc.is_established + + msg = ( + r"No exact matching context found for 'Unified Procedure Step " + r"- Push SOP Class', checking accepted contexts for other UPS " + r"SOP classes" + ) + ds = Dataset() + ds.TransactionUID = '1.2.3.4' + with caplog.at_level(logging.DEBUG, logger='pynetdicom'): + responses = assoc.send_c_find(ds, UnifiedProcedureStepPushSOPClass) + assert msg in caplog.text + + assoc.release() + scp.shutdown() + class TestEventHandlingAcceptor(object): """Test the transport events and handling as acceptor."""