Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Commit e431f20

Browse filesBrowse files
andriawang6gkevinzhengchalmerlowe
authored
fix: replace deprecated utcfromtimestamp (#1799)
addresses #1781 * Moves away from using `utcfromtimestamp()` in the `datetime` library due to deprecation. * Adds a new helper function to manage internal handling of timezone aware versus timezone naive timestamps. ``` DeprecationWarning: datetime.datetime.utcfromtimestamp() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.fromtimestamp(timestamp, datetime.UTC). ``` --------- Co-authored-by: Kevin Zheng <147537668+gkevinzheng@users.noreply.github.com> Co-authored-by: Chalmer Lowe <chalmerlowe@google.com>
1 parent 3f88a24 commit e431f20
Copy full SHA for e431f20

9 files changed

+102-27Lines changed: 102 additions & 27 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎google/auth/_helpers.py‎

Copy file name to clipboardExpand all lines: google/auth/_helpers.py
+20Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,26 @@ def utcnow():
124124
return now
125125

126126

127+
def utcfromtimestamp(timestamp):
128+
"""Returns the UTC datetime from a timestamp.
129+
130+
Args:
131+
timestamp (float): The timestamp to convert.
132+
133+
Returns:
134+
datetime: The time in UTC.
135+
"""
136+
# We used datetime.utcfromtimestamp() before, since it's deprecated from
137+
# python 3.12, we are using datetime.fromtimestamp(timestamp, timezone.utc)
138+
# now. "utcfromtimestamp()" is offset-native (no timezone info), but
139+
# "fromtimestamp(timestamp, timezone.utc)" is offset-aware (with timezone
140+
# info). This will cause datetime comparison problem. For backward
141+
# compatibility, we need to remove the timezone info.
142+
dt = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
143+
dt = dt.replace(tzinfo=None)
144+
return dt
145+
146+
127147
def datetime_to_secs(value):
128148
"""Convert a datetime object to the number of seconds since the UNIX epoch.
129149
Collapse file

‎google/auth/app_engine.py‎

Copy file name to clipboardExpand all lines: google/auth/app_engine.py
+1-2Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
https://cloud.google.com/appengine/docs/python/appidentity/
2323
"""
2424

25-
import datetime
2625

2726
from google.auth import _helpers
2827
from google.auth import credentials
@@ -128,7 +127,7 @@ def refresh(self, request):
128127
scopes = self._scopes if self._scopes is not None else self._default_scopes
129128
# pylint: disable=unused-argument
130129
token, ttl = app_identity.get_access_token(scopes, self._service_account_id)
131-
expiry = datetime.datetime.utcfromtimestamp(ttl)
130+
expiry = _helpers.utcfromtimestamp(ttl)
132131

133132
self.token, self.expiry = token, expiry
134133

Collapse file

‎google/auth/compute_engine/credentials.py‎

Copy file name to clipboardExpand all lines: google/auth/compute_engine/credentials.py
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ def _call_metadata_identity_endpoint(self, request):
498498
raise new_exc from caught_exc
499499

500500
_, payload, _, _ = jwt._unverified_decode(id_token)
501-
return id_token, datetime.datetime.utcfromtimestamp(payload["exp"])
501+
return id_token, _helpers.utcfromtimestamp(payload["exp"])
502502

503503
def refresh(self, request):
504504
"""Refreshes the ID token.
Collapse file

‎google/auth/impersonated_credentials.py‎

Copy file name to clipboardExpand all lines: google/auth/impersonated_credentials.py
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ def refresh(self, request):
649649
raise new_exc from caught_exc
650650

651651
self.token = id_token
652-
self.expiry = datetime.utcfromtimestamp(
652+
self.expiry = _helpers.utcfromtimestamp(
653653
jwt.decode(id_token, verify=False)["exp"]
654654
)
655655

Collapse file

‎google/oauth2/_client.py‎

Copy file name to clipboardExpand all lines: google/oauth2/_client.py
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ def call_iam_generate_id_token_endpoint(
368368
raise new_exc from caught_exc
369369

370370
payload = jwt.decode(id_token, verify=False)
371-
expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
371+
expiry = _helpers.utcfromtimestamp(payload["exp"])
372372

373373
return id_token, expiry
374374

@@ -420,7 +420,7 @@ def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
420420
raise new_exc from caught_exc
421421

422422
payload = jwt.decode(id_token, verify=False)
423-
expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
423+
expiry = _helpers.utcfromtimestamp(payload["exp"])
424424

425425
return id_token, expiry, response_data
426426

Collapse file

‎google/oauth2/_client_async.py‎

Copy file name to clipboardExpand all lines: google/oauth2/_client_async.py
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@
2323
.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
2424
"""
2525

26-
import datetime
2726
import http.client as http_client
2827
import json
2928
import urllib
3029

3130
from google.auth import _exponential_backoff
31+
from google.auth import _helpers
3232
from google.auth import exceptions
3333
from google.auth import jwt
3434
from google.oauth2 import _client as client
@@ -227,7 +227,7 @@ async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
227227
raise new_exc from caught_exc
228228

229229
payload = jwt.decode(id_token, verify=False)
230-
expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
230+
expiry = _helpers.utcfromtimestamp(payload["exp"])
231231

232232
return id_token, expiry, response_data
233233

Collapse file

‎tests/compute_engine/test_credentials.py‎

Copy file name to clipboardExpand all lines: tests/compute_engine/test_credentials.py
+22-14Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,7 @@ def test_default_state(self, get):
758758

759759
@mock.patch(
760760
"google.auth._helpers.utcnow",
761-
return_value=datetime.datetime.utcfromtimestamp(0),
761+
return_value=_helpers.utcfromtimestamp(0),
762762
)
763763
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
764764
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
@@ -791,7 +791,7 @@ def test_make_authorization_grant_assertion(self, sign, get, utcnow):
791791

792792
@mock.patch(
793793
"google.auth._helpers.utcnow",
794-
return_value=datetime.datetime.utcfromtimestamp(0),
794+
return_value=_helpers.utcfromtimestamp(0),
795795
)
796796
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
797797
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
@@ -823,7 +823,7 @@ def test_with_service_account(self, sign, get, utcnow):
823823

824824
@mock.patch(
825825
"google.auth._helpers.utcnow",
826-
return_value=datetime.datetime.utcfromtimestamp(0),
826+
return_value=_helpers.utcfromtimestamp(0),
827827
)
828828
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
829829
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
@@ -879,7 +879,7 @@ def test_token_uri(self):
879879

880880
@mock.patch(
881881
"google.auth._helpers.utcnow",
882-
return_value=datetime.datetime.utcfromtimestamp(0),
882+
return_value=_helpers.utcfromtimestamp(0),
883883
)
884884
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
885885
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
@@ -1001,7 +1001,7 @@ def test_with_target_audience_integration(self):
10011001

10021002
@mock.patch(
10031003
"google.auth._helpers.utcnow",
1004-
return_value=datetime.datetime.utcfromtimestamp(0),
1004+
return_value=_helpers.utcfromtimestamp(0),
10051005
)
10061006
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
10071007
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
@@ -1040,7 +1040,7 @@ def test_with_quota_project(self, sign, get, utcnow):
10401040

10411041
@mock.patch(
10421042
"google.auth._helpers.utcnow",
1043-
return_value=datetime.datetime.utcfromtimestamp(0),
1043+
return_value=_helpers.utcfromtimestamp(0),
10441044
)
10451045
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
10461046
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
@@ -1062,7 +1062,7 @@ def test_with_token_uri(self, sign, get, utcnow):
10621062

10631063
@mock.patch(
10641064
"google.auth._helpers.utcnow",
1065-
return_value=datetime.datetime.utcfromtimestamp(0),
1065+
return_value=_helpers.utcfromtimestamp(0),
10661066
)
10671067
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
10681068
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
@@ -1170,7 +1170,7 @@ def test_with_quota_project_integration(self):
11701170

11711171
@mock.patch(
11721172
"google.auth._helpers.utcnow",
1173-
return_value=datetime.datetime.utcfromtimestamp(0),
1173+
return_value=_helpers.utcfromtimestamp(0),
11741174
)
11751175
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
11761176
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
@@ -1181,7 +1181,11 @@ def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow):
11811181
]
11821182
sign.side_effect = [b"signature"]
11831183
id_token_jwt_grant.side_effect = [
1184-
("idtoken", datetime.datetime.utcfromtimestamp(3600), {})
1184+
(
1185+
"idtoken",
1186+
_helpers.utcfromtimestamp(3600),
1187+
{},
1188+
)
11851189
]
11861190

11871191
request = mock.create_autospec(transport.Request, instance=True)
@@ -1194,7 +1198,7 @@ def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow):
11941198

11951199
# Check that the credentials have the token and proper expiration
11961200
assert self.credentials.token == "idtoken"
1197-
assert self.credentials.expiry == (datetime.datetime.utcfromtimestamp(3600))
1201+
assert self.credentials.expiry == _helpers.utcfromtimestamp(3600)
11981202

11991203
# Check the credential info
12001204
assert self.credentials.service_account_email == "service-account@example.com"
@@ -1205,7 +1209,7 @@ def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow):
12051209

12061210
@mock.patch(
12071211
"google.auth._helpers.utcnow",
1208-
return_value=datetime.datetime.utcfromtimestamp(0),
1212+
return_value=_helpers.utcfromtimestamp(0),
12091213
)
12101214
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
12111215
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
@@ -1232,7 +1236,7 @@ def test_refresh_error(self, sign, get, utcnow):
12321236

12331237
@mock.patch(
12341238
"google.auth._helpers.utcnow",
1235-
return_value=datetime.datetime.utcfromtimestamp(0),
1239+
return_value=_helpers.utcfromtimestamp(0),
12361240
)
12371241
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
12381242
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
@@ -1243,7 +1247,11 @@ def test_before_request_refreshes(self, id_token_jwt_grant, sign, get, utcnow):
12431247
]
12441248
sign.side_effect = [b"signature"]
12451249
id_token_jwt_grant.side_effect = [
1246-
("idtoken", datetime.datetime.utcfromtimestamp(3600), {})
1250+
(
1251+
"idtoken",
1252+
_helpers.utcfromtimestamp(3600),
1253+
{},
1254+
)
12471255
]
12481256

12491257
request = mock.create_autospec(transport.Request, instance=True)
@@ -1312,7 +1320,7 @@ def test_get_id_token_from_metadata(
13121320
}
13131321

13141322
assert cred.token == SAMPLE_ID_TOKEN
1315-
assert cred.expiry == datetime.datetime.utcfromtimestamp(SAMPLE_ID_TOKEN_EXP)
1323+
assert cred.expiry == _helpers.utcfromtimestamp(SAMPLE_ID_TOKEN_EXP)
13161324
assert cred._use_metadata_identity_endpoint
13171325
assert cred._signer is None
13181326
assert cred._token_uri is None
Collapse file

‎tests/test__helpers.py‎

Copy file name to clipboardExpand all lines: tests/test__helpers.py
+47Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,3 +677,50 @@ def test_parse_response_no_json_method():
677677

678678
def test_parse_response_none():
679679
assert _helpers._parse_response(None) is None
680+
681+
682+
class TestUtcFromTimestamp:
683+
"""Tests for the utcfromtimestamp utility function."""
684+
685+
@pytest.mark.parametrize(
686+
"ts, expected",
687+
[
688+
(1704067200.0, datetime.datetime(2024, 1, 1, 0, 0, 0)),
689+
(0.0, datetime.datetime(1970, 1, 1, 0, 0, 0)),
690+
(1704067200.500123, datetime.datetime(2024, 1, 1, 0, 0, 0, 500123)),
691+
(-31536000.0, datetime.datetime(1969, 1, 1, 0, 0, 0)),
692+
(1000000000.0, datetime.datetime(2001, 9, 9, 1, 46, 40)),
693+
],
694+
ids=[
695+
"standard_timestamp",
696+
"unix_epoch",
697+
"subsecond_precision",
698+
"negative_timestamp",
699+
"timezone_independence",
700+
],
701+
)
702+
def test_success_cases(self, ts, expected):
703+
"""Verify correct UTC conversion and that the result is offset-naive."""
704+
result = _helpers.utcfromtimestamp(ts)
705+
706+
# 1. Check the datetime value is correct
707+
assert result == expected
708+
709+
# 2. Check it is naive (tzinfo is None) for backward compatibility
710+
assert result.tzinfo is None
711+
712+
@pytest.mark.parametrize(
713+
"invalid_input",
714+
["string", None, [123]],
715+
ids=["type_string", "type_none", "type_list"],
716+
)
717+
def test_invalid_types(self, invalid_input):
718+
"""Verify that passing invalid types raises a TypeError."""
719+
with pytest.raises(TypeError):
720+
_helpers.utcfromtimestamp(invalid_input)
721+
722+
def test_out_of_range(self):
723+
"""Test very large timestamps that exceed platform limits."""
724+
with pytest.raises((OverflowError, OSError, ValueError)):
725+
# Large enough to fail on most systems (Year 300,000+)
726+
_helpers.utcfromtimestamp(9999999999999)
Collapse file

‎tests/test_impersonated_credentials.py‎

Copy file name to clipboardExpand all lines: tests/test_impersonated_credentials.py
+6-5Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,8 @@ def test_id_token_success(
10471047
id_creds.refresh(request)
10481048

10491049
assert id_creds.token == ID_TOKEN_DATA
1050-
assert id_creds.expiry == datetime.datetime.utcfromtimestamp(ID_TOKEN_EXPIRY)
1050+
expected_expiry = _helpers.utcfromtimestamp(ID_TOKEN_EXPIRY)
1051+
assert id_creds.expiry == expected_expiry
10511052

10521053
def test_id_token_metrics(self, mock_donor_credentials):
10531054
credentials = self.make_credentials(lifetime=None)
@@ -1071,9 +1072,8 @@ def test_id_token_metrics(self, mock_donor_credentials):
10711072
id_creds.refresh(None)
10721073

10731074
assert id_creds.token == ID_TOKEN_DATA
1074-
assert id_creds.expiry == datetime.datetime.utcfromtimestamp(
1075-
ID_TOKEN_EXPIRY
1076-
)
1075+
expected_expiry = _helpers.utcfromtimestamp(ID_TOKEN_EXPIRY)
1076+
assert id_creds.expiry == expected_expiry
10771077
assert (
10781078
mock_post.call_args.kwargs["headers"]["x-goog-api-client"]
10791079
== ID_TOKEN_REQUEST_METRICS_HEADER_VALUE
@@ -1181,7 +1181,8 @@ def test_id_token_with_target_audience(
11811181
id_creds.refresh(request)
11821182

11831183
assert id_creds.token == ID_TOKEN_DATA
1184-
assert id_creds.expiry == datetime.datetime.utcfromtimestamp(ID_TOKEN_EXPIRY)
1184+
expected_expiry = _helpers.utcfromtimestamp(ID_TOKEN_EXPIRY)
1185+
assert id_creds.expiry == expected_expiry
11851186
assert id_creds._include_email is True
11861187

11871188
def test_id_token_invalid_cred(

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.