OntologyStructure

class encord.objects.ontology_labels_impl.OntologyStructure(objects=<factory>, classifications=<factory>)

objects: List[encord.objects.ontology_object.Object]

classifications: List[encord.objects.classification.Classification]

get_child_by_hash

Returns the first child node of this ontology tree node with the matching feature node hash. If there is more than one child with the same feature node hash in the ontology tree node, then the ontology would be in an invalid state. Throws if nothing is found or if the type is not matched.

get_child_by_hash(feature_node_hash, type_=None)

Parameters:

  • feature_node_hash (str) – the feature_node_hash of the child node to search for in the ontology.

  • type – The expected type of the item. If the found child does not match the type, an error will be thrown.

Return type:

[OntologyElementT] - Returns all the child nodes of this Ontology tree node with the matching title and matching type if specified. Titles in Ontologies do not need to be unique, however, we recommend unique titles when creating Ontologies.

    def get_child_by_hash(
        self,
        feature_node_hash: str,
        type_: Optional[Type[OntologyElementT]] = None,
    ) -> OntologyElementT:
        """
        Returns the first child node of this ontology tree node with the matching feature node hash. If there is
        more than one child with the same feature node hash in the ontology tree node, then the ontology would be in
        an invalid state. Throws if nothing is found or if the type is not matched.

        Args:
            feature_node_hash: the feature_node_hash of the child node to search for in the ontology.
            type_: The expected type of the item. If the found child does not match the type, an error will be thrown.
        """
        for object_ in self.objects:
            if object_.feature_node_hash == feature_node_hash:
                return checked_cast(object_, type_)

            found_item = _get_element_by_hash(feature_node_hash, object_.attributes)
            if found_item is not None:
                return checked_cast(found_item, type_)

        for classification in self.classifications:
            if classification.feature_node_hash == feature_node_hash:
                return checked_cast(classification, type_)
            found_item = _get_element_by_hash(feature_node_hash, classification.attributes)
            if found_item is not None:
                return checked_cast(found_item, type_)

        raise OntologyError(f"Item not found: can't find an item with a hash {feature_node_hash} in the ontology.")

get_child_by_title

Returns a child node of this ontology tree node with the matching title and matching type if specified. If more than one child in this Object have the same title, then an error will be thrown. If no item is found, an error will be thrown as well.

get_child_by_title(title, type_=None)

Parameters:

  • title (str) – The exact title of the child node to search for in the ontology.

  • type – The expected type of the child node. Only a node that matches this type will be returned.

Return type:

[OntologyElementT] - Returns all the child nodes of this Ontology tree node with the matching title and matching type if specified. Titles in Ontologies do not need to be unique, however, we recommend unique titles when creating Ontologies.

    def get_child_by_title(
        self,
        title: str,
        type_: Optional[Type[OntologyElementT]] = None,
    ) -> OntologyElementT:
        """
        Returns a child node of this ontology tree node with the matching title and matching type if specified. If more
        than one child in this Object have the same title, then an error will be thrown. If no item is found, an error
        will be thrown as well.

        Args:
            title: The exact title of the child node to search for in the ontology.
            type_: The expected type of the child node. Only a node that matches this type will be returned.
        """
        found_items = self.get_children_by_title(title, type_)
        _assert_singular_result_list(found_items, title, type_)
        return found_items[0]

get_children_by_title

Returns all the child nodes of this ontology tree node with the matching title and matching type if specified. Title in ontologies do not need to be unique, however, we recommend unique titles when creating ontologies.

get_children_by_title(title, type_=None)

Parameters:

  • title (str) – The exact title of the child node to search for in the ontology.

  • type – The expected type of the item. Only nodes that match this type will be returned.

Return type:

List[~OntologyElementT] - Returns all the child nodes of this Ontology tree node with the matching title and matching type if specified. Titles in Ontologies do not need to be unique, however, we recommend unique titles when creating Ontologies.

    def get_children_by_title(
        self,
        title: str,
        type_: Optional[Type[OntologyElementT]] = None,
    ) -> List[OntologyElementT]:
        """
        Returns all the child nodes of this ontology tree node with the matching title and matching type if specified.
        Title in ontologies do not need to be unique, however, we recommend unique titles when creating ontologies.

        Args:
            title: The exact title of the child node to search for in the ontology.
            type_: The expected type of the item. Only nodes that match this type will be returned.
        """
        ret: List[OntologyElement] = []
        for object_ in self.objects:
            if object_.title == title and does_type_match(object_, type_):
                ret.append(object_)

            if type_ is None or issubclass(type_, OntologyNestedElement):
                found_items = object_.get_children_by_title(title, type_=type_)
                ret.extend(found_items)

        for classification in self.classifications:
            if classification.title == title and does_type_match(classification, type_):
                ret.append(classification)

            if type_ is None or issubclass(type_, OntologyNestedElement):
                found_items = classification.get_children_by_title(title, type_=type_)
                ret.extend(found_items)

        # type checks in the code above guarantee the type conformity of the return value
        # but there is no obvious way to tell that to mypy, so just casting here for now
        return cast(List[OntologyElementT], ret)

from_dict

classmethod from_dict(d)

Parameters:

d (Dict[str, Any]) – a JSON blob of an “ontology structure” (for example, from the Encord web app)

Return type:

[OntologyStructure]

Raises:

KeyError – If the dict is missing a required field.

    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> OntologyStructure:
        """
        Args:
            d: a JSON blob of an "ontology structure" (e.g. from Encord web app)

        Raises:
            KeyError: If the dict is missing a required field.
        """
        objects_ret = [Object.from_dict(object_dict) for object_dict in d["objects"]]
        classifications_ret = [
            Classification.from_dict(classification_dict) for classification_dict in d["classifications"]
        ]
        return OntologyStructure(objects=objects_ret, classifications=classifications_ret)

to_dict

to_dict()

Return type:

Dict[str, List[Dict[str, Any]]]

Returns:

The dict equivalent to the Ontology.

Raises:

KeyError – If the dict is missing a required field.

    def to_dict(self) -> Dict[str, List[Dict[str, Any]]]:
        """
        Returns:
            The dict equivalent to the ontology.

        Raises:
            KeyError: If the dict is missing a required field.
        """
        ret: Dict[str, List[Dict[str, Any]]] = dict()
        ontology_objects: List[Dict[str, Any]] = list()
        ret["objects"] = ontology_objects
        for ontology_object in self.objects:
            ontology_objects.append(ontology_object.to_dict())

        ontology_classifications: List[Dict[str, Any]] = list()
        ret["classifications"] = ontology_classifications
        for ontology_classification in self.classifications:
            ontology_classifications.append(ontology_classification.to_dict())

        return ret

add_object

Adds an object class definition to the structure.

add_object(name, shape, uid=None, color=None, feature_node_hash=None)

Example:

from encord.objects.common import ChecklistAttribute, Shape
from encord.objects.ontology_structure import OntologyStructure

structure = OntologyStructure()
   
eye = structure.add_object(
   name="Eye",
   shape=Shape.POLYGON
)
nose = structure.add_object(
   name="Nose",
   shape=Shape.BOUNDING_BOX
)
nose_detail = nose.add_attribute(
   ChecklistAttribute,
   name='Checklist questions'
)

nose_detail.add_option(feature_node_hash="2bc17c88", label="A")
nose_detail.add_option(feature_node_hash="86eaa4f2", label="B ")

Parameters:

  • name (str) – The user-visible name of the object

  • shape (Shape) – The kind of object (bounding box, polygon, etc). See encord.objects.common.Shape for possible values

  • uid (Optional[int]) – Integer identifier of the object. Normally auto-generated. Omit this unless the aim is to create an exact clone of existing structure

  • color (Optional[str]) – The color of the object in the label editor. Normally auto-assigned, should be in ‘#1A2B3F’ syntax.

  • feature_node_hash (Optional[str]) – Global identifier of the object. Normally auto-generated. Omit this unless the aim is to create an exact clone of existing structure

Return type:

[Object]

Returns:

The created object class that can be further customized with attributes.

    def add_object(
        self,
        name: str,
        shape: Shape,
        uid: Optional[int] = None,
        color: Optional[str] = None,
        feature_node_hash: Optional[str] = None,
    ) -> Object:
        """
        Adds an object class definition to the structure.

        .. code::

            structure = ontology_structure.OntologyStructure()

            eye = structure.add_object(
                name="Eye",
            )
            nose = structure.add_object(
                name="Nose",
            )
            nose_detail = nose.add_attribute(
                encord.objects.common.ChecklistAttribute,
            )
            nose_detail.add_option(feature_node_hash="2bc17c88", label="Is it a cute nose?")
            nose_detail.add_option(feature_node_hash="86eaa4f2", label="Is it a wet nose? ")

        Args:
            name: the user-visible name of the object
            shape: the kind of object (bounding box, polygon, etc). See :py:class:`encord.objects.common.Shape` enum for possible values
            uid: integer identifier of the object. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing structure
            color: the color of the object in the label editor. Normally auto-assigned, should be in '#1A2B3F' syntax.
            feature_node_hash: global identifier of the object. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing structure

        Returns:
            the created object class that can be further customized with attributes.
        """
        if uid is None:
            if self.objects:
                uid = max([obj.uid for obj in self.objects]) + 1
            else:
                uid = 1
        else:
            if any([obj.uid == uid for obj in self.objects]):
                raise ValueError(f"Duplicate uid '{uid}'")

        if color is None:
            color_index = 0
            if self.objects:
                try:
                    color_index = AVAILABLE_COLORS.index(self.objects[-1].color) + 1
                    if color_index >= len(AVAILABLE_COLORS):
                        color_index = 0
                except ValueError:
                    pass
            color = AVAILABLE_COLORS[color_index]

        if feature_node_hash is None:
            feature_node_hash = str(uuid4())[:8]

        if any([obj.feature_node_hash == feature_node_hash for obj in self.objects]):
            raise ValueError(f"Duplicate feature_node_hash '{feature_node_hash}'")

        obj = Object(uid=uid, name=name, color=color, shape=shape, feature_node_hash=feature_node_hash)
        self.objects.append(obj)
        return obj

add_classification

Adds an classification definition to the ontology.

add_classification(uid=None, feature_node_hash=None)

Example

structure = ontology_structure.OntologyStructure()

cls = structure.add_classification(feature_node_hash="a39d81c0")
cat_standing = cls.add_attribute(
    encord.objects.common.RadioAttribute,
    feature_node_hash="a6136d14",
    name="Is the cat standing?",
    required=True,
)
cat_standing.add_option(feature_node_hash="a3aeb48d", label="Yes")
cat_standing.add_option(feature_node_hash="d0a4b373", label="No")

Parameters:

  • uid (Optional[int]) – Integer identifier of the object. Normally auto-generated. Omit this unless the aim is to create an exact clone of existing structure

  • feature_node_hash (Optional[str]) – Global identifier of the object. Normally auto-generated. Omit this unless the aim is to create an exact clone of existing structure

Return type:

[Classification]

Returns:

The created Classification node.

ℹ️

Note

The Classification attribute should be further specified by calling its add_attribute() method.

    def add_classification(
        self,
        uid: Optional[int] = None,
        feature_node_hash: Optional[str] = None,
    ) -> Classification:
        """
        Adds an classification definition to the ontology.

        .. code::

            structure = ontology_structure.OntologyStructure()

            cls = structure.add_classification(feature_node_hash="a39d81c0")
            cat_standing = cls.add_attribute(
                encord.objects.common.RadioAttribute,
                feature_node_hash="a6136d14",
                name="Is the cat standing?",
                required=True,
            )
            cat_standing.add_option(feature_node_hash="a3aeb48d", label="Yes")
            cat_standing.add_option(feature_node_hash="d0a4b373", label="No")

        Args:
            uid: integer identifier of the object. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing structure
            feature_node_hash: global identifier of the object. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing structure

        Returns:
            the created classification node. Note that classification attribute should be further specified by calling its `add_attribute()` method.
        """
        if uid is None:
            if self.classifications:
                uid = max([cls.uid for cls in self.classifications]) + 1
            else:
                uid = 1
        else:
            if any([cls.uid == uid for cls in self.classifications]):
                raise ValueError(f"Duplicate uid '{uid}'")

        if feature_node_hash is None:
            feature_node_hash = str(uuid4())[:8]

        if any([cls.feature_node_hash == feature_node_hash for cls in self.classifications]):
            raise ValueError(f"Duplicate feature_node_hash '{feature_node_hash}'")

        cls = Classification(uid=uid, feature_node_hash=feature_node_hash, attributes=list())
        self.classifications.append(cls)
        return cls

Object

class encord.objects.ontology_labels_impl.Object(feature_node_hash, uid, name, color, shape, attributes=<factory>)
  • uid: int
  • name: str
  • color: str
  • shape: [encord.objects.common.Shape]
  • feature_node_hash: str
  • attributes: List[encord.objects.attributes.Attribute]

property title: str

Return Type:
str

property children: Sequence[encord.objects.ontology_element.OntologyElement]

Return Type:

Sequence[OntologyElement] - Returns all the child nodes of this ontology tree node with the matching title and matching type if specified. Titles in Ontologies do not need to be unique, however, we recommend unique titles when creating Ontologies.

create_instance

Create a encord.objects.ObjectInstance to be used with a label row.

create_instance()

Return type:

[ObjectInstance]

    def create_instance(self) -> ObjectInstance:
        """Create a :class:`encord.objects.ObjectInstance` to be used with a label row."""
        return ObjectInstance(self)

from_dict

classmethod from_dict(d)

Return type:

[Object]

    @classmethod
    def from_dict(cls, d: dict) -> Object:
        shape_opt = Shape.from_string(d["shape"])
        if shape_opt is None:
            raise TypeError(f"The shape '{d['shape']}' of the object '{d}' is not recognised")

        attributes_ret: List[Attribute] = [
            attribute_from_dict(attribute_dict) for attribute_dict in d.get("attributes", [])
        ]
        return Object(
            uid=int(d["id"]),
            name=d["name"],
            color=d["color"],
            shape=shape_opt,
            feature_node_hash=d["featureNodeHash"],
            attributes=attributes_ret,
        )

to_dict

to_dict()

Return type:

Dict[str, Any]

    def to_dict(self) -> Dict[str, Any]:
        ret: Dict[str, Any] = {
            "id": str(self.uid),
            "name": self.name,
            "color": self.color,
            "shape": self.shape.value,
            "featureNodeHash": self.feature_node_hash,
        }
        if attributes_list := attributes_to_list_dict(self.attributes):
            ret["attributes"] = attributes_list

        return ret

T

alias of TypeVar(‘T’, bound=encord.objects.attributes.Attribute)

T = TypeVar("T", bound=Attribute)

add_attribute

Adds an attribute to the object.

add_attribute(cls, name, local_uid=None, feature_node_hash=None, required=False, dynamic=False)

Parameters:

  • cls (Type[T]) – attribute type, one of RadioAttribute, ChecklistAttribute, TextAttribute

  • name (str) – the user-visible name of the attribute

  • local_uid (Optional[int]) – integer identifier of the attribute. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology

  • feature_node_hash (Optional[str]) – global identifier of the attribute. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology

  • required (bool) – whether the label editor would mark this attribute as ‘required’

  • dynamic (bool) – whether the attribute can have a different answer for the same object across different frames.

Return type:

T

Returns:

The created attribute that can be further specified with Options, where appropriate

Raises:

ValueError – if specified local_uid or feature_node_hash violate uniqueness constraints

    def add_attribute(
        self,
        cls: Type[T],
        name: str,
        local_uid: Optional[int] = None,
        feature_node_hash: Optional[str] = None,
        required: bool = False,
        dynamic: bool = False,
    ) -> T:
        """
        Adds an attribute to the object.

        Args:
            cls: attribute type, one of `RadioAttribute`, `ChecklistAttribute`, `TextAttribute`
            name: the user-visible name of the attribute
            local_uid: integer identifier of the attribute. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing ontology
            feature_node_hash: global identifier of the attribute. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing ontology
            required: whether the label editor would mark this attribute as 'required'
            dynamic: whether the attribute can have a different answer for the same object across different frames.

        Returns:
            the created attribute that can be further specified with Options, where appropriate

        Raises:
            ValueError: if specified `local_uid` or `feature_node_hash` violate uniqueness constraints
        """
        return _add_attribute(self.attributes, cls, name, [self.uid], local_uid, feature_node_hash, required, dynamic)

Source

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Sequence, Type, TypeVar

from encord.objects.attributes import (
    Attribute,
    _add_attribute,
    attribute_from_dict,
    attributes_to_list_dict,
)
from encord.objects.common import Shape
from encord.objects.ontology_element import OntologyElement


@dataclass
class Object(OntologyElement):
    uid: int
    name: str
    color: str
    shape: Shape
    feature_node_hash: str
    attributes: List[Attribute] = field(default_factory=list)

    @property
    def title(self) -> str:
        return self.name

    @property
    def children(self) -> Sequence[OntologyElement]:
        return self.attributes

def create_instance(self) -> ObjectInstance:
        """Create a :class:`encord.objects.ObjectInstance` to be used with a label row."""
        return ObjectInstance(self)

@classmethod
    def from_dict(cls, d: dict) -> Object:
        shape_opt = Shape.from_string(d["shape"])
        if shape_opt is None:
            raise TypeError(f"The shape '{d['shape']}' of the object '{d}' is not recognised")

        attributes_ret: List[Attribute] = [
            attribute_from_dict(attribute_dict) for attribute_dict in d.get("attributes", [])
        ]
        return Object(
            uid=int(d["id"]),
            name=d["name"],
            color=d["color"],
            shape=shape_opt,
            feature_node_hash=d["featureNodeHash"],
            attributes=attributes_ret,
        )

def to_dict(self) -> Dict[str, Any]:
        ret: Dict[str, Any] = {
            "id": str(self.uid),
            "name": self.name,
            "color": self.color,
            "shape": self.shape.value,
            "featureNodeHash": self.feature_node_hash,
        }
        if attributes_list := attributes_to_list_dict(self.attributes):
            ret["attributes"] = attributes_list

        return ret

    T = TypeVar("T", bound=Attribute)

def add_attribute(
        self,
        cls: Type[T],
        name: str,
        local_uid: Optional[int] = None,
        feature_node_hash: Optional[str] = None,
        required: bool = False,
        dynamic: bool = False,
    ) -> T:
        """
        Adds an attribute to the object.

        Args:
            cls: attribute type, one of `RadioAttribute`, `ChecklistAttribute`, `TextAttribute`
            name: the user-visible name of the attribute
            local_uid: integer identifier of the attribute. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing ontology
            feature_node_hash: global identifier of the attribute. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing ontology
            required: whether the label editor would mark this attribute as 'required'
            dynamic: whether the attribute can have a different answer for the same object across different frames.

        Returns:
            the created attribute that can be further specified with Options, where appropriate

        Raises:
            ValueError: if specified `local_uid` or `feature_node_hash` violate uniqueness constraints
        """
        return _add_attribute(self.attributes, cls, name, [self.uid], local_uid, feature_node_hash, required, dynamic)


from encord.objects.ontology_object_instance import ObjectInstance

Classification

Represents a whole-image classification as part of Ontology structure. Wraps a single Attribute that describes the image in general rather than an individual object.

class encord.objects.ontology_labels_impl.Classification(feature_node_hash, uid, attributes)
  • uid: int
  • feature_node_hash: str
  • attributes: List[encord.objects.attributes.Attribute]
    • property title: str
    • Return type: str
  • property children: Sequence[encord.objects.ontology_element.OntologyElement]
    • Return type: Sequence[OntologyElement]

create_instance

Create a encord.objects.ClassificationInstance to be used with a label row.

create_instance()

Return type:

[ClassificationInstance]

    def create_instance(self) -> ClassificationInstance:
        """Create a :class:`encord.objects.ClassificationInstance` to be used with a label row."""
        return ClassificationInstance(self)

from_dict

classmethod from_dict(d)

Return type:

[Classification]

    @classmethod
    def from_dict(cls, d: dict) -> Classification:
        attributes_ret: List[Attribute] = [attribute_from_dict(attribute_dict) for attribute_dict in d["attributes"]]
        return Classification(
            uid=int(d["id"]),
            feature_node_hash=d["featureNodeHash"],
            attributes=attributes_ret,
        )

T

alias of TypeVar(‘T’, bound=encord.objects.attributes.Attribute)

 T = TypeVar("T", bound=Attribute)

add_attribute

Adds an attribute to the classification.

add_attribute(cls, name, local_uid=None, feature_node_hash=None, required=False)

Paramters:

  • cls (Type[T]) – attribute type, one of RadioAttribute, ChecklistAttribute, TextAttribute

  • name (str) – the user-visible name of the attribute

  • local_uid (Optional[int]) – integer identifier of the attribute. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology

  • feature_node_hash (Optional[str]) – global identifier of the attribute. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology

  • required (bool) – whether the label editor would mark this attribute as ‘required’

Return type:

T

Returns:

The created attribute that can be further specified with Options, where appropriate

Raises:

ValueError – if the classification already has an attribute assigned

    def add_attribute(
        self,
        cls: Type[T],
        name: str,
        local_uid: Optional[int] = None,
        feature_node_hash: Optional[str] = None,
        required: bool = False,
    ) -> T:
        """
        Adds an attribute to the classification.

        Args:
            cls: attribute type, one of `RadioAttribute`, `ChecklistAttribute`, `TextAttribute`
            name: the user-visible name of the attribute
            local_uid: integer identifier of the attribute. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing ontology
            feature_node_hash: global identifier of the attribute. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing ontology
            required: whether the label editor would mark this attribute as 'required'

        Returns:
            the created attribute that can be further specified with Options, where appropriate

        Raises:
            ValueError: if the classification already has an attribute assigned
        """
        if self.attributes:
            raise ValueError("Classification should have exactly one root attribute")
        return _add_attribute(self.attributes, cls, name, [self.uid], local_uid, feature_node_hash, required)

Source

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Sequence, Type, TypeVar

from encord.objects.attributes import (
    Attribute,
    _add_attribute,
    attribute_from_dict,
    attributes_to_list_dict,
)
from encord.objects.ontology_element import OntologyElement


@dataclass
class Classification(OntologyElement):
    """
    Represents a whole-image classification as part of Ontology structure. Wraps a single Attribute that describes
    the image in general rather than an individual object.
    """

    uid: int
    feature_node_hash: str
    attributes: List[Attribute]

    @property
    def title(self) -> str:
        return self.attributes[0].name

    @property
    def children(self) -> Sequence[OntologyElement]:
        return self.attributes

def create_instance(self) -> ClassificationInstance:
        """Create a :class:`encord.objects.ClassificationInstance` to be used with a label row."""
        return ClassificationInstance(self)

@classmethod
    def from_dict(cls, d: dict) -> Classification:
        attributes_ret: List[Attribute] = [attribute_from_dict(attribute_dict) for attribute_dict in d["attributes"]]
        return Classification(
            uid=int(d["id"]),
            feature_node_hash=d["featureNodeHash"],
            attributes=attributes_ret,
        )

def to_dict(self) -> Dict[str, Any]:
        ret: Dict[str, Any] = {
            "id": str(self.uid),
            "featureNodeHash": self.feature_node_hash,
        }
        if attributes_list := attributes_to_list_dict(self.attributes):
            ret["attributes"] = attributes_list

        return ret

    T = TypeVar("T", bound=Attribute)

def add_attribute(
        self,
        cls: Type[T],
        name: str,
        local_uid: Optional[int] = None,
        feature_node_hash: Optional[str] = None,
        required: bool = False,
    ) -> T:
        """
        Adds an attribute to the classification.

        Args:
            cls: attribute type, one of `RadioAttribute`, `ChecklistAttribute`, `TextAttribute`
            name: the user-visible name of the attribute
            local_uid: integer identifier of the attribute. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing ontology
            feature_node_hash: global identifier of the attribute. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing ontology
            required: whether the label editor would mark this attribute as 'required'

        Returns:
            the created attribute that can be further specified with Options, where appropriate

        Raises:
            ValueError: if the classification already has an attribute assigned
        """
        if self.attributes:
            raise ValueError("Classification should have exactly one root attribute")
        return _add_attribute(self.attributes, cls, name, [self.uid], local_uid, feature_node_hash, required)

    def __hash__(self):
        return hash(self.feature_node_hash)


from encord.objects.classification_instance import ClassificationInstance

Source

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Type, cast
from uuid import uuid4

from encord.exceptions import OntologyError
from encord.objects.classification import Classification
from encord.objects.common import Shape
from encord.objects.constants import AVAILABLE_COLORS
from encord.objects.ontology_element import (
    OntologyElement,
    OntologyElementT,
    OntologyNestedElement,
    _assert_singular_result_list,
    _get_element_by_hash,
)
from encord.objects.ontology_object import Object
from encord.objects.utils import checked_cast, does_type_match


[docs]@dataclass
class OntologyStructure:
    objects: List[Object] = field(default_factory=list)
    classifications: List[Classification] = field(default_factory=list)

def get_child_by_hash(
        self,
        feature_node_hash: str,
        type_: Optional[Type[OntologyElementT]] = None,
    ) -> OntologyElementT:
        """
        Returns the first child node of this ontology tree node with the matching feature node hash. If there is
        more than one child with the same feature node hash in the ontology tree node, then the ontology would be in
        an invalid state. Throws if nothing is found or if the type is not matched.

        Args:
            feature_node_hash: the feature_node_hash of the child node to search for in the ontology.
            type_: The expected type of the item. If the found child does not match the type, an error will be thrown.
        """
        for object_ in self.objects:
            if object_.feature_node_hash == feature_node_hash:
                return checked_cast(object_, type_)

            found_item = _get_element_by_hash(feature_node_hash, object_.attributes)
            if found_item is not None:
                return checked_cast(found_item, type_)

        for classification in self.classifications:
            if classification.feature_node_hash == feature_node_hash:
                return checked_cast(classification, type_)
            found_item = _get_element_by_hash(feature_node_hash, classification.attributes)
            if found_item is not None:
                return checked_cast(found_item, type_)

        raise OntologyError(f"Item not found: can't find an item with a hash {feature_node_hash} in the ontology.")

def get_child_by_title(
        self,
        title: str,
        type_: Optional[Type[OntologyElementT]] = None,
    ) -> OntologyElementT:
        """
        Returns a child node of this ontology tree node with the matching title and matching type if specified. If more
        than one child in this Object have the same title, then an error will be thrown. If no item is found, an error
        will be thrown as well.

        Args:
            title: The exact title of the child node to search for in the ontology.
            type_: The expected type of the child node. Only a node that matches this type will be returned.
        """
        found_items = self.get_children_by_title(title, type_)
        _assert_singular_result_list(found_items, title, type_)
        return found_items[0]

def get_children_by_title(
        self,
        title: str,
        type_: Optional[Type[OntologyElementT]] = None,
    ) -> List[OntologyElementT]:
        """
        Returns all the child nodes of this ontology tree node with the matching title and matching type if specified.
        Title in ontologies do not need to be unique, however, we recommend unique titles when creating ontologies.

        Args:
            title: The exact title of the child node to search for in the ontology.
            type_: The expected type of the item. Only nodes that match this type will be returned.
        """
        ret: List[OntologyElement] = []
        for object_ in self.objects:
            if object_.title == title and does_type_match(object_, type_):
                ret.append(object_)

            if type_ is None or issubclass(type_, OntologyNestedElement):
                found_items = object_.get_children_by_title(title, type_=type_)
                ret.extend(found_items)

        for classification in self.classifications:
            if classification.title == title and does_type_match(classification, type_):
                ret.append(classification)

            if type_ is None or issubclass(type_, OntologyNestedElement):
                found_items = classification.get_children_by_title(title, type_=type_)
                ret.extend(found_items)

        # type checks in the code above guarantee the type conformity of the return value
        # but there is no obvious way to tell that to mypy, so just casting here for now
        return cast(List[OntologyElementT], ret)

@classmethod
    def from_dict(cls, d: Dict[str, Any]) -> OntologyStructure:
        """
        Args:
            d: a JSON blob of an "ontology structure" (e.g. from Encord web app)

        Raises:
            KeyError: If the dict is missing a required field.
        """
        objects_ret = [Object.from_dict(object_dict) for object_dict in d["objects"]]
        classifications_ret = [
            Classification.from_dict(classification_dict) for classification_dict in d["classifications"]
        ]
        return OntologyStructure(objects=objects_ret, classifications=classifications_ret)

def to_dict(self) -> Dict[str, List[Dict[str, Any]]]:
        """
        Returns:
            The dict equivalent to the ontology.

        Raises:
            KeyError: If the dict is missing a required field.
        """
        ret: Dict[str, List[Dict[str, Any]]] = dict()
        ontology_objects: List[Dict[str, Any]] = list()
        ret["objects"] = ontology_objects
        for ontology_object in self.objects:
            ontology_objects.append(ontology_object.to_dict())

        ontology_classifications: List[Dict[str, Any]] = list()
        ret["classifications"] = ontology_classifications
        for ontology_classification in self.classifications:
            ontology_classifications.append(ontology_classification.to_dict())

        return ret

def add_object(
        self,
        name: str,
        shape: Shape,
        uid: Optional[int] = None,
        color: Optional[str] = None,
        feature_node_hash: Optional[str] = None,
    ) -> Object:
        """
        Adds an object class definition to the structure.

        .. code::

            structure = ontology_structure.OntologyStructure()

            eye = structure.add_object(
                name="Eye",
            )
            nose = structure.add_object(
                name="Nose",
            )
            nose_detail = nose.add_attribute(
                encord.objects.common.ChecklistAttribute,
            )
            nose_detail.add_option(feature_node_hash="2bc17c88", label="Is it a cute nose?")
            nose_detail.add_option(feature_node_hash="86eaa4f2", label="Is it a wet nose? ")

        Args:
            name: the user-visible name of the object
            shape: the kind of object (bounding box, polygon, etc). See :py:class:`encord.objects.common.Shape` enum for possible values
            uid: integer identifier of the object. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing structure
            color: the color of the object in the label editor. Normally auto-assigned, should be in '#1A2B3F' syntax.
            feature_node_hash: global identifier of the object. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing structure

        Returns:
            the created object class that can be further customised with attributes.
        """
        if uid is None:
            if self.objects:
                uid = max([obj.uid for obj in self.objects]) + 1
            else:
                uid = 1
        else:
            if any([obj.uid == uid for obj in self.objects]):
                raise ValueError(f"Duplicate uid '{uid}'")

        if color is None:
            color_index = 0
            if self.objects:
                try:
                    color_index = AVAILABLE_COLORS.index(self.objects[-1].color) + 1
                    if color_index >= len(AVAILABLE_COLORS):
                        color_index = 0
                except ValueError:
                    pass
            color = AVAILABLE_COLORS[color_index]

        if feature_node_hash is None:
            feature_node_hash = str(uuid4())[:8]

        if any([obj.feature_node_hash == feature_node_hash for obj in self.objects]):
            raise ValueError(f"Duplicate feature_node_hash '{feature_node_hash}'")

        obj = Object(uid=uid, name=name, color=color, shape=shape, feature_node_hash=feature_node_hash)
        self.objects.append(obj)
        return obj

def add_classification(
        self,
        uid: Optional[int] = None,
        feature_node_hash: Optional[str] = None,
    ) -> Classification:
        """
        Adds an classification definition to the ontology.

        .. code::

            structure = ontology_structure.OntologyStructure()

            cls = structure.add_classification(feature_node_hash="a39d81c0")
            cat_standing = cls.add_attribute(
                encord.objects.common.RadioAttribute,
                feature_node_hash="a6136d14",
                name="Is the cat standing?",
                required=True,
            )
            cat_standing.add_option(feature_node_hash="a3aeb48d", label="Yes")
            cat_standing.add_option(feature_node_hash="d0a4b373", label="No")

        Args:
            uid: integer identifier of the object. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing structure
            feature_node_hash: global identifier of the object. Normally auto-generated;
                    omit this unless the aim is to create an exact clone of existing structure

        Returns:
            the created classification node. Note that classification attribute should be further specified by calling its `add_attribute()` method.
        """
        if uid is None:
            if self.classifications:
                uid = max([cls.uid for cls in self.classifications]) + 1
            else:
                uid = 1
        else:
            if any([cls.uid == uid for cls in self.classifications]):
                raise ValueError(f"Duplicate uid '{uid}'")

        if feature_node_hash is None:
            feature_node_hash = str(uuid4())[:8]

        if any([cls.feature_node_hash == feature_node_hash for cls in self.classifications]):
            raise ValueError(f"Duplicate feature_node_hash '{feature_node_hash}'")

        cls = Classification(uid=uid, feature_node_hash=feature_node_hash, attributes=list())
        self.classifications.append(cls)
        return cls