Skip to content

Commit 6d8c8a0

Browse files
authored
Merge pull request #183 from superannotateai/re-design-sdk
Re design sdk
2 parents d04b8ca + 7cd414d commit 6d8c8a0

File tree

9 files changed

+149
-53
lines changed

9 files changed

+149
-53
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.0
33
log_cli=true
44
python_files = test_*.py
5-
addopts = -n32 --dist=loadscope
5+
;addopts = -n32 --dist=loadscope

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ def init(path_to_config_json: str):
5959
:param path_to_config_json: Location to config JSON file
6060
:type path_to_config_json: str or Path
6161
"""
62-
global controller
63-
controller = Controller(logger, path_to_config_json)
62+
# global controller
63+
controller.init(path_to_config_json)
6464

6565

6666
@validate_input
@@ -2599,9 +2599,25 @@ def upload_preannotations_from_folder_to_project(
25992599
"The function does not support projects containing videos attached with URLs"
26002600
)
26012601

2602+
if recursive_subfolders:
2603+
logger.info(
2604+
"When using recursive subfolder parsing same name annotations in different subfolders will overwrite each other.",
2605+
)
2606+
2607+
logger.info(
2608+
"The JSON files should follow specific naming convention. For Vector projects they should be named '<image_name>___objects.json', for Pixel projects JSON file should be names '<image_name>___pixel.json' and also second mask image file should be present with the name '<image_name>___save.png'. In both cases image with <image_name> should be already present on the platform."
2609+
)
2610+
logger.info("Existing annotations will be overwritten.",)
2611+
logger.info(
2612+
"Uploading all annotations from %s to project %s.", folder_path, project_name
2613+
)
2614+
26022615
annotation_paths = get_annotation_paths(
26032616
folder_path, from_s3_bucket, recursive_subfolders
26042617
)
2618+
logger.info(
2619+
"Uploading %s annotations to project %s.", len(annotation_paths), project_name
2620+
)
26052621
uploaded_annotations = []
26062622
failed_annotations = []
26072623
missing_annotations = []

src/superannotate/lib/app/mixp/decorators.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import functools
2-
import logging
32
import sys
43

54
from mixpanel import Mixpanel
@@ -11,7 +10,7 @@
1110

1211
mp = Mixpanel(TOKEN)
1312

14-
controller = Controller(logger=logging.getLogger())
13+
controller = Controller.get_instance()
1514
res = controller.get_team()
1615
user_id, team_name = res.data.creator_id, res.data.name
1716

src/superannotate/lib/core/serviceproviders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def delete_image_annotations(
303303
project_id: int,
304304
folder_id: int = None,
305305
image_names: List[str] = None,
306-
) -> int:
306+
) -> dict:
307307
raise NotImplementedError
308308

309309
def get_annotations_delete_progress(

src/superannotate/lib/core/usecases.py

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4077,12 +4077,14 @@ def execute(self):
40774077
image_ids = [image["id"] for image in images]
40784078
image_names = [image["name"] for image in images]
40794079

4080-
self._service.run_segmentation(
4080+
res = self._service.run_segmentation(
40814081
self._project.team_id,
40824082
self._project.uuid,
40834083
model_name=self._ml_model_name,
40844084
image_ids=image_ids,
40854085
)
4086+
if not res.ok:
4087+
return self._response
40864088

40874089
succeded_imgs = []
40884090
failed_imgs = []
@@ -4158,12 +4160,14 @@ def execute(self):
41584160
if model.name == self._ml_model_name:
41594161
ml_model = model
41604162

4161-
self._service.run_prediction(
4163+
res = self._service.run_prediction(
41624164
team_id=self._project.team_id,
41634165
project_id=self._project.uuid,
41644166
ml_model_id=ml_model.uuid,
41654167
image_ids=image_ids,
41664168
)
4169+
if not res.ok:
4170+
return self._response
41674171

41684172
success_images = []
41694173
failed_images = []
@@ -4266,7 +4270,7 @@ def __init__(
42664270
extensions=constances.DEFAULT_IMAGE_EXTENSIONS,
42674271
annotation_status="NotStarted",
42684272
from_s3_bucket=None,
4269-
exclude_file_patterns=constances.DEFAULT_FILE_EXCLUDE_PATTERNS,
4273+
exclude_file_patterns: List[str] = constances.DEFAULT_FILE_EXCLUDE_PATTERNS,
42704274
recursive_sub_folders: bool = False,
42714275
image_quality_in_editor=None,
42724276
):
@@ -4286,6 +4290,10 @@ def __init__(
42864290
self._from_s3_bucket = from_s3_bucket
42874291
self._extensions = extensions
42884292
self._recursive_sub_folders = recursive_sub_folders
4293+
if exclude_file_patterns:
4294+
list(exclude_file_patterns).extend(
4295+
list(constances.DEFAULT_FILE_EXCLUDE_PATTERNS)
4296+
)
42894297
self._exclude_file_patterns = exclude_file_patterns
42904298
self._annotation_status = annotation_status
42914299

@@ -4302,12 +4310,19 @@ def exclude_file_patterns(self):
43024310
return self._exclude_file_patterns
43034311

43044312
def validate_annotation_status(self):
4305-
if self._annotation_status and self._annotation_status not in constances.AnnotationStatus.values():
4313+
if (
4314+
self._annotation_status
4315+
and self._annotation_status.lower()
4316+
not in constances.AnnotationStatus.values()
4317+
):
43064318
raise AppValidationException("Invalid annotations status")
43074319

43084320
def validate_extensions(self):
43094321
if self._extensions and not all(
4310-
[extension in constances.DEFAULT_IMAGE_EXTENSIONS for extension in self._extensions]
4322+
[
4323+
extension in constances.DEFAULT_IMAGE_EXTENSIONS
4324+
for extension in self._extensions
4325+
]
43114326
):
43124327
raise AppValidationException("")
43134328

@@ -4420,14 +4435,16 @@ def paths(self):
44204435
paths.append(key)
44214436
break
44224437

4423-
paths = [str(path) for path in paths]
4424-
return [
4425-
path
4426-
for path in paths
4427-
if "___objects" not in path
4428-
and "___fuse" not in path
4429-
and "___pixel" not in path
4430-
]
4438+
data = []
4439+
for path in paths:
4440+
if all(
4441+
[
4442+
True if exclude_pattern not in str(path) else False
4443+
for exclude_pattern in self.exclude_file_patterns
4444+
]
4445+
):
4446+
data.append(str(path))
4447+
return data
44314448

44324449
@property
44334450
def images_to_upload(self):
@@ -4490,8 +4507,9 @@ def execute(self):
44904507
annotation_status=self._annotation_status,
44914508
).execute()
44924509

4493-
attachments, duplications = response.data
4510+
attachments, attach_duplications = response.data
44944511
uploaded.extend(attachments)
4512+
duplications.extend(attach_duplications)
44954513
uploaded = [image["name"] for image in uploaded]
44964514
failed_images = [image.split("/")[-1] for image in failed_images]
44974515

@@ -4517,17 +4535,12 @@ def __init__(
45174535

45184536
def execute(self) -> Response:
45194537

4520-
if self._folder.name == "root" and not self._image_names:
4521-
response = self._backend_service.delete_image_annotations(
4522-
project_id=self._project.uuid, team_id=self._project.team_id, image_names=self._image_names
4523-
)
4524-
else:
4525-
response = self._backend_service.delete_image_annotations(
4526-
project_id=self._project.uuid,
4527-
team_id=self._project.team_id,
4528-
folder_id=self._folder.uuid,
4529-
image_names=self._image_names,
4530-
)
4538+
response = self._backend_service.delete_image_annotations(
4539+
project_id=self._project.uuid,
4540+
team_id=self._project.team_id,
4541+
folder_id=self._folder.uuid,
4542+
image_names=self._image_names,
4543+
)
45314544

45324545
if response:
45334546
timeout_start = time.time()

src/superannotate/lib/infrastructure/controller.py

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,45 +38,58 @@ def __call__(cls, *args, **kwargs):
3838
SingleInstanceMetaClass._instances[cls] = super().__call__(*args, **kwargs)
3939
return SingleInstanceMetaClass._instances[cls]
4040

41+
def get_instance(cls):
42+
if cls._instances:
43+
return cls._instances[cls]
44+
4145

4246
class BaseController(metaclass=SingleInstanceMetaClass):
4347
def __init__(self, logger, config_path=constances.CONFIG_FILE_LOCATION):
44-
self._config_path = config_path
48+
self._config_path = None
49+
self._backend_client = None
4550
self._logger = logger
51+
self._s3_upload_auth_data = None
52+
self._projects = None
53+
self._folders = None
54+
self._teams = None
55+
self._images = None
56+
self._ml_models = None
57+
self._team_id = None
58+
self.init(config_path)
59+
60+
def init(self, config_path):
61+
self._config_path = config_path
4662
token, main_endpoint = (
47-
self.configs.get_one("token"),
48-
self.configs.get_one("main_endpoint"),
63+
self.configs.get_one("token").value,
64+
self.configs.get_one("main_endpoint").value,
4965
)
5066
if not main_endpoint:
5167
self.configs.insert(ConfigEntity("main_endpoint", constances.BACKEND_URL))
5268
if not token:
5369
self.configs.insert(ConfigEntity("token", ""))
54-
logger.warning("Fill config.json")
70+
self._logger.warning("Fill config.json")
5571
return
5672
verify_ssl_entity = self.configs.get_one("ssl_verify")
5773
if not verify_ssl_entity:
5874
verify_ssl = True
5975
else:
6076
verify_ssl = verify_ssl_entity.value
61-
self._backend_client = SuperannotateBackendService(
62-
api_url=self.configs.get_one("main_endpoint").value,
63-
auth_token=ConfigRepository().get_one("token").value,
64-
logger=logger,
65-
verify_ssl=verify_ssl
66-
)
67-
self._s3_upload_auth_data = None
68-
self._projects = None
69-
self._folders = None
70-
self._teams = None
71-
self._images = None
72-
self._ml_models = None
73-
self._team_id = None
77+
if not self._backend_client:
78+
self._backend_client = SuperannotateBackendService(
79+
api_url=self.configs.get_one("main_endpoint").value,
80+
auth_token=self.configs.get_one("token").value,
81+
logger=self._logger,
82+
verify_ssl=verify_ssl,
83+
)
84+
else:
85+
self._backend_client.api_url = self.configs.get_one("main_endpoint").value
86+
self._backend_client._auth_token = self.configs.get_one("token").value
7487

7588
def set_token(self, token):
7689
self.configs.insert(ConfigEntity("token", token))
7790
self._backend_client = SuperannotateBackendService(
7891
api_url=self.configs.get_one("main_endpoint").value,
79-
auth_token=ConfigRepository().get_one("token").value,
92+
auth_token=self.configs.get_one("token").value,
8093
logger=self._logger,
8194
)
8295

src/superannotate/lib/infrastructure/services.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,7 @@ def run_segmentation(
932932
params={"team_id": team_id, "project_id": project_id},
933933
data={"model_name": model_name, "image_ids": image_ids},
934934
)
935-
return res.json()
935+
return res
936936

937937
def run_prediction(
938938
self, team_id: int, project_id: int, ml_model_id: int, image_ids: list
@@ -948,15 +948,15 @@ def run_prediction(
948948
"image_ids": image_ids,
949949
},
950950
)
951-
return res.json()
951+
return res
952952

953953
def delete_image_annotations(
954954
self,
955955
team_id: int,
956956
project_id: int,
957957
folder_id: int = None,
958958
image_names: List[str] = None,
959-
) -> int:
959+
) -> dict:
960960
delete_annotations_url = urljoin(self.api_url, self.URL_DELETE_ANNOTATIONS)
961961
params = {"team_id": team_id, "project_id": project_id}
962962
data = {}

tests/integration/test_annotation_delete.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,25 @@ def test_delete_annotations(self):
4646
self.assertIsNotNone(data["annotation_json_filename"])
4747
self.assertIsNone(data["annotation_mask"])
4848

49+
@pytest.mark.skip(
50+
"waiting for deployment to dev",
51+
)
52+
def test_delete_annotations_by_name(self):
53+
sa.upload_images_from_folder_to_project(
54+
self.PROJECT_NAME, self.folder_path, annotation_status="InProgress"
55+
)
56+
sa.create_annotation_classes_from_classes_json(
57+
self.PROJECT_NAME, self.folder_path + "/classes/classes.json"
58+
)
59+
sa.upload_annotations_from_folder_to_project(
60+
self.PROJECT_NAME, f"{self.folder_path}"
61+
)
62+
sa.delete_annotations(self.PROJECT_NAME, [self.EXAMPLE_IMAGE_1])
63+
data = sa.get_image_annotations(self.PROJECT_NAME, self.EXAMPLE_IMAGE_1)
64+
self.assertIsNone(data["annotation_json"])
65+
self.assertIsNotNone(data["annotation_json_filename"])
66+
self.assertIsNone(data["annotation_mask"])
67+
4968
@pytest.mark.skip(
5069
"waiting for deployment to dev",
5170
)
@@ -61,3 +80,39 @@ def test_delete_annotations_by_not_existing_name(self):
6180
)
6281
self.assertRaises(Exception, sa.delete_annotations, self.PROJECT_NAME, [self.EXAMPLE_IMAGE_2])
6382

83+
@pytest.mark.skip(
84+
"waiting for deployment to dev",
85+
)
86+
def test_delete_annotations_wrong_path(self):
87+
sa.create_folder(self.PROJECT_NAME, self.TEST_FOLDER_NAME)
88+
sa.upload_images_from_folder_to_project(
89+
f"{self.PROJECT_NAME}/{self.TEST_FOLDER_NAME}", self.folder_path, annotation_status="InProgress"
90+
)
91+
sa.create_annotation_classes_from_classes_json(
92+
self.PROJECT_NAME, self.folder_path + "/classes/classes.json"
93+
)
94+
sa.upload_annotations_from_folder_to_project(
95+
f"{self.PROJECT_NAME}/{self.TEST_FOLDER_NAME}", f"{self.folder_path}"
96+
)
97+
self.assertRaises(Exception, sa.delete_annotations, self.PROJECT_NAME, [self.EXAMPLE_IMAGE_1])
98+
99+
@pytest.mark.skip(
100+
"waiting for deployment to dev",
101+
)
102+
def test_delete_annotations_from_folder(self):
103+
sa.create_folder(self.PROJECT_NAME, self.TEST_FOLDER_NAME)
104+
105+
sa.upload_images_from_folder_to_project(
106+
f"{self.PROJECT_NAME}/{self.TEST_FOLDER_NAME}", self.folder_path, annotation_status="InProgress"
107+
)
108+
sa.create_annotation_classes_from_classes_json(
109+
self.PROJECT_NAME, self.folder_path + "/classes/classes.json"
110+
)
111+
sa.upload_annotations_from_folder_to_project(
112+
f"{self.PROJECT_NAME}/{self.TEST_FOLDER_NAME}", f"{self.folder_path}"
113+
)
114+
sa.delete_annotations(f"{self.PROJECT_NAME}/{self.TEST_FOLDER_NAME}", [self.EXAMPLE_IMAGE_1])
115+
data = sa.get_image_annotations(f"{self.PROJECT_NAME}/{self.TEST_FOLDER_NAME}", self.EXAMPLE_IMAGE_1)
116+
self.assertIsNone(data["annotation_json"])
117+
self.assertIsNotNone(data["annotation_json_filename"])
118+
self.assertIsNone(data["annotation_mask"])

tests/integration/test_interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_upload_annotations_from_folder_to_project(self):
5858
sa.upload_images_from_folder_to_project(
5959
self.PROJECT_NAME,
6060
self.folder_path,
61-
annotation_status="InProgress",
61+
annotation_status="Completed",
6262
)
6363
uploaded_annotations, _, _ = sa.upload_annotations_from_folder_to_project(
6464
self.PROJECT_NAME, self.folder_path

0 commit comments

Comments
 (0)