Repository: pydicom/pynetdicom
Pull request #427 (2020-01-14 03:46:34)

[WIP] Various improvements
* Add well-known SOP Instances for the supported service classes
* Make `ssl` optional and and add test coverage
* Update TLS tests to not use TLS v1.3 (for now)
* Refactor `dimse_message.py` to use keywords instead of tags and to simplify code
* Add missing optional status related element to N-GET
* Other minor improvements


#### 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.4.0.rst b/docs/changelog/v1.4.0.rst
index 16d50e114..b902bb070 100644
--- a/docs/changelog/v1.4.0.rst
+++ b/docs/changelog/v1.4.0.rst
@@ -3,6 +3,8 @@
 1.4.0
 =====
 
+.. currentmodule:: pynetdicom
+
 
 Fixes
 .....
@@ -11,8 +13,8 @@ Fixes
   if no matches were yielded by the C-FIND request handler.
 * Fixed association being aborted due to failure to decode a rejected
   presentation context when the transfer syntax value is empty (:issue:`342`)
-* Checkpoint added to the reactor in ``Association`` to prevent possible
-  race conditions:
+* Checkpoint added to the reactor in :class:`~association.Association` to
+  prevent possible race conditions:
 
   * When acting as the Acceptor and calling one of the ``Association.send_*()``
     methods.
@@ -22,14 +24,16 @@ Fixes
 Enhancements
 ............
 
-* 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 :attr:`Event.move_destination<events.Event.move_destination>` (for
+  ``evt.EVT_C_MOVE``), :attr:`Event.action_type<events.Event.action_type>`
+  (for ``evt.EVT_N_ACTION``), :attr:`Event.event_type<events.Event.event_type>`
+  (for ``evt.EVT_N_EVENT_REPORT``) and :attr:`Event.event<events.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``.
+  Print Management Meta SOP Class* to :mod:`~pynetdicom.sop_class`.
 * Added support for the following service classes:
 
   * Protocol Approval Query Retrieve (:issue:`327`)
@@ -43,7 +47,7 @@ Enhancements
 * Added handling of non-conformant Presentation Context AC (reject) PDUs which
   contain no Transfer Syntax Sub-item (:issue:`361`)
 * ``ae_title`` and ``contexts`` keyword arguments added to
-  ``AE.start_server()``
+  :meth:`~ae.ApplicationEntity.start_server`
 * Invalid (0000,1030) *Move Originator Application Entity Title* values
   received in C-STORE service requests are now logged and ignored rather than
   raising an exception and aborting the association.
@@ -66,10 +70,11 @@ Changes
   * ``_config.DECODE_STORE_DATASETS``
   * ``DIMSEServiceProvider.on_send_dimse_message()``,
     ``DIMSEServiceProvider.on_receive_dimse_message()``
-* ``Association.send_c_find()``, ``Association.send_c_get()`` and
-  ``Association.send_c_move()`` now require a `query_model` parameter that
+* :meth:`Association.send_c_find`, :meth:`Association.send_c_get` and
+  :meth:`Association.send_c_move` now require a `query_model` parameter that
   is used to set the *Affected SOP Class UID* value. The parameter should
-  pass a pydicom UID or UID string. Passing a key that corresponds to a given
-  UID is deprecated and will be removed in v1.5. (:issue:`348`)
+  pass a pydicom :class:`pydicom.uid.UID` or UID string. Passing a key that
+  corresponds to a given UID is deprecated and will be removed in v1.5.
+  (:issue:`348`)
 * ``Event.name`` and ``Event.description`` properties removed, use
   ``Event.event.name`` and ``Event.event.description`` instead.
diff --git a/docs/changelog/v1.5.0.rst b/docs/changelog/v1.5.0.rst
index 710e2cb89..074fc6bb4 100644
--- a/docs/changelog/v1.5.0.rst
+++ b/docs/changelog/v1.5.0.rst
@@ -9,22 +9,24 @@ Fixes
 * Fixed the :class:`~pynetdicom.transport.AssociationServer` not properly
   closing its sockets (:issue:`382`)
 * Improved the robustness of determining the local IP address (:issue:`394`)
-* Fixed not being able to set an Implementation Version Name of length 1
+* Fixed not being able to set an *Implementation Version Name* of length 1
   (:issue:`414`)
 
 Enhancements
 ............
 
 * Added configuration option :attr:`_config.USE_SHORT_DIMSE_AET
-  <pynetdicom._config.USE_SHORT_DIMSE_AET>` so elements with a VR of *AE* in
+  <pynetdicom._config.USE_SHORT_DIMSE_AET>` so elements with a VR of **AE** in
   DIMSE messages are only padded to the shortest even length rather than to
   the maximum 16 characters. (:issue:`403`)
 * 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
+* Added :py:attr:`~pynetdicom.events.Event.message_id` to
   :class:`~pynetdicom.events.Event` for use when receiving a DIMSE service
   request
+* Added the well-known SOP Instances for the supported service classes to
+  :mod:`~pynetdicom.sop_class`.
 
 Changes
 .......
diff --git a/docs/reference/acse.rst b/docs/reference/acse.rst
index 18a19adb9..f4ac5f9ee 100644
--- a/docs/reference/acse.rst
+++ b/docs/reference/acse.rst
@@ -1,5 +1,7 @@
 .. _api_acse:
 
+.. py:module:: pynetdicom.acse
+
 ACSE Service (:mod:`pynetdicom.acse`)
 ======================================
 
diff --git a/docs/reference/ae.rst b/docs/reference/ae.rst
index 51a6f541c..b82138d1d 100644
--- a/docs/reference/ae.rst
+++ b/docs/reference/ae.rst
@@ -1,5 +1,7 @@
 .. _api_ae:
 
+.. py:module:: pynetdicom.ae
+
 Application Entity (:mod:`pynetdicom.ae`)
 =========================================================
 
diff --git a/docs/reference/association.rst b/docs/reference/association.rst
index b3951dc55..1beba3bea 100644
--- a/docs/reference/association.rst
+++ b/docs/reference/association.rst
@@ -1,5 +1,7 @@
 .. _api_association:
 
+.. py:module:: pynetdicom.association
+
 Association (:mod:`pynetdicom.association`)
 ============================================
 
diff --git a/docs/reference/config.rst b/docs/reference/config.rst
index 6e43fb58b..611db9afe 100644
--- a/docs/reference/config.rst
+++ b/docs/reference/config.rst
@@ -1,5 +1,7 @@
 .. _api_config:
 
+.. py:module:: pynetdicom._config
+
 Configuration Options (:mod:`pynetdicom._config`)
 =================================================
 
diff --git a/docs/reference/dimse.messages.rst b/docs/reference/dimse.messages.rst
index 9ce37752f..ea754d919 100644
--- a/docs/reference/dimse.messages.rst
+++ b/docs/reference/dimse.messages.rst
@@ -1,5 +1,7 @@
 .. _api_dimse_messages:
 
+.. py:module:: pynetdicom.dimse_messages
+
 Messages (:mod:`pynetdicom.dimse_messages`)
 ============================================
 
diff --git a/docs/reference/dimse.primitives.rst b/docs/reference/dimse.primitives.rst
index 265766383..85da71bc9 100644
--- a/docs/reference/dimse.primitives.rst
+++ b/docs/reference/dimse.primitives.rst
@@ -1,5 +1,7 @@
 .. _api_dimse_primitives:
 
+.. py:module:: pynetdicom.dimse_primitives
+
 Service Primitives (:mod:`pynetdicom.dimse_primitives`)
 ========================================================
 
diff --git a/docs/reference/dimse.provider.rst b/docs/reference/dimse.provider.rst
index 023f2922d..df7bd8227 100644
--- a/docs/reference/dimse.provider.rst
+++ b/docs/reference/dimse.provider.rst
@@ -1,5 +1,7 @@
 .. _api_dimse_provider:
 
+.. py:module:: pynetdicom.dimse
+
 Service Provider (:mod:`pynetdicom.dimse`)
 ===========================================
 
diff --git a/docs/reference/dsutils.rst b/docs/reference/dsutils.rst
index 0e6c2bd21..c7ea0f48a 100644
--- a/docs/reference/dsutils.rst
+++ b/docs/reference/dsutils.rst
@@ -1,5 +1,7 @@
 .. _api_dsutils:
 
+.. py:module:: pynetdicom.dsutils
+
 Dataset Utilities (:mod:`pynetdicom.dsutils`)
 ==============================================
 
diff --git a/docs/reference/dul.pdu.rst b/docs/reference/dul.pdu.rst
index d8eac8f88..8eadc6598 100644
--- a/docs/reference/dul.pdu.rst
+++ b/docs/reference/dul.pdu.rst
@@ -1,5 +1,7 @@
 .. _api_dul_pdu:
 
+.. py:module:: pynetdicom.pdu
+
 PDUs (:mod:`pynetdicom.pdu`)
 =============================
 
diff --git a/docs/reference/dul.pdu_items.rst b/docs/reference/dul.pdu_items.rst
index 3593ecc72..b63b8534e 100644
--- a/docs/reference/dul.pdu_items.rst
+++ b/docs/reference/dul.pdu_items.rst
@@ -1,5 +1,7 @@
 .. _api_dul_pduitems:
 
+.. py:module:: pynetdicom.pdu_items
+
 PDU Items and Sub-items (:mod:`pynetdicom.pdu_items`)
 ======================================================
 
diff --git a/docs/reference/dul.pdu_primitives.rst b/docs/reference/dul.pdu_primitives.rst
index f447e75ce..ee62ee90d 100644
--- a/docs/reference/dul.pdu_primitives.rst
+++ b/docs/reference/dul.pdu_primitives.rst
@@ -1,5 +1,7 @@
 .. _api_dul_primitives:
 
+.. py:module:: pynetdicom.pdu_primitives
+
 Service Primitives (:mod:`pynetdicom.pdu_primitives`)
 ======================================================
 
diff --git a/docs/reference/dul.provider.rst b/docs/reference/dul.provider.rst
index 345af4e0f..567cde9c4 100644
--- a/docs/reference/dul.provider.rst
+++ b/docs/reference/dul.provider.rst
@@ -1,5 +1,7 @@
 .. _api_dul_provider:
 
+.. py:module:: pynetdicom.dul
+
 DUL Service Provider (:mod:`pynetdicom.dul`)
 =============================================
 
diff --git a/docs/reference/events.rst b/docs/reference/events.rst
index 8623521d5..af6453fee 100644
--- a/docs/reference/events.rst
+++ b/docs/reference/events.rst
@@ -1,5 +1,7 @@
 .. _api_events:
 
+.. py:module:: pynetdicom.events
+
 Events and Handlers (:mod:`pynetdicom.events`)
 ==============================================
 
diff --git a/docs/reference/fsm.rst b/docs/reference/fsm.rst
index 0f9b7ae35..88db9e62c 100644
--- a/docs/reference/fsm.rst
+++ b/docs/reference/fsm.rst
@@ -1,5 +1,7 @@
 .. _api_fsm:
 
+.. py:module:: pynetdicom.fsm
+
 State Machine (:mod:`pynetdicom.fsm`)
 ======================================
 
diff --git a/docs/reference/presentation.rst b/docs/reference/presentation.rst
index 233e9c5e0..2bfc1be21 100644
--- a/docs/reference/presentation.rst
+++ b/docs/reference/presentation.rst
@@ -1,5 +1,7 @@
 .. _api_presentation:
 
+.. py:module:: pynetdicom.presentation
+
 Presentation Service (:mod:`pynetdicom.presentation`)
 ======================================================
 
diff --git a/docs/reference/sop_classes.rst b/docs/reference/sop_classes.rst
index 440c36505..931688efe 100644
--- a/docs/reference/sop_classes.rst
+++ b/docs/reference/sop_classes.rst
@@ -1,5 +1,7 @@
 .. _api_sopclasses:
 
+.. py:module:: pynetdicom.sop_class
+
 SOP Classes (:mod:`pynetdicom.sop_class`)
 ==============================================
 
@@ -375,3 +377,19 @@ Verification
    :toctree: generated/
 
    VerificationSOPClass
+
+
+Well-known SOP Instances
+------------------------
+
+.. autosummary::
+   :toctree: generated/
+
+   DisplaySystemSOPInstance
+   PrinterConfigurationRetrievalSOPInstance
+   PrinterSOPInstance
+   ProceduralEventLoggingSOPInstance
+   StorageCommitmentPushModelSOPInstance
+   SubstanceAdministrationLoggingSOPInstance
+   UPSFilteredGlobalSubscriptionSOPInstance
+   UPSGlobalSubscriptionSOPInstance
diff --git a/docs/reference/status.rst b/docs/reference/status.rst
index c3e9aa7d4..04b76182e 100644
--- a/docs/reference/status.rst
+++ b/docs/reference/status.rst
@@ -1,5 +1,7 @@
 .. _api_status:
 
+.. py:module:: pynetdicom.status
+
 Statuses (:mod:`pynetdicom.status`)
 ====================================
 
diff --git a/docs/reference/timer.rst b/docs/reference/timer.rst
index affe0b7db..d39140e88 100644
--- a/docs/reference/timer.rst
+++ b/docs/reference/timer.rst
@@ -1,5 +1,7 @@
 .. _api_timer:
 
+.. py:module:: pynetdicom.timer
+
 Timer (:mod:`pynetdicom.timer`)
 ================================
 
diff --git a/docs/reference/transport.rst b/docs/reference/transport.rst
index 6b4226bc8..7585f5b98 100644
--- a/docs/reference/transport.rst
+++ b/docs/reference/transport.rst
@@ -1,5 +1,7 @@
 .. _api_transport:
 
+.. py:module:: pynetdicom.transport
+
 Transport Service (:mod:`pynetdicom.transport`)
 ===============================================
 
diff --git a/docs/reference/utils.rst b/docs/reference/utils.rst
index d6cabac12..b77e90985 100644
--- a/docs/reference/utils.rst
+++ b/docs/reference/utils.rst
@@ -1,5 +1,7 @@
 .. _api_utils:
 
+.. py:module:: pynetdicom.utils
+
 Utilities (:mod:`pynetdicom.utils`)
 ====================================
 
diff --git a/pynetdicom/association.py b/pynetdicom/association.py
index 47148868c..314e53ab3 100644
--- a/pynetdicom/association.py
+++ b/pynetdicom/association.py
@@ -78,7 +78,7 @@ class Association(threading.Thread):
         """Create a new :class:`Association` instance.
 
         The association starts in State 1 (idle). Association negotiation
-        won't begin until an :class:`~pynetdicom.transport,AssociationSocket`
+        won't begin until an :class:`~pynetdicom.transport.AssociationSocket`
         is assigned using :meth:`set_socket` and
         :meth:`Association.start_server()<pynetdicom.Association.start_server>`
         is called.
diff --git a/pynetdicom/dimse_messages.py b/pynetdicom/dimse_messages.py
index f5c9bf3e6..7a4fc3137 100644
--- a/pynetdicom/dimse_messages.py
+++ b/pynetdicom/dimse_messages.py
@@ -6,8 +6,6 @@ import logging
 from math import ceil
 
 from pydicom.dataset import Dataset
-from pydicom.tag import Tag
-from pydicom._dicom_dict import DicomDictionary as dcm_dict
 
 from pynetdicom.dimse_primitives import (
     C_STORE, C_FIND, C_GET, C_MOVE, C_ECHO, C_CANCEL,
@@ -19,146 +17,146 @@ from pynetdicom.pdu_primitives import P_DATA
 
 LOGGER = logging.getLogger('pynetdicom.dimse')
 
-_MESSAGE_TYPES = {
-    0x0001: 'C-STORE-RQ',
-    0x8001: 'C-STORE-RSP',
-    0x0020: 'C-FIND-RQ',
-    0x8020: 'C-FIND-RSP',
-    0x0010: 'C-GET-RQ',
-    0x8010: 'C-GET-RSP',
-    0x0021: 'C-MOVE-RQ',
-    0x8021: 'C-MOVE-RSP',
-    0x0030: 'C-ECHO-RQ',
-    0x8030: 'C-ECHO-RSP',
-    0x0FFF: 'C-CANCEL-RQ',
-    0x0100: 'N-EVENT-REPORT-RQ',
-    0x8100: 'N-EVENT-REPORT-RSP',
-    0x0110: 'N-GET-RQ',
-    0x8110: 'N-GET-RSP',
-    0x0120: 'N-SET-RQ',
-    0x8120: 'N-SET-RSP',
-    0x0130: 'N-ACTION-RQ',
-    0x8130: 'N-ACTION-RSP',
-    0x0140: 'N-CREATE-RQ',
-    0x8140: 'N-CREATE-RSP',
-    0x0150: 'N-DELETE-RQ',
-    0x8150: 'N-DELETE-RSP'
-}
 
 # PS3.7 Section 9.3
-_COMMAND_SET_ELEM = {
+_COMMAND_SET_KEYWORDS = {
     'C-ECHO-RQ': (
-        0x00000000,  # CommandGroupLength
-        0x00000002,  # AffectedSOPClassUID
-        0x00000100,  # CommandField
-        0x00000110,  # MessageID
-        0x00000800,  # CommandDataSetType
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageID', 'CommandDataSetType',
     ),
     'C-ECHO-RSP': (
-        0x00000000, 0x00000002, 0x00000100,
-        0x00000120,  # MessageIDBeingRespondedTo
-        0x00000800,
-        0x00000900,  # Status
-        0x00000902,  # ErrorComment
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'ErrorComment',
     ),
     'C-STORE-RQ': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000110,
-        0x00000700,  # Priority
-        0x00000800,
-        0x00001000,  # AffectedSOPInstanceUID
-        0x00001030,  # MoveOriginatorApplicationEntityTitle
-        0x00001031,  # MoveOriginatorMessageID
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageID', 'Priority', 'CommandDataSetType',
+        'AffectedSOPInstanceUID', 'MoveOriginatorApplicationEntityTitle',
+        'MoveOriginatorMessageID',
     ),
     'C-STORE-RSP': (
-        0x00000000, 0x00000002, 0x00000100,
-        0x00000120, 0x00000800, 0x00000900,
-        0x00000901,  # OffendingElement
-        0x00000902, 0x00001000
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'AffectedSOPInstanceUID',
+        'OffendingElement', 'ErrorComment',
     ),
     'C-FIND-RQ': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000110, 0x00000700, 0x00000800
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageID', 'Priority', 'CommandDataSetType'
     ),
     'C-FIND-RSP': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000120,
-        0x00000800, 0x00000900, 0x00000901, 0x00000902
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'OffendingElement', 'ErrorComment'
     ),
     'C-CANCEL-RQ': (
-        0x00000000, 0x00000100, 0x00000120, 0x00000800
+        'CommandGroupLength', 'CommandField', 'MessageIDBeingRespondedTo',
+        'CommandDataSetType'
     ),
     'C-GET-RQ': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000110, 0x00000700, 0x00000800
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageID', 'Priority', 'CommandDataSetType'
     ),
     'C-GET-RSP': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000120,
-        0x00000800, 0x00000900, 0x00000901, 0x00000902,
-        0x00001020,  # NumberOfRemainingSuboperations
-        0x00001021,  # NumberOfCompletedSuboperations
-        0x00001022,  # NumberOfFailedSuboperations
-        0x00001023,  # NumberOfWarningSuboperations
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'NumberOfRemainingSuboperations', 'NumberOfCompletedSuboperations',
+        'NumberOfFailedSuboperations', 'NumberOfWarningSuboperations',
+        'OffendingElement', 'ErrorComment',
     ),
     'C-MOVE-RQ': (
-        0x00000000, 0x00000002, 0x00000100,
-        0x00000110, 0x00000700, 0x00000800,
-        0x00000600,  # MoveDestination
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageID', 'Priority', 'CommandDataSetType', 'MoveDestination',
     ),
     'C-MOVE-RSP': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000120,
-        0x00000800, 0x00000900, 0x00000901, 0x00000902,
-        0x00001020, 0x00001021, 0x00001022, 0x00001023
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'NumberOfRemainingSuboperations', 'NumberOfCompletedSuboperations',
+        'NumberOfFailedSuboperations', 'NumberOfWarningSuboperations',
+        'OffendingElement', 'ErrorComment'
     ),
     'N-EVENT-REPORT-RQ': (
-        0x00000000, 0x00000002, 0x00000100,
-        0x00000110, 0x00000800, 0x00001000,
-        0x00001002,   # EventTypeID
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageID', 'CommandDataSetType', 'AffectedSOPInstanceUID',
+        'EventTypeID',
     ),
     'N-EVENT-REPORT-RSP': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000120,
-        0x00000800, 0x00000900, 0x00001000, 0x00001002,
-        0x00001002, 0x00000903, 0x00000902
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'AffectedSOPInstanceUID', 'EventTypeID', 'EventTypeID',
+        'ErrorID', 'ErrorComment'
     ),
     'N-GET-RQ': (
-        0x00000000, 0x00000003, 0x00000100, 0x00000110, 0x00000800,
-        0x00001001,  # RequestedSOPInstanceUID
-        0x00001005,  # AttributeIdentifierList
+        'CommandGroupLength', 'RequestedSOPClassUID', 'CommandField',
+        'MessageID', 'CommandDataSetType', 'RequestedSOPInstanceUID',
+        'AttributeIdentifierList',
     ),
     'N-GET-RSP': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000120,
-        0x00000800, 0x00000900, 0x00001000, 0x00000902,
-        0x00000903,  # ErrorID
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'AffectedSOPInstanceUID',
+        'AttributeIdentifierList', 'ErrorComment', 'ErrorID',
     ),
     'N-SET-RQ': (
-        0x00000000, 0x00000003, 0x00000100, 0x00000110, 0x00000800, 0x00001001
+        'CommandGroupLength', 'RequestedSOPClassUID', 'CommandField',
+        'MessageID', 'CommandDataSetType', 'RequestedSOPInstanceUID'
     ),
     'N-SET-RSP': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000120,
-        0x00000800, 0x00000900, 0x00001000, 0x00001005, 0x00000902, 0x00000903
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'AffectedSOPInstanceUID',
+        'AttributeIdentifierList', 'ErrorComment', 'ErrorID'
     ),
     'N-ACTION-RQ': (
-        0x00000000, 0x00000003, 0x00000100,
-        0x00000110, 0x00000800, 0x00001001,
-        0x00001008,  # ActionTypeID
+        'CommandGroupLength', 'RequestedSOPClassUID', 'CommandField',
+        'MessageID', 'CommandDataSetType', 'RequestedSOPInstanceUID',
+        'ActionTypeID',
     ),
     'N-ACTION-RSP': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000120,
-        0x00000800, 0x00000900, 0x00001000, 0x00001008, 0x00000903, 0x00000902
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'AffectedSOPInstanceUID', 'ActionTypeID',
+        'ErrorID', 'ErrorComment'
     ),
     'N-CREATE-RQ': (
-        0x00000000, 0x00000002, 0x00000100,
-        0x00000110, 0x00000800, 0x00001000
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageID', 'CommandDataSetType', 'AffectedSOPInstanceUID'
     ),
     'N-CREATE-RSP': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000120,
-        0x00000800, 0x00000900, 0x00001000, 0x00000903, 0x00000902
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'AffectedSOPInstanceUID',
+        'ErrorID', 'ErrorComment'
     ),
     'N-DELETE-RQ': (
-        0x00000000, 0x00000003, 0x00000100, 0x00000110, 0x00000800, 0x00001001
+        'CommandGroupLength', 'RequestedSOPClassUID', 'CommandField',
+        'MessageID', 'CommandDataSetType', 'RequestedSOPInstanceUID'
     ),
     'N-DELETE-RSP': (
-        0x00000000, 0x00000002, 0x00000100, 0x00000120,
-        0x00000800, 0x00000900, 0x00001000, 0x00000902, 0x00000903,
+        'CommandGroupLength', 'AffectedSOPClassUID', 'CommandField',
+        'MessageIDBeingRespondedTo', 'CommandDataSetType', 'Status',
+        'AffectedSOPInstanceUID',
+        'ErrorComment', 'ErrorID',
     )
 }
 
+# Used with DIMSEMessage.message_to_primitive
+_MSG_TO_PRIMITVE = {
+    'C_ECHO' : C_ECHO,
+    'C_STORE' : C_STORE,
+    'C_FIND' : C_FIND,
+    'C_GET' : C_GET,
+    'C_MOVE' : C_MOVE,
+    'C_CANCEL' : C_CANCEL,
+    'N_EVENT_REPORT' : N_EVENT_REPORT,
+    'N_GET' : N_GET,
+    'N_SET' : N_SET,
+    'N_ACTION' : N_ACTION,
+    'N_CREATE' : N_CREATE,
+    'N_DELETE' : N_DELETE,
+}
+
 
 class DIMSEMessage(object):
     """Represents a DIMSE Message.
@@ -295,14 +293,14 @@ class DIMSEMessage(object):
                     # Command Set is always encoded Implicit VR Little Endian
                     #   decode(dataset, is_implicit_VR, is_little_endian)
                     # pylint: disable=attribute-defined-outside-init
-                    self.command_set = decode(self.encoded_command_set,
-                                              True,
-                                              True)
+                    self.command_set = decode(
+                        self.encoded_command_set, True, True
+                    )
 
                     # Determine which DIMSE Message class to use
-                    self.__class__ = _MESSAGE_CLASS_TYPES[
-                        self.command_set.CommandField
-                    ]
+                    self.__class__ = (
+                        _MESSAGE_TYPES[self.command_set.CommandField][1]
+                    )
 
                     # Determine if a Data Set is present by checking for
                     #   (0000, 0800) CommandDataSetType US 1. If the value is
@@ -455,9 +453,10 @@ class DIMSEMessage(object):
         fragment_length : int
             The maximum size of each fragment, a value of 0 is taken to mean
             the fragment is infinite. Cannot be between 1 and 7 as
-            each Presentation Data Value Item is:
-            1 - 4       | 5          | 6 ->
-            Item length | Context ID | Presentation data value ->
+            each Presentation Data Value Item is::
+
+                1 - 4       | 5          | 6 ->
+                Item length | Context ID | Presentation data value ->
 
         Yields
         ------
@@ -495,35 +494,16 @@ class DIMSEMessage(object):
             :ref:`pynetdicom.dimse_primitives<api_dimse_primitives>` generated
             from the current ``DIMSEMessage`` sub-class object.
         """
-        # pylint: disable=too-many-branches
-        messages = {
-            'C_ECHO' : C_ECHO,
-            'C_STORE' : C_STORE,
-            'C_FIND' : C_FIND,
-            'C_GET' : C_GET,
-            'C_MOVE' : C_MOVE,
-            'C_CANCEL' : C_CANCEL,
-            'N_EVENT_REPORT' : N_EVENT_REPORT,
-            'N_GET' : N_GET,
-            'N_SET' : N_SET,
-            'N_ACTION' : N_ACTION,
-            'N_CREATE' : N_CREATE,
-            'N_DELETE' : N_DELETE,
-        }
         cls_type_name = self.__class__.__name__
         final_underscore = cls_type_name.rfind('_R')
-        primitive = messages[cls_type_name[:final_underscore]]()
+        primitive = _MSG_TO_PRIMITVE[cls_type_name[:final_underscore]]()
 
         # Command Set
         # For each parameter in the primitive, set the appropriate value
         #   from the Message's Command Set elements
         for elem in self.command_set:
             if hasattr(primitive, elem.keyword):
-                setattr(
-                    primitive,
-                    elem.keyword,
-                    self.command_set.__getattr__(elem.keyword)
-                )
+                setattr(primitive, elem.keyword, elem.value)
 
         # Datasets
         # Set the primitive's DataSet/Identifier/etc attribute
@@ -550,20 +530,21 @@ class DIMSEMessage(object):
         """
         # pylint: disable=too-many-branches,too-many-statements
         cls_type_name = self.__class__.__name__.replace('_', '-')
-        if cls_type_name not in _COMMAND_SET_ELEM:
-            raise ValueError("Can't convert primitive to message for unknown "
-                             "DIMSE message type '{}'".format(cls_type_name))
+        if cls_type_name not in _COMMAND_SET_KEYWORDS:
+            raise ValueError(
+                "Can't convert primitive to message for unknown "
+                "DIMSE message type '{}'".format(cls_type_name)
+            )
 
         # Command Set
         # Convert the message command set to the primitive attributes
         for elem in self.command_set:
-            # Use the short version of the element names as these should
-            #   match the parameter names in the primitive
+            # Use the element keyword as these should match the parameter
+            #   names in the primitive
             if hasattr(primitive, elem.keyword):
                 # If value hasn't been set for a parameter then delete
                 #   the corresponding element
                 attr = getattr(primitive, elem.keyword)
-
                 if attr is not None:
                     elem.value = attr
                 else:
@@ -571,10 +552,7 @@ class DIMSEMessage(object):
 
         # Theres a one-to-one relationship in the _MESSAGE_TYPES dict, so
         #   invert it for convenience
-        rev_type = {}
-        for value in _MESSAGE_TYPES:
-            rev_type[_MESSAGE_TYPES[value]] = value
-
+        rev_type = {vv[0]: kk for kk, vv in _MESSAGE_TYPES.items()}
         self.command_set.CommandField = rev_type[cls_type_name]
 
         # Data Set
@@ -616,61 +594,49 @@ class DIMSEMessage(object):
 
 
 def _build_message_classes(message_name):
-    """
-    Create a new subclass instance of DIMSEMessage for the given DIMSE
+    """Create a new subclass instance of DIMSEMessage for the given DIMSE
     `message_name`.
 
     Parameters
     ----------
     message_name : str
         The name/type of message class to construct, one of the following:
-        * C-ECHO-RQ
-        * C-ECHO-RSP
-        * C-STORE-RQ
-        * C-STORE-RSP
-        * C-FIND-RQ
-        * C-FIND-RSP
-        * C-GET-RQ
-        * C-GET-RSP
-        * C-MOVE-RQ
-        * C-MOVE-RSP
+
+        * C-ECHO-RQ, C-ECHO-RSP
+        * C-STORE-RQ, C-STORE-RSP
+        * C-FIND-RQ, C-FIND-RSP
+        * C-GET-RQ, C-GET-RSP
+        * C-MOVE-RQ, C-MOVE-RSP
         * C-CANCEL-RQ
-        * N-EVENT-REPORT-RQ
-        * N-EVENT-REPORT-RSP
-        * N-GET-RQ
-        * N-GET-RSP
-        * N-SET-RQ
-        * N-SET-RSP
-        * N-ACTION-RQ
-        * N-ACTION-RSP
-        * N-CREATE-RQ
-        * N-CREATE-RSP
-        * N-DELETE-RQ
-        * N-DELETE-RSP
+        * N-EVENT-REPORT-RQ, N-EVENT-REPORT-RSP
+        * N-GET-RQ, N-GET-RSP
+        * N-SET-RQ, N-SET-RSP
+        * N-ACTION-RQ, N-ACTION-RSP
+        * N-CREATE-RQ, N-CREATE-RSP
+        * N-DELETE-RQ, N-DELETE-RSP
     """
-
     def __init__(self):
         DIMSEMessage.__init__(self)
         # Create a new Dataset object for the command_set attributes
         ds = Dataset()
-        for elem_tag in _COMMAND_SET_ELEM[message_name]:
-            tag = Tag(elem_tag)
-            vr = dcm_dict[elem_tag][0]
-
+        for keyword in _COMMAND_SET_KEYWORDS[message_name]:
             # If the required command set elements are expanded this will need
             #   to be checked to ensure it functions OK
             try:
-                ds.add_new(tag, vr, None)
+                # TODO: In pydicom v1.4 should only need to set using `None`
+                setattr(ds, keyword, None)
             except TypeError:
-                ds.add_new(tag, vr, '')
+                setattr(ds, keyword, '')
 
         self.command_set = ds
 
     # Create new subclass of DIMSE Message using the supplied name
     #   but replace hyphens with underscores
-    cls = type(message_name.replace('-', '_'),
-               (DIMSEMessage, ),
-               {"__init__": __init__})
+    cls = type(
+        message_name.replace('-', '_'),
+        (DIMSEMessage, ),
+        {"__init__": __init__}
+    )
 
     # Add the class to the module
     globals()[cls.__name__] = cls
@@ -678,36 +644,38 @@ def _build_message_classes(message_name):
     return cls
 
 
-for __msg_type in _COMMAND_SET_ELEM:
+for __msg_type in _COMMAND_SET_KEYWORDS:
     _build_message_classes(__msg_type)
 
+
 # Values from PS3.5
-_MESSAGE_CLASS_TYPES = {
-    0x0001: C_STORE_RQ,
-    0x8001: C_STORE_RSP,
-    0x0020: C_FIND_RQ,
-    0x8020: C_FIND_RSP,
-    0x0FFF: C_CANCEL_RQ,
-    0x0010: C_GET_RQ,
-    0x8010: C_GET_RSP,
-    0x0021: C_MOVE_RQ,
-    0x8021: C_MOVE_RSP,
-    0x0030: C_ECHO_RQ,
-    0x8030: C_ECHO_RSP,
-    0x0100: N_EVENT_REPORT_RQ,
-    0x8100: N_EVENT_REPORT_RSP,
-    0x0110: N_GET_RQ,
-    0x8110: N_GET_RSP,
-    0x0120: N_SET_RQ,
-    0x8120: N_SET_RSP,
-    0x0130: N_ACTION_RQ,
-    0x8130: N_ACTION_RSP,
-    0x0140: N_CREATE_RQ,
-    0x8140: N_CREATE_RSP,
-    0x0150: N_DELETE_RQ,
-    0x8150: N_DELETE_RSP
+_MESSAGE_TYPES = {
+    0x0001: ('C-STORE-RQ', C_STORE_RQ),
+    0x8001: ('C-STORE-RSP', C_STORE_RSP),
+    0x0020: ('C-FIND-RQ', C_FIND_RQ),
+    0x8020: ('C-FIND-RSP', C_FIND_RSP),
+    0x0010: ('C-GET-RQ', C_GET_RQ),
+    0x8010: ('C-GET-RSP', C_GET_RSP),
+    0x0021: ('C-MOVE-RQ', C_MOVE_RQ),
+    0x8021: ('C-MOVE-RSP', C_MOVE_RSP),
+    0x0030: ('C-ECHO-RQ', C_ECHO_RQ),
+    0x8030: ('C-ECHO-RSP', C_ECHO_RSP),
+    0x0FFF: ('C-CANCEL-RQ', C_CANCEL_RQ),
+    0x0100: ('N-EVENT-REPORT-RQ', N_EVENT_REPORT_RQ),
+    0x8100: ('N-EVENT-REPORT-RSP', N_EVENT_REPORT_RSP),
+    0x0110: ('N-GET-RQ', N_GET_RQ),
+    0x8110: ('N-GET-RSP', N_GET_RSP),
+    0x0120: ('N-SET-RQ', N_SET_RQ),
+    0x8120: ('N-SET-RSP', N_SET_RSP),
+    0x0130: ('N-ACTION-RQ', N_ACTION_RQ),
+    0x8130: ('N-ACTION-RSP', N_ACTION_RSP),
+    0x0140: ('N-CREATE-RQ', N_CREATE_RQ),
+    0x8140: ('N-CREATE-RSP', N_CREATE_RSP),
+    0x0150: ('N-DELETE-RQ', N_DELETE_RQ),
+    0x8150: ('N-DELETE-RSP', N_DELETE_RSP),
 }
 
+
 _DATASET_KEYWORDS = {
     'C_STORE_RQ' : 'DataSet',
     'C_FIND_RQ' : 'Identifier',
diff --git a/pynetdicom/dimse_primitives.py b/pynetdicom/dimse_primitives.py
index 3d8371844..d69cb1cf6 100644
--- a/pynetdicom/dimse_primitives.py
+++ b/pynetdicom/dimse_primitives.py
@@ -1590,7 +1590,9 @@ class N_GET(DIMSEPrimitive):
     Status : int
         The error or success notification of the operation.
     """
-    STATUS_OPTIONAL_KEYWORDS = ('ErrorComment', 'ErrorID', )
+    STATUS_OPTIONAL_KEYWORDS = (
+        'AttributeIdentifierList', 'ErrorComment', 'ErrorID',
+    )
     REQUEST_KEYWORDS = (
         'MessageID', 'RequestedSOPClassUID', 'RequestedSOPInstanceUID'
     )
diff --git a/pynetdicom/dsutils.py b/pynetdicom/dsutils.py
index 5bf5bc64e..e80c75d01 100644
--- a/pynetdicom/dsutils.py
+++ b/pynetdicom/dsutils.py
@@ -70,9 +70,9 @@ def encode(ds, is_implicit_vr, is_little_endian):
     fp.is_little_endian = is_little_endian
     try:
         write_dataset(fp, ds)
-    except Exception as ex:
+    except Exception as exc:
         LOGGER.error("pydicom.write_dataset() failed:")
-        LOGGER.error(ex)
+        LOGGER.exception(exc)
         fp.close()
         return None
 
diff --git a/pynetdicom/dul.py b/pynetdicom/dul.py
index 8a3e206e7..b028481d2 100644
--- a/pynetdicom/dul.py
+++ b/pynetdicom/dul.py
@@ -128,17 +128,7 @@ class DULServiceProvider(Thread):
         bytestream = bytes(bytestream)
         evt.trigger(self.assoc, evt.EVT_DATA_RECV, {'data' : bytestream})
 
-        pdu_types = {
-            b'\x01' : (A_ASSOCIATE_RQ, 'Evt6'),
-            b'\x02' : (A_ASSOCIATE_AC, 'Evt3'),
-            b'\x03' : (A_ASSOCIATE_RJ, 'Evt4'),
-            b'\x04' : (P_DATA_TF, 'Evt10'),
-            b'\x05' : (A_RELEASE_RQ, 'Evt12'),
-            b'\x06' : (A_RELEASE_RP, 'Evt13'),
-            b'\x07' : (A_ABORT_RQ, 'Evt16')
-        }
-
-        pdu, event = pdu_types[bytestream[0:1]]
+        pdu, event = _PDU_TYPES[bytestream[0:1]]
         pdu = pdu()
         pdu.decode(bytestream)
 
@@ -472,3 +462,14 @@ class DULServiceProvider(Thread):
             return True
 
         return False
+
+
+_PDU_TYPES = {
+    b'\x01' : (A_ASSOCIATE_RQ, 'Evt6'),
+    b'\x02' : (A_ASSOCIATE_AC, 'Evt3'),
+    b'\x03' : (A_ASSOCIATE_RJ, 'Evt4'),
+    b'\x04' : (P_DATA_TF, 'Evt10'),
+    b'\x05' : (A_RELEASE_RQ, 'Evt12'),
+    b'\x06' : (A_RELEASE_RP, 'Evt13'),
+    b'\x07' : (A_ABORT_RQ, 'Evt16')
+}
diff --git a/pynetdicom/events.py b/pynetdicom/events.py
index 789809bbc..21fba9cca 100644
--- a/pynetdicom/events.py
+++ b/pynetdicom/events.py
@@ -280,7 +280,7 @@ class Event(object):
     @property
     def action_information(self):
         """Return an N-ACTION request's `Action Information` as a *pydicom*
-        Dataset.
+        :class:`~pydicom.dataset.Dataset`.
 
         Because *pydicom* defers data parsing during decoding until an element
         is actually required the returned :class:`~pydicom.dataset.Dataset`
@@ -306,7 +306,7 @@ class Event(object):
 
     @property
     def action_type(self):
-        """Return an N-ACTION request's `Action Type ID` as an int.
+        """Return an N-ACTION request's `Action Type ID` as an :class:`int`.
 
         .. versionadded:: 1.4
 
@@ -330,8 +330,8 @@ class Event(object):
 
     @property
     def attribute_identifiers(self):
-        """Return an N-GET request's `Attribute Identifier List` as a list of
-        *pydicom* BaseTag.
+        """Return an N-GET request's `Attribute Identifier List` as a
+        :class:`list` of *pydicom* :class:`~pydicom.tag.BaseTag`.
 
         Returns
         -------
@@ -362,7 +362,7 @@ class Event(object):
     @property
     def attribute_list(self):
         """Return an N-CREATE request's `Attribute List` as a *pydicom*
-        Dataset.
+        :class:`~pydicom.dataset.Dataset`.
 
         Because *pydicom* defers data parsing during decoding until an element
         is actually required the returned :class:`~pydicom.dataset.Dataset`
@@ -388,7 +388,8 @@ class Event(object):
 
     @property
     def dataset(self):
-        """Return a C-STORE request's `Data Set` as a *pydicom* Dataset.
+        """Return a C-STORE request's `Data Set` as a *pydicom*
+        :class:`~pydicom.dataset.Dataset`.
 
         Because *pydicom* defers data parsing during decoding until an element
         is actually required the returned :class:`~pydicom.dataset.Dataset`
@@ -429,7 +430,7 @@ class Event(object):
     @property
     def event_information(self):
         """Return an N-EVENT-REPORT request's `Event Information` as a
-        *pydicom* Dataset.
+        *pydicom* :class:`~pydicom.dataset.Dataset`.
 
         Because *pydicom* defers data parsing during decoding until an element
         is actually required the returned :class:`~pydicom.dataset.Dataset`
@@ -455,7 +456,8 @@ class Event(object):
 
     @property
     def event_type(self):
-        """Return an N-EVENT-REPORT request's `Event Type ID` as an int.
+        """Return an N-EVENT-REPORT request's `Event Type ID` as an
+        :class:`int`.
 
         .. versionadded:: 1.4
 
@@ -479,7 +481,7 @@ class Event(object):
 
     @property
     def file_meta(self):
-        """Return a *pydicom* Dataset with the
+        """Return a *pydicom* :class:`~pydicom.dataset.Dataset` with the
         :dcm:`File Meta Information<part10/chapter_7.html#sect_7.1>` for a
         C-STORE request's `Data Set`.
 
@@ -602,7 +604,7 @@ class Event(object):
     @property
     def identifier(self):
         """Return a C-FIND, C-GET or C-MOVE request's `Identifier` as a
-        *pydicom* Dataset.
+        *pydicom* :class:`~pydicom.dataset.Dataset`.
 
         Because *pydicom* defers data parsing during decoding until an element
         is actually required the returned :class:`~pydicom.dataset.Dataset`
@@ -651,7 +653,7 @@ class Event(object):
 
     @property
     def message_id(self):
-        """Return a DIMSE service request's 'Message ID' as int.
+        """Return a DIMSE service request's `Message ID` as :class:`int`.
 
         .. versionadded:: 1.5
 
@@ -677,7 +679,7 @@ class Event(object):
     @property
     def modification_list(self):
         """Return an N-SET request's `Modification List` as a *pydicom*
-        Dataset.
+        :class:`~pydicom.dataset.Dataset`.
 
         Because *pydicom* defers data parsing during decoding until an element
         is actually required the returned :class:`~pydicom.dataset.Dataset`
@@ -703,7 +705,7 @@ class Event(object):
 
     @property
     def move_destination(self):
-        """Return a C-MOVE request's `Move Destination` as bytes.
+        """Return a C-MOVE request's `Move Destination` as :class:`bytes`.
 
         .. versionadded:: 1.4
 
diff --git a/pynetdicom/pdu.py b/pynetdicom/pdu.py
index 684a4de65..af2b6031b 100644
--- a/pynetdicom/pdu.py
+++ b/pynetdicom/pdu.py
@@ -216,11 +216,7 @@ class PDU(object):
     @property
     def pdu_type(self):
         """Return the *PDU Type* field value as :class:`int`."""
-        key_val = PDU_TYPES.items()
-        keys = [key for (key, val) in key_val]
-        vals = [val for (key, val) in key_val]
-
-        return keys[vals.index(self.__class__)]
+        return PDU_TYPES[self.__class__]
 
     @staticmethod
     def _wrap_bytes(bytestream):
@@ -2011,13 +2007,13 @@ class A_ABORT_RQ(PDU):
         return 'No reason given'
 
 
-# PDUs indexed by their type
+# PDUs indexed by their class
 PDU_TYPES = {
-    0x01 : A_ASSOCIATE_RQ,
-    0x02 : A_ASSOCIATE_AC,
-    0x03 : A_ASSOCIATE_RJ,
-    0x04 : P_DATA_TF,
-    0x05 : A_RELEASE_RQ,
-    0x06 : A_RELEASE_RP,
-    0x07 : A_ABORT_RQ,
+    A_ASSOCIATE_RQ : 0x01,
+    A_ASSOCIATE_AC : 0x02,
+    A_ASSOCIATE_RJ : 0x03,
+    P_DATA_TF : 0x04,
+    A_RELEASE_RQ : 0x05,
+    A_RELEASE_RP : 0x06,
+    A_ABORT_RQ : 0x07,
 }
diff --git a/pynetdicom/pdu_items.py b/pynetdicom/pdu_items.py
index 33b445477..6940840f0 100644
--- a/pynetdicom/pdu_items.py
+++ b/pynetdicom/pdu_items.py
@@ -217,7 +217,7 @@ class PDUItem(object):
     @property
     def item_type(self):
         """Return the item's *Item Type* field value as :class:`int`."""
-        return {vv: kk for kk, vv in PDU_ITEM_TYPES.items()}[type(self)]
+        return _TYPE_TO_PDU_ITEM[type(self)]
 
     def __len__(self):
         """Return the total length of the encoded item as :class:`int`."""
@@ -3899,3 +3899,5 @@ PDU_ITEM_TYPES = {
     0x58 : UserIdentitySubItemRQ,
     0x59 : UserIdentitySubItemAC
 }
+
+_TYPE_TO_PDU_ITEM = {vv: kk for kk, vv in PDU_ITEM_TYPES.items()}
diff --git a/pynetdicom/sop_class.py b/pynetdicom/sop_class.py
index 7488c710b..854766f5d 100644
--- a/pynetdicom/sop_class.py
+++ b/pynetdicom/sop_class.py
@@ -1,4 +1,4 @@
-"""Generates the supported SOP Classes."""
+"""Generates the supported SOP Classes and well-known SOP Instances."""
 
 import inspect
 import logging
@@ -457,3 +457,46 @@ def uid_to_sop_class(uid):
     sop_class._service_class = ServiceClass
 
     return sop_class
+
+
+# Well-known SOP Instance UIDs for the supported Service Classes
+DisplaySystemSOPInstance = UID('1.2.840.10008.5.1.1.40.1')
+"""``1.2.840.10008.5.1.1.40.1``
+
+.. versionadded:: 1.5
+"""
+PrinterConfigurationRetrievalSOPInstance = UID('1.2.840.10008.5.1.1.17.376')
+"""``1.2.840.10008.5.1.1.17.376``
+
+.. versionadded:: 1.5
+"""
+PrinterSOPInstance = UID('1.2.840.10008.5.1.1.17')
+"""``1.2.840.10008.5.1.1.17``
+
+.. versionadded:: 1.5
+"""
+ProceduralEventLoggingSOPInstance = UID('1.2.840.10008.1.40.1')
+"""``1.2.840.10008.1.40.1``
+
+.. versionadded:: 1.5
+"""
+StorageCommitmentPushModelSOPInstance = UID('1.2.840.10008.1.20.1.1')
+"""``1.2.840.10008.1.20.1.1``
+
+.. versionadded:: 1.5
+"""
+SubstanceAdministrationLoggingSOPInstance = UID('1.2.840.10008.1.42.1')
+"""``1.2.840.10008.1.42.1``
+
+.. versionadded:: 1.5
+"""
+UPSFilteredGlobalSubscriptionSOPInstance = UID('1.2.840.10008.5.1.4.34.5.1')
+"""``1.2.840.10008.5.1.4.34.5.1``
+
+.. versionadded:: 1.5
+"""
+UPSGlobalSubscriptionSOPInstance = UID('1.2.840.10008.5.1.4.34.5')
+"""``1.2.840.10008.5.1.4.34.5``
+
+.. versionadded:: 1.5
+"""
diff --git a/pynetdicom/transport.py b/pynetdicom/transport.py
index 79c4cbe73..01ffb0d40 100644
--- a/pynetdicom/transport.py
+++ b/pynetdicom/transport.py
@@ -9,7 +9,11 @@ try:
     from SocketServer import TCPServer, ThreadingMixIn, BaseRequestHandler
 except ImportError:
     from socketserver import TCPServer, ThreadingMixIn, BaseRequestHandler
-import ssl
+try:
+    import ssl
+    _HAS_SSL = True
+except ImportError:
+    _HAS_SSL = False
 from struct import pack
 import threading
 
@@ -85,7 +89,7 @@ class AssociationSocket(object):
             # Evt5: Transport connection indication
             self.event_queue.put('Evt5')
 
-        self.tls_args = None
+        self._tls_args = None
         self.select_timeout = 0.5
 
     @property
@@ -347,6 +351,32 @@ class AssociationSocket(object):
         """Return the string output for ``socket``."""
         return self.socket.__str__()
 
+    @property
+    def tls_args(self):
+        """Return the TLS context and hostname (if set) or ``None``."""
+        return self._tls_args
+
+    @tls_args.setter
+    def tls_args(self, tls_args):
+        """Set the TLS arguments for the socket.
+
+        Parameters
+        ----------
+        tls_args : 2-tuple
+            If the socket should be wrapped by TLS then this is
+            ``(context, hostname)``, where *context* is a
+            :class:`ssl.SSLContext` that will be used to wrap the socket and
+            *hostname* is the value to use for the *server_hostname* keyword
+            argument for :meth:`SSLContext.wrap_socket()
+            <ssl.SSLContext.wrap_socket>`.
+        """
+        if not _HAS_SSL:
+            raise RuntimeError(
+                "Your Python installation lacks support for SSL"
+            )
+
+        self._tls_args = tls_args
+
 
 class RequestHandler(BaseRequestHandler):
     """Connection request handler for the ``AssociationServer``.
@@ -482,6 +512,11 @@ class AssociationServer(TCPServer):
         self.ae = ae
         self.ae_title = ae_title
         self.contexts = contexts
+        # Cover Python 2: old style class
+        if ssl_context and not _HAS_SSL:
+            raise RuntimeError(
+                "Your Python installation lacks support for SSL"
+            )
         self.ssl_context = ssl_context
         self.allow_reuse_address = True
 
@@ -663,6 +698,30 @@ class AssociationServer(TCPServer):
         self.server_close()
         self.ae._servers.remove(self)
 
+    @property
+    def ssl_context(self):
+        """Return the :class:`ssl.SSLContext` (if available)."""
+        return self._ssl_context
+
+    @ssl_context.setter
+    def ssl_context(self, context):
+        """Set the SSL context for the socket.
+
+        Parameters
+        ----------
+        context : ssl.SSLContext or None
+            If TLS is to be used then this should be the
+            :class:`ssl.SSLContext` used to wrap the client sockets, otherwise
+            if ``None`` then no TLS will be used (default).
+        """
+        # TODO: Uncomment when no longer supporting Python 2
+        #if not _HAS_SSL:
+        #    raise RuntimeError(
+        #        "Your Python installation lacks support for SSL"
+        #    )
+
+        self._ssl_context = context
+
     def unbind(self, event, handler):
         """Unbind a callable `handler` from an `event`.
 


Test patch:
diff --git a/pynetdicom/tests/benchmark_script.py b/pynetdicom/tests/benchmark_script.py
index f67869d67..929d21e14 100755
--- a/pynetdicom/tests/benchmark_script.py
+++ b/pynetdicom/tests/benchmark_script.py
@@ -182,6 +182,72 @@ def receive_store(nr_assoc, ds_per_assoc, write_ds=False, use_yappi=False):
     server.shutdown()
 
 
+def receive_store_internal(nr_assoc, ds_per_assoc, write_ds=False, use_yappi=False):
+    """Run a Storage SCP and transfer datasets with pynetdicom alone.
+
+    Parameters
+    ----------
+    nr_assoc : int
+        The total number of (sequential) associations that will be made.
+    ds_per_assoc : int
+        The number of C-STORE requests sent per successful association.
+    write_ds : bool, optional
+        True to write the received dataset to file, False otherwise (default).
+    use_yappi : bool, optional
+        True to use the yappi profiler, False otherwise (default).
+    """
+    if use_yappi:
+        init_yappi()
+
+    def handle(event):
+        if write_ds:
+            # TODO: optimise write using event.request.DataSet instead
+            # Standard write using dataset decode and re-encode
+            tfile = tempfile.TemporaryFile(mode='w+b')
+            ds = event.dataset
+            ds.file_meta = event.file_meta
+            ds.save_as(tfile)
+
+        return 0x0000
+
+    ae = AE()
+    ae.acse_timeout = 5
+    ae.dimse_timeout = 5
+    ae.network_timeout = 5
+    ae.add_supported_context(DATASET.SOPClassUID, ImplicitVRLittleEndian)
+    ae.add_requested_context(DATASET.SOPClassUID, ImplicitVRLittleEndian)
+
+    server = ae.start_server(
+        ('', 11112), block=False, evt_handlers=[(evt.EVT_C_STORE, handle)]
+    )
+
+    time.sleep(0.5)
+
+    start_time = time.time()
+    run_times = []
+
+    is_successful = True
+
+    for ii in range(nr_assoc):
+        assoc = ae.associate('127.0.0.1', 11112)
+        if assoc.is_established:
+            for jj in range(ds_per_assoc):
+                assoc.send_c_store(DATASET)
+
+            assoc.release()
+
+    if is_successful:
+        print(
+            "C-STORE SCU/SCP transferred {} total datasets over {} "
+            "association(s) in {:.2f} s"
+            .format(nr_assoc * ds_per_assoc, nr_assoc, time.time() - start_time)
+        )
+    else:
+        print("C-STORE SCU/SCP benchmark failed")
+
+    server.shutdown()
+
+
 def receive_store_simultaneous(nr_assoc, ds_per_assoc, use_yappi=False):
     """Run a Storage SCP and transfer datasets with simultaneous storescu's.
 
@@ -322,7 +388,8 @@ if __name__ == "__main__":
     print("  3. Storage SCP, 1000 datasets over 1 association")
     print("  4. Storage SCP, 1000 datasets over 1 association (write)")
     print("  5. Storage SCP, 1 dataset per association over 1000 associations")
-    print("  6. Storage SCP, 1000 dataset per association over 10 simultaneous associations")
+    print("  6. Storage SCP, 1000 datasets per association over 10 simultaneous associations")
+    print("  7. Storage SCU/SCP, 1000 datasets over 1 association")
     bench_index = input()
 
     if bench_index == "1":
@@ -337,3 +404,5 @@ if __name__ == "__main__":
         receive_store(1000, 1, False, use_yappi)
     elif bench_index == "6":
         receive_store_simultaneous(10, 1000, use_yappi)
+    elif bench_index == "7":
+        receive_store_internal(1, 1000, False, use_yappi)
diff --git a/pynetdicom/tests/hide_modules.py b/pynetdicom/tests/hide_modules.py
new file mode 100644
index 000000000..c13f526f3
--- /dev/null
+++ b/pynetdicom/tests/hide_modules.py
@@ -0,0 +1,107 @@
+"""Hide modules from import
+
+ ModuleHider - import finder hook and context manager
+ hide_modules - decorator using ModuleHider
+
+https://github.com/roryyorke/py-hide-modules
+"""
+
+# Copyright (c) 2019 Rory Yorke
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+try:
+    import importlib.abc
+    # py>=3.3 has MetaPathFinder
+    _ModuleHiderBase = getattr(importlib.abc, 'MetaPathFinder',
+                               importlib.abc.Finder)
+except ImportError:
+    # py2
+    _ModuleHiderBase = object
+
+
+class ModuleHider(_ModuleHiderBase):
+    """Import finder hook to hide specified modules
+    ModuleHider(hidden_modules) -> instance
+    hidden_modules is a list of strings naming modules to hide.
+    """
+
+    def __init__(self, hidden):
+        self.hidden = hidden
+        self.hidden_modules = {}
+
+    # python <=3.3
+    def find_module(self, fullname, path=None):
+        return self.find_spec(fullname, path)
+
+    # python >=3.4
+    def find_spec(self, fullname, path, target=None):
+        if fullname in self.hidden:
+            raise ImportError('No module named {}'.format(fullname))
+
+    def hide(self):
+        "Starting hiding modules"
+        import sys
+        if self in sys.meta_path:
+            raise RuntimeError("Already hiding modules")
+        # must be first to override standard finders
+        sys.meta_path.insert(0, self)
+        # remove hidden modules to force reload
+        for m in self.hidden:
+            if m in sys.modules:
+                self.hidden_modules[m] = sys.modules[m]
+                del sys.modules[m]
+
+    def unhide(self):
+        "Unhide modules"
+        import sys
+        sys.meta_path.remove(self)
+        sys.modules.update(self.hidden_modules)
+        self.hidden_modules.clear()
+
+    def __enter__(self):
+        self.hide()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.unhide()
+
+    # there's much point in __del__: sys.meta_path will keep a
+    # reference to an object on which .unhide() is not called, so
+    # refcount will only go to 0 if the object is removed from
+    # sys.meta_path somehow (in which case deletion doesn't
+    # matter), or when Python exits (ditto)
+
+
+def hide_modules(hidden):
+    """hide_modules(hidden_modules) -> decorator
+
+    When decorated function is called, the specified list of modules
+    will be hidden; once the function exits, the modules will be
+    unhidden.
+    """
+    def applydec(f):
+        def decf(*args, **kwargs):
+            with ModuleHider(hidden):
+                f(*args, **kwargs)
+        # carry across name so that nose still finds the test
+        decf.__name__ = f.__name__
+        # and carry across doc for test descriptions (etc.)
+        decf.__doc__ = f.__doc__
+        return decf
+    return applydec
diff --git a/pynetdicom/tests/test_assoc_n.py b/pynetdicom/tests/test_assoc_n.py
index c5e016f86..08fd62dc0 100644
--- a/pynetdicom/tests/test_assoc_n.py
+++ b/pynetdicom/tests/test_assoc_n.py
@@ -974,6 +974,7 @@ class TestAssociationSendNGet(object):
             ds.Status = 0xFFF0
             ds.ErrorComment = 'Some comment'
             ds.ErrorID = 12
+            ds.AttributeIdentifierList = [0x00100020]
             return ds, None
 
         self.ae = ae = AE()
@@ -995,6 +996,7 @@ class TestAssociationSendNGet(object):
         assert status.Status == 0xFFF0
         assert status.ErrorComment == 'Some comment'
         assert status.ErrorID == 12
+        assert status.AttributeIdentifierList == [0x00100020]
         assert ds is None
         assoc.release()
         assert assoc.is_released
diff --git a/pynetdicom/tests/test_pdu.py b/pynetdicom/tests/test_pdu.py
index 146a3cb14..ca64ece17 100644
--- a/pynetdicom/tests/test_pdu.py
+++ b/pynetdicom/tests/test_pdu.py
@@ -147,7 +147,7 @@ class TestPDU(object):
     def test_pdu_type_raises(self):
         """Test PDU.pdu_type raises ValueError."""
         pdu = PDU()
-        with pytest.raises(ValueError):
+        with pytest.raises(KeyError):
             pdu.pdu_type
 
     def test_wrap_bytes(self):
diff --git a/pynetdicom/tests/test_sop.py b/pynetdicom/tests/test_sop.py
index 34bf60d9d..4a928b34d 100644
--- a/pynetdicom/tests/test_sop.py
+++ b/pynetdicom/tests/test_sop.py
@@ -55,6 +55,14 @@ from pynetdicom.sop_class import (
     UnifiedProcedureStepPullSOPClass,
     _VERIFICATION_CLASSES,
     VerificationSOPClass,
+    DisplaySystemSOPInstance,
+    PrinterConfigurationRetrievalSOPInstance,
+    PrinterSOPInstance,
+    ProceduralEventLoggingSOPInstance,
+    StorageCommitmentPushModelSOPInstance,
+    SubstanceAdministrationLoggingSOPInstance,
+    UPSFilteredGlobalSubscriptionSOPInstance,
+    UPSGlobalSubscriptionSOPInstance
 )
 from pynetdicom.service_class import (
     ServiceClass,
@@ -138,6 +146,18 @@ def test_all_service_classes():
         assert uid in UID_dictionary
 
 
+def test_all_sop_instances():
+    """Test the well-known SOP Instances are correct."""
+    assert DisplaySystemSOPInstance in UID_dictionary
+    assert PrinterConfigurationRetrievalSOPInstance in UID_dictionary
+    assert PrinterSOPInstance in UID_dictionary
+    assert ProceduralEventLoggingSOPInstance in UID_dictionary
+    assert StorageCommitmentPushModelSOPInstance in UID_dictionary
+    assert SubstanceAdministrationLoggingSOPInstance in UID_dictionary
+    assert UPSFilteredGlobalSubscriptionSOPInstance in UID_dictionary
+    assert UPSGlobalSubscriptionSOPInstance in UID_dictionary
+
+
 class TestUIDtoSOPlass(object):
     """Tests for uid_to_sop_class"""
     def test_missing_sop(self):
diff --git a/pynetdicom/tests/test_transport.py b/pynetdicom/tests/test_transport.py
index a9af92277..7e55562be 100644
--- a/pynetdicom/tests/test_transport.py
+++ b/pynetdicom/tests/test_transport.py
@@ -19,14 +19,17 @@ import pytest
 
 from pydicom import dcmread
 
+import pynetdicom
 from pynetdicom import AE, evt, _config, debug_logger
 from pynetdicom.association import Association
 from pynetdicom.events import Event
 from pynetdicom._globals import MODE_REQUESTOR, MODE_ACCEPTOR
+from pynetdicom import transport
 from pynetdicom.transport import (
     AssociationSocket, AssociationServer, ThreadedAssociationServer
 )
 from pynetdicom.sop_class import VerificationSOPClass, RTImageStorage
+from .hide_modules import hide_modules
 
 
 # This is the directory that contains test data
@@ -195,6 +198,22 @@ def server_context(request):
     context.verify_mode = ssl.CERT_REQUIRED
     context.load_cert_chain(certfile=SERVER_CERT, keyfile=SERVER_KEY)
     context.load_verify_locations(cafile=CLIENT_CERT)
+
+    # TLS v1.3 is not currently supported :(
+    # The actual available attributes/protocols depend on OS, OpenSSL version
+    #   and Python version, ugh
+    if hasattr(ssl, 'TLSVersion'):
+        # This is the current and future, but heavily depends on OpenSSL
+        # Python 3.7+, w/ OpenSSL 1.1.0g+
+        context.maximum_version = ssl.TLSVersion.TLSv1_2
+    else:
+        # Should work with older Python and OpenSSL versions
+        # Python 2.7, 3.5, 3.6
+        context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2)
+        context.verify_mode = ssl.CERT_REQUIRED
+        context.load_cert_chain(certfile=SERVER_CERT, keyfile=SERVER_KEY)
+        context.load_verify_locations(cafile=CLIENT_CERT)
+
     return context
 
 
@@ -212,11 +231,20 @@ class TestTLS(object):
     """Test using TLS to wrap the association."""
     def setup(self):
         self.ae = None
+        self.has_ssl = transport._HAS_SSL
 
     def teardown(self):
         if self.ae:
             self.ae.shutdown()
 
+        # Ensure ssl module is available again
+        import importlib
+        try:
+            importlib.reload(pynetdicom.transport)
+        except AttributeError:
+            # Python 2
+            reload(pynetdicom.transport)
+
     def test_tls_not_server_not_client(self):
         """Test associating with no TLS on either end."""
         self.ae = ae = AE()
@@ -331,6 +359,50 @@ class TestTLS(object):
 
         assert len(ds[0].PixelData) == 2097152
 
+    @hide_modules(['ssl'])
+    def test_no_ssl_scp(self):
+        """Test exception raised if no SSL available to Python as SCP."""
+        # Reload pynetdicom package
+        import importlib
+        try:
+            importlib.reload(pynetdicom.transport)
+        except AttributeError:
+            # Python 2
+            reload(pynetdicom.transport)
+
+        self.ae = ae = AE()
+        ae.acse_timeout = 5
+        ae.dimse_timeout = 5
+        ae.network_timeout = 5
+        ae.add_supported_context('1.2.840.10008.1.1')
+        msg = r"Your Python installation lacks support for SSL"
+        with pytest.raises(RuntimeError, match=msg):
+            ae.start_server(
+                ('', 11112),
+                block=False,
+                ssl_context=['random', 'object'],
+            )
+
+    @hide_modules(['ssl'])
+    def test_no_ssl_scu(self):
+        """Test exception raised if no SSL available to Python as SCU."""
+        # Reload pynetdicom package
+        import importlib
+        try:
+            importlib.reload(pynetdicom.transport)
+        except AttributeError:
+            # Python 2
+            reload(pynetdicom.transport)
+
+        self.ae = ae = AE()
+        ae.acse_timeout = 5
+        ae.dimse_timeout = 5
+        ae.network_timeout = 5
+        ae.add_requested_context('1.2.840.10008.1.1')
+        msg = r"Your Python installation lacks support for SSL"
+        with pytest.raises(RuntimeError, match=msg):
+            ae.associate('', 11112, tls_args=(['random', 'object'], None))
+
 
 class TestAssociationServer(object):
     def setup(self):
@@ -342,7 +414,7 @@ class TestAssociationServer(object):
 
     @pytest.mark.skip()
     def test_multi_assoc_block(self):
-        """Test that multiple requestors can association when blocking."""
+        """Test that multiple requestors can associate when blocking."""
         self.ae = ae = AE()
         ae.maximum_associations = 10
         ae.add_supported_context('1.2.840.10008.1.1')