Skip to content

Commit 6869c07

Browse files
committed
Added item context interface
1 parent 567aca3 commit 6869c07

File tree

18 files changed

+832
-78
lines changed

18 files changed

+832
-78
lines changed

docs/source/api_reference/api_item.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Items
88
.. automethod:: superannotate.SAClient.list_items
99
.. automethod:: superannotate.SAClient.search_items
1010
.. automethod:: superannotate.SAClient.attach_items
11+
.. automethod:: superannotate.SAClient.item_context
1112
.. automethod:: superannotate.SAClient.copy_items
1213
.. automethod:: superannotate.SAClient.move_items
1314
.. automethod:: superannotate.SAClient.delete_items

src/superannotate/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from lib.core import PACKAGE_VERSION_INFO_MESSAGE
1616
from lib.core import PACKAGE_VERSION_MAJOR_UPGRADE
1717
from lib.core.exceptions import AppException
18+
from lib.core.exceptions import FileChangedError
1819
from superannotate.lib.app.input_converters import convert_project_type
1920
from superannotate.lib.app.input_converters import export_annotation
2021
from superannotate.lib.app.input_converters import import_annotation
@@ -30,6 +31,7 @@
3031
# Utils
3132
"enums",
3233
"AppException",
34+
"FileChangedError",
3335
"import_annotation",
3436
"export_annotation",
3537
"convert_project_type",

src/superannotate/lib/app/interface/sdk_interface.py

Lines changed: 191 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
from tqdm import tqdm
2929

3030
import lib.core as constants
31+
from lib.infrastructure.controller import Controller
3132
from lib.app.helpers import get_annotation_paths
3233
from lib.app.helpers import get_name_url_duplicated_from_csv
3334
from lib.app.helpers import wrap_error as wrap_validation_errors
3435
from lib.app.interface.base_interface import BaseInterfaceFacade
3536
from lib.app.interface.base_interface import TrackableMeta
37+
3638
from lib.app.interface.types import EmailStr
3739
from lib.app.serializers import BaseSerializer
3840
from lib.app.serializers import FolderSerializer
@@ -45,7 +47,7 @@
4547
from lib.core.conditions import Condition
4648
from lib.core.jsx_conditions import Filter, OperatorEnum
4749
from lib.core.conditions import EmptyCondition
48-
from lib.core.entities import AttachmentEntity
50+
from lib.core.entities import AttachmentEntity, FolderEntity, BaseItemEntity
4951
from lib.core.entities import SettingEntity
5052
from lib.core.entities.classes import AnnotationClassEntity
5153
from lib.core.entities.classes import AttributeGroup
@@ -61,6 +63,9 @@
6163
from lib.core.pydantic_v1 import constr
6264
from lib.core.pydantic_v1 import conlist
6365
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
6469
from lib.infrastructure.utils import extract_project_folder
6570
from lib.infrastructure.validators import wrap_error
6671

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

72-
7377
PROJECT_STATUS = Literal["NotStarted", "InProgress", "Completed", "OnHold"]
7478

7579
PROJECT_TYPE = Literal[
@@ -82,7 +86,6 @@
8286
"Multimodal",
8387
]
8488

85-
8689
APPROVAL_STATUS = Literal["Approved", "Disapproved", None]
8790

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

112115

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+
113197
class SAClient(BaseInterfaceFacade, metaclass=TrackableMeta):
114198
"""Create SAClient instance to authorize SDK in a team scope.
115199
In case of no argument has been provided, SA_TOKEN environmental variable
@@ -3540,3 +3624,107 @@ def set_approval_statuses(
35403624
)
35413625
if response.errors:
35423626
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` instance for managing item annotations and metadata.
3636+
3637+
The function retrieves the specified project and folder based on the given `path`
3638+
(can be a string path or a tuple of project and folder names/IDs) and locates the
3639+
item by name or ID. It then returns an `ItemContext` object to handle the annotations
3640+
of the specified item. Changes to annotations are automatically saved upon exiting
3641+
the context.
3642+
3643+
:param path: Specifies the project and folder containing the item. Can be one of:
3644+
- A string path, e.g., "project_name/folder_name".
3645+
- A tuple of strings, e.g., ("project_name", "folder_name").
3646+
- A tuple of integers (IDs), e.g., (project_id, folder_id).
3647+
:type path: Union[str, Tuple[str, str], Tuple[int, int]]
3648+
3649+
:param item: The name or ID of the item for which the context is being created.
3650+
:type item: Union[str, int]
3651+
3652+
:param overwrite: If `True`, annotations are overwritten during saving. Defaults to `True`.
3653+
If `False`, raises a `FileChangedError` if the item was modified concurrently.
3654+
:type overwrite: bool
3655+
3656+
:raises AppException: If the provided `path` is invalid or if the item cannot be located.
3657+
3658+
:return: An `ItemContext` object to manage the specified item's annotations and metadata.
3659+
:rtype: ItemContext
3660+
3661+
**Examples:**
3662+
3663+
Create an `ItemContext` using a string path and item name:
3664+
```python
3665+
with client.item_context("project_name/folder_name", "item_name") as item_context:
3666+
metadata = item_context.get_metadata()
3667+
value = item_context.get_component_value("prompts")
3668+
item_context.set_component_value("prompts", value)
3669+
```
3670+
3671+
Create an `ItemContext` using a tuple of strings and an item ID:
3672+
```python
3673+
with client.item_context(("project_name", "folder_name"), 12345) as context:
3674+
metadata = context.get_metadata()
3675+
print(metadata)
3676+
```
3677+
3678+
Create an `ItemContext` using a tuple of IDs and an item name:
3679+
```python
3680+
with client.item_context((101, 202), "item_name") as context:
3681+
value = context.get_component_value("component_id")
3682+
print(value)
3683+
```
3684+
3685+
Save annotations automatically after modifying component values:
3686+
```python
3687+
with client.item_context("project_name/folder_name", "item_name", overwrite=True) as context:
3688+
context.set_component_value("component_id", "new_value")
3689+
# No need to call .save(), changes are saved automatically on context exit.
3690+
```
3691+
3692+
Handle exceptions during context execution:
3693+
```python
3694+
from superannotate import FileChangedError
3695+
3696+
try:
3697+
with client.item_context((101, 202), "item_name") as context:
3698+
context.set_component_value("component_id", "new_value")
3699+
except FileChangedError as e:
3700+
print(f"An error occurred: {e}")
3701+
```
3702+
"""
3703+
if isinstance(path, str):
3704+
project, folder = self.controller.get_project_folder_by_path(path)
3705+
elif len(path) == 2 and all([isinstance(i, str) for i in path]):
3706+
project = self.controller.get_project(path[0])
3707+
folder = self.controller.get_folder(project, path[1])
3708+
elif len(path) == 2 and all([isinstance(i, int) for i in path]):
3709+
project = self.controller.get_project_by_id(path[0]).data
3710+
folder = self.controller.get_folder_by_id(path[1], project.id).data
3711+
else:
3712+
raise AppException("Invalid path provided.")
3713+
if isinstance(item, int):
3714+
_item = self.controller.get_item_by_id(item_id=item, project=project)
3715+
else:
3716+
items = self.controller.items.list_items(project, folder, name=item)
3717+
if not items:
3718+
raise AppException("Item not found.")
3719+
_item = items[0]
3720+
if project.type != ProjectType.MULTIMODAL:
3721+
raise AppException(
3722+
f"The function is not supported for {project.type.name} projects."
3723+
)
3724+
return ItemContext(
3725+
controller=self.controller,
3726+
project=project,
3727+
folder=folder,
3728+
item=_item,
3729+
overwrite=overwrite,
3730+
)

src/superannotate/lib/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ def setup_logging(level=DEFAULT_LOGGING_LEVEL, file_path=LOG_FILE_LOCATION):
178178
"UploadFileType",
179179
"Tokenization",
180180
"ImageAutoAssignEnable",
181+
"TemplateState",
181182
]
182183

183184
__alL__ = (

src/superannotate/lib/core/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class IntegrationTypeEnum(BaseTitledEnum):
174174
GCP = "gcp", 2
175175
AZURE = "azure", 3
176176
CUSTOM = "custom", 4
177+
DATABRICKS = "databricks", 5
177178

178179

179180
class TrainingStatus(BaseTitledEnum):

src/superannotate/lib/core/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@ class PathError(AppException):
3535
"""
3636
User input Error
3737
"""
38+
39+
40+
class FileChangedError(AppException):
41+
"""
42+
User input Error
43+
"""

src/superannotate/lib/core/service_types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ def ok(self):
114114
return 199 < self.status < 300
115115
return False
116116

117+
def raise_for_status(self):
118+
if not self.ok:
119+
raise AppException(self.error)
120+
117121
@property
118122
def error(self):
119123
if self.res_error:

src/superannotate/lib/core/serviceproviders.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ def get_by_id(self, project_id: int):
9797
def create(self, entity: entities.ProjectEntity) -> ProjectResponse:
9898
raise NotImplementedError
9999

100+
@abstractmethod
101+
def attach_editor_template(
102+
self, team: entities.TeamEntity, project: entities.ProjectEntity, template: dict
103+
) -> ServiceResponse:
104+
raise NotImplementedError
105+
106+
@abstractmethod
107+
def get_editor_template(
108+
self, team: entities.TeamEntity, project: entities.ProjectEntity
109+
) -> ServiceResponse:
110+
raise NotImplementedError
111+
100112
@abstractmethod
101113
def list(self, condition: Condition = None) -> ProjectListResponse:
102114
raise NotImplementedError
@@ -346,6 +358,7 @@ async def get_big_annotation(
346358
project: entities.ProjectEntity,
347359
item: entities.BaseItemEntity,
348360
reporter: Reporter,
361+
transform_version: str = None,
349362
) -> dict:
350363
raise NotImplementedError
351364

@@ -357,6 +370,7 @@ async def list_small_annotations(
357370
item_ids: List[int],
358371
reporter: Reporter,
359372
callback: Callable = None,
373+
transform_version: str = None,
360374
) -> List[dict]:
361375
raise NotImplementedError
362376

@@ -396,6 +410,7 @@ async def upload_small_annotations(
396410
project: entities.ProjectEntity,
397411
folder: entities.FolderEntity,
398412
items_name_data_map: Dict[str, dict],
413+
transform_version: str = None,
399414
) -> UploadAnnotationsResponse:
400415
raise NotImplementedError
401416

@@ -407,6 +422,7 @@ async def upload_big_annotation(
407422
item_id: int,
408423
data: io.StringIO,
409424
chunk_size: int,
425+
transform_version: str = None,
410426
) -> bool:
411427
raise NotImplementedError
412428

@@ -429,6 +445,29 @@ def get_delete_progress(
429445
def get_schema(self, project_type: int, version: str) -> ServiceResponse:
430446
raise NotImplementedError
431447

448+
@abstractmethod
449+
def get_item_annotations(
450+
self,
451+
project: entities.ProjectEntity,
452+
folder: entities.FolderEntity,
453+
item_id: int,
454+
transform_version: str = "llmJsonV2",
455+
) -> ServiceResponse:
456+
raise NotImplementedError
457+
458+
@abstractmethod
459+
def set_item_annotations(
460+
self,
461+
project: entities.ProjectEntity,
462+
folder: entities.FolderEntity,
463+
item_id: int,
464+
data: dict,
465+
overwrite: bool,
466+
transform_version: str = "llmJsonV2",
467+
etag: str = None,
468+
) -> ServiceResponse:
469+
raise NotImplementedError
470+
432471

433472
class BaseIntegrationService(SuperannotateServiceProvider):
434473
@abstractmethod

0 commit comments

Comments
 (0)