Skip to content

Commit 33b0d9d

Browse files
authored
Merge pull request #451 from superannotateai/assests_provider
Assests provider
2 parents a5c8b12 + 073b880 commit 33b0d9d

File tree

56 files changed

+1562
-3118
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1562
-3118
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

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ setuptools~=57.4.0
2020
aiohttp==3.8.1
2121
email-validator>=1.0.3
2222
nest-asyncio==1.5.4
23+
jsonschema==3.2.0

src/superannotate/lib/app/annotation_helpers.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def add_annotation_comment_to_json(
8686
"y": comment_coords[1],
8787
"correspondence": [{"text": comment_text, "email": comment_author}],
8888
"resolved": resolved,
89+
"creationType": "Preannotation", # noqa
8990
"createdBy": user_action,
9091
"updatedBy": user_action,
9192
}
@@ -132,7 +133,6 @@ def add_annotation_bbox_to_json(
132133
"type": "bbox",
133134
"points": {"x1": bbox[0], "y1": bbox[1], "x2": bbox[2], "y2": bbox[3]},
134135
"className": annotation_class_name,
135-
"error": error,
136136
"groupId": 0,
137137
"pointLabels": {},
138138
"locked": False,
@@ -141,7 +141,8 @@ def add_annotation_bbox_to_json(
141141
if annotation_class_attributes is None
142142
else annotation_class_attributes,
143143
}
144-
144+
if error is not None:
145+
annotation["error"] = error
145146
annotation = _add_created_updated(annotation)
146147
annotation_json["instances"].append(annotation)
147148

@@ -175,13 +176,11 @@ def add_annotation_point_to_json(
175176
raise AppException("Point should be 2 element float list.")
176177

177178
annotation_json, path = _preprocess_annotation_json(annotation_json, image_name)
178-
179179
annotation = {
180180
"type": "point",
181181
"x": point[0],
182182
"y": point[1],
183183
"className": annotation_class_name,
184-
"error": error,
185184
"groupId": 0,
186185
"pointLabels": {},
187186
"locked": False,
@@ -190,6 +189,8 @@ def add_annotation_point_to_json(
190189
if annotation_class_attributes is None
191190
else annotation_class_attributes,
192191
}
192+
if error is not None:
193+
annotation["error"] = error
193194

194195
annotation = _add_created_updated(annotation)
195196
annotation_json["instances"].append(annotation)

src/superannotate/lib/app/helpers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import uuid
23
from pathlib import Path
34
from typing import List
@@ -128,3 +129,21 @@ def get_name_url_duplicated_from_csv(csv_path):
128129
else:
129130
duplicate_images.append(temp)
130131
return images_to_upload, duplicate_images
132+
133+
134+
def get_tabulation() -> int:
135+
try:
136+
return int(os.get_terminal_size().columns / 2)
137+
except OSError:
138+
return 48
139+
140+
141+
def wrap_error(errors_list: List[Tuple[str, str]]) -> str:
142+
tabulation = get_tabulation()
143+
msgs = []
144+
for key, value in errors_list:
145+
_tabulation = tabulation - len(key)
146+
if not key:
147+
key, value, _tabulation = value, "", 0
148+
msgs.append("{}{}{}".format(key, " " * _tabulation, value))
149+
return "\n".join(msgs)

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

Lines changed: 97 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from lib.app.helpers import extract_project_folder
2121
from lib.app.helpers import get_annotation_paths
2222
from lib.app.helpers import get_name_url_duplicated_from_csv
23+
from lib.app.helpers import wrap_error as wrap_validation_errors
2324
from lib.app.interface.base_interface import BaseInterfaceFacade
2425
from lib.app.interface.base_interface import TrackableMeta
2526
from lib.app.interface.types import AnnotationStatuses
@@ -42,15 +43,15 @@
4243
from lib.core import LIMITED_FUNCTIONS
4344
from lib.core.entities import AttachmentEntity
4445
from lib.core.entities import SettingEntity
46+
from lib.core.entities.classes import AnnotationClassEntity
47+
from lib.core.entities.classes import AttributeGroup
4548
from lib.core.entities.integrations import IntegrationEntity
46-
from lib.core.entities.project_entities import AnnotationClassEntity
4749
from lib.core.enums import ImageQuality
4850
from lib.core.exceptions import AppException
49-
from lib.core.types import AttributeGroup
5051
from lib.core.types import MLModel
5152
from lib.core.types import PriorityScore
5253
from lib.core.types import Project
53-
from lib.infrastructure.controller import Controller
54+
from lib.infrastructure.validators import wrap_error
5455
from pydantic import conlist
5556
from pydantic import parse_obj_as
5657
from pydantic import StrictBool
@@ -579,8 +580,7 @@ def search_annotation_classes(
579580
"""
580581
project_name, folder_name = extract_project_folder(project)
581582
classes = self.controller.search_annotation_classes(project_name, name_contains)
582-
classes = [BaseSerializer(attribute).serialize() for attribute in classes.data]
583-
return classes
583+
return [i.dict(fill_enum_values=True) for i in classes.data]
584584

585585
def set_project_default_image_quality_in_editor(
586586
self,
@@ -1140,31 +1140,94 @@ def create_annotation_class(
11401140
11411141
:param project: project name
11421142
:type project: str
1143+
11431144
:param name: name for the class
11441145
:type name: str
1145-
:param color: RGB hex color value, e.g., "#FFFFAA"
1146+
1147+
:param color: RGB hex color value, e.g., "#F9E0FA"
11461148
:type color: str
1147-
:param attribute_groups: example:
1148-
[ { "name": "tall", "is_multiselect": 0, "attributes": [ { "name": "yes" }, { "name": "no" } ] },
1149-
{ "name": "age", "is_multiselect": 0, "attributes": [ { "name": "young" }, { "name": "old" } ] } ]
1149+
1150+
:param attribute_groups: list of attribute group dicts.
1151+
The values for the "group_type" key are "radio"|"checklist"|"text"|"numeric".
1152+
Mandatory keys for each attribute group are
1153+
- "name"
11501154
:type attribute_groups: list of dicts
1151-
:param class_type: class type
1155+
1156+
:param class_type: class type. Should be either "object" or "tag"
11521157
:type class_type: str
11531158
11541159
:return: new class metadata
11551160
:rtype: dict
1161+
1162+
Request Example:
1163+
::
1164+
attributes_list = [
1165+
{
1166+
"group_type": "radio",
1167+
"name": "Vehicle",
1168+
"attributes": [
1169+
{
1170+
"name": "Car"
1171+
},
1172+
{
1173+
"name": "Track"
1174+
},
1175+
{
1176+
"name": "Bus"
1177+
}
1178+
],
1179+
"default_value": "Car"
1180+
},
1181+
{
1182+
"group_type": "checklist",
1183+
"name": "Color",
1184+
"attributes": [
1185+
{
1186+
"name": "Yellow"
1187+
},
1188+
{
1189+
"name": "Black"
1190+
},
1191+
{
1192+
"name": "White"
1193+
}
1194+
],
1195+
"default_value": ["Yellow", "White"]
1196+
},
1197+
{
1198+
"group_type": "text",
1199+
"name": "Timestamp"
1200+
},
1201+
{
1202+
"group_type": "numeric",
1203+
"name": "Description"
1204+
}
1205+
]
1206+
client.create_annotation_class(
1207+
project="Image Project",
1208+
name="Example Class",
1209+
color="#F9E0FA",
1210+
attribute_groups=attributes_list
1211+
)
1212+
11561213
"""
11571214
if isinstance(project, Project):
11581215
project = project.dict()
11591216
attribute_groups = (
11601217
list(map(lambda x: x.dict(), attribute_groups)) if attribute_groups else []
11611218
)
1219+
try:
1220+
annotation_class = AnnotationClassEntity(
1221+
name=name,
1222+
color=color, # noqa
1223+
attribute_groups=attribute_groups,
1224+
type=class_type, # noqa
1225+
)
1226+
except ValidationError as e:
1227+
raise AppException(wrap_error(e))
1228+
11621229
response = self.controller.create_annotation_class(
1163-
project_name=project,
1164-
name=name,
1165-
color=color,
1166-
attribute_groups=attribute_groups,
1167-
class_type=class_type,
1230+
project_name=project, annotation_class=annotation_class
11681231
)
11691232
if response.errors:
11701233
raise AppException(response.errors)
@@ -1237,9 +1300,8 @@ def create_annotation_classes_from_classes_json(
12371300
classes_json = json.load(data)
12381301
try:
12391302
annotation_classes = parse_obj_as(List[AnnotationClassEntity], classes_json)
1240-
except ValidationError:
1303+
except ValidationError as _:
12411304
raise AppException("Couldn't validate annotation classes.")
1242-
logger.info(f"Creating annotation classes in project {project}.")
12431305
response = self.controller.create_annotation_classes(
12441306
project_name=project,
12451307
annotation_classes=annotation_classes,
@@ -1830,13 +1892,14 @@ def add_annotation_point_to_image(
18301892
annotations,
18311893
point,
18321894
annotation_class_name,
1833-
image_name,
18341895
annotation_class_attributes,
18351896
error,
18361897
)
1837-
self.controller.upload_image_annotations(
1898+
response = self.controller.upload_image_annotations(
18381899
project_name, folder_name, image_name, annotations
18391900
)
1901+
if response.errors:
1902+
raise AppException(response.errors)
18401903

18411904
def add_annotation_comment_to_image(
18421905
self,
@@ -2103,7 +2166,9 @@ def delete_annotations(
21032166
raise AppException(response.errors)
21042167

21052168
def validate_annotations(
2106-
self, project_type: ProjectTypes, annotations_json: Union[NotEmptyStr, Path]
2169+
self,
2170+
project_type: ProjectTypes,
2171+
annotations_json: Union[NotEmptyStr, Path, dict],
21072172
):
21082173
"""Validates given annotation JSON.
21092174
@@ -2116,16 +2181,18 @@ def validate_annotations(
21162181
:return: The success of the validation
21172182
:rtype: bool
21182183
"""
2119-
with open(annotations_json) as file:
2120-
annotation_data = json.loads(file.read())
2121-
response = Controller.validate_annotations(project_type, annotation_data)
2122-
if response.errors:
2123-
raise AppException(response.errors)
2124-
is_valid, _ = response.data
2125-
if is_valid:
2126-
return True
2127-
print(response.report)
2128-
return False
2184+
if isinstance(annotations_json, dict):
2185+
annotation_data = annotations_json
2186+
else:
2187+
annotation_data = json.load(open(annotations_json))
2188+
response = self.controller.validate_annotations(project_type, annotation_data)
2189+
if response.errors:
2190+
raise AppException(response.errors)
2191+
report = response.data
2192+
if not report:
2193+
return True
2194+
print(wrap_validation_errors(report))
2195+
return False
21292196

21302197
def add_contributors_to_project(
21312198
self,

src/superannotate/lib/app/serializers.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ def _serialize(
4848
by_alias: bool = False,
4949
flat: bool = False,
5050
exclude: Set[str] = None,
51+
**kwargs
5152
):
53+
if not entity:
54+
return None
5255
if isinstance(entity, dict):
5356
return entity
5457
if isinstance(entity, BaseModel):
@@ -57,13 +60,15 @@ def _serialize(
5760
if len(fields) == 1:
5861
if flat:
5962
return entity.dict(
60-
include=fields, by_alias=by_alias, exclude=exclude
63+
include=fields, by_alias=by_alias, exclude=exclude, **kwargs
6164
)[next(iter(fields))]
6265
return entity.dict(
63-
include=fields, by_alias=by_alias, exclude=exclude
66+
include=fields, by_alias=by_alias, exclude=exclude, **kwargs
6467
)
65-
return entity.dict(include=fields, by_alias=by_alias, exclude=exclude)
66-
return entity.dict(by_alias=by_alias, exclude=exclude)
68+
return entity.dict(
69+
include=fields, by_alias=by_alias, exclude=exclude, **kwargs
70+
)
71+
return entity.dict(by_alias=by_alias, exclude=exclude, **kwargs)
6772
return entity.to_dict()
6873

6974
@classmethod
@@ -74,12 +79,13 @@ def serialize_iterable(
7479
by_alias: bool = False,
7580
flat: bool = False,
7681
exclude: Set = None,
82+
**kwargs
7783
) -> List[Any]:
7884
serialized_data = []
7985
for i in data:
8086
serialized_data.append(
8187
cls._fill_enum_values(
82-
cls._serialize(i, fields, by_alias, flat, exclude=exclude)
88+
cls._serialize(i, fields, by_alias, flat, exclude=exclude, **kwargs)
8389
)
8490
)
8591
return serialized_data
@@ -150,3 +156,19 @@ def serialize(
150156
if data["attribute"] == "ImageQuality":
151157
data["value"] = constance.ImageQuality.get_name(data["value"])
152158
return data
159+
160+
161+
class EntitySerializer:
162+
@classmethod
163+
def serialize(
164+
cls, data: Union[BaseModel, List[BaseModel]], **kwargs
165+
) -> Union[List[dict], dict]:
166+
if isinstance(data, (list, set)):
167+
for idx, item in enumerate(data):
168+
data[idx] = cls.serialize(item, **kwargs)
169+
for key, nested_model in data:
170+
if isinstance(nested_model, BaseModel) and getattr(
171+
nested_model, "fill_enum_values", False
172+
):
173+
setattr(data, key, cls.serialize(nested_model, **kwargs))
174+
return data.dict(**kwargs)

0 commit comments

Comments
 (0)