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