Repository: pydicom/pynetdicom
Pull request #422 (2019-12-20 10:55:19)

[MRG] Add Event.message_id
#### Reference issue
Adds message_id to Event

#### Tasks
 - [x] Unit tests added that reproduce issue or prove feature is working
 - [x] Fix or feature added
 - [x] Documentation and examples updated (if relevant)
 - [x] Unit tests passing and coverage at 100% after adding fix/feature



Patch:
diff --git a/docs/changelog/v1.5.0.rst b/docs/changelog/v1.5.0.rst
index 3ffafac54..710e2cb89 100644
--- a/docs/changelog/v1.5.0.rst
+++ b/docs/changelog/v1.5.0.rst
@@ -11,7 +11,7 @@ Fixes
 * Improved the robustness of determining the local IP address (:issue:`394`)
 * Fixed not being able to set an Implementation Version Name of length 1
   (:issue:`414`)
-  
+
 Enhancements
 ............
 
@@ -22,6 +22,9 @@ Enhancements
 * When acting as the association Acceptor check that the user hasn't sent
   and A-ABORT or A-ASSOCIATE-RJ before attempting association negotiation
   (:issue:`408`)
+* Added :attr:`~pynetdicom.events.Event.message_id` to
+  :class:`~pynetdicom.events.Event` for use when receiving a DIMSE service
+  request
 
 Changes
 .......
diff --git a/pynetdicom/_handlers.py b/pynetdicom/_handlers.py
index 7e4376cc3..9884cdf95 100644
--- a/pynetdicom/_handlers.py
+++ b/pynetdicom/_handlers.py
@@ -2048,6 +2048,11 @@ def doc_handle_echo(event):
           that the C-ECHO request was processed by the service as
           :class:`datetime.datetime`.
 
+        :class:`~pynetdicom.events.Event` properties are:
+
+        * :attr:`~pynetdicom.events.Event.message_id`: the C-ECHO request's
+          *Message ID* as :class:`int`.
+
     Returns
     -------
     status : pydicom.dataset.Dataset or int
@@ -2161,6 +2166,8 @@ def doc_handle_find(event):
           C-CANCEL request has been received, ``False`` otherwise. If a
           C-CANCEL is received then the handler should ``yield (0xFE00, None)``
           and ``return``.
+        * :attr:`~pynetdicom.events.Event.message_id`: the C-FIND request's
+          *Message ID* as :class:`int`.
 
     Yields
     ------
@@ -2310,6 +2317,8 @@ def doc_handle_c_get(event):
           C-CANCEL request has been received, ``False`` otherwise. If a
           C-CANCEL is received then the handler should ``yield (0xFE00, None)``
           and ``return``.
+        * :attr:`~pynetdicom.events.Event.message_id`: the C-GET request's
+          *Message ID* as :class:`int`.
 
     Yields
     ------
@@ -2449,8 +2458,6 @@ def doc_handle_move(event):
           as a :class:`~pynetdicom.presentation.PresentationContextTuple`.
         * :attr:`~pynetdicom.events.Event.event`: the event that occurred as
           :class:`~pynetdicom.events.InterventionEvent`.
-        * :attr:`~pynetdicom.events.Event.move_destination`: the C-MOVE
-          request's *Move Destination* value as :class:`bytes`.
         * :attr:`~pynetdicom.events.Event.request`: the received
           :class:`C-MOVE request <pynetdicom.dimse_primitives.C_MOVE>`
         * :attr:`~pynetdicom.events.Event.timestamp`: the date and time
@@ -2469,6 +2476,10 @@ def doc_handle_move(event):
           C-CANCEL request has been received, ``False`` otherwise. If a
           C-CANCEL is received then the handler should yield a
           ``(0xFE00, None)`` status/dataset pair and ``return``.
+        * :attr:`~pynetdicom.events.Event.message_id`: the C-MOVE request's
+          *Message ID* as :class:`int`.
+        * :attr:`~pynetdicom.events.Event.move_destination`: the C-MOVE
+          request's *Move Destination* value as :class:`bytes`.
 
 
     Yields
@@ -2613,6 +2624,8 @@ def doc_handle_store(event):
           conformant File Meta Information that can be used with the decoded
           dataset when saving to file: ``event.dataset.file_meta =
           event.file_meta``.
+        * :attr:`~pynetdicom.events.Event.message_id`: the C-STORE request's
+          *Message ID* as :class:`int`.
 
     Returns
     -------
@@ -2758,8 +2771,6 @@ def doc_handle_action(event):
         The event representing a service class receiving a N-ACTION
         request message. :class:`~pynetdicom.events.Event` attributes are:
 
-        * :attr:`~pynetdicom.events.Event.action_type`: the N-ACTION request's
-          *Action Type ID* parameter value as :class:`int`.
         * :attr:`~pynetdicom.events.Event.assoc`: the
           :class:`~pynetdicom.association.Association`
           that is running the service that received the N-ACTION request.
@@ -2782,6 +2793,10 @@ def doc_handle_action(event):
           uses a deferred read when decoding data, if the decode fails the
           returned :class:`~pydicom.dataset.Dataset` will only raise an
           exception at the time of use.
+        * :attr:`~pynetdicom.events.Event.action_type`: the N-ACTION request's
+          *Action Type ID* as :class:`int`.
+        * :attr:`~pynetdicom.events.Event.message_id`: the N-ACTION request's
+          *Message ID* as :class:`int`.
 
     Returns
     -------
@@ -2932,6 +2947,8 @@ def doc_handle_create(event):
           uses a deferred read when decoding data, if the decode fails the
           returned :class:`~pydicom.dataset.Dataset` will only raise an
           exception at the time of use.
+        * :attr:`~pynetdicom.events.Event.message_id`: the N-CREATE request's
+          *Message ID* as :class:`int`.
 
     Returns
     -------
@@ -3042,6 +3059,12 @@ def doc_handle_delete(event):
           that the N-DELETE request was processed by the service as
           :class:`datetime.datetime`.
 
+        :class:`~pynetdicom.events.Event` properties are:
+
+        * :attr:`~pynetdicom.events.Event.message_id`: the N-DELETE request's
+          *Message ID* as :class:`int`.
+
+
     Returns
     -------
     status : pydicom.dataset.Dataset or int
@@ -3137,8 +3160,6 @@ def doc_handle_event_report(event):
           as a :class:`~pynetdicom.presentation.PresentationContextTuple`.
         * :attr:`~pynetdicom.events.Event.event`: the event that occurred as
           :class:`~pynetdicom.events.InterventionEvent`.
-        * ``event_type``: the N-EVENT-REPORT request's *Event Type
-          ID* parameter value as :class:`int`.
         * :attr:`~pynetdicom.events.Event.request`: the received
           :class:`N-EVENT-REPORT request
           <pynetdicom.dimse_primitives.N_EVENT_REPORT>`
@@ -3154,6 +3175,10 @@ def doc_handle_event_report(event):
           *pydicom* uses a deferred read when decoding data, if the decode
           fails the returned :class:`~pydicom.dataset.Dataset` will only raise
           an exception at the time of use.
+        * :attr:`~pynetdicom.events.Event.event_type`: the N-EVENT-REPORT
+          request's *Event Type ID* parameter value as :class:`int`.
+        * :attr:`~pynetdicom.events.Event.message_id`: the N-EVENT-REPORT
+          request's *Message ID* as :class:`int`.
 
     Returns
     -------
@@ -3279,6 +3304,8 @@ def doc_handle_n_get(event):
         * :attr:`~pynetdicom.events.Event.attribute_identifiers`: a list of
           attribute :class:`BaseTag<pydicom.tag.BaseTag>` contained within the
           N-GET request's *Attribute Identifier List* parameter.
+        * :attr:`~pynetdicom.events.Event.message_id`: the N-GET request's
+          *Message ID* as :class:`int`.
 
     Returns
     -------
@@ -3425,6 +3452,8 @@ def doc_handle_set(event):
 
         :class:`~pynetdicom.events.Event` properties are:
 
+        * :attr:`~pynetdicom.events.Event.message_id`: the N-SET request's
+          *Message ID* as :class:`int`.
         * :attr:`~pynetdicom.events.Event.modification_list`: the decoded
           :class:`~pydicom.dataset.Dataset` contained within the
           N-SET request's *Modification List* parameter. Because *pydicom*
diff --git a/pynetdicom/events.py b/pynetdicom/events.py
index 5f102e25d..568d36408 100644
--- a/pynetdicom/events.py
+++ b/pynetdicom/events.py
@@ -632,6 +632,29 @@ class Event(object):
 
         return False
 
+    @property
+    def message_id(self):
+        """Return a DIMSE service request's 'Message ID' as int.
+
+        Returns
+        -------
+        int
+            The request's (0000,0110) *Message ID* value.
+
+        Raises
+        ------
+        AttributeError
+            If the corresponding event is not one of the DIMSE service
+            requests.
+        """
+        try:
+            return self.request.MessageID
+        except AttributeError:
+            raise AttributeError(
+                "The corresponding event is not a DIMSE service request and "
+                "has no 'Message ID' parameter"
+            )
+
     @property
     def modification_list(self):
         """Return an N-SET request's `Modification List` as a *pydicom*


Test patch:
diff --git a/pynetdicom/tests/test_events.py b/pynetdicom/tests/test_events.py
index b7735b5ec..667704fe6 100644
--- a/pynetdicom/tests/test_events.py
+++ b/pynetdicom/tests/test_events.py
@@ -181,6 +181,13 @@ class TestEvent(object):
         with pytest.raises(AttributeError, match=msg):
             event.event_type
 
+        msg = (
+            r"The corresponding event is not a DIMSE service request and "
+            r"has no 'Message ID' parameter"
+        )
+        with pytest.raises(AttributeError, match=msg):
+            event.message_id
+
     def test_is_cancelled_non(self):
         """Test Event.is_cancelled with wrong event type."""
         event = evt.Event(None, evt.EVT_DATA_RECV)
@@ -326,6 +333,18 @@ class TestEvent(object):
         event._hash = None
         assert 'PatientID' not in event.modification_list
 
+    def test_message_id(self):
+        """Test Event.modification_list."""
+        request = N_SET()
+        request.MessageID = 1234
+        event = Event(
+            None,
+            evt.EVT_N_CREATE,
+            {'request' : request, 'context' : self.context.as_tuple}
+        )
+
+        assert 1234 == event.message_id
+
     def test_empty_dataset(self):
         """Test with an empty dataset-like."""
         request = N_EVENT_REPORT()