Skip to content

Commit ee9df53

Browse files
committed
Add an alias that makes dateutil optional. (#999)
* Add an alias that makes dateutil optional. * fix test * remove fallback to dateutil by default * flake8 + doc * doc
1 parent 7072a9a commit ee9df53

File tree

6 files changed

+101
-4
lines changed

6 files changed

+101
-4
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.PHONY: quality style test
22

33

4-
check_dirs := tests src
4+
check_dirs := tests src setup.py
55

66

77
quality:

src/huggingface_hub/community.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from datetime import datetime
1010
from typing import List, Optional
1111

12-
from dateutil.parser import parse as parse_datetime
12+
from .utils import parse_datetime
1313

1414

1515
if sys.version_info >= (3, 8):

src/huggingface_hub/hf_api.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from urllib.parse import quote
2323

2424
import requests
25-
from dateutil.parser import parse as parse_datetime
2625
from requests.exceptions import HTTPError
2726

2827
from ._commit_api import (
@@ -51,7 +50,7 @@
5150
REPO_TYPES_URL_PREFIXES,
5251
SPACES_SDK_TYPES,
5352
)
54-
from .utils import filter_repo_objects, logging
53+
from .utils import filter_repo_objects, logging, parse_datetime
5554
from .utils._deprecation import _deprecate_positional_args
5655
from .utils._errors import (
5756
_raise_convert_bad_request,

src/huggingface_hub/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# limitations under the License
1717

1818
from . import tqdm as _tqdm # _tqdm is the module
19+
from ._datetime import parse_datetime
1920
from ._errors import (
2021
EntryNotFoundError,
2122
LocalEntryNotFoundError,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# coding=utf-8
2+
# Copyright 2022-present, the HuggingFace Inc. team.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Contains utilities to handle datetimes in Huggingface Hub."""
16+
from datetime import datetime, timezone
17+
18+
19+
# Local machine offset compared to UTC
20+
# Taken from https://stackoverflow.com/a/3168394.
21+
UTC_OFFSET = datetime.now(timezone.utc).astimezone().utcoffset()
22+
23+
24+
def parse_datetime(date_string: str) -> datetime:
25+
"""
26+
Parses a date_string returned from the server to a datetime object.
27+
28+
This parser is a weak-parser is the sense that it handles only a single format of
29+
date_string. It is expected that the server format will never change. The
30+
implementation depends only on the standard lib to avoid an external dependency
31+
(python-dateutil). See full discussion about this decision on PR:
32+
https://github.com/huggingface/huggingface_hub/pull/999.
33+
34+
Usage:
35+
```py
36+
> parse_datetime('2022-08-19T07:19:38.123Z')
37+
datetime.datetime(2022, 8, 19, 7, 19, 38, 123000, tzinfo=timezone.utc)
38+
```
39+
40+
Args:
41+
date_string (`str`):
42+
A string representing a datetime returned by the Hub server.
43+
String is expected to follow '%Y-%m-%dT%H:%M:%S.%fZ' pattern.
44+
45+
Returns:
46+
A python datetime object.
47+
48+
Raises:
49+
:class:`ValueError`:
50+
If `date_string` cannot be parsed.
51+
"""
52+
try:
53+
# Datetime ending with a Z means "UTC". Here we parse the date as local machine
54+
# timezone and then move it to the appropriate UTC timezone.
55+
# See https://en.wikipedia.org/wiki/ISO_8601#Coordinated_Universal_Time_(UTC)
56+
# Taken from https://stackoverflow.com/a/3168394.
57+
58+
dt = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S.%fZ")
59+
dt += UTC_OFFSET # By default, datetime is not timezoned -> move to UTC time
60+
return dt.astimezone(timezone.utc) # Set explicit timezone
61+
except ValueError as e:
62+
raise ValueError(
63+
f"Cannot parse '{date_string}' as a datetime. Date string is expected to"
64+
" follow '%Y-%m-%dT%H:%M:%S.%fZ' pattern."
65+
) from e

tests/test_utils_datetime.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import unittest
2+
from datetime import datetime, timezone
3+
4+
import pytest
5+
6+
from huggingface_hub.utils import parse_datetime
7+
8+
9+
class TestDatetimeUtils(unittest.TestCase):
10+
def test_parse_datetime(self):
11+
"""Test `parse_datetime` works correctly on datetimes returned by server."""
12+
self.assertEqual(
13+
parse_datetime("2022-08-19T07:19:38.123Z"),
14+
datetime(2022, 8, 19, 7, 19, 38, 123000, tzinfo=timezone.utc),
15+
)
16+
17+
with pytest.raises(
18+
ValueError, match=r".*Cannot parse '2022-08-19T07:19:38' as a datetime.*"
19+
):
20+
parse_datetime("2022-08-19T07:19:38")
21+
22+
with pytest.raises(
23+
ValueError,
24+
match=r".*Cannot parse '2022-08-19T07:19:38.123' as a datetime.*",
25+
):
26+
parse_datetime("2022-08-19T07:19:38.123")
27+
28+
with pytest.raises(
29+
ValueError,
30+
match=r".*Cannot parse '2022-08-19 07:19:38.123Z\+6:00' as a datetime.*",
31+
):
32+
parse_datetime("2022-08-19 07:19:38.123Z+6:00")

0 commit comments

Comments
 (0)