Repository: pydicom/pynetdicom
Pull request #379 (2019-07-16 09:03:59)

[MRG] Handle N-EVENT-REPORT service requests immediately
#### Reference issue
In theory this should mean both synchronous and asynchronous N-EVENT-REPORT requests are handled properly. In theory....

Related to #8, #319

#### 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/README.rst b/README.rst
index 6481cb073..846556fc0 100644
--- a/README.rst
+++ b/README.rst
@@ -67,22 +67,25 @@ Supported Service Classes
 - `Implant Template Query/Retrieve Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_BB>`_
 - `Instance Availability Notification Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_R>`_
 - `Media Creation Management Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_S>`_
-- `Modality Performed Procedure Step Management <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_F>`_ (excluding *MPPS Notification SOP Class*)
+- `Modality Performed Procedure Step Management <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_F>`_
 - `Non-Patient Object Storage Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_GG>`_
+- `Print Management Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_H>`_
 - `Protocol Approval Query/Retrieve Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_II>`_
 - `Query/Retrieve Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_C>`_
 
   - `Composite Instance Retrieve Without Bulk Data <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_Z>`_
   - `Instance and Frame Level Retrieve <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_Y>`_
 - `Relevant Patient Information Query Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_Q>`_
+- `RT Machine Verification Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_DD>`_
 - `Storage Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_B>`_
 
   - `Ophthalmic Refractive Measurements Storage <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_AA>`_
   - `Softcopy Presentation State Storage <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_N>`_
   - `Structured Reporting Storage <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_O>`_
   - `Volumetric Presentation State Storage <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_FF>`_
+- `Storage Commitment Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_J>`_
 - `Substance Administration Query Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_V>`_
-- `Unified Procedure Step Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_CC>`_ (excluding *UPS Event SOP Class*)
+- `Unified Procedure Step Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_CC>`_
 - `Verification Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_A>`_
 
 
diff --git a/docs/changelog/v1.4.0.rst b/docs/changelog/v1.4.0.rst
index ccc0d4e30..16d50e114 100644
--- a/docs/changelog/v1.4.0.rst
+++ b/docs/changelog/v1.4.0.rst
@@ -22,21 +22,20 @@ Fixes
 Enhancements
 ............
 
-* Added ``Event.move_destination`` and ``Event.event`` properties
-  (:issue:`353`)
+* Added ``Event.move_destination`` (for ``evt.EVT_C_MOVE``),
+  ``Event.action_type`` (for ``evt.EVT_N_ACTION``), ``Event.event_type`` (for
+  ``evt.EVT_N_EVENT_REPORT``) and ``Event.event`` properties (:issue:`353`)
 * Added ``meta_uid`` keyword parameters to the ``Association.send_n_*()``
   methods to allow the use of Meta SOP Classes (such as those used in the
   Print Management service class)
 * Added *Basic Grayscale Print Management Meta SOP Class* and *Basic Color
   Print Management Meta SOP Class* to ``sop_class``.
-* Added full support for the following service classes:
+* Added support for the following service classes:
 
   * Protocol Approval Query Retrieve (:issue:`327`)
   * Application Event Logging
   * Instance Availability Notification
   * Media Creation Management
-* Added partial support for the following service classes:
-
   * Print Management
   * Unified Procedure Step
   * RT Machine Verification
@@ -49,7 +48,7 @@ Enhancements
   received in C-STORE service requests are now logged and ignored rather than
   raising an exception and aborting the association.
 * Improved handling of DIMSE service messages with empty element values.
-* Added handling for DIMSE service requests when the acting as the association
+* Added handling for DIMSE service requests when acting as the association
   Requestor
 
 
diff --git a/docs/examples/print.rst b/docs/examples/print.rst
index 5d32df225..f41aa9455 100644
--- a/docs/examples/print.rst
+++ b/docs/examples/print.rst
@@ -72,20 +72,34 @@ Over a single association:
    attributes with the image data and image attributes that are to be printed.
 5. Use N-ACTION to command the *Film Box* to be printed or N-DELETE to delete
    the *Film Box*.
-6. Repeat steps 2-4 as required.
+6. Repeat steps 1-5 as required.
 7. Terminate the association to delete the *Film Session* hierarchy.
 
+A Print SCP may send N-EVENT-REPORT service requests to the Print SCU (under
+the Printer SOP Class) using one of the following methods,
+depending on the implementation (check the conformance statement):
+
+1. Over the same association as the Print SCU service request
+2. Over a new association initiated by the Print SCP, which requires the SCP be
+   configured with the details of the SCU
+3. The next time the SCU associates with the SCP
+
+Depending on which method the Print SCP uses you should:
+
+* For methods 1 and 2, simply bind a handler to ``evt.EVT_N_REPORT``
+  when calling ``AE.associate()``
+* For method 3, start an ``AssociationServer`` instance with
+  ``AE.start_server((addr, port), block=False)`` with a handler bound to
+  ``evt.EVT_N_EVENT_REPORT``
+
+To support minimal conformance for a Print SCU you can just use the handler
+to return a status of ``0x0000`` and get the printer's current status manually
+by sending an N-GET request.
+
 
 DIMSE Services Available
 ........................
 
-.. warning::
-   The use of asynchronous N-EVENT-REPORT requests sent by the SCP to the SCU
-   with the *Printer SOP Class* is not currently supported. Any N-EVENT-REPORT
-   requests that are received when acting as the SCU will automatically be
-   responded to with a status of ``0x0000`` - Success. In order to get the
-   printer's current status send an N-GET request with the *Printer SOP Class*.
-
 +-----------------+-------------------------+
 | DIMSE-N Service | Usage SCU/SCP           |
 +=================+=========================+
@@ -130,6 +144,9 @@ example assumes that the Film Session's and Film Box's
 N-CREATE responses include conformant *Basic Film Session SOP Class* and
 *Basic Film Box SOP Class* instances (which may not always be the case).
 
+We also assume that the Print SCP sends the Printer SOP Class' N-EVENT-REPORT
+notifications over the same association (and ignore them).
+
 .. code-block:: python
 
     import sys
@@ -138,7 +155,7 @@ N-CREATE responses include conformant *Basic Film Session SOP Class* and
     from pydicom.dataset import Dataset
     from pydicom.uid import generate_uid
 
-    from pynetdicom import AE
+    from pynetdicom import AE, evt
     from pynetdicom.sop_class import (
         BasicGrayscalePrintManagementMetaSOPClass,
         BasicFilmSessionSOPClass,
@@ -236,9 +253,15 @@ N-CREATE responses include conformant *Basic Film Session SOP Class* and
 
         return attr_list
 
+    def handle_n_er(event):
+        """Ignore the N-EVENT-REPORT notification"""
+        return 0x0000
+
+    handlers = [(evt.EVT_N_EVENT_REPORT, handle_n_er)]
+
     ae = AE()
     ae.add_requested_context(BasicGrayscalePrintManagementMetaSOPClass)
-    assoc = ae.associate('localhost', 11112)
+    assoc = ae.associate('localhost', 11112, evt_handlers=handlers)
 
     if assoc.is_established:
         # Step 1: Check the status of the printer
diff --git a/docs/index.rst b/docs/index.rst
index bdffcfe1a..2fd18a0ea 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -44,24 +44,24 @@ Supported Service Classes
 * :doc:`Instance Availability Notification <service_classes/instance_availability>`
 * :doc:`Media Creation Management <service_classes/media_creation>`
 * :doc:`Non-Patient Object Storage <service_classes/non_patient_service_class>`
-* :doc:`Print Management <service_classes/print_management>` (partial)
-* :doc:`Procedure Step <service_classes/modality_performed_procedure_step>` (partial)
+* :doc:`Print Management <service_classes/print_management>`
+* :doc:`Procedure Step <service_classes/modality_performed_procedure_step>`
 * :doc:`Protocol Approval Query/Retrieve <service_classes/protocol_approval_service_class>`
 * :doc:`Query/Retrieve <service_classes/query_retrieve_service_class>`
 
   * Composite Instance Retrieve Without Bulk Data
   * Instance and Frame Level Retrieve
 * :doc:`Relevant Patient Information Query <service_classes/relevant_patient_service_class>`
-* :doc:`RT Machine Verification <service_classes/rt_machine>` (partial)
+* :doc:`RT Machine Verification <service_classes/rt_machine>`
 * :doc:`Storage <service_classes/storage_service_class>`
 
   * Ophthalmic Refractive Measurements
   * Softcopy Presentation State
   * Structured Reporting
   * Volumetric Presentation State
-* :doc:`Storage Commitment <service_classes/storage_commitment>` (partial)
+* :doc:`Storage Commitment <service_classes/storage_commitment>`
 * :doc:`Substance Administration Query <service_classes/substance_admin_service_class>`
-* :doc:`Unified Procedure Step <service_classes/ups>` (partial)
+* :doc:`Unified Procedure Step <service_classes/ups>`
 * :doc:`Verification <service_classes/verification_service_class>`
 
 
diff --git a/docs/service_classes/modality_performed_procedure_step.rst b/docs/service_classes/modality_performed_procedure_step.rst
index 9bbad3672..e7aec12cd 100644
--- a/docs/service_classes/modality_performed_procedure_step.rst
+++ b/docs/service_classes/modality_performed_procedure_step.rst
@@ -9,10 +9,6 @@ by a modality.
 Supported SOP Classes
 ---------------------
 
-.. warning::
-   The use of asynchronous N-EVENT-REPORT requests sent by the SCP to the SCU
-   is not currently supported.
-
 +-----------------------------+----------------------------------------------------+
 | UID                         | SOP Class                                          |
 +=============================+====================================================+
diff --git a/docs/service_classes/print_management.rst b/docs/service_classes/print_management.rst
index 188cb4c74..552cbb342 100644
--- a/docs/service_classes/print_management.rst
+++ b/docs/service_classes/print_management.rst
@@ -11,10 +11,6 @@ facilitate the print of images and image related data.
 Supported SOP Classes
 ---------------------
 
-.. warning::
-   The use of asynchronous N-EVENT-REPORT requests sent by the SCP to the SCU
-   is not currently supported.
-
 +----------------------------+------------------------------------------------+
 | UID                        | SOP Class                                      |
 +============================+================================================+
diff --git a/docs/service_classes/rt_machine.rst b/docs/service_classes/rt_machine.rst
index 432e49643..ecde33074 100644
--- a/docs/service_classes/rt_machine.rst
+++ b/docs/service_classes/rt_machine.rst
@@ -12,10 +12,6 @@ on a radiation delivery system priot to delivery of treatment.
 Supported SOP Classes
 ---------------------
 
-.. warning::
-   The use of asynchronous N-EVENT-REPORT requests sent by the SCP to the SCU
-   is not currently supported.
-
 +----------------------------+------------------------------------------------+
 | UID                        | SOP Class                                      |
 +============================+================================================+
diff --git a/docs/service_classes/storage_commitment.rst b/docs/service_classes/storage_commitment.rst
index 8ce8a1931..5a8f1b52c 100644
--- a/docs/service_classes/storage_commitment.rst
+++ b/docs/service_classes/storage_commitment.rst
@@ -11,10 +11,6 @@ safekeeping of SOP Instances.
 Supported SOP Classes
 ---------------------
 
-.. warning::
-   The use of asynchronous N-EVENT-REPORT requests sent by the SCP to the SCU
-   is not currently supported.
-
 +----------------------------+------------------------------------------------+
 | UID                        | SOP Class                                      |
 +============================+================================================+
diff --git a/docs/service_classes/ups.rst b/docs/service_classes/ups.rst
index 53aa629d9..56634f426 100644
--- a/docs/service_classes/ups.rst
+++ b/docs/service_classes/ups.rst
@@ -12,10 +12,6 @@ items, querying worklists and communicating progress and results.
 Supported SOP Classes
 ---------------------
 
-.. warning::
-   The use of asynchronous N-EVENT-REPORT requests sent by the SCP to the SCU
-   is not currently supported.
-
 +----------------------------+------------------------------------------------+
 | UID                        | SOP Class                                      |
 +============================+================================================+
diff --git a/docs/user/association.rst b/docs/user/association.rst
index 6cf69eb5c..c7ce87efd 100644
--- a/docs/user/association.rst
+++ b/docs/user/association.rst
@@ -518,10 +518,6 @@ implementation that always returns a 0x0000 *Success* response):
 | N-SET          | ``evt.EVT_N_SET``          |
 +----------------+----------------------------+
 
-.. warning::
-   The use of asynchronous N-EVENT-REPORT requests sent by the SCP to the SCU
-   is not currently supported.
-
 For instance, if your SCP is to support the Storage Service then you would
 implement and bind a handler for the ``evt.EVT_C_STORE`` event in manner
 similar to:
diff --git a/docs/user/events.rst b/docs/user/events.rst
index be8909ab0..a894f3a76 100644
--- a/docs/user/events.rst
+++ b/docs/user/events.rst
@@ -90,46 +90,61 @@ or extended negotiation ignored). The sole exception is the default handler
 for ``evt.EVT_C_ECHO`` which returns an ``0x0000`` *Success* status. The
 table below lists the possible intervention events.
 
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| Event                      | Description                    |                                                                              |
-+============================+================================+==============================================================================+
-| Association request includes extended negotiation items                                                                                    |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| ``evt.EVT_ASYNC_OPS``      | Association request includes   | `Handler documentation                                                       |
-|                            | Asynchronous Operations Window | <../reference/generated/pynetdicom._handlers.doc_handle_async.html>`_        |
-|                            | negotiation item               |                                                                              |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| ``evt.EVT_SOP_COMMON``     | Association request includes   | `Handler documentation                                                       |
-|                            | SOP Class Common Extended      | <../reference/generated/pynetdicom._handlers.doc_handle_sop_common.html>`_   |
-|                            | negotiation item(s)            |                                                                              |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| ``evt.EVT_SOP_EXTENDED``   | Association request includes   | `Handler documentation                                                       |
-|                            | SOP Class Extended negotiation | <../reference/generated/pynetdicom._handlers.doc_handle_sop_extended.html>`_ |
-|                            | item(s)                        |                                                                              |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| ``evt.EVT_USER_ID``        | Association request includes   | `Handler documentation                                                       |
-|                            | User Identity negotiation item | <../reference/generated/pynetdicom._handlers.doc_handle_userid.html>`_       |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| Service class received a DIMSE service request                                                                                             |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| ``evt.EVT_C_ECHO``         | Service class received         | `Handler documentation                                                       |
-|                            | C-ECHO request                 | <../reference/generated/pynetdicom._handlers.doc_handle_echo.html>`_         |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| ``evt.EVT_C_FIND``         | Service class received         | `Handler documentation                                                       |
-|                            | C-FIND request                 | <../reference/generated/pynetdicom._handlers.doc_handle_find.html>`_         |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| ``evt.EVT_C_GET``          | Service class received         | `Handler documentation                                                       |
-|                            | C-GET request                  | <../reference/generated/pynetdicom._handlers.doc_handle_c_get.html>`_        |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| ``evt.EVT_C_MOVE``         | Service class received         | `Handler documentation                                                       |
-|                            | C-MOVE request                 | <../reference/generated/pynetdicom._handlers.doc_handle_move.html>`_         |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| ``evt.EVT_C_STORE``        | Service class received         | `Handler documentation                                                       |
-|                            | C-STORE request                | <../reference/generated/pynetdicom._handlers.doc_handle_store.html>`_        |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
-| ``evt.EVT_N_GET``          | Service class received         | `Handler documentation                                                       |
-|                            | N-GET request                  | <../reference/generated/pynetdicom._handlers.doc_handle_n_get.html>`_        |
-+----------------------------+--------------------------------+------------------------------------------------------------------------------+
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| Event                      | Description                    |                                                                                |
++============================+================================+================================================================================+
+| Association request includes extended negotiation items                                                                                      |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_ASYNC_OPS``      | Association request includes   | `Handler documentation                                                         |
+|                            | Asynchronous Operations Window | <../reference/generated/pynetdicom._handlers.doc_handle_async.html>`_          |
+|                            | negotiation item               |                                                                                |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_SOP_COMMON``     | Association request includes   | `Handler documentation                                                         |
+|                            | SOP Class Common Extended      | <../reference/generated/pynetdicom._handlers.doc_handle_sop_common.html>`_     |
+|                            | negotiation item(s)            |                                                                                |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_SOP_EXTENDED``   | Association request includes   | `Handler documentation                                                         |
+|                            | SOP Class Extended negotiation | <../reference/generated/pynetdicom._handlers.doc_handle_sop_extended.html>`_   |
+|                            | item(s)                        |                                                                                |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_USER_ID``        | Association request includes   | `Handler documentation                                                         |
+|                            | User Identity negotiation item | <../reference/generated/pynetdicom._handlers.doc_handle_userid.html>`_         |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| Service class received a DIMSE service request                                                                                               |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_C_ECHO``         | Service class received         | `Handler documentation                                                         |
+|                            | C-ECHO request                 | <../reference/generated/pynetdicom._handlers.doc_handle_echo.html>`_           |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_C_FIND``         | Service class received         | `Handler documentation                                                         |
+|                            | C-FIND request                 | <../reference/generated/pynetdicom._handlers.doc_handle_find.html>`_           |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_C_GET``          | Service class received         | `Handler documentation                                                         |
+|                            | C-GET request                  | <../reference/generated/pynetdicom._handlers.doc_handle_c_get.html>`_          |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_C_MOVE``         | Service class received         | `Handler documentation                                                         |
+|                            | C-MOVE request                 | <../reference/generated/pynetdicom._handlers.doc_handle_move.html>`_           |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_C_STORE``        | Service class received         | `Handler documentation                                                         |
+|                            | C-STORE request                | <../reference/generated/pynetdicom._handlers.doc_handle_store.html>`_          |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_N_ACTION``       | Service class received         | `Handler documentation                                                         |
+|                            | N-ACTION request               | <../reference/generated/pynetdicom._handlers.doc_handle_n_action.html>`_       |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_N_CREATE``       | Service class received         | `Handler documentation                                                         |
+|                            | N-CREATE request               | <../reference/generated/pynetdicom._handlers.doc_handle_n_create.html>`_       |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_N_DELETE``       | Service class received         | `Handler documentation                                                         |
+|                            | N-DELETE request               | <../reference/generated/pynetdicom._handlers.doc_handle_n_delete.html>`_       |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_N_EVENT_REPORT`` | Service class received         | `Handler documentation                                                         |
+|                            | N-EVENT-REPORT request         | <../reference/generated/pynetdicom._handlers.doc_handle_n_event_report.html>`_ |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_N_GET``          | Service class received         | `Handler documentation                                                         |
+|                            | N-GET request                  | <../reference/generated/pynetdicom._handlers.doc_handle_n_get.html>`_          |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
+| ``evt.EVT_N_SET``          | Service class received         | `Handler documentation                                                         |
+|                            | N-SET request                  | <../reference/generated/pynetdicom._handlers.doc_handle_n_set.html>`_          |
++----------------------------+--------------------------------+--------------------------------------------------------------------------------+
 
 
 Event Handlers
diff --git a/pynetdicom/_handlers.py b/pynetdicom/_handlers.py
index ccda13018..bf01b19c0 100644
--- a/pynetdicom/_handlers.py
+++ b/pynetdicom/_handlers.py
@@ -2426,6 +2426,8 @@ def doc_handle_move(event):
         * ``context`` : the presentation context the request was sent under
           as a ``presentation.PresentationContextTuple``.
         * ``event`` : the event that occurred as ``namedtuple``.
+        * ``move_destination`` : the C-MOVE request's *Move Destination*
+          value as ``bytes``.
         * ``request`` : the received
           :py:class:`C-MOVE request <pynetdicom.dimse_primitives.C_MOVE>`
         * ``timestamp`` : the
@@ -2443,8 +2445,7 @@ 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.
-        * ``move_destination`` : returns the C-MOVE request's *Move
-          Destination* value as ``bytes``.
+
 
     Yields
     ------
@@ -2724,6 +2725,8 @@ def doc_handle_action(event):
         The event representing a service class receiving a N-ACTION
         request message. ``Event`` attributes are:
 
+        * ``action_type`` : the N-ACTION request's *Action Type
+          ID* parameter value as ``int``.
         * ``assoc`` : the
           :py:class:`association <pynetdicom.association.Association>`
           that is running the service that received the N-ACTION request.
@@ -3027,6 +3030,8 @@ def doc_handle_event_report(event):
         * ``context`` : the presentation context the request was sent under
           as a ``presentation.PresentationContextTuple``.
         * ``event`` : the event that occurred as ``namedtuple``.
+        * ``event_type`` : the N-EVENT-REPORT request's *Event Type
+          ID* parameter value as ``int``.
         * ``request`` : the received
           :py:class:`N-EVENT-REPORT request <pynetdicom.dimse_primitives.N_EVENT_REPORT>`
         * ``timestamp`` : the
diff --git a/pynetdicom/_version.py b/pynetdicom/_version.py
index 4182ad461..41af3a0b0 100644
--- a/pynetdicom/_version.py
+++ b/pynetdicom/_version.py
@@ -10,7 +10,7 @@ Parts of `extract_components` are taken from the pypa packaging project
 import re
 
 
-__version__ = '1.4.0.dev0'
+__version__ = '1.4.0'
 
 
 VERSION_PATTERN = r"""
diff --git a/pynetdicom/association.py b/pynetdicom/association.py
index a54116a47..fe29ecc6b 100644
--- a/pynetdicom/association.py
+++ b/pynetdicom/association.py
@@ -343,8 +343,9 @@ class Association(threading.Thread):
             the transfer syntax will not be used for matching. If the value
             corresponds to an uncompressed syntax then matches will be made
             with any uncompressed transfer syntaxes.
-        role : str
-            One of 'scu' or 'scp', the required role of the context.
+        role : str or None
+            One of 'scu' or 'scp', the required role of the context. If None
+            then the accepted role will be ignored.
         context_id : int or None
             If not None then the ID of the presentation context to use. It will
             be checked against the available parameter values.
@@ -387,6 +388,7 @@ class Association(threading.Thread):
             # Only a valid presentation context can reach this point
             return cx
 
+        role = role or 'scu'
         msg = (
             "No suitable presentation context for the {} role has been "
             "accepted by the peer for the SOP Class '{}'"
@@ -567,69 +569,9 @@ class Association(threading.Thread):
 
             # Check with the DIMSE provider to see if a completely decoded
             #   message is available
-            msg_context_id, msg = self.dimse.get_msg(block=False)
-
-            # DIMSE message received, should be a service request
-            if msg and not msg.is_valid_request:
-                LOGGER.warning(
-                    "Received unexpected {} service message"
-                    .format(msg.msg_type)
-                )
-            elif msg:
-                # Use the Message's Affected SOP Class UID or Requested SOP
-                #   Class UID to determine which service to use
-                # If there's no AffectedSOPClassUID or RequestedSOPClassUID
-                #   then we received a C-CANCEL request
-                class_uid = ''
-                if getattr(msg, 'AffectedSOPClassUID', None) is not None:
-                    # DIMSE-C, N-EVENT-REPORT, N-CREATE use AffectedSOPClassUID
-                    class_uid = msg.AffectedSOPClassUID
-                elif getattr(msg, 'RequestedSOPClassUID', None) is not None:
-                    # N-GET, N-SET, N-ACTION, N-DELETE use RequestedSOPClassUID
-                    class_uid = msg.RequestedSOPClassUID
-
-                # SOP Class Common Extended Negotiation
-                try:
-                    # The service class UID
-                    class_uid = (
-                        self.acceptor.accepted_common_extended[class_uid][0]
-                    )
-                except KeyError:
-                    pass
-
-                # Convert the SOP/Service UID to the corresponding service
-                service_class = uid_to_service_class(class_uid)(self)
-
-                try:
-                    context = self._accepted_cx[msg_context_id]
-                except KeyError:
-                    LOGGER.info(
-                        "Received DIMSE message with invalid or rejected "
-                        "context ID: %d", msg_context_id
-                    )
-                    LOGGER.debug("%s", msg)
-                    self.abort()
-                    return
-
-                # Run corresponding Service Class in SCP mode
-                try:
-                    # Clear out any C-CANCEL requests received beforehand
-                    self.dimse.cancel_req = {}
-                    service_class.SCP(msg, context)
-                    # Clear out any unacted upon requests received during
-                    self.dimse.cancel_req = {}
-                except NotImplementedError:
-                    # SCP isn't implemented
-                    LOGGER.error(
-                        "No supported service class available for the SOP "
-                        "Class UID '{}'".format(class_uid)
-                    )
-                    self.abort()
-                    return
-                except Exception as exc:
-                    LOGGER.exception(exc)
-                    self.abort()
-                    return
+            context_id, msg = self.dimse.get_msg(block=False)
+            if msg:
+                self._serve_request(msg, context_id)
 
             # Check for release request
             if self.acse.is_release_requested():
@@ -2690,7 +2632,10 @@ class Association(threading.Thread):
 
         # 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')
+        # As far as I can tell, N-EVENT-REPORT doesn't use SCP/SCU Role
+        #   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)
         transfer_syntax = context.transfer_syntax[0]
 
         # Build N-EVENT-REPORT request primitive
@@ -3183,6 +3128,75 @@ class Association(threading.Thread):
 
         return status, attribute_list
 
+    def _serve_request(self, msg, context_id):
+        """Handle a DIMSE service request.
+
+        Parameters
+        ----------
+        msg : dimse_primitives.DIMSEPrimitive subclass
+            The DIMSE service request primitive.
+        context_id : int
+            The ID of the presentation context that the request is being
+            made under.
+        """
+        # No message or not a service request
+        if not msg.is_valid_request:
+            LOGGER.warning(
+                "Received unexpected {} service message".format(msg.msg_type)
+            )
+            return
+
+        # Use the Message's Affected SOP Class UID or Requested SOP
+        #   Class UID to determine which service to use
+        class_uid = ''
+        if getattr(msg, 'AffectedSOPClassUID', None) is not None:
+            # DIMSE-C, N-EVENT-REPORT, N-CREATE use AffectedSOPClassUID
+            class_uid = msg.AffectedSOPClassUID
+        elif getattr(msg, 'RequestedSOPClassUID', None) is not None:
+            # N-GET, N-SET, N-ACTION, N-DELETE use RequestedSOPClassUID
+            class_uid = msg.RequestedSOPClassUID
+
+        # SOP Class Common Extended Negotiation
+        try:
+            # The service class UID
+            class_uid = self.acceptor.accepted_common_extended[class_uid][0]
+        except KeyError:
+            pass
+
+        # Convert the SOP/Service UID to the corresponding service
+        service_class = uid_to_service_class(class_uid)(self)
+
+        try:
+            context = self._accepted_cx[context_id]
+        except KeyError:
+            LOGGER.info(
+                "Received DIMSE message with invalid or rejected "
+                "context ID: %d", context_id
+            )
+            LOGGER.debug("%s", msg)
+            self.abort()
+            return
+
+        # Run corresponding Service Class in SCP mode
+        try:
+            # Clear out any C-CANCEL requests received beforehand
+            self.dimse.cancel_req = {}
+            service_class.SCP(msg, context)
+            # Clear out any unacted upon requests received during
+            self.dimse.cancel_req = {}
+        except NotImplementedError:
+            # SCP isn't implemented
+            LOGGER.error(
+                "No supported service class available for the SOP "
+                "Class UID '{}'".format(class_uid)
+            )
+            self.abort()
+            return
+        except Exception as exc:
+            LOGGER.exception(exc)
+            self.abort()
+            return
+
 
 class ServiceUser(object):
     """Convenience class for the ``Association`` service user.
diff --git a/pynetdicom/dimse.py b/pynetdicom/dimse.py
index 175b80b37..962d30075 100644
--- a/pynetdicom/dimse.py
+++ b/pynetdicom/dimse.py
@@ -288,6 +288,10 @@ class DIMSEServiceProvider(object):
             if isinstance(primitive, C_CANCEL) and len(self.cancel_req) < 10:
                 msg_id = primitive.MessageIDBeingRespondedTo
                 self.cancel_req[msg_id] = primitive
+            elif (isinstance(primitive, N_EVENT_REPORT) and
+                                                primitive.is_valid_request):
+                # N-EVENT-REPORT service requests are handled immediately
+                self.assoc._serve_request(primitive, context_id)
             else:
                 self.msg_queue.put((context_id, primitive))
 
diff --git a/pynetdicom/events.py b/pynetdicom/events.py
index 75f9ab3d9..54be3d943 100644
--- a/pynetdicom/events.py
+++ b/pynetdicom/events.py
@@ -248,6 +248,28 @@ class Event(object):
         )
         return self._get_dataset("ActionInformation", msg)
 
+    @property
+    def action_type(self):
+        """Return an N-ACTION request's `Action Type ID` as an int.
+
+        Returns
+        -------
+        int
+            The request's (0000,1008) *Action Type ID* value.
+
+        Raises
+        ------
+        AttributeError
+            If the corresponding event is not an N-ACTION request.
+        """
+        try:
+            return self.request.ActionTypeID
+        except AttributeError:
+            raise AttributeError(
+                "The corresponding event is not an N-ACTION request and has "
+                "no 'Action Type ID' parameter"
+            )
+
     @property
     def attribute_identifiers(self):
         """Return an N-GET request's `Attribute Identifier List` as a list of
@@ -370,6 +392,28 @@ class Event(object):
         )
         return self._get_dataset("EventInformation", msg)
 
+    @property
+    def event_type(self):
+        """Return an N-EVENT-REPORT request's `Event Type ID` as an int.
+
+        Returns
+        -------
+        int
+            The request's (0000,1002) *Event Type ID* value.
+
+        Raises
+        ------
+        AttributeError
+            If the corresponding event is not an N-EVENT-REPORT request.
+        """
+        try:
+            return self.request.EventTypeID
+        except AttributeError:
+            raise AttributeError(
+                "The corresponding event is not an N-EVENT-REPORT request "
+                "and has no 'Event Type ID' parameter"
+            )
+
     @property
     def file_meta(self):
         """Return a *pydicom* Dataset with the File Meta Information for a


Test patch:
diff --git a/pynetdicom/tests/test_events.py b/pynetdicom/tests/test_events.py
index 703398bdb..b7735b5ec 100644
--- a/pynetdicom/tests/test_events.py
+++ b/pynetdicom/tests/test_events.py
@@ -167,6 +167,20 @@ class TestEvent(object):
         with pytest.raises(AttributeError, match=msg):
             event.attribute_identifiers
 
+        msg = (
+            r"The corresponding event is not an N-ACTION request and has no "
+            r"'Action Type ID' parameter"
+        )
+        with pytest.raises(AttributeError, match=msg):
+            event.action_type
+
+        msg = (
+            r"The corresponding event is not an N-EVENT-REPORT request and "
+            r"has no 'Event Type ID' parameter"
+        )
+        with pytest.raises(AttributeError, match=msg):
+            event.event_type
+
     def test_is_cancelled_non(self):
         """Test Event.is_cancelled with wrong event type."""
         event = evt.Event(None, evt.EVT_DATA_RECV)
@@ -362,6 +376,30 @@ class TestEvent(object):
         assert isinstance(tags[1], BaseTag)
         assert tags[1] == 0x00100020
 
+    def test_action_type(self):
+        """Test with action_type."""
+        request = N_ACTION()
+        request.ActionTypeID = 2
+        event = Event(
+            None,
+            evt.EVT_N_ACTION,
+            {'request' : request, 'context' : self.context.as_tuple}
+        )
+
+        assert event.action_type == 2
+
+    def test_event_type(self):
+        """Test with event_type."""
+        request = N_EVENT_REPORT()
+        request.EventTypeID = 2
+        event = Event(
+            None,
+            evt.EVT_N_EVENT_REPORT,
+            {'request' : request, 'context' : self.context.as_tuple}
+        )
+
+        assert event.event_type == 2
+
 
 # TODO: Should be able to remove in v1.4
 INTERVENTION_HANDLERS = [
diff --git a/pynetdicom/tests/test_service_n.py b/pynetdicom/tests/test_service_n.py
index d4c9952c4..50bd50c48 100644
--- a/pynetdicom/tests/test_service_n.py
+++ b/pynetdicom/tests/test_service_n.py
@@ -1932,3 +1932,117 @@ class TestUPSFindServiceClass(object):
         assert cancel_results == [False, True]
 
         scp.shutdown()
+
+
+class TestNEventReport(object):
+    """Functional tests for N-EVENT-REPORT services."""
+    def setup(self):
+        """Run prior to each test"""
+        self.ae = None
+
+    def teardown(self):
+        """Clear any active threads"""
+        if self.ae:
+            self.ae.shutdown()
+
+    def test_same_assoc(self):
+        """Test SCP sending over the same association."""
+        def trigger_ner(event):
+            ds = Dataset()
+            ds.PatientName = 'Test2'
+            event.assoc.send_n_event_report(
+                ds,
+                1,
+                PrintJobSOPClass,
+                '1.2.3'
+            )
+
+            ds = Dataset()
+            ds.PatientName = 'Test'
+            return 0x0000, ds
+
+        scp_hh = [(evt.EVT_N_GET, trigger_ner)]
+
+        self.ae = ae = AE()
+        ae.acse_timeout = 5
+        ae.dimse_timeout = 5
+        ae.network_timeout = 5
+        ae.add_supported_context(PrintJobSOPClass)
+        scp = ae.start_server(('', 11112), block=False, evt_handlers=scp_hh)
+
+        events = []
+        def handle_ner(event):
+            events.append(event)
+            ds = Dataset()
+            ds.PatientName = 'Test3'
+            return 0x0000, ds
+
+        scu_hh = [(evt.EVT_N_EVENT_REPORT, handle_ner)]
+
+        ae.add_requested_context(PrintJobSOPClass)
+        assoc = ae.associate('', 11112, evt_handlers=scu_hh)
+        assert assoc.is_established
+
+        status, attr = assoc.send_n_get(
+            [(0x00080008)],
+            PrintJobSOPClass,
+            '1.2.3.4'
+        )
+
+        assoc.release()
+        assert assoc.is_released
+
+        scp.shutdown()
+
+        e = events[0]
+        assert e.event == evt.EVT_N_EVENT_REPORT
+        assert e.event_type == 1
+        assert e.event_information.PatientName == 'Test2'
+
+    def test_new_assoc(self):
+        """Test SCP sending over a new association."""
+        def trigger_ner(event):
+            ds = Dataset()
+            ds.PatientName = 'Test2'
+
+            assoc = event.assoc.ae.associate('', 11113)
+            assoc.send_n_event_report(
+                ds,
+                1,
+                PrintJobSOPClass,
+                '1.2.3'
+            )
+            assoc.release()
+
+        scp_hh = [(evt.EVT_ESTABLISHED, trigger_ner)]
+
+        self.ae = ae = AE()
+        ae.acse_timeout = 5
+        ae.dimse_timeout = 5
+        ae.network_timeout = 5
+        ae.add_supported_context(PrintJobSOPClass)
+        scp = ae.start_server(('', 11112), block=False, evt_handlers=scp_hh)
+
+        events = []
+        def handle_ner(event):
+            events.append(event)
+            ds = Dataset()
+            ds.PatientName = 'Test3'
+            return 0x0000, ds
+
+        scu_hh = [(evt.EVT_N_EVENT_REPORT, handle_ner)]
+        ner_scp = ae.start_server(('', 11113), block=False, evt_handlers=scu_hh)
+
+        ae.add_requested_context(PrintJobSOPClass)
+        assoc = ae.associate('', 11112)
+        assert assoc.is_established
+
+        assoc.release()
+        assert assoc.is_released
+
+        scp.shutdown()
+
+        e = events[0]
+        assert e.event == evt.EVT_N_EVENT_REPORT
+        assert e.event_type == 1
+        assert e.event_information.PatientName == 'Test2'