Source code for encord.objects.common

from __future__ import annotations

import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Any, List, Optional, Tuple, Type, TypeVar, Union

from encord.exceptions import OntologyError
from encord.objects.utils import (
    _decode_nested_uid,
    check_type,
    filter_by_type,
    short_uuid_str,
)
from encord.orm.project import StringEnum

NestedID = List[int]


[docs]class PropertyType(StringEnum): RADIO = "radio" TEXT = "text" CHECKLIST = "checklist"
[docs]class Shape(StringEnum): BOUNDING_BOX = "bounding_box" POLYGON = "polygon" POINT = "point" SKELETON = "skeleton" POLYLINE = "polyline" ROTATABLE_BOUNDING_BOX = "rotatable_bounding_box"
[docs]@dataclass class Attribute(ABC): """ Base class for shared Attribute fields """ uid: NestedID feature_node_hash: str name: str required: bool dynamic: bool """ The `dynamic` member is part of every attribute. However it can only be true for top level (not nested) attributes that are part of an :class:`encord.objects.ontology_object.Object`. """
[docs] @abstractmethod def get_property_type(self) -> PropertyType: pass
[docs] @classmethod @abstractmethod def has_options_field(cls) -> bool: pass
[docs] @abstractmethod def get_child_by_hash( self, feature_node_hash: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> Union[AttributeClasses, OptionClasses]: """ 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. """ raise NotImplementedError("This method is not implemented for this class")
[docs] def get_child_by_title( self, title: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> Union[AttributeClasses, OptionClasses]: """ 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_) _handle_wrong_number_of_found_items(found_items, title, type_) return found_items[0]
[docs] @abstractmethod def get_children_by_title( self, title: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> List[Union[AttributeClasses, OptionClasses]]: """ 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. """ raise NotImplementedError("This method is not implemented for this class")
[docs] def to_dict(self) -> dict: ret = self._encode_base() options = self._encode_options() if options is not None: ret["options"] = options return ret
[docs] @classmethod def from_dict(cls, d: dict) -> Attribute: property_type = d["type"] common_attribute_fields = cls._decode_common_attribute_fields(d) if property_type == "radio": options_ret: List[NestableOption] = list() if "options" in d: for options_dict in d["options"]: options_ret.append(NestableOption.from_dict(options_dict)) return RadioAttribute( **common_attribute_fields, options=options_ret, ) elif property_type == "checklist": options_ret: List[FlatOption] = list() if "options" in d: for options_dict in d["options"]: options_ret.append(FlatOption.from_dict(options_dict)) return ChecklistAttribute( **common_attribute_fields, options=options_ret, ) elif property_type == "text": return TextAttribute( **common_attribute_fields, ) raise TypeError( f"Attribute is ill-formed: '{d}'. Expected to see either " f"attribute specific fields or option specific fields. Got both or none of them." )
def _encode_base(self) -> dict: ret = dict() ret["id"] = _decode_nested_uid(self.uid) ret["name"] = self.name ret["type"] = self.get_property_type().value ret["featureNodeHash"] = self.feature_node_hash ret["required"] = self.required ret["dynamic"] = self.dynamic return ret def _encode_options(self) -> Optional[list]: if not self.has_options_field() or not self.options: return None self: OptionAttribute ret = list() for option in self.options: ret.append(option.to_dict()) return ret @staticmethod def _decode_common_attribute_fields(attribute_dict: dict) -> dict: return { "uid": _attribute_id_from_json_str(attribute_dict["id"]), "feature_node_hash": attribute_dict["featureNodeHash"], "name": attribute_dict["name"], "required": attribute_dict["required"], "dynamic": attribute_dict.get("dynamic", False), }
[docs]@dataclass class RadioAttribute(Attribute): """ This class is currently in BETA. Its API might change in future minor version releases. """ options: List[NestableOption] = field(default_factory=list)
[docs] def get_property_type(self) -> PropertyType: return PropertyType.RADIO
[docs] @classmethod def has_options_field(cls) -> bool: return True
[docs] def get_child_by_hash( self, feature_node_hash: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> Union[AttributeClasses, OptionClasses]: """ 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. """ found_item = _get_option_by_hash(feature_node_hash, self.options) if found_item is None: raise OntologyError("Item not found.") check_type(found_item, type_) return found_item
[docs] def get_children_by_title( self, title: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> List[Union[AttributeClasses, OptionClasses]]: """ 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. """ found_items = _get_options_by_title(title, self.options) return filter_by_type(found_items, type_) # noqa
[docs] def add_option( self, label: str, value: Optional[str] = None, local_uid: Optional[int] = None, feature_node_hash: Optional[str] = None, ) -> NestableOption: """ Args: label: user-visible name of the option value: internal unique value; optional; normally mechanically constructed from the label local_uid: integer identifier of the option. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology feature_node_hash: global identifier of the option. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology Returns: a `NestableOption` instance attached to the attribute. This can be further specified by adding nested attributes. """ return _add_option(self.options, NestableOption, label, self.uid, local_uid, feature_node_hash, value)
[docs]@dataclass class ChecklistAttribute(Attribute): """ This class is currently in BETA. Its API might change in future minor version releases. """ options: List[FlatOption] = field(default_factory=list)
[docs] def get_property_type(self) -> PropertyType: return PropertyType.CHECKLIST
[docs] @classmethod def has_options_field(cls) -> bool: return True
[docs] def get_child_by_hash( self, feature_node_hash: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> Union[AttributeClasses, OptionClasses]: """ 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. """ found_item = _get_option_by_hash(feature_node_hash, self.options) if found_item is None: raise OntologyError("Item not found.") check_type(found_item, type_) return found_item
[docs] def get_children_by_title( self, title: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> List[Union[AttributeClasses, OptionClasses]]: """ 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. """ found_items = _get_options_by_title(title, self.options) return filter_by_type(found_items, type_) # noqa
[docs] def add_option( self, label: str, value: Optional[str] = None, local_uid: Optional[int] = None, feature_node_hash: Optional[str] = None, ): """ Args: label: user-visible name of the option value: internal unique value; optional; normally mechanically constructed from the label local_uid: integer identifier of the option. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology feature_node_hash: global identifier of the option. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology Returns: a `FlatOption` instance attached to the attribute. """ return _add_option(self.options, FlatOption, label, self.uid, local_uid, feature_node_hash, value)
[docs]@dataclass class TextAttribute(Attribute): """ This class is currently in BETA. Its API might change in future minor version releases. """
[docs] def get_property_type(self) -> PropertyType: return PropertyType.TEXT
[docs] @classmethod def has_options_field(cls) -> bool: return False
[docs] def get_child_by_hash( self, feature_node_hash: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> Union[AttributeClasses, OptionClasses]: """ 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. For TextAttributes this will always throw as they have no children. 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. """ raise OntologyError("No nested options available for text attributes.")
[docs] def get_children_by_title( self, title: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> List[Union[AttributeClasses, OptionClasses]]: """ 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. For TextAttributes this will always return an empty list. 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. """ return []
OptionAttribute = Union[RadioAttribute, ChecklistAttribute] """ This class is currently in BETA. Its API might change in future minor version releases. """ def _attribute_id_from_json_str(attribute_id: str) -> NestedID: nested_ids = attribute_id.split(".") return [int(x) for x in nested_ids]
[docs]def attribute_from_dict(d: dict) -> Attribute: """Convenience functions as you cannot call static member on union types.""" return Attribute.from_dict(d)
[docs]def attributes_to_list_dict(attributes: List[Attribute]) -> list: attributes_list = list() for attribute in attributes: attributes_list.append(attribute.to_dict()) return attributes_list
[docs]class OptionType(Enum): FLAT = auto() NESTABLE = auto()
[docs]@dataclass class Option(ABC): """ Base class for shared Option fields """ uid: NestedID feature_node_hash: str label: str value: str
[docs] @abstractmethod def get_option_type(self) -> OptionType: pass
[docs] @abstractmethod def get_child_by_hash( self, feature_node_hash: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> Union[AttributeClasses, OptionClasses]: """ 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. """ raise NotImplementedError("This method is not implemented for this class")
[docs] def get_child_by_title( self, title: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> Union[AttributeClasses, OptionClasses]: """ 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_) _handle_wrong_number_of_found_items(found_items, title, type_) return found_items[0]
[docs] @abstractmethod def get_children_by_title( self, title: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> List[Union[AttributeClasses, OptionClasses]]: """ 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. """ raise NotImplementedError("This method is not implemented for this class")
[docs] def to_dict(self) -> dict: ret = dict() ret["id"] = _decode_nested_uid(self.uid) ret["label"] = self.label ret["value"] = self.value ret["featureNodeHash"] = self.feature_node_hash nested_options = self._encode_nested_options() if nested_options: ret["options"] = nested_options return ret
@abstractmethod def _encode_nested_options(self) -> list: pass @staticmethod def _decode_common_option_fields(option_dict: dict) -> dict: return { "uid": _attribute_id_from_json_str(option_dict["id"]), "label": option_dict["label"], "value": option_dict["value"], "feature_node_hash": option_dict["featureNodeHash"], }
[docs]@dataclass class FlatOption(Option): """ This class is currently in BETA. Its API might change in future minor version releases. """
[docs] def get_option_type(self) -> OptionType: return OptionType.FLAT
[docs] def get_child_by_hash( self, feature_node_hash: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> Union[AttributeClasses, OptionClasses]: """ 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. For FlatOptions this will always throw as they have no children. 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. """ raise OntologyError("No nested attributes for flat options.")
[docs] def get_children_by_title( self, title: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> List[Union[AttributeClasses, OptionClasses]]: """ 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. For FlatOptions this will always return an empty list. 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. """ return []
[docs] @classmethod def from_dict(cls, d: dict) -> FlatOption: return FlatOption(**cls._decode_common_option_fields(d))
def _encode_nested_options(self) -> list: return []
[docs]@dataclass class NestableOption(Option): """ This class is currently in BETA. Its API might change in future minor version releases. """ nested_options: List[Attribute] = field(default_factory=list)
[docs] def get_option_type(self) -> OptionType: return OptionType.NESTABLE
[docs] def get_child_by_hash( self, feature_node_hash: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> Union[AttributeClasses, OptionClasses]: """ 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. """ found_item = _get_attribute_by_hash(feature_node_hash, self.nested_options) if found_item is None: raise OntologyError("Item not found.") check_type(found_item, type_) return found_item
[docs] def get_children_by_title( self, title: str, type_: Union[AttributeTypes, OptionTypes, None] = None, ) -> List[Union[AttributeClasses, OptionClasses]]: """ 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. """ found_items = _get_attributes_by_title(title, self.nested_options) return filter_by_type(found_items, type_) # noqa
def _encode_nested_options(self) -> list: return attributes_to_list_dict(self.nested_options)
[docs] @classmethod def from_dict(cls, d: dict) -> NestableOption: nested_options_ret: List[Attribute] = list() if "options" in d: for nested_option in d["options"]: nested_options_ret.append(attribute_from_dict(nested_option)) return NestableOption( **cls._decode_common_option_fields(d), nested_options=nested_options_ret, )
[docs] def add_nested_option( self, cls: Type[T], name: str, local_uid: Optional[int] = None, feature_node_hash: Optional[str] = None, required: bool = False, ) -> T: """ Adds a nested attribute to a RadioAttribute option. 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 object. 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 specified `local_uid` or `feature_node_hash` violate uniqueness constraints """ return _add_attribute(self.nested_options, cls, name, self.uid, local_uid, feature_node_hash, required)
def __hash__(self): return hash(self.feature_node_hash)
def __build_identifiers( existent_items: Union[Attribute, Option], local_uid: Optional[int] = None, feature_node_hash: Optional[str] = None, ) -> Tuple[int, str]: if local_uid is None: if existent_items: local_uid = max([item.uid[-1] for item in existent_items]) + 1 else: local_uid = 1 else: if any([item.uid[-1] == local_uid for item in existent_items]): raise ValueError(f"Duplicate uid '{local_uid}'") if feature_node_hash is None: feature_node_hash = short_uuid_str() elif any([item.feature_node_hash == feature_node_hash for item in existent_items]): raise ValueError(f"Duplicate feature_node_hash '{feature_node_hash}'") return local_uid, feature_node_hash T = TypeVar("T", bound=Attribute) def _add_attribute( attributes: List[Attribute], cls: Type[T], name: str, parent_uid: List[int], local_uid: Optional[int] = None, feature_node_hash: Optional[str] = None, required: bool = False, dynamic: bool = False, ) -> T: local_uid, feature_node_hash = __build_identifiers(attributes, local_uid, feature_node_hash) constructor_params = { "name": name, "uid": parent_uid + [local_uid], "feature_node_hash": feature_node_hash, "required": required, "dynamic": dynamic, } if cls.has_options_field(): constructor_params["options"] = [] attr = cls(**constructor_params) attributes.append(attr) return attr OT = TypeVar("OT", bound=Option) def _add_option( options: List[Option], cls: Type[OT], label: str, parent_uid: List[int], local_uid: Optional[int] = None, feature_node_hash: Optional[str] = None, value: Optional[str] = None, ) -> OT: local_uid, feature_node_hash = __build_identifiers(options, local_uid, feature_node_hash) if not value: value = re.sub(r"[\s]", "_", label).lower() option = cls(parent_uid + [local_uid], feature_node_hash, label, value) options.append(option) return option def _get_option_by_hash( feature_node_hash: str, options: List[Option] ) -> Union[RadioAttribute, ChecklistAttribute, TextAttribute, NestableOption, FlatOption, None]: for option_ in options: if option_.feature_node_hash == feature_node_hash: return option_ if option_.get_option_type() == OptionType.NESTABLE: found_item = _get_attribute_by_hash(feature_node_hash, option_.nested_options) if found_item is not None: return found_item return None def _get_attribute_by_hash( feature_node_hash: str, attributes: List[Attribute] ) -> Union[RadioAttribute, ChecklistAttribute, TextAttribute, NestableOption, FlatOption, None]: for attribute in attributes: if attribute.feature_node_hash == feature_node_hash: return attribute if attribute.has_options_field(): found_item = _get_option_by_hash(feature_node_hash, attribute.options) if found_item is not None: return found_item return None def _get_options_by_title( title: str, options: List[Option] ) -> List[Union[RadioAttribute, ChecklistAttribute, TextAttribute, NestableOption, FlatOption]]: ret = [] for option_ in options: if option_.label == title: ret.append(option_) if option_.get_option_type() == OptionType.NESTABLE: found_items = _get_attributes_by_title(title, option_.nested_options) ret.extend(found_items) return ret def _get_attributes_by_title( title: str, attributes: List[Attribute] ) -> List[Union[RadioAttribute, ChecklistAttribute, TextAttribute, NestableOption, FlatOption]]: ret = [] for attribute in attributes: if attribute.name == title: ret.append(attribute) if attribute.has_options_field(): found_items = _get_options_by_title(title, attribute.options) ret.extend(found_items) return ret def _handle_wrong_number_of_found_items( found_items: List[Union[RadioAttribute, ChecklistAttribute, TextAttribute, NestableOption, FlatOption]], title: str, type_: Any, ) -> None: if len(found_items) == 0: raise OntologyError(f"No item was found in the ontology with the given title `{title}` and type `{type_}`") elif len(found_items) > 1: raise OntologyError( f"More than one item was found in the ontology with the given title `{title}` and type `{type_}`. " f"Use the `get_children_by_title` or `get_child_by_hash` function instead. " f"The found items are `{found_items}`." ) AttributeTypes = Union[ Type[RadioAttribute], Type[ChecklistAttribute], Type[TextAttribute], Type[Attribute], ] AttributeClasses = Union[RadioAttribute, ChecklistAttribute, TextAttribute, Attribute] OptionTypes = Union[Type[FlatOption], Type[NestableOption], Type[Option]] OptionClasses = Union[FlatOption, NestableOption, Option]