.. testsetup:: data-components import imfusion prop = imfusion.Properties({"patientId": "my_patient_id"}) image_info_data_comp = imfusion.data.components.create( "ImageInfoDataComponent", prop ) Attaching Metadata to Data ========================== The :class:`~imfusion.data.components.DataComponent` class allows attaching arbitrary metadata to any instance of :class:`~imfusion.data.Data`. The example below demonstrates this by attaching an :class:`~imfusion.data.components.ImageInfoDataComponent`, which can hold information such as patient ID, patient name, etc., to :class:`~imfusion.data.Data` , e.g. a :class:`~imfusion.data.SharedImageSet`. >>> import imfusion >>> sis = imfusion.SharedImageSet() >>> sis.components.add(imfusion.data.components.ImageInfoDataComponent()) ImageInfoDataComponent( patient_id="", patient_name="", ...) >>> sis.components.image_info.patient_id = "my_patient_id" There is a variety of built-in :class:`~imfusion.data.components.DataComponent` available in the ImFusion SDK. The full list of :class:`~imfusion.data.components.DataComponent` can be obtained with :func:`imfusion.data.components.list_available`. .. note:: The :func:`imfusion.data.components.list_available` function returns all components that are implemented in the ImFusion C++ SDK, including those that are not yet available in the Python bindings and hence do not have a corresponding specialized class in the :mod:`imfusion.data.components` Python module. Regardless of whether Python bindings exist, :func:`imfusion.data.components.create` enables creating every :class:`~imfusion.data.components.DataComponent` in Python: >>> prop = imfusion.Properties({"patientId": "my_patient_id"}) >>> image_info_data_comp = imfusion.data.components.create("ImageInfoDataComponent", prop) >>> image_info_data_comp ImageInfoDataComponent( patient_id="my_patient_id", patient_name="", ...) As with any :class:`~imfusion.data.components.DataComponent`, the actual data fields of :class:`~imfusion.data.components.ImageInfoDataComponent` are accessible through Python attributes: .. doctest:: data-components >>> image_info_data_comp.patient_id 'my_patient_id' .. note:: There is a difference in naming conventions between the :class:`~imfusion.data.components.DataComponent`'s Python attributes (e.g., `patient_id`) and the corresponding :class:`~imfusion.Properties` keys (e.g., `patientId`). The former are listed in the documentation of the specific class, e.g., :class:`~imfusion.data.components.ImageInfoDataComponent`. To view the keys used in :class:`~imfusion.Properties`, you can instantiate the respective data component with its default constructor and inspect the parameters of its :class:`~imfusion.Properties`, as in the example below: >>> image_info_data_comp = imfusion.data.components.create("ImageInfoDataComponent") >>> prop: imfusion.Properties = image_info_data_comp.configuration() >>> prop.params() ['patientId', 'patientName', ...] :class:`~imfusion.Properties` may also be used to modify the data component after it has been created: >>> image_info_data_comp = imfusion.data.components.create("ImageInfoDataComponent") >>> prop = imfusion.Properties({"patientId": "my_patient_id"}) >>> image_info_data_comp.configure(prop) Provided the data component is available in our Python bindings, you can of course also instantiate it directly: >>> image_info_data_comp = imfusion.data.components.ImageInfoDataComponent() Custom Python Data Components ----------------------------- You can also define custom data components directly in Python using the :func:`~imfusion.data.components.register` decorator together with :func:`~imfusion.data.components.field`: >>> @imfusion.data.components.register(name="my_component") ... class MyComponent: ... custom_info: str = imfusion.data.components.field() ... ... def __init__(self, custom_info: str = ""): ... self.custom_info = custom_info The registered ``name`` serves as both the factory ID (see :func:`~imfusion.data.components.create`) and the named attribute on :class:`~imfusion.data.components.DataComponentList`: >>> sis = imfusion.SharedImageSet() >>> sis.components.add(MyComponent(custom_info="my_custom_info")) MyComponent(custom_info='my_custom_info') >>> assert sis.components.my_component.custom_info == "my_custom_info" :func:`~imfusion.data.components.register` automatically injects ``configure``, ``configuration``, ``id``, and ``name`` into the class, so the component fully conforms to the ``imfusion`` data component interface. If ``__eq__`` and ``__repr__`` are not defined, field-based implementations are generated automatically. Generated equality compares only attributes declared with :func:`~imfusion.data.components.field`. Other instance attributes are ignored. Define ``__eq__`` explicitly if those attributes must participate in equality. .. warning:: Defining ``configure``, ``configuration``, ``id``, or ``name`` in the class body raises a ``TypeError`` at registration time, since these names are reserved for the injected implementations. For static type-checker support, the class may optionally inherit from :class:`imfusion.data.components.typing.PyDataComponent`, which declares the injected methods. This is not required and has no effect at runtime. Advanced usage: custom serialization ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :func:`~imfusion.data.components.field` accepts optional ``serializer`` and ``deserializer`` callbacks for types that :class:`~imfusion.Properties` does not natively support, such as ``dict``. The ``serializer`` must return a ``str``, and ``serializer`` and ``deserializer`` must always be provided together: >>> import json >>> @imfusion.data.components.register(name="my_component_advanced") ... class MyComponentAdvanced(): ... metadata: dict = imfusion.data.components.field( ... serializer=json.dumps, ... deserializer=json.loads, ... ) ... ... def __init__(self, metadata: dict | None = None): ... self.metadata = metadata or {} >>> sis = imfusion.SharedImageSet() >>> comp = MyComponentAdvanced(metadata={"label": "tumor", "confidence": 0.95}) >>> sis.components.add(comp) MyComponentAdvanced(metadata={'label': 'tumor', 'confidence': 0.95}) >>> assert sis.components.my_component_advanced.metadata["label"] == "tumor" Field values are always stored as native Python objects on the component instance, regardless of whether a ``serializer`` is provided. The ``serializer`` is called on assignment to validate that it returns a ``str`` (an early check), and again by ``configuration()`` to produce the serialized form for :class:`~imfusion.Properties`. The ``deserializer`` is called by ``configure()`` to restore the Python value when the component is loaded, for example from an ``.imf`` file.