Skip to content

Commit c620bb1

Browse files
authored
Merge pull request #416 from Petru101/fix/oauth-expiry-string
Accept an ISO 8601 string as expiry_time in load_oauth_session
2 parents 14c389f + f763fec commit c620bb1

3 files changed

Lines changed: 78 additions & 3 deletions

File tree

‎HISTORY.rst‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ v0.9.0
1616
* Bugfix: `login_oauth` works after `client_enable_hires` has been called - tehkillerbee_
1717
* Bugfix: Sessions created without a config no longer share one `Config` object - tehkillerbee_
1818
* `save_session_to_file` and `load_session_from_file` now persist `expiry_time` - tehkillerbee_
19-
* `Session.expiry_time` is now a timezone-aware UTC datetime, replacing the deprecated `datetime.utcnow()`. Naive datetimes passed to `load_oauth_session` are still supported - tehkillerbee_
19+
* `Session.expiry_time` is now a timezone-aware UTC datetime, replacing the deprecated `datetime.utcnow()`. Naive datetimes passed to `load_oauth_session` are treated as UTC - tehkillerbee_
2020
* Added `AbuseDetected`, raised when TIDAL answers with an anti-abuse HTTP 403 - tehkillerbee_
2121
* Added `Config(backoff=...)`, enabled by default: stop sending requests after a rate limit or abuse response and raise `TidalBackoffError` until it has passed. Pass `backoff=False` to disable - tehkillerbee_
2222
* Bugfix: Non-JSON error responses and `Retry-After` given as a date no longer break error handling - tehkillerbee_
2323
* Bugfix: Parsing tracks and albums no longer fails when TIDAL returns no or empty artist information - tehkillerbee_
2424
* Parsed `Track` flags (`pay_to_stream`, `premium_streaming_only`, `editable`, `upload`, `spotlighted`) are now `False` instead of `None` when TIDAL omits them, and `url` is an empty string - tehkillerbee_
25+
* Bugfix: `load_oauth_session` accepts `expiry_time` as an ISO 8601 string. Applications that store it as text (e.g. High Tide) would otherwise fail on every request, now that the expiry time is checked before each request - Petru101_
2526

2627
v0.8.11
2728
--------
@@ -324,4 +325,5 @@ v0.6.2
324325
.. _nilathedragon: https://github.com/nilathedragon
325326
.. _semohr: https://github.com/semohr
326327
.. _rafrombrc: https://github.com/rafrombrc
328+
.. _Petru101: https://github.com/Petru101
327329

‎tests/test_token_refresh.py‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,65 @@ def test_failed_proactive_refresh_does_not_fail_the_request():
215215
assert session.request.basic_request("GET", "tracks/1").ok
216216

217217

218+
def _load_oauth_session(expiry_time):
219+
session = _refreshing_session(None)
220+
session.request_session.request.return_value = fake_response(
221+
200, {"sessionId": "sid", "countryCode": "US", "userId": 1}
222+
)
223+
with mock.patch.object(tidalapi.user.User, "factory"):
224+
assert session.load_oauth_session(
225+
"Bearer", "old-access", "old-refresh", expiry_time, is_pkce=True
226+
)
227+
return session
228+
229+
230+
def test_load_oauth_session_accepts_expiry_time_string():
231+
# A naive string, as stored by applications using a previous tidalapi version
232+
now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
233+
expired = now - datetime.timedelta(days=1)
234+
235+
session = _load_oauth_session(str(expired))
236+
237+
# The expired token is refreshed up front, which sets a new expiry time
238+
session.request_session.post.assert_called_once()
239+
assert session.access_token == "new-access"
240+
assert session.expiry_time > datetime.datetime.now(datetime.timezone.utc)
241+
242+
243+
def test_load_oauth_session_accepts_iso_format_expiry_time():
244+
expiry_time = datetime.datetime(2030, 1, 2, 3, 4, 5)
245+
246+
session = _load_oauth_session(expiry_time.isoformat())
247+
248+
assert session.expiry_time == expiry_time.replace(tzinfo=datetime.timezone.utc)
249+
session.request_session.post.assert_not_called()
250+
251+
252+
def test_load_oauth_session_accepts_timezone_aware_expiry_time_string():
253+
expiry_time = datetime.datetime(2030, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)
254+
255+
session = _load_oauth_session(str(expiry_time))
256+
257+
assert session.expiry_time == expiry_time
258+
session.request_session.post.assert_not_called()
259+
260+
261+
def test_load_oauth_session_treats_naive_datetime_as_utc():
262+
expiry_time = datetime.datetime(2030, 1, 2, 3, 4, 5)
263+
264+
session = _load_oauth_session(expiry_time)
265+
266+
assert session.expiry_time == expiry_time.replace(tzinfo=datetime.timezone.utc)
267+
session.request_session.post.assert_not_called()
268+
269+
270+
def test_load_oauth_session_ignores_invalid_expiry_time_string():
271+
session = _load_oauth_session("None")
272+
273+
assert session.expiry_time is None
274+
session.request_session.post.assert_not_called()
275+
276+
218277
def test_session_file_round_trips_expiry_time(tmp_path):
219278
session_file = tmp_path / "session.json"
220279
expiry_time = datetime.datetime(2030, 1, 2, 3, 4, 5)

‎tidalapi/session.py‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ def load_oauth_session(
384384
token_type: str,
385385
access_token: str,
386386
refresh_token: str | None = None,
387-
expiry_time: datetime.datetime | None = None,
387+
expiry_time: datetime.datetime | str | None = None,
388388
is_pkce: bool | None = False,
389389
) -> bool:
390390
"""Login to TIDAL using details from a previous OAuth login, automatically
@@ -394,10 +394,24 @@ def load_oauth_session(
394394
:param access_token: The access token received from an oauth login or refresh
395395
:param refresh_token: (Optional) A refresh token that lets you get a new access
396396
token after it has expired
397-
:param expiry_time: (Optional) The datetime the access token will expire
397+
:param expiry_time: (Optional) The datetime the access token will expire. A
398+
string in ISO 8601 format (e.g. ``str(datetime)``) is accepted too. Values
399+
without a timezone are treated as UTC
398400
:param is_pkce: (Optional) Is session pkce?
399401
:return: True if we believe the login was successful, otherwise false.
400402
"""
403+
if isinstance(expiry_time, str):
404+
# Applications often store the expiry time as text
405+
try:
406+
expiry_time = datetime.datetime.fromisoformat(expiry_time)
407+
except ValueError:
408+
log.info("Ignoring invalid expiry_time %r", expiry_time)
409+
expiry_time = None
410+
411+
if expiry_time is not None and expiry_time.tzinfo is None:
412+
# Treat naive datetimes (e.g. from legacy utcnow()) as UTC
413+
expiry_time = expiry_time.replace(tzinfo=datetime.timezone.utc)
414+
401415
self.token_type = token_type
402416
self.access_token = access_token
403417
self.refresh_token = refresh_token

0 commit comments

Comments
 (0)