Skip to content

Commit 5e088ed

Browse files
committed
merge
2 parents 5575d0e + 3b97a4c commit 5e088ed

File tree

10 files changed

+186
-176
lines changed

10 files changed

+186
-176
lines changed

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

Lines changed: 20 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import os
66
import tempfile
77
import time
8-
from collections import Counter
98
from collections import namedtuple
109
from pathlib import Path
1110
from typing import Iterable
@@ -43,7 +42,6 @@
4342
from lib.core import LIMITED_FUNCTIONS
4443
from lib.core.enums import ImageQuality
4544
from lib.core.exceptions import AppException
46-
from lib.core.exceptions import AppValidationException
4745
from lib.core.types import AttributeGroup
4846
from lib.core.types import ClassesJson
4947
from lib.core.types import MLModel
@@ -1070,8 +1068,10 @@ def delete_image(project: Union[NotEmptyStr, dict], image_name: str):
10701068
:param image_name: image name
10711069
:type image_name: str
10721070
"""
1073-
project_name, _ = extract_project_folder(project)
1074-
response = controller.delete_image(image_name=image_name, project_name=project_name)
1071+
project_name, folder_name = extract_project_folder(project)
1072+
response = controller.delete_image(
1073+
image_name=image_name, folder_name=folder_name, project_name=project_name
1074+
)
10751075
if response.errors:
10761076
raise AppException("Couldn't delete image ")
10771077
logger.info(f"Successfully deleted image {image_name}.")
@@ -2122,7 +2122,7 @@ def move_image(
21222122
is_pinned=1,
21232123
)
21242124

2125-
controller.delete_image(image_name, source_project_name)
2125+
controller.delete_image(image_name, source_project_name, source_folder_name)
21262126

21272127

21282128
@Trackable
@@ -3490,104 +3490,29 @@ def upload_images_to_project(
34903490
:return: uploaded, could-not-upload, existing-images filepaths
34913491
:rtype: tuple (3 members) of list of strs
34923492
"""
3493-
uploaded_image_entities = []
3494-
failed_images = []
34953493
project_name, folder_name = extract_project_folder(project)
3496-
project = controller.get_project_metadata(project_name).data
3497-
if project["project"].project_type in [
3498-
constances.ProjectType.VIDEO.value,
3499-
constances.ProjectType.DOCUMENT.value,
3500-
]:
3501-
raise AppException(LIMITED_FUNCTIONS[project["project"].project_type])
3502-
3503-
ProcessedImage = namedtuple("ProcessedImage", ["uploaded", "path", "entity"])
35043494

3505-
def _upload_local_image(image_path: str):
3506-
try:
3507-
with open(image_path, "rb") as image:
3508-
image_bytes = io.BytesIO(image.read())
3509-
upload_response = controller.upload_image_to_s3(
3510-
project_name=project_name,
3511-
image_path=image_path,
3512-
image_bytes=image_bytes,
3513-
folder_name=folder_name,
3514-
image_quality_in_editor=image_quality_in_editor,
3515-
)
3516-
3517-
if not upload_response.errors and upload_response.data:
3518-
entity = upload_response.data
3519-
return ProcessedImage(
3520-
uploaded=True, path=entity.path, entity=entity
3521-
)
3522-
else:
3523-
return ProcessedImage(uploaded=False, path=image_path, entity=None)
3524-
except FileNotFoundError:
3525-
return ProcessedImage(uploaded=False, path=image_path, entity=None)
3526-
3527-
def _upload_s3_image(image_path: str):
3528-
try:
3529-
image_bytes = controller.get_image_from_s3(
3530-
s3_bucket=from_s3_bucket, image_path=image_path
3531-
).data
3532-
except AppValidationException as e:
3533-
logger.warning(e)
3534-
return image_path
3535-
upload_response = controller.upload_image_to_s3(
3536-
project_name=project_name,
3537-
image_path=image_path,
3538-
image_bytes=image_bytes,
3539-
folder_name=folder_name,
3540-
image_quality_in_editor=image_quality_in_editor,
3541-
)
3542-
if not upload_response.errors and upload_response.data:
3543-
entity = upload_response.data
3544-
return ProcessedImage(uploaded=True, path=entity.path, entity=entity)
3545-
else:
3546-
return ProcessedImage(uploaded=False, path=image_path, entity=None)
3547-
3548-
filtered_paths = img_paths
3549-
duplication_counter = Counter(filtered_paths)
3550-
images_to_upload, duplicated_images = (
3551-
set(filtered_paths),
3552-
[item for item in duplication_counter if duplication_counter[item] > 1],
3495+
use_case = controller.upload_images_to_project(
3496+
project_name=project_name,
3497+
folder_name=folder_name,
3498+
paths=img_paths,
3499+
annotation_status=annotation_status,
3500+
image_quality_in_editor=image_quality_in_editor,
3501+
from_s3_bucket=from_s3_bucket,
35533502
)
3554-
upload_method = _upload_s3_image if from_s3_bucket else _upload_local_image
3555-
3556-
logger.info(f"Uploading {len(images_to_upload)} images to project.")
3557-
3558-
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
3559-
results = [
3560-
executor.submit(upload_method, image_path)
3561-
for image_path in images_to_upload
3562-
]
3563-
with tqdm(total=len(images_to_upload), desc="Uploading images") as progress_bar:
3564-
for future in concurrent.futures.as_completed(results):
3565-
processed_image = future.result()
3566-
if processed_image.uploaded and processed_image.entity:
3567-
uploaded_image_entities.append(processed_image.entity)
3568-
else:
3569-
failed_images.append(processed_image.path)
3570-
progress_bar.update(1)
3571-
uploaded = []
3572-
duplicates = []
3573-
3574-
for i in range(0, len(uploaded_image_entities), 500):
3575-
response = controller.upload_images(
3576-
project_name=project_name,
3577-
folder_name=folder_name,
3578-
images=uploaded_image_entities[i : i + 500], # noqa: E203
3579-
annotation_status=annotation_status,
3580-
)
3581-
attachments, duplications = response.data
3582-
uploaded.extend([attachment["name"] for attachment in attachments])
3583-
duplicates.extend(duplications)
35843503

3504+
images_to_upload, duplicates = use_case.images_to_upload
35853505
if len(duplicates):
35863506
logger.warning(
35873507
"%s already existing images found that won't be uploaded.", len(duplicates)
35883508
)
3589-
3590-
return uploaded, failed_images, duplicates
3509+
logger.info(f"Uploading {len(images_to_upload)} images to project {project}.")
3510+
if use_case.is_valid():
3511+
with tqdm(total=len(images_to_upload), desc="Uploading images") as progress_bar:
3512+
for _ in use_case.execute():
3513+
progress_bar.update(1)
3514+
return use_case.data
3515+
raise AppException(use_case.response.errors)
35913516

35923517

35933518
@Trackable

src/superannotate/lib/core/usecases.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,36 @@ def validate_project_type(self):
341341
constances.LIMITED_FUNCTIONS[self._project.project_type]
342342
)
343343

344+
def validate_project_name(self):
345+
if self._project_to_create.name:
346+
if (
347+
len(
348+
set(self._project_to_create.name).intersection(
349+
constances.SPECIAL_CHARACTERS_IN_PROJECT_FOLDER_NAMES
350+
)
351+
)
352+
> 0
353+
):
354+
self._project_to_create.name = "".join(
355+
"_"
356+
if char in constances.SPECIAL_CHARACTERS_IN_PROJECT_FOLDER_NAMES
357+
else char
358+
for char in self._project_to_create.name
359+
)
360+
logger.warning(
361+
"New folder name has special characters. Special characters will be replaced by underscores."
362+
)
363+
condition = Condition("name", self._project_to_create.name, EQ) & Condition(
364+
"team_id", self._project.team_id, EQ
365+
)
366+
for project in self._projects.get_all(condition):
367+
if project.name == self._project_to_create.name:
368+
logger.error("There are duplicated names.")
369+
raise AppValidationException(
370+
f"Project name {self._project_to_create.name} is not unique. "
371+
f"To use SDK please make project names unique."
372+
)
373+
344374
def execute(self):
345375
if self.is_valid():
346376
project = self._projects.insert(self._project_to_create)
@@ -3263,6 +3293,9 @@ def __init__(
32633293
self._mask = mask
32643294
self._verbose = verbose
32653295
self._annotation_path = annotation_path
3296+
self.unknown_classes = []
3297+
self.unknown_attribute_groups = []
3298+
self.unknown_attributes = []
32663299

32673300
def validate_project_type(self):
32683301
if self._project.project_type in constances.LIMITED_FUNCTIONS:
@@ -3303,18 +3336,21 @@ def fill_classes_data(self, annotations: dict):
33033336
annotation_classes = self.annotation_classes_name_map
33043337
if "instances" not in annotations:
33053338
return
3339+
33063340
unknown_classes = {}
33073341
for annotation in [i for i in annotations["instances"] if "className" in i]:
33083342
if "className" not in annotation:
33093343
return
33103344
annotation_class_name = annotation["className"]
3311-
if annotation_class_name not in annotation_classes:
3345+
if annotation_class_name not in annotation_classes.keys():
33123346
if annotation_class_name not in unknown_classes:
3347+
self.unknown_classes.append(annotation_class_name)
33133348
unknown_classes[annotation_class_name] = {
33143349
"id": -(len(unknown_classes) + 1),
33153350
"attribute_groups": {},
33163351
}
3317-
annotation_classes.update(unknown_classes)
3352+
if unknown_classes:
3353+
annotation_classes.update(unknown_classes)
33183354
templates = self.get_templates_mapping()
33193355
for annotation in (
33203356
i for i in annotations["instances"] if i.get("type", None) == "template"
@@ -3325,14 +3361,15 @@ def fill_classes_data(self, annotations: dict):
33253361

33263362
for annotation in [i for i in annotations["instances"] if "className" in i]:
33273363
annotation_class_name = annotation["className"]
3328-
if annotation_class_name not in annotation_classes:
3364+
if annotation_class_name not in annotation_classes.keys():
33293365
continue
33303366
annotation["classId"] = annotation_classes[annotation_class_name]["id"]
33313367
for attribute in annotation["attributes"]:
33323368
if (
33333369
attribute["groupName"]
33343370
not in annotation_classes[annotation_class_name]["attribute_groups"]
33353371
):
3372+
self.unknown_attributes.append(attribute["groupName"])
33363373
continue
33373374
attribute["groupId"] = annotation_classes[annotation_class_name][
33383375
"attribute_groups"
@@ -3344,11 +3381,22 @@ def fill_classes_data(self, annotations: dict):
33443381
][attribute["groupName"]]["attributes"]
33453382
):
33463383
del attribute["groupId"]
3384+
self.unknown_attributes.append(attribute["name"])
33473385
continue
33483386
attribute["id"] = annotation_classes[annotation_class_name][
33493387
"attribute_groups"
33503388
][attribute["groupName"]]["attributes"][attribute["name"]]
33513389

3390+
def report_unknown_data(self):
3391+
if self.unknown_classes:
3392+
logger.warning(f"Unknown classes [{', '.join(self.unknown_classes)}]")
3393+
if self.unknown_attribute_groups:
3394+
logger.warning(
3395+
f"Unknown attribute_groups [{', '.join(self.unknown_attribute_groups)}]"
3396+
)
3397+
if self.unknown_attributes:
3398+
logger.warning(f"Unknown attributes [{', '.join(self.unknown_attributes)}]")
3399+
33523400
def execute(self):
33533401
if self.is_valid():
33543402
image_data = self._backend_service.get_bulk_images(
@@ -3376,6 +3424,7 @@ def execute(self):
33763424
resource = session.resource("s3")
33773425
bucket = resource.Bucket(response.data.bucket)
33783426
self.fill_classes_data(self._annotations)
3427+
self.report_unknown_data()
33793428
bucket.put_object(
33803429
Key=response.data.images[image_data["id"]]["annotation_json_path"],
33813430
Body=json.dumps(self._annotations),
@@ -4802,7 +4851,15 @@ def _upload_image(self, image_path: str):
48024851
.data
48034852
)
48044853
else:
4805-
image_bytes = io.BytesIO(open(image_path, "rb").read())
4854+
try:
4855+
image_bytes = io.BytesIO(open(image_path, "rb").read())
4856+
except FileNotFoundError:
4857+
return ProcessedImage(
4858+
uploaded=False,
4859+
path=image_path,
4860+
entity=None,
4861+
name=Path(image_path).name,
4862+
)
48064863
upload_response = UploadImageS3UseCase(
48074864
project=self._project,
48084865
project_settings=self._settings,

src/superannotate/lib/infrastructure/controller.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -636,9 +636,8 @@ def search_images(
636636
return use_case.execute()
637637

638638
def _get_image(
639-
self, project: ProjectEntity, image_name: str, folder_path: str = None,
639+
self, project: ProjectEntity, image_name: str, folder: FolderEntity = None,
640640
) -> ImageEntity:
641-
folder = self._get_folder(project, folder_path)
642641
use_case = usecases.GetImageUseCase(
643642
service=self._backend_client,
644643
project=project,
@@ -651,7 +650,9 @@ def _get_image(
651650
def get_image(
652651
self, project_name: str, image_name: str, folder_path: str = None
653652
) -> ImageEntity:
654-
return self._get_image(self._get_project(project_name), image_name, folder_path)
653+
project = self._get_project(project_name)
654+
folder = self._get_folder(project, folder_path)
655+
return self._get_image(project, image_name, folder)
655656

656657
def update_folder(self, project_name: str, folder_name: str, folder_data: dict):
657658
project = self._get_project(project_name)
@@ -669,7 +670,8 @@ def get_image_bytes(
669670
image_variant: str = None,
670671
):
671672
project = self._get_project(project_name)
672-
image = self._get_image(project, image_name, folder_name)
673+
folder = self._get_folder(project, folder_name)
674+
image = self._get_image(project, image_name, folder)
673675
use_case = usecases.GetImageBytesUseCase(
674676
image=image,
675677
backend_service_provider=self._backend_client,
@@ -686,12 +688,12 @@ def copy_image_annotation_classes(
686688
image_name: str,
687689
):
688690
from_project = self._get_project(from_project_name)
689-
image = self._get_image(
690-
from_project, folder_path=from_folder_name, image_name=image_name
691-
)
691+
from_folder = self._get_folder(from_project, from_folder_name)
692+
image = self._get_image(from_project, folder=from_folder, image_name=image_name)
692693
to_project = self._get_project(to_project_name)
694+
to_folder = self._get_folder(to_project, to_folder_name)
693695
uploaded_image = self._get_image(
694-
to_project, folder_path=to_folder_name, image_name=image_name
696+
to_project, folder=to_folder, image_name=image_name
695697
)
696698

697699
use_case = usecases.CopyImageAnnotationClasses(
@@ -857,14 +859,16 @@ def set_project_settings(self, project_name: str, new_settings: List[dict]):
857859
)
858860
return use_case.execute()
859861

860-
def delete_image(self, image_name, project_name):
861-
image = self.get_image(project_name=project_name, image_name=image_name)
862-
project_entity = self._get_project(project_name)
862+
def delete_image(self, project_name: str, image_name: str, folder_name: str):
863+
project = self._get_project(project_name)
864+
folder = self._get_folder(project, folder_name)
865+
image = self._get_image(project=project, image_name=image_name, folder=folder)
866+
863867
use_case = usecases.DeleteImageUseCase(
864868
images=ImageRepository(service=self._backend_client),
865869
image=image,
866-
team_id=project_entity.team_id,
867-
project_id=project_entity.uuid,
870+
team_id=project.team_id,
871+
project_id=project.uuid,
868872
)
869873
return use_case.execute()
870874

@@ -1231,7 +1235,7 @@ def download_image(
12311235
):
12321236
project = self._get_project(project_name)
12331237
folder = self._get_folder(project, folder_name)
1234-
image = self._get_image(project, image_name, folder_name)
1238+
image = self._get_image(project, image_name, folder)
12351239

12361240
use_case = usecases.DownloadImageUseCase(
12371241
project=project,

src/superannotate/lib/infrastructure/repositories.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,8 @@ def insert(self, entity: ImageEntity) -> ImageEntity:
361361
raise NotImplementedError
362362

363363
def delete(self, uuid: int, team_id: int, project_id: int):
364-
self._service.delete_image(
365-
image_id=uuid, team_id=team_id, project_id=project_id
364+
self._service.delete_images(
365+
image_ids=[uuid], team_id=team_id, project_id=project_id
366366
)
367367

368368
def update(self, entity: ImageEntity):

0 commit comments

Comments
 (0)