Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ v0.9.0
* Bugfix: `login_oauth` works after `client_enable_hires` has been called - tehkillerbee_
* Bugfix: Sessions created without a config no longer share one `Config` object - tehkillerbee_
* `save_session_to_file` and `load_session_from_file` now persist `expiry_time` - tehkillerbee_
* `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_
* `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_
* Added `AbuseDetected`, raised when TIDAL answers with an anti-abuse HTTP 403 - tehkillerbee_
* 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_
* Bugfix: Non-JSON error responses and `Retry-After` given as a date no longer break error handling - tehkillerbee_
* Bugfix: Parsing tracks and albums no longer fails when TIDAL returns no or empty artist information - tehkillerbee_
* 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_
* 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_

v0.8.11
--------
Expand Down Expand Up @@ -324,4 +325,5 @@ v0.6.2
.. _nilathedragon: https://github.com/nilathedragon
.. _semohr: https://github.com/semohr
.. _rafrombrc: https://github.com/rafrombrc
.. _Petru101: https://github.com/Petru101

59 changes: 59 additions & 0 deletions tests/test_token_refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,65 @@ def test_failed_proactive_refresh_does_not_fail_the_request():
assert session.request.basic_request("GET", "tracks/1").ok


def _load_oauth_session(expiry_time):
session = _refreshing_session(None)
session.request_session.request.return_value = fake_response(
200, {"sessionId": "sid", "countryCode": "US", "userId": 1}
)
with mock.patch.object(tidalapi.user.User, "factory"):
assert session.load_oauth_session(
"Bearer", "old-access", "old-refresh", expiry_time, is_pkce=True
)
return session


def test_load_oauth_session_accepts_expiry_time_string():
# A naive string, as stored by applications using a previous tidalapi version
now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
expired = now - datetime.timedelta(days=1)

session = _load_oauth_session(str(expired))

# The expired token is refreshed up front, which sets a new expiry time
session.request_session.post.assert_called_once()
assert session.access_token == "new-access"
assert session.expiry_time > datetime.datetime.now(datetime.timezone.utc)


def test_load_oauth_session_accepts_iso_format_expiry_time():
expiry_time = datetime.datetime(2030, 1, 2, 3, 4, 5)

session = _load_oauth_session(expiry_time.isoformat())

assert session.expiry_time == expiry_time.replace(tzinfo=datetime.timezone.utc)
session.request_session.post.assert_not_called()


def test_load_oauth_session_accepts_timezone_aware_expiry_time_string():
expiry_time = datetime.datetime(2030, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)

session = _load_oauth_session(str(expiry_time))

assert session.expiry_time == expiry_time
session.request_session.post.assert_not_called()


def test_load_oauth_session_treats_naive_datetime_as_utc():
expiry_time = datetime.datetime(2030, 1, 2, 3, 4, 5)

session = _load_oauth_session(expiry_time)

assert session.expiry_time == expiry_time.replace(tzinfo=datetime.timezone.utc)
session.request_session.post.assert_not_called()


def test_load_oauth_session_ignores_invalid_expiry_time_string():
session = _load_oauth_session("None")

assert session.expiry_time is None
session.request_session.post.assert_not_called()


def test_session_file_round_trips_expiry_time(tmp_path):
session_file = tmp_path / "session.json"
expiry_time = datetime.datetime(2030, 1, 2, 3, 4, 5)
Expand Down
18 changes: 16 additions & 2 deletions tidalapi/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ def load_oauth_session(
token_type: str,
access_token: str,
refresh_token: str | None = None,
expiry_time: datetime.datetime | None = None,
expiry_time: datetime.datetime | str | None = None,
is_pkce: bool | None = False,
) -> bool:
"""Login to TIDAL using details from a previous OAuth login, automatically
Expand All @@ -394,10 +394,24 @@ def load_oauth_session(
:param access_token: The access token received from an oauth login or refresh
:param refresh_token: (Optional) A refresh token that lets you get a new access
token after it has expired
:param expiry_time: (Optional) The datetime the access token will expire
:param expiry_time: (Optional) The datetime the access token will expire. A
string in ISO 8601 format (e.g. ``str(datetime)``) is accepted too. Values
without a timezone are treated as UTC
:param is_pkce: (Optional) Is session pkce?
:return: True if we believe the login was successful, otherwise false.
"""
if isinstance(expiry_time, str):
# Applications often store the expiry time as text
try:
expiry_time = datetime.datetime.fromisoformat(expiry_time)
except ValueError:
log.info("Ignoring invalid expiry_time %r", expiry_time)
expiry_time = None

if expiry_time is not None and expiry_time.tzinfo is None:
# Treat naive datetimes (e.g. from legacy utcnow()) as UTC
expiry_time = expiry_time.replace(tzinfo=datetime.timezone.utc)

self.token_type = token_type
self.access_token = access_token
self.refresh_token = refresh_token
Expand Down
Loading