From 2ad0108995f3924d1f77d2bfd26a085b710b5771 Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Thu, 8 Feb 2024 14:10:56 -0800 Subject: [PATCH 1/5] Add pydantic 2 compatibility library --- labelbox/pydantic_compat.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 labelbox/pydantic_compat.py diff --git a/labelbox/pydantic_compat.py b/labelbox/pydantic_compat.py new file mode 100644 index 000000000..77403117a --- /dev/null +++ b/labelbox/pydantic_compat.py @@ -0,0 +1,35 @@ +from typing import Optional + + +def pydantic_import(class_name, sub_module_path: Optional[str] = None): + import importlib + import pkg_resources + + # Get the version of pydantic + pydantic_version = pkg_resources.get_distribution("pydantic").version + + # Check if the version is 1 + if pydantic_version.startswith("1"): + pydantic_v1_module_name = "pydantic" if sub_module_path is None else f"pydantic.{sub_module_path}" + klass = getattr(importlib.import_module("pydantic"), class_name) + else: # use pydantic 2 v1 thunk + pydantic_v1_module_name = "pydantic.v1" if sub_module_path is None else f"pydantic.{sub_module_path}" + + klass = getattr(importlib.import_module(pydantic_v1_module_name), class_name) + + return klass + + +BaseModel = pydantic_import("BaseModel") +PrivateAttr = pydantic_import("PrivateAttr") +Field = pydantic_import("Field") +ModelField = pydantic_import("ModelField", "fields") +ValidationError = pydantic_import("ValidationError") +ErrorWrapper = pydantic_import("ErrorWrapper", "error_wrappers") + +validator = pydantic_import("validator") +root_validator = pydantic_import("root_validator") +conint = pydantic_import("conint") +conlist = pydantic_import("conlist") +constr = pydantic_import("constr") +confloat = pydantic_import("confloat") From 2611b389a56c2a88eae8c488db6ecbc2f8e96407 Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Thu, 8 Feb 2024 14:13:26 -0800 Subject: [PATCH 2/5] Replace pydantic imports using pydantic_compat --- .../data/annotation_types/base_annotation.py | 4 +- .../classification/classification.py | 8 +- .../data/annotation_types/data/base_data.py | 4 +- labelbox/data/annotation_types/data/raster.py | 7 +- labelbox/data/annotation_types/data/text.py | 4 +- .../data/annotation_types/data/tiled_image.py | 17 ++-- labelbox/data/annotation_types/data/video.py | 5 +- labelbox/data/annotation_types/feature.py | 6 +- .../annotation_types/geometry/geometry.py | 4 +- .../data/annotation_types/geometry/line.py | 5 +- .../data/annotation_types/geometry/mask.py | 5 +- .../data/annotation_types/geometry/polygon.py | 5 +- labelbox/data/annotation_types/label.py | 4 +- .../data/annotation_types/metrics/base.py | 16 ++-- .../metrics/confusion_matrix.py | 7 +- .../data/annotation_types/metrics/scalar.py | 8 +- .../annotation_types/ner/document_entity.py | 9 +-- .../data/annotation_types/ner/text_entity.py | 6 +- .../data/annotation_types/relationship.py | 4 +- labelbox/data/annotation_types/types.py | 8 +- labelbox/data/annotation_types/video.py | 6 +- labelbox/data/mixins.py | 14 ++-- .../data/serialization/coco/annotation.py | 8 +- .../data/serialization/coco/categories.py | 4 +- .../serialization/coco/instance_dataset.py | 4 +- .../serialization/coco/panoptic_dataset.py | 4 +- labelbox/data/serialization/coco/path.py | 4 +- .../labelbox_v1/classification.py | 4 +- .../data/serialization/labelbox_v1/feature.py | 6 +- .../data/serialization/labelbox_v1/label.py | 26 ++++--- .../data/serialization/labelbox_v1/objects.py | 30 ++++---- labelbox/data/serialization/ndjson/base.py | 8 +- .../serialization/ndjson/classification.py | 10 +-- labelbox/data/serialization/ndjson/label.py | 8 +- labelbox/data/serialization/ndjson/objects.py | 20 ++--- .../data/serialization/ndjson/relationship.py | 4 +- labelbox/pydantic_compat.py | 5 +- labelbox/schema/bulk_import_request.py | 77 ++++++++++--------- labelbox/schema/data_row_metadata.py | 22 ++++-- labelbox/schema/export_task.py | 8 +- labelbox/schema/foundry/app.py | 4 +- labelbox/schema/foundry/model.py | 4 +- labelbox/schema/ontology.py | 8 +- labelbox/utils.py | 4 +- .../classification/test_classification.py | 21 ++--- .../data/annotation_types/data/test_raster.py | 4 +- tests/data/annotation_types/data/test_text.py | 4 +- .../data/annotation_types/data/test_video.py | 4 +- .../annotation_types/geometry/test_line.py | 6 +- .../annotation_types/geometry/test_mask.py | 4 +- .../annotation_types/geometry/test_point.py | 4 +- .../annotation_types/geometry/test_polygon.py | 8 +- .../geometry/test_rectangle.py | 4 +- .../data/annotation_types/test_annotation.py | 8 +- tests/data/annotation_types/test_metrics.py | 18 ++--- .../data/annotation_types/test_tiled_image.py | 4 +- 56 files changed, 265 insertions(+), 252 deletions(-) diff --git a/labelbox/data/annotation_types/base_annotation.py b/labelbox/data/annotation_types/base_annotation.py index ea8ed8c1b..9949bd7f8 100644 --- a/labelbox/data/annotation_types/base_annotation.py +++ b/labelbox/data/annotation_types/base_annotation.py @@ -1,7 +1,7 @@ import abc from uuid import UUID from typing import Any, Dict, Optional -from pydantic import PrivateAttr +from labelbox import pydantic_compat from .feature import FeatureSchema @@ -9,7 +9,7 @@ class BaseAnnotation(FeatureSchema, abc.ABC): """ Base annotation class. Shouldn't be directly instantiated """ - _uuid: Optional[UUID] = PrivateAttr() + _uuid: Optional[UUID] = pydantic_compat.PrivateAttr() extra: Dict[str, Any] = {} def __init__(self, **data): diff --git a/labelbox/data/annotation_types/classification/classification.py b/labelbox/data/annotation_types/classification/classification.py index 06366d3d9..68ea7e0e7 100644 --- a/labelbox/data/annotation_types/classification/classification.py +++ b/labelbox/data/annotation_types/classification/classification.py @@ -9,12 +9,12 @@ except: from typing_extensions import Literal -from pydantic import BaseModel, validator +from labelbox import pydantic_compat, validator from ..feature import FeatureSchema # TODO: Replace when pydantic adds support for unions that don't coerce types -class _TempName(ConfidenceMixin, BaseModel): +class _TempName(ConfidenceMixin, pydantic_compat.BaseModel): name: str def dict(self, *args, **kwargs): @@ -47,7 +47,7 @@ def dict(self, *args, **kwargs) -> Dict[str, str]: return res -class Radio(ConfidenceMixin, CustomMetricsMixin, BaseModel): +class Radio(ConfidenceMixin, CustomMetricsMixin, pydantic_compat.BaseModel): """ A classification with only one selected option allowed >>> Radio(answer = ClassificationAnswer(name = "dog")) @@ -66,7 +66,7 @@ class Checklist(_TempName): answer: List[ClassificationAnswer] -class Text(ConfidenceMixin, CustomMetricsMixin, BaseModel): +class Text(ConfidenceMixin, CustomMetricsMixin, pydantic_compat.BaseModel): """ Free form text >>> Text(answer = "some text answer") diff --git a/labelbox/data/annotation_types/data/base_data.py b/labelbox/data/annotation_types/data/base_data.py index 2ccda34c3..ab0dd1e53 100644 --- a/labelbox/data/annotation_types/data/base_data.py +++ b/labelbox/data/annotation_types/data/base_data.py @@ -1,10 +1,10 @@ from abc import ABC from typing import Optional, Dict, List, Any -from pydantic import BaseModel +from labelbox import pydantic_compat -class BaseData(BaseModel, ABC): +class BaseData(pydantic_compat.BaseModel, ABC): """ Base class for objects representing data. This class shouldn't directly be used diff --git a/labelbox/data/annotation_types/data/raster.py b/labelbox/data/annotation_types/data/raster.py index 45998cb26..94b8a2a7e 100644 --- a/labelbox/data/annotation_types/data/raster.py +++ b/labelbox/data/annotation_types/data/raster.py @@ -5,18 +5,17 @@ from PIL import Image from google.api_core import retry -from pydantic import BaseModel -from pydantic import root_validator from requests.exceptions import ConnectTimeout import requests import numpy as np +from labelbox import pydantic_compat from labelbox.exceptions import InternalServerError from .base_data import BaseData from ..types import TypedArray -class RasterData(BaseModel, ABC): +class RasterData(pydantic_compat.BaseModel, ABC): """Represents an image or segmentation mask. """ im_bytes: Optional[bytes] = None @@ -156,7 +155,7 @@ def create_url(self, signer: Callable[[bytes], str]) -> str: "One of url, im_bytes, file_path, arr must not be None.") return self.url - @root_validator() + @pydantic_compat.root_validator() def validate_args(cls, values): file_path = values.get("file_path") im_bytes = values.get("im_bytes") diff --git a/labelbox/data/annotation_types/data/text.py b/labelbox/data/annotation_types/data/text.py index 704d2e7b4..e46eee507 100644 --- a/labelbox/data/annotation_types/data/text.py +++ b/labelbox/data/annotation_types/data/text.py @@ -3,8 +3,8 @@ import requests from requests.exceptions import ConnectTimeout from google.api_core import retry -from pydantic import root_validator +from labelbox import pydantic_compat from labelbox.exceptions import InternalServerError from labelbox.typing_imports import Literal from labelbox.utils import _NoCoercionMixin @@ -90,7 +90,7 @@ def create_url(self, signer: Callable[[bytes], str]) -> None: "One of url, im_bytes, file_path, numpy must not be None.") return self.url - @root_validator + @pydantic_compat.root_validator def validate_date(cls, values): file_path = values.get("file_path") text = values.get("text") diff --git a/labelbox/data/annotation_types/data/tiled_image.py b/labelbox/data/annotation_types/data/tiled_image.py index d1bfe8532..6a3bd6988 100644 --- a/labelbox/data/annotation_types/data/tiled_image.py +++ b/labelbox/data/annotation_types/data/tiled_image.py @@ -12,8 +12,7 @@ from PIL import Image from pyproj import Transformer from pygeotile.point import Point as PygeoPoint -from pydantic import BaseModel, validator -from pydantic.class_validators import root_validator +from labelbox import pydantic_compat from labelbox.data.annotation_types import Rectangle, Point, Line, Polygon from .base_data import BaseData @@ -41,7 +40,7 @@ class EPSG(Enum): EPSG3857 = 3857 -class TiledBounds(BaseModel): +class TiledBounds(pydantic_compat.BaseModel): """ Bounds for a tiled image asset related to the relevant epsg. Bounds should be Point objects. @@ -55,7 +54,7 @@ class TiledBounds(BaseModel): epsg: EPSG bounds: List[Point] - @validator('bounds') + @pydantic_compat.validator('bounds') def validate_bounds_not_equal(cls, bounds): first_bound = bounds[0] second_bound = bounds[1] @@ -67,7 +66,7 @@ def validate_bounds_not_equal(cls, bounds): return bounds #validate bounds are within lat,lng range if they are EPSG4326 - @root_validator + @pydantic_compat.root_validator def validate_bounds_lat_lng(cls, values): epsg = values.get('epsg') bounds = values.get('bounds') @@ -83,7 +82,7 @@ def validate_bounds_lat_lng(cls, values): return values -class TileLayer(BaseModel): +class TileLayer(pydantic_compat.BaseModel): """ Url that contains the tile layer. Must be in the format: https://c.tile.openstreetmap.org/{z}/{x}/{y}.png @@ -99,7 +98,7 @@ class TileLayer(BaseModel): def asdict(self) -> Dict[str, str]: return {"tileLayerUrl": self.url, "name": self.name} - @validator('url') + @pydantic_compat.validator('url') def validate_url(cls, url): xyz_format = "/{z}/{x}/{y}" if xyz_format not in url: @@ -344,7 +343,7 @@ def _validate_num_tiles(self, xstart: float, ystart: float, xend: float, f"Max allowed tiles are {max_tiles}" f"Increase max tiles or reduce zoom level.") - @validator('zoom_levels') + @pydantic_compat.validator('zoom_levels') def validate_zoom_levels(cls, zoom_levels): if zoom_levels[0] > zoom_levels[1]: raise ValueError( @@ -353,7 +352,7 @@ def validate_zoom_levels(cls, zoom_levels): return zoom_levels -class EPSGTransformer(BaseModel): +class EPSGTransformer(pydantic_compat.BaseModel): """Transformer class between different EPSG's. Useful when wanting to project in different formats. """ diff --git a/labelbox/data/annotation_types/data/video.py b/labelbox/data/annotation_types/data/video.py index 53cee7280..3ebda5c4c 100644 --- a/labelbox/data/annotation_types/data/video.py +++ b/labelbox/data/annotation_types/data/video.py @@ -8,11 +8,12 @@ import cv2 import numpy as np from google.api_core import retry -from pydantic import root_validator from .base_data import BaseData from ..types import TypedArray +from labelbox import pydantic_compat + logger = logging.getLogger(__name__) @@ -147,7 +148,7 @@ def frames_to_video(self, out.release() return file_path - @root_validator + @pydantic_compat.root_validator def validate_data(cls, values): file_path = values.get("file_path") url = values.get("url") diff --git a/labelbox/data/annotation_types/feature.py b/labelbox/data/annotation_types/feature.py index f3ae8eb8f..21e3eb413 100644 --- a/labelbox/data/annotation_types/feature.py +++ b/labelbox/data/annotation_types/feature.py @@ -1,11 +1,11 @@ from typing import Optional -from pydantic import BaseModel, root_validator +from labelbox import pydantic_compat from .types import Cuid -class FeatureSchema(BaseModel): +class FeatureSchema(pydantic_compat.BaseModel): """ Class that represents a feature schema. Could be a annotation, a subclass, or an option. @@ -14,7 +14,7 @@ class FeatureSchema(BaseModel): name: Optional[str] = None feature_schema_id: Optional[Cuid] = None - @root_validator + @pydantic_compat.root_validator def must_set_one(cls, values): if values['feature_schema_id'] is None and values['name'] is None: raise ValueError( diff --git a/labelbox/data/annotation_types/geometry/geometry.py b/labelbox/data/annotation_types/geometry/geometry.py index 731e9a591..2394f011f 100644 --- a/labelbox/data/annotation_types/geometry/geometry.py +++ b/labelbox/data/annotation_types/geometry/geometry.py @@ -3,12 +3,12 @@ import geojson import numpy as np -from pydantic import BaseModel +from labelbox import pydantic_compat from shapely import geometry as geom -class Geometry(BaseModel, ABC): +class Geometry(pydantic_compat.BaseModel, ABC): """Abstract base class for geometry objects """ extra: Dict[str, Any] = {} diff --git a/labelbox/data/annotation_types/geometry/line.py b/labelbox/data/annotation_types/geometry/line.py index 680ec2df9..0ae0c3b1d 100644 --- a/labelbox/data/annotation_types/geometry/line.py +++ b/labelbox/data/annotation_types/geometry/line.py @@ -3,13 +3,14 @@ import geojson import numpy as np import cv2 -from pydantic import validator from shapely.geometry import LineString as SLineString from .point import Point from .geometry import Geometry +from labelbox import pydantic_compat + class Line(Geometry): """Line annotation @@ -64,7 +65,7 @@ def draw(self, color=color, thickness=thickness) - @validator('points') + @pydantic_compat.validator('points') def is_geom_valid(cls, points): if len(points) < 2: raise ValueError( diff --git a/labelbox/data/annotation_types/geometry/mask.py b/labelbox/data/annotation_types/geometry/mask.py index 11728f7db..7c903b644 100644 --- a/labelbox/data/annotation_types/geometry/mask.py +++ b/labelbox/data/annotation_types/geometry/mask.py @@ -1,7 +1,6 @@ from typing import Callable, Optional, Tuple, Union, Dict, List import numpy as np -from pydantic.class_validators import validator import cv2 from shapely.geometry import MultiPolygon, Polygon @@ -9,6 +8,8 @@ from ..data import MaskData from .geometry import Geometry +from labelbox import pydantic_compat + class Mask(Geometry): """Mask used to represent a single class in a larger segmentation mask @@ -121,7 +122,7 @@ def create_url(self, signer: Callable[[bytes], str]) -> str: """ return self.mask.create_url(signer) - @validator('color') + @pydantic_compat.validator('color') def is_valid_color(cls, color): if isinstance(color, (tuple, list)): if len(color) == 1: diff --git a/labelbox/data/annotation_types/geometry/polygon.py b/labelbox/data/annotation_types/geometry/polygon.py index afd0e46a0..423861e31 100644 --- a/labelbox/data/annotation_types/geometry/polygon.py +++ b/labelbox/data/annotation_types/geometry/polygon.py @@ -3,13 +3,14 @@ import cv2 import geojson import numpy as np -from pydantic import validator from shapely.geometry import Polygon as SPolygon from .geometry import Geometry from .point import Point +from labelbox import pydantic_compat + class Polygon(Geometry): """Polygon geometry @@ -67,7 +68,7 @@ def draw(self, return cv2.fillPoly(canvas, pts, color) return cv2.polylines(canvas, pts, True, color, thickness) - @validator('points') + @pydantic_compat.validator('points') def is_geom_valid(cls, points): if len(points) < 3: raise ValueError( diff --git a/labelbox/data/annotation_types/label.py b/labelbox/data/annotation_types/label.py index a7009e3de..1043cff7a 100644 --- a/labelbox/data/annotation_types/label.py +++ b/labelbox/data/annotation_types/label.py @@ -2,7 +2,7 @@ from typing import Any, Callable, Dict, List, Union, Optional import warnings -from pydantic import BaseModel, validator +from labelbox import pydantic_compat, validator import labelbox from labelbox.data.annotation_types.data.tiled_image import TiledImageData @@ -24,7 +24,7 @@ LlmResponseCreationData] -class Label(BaseModel): +class Label(pydantic_compat.BaseModel): """Container for holding data and annotations >>> Label( diff --git a/labelbox/data/annotation_types/metrics/base.py b/labelbox/data/annotation_types/metrics/base.py index 910e5d47f..79cf22419 100644 --- a/labelbox/data/annotation_types/metrics/base.py +++ b/labelbox/data/annotation_types/metrics/base.py @@ -1,15 +1,15 @@ from abc import ABC -from pydantic import ValidationError, confloat, BaseModel, validator -from pydantic.error_wrappers import ErrorWrapper from typing import Dict, Optional, Any, Union -ConfidenceValue = confloat(ge=0, le=1) +from labelbox import pydantic_compat + +ConfidenceValue = pydantic_compat.confloat(ge=0, le=1) MIN_CONFIDENCE_SCORES = 2 MAX_CONFIDENCE_SCORES = 15 -class BaseMetric(BaseModel, ABC): +class BaseMetric(pydantic_compat.BaseModel, ABC): value: Union[Any, Dict[float, Any]] feature_name: Optional[str] = None subclass_name: Optional[str] = None @@ -19,17 +19,17 @@ def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) return {k: v for k, v in res.items() if v is not None} - @validator('value') + @pydantic_compat.validator('value') def validate_value(cls, value): if isinstance(value, Dict): if not (MIN_CONFIDENCE_SCORES <= len(value) <= MAX_CONFIDENCE_SCORES): - raise ValidationError([ - ErrorWrapper(ValueError( + raise pydantic_compat.ValidationError([ + pydantic_compat.ErrorWrapper(ValueError( "Number of confidence scores must be greater" f" than or equal to {MIN_CONFIDENCE_SCORES} and" f" less than or equal to {MAX_CONFIDENCE_SCORES}. Found {len(value)}" ), - loc='value') + loc='value') ], cls) return value diff --git a/labelbox/data/annotation_types/metrics/confusion_matrix.py b/labelbox/data/annotation_types/metrics/confusion_matrix.py index a907e71c3..f915e2f25 100644 --- a/labelbox/data/annotation_types/metrics/confusion_matrix.py +++ b/labelbox/data/annotation_types/metrics/confusion_matrix.py @@ -1,12 +1,11 @@ from enum import Enum from typing import Tuple, Dict, Union -from pydantic import conint, Field -from pydantic.main import BaseModel +from labelbox import pydantic_compat from .base import ConfidenceValue, BaseMetric -Count = conint(ge=0, le=1e10) +Count = pydantic_compat.conint(ge=0, le=1e10) ConfusionMatrixMetricValue = Tuple[Count, Count, Count, Count] ConfusionMatrixMetricConfidenceValue = Dict[ConfidenceValue, @@ -31,5 +30,5 @@ class ConfusionMatrixMetric(BaseMetric): metric_name: str value: Union[ConfusionMatrixMetricValue, ConfusionMatrixMetricConfidenceValue] - aggregation: ConfusionMatrixAggregation = Field( + aggregation: ConfusionMatrixAggregation = pydantic_compat.Field( ConfusionMatrixAggregation.CONFUSION_MATRIX, const=True) diff --git a/labelbox/data/annotation_types/metrics/scalar.py b/labelbox/data/annotation_types/metrics/scalar.py index fe93849b6..5f1279fd6 100644 --- a/labelbox/data/annotation_types/metrics/scalar.py +++ b/labelbox/data/annotation_types/metrics/scalar.py @@ -1,11 +1,11 @@ from typing import Dict, Optional, Union from enum import Enum -from pydantic import confloat, validator - from .base import ConfidenceValue, BaseMetric -ScalarMetricValue = confloat(ge=0, le=100_000_000) +from labelbox import pydantic_compat + +ScalarMetricValue = pydantic_compat.confloat(ge=0, le=100_000_000) ScalarMetricConfidenceValue = Dict[ConfidenceValue, ScalarMetricValue] @@ -33,7 +33,7 @@ class ScalarMetric(BaseMetric): value: Union[ScalarMetricValue, ScalarMetricConfidenceValue] aggregation: ScalarMetricAggregation = ScalarMetricAggregation.ARITHMETIC_MEAN - @validator('metric_name') + @pydantic_compat.validator('metric_name') def validate_metric_name(cls, name: Union[str, None]): if name is None: return None diff --git a/labelbox/data/annotation_types/ner/document_entity.py b/labelbox/data/annotation_types/ner/document_entity.py index fe40a2136..77141de06 100644 --- a/labelbox/data/annotation_types/ner/document_entity.py +++ b/labelbox/data/annotation_types/ner/document_entity.py @@ -1,22 +1,21 @@ from typing import List -from pydantic import BaseModel, validator - +from labelbox import pydantic_compat from labelbox.utils import _CamelCaseMixin -class DocumentTextSelection(_CamelCaseMixin, BaseModel): +class DocumentTextSelection(_CamelCaseMixin, pydantic_compat.BaseModel): token_ids: List[str] group_id: str page: int - @validator("page") + @pydantic_compat.validator("page") def validate_page(cls, v): if v < 1: raise ValueError("Page must be greater than 1") return v -class DocumentEntity(_CamelCaseMixin, BaseModel): +class DocumentEntity(_CamelCaseMixin, pydantic_compat.BaseModel): """ Represents a text entity """ text_selections: List[DocumentTextSelection] diff --git a/labelbox/data/annotation_types/ner/text_entity.py b/labelbox/data/annotation_types/ner/text_entity.py index 2513aa44e..6f410987f 100644 --- a/labelbox/data/annotation_types/ner/text_entity.py +++ b/labelbox/data/annotation_types/ner/text_entity.py @@ -1,15 +1,15 @@ from typing import Dict, Any -from pydantic import BaseModel, root_validator +from labelbox import pydantic_compat -class TextEntity(BaseModel): +class TextEntity(pydantic_compat.BaseModel): """ Represents a text entity """ start: int end: int extra: Dict[str, Any] = {} - @root_validator + @pydantic_compat.root_validator def validate_start_end(cls, values): if 'start' in values and 'end' in values: if (isinstance(values['start'], int) and diff --git a/labelbox/data/annotation_types/relationship.py b/labelbox/data/annotation_types/relationship.py index 27a833830..db61883d5 100644 --- a/labelbox/data/annotation_types/relationship.py +++ b/labelbox/data/annotation_types/relationship.py @@ -1,9 +1,9 @@ -from pydantic import BaseModel +from labelbox import pydantic_compat from enum import Enum from labelbox.data.annotation_types.annotation import BaseAnnotation, ObjectAnnotation -class Relationship(BaseModel): +class Relationship(pydantic_compat.BaseModel): class Type(Enum): UNIDIRECTIONAL = "unidirectional" diff --git a/labelbox/data/annotation_types/types.py b/labelbox/data/annotation_types/types.py index ba7b88ba2..3305462b1 100644 --- a/labelbox/data/annotation_types/types.py +++ b/labelbox/data/annotation_types/types.py @@ -3,11 +3,11 @@ from typing_extensions import Annotated from packaging import version -from pydantic import Field -from pydantic.fields import ModelField import numpy as np -Cuid = Annotated[str, Field(min_length=25, max_length=25)] +from labelbox import pydantic_compat + +Cuid = Annotated[str, pydantic_compat.Field(min_length=25, max_length=25)] DType = TypeVar('DType') DShape = TypeVar('DShape') @@ -20,7 +20,7 @@ def __get_validators__(cls): yield cls.validate @classmethod - def validate(cls, val, field: ModelField): + def validate(cls, val, field: pydantic_compat.ModelField): if not isinstance(val, np.ndarray): raise TypeError(f"Expected numpy array. Found {type(val)}") diff --git a/labelbox/data/annotation_types/video.py b/labelbox/data/annotation_types/video.py index a7e3c187a..83d79c1c9 100644 --- a/labelbox/data/annotation_types/video.py +++ b/labelbox/data/annotation_types/video.py @@ -1,7 +1,7 @@ from enum import Enum from typing import List, Optional, Tuple -from pydantic import BaseModel, validator, root_validator +from labelbox import pydantic_compat, validator, root_validator from labelbox.data.annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation from labelbox.data.annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation @@ -87,7 +87,7 @@ class DICOMObjectAnnotation(VideoObjectAnnotation): group_key: GroupKey -class MaskFrame(_CamelCaseMixin, BaseModel): +class MaskFrame(_CamelCaseMixin, pydantic_compat.BaseModel): index: int instance_uri: Optional[str] = None im_bytes: Optional[bytes] = None @@ -113,7 +113,7 @@ class MaskInstance(_CamelCaseMixin, FeatureSchema): name: str -class VideoMaskAnnotation(BaseModel): +class VideoMaskAnnotation(pydantic_compat.BaseModel): """Video mask annotation >>> VideoMaskAnnotation( >>> frames=[ diff --git a/labelbox/data/mixins.py b/labelbox/data/mixins.py index a6fdf416e..36fd91671 100644 --- a/labelbox/data/mixins.py +++ b/labelbox/data/mixins.py @@ -1,14 +1,14 @@ from typing import Optional, List -from pydantic import BaseModel, validator +from labelbox import pydantic_compat from labelbox.exceptions import ConfidenceNotSupportedException, CustomMetricsNotSupportedException -class ConfidenceMixin(BaseModel): +class ConfidenceMixin(pydantic_compat.BaseModel): confidence: Optional[float] = None - @validator("confidence") + @pydantic_compat.validator("confidence") def confidence_valid_float(cls, value): if value is None: return value @@ -32,24 +32,24 @@ def __new__(cls, *args, **kwargs): return super().__new__(cls) -class CustomMetric(BaseModel): +class CustomMetric(pydantic_compat.BaseModel): name: str value: float - @validator("name") + @pydantic_compat.validator("name") def confidence_valid_float(cls, value): if not isinstance(value, str): raise ValueError("Name must be a string") return value - @validator("value") + @pydantic_compat.validator("value") def value_valid_float(cls, value): if not isinstance(value, (int, float)): raise ValueError("Value must be a number") return value -class CustomMetricsMixin(BaseModel): +class CustomMetricsMixin(pydantic_compat.BaseModel): custom_metrics: Optional[List[CustomMetric]] = None def dict(self, *args, **kwargs): diff --git a/labelbox/data/serialization/coco/annotation.py b/labelbox/data/serialization/coco/annotation.py index 0ca897670..9269530c6 100644 --- a/labelbox/data/serialization/coco/annotation.py +++ b/labelbox/data/serialization/coco/annotation.py @@ -2,7 +2,7 @@ from pathlib import Path from collections import defaultdict -from pydantic import BaseModel +from labelbox import pydantic_compat import numpy as np from .path import PathSerializerMixin @@ -26,7 +26,7 @@ def get_annotation_lookup(annotations): return annotation_lookup -class SegmentInfo(BaseModel): +class SegmentInfo(pydantic_compat.BaseModel): id: int category_id: int area: int @@ -34,12 +34,12 @@ class SegmentInfo(BaseModel): iscrowd: int = 0 -class RLE(BaseModel): +class RLE(pydantic_compat.BaseModel): counts: List[int] size: Tuple[int, int] # h,w or w,h? -class COCOObjectAnnotation(BaseModel): +class COCOObjectAnnotation(pydantic_compat.BaseModel): # All segmentations for a particular class in an image... # So each image will have one of these for each class present in the image.. # Annotations only exist if there is data.. diff --git a/labelbox/data/serialization/coco/categories.py b/labelbox/data/serialization/coco/categories.py index 07ecacb03..0f26f1013 100644 --- a/labelbox/data/serialization/coco/categories.py +++ b/labelbox/data/serialization/coco/categories.py @@ -1,10 +1,10 @@ import sys from hashlib import md5 -from pydantic import BaseModel +from labelbox import pydantic_compat -class Categories(BaseModel): +class Categories(pydantic_compat.BaseModel): id: int name: str supercategory: str diff --git a/labelbox/data/serialization/coco/instance_dataset.py b/labelbox/data/serialization/coco/instance_dataset.py index d5568f299..b52d24e91 100644 --- a/labelbox/data/serialization/coco/instance_dataset.py +++ b/labelbox/data/serialization/coco/instance_dataset.py @@ -6,7 +6,7 @@ import numpy as np from tqdm import tqdm -from pydantic import BaseModel +from labelbox import pydantic_compat from ...annotation_types import ImageData, MaskData, Mask, ObjectAnnotation, Label, Polygon, Point, Rectangle from ...annotation_types.collection import LabelCollection @@ -129,7 +129,7 @@ def process_label( return image, coco_annotations, categories -class CocoInstanceDataset(BaseModel): +class CocoInstanceDataset(pydantic_compat.BaseModel): info: Dict[str, Any] = {} images: List[CocoImage] annotations: List[COCOObjectAnnotation] diff --git a/labelbox/data/serialization/coco/panoptic_dataset.py b/labelbox/data/serialization/coco/panoptic_dataset.py index d041c6e30..b6d2c9ae6 100644 --- a/labelbox/data/serialization/coco/panoptic_dataset.py +++ b/labelbox/data/serialization/coco/panoptic_dataset.py @@ -2,7 +2,7 @@ from typing import Dict, Any, List, Union from pathlib import Path -from pydantic import BaseModel +from labelbox import pydantic_compat from tqdm import tqdm import numpy as np from PIL import Image @@ -115,7 +115,7 @@ def process_label(label: Label, segments_info=segments), categories, is_thing -class CocoPanopticDataset(BaseModel): +class CocoPanopticDataset(pydantic_compat.BaseModel): info: Dict[str, Any] = {} images: List[CocoImage] annotations: List[PanopticAnnotation] diff --git a/labelbox/data/serialization/coco/path.py b/labelbox/data/serialization/coco/path.py index ecd860417..6f523152c 100644 --- a/labelbox/data/serialization/coco/path.py +++ b/labelbox/data/serialization/coco/path.py @@ -1,8 +1,8 @@ -from pydantic import BaseModel +from labelbox import pydantic_compat from pathlib import Path -class PathSerializerMixin(BaseModel): +class PathSerializerMixin(pydantic_compat.BaseModel): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) diff --git a/labelbox/data/serialization/labelbox_v1/classification.py b/labelbox/data/serialization/labelbox_v1/classification.py index 5481e85ba..8600f08e3 100644 --- a/labelbox/data/serialization/labelbox_v1/classification.py +++ b/labelbox/data/serialization/labelbox_v1/classification.py @@ -1,6 +1,6 @@ from typing import List, Union -from pydantic.main import BaseModel +from labelbox import pydantic_compat from .feature import LBV1Feature from ...annotation_types.annotation import ClassificationAnnotation @@ -90,7 +90,7 @@ def from_common(cls, text: Text, feature_schema_id: Cuid, return cls(schema_id=feature_schema_id, answer=text.answer, **extra) -class LBV1Classifications(BaseModel): +class LBV1Classifications(pydantic_compat.BaseModel): classifications: List[Union[LBV1Text, LBV1Radio, LBV1Dropdown, LBV1Checklist]] = [] diff --git a/labelbox/data/serialization/labelbox_v1/feature.py b/labelbox/data/serialization/labelbox_v1/feature.py index 649b84c48..cefddd079 100644 --- a/labelbox/data/serialization/labelbox_v1/feature.py +++ b/labelbox/data/serialization/labelbox_v1/feature.py @@ -1,19 +1,19 @@ from typing import Optional -from pydantic import BaseModel, root_validator +from labelbox import pydantic_compat from labelbox.utils import camel_case from ...annotation_types.types import Cuid -class LBV1Feature(BaseModel): +class LBV1Feature(pydantic_compat.BaseModel): keyframe: Optional[bool] = None title: str = None value: Optional[str] = None schema_id: Optional[Cuid] = None feature_id: Optional[Cuid] = None - @root_validator + @pydantic_compat.root_validator def check_ids(cls, values): if values.get('value') is None: values['value'] = values['title'] diff --git a/labelbox/data/serialization/labelbox_v1/label.py b/labelbox/data/serialization/labelbox_v1/label.py index 819dd995d..ee45bc8f0 100644 --- a/labelbox/data/serialization/labelbox_v1/label.py +++ b/labelbox/data/serialization/labelbox_v1/label.py @@ -2,7 +2,7 @@ from labelbox.utils import camel_case from typing import List, Optional, Union, Dict, Any -from pydantic import BaseModel, Field +from labelbox import pydantic_compat from ...annotation_types.annotation import (ClassificationAnnotation, ObjectAnnotation) @@ -35,7 +35,7 @@ def from_common( class LBV1LabelAnnotationsVideo(LBV1LabelAnnotations): - frame_number: int = Field(..., alias='frameNumber') + frame_number: int = pydantic_compat.Field(..., alias='frameNumber') def to_common( self @@ -104,7 +104,7 @@ class Config: allow_population_by_field_name = True -class Review(BaseModel): +class Review(pydantic_compat.BaseModel): score: int id: str created_at: str @@ -115,19 +115,21 @@ class Config: alias_generator = camel_case -Extra = lambda name: Field(None, alias=name, extra_field=True) +Extra = lambda name: pydantic_compat.Field(None, alias=name, extra_field=True) -class LBV1Label(BaseModel): +class LBV1Label(pydantic_compat.BaseModel): label: Union[LBV1LabelAnnotations, - List[LBV1LabelAnnotationsVideo]] = Field(..., alias='Label') - data_row_id: str = Field(..., alias="DataRow ID") - row_data: str = Field(None, alias="Labeled Data") - id: Optional[str] = Field(None, alias='ID') - external_id: Optional[str] = Field(None, alias="External ID") - data_row_media_attributes: Optional[Dict[str, Any]] = Field( + List[LBV1LabelAnnotationsVideo]] = pydantic_compat.Field( + ..., alias='Label') + data_row_id: str = pydantic_compat.Field(..., alias="DataRow ID") + row_data: str = pydantic_compat.Field(None, alias="Labeled Data") + id: Optional[str] = pydantic_compat.Field(None, alias='ID') + external_id: Optional[str] = pydantic_compat.Field(None, + alias="External ID") + data_row_media_attributes: Optional[Dict[str, Any]] = pydantic_compat.Field( {}, alias="Media Attributes") - data_row_metadata: Optional[List[Dict[str, Any]]] = Field( + data_row_metadata: Optional[List[Dict[str, Any]]] = pydantic_compat.Field( [], alias="DataRow Metadata") created_by: Optional[str] = Extra('Created By') diff --git a/labelbox/data/serialization/labelbox_v1/objects.py b/labelbox/data/serialization/labelbox_v1/objects.py index 8aeb79877..19f6c0717 100644 --- a/labelbox/data/serialization/labelbox_v1/objects.py +++ b/labelbox/data/serialization/labelbox_v1/objects.py @@ -4,7 +4,7 @@ except: from typing_extensions import Literal -from pydantic import BaseModel, validator, Field +from labelbox import pydantic_compat import numpy as np from .classification import LBV1Checklist, LBV1Classifications, LBV1Radio, LBV1Text, LBV1Dropdown @@ -32,7 +32,7 @@ def dict(self, *args, **kwargs) -> Dict[str, Any]: res.pop('instanceURI') return res - @validator('classifications', pre=True) + @pydantic_compat.validator('classifications', pre=True) def validate_subclasses(cls, value, field): # checklist subclasses create extra unessesary nesting. So we just remove it. if isinstance(value, list) and len(value): @@ -49,24 +49,24 @@ def validate_subclasses(cls, value, field): return value -class TIPointCoordinate(BaseModel): +class TIPointCoordinate(pydantic_compat.BaseModel): coordinates: List[float] -class TILineCoordinate(BaseModel): +class TILineCoordinate(pydantic_compat.BaseModel): coordinates: List[List[float]] -class TIPolygonCoordinate(BaseModel): +class TIPolygonCoordinate(pydantic_compat.BaseModel): coordinates: List[List[List[float]]] -class TIRectangleCoordinate(BaseModel): +class TIRectangleCoordinate(pydantic_compat.BaseModel): coordinates: List[List[List[float]]] class LBV1TIPoint(LBV1ObjectBase): - object_type: Literal['point'] = Field(..., alias='type') + object_type: Literal['point'] = pydantic_compat.Field(..., alias='type') geometry: TIPointCoordinate def to_common(self) -> Point: @@ -75,7 +75,7 @@ def to_common(self) -> Point: class LBV1TILine(LBV1ObjectBase): - object_type: Literal['polyline'] = Field(..., alias='type') + object_type: Literal['polyline'] = pydantic_compat.Field(..., alias='type') geometry: TILineCoordinate def to_common(self) -> Line: @@ -85,7 +85,7 @@ def to_common(self) -> Line: class LBV1TIPolygon(LBV1ObjectBase): - object_type: Literal['polygon'] = Field(..., alias='type') + object_type: Literal['polygon'] = pydantic_compat.Field(..., alias='type') geometry: TIPolygonCoordinate def to_common(self) -> Polygon: @@ -95,7 +95,7 @@ def to_common(self) -> Polygon: class LBV1TIRectangle(LBV1ObjectBase): - object_type: Literal['rectangle'] = Field(..., alias='type') + object_type: Literal['rectangle'] = pydantic_compat.Field(..., alias='type') geometry: TIRectangleCoordinate def to_common(self) -> Rectangle: @@ -111,12 +111,12 @@ def to_common(self) -> Rectangle: end=Point(x=end[0], y=end[1])) -class _Point(BaseModel): +class _Point(pydantic_compat.BaseModel): x: float y: float -class _Box(BaseModel): +class _Box(pydantic_compat.BaseModel): top: float left: float height: float @@ -230,12 +230,12 @@ def from_common(cls, mask: Mask, }) -class _TextPoint(BaseModel): +class _TextPoint(pydantic_compat.BaseModel): start: int end: int -class _Location(BaseModel): +class _Location(pydantic_compat.BaseModel): location: _TextPoint @@ -263,7 +263,7 @@ def from_common(cls, text_entity: TextEntity, **extra) -class LBV1Objects(BaseModel): +class LBV1Objects(pydantic_compat.BaseModel): objects: List[Union[ LBV1Line, LBV1Point, diff --git a/labelbox/data/serialization/ndjson/base.py b/labelbox/data/serialization/ndjson/base.py index 5de2bf252..2a9186e02 100644 --- a/labelbox/data/serialization/ndjson/base.py +++ b/labelbox/data/serialization/ndjson/base.py @@ -1,8 +1,8 @@ from typing import Optional from uuid import uuid4 -from pydantic import root_validator, validator from labelbox.utils import _CamelCaseMixin, is_exactly_one_set +from labelbox import pydantic_compat from ...annotation_types.types import Cuid @@ -10,7 +10,7 @@ class DataRow(_CamelCaseMixin): id: str = None global_key: str = None - @root_validator() + @pydantic_compat.root_validator() def must_set_one(cls, values): if not is_exactly_one_set(values.get('id'), values.get('global_key')): raise ValueError("Must set either id or global_key") @@ -21,7 +21,7 @@ class NDJsonBase(_CamelCaseMixin): uuid: str = None data_row: DataRow - @validator('uuid', pre=True, always=True) + @pydantic_compat.validator('uuid', pre=True, always=True) def set_id(cls, v): return v or str(uuid4()) @@ -42,7 +42,7 @@ class NDAnnotation(NDJsonBase): page: Optional[int] = None unit: Optional[str] = None - @root_validator() + @pydantic_compat.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): diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index 0a82e294f..028eeded8 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, root_validator +from labelbox import pydantic_compat from labelbox.data.mixins import ConfidenceMixin, CustomMetric, CustomMetricsMixin from labelbox.data.serialization.ndjson.base import DataRow, NDAnnotation @@ -17,7 +17,7 @@ class NDAnswer(ConfidenceMixin, CustomMetricsMixin): schema_id: Optional[Cuid] = None classifications: Optional[List['NDSubclassificationType']] = [] - @root_validator() + @pydantic_compat.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): @@ -43,12 +43,12 @@ class Config: alias_generator = camel_case -class FrameLocation(BaseModel): +class FrameLocation(pydantic_compat.BaseModel): end: int start: int -class VideoSupported(BaseModel): +class VideoSupported(pydantic_compat.BaseModel): # Note that frames are only allowed as top level inferences for video frames: Optional[List[FrameLocation]] = None @@ -81,7 +81,7 @@ def from_common(cls, text: Text, name: str, class NDChecklistSubclass(NDAnswer): - answer: List[NDAnswer] = Field(..., alias='answers') + answer: List[NDAnswer] = pydantic_compat.Field(..., alias='answers') def to_common(self) -> Checklist: diff --git a/labelbox/data/serialization/ndjson/label.py b/labelbox/data/serialization/ndjson/label.py index 4b8211d14..1b649a80e 100644 --- a/labelbox/data/serialization/ndjson/label.py +++ b/labelbox/data/serialization/ndjson/label.py @@ -4,7 +4,7 @@ from collections import defaultdict import warnings -from pydantic import BaseModel +from labelbox import pydantic_compat from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation from ...annotation_types.relationship import RelationshipAnnotation @@ -28,16 +28,16 @@ NDSegments, NDDicomMasks, NDVideoMasks, NDRelationship] -class NDLabel(BaseModel): +class NDLabel(pydantic_compat.BaseModel): annotations: List[AnnotationType] - class _Relationship(BaseModel): + class _Relationship(pydantic_compat.BaseModel): """This object holds information about the relationship""" ndjson: NDRelationship source: str target: str - class _AnnotationGroup(BaseModel): + class _AnnotationGroup(pydantic_compat.BaseModel): """Stores all the annotations and relationships per datarow""" data_row: DataRow = None ndjson_annotations: Dict[str, AnnotationType] = {} diff --git a/labelbox/data/serialization/ndjson/objects.py b/labelbox/data/serialization/ndjson/objects.py index 91822f2ab..fd13a6bf6 100644 --- a/labelbox/data/serialization/ndjson/objects.py +++ b/labelbox/data/serialization/ndjson/objects.py @@ -7,7 +7,7 @@ from labelbox.data.mixins import ConfidenceMixin, CustomMetricsMixin, CustomMetric, CustomMetricsNotSupportedMixin import numpy as np -from pydantic import BaseModel +from labelbox import pydantic_compat from PIL import Image from labelbox.data.annotation_types import feature @@ -27,21 +27,21 @@ class NDBaseObject(NDAnnotation): classifications: List[NDSubclassificationType] = [] -class VideoSupported(BaseModel): +class VideoSupported(pydantic_compat.BaseModel): # support for video for objects are per-frame basis frame: int -class DicomSupported(BaseModel): +class DicomSupported(pydantic_compat.BaseModel): group_key: str -class _Point(BaseModel): +class _Point(pydantic_compat.BaseModel): x: float y: float -class Bbox(BaseModel): +class Bbox(pydantic_compat.BaseModel): top: float left: float height: float @@ -328,7 +328,7 @@ def from_common( classifications=classifications) -class NDSegment(BaseModel): +class NDSegment(pydantic_compat.BaseModel): keyframes: List[Union[NDFrameRectangle, NDFramePoint, NDFrameLine]] @staticmethod @@ -454,12 +454,12 @@ def from_common(cls, segments: List[DICOMObjectAnnotation], data: VideoData, group_key=group_key) -class _URIMask(BaseModel): +class _URIMask(pydantic_compat.BaseModel): instanceURI: str colorRGB: Tuple[int, int, int] -class _PNGMask(BaseModel): +class _PNGMask(pydantic_compat.BaseModel): png: str @@ -512,7 +512,7 @@ def from_common( custom_metrics=custom_metrics) -class NDVideoMasksFramesInstances(BaseModel): +class NDVideoMasksFramesInstances(pydantic_compat.BaseModel): frames: List[MaskFrame] instances: List[MaskInstance] @@ -564,7 +564,7 @@ def from_common(cls, annotation, data): ) -class Location(BaseModel): +class Location(pydantic_compat.BaseModel): start: int end: int diff --git a/labelbox/data/serialization/ndjson/relationship.py b/labelbox/data/serialization/ndjson/relationship.py index d95c1584f..82976aedb 100644 --- a/labelbox/data/serialization/ndjson/relationship.py +++ b/labelbox/data/serialization/ndjson/relationship.py @@ -1,5 +1,5 @@ from typing import Union -from pydantic import BaseModel +from labelbox import pydantic_compat from .base import NDAnnotation, DataRow from ...annotation_types.data import ImageData, TextData from ...annotation_types.relationship import RelationshipAnnotation @@ -10,7 +10,7 @@ SUPPORTED_ANNOTATIONS = NDObjectType -class _Relationship(BaseModel): +class _Relationship(pydantic_compat.BaseModel): source: str target: str type: str diff --git a/labelbox/pydantic_compat.py b/labelbox/pydantic_compat.py index 77403117a..649ae91d6 100644 --- a/labelbox/pydantic_compat.py +++ b/labelbox/pydantic_compat.py @@ -12,10 +12,11 @@ def pydantic_import(class_name, sub_module_path: Optional[str] = None): if pydantic_version.startswith("1"): pydantic_v1_module_name = "pydantic" if sub_module_path is None else f"pydantic.{sub_module_path}" klass = getattr(importlib.import_module("pydantic"), class_name) - else: # use pydantic 2 v1 thunk + else: # use pydantic 2 v1 thunk pydantic_v1_module_name = "pydantic.v1" if sub_module_path is None else f"pydantic.{sub_module_path}" - klass = getattr(importlib.import_module(pydantic_v1_module_name), class_name) + klass = getattr(importlib.import_module(pydantic_v1_module_name), + class_name) return klass diff --git a/labelbox/schema/bulk_import_request.py b/labelbox/schema/bulk_import_request.py index 9b4ff85ef..65b71a310 100644 --- a/labelbox/schema/bulk_import_request.py +++ b/labelbox/schema/bulk_import_request.py @@ -5,11 +5,10 @@ import logging from pathlib import Path -import pydantic from google.api_core import retry from labelbox import parser import requests -from pydantic import BaseModel, root_validator, validator +from labelbox import pydantic_compat from typing_extensions import Literal from typing import (Any, List, Optional, BinaryIO, Dict, Iterable, Tuple, Union, Type, Set, TYPE_CHECKING) @@ -429,7 +428,8 @@ def _validate_ndjson(lines: Iterable[Dict[str, Any]], f'{uuid} already used in this import job, ' 'must be unique for the project.') uids.add(uuid) - except (pydantic.ValidationError, ValueError, TypeError, KeyError) as e: + except (pydantic_compat.ValidationError, ValueError, TypeError, + KeyError) as e: raise lb_exceptions.MALValidationError( f"Invalid NDJson on line {idx}") from e @@ -502,33 +502,33 @@ def get_mal_schemas(ontology): return valid_feature_schemas_by_schema_id, valid_feature_schemas_by_name -LabelboxID: str = pydantic.Field(..., min_length=25, max_length=25) +LabelboxID: str = pydantic_compat.Field(..., min_length=25, max_length=25) -class Bbox(BaseModel): +class Bbox(pydantic_compat.BaseModel): top: float left: float height: float width: float -class Point(BaseModel): +class Point(pydantic_compat.BaseModel): x: float y: float -class FrameLocation(BaseModel): +class FrameLocation(pydantic_compat.BaseModel): end: int start: int -class VideoSupported(BaseModel): +class VideoSupported(pydantic_compat.BaseModel): #Note that frames are only allowed as top level inferences for video frames: Optional[List[FrameLocation]] #Base class for a special kind of union. -# Compatible with pydantic. Improves error messages over a traditional union +# Compatible with pydantic_compat. Improves error messages over a traditional union class SpecialUnion: def __new__(cls, **kwargs): @@ -554,17 +554,18 @@ def get_union_types(cls): return union_types[0].__args__[0].__args__ @classmethod - def build(cls: Any, data: Union[dict, BaseModel]) -> "NDBase": + def build(cls: Any, data: Union[dict, + pydantic_compat.BaseModel]) -> "NDBase": """ Checks through all objects in the union to see which matches the input data. Args: - data (Union[dict, BaseModel]) : The data for constructing one of the objects in the union + data (Union[dict, pydantic_compat.BaseModel]) : The data for constructing one of the objects in the union raises: KeyError: data does not contain the determinant fields for any of the types supported by this SpecialUnion - ValidationError: Error while trying to construct a specific object in the union + pydantic_compat.ValidationError: Error while trying to construct a specific object in the union """ - if isinstance(data, BaseModel): + if isinstance(data, pydantic_compat.BaseModel): data = data.dict() top_level_fields = [] @@ -606,15 +607,15 @@ def schema(cls): return results -class DataRow(BaseModel): +class DataRow(pydantic_compat.BaseModel): id: str -class NDFeatureSchema(BaseModel): +class NDFeatureSchema(pydantic_compat.BaseModel): schemaId: Optional[str] = None name: Optional[str] = None - @root_validator + @pydantic_compat.root_validator def must_set_one(cls, values): if values['schemaId'] is None and values['name'] is None: raise ValueError( @@ -676,15 +677,15 @@ def determinants(parent_cls) -> List[str]: class NDText(NDBase): ontology_type: Literal["text"] = "text" - answer: str = pydantic.Field(determinant=True) + answer: str = pydantic_compat.Field(determinant=True) #No feature schema to check class NDChecklist(VideoSupported, NDBase): ontology_type: Literal["checklist"] = "checklist" - answers: List[NDFeatureSchema] = pydantic.Field(determinant=True) + answers: List[NDFeatureSchema] = pydantic_compat.Field(determinant=True) - @validator('answers', pre=True) + @pydantic_compat.validator('answers', pre=True) def validate_answers(cls, value, field): #constr not working with mypy. if not len(value): @@ -715,7 +716,7 @@ def validate_feature_schemas(self, valid_feature_schemas_by_id, class NDRadio(VideoSupported, NDBase): ontology_type: Literal["radio"] = "radio" - answer: NDFeatureSchema = pydantic.Field(determinant=True) + answer: NDFeatureSchema = pydantic_compat.Field(determinant=True) def validate_feature_schemas(self, valid_feature_schemas_by_id, valid_feature_schemas_by_name): @@ -761,7 +762,7 @@ def validate_feature_schemas(self, valid_feature_schemas_by_id, if self.name else valid_feature_schemas_by_id[ self.schemaId]['classificationsByName']) - @validator('classifications', pre=True) + @pydantic_compat.validator('classifications', pre=True) def validate_subclasses(cls, value, field): #Create uuid and datarow id so we don't have to define classification objects twice #This is caused by the fact that we require these ids for top level classifications but not for subclasses @@ -779,9 +780,9 @@ def validate_subclasses(cls, value, field): class NDPolygon(NDBaseTool): ontology_type: Literal["polygon"] = "polygon" - polygon: List[Point] = pydantic.Field(determinant=True) + polygon: List[Point] = pydantic_compat.Field(determinant=True) - @validator('polygon') + @pydantic_compat.validator('polygon') def is_geom_valid(cls, v): if len(v) < 3: raise ValueError( @@ -791,9 +792,9 @@ def is_geom_valid(cls, v): class NDPolyline(NDBaseTool): ontology_type: Literal["line"] = "line" - line: List[Point] = pydantic.Field(determinant=True) + line: List[Point] = pydantic_compat.Field(determinant=True) - @validator('line') + @pydantic_compat.validator('line') def is_geom_valid(cls, v): if len(v) < 2: raise ValueError( @@ -803,28 +804,28 @@ def is_geom_valid(cls, v): class NDRectangle(NDBaseTool): ontology_type: Literal["rectangle"] = "rectangle" - bbox: Bbox = pydantic.Field(determinant=True) + bbox: Bbox = pydantic_compat.Field(determinant=True) #Could check if points are positive class NDPoint(NDBaseTool): ontology_type: Literal["point"] = "point" - point: Point = pydantic.Field(determinant=True) + point: Point = pydantic_compat.Field(determinant=True) #Could check if points are positive -class EntityLocation(BaseModel): +class EntityLocation(pydantic_compat.BaseModel): start: int end: int class NDTextEntity(NDBaseTool): ontology_type: Literal["named-entity"] = "named-entity" - location: EntityLocation = pydantic.Field(determinant=True) + location: EntityLocation = pydantic_compat.Field(determinant=True) - @validator('location') + @pydantic_compat.validator('location') def is_valid_location(cls, v): - if isinstance(v, BaseModel): + if isinstance(v, pydantic_compat.BaseModel): v = v.dict() if len(v) < 2: @@ -839,11 +840,11 @@ def is_valid_location(cls, v): return v -class RLEMaskFeatures(BaseModel): +class RLEMaskFeatures(pydantic_compat.BaseModel): counts: List[int] size: List[int] - @validator('counts') + @pydantic_compat.validator('counts') def validate_counts(cls, counts): if not all([count >= 0 for count in counts]): raise ValueError( @@ -851,7 +852,7 @@ def validate_counts(cls, counts): ) return counts - @validator('size') + @pydantic_compat.validator('size') def validate_size(cls, size): if len(size) != 2: raise ValueError( @@ -863,16 +864,16 @@ def validate_size(cls, size): return size -class PNGMaskFeatures(BaseModel): +class PNGMaskFeatures(pydantic_compat.BaseModel): # base64 encoded png bytes png: str -class URIMaskFeatures(BaseModel): +class URIMaskFeatures(pydantic_compat.BaseModel): instanceURI: str colorRGB: Union[List[int], Tuple[int, int, int]] - @validator('colorRGB') + @pydantic_compat.validator('colorRGB') def validate_color(cls, colorRGB): #Does the dtype matter? Can it be a float? if not isinstance(colorRGB, (tuple, list)): @@ -892,7 +893,7 @@ def validate_color(cls, colorRGB): class NDMask(NDBaseTool): ontology_type: Literal["superpixel"] = "superpixel" mask: Union[URIMaskFeatures, PNGMaskFeatures, - RLEMaskFeatures] = pydantic.Field(determinant=True) + RLEMaskFeatures] = pydantic_compat.Field(determinant=True) #A union with custom construction logic to improve error messages diff --git a/labelbox/schema/data_row_metadata.py b/labelbox/schema/data_row_metadata.py index 90b043ee9..3a6b58706 100644 --- a/labelbox/schema/data_row_metadata.py +++ b/labelbox/schema/data_row_metadata.py @@ -7,7 +7,7 @@ from typing import List, Optional, Dict, Union, Callable, Type, Any, Generator, overload -from pydantic import BaseModel, conlist, constr +from labelbox import pydantic_compat from labelbox.schema.identifiables import DataRowIdentifiers, UniqueIds from labelbox.schema.identifiable import UniqueId, GlobalKey @@ -25,9 +25,11 @@ class DataRowMetadataKind(Enum): # Metadata schema -class DataRowMetadataSchema(BaseModel): +class DataRowMetadataSchema(pydantic_compat.BaseModel): uid: SchemaId - name: constr(strip_whitespace=True, min_length=1, max_length=100) + name: pydantic_compat.constr(strip_whitespace=True, + min_length=1, + max_length=100) reserved: bool kind: DataRowMetadataKind options: Optional[List["DataRowMetadataSchema"]] @@ -36,8 +38,10 @@ class DataRowMetadataSchema(BaseModel): DataRowMetadataSchema.update_forward_refs() -Embedding: Type[List[float]] = conlist(float, min_items=128, max_items=128) -String: Type[str] = constr(max_length=4096) +Embedding: Type[List[float]] = pydantic_compat.conlist(float, + min_items=128, + max_items=128) +String: Type[str] = pydantic_compat.constr(max_length=4096) # Metadata base class @@ -120,13 +124,17 @@ def dict(self, *args, **kwargs): class _UpsertCustomMetadataSchemaEnumOptionInput(_CamelCaseMixin): id: Optional[SchemaId] - name: constr(strip_whitespace=True, min_length=1, max_length=100) + name: pydantic_compat.constr(strip_whitespace=True, + min_length=1, + max_length=100) kind: str class _UpsertCustomMetadataSchemaInput(_CamelCaseMixin): id: Optional[SchemaId] - name: constr(strip_whitespace=True, min_length=1, max_length=100) + name: pydantic_compat.constr(strip_whitespace=True, + min_length=1, + max_length=100) kind: str options: Optional[List[_UpsertCustomMetadataSchemaEnumOptionInput]] diff --git a/labelbox/schema/export_task.py b/labelbox/schema/export_task.py index 82303726e..3ec94bd2a 100644 --- a/labelbox/schema/export_task.py +++ b/labelbox/schema/export_task.py @@ -19,7 +19,7 @@ ) import requests -from pydantic import BaseModel +from labelbox import pydantic_compat from labelbox.schema.task import Task from labelbox.utils import _CamelCaseMixin @@ -37,19 +37,19 @@ class StreamType(Enum): ERRORS = "ERRORS" -class Range(_CamelCaseMixin, BaseModel): # pylint: disable=too-few-public-methods +class Range(_CamelCaseMixin, pydantic_compat.BaseModel): # pylint: disable=too-few-public-methods """Represents a range.""" start: int end: int -class _MetadataHeader(_CamelCaseMixin, BaseModel): # pylint: disable=too-few-public-methods +class _MetadataHeader(_CamelCaseMixin, pydantic_compat.BaseModel): # pylint: disable=too-few-public-methods total_size: int total_lines: int -class _MetadataFileInfo(_CamelCaseMixin, BaseModel): # pylint: disable=too-few-public-methods +class _MetadataFileInfo(_CamelCaseMixin, pydantic_compat.BaseModel): # pylint: disable=too-few-public-methods offsets: Range lines: Range file: str diff --git a/labelbox/schema/foundry/app.py b/labelbox/schema/foundry/app.py index b31709f80..eead39518 100644 --- a/labelbox/schema/foundry/app.py +++ b/labelbox/schema/foundry/app.py @@ -1,11 +1,11 @@ from labelbox.utils import _CamelCaseMixin -from pydantic import BaseModel +from labelbox import pydantic_compat from typing import Any, Dict, Optional -class App(_CamelCaseMixin, BaseModel): +class App(_CamelCaseMixin, pydantic_compat.BaseModel): id: Optional[str] model_id: str name: str diff --git a/labelbox/schema/foundry/model.py b/labelbox/schema/foundry/model.py index 3e7ebd6e7..16ccae422 100644 --- a/labelbox/schema/foundry/model.py +++ b/labelbox/schema/foundry/model.py @@ -1,12 +1,12 @@ from labelbox.utils import _CamelCaseMixin -from pydantic import BaseModel +from labelbox import pydantic_compat from datetime import datetime from typing import Dict -class Model(_CamelCaseMixin, BaseModel): +class Model(_CamelCaseMixin, pydantic_compat.BaseModel): id: str description: str inference_params_json_schema: Dict diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index 08d3e62a8..66d1463df 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -6,15 +6,15 @@ from typing import Any, Dict, List, Optional, Union, Type import warnings -from pydantic import constr - from labelbox.exceptions import InconsistentOntologyException from labelbox.orm.db_object import DbObject from labelbox.orm.model import Field, Relationship +from labelbox import pydantic_compat import json -FeatureSchemaId: Type[str] = constr(min_length=25, max_length=25) -SchemaId: Type[str] = constr(min_length=25, max_length=25) +FeatureSchemaId: Type[str] = pydantic_compat.constr(min_length=25, + max_length=25) +SchemaId: Type[str] = pydantic_compat.constr(min_length=25, max_length=25) class DeleteFeatureFromOntologyResult: diff --git a/labelbox/utils.py b/labelbox/utils.py index bfe39a3e2..da2dbdec4 100644 --- a/labelbox/utils.py +++ b/labelbox/utils.py @@ -6,7 +6,7 @@ from dateutil.utils import default_tzinfo from urllib.parse import urlparse -from pydantic import BaseModel +from labelbox import pydantic_compat UPPERCASE_COMPONENTS = ['uri', 'rgb'] ISO_DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' @@ -51,7 +51,7 @@ def is_valid_uri(uri): return False -class _CamelCaseMixin(BaseModel): +class _CamelCaseMixin(pydantic_compat.BaseModel): class Config: allow_population_by_field_name = True diff --git a/tests/data/annotation_types/classification/test_classification.py b/tests/data/annotation_types/classification/test_classification.py index 0ad4329cc..11a3a0514 100644 --- a/tests/data/annotation_types/classification/test_classification.py +++ b/tests/data/annotation_types/classification/test_classification.py @@ -1,13 +1,14 @@ import pytest -from pydantic import ValidationError from labelbox.data.annotation_types import (Checklist, ClassificationAnswer, Dropdown, Radio, Text, ClassificationAnnotation) +from labelbox import pydantic_compat + def test_classification_answer(): - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): ClassificationAnswer() feature_schema_id = "schema_id" @@ -36,7 +37,7 @@ def test_classification(): name="a classification") assert classification.dict()['value']['answer'] == answer - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): ClassificationAnnotation() @@ -44,7 +45,7 @@ def test_subclass(): answer = "1234" feature_schema_id = "11232" name = "my_feature" - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): # Should have feature schema info classification = ClassificationAnnotation(value=Text(answer=answer)) classification = ClassificationAnnotation(value=Text(answer=answer), @@ -97,11 +98,11 @@ def test_radio(): feature_schema_id = "feature_schema_id" name = "my_feature" - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): classification = ClassificationAnnotation(value=Radio( answer=answer.name)) - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): classification = Radio(answer=[answer]) classification = Radio(answer=answer,) assert classification.dict() == { @@ -158,10 +159,10 @@ def test_checklist(): feature_schema_id = "feature_schema_id" name = "my_feature" - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): classification = Checklist(answer=answer.name) - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): classification = Checklist(answer=answer) classification = Checklist(answer=[answer]) @@ -207,11 +208,11 @@ def test_dropdown(): feature_schema_id = "feature_schema_id" name = "my_feature" - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): classification = ClassificationAnnotation( value=Dropdown(answer=answer.name), name="test") - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): classification = Dropdown(answer=answer) classification = Dropdown(answer=[answer]) assert classification.dict() == { diff --git a/tests/data/annotation_types/data/test_raster.py b/tests/data/annotation_types/data/test_raster.py index e99a621a5..40c8d5648 100644 --- a/tests/data/annotation_types/data/test_raster.py +++ b/tests/data/annotation_types/data/test_raster.py @@ -4,13 +4,13 @@ import numpy as np import pytest from PIL import Image -from pydantic import ValidationError from labelbox.data.annotation_types.data import ImageData +from labelbox import pydantic_compat def test_validate_schema(): - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): data = ImageData() diff --git a/tests/data/annotation_types/data/test_text.py b/tests/data/annotation_types/data/test_text.py index 641bdd764..970b8382b 100644 --- a/tests/data/annotation_types/data/test_text.py +++ b/tests/data/annotation_types/data/test_text.py @@ -1,13 +1,13 @@ import os import pytest -from pydantic import ValidationError from labelbox.data.annotation_types import TextData +from labelbox import pydantic_compat def test_validate_schema(): - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): data = TextData() diff --git a/tests/data/annotation_types/data/test_video.py b/tests/data/annotation_types/data/test_video.py index 9622be319..f0e42b83f 100644 --- a/tests/data/annotation_types/data/test_video.py +++ b/tests/data/annotation_types/data/test_video.py @@ -1,12 +1,12 @@ import numpy as np import pytest -from pydantic import ValidationError from labelbox.data.annotation_types import VideoData +from labelbox import pydantic_compat def test_validate_schema(): - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): data = VideoData() diff --git a/tests/data/annotation_types/geometry/test_line.py b/tests/data/annotation_types/geometry/test_line.py index 83d2c47fe..f0d0673df 100644 --- a/tests/data/annotation_types/geometry/test_line.py +++ b/tests/data/annotation_types/geometry/test_line.py @@ -1,15 +1,15 @@ -from pydantic import ValidationError import pytest import cv2 from labelbox.data.annotation_types.geometry import Point, Line +from labelbox import pydantic_compat def test_line(): - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): line = Line() - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): line = Line(points=[[0, 1], [2, 3]]) points = [[0, 1], [0, 2], [2, 2]] diff --git a/tests/data/annotation_types/geometry/test_mask.py b/tests/data/annotation_types/geometry/test_mask.py index 1d016f438..7a2b713ee 100644 --- a/tests/data/annotation_types/geometry/test_mask.py +++ b/tests/data/annotation_types/geometry/test_mask.py @@ -1,14 +1,14 @@ -from pydantic import ValidationError import pytest import numpy as np import cv2 from labelbox.data.annotation_types import Point, Rectangle, Mask, MaskData +from labelbox import pydantic_compat def test_mask(): - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): mask = Mask() mask_data = np.zeros((32, 32, 3), dtype=np.uint8) diff --git a/tests/data/annotation_types/geometry/test_point.py b/tests/data/annotation_types/geometry/test_point.py index 1e4f8557f..47c152d2b 100644 --- a/tests/data/annotation_types/geometry/test_point.py +++ b/tests/data/annotation_types/geometry/test_point.py @@ -1,12 +1,12 @@ -from pydantic import ValidationError import pytest import cv2 from labelbox.data.annotation_types import Point +from labelbox import pydantic_compat def test_point(): - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): line = Point() with pytest.raises(TypeError): diff --git a/tests/data/annotation_types/geometry/test_polygon.py b/tests/data/annotation_types/geometry/test_polygon.py index 9c144143e..8a7525e8f 100644 --- a/tests/data/annotation_types/geometry/test_polygon.py +++ b/tests/data/annotation_types/geometry/test_polygon.py @@ -1,18 +1,18 @@ -from pydantic import ValidationError import pytest import cv2 from labelbox.data.annotation_types import Polygon, Point +from labelbox import pydantic_compat def test_polygon(): - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): polygon = Polygon() - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): polygon = Polygon(points=[[0, 1], [2, 3]]) - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): polygon = Polygon(points=[Point(x=0, y=1), Point(x=0, y=1)]) points = [[0., 1.], [0., 2.], [2., 2.], [2., 0.]] diff --git a/tests/data/annotation_types/geometry/test_rectangle.py b/tests/data/annotation_types/geometry/test_rectangle.py index d8586aeb7..3c01ef6ed 100644 --- a/tests/data/annotation_types/geometry/test_rectangle.py +++ b/tests/data/annotation_types/geometry/test_rectangle.py @@ -1,12 +1,12 @@ import cv2 import pytest -from pydantic import ValidationError from labelbox.data.annotation_types import Point, Rectangle +from labelbox import pydantic_compat def test_rectangle(): - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): rectangle = Rectangle() rectangle = Rectangle(start=Point(x=0, y=1), end=Point(x=10, y=10)) diff --git a/tests/data/annotation_types/test_annotation.py b/tests/data/annotation_types/test_annotation.py index faf65bef7..b6dc00041 100644 --- a/tests/data/annotation_types/test_annotation.py +++ b/tests/data/annotation_types/test_annotation.py @@ -1,5 +1,4 @@ import pytest -from pydantic import ValidationError from labelbox.data.annotation_types import (Text, Point, Line, ClassificationAnnotation, @@ -8,6 +7,7 @@ from labelbox.data.annotation_types.geometry.rectangle import Rectangle from labelbox.data.annotation_types.video import VideoClassificationAnnotation from labelbox.exceptions import ConfidenceNotSupportedException +from labelbox import pydantic_compat def test_annotation(): @@ -35,7 +35,7 @@ def test_annotation(): ) # Invalid subclass - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): ObjectAnnotation( value=line, name=name, @@ -56,11 +56,11 @@ def test_video_annotations(): line = Line(points=[Point(x=1, y=2), Point(x=2, y=2)]) # Wrong type - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): VideoClassificationAnnotation(value=line, name=name, frame=1) # Missing frames - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): VideoClassificationAnnotation(value=line, name=name) VideoObjectAnnotation(value=line, name=name, keyframe=True, frame=2) diff --git a/tests/data/annotation_types/test_metrics.py b/tests/data/annotation_types/test_metrics.py index 3bc447309..c68324842 100644 --- a/tests/data/annotation_types/test_metrics.py +++ b/tests/data/annotation_types/test_metrics.py @@ -1,10 +1,10 @@ -from pydantic import ValidationError import pytest from labelbox.data.annotation_types.metrics import ConfusionMatrixAggregation, ScalarMetricAggregation from labelbox.data.annotation_types.metrics import ConfusionMatrixMetric, ScalarMetric from labelbox.data.annotation_types import ScalarMetric, Label, ImageData from labelbox.data.annotation_types.metrics.scalar import RESERVED_METRIC_NAMES +from labelbox import pydantic_compat def test_legacy_scalar_metric(): @@ -156,19 +156,19 @@ def test_custom_confusison_matrix_metric(feature_name, subclass_name, def test_name_exists(): # Name is only required for ConfusionMatrixMetric for now. - with pytest.raises(ValidationError) as exc_info: + with pytest.raises(pydantic_compat.ValidationError) as exc_info: metric = ConfusionMatrixMetric(value=[0, 1, 2, 3]) assert "field required (type=value_error.missing)" in str(exc_info.value) def test_invalid_aggregations(): - with pytest.raises(ValidationError) as exc_info: + with pytest.raises(pydantic_compat.ValidationError) as exc_info: metric = ScalarMetric( metric_name="invalid aggregation", value=0.1, aggregation=ConfusionMatrixAggregation.CONFUSION_MATRIX) assert "value is not a valid enumeration member" in str(exc_info.value) - with pytest.raises(ValidationError) as exc_info: + with pytest.raises(pydantic_compat.ValidationError) as exc_info: metric = ConfusionMatrixMetric(metric_name="invalid aggregation", value=[0, 1, 2, 3], aggregation=ScalarMetricAggregation.SUM) @@ -176,18 +176,18 @@ def test_invalid_aggregations(): def test_invalid_number_of_confidence_scores(): - with pytest.raises(ValidationError) as exc_info: + with pytest.raises(pydantic_compat.ValidationError) as exc_info: metric = ScalarMetric(metric_name="too few scores", value={0.1: 0.1}) assert "Number of confidence scores must be greater" in str(exc_info.value) - with pytest.raises(ValidationError) as exc_info: + with pytest.raises(pydantic_compat.ValidationError) as exc_info: metric = ConfusionMatrixMetric(metric_name="too few scores", value={0.1: [0, 1, 2, 3]}) assert "Number of confidence scores must be greater" in str(exc_info.value) - with pytest.raises(ValidationError) as exc_info: + with pytest.raises(pydantic_compat.ValidationError) as exc_info: metric = ScalarMetric(metric_name="too many scores", value={i / 20.: 0.1 for i in range(20)}) assert "Number of confidence scores must be greater" in str(exc_info.value) - with pytest.raises(ValidationError) as exc_info: + with pytest.raises(pydantic_compat.ValidationError) as exc_info: metric = ConfusionMatrixMetric( metric_name="too many scores", value={i / 20.: [0, 1, 2, 3] for i in range(20)}) @@ -196,6 +196,6 @@ def test_invalid_number_of_confidence_scores(): @pytest.mark.parametrize("metric_name", RESERVED_METRIC_NAMES) def test_reserved_names(metric_name: str): - with pytest.raises(ValidationError) as exc_info: + with pytest.raises(pydantic_compat.ValidationError) as exc_info: ScalarMetric(metric_name=metric_name, value=0.5) assert 'is a reserved metric name' in exc_info.value.errors()[0]['msg'] diff --git a/tests/data/annotation_types/test_tiled_image.py b/tests/data/annotation_types/test_tiled_image.py index aea6587f6..cd96fee6d 100644 --- a/tests/data/annotation_types/test_tiled_image.py +++ b/tests/data/annotation_types/test_tiled_image.py @@ -7,7 +7,7 @@ TileLayer, TiledImageData, EPSGTransformer) -from pydantic import ValidationError +from labelbox import pydantic_compat @pytest.mark.parametrize("epsg", list(EPSG)) @@ -28,7 +28,7 @@ def test_tiled_bounds(epsg): @pytest.mark.parametrize("epsg", list(EPSG)) def test_tiled_bounds_same(epsg): single_bound = Point(x=0, y=0) - with pytest.raises(ValidationError): + with pytest.raises(pydantic_compat.ValidationError): tiled_bounds = TiledBounds(epsg=epsg, bounds=[single_bound, single_bound]) From 779f692cbdf8c2af6722903ea5557a6659635690 Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Fri, 9 Feb 2024 11:12:42 -0800 Subject: [PATCH 3/5] Fix pydantic_compat --- .../data/annotation_types/classification/classification.py | 2 +- labelbox/data/annotation_types/label.py | 4 ++-- labelbox/data/annotation_types/video.py | 6 +++--- labelbox/pydantic_compat.py | 1 - 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/labelbox/data/annotation_types/classification/classification.py b/labelbox/data/annotation_types/classification/classification.py index 68ea7e0e7..9a1867ff2 100644 --- a/labelbox/data/annotation_types/classification/classification.py +++ b/labelbox/data/annotation_types/classification/classification.py @@ -9,7 +9,7 @@ except: from typing_extensions import Literal -from labelbox import pydantic_compat, validator +from labelbox import pydantic_compat from ..feature import FeatureSchema diff --git a/labelbox/data/annotation_types/label.py b/labelbox/data/annotation_types/label.py index 1043cff7a..16b2214be 100644 --- a/labelbox/data/annotation_types/label.py +++ b/labelbox/data/annotation_types/label.py @@ -2,7 +2,7 @@ from typing import Any, Callable, Dict, List, Union, Optional import warnings -from labelbox import pydantic_compat, validator +from labelbox import pydantic_compat import labelbox from labelbox.data.annotation_types.data.tiled_image import TiledImageData @@ -189,7 +189,7 @@ def _assign_option(self, classification: ClassificationAnnotation, f"Unexpected type for answer found. {type(classification.value.answer)}" ) - @validator("annotations", pre=True) + @pydantic_compat.validator("annotations", pre=True) def validate_union(cls, value): supported = tuple([ field.type_ diff --git a/labelbox/data/annotation_types/video.py b/labelbox/data/annotation_types/video.py index 83d79c1c9..91b258de3 100644 --- a/labelbox/data/annotation_types/video.py +++ b/labelbox/data/annotation_types/video.py @@ -1,7 +1,7 @@ from enum import Enum from typing import List, Optional, Tuple -from labelbox import pydantic_compat, validator, root_validator +from labelbox import pydantic_compat from labelbox.data.annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation from labelbox.data.annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation @@ -92,7 +92,7 @@ class MaskFrame(_CamelCaseMixin, pydantic_compat.BaseModel): instance_uri: Optional[str] = None im_bytes: Optional[bytes] = None - @root_validator() + @pydantic_compat.root_validator() def validate_args(cls, values): im_bytes = values.get("im_bytes") instance_uri = values.get("instance_uri") @@ -101,7 +101,7 @@ def validate_args(cls, values): raise ValueError("One of `instance_uri`, `im_bytes` required.") return values - @validator("instance_uri") + @pydantic_compat.validator("instance_uri") def validate_uri(cls, v): if not is_valid_uri(v): raise ValueError(f"{v} is not a valid uri") diff --git a/labelbox/pydantic_compat.py b/labelbox/pydantic_compat.py index 649ae91d6..58aaf8458 100644 --- a/labelbox/pydantic_compat.py +++ b/labelbox/pydantic_compat.py @@ -11,7 +11,6 @@ def pydantic_import(class_name, sub_module_path: Optional[str] = None): # Check if the version is 1 if pydantic_version.startswith("1"): pydantic_v1_module_name = "pydantic" if sub_module_path is None else f"pydantic.{sub_module_path}" - klass = getattr(importlib.import_module("pydantic"), class_name) else: # use pydantic 2 v1 thunk pydantic_v1_module_name = "pydantic.v1" if sub_module_path is None else f"pydantic.{sub_module_path}" From f592269db695c83d8b6adc3edb633670db0312ed Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Mon, 12 Feb 2024 17:34:57 -0800 Subject: [PATCH 4/5] Remove restriction on pydantic2 --- requirements.txt | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 105abe1f4..6aec8d2d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ nbconvert~=7.2.6 nbformat~=5.7.0 opencv-python PILLOW -pydantic>=1.8,<2.0 +pydantic>=1.8 pygeotile pyproj pytest-xdist diff --git a/setup.py b/setup.py index a4407085a..557a3b8b2 100644 --- a/setup.py +++ b/setup.py @@ -20,8 +20,8 @@ url="https://labelbox.com", packages=setuptools.find_packages(), install_requires=[ - "requests>=2.22.0", "google-api-core>=1.22.1", "pydantic>=1.8,<2.0", - "tqdm", "python-dateutil>=2.8.2,<2.9.0" + "requests>=2.22.0", "google-api-core>=1.22.1", "pydantic>=1.8", "tqdm", + "python-dateutil>=2.8.2,<2.9.0" ], extras_require={ 'data': [ From ce3ef4c8c4f3415875512af9ba650d5ad6894305 Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Tue, 13 Feb 2024 13:11:21 -0800 Subject: [PATCH 5/5] PR feedback: refactor / DRY pydantic_compat logic --- labelbox/pydantic_compat.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/labelbox/pydantic_compat.py b/labelbox/pydantic_compat.py index 58aaf8458..b908de309 100644 --- a/labelbox/pydantic_compat.py +++ b/labelbox/pydantic_compat.py @@ -8,14 +8,13 @@ def pydantic_import(class_name, sub_module_path: Optional[str] = None): # Get the version of pydantic pydantic_version = pkg_resources.get_distribution("pydantic").version - # Check if the version is 1 - if pydantic_version.startswith("1"): - pydantic_v1_module_name = "pydantic" if sub_module_path is None else f"pydantic.{sub_module_path}" - else: # use pydantic 2 v1 thunk - pydantic_v1_module_name = "pydantic.v1" if sub_module_path is None else f"pydantic.{sub_module_path}" - - klass = getattr(importlib.import_module(pydantic_v1_module_name), - class_name) + # Determine the module name based on the version + module_name = "pydantic" if pydantic_version.startswith( + "1") else "pydantic.v1" + module_name = f"{module_name}.{sub_module_path}" if sub_module_path else module_name + + # Import the class from the module + klass = getattr(importlib.import_module(module_name), class_name) return klass