diff --git a/src/superannotate_schemas/__init__.py b/src/superannotate_schemas/__init__.py index 8053363..1b08015 100644 --- a/src/superannotate_schemas/__init__.py +++ b/src/superannotate_schemas/__init__.py @@ -6,7 +6,7 @@ from superannotate_schemas.validators import AnnotationValidators -__version__ = '1.0.41' +__version__ = '1.0.42' __all__ = [ "__version__", diff --git a/src/superannotate_schemas/schemas/base.py b/src/superannotate_schemas/schemas/base.py index ee2cec7..288af07 100644 --- a/src/superannotate_schemas/schemas/base.py +++ b/src/superannotate_schemas/schemas/base.py @@ -1,5 +1,7 @@ +import datetime from typing import Dict from typing import List +from math import isnan from typing import Optional from typing import Union @@ -7,8 +9,8 @@ from pydantic import conlist from pydantic import constr from pydantic import Extra -from pydantic import StrictInt from pydantic import StrictFloat +from pydantic import StrictInt from pydantic import StrictStr from pydantic import StrictBool from pydantic import Field @@ -18,6 +20,9 @@ from pydantic.errors import EnumMemberError from pydantic import validator from pydantic.validators import strict_str_validator +from pydantic.validators import strict_float_validator +from pydantic.validators import strict_int_validator +from pydantic.validators import number_multiple_validator from pydantic.color import Color from pydantic.color import ColorType @@ -59,6 +64,7 @@ def validate(cls, value: Union[str]) -> Union[str]: class BaseModel(PyDanticBaseModel): + class Config: extra = Extra.allow use_enum_values = True @@ -69,6 +75,45 @@ class Config: } +class StrictPointNumber(BaseModel): + __root__: Union[StrictInt, StrictFloat] + + @classmethod + def __get_validators__(cls): + yield cls._validate_types + + @classmethod + def _validate_types(cls, value): + is_valid_float, is_valid_int = True, True + try: + cls._validate_float(value) + except TypeError: + is_valid_float = False + if not is_valid_float: + try: + cls._validate_int(value) + except TypeError: + is_valid_int = False + if is_valid_float or is_valid_int: + return value + raise TypeError("is not a valid number. Integer or float types are expected") + + @classmethod + def _validate_int(cls, value): + return strict_int_validator(value) + + @classmethod + def _validate_float(cls, value): + strict_float_validator(value) + return cls._validate_nan(value) + + @staticmethod + def _validate_nan(v): + if isnan(v): + raise TypeError("NaN is not a valid float") + return v + + class AxisPoint(BaseModel): x: StrictNumber y: StrictNumber @@ -96,14 +141,17 @@ class TimedBaseModel(BaseModel): created_at: Optional[constr(regex=DATE_REGEX)] = Field(None, alias="createdAt") updated_at: Optional[constr(regex=DATE_REGEX)] = Field(None, alias="updatedAt") - @validator("created_at", "updated_at", pre=True) + @validator("created_at", "updated_at", pre=True, always=True) def validate_created_at(cls, value): - try: - if value is not None: - constr(regex=DATE_REGEX, strict=True).validate(value) - except (TypeError, StrRegexError): - raise TypeError(DATE_TIME_FORMAT_ERROR_MESSAGE) - return value + if value: + try: + if value is not None: + constr(regex=DATE_REGEX, strict=True).validate(value) + except (TypeError, StrRegexError): + raise TypeError(DATE_TIME_FORMAT_ERROR_MESSAGE) + return value + else: + return datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" class UserAction(BaseModel): diff --git a/src/superannotate_schemas/schemas/external/pixel.py b/src/superannotate_schemas/schemas/external/pixel.py index 23de3a1..7d0ef06 100644 --- a/src/superannotate_schemas/schemas/external/pixel.py +++ b/src/superannotate_schemas/schemas/external/pixel.py @@ -5,7 +5,6 @@ from superannotate_schemas.schemas.base import BaseAttribute from superannotate_schemas.schemas.base import BaseImageMetadata from superannotate_schemas.schemas.base import NotEmptyStr -from superannotate_schemas.schemas.base import StrictStr from superannotate_schemas.schemas.base import HexColor from superannotate_schemas.schemas.base import Tag from superannotate_schemas.schemas.base import Comment diff --git a/src/superannotate_schemas/schemas/external/vector.py b/src/superannotate_schemas/schemas/external/vector.py index fcbcc36..59734e5 100644 --- a/src/superannotate_schemas/schemas/external/vector.py +++ b/src/superannotate_schemas/schemas/external/vector.py @@ -3,13 +3,13 @@ from typing import Union from pydantic import Field -from pydantic import StrictFloat from pydantic import StrictInt from pydantic import StrictStr from pydantic import ValidationError from pydantic import conlist from pydantic.error_wrappers import ErrorWrapper +from superannotate_schemas.schemas.base import StrictPointNumber from superannotate_schemas.schemas.base import AxisPoint from superannotate_schemas.schemas.base import BaseAttribute from superannotate_schemas.schemas.base import BaseImageMetadata @@ -47,12 +47,12 @@ class Point(VectorInstance, AxisPoint): class PolyLine(VectorInstance): - points: List[Union[StrictFloat, StrictInt]] + points: List[StrictPointNumber] class Polygon(VectorInstance): - points: conlist(Union[StrictFloat, StrictInt], min_items=3) - exclude: Optional[List[List[Union[StrictFloat, StrictInt]]]] = [] + points: conlist(StrictPointNumber, min_items=3) + exclude: Optional[List[List[StrictPointNumber]]] = [] class Bbox(VectorInstance): diff --git a/src/superannotate_schemas/schemas/internal/vector.py b/src/superannotate_schemas/schemas/internal/vector.py index 59de7bd..c81d75c 100644 --- a/src/superannotate_schemas/schemas/internal/vector.py +++ b/src/superannotate_schemas/schemas/internal/vector.py @@ -18,7 +18,7 @@ from superannotate_schemas.schemas.base import NotEmptyStr from pydantic import StrictInt -from pydantic import StrictFloat +from superannotate_schemas.schemas.base import StrictFloat from pydantic import conlist from pydantic import Field from pydantic import validate_model diff --git a/src/superannotate_schemas/schemas/internal/video.py b/src/superannotate_schemas/schemas/internal/video.py index b907073..0435b81 100644 --- a/src/superannotate_schemas/schemas/internal/video.py +++ b/src/superannotate_schemas/schemas/internal/video.py @@ -13,7 +13,7 @@ from superannotate_schemas.schemas.base import AnnotationStatusEnum from superannotate_schemas.schemas.base import BaseModel -from pydantic import StrictFloat +from superannotate_schemas.schemas.base import StrictFloat from pydantic import constr from pydantic import Field from pydantic import StrictBool diff --git a/tests/test_multi_instance_list.py b/tests/test_multi_instance_list.py deleted file mode 100644 index ca220fa..0000000 --- a/tests/test_multi_instance_list.py +++ /dev/null @@ -1,11 +0,0 @@ -from unittest import TestCase -from pydantic import BaseModel -from pydantic import Field -from typing import List -from typing import Literal -from typing import Optional -from typing import Union -from pydantic import StrictStr -from pydantic import StrictInt -from pydantic.error_wrappers import ValidationError -import pytest diff --git a/tests/test_validators.py b/tests/test_validators.py index e583f55..9022591 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -5,6 +5,7 @@ from superannotate_schemas.schemas.classes import AnnotationClass from superannotate_schemas.validators import AnnotationValidators +from superannotate_schemas.schemas.external.vector import Polygon class TestSchemas(TestCase): @@ -974,162 +975,6 @@ def test_validate_video_invalid_instance_without_type_and_attr_annotation(self, self.assertEqual(validator.generate_report(), "instances[2].meta.type field required") - @patch('builtins.print') - def test_validate_vector_template_polygon_polyline_min_annotation(self, mock_print): - json_name = "test.json" - with tempfile.TemporaryDirectory() as tmpdir_name: - with open(f"{tmpdir_name}/{json_name}", "w") as json_file: - json_file.write( - ''' - { - "metadata": { - "lastAction": { - "email": "some@some.com", - "timestamp": 1636964198056 - }, - "width": "1234", - "height": 1540, - "name": "t.png", - "projectId": 164988, - "isPredicted": false, - "status": "Completed", - "pinned": false, - "annotatorEmail": null, - "qaEmail": null - }, - "comments": [], - "tags": [], - "instances": [ - { - "type": "template", - "classId": 880080, - "probability": 100, - "points": [ - ], - "connections": [ - { - "id": 1, - "from": 1, - "to": 2 - } - ], - "groupId": 0, - "pointLabels": {}, - "locked": false, - "visible": true, - "attributes": [], - "templateId": 4728, - "trackingId": null, - "error": null, - "createdAt": "2021-11-15T08:24:40.712Z", - "createdBy": { - "email": "shab.prog@gmail.com", - "role": "Admin" - }, - "creationType": "Manual", - "updatedAt": "2021-11-15T08:24:46.440Z", - "updatedBy": { - "email": "shab.prog@gmail.com", - "role": "Admin" - }, - "className": "kj", - "templateName": "templ1" - }, - { - "type": "polygon", - "classId": 880080, - "probability": 100, - "points": [ - 233.69 - ], - "groupId": 0, - "pointLabels": {}, - "locked": true, - "visible": true, - "attributes": [], - "trackingId": null, - "error": null, - "createdAt": "2021-11-15T08:18:16.103Z", - "createdBy": { - "email": "some@some.com", - "role": "Admin" - }, - "creationType": "Manual", - "updatedAt": "2021-11-15T08:18:20.233Z", - "updatedBy": { - "email": "some@some.com", - "role": "Admin" - }, - "className": "kj" - }, - { - "type": "polyline", - "classId": 880080, - "probability": 100, - "points": [ - 218.22 - ], - "groupId": 0, - "pointLabels": {}, - "locked": false, - "visible": true, - "attributes": [], - "trackingId": null, - "error": null, - "createdAt": "2021-11-15T08:18:06.203Z", - "createdBy": { - "email": "some@some.com", - "role": "Admin" - }, - "creationType": "Manual", - "updatedAt": "2021-11-15T08:18:13.439Z", - "updatedBy": { - "email": "some@some.com", - "role": "Admin" - }, - "className": "kj" - }, - { - "type": "bbox", - "classId": 880080, - "probability": 100, - "points": { - "x1": 487.78, - "x2": 1190.87, - "y1": 863.91, - "y2": 1463.78 - }, - "groupId": 0, - "pointLabels": {}, - "locked": false, - "visible": true, - "attributes": [], - "trackingId": null, - "error": null, - "createdAt": "2021-11-15T06:43:09.812Z", - "createdBy": { - "email": "some@some.com", - "role": "Admin" - }, - "creationType": "Manual", - "updatedAt": "2021-11-15T08:16:48.807Z", - "updatedBy": { - "email": "some@some.com", - "role": "Admin" - }, - "className": "kj" - } - ] - } - ''' - ) - - with open(f"{tmpdir_name}/{json_name}", "r") as f: - data = json.loads(f.read()) - validator = AnnotationValidators.get_validator("vector")(data) - self.assertFalse(validator.is_valid()) - self.assertEqual(len(validator.generate_report()), 246) - def test_validate_video_point_labels(self): with tempfile.TemporaryDirectory() as tmpdir_name: with open(f"{tmpdir_name}/test_validate_video_point_labels.json", @@ -2114,3 +1959,9 @@ def test_validate_tag_without_class_name(self): self.assertFalse(validator.is_valid()) print(validator.generate_report()) self.assertEqual(len(validator.generate_report()), 191) + + def test_strict_points(self): + try: + Polygon(points=["asd", 1, 1.0, 3], type="polygon") + except Exception as e: + print(333, e) \ No newline at end of file