Skip to content

Commit 2b20188

Browse files
authored
Merge pull request #729 from superannotateai/develop
Develop
2 parents 31d6c2f + ce3c84f commit 2b20188

File tree

21 files changed

+833
-80
lines changed

21 files changed

+833
-80
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ History
66

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

9+
4.4.28 - Dec 13, 2024
10+
________________________
11+
**Added**
12+
13+
- ``SAClient.item_context`` creates an “ItemContext” for managing item annotations and metadata.
14+
915
4.4.27 - Nov 14, 2024
1016
________________________
1117
**Fixed**

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

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ minversion = 3.7
33
log_cli=true
44
python_files = test_*.py
55
;pytest_plugins = ['pytest_profiling']
6-
addopts = -n 4 --dist loadscope
6+
;addopts = -n 4 --dist loadscope

src/superannotate/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44

55

6-
__version__ = "4.4.27"
6+
__version__ = "4.4.28"
77

88
os.environ.update({"sa_version": __version__})
99
sys.path.append(os.path.split(os.path.realpath(__file__))[0])
@@ -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: 199 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,115 @@ 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” 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+
)

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ class IntegrationTypeEnum(BaseTitledEnum):
174174
GCP = "gcp", 2
175175
AZURE = "azure", 3
176176
CUSTOM = "custom", 4
177+
DATABRICKS = "databricks", 5
178+
SNOWFLAKE = "snowflake", 6
177179

178180

179181
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:

0 commit comments

Comments
 (0)