|
28 | 28 | from tqdm import tqdm
|
29 | 29 |
|
30 | 30 | import lib.core as constants
|
| 31 | +from lib.infrastructure.controller import Controller |
31 | 32 | from lib.app.helpers import get_annotation_paths
|
32 | 33 | from lib.app.helpers import get_name_url_duplicated_from_csv
|
33 | 34 | from lib.app.helpers import wrap_error as wrap_validation_errors
|
34 | 35 | from lib.app.interface.base_interface import BaseInterfaceFacade
|
35 | 36 | from lib.app.interface.base_interface import TrackableMeta
|
| 37 | + |
36 | 38 | from lib.app.interface.types import EmailStr
|
37 | 39 | from lib.app.serializers import BaseSerializer
|
38 | 40 | from lib.app.serializers import FolderSerializer
|
|
45 | 47 | from lib.core.conditions import Condition
|
46 | 48 | from lib.core.jsx_conditions import Filter, OperatorEnum
|
47 | 49 | from lib.core.conditions import EmptyCondition
|
48 |
| -from lib.core.entities import AttachmentEntity |
| 50 | +from lib.core.entities import AttachmentEntity, FolderEntity, BaseItemEntity |
49 | 51 | from lib.core.entities import SettingEntity
|
50 | 52 | from lib.core.entities.classes import AnnotationClassEntity
|
51 | 53 | from lib.core.entities.classes import AttributeGroup
|
|
61 | 63 | from lib.core.pydantic_v1 import constr
|
62 | 64 | from lib.core.pydantic_v1 import conlist
|
63 | 65 | from lib.core.pydantic_v1 import parse_obj_as
|
| 66 | +from lib.infrastructure.annotation_adapter import BaseMultimodalAnnotationAdapter |
| 67 | +from lib.infrastructure.annotation_adapter import MultimodalSmallAnnotationAdapter |
| 68 | +from lib.infrastructure.annotation_adapter import MultimodalLargeAnnotationAdapter |
64 | 69 | from lib.infrastructure.utils import extract_project_folder
|
65 | 70 | from lib.infrastructure.validators import wrap_error
|
66 | 71 |
|
|
69 | 74 | # NotEmptyStr = TypeVar("NotEmptyStr", bound=constr(strict=True, min_length=1))
|
70 | 75 | NotEmptyStr = constr(strict=True, min_length=1)
|
71 | 76 |
|
72 |
| - |
73 | 77 | PROJECT_STATUS = Literal["NotStarted", "InProgress", "Completed", "OnHold"]
|
74 | 78 |
|
75 | 79 | PROJECT_TYPE = Literal[
|
|
82 | 86 | "Multimodal",
|
83 | 87 | ]
|
84 | 88 |
|
85 |
| - |
86 | 89 | APPROVAL_STATUS = Literal["Approved", "Disapproved", None]
|
87 | 90 |
|
88 | 91 | IMAGE_QUALITY = Literal["compressed", "original"]
|
@@ -110,6 +113,87 @@ class Attachment(TypedDict, total=False):
|
110 | 113 | integration: NotRequired[str] # noqa
|
111 | 114 |
|
112 | 115 |
|
| 116 | +class ItemContext: |
| 117 | + def __init__( |
| 118 | + self, |
| 119 | + controller: Controller, |
| 120 | + project: Project, |
| 121 | + folder: FolderEntity, |
| 122 | + item: BaseItemEntity, |
| 123 | + overwrite: bool = True, |
| 124 | + ): |
| 125 | + self.controller = controller |
| 126 | + self.project = project |
| 127 | + self.folder = folder |
| 128 | + self.item = item |
| 129 | + self._annotation_adapter: Optional[BaseMultimodalAnnotationAdapter] = None |
| 130 | + self._overwrite = overwrite |
| 131 | + self._annotation = None |
| 132 | + |
| 133 | + def _set_small_annotation_adapter(self, annotation: dict = None): |
| 134 | + self._annotation_adapter = MultimodalSmallAnnotationAdapter( |
| 135 | + project=self.project, |
| 136 | + folder=self.folder, |
| 137 | + item=self.item, |
| 138 | + controller=self.controller, |
| 139 | + overwrite=self._overwrite, |
| 140 | + annotation=annotation, |
| 141 | + ) |
| 142 | + |
| 143 | + def _set_large_annotation_adapter(self, annotation: dict = None): |
| 144 | + self._annotation_adapter = MultimodalLargeAnnotationAdapter( |
| 145 | + project=self.project, |
| 146 | + folder=self.folder, |
| 147 | + item=self.item, |
| 148 | + controller=self.controller, |
| 149 | + annotation=annotation, |
| 150 | + ) |
| 151 | + |
| 152 | + @property |
| 153 | + def annotation_adapter(self) -> BaseMultimodalAnnotationAdapter: |
| 154 | + if self._annotation_adapter is None: |
| 155 | + res = self.controller.service_provider.annotations.get_upload_chunks( |
| 156 | + project=self.project, item_ids=[self.item.id] |
| 157 | + ) |
| 158 | + small_item = next(iter(res["small"]), None) |
| 159 | + if small_item: |
| 160 | + self._set_small_annotation_adapter() |
| 161 | + else: |
| 162 | + self._set_large_annotation_adapter() |
| 163 | + return self._annotation_adapter |
| 164 | + |
| 165 | + @property |
| 166 | + def annotation(self): |
| 167 | + return self.annotation_adapter.annotation |
| 168 | + |
| 169 | + def __enter__(self): |
| 170 | + return self |
| 171 | + |
| 172 | + def __exit__(self, exc_type, exc_val, exc_tb): |
| 173 | + if exc_type: |
| 174 | + return False |
| 175 | + |
| 176 | + self.save() |
| 177 | + return True |
| 178 | + |
| 179 | + def save(self): |
| 180 | + if len(json.dumps(self.annotation).encode("utf-8")) > 16 * 1024 * 1024: |
| 181 | + self._set_large_annotation_adapter(self.annotation) |
| 182 | + else: |
| 183 | + self._set_small_annotation_adapter(self.annotation) |
| 184 | + self._annotation_adapter.save() |
| 185 | + |
| 186 | + def get_metadata(self): |
| 187 | + return self.annotation["metadata"] |
| 188 | + |
| 189 | + def get_component_value(self, component_id: str): |
| 190 | + return self.annotation_adapter.get_component_value(component_id) |
| 191 | + |
| 192 | + def set_component_value(self, component_id: str, value: Any): |
| 193 | + self.annotation_adapter.set_component_value(component_id, value) |
| 194 | + return self |
| 195 | + |
| 196 | + |
113 | 197 | class SAClient(BaseInterfaceFacade, metaclass=TrackableMeta):
|
114 | 198 | """Create SAClient instance to authorize SDK in a team scope.
|
115 | 199 | In case of no argument has been provided, SA_TOKEN environmental variable
|
@@ -3540,3 +3624,115 @@ def set_approval_statuses(
|
3540 | 3624 | )
|
3541 | 3625 | if response.errors:
|
3542 | 3626 | raise AppException(response.errors)
|
| 3627 | + |
| 3628 | + def item_context( |
| 3629 | + self, |
| 3630 | + path: Union[str, Tuple[NotEmptyStr, NotEmptyStr], Tuple[int, int]], |
| 3631 | + item: Union[NotEmptyStr, int], |
| 3632 | + overwrite: bool = True, |
| 3633 | + ) -> ItemContext: |
| 3634 | + """ |
| 3635 | + Creates an “ItemContext” for managing item annotations and metadata. |
| 3636 | +
|
| 3637 | + This function allows you to manage annotations and metadata for an item located within a |
| 3638 | + specified project and folder. The path to the item can be provided either as a string or a tuple, |
| 3639 | + and you can specify the item using its name or ID. |
| 3640 | + It returns an “ItemContext” that automatically saves any changes to annotations when the context is exited. |
| 3641 | +
|
| 3642 | + :param path: Specifies the project and folder containing the item. Can be one of: |
| 3643 | + - A string path, e.g., "project_name/folder_name". |
| 3644 | + - A tuple of strings, e.g., ("project_name", "folder_name"). |
| 3645 | + - A tuple of integers (IDs), e.g., (project_id, folder_id). |
| 3646 | + :type path: Union[str, Tuple[str, str], Tuple[int, int]] |
| 3647 | +
|
| 3648 | + :param item: The name or ID of the item for which the context is being created. |
| 3649 | + :type item: Union[str, int] |
| 3650 | +
|
| 3651 | + :param overwrite: If `True`, annotations are overwritten during saving. Defaults is `True`. |
| 3652 | + If `False`, raises a `FileChangedError` if the item was modified concurrently. |
| 3653 | + :type overwrite: bool |
| 3654 | +
|
| 3655 | + :raises AppException: If the provided `path` is invalid or if the item cannot be located. |
| 3656 | +
|
| 3657 | + :return: An `ItemContext` object to manage the specified item's annotations and metadata. |
| 3658 | + :rtype: ItemContext |
| 3659 | +
|
| 3660 | + **Examples:** |
| 3661 | +
|
| 3662 | + Create an `ItemContext` using a string path and item name: |
| 3663 | +
|
| 3664 | + .. code-block:: python |
| 3665 | +
|
| 3666 | + with client.item_context("project_name/folder_name", "item_name") as item_context: |
| 3667 | + metadata = item_context.get_metadata() |
| 3668 | + value = item_context.get_component_value("prompts") |
| 3669 | + item_context.set_component_value("prompts", value) |
| 3670 | +
|
| 3671 | + Create an `ItemContext` using a tuple of strings and an item ID: |
| 3672 | +
|
| 3673 | + .. code-block:: python |
| 3674 | +
|
| 3675 | + with client.item_context(("project_name", "folder_name"), 12345) as context: |
| 3676 | + metadata = context.get_metadata() |
| 3677 | + print(metadata) |
| 3678 | +
|
| 3679 | + Create an `ItemContext` using a tuple of IDs and an item name: |
| 3680 | +
|
| 3681 | + .. code-block:: python |
| 3682 | +
|
| 3683 | + with client.item_context((101, 202), "item_name") as context: |
| 3684 | + value = context.get_component_value("component_id") |
| 3685 | + print(value) |
| 3686 | +
|
| 3687 | + Save annotations automatically after modifying component values: |
| 3688 | +
|
| 3689 | + .. code-block:: python |
| 3690 | +
|
| 3691 | + with client.item_context("project_name/folder_name", "item_name", overwrite=True) as context: |
| 3692 | + context.set_component_value("component_id", "new_value") |
| 3693 | + # No need to call .save(), changes are saved automatically on context exit. |
| 3694 | +
|
| 3695 | + Handle exceptions during context execution: |
| 3696 | +
|
| 3697 | + .. code-block:: python |
| 3698 | +
|
| 3699 | + from superannotate import FileChangedError |
| 3700 | +
|
| 3701 | + try: |
| 3702 | + with client.item_context((101, 202), "item_name") as context: |
| 3703 | + context.set_component_value("component_id", "new_value") |
| 3704 | + except FileChangedError as e: |
| 3705 | + print(f"An error occurred: {e}") |
| 3706 | + """ |
| 3707 | + if isinstance(path, str): |
| 3708 | + project, folder = self.controller.get_project_folder_by_path(path) |
| 3709 | + elif len(path) == 2 and all([isinstance(i, str) for i in path]): |
| 3710 | + project = self.controller.get_project(path[0]) |
| 3711 | + folder = self.controller.get_folder(project, path[1]) |
| 3712 | + elif len(path) == 2 and all([isinstance(i, int) for i in path]): |
| 3713 | + project = self.controller.get_project_by_id(path[0]).data |
| 3714 | + folder = self.controller.get_folder_by_id(path[1], project.id).data |
| 3715 | + else: |
| 3716 | + raise AppException("Invalid path provided.") |
| 3717 | + if project.type != ProjectType.MULTIMODAL: |
| 3718 | + raise AppException( |
| 3719 | + "This function is only supported for Multimodal projects." |
| 3720 | + ) |
| 3721 | + if isinstance(item, int): |
| 3722 | + _item = self.controller.get_item_by_id(item_id=item, project=project) |
| 3723 | + else: |
| 3724 | + items = self.controller.items.list_items(project, folder, name=item) |
| 3725 | + if not items: |
| 3726 | + raise AppException("Item not found.") |
| 3727 | + _item = items[0] |
| 3728 | + if project.type != ProjectType.MULTIMODAL: |
| 3729 | + raise AppException( |
| 3730 | + f"The function is not supported for {project.type.name} projects." |
| 3731 | + ) |
| 3732 | + return ItemContext( |
| 3733 | + controller=self.controller, |
| 3734 | + project=project, |
| 3735 | + folder=folder, |
| 3736 | + item=_item, |
| 3737 | + overwrite=overwrite, |
| 3738 | + ) |
0 commit comments