Skip to content

Commit ea07207

Browse files
committed
Provide option to retry calls to ScriptAuthorizer.refresh
1 parent bb0e005 commit ea07207

File tree

3 files changed

+57
-14
lines changed

3 files changed

+57
-14
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Unreleased
1212
- ``Requestor`` is now initialzed with a ``timeout`` parameter.
1313
- ``ScriptAuthorizer``, ``ReadOnlyAuthorizer``, and ``DeviceIDAuthorizer`` have a
1414
new parameter, ``scopes``, which determines the scope of access requests.
15+
- ``ScriptAuthorizer.refresh`` can now retry authorization attempts that raise
16+
instances of ``OAuthException``.
1517

1618
2.2.0 (2021-06-10)
1719
------------------

prawcore/auth.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -392,28 +392,32 @@ def __init__(
392392
password,
393393
two_factor_callback=None,
394394
scopes=None,
395+
retries=0,
395396
):
396397
"""Represent a single personal-use authorization to Reddit's API.
397398
398399
:param authenticator: An instance of :class:`TrustedAuthenticator`.
399400
:param username: The Reddit username of one of the application's developers.
400401
:param password: The password associated with ``username``.
401-
:param two_factor_callback: A function that returns OTPs (One-Time
402-
Passcodes), also known as 2FA auth codes. If this function is
403-
provided, prawcore will call it when authenticating.
402+
:param two_factor_callback: (Optional) A function that returns a two factor
403+
authentication code (default: None).
404404
:param scopes: (Optional) A list of OAuth scopes to request authorization for
405405
(default: None). The scope ``*`` is requested when the default argument is
406406
used.
407+
:param retries: (Optional) The number of times to retry an authorization
408+
attempt that raises an ``OAuthException`` (default: 0). The argument should
409+
be a nonnegative integer less than or equal to ten. This setting is ignored
410+
if two_factor_callback does not return a value that is ``True``.
407411
408412
"""
409-
super(ScriptAuthorizer, self).__init__(authenticator)
413+
super().__init__(authenticator)
410414
self._password = password
415+
self._retries = abs(retries)
411416
self._scopes = scopes
412417
self._two_factor_callback = two_factor_callback
413418
self._username = username
414419

415-
def refresh(self):
416-
"""Obtain a new personal-use script type access token."""
420+
def _refresh_with_retries(self, count=0):
417421
additional_kwargs = {}
418422
if self._scopes:
419423
additional_kwargs["scope"] = " ".join(self._scopes)
@@ -422,9 +426,21 @@ def refresh(self):
422426
)
423427
if two_factor_code:
424428
additional_kwargs["otp"] = two_factor_code
425-
self._request_token(
426-
grant_type="password",
427-
username=self._username,
428-
password=self._password,
429-
**additional_kwargs,
430-
)
429+
try:
430+
self._request_token(
431+
grant_type="password",
432+
username=self._username,
433+
password=self._password,
434+
**additional_kwargs,
435+
)
436+
except OAuthException:
437+
if two_factor_code:
438+
if count >= min(self._retries, 10):
439+
raise
440+
self._refresh_with_retries(count + 1)
441+
else:
442+
raise
443+
444+
def refresh(self):
445+
"""Obtain a new personal-use script type access token."""
446+
self._refresh_with_retries()

tests/test_authorizer.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Test for prawcore.auth.Authorizer classes."""
22
import unittest
3+
from traceback import format_exc
34

45
from betamax import Betamax
6+
from mock import Mock, patch
57

68
import prawcore
79

@@ -14,8 +16,8 @@
1416
REFRESH_TOKEN,
1517
REQUESTOR,
1618
TEMPORARY_GRANT_CODE,
17-
two_factor_callback,
1819
USERNAME,
20+
two_factor_callback,
1921
)
2022

2123

@@ -386,7 +388,7 @@ def test_refresh__with_invalid_otp(self):
386388
"ScriptAuthorizer_refresh__with_invalid_otp"
387389
):
388390
self.assertRaises(prawcore.OAuthException, authorizer.refresh)
389-
self.assertFalse(authorizer.is_valid())
391+
self.assertFalse(authorizer.is_valid())
390392

391393
def test_refresh__with_invalid_username_or_password(self):
392394
authorizer = prawcore.ScriptAuthorizer(
@@ -398,6 +400,29 @@ def test_refresh__with_invalid_username_or_password(self):
398400
self.assertRaises(prawcore.OAuthException, authorizer.refresh)
399401
self.assertFalse(authorizer.is_valid())
400402

403+
@patch("time.sleep", return_value=None)
404+
@patch("prawcore.Requestor.request")
405+
def test_refresh_with_retries(self, mock_post, _):
406+
response = Mock(
407+
json=lambda: {"error": "invalid grant"}, status_code=200
408+
)
409+
mock_post.return_value = response
410+
mock_callback = Mock(return_value="123456")
411+
authorizer = prawcore.ScriptAuthorizer(
412+
self.authentication,
413+
"dummy",
414+
"dummy",
415+
two_factor_callback=mock_callback,
416+
retries=13,
417+
)
418+
try:
419+
authorizer.refresh()
420+
except prawcore.OAuthException:
421+
traceback = format_exc()
422+
assert traceback.count("prawcore.exceptions.OAuthException") == 11
423+
assert mock_callback.call_count == 11
424+
assert mock_post.call_count == 11
425+
401426
def test_refresh__with_scopes(self):
402427
scope_list = ["adsedit", "adsread", "creddits", "history"]
403428
authorizer = prawcore.ScriptAuthorizer(

0 commit comments

Comments
 (0)