Skip to content

[PLT-600] Converted SDK to use pydantic V2 #1738

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
94e76be
made test work
Gabefire Jul 19, 2024
8b03727
fixed data row metadata tests
Gabefire Jul 20, 2024
e6bfae7
fixed linting test
Gabefire Jul 20, 2024
b0d36e9
more tests fixed
Gabefire Jul 20, 2024
266e394
fixed another set up tests
Gabefire Jul 20, 2024
88ef803
fixed more tests
Gabefire Jul 20, 2024
1482090
finished unit tests
Gabefire Jul 20, 2024
1a704e1
reverted changes to pyproject
Gabefire Jul 20, 2024
19df3a0
changed feature_serilazier
Gabefire Jul 20, 2024
ab90e06
removed all pydantic_compat
Gabefire Jul 21, 2024
1021dc3
fixed tests
Gabefire Jul 21, 2024
3fab04e
removed gets
Gabefire Jul 21, 2024
646a61a
fixed a lot of tests
Gabefire Jul 21, 2024
e72d21a
finished serilazation tests
Gabefire Jul 23, 2024
35f3013
fixed more test
Gabefire Jul 23, 2024
ba26141
revert mistake
Gabefire Jul 23, 2024
a937770
fixed final bad test
Gabefire Jul 23, 2024
2430a9d
cleaned pr up
Gabefire Jul 23, 2024
bec6157
converted to self
Gabefire Jul 23, 2024
8f890d7
fixed bad tests
Gabefire Jul 23, 2024
fbb090f
added notes
Gabefire Jul 23, 2024
43b38dc
fixed last test
Gabefire Jul 23, 2024
dc8cda7
fixed last test
Gabefire Jul 23, 2024
4e5f564
Fix LabelingService classes
Aug 26, 2024
51127ae
Convert labeling dashboard models to pydantic2
Aug 27, 2024
8d1b666
Fix search filters
Aug 27, 2024
2fdd502
Update pydantic types to Annotated
Aug 30, 2024
d796a98
feedback
Gabefire Sep 2, 2024
a43290e
added validation_alias
Gabefire Sep 2, 2024
20fd783
Fix tests post-merge
Sep 3, 2024
a33f238
Update pydantic requirement to 2
Sep 4, 2024
c8e96b6
Migrate mmc classes
Sep 5, 2024
a78c5c7
Fixed typo I noticed
Gabefire Sep 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libs/labelbox/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "Labelbox Python API"
authors = [{ name = "Labelbox", email = "engineering@labelbox.com" }]
dependencies = [
"google-api-core>=1.22.1",
"pydantic>=1.8",
"pydantic>=2.0",
"python-dateutil>=2.8.2, <2.10.0",
"requests>=2.22.0",
"strenum>=0.4.15",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@

from .classification import Checklist
from .classification import ClassificationAnswer
from .classification import Dropdown
from .classification import Radio
from .classification import Text

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from labelbox.data.annotation_types.classification.classification import ClassificationAnnotation
from .ner import DocumentEntity, TextEntity, ConversationEntity
from typing import Optional


class ObjectAnnotation(BaseAnnotation, ConfidenceMixin, CustomMetricsMixin):
Expand All @@ -29,4 +30,4 @@ class ObjectAnnotation(BaseAnnotation, ConfidenceMixin, CustomMetricsMixin):
"""

value: Union[TextEntity, ConversationEntity, DocumentEntity, Geometry]
classifications: List[ClassificationAnnotation] = []
classifications: Optional[List[ClassificationAnnotation]] = []
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import abc
from uuid import UUID, uuid4
from typing import Any, Dict, Optional
from labelbox import pydantic_compat

from .feature import FeatureSchema
from pydantic import PrivateAttr, ConfigDict


class BaseAnnotation(FeatureSchema, abc.ABC):
""" Base annotation class. Shouldn't be directly instantiated
"""
_uuid: Optional[UUID] = pydantic_compat.PrivateAttr()
_uuid: Optional[UUID] = PrivateAttr()
extra: Dict[str, Any] = {}

model_config = ConfigDict(extra="allow")

def __init__(self, **data):
super().__init__(**data)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .classification import (Checklist, ClassificationAnswer, Dropdown, Radio,
from .classification import (Checklist, ClassificationAnswer, Radio,
Text)
Original file line number Diff line number Diff line change
@@ -1,28 +1,12 @@
from typing import Any, Dict, List, Union, Optional
import warnings
from labelbox.data.annotation_types.base_annotation import BaseAnnotation

from labelbox.data.mixins import ConfidenceMixin, CustomMetricsMixin

try:
from typing import Literal
except:
from typing_extensions import Literal

from labelbox import pydantic_compat
from pydantic import BaseModel
from ..feature import FeatureSchema


# TODO: Replace when pydantic adds support for unions that don't coerce types
class _TempName(ConfidenceMixin, pydantic_compat.BaseModel):
name: str

def dict(self, *args, **kwargs):
res = super().dict(*args, **kwargs)
res.pop('name')
return res


class ClassificationAnswer(FeatureSchema, ConfidenceMixin, CustomMetricsMixin):
"""
- Represents a classification option.
Expand All @@ -36,18 +20,10 @@ class ClassificationAnswer(FeatureSchema, ConfidenceMixin, CustomMetricsMixin):
"""
extra: Dict[str, Any] = {}
keyframe: Optional[bool] = None
classifications: List['ClassificationAnnotation'] = []
classifications: Optional[List['ClassificationAnnotation']] = None

def dict(self, *args, **kwargs) -> Dict[str, str]:
res = super().dict(*args, **kwargs)
if res['keyframe'] is None:
res.pop('keyframe')
if res['classifications'] == []:
res.pop('classifications')
return res


class Radio(ConfidenceMixin, CustomMetricsMixin, pydantic_compat.BaseModel):
class Radio(ConfidenceMixin, CustomMetricsMixin, BaseModel):
""" A classification with only one selected option allowed

>>> Radio(answer = ClassificationAnswer(name = "dog"))
Expand All @@ -56,17 +32,16 @@ class Radio(ConfidenceMixin, CustomMetricsMixin, pydantic_compat.BaseModel):
answer: ClassificationAnswer


class Checklist(_TempName):
class Checklist(ConfidenceMixin, BaseModel):
""" A classification with many selected options allowed

>>> Checklist(answer = [ClassificationAnswer(name = "cloudy")])

"""
name: Literal["checklist"] = "checklist"
answer: List[ClassificationAnswer]


class Text(ConfidenceMixin, CustomMetricsMixin, pydantic_compat.BaseModel):
class Text(ConfidenceMixin, CustomMetricsMixin, BaseModel):
""" Free form text

>>> Text(answer = "some text answer")
Expand All @@ -75,24 +50,6 @@ class Text(ConfidenceMixin, CustomMetricsMixin, pydantic_compat.BaseModel):
answer: str


class Dropdown(_TempName):
"""
- A classification with many selected options allowed .
- This is not currently compatible with MAL.

Deprecation Notice: Dropdown classification is deprecated and will be
removed in a future release. Dropdown will also
no longer be able to be created in the Editor on 3/31/2022.
"""
name: Literal["dropdown"] = "dropdown"
answer: List[ClassificationAnswer]

def __init__(self, **data: Any):
super().__init__(**data)
warnings.warn("Dropdown classification is deprecated and will be "
"removed in a future release")


class ClassificationAnnotation(BaseAnnotation, ConfidenceMixin,
CustomMetricsMixin):
"""Classification annotations (non localized)
Expand All @@ -106,12 +63,9 @@ class ClassificationAnnotation(BaseAnnotation, ConfidenceMixin,
name (Optional[str])
classifications (Optional[List[ClassificationAnnotation]]): Optional sub classification of the annotation
feature_schema_id (Optional[Cuid])
value (Union[Text, Checklist, Radio, Dropdown])
value (Union[Text, Checklist, Radio])
extra (Dict[str, Any])
"""

value: Union[Text, Checklist, Radio, Dropdown]
value: Union[Text, Checklist, Radio]
message_id: Optional[str] = None


ClassificationAnswer.update_forward_refs()
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from abc import ABC
from typing import Optional, Dict, List, Any

from labelbox import pydantic_compat
from pydantic import BaseModel


class BaseData(pydantic_compat.BaseModel, ABC):
class BaseData(BaseModel, ABC):
"""
Base class for objects representing data.
This class shouldn't directly be used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
from .base_data import BaseData


class ConversationData(BaseData):
class_name: Literal["ConversationData"] = "ConversationData"
class ConversationData(BaseData, _NoCoercionMixin):
class_name: Literal["ConversationData"] = "ConversationData"
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Callable, Literal, Optional

from labelbox import pydantic_compat
from labelbox.data.annotation_types.data.base_data import BaseData
from labelbox.utils import _NoCoercionMixin
from pydantic import model_validator


class GenericDataRowData(BaseData, _NoCoercionMixin):
Expand All @@ -14,7 +14,8 @@ class GenericDataRowData(BaseData, _NoCoercionMixin):
def create_url(self, signer: Callable[[bytes], str]) -> Optional[str]:
return self.url

@pydantic_compat.root_validator(pre=True)
@model_validator(mode="before")
@classmethod
def validate_one_datarow_key_present(cls, data):
keys = ['external_id', 'global_key', 'uid']
count = sum([key in data for key in keys])
Expand Down
33 changes: 15 additions & 18 deletions libs/labelbox/src/labelbox/data/annotation_types/data/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@
import requests
import numpy as np

from labelbox import pydantic_compat
from pydantic import BaseModel, model_validator, ConfigDict
from labelbox.exceptions import InternalServerError
from .base_data import BaseData
from ..types import TypedArray


class RasterData(pydantic_compat.BaseModel, ABC):
class RasterData(BaseModel, ABC):
"""Represents an image or segmentation mask.
"""
im_bytes: Optional[bytes] = None
file_path: Optional[str] = None
url: Optional[str] = None
uid: Optional[str] = None
global_key: Optional[str] = None
arr: Optional[TypedArray[Literal['uint8']]] = None
model_config = ConfigDict(extra="forbid", copy_on_model_validation="none")

@classmethod
def from_2D_arr(cls, arr: Union[TypedArray[Literal['uint8']],
Expand Down Expand Up @@ -155,14 +158,14 @@ def create_url(self, signer: Callable[[bytes], str]) -> str:
"One of url, im_bytes, file_path, arr must not be None.")
return self.url

@pydantic_compat.root_validator()
def validate_args(cls, values):
file_path = values.get("file_path")
im_bytes = values.get("im_bytes")
url = values.get("url")
arr = values.get("arr")
uid = values.get('uid')
global_key = values.get('global_key')
@model_validator(mode="after")
def validate_args(self, values):
file_path = self.file_path
im_bytes = self.im_bytes
url = self.url
arr = self.arr
uid = self.uid
global_key = self.global_key
if uid == file_path == im_bytes == url == global_key == None and arr is None:
raise ValueError(
"One of `file_path`, `im_bytes`, `url`, `uid`, `global_key` or `arr` required."
Expand All @@ -175,8 +178,8 @@ def validate_args(cls, values):
elif len(arr.shape) != 3:
raise ValueError(
"unsupported image format. Must be 3D ([H,W,C])."
f"Use {cls.__name__}.from_2D_arr to construct from 2D")
return values
f"Use {self.__name__}.from_2D_arr to construct from 2D")
return self

def __repr__(self) -> str:
symbol_or_none = lambda data: '...' if data is not None else None
Expand All @@ -185,12 +188,6 @@ def __repr__(self) -> str:
f"url={self.url}," \
f"arr={symbol_or_none(self.arr)})"

class Config:
# Required for sharing references
copy_on_model_validation = 'none'
# Required for discriminating between data types
extra = 'forbid'


class MaskData(RasterData):
"""Used to represent a segmentation Mask
Expand Down
25 changes: 11 additions & 14 deletions libs/labelbox/src/labelbox/data/annotation_types/data/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from requests.exceptions import ConnectTimeout
from google.api_core import retry

from labelbox import pydantic_compat
from pydantic import ConfigDict, model_validator
from labelbox.exceptions import InternalServerError
from labelbox.typing_imports import Literal
from labelbox.utils import _NoCoercionMixin
Expand All @@ -26,6 +26,7 @@ class TextData(BaseData, _NoCoercionMixin):
file_path: Optional[str] = None
text: Optional[str] = None
url: Optional[str] = None
model_config = ConfigDict(extra="forbid")

@property
def value(self) -> str:
Expand Down Expand Up @@ -64,7 +65,7 @@ def fetch_remote(self) -> str:
"""
response = requests.get(self.url)
if response.status_code in [500, 502, 503, 504]:
raise labelbox.exceptions.InternalServerError(response.text)
raise InternalServerError(response.text)
response.raise_for_status()
return response.text

Expand All @@ -90,24 +91,20 @@ def create_url(self, signer: Callable[[bytes], str]) -> None:
"One of url, im_bytes, file_path, numpy must not be None.")
return self.url

@pydantic_compat.root_validator
def validate_date(cls, values):
file_path = values.get("file_path")
text = values.get("text")
url = values.get("url")
uid = values.get('uid')
global_key = values.get('global_key')
@model_validator(mode="after")
def validate_date(self, values):
file_path = self.file_path
text = self.text
url = self.url
uid = self.uid
global_key = self.global_key
if uid == file_path == text == url == global_key == None:
raise ValueError(
"One of `file_path`, `text`, `uid`, `global_key` or `url` required."
)
return values
return self

def __repr__(self) -> str:
return f"TextData(file_path={self.file_path}," \
f"text={self.text[:30] + '...' if self.text is not None else None}," \
f"url={self.url})"

class config:
# Required for discriminating between data types
extra = 'forbid'
Loading
Loading