Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MRG] Add convenience function Event.encoded_dataset() #888

Merged
merged 10 commits into from
Nov 17, 2023
Prev Previous commit
Next Next commit
Fix list capitalisation
  • Loading branch information
scaramallion committed Nov 17, 2023
commit d7132926aa6ef43c576d34b431020e820922f741
4 changes: 2 additions & 2 deletions docs/tutorials/create_scp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,8 @@ complex code:
We've modified the handler to use :meth:`~pynetdicom.events.Event.encoded_dataset`,
which writes the preamble, prefix, file meta information elements and the
:attr:`raw dataset<dimse_primitives.C_STORE.DataSet>` received in the C-STORE
request directly to file. If you need separate access to the raw encoded dataset
then you can get it via the ``event.request.DataSet`` attribute.
request directly to file. If you need separate access to just encoded dataset
then you can call ``encoded_dataset(include_meta=False)`` instead.

The second change we've made is to demonstrate how extra parameters can be
passed to the handler by binding using a 3-tuple rather than a 2-tuple. The
Expand Down
13 changes: 12 additions & 1 deletion pynetdicom/dsutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
from io import BytesIO
import logging
from pathlib import Path
from struct import pack
import zlib

from pydicom import Dataset
from pydicom.dataset import FileMetaDataset
from pydicom.dataelem import DataElement
from pydicom.filebase import DicomBytesIO
from pydicom.filereader import read_dataset, read_preamble
from pydicom.filewriter import write_dataset
from pydicom.filewriter import write_dataset, write_file_meta_info
from pydicom.tag import BaseTag
from pydicom.uid import UID

Expand Down Expand Up @@ -181,6 +182,16 @@ def encode(
return bytestring


def encode_file_meta(file_meta: FileMetaDataset) -> bytes:
"""Return the encoded File Meta Information elements in `file_meta`."""

buffer = DicomBytesIO()
buffer.is_little_endian = True
buffer.is_implicit_VR = False
write_file_meta_info(buffer, file_meta)
return buffer.getvalue()


def pretty_dataset(ds: Dataset, indent: int = 0, indent_char: str = " ") -> list[str]:
"""Return a list of pretty dataset strings.

Expand Down
42 changes: 21 additions & 21 deletions pynetdicom/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from pydicom.tag import BaseTag
from pydicom.uid import UID

from pynetdicom.dsutils import decode, encode, create_file_meta
from pynetdicom.dsutils import decode, encode, create_file_meta, encode_file_meta

if TYPE_CHECKING: # pragma: no cover
from pynetdicom.association import Association
Expand Down Expand Up @@ -323,11 +323,11 @@ def trigger(
* an N-ACTION `request` key then :attr:`Event.action_information` will
return the decoded *Action Information* parameter value.
* an N-CREATE `request` key then :attr:`Event.attribute_list` will return
the decoded *Attribute list* parameter value.
the decoded *Attribute List* parameter value.
* an N-EVENT-REPORT `request` key then :attr:`Event.event_information` will
return the decoded *Event Information* parameter value.
* an N-SET `request` key then :attr:`Event.modification_list` will return
the decoded *Modification list* parameter value.
the decoded *Modification List* parameter value.

Parameters
----------
Expand Down Expand Up @@ -510,14 +510,14 @@ def action_type(self) -> int | None:

@property
def attribute_identifiers(self) -> list[BaseTag]:
"""Return an N-GET request's `Attribute Identifier list` as a
"""Return an N-GET request's `Attribute Identifier List` as a
:class:`list` of *pydicom* :class:`~pydicom.tag.BaseTag`.

Returns
-------
list of pydicom.tag.BaseTag
The (0000,1005) *Attribute Identifier list* tags, may be an empty
list if no *Attribute Identifier list* was included in the C-GET
The (0000,1005) *Attribute Identifier List* tags, may be an empty
list if no *Attribute Identifier List* was included in the C-GET
request.

Raises
Expand All @@ -526,7 +526,7 @@ def attribute_identifiers(self) -> list[BaseTag]:
If the corresponding event is not an N-GET request.
"""
try:
attr_list = cast("N_GET", self.request).AttributeIdentifierlist
attr_list = cast("N_GET", self.request).AttributeIdentifierList
if attr_list is None:
return []

Expand All @@ -539,12 +539,12 @@ def attribute_identifiers(self) -> list[BaseTag]:

raise AttributeError(
"The corresponding event is not an N-GET request and has no "
"'Attribute Identifier list' parameter"
"'Attribute Identifier List' parameter"
)

@property
def attribute_list(self) -> Dataset:
"""Return an N-CREATE request's `Attribute list` as a *pydicom*
"""Return an N-CREATE request's `Attribute List` as a *pydicom*
:class:`~pydicom.dataset.Dataset`.

Because *pydicom* defers data parsing during decoding until an element
Expand All @@ -556,7 +556,7 @@ def attribute_list(self) -> Dataset:
Returns
-------
pydicom.dataset.Dataset
The decoded *Attribute list* dataset.
The decoded *Attribute List* dataset.

Raises
------
Expand All @@ -565,9 +565,9 @@ def attribute_list(self) -> Dataset:
"""
msg = (
"The corresponding event is not an N-CREATE request and has no "
"'Attribute list' parameter"
"'Attribute List' parameter"
)
return self._get_dataset("Attributelist", msg)
return self._get_dataset("AttributeList", msg)

@property
def dataset(self) -> Dataset:
Expand Down Expand Up @@ -652,21 +652,21 @@ def handle_store(event: pynetdicom.evt.Event, dst: pathlib.Path) -> int:
meta information.
"""
try:
dataset = self.request.DataSet.getvalue()
stream = self.request.DataSet.getvalue()
except AttributeError:
raise AttributeError(
"The corresponding event is not a C-STORE request and has no "
"'Data Set' parameter"
)

if not include_meta:
return dataset
return stream

return b"".join([
b"\x00" * 128,
b"DICM",
encode(self.file_meta, is_implicit_VR=False, is_little_endian=True),
dataset,
encode_file_meta(self.file_meta),
stream,
])

@property
Expand Down Expand Up @@ -816,7 +816,7 @@ def _get_dataset(self, attr: str, exc_msg: str) -> Dataset:
----------
attr : str
The name of the DIMSE primitive's dataset-like parameter, one of
'DataSet', 'Identifier', 'Attributelist', 'Modificationlist',
'DataSet', 'Identifier', 'AttributeList', 'ModificationList',
'EventInformation', 'ActionInformation'.
exc_msg : str
The exception message to use if the request primitive has no
Expand Down Expand Up @@ -945,7 +945,7 @@ def message_id(self) -> int:

@property
def modification_list(self) -> Dataset:
"""Return an N-SET request's `Modification list` as a *pydicom*
"""Return an N-SET request's `Modification List` as a *pydicom*
:class:`~pydicom.dataset.Dataset`.

Because *pydicom* defers data parsing during decoding until an element
Expand All @@ -957,7 +957,7 @@ def modification_list(self) -> Dataset:
Returns
-------
pydicom.dataset.Dataset
The decoded *Modification list* dataset.
The decoded *Modification List* dataset.

Raises
------
Expand All @@ -966,9 +966,9 @@ def modification_list(self) -> Dataset:
"""
msg = (
"The corresponding event is not an N-SET request and has no "
"'Modification list' parameter"
"'Modification List' parameter"
)
return self._get_dataset("Modificationlist", msg)
return self._get_dataset("ModificationList", msg)

@property
def move_destination(self) -> str | None:
Expand Down
34 changes: 34 additions & 0 deletions pynetdicom/tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,40 @@ def test_event_type(self):

assert event.event_type == 2

def test_encode_dataset(self):
"""Test Event.encode_dataset()"""
request = C_STORE()
request.AffectedSOPClassUID = "1.2"
request.AffectedSOPInstanceUID = "1.3"
request.DataSet = BytesIO(b"\x00\x01")

event = Event(
None,
evt.EVT_C_STORE,
{"request": request, "context": self.context.as_tuple},
)
bs = event.encoded_dataset()

from pynetdicom.utils import pretty_bytes

assert bs[:128] == b"\x00" * 128
assert bs[128:132] == b"DICM"
assert bs[132:144] == b"\x02\x00\x00\x00\x55\x4c\x04\x00\x7e\x00\x00\x00"
assert bs[144:144 + 64] == (
b"\x02\x00\x01\x00\x4f\x42\x00\x00\x02\x00\x00\x00\x00\x01"
b"\x02\x00\x02\x00\x55\x49\x04\x00\x31\x2e\x32\x00"
b"\x02\x00\x03\x00\x55\x49\x04\x00\x31\x2e\x33\x00"
b"\x02\x00\x10\x00\x55\x49\x12\x00\x31\x2e\x32\x2e\x38\x34"
b"\x30\x2e\x31\x30\x30\x30\x38\x2e\x31\x2e\x32\x00"
)

# Note: may not be 126 if Implementation Class and Version change
assert 128 + 4 + 12 + 126 + 2== len(bs)

# Test without file_meta
bs = event.encoded_dataset(include_meta=False)
assert bs == b"\x00\x01"


# TODO: Should be able to remove in v1.4
INTERVENTION_HANDLERS = [
Expand Down