Skip to content
Open
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
254 changes: 253 additions & 1 deletion src/superannotate_core/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import asyncio
import decimal
import logging
from collections import defaultdict
from functools import wraps
from operator import itemgetter
from pathlib import Path
Expand All @@ -17,19 +19,25 @@
from superannotate_core.core.conditions import Condition
from superannotate_core.core.conditions import CONDITION_EQ as EQ
from superannotate_core.core.conditions import EmptyCondition
from superannotate_core.core.constants import PROJECT_SETTINGS_VALID_ATTRIBUTES
from superannotate_core.core.constants import SPECIAL_CHARACTERS_IN_PROJECT_FOLDER_NAMES
from superannotate_core.core.entities import AnnotationClassEntity
from superannotate_core.core.entities import AttributeGroupSchema
from superannotate_core.core.entities import BaseItemEntity
from superannotate_core.core.entities import FolderEntity
from superannotate_core.core.entities import ImageEntity
from superannotate_core.core.entities import ProjectEntity
from superannotate_core.core.entities import SettingEntity
from superannotate_core.core.entities import ViedoEntity
from superannotate_core.core.entities.project import WorkflowEntity
from superannotate_core.core.enums import AnnotationStatus
from superannotate_core.core.enums import ApprovalStatus
from superannotate_core.core.enums import ClassTypeEnum
from superannotate_core.core.enums import FolderStatus
from superannotate_core.core.enums import ImageQuality
from superannotate_core.core.enums import ProjectType
from superannotate_core.core.enums import UploadStateEnum
from superannotate_core.core.enums import UserRole
from superannotate_core.core.exceptions import SAException
from superannotate_core.core.exceptions import SAInvalidInput
from superannotate_core.core.exceptions import SAValidationException
Expand All @@ -42,12 +50,16 @@
from superannotate_core.infrastructure.repositories import FolderRepository
from superannotate_core.infrastructure.repositories import ItemRepository
from superannotate_core.infrastructure.repositories import ProjectRepository
from superannotate_core.infrastructure.repositories import SettingRepository
from superannotate_core.infrastructure.repositories import SubsetRepository
from superannotate_core.infrastructure.repositories.item_repository import Attachment
from superannotate_core.infrastructure.repositories.item_repository import (
AttachmentMeta,
)
from superannotate_core.infrastructure.repositories.utils import run_async
from superannotate_core.infrastructure.repositories.workflow_repository import (
WorkflowRepository,
)
from superannotate_core.infrastructure.session import Session


Expand Down Expand Up @@ -599,6 +611,7 @@ class VideoItem(Item, ViedoEntity):
ProjectType.Video: VideoItem,
ProjectType.Tiled: ImageItem,
ProjectType.Document: ImageItem,
ProjectType.GenAI: ImageItem,
}


Expand Down Expand Up @@ -1038,12 +1051,112 @@ def get_item(self, pk: Union[str, int], include_custom_metadata=False):
include_custom_metadata=include_custom_metadata,
)

# TODO delete after adding validation in backend
@staticmethod
def _validate_settings(
project_type: ProjectType, settings: List[SettingEntity]
) -> None:
for setting in settings:
if setting.attribute not in PROJECT_SETTINGS_VALID_ATTRIBUTES:
settings.remove(setting)
if setting.attribute == "ImageQuality" and isinstance(setting.value, str):
setting.value = ImageQuality.get_value(setting.value)
elif setting.attribute == "ImageQuality" and project_type in [
ProjectType.Video.value,
ProjectType.Document.value,
]:
raise SAValidationException(
"The function does not support projects containing"
" videos / documents attached with URLs"
)
elif setting.attribute == "FrameRate":
if not project_type == ProjectType.Video.value:
raise SAValidationException(
"FrameRate is available only for Video projects"
)
try:
setting.value = float(setting.value)
if (
not (0.0001 < setting.value < 120)
or decimal.Decimal(str(setting.value)).as_tuple().exponent < -3
):
raise SAValidationException(
"The FrameRate value range is between 0.001 - 120"
)
frame_mode = next(
filter(lambda x: x.attribute == "FrameMode", settings),
None,
)
if not frame_mode:
settings.append(
SettingEntity.from_json(
{"attribute": "FrameMode", "value": 1}
)
)
except ValueError:
raise SAValidationException("The FrameRate value should be float")

# TODO delete after adding validation in backend
@staticmethod
def _validate_project_name(session: Session, project_name: str) -> str:
if (
len(
set(project_name).intersection(
SPECIAL_CHARACTERS_IN_PROJECT_FOLDER_NAMES
)
)
> 0
):
project_name = "".join(
"_" if char in SPECIAL_CHARACTERS_IN_PROJECT_FOLDER_NAMES else char
for char in project_name
)
logger.warning(
"New folder name has special characters. Special characters will be replaced by underscores."
)
condition = Condition("name", project_name, EQ)
projects_list = ProjectRepository(session).list(condition=condition)
for project in projects_list:
if project.name == project_name:
logger.error("There are duplicated names.")
raise SAValidationException(
f"Project name {project_name} is not unique. "
f"To use SDK please make project names unique."
)
return project_name

@classmethod
def create(cls, session, **data) -> "Project":
project_name = data.get("name")
if project_name:
data["name"] = cls._validate_project_name(session, project_name)
settings = data.get("settings")
if settings:
settings = [SettingEntity.from_json(i) for i in settings]
cls._validate_settings(data.get("type"), settings)
data["settings"] = settings
return cls._from_entity(
ProjectRepository(session).create(ProjectEntity(**data))
ProjectRepository(session).create(ProjectEntity.from_json(data))
)

def update(self, **data) -> "Project":
if "id" not in data.keys():
data["id"] = self.id
project_name = data.get("name")
if project_name:
data["name"] = self._validate_project_name(self.session, project_name)
settings = data.get("settings")
if settings:
settings = [SettingEntity.from_json(i) for i in settings]
self._validate_settings(data.get("type"), settings)
data["settings"] = settings
return self._from_entity(
ProjectRepository(self.session).update(ProjectEntity.from_json(data))
)

def delete(self):
return ProjectRepository(self.session).delete(self.id)

def create_folder(self, name: str) -> Folder:
return Folder.create(self.session, project_id=self.id, name=name)

Expand Down Expand Up @@ -1191,3 +1304,142 @@ def delete_custom_field(self, fields: List[str]):
return CustomFieldRepository(session=self.session).delete_fields(
project_id=self.id, field_names=fields
)

def list_workflows(self):
workflows = WorkflowRepository(session=self.session).list_workflows(
project_id=self.id
)
annotation_classes = self.list_annotation_classes()
annotation_classes_id_name_map = {i.id: i.name for i in annotation_classes}
for workflow in workflows:
workflow.className = annotation_classes_id_name_map.get(workflow.class_id)
return workflows

def set_workflows(self, workflows: List[dict]) -> None:
annotation_classes = self.list_annotation_classes()
annotation_classes_map = {}
annotations_classes_attributes_map = {}
for annotation_class in annotation_classes:
annotation_classes_map[annotation_class.name] = annotation_class.id
for attribute_group in annotation_class.attribute_groups:
for attribute in attribute_group["attributes"]:
annotations_classes_attributes_map[
f"{annotation_class.name}__{attribute_group['name']}__{attribute['name']}"
] = attribute["id"]

for workflow in [i for i in workflows if "className" in i]:
if workflow.get("id"):
del workflow["id"]
workflow["class_id"] = annotation_classes_map.get(
workflow["className"], None
)
if not workflow["class_id"]:
raise SAException("Annotation class not found.")

workflows = [WorkflowEntity.from_json(i) for i in workflows]
WorkflowRepository(self.session).set_workflows(
project_id=self.id, workflows=workflows
)
existing_workflows = self.list_workflows()
existing_workflows_map = {}
for workflow in existing_workflows:
existing_workflows_map[workflow.step] = workflow.id

attributes = []
for workflow in workflows:
annotation_class_name = workflow.className
for attribute in workflow.attribute:
attribute_name = attribute["attribute"]["name"]
attribute_group_name = attribute["attribute"]["attribute_group"]["name"]
if not annotations_classes_attributes_map.get(
f"{annotation_class_name}__{attribute_group_name}__{attribute_name}",
None,
):
raise SAException(
f"Attribute group name or attribute name not found {attribute_group_name}."
)

if not existing_workflows_map.get(workflow.step, None):
raise SAException("Couldn't find step in workflow")
attributes.append(
{
"workflow_id": existing_workflows_map[workflow.step],
"attribute_id": annotations_classes_attributes_map[
f"{annotation_class_name}__{attribute_group_name}__{attribute_name}"
],
}
)
WorkflowRepository(self.session).set_project_workflow_attributes(
project_id=self.id, attributes=attributes
)
self.workflows = self.list_workflows()
return self.workflows

def list_settings(self) -> List[SettingEntity]:
return SettingRepository(session=self.session).list(project_id=self.id)

def set_settings(self, settings: List[dict]) -> List[SettingEntity]:
settings = [SettingEntity.from_json(i) for i in settings]
self._validate_settings(project_type=self.type, settings=settings)

attr_id_mapping = {i.attribute: i.id for i in self.list_settings()}
new_settings_to_update = []
for new_setting in settings:
new_settings_to_update.append(
SettingEntity.from_json(
{
"id": attr_id_mapping[new_setting.attribute],
"attribute": new_setting.attribute,
"value": new_setting.value,
}
)
)
return SettingRepository(session=self.session).set_settings(
project_id=self.id, settings=new_settings_to_update
)

def add_contributors(self, contributors: List[dict]):
team_data = self.session.get_team()
team_users = set()
project_users = set()
if self.users:
project_users = {user["user_id"] for user in self.users}
for user in team_data["users"]:
if user["user_role"] > UserRole.Admin.value:
team_users.add(user["email"])
# collecting pending team users which is not admin
for user in team_data["pending_invitations"]:
if user["user_role"] > UserRole.Admin.value:
team_users.add(user["email"])
role_email_map = defaultdict(list)
to_skip = []
to_add = []
for contributor in contributors:
role_email_map[contributor["user_role"]].append(contributor["user_id"])
for role, emails in role_email_map.items():
_to_add = list(team_users.intersection(emails) - project_users)
to_add.extend(_to_add)
to_skip.extend(list(set(emails).difference(_to_add)))
if _to_add:
response = ProjectRepository(session=self.session).share(
project_id=self.id,
users=[
dict(
user_id=user_id,
user_role=role,
)
for user_id in _to_add
],
)
if response and not response.get("invalidUsers"):
logger.info(
f"Added {len(_to_add)}/{len(emails)} "
f"contributors to the project {self.name} with the {UserRole.get_name(role)} role."
)

if to_skip:
logger.warning(
f"Skipped {len(to_skip)}/{len(contributors)} "
"contributors that are out of the team scope or already have access to the project."
)
return to_add, to_skip
30 changes: 30 additions & 0 deletions src/superannotate_core/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,33 @@
ANNOTATION_FILE_SIZE_THRESHOLD = 15 * 1024 * 1024 # 15 MB
SMALL_ANNOTATIONS_MEMERY_LIMIT = 3 * ANNOTATION_FILE_SIZE_THRESHOLD
LARGE_ANNOTATIONS_MEMERY_LIMIT = 5 * ANNOTATION_FILE_SIZE_THRESHOLD
PROJECT_SETTINGS_VALID_ATTRIBUTES = [
"Brightness",
"Fill",
"Contrast",
"ShowLabels",
"ShowComments",
"Image",
"Lines",
"AnnotatorFinish",
"PointSize",
"FontSize",
"WorkflowEnable",
"ClassChange",
"ShowEntropy",
"UploadImages",
"DeleteImages",
"Download",
"RunPredictions",
"RunSegmentations",
"ImageQuality",
"ImageAutoAssignCount",
"FrameMode",
"FrameRate",
"JumpBackward",
"JumpForward",
"UploadFileType",
"Tokenization",
"ImageAutoAssignEnable",
]
SPECIAL_CHARACTERS_IN_PROJECT_FOLDER_NAMES = set('/\\:*?"<>|“')
4 changes: 2 additions & 2 deletions src/superannotate_core/core/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
from superannotate_core.core.entities.item import ViedoEntity
from superannotate_core.core.entities.project import FolderEntity
from superannotate_core.core.entities.project import ProjectEntity
from superannotate_core.core.entities.project import Setting
from superannotate_core.core.entities.project import SettingEntity


__all__ = [
"ProjectEntity",
"FolderEntity",
"AnnotationClassEntity",
"Setting",
"SettingEntity",
"AttributeGroupSchema",
"BaseItemEntity",
"ViedoEntity",
Expand Down
2 changes: 2 additions & 0 deletions src/superannotate_core/core/entities/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
import inspect
import typing
from abc import ABC
Expand Down Expand Up @@ -152,6 +153,7 @@ def dict(

@classmethod
def from_json(cls, data: dict):
data = copy.copy(data)
alias_handler = getattr(cls, "ALIAS_HANDLER", None)
if alias_handler:
data = alias_handler.handle(data, raise_exception=False)
Expand Down
Loading