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
1 change: 1 addition & 0 deletions docs/source/superannotate.sdk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ ______
.. _ref_search_images:
.. autofunction:: superannotate.search_images
.. autofunction:: superannotate.search_images_all_folders
.. autofunction:: superannotate.query
.. autofunction:: superannotate.get_image_metadata
.. autofunction:: superannotate.download_image
.. autofunction:: superannotate.set_image_annotation_status
Expand Down
3 changes: 3 additions & 0 deletions src/superannotate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from superannotate.lib.app.interface.sdk_interface import move_images
from superannotate.lib.app.interface.sdk_interface import pin_image
from superannotate.lib.app.interface.sdk_interface import prepare_export
from superannotate.lib.app.interface.sdk_interface import query
from superannotate.lib.app.interface.sdk_interface import rename_project
from superannotate.lib.app.interface.sdk_interface import run_prediction
from superannotate.lib.app.interface.sdk_interface import search_annotation_classes
Expand Down Expand Up @@ -171,6 +172,8 @@
"search_folders",
"assign_folder",
"unassign_folder",
# Entities Section
"query",
# Image Section
"copy_images",
"move_images",
Expand Down
1 change: 0 additions & 1 deletion src/superannotate/lib/app/interface/cli_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from lib.infrastructure.repositories import ConfigRepository



class CLIFacade(BaseInterfaceFacade):
"""
With SuperAnnotate CLI, basic tasks can be accomplished using shell commands:
Expand Down
24 changes: 21 additions & 3 deletions src/superannotate/lib/app/interface/sdk_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -2955,9 +2955,6 @@ def attach_items_from_integrated_storage(
:param project: project name or folder path where items should be attached (e.g., “project1/folder1”).
:type project: str

:param project: project name or folder path where items should be attached (e.g., “project1/folder1”).
:type project: str

:param integration: existing integration name or metadata dict to pull items from.
Mandatory keys in integration metadata’s dict is “name”.
:type integration: str or dict
Expand All @@ -2972,3 +2969,24 @@ def attach_items_from_integrated_storage(
response = Controller.get_default().attach_integrations(project_name, folder_name, integration, folder_path)
if response.errors:
raise AppException(response.errors)


@Trackable
@validate_arguments
def query(project: NotEmptyStr, query: Optional[NotEmptyStr]):
"""Return items

:param project: project name or folder path (e.g., “project1/folder1”)
:type project: str

:param query: SAQuL query string.
:type query: str

:return: queried items’ metadata list
:rtype: list of dicts
"""
project_name, folder_name = extract_project_folder(project)
response = Controller.get_default().query_entities(project_name, folder_name, query)
if response.errors:
raise AppException(response.errors)
return BaseSerializers.serialize_iterable(response.data)
14 changes: 12 additions & 2 deletions src/superannotate/lib/app/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ class BaseSerializers(ABC):
def __init__(self, entity: BaseEntity):
self._entity = entity

@staticmethod
def _fill_enum_values(data: dict):
if isinstance(data, dict):
for key, value in data.items():
if hasattr(value, "_type") and value._type == "titled_enum":
data[key] = value.__doc__
return data

def serialize(self, fields: List[str] = None, by_alias: bool = True, flat: bool = False):
return self._serialize(self._entity, fields, by_alias, flat)
return self._fill_enum_values(self._serialize(self._entity, fields, by_alias, flat))

@staticmethod
def _serialize(entity: Any, fields: List[str] = None, by_alias: bool = False, flat: bool = False):
Expand All @@ -43,7 +51,9 @@ def serialize_iterable(
) -> List[Any]:
serialized_data = []
for i in data:
serialized_data.append(cls._serialize(i, fields, by_alias, flat))
serialized_data.append(
cls._fill_enum_values(cls._serialize(i, fields, by_alias, flat))
)
return serialized_data


Expand Down
34 changes: 34 additions & 0 deletions src/superannotate/lib/core/entities/project_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
from typing import Any
from typing import Iterable
from typing import List
from typing import Optional
from typing import Union

from lib.core.enums import AnnotationStatus
from lib.core.enums import ClassTypeEnum
from lib.core.enums import SegmentationStatus
from pydantic import BaseModel
from pydantic import Field
from superannotate_schemas.schemas.classes import AnnotationClass


Expand Down Expand Up @@ -477,3 +481,33 @@ def to_dict(self):
"is_global": self.is_global,
**self.hyper_parameters,
}


class Entity(BaseModel):
id: int
name: str
path: Optional[str] = Field(None, description="Item’s path in SuperAnnotate project")
url: Optional[str] = Field(None, description="Publicly available HTTP address")
annotation_status: AnnotationStatus = Field(description="Item annotation status")
annotator_name: Optional[str] = Field(description="Annotator email")
qa_name: Optional[str] = Field(description="QA email")
entropy_value: Optional[str] = Field(description="Priority score of given item")
created_at: str = Field(alias="createdAt", description="Date of creation")
updated_at: str = Field(alias="updatedAt", description="Update date")

class Config:
ignore_extra = True


class TmpImageEntity(Entity):
prediction_status: Optional[SegmentationStatus] = Field(SegmentationStatus.NOT_STARTED)
segmentation_status: Optional[SegmentationStatus] = Field(SegmentationStatus.NOT_STARTED)
approval_status: bool = None


class VideoEntity(Entity):
pass


class DocumentEntity(Entity):
pass
29 changes: 18 additions & 11 deletions src/superannotate/lib/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,44 @@
from types import DynamicClassAttribute


class BaseTitledEnum(Enum):
class BaseTitledEnum(int, Enum):
def __new__(cls, title, value):
obj = int.__new__(cls, value)
obj._value_ = value
obj.__doc__ = title
obj._type = "titled_enum"
return obj

@DynamicClassAttribute
def name(self) -> str:
return super().value[0]
return self.__doc__

@DynamicClassAttribute
def value(self):
return super().value[1]
return super().value

@classmethod
def get_name(cls, value):
for enum in list(cls):
if enum.value == value:
return enum.name
return enum.__doc__

@classmethod
def get_value(cls, name):
for enum in list(cls):
if enum.name.lower() == name.lower():
if enum.__doc__.lower() == name.lower():
return enum.value

@classmethod
def values(cls):
return [enum.name.lower() for enum in list(cls)]
return [enum.__doc__.lower() for enum in list(cls)]

@classmethod
def titles(cls):
return [enum.name for enum in list(cls)]
return [enum.__doc__ for enum in list(cls)]

def equals(self, other: Enum):
return self.name.lower() == other.lower()
return self.__doc__.lower() == other.__doc__.lower()


class ProjectType(BaseTitledEnum):
Expand Down Expand Up @@ -92,9 +99,9 @@ class ClassTypeEnum(BaseTitledEnum):
@classmethod
def get_value(cls, name):
for enum in list(cls):
if enum.name.lower() == name.lower():
if enum.__doc__.lower() == name.lower():
return enum.value
return "object"
return cls.OBJECT.value


class TrainingStatus(BaseTitledEnum):
Expand All @@ -113,7 +120,7 @@ class SegmentationStatus(BaseTitledEnum):
FAILED = "Failed", 4


class TrainingTask(BaseTitledEnum):
class TrainingTask:
INSTANCE_SEGMENTATION_PIXEL = (
"Instance Segmentation for Pixel Projects",
"instance_segmentation_pixel",
Expand Down
7 changes: 5 additions & 2 deletions src/superannotate/lib/core/service_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,17 @@ class ServiceResponse(BaseModel):
content: Union[bytes, str]
data: Any

def __init__(self, response, content_type):
def __init__(self, response, content_type=None):
data = {
"status": response.status_code,
"reason": response.reason,
"content": response.content,
}
if response.ok:
data["data"] = content_type(**response.json())
if content_type:
data["data"] = content_type(**response.json())
else:
data["data"] = response.json()
super().__init__(**data)

@property
Expand Down
6 changes: 6 additions & 0 deletions src/superannotate/lib/core/serviceproviders.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,9 @@ def get_integrations(self, team_id: int) -> List[dict]:
def attach_integrations(self, team_id: int, project_id: int, integration_id: int, folder_id: int,
folder_name: str) -> bool:
raise NotImplementedError

def saqul_query(self, team_id: int, project_id: int, folder_id: int, query: str) -> ServiceResponse:
raise NotImplementedError

def validate_saqul_query(self, team_id: int, project_id: int, query: str) -> dict:
raise NotImplementedError
1 change: 1 addition & 0 deletions src/superannotate/lib/core/usecases/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from lib.core.usecases.annotations import * # noqa: F403 F401
from lib.core.usecases.entities import * # noqa: F403 F401
from lib.core.usecases.folders import * # noqa: F403 F401
from lib.core.usecases.images import * # noqa: F403 F401
from lib.core.usecases.integrations import * # noqa: F403 F401
Expand Down
54 changes: 54 additions & 0 deletions src/superannotate/lib/core/usecases/entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import List

import superannotate.lib.core as constances
from lib.core.entities.project_entities import Entity
from lib.core.entities.project_entities import FolderEntity
from lib.core.entities.project_entities import ProjectEntity
from lib.core.entities.project_entities import TmpImageEntity
from lib.core.exceptions import AppException
from lib.core.reporter import Reporter
from lib.core.response import Response
from lib.core.serviceproviders import SuperannotateServiceProvider
from lib.core.usecases.base import BaseReportableUseCae
from pydantic import parse_obj_as


class QueryEntities(BaseReportableUseCae):
def __init__(
self,
reporter: Reporter,
project: ProjectEntity,
folder: FolderEntity,
backend_service_provider: SuperannotateServiceProvider,
query: str

):
super().__init__(reporter)
self._project = project
self._folder = folder
self._backend_client = backend_service_provider
self._query = query

def validate_query(self):
response = self._backend_client.validate_saqul_query(self._project.team_id, self._project.uuid, self._query)
if response.get("error"):
raise AppException(response["error"])
if not response.get("isValidQuery", False):
raise AppException("Incorrect query.")

def execute(self) -> Response:
if self.is_valid():
service_response = self._backend_client.saqul_query(
self._project.team_id,
self._project.uuid,
self._folder.uuid,
self._query
)
if service_response.ok:
if self._project.project_type == constances.ProjectType.VECTOR.value:
self._response.data = parse_obj_as(List[TmpImageEntity], service_response.data)
else:
self._response.data = parse_obj_as(List[Entity], service_response.data)
else:
self._response.errors = service_response.data
return self._response
13 changes: 13 additions & 0 deletions src/superannotate/lib/infrastructure/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -1693,3 +1693,16 @@ def attach_integrations(self, project_name: str, folder_name: str, integration:
folder_path=folder_path
)
return use_case.execute()

def query_entities(self, project_name: str, folder_name: str, query: str = None):
project = self._get_project(project_name)
folder = self._get_folder(project, folder_name)

use_case = usecases.QueryEntities(
reporter=self.default_reporter,
project=project,
folder=folder,
query=query,
backend_service_provider=self.backend_client
)
return use_case.execute()
39 changes: 39 additions & 0 deletions src/superannotate/lib/infrastructure/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class BaseBackendService(SuperannotateServiceProvider):
AUTH_TYPE = "sdk"
PAGINATE_BY = 100
LIMIT = 100
MAX_ITEMS_COUNT = 500 * 1000

"""
Base service class
Expand Down Expand Up @@ -226,6 +227,8 @@ class SuperannotateBackendService(BaseBackendService):
URL_UPLOAD_PRIORITY_SCORES = "images/updateEntropy"
URL_GET_INTEGRATIONS = "integrations"
URL_ATTACH_INTEGRATIONS = "image/integration/create"
URL_SAQUL_QUERY = "/images/search/advanced"
URL_VALIDATE_SAQUL_QUERY = "/images/validate/advanced"

def upload_priority_scores(
self, team_id: int, project_id: int, folder_id: int, priorities: list
Expand Down Expand Up @@ -1093,3 +1096,39 @@ def attach_integrations(
data=data
)
return response.ok

def saqul_query(self, team_id: int, project_id: int, folder_id: int, query: str) -> ServiceResponse:
CHUNK_SIZE = 50
query_url = urljoin(self.api_url, self.URL_SAQUL_QUERY)
params = {
"team_id": team_id,
"project_id": project_id,
"folder_id": folder_id,
}
data = {
"query": query,
"image_index": 0
}
items = []
for _ in range(self.MAX_ITEMS_COUNT):
response = self._request(query_url, "post", params=params, data=data)
if response.ok:
response_items = response.json()
items.extend(response_items)
if len(response_items) < CHUNK_SIZE:
service_response = ServiceResponse(response)
service_response.data = items
return service_response
data["image_index"] += CHUNK_SIZE
return ServiceResponse(response)

def validate_saqul_query(self, team_id: int, project_id: int, query: str) -> dict:
validate_query_url = urljoin(self.api_url, self.URL_VALIDATE_SAQUL_QUERY)
params = {
"team_id": team_id,
"project_id": project_id,
}
data = {
"query": query,
}
return self._request(validate_query_url, "post", params=params, data=data).json()
2,944 changes: 2,943 additions & 1 deletion tests/data_set/sample_project_vector/example_image_1.jpg___objects.json

Large diffs are not rendered by default.

Loading