Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ History

All release highlights of this project will be documented in this file.

4.4.28 - Dec 13, 2024
________________________
**Fixed**

- ``SAClient.item_context`` creates an “ItemContext” for managing item annotations and metadata..

4.4.27 - Nov 14, 2024
________________________
**Fixed**
Expand Down
1 change: 1 addition & 0 deletions docs/source/api_reference/api_item.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Items
.. automethod:: superannotate.SAClient.list_items
.. automethod:: superannotate.SAClient.search_items
.. automethod:: superannotate.SAClient.attach_items
.. automethod:: superannotate.SAClient.item_context
.. automethod:: superannotate.SAClient.copy_items
.. automethod:: superannotate.SAClient.move_items
.. automethod:: superannotate.SAClient.delete_items
Expand Down
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ minversion = 3.7
log_cli=true
python_files = test_*.py
;pytest_plugins = ['pytest_profiling']
addopts = -n 4 --dist loadscope
;addopts = -n 4 --dist loadscope
4 changes: 3 additions & 1 deletion src/superannotate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys


__version__ = "4.4.27"
__version__ = "4.4.28dev1"

os.environ.update({"sa_version": __version__})
sys.path.append(os.path.split(os.path.realpath(__file__))[0])
Expand All @@ -15,6 +15,7 @@
from lib.core import PACKAGE_VERSION_INFO_MESSAGE
from lib.core import PACKAGE_VERSION_MAJOR_UPGRADE
from lib.core.exceptions import AppException
from lib.core.exceptions import FileChangedError
from superannotate.lib.app.input_converters import convert_project_type
from superannotate.lib.app.input_converters import export_annotation
from superannotate.lib.app.input_converters import import_annotation
Expand All @@ -30,6 +31,7 @@
# Utils
"enums",
"AppException",
"FileChangedError",
"import_annotation",
"export_annotation",
"convert_project_type",
Expand Down
202 changes: 199 additions & 3 deletions src/superannotate/lib/app/interface/sdk_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
from tqdm import tqdm

import lib.core as constants
from lib.infrastructure.controller import Controller
from lib.app.helpers import get_annotation_paths
from lib.app.helpers import get_name_url_duplicated_from_csv
from lib.app.helpers import wrap_error as wrap_validation_errors
from lib.app.interface.base_interface import BaseInterfaceFacade
from lib.app.interface.base_interface import TrackableMeta

from lib.app.interface.types import EmailStr
from lib.app.serializers import BaseSerializer
from lib.app.serializers import FolderSerializer
Expand All @@ -45,7 +47,7 @@
from lib.core.conditions import Condition
from lib.core.jsx_conditions import Filter, OperatorEnum
from lib.core.conditions import EmptyCondition
from lib.core.entities import AttachmentEntity
from lib.core.entities import AttachmentEntity, FolderEntity, BaseItemEntity
from lib.core.entities import SettingEntity
from lib.core.entities.classes import AnnotationClassEntity
from lib.core.entities.classes import AttributeGroup
Expand All @@ -61,6 +63,9 @@
from lib.core.pydantic_v1 import constr
from lib.core.pydantic_v1 import conlist
from lib.core.pydantic_v1 import parse_obj_as
from lib.infrastructure.annotation_adapter import BaseMultimodalAnnotationAdapter
from lib.infrastructure.annotation_adapter import MultimodalSmallAnnotationAdapter
from lib.infrastructure.annotation_adapter import MultimodalLargeAnnotationAdapter
from lib.infrastructure.utils import extract_project_folder
from lib.infrastructure.validators import wrap_error

Expand All @@ -69,7 +74,6 @@
# NotEmptyStr = TypeVar("NotEmptyStr", bound=constr(strict=True, min_length=1))
NotEmptyStr = constr(strict=True, min_length=1)


PROJECT_STATUS = Literal["NotStarted", "InProgress", "Completed", "OnHold"]

PROJECT_TYPE = Literal[
Expand All @@ -82,7 +86,6 @@
"Multimodal",
]


APPROVAL_STATUS = Literal["Approved", "Disapproved", None]

IMAGE_QUALITY = Literal["compressed", "original"]
Expand Down Expand Up @@ -110,6 +113,87 @@ class Attachment(TypedDict, total=False):
integration: NotRequired[str] # noqa


class ItemContext:
def __init__(
self,
controller: Controller,
project: Project,
folder: FolderEntity,
item: BaseItemEntity,
overwrite: bool = True,
):
self.controller = controller
self.project = project
self.folder = folder
self.item = item
self._annotation_adapter: Optional[BaseMultimodalAnnotationAdapter] = None
self._overwrite = overwrite
self._annotation = None

def _set_small_annotation_adapter(self, annotation: dict = None):
self._annotation_adapter = MultimodalSmallAnnotationAdapter(
project=self.project,
folder=self.folder,
item=self.item,
controller=self.controller,
overwrite=self._overwrite,
annotation=annotation,
)

def _set_large_annotation_adapter(self, annotation: dict = None):
self._annotation_adapter = MultimodalLargeAnnotationAdapter(
project=self.project,
folder=self.folder,
item=self.item,
controller=self.controller,
annotation=annotation,
)

@property
def annotation_adapter(self) -> BaseMultimodalAnnotationAdapter:
if self._annotation_adapter is None:
res = self.controller.service_provider.annotations.get_upload_chunks(
project=self.project, item_ids=[self.item.id]
)
small_item = next(iter(res["small"]), None)
if small_item:
self._set_small_annotation_adapter()
else:
self._set_large_annotation_adapter()
return self._annotation_adapter

@property
def annotation(self):
return self.annotation_adapter.annotation

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type:
return False

self.save()
return True

def save(self):
if len(json.dumps(self.annotation).encode("utf-8")) > 16 * 1024 * 1024:
self._set_large_annotation_adapter(self.annotation)
else:
self._set_small_annotation_adapter(self.annotation)
self._annotation_adapter.save()

def get_metadata(self):
return self.annotation["metadata"]

def get_component_value(self, component_id: str):
return self.annotation_adapter.get_component_value(component_id)

def set_component_value(self, component_id: str, value: Any):
self.annotation_adapter.set_component_value(component_id, value)
return self


class SAClient(BaseInterfaceFacade, metaclass=TrackableMeta):
"""Create SAClient instance to authorize SDK in a team scope.
In case of no argument has been provided, SA_TOKEN environmental variable
Expand Down Expand Up @@ -3540,3 +3624,115 @@ def set_approval_statuses(
)
if response.errors:
raise AppException(response.errors)

def item_context(
self,
path: Union[str, Tuple[NotEmptyStr, NotEmptyStr], Tuple[int, int]],
item: Union[NotEmptyStr, int],
overwrite: bool = True,
) -> ItemContext:
"""
Creates an “ItemContext” for managing item annotations and metadata.

This function allows you to manage annotations and metadata for an item located within a
specified project and folder. The path to the item can be provided either as a string or a tuple,
and you can specify the item using its name or ID.
It returns an “ItemContext” that automatically saves any changes to annotations when the context is exited.

:param path: Specifies the project and folder containing the item. Can be one of:
- A string path, e.g., "project_name/folder_name".
- A tuple of strings, e.g., ("project_name", "folder_name").
- A tuple of integers (IDs), e.g., (project_id, folder_id).
:type path: Union[str, Tuple[str, str], Tuple[int, int]]

:param item: The name or ID of the item for which the context is being created.
:type item: Union[str, int]

:param overwrite: If `True`, annotations are overwritten during saving. Defaults is `True`.
If `False`, raises a `FileChangedError` if the item was modified concurrently.
:type overwrite: bool

:raises AppException: If the provided `path` is invalid or if the item cannot be located.

:return: An `ItemContext` object to manage the specified item's annotations and metadata.
:rtype: ItemContext

**Examples:**

Create an `ItemContext` using a string path and item name:

.. code-block:: python

with client.item_context("project_name/folder_name", "item_name") as item_context:
metadata = item_context.get_metadata()
value = item_context.get_component_value("prompts")
item_context.set_component_value("prompts", value)

Create an `ItemContext` using a tuple of strings and an item ID:

.. code-block:: python

with client.item_context(("project_name", "folder_name"), 12345) as context:
metadata = context.get_metadata()
print(metadata)

Create an `ItemContext` using a tuple of IDs and an item name:

.. code-block:: python

with client.item_context((101, 202), "item_name") as context:
value = context.get_component_value("component_id")
print(value)

Save annotations automatically after modifying component values:

.. code-block:: python

with client.item_context("project_name/folder_name", "item_name", overwrite=True) as context:
context.set_component_value("component_id", "new_value")
# No need to call .save(), changes are saved automatically on context exit.

Handle exceptions during context execution:

.. code-block:: python

from superannotate import FileChangedError

try:
with client.item_context((101, 202), "item_name") as context:
context.set_component_value("component_id", "new_value")
except FileChangedError as e:
print(f"An error occurred: {e}")
"""
if isinstance(path, str):
project, folder = self.controller.get_project_folder_by_path(path)
elif len(path) == 2 and all([isinstance(i, str) for i in path]):
project = self.controller.get_project(path[0])
folder = self.controller.get_folder(project, path[1])
elif len(path) == 2 and all([isinstance(i, int) for i in path]):
project = self.controller.get_project_by_id(path[0]).data
folder = self.controller.get_folder_by_id(path[1], project.id).data
else:
raise AppException("Invalid path provided.")
if project.type != ProjectType.MULTIMODAL:
raise AppException(
"This function is only supported for Multimodal projects."
)
if isinstance(item, int):
_item = self.controller.get_item_by_id(item_id=item, project=project)
else:
items = self.controller.items.list_items(project, folder, name=item)
if not items:
raise AppException("Item not found.")
_item = items[0]
if project.type != ProjectType.MULTIMODAL:
raise AppException(
f"The function is not supported for {project.type.name} projects."
)
return ItemContext(
controller=self.controller,
project=project,
folder=folder,
item=_item,
overwrite=overwrite,
)
1 change: 1 addition & 0 deletions src/superannotate/lib/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def setup_logging(level=DEFAULT_LOGGING_LEVEL, file_path=LOG_FILE_LOCATION):
"UploadFileType",
"Tokenization",
"ImageAutoAssignEnable",
"TemplateState",
]

__alL__ = (
Expand Down
2 changes: 2 additions & 0 deletions src/superannotate/lib/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ class IntegrationTypeEnum(BaseTitledEnum):
GCP = "gcp", 2
AZURE = "azure", 3
CUSTOM = "custom", 4
DATABRICKS = "databricks", 5
SNOWFLAKE = "snowflake", 6


class TrainingStatus(BaseTitledEnum):
Expand Down
6 changes: 6 additions & 0 deletions src/superannotate/lib/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ class PathError(AppException):
"""
User input Error
"""


class FileChangedError(AppException):
"""
User input Error
"""
4 changes: 4 additions & 0 deletions src/superannotate/lib/core/service_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ def ok(self):
return 199 < self.status < 300
return False

def raise_for_status(self):
if not self.ok:
raise AppException(self.error)

@property
def error(self):
if self.res_error:
Expand Down
Loading