Skip to content

Commit e3c3234

Browse files
authored
Merge pull request #446 from superannotateai/1100_enums
1100 enums
2 parents 065b60e + 25e1421 commit e3c3234

File tree

13 files changed

+151
-85
lines changed

13 files changed

+151
-85
lines changed

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
minversion = 3.7
33
log_cli=true
44
python_files = test_*.py
5-
addopts = -n auto --dist=loadscope
5+
;addopts = -n auto --dist=loadscope

src/superannotate/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
from superannotate.lib.core import PACKAGE_VERSION_UPGRADE # noqa
2121
from superannotate.logger import get_default_logger # noqa
2222
from superannotate.version import __version__ # noqa
23+
import superannotate.lib.core.enums as enums # noqa
2324

2425
SESSIONS = {}
2526

2627
__all__ = [
2728
"__version__",
2829
"SAClient",
2930
# Utils
31+
"enums",
3032
"AppException",
3133
# analytics
3234
"class_distribution",

src/superannotate/lib/app/interface/base_interface.py

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from types import FunctionType
77
from typing import Iterable
88
from typing import Sized
9+
from typing import Tuple
910

1011
import lib.core as constants
1112
from lib.app.helpers import extract_project_folder
@@ -21,41 +22,44 @@
2122
class BaseInterfaceFacade:
2223
REGISTRY = []
2324

24-
def __init__(
25-
self,
26-
token: str = None,
27-
config_path: str = constants.CONFIG_PATH,
28-
):
29-
env_token = os.environ.get("SA_TOKEN")
30-
host = os.environ.get("SA_URL", constants.BACKEND_URL)
25+
def __init__(self, token: str = None, config_path: str = None):
3126
version = os.environ.get("SA_VERSION", "v1")
32-
ssl_verify = bool(os.environ.get("SA_SSL", True))
27+
_token, _config_path = None, None
28+
_host = os.environ.get("SA_URL", constants.BACKEND_URL)
29+
_ssl_verify = bool(os.environ.get("SA_SSL", True))
3330
if token:
34-
token = Controller.validate_token(token=token)
35-
elif env_token:
36-
host = os.environ.get("SA_URL", constants.BACKEND_URL)
37-
token = Controller.validate_token(env_token)
31+
_token = Controller.validate_token(token=token)
32+
elif config_path:
33+
_token, _host, _ssl_verify = self._retrieve_configs(config_path)
3834
else:
39-
config_path = os.path.expanduser(str(config_path))
40-
if not Path(config_path).is_file() or not os.access(config_path, os.R_OK):
41-
raise AppException(
42-
f"SuperAnnotate config file {str(config_path)} not found."
43-
f" Please provide correct config file location to sa.init(<path>) or use "
44-
f"CLI's superannotate init to generate default location config file."
35+
_token = os.environ.get("SA_TOKEN")
36+
if not _token:
37+
_toke, _host, _ssl_verify = self._retrieve_configs(
38+
constants.CONFIG_PATH
4539
)
46-
config_repo = ConfigRepository(config_path)
47-
main_endpoint = config_repo.get_one("main_endpoint").value
48-
if not main_endpoint:
49-
main_endpoint = constants.BACKEND_URL
50-
token, host, ssl_verify = (
51-
Controller.validate_token(config_repo.get_one("token").value),
52-
main_endpoint,
53-
config_repo.get_one("ssl_verify").value,
40+
self._token, self._host = _host, _token
41+
self.controller = Controller(_token, _host, _ssl_verify, version)
42+
43+
def __new__(cls, *args, **kwargs):
44+
obj = super().__new__(cls, *args, **kwargs)
45+
cls.REGISTRY.append(obj)
46+
return obj
47+
48+
@staticmethod
49+
def _retrieve_configs(path) -> Tuple[str, str, str]:
50+
config_path = os.path.expanduser(str(path))
51+
if not Path(config_path).is_file() or not os.access(config_path, os.R_OK):
52+
raise AppException(
53+
f"SuperAnnotate config file {str(config_path)} not found."
54+
f" Please provide correct config file location to sa.init(<path>) or use "
55+
f"CLI's superannotate init to generate default location config file."
5456
)
55-
self._host = host
56-
self._token = token
57-
self.controller = Controller(token, host, ssl_verify, version)
58-
BaseInterfaceFacade.REGISTRY.append(self)
57+
config_repo = ConfigRepository(config_path)
58+
return (
59+
Controller.validate_token(config_repo.get_one("token").value),
60+
config_repo.get_one("main_endpoint").value,
61+
config_repo.get_one("ssl_verify").value,
62+
)
5963

6064
@property
6165
def host(self):

src/superannotate/lib/app/interface/sdk_interface.py

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from typing import Union
1414

1515
import boto3
16-
import lib.core as constances
16+
import lib.core as constants
1717
from lib.app.annotation_helpers import add_annotation_bbox_to_json
1818
from lib.app.annotation_helpers import add_annotation_comment_to_json
1919
from lib.app.annotation_helpers import add_annotation_point_to_json
@@ -62,6 +62,13 @@
6262

6363

6464
class SAClient(BaseInterfaceFacade, metaclass=TrackableMeta):
65+
def __init__(
66+
self,
67+
token: str = None,
68+
config_path: str = constants.CONFIG_PATH,
69+
):
70+
super().__init__(token, config_path)
71+
6572
def get_team_metadata(self):
6673
"""Returns team metadata
6774
@@ -415,11 +422,11 @@ def copy_image(
415422
).data
416423

417424
if destination_project_metadata["project"].type in [
418-
constances.ProjectType.VIDEO.value,
419-
constances.ProjectType.DOCUMENT.value,
425+
constants.ProjectType.VIDEO.value,
426+
constants.ProjectType.DOCUMENT.value,
420427
] or source_project_metadata["project"].type in [
421-
constances.ProjectType.VIDEO.value,
422-
constances.ProjectType.DOCUMENT.value,
428+
constants.ProjectType.VIDEO.value,
429+
constants.ProjectType.DOCUMENT.value,
423430
]:
424431
raise AppException(
425432
LIMITED_FUNCTIONS[source_project_metadata["project"].type]
@@ -738,8 +745,8 @@ def assign_images(
738745
project = self.controller.get_project_metadata(project_name).data
739746

740747
if project["project"].type in [
741-
constances.ProjectType.VIDEO.value,
742-
constances.ProjectType.DOCUMENT.value,
748+
constants.ProjectType.VIDEO.value,
749+
constants.ProjectType.DOCUMENT.value,
743750
]:
744751
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
745752

@@ -864,12 +871,12 @@ def upload_images_from_folder_to_project(
864871
folder_path: Union[NotEmptyStr, Path],
865872
extensions: Optional[
866873
Union[List[NotEmptyStr], Tuple[NotEmptyStr]]
867-
] = constances.DEFAULT_IMAGE_EXTENSIONS,
874+
] = constants.DEFAULT_IMAGE_EXTENSIONS,
868875
annotation_status="NotStarted",
869876
from_s3_bucket=None,
870877
exclude_file_patterns: Optional[
871878
Iterable[NotEmptyStr]
872-
] = constances.DEFAULT_FILE_EXCLUDE_PATTERNS,
879+
] = constants.DEFAULT_FILE_EXCLUDE_PATTERNS,
873880
recursive_subfolders: Optional[StrictBool] = False,
874881
image_quality_in_editor: Optional[str] = None,
875882
):
@@ -926,7 +933,7 @@ def upload_images_from_folder_to_project(
926933

927934
if exclude_file_patterns:
928935
exclude_file_patterns = list(exclude_file_patterns) + list(
929-
constances.DEFAULT_FILE_EXCLUDE_PATTERNS
936+
constants.DEFAULT_FILE_EXCLUDE_PATTERNS
930937
)
931938
exclude_file_patterns = list(set(exclude_file_patterns))
932939

@@ -1082,12 +1089,12 @@ def prepare_export(
10821089
folders = folder_names
10831090
if not annotation_statuses:
10841091
annotation_statuses = [
1085-
constances.AnnotationStatus.NOT_STARTED.name,
1086-
constances.AnnotationStatus.IN_PROGRESS.name,
1087-
constances.AnnotationStatus.QUALITY_CHECK.name,
1088-
constances.AnnotationStatus.RETURNED.name,
1089-
constances.AnnotationStatus.COMPLETED.name,
1090-
constances.AnnotationStatus.SKIPPED.name,
1092+
constants.AnnotationStatus.NOT_STARTED.name,
1093+
constants.AnnotationStatus.IN_PROGRESS.name,
1094+
constants.AnnotationStatus.QUALITY_CHECK.name,
1095+
constants.AnnotationStatus.RETURNED.name,
1096+
constants.AnnotationStatus.COMPLETED.name,
1097+
constants.AnnotationStatus.SKIPPED.name,
10911098
]
10921099
response = self.controller.prepare_export(
10931100
project_name=project_name,
@@ -1106,7 +1113,7 @@ def upload_videos_from_folder_to_project(
11061113
folder_path: Union[NotEmptyStr, Path],
11071114
extensions: Optional[
11081115
Union[Tuple[NotEmptyStr], List[NotEmptyStr]]
1109-
] = constances.DEFAULT_VIDEO_EXTENSIONS,
1116+
] = constants.DEFAULT_VIDEO_EXTENSIONS,
11101117
exclude_file_patterns: Optional[List[NotEmptyStr]] = (),
11111118
recursive_subfolders: Optional[StrictBool] = False,
11121119
target_fps: Optional[int] = None,
@@ -1593,8 +1600,8 @@ def upload_preannotations_from_folder_to_project(
15931600
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
15941601
project = self.controller.get_project_metadata(project_name).data
15951602
if project["project"].type in [
1596-
constances.ProjectType.VIDEO.value,
1597-
constances.ProjectType.DOCUMENT.value,
1603+
constants.ProjectType.VIDEO.value,
1604+
constants.ProjectType.DOCUMENT.value,
15981605
]:
15991606
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
16001607
if recursive_subfolders:
@@ -1649,8 +1656,8 @@ def upload_image_annotations(
16491656

16501657
project = self.controller.get_project_metadata(project_name).data
16511658
if project["project"].type in [
1652-
constances.ProjectType.VIDEO.value,
1653-
constances.ProjectType.DOCUMENT.value,
1659+
constants.ProjectType.VIDEO.value,
1660+
constants.ProjectType.DOCUMENT.value,
16541661
]:
16551662
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
16561663

@@ -1677,7 +1684,7 @@ def upload_image_annotations(
16771684
mask=mask,
16781685
verbose=verbose,
16791686
)
1680-
if response.errors and not response.errors == constances.INVALID_JSON_MESSAGE:
1687+
if response.errors and not response.errors == constants.INVALID_JSON_MESSAGE:
16811688
raise AppException(response.errors)
16821689

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

17361743
project = self.controller.get_project_metadata(project_name).data
17371744
if project["project"].type in [
1738-
constances.ProjectType.VIDEO.value,
1739-
constances.ProjectType.DOCUMENT.value,
1745+
constants.ProjectType.VIDEO.value,
1746+
constants.ProjectType.DOCUMENT.value,
17401747
]:
17411748
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
17421749

@@ -1887,8 +1894,8 @@ def add_annotation_bbox_to_image(
18871894
project_name, folder_name = extract_project_folder(project)
18881895
project = self.controller.get_project_metadata(project_name).data
18891896
if project["project"].type in [
1890-
constances.ProjectType.VIDEO.value,
1891-
constances.ProjectType.DOCUMENT.value,
1897+
constants.ProjectType.VIDEO.value,
1898+
constants.ProjectType.DOCUMENT.value,
18921899
]:
18931900
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
18941901
response = self.controller.get_annotations(
@@ -1945,8 +1952,8 @@ def add_annotation_point_to_image(
19451952
project_name, folder_name = extract_project_folder(project)
19461953
project = self.controller.get_project_metadata(project_name).data
19471954
if project["project"].type in [
1948-
constances.ProjectType.VIDEO.value,
1949-
constances.ProjectType.DOCUMENT.value,
1955+
constants.ProjectType.VIDEO.value,
1956+
constants.ProjectType.DOCUMENT.value,
19501957
]:
19511958
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
19521959
response = self.controller.get_annotations(
@@ -2000,8 +2007,8 @@ def add_annotation_comment_to_image(
20002007
project_name, folder_name = extract_project_folder(project)
20012008
project = self.controller.get_project_metadata(project_name).data
20022009
if project["project"].type in [
2003-
constances.ProjectType.VIDEO.value,
2004-
constances.ProjectType.DOCUMENT.value,
2010+
constants.ProjectType.VIDEO.value,
2011+
constants.ProjectType.DOCUMENT.value,
20052012
]:
20062013
raise AppException(LIMITED_FUNCTIONS[project["project"].type])
20072014
response = self.controller.get_annotations(
@@ -2187,8 +2194,8 @@ def aggregate_annotations_as_df(
21872194
:rtype: pandas DataFrame
21882195
"""
21892196
if project_type in (
2190-
constances.ProjectType.VECTOR.name,
2191-
constances.ProjectType.PIXEL.name,
2197+
constants.ProjectType.VECTOR.name,
2198+
constants.ProjectType.PIXEL.name,
21922199
):
21932200
from superannotate.lib.app.analytics.common import (
21942201
aggregate_image_annotations_as_df,
@@ -2202,8 +2209,8 @@ def aggregate_annotations_as_df(
22022209
folder_names=folder_names,
22032210
)
22042211
elif project_type in (
2205-
constances.ProjectType.VIDEO.name,
2206-
constances.ProjectType.DOCUMENT.name,
2212+
constants.ProjectType.VIDEO.name,
2213+
constants.ProjectType.DOCUMENT.name,
22072214
):
22082215
from superannotate.lib.app.analytics.aggregators import DataAggregator
22092216

@@ -2214,21 +2221,21 @@ def aggregate_annotations_as_df(
22142221
).aggregate_annotations_as_df()
22152222

22162223
def delete_annotations(
2217-
self, project: NotEmptyStr, image_names: Optional[List[NotEmptyStr]] = None
2224+
self, project: NotEmptyStr, item_names: Optional[List[NotEmptyStr]] = None
22182225
):
22192226
"""
2220-
Delete image annotations from a given list of images.
2227+
Delete item annotations from a given list of items.
22212228
22222229
:param project: project name or folder path (e.g., "project1/folder1")
22232230
:type project: str
2224-
:param image_names: image names. If None, all image annotations from a given project/folder will be deleted.
2225-
:type image_names: list of strs
2231+
:param item_names: image names. If None, all image annotations from a given project/folder will be deleted.
2232+
:type item_names: list of strs
22262233
"""
22272234

22282235
project_name, folder_name = extract_project_folder(project)
22292236

22302237
response = self.controller.delete_annotations(
2231-
project_name=project_name, folder_name=folder_name, item_names=image_names
2238+
project_name=project_name, folder_name=folder_name, item_names=item_names
22322239
)
22332240
if response.errors:
22342241
raise AppException(response.errors)

src/superannotate/lib/app/interface/types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,31 @@
1414
from pydantic import BaseModel
1515
from pydantic import conlist
1616
from pydantic import constr
17+
from pydantic import errors
1718
from pydantic import Extra
1819
from pydantic import Field
1920
from pydantic import parse_obj_as
2021
from pydantic import root_validator
2122
from pydantic import StrictStr
2223
from pydantic import validate_arguments as pydantic_validate_arguments
2324
from pydantic import ValidationError
25+
from pydantic.errors import PydanticTypeError
2426
from pydantic.errors import StrRegexError
2527

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

2830

31+
class EnumMemberError(PydanticTypeError):
32+
code = "enum"
33+
34+
def __str__(self) -> str:
35+
permitted = ", ".join(str(v.name) for v in self.enum_values) # type: ignore
36+
return f"Available values are: {permitted}"
37+
38+
39+
errors.EnumMemberError = EnumMemberError
40+
41+
2942
class EmailStr(StrictStr):
3043
@classmethod
3144
def validate(cls, value: Union[str]) -> Union[str]:

src/superannotate/lib/core/enums.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ def __new__(cls, title, value):
88
obj._value_ = value
99
obj.__doc__ = title
1010
obj._type = "titled_enum"
11+
cls._value2member_map_[title] = obj
1112
return obj
1213

14+
@classmethod
15+
def choices(cls):
16+
return tuple(cls._value2member_map_.keys())
17+
1318
@DynamicClassAttribute
1419
def name(self) -> str:
1520
return self.__doc__
@@ -41,6 +46,9 @@ def titles(cls):
4146
def equals(self, other: Enum):
4247
return self.__doc__.lower() == other.__doc__.lower()
4348

49+
def __eq__(self, other):
50+
return super().__eq__(other)
51+
4452

4553
class AnnotationTypes(str, Enum):
4654
BBOX = "bbox"

0 commit comments

Comments
 (0)