diff --git a/src/superannotate/__init__.py b/src/superannotate/__init__.py index 2959deb09..b005463a7 100644 --- a/src/superannotate/__init__.py +++ b/src/superannotate/__init__.py @@ -3,7 +3,7 @@ import sys -__version__ = "4.4.38" +__version__ = "4.4.39dev1" os.environ.update({"sa_version": __version__}) diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index c3512dff1..c0b23c070 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -335,7 +335,12 @@ def get_folder_by_id(self, project_id: int, folder_id: int): exclude={"completedCount", "is_root"} ) - def get_item_by_id(self, project_id: int, item_id: int): + def get_item_by_id( + self, + project_id: int, + item_id: int, + include: List[Literal["custom_metadata", "categories"]] = None, + ): """Returns the item metadata :param project_id: the id of the project @@ -344,16 +349,50 @@ def get_item_by_id(self, project_id: int, item_id: int): :param item_id: the id of the item :type item_id: int + :param include: Specifies additional fields to include in the response. + + Possible values are + + - "custom_metadata": Includes custom metadata attached to the item. + - "categories": Includes categories attached to the item. + :type include: list of str, optional + :return: item metadata :rtype: dict """ project_response = self.controller.get_project_by_id(project_id=project_id) project_response.raise_for_status() - item = self.controller.get_item_by_id( - item_id=item_id, project=project_response.data + + if ( + include + and "categories" in include + and project_response.data.type != ProjectType.MULTIMODAL.value + ): + raise AppException( + "The 'categories' option in the 'include' field is only supported for Multimodal projects." + ) + + # always join assignments for all project types + _include = {"assignments"} + if include: + _include.update(set(include)) + include = list(_include) + + include_custom_metadata = "custom_metadata" in include + if include_custom_metadata: + include.remove("custom_metadata") + + item = self.controller.items.get_item_by_id( + item_id=item_id, project=project_response.data, include=include ) - return BaseSerializer(item).serialize(exclude={"url", "meta"}) + if include_custom_metadata: + item_custom_fields = self.controller.custom_fields.list_fields( + project=project_response.data, item_ids=[item.id] + ) + item.custom_metadata = item_custom_fields[item.id] + + return BaseSerializer(item).serialize(exclude={"url", "meta"}, by_alias=False) def get_team_metadata(self, include: List[Literal["scores"]] = None): """ @@ -5237,7 +5276,7 @@ def item_context( "This function is only supported for Multimodal projects." ) if isinstance(item, int): - _item = self.controller.get_item_by_id(item_id=item, project=project) + _item = self.controller.items.get_item_by_id(item_id=item, project=project) else: items = self.controller.items.list_items(project, folder, name=item) if not items: diff --git a/src/superannotate/lib/infrastructure/controller.py b/src/superannotate/lib/infrastructure/controller.py index ed0b18a3a..74aca75da 100644 --- a/src/superannotate/lib/infrastructure/controller.py +++ b/src/superannotate/lib/infrastructure/controller.py @@ -50,8 +50,6 @@ from lib.core.jsx_conditions import Query from lib.core.reporter import Reporter from lib.core.response import Response -from lib.core.service_types import PROJECT_TYPE_RESPONSE_MAP -from lib.core.usecases import serialize_item_entity from lib.infrastructure.custom_entities import generate_schema from lib.infrastructure.helpers import timed_lru_cache from lib.infrastructure.query_builder import FieldValidationHandler @@ -932,7 +930,7 @@ def process_response( service_provider, items: List[BaseItemEntity], project: ProjectEntity, - folder: FolderEntity, + folder: Optional[FolderEntity] = None, map_fields: bool = True, ) -> List[BaseItemEntity]: """Process the response data and return a list of serialized items.""" @@ -940,7 +938,8 @@ def process_response( for item in items: if map_fields: item = usecases.serialize_item_entity(item, project) - item = usecases.add_item_path(project, folder, item) + if folder: + item = usecases.add_item_path(project, folder, item) else: item = usecases.serialize_item_entity(item, project, map_fields=False) item.annotation_status = service_provider.get_annotation_status_name( @@ -985,6 +984,30 @@ def list_items( data = self.service_provider.item_service.list(project.id, folder.id, query) return self.process_response(self.service_provider, data, project, folder) + def get_item_by_id( + self, + item_id: int, + project: ProjectEntity, + include: List[Literal["categories", "assignments"]] = None, + ) -> BaseItemEntity: + query = EmptyQuery() + + if include: + if "assignments" in include: + query &= Join("assignments") + if "categories" in include: + # join item categories for multimodal projects + query &= Join("categories") + + response = self.service_provider.item_service.get( + project_id=project.id, item_id=item_id, query=query + ) + if response.error: + raise AppException(response.error) + + item = self.process_response(self.service_provider, [response.data], project)[0] + return item + def attach( self, project: ProjectEntity, @@ -1631,19 +1654,6 @@ def get_project_by_id(self, project_id: int): raise AppException("Project not found.") return response - def get_item_by_id(self, item_id: int, project: ProjectEntity) -> BaseItemEntity: - response = self.service_provider.item_service.get( - project_id=project.id, item_id=item_id - ) - if response.error: - raise AppException(response.error) - PROJECT_TYPE_RESPONSE_MAP[project.type] = response.data - item = serialize_item_entity(response.data, project) - item.annotation_status = self.service_provider.get_annotation_status_name( - project, item.annotation_status - ) - return item - def get_project_folder_by_path( self, path: Union[str, Path] ) -> Tuple[ProjectEntity, FolderEntity]: @@ -2044,7 +2054,7 @@ def get_item( self, project: ProjectEntity, folder: FolderEntity, item: Union[int, str] ) -> BaseItemEntity: if isinstance(item, int): - return self.get_item_by_id(item_id=item, project=project) + return self.items.get_item_by_id(item_id=item, project=project) else: return self.items.get_by_name(project, folder, item) diff --git a/src/superannotate/lib/infrastructure/services/item_service.py b/src/superannotate/lib/infrastructure/services/item_service.py index 5a4bbe238..bf1b5ed7b 100644 --- a/src/superannotate/lib/infrastructure/services/item_service.py +++ b/src/superannotate/lib/infrastructure/services/item_service.py @@ -18,9 +18,9 @@ class ItemService(SuperannotateServiceProvider): URL_LIST = "items" URL_GET = "items/{item_id}" - def get(self, project_id: int, item_id: int): + def get(self, project_id: int, item_id: int, query: Query): result = self.client.request( - url=self.URL_GET.format(item_id=item_id), + url=f"{self.URL_GET.format(item_id=item_id)}?{query.build_query()}", method="GET", content_type=BaseItemResponse, headers={ diff --git a/tests/integration/custom_fields/test_custom_schema.py b/tests/integration/custom_fields/test_custom_schema.py index d076a8a90..8bb93a06c 100644 --- a/tests/integration/custom_fields/test_custom_schema.py +++ b/tests/integration/custom_fields/test_custom_schema.py @@ -136,6 +136,11 @@ def test_get_item_metadata(self): item = sa.get_item_metadata( self.PROJECT_NAME, item_name, include_custom_metadata=True ) + # test custom_metadata using get_item_by_id + assert item["custom_metadata"] == payload + item = sa.get_item_by_id( + self._project["id"], item["id"], include=["custom_metadata"] + ) assert item["custom_metadata"] == payload def test_get_item_metadata_without_custom_metadata(self): diff --git a/tests/integration/items/test_attach_category.py b/tests/integration/items/test_attach_category.py index 18195e2b7..c53cba815 100644 --- a/tests/integration/items/test_attach_category.py +++ b/tests/integration/items/test_attach_category.py @@ -92,6 +92,12 @@ def test_attache_category(self): items = sa.list_items(self.PROJECT_NAME, include=["categories"]) assert all(i["categories"][0]["value"] == "category-1" for i in items) + # test get categories to use get_item_by_id + item = sa.get_item_by_id( + self._project["id"], items[0]["id"], include=["categories"] + ) + assert item["categories"][0]["value"] == "category-1" + def test_remove_items_category(self): self._attach_items(self.PROJECT_NAME, ["item-1", "item-2", "item-3"]) sa.create_categories(self.PROJECT_NAME, ["category-1", "category-2"]) diff --git a/tests/integration/work_management/test_pause_resume_user_activity.py b/tests/integration/work_management/test_pause_resume_user_activity.py index 50ae15919..6cd3e3f7f 100644 --- a/tests/integration/work_management/test_pause_resume_user_activity.py +++ b/tests/integration/work_management/test_pause_resume_user_activity.py @@ -67,6 +67,11 @@ def test_pause_and_resume_user_activity(self): [i["name"] for i in self.ATTACHMENT_LIST], scapegoat["email"], ) + # test assignments use get_item_by_id + items_id = [i["id"] for i in sa.list_items(self.PROJECT_NAME)][0] + item = sa.get_item_by_id(project_id=self._project["id"], item_id=items_id) + assert len(item["assignments"]) == 1 + assert item["assignments"][0]["user_role"] == "QA" @skip("For not send real email") def test_pause_resume_pending_user(self):