diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e22852e..023ab7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Coveralls Finished - uses: coverallsapp/github-action@57daa114 + uses: coverallsapp/github-action@57daa114ba54fd8e1c8563e8027325c0bf2f5e80 with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true @@ -62,6 +62,6 @@ jobs: run: coveralls strategy: matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] name: CI on: [pull_request, push] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bedfa07..7329d75 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,7 @@ -default_language_version: - python: python3.7 fail_fast: true repos: - - hooks: - - id: black - repo: https://github.com/ambv/black - rev: stable +- hooks: + - id: black + language_version: python3 + repo: https://github.com/psf/black + rev: 20.8b1 diff --git a/CHANGES.rst b/CHANGES.rst index 344c614..542b817 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,23 @@ Change Log prawcore follows `semantic versioning `_ with the exception that deprecations will not be announced by a minor release. +Unreleased +---------- + +**Added** + +* ``Authorizer`` optionally takes a ``pre_refresh_callback`` keyword + argument. If provided, the function will called with the instance of + ``Authorizer`` prior to refreshing the access and refresh tokens. +* ``Authorizer`` optionally takes a ``post_refresh_callback`` keyword + argument. If provided, the function will called with the instance of + ``Authorizer`` after refreshing the access and refresh tokens. + +**Changed** + +* The ``refresh_token`` argument to ``Authorizer`` must now be passed by + keyword, and cannot be passed as a positional argument. + 1.5.0 (2020-08-04) ------------------ diff --git a/examples/obtain_refresh_token.py b/examples/obtain_refresh_token.py index f36fd4d..33c4423 100755 --- a/examples/obtain_refresh_token.py +++ b/examples/obtain_refresh_token.py @@ -49,7 +49,7 @@ def main(): prawcore.Requestor("prawcore_refresh_token_example"), os.environ["PRAWCORE_CLIENT_ID"], os.environ["PRAWCORE_CLIENT_SECRET"], - os.environ["PRAWCORE_REDIRECT_URI"], + "http://localhost:8080", ) state = str(random.randint(0, 65000)) diff --git a/prawcore/auth.py b/prawcore/auth.py index 7c28f06..7f1659b 100644 --- a/prawcore/auth.py +++ b/prawcore/auth.py @@ -200,15 +200,34 @@ class Authorizer(BaseAuthorizer): AUTHENTICATOR_CLASS = BaseAuthenticator - def __init__(self, authenticator, refresh_token=None): + def __init__( + self, + authenticator, + *, + post_refresh_callback=None, + pre_refresh_callback=None, + refresh_token=None + ): """Represent a single authorization to Reddit's API. :param authenticator: An instance of a subclass of :class:`BaseAuthenticator`. + :param post_refresh_callback: (Optional) When a single-argument + function is passed, the function will be called prior to refreshing + the access and refresh tokens. The argument to the callback is the + :class:`Authorizor` instance. This callback can be used to inspect + and modify the attributes of the :class:`Authorizor`. + :param pre_refresh_callback: (Optional) When a single-argument function + is passed, the function will be called after refreshing the access + and refresh tokens. The argument to the callback is the + :class:`Authorizor` instance. This callback can be used to inspect + and modify the attributes of the :class:`Authorizor`. :param refresh_token: (Optional) Enables the ability to refresh the authorization. """ super(Authorizer, self).__init__(authenticator) + self._post_refresh_callback = post_refresh_callback + self._pre_refresh_callback = pre_refresh_callback self.refresh_token = refresh_token def authorize(self, code): @@ -228,11 +247,15 @@ def authorize(self, code): def refresh(self): """Obtain a new access token from the refresh_token.""" + if self._pre_refresh_callback: + self._pre_refresh_callback(self) if self.refresh_token is None: raise InvalidInvocation("refresh token not provided") self._request_token( grant_type="refresh_token", refresh_token=self.refresh_token ) + if self._post_refresh_callback: + self._post_refresh_callback(self) def revoke(self, only_access=False): """Revoke the current Authorization. diff --git a/setup.py b/setup.py index e074b7b..da12aa1 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ], description="Low-level communication layer for PRAW 4+.", extras_require=extras, diff --git a/tests/cassettes/Authorizer_refresh.json b/tests/cassettes/Authorizer_refresh.json index 9906719..0cfb8ba 100644 --- a/tests/cassettes/Authorizer_refresh.json +++ b/tests/cassettes/Authorizer_refresh.json @@ -1,45 +1,86 @@ { "http_interactions": [ { - "recorded_at": "2016-02-07T00:06:41", + "recorded_at": "2021-02-22T05:34:53", "request": { "body": { "encoding": "utf-8", "string": "grant_type=refresh_token&refresh_token=" }, "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "Authorization": "Basic ", - "Connection": "keep-alive", - "Content-Length": "66", - "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "prawcore/0.0.1a1" + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Authorization": [ + "Basic " + ], + "Connection": [ + "close" + ], + "Content-Length": [ + "77" + ], + "Content-Type": [ + "application/x-www-form-urlencoded" + ], + "User-Agent": [ + "prawcore:test (by /u/bboe) prawcore/1.5.0" + ] }, "method": "POST", "uri": "https://www.reddit.com/api/v1/access_token" }, "response": { "body": { - "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUDI3NjCyMDPXzQk3cAlILzTPL83NdtX1TzfNCA8rSPIzTzU0UdJRUAKrjy+pLEgFaUpKTSxKLQKJp1YUZBalFsdnggwzNjMw0FFQKk7OhyjLTEnNK8ksqVSqBQDBWguReAAAAA==", "encoding": "UTF-8", - "string": "" + "string": "{\"access_token\": \"0000000-aaaaaaaaaaaaaaaaaaaaaa-0000000\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"refresh_token\": \"aaaaaaa-0000000000000000000000-aaaaaaa\", \"scope\": \"modmail\"}" }, "headers": { - "CF-RAY": "270ad9ad977039a6-PHX", - "Connection": "keep-alive", - "Content-Encoding": "gzip", - "Content-Type": "application/json; charset=UTF-8", - "Date": "Sun, 07 Feb 2016 00:06:41 GMT", - "Server": "cloudflare-nginx", - "Set-Cookie": "__cfduid=d533ca9cde1a0f39d6b51e0a77caee21f1454803601; expires=Mon, 06-Feb-17 00:06:41 GMT; path=/; domain=.reddit.com; HttpOnly", - "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", - "Transfer-Encoding": "chunked", - "X-Moose": "majestic", - "cache-control": "max-age=0, must-revalidate", - "x-content-type-options": "nosniff", - "x-frame-options": "SAMEORIGIN", - "x-xss-protection": "1; mode=block" + "Accept-Ranges": [ + "bytes" + ], + "Connection": [ + "close" + ], + "Content-Length": [ + "181" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Date": [ + "Mon, 22 Feb 2021 05:34:53 GMT" + ], + "Server": [ + "snooserv" + ], + "Set-Cookie": [ + "edgebucket=f5KZ6I9GmO6zC9InB3; Domain=reddit.com; Max-Age=63071999; Path=/; secure" + ], + "Strict-Transport-Security": [ + "max-age=15552000; includeSubDomains; preload" + ], + "Via": [ + "1.1 varnish" + ], + "X-Moose": [ + "majestic" + ], + "cache-control": [ + "max-age=0, must-revalidate" + ], + "x-content-type-options": [ + "nosniff" + ], + "x-frame-options": [ + "SAMEORIGIN" + ], + "x-xss-protection": [ + "1; mode=block" + ] }, "status": { "code": 200, @@ -49,5 +90,5 @@ } } ], - "recorded_with": "betamax/0.5.1" + "recorded_with": "betamax/0.8.1" } \ No newline at end of file diff --git a/tests/test_authorizer.py b/tests/test_authorizer.py index d94d4e9..a215f5e 100644 --- a/tests/test_authorizer.py +++ b/tests/test_authorizer.py @@ -80,7 +80,9 @@ def test_initialize(self): self.assertFalse(authorizer.is_valid()) def test_initialize__with_refresh_token(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) self.assertIsNone(authorizer.access_token) self.assertIsNone(authorizer.scopes) self.assertEqual(REFRESH_TOKEN, authorizer.refresh_token) @@ -95,7 +97,44 @@ def test_initialize__with_untrusted_authenticator(self): self.assertFalse(authorizer.is_valid()) def test_refresh(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) + with Betamax(REQUESTOR).use_cassette("Authorizer_refresh"): + authorizer.refresh() + + self.assertIsNotNone(authorizer.access_token) + self.assertIsInstance(authorizer.scopes, set) + self.assertTrue(len(authorizer.scopes) > 0) + self.assertTrue(authorizer.is_valid()) + + def test_refresh__post_refresh_callback(self): + def callback(authorizer): + self.assertNotEqual(REFRESH_TOKEN, authorizer.refresh_token) + authorizer.refresh_token = "manually_updated" + + authorizer = prawcore.Authorizer( + self.authentication, + post_refresh_callback=callback, + refresh_token=REFRESH_TOKEN, + ) + with Betamax(REQUESTOR).use_cassette("Authorizer_refresh"): + authorizer.refresh() + + self.assertIsNotNone(authorizer.access_token) + self.assertEqual("manually_updated", authorizer.refresh_token) + self.assertIsInstance(authorizer.scopes, set) + self.assertTrue(len(authorizer.scopes) > 0) + self.assertTrue(authorizer.is_valid()) + + def test_refresh__pre_refresh_callback(self): + def callback(authorizer): + self.assertIsNone(authorizer.refresh_token) + authorizer.refresh_token = REFRESH_TOKEN + + authorizer = prawcore.Authorizer( + self.authentication, pre_refresh_callback=callback + ) with Betamax(REQUESTOR).use_cassette("Authorizer_refresh"): authorizer.refresh() @@ -105,7 +144,9 @@ def test_refresh(self): self.assertTrue(authorizer.is_valid()) def test_refresh__with_invalid_token(self): - authorizer = prawcore.Authorizer(self.authentication, "INVALID_TOKEN") + authorizer = prawcore.Authorizer( + self.authentication, refresh_token="INVALID_TOKEN" + ) with Betamax(REQUESTOR).use_cassette( "Authorizer_refresh__with_invalid_token" ): @@ -118,7 +159,9 @@ def test_refresh__without_refresh_token(self): self.assertFalse(authorizer.is_valid()) def test_revoke__access_token_with_refresh_set(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) with Betamax(REQUESTOR).use_cassette( "Authorizer_revoke__access_token_with_refresh_set" ): @@ -149,7 +192,9 @@ def test_revoke__access_token_without_refresh_set(self): self.assertFalse(authorizer.is_valid()) def test_revoke__refresh_token_with_access_set(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) with Betamax(REQUESTOR).use_cassette( "Authorizer_revoke__refresh_token_with_access_set" ): @@ -162,7 +207,9 @@ def test_revoke__refresh_token_with_access_set(self): self.assertFalse(authorizer.is_valid()) def test_revoke__refresh_token_without_access_set(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) with Betamax(REQUESTOR).use_cassette( "Authorizer_revoke__refresh_token_without_access_set" ): @@ -174,7 +221,9 @@ def test_revoke__refresh_token_without_access_set(self): self.assertFalse(authorizer.is_valid()) def test_revoke__without_access_token(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) self.assertRaises( prawcore.InvalidInvocation, authorizer.revoke, only_access=True ) diff --git a/tests/test_sessions.py b/tests/test_sessions.py index c2fa3ee..57275ae 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -39,7 +39,9 @@ def client_authorizer(): authenticator = prawcore.TrustedAuthenticator( REQUESTOR, CLIENT_ID, CLIENT_SECRET ) - authorizer = prawcore.Authorizer(authenticator, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + authenticator, refresh_token=REFRESH_TOKEN + ) authorizer.refresh() return authorizer