Skip to content

Commit

Permalink
Add support for Storage Management (#889)
Browse files Browse the repository at this point in the history
  • Loading branch information
scaramallion authored Nov 18, 2023
1 parent d38eb18 commit 3251a34
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 43 deletions.
2 changes: 2 additions & 0 deletions docs/changelog/v2.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Enhancements
:class:`~pynetdicom.service_class.QueryRetrieveServiceClass` (:issue:`878`)
* Added support for :class:`Inventory Query/Retrieve Service Class
<pynetdicom.service_class.InventoryQueryRetrieveServiceClass>` (:issue:`879`)
* Added support for :class:`Storage Management Service Class
<pynetdicom.service_class.StorageManagementServiceClass>` (:issue:`880`)
* Added :meth:`~pynetdicom.events.Event.encoded_dataset` to simplify accessing
the encoded dataset without first decoding it

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Supported Service Classes
* Structured Reporting
* Volumetric Presentation State
* :doc:`Storage Commitment <service_classes/storage_commitment>`
* :doc:`Storage Management <service_classes/storage_management>`
* :doc:`Substance Administration Query <service_classes/substance_admin_service_class>`
* :doc:`Unified Procedure Step <service_classes/ups>`
* :doc:`Verification <service_classes/verification_service_class>`
Expand Down
1 change: 1 addition & 0 deletions docs/service_classes/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Supported Service Classes
rt_machine
storage_service_class
storage_commitment
storage_management
substance_admin_service_class
ups
verification_service_class
106 changes: 106 additions & 0 deletions docs/service_classes/storage_management.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
.. _service_storemanage:

Storage Management Service Class
================================
The :dcm:`Storage Management Service Class<part04/chapter_KK.html>`
defines a service that uses the DIMSE N-ACTION and N-EVENT-REPORT services to
facilitate peer-to-peer controls for the management of persistent storage of
Composite SOP Instances.

.. _storeman_sops:

Supported SOP Classes
---------------------

+-------------------------------+---------------------------------------------+
| UID | SOP Class |
+===============================+=============================================+
| 1.2.840.10008.5.1.4.1.1.201.5 | InventoryCreation |
+-------------------------------+---------------------------------------------+


DIMSE Services
--------------

+-----------------+-----------------------------------------+
| DIMSE Service | Usage SCU/SCP |
+=================+=========================================+
| N-EVENT-REPORT | Mandatory/Mandatory |
+-----------------+-----------------------------------------+
| N-ACTION | Mandatory/Mandatory |
+-----------------+-----------------------------------------+


.. _storeman_statuses:

Statuses
--------

N-ACTION Statuses
~~~~~~~~~~~~~~~~~

+------------------+----------+-----------------------------------------------+
| Code (hex) | Category | Description |
+==================+==========+===============================================+
| 0x0000 | Success | Success |
+------------------+----------+-----------------------------------------------+
| 0x0112 | Failure | No such SOP Instance |
+------------------+----------+-----------------------------------------------+
| 0x0114 | Failure | No such argument |
+------------------+----------+-----------------------------------------------+
| 0x0115 | Failure | Invalid argument value |
+------------------+----------+-----------------------------------------------+
| 0x0117 | Failure | Invalid object instance |
+------------------+----------+-----------------------------------------------+
| 0x0118 | Failure | No such SOP Class |
+------------------+----------+-----------------------------------------------+
| 0x0119 | Failure | Class-Instance conflict |
+------------------+----------+-----------------------------------------------+
| 0x0123 | Failure | No such action |
+------------------+----------+-----------------------------------------------+
| 0x0124 | Failure | Refused: not authorised |
+------------------+----------+-----------------------------------------------+
| 0x0210 | Failure | Duplicate invocation |
+------------------+----------+-----------------------------------------------+
| 0x0211 | Failure | Unrecognised operation |
+------------------+----------+-----------------------------------------------+
| 0x0212 | Failure | Mistyped argument |
+------------------+----------+-----------------------------------------------+
| 0x0213 | Failure | Resource limitation |
+------------------+----------+-----------------------------------------------+
| 0xB010 | Warning | Attribute list error - One or more of Key |
| | | Attributes are not supported for matching |
+------------------+----------+-----------------------------------------------+

N-EVENT-REPORT Statuses
~~~~~~~~~~~~~~~~~~~~~~~

+------------------+----------+----------------------------------+
| Code (hex) | Category | Description |
+==================+==========+==================================+
| 0x0000 | Success | Success |
+------------------+----------+----------------------------------+
| 0x0110 | Failure | Processing failure |
+------------------+----------+----------------------------------+
| 0x0112 | Failure | No such SOP Instance |
+------------------+----------+----------------------------------+
| 0x0113 | Failure | No such event type |
+------------------+----------+----------------------------------+
| 0x0114 | Failure | No such argument |
+------------------+----------+----------------------------------+
| 0x0115 | Failure | Invalid argument value |
+------------------+----------+----------------------------------+
| 0x0117 | Failure | Invalid object Instance |
+------------------+----------+----------------------------------+
| 0x0118 | Failure | No such SOP Class |
+------------------+----------+----------------------------------+
| 0x0119 | Failure | Class-Instance conflict |
+------------------+----------+----------------------------------+
| 0x0210 | Failure | Duplicate invocation |
+------------------+----------+----------------------------------+
| 0x0211 | Failure | Unrecognised operation |
+------------------+----------+----------------------------------+
| 0x0212 | Failure | Mistyped argument |
+------------------+----------+----------------------------------+
| 0x0213 | Failure | Resource limitation |
+------------------+----------+----------------------------------+
11 changes: 11 additions & 0 deletions pynetdicom/association.py
Original file line number Diff line number Diff line change
Expand Up @@ -2269,6 +2269,13 @@ def send_n_action(
| ``0x0212`` - Mistyped argument
| ``0x0213`` - Resource limitation
*Storage Management Service* specific (DICOM
Standard Part 4, Annex KK.2.2.3):
Warning
| ``0xB010`` - Attribute list error - One or more of Key
Attributes are not supported for matching
action_reply : pydicom.dataset.Dataset or None
If the status category is 'Success' or 'Warning' then a
:class:`~pydicom.dataset.Dataset` containing attributes
Expand All @@ -2288,6 +2295,7 @@ def send_n_action(
:class:`~pynetdicom.service_class_n.PrintManagementServiceClass`
:class:`~pynetdicom.service_class_n.RTMachineVerificationServiceClass`
:class:`~pynetdicom.service_class_n.StorageCommitmentServiceClass`
:class:`~pynetdicom.service_class_n.StorageManagementServiceClass`
:class:`~pynetdicom.service_class_n.UnifiedProcedureStepServiceClass`
References
Expand All @@ -2299,6 +2307,7 @@ def send_n_action(
* DICOM Standard, Part 4, :dcm:`Annex S<part04/chapter_S.html>`
* DICOM Standard, Part 4, :dcm:`Annex CC<part04/chapter_CC.html>`
* DICOM Standard, Part 4, :dcm:`Annex DD<part04/chapter_DD.html>`
* DICOM Standard, Part 4, :dcm:`Annex KK<part04/chapter_KK.html>`
* DICOM Standard, Part 7, Sections
:dcm:`10.1.4<part07/chapter_10.html#sect_10.1.4>`,
:dcm:`10.3.4<part07/sect_10.3.4.html>` and
Expand Down Expand Up @@ -2864,6 +2873,7 @@ def send_n_event_report(
:class:`~pynetdicom.service_class_n.ProcedureStepServiceClass`
:class:`~pynetdicom.service_class_n.RTMachineVerificationServiceClass`
:class:`~pynetdicom.service_class_n.StorageCommitmentServiceClass`
:class:`~pynetdicom.service_class_n.StorageManagementServiceClass`
:class:`~pynetdicom.service_class_n.UnifiedProcedureStepServiceClass`
References
Expand All @@ -2874,6 +2884,7 @@ def send_n_event_report(
* DICOM Standard, Part 4, :dcm:`Annex J <part04/chapter_J.html>`
* DICOM Standard, Part 4, :dcm:`Annex CC <part04/chapter_CC.html>`
* DICOM Standard, Part 4, :dcm:`Annex DD <part04/chapter_DD.html>`
* DICOM Standard, Part 4, :dcm:`Annex KK <part04/chapter_KK.html>`
* DICOM Standard, Part 7, Sections
:dcm:`10.1.1 <part07/chapter_10.html#sect_10.1.1>`,
:dcm:`10.3.1 <part07/sect_10.3.html#sect_10.3.1>`
Expand Down
2 changes: 1 addition & 1 deletion pynetdicom/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ def encoded_dataset(self, include_meta: bool = True) -> bytes:
Retrieve the encoded dataset as sent by the peer::
def handle_store(event: pynetdicom.events.Event) -> int:
stream: bytes = event.encoded_dataset(inclue_meta=False)
stream: bytes = event.encoded_dataset(include_meta=False)
return 0x0000
Expand Down
39 changes: 19 additions & 20 deletions pynetdicom/service_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,12 @@
from types import TracebackType
from typing import (
TYPE_CHECKING,
Optional,
Type,
cast,
Union,
Tuple,
Any,
TypeVar,
Iterator,
Sequence,
Dict,
)

from pydicom.dataset import Dataset
Expand Down Expand Up @@ -67,17 +63,17 @@
from pynetdicom.presentation import PresentationContext
from pynetdicom.transport import AssociationSocket

_QR = Union[C_FIND, C_MOVE, C_GET]
_QR = C_FIND | C_MOVE | C_GET


StatusType = Union[int, Dataset]
DatasetType = Optional[Dataset]
UserReturnType = Tuple[StatusType, DatasetType]
StatusType = int | Dataset
DatasetType = Dataset | None
UserReturnType = tuple[StatusType, DatasetType]
_T = TypeVar("_T", bound=DIMSEPrimitive)
_ExcInfoType = Union[
Tuple[None, None, None], Tuple[Type[BaseException], BaseException, TracebackType]
]
DestinationType = Union[Tuple[str, int], Tuple[str, int, Dict[str, Any]]]
_ExcInfoType = (
tuple[None, None, None] | tuple[Type[BaseException], BaseException, TracebackType]
)
DestinationType = tuple[str, int] | tuple[str, int, dict[str, Any]]


LOGGER = logging.getLogger("pynetdicom.service-c")
Expand Down Expand Up @@ -108,10 +104,10 @@ def __enter__(self) -> "attempt":

def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> Optional[bool]:
exc_type: Type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> bool | None:
if exc_type is None:
# No exceptions raised
return None
Expand Down Expand Up @@ -285,7 +281,7 @@ def _c_find_scp(self, req: C_FIND, context: "PresentationContext") -> None:
for ii, (result, exc) in enumerate(self._wrap_handler(generator)):
# Reset the response Identifier
rsp.Identifier = None
dataset: Optional[Dataset]
dataset: Dataset | None
rsp_status: StatusType

# Exception raised by user's generator
Expand Down Expand Up @@ -1319,7 +1315,7 @@ def SCP(self, req: Any, context: "PresentationContext") -> None:
)
raise NotImplementedError(msg)

def validate_status(self, status: Union[int, Dataset], rsp: _T) -> _T:
def validate_status(self, status: int | Dataset, rsp: _T) -> _T:
"""Validate `status` and set `rsp.Status` accordingly.
Parameters
Expand Down Expand Up @@ -1375,7 +1371,7 @@ def validate_status(self, status: Union[int, Dataset], rsp: _T) -> _T:

def _wrap_handler(
self, handler: Iterator
) -> Iterator[Union[Tuple[None, _ExcInfoType], Tuple[UserReturnType, None]]]:
) -> Iterator[tuple[None, _ExcInfoType] | tuple[UserReturnType, None]]:
"""Wrap a generator handler to catch exceptions.
Parameters
Expand Down Expand Up @@ -2460,7 +2456,10 @@ class ImplantTemplateQueryRetrieveServiceClass(QueryRetrieveServiceClass):


class InventoryQueryRetrieveServiceClass(QueryRetrieveServiceClass):
"""Implementation of the Inventory QR Service."""
"""Implementation of the Inventory QR Service.
.. versionadded:: 2.1
"""

pass

Expand Down
43 changes: 37 additions & 6 deletions pynetdicom/service_class_n.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Implements the supported Service Classes that make use of DIMSE-N."""

import logging
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING

from pynetdicom.dimse_primitives import (
N_ACTION,
Expand All @@ -20,18 +20,20 @@
PRINT_JOB_MANAGEMENT_SERVICE_CLASS_STATUS,
PROCEDURE_STEP_STATUS,
STORAGE_COMMITMENT_SERVICE_CLASS_STATUS,
STORAGE_MANAGEMENT_SERVICE_CLASS_STATUS,
RT_MACHINE_VERIFICATION_SERVICE_CLASS_STATUS,
UNIFIED_PROCEDURE_STEP_SERVICE_CLASS_STATUS,
)

if TYPE_CHECKING: # pragma: no cover
from pynetdicom.presentation import PresentationContext

_MCM = Union[N_CREATE, N_GET, N_ACTION]
_PJ = Union[N_CREATE, N_EVENT_REPORT, N_GET, N_SET, N_ACTION, N_DELETE]
_PS = Union[N_CREATE, N_EVENT_REPORT, N_GET, N_SET]
_SCS = Union[N_EVENT_REPORT, N_ACTION]
_UPS = Union[N_CREATE, N_EVENT_REPORT, N_GET, N_SET, N_ACTION, C_FIND]
_MCM = N_CREATE | N_GET | N_ACTION
_PJ = N_CREATE | N_EVENT_REPORT | N_GET | N_SET | N_ACTION | N_DELETE
_PS = N_CREATE | N_EVENT_REPORT | N_GET | N_SET
_SCS = N_EVENT_REPORT | N_ACTION
_SMS = N_ACTION | N_EVENT_REPORT
_UPS = N_CREATE | N_EVENT_REPORT | N_GET | N_SET | N_ACTION | C_FIND


LOGGER = logging.getLogger("pynetdicom.service-n")
Expand Down Expand Up @@ -289,6 +291,35 @@ def SCP(self, req: "_SCS", context: "PresentationContext") -> None:
)


class StorageManagementServiceClass(ServiceClass):
"""Implementation of the Storage Management Service Class
.. versionadded:: 2.1
"""

statuses = STORAGE_MANAGEMENT_SERVICE_CLASS_STATUS

def SCP(self, req: "_SMS", context: "PresentationContext") -> None:
"""The SCP implementation for Storage Management Service Class.
Parameters
----------
req : dimse_primitives.N_EVENT_REPORT or N_ACTION
The N-ACTION or N-EVENT-REPORT request primitive sent by the peer.
context : presentation.PresentationContext
The presentation context that the service is operating under.
"""
if isinstance(req, N_EVENT_REPORT):
self._n_event_report_scp(req, context)
elif isinstance(req, N_ACTION):
self._n_action_scp(req, context)
else:
raise ValueError(
f"Invalid DIMSE primitive '{req.__class__.__name__}' used "
f"with Storage Management"
)


class UnifiedProcedureStepServiceClass(ServiceClass):
"""Implementation of the Unified Procedure Step Service Class
Expand Down
4 changes: 4 additions & 0 deletions pynetdicom/sop_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
ProcedureStepServiceClass,
RTMachineVerificationServiceClass,
StorageCommitmentServiceClass,
StorageManagementServiceClass,
UnifiedProcedureStepServiceClass,
)

Expand Down Expand Up @@ -122,6 +123,9 @@ def uid_to_service_class(uid: str) -> Type[ServiceClass]:
if uid in _STORAGE_COMMITMENT_CLASSES.values():
return StorageCommitmentServiceClass

if uid in _STORAGE_MANAGEMENT_CLASSES.values():
return StorageManagementServiceClass

if uid in _SUBSTANCE_ADMINISTRATION_CLASSES.values():
return SubstanceAdministrationQueryServiceClass

Expand Down
Loading

0 comments on commit 3251a34

Please sign in to comment.