99import backoff
1010import ndjson
1111import requests
12- from pydantic import BaseModel , validator
12+ from pydantic import BaseModel , root_validator , validator
1313from typing_extensions import Literal
1414from typing import (Any , List , Optional , BinaryIO , Dict , Iterable , Tuple , Union ,
1515 Type , Set , TYPE_CHECKING )
1616
1717from labelbox import exceptions as lb_exceptions
18+ from labelbox .data .annotation_types .types import Cuid
19+ from labelbox .data .ontology import get_feature_schema_lookup
1820from labelbox .orm .model import Entity
1921from labelbox import utils
2022from labelbox .orm import query
@@ -408,12 +410,14 @@ def _validate_ndjson(lines: Iterable[Dict[str, Any]],
408410 MALValidationError: Raise for invalid NDJson
409411 UuidError: Duplicate UUID in upload
410412 """
411- feature_schemas = get_mal_schemas (project .ontology ())
413+ feature_schemas_by_id , feature_schemas_by_name = get_mal_schemas (
414+ project .ontology ())
412415 uids : Set [str ] = set ()
413416 for idx , line in enumerate (lines ):
414417 try :
415418 annotation = NDAnnotation (** line )
416- annotation .validate_instance (feature_schemas )
419+ annotation .validate_instance (feature_schemas_by_id ,
420+ feature_schemas_by_name )
417421 uuid = str (annotation .uuid )
418422 if uuid in uids :
419423 raise lb_exceptions .UuidError (
@@ -437,14 +441,18 @@ def parse_classification(tool):
437441 dict
438442 """
439443 if tool ['type' ] in ['radio' , 'checklist' ]:
444+ option_schema_ids = [r ['featureSchemaId' ] for r in tool ['options' ]]
445+ option_names = [r ['value' ] for r in tool ['options' ]]
440446 return {
441447 'tool' : tool ['type' ],
442448 'featureSchemaId' : tool ['featureSchemaId' ],
443- 'options' : [r ['featureSchemaId' ] for r in tool ['options' ]]
449+ 'name' : tool ['name' ],
450+ 'options' : [* option_schema_ids , * option_names ]
444451 }
445452 elif tool ['type' ] == 'text' :
446453 return {
447454 'tool' : tool ['type' ],
455+ 'name' : tool ['name' ],
448456 'featureSchemaId' : tool ['featureSchemaId' ]
449457 }
450458
@@ -456,24 +464,37 @@ def get_mal_schemas(ontology):
456464 Args:
457465 ontology (Ontology)
458466 Returns:
459- Dict : Useful for looking up a tool from a given feature schema id
467+ Dict, Dict : Useful for looking up a tool from a given feature schema id or name
460468 """
461469
462- valid_feature_schemas = {}
470+ valid_feature_schemas_by_schema_id = {}
471+ valid_feature_schemas_by_name = {}
463472 for tool in ontology .normalized ['tools' ]:
464473 classifications = [
465474 parse_classification (classification_tool )
466475 for classification_tool in tool ['classifications' ]
467476 ]
468- classifications = {v ['featureSchemaId' ]: v for v in classifications }
469- valid_feature_schemas [tool ['featureSchemaId' ]] = {
477+ classifications_by_schema_id = {
478+ v ['featureSchemaId' ]: v for v in classifications
479+ }
480+ classifications_by_name = {v ['name' ]: v for v in classifications }
481+ valid_feature_schemas_by_schema_id [tool ['featureSchemaId' ]] = {
482+ 'tool' : tool ['tool' ],
483+ 'classificationsBySchemaId' : classifications_by_schema_id ,
484+ 'classificationsByName' : classifications_by_name ,
485+ 'name' : tool ['name' ]
486+ }
487+ valid_feature_schemas_by_name [tool ['name' ]] = {
470488 'tool' : tool ['tool' ],
471- 'classifications' : classifications
489+ 'classificationsBySchemaId' : classifications_by_schema_id ,
490+ 'classificationsByName' : classifications_by_name ,
491+ 'name' : tool ['name' ]
472492 }
473493 for tool in ontology .normalized ['classifications' ]:
474- valid_feature_schemas [tool ['featureSchemaId' ]] = parse_classification (
475- tool )
476- return valid_feature_schemas
494+ valid_feature_schemas_by_schema_id [
495+ tool ['featureSchemaId' ]] = parse_classification (tool )
496+ valid_feature_schemas_by_name [tool ['name' ]] = parse_classification (tool )
497+ return valid_feature_schemas_by_schema_id , valid_feature_schemas_by_name
477498
478499
479500LabelboxID : str = pydantic .Field (..., min_length = 25 , max_length = 25 )
@@ -585,27 +606,52 @@ class DataRow(BaseModel):
585606
586607
587608class NDFeatureSchema (BaseModel ):
588- schemaId : str = LabelboxID
609+ schemaId : Optional [Cuid ] = None
610+ name : Optional [str ] = None
611+
612+ @root_validator
613+ def must_set_one (cls , values ):
614+ if values ['schemaId' ] is None and values ['name' ] is None :
615+ raise ValueError (
616+ "Must set either schemaId or name for all feature schemas" )
617+ return values
589618
590619
591620class NDBase (NDFeatureSchema ):
592621 ontology_type : str
593622 uuid : UUID
594623 dataRow : DataRow
595624
596- def validate_feature_schemas (self , valid_feature_schemas ):
597- if self .schemaId not in valid_feature_schemas :
598- raise ValueError (
599- f"schema id { self .schemaId } is not valid for the provided project's ontology."
600- )
625+ def validate_feature_schemas (self , valid_feature_schemas_by_id ,
626+ valid_feature_schemas_by_name ):
627+ if self .name :
628+ if self .name not in valid_feature_schemas_by_name :
629+ raise ValueError (
630+ f"name { self .name } is not valid for the provided project's ontology."
631+ )
601632
602- if self .ontology_type != valid_feature_schemas [self .schemaId ]['tool' ]:
603- raise ValueError (
604- f"Schema id { self .schemaId } does not map to the assigned tool { valid_feature_schemas [self .schemaId ]['tool' ]} "
605- )
633+ if self .ontology_type != valid_feature_schemas_by_name [
634+ self .name ]['tool' ]:
635+ raise ValueError (
636+ f"Name { self .name } does not map to the assigned tool { valid_feature_schemas_by_name [self .name ]['tool' ]} "
637+ )
638+
639+ if self .schemaId :
640+ if self .schemaId not in valid_feature_schemas_by_id :
641+ raise ValueError (
642+ f"schema id { self .schemaId } is not valid for the provided project's ontology."
643+ )
606644
607- def validate_instance (self , valid_feature_schemas ):
608- self .validate_feature_schemas (valid_feature_schemas )
645+ if self .ontology_type != valid_feature_schemas_by_id [
646+ self .schemaId ]['tool' ]:
647+ raise ValueError (
648+ f"Schema id { self .schemaId } does not map to the assigned tool { valid_feature_schemas_by_id [self .schemaId ]['tool' ]} "
649+ )
650+
651+ def validate_instance (self , valid_feature_schemas_by_id ,
652+ valid_feature_schemas_by_name ):
653+ self .validate_feature_schemas (valid_feature_schemas_by_id ,
654+ valid_feature_schemas_by_name )
609655
610656 class Config :
611657 #Users shouldn't to add extra data to the payload
@@ -629,9 +675,20 @@ class NDText(NDBase):
629675 #No feature schema to check
630676
631677
678+ class NDAnswer (BaseModel ):
679+ schemaId : Optional [Cuid ] = None
680+ value : Optional [str ] = None
681+
682+ @root_validator
683+ def must_set_one (cls , values ):
684+ if values ['schemaId' ] is None and values ['value' ] is None :
685+ raise ValueError ("Must set either schemaId or value for answers" )
686+ return values
687+
688+
632689class NDChecklist (VideoSupported , NDBase ):
633690 ontology_type : Literal ["checklist" ] = "checklist"
634- answers : List [NDFeatureSchema ] = pydantic .Field (determinant = True )
691+ answers : List [NDAnswer ] = pydantic .Field (determinant = True )
635692
636693 @validator ('answers' , pre = True )
637694 def validate_answers (cls , value , field ):
@@ -640,32 +697,43 @@ def validate_answers(cls, value, field):
640697 raise ValueError ("Checklist answers should not be empty" )
641698 return value
642699
643- def validate_feature_schemas (self , valid_feature_schemas ):
700+ def validate_feature_schemas (self , valid_feature_schemas_by_id ,
701+ valid_feature_schemas_by_name ):
644702 #Test top level feature schema for this tool
645- super (NDChecklist , self ).validate_feature_schemas (valid_feature_schemas )
703+ super (NDChecklist ,
704+ self ).validate_feature_schemas (valid_feature_schemas_by_id ,
705+ valid_feature_schemas_by_name )
646706 #Test the feature schemas provided to the answer field
647- if len (set ([answer .schemaId for answer in self .answers ])) != len (
648- self .answers ):
707+ if len (set ([answer .value or answer . schemaId for answer in self .answers
708+ ])) != len ( self .answers ):
649709 raise ValueError (
650710 f"Duplicated featureSchema found for checklist { self .uuid } " )
651711 for answer in self .answers :
652- options = valid_feature_schemas [self .schemaId ]['options' ]
653- if answer .schemaId not in options :
712+ options = valid_feature_schemas_by_name [
713+ self .
714+ name ]['options' ] if self .name else valid_feature_schemas_by_id [
715+ self .schemaId ]['options' ]
716+ if answer .value not in options and answer .schemaId not in options :
654717 raise ValueError (
655718 f"Feature schema provided to { self .ontology_type } invalid. Expected on of { options } . Found { answer } "
656719 )
657720
658721
659722class NDRadio (VideoSupported , NDBase ):
660723 ontology_type : Literal ["radio" ] = "radio"
661- answer : NDFeatureSchema = pydantic .Field (determinant = True )
662-
663- def validate_feature_schemas (self , valid_feature_schemas ):
664- super (NDRadio , self ).validate_feature_schemas (valid_feature_schemas )
665- options = valid_feature_schemas [self .schemaId ]['options' ]
666- if self .answer .schemaId not in options :
724+ answer : NDAnswer = pydantic .Field (determinant = True )
725+
726+ def validate_feature_schemas (self , valid_feature_schemas_by_id ,
727+ valid_feature_schemas_by_name ):
728+ super (NDRadio ,
729+ self ).validate_feature_schemas (valid_feature_schemas_by_id ,
730+ valid_feature_schemas_by_name )
731+ options = valid_feature_schemas_by_name [
732+ self .name ]['options' ] if self .name else valid_feature_schemas_by_id [
733+ self .schemaId ]['options' ]
734+ if self .answer .value not in options and self .answer .schemaId not in options :
667735 raise ValueError (
668- f"Feature schema provided to { self .ontology_type } invalid. Expected on of { options } . Found { self .answer .schemaId } "
736+ f"Feature schema provided to { self .ontology_type } invalid. Expected on of { options } . Found { self .answer .value or self . answer . schemaId } "
669737 )
670738
671739
@@ -684,11 +752,20 @@ class NDBaseTool(NDBase):
684752 classifications : List [NDClassification ] = []
685753
686754 #This is indepdent of our problem
687- def validate_feature_schemas (self , valid_feature_schemas ):
688- super (NDBaseTool , self ).validate_feature_schemas (valid_feature_schemas )
755+ def validate_feature_schemas (self , valid_feature_schemas_by_id ,
756+ valid_feature_schemas_by_name ):
757+ super (NDBaseTool ,
758+ self ).validate_feature_schemas (valid_feature_schemas_by_id ,
759+ valid_feature_schemas_by_name )
689760 for classification in self .classifications :
690761 classification .validate_feature_schemas (
691- valid_feature_schemas [self .schemaId ]['classifications' ])
762+ valid_feature_schemas_by_name [
763+ self .name ]['classificationsBySchemaId' ]
764+ if self .name else valid_feature_schemas_by_id [self .schemaId ]
765+ ['classificationsBySchemaId' ], valid_feature_schemas_by_name [
766+ self .name ]['classificationsByName' ]
767+ if self .name else valid_feature_schemas_by_id [
768+ self .schemaId ]['classificationsByName' ])
692769
693770 @validator ('classifications' , pre = True )
694771 def validate_subclasses (cls , value , field ):
0 commit comments