diff --git a/src/superannotate_core/app/__init__.py b/src/superannotate_core/app/__init__.py index 01ea25c..b82ac6b 100644 --- a/src/superannotate_core/app/__init__.py +++ b/src/superannotate_core/app/__init__.py @@ -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 @@ -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 @@ -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 @@ -599,6 +611,7 @@ class VideoItem(Item, ViedoEntity): ProjectType.Video: VideoItem, ProjectType.Tiled: ImageItem, ProjectType.Document: ImageItem, + ProjectType.GenAI: ImageItem, } @@ -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) @@ -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 diff --git a/src/superannotate_core/core/constants.py b/src/superannotate_core/core/constants.py index 8deceb3..3ef6d96 100644 --- a/src/superannotate_core/core/constants.py +++ b/src/superannotate_core/core/constants.py @@ -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('/\\:*?"<>|“') diff --git a/src/superannotate_core/core/entities/__init__.py b/src/superannotate_core/core/entities/__init__.py index fb9c0b8..7ac3606 100644 --- a/src/superannotate_core/core/entities/__init__.py +++ b/src/superannotate_core/core/entities/__init__.py @@ -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", diff --git a/src/superannotate_core/core/entities/base.py b/src/superannotate_core/core/entities/base.py index a0c4c29..dc3bce1 100644 --- a/src/superannotate_core/core/entities/base.py +++ b/src/superannotate_core/core/entities/base.py @@ -1,3 +1,4 @@ +import copy import inspect import typing from abc import ABC @@ -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) diff --git a/src/superannotate_core/core/entities/project.py b/src/superannotate_core/core/entities/project.py index 58ad4d3..2d547e0 100644 --- a/src/superannotate_core/core/entities/project.py +++ b/src/superannotate_core/core/entities/project.py @@ -1,15 +1,28 @@ from __future__ import annotations -from typing import Any from typing import List +from typing import Optional +from typing import Union from superannotate_core.core.entities.base import AliasHandler from superannotate_core.core.entities.base import BaseEntity from superannotate_core.core.entities.base import TimedEntity +from superannotate_core.core.entities.classes import AnnotationClassEntity from superannotate_core.core.entities.user import ContributorEntity from superannotate_core.core.enums import FolderStatus from superannotate_core.core.enums import ProjectStatus from superannotate_core.core.enums import ProjectType +from superannotate_core.core.enums import UploadStateEnum + + +class WorkflowEntity(BaseEntity): + id: Optional[int] + project_id: Optional[int] + class_id: Optional[int] + className: Optional[str] + step: Optional[int] + tool: Optional[int] + attribute: List[dict] class FolderEntity(TimedEntity): @@ -30,11 +43,11 @@ def __repr__(self): ) -class Setting(BaseEntity): +class SettingEntity(BaseEntity): id: int project_id: int attribute: str - value: Any + value: Union[str, int, float, bool] def __repr__(self): return ( @@ -43,7 +56,7 @@ def __repr__(self): ) -class ProjectEntity(BaseEntity): +class ProjectEntity(TimedEntity): id: int team_id: int name: str @@ -56,14 +69,18 @@ class ProjectEntity(BaseEntity): status: ProjectStatus folder_id: int sync_status: int - upload_state: int + upload_state: UploadStateEnum users: List[ContributorEntity] + settings: List[SettingEntity] + classes: List[AnnotationClassEntity] = [] + workflows: Optional[List[WorkflowEntity]] = [] + completed_items_count: int class Meta: alias_handler = AliasHandler( { "imageCount": "item_count", - "completedImagesCount": "completed_images_count", + "completedImagesCount": "completed_items_count", "rootFolderCompletedImagesCount": "root_folder_completed_images_count", } ) diff --git a/src/superannotate_core/core/enums.py b/src/superannotate_core/core/enums.py index 08c1c46..c657113 100644 --- a/src/superannotate_core/core/enums.py +++ b/src/superannotate_core/core/enums.py @@ -13,6 +13,22 @@ def __get__(self, instance, owner): ApprovalStatus = IntEnum("ApprovalStatus", {"None": 0, "Disapproved": 1, "Approved": 2}) +class BaseIntEnum(IntEnum): + @classmethod + def get_value(cls, name): + for enum in list(cls): + if enum.name.lower() == name.lower(): + return enum.value + return cls.object.value + + @classmethod + def get_name(cls, value): + for enum in list(cls): + if enum.value == value: + return enum.name + return cls.object.name + + class AnnotationTypes(str, Enum): BBOX = "bbox" EVENT = "event" @@ -21,7 +37,7 @@ class AnnotationTypes(str, Enum): POLYLINE = "polyline" -class ProjectType(IntEnum): +class ProjectType(BaseIntEnum): Vector = 1 Pixel = 2 Video = 3 @@ -38,7 +54,7 @@ def images(self): return self.Vector, self.Pixel.value, self.Tiled.value -class UserRole(Enum): +class UserRole(BaseIntEnum): Superadmin = 1 Admin = 2 Annotator = 3 @@ -47,18 +63,18 @@ class UserRole(Enum): Viewer = 6 -class UploadStateEnum(IntEnum): +class UploadStateEnum(BaseIntEnum): INITIAL = 1 BASIC = 2 EXTERNAL = 3 -class ImageQuality(Enum): +class ImageQuality(BaseIntEnum): original = 100 compressed = 60 -class ProjectStatus(Enum): +class ProjectStatus(BaseIntEnum): Undefined = -1 NotStarted = 1 InProgress = 2 @@ -66,7 +82,7 @@ class ProjectStatus(Enum): OnHold = 4 -class SegmentationStatus(IntEnum): +class SegmentationStatus(BaseIntEnum): NotStarted = 1 InProgress = 2 Completed = 3 @@ -88,7 +104,7 @@ class GroupTypeEnum(str, Enum): # relationship = 3 -class FolderStatus(IntEnum): +class FolderStatus(BaseIntEnum): Undefined = -1 NotStarted = 1 InProgress = 2 @@ -96,14 +112,14 @@ class FolderStatus(IntEnum): OnHold = 4 -class ExportStatus(Enum): +class ExportStatus(BaseIntEnum): inProgress = 1 complete = 2 canceled = 3 error = 4 -class AnnotationStatus(IntEnum): +class AnnotationStatus(BaseIntEnum): NotStarted = 1 InProgress = 2 QualityCheck = 3 @@ -112,20 +128,13 @@ class AnnotationStatus(IntEnum): Skipped = 6 -class ClassTypeEnum(IntEnum): +class ClassTypeEnum(BaseIntEnum): object = 1 tag = 2 relationship = 3 - @classmethod - def get_value(cls, name): - for enum in list(cls): - if enum.name.lower() == name.lower(): - return enum.value - return cls.object.value - -class IntegrationTypeEnum(Enum): +class IntegrationTypeEnum(BaseIntEnum): aws = 1 gcp = 2 azure = 3 diff --git a/src/superannotate_core/infrastructure/repositories/proejct_repository.py b/src/superannotate_core/infrastructure/repositories/proejct_repository.py index 834ed28..e246a97 100644 --- a/src/superannotate_core/infrastructure/repositories/proejct_repository.py +++ b/src/superannotate_core/infrastructure/repositories/proejct_repository.py @@ -10,6 +10,7 @@ class ProjectRepository(BaseHttpRepository): URL_CREATE = "project" URL_LIST = "projects" URL_RETRIEVE = "project/{project_id}" + URL_SHARE = "project/{project_id}/share/bulk" def get_by_id(self, pk: int) -> ProjectEntity: response = self._session.request( @@ -26,19 +27,34 @@ def list(self, condition: Condition) -> List[ProjectEntity]: ) return self.serialize_entity(data) - def create(self, entity: ProjectEntity) -> ProjectEntity: - response = self._session.request(self.URL_CREATE, "post", data=entity.to_json()) + def create(self, project: ProjectEntity) -> ProjectEntity: + response = self._session.request( + self.URL_CREATE, "post", json=project.to_json(exclude_none=True) + ) response.raise_for_status() return self.serialize_entity(response.json()) - def update(self, entity: ProjectEntity) -> ProjectEntity: + def update(self, project: ProjectEntity) -> ProjectEntity: response = self._session.request( - self.URL_RETRIEVE.format(entity.id), + self.URL_RETRIEVE.format(project_id=project.id), "put", - data=entity.to_json(), + json=project.to_json(exclude_none=True), ) response.raise_for_status() return self.serialize_entity(response.json()) - def delete(self, pk: int) -> None: - return self._session.request(self.URL_RETRIEVE.format(pk), "delete") + def delete(self, project_id: int) -> None: + response = self._session.request( + self.URL_RETRIEVE.format(project_id=project_id), "delete" + ) + response.raise_for_status() + return response.json() + + def share(self, project_id: int, users: List[dict]): + response = self._session.request( + self.URL_SHARE.format(project_id=project_id), + "post", + json={"users": users}, + ) + response.raise_for_status() + return response.json() diff --git a/src/superannotate_core/infrastructure/repositories/setting_repository.py b/src/superannotate_core/infrastructure/repositories/setting_repository.py index e39ba8d..335b67a 100644 --- a/src/superannotate_core/infrastructure/repositories/setting_repository.py +++ b/src/superannotate_core/infrastructure/repositories/setting_repository.py @@ -1,23 +1,33 @@ from typing import List from superannotate_core.core.conditions import Condition -from superannotate_core.core.entities import Setting +from superannotate_core.core.entities import SettingEntity from superannotate_core.infrastructure.repositories.base import BaseHttpRepository class SettingRepository(BaseHttpRepository): - URL_CREATE = "project" - URL_LIST = "projects" - URL_RETRIEVE = "project/{project_id}" + ENTITY = SettingEntity + URL_SETTINGS = "project/{}/settings" - def __init__(self, client, project_id: int): - super().__init__(client) - - self._project_id = project_id + def list(self, project_id: int, condition: Condition = None) -> List[SettingEntity]: + response = self._session.paginate( + self.URL_SETTINGS.format(project_id), + query_params=condition.get_as_params_dict() if condition else None, + ) + return self.serialize_entity(response) - def list(self, condition: Condition) -> List[Setting]: - data = self._session.paginate( - url=self.URL_LIST, - query_params=condition.get_as_params_dict(), + def set_settings( + self, project_id: int, settings: List[SettingEntity] + ) -> List[SettingEntity]: + response = self._session.request( + self.URL_SETTINGS.format(project_id), + "put", + json={ + "settings": [ + SettingEntity.to_json(setting, exclude_none=True) + for setting in settings + ] + }, ) - return [Setting.from_json(i) for i in data] + response.raise_for_status() + return self.serialize_entity(response.json()) diff --git a/src/superannotate_core/infrastructure/repositories/workflow_repository.py b/src/superannotate_core/infrastructure/repositories/workflow_repository.py new file mode 100644 index 0000000..4cf0f45 --- /dev/null +++ b/src/superannotate_core/infrastructure/repositories/workflow_repository.py @@ -0,0 +1,38 @@ +from typing import List + +from superannotate_core.core.entities.project import WorkflowEntity +from superannotate_core.infrastructure.repositories.base import BaseHttpRepository + + +class WorkflowRepository(BaseHttpRepository): + ENTITY = WorkflowEntity + URL_WORKFLOW_LIST = "project/{}/workflow" + URL_WORKFLOW_ATTRIBUTE = "project/{}/workflow_attribute" + + def list_workflows(self, project_id: int): + response = self._session.paginate(self.URL_WORKFLOW_LIST.format(project_id)) + return self.serialize_entity(response) + + def set_workflow(self, project_id: int, workflow: WorkflowEntity): + return self._session.request( + self.URL_WORKFLOW_LIST.format(project_id), + "post", + json={"steps": [WorkflowEntity.to_json(workflow, exclude_none=True)]}, + ) + + def set_workflows(self, project_id: int, workflows: List[WorkflowEntity]): + workflows = [WorkflowEntity.to_json(s, exclude_none=False) for s in workflows] + response = self._session.request( + self.URL_WORKFLOW_LIST.format(project_id), + "post", + json={"steps": workflows}, + ) + response.raise_for_status() + return self.serialize_entity(response.json()) + + def set_project_workflow_attributes(self, project_id: int, attributes: List[dict]): + return self._session.request( + self.URL_WORKFLOW_ATTRIBUTE.format(project_id), + "post", + json={"data": attributes}, + ) diff --git a/src/superannotate_core/infrastructure/session.py b/src/superannotate_core/infrastructure/session.py index f384992..aa87703 100644 --- a/src/superannotate_core/infrastructure/session.py +++ b/src/superannotate_core/infrastructure/session.py @@ -21,6 +21,7 @@ class Session: MAX_COROUTINE_COUNT = 8 ANNOTATION_VERSION = "V1.00" URL_USER = "user/ME" + URL_TEAM = "team" def __init__( self, @@ -59,6 +60,14 @@ def user_id(self): response = self.request(method="get", url=url) return response.json()["id"] + def get_team(self): + response = self.request( + f"{self.URL_TEAM}/{self._team_id}", + "get", + ) + response.raise_for_status() + return response.json() + @property def assets_provider_url(self): if self._api_url == "https://api.devsuperannotate.com":