Repository: medley56/space_packet_parser

Issue:
Prepend `Xtce` to all the XTCE-derived Classes
In order to save namespace names, we can prepend Xtce to the class names for all the classes that are basically direct representation of XTCE elements.

This would allow us to reclaim `Parameter` for the parsed parameters that are returned from the parsers. Currently these subclasses of primitives with a `DataItem` mixin. I suggest a Parameter mixin and renaming them to, e.g. `StrParameter` instead of `StrDataItem`.

Putting this in Milestone 5.0.0 since it changes the user-facing API.

Discussion:
@medley56: I've changed my mind. This is not really a useful change and would really make the definition classes ugly.

Solution patch:
diff --git a/docs/source/changelog.md b/docs/source/changelog.md
index 9417eea..30fd512 100644
--- a/docs/source/changelog.md
+++ b/docs/source/changelog.md
@@ -17,6 +17,9 @@ Release notes for the `space_packet_parser` library
   - Much faster parsing speed
   - Users that are passing `bitstring.ConstBitStream` objects to `generator` will need to pass a 
     binary filelike object instead
+- BREAKING: The ``ParsedDataItem`` class has been removed and the derived values are being returned now.
+  The ``raw_value`` is stored as an attribute on the returned object. The other items can be accessed
+  through the packet definition object ``my_packet_definition.named_parameters["my_item"].short_description``
 - BREAKING: The return type of BinaryDataEncoding is now the raw bytes.
   To get the previous behavior you can convert the data to an integer and then format it as a binary string.
   ``f"{int.from_bytes(data, byteorder='big'):0{len(data)*8}b}"``
diff --git a/space_packet_parser/comparisons.py b/space_packet_parser/comparisons.py
index 31da52b..e57e988 100644
--- a/space_packet_parser/comparisons.py
+++ b/space_packet_parser/comparisons.py
@@ -178,7 +178,7 @@ def evaluate(self,
         """
         if self.referenced_parameter in packet:
             if self.use_calibrated_value:
-                parsed_value = packet[self.referenced_parameter].derived_value
+                parsed_value = packet[self.referenced_parameter]
                 if not parsed_value:
                     raise ComparisonError(f"Comparison {self} was instructed to useCalibratedValue (the default)"
                                           f"but {self.referenced_parameter} does not appear to have a derived value.")
@@ -341,7 +341,7 @@ def evaluate(self,
         def _get_parsed_value(parameter_name: str, use_calibrated: bool):
             """Retrieves the previously parsed value from the passed in packet"""
             try:
-                return packet[parameter_name].derived_value if use_calibrated \
+                return packet[parameter_name] if use_calibrated \
                     else packet[parameter_name].raw_value
             except KeyError as e:
                 raise ComparisonError(f"Attempting to perform a Condition evaluation on {self.left_param} but "
diff --git a/space_packet_parser/definitions.py b/space_packet_parser/definitions.py
index d7e6200..cdf0cc2 100644
--- a/space_packet_parser/definitions.py
+++ b/space_packet_parser/definitions.py
@@ -370,10 +370,8 @@ def _parse_header(packet_data: bytes) -> dict:
         header = {}
         current_bit = 0
         for item in CCSDS_HEADER_DEFINITION:
-            header[item.name] = packets.ParsedDataItem(
-                name=item.name,
-                # pylint: disable=protected-access
-                raw_value=packets._extract_bits(packet_data, current_bit, item.nbits))
+            # pylint: disable=protected-access
+            header[item.name] = packets._extract_bits(packet_data, current_bit, item.nbits)
             current_bit += item.nbits
         return header
 
@@ -417,7 +415,7 @@ def parse_ccsds_packet(self,
                     raise UnrecognizedPacketTypeError(
                         f"Detected an abstract container with no valid inheritors by restriction criteria. This might "
                         f"mean this packet type is not accounted for in the provided packet definition. "
-                        f"APID={packet['PKT_APID'].raw_value}.",
+                        f"APID={packet['PKT_APID']}.",
                         partial_data=packet)
                 break
 
@@ -593,7 +591,7 @@ def packet_generator(  # pylint: disable=too-many-branches,too-many-statements
             # per the CCSDS spec
             # 4.1.3.5.3 The length count C shall be expressed as:
             #   C = (Total Number of Octets in the Packet Data Field) – 1
-            n_bytes_data = header['PKT_LEN'].raw_value + 1
+            n_bytes_data = header['PKT_LEN'] + 1
             n_bytes_packet = CCSDS_HEADER_LENGTH_BYTES + n_bytes_data
 
             # Based on PKT_LEN fill buffer enough to read a full packet
@@ -623,17 +621,17 @@ def packet_generator(  # pylint: disable=too-many-branches,too-many-statements
                 packet = self.parse_ccsds_packet(packet,
                                                  root_container_name=root_container_name)
             except UnrecognizedPacketTypeError as e:
-                logger.debug(f"Unrecognized error on packet with APID {header['PKT_APID'].raw_value}'")
+                logger.debug(f"Unrecognized error on packet with APID {header['PKT_APID']}'")
                 if yield_unrecognized_packet_errors:
                     # Yield the caught exception without raising it (raising ends generator)
                     yield e
                 # Continue to next packet
                 continue
 
-            if packet.header['PKT_LEN'].raw_value != header['PKT_LEN'].raw_value:
+            if packet.header['PKT_LEN'] != header['PKT_LEN']:
                 raise ValueError(f"Hardcoded header parsing found a different packet length "
-                                 f"{header['PKT_LEN'].raw_value} than the definition-based parsing found "
-                                 f"{packet.header['PKT_LEN'].raw_value}. This might be because the CCSDS header is "
+                                 f"{header['PKT_LEN']} than the definition-based parsing found "
+                                 f"{packet.header['PKT_LEN']}. This might be because the CCSDS header is "
                                  f"incorrectly represented in your packet definition document.")
 
             actual_length_parsed = packet.raw_data.pos // 8
@@ -644,7 +642,7 @@ def packet_generator(  # pylint: disable=too-many-branches,too-many-statements
                                f"Updating the position to the correct position "
                                "indicated by CCSDS header.")
                 if not parse_bad_pkts:
-                    logger.warning(f"Skipping (not yielding) bad packet with apid {header['PKT_APID'].raw_value}.")
+                    logger.warning(f"Skipping (not yielding) bad packet with apid {header['PKT_APID']}.")
                     continue
 
             yield packet
diff --git a/space_packet_parser/encodings.py b/space_packet_parser/encodings.py
index d3bb585..9d53662 100644
--- a/space_packet_parser/encodings.py
+++ b/space_packet_parser/encodings.py
@@ -3,8 +3,8 @@
 from abc import ABCMeta, abstractmethod
 import logging
 import struct
-from typing import Any, List, Optional, Tuple, Union
-# Installed
+from typing import List, Optional, Union
+
 import lxml.etree as ElementTree
 # Local
 from space_packet_parser import calibrators, comparisons, packets
@@ -145,7 +145,7 @@ def _calculate_size(self, packet: packets.CCSDSPacket) -> int:
         """
         raise NotImplementedError()
 
-    def parse_value(self, packet: packets.CCSDSPacket, **kwargs) -> Tuple[Any, Any]:
+    def parse_value(self, packet: packets.CCSDSPacket, **kwargs) -> packets.ParameterDataTypes:
         """Parse a value from packet data, possibly using previously parsed data items to inform parsing.
 
         Parameters
@@ -155,10 +155,8 @@ def parse_value(self, packet: packets.CCSDSPacket, **kwargs) -> Tuple[Any, Any]:
             previously parsed data items to infer field lengths.
         Returns
         -------
-        : any
-            Raw value
-        : any
-            Calibrated value
+        : packets.ParameterDataTypes
+            Derived value with a `raw_value` attribute that can be used to get the original packet data.
         """
         raise NotImplementedError()
 
@@ -294,8 +292,8 @@ def _calculate_size(self, packet: packets.CCSDSPacket) -> int:
                 raise ValueError('List of discrete lookup values being used for determining length of '
                                  f'string {self} found no matches based on {packet}.')
         elif self.dynamic_length_reference:
-            if self.use_calibrated_value is True:
-                buflen_bits = packet[self.dynamic_length_reference].derived_value
+            if self.use_calibrated_value:
+                buflen_bits = packet[self.dynamic_length_reference]
             else:
                 buflen_bits = packet[self.dynamic_length_reference].raw_value
 
@@ -336,7 +334,7 @@ def _get_raw_buffer(self, packet: packets.CCSDSPacket) -> bytes:
         ).to_bytes(buflen_bytes, "big")
         return raw_string_buffer
 
-    def parse_value(self, packet: packets.CCSDSPacket, **kwargs) -> Tuple[bytes, str]:
+    def parse_value(self, packet: packets.CCSDSPacket, **kwargs) -> packets.StrParameter:
         """Parse a string value from packet data, possibly using previously parsed data items to inform parsing.
 
         Parameters
@@ -347,11 +345,11 @@ def parse_value(self, packet: packets.CCSDSPacket, **kwargs) -> Tuple[bytes, str
 
         Returns
         -------
-        : bytes
-            Raw string bytes buffer. This includes any leading size or termination character and may be a non-integer
+        : StrParameter
+            Parsed string value.
+            The `raw_value` attribute contains the raw string bytes buffer.
+            This includes any leading size or termination character and may be a non-integer
             number of bytes, padded on the RHS.
-        : str
-            Parsed string value, as a python string object.
         """
         raw_string_buffer = packets.RawPacketData(self._get_raw_buffer(packet))
         if self.leading_length_size:
@@ -371,7 +369,7 @@ def parse_value(self, packet: packets.CCSDSPacket, **kwargs) -> Tuple[bytes, str
             # Indicates there is no further parsing. The raw string value is the whole string value.
             parsed_string = raw_string_buffer.decode(self.encoding)
 
-        return bytes(raw_string_buffer), parsed_string
+        return packets.StrParameter(parsed_string, bytes(raw_string_buffer))
 
     @classmethod
     def from_data_encoding_xml_element(cls, element: ElementTree.Element, ns: dict) -> 'StringDataEncoding':
@@ -465,6 +463,7 @@ def from_data_encoding_xml_element(cls, element: ElementTree.Element, ns: dict)
 
 class NumericDataEncoding(DataEncoding, metaclass=ABCMeta):
     """Abstract class that is inherited by IntegerDataEncoding and FloatDataEncoding"""
+    _data_return_class = packets.FloatParameter
 
     def __init__(self,
                  size_in_bits: int,
@@ -530,7 +529,7 @@ def _twos_complement(val: int, bit_width: int) -> int:
 
     def parse_value(self,
                     packet: packets.CCSDSPacket,
-                    **kwargs) -> Tuple[Union[int, float], Union[int, float]]:
+                    **kwargs) -> Union[packets.FloatParameter, packets.IntParameter]:
         """Parse a value from packet data, possibly using previously parsed data items to inform parsing.
 
         Parameters
@@ -540,29 +539,28 @@ def parse_value(self,
             previously parsed data items to infer field lengths.
         Returns
         -------
-        : any
-            Parsed value
-        : any
-            Calibrated value
+        : packets.FloatParameter or packets.IntParameter
+            Parsed data item as either a float or integer depending on the type of encoding.
         """
         parsed_value = self._get_raw_value(packet)
-        # Attempt to calibrate
-        calibrated_value = parsed_value  # Provides a fall through in case we have no calibrators
+        # Attempt to calibrate (any calibrated values are returned as FloatParameters)
         if self.context_calibrators:
             for calibrator in self.context_calibrators:
                 match_criteria = calibrator.match_criteria
                 if all(criterion.evaluate(packet, parsed_value) for criterion in match_criteria):
                     # If the parsed data so far satisfy all the match criteria
                     calibrated_value = calibrator.calibrate(parsed_value)
-                    return parsed_value, calibrated_value
+                    return packets.FloatParameter(calibrated_value, parsed_value)
         if self.default_calibrator:  # If no context calibrators or if none apply and there is a default
             calibrated_value = self.default_calibrator.calibrate(parsed_value)
-        # Ultimate fallthrough
-        return parsed_value, calibrated_value
+            return packets.FloatParameter(calibrated_value, parsed_value)
+        # No calibrations applied, we need to determine if it's an int or a float encoding calling this routine
+        return self._data_return_class(parsed_value)
 
 
 class IntegerDataEncoding(NumericDataEncoding):
     """<xtce:IntegerDataEncoding>"""
+    _data_return_class = packets.IntParameter
 
     def _get_raw_value(self, packet: packets.CCSDSPacket) -> int:
         # Extract the bits from the data in big-endian order from the packet
@@ -608,6 +606,7 @@ def from_data_encoding_xml_element(cls, element: ElementTree.Element, ns: dict)
 class FloatDataEncoding(NumericDataEncoding):
     """<xtce:FloatDataEncoding>"""
     _supported_encodings = ['IEEE-754', 'MIL-1750A']
+    _data_return_class = packets.FloatParameter
 
     def __init__(
             self,
@@ -775,7 +774,7 @@ def _calculate_size(self, packet: packets.CCSDSPacket) -> int:
         elif self.size_reference_parameter is not None:
             field_length_reference = self.size_reference_parameter
             if self.use_calibrated_value:
-                len_bits = packet[field_length_reference].derived_value
+                len_bits = packet[field_length_reference]
             else:
                 len_bits = packet[field_length_reference].raw_value
         elif self.size_discrete_lookup_list is not None:
@@ -794,7 +793,7 @@ def _calculate_size(self, packet: packets.CCSDSPacket) -> int:
             len_bits = self.linear_adjuster(len_bits)
         return len_bits
 
-    def parse_value(self, packet: packets.CCSDSPacket, **kwargs):
+    def parse_value(self, packet: packets.CCSDSPacket, **kwargs) -> packets.BinaryParameter:
         """Parse a value from packet data, possibly using previously parsed data items to inform parsing.
 
         Parameters
@@ -805,14 +804,12 @@ def parse_value(self, packet: packets.CCSDSPacket, **kwargs):
 
         Returns
         -------
-        : any
-            Parsed value
-        : any
-            Calibrated value
+        : packets.BinaryParameter
+            Parsed binary data item.
         """
         nbits = self._calculate_size(packet)
         parsed_value = packet.raw_data.read_as_bytes(nbits)
-        return parsed_value, None
+        return packets.BinaryParameter(parsed_value)
 
     @classmethod
     def from_data_encoding_xml_element(cls, element: ElementTree.Element, ns: dict) -> 'BinaryDataEncoding':
diff --git a/space_packet_parser/packets.py b/space_packet_parser/packets.py
index 7d44153..6640772 100644
--- a/space_packet_parser/packets.py
+++ b/space_packet_parser/packets.py
@@ -2,34 +2,58 @@
 """Packet containers and parsing utilities for space packets."""
 
 from dataclasses import dataclass, field
-from typing import Union, Optional, Protocol, List
+from typing import List, Optional, Protocol, Union
 
+BuiltinDataTypes = Union[bytes, float, int, str]
 
-@dataclass
-class ParsedDataItem:
-    """Representation of a parsed parameter
 
-    Parameters
-    ----------
-    name : str
-        Parameter name
-    unit : str
-        Parameter units
-    raw_value : any
-        Raw representation of the parsed value. May be lots of different types but most often an integer
-    derived_value : float or str
-        May be a calibrated value or an enum lookup
-    short_description : str
-        Parameter short description
-    long_description : str
-        Parameter long description
+class _Parameter:
+    """Mixin class for storing access to the raw value of a parsed data item.
+
+    The raw value is the closest representation of the data item as it appears in the packet.
+    e.g. bytes for binary data, int for integer data, etc. It has not been calibrated or
+    adjusted in any way and is an easy way for user's to debug the transformations that
+    happened after the fact.
+
+    Notes
+    -----
+    We need to override the __new__ method to store the raw value of the data item
+    on immutable built-in types. So this is just a way of allowing us to inject our
+    own attribute into the built-in types.
     """
-    name: str
-    raw_value: Union[bytes, float, int, str]
-    unit: Optional[str] = None
-    derived_value: Optional[Union[float, str]] = None
-    short_description: Optional[str] = None
-    long_description: Optional[str] = None
+    def __new__(cls, value: BuiltinDataTypes, raw_value: BuiltinDataTypes = None) -> BuiltinDataTypes:
+        obj = super().__new__(cls, value)
+        # Default to the same value as the parsed value if it isn't provided
+        obj.raw_value = raw_value or value
+        return obj
+
+
+class BinaryParameter(_Parameter, bytes):
+    """A class to represent a binary data item."""
+
+
+class BoolParameter(_Parameter, int):
+    """A class to represent a parsed boolean data item."""
+    # A bool is a subclass of int, so all we are really doing here
+    # is making a nice representation using the bool type because
+    # bool can't be subclassed directly.
+    def __repr__(self) -> str:
+        return bool.__repr__(bool(self))
+
+
+class FloatParameter(_Parameter, float):
+    """A class to represent a float data item."""
+
+
+class IntParameter(_Parameter, int):
+    """A class to represent a integer data item."""
+
+
+class StrParameter(_Parameter, str):
+    """A class to represent a string data item."""
+
+
+ParameterDataTypes = Union[BinaryParameter, BoolParameter, FloatParameter, IntParameter, StrParameter]
 
 
 class RawPacketData(bytes):
diff --git a/space_packet_parser/parameters.py b/space_packet_parser/parameters.py
index 143262f..a93395b 100644
--- a/space_packet_parser/parameters.py
+++ b/space_packet_parser/parameters.py
@@ -252,19 +252,17 @@ def parse_value(self, packet: packets.CCSDSPacket, **kwargs):
 
         Returns
         -------
-        parsed_value : int
-            Raw encoded value
-        derived_value : str
+        derived_value : StrParameter
             Resulting enum label associated with the (usually integer-)encoded data value.
         """
-        raw, _ = super().parse_value(packet, **kwargs)
+        raw = super().parse_value(packet, **kwargs)
         # Note: The enum lookup only operates on raw values. This is specified in 4.3.2.4.3.6 of the XTCE spec "
         # CCSDS 660.1-G-2
         try:
             label = self.enumeration[raw]
         except KeyError as exc:
             raise ValueError(f"Failed to find raw value {raw} in enum lookup list {self.enumeration}.") from exc
-        return raw, label
+        return packets.StrParameter(label, raw)
 
 
 class BinaryParameterType(ParameterType):
@@ -312,15 +310,15 @@ def parse_value(self, packet: packets.CCSDSPacket, **kwargs):
 
         Returns
         -------
-        parsed_value : int
-            Raw encoded value
-        derived_value : str
+        derived_value : BoolParameter
             Resulting boolean representation of the encoded raw value
         """
-        raw, _ = super().parse_value(packet, **kwargs)
-        # Note: This behaves very strangely for String and Binary data encodings.
+        # NOTE: There is an intermediate value here that we are not representing in the final object return
+        #       raw data -> parsed/processed (calibrated) value -> boolean
+        parsed_value = super().parse_value(packet, **kwargs)
+        # NOTE: This behaves very strangely for String and Binary data encodings.
         # Don't use those for Boolean parameters. The behavior isn't specified well in XTCE.
-        return raw, bool(raw)
+        return packets.BoolParameter(bool(parsed_value), parsed_value.raw_value)
 
 
 class TimeParameterType(ParameterType, metaclass=ABCMeta):
@@ -532,16 +530,7 @@ class Parameter(packets.Parseable):
     def parse(self, packet: packets.CCSDSPacket, **parse_value_kwargs) -> None:
         """Parse this parameter from the packet data.
 
-        Create a ``ParsedDataItem`` and add it to the packet dictionary.
+        Parse the parameter and add it to the packet dictionary.
         """
-        parsed_value, derived_value = self.parameter_type.parse_value(
+        packet[self.name] = self.parameter_type.parse_value(
             packet, **parse_value_kwargs)
-
-        packet[self.name] = packets.ParsedDataItem(
-            name=self.name,
-            unit=self.parameter_type.unit,
-            raw_value=parsed_value,
-            derived_value=derived_value,
-            short_description=self.short_description,
-            long_description=self.long_description
-        )


Test patch:
diff --git a/tests/integration/test_xtce_based_parsing/test_jpss_parsing.py b/tests/integration/test_xtce_based_parsing/test_jpss_parsing.py
index b889d68..21d399d 100644
--- a/tests/integration/test_xtce_based_parsing/test_jpss_parsing.py
+++ b/tests/integration/test_xtce_based_parsing/test_jpss_parsing.py
@@ -9,6 +9,9 @@ def test_jpss_xtce_packet_parsing(jpss_test_data_dir):
     jpss_xtce = jpss_test_data_dir / 'jpss1_geolocation_xtce_v1.xml'
     jpss_definition = definitions.XtcePacketDefinition(xtce_document=jpss_xtce)
     assert isinstance(jpss_definition, definitions.XtcePacketDefinition)
+    assert jpss_definition.named_parameters['USEC'].short_description == "Secondary Header Fine Time (microsecond)"
+    assert jpss_definition.named_parameters['USEC'].long_description == "CCSDS Packet 2nd Header Fine Time in microseconds."
+            
 
     jpss_packet_file = jpss_test_data_dir / 'J01_G011_LZ_2021-04-09T00-00-00Z_V01.DAT1'
 
@@ -20,7 +23,5 @@ def test_jpss_xtce_packet_parsing(jpss_test_data_dir):
             assert isinstance(jpss_packet, packets.CCSDSPacket)
             assert jpss_packet.header['PKT_APID'].raw_value == 11
             assert jpss_packet.header['VERSION'].raw_value == 0
-            assert jpss_packet['USEC'].short_description == "Secondary Header Fine Time (microsecond)"
-            assert jpss_packet['USEC'].long_description == "CCSDS Packet 2nd Header Fine Time in microseconds."
             n_packets += 1
         assert n_packets == 7200
diff --git a/tests/unit/test_packets.py b/tests/unit/test_packets.py
index ba31869..b418b16 100644
--- a/tests/unit/test_packets.py
+++ b/tests/unit/test_packets.py
@@ -49,23 +49,3 @@ def test_ccsds_packet():
 
     with pytest.raises(KeyError):
         packet[10]
-
-
-@pytest.mark.parametrize(
-    ('name', 'raw_value', 'unit', 'derived_value', 'short_description', 'long_description', 'valid'),
-    [
-        ('TEST', 0, 'smoots', 10, "short", "long", True),
-        ('TEST', 10, None, None, None, None, True),
-        (None, 10, 'foo', 10, None, None, False),
-        ('TEST', None, None, None, None, None, False)
-    ]
-)
-def test_parsed_data_item(name, raw_value, unit, derived_value, short_description, long_description, valid):
-    """Test ParsedDataItem"""
-    pdi = packets.ParsedDataItem(name, raw_value, unit, derived_value, short_description, long_description)
-    assert pdi.name == name
-    assert pdi.raw_value == raw_value
-    assert pdi.unit == unit
-    assert pdi.derived_value == derived_value
-    assert pdi.short_description == short_description
-    assert pdi.long_description == long_description
diff --git a/tests/unit/test_xtcedef.py b/tests/unit/test_xtcedef.py
index cd725d4..6b0d8bf 100644
--- a/tests/unit/test_xtcedef.py
+++ b/tests/unit/test_xtcedef.py
@@ -122,82 +122,82 @@ def ignored(self, x):
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="==" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 678)}, None, True),
+         {'MSN__PARAM': packets.FloatParameter(678, 3)}, None, True),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="eq" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 668)}, None, False),
+         {'MSN__PARAM': packets.FloatParameter(668, 3)}, None, False),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="!=" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 678)}, None, False),
+         {'MSN__PARAM': packets.FloatParameter(678, 3)}, None, False),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="neq" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 658)}, None, True),
+         {'MSN__PARAM': packets.FloatParameter(658, 3)}, None, True),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="&lt;" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 679)}, None, False),
+         {'MSN__PARAM': packets.FloatParameter(679, 3)}, None, False),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="lt" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 670)}, None, True),
+         {'MSN__PARAM': packets.FloatParameter(670, 3)}, None, True),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="&gt;" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 678)}, None, False),
+         {'MSN__PARAM': packets.FloatParameter(678, 3)}, None, False),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="gt" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 679)}, None, True),
+         {'MSN__PARAM': packets.FloatParameter(679, 3)}, None, True),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="&lt;=" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 660)}, None, True),
+         {'MSN__PARAM': packets.FloatParameter(660, 3)}, None, True),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="leq" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 690)}, None, False),
+         {'MSN__PARAM': packets.FloatParameter(690, 3)}, None, False),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="&gt;=" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 660)}, None, False),
+         {'MSN__PARAM': packets.FloatParameter(660, 3)}, None, False),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="geq" value="678" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 690)}, None, True),
+         {'MSN__PARAM': packets.FloatParameter(690, 3)}, None, True),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="==" value="678" parameterRef="MSN__PARAM" useCalibratedValue="false"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 678, None, 690)}, None, True),
+         {'MSN__PARAM': packets.FloatParameter(690, 678)}, None, True),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="==" value="678" parameterRef="MSN__PARAM" useCalibratedValue="true"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 3, None, 678)}, None, True),
+         {'MSN__PARAM': packets.FloatParameter(678, 3)}, None, True),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="==" value="foostring" parameterRef="MSN__PARAM" useCalibratedValue="false"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 'foostring', None, 'calibratedfoostring')}, None, True),
+         {'MSN__PARAM': packets.StrParameter('calibratedfoostring', 'foostring')}, None, True),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="==" value="3.14" parameterRef="MSN__PARAM"/>
 """,
-         {'MSN__PARAM': packets.ParsedDataItem('MSN__PARAM', 1, None, 3.14)}, None, True),
+         {'MSN__PARAM': packets.FloatParameter(3.14, 1)}, None, True),
         ("""
 <xtce:Comparison xmlns:xtce="http://www.omg.org/space/xtce" 
     comparisonOperator="==" value="3.0" parameterRef="REFERENCE_TO_OWN_RAW_VAL"/>
@@ -242,8 +242,8 @@ def test_comparison(xml_string, test_parsed_data, current_parsed_value, expected
     <xtce:ParameterInstanceRef parameterRef="P2"/>
 </xtce:Condition>
 """,
-         {'P1': packets.ParsedDataItem('P1', 4, None, 700),
-          'P2': packets.ParsedDataItem('P2', 3, None, 678)}, True),
+         {'P1': packets.IntParameter(700, 4),
+          'P2': packets.IntParameter(678, 3)}, True),
         ("""
 <xtce:Condition xmlns:xtce="http://www.omg.org/space/xtce">
     <xtce:ParameterInstanceRef parameterRef="P1"/>
@@ -251,7 +251,7 @@ def test_comparison(xml_string, test_parsed_data, current_parsed_value, expected
     <xtce:Value>4</xtce:Value>
 </xtce:Condition>
 """,
-         {'P1': packets.ParsedDataItem('P1', 4, None, 700)}, True),
+         {'P1': packets.IntParameter(700, 4)}, True),
         ("""
 <xtce:Condition xmlns:xtce="http://www.omg.org/space/xtce">
     <xtce:ParameterInstanceRef parameterRef="P1"/>
@@ -259,8 +259,8 @@ def test_comparison(xml_string, test_parsed_data, current_parsed_value, expected
     <xtce:ParameterInstanceRef parameterRef="P2"/>
 </xtce:Condition>
 """,
-         {'P1': packets.ParsedDataItem('P1', 4, None, 700),
-          'P2': packets.ParsedDataItem('P2', 3, None, 678)}, False),
+         {'P1': packets.IntParameter(700, 4),
+          'P2': packets.IntParameter(678, 3)}, False),
         ("""
 <xtce:Condition xmlns:xtce="http://www.omg.org/space/xtce">
     <xtce:ParameterInstanceRef parameterRef="P1" useCalibratedValue="false"/>
@@ -268,8 +268,8 @@ def test_comparison(xml_string, test_parsed_data, current_parsed_value, expected
     <xtce:ParameterInstanceRef parameterRef="P2" useCalibratedValue="false"/>
 </xtce:Condition>
 """,
-         {'P1': packets.ParsedDataItem('P1', 'abcd', None),
-          'P2': packets.ParsedDataItem('P2', 'abcd', None)}, True),
+         {'P1': packets.StrParameter('abcd'),
+          'P2': packets.StrParameter('abcd')}, True),
         ("""
 <xtce:Condition xmlns:xtce="http://www.omg.org/space/xtce">
     <xtce:ParameterInstanceRef parameterRef="P1"/>
@@ -277,8 +277,8 @@ def test_comparison(xml_string, test_parsed_data, current_parsed_value, expected
     <xtce:ParameterInstanceRef parameterRef="P2"/>
 </xtce:Condition>
 """,
-         {'P1': packets.ParsedDataItem('P1', 1, None, 3.14),
-          'P2': packets.ParsedDataItem('P2', 180, None, 3.14)}, True),
+         {'P1': packets.FloatParameter(3.14, 1),
+          'P2': packets.FloatParameter(3.14, 180)}, True),
     ]
 )
 def test_condition(xml_string, test_parsed_data, expected_condition_result):
@@ -314,10 +314,10 @@ def test_condition(xml_string, test_parsed_data, expected_condition_result):
     </xtce:ORedConditions>
 </xtce:BooleanExpression>
 """,
-         {'P': packets.ParsedDataItem('P', 4, None, 0),
-          'P2': packets.ParsedDataItem('P2', 4, None, 700),
-          'P3': packets.ParsedDataItem('P3', 4, None, 701),
-          'P4': packets.ParsedDataItem('P4', 4, None, 98)}, True),
+         {'P': packets.IntParameter(0, 4),
+          'P2': packets.IntParameter(700, 4),
+          'P3': packets.IntParameter(701, 4),
+          'P4': packets.IntParameter(98, 4)}, True),
         ("""
 <xtce:BooleanExpression xmlns:xtce="http://www.omg.org/space/xtce">
     <xtce:ANDedConditions>
@@ -346,12 +346,12 @@ def test_condition(xml_string, test_parsed_data, expected_condition_result):
     </xtce:ANDedConditions>
 </xtce:BooleanExpression>
 """,
-         {'P': packets.ParsedDataItem('P', 4, None, 100),
-          'P0': packets.ParsedDataItem('P0', 4, None, 678),
-          'P1': packets.ParsedDataItem('P1', 4, None, 500),
-          'P2': packets.ParsedDataItem('P2', 4, None, 700),
-          'P3': packets.ParsedDataItem('P3', 4, None, 701),
-          'P4': packets.ParsedDataItem('P4', 4, None, 99)}, True),
+         {'P': packets.IntParameter(100, 4),
+          'P0': packets.IntParameter(678, 4),
+          'P1': packets.IntParameter(500, 4),
+          'P2': packets.IntParameter(700, 4),
+          'P3': packets.IntParameter(701, 4),
+          'P4': packets.IntParameter(99, 4)}, True),
     ]
 )
 def test_boolean_expression(xml_string, test_parsed_data, expected_result):
@@ -373,13 +373,13 @@ def test_boolean_expression(xml_string, test_parsed_data, expected_result):
     <xtce:Comparison useCalibratedValue="false" parameterRef="P1" value="1"/>
 </xtce:DiscreteLookup>
 """,
-         {'P1': packets.ParsedDataItem('P1', 1, None, 678)}, 10),
+         {'P1': packets.IntParameter(678, 1)}, 10),
         ("""
 <xtce:DiscreteLookup value="10" xmlns:xtce="http://www.omg.org/space/xtce">
     <xtce:Comparison useCalibratedValue="false" parameterRef="P1" value="1"/>
 </xtce:DiscreteLookup>
 """,
-         {'P1': packets.ParsedDataItem('P1', 0, None, 678)}, None),
+         {'P1': packets.IntParameter(678, 0)}, None),
         ("""
 <xtce:DiscreteLookup value="11" xmlns:xtce="http://www.omg.org/space/xtce">
     <xtce:ComparisonList>
@@ -389,8 +389,8 @@ def test_boolean_expression(xml_string, test_parsed_data, expected_result):
 </xtce:DiscreteLookup>
 """,
          {
-             'MSN__PARAM1': packets.ParsedDataItem('MSN__PARAM1', 3, None, 680),
-             'MSN__PARAM2': packets.ParsedDataItem('MSN__PARAM2', 3, None, 3000)
+             'MSN__PARAM1': packets.IntParameter(680, 3),
+             'MSN__PARAM2': packets.IntParameter(3000, 3),
          }, 11),
     ]
 )
@@ -536,7 +536,7 @@ def test_context_calibrator(xml_string, expectation):
                 calibrators.PolynomialCoefficient(coefficient=0.5, exponent=0),
                 calibrators.PolynomialCoefficient(coefficient=1.5, exponent=1)
             ])),
-         {"EXI__FPGAT": packets.ParsedDataItem("EXI__FPGAT", 600, derived_value=700)},
+         {"EXI__FPGAT": packets.IntParameter(700, 600)},
          42, True, 63.5),
         (calibrators.ContextCalibrator(
             match_criteria=[
@@ -547,7 +547,7 @@ def test_context_calibrator(xml_string, expectation):
                 calibrators.PolynomialCoefficient(coefficient=0.5, exponent=0),
                 calibrators.PolynomialCoefficient(coefficient=1.5, exponent=1),
             ])),
-         {"EXI__FPGAT": packets.ParsedDataItem("EXI__FPGAT", 3.14, derived_value=700.0)},
+         {"EXI__FPGAT": packets.FloatParameter(700.0, 3.14)},
          42, True, 63.5),
         (calibrators.ContextCalibrator(
             match_criteria=[
@@ -567,8 +567,8 @@ def test_context_calibrator(xml_string, expectation):
                 calibrators.PolynomialCoefficient(coefficient=0.5, exponent=0),
                 calibrators.PolynomialCoefficient(coefficient=1.5, exponent=1),
             ])),
-         {"P1": packets.ParsedDataItem("P1", 100.0, derived_value=700.0),
-          "P2": packets.ParsedDataItem("P2", 99, derived_value=700.0)},
+         {"P1": packets.FloatParameter(700.0, 100.0),
+          "P2": packets.FloatParameter(700.0, 99)},
          42, True, 63.5),
         (calibrators.ContextCalibrator(
             match_criteria=[
@@ -589,8 +589,8 @@ def test_context_calibrator(xml_string, expectation):
                 calibrators.PolynomialCoefficient(coefficient=0.5, exponent=0),
                 calibrators.PolynomialCoefficient(coefficient=1.5, exponent=1),
             ])),
-         {"P1": packets.ParsedDataItem("P1", 100.0, derived_value=700.0),
-          "P2": packets.ParsedDataItem("P2", 99, derived_value=700.0)},
+         {"P1": packets.FloatParameter(700.0, 100.0),
+          "P2": packets.FloatParameter(700.0, 99)},
          42, False, 63.5),
     ]
 )
@@ -1313,14 +1313,14 @@ def test_string_parameter_type(xml_string: str, expectation):
 def test_string_parameter_parsing(parameter_type, raw_data, current_pos, expected_raw, expected_derived):
     """Test parsing a string parameter"""
     # pre parsed data to reference for lookups
-    packet = packets.CCSDSPacket(raw_data=raw_data, **{'P1': packets.ParsedDataItem('P1', 7, None, 7.55),
-                                                       'P2': packets.ParsedDataItem('P2', 99, None, 100),
-                                                       'STR_LEN': packets.ParsedDataItem('STR_LEN', 8, None)})
+    packet = packets.CCSDSPacket(raw_data=raw_data, **{'P1': packets.FloatParameter(7.55, 7),
+                                                       'P2': packets.IntParameter(100, 99),
+                                                       'STR_LEN': packets.IntParameter(8)})
     # Artificially set the current position of the packet data read so far
     packet.raw_data.pos = current_pos
-    raw, derived = parameter_type.parse_value(packet)
-    assert raw == expected_raw
-    assert derived == expected_derived
+    value = parameter_type.parse_value(packet)
+    assert value == expected_derived
+    assert value.raw_value == expected_raw
 
 
 @pytest.mark.parametrize(
@@ -1504,13 +1504,10 @@ def test_integer_parameter_type(xml_string: str, expectation):
 def test_integer_parameter_parsing(parameter_type, raw_data, current_pos, expected):
     """Testing parsing an integer parameters"""
     # pre parsed data to reference for lookups
-    packet = packets.CCSDSPacket(raw_data=raw_data, PKT_APID=packets.ParsedDataItem('PKT_APID', 1101))
+    packet = packets.CCSDSPacket(raw_data=raw_data, PKT_APID=packets.IntParameter(1101))
     packet.raw_data.pos = current_pos
-    raw, derived = parameter_type.parse_value(packet)
-    if derived:
-        assert derived == expected
-    else:
-        assert raw == expected
+    value = parameter_type.parse_value(packet)
+    assert value == expected
 
 
 @pytest.mark.parametrize(
@@ -1702,13 +1699,10 @@ def test_float_parameter_type(xml_string: str, expectation):
 def test_float_parameter_parsing(parameter_type, raw_data, expected):
     """Test parsing float parameters"""
     # pre parsed data to reference for lookups
-    packet = packets.CCSDSPacket(raw_data=raw_data, **{'PKT_APID': packets.ParsedDataItem('PKT_APID', 1101)})
-    raw, derived = parameter_type.parse_value(packet)
+    packet = packets.CCSDSPacket(raw_data=raw_data, **{'PKT_APID': packets.IntParameter(1101)})
+    value = parameter_type.parse_value(packet)
     # NOTE: These results are compared with a relative tolerance due to the imprecise storage of floats
-    if derived:
-        assert derived == pytest.approx(expected, rel=1E-7)
-    else:
-        assert raw == pytest.approx(expected, rel=1E-7)
+    assert value == pytest.approx(expected, rel=1E-7)
 
 
 @pytest.mark.parametrize(
@@ -1764,11 +1758,8 @@ def test_enumerated_parameter_type(xml_string: str, expectation):
 def test_enumerated_parameter_parsing(parameter_type, raw_data, expected):
     """"Test parsing enumerated parameters"""
     packet = packets.CCSDSPacket(raw_data=raw_data)
-    raw, derived = parameter_type.parse_value(packet)
-    if derived:
-        assert derived == expected
-    else:
-        assert raw == expected
+    value = parameter_type.parse_value(packet)
+    assert value == expected
 
 
 @pytest.mark.parametrize(
@@ -1880,10 +1871,10 @@ def test_binary_parameter_parsing(parameter_type, raw_data, expected):
     """Test parsing binary parameters"""
     # pre parsed data to reference for lookups
     packet = packets.CCSDSPacket(raw_data=raw_data, **{
-        'P1': packets.ParsedDataItem('P1', 1, None, 7.4),
-        'BIN_LEN': packets.ParsedDataItem('BIN_LEN', 2, None)})
-    raw, _ = parameter_type.parse_value(packet)
-    assert raw == expected
+        'P1': packets.FloatParameter(7.4, 1),
+        'BIN_LEN': packets.IntParameter(2)})
+    value = parameter_type.parse_value(packet)
+    assert value == expected
 
 
 @pytest.mark.parametrize(
@@ -1990,9 +1981,9 @@ def test_boolean_parameter_parsing(parameter_type, raw_data, current_pos, expect
     """Test parsing boolean parameters"""
     packet = packets.CCSDSPacket(raw_data=raw_data)
     packet.raw_data.pos = current_pos
-    raw, derived = parameter_type.parse_value(packet)
-    assert raw == expected_raw
-    assert derived == expected_derived
+    value = parameter_type.parse_value(packet)
+    assert value.raw_value == expected_raw
+    assert value == expected_derived
 
 
 @pytest.mark.parametrize(
@@ -2142,10 +2133,10 @@ def test_absolute_time_parameter_type(xml_string, expectation):
 def test_absolute_time_parameter_parsing(parameter_type, raw_data, current_pos, expected_raw, expected_derived):
     packet = packets.CCSDSPacket(raw_data=raw_data)
     packet.raw_data.pos = current_pos
-    raw, derived = parameter_type.parse_value(packet)
-    assert raw == pytest.approx(expected_raw, rel=1E-6)
+    value = parameter_type.parse_value(packet)
+    assert value.raw_value == pytest.approx(expected_raw, rel=1E-6)
     # NOTE: derived values are rounded for comparison due to imprecise storage of floats
-    assert derived == pytest.approx(expected_derived, rel=1E-6)
+    assert value == pytest.approx(expected_derived, rel=1E-6)
 
 
 # ---------------