"""Implements the supported Service Classes."""
from io import BytesIO
import logging
import sys
from pydicom.dataset import Dataset
from pynetdicom.dsutils import decode, encode
from pynetdicom.dimse_primitives import C_STORE, C_ECHO, C_MOVE, C_GET, C_FIND
from pynetdicom.status import (
VERIFICATION_SERVICE_CLASS_STATUS,
STORAGE_SERVICE_CLASS_STATUS,
QR_FIND_SERVICE_CLASS_STATUS,
QR_MOVE_SERVICE_CLASS_STATUS,
QR_GET_SERVICE_CLASS_STATUS,
MODALITY_WORKLIST_SERVICE_CLASS_STATUS,
RELEVANT_PATIENT_SERVICE_CLASS_STATUS,
SUBSTANCE_ADMINISTRATION_SERVICE_CLASS_STATUS,
NON_PATIENT_SERVICE_CLASS_STATUS,
)
from pynetdicom._globals import (
STATUS_FAILURE,
STATUS_SUCCESS,
STATUS_WARNING,
STATUS_PENDING,
STATUS_CANCEL,
)
LOGGER = logging.getLogger('pynetdicom.service')
class ServiceClass(object):
"""The base class for all the service classes.
Attributes
----------
assoc : association.Association
The association instance offering the service.
"""
def __init__(self, assoc):
"""Create a new ServiceClass."""
self.assoc = assoc
@property
def ae(self):
"""Return the AE."""
return self.assoc.ae
@property
def dimse(self):
"""Return the DIMSE service provider."""
return self.assoc.dimse
def is_valid_status(self, status):
"""Return True if `status` is valid for the service class.
Parameters
----------
status : int
The Status value to check for validity.
Returns
-------
bool
True if the status is valid, False otherwise.
"""
if status in self.statuses:
return True
return False
def SCP(self, req, context, info):
"""The implementation of the corresponding service class.
Parameters
----------
req : A DIMSE message primitive
The message request primitive sent by the peer.
context : presentation.PresentationContext
The presentation context that the SCP is operating under.
info : dict
A dict containing details about the association.
"""
msg = (
"No service has been implemented for the SOP Class UID '{}'"
.format(context.abstract_syntax)
)
LOGGER.error(msg)
raise NotImplementedError(msg)
def validate_status(self, status, rsp):
"""Validate `status` and set `rsp.Status` accordingly.
Parameters
----------
status : pydicom.dataset.Dataset or int
A Dataset containing a Status element or an int.
rsp : dimse_primitive
The response primitive to be sent to the peer.
Returns
-------
rsp : dimse_primitive
The response primitie to be sent to the peer (containing a valid
Status parameter).
"""
# Check the callback's returned Status dataset
if isinstance(status, Dataset):
# Check that the returned status dataset contains a Status element
if 'Status' in status:
# For the elements in the status dataset, try and set the
# corresponding response primitive attribute
for elem in status:
if hasattr(rsp, elem.keyword):
setattr(rsp, elem.keyword, elem.value)
else:
LOGGER.warning("Status dataset returned by callback "
"contained an unsupported Element "
"'%s'.", elem.keyword)
else:
LOGGER.error("User callback returned a `Dataset` without a "
"Status element.")
# Failure: Cannot Understand - callback returned
# a pydicom.dataset.Dataset without a Status element
rsp.Status = 0xC001
elif isinstance(status, int):
rsp.Status = status
else:
LOGGER.error("Invalid status returned by callback")
# Failure: Cannot Understand - callback didn't return
# a valid status type
rsp.Status = 0xC002
if not self.is_valid_status(rsp.Status):
# Failure: Cannot Understand - Unknown status returned by the
# callback
LOGGER.warning("Unknown status value returned by "
"callback - 0x{0:04x}".format(rsp.Status))
return rsp
# Service Class implementations
[docs]class VerificationServiceClass(ServiceClass):
"""Implementation of the Verification Service Class."""
statuses = VERIFICATION_SERVICE_CLASS_STATUS
def SCP(self, req, context, info):
"""The SCP implementation for the Verification Service Class.
Will always return 0x0000 (Success) unless the user returns a different
(valid) status value from the `AE.on_c_echo` callback.
Parameters
----------
req : dimse_primitives.C_ECHO
The C-ECHO request primitive sent by the peer.
context : presentation.PresentationContext
The presentation context that the SCP is operating under.
info : dict
A dict containing details about the association.
See Also
--------
ae.ApplicationEntity.on_c_echo
association.Association.send_c_echo
Notes
-----
**C-ECHO Request**
*Parameters*
| (M) Message ID
| (M) Affected SOP Class UID
**C-ECHO Response**
*Parameters*
| (U) Message ID
| (M) Message ID Being Responded To
| (U) Affected SOP Class UID
| (M) Status
*Status*
The DICOM Standard, Part 7 (Table 9.3-13) indicates that the Status
value of a C-ECHO response "shall have a value of Success". However
Section 9.1.5.1.4 indicates it may have any of the following values:
Success
| ``0x0000`` Success
Failure
| ``0x0122`` Refused: SOP Class Not Supported
| ``0x0210`` Refused: Duplicate Invocation
| ``0x0211`` Refused: Unrecognised Operation
| ``0x0212`` Refused: Mistyped Argument
References
----------
* DICOM Standard, Part 4, `Annex A <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_A>`_
* DICOM Standard, Part 7, Sections
`9.1.5 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.1.5>`_,
`9.3.5 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.3.5>`_
and `Annex C <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_C>`_
"""
# Build C-ECHO response primitive
rsp = C_ECHO()
rsp.MessageID = req.MessageID
rsp.MessageIDBeingRespondedTo = req.MessageID
rsp.AffectedSOPClassUID = req.AffectedSOPClassUID
info['parameters'] = {
'message_id' : req.MessageID
}
# Try and run the user's on_c_echo callback. The callback should return
# the Status as either an int or Dataset, and any failures in the
# callback results in 0x0000 'Success'
try:
status = self.ae.on_c_echo(context.as_tuple, info)
if isinstance(status, Dataset):
if 'Status' not in status:
raise AttributeError("The 'status' dataset returned by "
"'on_c_echo' must contain"
"a (0000,0900) Status element")
for elem in status:
if hasattr(rsp, elem.keyword):
setattr(rsp, elem.keyword, elem.value)
else:
LOGGER.warning("The 'status' dataset returned by "
"'on_c_echo' contained an unsupported "
"Element '%s'.", elem.keyword)
elif isinstance(status, int):
rsp.Status = status
else:
raise TypeError("Invalid 'status' returned by 'on_c_echo'")
# Check Status validity
if not self.is_valid_status(rsp.Status):
LOGGER.warning(
"Unknown 'status' value returned by 'on_c_echo' "
"callback - 0x{0:04x}".format(rsp.Status)
)
except Exception as ex:
LOGGER.exception(ex)
LOGGER.error("Exception in the 'on_c_echo' callback, responding "
"with default 'status' value of 0x0000 (Success).")
rsp.Status = 0x0000
# Send primitive
self.dimse.send_msg(rsp, context.context_id)
[docs]class StorageServiceClass(ServiceClass):
"""Implementation of the Storage Service Class."""
uid = '1.2.840.10008.4.2'
statuses = STORAGE_SERVICE_CLASS_STATUS
def SCP(self, req, context, info):
"""The SCP implementation for the Storage Service Class.
Parameters
----------
req : dimse_primitives.C_STORE
The C-STORE request primitive sent by the peer.
context : presentation.PresentationContext
The presentation context that the SCP is operating under.
info : dict
A dict containing details about the association.
See Also
--------
ae.ApplicationEntity.on_c_store
association.Association.send_c_store
Notes
-----
**C-STORE Request**
*Parameters*
| (M) Message ID
| (M) Affected SOP Class UID
| (M) Affected SOP Instance UID
| (M) Priority
| (U) Move Originator Application Entity Title
| (U) Move Originator Message ID
| (M) Data Set
**C-STORE Response**
*Parameters*
| (U) Message ID
| (M) Message ID Being Responded To
| (U) Affected SOP Class UID
| (U) Affected SOP Instance UID
| (M) Status
*Status*
Success
| ``0x0000`` Success
Warning
| ``0xB000`` Warning: Coercion of Data Elements
| ``0xB006`` Warning: Elements Discarded
| ``0xB007`` Warning: Data Set Does Not Match SOP Class
Failure
| ``0x0117`` Refused: Invalid SOP Instance
| ``0x0122`` Refused: SOP Class Not Supported
| ``0x0124`` Refused: Not Authorised
| ``0x0210`` Refused: Duplicate Invocation
| ``0x0211`` Refused: Unrecognised Operation
| ``0x0212`` Refused: Mistyped Argument
| ``0xA700`` to ``0xA7FF`` Refused: Out of Resources
| ``0xA900`` to ``0xA9FF`` Error: Data Set Does Not Match SOP Class
| ``0xC000`` to ``0xCFFF`` Error: Cannot Understand
References
----------
* DICOM Standard, Part 4, `Annex B <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_B>`_.
* DICOM Standard, Part 4, `Annex GG <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_GG>`_.
* DICOM Standard, Part 7, Sections
`9.1.1 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.1.1>`_,
`9.3.1 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.3.1>`_
and `Annex C <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_C>`_.
"""
# Build C-STORE response primitive
rsp = C_STORE()
rsp.MessageID = req.MessageID
rsp.MessageIDBeingRespondedTo = req.MessageID
rsp.AffectedSOPInstanceUID = req.AffectedSOPInstanceUID
rsp.AffectedSOPClassUID = req.AffectedSOPClassUID
# Attempt to decode the request's dataset
transfer_syntax = context.transfer_syntax[0]
try:
ds = decode(req.DataSet,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
# Trigger exception if bad dataset
for elem in ds:
pass
except Exception as ex:
LOGGER.error("Failed to decode the received dataset")
LOGGER.exception(ex)
# Failure: Cannot Understand - Dataset decoding error
rsp.Status = 0xC210
rsp.ErrorComment = 'Unable to decode the dataset'
self.dimse.send_msg(rsp, context.context_id)
return
info['parameters'] = {
'message_id' : req.MessageID,
'priority' : req.Priority,
'originator_aet' : req.MoveOriginatorApplicationEntityTitle,
'originator_message_id' : req.MoveOriginatorMessageID
}
# Attempt to run the ApplicationEntity's on_c_store callback
try:
rsp_status = self.ae.on_c_store(ds, context.as_tuple, info)
except Exception as ex:
LOGGER.error("Exception in the ApplicationEntity.on_c_store() "
"callback")
LOGGER.exception(ex)
# Failure: Cannot Understand - Error in on_c_store callback
rsp_status = 0xC211
# Validate rsp_status and set rsp.Status accordingly
rsp = self.validate_status(rsp_status, rsp)
self.dimse.send_msg(rsp, context.context_id)
[docs]class QueryRetrieveServiceClass(ServiceClass):
"""Implementation of the Query/Retrieve Service Class."""
statuses = None
# Used with Composite Instance Retrieve Without Bulk Data
BULK_DATA_KEYWORDS = [
'PixelData', 'FloatPixelData', 'DoubleFloatPixelData',
'PixelDataProviderURL', 'SpectroscopyData', 'OverlayData',
'CurveData', 'AudioSampleData', 'EncapsulatedDocument'
]
def SCP(self, req, context, info):
"""The SCP implementation for the Query/Retrieve Service Class.
Parameters
----------
req : dimse_primitives.C_FIND or C_GET or C_MOVE
The request primitive received from the peer.
context : presentation.PresentationContext
The presentation context that the SCP is operating under.
info : dict
A dict containing details about the association.
"""
if context.abstract_syntax in ['1.2.840.10008.5.1.4.1.2.1.1',
'1.2.840.10008.5.1.4.1.2.2.1',
'1.2.840.10008.5.1.4.1.2.3.1',
'1.2.840.10008.5.1.4.20.1',
'1.2.840.10008.5.1.4.38.2',
'1.2.840.10008.5.1.4.39.2',
'1.2.840.10008.5.1.4.43.2',
'1.2.840.10008.5.1.4.44.2',
'1.2.840.10008.5.1.4.45.2']:
self.statuses = QR_FIND_SERVICE_CLASS_STATUS
self._find_scp(req, context, info)
elif context.abstract_syntax in ['1.2.840.10008.5.1.4.1.2.1.3',
'1.2.840.10008.5.1.4.1.2.2.3',
'1.2.840.10008.5.1.4.1.2.3.3',
'1.2.840.10008.5.1.4.1.2.4.3',
'1.2.840.10008.5.1.4.1.2.5.3',
'1.2.840.10008.5.1.4.20.3',
'1.2.840.10008.5.1.4.38.4',
'1.2.840.10008.5.1.4.39.4',
'1.2.840.10008.5.1.4.43.4',
'1.2.840.10008.5.1.4.44.4',
'1.2.840.10008.5.1.4.45.4']:
self.statuses = QR_GET_SERVICE_CLASS_STATUS
self._get_scp(req, context, info)
elif context.abstract_syntax in ['1.2.840.10008.5.1.4.1.2.1.2',
'1.2.840.10008.5.1.4.1.2.2.2',
'1.2.840.10008.5.1.4.1.2.3.2',
'1.2.840.10008.5.1.4.1.2.4.2',
'1.2.840.10008.5.1.4.20.2',
'1.2.840.10008.5.1.4.38.3',
'1.2.840.10008.5.1.4.39.3',
'1.2.840.10008.5.1.4.43.3',
'1.2.840.10008.5.1.4.44.3',
'1.2.840.10008.5.1.4.45.3']:
self.statuses = QR_MOVE_SERVICE_CLASS_STATUS
self._move_scp(req, context, info)
else:
raise ValueError(
'The supplied abstract syntax is not valid for use with the '
'Query/Retrieve Service Class'
)
def _find_scp(self, req, context, info):
"""The SCP implementation for Query/Retrieve - Find.
Parameters
----------
req : dimse_primitives.C_FIND
The C-FIND request primitive received from the peer.
context : presentation.PresentationContext
The presentation context that the SCP is operating under.
info : dict
A dict containing details about the association.
See Also
--------
ae.ApplicationEntity.on_c_find
association.Association.send_c_find
Notes
-----
**C-FIND Request**
*Parameters*
| (M) Message ID
| (M) Affected SOP Class UID
| (M) Priority
| (M) Identifier
*Identifier*
The C-FIND request Identifier shall contain:
* Key Attributes values to be matched against the values of storage
SOP Instances managed by the SCP.
* (0008,0052) Query/Retrieve Level.
* (0008,0053) Query/Retrieve View, if Enhanced Multi-Frame Image
Conversion has been accepted during Extended Negotiation. It shall
not be present otherwise.
* (0008,0005) Specific Character Set, if expanded or replacement
character sets may be used in any of the Attributes in the request
Identifier. It shall not be present otherwise.
* (0008,0201) Timezone Offset From UTC, if any Attributes of time in
the request Identifier are to be interpreted explicitly in the
designated local time zone. It shall not be present otherwise.
**C-FIND Response**
*Parameters*
| (U) Message ID
| (M) Message ID Being Responded To
| (U) Affected SOP Class UID
| (C) Identifier
| (M) Status
*Identifier*
The C-FIND response shall only include an Identifier when the Status is
'Pending'. When sent, the Identifier shall contain:
* Key Attributes with values corresponding to Key Attributes contained
in the Identifier of the req.
* (0008,0052) Query/Retrieve Level.
* (0008,0005) Specific Character Set, if expanded or replacement
character sets may be used in any of the Attributes in the response
Identifier. It shall not be present otherwise.
* (0008,0201) Timezone Offset From UTC, if any Attributes of time in
the response Identifier are to be interpreted explicitly in the
designated local time zone. It shall not be present otherwise.
The C-FIND response Identifier shall also contain either or both of:
* (0008,0130) Storage Media File-Set ID and (0088,0140) Storage Media
File-Set UID.
* (0008,0054) Retrieve AE Title.
The C-FIND response Identifier may also (but is not required to)
include the (0008,0056) Instance Availability element.
*Status*
Success
| ``0x0000`` Success
Pending
| ``0xFF00`` Matches are continuing, current match supplied
| ``0xFF01`` Matches are continuing, warning
Cancel
| ``0xFE00`` Cancel
Failure
| ``0x0122`` SOP class not supported
| ``0xA700`` Out of resources
| ``0xA900`` Identifier does not match SOP class
| ``0xC000`` to ``0xCFFF`` Unable to process
References
----------
.. [1] DICOM Standard, Part 4, `Annex C <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_C>`_.
.. [2] DICOM Standard, Part 7, Sections
`9.1.2 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.1.2>`_,
`9.3.2 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.3.2>`_
and `Annex C <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_C>`_.
"""
# Build C-FIND response primitive
rsp = C_FIND()
rsp.MessageID = req.MessageID
rsp.MessageIDBeingRespondedTo = req.MessageID
rsp.AffectedSOPClassUID = req.AffectedSOPClassUID
# Decode and log Identifier
transfer_syntax = context.transfer_syntax[0]
try:
identifier = decode(req.Identifier,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
LOGGER.info('Find SCP Request Identifiers:')
LOGGER.info('')
LOGGER.debug('# DICOM Dataset')
for elem in identifier.iterall():
LOGGER.info(elem)
LOGGER.info('')
except Exception as ex:
LOGGER.error("Failed to decode the request's Identifier dataset.")
LOGGER.exception(ex)
# Failure - Unable to Process - Failed to decode Identifier
rsp.Status = 0xC310
rsp.ErrorComment = 'Unable to decode the dataset'
self.dimse.send_msg(rsp, context.context_id)
return
info['parameters'] = {
'message_id' : req.MessageID,
'priority' : req.Priority
}
stopper = object()
# This will wrap exceptions during iteration and return a good value.
def wrap_on_c_find():
"""Wrapper for exceptions"""
try:
# We unpack here so that the error is still caught
for val1, val2 in self.ae.on_c_find(identifier,
context.as_tuple,
info):
yield val1, val2
except Exception:
# TODO: special (singleton) value
yield stopper, sys.exc_info()
ii = -1 # So if there are no results, log below doesn't break
# Iterate through the results
for ii, (rsp_status, rsp_identifier) in enumerate(wrap_on_c_find()):
# We only want to catch exceptions in the user code, not in ours.
if rsp_status is stopper:
exc_info = rsp_identifier
LOGGER.exception(
"Exception in user's on_c_find implementation.",
exc_info=exc_info
)
# Failure - Unable to Process - Error in on_c_find callback
rsp.Status = 0xC311
self.dimse.send_msg(rsp, context.context_id)
return
# Validate rsp_status and set rsp.Status accordingly
rsp = self.validate_status(rsp_status, rsp)
if rsp.Status in self.statuses:
status = self.statuses[rsp.Status]
else:
# Unknown status
self.dimse.send_msg(rsp, context.context_id)
return
if status[0] == STATUS_CANCEL:
# If cancel, then rsp_identifier is None
LOGGER.info('Received C-CANCEL-FIND RQ from peer')
LOGGER.info('Find SCP Response: (Cancel)')
self.dimse.send_msg(rsp, context.context_id)
return
elif status[0] == STATUS_FAILURE:
# If failed, then rsp_identifier is None
LOGGER.info('Find SCP Response: (Failure - %s)', status[1])
self.dimse.send_msg(rsp, context.context_id)
return
elif status[0] == STATUS_SUCCESS:
# User isn't supposed to send these, but handle anyway
# If success, then rsp_identifier is None
LOGGER.info('Find SCP Response: %s (Success)', ii + 1)
self.dimse.send_msg(rsp, context.context_id)
return
elif status[0] == STATUS_PENDING:
# If pending, the rsp_identifier is the Identifier dataset
bytestream = encode(rsp_identifier,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
bytestream = BytesIO(bytestream)
if bytestream.getvalue() == b'':
LOGGER.error("Failed to encode the received Identifier "
"dataset")
# Failure: Unable to Process - Can't decode dataset
# returned by on_c_find callback
rsp.Status = 0xC312
self.dimse.send_msg(rsp, context.context_id)
return
rsp.Identifier = bytestream
LOGGER.info('Find SCP Response: %s (Pending)', ii + 1)
LOGGER.debug('Find SCP Response Identifier:')
LOGGER.debug('')
LOGGER.debug('# DICOM Dataset')
for elem in rsp_identifier.iterall():
LOGGER.debug(elem)
LOGGER.debug('')
self.dimse.send_msg(rsp, context.context_id)
# Reset the response Identifier
rsp.Identifier = None
# Send final success response
rsp.Status = 0x0000
LOGGER.info('Find SCP Response: %s (Success)', ii + 2)
self.dimse.send_msg(rsp, context.context_id)
def _get_scp(self, req, context, info):
"""The SCP implementation for Query/Retrieve - Get.
Parameters
----------
req : dimse_primitives.C_GET
The C-GET request primitive sent by the peer.
context : presentation.PresentationContext
The presentation context that the SCP is operating under.
info : dict
A dict containing details about the association.
See Also
--------
ae.ApplicationEntity.on_c_get
association.Association.send_c_get
Notes
-----
**C-GET Request**
*Parameters*
| (M) Message ID
| (M) Affected SOP Class UID
| (M) Priority
| (M) Identifier
*Identifier*
The C-GET request Identifier shall contain:
* (0008,0052) Query/Retrieve Level.
* Unique Key Attributes, which may include:
- (0010,0020) Patient ID
- (0020,000D) Study Instance UIDs
- (0020,000E) Series Instance UIDs
- (0008,0018) SOP Instance UIDs
* (0008,0053) Query/Retrieve View, if Enhanced Multi-Frame Image
Conversion has been accepted during Extended Negotiation. It shall
not be present otherwise.
* (0008,0005) Specific Character Set, if (0010,0020) Patient ID is
using a character set other than the default character repertoire.
**C-GET Response**
*Parameters*
| (U) Message ID
| (M) Message ID Being Responded To
| (U) Affected SOP Class UID
| (U) Identifier
| (M) Status
| (C) Number of Remaining Sub-operations
| (C) Number of Completed Sub-operations
| (C) Number of Failed Sub-operations
| (C) Number of Warning Sub-operations
*Identifier*
If the C-GET response Status is 'Cancelled', 'Failure', 'Refused' or
'Warning' then the Identifier shall contain:
* (0008,0058) Failed SOP Instance UID List
If the C-GET response Status is 'Pending' then there is no Identifier.
*Status*
Success
| ``0x0000`` Sub-operations complete: no failures or warnings
Pending
| ``0xFF00`` Sub-operations are continuing
Cancel
| ``0xFE00`` Sub-operations terminated due to Cancel indication
Failure
| ``0x0122`` SOP class not supported
| ``0x0124`` Not authorised
| ``0x0210`` Duplicate invocation
| ``0x0211`` Unrecognised operation
| ``0x0212`` Mistyped argument
| ``0xA701`` Out of resources: unable to calculate number of matches
| ``0xA702`` Out of resources: unable to perform sub-operations
| ``0xA900`` Identifier does not match SOP class
| ``0xC000`` to ``0xCFFF`` Unable to process
Warning
| ``0xB000`` Sub-operations complete: one or more failures or
warnings
*Number of X Sub-operations*
Inclusion of the 'Number of X Sub-operations' parameters is conditional
on the value of the response Status. For a given Status category, the
table below states whether or not the response shall contain, shall not
contain or may contain the 'Number of X Sub-operations' parameter.
+-----------+------------------------------------------+
| | Number of "X" Sub-operations |
+-----------+-----------+-----------+--------+---------+
| Status | Remaining | Completed | Failed | Warning |
+===========+===========+===========+========+=========+
| Pending | shall | shall | shall | shall |
+-----------+-----------+-----------+--------+---------+
| Cancelled | may | may | may | may |
+-----------+-----------+-----------+--------+---------+
| Warning | shall not | may | may | may |
+-----------+-----------+-----------+--------+---------+
| Failure | shall not | may | may | may |
+-----------+-----------+-----------+--------+---------+
| Success | shall not | may | may | may |
+-----------+-----------+-----------+--------+---------+
References
----------
.. [1] DICOM Standard, Part 4, `Annex C <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_C>`_.
.. [2] DICOM Standard, Part 7, Sections
`9.1.3 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.1.3>`_,
`9.3.3 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.3.3>`_
and `Annex C <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_C>`_.
"""
# Build C-GET response primitive
rsp = C_GET()
rsp.MessageID = req.MessageID
rsp.MessageIDBeingRespondedTo = req.MessageID
rsp.AffectedSOPClassUID = req.AffectedSOPClassUID
# Attempt to decode the request's Identifier dataset
transfer_syntax = context.transfer_syntax[0]
try:
identifier = decode(req.Identifier,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
LOGGER.info('Get SCP Request Identifier:')
LOGGER.info('')
LOGGER.debug('# DICOM Data Set')
for elem in identifier.iterall():
LOGGER.info(elem)
LOGGER.info('')
except Exception as ex:
LOGGER.error("Failed to decode the request's Identifier dataset")
LOGGER.exception(ex)
# Failure: Cannot Understand - Dataset decoding error
rsp.Status = 0xC410
rsp.ErrorComment = 'Unable to decode the dataset'
self.dimse.send_msg(rsp, context.context_id)
return
info['parameters'] = {
'message_id' : req.MessageID,
'priority' : req.Priority
}
# Callback - C-GET
try:
# yields int, (status, dataset), ...
result = self.ae.on_c_get(identifier, context.as_tuple, info)
except Exception as ex:
LOGGER.error("Exception in user's on_c_get implementation.")
LOGGER.exception(ex)
rsp.Status = 0xC411
self.dimse.send_msg(rsp, context.context_id)
return
# Number of C-STORE sub-operations
try:
no_suboperations = int(next(result))
except Exception as ex:
LOGGER.error("'on_c_get' yielded an invalid number of "
"sub-operations value")
LOGGER.exception(ex)
rsp.Status = 0xC413
self.dimse.send_msg(rsp, context.context_id)
return
# Track the sub operation results
# [remaining, failed, warning, complete]
store_results = [no_suboperations, 0, 0, 0]
# Store the SOP Instance UIDs from any failed C-STORE sub-operations
failed_instances = []
def _add_failed_instance(ds):
if hasattr(ds, 'SOPInstanceUID'):
failed_instances.append(ds.SOPInstanceUID)
# Iterate through the results
# C-GET Pending responses are optional!
for ii, (rsp_status, dataset) in enumerate(result):
# All sub-operations are complete
if store_results[0] <= 0:
LOGGER.warning("'on_c_get' yielded further (status, dataset) "
"results but these will be ignored as the "
"sub-operations are complete")
break
# Validate rsp_status and set rsp.Status accordingly
rsp = self.validate_status(rsp_status, rsp)
if rsp.Status in self.statuses:
status = self.statuses[rsp.Status]
else:
# Unknown status
self.dimse.send_msg(rsp, context.context_id)
return
if status[0] == STATUS_CANCEL:
# If cancel, dataset is a Dataset with a
# 'FailedSOPInstanceUIDList' element
LOGGER.info('Get SCP Received C-CANCEL-GET RQ from peer')
rsp.NumberOfRemainingSuboperations = store_results[0]
rsp.NumberOfFailedSuboperations = store_results[1]
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
# In case user didn't include it
if (not isinstance(dataset, Dataset) or
'FailedSOPInstanceUIDList' not in dataset):
dataset = Dataset()
dataset.FailedSOPInstanceUIDList = failed_instances
bytestream = encode(dataset,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
rsp.Identifier = BytesIO(bytestream)
self.dimse.send_msg(rsp, context.context_id)
return
elif status[0] in [STATUS_FAILURE, STATUS_WARNING]:
# If failure or warning, dataset is a Dataset with a
# 'FailedSOPInstanceUIDList' element
LOGGER.info('Get SCP Result (%s - %s)', status[0], status[1])
rsp.NumberOfRemainingSuboperations = None
rsp.NumberOfFailedSuboperations = (
store_results[1] + store_results[0]
)
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
# In case user didn't include it
if (not isinstance(dataset, Dataset) or
'FailedSOPInstanceUIDList' not in dataset):
dataset = Dataset()
dataset.FailedSOPInstanceUIDList = failed_instances
bytestream = encode(dataset,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
rsp.Identifier = BytesIO(bytestream)
self.dimse.send_msg(rsp, context.context_id)
return
elif status[0] == STATUS_SUCCESS:
# If user yields Success, check it
# dataset is None
if store_results[1] or store_results[2]:
LOGGER.info('Get SCP Response: (Warning)')
rsp.Status = 0xB000
ds = Dataset()
ds.FailedSOPInstanceUIDList = failed_instances
bytestream = encode(ds,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
rsp.Identifier = BytesIO(bytestream)
else:
LOGGER.info('Get SCP Response: (Success)')
rsp.Identifier = None
rsp.NumberOfRemainingSuboperations = None
rsp.NumberOfFailedSuboperations = store_results[1]
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
self.dimse.send_msg(rsp, context.context_id)
return
elif status[0] == STATUS_PENDING and dataset:
# If pending, dataset is the Dataset to send
if not isinstance(dataset, Dataset):
LOGGER.error('Received invalid dataset from callback')
# Count as a sub-operation failure
store_results[1] += 1
failed_instances.append('')
rsp.Identifier = None
rsp.NumberOfRemainingSuboperations = store_results[0]
rsp.NumberOfFailedSuboperations = store_results[1]
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
self.dimse.send_msg(rsp, context.context_id)
continue
LOGGER.info('Get SCP Response: %s (Pending)', ii + 1)
# If the Composite Instance Retrieve Without Bulk Data Service
# is being used then we must remove the bulk data elements
# (if present)
if context.abstract_syntax == '1.2.840.10008.5.1.4.1.2.5.3':
# Doesn't include WaveformData
_bulk_data = [
kw for kw in self.BULK_DATA_KEYWORDS if kw in dataset
]
for keyword in _bulk_data:
delattr(dataset, keyword)
# Needs to be handled separately
if 'WaveformSequence' in dataset:
for item in dataset.WaveformSequence:
if 'WaveformData' in item:
del item.WaveformData
if 'WaveformData' not in _bulk_data:
_bulk_data.append('WaveformData')
if _bulk_data:
LOGGER.warning(
"The Query/Retrieve - Composite Instance Retrieve "
"Without Bulk Data service is requested but a "
"yielded dataset contains the following (to be "
"removed) bulk data elements: {}"
.format(','.join(_bulk_data))
)
# Send `dataset` via C-STORE sub-operations over the existing
# association and check that the response's Status exists and
# is a known value
try:
store_status = self.assoc.send_c_store(dataset)
store_status = (
STORAGE_SERVICE_CLASS_STATUS[store_status.Status]
)
except Exception as ex:
# An exception implies a C-STORE failure
LOGGER.warning("C-STORE sub-operation failed.")
store_status = [STATUS_FAILURE, 'Unknown']
LOGGER.info('Get SCP: Received Store SCU response (%s)',
store_status[0])
# Update the C-STORE sub-operation result tracker
if store_status[0] == STATUS_FAILURE:
store_results[1] += 1
_add_failed_instance(dataset)
elif store_status[0] == STATUS_WARNING:
store_results[2] += 1
_add_failed_instance(dataset)
elif store_status[0] == STATUS_SUCCESS:
store_results[3] += 1
store_results[0] -= 1
rsp.Identifier = None
rsp.NumberOfRemainingSuboperations = store_results[0]
rsp.NumberOfFailedSuboperations = store_results[1]
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
self.dimse.send_msg(rsp, context.context_id)
# If not already done, send the final 'Success' or 'Warning' response
if not store_results[1] and not store_results[2]:
# Success response - no failures or warnings
LOGGER.info('Get SCP Result: (Success)')
rsp.Status = 0x0000
else:
# Warning response - one or more failures or warnings
LOGGER.info('Get SCP Result: (Warning)')
rsp.Status = 0xB000
# If Warning response, need to return an Identifier with
# (0008,0058) Failed SOP Instance UID List element
ds = Dataset()
ds.FailedSOPInstanceUIDList = failed_instances
bytestream = encode(ds,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
rsp.Identifier = BytesIO(bytestream)
rsp.NumberOfRemainingSuboperations = None
rsp.NumberOfFailedSuboperations = store_results[1]
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
self.dimse.send_msg(rsp, context.context_id)
def _move_scp(self, req, context, info):
"""The SCP implementation for Query/Retrieve - Move.
Parameters
----------
req : dimse_primitives.C_MOVE
The C-MOVE request primitive sent by the peer.
context : presentation.PresentationContext
The presentation context that the SCP is operating under.
info : dict
A dict containing details about the association.
See Also
--------
ae.ApplicationEntity.on_c_move
association.Association.send_c_move
Notes
-----
**C-MOVE Request**
*Parameters*
| (M) Message ID
| (M) Affected SOP Class UID
| (M) Priority
| (M) Move Destination
| (M) Identifier
*Identifier*
The C-MOVE request Identifier shall contain:
* (0008,0052) Query/Retrieve Level.
* Unique Key Attributes, which may include:
- (0010,0020) Patient ID
- (0020,000D) Study Instance UIDs
- (0020,000E) Series Instance UIDs
- (0008,0018) SOP Instance UIDs
* (0008,0053) Query/Retrieve View, if Enhanced Multi-Frame Image
Conversion has been accepted during Extended Negotiation. It shall
not be present otherwise.
* (0008,0005) Specific Character Set, if (0010,0020) Patient ID is
using a character set other than the default character repertoire.
**C-MOVE Response**
*Parameters*
| (U) Message ID
| (M) Message ID Being Responded To
| (U) Affected SOP Class UID
| (U) Identifier
| (M) Status
| (C) Number of Remaining Sub-operations
| (C) Number of Completed Sub-operations
| (C) Number of Failed Sub-operations
| (C) Number of Warning Sub-operations
*Identifier*
If the C-MOVE response Status is 'Cancelled', 'Failure', 'Refused' or
'Warning' then the Identifier shall contain:
* (0008,0058) Failed SOP Instance UID List
If the C-MOVE response Status is 'Pending' then there is no Identifier.
*Status*
Success
| ``0x0000`` Sub-operations complete: no failures
Pending
| ``0xFF00`` Sub-operations are continuing
Cancel
| ``0xFE00`` Sub-operations terminated due to Cancel indication
Failure
| ``0x0122`` SOP class not supported
| ``0x0124`` Not authorised
| ``0x0210`` Duplicate invocation
| ``0x0211`` Unrecognised operation
| ``0x0212`` Mistyped argument
| ``0xA701`` Out of resources: unable to calculate number of matches
| ``0xA702`` Out of resources, unable to perform sub-operations
| ``0xA801`` Move destination unknown
| ``0xA900`` Identifier does not match SOP class
| ``0xC000`` to ``0xCFFF`` Unable to process
Warning
| ``0xB000`` Sub-operations complete: one or more failures
*Number of X Sub-operations*
Inclusion of the 'Number of X Sub-operations' parameters is conditional
on the value of the response Status. For a given Status category, the
table below states whether or not the response shall contain, shall not
contain or may contain the 'Number of X Sub-operations' parameter.
+-----------+------------------------------------------+
| | Number of "X" Sub-operations |
+-----------+-----------+-----------+--------+---------+
| Status | Remaining | Completed | Failed | Warning |
+===========+===========+===========+========+=========+
| Pending | shall | shall | shall | shall |
+-----------+-----------+-----------+--------+---------+
| Cancelled | may | may | may | may |
+-----------+-----------+-----------+--------+---------+
| Warning | shall not | may | may | may |
+-----------+-----------+-----------+--------+---------+
| Failure | shall not | may | may | may |
+-----------+-----------+-----------+--------+---------+
| Success | shall not | may | may | may |
+-----------+-----------+-----------+--------+---------+
References
----------
.. [1] DICOM Standard, Part 4, `Annex C <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_C>`_.
.. [2] DICOM Standard, Part 7, Sections
`9.1.4 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.1.4>`_,
`9.3.4 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.3.4>`_
and `Annex C <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_C>`_.
"""
# Build C-MOVE response primitive
rsp = C_MOVE()
rsp.MessageID = req.MessageID
rsp.MessageIDBeingRespondedTo = req.MessageID
rsp.AffectedSOPClassUID = req.AffectedSOPClassUID
# Attempt to decode the request's Identifier dataset
transfer_syntax = context.transfer_syntax[0]
try:
identifier = decode(req.Identifier,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
LOGGER.info('Move SCP Request Identifier:')
LOGGER.info('')
LOGGER.debug('# DICOM Data Set')
for elem in identifier.iterall():
LOGGER.info(elem)
LOGGER.info('')
except Exception as ex:
LOGGER.error("Failed to decode the request's Identifier dataset")
LOGGER.exception(ex)
# Failure: Cannot Understand - Dataset decoding error
rsp.Status = 0xC510
rsp.ErrorComment = 'Unable to decode the dataset'
self.dimse.send_msg(rsp, context.context_id)
return
info['parameters'] = {
'message_id' : req.MessageID,
'priority' : req.Priority
}
# Callback - C-MOVE
try:
# yields (addr, port), int, (status, dataset), ...
result = self.ae.on_c_move(identifier,
req.MoveDestination,
context.as_tuple,
info)
except Exception as ex:
LOGGER.error("Exception in user's on_c_move implementation.")
LOGGER.exception(ex)
# Failure - Unable to process - Error in on_c_move callback
rsp.Status = 0xC511
self.dimse.send_msg(rsp, context.context_id)
return
try:
destination = next(result)
no_suboperations = next(result)
except StopIteration:
LOGGER.exception("The on_c_move callback must yield the (address, "
"port) of the destination AE, then yield the "
"number of sub-operations, then yield (status "
"dataset) pairs.")
# Failure - Unable to process - Error in on_c_move yield
rsp.Status = 0xC514
self.dimse.send_msg(rsp, context.context_id)
return
# Check number of C-STORE sub-operations
try:
no_suboperations = int(no_suboperations)
except Exception as ex:
LOGGER.error("'on_c_move' yielded an invalid number of "
"sub-operations value")
LOGGER.exception(ex)
rsp.Status = 0xC513
self.dimse.send_msg(rsp, context.context_id)
return
# Request new association with Move Destination
try:
# Unknown Move Destination
if None in destination:
LOGGER.error('Unknown Move Destination: %s',
req.MoveDestination.decode('ascii'))
# Failure - Move destination unknown
rsp.Status = 0xA801
self.dimse.send_msg(rsp, context.context_id)
return
store_assoc = self.ae.associate(destination[0],
destination[1],
ae_title=req.MoveDestination)
except Exception as ex:
LOGGER.error(
"'on_c_move' yielded an invalid destination AE (addr, "
"port) value"
)
LOGGER.exception(ex)
# Failure - Unable to process - Bad on_c_move destination
rsp.Status = 0xC515
self.dimse.send_msg(rsp, context.context_id)
return
# Track the sub operation results
# [remaining, failed, warning, complete]
store_results = [no_suboperations, 0, 0, 0]
# Store the SOP Instance UIDs from any failed C-STORE sub-operations
failed_instances = []
def _add_failed_instance(ds):
if hasattr(ds, 'SOPInstanceUID'):
failed_instances.append(ds.SOPInstanceUID)
if store_assoc.is_established:
# Iterate through the remaining callback (status, dataset) yields
for ii, (rsp_status, dataset) in enumerate(result):
# All sub-operations are complete
if store_results[0] <= 0:
LOGGER.warning("'on_c_move' yielded further (status, "
"dataset) results but these will be "
"ignored as the sub-operations are "
"complete")
break
# Validate rsp_status and set rsp.Status accordingly
rsp = self.validate_status(rsp_status, rsp)
if rsp.Status in self.statuses:
status = self.statuses[rsp.Status]
else:
# Unknown status
store_assoc.release()
self.dimse.send_msg(rsp, context.context_id)
return
# If usr_status is Cancel, Failure, Warning or Success then
# generate a final response, if Pending then do C-STORE
# sub-operation
if status[0] == STATUS_CANCEL:
# If cancel, then dataset is a Dataset with a
# 'FailedSOPInstanceUIDList' element
LOGGER.info(
'Move SCP Received C-CANCEL-MOVE RQ from peer'
)
store_assoc.release()
# In case user didn't include it
if (not isinstance(dataset, Dataset) or
'FailedSOPInstanceUIDList' not in dataset):
dataset = Dataset()
dataset.FailedSOPInstanceUIDList = failed_instances
bytestream = encode(dataset,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
rsp.Identifier = BytesIO(bytestream)
rsp.NumberOfRemainingSuboperations = store_results[0]
rsp.NumberOfFailedSuboperations = store_results[1]
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
self.dimse.send_msg(rsp, context.context_id)
return
elif status[0] in [STATUS_FAILURE, STATUS_WARNING]:
# If failed or warning, then dataset is a Dataset with a
# 'FailedSOPInstanceUIDList' element
LOGGER.info(
'Move SCP Result (%s - %s)', status[0], status[1]
)
store_assoc.release()
# In case user didn't include it
if (not isinstance(dataset, Dataset) or
'FailedSOPInstanceUIDList' not in dataset):
dataset = Dataset()
dataset.FailedSOPInstanceUIDList = failed_instances
bytestream = encode(dataset,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
rsp.Identifier = BytesIO(bytestream)
rsp.NumberOfRemainingSuboperations = None
rsp.NumberOfFailedSuboperations = (
store_results[1] + store_results[0]
)
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
self.dimse.send_msg(rsp, context.context_id)
return
elif status[0] == STATUS_SUCCESS:
# If success, then dataset is None
store_assoc.release()
# If the user yields Success, check it
if store_results[1] or store_results[2]:
LOGGER.info('Move SCP Response: (Warning)')
ds = Dataset()
ds.FailedSOPInstanceUIDList = failed_instances
bytestream = encode(ds,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
rsp.Identifier = BytesIO(bytestream)
rsp.Status = 0xB000
else:
LOGGER.info('Move SCP Response: (Warning)')
rsp.Identifier = None
rsp.NumberOfRemainingSuboperations = None
rsp.NumberOfFailedSuboperations = store_results[1]
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
self.dimse.send_msg(rsp, context.context_id)
return
elif status[0] == STATUS_PENDING and dataset:
# If pending, then dataset is the Dataset to send
if not isinstance(dataset, Dataset):
LOGGER.error('Received invalid dataset from callback')
# Count as a sub-operation failure
store_results[1] += 1
failed_instances.append('')
rsp.Identifier = None
rsp.NumberOfRemainingSuboperations = store_results[0]
rsp.NumberOfFailedSuboperations = store_results[1]
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
self.dimse.send_msg(rsp, context.context_id)
continue
LOGGER.info('Move SCP Response %s (Pending)', ii)
# Send `dataset` via C-STORE sub-operations over the
# association and check that the response's Status exists
# and is a known value
try:
store_status = store_assoc.send_c_store(
dataset,
originator_aet=self.ae.ae_title,
originator_id=1
)
# FIXME: Should probably split status check?
store_status = STORAGE_SERVICE_CLASS_STATUS[
store_status.Status
]
except Exception as ex:
# An exception implies a C-STORE failure
LOGGER.warning("C-STORE sub-operation failed.")
store_status = [STATUS_FAILURE, 'Unknown']
LOGGER.info('Move SCP: Received Store SCU response (%s)',
store_status[0])
# Update the C-STORE sub-operation result tracker
if store_status[0] == STATUS_FAILURE:
store_results[1] += 1
_add_failed_instance(dataset)
elif store_status[0] == STATUS_WARNING:
store_results[2] += 1
_add_failed_instance(dataset)
elif store_status[0] == STATUS_SUCCESS:
store_results[3] += 1
store_results[0] -= 1
rsp.Identifier = None
rsp.NumberOfRemainingSuboperations = store_results[0]
rsp.NumberOfFailedSuboperations = store_results[1]
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
self.dimse.send_msg(rsp, context.context_id)
store_assoc.release()
else:
# Failed to associate with Move Destination AE
LOGGER.error('Move SCP: Unable to associate with destination AE')
rsp.Status = 0xA801
self.dimse.send_msg(rsp, context.context_id)
# FIXME - shouldn't have to manually close the socket like this
store_assoc.dul.scu_socket.close()
return
# If not already done, send the final 'Success' or 'Warning' response
if not store_results[1] and not store_results[2]:
# Success response - no failures or warnings
LOGGER.info('Move SCP Result: (Success)')
rsp.Status = 0x0000
else:
# Warning response - one or more failures or warnings
LOGGER.info('Move SCP Result: (Warning)')
rsp.Status = 0xB000
# If Warning response, need to return an Identifier with
# (0008, 0058) Failed SOP Instance UID List element
ds = Dataset()
ds.FailedSOPInstanceUIDList = failed_instances
bytestream = encode(ds,
transfer_syntax.is_implicit_VR,
transfer_syntax.is_little_endian)
rsp.Identifier = BytesIO(bytestream)
rsp.NumberOfRemainingSuboperations = None
rsp.NumberOfFailedSuboperations = store_results[1]
rsp.NumberOfWarningSuboperations = store_results[2]
rsp.NumberOfCompletedSuboperations = store_results[3]
self.dimse.send_msg(rsp, context.context_id)
[docs]class BasicWorklistManagementServiceClass(QueryRetrieveServiceClass):
"""Implementation of the Basic Worklist Management Service Class."""
statuses = QR_FIND_SERVICE_CLASS_STATUS
[docs] def __init__(self, assoc):
super(BasicWorklistManagementServiceClass, self).__init__(assoc)
def SCP(self, req, context, info):
"""The SCP implementation for Basic Worklist Management.
Parameters
----------
req : dimse_primitives.C_FIND
The C-FIND request primitive received from the peer.
context : presentation.PresentationContext
The presentation context that the SCP is operating under.
info : dict
A dict containing details about the association.
See Also
--------
ae.ApplicationEntity.on_c_find
association.Association.send_c_find
Notes
-----
**C-FIND Request**
*Parameters*
| (M) Message ID
| (M) Affected SOP Class UID
| (M) Priority
| (M) Identifier
*Identifier*
The C-FIND request Identifier shall contain:
* Key Attributes values to be matched against the values of attributes
specified in that SOP Class.
* (0008,0005) Specific Character Set, if expanded or replacement
character sets may be used in any of the Attributes in the request
Identifier. It shall not be present otherwise.
* (0008,0201) Timezone Offset From UTC, if any Attributes of time in
the request Identifier are to be interpreted explicitly in the
designated local time zone. It shall not be present otherwise.
**C-FIND Response**
*Parameters*
| (U) Message ID
| (M) Message ID Being Responded To
| (U) Affected SOP Class UID
| (C) Identifier
| (M) Status
*Identifier*
The C-FIND response shall only include an Identifier when the Status is
'Pending'. When sent, the Identifier shall contain:
* Key Attributes with values corresponding to Key Attributes contained
in the Identifier of the requeset.
* (0008,0005) Specific Character Set, if expanded or replacement
character sets may be used in any of the Attributes in the response
Identifier. It shall not be present otherwise.
* (0008,0201) Timezone Offset From UTC, if any Attributes of time in
the response Identifier are to be interpreted explicitly in the
designated local time zone. It shall not be present otherwise.
*Status*
Success
| ``0x0000`` Success
Pending
| ``0xFF00`` Matches are continuing, current match supplied
| ``0xFF01`` Matches are continuing, warning
Cancel
| ``0xFE00`` Cancel
Failure
| ``0x0122`` SOP class not supported
| ``0xA700`` Out of resources
| ``0xA900`` Dataset does not match SOP class
| ``0xC000`` to ``0xCFFF`` Unable to process
References
----------
* DICOM Standard, Part 4, `Annex K <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_K>`_.
* [2] DICOM Standard, Part 7, Sections
`9.1.2 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.1.2>`_,
`9.3.2 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.3.2>`_
and `Annex C <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_C>`_.
"""
if context.abstract_syntax == '1.2.840.10008.5.1.4.31':
self._find_scp(req, context, info)
else:
raise ValueError(
'The supplied abstract syntax is not valid for use with the '
'Basic Worklist Management Service Class'
)
class SubstanceAdministrationQueryServiceClass(QueryRetrieveServiceClass):
"""Implementation of the Substance Administration Query Service"""
statuses = SUBSTANCE_ADMINISTRATION_SERVICE_CLASS_STATUS
def __init__(self, assoc):
super(SubstanceAdministrationQueryServiceClass, self).__init__(assoc)
def SCP(self, req, context, info):
"""The SCP implementation for the Relevant Patient Information Query
Service Class.
Parameters
----------
req : dimse_primitives.C_FIND
The C-FIND request primitive sent by the peer.
context : presentation.PresentationContext
The presentation context that the SCP is operating under.
info : dict
A dict containing details about the association.
See Also
--------
ae.ApplicationEntity.on_c_find
association.Association.send_c_find
Notes
-----
**C-FIND Request**
*Parameters*
| (M) Message ID
| (M) Affected SOP Class UID
| (M) Priority
| (M) Identifier
*Identifier*
The C-FIND request Identifier shall contain:
* Key Attributes with values corresponding to Key Attributes contained
in the Identifier of the request.
* (0008,0005) Specific Character Set, if expanded or replacement
character sets may be used in any of the Attributes in the request
Identifier. It shall not be present otherwise.
**C-FIND Response**
*Parameters*
| (U) Message ID
| (M) Message ID Being Responded To
| (U) Affected SOP Class UID
| (C) Identifier
| (M) Status
*Status*
Success
| ``0x0000`` Success
Pending
| ``0xFF00`` Matches are continuing, current match supplied
| ``0xFF01`` Matches are continuing, warning
Cancel
| ``0xFE00`` Matching terminated due to cancel request
Failure
| ``0x0122`` SOP class not supported
| ``0xA700`` Out of resources
| ``0xA900`` Identifier does not match SOP class
| ``0xC000`` to ``0xCFFF`` Unable to process
References
----------
* DICOM Standard, Part 4, `Annex V <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_V>`_.
* DICOM Standard, Part 7, Sections
`9.1.2 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.1.2>`_,
`9.3.2 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.3.2>`_
and `Annex C <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_C>`_.
"""
self._find_scp(req, context, info)
class NonPatientObjectStorageServiceClass(StorageServiceClass):
"""Implementation of the Non-Patient Object Storage Service"""
statuses = NON_PATIENT_SERVICE_CLASS_STATUS
class HangingProtocolQueryRetrieveServiceClass(QueryRetrieveServiceClass):
"""Implementation of the Hanging Protocol QR Service."""
pass
class DefinedProcedureProtocolQueryRetrieveServiceClass(QueryRetrieveServiceClass):
"""Implementation of the Defined Procedure Protocol QR Service."""
pass
class ColorPaletteQueryRetrieveServiceClass(QueryRetrieveServiceClass):
"""Implementation of the Color Palette QR Service."""
pass
class ImplantTemplateQueryRetrieveServiceClass(QueryRetrieveServiceClass):
"""Implementation of the Implant Template QR Service."""
pass