Skip to content

Commit c219f91

Browse files
authored
Merge pull request #298 from superannotateai/nested_data_validation
Added nested models validation
2 parents 03017b1 + 030fb48 commit c219f91

File tree

8 files changed

+132
-35
lines changed

8 files changed

+132
-35
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3642,8 +3642,8 @@ def validate_annotations(
36423642
Validates given annotation JSON.
36433643
:param project_type: project_type (str) – the project type Vector, Pixel, Video or Document
36443644
:type project_type: str
3645-
:param annotation_json: path to annotation JSON
3646-
:type annotation_json: Path-like (str or Path)
3645+
:param annotations_json: path to annotation JSON
3646+
:type annotations_json: Path-like (str or Path)
36473647
36483648
:return: The success of the validation
36493649
:rtype: bool

src/superannotate/lib/core/entities/document.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
from lib.core.entities.utils import Attribute
55
from lib.core.entities.utils import BaseInstance
6+
from lib.core.entities.utils import BaseModel
67
from lib.core.entities.utils import MetadataBase
78
from lib.core.entities.utils import Tag
8-
from pydantic import BaseModel
99
from pydantic import Field
1010

1111

src/superannotate/lib/core/entities/pixel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from typing import Optional
33

44
from lib.core.entities.utils import BaseImageInstance
5+
from lib.core.entities.utils import BaseModel
56
from lib.core.entities.utils import MetadataBase
67
from lib.core.entities.utils import PixelColor
78
from lib.core.entities.utils import Tag
8-
from pydantic import BaseModel
99
from pydantic import Field
1010

1111

src/superannotate/lib/core/entities/utils.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Config:
2626
"type_error.string": "str type expected",
2727
"value_error.missing": "field required",
2828
}
29-
29+
3030

3131
class StringDate(datetime):
3232
@classmethod
@@ -38,7 +38,7 @@ def __get_validators__(cls):
3838
def validate(cls, v: datetime):
3939
return f'{v.replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]}Z'
4040

41-
41+
4242
class VectorAnnotationTypeEnum(str, Enum):
4343
BBOX = "bbox"
4444
ELLIPSE = "ellipse"
@@ -129,8 +129,7 @@ class LastUserAction(BaseModel):
129129

130130

131131
class BaseInstance(TrackableModel, TimedBaseModel):
132-
# TODO check id: Optional[str]
133-
class_id: int = Field(alias="classId")
132+
class_id: Optional[str] = Field(None, alias="classId")
134133
class_name: Optional[str] = Field(None, alias="className")
135134

136135

src/superannotate/lib/core/entities/vector.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
from typing import Union
44

55
from lib.core.entities.utils import AttributeGroup
6+
from lib.core.entities.utils import BaseModel
67
from lib.core.entities.utils import BaseVectorInstance
78
from lib.core.entities.utils import BboxPoints
89
from lib.core.entities.utils import Comment
910
from lib.core.entities.utils import Metadata
1011
from lib.core.entities.utils import Tag
1112
from lib.core.entities.utils import VectorAnnotationTypeEnum
12-
from pydantic import BaseModel
1313
from pydantic import conlist
1414
from pydantic import Field
1515
from pydantic import StrictStr
@@ -114,23 +114,29 @@ class Cuboid(BaseVectorInstance):
114114
}
115115

116116

117+
class AnnotationInstance(BaseModel):
118+
__root__: Union[Template, Cuboid, Point, PolyLine, Polygon, Bbox, Ellipse, RotatedBox]
119+
120+
@classmethod
121+
def __get_validators__(cls):
122+
yield cls.return_action
123+
124+
@classmethod
125+
def return_action(cls, values):
126+
try:
127+
instance_type = values["type"]
128+
except KeyError:
129+
raise ValueError(
130+
f"meta.type required"
131+
)
132+
try:
133+
return ANNOTATION_TYPES[instance_type](**values)
134+
except KeyError:
135+
raise ValueError(f"invalid type, valid types is {', '.join(ANNOTATION_TYPES.keys())}")
136+
137+
117138
class VectorAnnotation(BaseModel):
118139
metadata: Metadata
119140
comments: Optional[List[Comment]] = Field(list())
120141
tags: Optional[List[Tag]] = Field(list())
121-
instances: Optional[
122-
List[
123-
Union[Template, Cuboid, Point, PolyLine, Polygon, Bbox, Ellipse, RotatedBox]
124-
]
125-
] = Field(list())
126-
127-
@validator("instances", pre=True, each_item=True)
128-
def check_instances(cls, instance):
129-
# todo add type checking
130-
annotation_type = instance.get("type")
131-
result = validate_model(ANNOTATION_TYPES[annotation_type], instance)
132-
if result[2]:
133-
raise ValidationError(
134-
result[2].raw_errors, model=ANNOTATION_TYPES[annotation_type]
135-
)
136-
return instance
142+
instances: Optional[List[AnnotationInstance]] = Field(list())

src/superannotate/lib/core/entities/video_export.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
from lib.core.entities.utils import AnnotationStatusEnum
88
from lib.core.entities.utils import Attribute
99
from lib.core.entities.utils import BaseInstance
10+
from lib.core.entities.utils import BaseModel
1011
from lib.core.entities.utils import BboxPoints
1112
from lib.core.entities.utils import MetadataBase
1213
from lib.core.entities.utils import NotEmptyStr
1314
from lib.core.entities.utils import PointLabels
1415
from lib.core.entities.utils import Tag
15-
from pydantic import BaseModel
1616
from pydantic import conlist
1717
from pydantic import Field
18+
from pydantic import validator
1819

1920

2021
class VideoType(str, Enum):
@@ -24,15 +25,15 @@ class VideoType(str, Enum):
2425

2526
class MetaData(MetadataBase):
2627
name: NotEmptyStr
27-
url: str
28+
url: Optional[str]
2829
status: Optional[AnnotationStatusEnum]
2930
duration: Optional[int]
3031
error: Optional[bool]
3132

3233

3334
class BaseTimeStamp(BaseModel):
3435
timestamp: int
35-
attributes: List[Attribute] # TODO check is it required
36+
attributes: List[Attribute]
3637

3738

3839
class BboxTimeStamp(BaseTimeStamp):
@@ -55,11 +56,11 @@ class Config:
5556

5657

5758
class BBoxInstanceMetadata(InstanceMetadata):
58-
type: str = Field(VideoType.BBOX, const=True)
59+
type: VideoType = Field(VideoType.BBOX.value, const=True)
5960

6061

6162
class EventInstanceMetadata(InstanceMetadata):
62-
type: str = Field(VideoType.EVENT, const=True)
63+
type: VideoType = Field(VideoType.EVENT.value, const=True)
6364

6465

6566
class BaseVideoInstance(BaseModel):
@@ -93,10 +94,34 @@ class EventInstance(BaseModel):
9394
parameters: conlist(EventParameter, min_items=1)
9495

9596

96-
ANNOTATION_TYPES = {VideoType.BBOX: BboxInstance, VideoType.EVENT: EventInstance}
97+
INSTANCES = {
98+
VideoType.BBOX.value: BboxInstance,
99+
VideoType.EVENT.value: EventInstance
100+
}
101+
102+
103+
class VideoInstance(BaseModel):
104+
__root__: Union[BboxInstance, EventInstance]
105+
106+
@classmethod
107+
def __get_validators__(cls):
108+
yield cls.return_action
109+
110+
@classmethod
111+
def return_action(cls, values):
112+
try:
113+
instance_type = values["meta"]["type"]
114+
except KeyError:
115+
raise ValueError(
116+
f"meta.type required"
117+
)
118+
try:
119+
return INSTANCES[instance_type](**values)
120+
except KeyError:
121+
raise ValueError(f"invalid type, valid types is {', '.join(INSTANCES.keys())}")
97122

98123

99124
class VideoAnnotation(BaseModel):
100125
metadata: MetaData
101-
instances: Optional[List[Union[EventInstance, BboxInstance]]] = Field(list())
126+
instances: Optional[List[VideoInstance]] = Field(list())
102127
tags: Optional[List[Tag]] = Field(list())

tests/integration/test_interface.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,3 @@ def test_create_folder_with_special_character(self):
251251
'Folder __abc (1) created in project Interface Pixel test',
252252
logs.output[4]
253253
)
254-
255-
def test_(self):
256-
sa.upload_annotations_from_folder_to_project("PROJECT_2", "/Users/vaghinak.basentsyan/www/for_fun/data")

tests/unit/test_validators.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,73 @@ def test_validate_annotation_without_metadata(self):
8686
with catch_prints() as out:
8787
sa.validate_annotations("Vector", os.path.join(self.vector_folder_path, f"{tmpdir_name}/vector.json"))
8888
self.assertIn("metadatafieldrequired", out.getvalue().strip().replace(" ", ""))
89+
90+
91+
class TestTypeHandling(TestCase):
92+
ANNOTATION = """
93+
{
94+
"metadata": {
95+
"name": "example_image_1.jpg",
96+
"width": 1024,
97+
"height": 683,
98+
"status": "Completed",
99+
"pinned": false,
100+
"isPredicted": null,
101+
"projectId": null,
102+
"annotatorEmail": null,
103+
"qaEmail": null
104+
},
105+
"instances": [
106+
{
107+
"type": "invalid_type",
108+
"classId": 72274,
109+
"probability": 100,
110+
"points": {
111+
112+
"x2": 465.23,
113+
"y1": 341.5,
114+
"y2": 357.09
115+
},
116+
"groupId": 0,
117+
"pointLabels": {},
118+
"locked": false,
119+
"visible": false,
120+
"attributes": [
121+
{
122+
"id": 117845,
123+
"groupId": 28230,
124+
"name": "2",
125+
"groupName": "Num doors"
126+
}
127+
],
128+
"trackingId": "aaa97f80c9e54a5f2dc2e920fc92e5033d9af45b",
129+
"error": null,
130+
"createdAt": null,
131+
"createdBy": null,
132+
"creationType": null,
133+
"updatedAt": null,
134+
"updatedBy": null,
135+
"className": "Personal vehicle"
136+
}
137+
]
138+
}
139+
"""
140+
141+
TEST_VECTOR_FOLDER_PATH = "data_set/sample_project_vector"
142+
VECTOR_JSON = "example_image_1.jpg___objects.json"
143+
144+
@property
145+
def vector_folder_path(self):
146+
return os.path.join(dirname(dirname(__file__)), self.TEST_VECTOR_FOLDER_PATH)
147+
148+
def test_validate_annotation_with_wrong_bbox(self):
149+
with tempfile.TemporaryDirectory() as tmpdir_name:
150+
with open(f"{tmpdir_name}/vector.json", "w") as vector_json:
151+
vector_json.write(self.ANNOTATION)
152+
with catch_prints() as out:
153+
sa.validate_annotations("Vector", os.path.join(self.vector_folder_path, f"{tmpdir_name}/vector.json"))
154+
self.assertEqual(
155+
"instances[0]invalidtype,validtypesisbbox,"
156+
"template,cuboid,polygon,point,polyline,ellipse,rbbox",
157+
out.getvalue().strip().replace(" ", "")
158+
)

0 commit comments

Comments
 (0)