Source code for medkit.core.attribute_container
from __future__ import annotations
__all__ = ["AttributeContainer"]
import typing
from typing import Iterator
from medkit.core.attribute import Attribute
from medkit.core.store import GlobalStore, Store
[docs]
class AttributeContainer:
"""Manage a list of attributes attached to another data structure.
For example, it may be a document or an annotation.
This behaves more or less like a list: calling `len()` and iterating are
supported. Additional filtering is available through the `get()` method.
The attributes will be stored in a :class:`~medkit.core.Store`, which can
rely on a simple dict or something more complicated like a database.
This global store may be initialized using :class:~medkit.core.GlobalStore.
Otherwise, a default one (i.e. dict store) is used.
"""
def __init__(self, owner_id: str):
self._store: Store = GlobalStore.get_store()
self._owner_id = owner_id
self._attr_ids: list[str] = []
self._attr_ids_by_label: dict[str, list[str]] = {}
def __len__(self) -> int:
"""Add support for calling `len()`"""
return len(self._attr_ids)
def __iter__(self) -> Iterator[Attribute]:
"""Add support for iterating over an `AttributeContainer` (will yield each
attribute)
"""
return iter(self.get_by_id(uid) for uid in self._attr_ids)
def __getitem__(self, key: int | slice) -> Attribute | list[Attribute]:
"""Add support for subscript access"""
if isinstance(key, slice):
return [self.get_by_id(uid) for uid in self._attr_ids[key]]
return self.get_by_id(self._attr_ids[key])
[docs]
def get(self, *, label: str | None = None) -> list[Attribute]:
"""Return a list of the attributes of the annotation, optionally filtering
by label.
Parameters
----------
label : str, optional
Label to use to filter attributes.
Returns
-------
list of Attribute
The list of all attributes of the annotation, filtered by label if specified.
"""
if label:
return [self.get_by_id(uid) for uid in self._attr_ids_by_label.get(label, [])]
return list(iter(self))
[docs]
def add(self, attr: Attribute):
"""Attach an attribute to the annotation.
Parameters
----------
attr : Attribute
Attribute to add.
Raises
------
ValueError
If the attribute is already attached to the annotation (based on
`attr.uid`).
"""
uid = attr.uid
if uid in self._attr_ids:
msg = f"Attribute with uid {uid} already attached to annotation"
raise ValueError(msg)
self._attr_ids.append(uid)
self._store.store_data_item(data_item=attr, parent_id=self._owner_id)
# update label index
label = attr.label
if label not in self._attr_ids_by_label:
self._attr_ids_by_label[label] = []
self._attr_ids_by_label[label].append(uid)
[docs]
def get_by_id(self, uid: str) -> Attribute:
"""Return the attribute corresponding to a specific identifier.
Parameters
----------
uid : str
Identifier of the attribute to return.
Returns
-------
Attribute
The attribute corresponding to the identifier
"""
attr = self._store.get_data_item(uid)
if attr is None:
msg = f"No known attribute with uid '{uid}'"
raise ValueError(msg)
return typing.cast(Attribute, attr)
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
return False
return self.get() == other.get()
def __repr__(self) -> str:
attrs = self.get()
return f"{self.__class__.__name__}(ann_id={self._owner_id!r}, attrs={attrs!r})"