diff --git a/labelbox/data/annotation_types/collection.py b/labelbox/data/annotation_types/collection.py index e5c60a15b..89fe70a33 100644 --- a/labelbox/data/annotation_types/collection.py +++ b/labelbox/data/annotation_types/collection.py @@ -34,12 +34,13 @@ def assign_feature_schema_ids( self, ontology_builder: "ontology.OntologyBuilder") -> "LabelList": """ Adds schema ids to all FeatureSchema objects in the Labels. - This is necessary for MAL. Args: ontology_builder: The ontology that matches the feature names assigned to objects in this LabelList Returns: LabelList. useful for chaining these modifying functions + + Note: You can now import annotations using names directly without having to lookup schema_ids """ for label in self._data: label.assign_feature_schema_ids(ontology_builder) diff --git a/labelbox/data/annotation_types/feature.py b/labelbox/data/annotation_types/feature.py index a6b08b9b5..f3ae8eb8f 100644 --- a/labelbox/data/annotation_types/feature.py +++ b/labelbox/data/annotation_types/feature.py @@ -10,9 +10,6 @@ class FeatureSchema(BaseModel): Class that represents a feature schema. Could be a annotation, a subclass, or an option. Schema ids might not be known when constructing these objects so both a name and schema id are valid. - - Use `LabelList.assign_feature_schema_ids` or `LabelGenerator.assign_feature_schema_ids` - to retroactively add schema ids by looking them up from the names. """ name: Optional[str] = None feature_schema_id: Optional[Cuid] = None @@ -24,3 +21,11 @@ def must_set_one(cls, values): "Must set either feature_schema_id or name for all feature schemas" ) return values + + def dict(self, *args, **kwargs): + res = super().dict(*args, **kwargs) + if 'name' in res and res['name'] is None: + res.pop('name') + if 'featureSchemaId' in res and res['featureSchemaId'] is None: + res.pop('featureSchemaId') + return res diff --git a/labelbox/data/annotation_types/label.py b/labelbox/data/annotation_types/label.py index 8ce4aed25..4f5539214 100644 --- a/labelbox/data/annotation_types/label.py +++ b/labelbox/data/annotation_types/label.py @@ -130,12 +130,13 @@ def assign_feature_schema_ids( self, ontology_builder: ontology.OntologyBuilder) -> "Label": """ Adds schema ids to all FeatureSchema objects in the Labels. - This is necessary for MAL. Args: ontology_builder: The ontology that matches the feature names assigned to objects in this dataset Returns: Label. useful for chaining these modifying functions + + Note: You can now import annotations using names directly without having to lookup schema_ids """ tool_lookup, classification_lookup = get_feature_schema_lookup( ontology_builder) diff --git a/labelbox/data/serialization/ndjson/base.py b/labelbox/data/serialization/ndjson/base.py index 2561fd005..520446bcd 100644 --- a/labelbox/data/serialization/ndjson/base.py +++ b/labelbox/data/serialization/ndjson/base.py @@ -1,5 +1,6 @@ +from typing import Optional from uuid import uuid4 -from pydantic import BaseModel, validator, Field +from pydantic import BaseModel, root_validator, validator, Field from labelbox.utils import camel_case from ...annotation_types.types import Cuid @@ -32,12 +33,21 @@ class Config: class NDAnnotation(NDJsonBase): - schema_id: Cuid - - @validator('schema_id', pre=True, always=True) - def validate_id(cls, v): - if v is None: - raise ValueError( - "Schema ids are not set. Use `LabelGenerator.assign_feature_schema_ids`, `LabelList.assign_feature_schema_ids`, or `Label.assign_feature_schema_ids`." - ) - return v + name: Optional[str] = None + schema_id: Optional[Cuid] = None + + @root_validator() + def must_set_one(cls, values): + if ('schema_id' not in values or + values['schema_id'] is None) and ('name' not in values or + values['name'] is None): + raise ValueError("Schema id or name are not set. Set either one.") + return values + + def dict(self, *args, **kwargs): + res = super().dict(*args, **kwargs) + if 'name' in res and res['name'] is None: + res.pop('name') + if 'schemaId' in res and res['schemaId'] is None: + res.pop('schemaId') + return res \ No newline at end of file diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index 6a28f09cf..dd4027238 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Union, Optional -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, root_validator from labelbox.utils import camel_case from ...annotation_types.annotation import ClassificationAnnotation, VideoClassificationAnnotation @@ -11,15 +11,24 @@ class NDFeature(BaseModel): - schema_id: Cuid + name: Optional[str] = None + schema_id: Optional[Cuid] = None - @validator('schema_id', pre=True, always=True) - def validate_id(cls, v): - if v is None: - raise ValueError( - "Schema ids are not set. Use `LabelGenerator.assign_feature_schema_ids`, `LabelList.assign_feature_schema_ids`, or `Label.assign_feature_schema_ids`." - ) - return v + @root_validator() + def must_set_one(cls, values): + if ('schema_id' not in values or + values['schema_id'] is None) and ('name' not in values or + values['name'] is None): + raise ValueError("Schema id or name are not set. Set either one.") + return values + + def dict(self, *args, **kwargs): + res = super().dict(*args, **kwargs) + if 'name' in res and res['name'] is None: + res.pop('name') + if 'schemaId' in res and res['schemaId'] is None: + res.pop('schemaId') + return res class Config: allow_population_by_field_name = True @@ -50,9 +59,9 @@ def to_common(self) -> Text: return Text(answer=self.answer) @classmethod - def from_common(cls, text: Text, + def from_common(cls, text: Text, name: str, feature_schema_id: Cuid) -> "NDTextSubclass": - return cls(answer=text.answer, schema_id=feature_schema_id) + return cls(answer=text.answer, name=name, schema_id=feature_schema_id) class NDChecklistSubclass(NDFeature): @@ -60,17 +69,19 @@ class NDChecklistSubclass(NDFeature): def to_common(self) -> Checklist: return Checklist(answer=[ - ClassificationAnswer(feature_schema_id=answer.schema_id) + ClassificationAnswer(name=answer.name, + feature_schema_id=answer.schema_id) for answer in self.answer ]) @classmethod - def from_common(cls, checklist: Checklist, + def from_common(cls, checklist: Checklist, name: str, feature_schema_id: Cuid) -> "NDChecklistSubclass": return cls(answer=[ - NDFeature(schema_id=answer.feature_schema_id) + NDFeature(name=answer.name, schema_id=answer.feature_schema_id) for answer in checklist.answer ], + name=name, schema_id=feature_schema_id) def dict(self, *args, **kwargs): @@ -85,12 +96,14 @@ class NDRadioSubclass(NDFeature): def to_common(self) -> Radio: return Radio(answer=ClassificationAnswer( - feature_schema_id=self.answer.schema_id)) + name=self.answer.name, feature_schema_id=self.answer.schema_id)) @classmethod - def from_common(cls, radio: Radio, + def from_common(cls, radio: Radio, name: str, feature_schema_id: Cuid) -> "NDRadioSubclass": - return cls(answer=NDFeature(schema_id=radio.answer.feature_schema_id), + return cls(answer=NDFeature(name=radio.answer.name, + schema_id=radio.answer.feature_schema_id), + name=name, schema_id=feature_schema_id) @@ -100,12 +113,13 @@ def from_common(cls, radio: Radio, class NDText(NDAnnotation, NDTextSubclass): @classmethod - def from_common(cls, text: Text, feature_schema_id: Cuid, + def from_common(cls, text: Text, name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[TextData, ImageData]) -> "NDText": return cls( answer=text.answer, data_row={'id': data.uid}, + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), ) @@ -115,14 +129,15 @@ class NDChecklist(NDAnnotation, NDChecklistSubclass, VideoSupported): @classmethod def from_common( - cls, checklist: Checklist, feature_schema_id: Cuid, + cls, checklist: Checklist, name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[VideoData, TextData, ImageData]) -> "NDChecklist": return cls(answer=[ - NDFeature(schema_id=answer.feature_schema_id) + NDFeature(name=answer.name, schema_id=answer.feature_schema_id) for answer in checklist.answer ], data_row={'id': data.uid}, + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), frames=extra.get('frames')) @@ -131,11 +146,13 @@ def from_common( class NDRadio(NDAnnotation, NDRadioSubclass, VideoSupported): @classmethod - def from_common(cls, radio: Radio, feature_schema_id: Cuid, + def from_common(cls, radio: Radio, name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[VideoData, TextData, ImageData]) -> "NDRadio": - return cls(answer=NDFeature(schema_id=radio.answer.feature_schema_id), + return cls(answer=NDFeature(name=radio.answer.name, + schema_id=radio.answer.feature_schema_id), data_row={'id': data.uid}, + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), frames=extra.get('frames')) @@ -152,13 +169,14 @@ def from_common( raise TypeError( f"Unable to convert object to MAL format. `{type(annotation.value)}`" ) - return classify_obj.from_common(annotation.value, + return classify_obj.from_common(annotation.value, annotation.name, annotation.feature_schema_id) @staticmethod def to_common( annotation: "NDClassificationType") -> ClassificationAnnotation: return ClassificationAnnotation(value=annotation.to_common(), + name=annotation.name, feature_schema_id=annotation.schema_id) @staticmethod @@ -182,6 +200,7 @@ def to_common( ) -> Union[ClassificationAnnotation, VideoClassificationAnnotation]: common = ClassificationAnnotation( value=annotation.to_common(), + name=annotation.name, feature_schema_id=annotation.schema_id, extra={'uuid': annotation.uuid}) if getattr(annotation, 'frames', None) is None: @@ -204,7 +223,7 @@ def from_common( raise TypeError( f"Unable to convert object to MAL format. `{type(annotation.value)}`" ) - return classify_obj.from_common(annotation.value, + return classify_obj.from_common(annotation.value, annotation.name, annotation.feature_schema_id, annotation.extra, data) diff --git a/labelbox/data/serialization/ndjson/label.py b/labelbox/data/serialization/ndjson/label.py index f62c8af9d..1a12bcd30 100644 --- a/labelbox/data/serialization/ndjson/label.py +++ b/labelbox/data/serialization/ndjson/label.py @@ -50,7 +50,8 @@ def _generate_annotations( for annotation in annotations: if isinstance(annotation, NDSegments): annots.extend( - NDSegments.to_common(annotation, annotation.schema_id)) + NDSegments.to_common(annotation, annotation.name, + annotation.schema_id)) elif isinstance(annotation, NDObjectType.__args__): annots.append(NDObject.to_common(annotation)) elif isinstance(annotation, NDClassificationType.__args__): @@ -97,7 +98,8 @@ def _create_video_annotations( if isinstance( annot, (VideoClassificationAnnotation, VideoObjectAnnotation)): - video_annotations[annot.feature_schema_id].append(annot) + video_annotations[annot.feature_schema_id or + annot.name].append(annot) for annotation_group in video_annotations.values(): consecutive_frames = cls._get_consecutive_frames( diff --git a/labelbox/data/serialization/ndjson/objects.py b/labelbox/data/serialization/ndjson/objects.py index fadf95fc5..f2200e8f5 100644 --- a/labelbox/data/serialization/ndjson/objects.py +++ b/labelbox/data/serialization/ndjson/objects.py @@ -6,6 +6,7 @@ from pydantic import BaseModel from PIL import Image +from labelbox.data.annotation_types import feature from labelbox.data.annotation_types.data.video import VideoData @@ -47,7 +48,7 @@ def to_common(self) -> Point: @classmethod def from_common(cls, point: Point, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDPoint": return cls(point={ @@ -55,6 +56,7 @@ def from_common(cls, point: Point, 'y': point.y }, dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -63,9 +65,11 @@ def from_common(cls, point: Point, class NDFramePoint(VideoSupported): point: _Point - def to_common(self, feature_schema_id: Cuid) -> VideoObjectAnnotation: + def to_common(self, name: str, + feature_schema_id: Cuid) -> VideoObjectAnnotation: return VideoObjectAnnotation(frame=self.frame, keyframe=True, + name=name, feature_schema_id=feature_schema_id, value=Point(x=self.point.x, y=self.point.y)) @@ -83,7 +87,7 @@ def to_common(self) -> Line: @classmethod def from_common(cls, line: Line, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDLine": return cls(line=[{ @@ -91,6 +95,7 @@ def from_common(cls, line: Line, 'y': pt.y } for pt in line.points], dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -99,10 +104,12 @@ def from_common(cls, line: Line, class NDFrameLine(VideoSupported): line: List[_Point] - def to_common(self, feature_schema_id: Cuid) -> VideoObjectAnnotation: + def to_common(self, name: str, + feature_schema_id: Cuid) -> VideoObjectAnnotation: return VideoObjectAnnotation( frame=self.frame, keyframe=True, + name=name, feature_schema_id=feature_schema_id, value=Line(points=[Point(x=pt.x, y=pt.y) for pt in self.line])) @@ -123,7 +130,7 @@ def to_common(self) -> Polygon: @classmethod def from_common(cls, polygon: Polygon, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDPolygon": return cls(polygon=[{ @@ -131,6 +138,7 @@ def from_common(cls, polygon: Polygon, 'y': pt.y } for pt in polygon.points], dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -146,7 +154,7 @@ def to_common(self) -> Rectangle: @classmethod def from_common(cls, rectangle: Rectangle, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDRectangle": return cls(bbox=Bbox(top=rectangle.start.y, @@ -154,6 +162,7 @@ def from_common(cls, rectangle: Rectangle, height=rectangle.end.y - rectangle.start.y, width=rectangle.end.x - rectangle.start.x), dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -162,10 +171,12 @@ def from_common(cls, rectangle: Rectangle, class NDFrameRectangle(VideoSupported): bbox: Bbox - def to_common(self, feature_schema_id: Cuid) -> VideoObjectAnnotation: + def to_common(self, name: str, + feature_schema_id: Cuid) -> VideoObjectAnnotation: return VideoObjectAnnotation( frame=self.frame, keyframe=True, + name=name, feature_schema_id=feature_schema_id, value=Rectangle(start=Point(x=self.bbox.left, y=self.bbox.top), end=Point(x=self.bbox.left + self.bbox.width, @@ -200,9 +211,11 @@ def segment_with_uuid(keyframe: Union[NDFrameRectangle, NDFramePoint, keyframe.extra = {'uuid': uuid} return keyframe - def to_common(self, feature_schema_id: Cuid, uuid: str): + def to_common(self, name: str, feature_schema_id: Cuid, uuid: str): return [ - self.segment_with_uuid(keyframe.to_common(feature_schema_id), uuid) + self.segment_with_uuid( + keyframe.to_common(name=name, + feature_schema_id=feature_schema_id), uuid) for keyframe in self.keyframes ] @@ -220,22 +233,26 @@ def from_common(cls, segment): class NDSegments(NDBaseObject): segments: List[NDSegment] - def to_common(self, feature_schema_id: Cuid): + def to_common(self, name: str, feature_schema_id: Cuid): result = [] for segment in self.segments: result.extend( - NDSegment.to_common(segment, feature_schema_id, self.uuid)) + NDSegment.to_common(segment, + name=name, + feature_schema_id=feature_schema_id, + uuid=self.uuid)) return result @classmethod def from_common(cls, segments: List[VideoObjectAnnotation], data: VideoData, - feature_schema_id: Cuid, extra: Dict[str, - Any]) -> "NDSegments": + name: str, feature_schema_id: Cuid, + extra: Dict[str, Any]) -> "NDSegments": segments = [NDSegment.from_common(segment) for segment in segments] return cls(segments=segments, dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid')) @@ -268,7 +285,7 @@ def to_common(self) -> Mask: @classmethod def from_common(cls, mask: Mask, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDMask": @@ -283,6 +300,7 @@ def from_common(cls, mask: Mask, return cls(mask=lbv1_mask, dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -301,7 +319,7 @@ def to_common(self) -> TextEntity: @classmethod def from_common(cls, text_entity: TextEntity, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDTextEntity": return cls(location=Location( @@ -309,6 +327,7 @@ def from_common(cls, text_entity: TextEntity, end=text_entity.end, ), dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -324,6 +343,7 @@ def to_common(annotation: "NDObjectType") -> ObjectAnnotation: for annot in annotation.classifications ] return ObjectAnnotation(value=common_annotation, + name=annotation.name, feature_schema_id=annotation.schema_id, classifications=classifications, extra={'uuid': annotation.uuid}) @@ -341,6 +361,7 @@ def from_common( return obj.from_common( annotation, data, + name=annotation[0][0].name, feature_schema_id=annotation[0][0].feature_schema_id, extra=annotation[0][0].extra) @@ -348,7 +369,7 @@ def from_common( NDSubclassification.from_common(annot) for annot in annotation.classifications ] - return obj.from_common(annotation.value, subclasses, + return obj.from_common(annotation.value, subclasses, annotation.name, annotation.feature_schema_id, annotation.extra, data) diff --git a/labelbox/schema/bulk_import_request.py b/labelbox/schema/bulk_import_request.py index c0093473f..b98fd3c0d 100644 --- a/labelbox/schema/bulk_import_request.py +++ b/labelbox/schema/bulk_import_request.py @@ -9,12 +9,14 @@ import backoff import ndjson import requests -from pydantic import BaseModel, validator +from pydantic import BaseModel, root_validator, validator from typing_extensions import Literal from typing import (Any, List, Optional, BinaryIO, Dict, Iterable, Tuple, Union, Type, Set, TYPE_CHECKING) from labelbox import exceptions as lb_exceptions +from labelbox.data.annotation_types.types import Cuid +from labelbox.data.ontology import get_feature_schema_lookup from labelbox.orm.model import Entity from labelbox import utils from labelbox.orm import query @@ -408,12 +410,14 @@ def _validate_ndjson(lines: Iterable[Dict[str, Any]], MALValidationError: Raise for invalid NDJson UuidError: Duplicate UUID in upload """ - feature_schemas = get_mal_schemas(project.ontology()) + feature_schemas_by_id, feature_schemas_by_name = get_mal_schemas( + project.ontology()) uids: Set[str] = set() for idx, line in enumerate(lines): try: annotation = NDAnnotation(**line) - annotation.validate_instance(feature_schemas) + annotation.validate_instance(feature_schemas_by_id, + feature_schemas_by_name) uuid = str(annotation.uuid) if uuid in uids: raise lb_exceptions.UuidError( @@ -437,14 +441,18 @@ def parse_classification(tool): dict """ if tool['type'] in ['radio', 'checklist']: + option_schema_ids = [r['featureSchemaId'] for r in tool['options']] + option_names = [r['value'] for r in tool['options']] return { 'tool': tool['type'], 'featureSchemaId': tool['featureSchemaId'], - 'options': [r['featureSchemaId'] for r in tool['options']] + 'name': tool['name'], + 'options': [*option_schema_ids, *option_names] } elif tool['type'] == 'text': return { 'tool': tool['type'], + 'name': tool['name'], 'featureSchemaId': tool['featureSchemaId'] } @@ -456,24 +464,37 @@ def get_mal_schemas(ontology): Args: ontology (Ontology) Returns: - Dict : Useful for looking up a tool from a given feature schema id + Dict, Dict : Useful for looking up a tool from a given feature schema id or name """ - valid_feature_schemas = {} + valid_feature_schemas_by_schema_id = {} + valid_feature_schemas_by_name = {} for tool in ontology.normalized['tools']: classifications = [ parse_classification(classification_tool) for classification_tool in tool['classifications'] ] - classifications = {v['featureSchemaId']: v for v in classifications} - valid_feature_schemas[tool['featureSchemaId']] = { + classifications_by_schema_id = { + v['featureSchemaId']: v for v in classifications + } + classifications_by_name = {v['name']: v for v in classifications} + valid_feature_schemas_by_schema_id[tool['featureSchemaId']] = { + 'tool': tool['tool'], + 'classificationsBySchemaId': classifications_by_schema_id, + 'classificationsByName': classifications_by_name, + 'name': tool['name'] + } + valid_feature_schemas_by_name[tool['name']] = { 'tool': tool['tool'], - 'classifications': classifications + 'classificationsBySchemaId': classifications_by_schema_id, + 'classificationsByName': classifications_by_name, + 'name': tool['name'] } for tool in ontology.normalized['classifications']: - valid_feature_schemas[tool['featureSchemaId']] = parse_classification( - tool) - return valid_feature_schemas + valid_feature_schemas_by_schema_id[ + tool['featureSchemaId']] = parse_classification(tool) + valid_feature_schemas_by_name[tool['name']] = parse_classification(tool) + return valid_feature_schemas_by_schema_id, valid_feature_schemas_by_name LabelboxID: str = pydantic.Field(..., min_length=25, max_length=25) @@ -585,7 +606,15 @@ class DataRow(BaseModel): class NDFeatureSchema(BaseModel): - schemaId: str = LabelboxID + schemaId: Optional[Cuid] = None + name: Optional[str] = None + + @root_validator + def must_set_one(cls, values): + if values['schemaId'] is None and values['name'] is None: + raise ValueError( + "Must set either schemaId or name for all feature schemas") + return values class NDBase(NDFeatureSchema): @@ -593,19 +622,36 @@ class NDBase(NDFeatureSchema): uuid: UUID dataRow: DataRow - def validate_feature_schemas(self, valid_feature_schemas): - if self.schemaId not in valid_feature_schemas: - raise ValueError( - f"schema id {self.schemaId} is not valid for the provided project's ontology." - ) + def validate_feature_schemas(self, valid_feature_schemas_by_id, + valid_feature_schemas_by_name): + if self.name: + if self.name not in valid_feature_schemas_by_name: + raise ValueError( + f"Name {self.name} is not valid for the provided project's ontology." + ) - if self.ontology_type != valid_feature_schemas[self.schemaId]['tool']: - raise ValueError( - f"Schema id {self.schemaId} does not map to the assigned tool {valid_feature_schemas[self.schemaId]['tool']}" - ) + if self.ontology_type != valid_feature_schemas_by_name[ + self.name]['tool']: + raise ValueError( + f"Name {self.name} does not map to the assigned tool {valid_feature_schemas_by_name[self.name]['tool']}" + ) + + if self.schemaId: + if self.schemaId not in valid_feature_schemas_by_id: + raise ValueError( + f"Schema id {self.schemaId} is not valid for the provided project's ontology." + ) + + if self.ontology_type != valid_feature_schemas_by_id[ + self.schemaId]['tool']: + raise ValueError( + f"Schema id {self.schemaId} does not map to the assigned tool {valid_feature_schemas_by_id[self.schemaId]['tool']}" + ) - def validate_instance(self, valid_feature_schemas): - self.validate_feature_schemas(valid_feature_schemas) + def validate_instance(self, valid_feature_schemas_by_id, + valid_feature_schemas_by_name): + self.validate_feature_schemas(valid_feature_schemas_by_id, + valid_feature_schemas_by_name) class Config: #Users shouldn't to add extra data to the payload @@ -640,17 +686,23 @@ def validate_answers(cls, value, field): raise ValueError("Checklist answers should not be empty") return value - def validate_feature_schemas(self, valid_feature_schemas): + def validate_feature_schemas(self, valid_feature_schemas_by_id, + valid_feature_schemas_by_name): #Test top level feature schema for this tool - super(NDChecklist, self).validate_feature_schemas(valid_feature_schemas) + super(NDChecklist, + self).validate_feature_schemas(valid_feature_schemas_by_id, + valid_feature_schemas_by_name) #Test the feature schemas provided to the answer field - if len(set([answer.schemaId for answer in self.answers])) != len( - self.answers): + if len(set([answer.name or answer.schemaId for answer in self.answers + ])) != len(self.answers): raise ValueError( f"Duplicated featureSchema found for checklist {self.uuid}") for answer in self.answers: - options = valid_feature_schemas[self.schemaId]['options'] - if answer.schemaId not in options: + options = valid_feature_schemas_by_name[ + self. + name]['options'] if self.name else valid_feature_schemas_by_id[ + self.schemaId]['options'] + if answer.name not in options and answer.schemaId not in options: raise ValueError( f"Feature schema provided to {self.ontology_type} invalid. Expected on of {options}. Found {answer}" ) @@ -660,12 +712,17 @@ class NDRadio(VideoSupported, NDBase): ontology_type: Literal["radio"] = "radio" answer: NDFeatureSchema = pydantic.Field(determinant=True) - def validate_feature_schemas(self, valid_feature_schemas): - super(NDRadio, self).validate_feature_schemas(valid_feature_schemas) - options = valid_feature_schemas[self.schemaId]['options'] - if self.answer.schemaId not in options: + def validate_feature_schemas(self, valid_feature_schemas_by_id, + valid_feature_schemas_by_name): + super(NDRadio, + self).validate_feature_schemas(valid_feature_schemas_by_id, + valid_feature_schemas_by_name) + options = valid_feature_schemas_by_name[ + self.name]['options'] if self.name else valid_feature_schemas_by_id[ + self.schemaId]['options'] + if self.answer.name not in options and self.answer.schemaId not in options: raise ValueError( - f"Feature schema provided to {self.ontology_type} invalid. Expected on of {options}. Found {self.answer.schemaId}" + f"Feature schema provided to {self.ontology_type} invalid. Expected on of {options}. Found {self.answer.name or self.answer.schemaId}" ) @@ -684,11 +741,20 @@ class NDBaseTool(NDBase): classifications: List[NDClassification] = [] #This is indepdent of our problem - def validate_feature_schemas(self, valid_feature_schemas): - super(NDBaseTool, self).validate_feature_schemas(valid_feature_schemas) + def validate_feature_schemas(self, valid_feature_schemas_by_id, + valid_feature_schemas_by_name): + super(NDBaseTool, + self).validate_feature_schemas(valid_feature_schemas_by_id, + valid_feature_schemas_by_name) for classification in self.classifications: classification.validate_feature_schemas( - valid_feature_schemas[self.schemaId]['classifications']) + valid_feature_schemas_by_name[ + self.name]['classificationsBySchemaId'] + if self.name else valid_feature_schemas_by_id[self.schemaId] + ['classificationsBySchemaId'], valid_feature_schemas_by_name[ + self.name]['classificationsByName'] + if self.name else valid_feature_schemas_by_id[ + self.schemaId]['classificationsByName']) @validator('classifications', pre=True) def validate_subclasses(cls, value, field): diff --git a/tests/data/assets/ndjson/classification_import_name_only.json b/tests/data/assets/ndjson/classification_import_name_only.json new file mode 100644 index 000000000..0261450f2 --- /dev/null +++ b/tests/data/assets/ndjson/classification_import_name_only.json @@ -0,0 +1,20 @@ +[ + { + "answer": { "name": "choice 1" }, + "name": "classification a", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673" + }, + { + "answer": [{ "name": "choice 2" }], + "name": "classification b", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + }, + { + "answer": "a value", + "name": "classification c", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + } +] diff --git a/tests/data/assets/ndjson/image_import_name_only.json b/tests/data/assets/ndjson/image_import_name_only.json new file mode 100644 index 000000000..ada0e3bea --- /dev/null +++ b/tests/data/assets/ndjson/image_import_name_only.json @@ -0,0 +1,215 @@ +[ + { + "uuid": "b862c586-8614-483c-b5e6-82810f70cac0", + "name": "box a", + "dataRow": { "id": "ckrazctum0z8a0ybc0b0o0g0v" }, + "bbox": { "top": 1352, "left": 2275, "height": 350, "width": 139 } + }, + { + "uuid": "751fc725-f7b6-48ed-89b0-dd7d94d08af6", + "name": "mask a", + "dataRow": { "id": "ckrazctum0z8a0ybc0b0o0g0v" }, + "mask": { + "instanceURI": "https://storage.labelbox.com/ckqcx1czn06830y61gh9v02cs%2F3e729327-f038-f66c-186e-45e921ef9717-1?Expires=1626806874672&KeyName=labelbox-assets-key-3&Signature=YsUOGKrsqmAZ68vT9BlPJOaRyLY", + "colorRGB": [255, 0, 0] + } + }, + { + "uuid": "43d719ac-5d7f-4aea-be00-2ebfca0900fd", + "name": "polygon a", + "dataRow": { "id": "ckrazctum0z8a0ybc0b0o0g0v" }, + "polygon": [ + { "x": 1118, "y": 935 }, + { "x": 1117, "y": 935 }, + { "x": 1116, "y": 935 }, + { "x": 1115, "y": 935 }, + { "x": 1114, "y": 935 }, + { "x": 1113, "y": 935 }, + { "x": 1112, "y": 935 }, + { "x": 1111, "y": 935 }, + { "x": 1110, "y": 935 }, + { "x": 1109, "y": 935 }, + { "x": 1108, "y": 935 }, + { "x": 1108, "y": 934 }, + { "x": 1107, "y": 934 }, + { "x": 1106, "y": 934 }, + { "x": 1105, "y": 934 }, + { "x": 1105, "y": 933 }, + { "x": 1104, "y": 933 }, + { "x": 1103, "y": 933 }, + { "x": 1103, "y": 932 }, + { "x": 1102, "y": 932 }, + { "x": 1101, "y": 932 }, + { "x": 1100, "y": 932 }, + { "x": 1099, "y": 932 }, + { "x": 1098, "y": 932 }, + { "x": 1097, "y": 932 }, + { "x": 1097, "y": 931 }, + { "x": 1096, "y": 931 }, + { "x": 1095, "y": 931 }, + { "x": 1094, "y": 931 }, + { "x": 1093, "y": 931 }, + { "x": 1092, "y": 931 }, + { "x": 1091, "y": 931 }, + { "x": 1090, "y": 931 }, + { "x": 1090, "y": 930 }, + { "x": 1089, "y": 930 }, + { "x": 1088, "y": 930 }, + { "x": 1087, "y": 930 }, + { "x": 1087, "y": 929 }, + { "x": 1086, "y": 929 }, + { "x": 1085, "y": 929 }, + { "x": 1084, "y": 929 }, + { "x": 1084, "y": 928 }, + { "x": 1083, "y": 928 }, + { "x": 1083, "y": 927 }, + { "x": 1082, "y": 927 }, + { "x": 1081, "y": 927 }, + { "x": 1081, "y": 926 }, + { "x": 1080, "y": 926 }, + { "x": 1080, "y": 925 }, + { "x": 1079, "y": 925 }, + { "x": 1078, "y": 925 }, + { "x": 1078, "y": 924 }, + { "x": 1077, "y": 924 }, + { "x": 1076, "y": 924 }, + { "x": 1076, "y": 923 }, + { "x": 1075, "y": 923 }, + { "x": 1074, "y": 923 }, + { "x": 1073, "y": 923 }, + { "x": 1073, "y": 922 }, + { "x": 1072, "y": 922 }, + { "x": 1071, "y": 922 }, + { "x": 1070, "y": 922 }, + { "x": 1070, "y": 921 }, + { "x": 1069, "y": 921 }, + { "x": 1068, "y": 921 }, + { "x": 1067, "y": 921 }, + { "x": 1066, "y": 921 }, + { "x": 1065, "y": 921 }, + { "x": 1064, "y": 921 }, + { "x": 1063, "y": 921 }, + { "x": 1062, "y": 921 }, + { "x": 1061, "y": 921 }, + { "x": 1060, "y": 921 }, + { "x": 1059, "y": 921 }, + { "x": 1058, "y": 921 }, + { "x": 1058, "y": 920 }, + { "x": 1057, "y": 920 }, + { "x": 1057, "y": 919 }, + { "x": 1056, "y": 919 }, + { "x": 1057, "y": 918 }, + { "x": 1057, "y": 918 }, + { "x": 1057, "y": 917 }, + { "x": 1058, "y": 916 }, + { "x": 1058, "y": 916 }, + { "x": 1059, "y": 915 }, + { "x": 1059, "y": 915 }, + { "x": 1060, "y": 914 }, + { "x": 1060, "y": 914 }, + { "x": 1061, "y": 913 }, + { "x": 1061, "y": 913 }, + { "x": 1062, "y": 912 }, + { "x": 1063, "y": 912 }, + { "x": 1063, "y": 912 }, + { "x": 1064, "y": 911 }, + { "x": 1064, "y": 911 }, + { "x": 1065, "y": 910 }, + { "x": 1066, "y": 910 }, + { "x": 1066, "y": 910 }, + { "x": 1067, "y": 909 }, + { "x": 1068, "y": 909 }, + { "x": 1068, "y": 909 }, + { "x": 1069, "y": 908 }, + { "x": 1070, "y": 908 }, + { "x": 1071, "y": 908 }, + { "x": 1072, "y": 908 }, + { "x": 1072, "y": 908 }, + { "x": 1073, "y": 907 }, + { "x": 1074, "y": 907 }, + { "x": 1075, "y": 907 }, + { "x": 1076, "y": 907 }, + { "x": 1077, "y": 907 }, + { "x": 1078, "y": 907 }, + { "x": 1079, "y": 907 }, + { "x": 1080, "y": 907 }, + { "x": 1081, "y": 907 }, + { "x": 1082, "y": 907 }, + { "x": 1083, "y": 907 }, + { "x": 1084, "y": 907 }, + { "x": 1085, "y": 907 }, + { "x": 1086, "y": 907 }, + { "x": 1087, "y": 907 }, + { "x": 1088, "y": 907 }, + { "x": 1089, "y": 907 }, + { "x": 1090, "y": 907 }, + { "x": 1091, "y": 907 }, + { "x": 1091, "y": 908 }, + { "x": 1092, "y": 908 }, + { "x": 1093, "y": 908 }, + { "x": 1094, "y": 908 }, + { "x": 1095, "y": 908 }, + { "x": 1095, "y": 909 }, + { "x": 1096, "y": 909 }, + { "x": 1097, "y": 909 }, + { "x": 1097, "y": 910 }, + { "x": 1098, "y": 910 }, + { "x": 1099, "y": 910 }, + { "x": 1099, "y": 911 }, + { "x": 1100, "y": 911 }, + { "x": 1101, "y": 911 }, + { "x": 1101, "y": 912 }, + { "x": 1102, "y": 912 }, + { "x": 1103, "y": 912 }, + { "x": 1103, "y": 913 }, + { "x": 1104, "y": 913 }, + { "x": 1104, "y": 914 }, + { "x": 1105, "y": 914 }, + { "x": 1105, "y": 915 }, + { "x": 1106, "y": 915 }, + { "x": 1107, "y": 915 }, + { "x": 1107, "y": 916 }, + { "x": 1108, "y": 916 }, + { "x": 1108, "y": 917 }, + { "x": 1109, "y": 917 }, + { "x": 1109, "y": 918 }, + { "x": 1110, "y": 918 }, + { "x": 1110, "y": 919 }, + { "x": 1111, "y": 919 }, + { "x": 1111, "y": 920 }, + { "x": 1112, "y": 920 }, + { "x": 1112, "y": 921 }, + { "x": 1113, "y": 921 }, + { "x": 1113, "y": 922 }, + { "x": 1114, "y": 922 }, + { "x": 1114, "y": 923 }, + { "x": 1115, "y": 923 }, + { "x": 1115, "y": 924 }, + { "x": 1115, "y": 925 }, + { "x": 1116, "y": 925 }, + { "x": 1116, "y": 926 }, + { "x": 1117, "y": 926 }, + { "x": 1117, "y": 927 }, + { "x": 1117, "y": 928 }, + { "x": 1118, "y": 928 }, + { "x": 1118, "y": 929 }, + { "x": 1119, "y": 929 }, + { "x": 1119, "y": 930 }, + { "x": 1120, "y": 930 }, + { "x": 1120, "y": 931 }, + { "x": 1120, "y": 932 }, + { "x": 1120, "y": 932 }, + { "x": 1119, "y": 933 }, + { "x": 1119, "y": 934 }, + { "x": 1119, "y": 934 }, + { "x": 1118, "y": 935 }, + { "x": 1118, "y": 935 } + ] + }, + { + "uuid": "b98f3a45-3328-41a0-9077-373a8177ebf2", + "name": "point a", + "dataRow": { "id": "ckrazctum0z8a0ybc0b0o0g0v" }, + "point": { "x": 2122, "y": 1457 } + } +] diff --git a/tests/data/assets/ndjson/nested_import_name_only.json b/tests/data/assets/ndjson/nested_import_name_only.json new file mode 100644 index 000000000..9ca899d95 --- /dev/null +++ b/tests/data/assets/ndjson/nested_import_name_only.json @@ -0,0 +1,48 @@ +[ + { + "bbox": { "height": 350, "left": 2275, "top": 1352, "width": 139 }, + "classifications": [ + { + "answer": { "name": "first answer" }, + "name": "classification a" + } + ], + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "name": "box a", + "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673" + }, + { + "bbox": { "height": 428, "left": 2089, "top": 1251, "width": 158 }, + "classifications": [ + { + "answer": { "name": "second answer" }, + "name": "classification b" + } + ], + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "name": "box b", + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + }, + { + "bbox": { "height": 428, "left": 2089, "top": 1251, "width": 158 }, + "classifications": [ + { + "answer": [{ "name": "third answer" }], + "name": "classification c" + } + ], + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "name": "box c", + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + }, + { + "bbox": { "height": 428, "left": 2089, "top": 1251, "width": 158 }, + "classifications": [ + { "answer": "a string", "name": "a string" } + ], + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "name": "box c", + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + } + ] + \ No newline at end of file diff --git a/tests/data/assets/ndjson/text_import.json b/tests/data/assets/ndjson/text_import.json index 6aa682802..d17c97ba4 100644 --- a/tests/data/assets/ndjson/text_import.json +++ b/tests/data/assets/ndjson/text_import.json @@ -1 +1,14 @@ -[{"answer": {"schemaId": "ckrb1sfl8099g0y91cxbd5ftb"}, "schemaId": "ckrb1sfjx099a0y914hl319ie", "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673"}, {"answer": [{"schemaId": "ckrb1sfl8099e0y919v260awv"}], "schemaId": "ckrb1sfkn099c0y910wbo0p1a", "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, "uuid": "d009925d-91a3-4f67-abd9-753453f5a584"}] \ No newline at end of file +[ + { + "answer": { "schemaId": "ckrb1sfl8099g0y91cxbd5ftb" }, + "schemaId": "ckrb1sfjx099a0y914hl319ie", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673" + }, + { + "answer": [{ "schemaId": "ckrb1sfl8099e0y919v260awv" }], + "schemaId": "ckrb1sfkn099c0y910wbo0p1a", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + } +] diff --git a/tests/data/assets/ndjson/text_import_name_only.json b/tests/data/assets/ndjson/text_import_name_only.json new file mode 100644 index 000000000..cf1477988 --- /dev/null +++ b/tests/data/assets/ndjson/text_import_name_only.json @@ -0,0 +1,15 @@ +[ + { + "answer": { "name": "answer a" }, + "name": "question 1", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673" + }, + { + "answer": [{ "name": "answer b" }], + "name": "question 2", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + } + ] + \ No newline at end of file diff --git a/tests/data/assets/ndjson/video_import_name_only.json b/tests/data/assets/ndjson/video_import_name_only.json new file mode 100644 index 000000000..acdc42e6e --- /dev/null +++ b/tests/data/assets/ndjson/video_import_name_only.json @@ -0,0 +1,94 @@ +[ + { + "answer": {"name": "answer 1"}, + "name": "question 1", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673", + "frames": [{"start": 30, "end": 35}, {"start": 50, "end": 51}] + }, + { + "answer": [{"name": "answer 2"}], + "name": "question 2", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584", + "frames": [{"start": 0, "end": 5}] + }, + { + "answer": "a value", + "name": "question 3", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + }, + { + "classifications": [], + "name": "segment 1", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "6f7c835a-0139-4896-b73f-66a6baa89e94", + "segments": [ + { + "keyframes": [ + { + "frame": 1, + "line": [{"x": 10.0, "y": 10.0}, {"x": 100.0, "y": 100.0}, {"x": 50.0, "y": 30.0}] + } + ] + }, + { + "keyframes": [ + { + "frame": 5, + "line": [{"x": 100.0, "y": 10.0}, {"x": 50.0, "y": 100.0}, {"x": 50.0, "y": 30.0}] + } + ] + } + ] + }, + { + "classifications": [], + "name": "segment 2", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "f963be22-227b-4efe-9be4-2738ed822216", + "segments": [ + { + "keyframes": [ + { + "frame": 1, + "point": {"x": 10.0, "y": 10.0} + } + ] + }, + { + "keyframes": [ + { + "frame": 5, + "point": {"x": 50.0, "y": 50.0} + } + ] + } + ] + }, + { + "classifications": [], + "name": "segment 3", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "13b2ee0e-2355-4336-8b83-d74d09e3b1e7", + "segments": [ + { + "keyframes": [ + { + "frame": 1, + "bbox": {"top": 10.0, "left": 5.0, "height": 100.0, "width": 150.0} + } + ] + }, + { + "keyframes": [ + { + "frame": 5, + "bbox": {"top": 300.0, "left": 200.0, "height": 400.0, "width": 150.0} + } + ] + } + ] + } +] \ No newline at end of file diff --git a/tests/data/serialization/ndjson/test_classification.py b/tests/data/serialization/ndjson/test_classification.py index b42825e33..c897717d4 100644 --- a/tests/data/serialization/ndjson/test_classification.py +++ b/tests/data/serialization/ndjson/test_classification.py @@ -1,6 +1,8 @@ import json +from labelbox.data.serialization.ndjson.classification import NDRadio from labelbox.data.serialization.ndjson.converter import NDJsonConverter +from labelbox.data.serialization.ndjson.objects import NDLine def test_classification(): @@ -10,3 +12,12 @@ def test_classification(): res = NDJsonConverter.deserialize(data).as_list() res = list(NDJsonConverter.serialize(res)) assert res == data + + +def test_classification_with_name(): + with open('tests/data/assets/ndjson/classification_import_name_only.json', + 'r') as file: + data = json.load(file) + res = NDJsonConverter.deserialize(data).as_list() + res = list(NDJsonConverter.serialize(res)) + assert res == data diff --git a/tests/data/serialization/ndjson/test_export_video_objects.py b/tests/data/serialization/ndjson/test_export_video_objects.py index a10fef1be..84434d786 100644 --- a/tests/data/serialization/ndjson/test_export_video_objects.py +++ b/tests/data/serialization/ndjson/test_export_video_objects.py @@ -526,6 +526,8 @@ def video_serialized_bbox_label(): }, 'schemaId': 'ckz38ofop0mci0z9i9w3aa9o4', + 'name': + 'bbox toy', 'classifications': [], 'segments': [{ 'keyframes': [{ diff --git a/tests/data/serialization/ndjson/test_image.py b/tests/data/serialization/ndjson/test_image.py index 4b4825fd5..242604ad9 100644 --- a/tests/data/serialization/ndjson/test_image.py +++ b/tests/data/serialization/ndjson/test_image.py @@ -30,6 +30,18 @@ def test_image(): assert [round_dict(x) for x in res] == [round_dict(x) for x in data] +def test_image_with_name_only(): + with open('tests/data/assets/ndjson/image_import_name_only.json', + 'r') as file: + data = json.load(file) + + res = NDJsonConverter.deserialize(data).as_list() + res = list(NDJsonConverter.serialize(res)) + for r in res: + r.pop('classifications', None) + assert [round_dict(x) for x in res] == [round_dict(x) for x in data] + + def test_mask(): data = [{ "uuid": "b862c586-8614-483c-b5e6-82810f70cac0", diff --git a/tests/data/serialization/ndjson/test_nested.py b/tests/data/serialization/ndjson/test_nested.py index 64fd199ff..54b2d32ee 100644 --- a/tests/data/serialization/ndjson/test_nested.py +++ b/tests/data/serialization/ndjson/test_nested.py @@ -9,3 +9,12 @@ def test_nested(): res = NDJsonConverter.deserialize(data).as_list() res = list(NDJsonConverter.serialize(res)) assert res == data + + +def test_nested_name_only(): + with open('tests/data/assets/ndjson/nested_import_name_only.json', + 'r') as file: + data = json.load(file) + res = NDJsonConverter.deserialize(data).as_list() + res = list(NDJsonConverter.serialize(res)) + assert res == data diff --git a/tests/data/serialization/ndjson/test_text.py b/tests/data/serialization/ndjson/test_text.py index 1c1b69fe2..30e0f6e46 100644 --- a/tests/data/serialization/ndjson/test_text.py +++ b/tests/data/serialization/ndjson/test_text.py @@ -9,3 +9,12 @@ def test_text(): res = NDJsonConverter.deserialize(data).as_list() res = list(NDJsonConverter.serialize(res)) assert res == data + + +def test_text_name_only(): + with open('tests/data/assets/ndjson/text_import_name_only.json', + 'r') as file: + data = json.load(file) + res = NDJsonConverter.deserialize(data).as_list() + res = list(NDJsonConverter.serialize(res)) + assert res == data diff --git a/tests/data/serialization/ndjson/test_video.py b/tests/data/serialization/ndjson/test_video.py index 01eda7175..baa029f34 100644 --- a/tests/data/serialization/ndjson/test_video.py +++ b/tests/data/serialization/ndjson/test_video.py @@ -10,3 +10,13 @@ def test_video(): res = NDJsonConverter.deserialize(data).as_list() res = list(NDJsonConverter.serialize(res)) assert res == [data[2], data[0], data[1], data[3], data[4], data[5]] + + +def test_video_name_only(): + with open('tests/data/assets/ndjson/video_import_name_only.json', + 'r') as file: + data = json.load(file) + + res = NDJsonConverter.deserialize(data).as_list() + res = list(NDJsonConverter.serialize(res)) + assert res == [data[2], data[0], data[1], data[3], data[4], data[5]] diff --git a/tests/integration/annotation_import/conftest.py b/tests/integration/annotation_import/conftest.py index 88d727b31..c00df1ce3 100644 --- a/tests/integration/annotation_import/conftest.py +++ b/tests/integration/annotation_import/conftest.py @@ -135,6 +135,7 @@ def prediction_id_mapping(configured_project): result[tool_type] = { "uuid": str(uuid.uuid4()), "schemaId": tool['featureSchemaId'], + "name": tool['name'], "dataRow": { "id": configured_project.data_row_ids[idx], }, @@ -178,10 +179,15 @@ def rectangle_inference(prediction_id_mapping): 'classifications': [{ "schemaId": rectangle['tool']['classifications'][0]['featureSchemaId'], + "name": + rectangle['tool']['classifications'][0]['name'], "answer": { "schemaId": rectangle['tool']['classifications'][0]['options'][0] - ['featureSchemaId'] + ['featureSchemaId'], + "name": + rectangle['tool']['classifications'][0]['options'][0] + ['value'] } }] }) diff --git a/tests/integration/annotation_import/test_ndjson_validation.py b/tests/integration/annotation_import/test_ndjson_validation.py index e490a36a2..355c4909e 100644 --- a/tests/integration/annotation_import/test_ndjson_validation.py +++ b/tests/integration/annotation_import/test_ndjson_validation.py @@ -82,6 +82,10 @@ def test_invalid_checklist_item(checklist_inference, configured_project): with pytest.raises(MALValidationError): _validate_ndjson([pred], configured_project) + pred['answers'] = [{"name": "asdfg"}] + with pytest.raises(MALValidationError): + _validate_ndjson([pred], configured_project) + pred['answers'] = [{"schemaId": "1232132132"}] with pytest.raises(MALValidationError): _validate_ndjson([pred], configured_project) @@ -177,10 +181,25 @@ def test_invalid_feature_schema(configured_project, rectangle_inference): _validate_ndjson([pred], configured_project) +def test_name_only_feature_schema(configured_project, rectangle_inference): + #Trying to upload a polygon and rectangle at the same time + pred = rectangle_inference.copy() + del pred['schemaId'] + _validate_ndjson([pred], configured_project) + + +def test_schema_id_only_feature_schema(configured_project, rectangle_inference): + #Trying to upload a polygon and rectangle at the same time + pred = rectangle_inference.copy() + del pred['name'] + _validate_ndjson([pred], configured_project) + + def test_missing_feature_schema(configured_project, rectangle_inference): #Trying to upload a polygon and rectangle at the same time pred = rectangle_inference.copy() del pred['schemaId'] + del pred['name'] with pytest.raises(MALValidationError): _validate_ndjson([pred], configured_project)