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
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
minversion = 3.7
log_cli=true
python_files = test_*.py
addopts = -n auto --dist=loadscope
;addopts = -n auto --dist=loadscope
2 changes: 2 additions & 0 deletions src/superannotate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
from superannotate.lib.core import PACKAGE_VERSION_UPGRADE # noqa
from superannotate.logger import get_default_logger # noqa
from superannotate.version import __version__ # noqa
import superannotate.lib.core.enums as enums # noqa

SESSIONS = {}

__all__ = [
"__version__",
"SAClient",
# Utils
"enums",
"AppException",
# analytics
"class_distribution",
Expand Down
64 changes: 34 additions & 30 deletions src/superannotate/lib/app/interface/base_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from types import FunctionType
from typing import Iterable
from typing import Sized
from typing import Tuple

import lib.core as constants
from lib.app.helpers import extract_project_folder
Expand All @@ -21,41 +22,44 @@
class BaseInterfaceFacade:
REGISTRY = []

def __init__(
self,
token: str = None,
config_path: str = constants.CONFIG_PATH,
):
env_token = os.environ.get("SA_TOKEN")
host = os.environ.get("SA_URL", constants.BACKEND_URL)
def __init__(self, token: str = None, config_path: str = None):
version = os.environ.get("SA_VERSION", "v1")
ssl_verify = bool(os.environ.get("SA_SSL", True))
_token, _config_path = None, None
_host = os.environ.get("SA_URL", constants.BACKEND_URL)
_ssl_verify = bool(os.environ.get("SA_SSL", True))
if token:
token = Controller.validate_token(token=token)
elif env_token:
host = os.environ.get("SA_URL", constants.BACKEND_URL)
token = Controller.validate_token(env_token)
_token = Controller.validate_token(token=token)
elif config_path:
_token, _host, _ssl_verify = self._retrieve_configs(config_path)
else:
config_path = os.path.expanduser(str(config_path))
if not Path(config_path).is_file() or not os.access(config_path, os.R_OK):
raise AppException(
f"SuperAnnotate config file {str(config_path)} not found."
f" Please provide correct config file location to sa.init(<path>) or use "
f"CLI's superannotate init to generate default location config file."
_token = os.environ.get("SA_TOKEN")
if not _token:
_toke, _host, _ssl_verify = self._retrieve_configs(
constants.CONFIG_PATH
)
config_repo = ConfigRepository(config_path)
main_endpoint = config_repo.get_one("main_endpoint").value
if not main_endpoint:
main_endpoint = constants.BACKEND_URL
token, host, ssl_verify = (
Controller.validate_token(config_repo.get_one("token").value),
main_endpoint,
config_repo.get_one("ssl_verify").value,
self._token, self._host = _host, _token
self.controller = Controller(_token, _host, _ssl_verify, version)

def __new__(cls, *args, **kwargs):
obj = super().__new__(cls, *args, **kwargs)
cls.REGISTRY.append(obj)
return obj

@staticmethod
def _retrieve_configs(path) -> Tuple[str, str, str]:
config_path = os.path.expanduser(str(path))
if not Path(config_path).is_file() or not os.access(config_path, os.R_OK):
raise AppException(
f"SuperAnnotate config file {str(config_path)} not found."
f" Please provide correct config file location to sa.init(<path>) or use "
f"CLI's superannotate init to generate default location config file."
)
self._host = host
self._token = token
self.controller = Controller(token, host, ssl_verify, version)
BaseInterfaceFacade.REGISTRY.append(self)
config_repo = ConfigRepository(config_path)
return (
Controller.validate_token(config_repo.get_one("token").value),
config_repo.get_one("main_endpoint").value,
config_repo.get_one("ssl_verify").value,
)

@property
def host(self):
Expand Down
85 changes: 46 additions & 39 deletions src/superannotate/lib/app/interface/sdk_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from typing import Union

import boto3
import lib.core as constances
import lib.core as constants
from lib.app.annotation_helpers import add_annotation_bbox_to_json
from lib.app.annotation_helpers import add_annotation_comment_to_json
from lib.app.annotation_helpers import add_annotation_point_to_json
Expand Down Expand Up @@ -62,6 +62,13 @@


class SAClient(BaseInterfaceFacade, metaclass=TrackableMeta):
def __init__(
self,
token: str = None,
config_path: str = constants.CONFIG_PATH,
):
super().__init__(token, config_path)

def get_team_metadata(self):
"""Returns team metadata

Expand Down Expand Up @@ -415,11 +422,11 @@ def copy_image(
).data

if destination_project_metadata["project"].type in [
constances.ProjectType.VIDEO.value,
constances.ProjectType.DOCUMENT.value,
constants.ProjectType.VIDEO.value,
constants.ProjectType.DOCUMENT.value,
] or source_project_metadata["project"].type in [
constances.ProjectType.VIDEO.value,
constances.ProjectType.DOCUMENT.value,
constants.ProjectType.VIDEO.value,
constants.ProjectType.DOCUMENT.value,
]:
raise AppException(
LIMITED_FUNCTIONS[source_project_metadata["project"].type]
Expand Down Expand Up @@ -738,8 +745,8 @@ def assign_images(
project = self.controller.get_project_metadata(project_name).data

if project["project"].type in [
constances.ProjectType.VIDEO.value,
constances.ProjectType.DOCUMENT.value,
constants.ProjectType.VIDEO.value,
constants.ProjectType.DOCUMENT.value,
]:
raise AppException(LIMITED_FUNCTIONS[project["project"].type])

Expand Down Expand Up @@ -864,12 +871,12 @@ def upload_images_from_folder_to_project(
folder_path: Union[NotEmptyStr, Path],
extensions: Optional[
Union[List[NotEmptyStr], Tuple[NotEmptyStr]]
] = constances.DEFAULT_IMAGE_EXTENSIONS,
] = constants.DEFAULT_IMAGE_EXTENSIONS,
annotation_status="NotStarted",
from_s3_bucket=None,
exclude_file_patterns: Optional[
Iterable[NotEmptyStr]
] = constances.DEFAULT_FILE_EXCLUDE_PATTERNS,
] = constants.DEFAULT_FILE_EXCLUDE_PATTERNS,
recursive_subfolders: Optional[StrictBool] = False,
image_quality_in_editor: Optional[str] = None,
):
Expand Down Expand Up @@ -926,7 +933,7 @@ def upload_images_from_folder_to_project(

if exclude_file_patterns:
exclude_file_patterns = list(exclude_file_patterns) + list(
constances.DEFAULT_FILE_EXCLUDE_PATTERNS
constants.DEFAULT_FILE_EXCLUDE_PATTERNS
)
exclude_file_patterns = list(set(exclude_file_patterns))

Expand Down Expand Up @@ -1082,12 +1089,12 @@ def prepare_export(
folders = folder_names
if not annotation_statuses:
annotation_statuses = [
constances.AnnotationStatus.NOT_STARTED.name,
constances.AnnotationStatus.IN_PROGRESS.name,
constances.AnnotationStatus.QUALITY_CHECK.name,
constances.AnnotationStatus.RETURNED.name,
constances.AnnotationStatus.COMPLETED.name,
constances.AnnotationStatus.SKIPPED.name,
constants.AnnotationStatus.NOT_STARTED.name,
constants.AnnotationStatus.IN_PROGRESS.name,
constants.AnnotationStatus.QUALITY_CHECK.name,
constants.AnnotationStatus.RETURNED.name,
constants.AnnotationStatus.COMPLETED.name,
constants.AnnotationStatus.SKIPPED.name,
]
response = self.controller.prepare_export(
project_name=project_name,
Expand All @@ -1106,7 +1113,7 @@ def upload_videos_from_folder_to_project(
folder_path: Union[NotEmptyStr, Path],
extensions: Optional[
Union[Tuple[NotEmptyStr], List[NotEmptyStr]]
] = constances.DEFAULT_VIDEO_EXTENSIONS,
] = constants.DEFAULT_VIDEO_EXTENSIONS,
exclude_file_patterns: Optional[List[NotEmptyStr]] = (),
recursive_subfolders: Optional[StrictBool] = False,
target_fps: Optional[int] = None,
Expand Down Expand Up @@ -1593,8 +1600,8 @@ def upload_preannotations_from_folder_to_project(
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
project = self.controller.get_project_metadata(project_name).data
if project["project"].type in [
constances.ProjectType.VIDEO.value,
constances.ProjectType.DOCUMENT.value,
constants.ProjectType.VIDEO.value,
constants.ProjectType.DOCUMENT.value,
]:
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
if recursive_subfolders:
Expand Down Expand Up @@ -1649,8 +1656,8 @@ def upload_image_annotations(

project = self.controller.get_project_metadata(project_name).data
if project["project"].type in [
constances.ProjectType.VIDEO.value,
constances.ProjectType.DOCUMENT.value,
constants.ProjectType.VIDEO.value,
constants.ProjectType.DOCUMENT.value,
]:
raise AppException(LIMITED_FUNCTIONS[project["project"].type])

Expand All @@ -1677,7 +1684,7 @@ def upload_image_annotations(
mask=mask,
verbose=verbose,
)
if response.errors and not response.errors == constances.INVALID_JSON_MESSAGE:
if response.errors and not response.errors == constants.INVALID_JSON_MESSAGE:
raise AppException(response.errors)

def download_model(self, model: MLModel, output_dir: Union[str, Path]):
Expand Down Expand Up @@ -1735,8 +1742,8 @@ def benchmark(

project = self.controller.get_project_metadata(project_name).data
if project["project"].type in [
constances.ProjectType.VIDEO.value,
constances.ProjectType.DOCUMENT.value,
constants.ProjectType.VIDEO.value,
constants.ProjectType.DOCUMENT.value,
]:
raise AppException(LIMITED_FUNCTIONS[project["project"].type])

Expand Down Expand Up @@ -1887,8 +1894,8 @@ def add_annotation_bbox_to_image(
project_name, folder_name = extract_project_folder(project)
project = self.controller.get_project_metadata(project_name).data
if project["project"].type in [
constances.ProjectType.VIDEO.value,
constances.ProjectType.DOCUMENT.value,
constants.ProjectType.VIDEO.value,
constants.ProjectType.DOCUMENT.value,
]:
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
response = self.controller.get_annotations(
Expand Down Expand Up @@ -1945,8 +1952,8 @@ def add_annotation_point_to_image(
project_name, folder_name = extract_project_folder(project)
project = self.controller.get_project_metadata(project_name).data
if project["project"].type in [
constances.ProjectType.VIDEO.value,
constances.ProjectType.DOCUMENT.value,
constants.ProjectType.VIDEO.value,
constants.ProjectType.DOCUMENT.value,
]:
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
response = self.controller.get_annotations(
Expand Down Expand Up @@ -2000,8 +2007,8 @@ def add_annotation_comment_to_image(
project_name, folder_name = extract_project_folder(project)
project = self.controller.get_project_metadata(project_name).data
if project["project"].type in [
constances.ProjectType.VIDEO.value,
constances.ProjectType.DOCUMENT.value,
constants.ProjectType.VIDEO.value,
constants.ProjectType.DOCUMENT.value,
]:
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
response = self.controller.get_annotations(
Expand Down Expand Up @@ -2187,8 +2194,8 @@ def aggregate_annotations_as_df(
:rtype: pandas DataFrame
"""
if project_type in (
constances.ProjectType.VECTOR.name,
constances.ProjectType.PIXEL.name,
constants.ProjectType.VECTOR.name,
constants.ProjectType.PIXEL.name,
):
from superannotate.lib.app.analytics.common import (
aggregate_image_annotations_as_df,
Expand All @@ -2202,8 +2209,8 @@ def aggregate_annotations_as_df(
folder_names=folder_names,
)
elif project_type in (
constances.ProjectType.VIDEO.name,
constances.ProjectType.DOCUMENT.name,
constants.ProjectType.VIDEO.name,
constants.ProjectType.DOCUMENT.name,
):
from superannotate.lib.app.analytics.aggregators import DataAggregator

Expand All @@ -2214,21 +2221,21 @@ def aggregate_annotations_as_df(
).aggregate_annotations_as_df()

def delete_annotations(
self, project: NotEmptyStr, image_names: Optional[List[NotEmptyStr]] = None
self, project: NotEmptyStr, item_names: Optional[List[NotEmptyStr]] = None
):
"""
Delete image annotations from a given list of images.
Delete item annotations from a given list of items.

:param project: project name or folder path (e.g., "project1/folder1")
:type project: str
:param image_names: image names. If None, all image annotations from a given project/folder will be deleted.
:type image_names: list of strs
:param item_names: image names. If None, all image annotations from a given project/folder will be deleted.
:type item_names: list of strs
"""

project_name, folder_name = extract_project_folder(project)

response = self.controller.delete_annotations(
project_name=project_name, folder_name=folder_name, item_names=image_names
project_name=project_name, folder_name=folder_name, item_names=item_names
)
if response.errors:
raise AppException(response.errors)
Expand Down
13 changes: 13 additions & 0 deletions src/superannotate/lib/app/interface/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,31 @@
from pydantic import BaseModel
from pydantic import conlist
from pydantic import constr
from pydantic import errors
from pydantic import Extra
from pydantic import Field
from pydantic import parse_obj_as
from pydantic import root_validator
from pydantic import StrictStr
from pydantic import validate_arguments as pydantic_validate_arguments
from pydantic import ValidationError
from pydantic.errors import PydanticTypeError
from pydantic.errors import StrRegexError

NotEmptyStr = constr(strict=True, min_length=1)


class EnumMemberError(PydanticTypeError):
code = "enum"

def __str__(self) -> str:
permitted = ", ".join(str(v.name) for v in self.enum_values) # type: ignore
return f"Available values are: {permitted}"


errors.EnumMemberError = EnumMemberError


class EmailStr(StrictStr):
@classmethod
def validate(cls, value: Union[str]) -> Union[str]:
Expand Down
8 changes: 8 additions & 0 deletions src/superannotate/lib/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ def __new__(cls, title, value):
obj._value_ = value
obj.__doc__ = title
obj._type = "titled_enum"
cls._value2member_map_[title] = obj
return obj

@classmethod
def choices(cls):
return tuple(cls._value2member_map_.keys())

@DynamicClassAttribute
def name(self) -> str:
return self.__doc__
Expand Down Expand Up @@ -41,6 +46,9 @@ def titles(cls):
def equals(self, other: Enum):
return self.__doc__.lower() == other.__doc__.lower()

def __eq__(self, other):
return super().__eq__(other)


class AnnotationTypes(str, Enum):
BBOX = "bbox"
Expand Down
Loading