From c233e674e96caa988eb4f8d7c633e6eb8725aad8 Mon Sep 17 00:00:00 2001 From: Vaghinak Basentsyan Date: Mon, 14 Feb 2022 18:57:25 +0400 Subject: [PATCH] Added interface function get_anntoations --- pytest.ini | 2 +- requirements.txt | 1 - requirements_dev.txt | 9 +- requirements_extra.txt | 5 + requirements_prod.txt | 2 + setup.py | 4 +- src/superannotate/__init__.py | 6 +- src/superannotate/lib/__init__.py | 1 - .../lib/app/analytics/aggregators.py | 2 +- .../lib/app/interface/cli_interface.py | 2 +- .../lib/app/interface/sdk_interface.py | 47 ++++++ src/superannotate/lib/app/mixp/decorators.py | 14 +- .../lib/app/mixp/utils/parsers.py | 1 - .../lib/app/superannotate/__init__.py | 0 src/superannotate/lib/core/enums.py | 7 + .../lib/core/serviceproviders.py | 4 + .../lib/core/usecases/annotations.py | 74 +++++++++ src/superannotate/lib/core/video_convertor.py | 145 ++++++++++++++++++ .../lib/infrastructure/controller.py | 27 ++++ .../lib/infrastructure/repositories.py | 2 +- .../lib/infrastructure/services.py | 25 +++ .../lib/infrastructure/stream_data_handler.py | 55 +++++++ .../annotations/test_get_annotations.py | 41 +++++ .../test_get_annotations_per_frame.py | 34 ++++ 24 files changed, 490 insertions(+), 20 deletions(-) create mode 100644 requirements_prod.txt delete mode 100644 src/superannotate/lib/app/superannotate/__init__.py create mode 100644 src/superannotate/lib/core/video_convertor.py create mode 100644 src/superannotate/lib/infrastructure/stream_data_handler.py create mode 100644 tests/integration/annotations/test_get_annotations.py create mode 100644 tests/integration/annotations/test_get_annotations_per_frame.py diff --git a/pytest.ini b/pytest.ini index 084beff5b..d9ab3b434 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,4 +2,4 @@ minversion = 3.0 log_cli=true python_files = test_*.py -addopts = -n auto --dist=loadscope \ No newline at end of file +;addopts = -n auto --dist=loadscope \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f745b4979..a6c1ddeef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,4 +18,3 @@ mixpanel==4.8.3 pydantic>=1.8.2 pydantic[email] setuptools~=57.4.0 -superannotate_schemas \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index 023e3a50e..f5bad2d5c 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,8 +1,3 @@ -Sphinx==3.1.2 -tox==3.24.2 -pytest==6.2.4 -pytest-xdist==2.3.0 -pytest-parallel==0.1.0 -pytest-rerunfailures==10.2 -sphinx_rtd_theme==1.0.0 +-r requirements.txt +superannotate_schemas>=1.0.38.b1 diff --git a/requirements_extra.txt b/requirements_extra.txt index 704251505..9e3d802fb 100644 --- a/requirements_extra.txt +++ b/requirements_extra.txt @@ -1,2 +1,7 @@ Sphinx==3.1.2 tox==3.24.2 +pytest==6.2.4 +pytest-xdist==2.3.0 +pytest-parallel==0.1.0 +pytest-rerunfailures==10.2 +sphinx_rtd_theme==1.0.0 \ No newline at end of file diff --git a/requirements_prod.txt b/requirements_prod.txt new file mode 100644 index 000000000..f47015fe2 --- /dev/null +++ b/requirements_prod.txt @@ -0,0 +1,2 @@ +-r requirements.txt +superannotate_schemas=1.0.39 \ No newline at end of file diff --git a/setup.py b/setup.py index ee13a76df..d2b9ce871 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,15 @@ import sys +from packaging.version import parse from setuptools import find_packages, setup with open('src/superannotate/version.py') as f: version = f.read().rstrip()[15:-1] +requirements_path = f"requirements_{'dev' if parse(version).is_prerelease else 'prod'}.txt" -with open('requirements.txt') as f: +with open(requirements_path) as f: requirements = f.read() requirements = requirements.splitlines() diff --git a/src/superannotate/__init__.py b/src/superannotate/__init__.py index 45e317788..7f3ef5d76 100644 --- a/src/superannotate/__init__.py +++ b/src/superannotate/__init__.py @@ -4,8 +4,8 @@ import requests import superannotate.lib.core as constances -from superannotate.lib import get_default_controller from packaging.version import parse +from superannotate.lib import get_default_controller from superannotate.lib.app.analytics.class_analytics import class_distribution from superannotate.lib.app.exceptions import AppException from superannotate.lib.app.input_converters.conversion import convert_json_version @@ -50,6 +50,7 @@ from superannotate.lib.app.interface.sdk_interface import download_image from superannotate.lib.app.interface.sdk_interface import download_image_annotations from superannotate.lib.app.interface.sdk_interface import download_model +from superannotate.lib.app.interface.sdk_interface import get_annotations from superannotate.lib.app.interface.sdk_interface import get_exports from superannotate.lib.app.interface.sdk_interface import get_folder_metadata from superannotate.lib.app.interface.sdk_interface import get_image_annotations @@ -107,7 +108,6 @@ ) from superannotate.lib.app.interface.sdk_interface import validate_annotations from superannotate.logger import get_default_logger -from superannotate.lib.infrastructure.controller import Controller from superannotate.version import __version__ @@ -128,6 +128,8 @@ "class_distribution", "aggregate_annotations_as_df", "get_exports", + # annotations + "get_annotations", # converters "convert_json_version", "import_annotation", diff --git a/src/superannotate/lib/__init__.py b/src/superannotate/lib/__init__.py index 281eecbd3..bc765c353 100644 --- a/src/superannotate/lib/__init__.py +++ b/src/superannotate/lib/__init__.py @@ -9,4 +9,3 @@ def get_default_controller(): from lib.infrastructure.controller import Controller return Controller.get_default() - diff --git a/src/superannotate/lib/app/analytics/aggregators.py b/src/superannotate/lib/app/analytics/aggregators.py index b33368095..e29f88c6e 100644 --- a/src/superannotate/lib/app/analytics/aggregators.py +++ b/src/superannotate/lib/app/analytics/aggregators.py @@ -1,6 +1,5 @@ import copy import json -from dataclasses import dataclass from pathlib import Path from typing import List from typing import Optional @@ -8,6 +7,7 @@ import lib.core as constances import pandas as pd +from dataclasses import dataclass from lib.app.exceptions import AppException from lib.core import ATTACHED_VIDEO_ANNOTATION_POSTFIX from lib.core import PIXEL_ANNOTATION_POSTFIX diff --git a/src/superannotate/lib/app/interface/cli_interface.py b/src/superannotate/lib/app/interface/cli_interface.py index b6226d845..fae9ac000 100644 --- a/src/superannotate/lib/app/interface/cli_interface.py +++ b/src/superannotate/lib/app/interface/cli_interface.py @@ -19,8 +19,8 @@ from lib.app.interface.sdk_interface import upload_preannotations_from_folder_to_project from lib.app.interface.sdk_interface import upload_videos_from_folder_to_project from lib.core.entities import ConfigEntity -from lib.infrastructure.repositories import ConfigRepository from lib.infrastructure.controller import Controller +from lib.infrastructure.repositories import ConfigRepository controller = Controller.get_default() diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index 0b9c3bb66..72dda37a5 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -2844,3 +2844,50 @@ def invite_contributors_to_team( if response.errors: raise AppException(response.errors) return response.data + + +@Trackable +@validate_arguments +def get_annotations(project: NotEmptyStr, items: Optional[List[NotEmptyStr]]): + """Returns annotations for the given list of items. + + :param project: project name + :type project: str + + :param items: item names. If None all items in the project will be exported + :type items: list of strs + + :return: list of annotations + :rtype: list of strs + """ + project_name, folder_name = extract_project_folder(project) + response = Controller.get_default().get_annotations(project_name, folder_name, items) + if response.errors: + raise AppException(response.errors) + return response.data + + +@Trackable +@validate_arguments +def get_annotations_per_frame(project: NotEmptyStr, video: NotEmptyStr, fps: int = 1): + """Returns per frame annotations for the given video. + + + :param project: project name + :type project: str + + :param video: video name + :type video: str + + :param fps: how many frames per second needs to be extracted from the video. + Will extract 1 frame per second by default. + :type fps: str + + :return: list of annotation objects + :rtype: list of dicts + """ + project_name, folder_name = extract_project_folder(project) + response = Controller.get_default().get_annotations_per_frame(project_name, folder_name, video_name=video, fps=fps) + if response.errors: + raise AppException(response.errors) + return response.data diff --git a/src/superannotate/lib/app/mixp/decorators.py b/src/superannotate/lib/app/mixp/decorators.py index 8e1a0040e..ba5454162 100644 --- a/src/superannotate/lib/app/mixp/decorators.py +++ b/src/superannotate/lib/app/mixp/decorators.py @@ -80,9 +80,17 @@ def track(self, *args, **kwargs): def __call__(self, *args, **kwargs): try: - self.__class__.TEAM_DATA = get_default_controller().get_team() - result = self.function(*args, **kwargs) - self._success = True + controller = get_default_controller() + if controller: + self.__class__.TEAM_DATA = controller.get_team() + result = self.function(*args, **kwargs) + self._success = True + else: + raise Exception( + "SuperAnnotate config file not found." + f" Please provide correct config file location to sa.init() or use " + f"CLI's superannotate init to generate default location config file." + ) except Exception as e: self._success = False logger.debug(str(e), exc_info=True) diff --git a/src/superannotate/lib/app/mixp/utils/parsers.py b/src/superannotate/lib/app/mixp/utils/parsers.py index fe1c2e9f4..8643e9736 100644 --- a/src/superannotate/lib/app/mixp/utils/parsers.py +++ b/src/superannotate/lib/app/mixp/utils/parsers.py @@ -1,5 +1,4 @@ import lib.core as constances -from lib import get_default_controller from lib.app.helpers import extract_project_folder from lib.core.enums import ProjectType from lib.infrastructure.controller import Controller diff --git a/src/superannotate/lib/app/superannotate/__init__.py b/src/superannotate/lib/app/superannotate/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/superannotate/lib/core/enums.py b/src/superannotate/lib/core/enums.py index de27f3519..b9314536e 100644 --- a/src/superannotate/lib/core/enums.py +++ b/src/superannotate/lib/core/enums.py @@ -82,6 +82,13 @@ class ClassTypeEnum(BaseTitledEnum): OBJECT = "object", 1 TAG = "tag", 2 + @classmethod + def get_value(cls, name): + for enum in list(cls): + if enum.name.lower() == name.lower(): + return enum.value + return "object" + class TrainingStatus(BaseTitledEnum): NOT_STARTED = "NotStarted", 1 diff --git a/src/superannotate/lib/core/serviceproviders.py b/src/superannotate/lib/core/serviceproviders.py index 7e73e49ef..8c7df3276 100644 --- a/src/superannotate/lib/core/serviceproviders.py +++ b/src/superannotate/lib/core/serviceproviders.py @@ -302,3 +302,7 @@ def get_limitations( self, team_id: int, project_id: int, folder_id: int = None ) -> ServiceResponse: raise NotImplementedError + + @abstractmethod + def get_annotations(self, project_id: int, team_id: int, folder_id: int, items: List[str]) -> List[dict]: + raise NotImplementedError diff --git a/src/superannotate/lib/core/usecases/annotations.py b/src/superannotate/lib/core/usecases/annotations.py index c8b506917..52c70b5ac 100644 --- a/src/superannotate/lib/core/usecases/annotations.py +++ b/src/superannotate/lib/core/usecases/annotations.py @@ -4,6 +4,7 @@ import os from collections import namedtuple from typing import List +from typing import Optional from typing import Tuple import boto3 @@ -18,6 +19,7 @@ from lib.core.entities import ImageEntity from lib.core.entities import ProjectEntity from lib.core.entities import TeamEntity +from lib.core.exceptions import AppException from lib.core.reporter import Reporter from lib.core.repositories import BaseManageableRepository from lib.core.service_types import UploadAnnotationAuthData @@ -25,6 +27,7 @@ from lib.core.usecases.base import BaseReportableUseCae from lib.core.usecases.images import GetBulkImages from lib.core.usecases.images import ValidateAnnotationUseCase +from lib.core.video_convertor import VideoFrameGenerator from superannotate.logger import get_default_logger from superannotate_schemas.validators import AnnotationValidators @@ -483,3 +486,74 @@ def execute(self): f"Couldn't validate annotations. {constances.USE_VALIDATE_MESSAGE}" ) return self._response + + +class GetAnnotations(BaseReportableUseCae): + def __init__( + self, + reporter: Reporter, + project: ProjectEntity, + folder: FolderEntity, + item_names: Optional[List[str]], + backend_service_provider: SuerannotateServiceProvider + ): + super().__init__(reporter) + self._project = project + self._folder = folder + self._item_names = item_names + self._client = backend_service_provider + + def validate_item_names(self): + if self._item_names: + item_names = list(dict.fromkeys(self._item_names)) + len_unique_items, len_items = len(item_names), len(self._item_names) + if len_unique_items < len_items: + self.reporter.log_info( + f"Dropping duplicates. Found {len_unique_items}/{len_items} unique items." + ) + self._item_names = item_names + + def execute(self): + annotations = self._client.get_annotations( + team_id=self._project.team_id, + project_id=self._project.uuid, + folder_id=self._folder.uuid, + items=self._item_names + ) + self._response.data = annotations + return self._response + + +class GetVideoAnnotationsPerFrame(BaseReportableUseCae): + def __init__( + self, + reporter: Reporter, + project: ProjectEntity, + folder: FolderEntity, + video_name: str, + fps: int, + backend_service_provider: SuerannotateServiceProvider + ): + super().__init__(reporter) + self._project = project + self._folder = folder + self._video_name = video_name + self._fps = fps + self._client = backend_service_provider + + def execute(self): + response = GetAnnotations( + reporter=self.reporter, + project=self._project, + folder=self._folder, + item_names=[self._video_name], + backend_service_provider=self._client + ).execute() + if response.errors: + self._response.errors = response.errors + return self._response + if not response.data: + self._response.errors = AppException(f"Video {self._video_name} not found.") + + self._response.data = VideoFrameGenerator(response.data[1], fps=self._fps) + return self._response diff --git a/src/superannotate/lib/core/video_convertor.py b/src/superannotate/lib/core/video_convertor.py new file mode 100644 index 000000000..c065719d6 --- /dev/null +++ b/src/superannotate/lib/core/video_convertor.py @@ -0,0 +1,145 @@ +from collections import defaultdict +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +from pydantic import BaseModel + + +class Annotation(BaseModel): + type: str + className: str + points: Optional[Dict] + attributes: Optional[List[Any]] = [] + keyframe: bool = False + + +class FrameAnnotation(BaseModel): + frame: int + annotations: List[Annotation] = [] + + def append_annotation(self, annotation: Annotation): + self.annotations.append(annotation) + + +class Annotations(BaseModel): + __root__: List[FrameAnnotation] = [] + + def append(self, value: FrameAnnotation): + self.__root__.append(value) + + +class VideoFrameGenerator: + class DefaultDict(defaultdict): + def __missing__(self, key): + return self.default_factory(key) + + def __init__(self, annotation_data: dict, fps: int): + self._annotation_data = annotation_data + self.duration = annotation_data["metadata"]["duration"] / (1000 * 1000) + self.fps = fps + self.ratio = 1000 * 1000 / fps + self._frame_id = 1 + self._frames_count = self.duration * fps + self.annotations: dict = {} + self._mapping = {} + self._process() + + def get_frame(self, frame_no: int): + try: + return self.annotations[frame_no] + except KeyError: + self.annotations[frame_no] = FrameAnnotation(frame=frame_no) + return self.annotations[frame_no] + + def interpolate_annotations( + self, + class_name: str, + from_frame: int, + to_frame: int, + data: dict, + steps: dict = None, + annotation_type: str = "bbox" + ): + for idx, frame_idx in enumerate(range(from_frame, to_frame), 1): + keyframe = False + if idx == from_frame or idx - 1 == to_frame: + keyframe = True + points = None + if annotation_type == "bbox": + points = { + "x1": round(data["points"]["x1"] + steps["x1"] * idx, 2), + "y1": round(data["points"]["y1"] + steps["y1"] * idx, 2), + "x2": round(data["points"]["x2"] + steps["x2"] * idx, 2), + "y2": round(data["points"]["y2"] + steps["y2"] * idx, 2), + } + frame = self.get_frame(frame_idx) + frame.annotations.append(Annotation( + type=annotation_type, + className=class_name, + points=points, + attributes=data["attributes"], + keyframe=keyframe + )) + + def _process(self): + for instance in self._annotation_data["instances"]: + for parameter in instance["parameters"]: + time_stamp_frame_map = [] + for timestamp in parameter["timestamps"]: + time_stamp_frame_map.append((round(timestamp["timestamp"] / self.ratio), timestamp)) + for idx, (frame_no, timestamp_data) in enumerate(time_stamp_frame_map): + annotation_type = instance["meta"]["type"] + try: + next_frame_no, next_timestamp = time_stamp_frame_map[idx + 1] + if frame_no == next_frame_no: + continue + frames_diff = next_frame_no - frame_no + steps = None + if annotation_type == "bbox": + if not frames_diff: + steps = { + "y1": 0, + "x2": 0, + "x1": 0, + "y2": 0 + } + else: + steps = { + "y1": round( + (next_timestamp["points"]["y1"] - timestamp_data["points"]["y1"]) / frames_diff, + 2), + "x2": round( + (next_timestamp["points"]["x2"] - timestamp_data["points"]["x2"]) / frames_diff, + 2), + "x1": round( + (next_timestamp["points"]["x1"] - timestamp_data["points"]["x1"]) / frames_diff, + 2), + "y2": round( + (next_timestamp["points"]["y2"] - timestamp_data["points"]["y2"]) / frames_diff, + 2), + } + self.interpolate_annotations( + class_name=instance["meta"]["className"], + from_frame=frame_no, + to_frame=next_frame_no, + data=timestamp_data, + steps=steps, + annotation_type=annotation_type + ) + except IndexError: + last_frame_no, last_timestamp = time_stamp_frame_map[-1] + end = round(parameter["end"] / self.ratio) + self.interpolate_annotations( + annotation_type=annotation_type, + class_name=instance["meta"]["className"], + from_frame=last_frame_no, + to_frame=end, + data=last_timestamp, + steps={"x1": 0, "y1": 0, "x2": 0, "y2": 0} + ) + + def __iter__(self): + for frame_no in range(1, int(self._frames_count)): + yield self.get_frame(frame_no).dict() diff --git a/src/superannotate/lib/infrastructure/controller.py b/src/superannotate/lib/infrastructure/controller.py index ad1c05991..ea63dc182 100644 --- a/src/superannotate/lib/infrastructure/controller.py +++ b/src/superannotate/lib/infrastructure/controller.py @@ -1619,3 +1619,30 @@ def upload_videos( image_quality_in_editor=image_quality_in_editor, ) return use_case.execute() + + def get_annotations(self, project_name: str, folder_name: str, item_names: List[str]): + project = self._get_project(project_name) + folder = self._get_folder(project, folder_name) + + use_case = usecases.GetAnnotations( + reporter=self.default_reporter, + project=project, + folder=folder, + item_names=item_names, + backend_service_provider=self.backend_client + ) + return use_case.execute() + + def get_annotations_per_frame(self, project_name: str, folder_name: str, video_name: str, fps: int): + project = self._get_project(project_name) + folder = self._get_folder(project, folder_name) + + use_case = usecases.GetVideoAnnotationsPerFrame( + reporter=self.default_reporter, + project=project, + folder=folder, + video_name=video_name, + fps=fps, + backend_service_provider=self.backend_client + ) + return use_case.execute() diff --git a/src/superannotate/lib/infrastructure/repositories.py b/src/superannotate/lib/infrastructure/repositories.py index 7f500d124..d5e5a1fcc 100644 --- a/src/superannotate/lib/infrastructure/repositories.py +++ b/src/superannotate/lib/infrastructure/repositories.py @@ -360,7 +360,7 @@ def dict2entity(data: dict) -> AnnotationClassEntity: createdAt=data["createdAt"], updatedAt=data["updatedAt"], attribute_groups=data["attribute_groups"], - type=ClassTypeEnum.get_name(data.get("type")), + type=ClassTypeEnum.get_name(data.get("type", 1)), ) diff --git a/src/superannotate/lib/infrastructure/services.py b/src/superannotate/lib/infrastructure/services.py index 1d7c381c8..5f7fc337f 100644 --- a/src/superannotate/lib/infrastructure/services.py +++ b/src/superannotate/lib/infrastructure/services.py @@ -1,3 +1,4 @@ +import asyncio import json import time from contextlib import contextmanager @@ -18,6 +19,7 @@ from lib.core.service_types import UserLimits from lib.core.serviceproviders import SuerannotateServiceProvider from lib.infrastructure.helpers import timed_lru_cache +from lib.infrastructure.stream_data_handler import StreamedAnnotations from requests.exceptions import HTTPError @@ -163,6 +165,8 @@ class SuperannotateBackendService(BaseBackendService): """ Manage projects, images and team in the Superannotate """ + DEFAULT_CHUNK_SIZE = 1000 + STREAMED_DATA_PROVIDER_URL = "https://assets-provider.devsuperannotate.com" URL_USERS = "users" URL_LIST_PROJECTS = "projects" @@ -211,6 +215,7 @@ class SuperannotateBackendService(BaseBackendService): URL_DELETE_ANNOTATIONS = "annotations/remove" URL_DELETE_ANNOTATIONS_PROGRESS = "annotations/getRemoveStatus" URL_GET_LIMITS = "project/{}/limitationDetails" + URL_GET_ANNOTATIONS = "api/v1/images/annotations/stream" def get_project(self, uuid: int, team_id: int): get_project_url = urljoin(self.api_url, self.URL_GET_PROJECT.format(uuid)) @@ -1004,3 +1009,23 @@ def get_limitations( params={"team_id": team_id, "folder_id": folder_id}, content_type=UserLimits, ) + + def get_annotations(self, project_id: int, team_id: int, folder_id: int, items: List[str]) -> List[dict]: + get_limits_url = urljoin(self.STREAMED_DATA_PROVIDER_URL, self.URL_GET_ANNOTATIONS) + query_params = { + "team_id": team_id, + "project_id": project_id, + } + if folder_id: + query_params["folder_id"] = folder_id + + handler = StreamedAnnotations(self.default_headers) + loop = asyncio.new_event_loop() + + return loop.run_until_complete(handler.get_data( + url=get_limits_url, + data=items, + params=query_params, + chunk_size=self.DEFAULT_CHUNK_SIZE, + map_function=lambda x: {"image_names": x} + )) diff --git a/src/superannotate/lib/infrastructure/stream_data_handler.py b/src/superannotate/lib/infrastructure/stream_data_handler.py new file mode 100644 index 000000000..4bb801e6f --- /dev/null +++ b/src/superannotate/lib/infrastructure/stream_data_handler.py @@ -0,0 +1,55 @@ +import json +from typing import Callable +from typing import List + +import aiohttp + + +def map_image_names_to_fetch_streamed_data(data: List[str]): + mapping = {"image_names": []} + for image_name in data: + mapping["image_names"].append(image_name) + return mapping + + +class StreamedAnnotations: + DELIMITER = b";)" + + def __init__(self, headers: dict): + self._headers = headers + self._annotations = [] + + async def fetch(self, method: str, session: aiohttp.ClientSession, url: str, data: dict = None, params: dict = None): + response = await session._request(method, url, json=data, params=params) + buffer = b"" + async for line in response.content: + slices = line.split(self.DELIMITER) + if slices[0]: + self._annotations.append(json.loads(buffer + slices[0])) + for data in slices[1:-1]: + self._annotations.append(json.loads(data)) + buffer = slices[-1] + if buffer: + self._annotations.append(json.loads(buffer)) + return self._annotations + + async def get_data( + self, + url: str, + data: list, + method: str = "post", + params=None, + chunk_size: int = 100, + map_function: Callable = lambda x: x, + verify_ssl: bool = False, + ): + + async with aiohttp.ClientSession(raise_for_status=True, headers=self._headers, + connector=aiohttp.TCPConnector(ssl=verify_ssl)) as session: + + if chunk_size: + for i in range(0, len(data), chunk_size): + await self.fetch(method, session, url, map_function(data[i:i + chunk_size]), params=params) + else: + await self.fetch(method, session, url, map_function(data), params=params) + return self._annotations diff --git a/tests/integration/annotations/test_get_annotations.py b/tests/integration/annotations/test_get_annotations.py new file mode 100644 index 000000000..4e87a51e9 --- /dev/null +++ b/tests/integration/annotations/test_get_annotations.py @@ -0,0 +1,41 @@ +from pathlib import Path +import os +from typing import List +import json +import pytest + +import src.superannotate as sa +from pydantic import parse_obj_as +from superannotate_schemas.schemas.internal import VectorAnnotation +from tests.integration.base import BaseTestCase + + +class TestGetAnnotations(BaseTestCase): + PROJECT_NAME = "Test-get_annotations" + PROJECT_DESCRIPTION = "Desc" + PROJECT_TYPE = "Vector" + TEST_FOLDER_PATH = "data_set/sample_project_vector" + IMAGE_NAME = "example_image_1.jpg" + + @property + def folder_path(self): + return os.path.join(Path(__file__).parent.parent.parent, self.TEST_FOLDER_PATH) + + @pytest.mark.flaky(reruns=3) + def test_get_annotations(self): + sa.upload_images_from_folder_to_project( + self.PROJECT_NAME, self.folder_path, annotation_status="InProgress" + ) + sa.create_annotation_classes_from_classes_json( + self.PROJECT_NAME, f"{self.folder_path}/classes/classes.json" + ) + _, _, _ = sa.upload_annotations_from_folder_to_project( + self.PROJECT_NAME, self.folder_path + ) + + annotations = sa.get_annotations(f"{self.PROJECT_NAME}", [self.IMAGE_NAME]) + self.assertEqual(len(annotations), 1) + with open(f"{self.folder_path}/{self.IMAGE_NAME}___objects.json", "r") as annotation_file: + annotation_data = json.load(annotation_file) + self.assertEqual(len(annotation_data["instances"]), len(annotations[0]["instances"])) + parse_obj_as(List[VectorAnnotation], annotations) diff --git a/tests/integration/annotations/test_get_annotations_per_frame.py b/tests/integration/annotations/test_get_annotations_per_frame.py new file mode 100644 index 000000000..2b4dd4e2a --- /dev/null +++ b/tests/integration/annotations/test_get_annotations_per_frame.py @@ -0,0 +1,34 @@ +import os +from os.path import dirname +from typing import List +import json +import pytest + +import src.superannotate as sa +from pydantic import parse_obj_as +from superannotate_schemas.schemas.internal import VectorAnnotation +from tests.integration.base import BaseTestCase + + +class TestGetAnnotations(BaseTestCase): + PROJECT_NAME = "test attach video urls" + PATH_TO_URLS = "data_set/attach_urls.csv" + PATH_TO_URLS_WITHOUT_NAMES = "data_set/attach_urls_with_no_name.csv" + PATH_TO_50K_URLS = "data_set/501_urls.csv" + PROJECT_DESCRIPTION = "desc" + PROJECT_TYPE = "Video" + + @property + def csv_path(self): + return os.path.join(dirname(dirname(__file__)), self.PATH_TO_URLS) + + @property + def csv_path_without_name_column(self): + return os.path.join(dirname(dirname(__file__)), self.PATH_TO_URLS_WITHOUT_NAMES) + + def test_attach_video_urls(self): + uploaded, _, __ = sa.attach_video_urls_to_project( + self.PROJECT_NAME, + self.csv_path, + ) +