diff --git a/labelbox/__init__.py b/labelbox/__init__.py index d1e18b3ad..98f6a0dae 100644 --- a/labelbox/__init__.py +++ b/labelbox/__init__.py @@ -1,10 +1,6 @@ name = "labelbox" __version__ = "3.47.1" -from backports.datetime_fromisoformat import MonkeyPatch - -MonkeyPatch.patch_fromisoformat() - from labelbox.client import Client from labelbox.schema.project import Project from labelbox.schema.model import Model diff --git a/labelbox/schema/data_row_metadata.py b/labelbox/schema/data_row_metadata.py index 3fd33e50a..61982e69d 100644 --- a/labelbox/schema/data_row_metadata.py +++ b/labelbox/schema/data_row_metadata.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, conlist, constr from labelbox.schema.ontology import SchemaId -from labelbox.utils import _CamelCaseMixin, format_iso_datetime +from labelbox.utils import _CamelCaseMixin, format_iso_datetime, format_iso_from_string class DataRowMetadataKind(Enum): @@ -466,7 +466,7 @@ def parse_metadata_fields( value=schema.uid) elif schema.kind == DataRowMetadataKind.datetime: field = DataRowMetadataField(schema_id=schema.uid, - value=datetime.fromisoformat( + value=format_iso_from_string( f["value"])) else: field = DataRowMetadataField(schema_id=schema.uid, @@ -838,7 +838,7 @@ def _validate_parse_number( def _validate_parse_datetime( field: DataRowMetadataField) -> List[Dict[str, Union[SchemaId, str]]]: if isinstance(field.value, str): - field.value = datetime.fromisoformat(field.value) + field.value = format_iso_from_string(field.value) elif not isinstance(field.value, datetime): raise TypeError( f"Value for datetime fields must be either a string or datetime object. Found {type(field.value)}" diff --git a/labelbox/utils.py b/labelbox/utils.py index 5ec90f403..bfe39a3e2 100644 --- a/labelbox/utils.py +++ b/labelbox/utils.py @@ -1,10 +1,16 @@ import datetime import re + +from dateutil.tz import tzoffset +from dateutil.parser import isoparse as dateutil_parse +from dateutil.utils import default_tzinfo + from urllib.parse import urlparse from pydantic import BaseModel UPPERCASE_COMPONENTS = ['uri', 'rgb'] ISO_DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' +DFLT_TZ = tzoffset("UTC", 0000) def _convert(s, sep, title): @@ -80,4 +86,14 @@ def format_iso_datetime(dt: datetime.datetime) -> str: Formats a datetime object into the format: 2011-11-04T00:05:23Z Note that datetime.isoformat() outputs 2011-11-04T00:05:23+00:00 """ - return dt.strftime(ISO_DATETIME_FORMAT) \ No newline at end of file + return dt.astimezone(datetime.timezone.utc).strftime(ISO_DATETIME_FORMAT) + + +def format_iso_from_string(date_string: str) -> datetime.datetime: + """ + Converts a string even if offset is missing: 2011-11-04T00:05:23Z or 2011-11-04T00:05:23+00:00 or 2011-11-04T00:05:23 + to a datetime object. + For missing offsets, the default offset is UTC. + """ + # return datetime.datetime.fromisoformat(date_string) + return default_tzinfo(dateutil_parse(date_string), DFLT_TZ) diff --git a/requirements.txt b/requirements.txt index 61d8b559c..b48d8bd12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,19 @@ -requests==2.22.0 backoff==1.10.0 -google-api-core>=1.22.1 -pydantic>=1.8,<2.0 -shapely -tqdm geojson +google-api-core>=1.22.1 +imagesize +nbconvert~=7.2.6 +nbformat~=5.7.0 numpy -PILLOW opencv-python -imagesize -pyproj +PILLOW +pydantic>=1.8,<2.0 pygeotile -typing-extensions==4.5.0 +pyproj pytest-xdist -nbformat~=5.7.0 -nbconvert~=7.2.6 +python-dateutil>=2.8.2,<2.9.0 +requests==2.22.0 +shapely +tqdm typeguard==2.13.3 -backports-datetime-fromisoformat~=2.0 \ No newline at end of file +typing-extensions==4.5.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 151a4ba15..e459b0f5e 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ packages=setuptools.find_packages(), install_requires=[ "backoff==1.10.0", "requests>=2.22.0", "google-api-core>=1.22.1", - "pydantic>=1.8,<2.0", "tqdm", "backports-datetime-fromisoformat~=2.0" + "pydantic>=1.8,<2.0", "tqdm", "python-dateutil>=2.8.2,<2.9.0" ], extras_require={ 'data': [ @@ -31,7 +31,6 @@ ], }, classifiers=[ - 'Development Status :: 3 - Alpha', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', diff --git a/tests/integration/test_data_row_metadata.py b/tests/integration/test_data_row_metadata.py index 3c0d06d33..41f020c51 100644 --- a/tests/integration/test_data_row_metadata.py +++ b/tests/integration/test_data_row_metadata.py @@ -445,7 +445,7 @@ def test_delete_schema(mdo): @pytest.mark.parametrize('datetime_str', - ['2011-11-04T00:05:23Z', '2011-05-07T14:34:14+00:00']) + ['2011-11-04T00:05:23Z', '2011-11-04T00:05:23+00:00']) def test_upsert_datarow_date_metadata(data_row, mdo, datetime_str): metadata = [ DataRowMetadata(data_row_id=data_row.uid, @@ -458,11 +458,11 @@ def test_upsert_datarow_date_metadata(data_row, mdo, datetime_str): assert len(errors) == 0 metadata = mdo.bulk_export([data_row.uid]) - assert metadata[0].fields[0].value == datetime.fromisoformat(datetime_str) + assert f"{metadata[0].fields[0].value}" == "2011-11-04 00:05:23+00:00" @pytest.mark.parametrize('datetime_str', - ['2011-11-04T00:05:23Z', '2011-05-07T14:34:14+00:00']) + ['2011-11-04T00:05:23Z', '2011-11-04T00:05:23+00:00']) def test_create_data_row_with_metadata(dataset, image_url, datetime_str): client = dataset.client assert len(list(dataset.data_rows())) == 0 @@ -475,5 +475,4 @@ def test_create_data_row_with_metadata(dataset, image_url, datetime_str): metadata_fields=metadata_fields) retrieved_data_row = client.get_data_row(data_row.uid) - assert retrieved_data_row.metadata[0].value == datetime.fromisoformat( - datetime_str) + assert f"{retrieved_data_row.metadata[0].value}" == "2011-11-04 00:05:23+00:00" diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 000000000..129edcd72 --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,13 @@ +import pytest +from labelbox.utils import format_iso_datetime, format_iso_from_string + + +@pytest.mark.parametrize('datetime_str, expected_datetime_str', + [('2011-11-04T00:05:23Z', '2011-11-04T00:05:23Z'), + ('2011-11-04T00:05:23+00:00', '2011-11-04T00:05:23Z'), + ('2011-11-04T00:05:23+05:00', '2011-11-03T19:05:23Z'), + ('2011-11-04T00:05:23', '2011-11-04T00:05:23Z')]) +def test_datetime_parsing(datetime_str, expected_datetime_str): + # NOTE I would normally not take 'expected' using another function from sdk code, but in this case this is exactly the usage in _validate_parse_datetime + assert format_iso_datetime( + format_iso_from_string(datetime_str)) == expected_datetime_str diff --git a/tox.ini b/tox.ini index b98fd7fa8..8dd46d939 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # content of: tox.ini , put in same dir as setup.py [tox] -envlist = py36, py37, py38 +envlist = py37, py38, py39 [testenv] # install pytest in the virtualenv where commands will be executed