Repository: pydicom/pynetdicom
Pull request #888 (2023-11-17 10:09:10)

[MRG] Add convenience function Event.encoded_dataset()
#### Reference issue
Adds a convenience function for accessing the encoded dataset and file meta

#### 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
- [x] Type annotations updated and passing with mypy


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pydicom/pynetdicom/pull/888?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) Report
> Merging [#888](https://app.codecov.io/gh/pydicom/pynetdicom/pull/888?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (f8a1f4d) into [main](https://app.codecov.io/gh/pydicom/pynetdicom/commit/37388d13d8a122378cfe8b80197105961db8bbe5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (37388d1) will **decrease** coverage by `0.15%`.
> The diff coverage is `100.00%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##              main     #888      +/-   ##
===========================================
- Coverage   100.00%   99.85%   -0.15%     
===========================================
  Files           28       28              
  Lines         8663     8679      +16     
===========================================
+ Hits          8663     8666       +3     
- Misses           0       13      +13     
```

| [Files](https://app.codecov.io/gh/pydicom/pynetdicom/pull/888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) | Coverage Δ | |
|---|---|---|
| [pynetdicom/dsutils.py](https://app.codecov.io/gh/pydicom/pynetdicom/pull/888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHluZXRkaWNvbS9kc3V0aWxzLnB5) | `100.00% <100.00%> (ø)` | |
| [pynetdicom/events.py](https://app.codecov.io/gh/pydicom/pynetdicom/pull/888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHluZXRkaWNvbS9ldmVudHMucHk=) | `100.00% <100.00%> (ø)` | |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/pydicom/pynetdicom/pull/888/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)

</details>

Patch:
diff --git a/docs/changelog/v2.1.0.rst b/docs/changelog/v2.1.0.rst
index 61229ca7a..0a7c0cb61 100644
--- a/docs/changelog/v2.1.0.rst
+++ b/docs/changelog/v2.1.0.rst
@@ -9,9 +9,9 @@ Fixes
 * Fixed reserved A-ASSOCIATE-AC parameters being tested (:issue:`746`)
 * Fixed datasets not transferring correctly when using
   :attr:`~pynetdicom._config.STORE_RECV_CHUNKED_DATASET` (:issue:`756`)
-* Fixed maximum length of PatientID attribute in qrscp app (:issue:`785`)
-* Sanitize filenames for received datasets for non-conformant SOP Instance UIDs
-  (:issue:`823`)
+* Fixed maximum length of *Patient ID* attribute in ``qrscp`` app (:issue:`785`)
+* Sanitise filenames for received datasets for non-conformant SOP Instance
+  UIDs (:issue:`823`)
 
 Enhancements
 ............
@@ -23,8 +23,11 @@ Enhancements
   :class:`~pynetdicom.service_class.QueryRetrieveServiceClass` (:issue:`878`)
 * Added support for :class:`Inventory Query/Retrieve Service Class
   <pynetdicom.service_class.InventoryQueryRetrieveServiceClass>` (:issue:`879`)
+* Added :meth:`~pynetdicom.events.Event.encoded_dataset` to simplify accessing
+  the encoded dataset without first decoding it
 
 Changes
 .......
 
-* Add preliminary support for Python 3.12
+* Dropped support for Python 3.7, 3.8 and 3.9
+* Added support for Python 3.12
diff --git a/docs/examples/storage.rst b/docs/examples/storage.rst
index 917075662..84c8de333 100644
--- a/docs/examples/storage.rst
+++ b/docs/examples/storage.rst
@@ -157,21 +157,16 @@ multiple C-STORE requests, depending on the size of the datasets:
 
 .. code-block:: python
 
-    from pydicom.filewriter import write_file_meta_info
-
+    import uuid
     from pynetdicom import AE, evt, AllStoragePresentationContexts
 
     # Implement a handler for evt.EVT_C_STORE
     def handle_store(event):
         """Handle a C-STORE request event."""
-        with open(event.request.AffectedSOPInstanceUID, 'wb') as f:
-            # Write the preamble and prefix
-            f.write(b'\x00' * 128)
-            f.write(b'DICM')
-            # Encode and write the File Meta Information
-            write_file_meta_info(f, event.file_meta)
-            # Write the encoded dataset
-            f.write(event.request.DataSet.getvalue())
+        with open(f"{uuid.uuid4()}", 'wb') as f:
+            # Write the preamble, prefix, file meta information
+            #   and encoded dataset to `f`
+            f.write(event.encoded_dataset())
 
         # Return a 'Success' status
         return 0x0000
diff --git a/docs/tutorials/create_scp.rst b/docs/tutorials/create_scp.rst
index 995d6e482..359c96700 100644
--- a/docs/tutorials/create_scp.rst
+++ b/docs/tutorials/create_scp.rst
@@ -397,11 +397,10 @@ complex code:
 
 .. code-block:: python
    :linenos:
-   :emphasize-lines: 1,3,12,14-18,21-28,32
+   :emphasize-lines: 1-2,11,13-17,19-23,27
 
-    import os
-
-    from pydicom.filewriter import write_file_meta_info
+    import uuid
+    from pathlib import Path
 
     from pynetdicom import (
         AE, debug_logger, evt, AllStoragePresentationContexts,
@@ -418,15 +417,11 @@ complex code:
             # Unable to create output dir, return failure status
             return 0xC001
 
-        # We rely on the UID from the C-STORE request instead of decoding
-        fname = os.path.join(storage_dir, event.request.AffectedSOPInstanceUID)
-        with open(fname, 'wb') as f:
-            # Write the preamble, prefix and file meta information elements
-            f.write(b'\x00' * 128)
-            f.write(b'DICM')
-            write_file_meta_info(f, event.file_meta)
-            # Write the raw encoded dataset
-            f.write(event.request.DataSet.getvalue())
+        path = Path(storage_dir) / f"{uuid.uuid4()}"
+        with path.open('wb') as f:
+            # Write the preamble, prefix, file meta information elements
+            #   and the raw encoded dataset to `f`
+            f.write(event.encoded_dataset())
 
         return 0x0000
 
@@ -441,12 +436,12 @@ complex code:
 
     ae.start_server(("127.0.0.1", 11112), block=True, evt_handlers=handlers)
 
-We've modified the handler to write the preamble and prefix to file,
-encode and write the file meta information elements using *pydicom's*
-:func:`~pydicom.filewriter.write_file_meta_info` function, then finally write
-the encoded dataset using the :attr:`raw dataset
-<dimse_primitives.C_STORE.DataSet>` received directly from the C-STORE
-request via the ``event.request`` attribute.
+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 just the encoded dataset
+then you can call :meth:`~pynetdicom.events.Event.encoded_dataset` with
+`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
diff --git a/pynetdicom/apps/common.py b/pynetdicom/apps/common.py
index 657199ab8..6c9232356 100644
--- a/pynetdicom/apps/common.py
+++ b/pynetdicom/apps/common.py
@@ -8,12 +8,9 @@ from struct import pack
 from pydicom import dcmread
 from pydicom.datadict import tag_for_keyword, repeater_has_keyword, get_entry
 from pydicom.dataset import Dataset
-from pydicom.filewriter import write_file_meta_info
 from pydicom.tag import Tag
 from pydicom.uid import DeflatedExplicitVRLittleEndian
 
-from pynetdicom.dsutils import encode
-
 
 def create_dataset(args, logger=None):
     """Return a new or updated dataset.
@@ -629,10 +626,7 @@ def handle_store(event, args, app_logger):
         if event.context.transfer_syntax == DeflatedExplicitVRLittleEndian:
             # Workaround for pydicom issue #1086
             with open(filename, "wb") as f:
-                f.write(b"\x00" * 128)
-                f.write(b"DICM")
-                write_file_meta_info(f, event.file_meta)
-                f.write(encode(ds, False, True, True))
+                f.write(event.encoded_dataset())
         else:
             # We use `write_like_original=False` to ensure that a compliant
             #   File Meta Information Header is written
diff --git a/pynetdicom/dsutils.py b/pynetdicom/dsutils.py
index d661a0d1c..68eaa6af3 100644
--- a/pynetdicom/dsutils.py
+++ b/pynetdicom/dsutils.py
@@ -3,7 +3,6 @@
 from io import BytesIO
 import logging
 from pathlib import Path
-from typing import Optional, List, Tuple
 import zlib
 
 from pydicom import Dataset
@@ -11,7 +10,7 @@ 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
 
@@ -128,7 +127,7 @@ def decode(
 
 def encode(
     ds: Dataset, is_implicit_vr: bool, is_little_endian: bool, deflated: bool = False
-) -> Optional[bytes]:
+) -> bytes | None:
     """Encode a *pydicom* :class:`~pydicom.dataset.Dataset` `ds`.
 
     .. versionchanged:: 1.5
@@ -182,7 +181,21 @@ def encode(
     return bytestring
 
 
-def pretty_dataset(ds: Dataset, indent: int = 0, indent_char: str = "  ") -> List[str]:
+def encode_file_meta(file_meta: FileMetaDataset) -> bytes:
+    """Return the encoded File Meta Information elements in `file_meta`.
+
+    .. versionadded:: 2.1
+
+    """
+
+    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.
 
     .. versionadded:: 1.5
@@ -270,7 +283,7 @@ def pretty_element(elem: DataElement) -> str:
     )
 
 
-def split_dataset(path: Path) -> Tuple[Dataset, int]:
+def split_dataset(path: Path) -> tuple[Dataset, int]:
     """Return the file meta elements and the offset to the start of the dataset
 
     .. versionadded:: 2.0
@@ -288,7 +301,7 @@ def split_dataset(path: Path) -> Tuple[Dataset, int]:
         no File Meta is present.
     """
 
-    def _not_group_0002(tag: BaseTag, VR: Optional[str], length: int) -> bool:
+    def _not_group_0002(tag: BaseTag, VR: str | None, length: int) -> bool:
         """Return True if the tag is not in group 0x0002, False otherwise."""
         return tag.group != 2
 
diff --git a/pynetdicom/events.py b/pynetdicom/events.py
index 28cb495f9..0d79a3a17 100644
--- a/pynetdicom/events.py
+++ b/pynetdicom/events.py
@@ -3,30 +3,20 @@ the state machine events.
 """
 
 from datetime import datetime
+from io import BytesIO
 import inspect
 import logging
 from pathlib import Path
 import sys
-from typing import (
-    Union,
-    Callable,
-    Any,
-    Tuple,
-    List,
-    NamedTuple,
-    Optional,
-    TYPE_CHECKING,
-    Dict,
-    cast,
-    Iterator,
-)
+from typing import Callable, Any, NamedTuple, TYPE_CHECKING, cast, Iterator, Union
 
 from pydicom.dataset import Dataset, FileMetaDataset
+from pynetdicom.dimse_primitives import C_STORE
 from pydicom.filereader import dcmread
 from pydicom.tag import BaseTag
 from pydicom.uid import UID
 
-from pynetdicom.dsutils import decode, create_file_meta
+from pynetdicom.dsutils import decode, create_file_meta, encode_file_meta
 
 if TYPE_CHECKING:  # pragma: no cover
     from pynetdicom.association import Association
@@ -36,7 +26,6 @@ if TYPE_CHECKING:  # pragma: no cover
         C_FIND,
         C_GET,
         C_MOVE,
-        C_STORE,
         N_ACTION,
         N_CREATE,
         N_DELETE,
@@ -48,31 +37,29 @@ if TYPE_CHECKING:  # pragma: no cover
     from pynetdicom.pdu_primitives import SOPClassCommonExtendedNegotiation
     from pynetdicom.presentation import PresentationContextTuple
 
-    _RequestType = Union[
-        C_ECHO,
-        C_FIND,
-        C_GET,
-        C_MOVE,
-        C_STORE,
-        N_ACTION,
-        N_CREATE,
-        N_DELETE,
-        N_EVENT_REPORT,
-        N_GET,
-        N_SET,
-    ]
+    _RequestType = (
+        C_ECHO
+        | C_FIND
+        | C_GET
+        | C_MOVE
+        | C_STORE
+        | N_ACTION
+        | N_CREATE
+        | N_DELETE
+        | N_EVENT_REPORT
+        | N_GET
+        | N_SET
+    )
 
 
 LOGGER = logging.getLogger("pynetdicom.events")
 
 
 EventType = Union["NotificationEvent", "InterventionEvent"]
-EventHandlerType = Union[
-    Tuple[EventType, Callable], Tuple[EventType, Callable, List[Any]]
-]
-_BasicReturnType = Union[Dataset, int]
-_DatasetReturnType = Tuple[_BasicReturnType, Optional[Dataset]]
-_IteratorType = Iterator[Tuple[_BasicReturnType, Optional[Dataset]]]
+EventHandlerType = tuple[EventType, Callable] | tuple[EventType, Callable, list[Any]]
+_BasicReturnType = Dataset | int
+_DatasetReturnType = tuple[_BasicReturnType, Dataset | None]
+_IteratorType = Iterator[tuple[_BasicReturnType, Dataset | None]]
 
 
 # Notification events
@@ -222,11 +209,11 @@ _NOTIFICATION_EVENTS = [
 ]
 
 
-_HandlerBase = Tuple[Callable, Optional[List[Any]]]
-_NotificationHandlerAttr = List[_HandlerBase]
+_HandlerBase = tuple[Callable, list[Any] | None]
+_NotificationHandlerAttr = list[_HandlerBase]
 _InterventionHandlerAttr = _HandlerBase
-HandlerArgType = Union[_NotificationHandlerAttr, _InterventionHandlerAttr]
-_HandlerAttr = Dict[EventType, HandlerArgType]
+HandlerArgType = _NotificationHandlerAttr | _InterventionHandlerAttr
+_HandlerAttr = dict[EventType, HandlerArgType]
 
 
 def _add_handler(
@@ -239,11 +226,10 @@ def _add_handler(
     event : NotificationEvent or InterventionEvent
         The event the handler should be bound to.
     handlers_attr : dict
-        The object attribute of {event: Union[
-            [(handler, Optional[args])],
-            (handler, Optional[args])
-        ]} used to record bindings.
-    handler_arg : Tuple[Callable, Optional[List[Any]]]
+        The object attribute of
+        {event: [(handler, None | args)] | (handler, None | args)} used to
+        record bindings.
+    handler_arg : tuple[Callable, None | list[Any]]
         The handler and optional arguments to be bound.
     """
     if isinstance(event, NotificationEvent):
@@ -270,12 +256,8 @@ def _remove_handler(
         The event the handler should be unbound from.
     handlers_attr : dict
         The object attribute of
-        {
-            event: Union[
-                List[(handler, Optional[args])],
-                (handler, Optional[args])
-            ]
-        } used to record bindings.
+        {event: list[(handler, None | args)] | (handler, None | args)} used
+        to record bindings.
     handler_arg : Callable
         The handler to be unbound.
     """
@@ -320,8 +302,8 @@ def get_default_handler(event: InterventionEvent) -> Callable[["Event"], Any]:
 
 
 def trigger(
-    assoc: "Association", event: EventType, attrs: Optional[Dict[str, Any]] = None
-) -> Optional[Any]:
+    assoc: "Association", event: EventType, attrs: dict[str, Any] | None = None
+) -> Any | None:
     """Trigger an `event` and call any bound handler(s).
 
     .. versionadded:: 1.3
@@ -435,7 +417,7 @@ class Event:
         self,
         assoc: "Association",
         event: EventType,
-        attrs: Optional[Dict[str, Any]] = None,
+        attrs: dict[str, Any] | None = None,
     ) -> None:
         """Create a new Event.
 
@@ -454,8 +436,8 @@ class Event:
         self.timestamp = datetime.now()
 
         # Only decode a dataset when necessary
-        self._hash: Optional[int] = None
-        self._decoded: Optional[Dataset] = None
+        self._hash: int | None = None
+        self._decoded: Dataset | None = None
 
         # Define type hints for dynamic attributes
         self.request: "_RequestType"
@@ -503,7 +485,7 @@ class Event:
         return self._get_dataset("ActionInformation", msg)
 
     @property
-    def action_type(self) -> Optional[int]:
+    def action_type(self) -> int | None:
         """Return an N-ACTION request's `Action Type ID` as an :class:`int`.
 
         .. versionadded:: 1.4
@@ -528,7 +510,7 @@ class Event:
             )
 
     @property
-    def attribute_identifiers(self) -> List[BaseTag]:
+    def attribute_identifiers(self) -> list[BaseTag]:
         """Return an N-GET request's `Attribute Identifier List` as a
         :class:`list` of *pydicom* :class:`~pydicom.tag.BaseTag`.
 
@@ -644,6 +626,66 @@ class Event:
 
         return cast(Path, path)
 
+    def encoded_dataset(self, include_meta: bool = True) -> bytes:
+        """Return the encoded C-STORE dataset sent by the peer without first
+        decoding it.
+
+        .. versionadded:: 2.1
+
+        Examples
+        --------
+        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)
+
+              return 0x0000
+
+        Write the encoded dataset to file in the DICOM File Format without
+        having to first decode it::
+
+          def handle_store(event: pynetdicom.events.Event, dst: pathlib.Path) -> int:
+              with dst.open("wb") as f:
+                  f.write(event.encoded_dataset())
+
+              return 0x0000
+
+        Parameters
+        ----------
+        include_meta : bool, optional
+            If ``True`` (default) then include the encoded DICOM preamble,
+            prefix and file meta information with the returned bytestream.
+
+        Returns
+        -------
+        bytes
+            The encoded dataset as sent by the peer, with or without the file
+            meta information.
+
+        Raises
+        ------
+        AttributeError
+            If the corresponding event is not a C-STORE request.
+        """
+        try:
+            request = cast(C_STORE, self.request)
+            stream = cast(BytesIO, 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 stream
+
+        return b"".join((
+            b"\x00" * 128,
+            b"DICM",
+            encode_file_meta(self.file_meta),
+            stream,
+        ))
+
     @property
     def event(self) -> EventType:
         """Return the corresponding event.
@@ -686,7 +728,7 @@ class Event:
         return self._get_dataset("EventInformation", msg)
 
     @property
-    def event_type(self) -> Optional[int]:
+    def event_type(self) -> int | None:
         """Return an N-EVENT-REPORT request's `Event Type ID` as an
         :class:`int`.
 
@@ -747,7 +789,8 @@ class Event:
 
         Encode the File Meta Information in a new file and append the encoded
         *Data Set* to it. This skips having to decode/re-encode the *Data Set*
-        as in the previous example.
+        as in the previous example (or alternatively, just use the
+        :meth:`~pynetdicom.events.Event.encoded_dataset` method).
 
         .. code-block:: python
 
@@ -945,7 +988,7 @@ class Event:
         return self._get_dataset("ModificationList", msg)
 
     @property
-    def move_destination(self) -> Optional[str]:
+    def move_destination(self) -> str | None:
         """Return a C-MOVE request's `Move Destination` as :class:`str`.
 
         .. versionadded:: 1.4
@@ -976,7 +1019,7 @@ class Event:
 
 
 # Default extended negotiation event handlers
-def _async_ops_handler(event: Event) -> Tuple[int, int]:
+def _async_ops_handler(event: Event) -> tuple[int, int]:
     """Default handler for when an Asynchronous Operations Window Negotiation
     item is include in the association request.
 
@@ -989,7 +1032,7 @@ def _async_ops_handler(event: Event) -> Tuple[int, int]:
     )
 
 
-def _sop_common_handler(event: Event) -> Dict[UID, "SOPClassCommonExtendedNegotiation"]:
+def _sop_common_handler(event: Event) -> dict[UID, "SOPClassCommonExtendedNegotiation"]:
     """Default handler for when one or more SOP Class Common Extended
     Negotiation items are included in the association request.
 
@@ -998,7 +1041,7 @@ def _sop_common_handler(event: Event) -> Dict[UID, "SOPClassCommonExtendedNegoti
     return {}
 
 
-def _sop_extended_handler(event: Event) -> Dict[UID, bytes]:
+def _sop_extended_handler(event: Event) -> dict[UID, bytes]:
     """Default handler for when one or more SOP Class Extended Negotiation
     items are included in the association request.
 
@@ -1007,7 +1050,7 @@ def _sop_extended_handler(event: Event) -> Dict[UID, bytes]:
     return {}
 
 
-def _user_identity_handler(event: Event) -> Tuple[bool, Optional[bytes]]:
+def _user_identity_handler(event: Event) -> tuple[bool, bytes | None]:
     """Default handler for when a user identity negotiation item is included
     with the association request.
 


Test patch:
diff --git a/pynetdicom/tests/test_events.py b/pynetdicom/tests/test_events.py
index fbbeeaa89..dbb6794bd 100644
--- a/pynetdicom/tests/test_events.py
+++ b/pynetdicom/tests/test_events.py
@@ -149,6 +149,13 @@ class TestEvent:
         with pytest.raises(AttributeError, match=msg):
             event.dataset
 
+        msg = (
+            r"The corresponding event is not a C-STORE request "
+            r"and has no 'Data Set' parameter"
+        )
+        with pytest.raises(AttributeError, match=msg):
+            event.encoded_dataset()
+
         msg = (
             r"The corresponding event is not a C-FIND, C-GET or C-MOVE request "
             r"and has no 'Identifier' parameter"
@@ -511,6 +518,40 @@ class TestEvent:
 
         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 = [