Skip to content

Commit cafa90d

Browse files
committed
Merge branch 're-design-sdk' of https://github.com/superannotateai/superannotate-python-sdk into re-design-sdk
2 parents 82926d9 + 9c988f5 commit cafa90d

File tree

10 files changed

+98
-92
lines changed

10 files changed

+98
-92
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ SDK is available on PyPI:
2121
pip install superannotate
2222
```
2323

24-
The package officially supports Python 3.8+ and was tested under Linux and
24+
The package officially supports Python 3.6+ and was tested under Linux and
2525
Windows ([Anaconda](https://www.anaconda.com/products/individual#windows)) platforms.
2626

2727
For more detailed installation steps and package usage please have a look at the

docs/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ SDK is available on PyPI:
4848
pip install superannotate
4949
5050
51-
The package officially supports Python 3.8+ and was tested under Linux and
51+
The package officially supports Python 3.6+ and was tested under Linux and
5252
Windows (`Anaconda <https://www.anaconda.com/products/individual#windows>`_) platforms.
5353

5454
For more detailed installation steps and package usage please have a look at

docs/source/tutorial.sdk.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ SDK is available on PyPI:
1717
1818
pip install superannotate
1919
20-
The package officially supports Python 3.8+ and was tested under Linux and
20+
The package officially supports Python 3.6+ and was tested under Linux and
2121
Windows (`Anaconda <https://www.anaconda.com/products/individual#windows>`_) platforms.
2222

2323
For certain video related functions to work, ffmpeg package needs to be installed.

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

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,5 @@
4242
entry_points={
4343
'console_scripts': ['superannotatecli = superannotate.lib.app.bin.superannotate:main']
4444
},
45-
python_requires='>=3.8'
45+
python_requires='>=3.6'
4646
)

src/superannotate/lib/app/helpers.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22
from functools import wraps
33
from inspect import getfullargspec
44
from pathlib import Path
5-
from typing import get_args
6-
from typing import get_origin
75
from typing import get_type_hints
86
from typing import List
9-
from typing import Literal
107
from typing import Optional
118
from typing import Tuple
129
from typing import Union
@@ -134,23 +131,18 @@ def check_types(obj, **kwargs):
134131
if attr_name == "return":
135132
continue
136133
try:
137-
if get_origin(attr_type) is list:
138-
if get_args(attr_type):
134+
if attr_type.__origin__ is list:
135+
if attr_type.__args__:
139136
for elem in kwargs[attr_name]:
140-
if type(elem) in get_args(attr_type):
137+
if type(elem) in attr_type.__args__:
141138
continue
142139
else:
143140
errors.append(f"Invalid input {attr_name}")
144141
elif not isinstance(kwargs[attr_name], list):
145142
errors.append(f"Argument {attr_name} is not of type {attr_type}")
146-
if get_origin(attr_type) is Literal:
147-
if kwargs[attr_name] not in get_args(attr_type):
148-
errors.append(
149-
f'The value of {attr_name} should be {", ".join(get_args(attr_type))}'
150-
)
151-
elif get_origin(attr_type) is Union:
143+
elif attr_type.__origin__ is Union:
152144
if kwargs.get(attr_name) and not isinstance(
153-
kwargs[attr_name], get_args(attr_type)
145+
kwargs[attr_name], attr_type.__args__
154146
):
155147
errors.append(f"Argument {attr_name} is not of type {attr_type}")
156148
elif kwargs.get(attr_name) and not isinstance(kwargs[attr_name], attr_type):

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

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pathlib import Path
1212
from typing import Any
1313
from typing import Optional
14+
from tqdm import tqdm
1415

1516
import lib.core as constances
1617
import pandas as pd
@@ -152,17 +153,19 @@ def upload_image(image_path: str):
152153
set(filtered_paths),
153154
[item for item in duplication_counter if duplication_counter[item] > 1],
154155
)
155-
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
156-
results = [
157-
executor.submit(upload_image, image_path)
158-
for image_path in images_to_upload
159-
]
160-
for future in concurrent.futures.as_completed(results):
161-
processed_image = future.result()
162-
if processed_image.uploaded and processed_image.entity:
163-
uploaded_image_entities.append(processed_image.entity)
164-
else:
165-
failed_images.append(processed_image.path)
156+
with tqdm(total=len(images_to_upload)) as progress_bar:
157+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
158+
results = [
159+
executor.submit(upload_image, image_path)
160+
for image_path in images_to_upload
161+
]
162+
for future in concurrent.futures.as_completed(results):
163+
processed_image = future.result()
164+
if processed_image.uploaded and processed_image.entity:
165+
uploaded_image_entities.append(processed_image.entity)
166+
else:
167+
failed_images.append(processed_image.path)
168+
progress_bar.update(1)
166169

167170
for i in range(0, len(uploaded_image_entities), 500):
168171
self.controller.upload_images(
@@ -248,7 +251,7 @@ def _upload_annotations(
248251
project_name, folder_name = split_project_path(project)
249252
project = self.controller.get_project_metadata(project_name=project_name).data
250253
if not format:
251-
format = "COCO"
254+
format = "SuperAnnotate"
252255
if not data_set_name and format == "COCO":
253256
raise Exception("Data-set name is required")
254257
elif not data_set_name:

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

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from io import BytesIO
1212
from pathlib import Path
1313
from typing import List
14-
from typing import Literal
1514
from typing import Optional
1615
from typing import Union
1716
from urllib.parse import urlparse
@@ -481,7 +480,7 @@ def search_folders(
481480
def get_image_bytes(
482481
project: Union[str, dict],
483482
image_name: str,
484-
variant: Literal["original", "lores"] = "original",
483+
variant: str = "original",
485484
):
486485
"""Returns an io.BytesIO() object of the image. Suitable for creating
487486
PIL.Image out of it.
@@ -597,17 +596,8 @@ def upload_images_from_public_urls_to_project(
597596
project: Union[str, dict],
598597
img_urls: List[str],
599598
img_names: Optional[List[str]] = None,
600-
annotation_status: Optional[
601-
Literal[
602-
"NotStarted",
603-
"InProgress",
604-
"QualityCheck",
605-
"Returned",
606-
"Completed",
607-
"Skipped",
608-
]
609-
] = "NotStarted",
610-
image_quality_in_editor: Optional[Literal["compressed", "original"]] = None,
599+
annotation_status: Optional[str] = "NotStarted",
600+
image_quality_in_editor: Optional[str] = None,
611601
):
612602
"""Uploads all images given in the list of URL strings in img_urls to the project.
613603
Sets status of all the uploaded images to annotation_status if it is not None.
@@ -823,7 +813,7 @@ def move_images(
823813
).data
824814
moved_count = len(moved_images)
825815
message_postfix = "{from_path} to {to_path}."
826-
message_prefix = "Copied images from "
816+
message_prefix = "Moved images from "
827817
if moved_count > 1 or moved_count == 0:
828818
message_prefix = f"Moved {moved_count}/{len(image_names)} images from "
829819
elif moved_count == 1:
@@ -992,7 +982,7 @@ def get_project_default_image_quality_in_editor(project: Union[str, dict]):
992982
@validate_input
993983
def set_project_default_image_quality_in_editor(
994984
project: Union[str, dict],
995-
image_quality_in_editor: Optional[Literal["compressed", "original"]],
985+
image_quality_in_editor: Optional[str],
996986
):
997987
"""Sets project's default image quality in editor setting.
998988
@@ -1087,9 +1077,7 @@ def get_image_metadata(project: Union[str, dict], image_name: str, *_, **__):
10871077
def set_images_annotation_statuses(
10881078
project: Union[str, dict],
10891079
image_names: List[str],
1090-
annotation_status: Literal[
1091-
"NotStarted", "InProgress", "QualityCheck", "Returned", "Completed", "Skipped",
1092-
],
1080+
annotation_status: str,
10931081
):
10941082
"""Sets annotation statuses of images
10951083
@@ -1497,7 +1485,7 @@ def upload_images_from_folder_to_project(
14971485
from_s3_bucket=None,
14981486
exclude_file_patterns: Optional[str] = constances.DEFAULT_FILE_EXCLUDE_PATTERNS,
14991487
recursive_subfolders: Optional[bool] = False,
1500-
image_quality_in_editor: Optional[Literal["compressed", "original"]] = None,
1488+
image_quality_in_editor: Optional[str] = None,
15011489
):
15021490
"""Uploads all images with given extensions from folder_path to the project.
15031491
Sets status of all the uploaded images to set_status if it is not None.
@@ -1689,7 +1677,7 @@ def upload_images_from_s3_bucket_to_project(
16891677
secretAccessKey: str,
16901678
bucket_name: str,
16911679
folder_path: str,
1692-
image_quality_in_editor: Optional[Literal["compressed", "original"]] = None,
1680+
image_quality_in_editor: Optional[str] = None,
16931681
):
16941682
"""Uploads all images from AWS S3 bucket to the project.
16951683

src/superannotate/lib/core/usecases.py

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from abc import abstractmethod
1313
from collections import defaultdict
1414
from collections import namedtuple
15-
from functools import cached_property
1615
from pathlib import Path
1716
from typing import Iterable
1817
from typing import List
@@ -4102,6 +4101,8 @@ def __init__(
41024101
super().__init__()
41034102

41044103
self._auth_data = None
4104+
self._s3_repo_instance = None
4105+
self._images_to_upload = None
41054106

41064107
self._project = project
41074108
self._folder = folder
@@ -4134,22 +4135,25 @@ def validate_project_type(self):
41344135
"The function does not support projects containing videos attached with URLs"
41354136
)
41364137

4137-
@cached_property
4138+
@property
41384139
def auth_data(self):
4139-
return self._backend_client.get_s3_upload_auth_token(
4140+
if not self._auth_data:
4141+
self._auth_data = self._backend_client.get_s3_upload_auth_token(
41404142
team_id=self._project.team_id,
41414143
folder_id=self._folder.uuid,
4142-
project_id=self._project.uuid,
4143-
)
4144+
project_id=self._project.uuid, )
4145+
return self._auth_data
41444146

4145-
@cached_property
4147+
@property
41464148
def s3_repository(self):
4147-
return self._s3_repo(
4149+
if not self._s3_repo_instance:
4150+
self._s3_repo_instance = self._s3_repo(
41484151
self.auth_data["accessKeyId"],
41494152
self.auth_data["secretAccessKey"],
41504153
self.auth_data["sessionToken"],
41514154
self.auth_data["bucket"],
41524155
)
4156+
return self._s3_repo_instance
41534157

41544158
def _upload_image(self, image_path: str):
41554159
ProcessedImage = namedtuple("ProcessedImage", ["uploaded", "path", "entity"])
@@ -4229,30 +4233,32 @@ def paths(self):
42294233
and "___pixel" not in path
42304234
]
42314235

4232-
@cached_property
4236+
@property
42334237
def images_to_upload(self):
4234-
paths = self.paths
4235-
filtered_paths = []
4236-
duplicated_paths = []
4237-
images = self._backend_client.get_bulk_images(
4238-
project_id=self._project.uuid,
4239-
team_id=self._project.team_id,
4240-
folder_id=self._folder.uuid,
4241-
images=[Path(image).name for image in paths],
4242-
)
4243-
image_names = [image["name"] for image in images]
4244-
4245-
for path in paths:
4246-
not_in_exclude_list = [
4247-
x not in Path(path).name for x in self.exclude_file_patterns
4248-
]
4249-
non_in_service_list = [x not in Path(path).name for x in image_names]
4250-
if all(not_in_exclude_list) and all(non_in_service_list):
4251-
filtered_paths.append(path)
4252-
if not all(non_in_service_list):
4253-
duplicated_paths.append(path)
4238+
if not self._images_to_upload:
4239+
paths = self.paths
4240+
filtered_paths = []
4241+
duplicated_paths = []
4242+
images = self._backend_client.get_bulk_images(
4243+
project_id=self._project.uuid,
4244+
team_id=self._project.team_id,
4245+
folder_id=self._folder.uuid,
4246+
images=[Path(image).name for image in paths],
4247+
)
4248+
image_names = [image["name"] for image in images]
42544249

4255-
return list(set(filtered_paths)), duplicated_paths
4250+
for path in paths:
4251+
not_in_exclude_list = [
4252+
x not in Path(path).name for x in self.exclude_file_patterns
4253+
]
4254+
non_in_service_list = [x not in Path(path).name for x in image_names]
4255+
if all(not_in_exclude_list) and all(non_in_service_list):
4256+
filtered_paths.append(path)
4257+
if not all(non_in_service_list):
4258+
duplicated_paths.append(path)
4259+
4260+
self._images_to_upload = list(set(filtered_paths)), duplicated_paths
4261+
return self._images_to_upload
42564262

42574263
def execute(self):
42584264
images_to_upload, duplications = self.images_to_upload

src/superannotate/lib/infrastructure/controller.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import copy
22
import io
3-
from functools import cached_property
43
from typing import Iterable
54
from typing import List
65
from typing import Optional
@@ -51,6 +50,12 @@ def __init__(self, logger, config_path=constances.CONFIG_FILE_LOCATION):
5150
logger=logger,
5251
)
5352
self._s3_upload_auth_data = None
53+
self._projects = None
54+
self._folders = None
55+
self._teams = None
56+
self._images = None
57+
self._ml_models = None
58+
self._team_id = None
5459

5560
def set_token(self, token):
5661
self.configs.insert(ConfigEntity("token", token))
@@ -60,33 +65,45 @@ def set_token(self, token):
6065
logger=self._logger,
6166
)
6267

63-
@cached_property
68+
@property
6469
def projects(self):
65-
return ProjectRepository(self._backend_client)
70+
if not self._projects:
71+
self._projects = ProjectRepository(self._backend_client)
72+
return self._projects
6673

67-
@cached_property
74+
@property
6875
def folders(self):
69-
return FolderRepository(self._backend_client)
76+
if not self._folders:
77+
self._folders = FolderRepository(self._backend_client)
78+
return self._folders
7079

71-
@cached_property
80+
@property
7281
def ml_models(self):
73-
return MLModelRepository(self._backend_client, self.team_id)
82+
if not self._ml_models:
83+
self._ml_models = MLModelRepository(self._backend_client, self.team_id)
84+
return self._ml_models
7485

75-
@cached_property
86+
@property
7687
def teams(self):
77-
return TeamRepository(self._backend_client)
88+
if not self._teams:
89+
self._teams = TeamRepository(self._backend_client)
90+
return self._teams
7891

79-
@cached_property
92+
@property
8093
def images(self):
81-
return ImageRepository(self._backend_client)
94+
if not self._images:
95+
self._images = ImageRepository(self._backend_client)
96+
return self._images
8297

8398
@property
8499
def configs(self):
85100
return ConfigRepository(self._config_path)
86101

87-
@cached_property
102+
@property
88103
def team_id(self) -> int:
89-
return int(self.configs.get_one("token").value.split("=")[-1])
104+
if not self._team_id:
105+
self._team_id = int(self.configs.get_one("token").value.split("=")[-1])
106+
return self._team_id
90107

91108
@timed_lru_cache(seconds=3600)
92109
def get_auth_data(self, project_id: int, team_id: int, folder_id: int):

0 commit comments

Comments
 (0)