Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade setup #149

Merged
merged 2 commits into from Sep 25, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Expand Up @@ -28,7 +28,7 @@ body:
- id: credential-check
attributes:
label: |
The `Reddit()` initialization in my code example does not include the following parameters to prevent credential leakage:
The `Reddit()` initialization in my code example does not include the following parameters to prevent credential leakage:
`client_secret`, `password`, or `refresh_token`.
options:
- label: "Yes"
Expand Down
57 changes: 30 additions & 27 deletions .pre-commit-config.yaml
@@ -1,10 +1,36 @@
default_language_version:
python: python3.11
repos:

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-added-large-files
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
exclude: .*\.txt
rev: v4.4.0
- id: mixed-line-ending
args: [ --fix=no ]
- id: name-tests-test
args: [ --pytest-test-first ]
files: ^tests/integration/.*\.py|tests/unit/.*\.py$
- id: sort-simple-yaml
files: ^(\.github/workflows/.*\.ya?ml|\.readthedocs.ya?ml)$
- id: trailing-whitespace

- repo: https://github.com/pappasam/toml-sort
rev: v0.23.1
hooks:
- id: toml-sort-fix
files: ^(.*\.toml)$

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.290
hooks:
- id: ruff
args: [ --exit-non-zero-on-fix, --fix ]
files: ^(prawcore/.*.py)$

- repo: https://github.com/psf/black
hooks:
Expand All @@ -14,28 +40,5 @@ repos:
- repo: https://github.com/LilSpazJoekp/docstrfmt
hooks:
- id: docstrfmt
require_serial: true
rev: v1.5.1

- repo: https://github.com/pycqa/flake8
hooks:
- id: flake8
rev: 6.1.0

- repo: https://github.com/ikamensh/flynt/
hooks:
- id: flynt
args:
- '-ll'
- '1000'
rev: '1.0.1'

- repo: https://github.com/pycqa/isort
hooks:
- id: isort
rev: 5.12.0

- repo: https://github.com/pycqa/pydocstyle
hooks:
- id: pydocstyle
files: prawcore/.*
rev: 6.3.0
Empty file modified examples/caching_requestor.py 100644 → 100755
Empty file.
12 changes: 6 additions & 6 deletions prawcore/__init__.py
@@ -1,8 +1,8 @@
"""prawcore: Low-level communication layer for PRAW 4+."""
""""Low-level communication layer for PRAW 4+."""

import logging

from .auth import ( # noqa
from .auth import (
Authorizer,
DeviceIDAuthorizer,
ImplicitAuthorizer,
Expand All @@ -11,9 +11,9 @@
TrustedAuthenticator,
UntrustedAuthenticator,
)
from .const import __version__ # noqa
from .exceptions import * # noqa
from .requestor import Requestor # noqa
from .sessions import Session, session # noqa
from .const import __version__
from .exceptions import * # noqa: F403
from .requestor import Requestor
from .sessions import Session, session

logging.getLogger(__package__).addHandler(logging.NullHandler())
87 changes: 47 additions & 40 deletions prawcore/auth.py
@@ -1,7 +1,9 @@
"""Provides Authentication and Authorization classes."""
from __future__ import annotations

import time
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Callable, List, Optional, Set, Tuple, Type, Union
from typing import TYPE_CHECKING, Any, Callable

from requests import Request
from requests.status_codes import codes
Expand All @@ -24,9 +26,9 @@ def _auth(self):

def __init__(
self,
requestor: "Requestor",
requestor: Requestor,
client_id: str,
redirect_uri: Optional[str] = None,
redirect_uri: str | None = None,
) -> None:
"""Represent a single authentication to Reddit's API.

Expand All @@ -43,7 +45,9 @@ def __init__(
self.client_id = client_id
self.redirect_uri = redirect_uri

def _post(self, url: str, success_status: int = codes["ok"], **data) -> "Response":
def _post(
self, url: str, success_status: int = codes["ok"], **data: Any
) -> Response:
response = self._requestor.request(
"post",
url,
Expand All @@ -58,7 +62,7 @@ def _post(self, url: str, success_status: int = codes["ok"], **data) -> "Respons
def authorize_url(
self,
duration: str,
scopes: List[str],
scopes: list[str],
state: str,
implicit: bool = False,
) -> str:
Expand Down Expand Up @@ -86,16 +90,16 @@ def authorize_url(

"""
if self.redirect_uri is None:
raise InvalidInvocation("redirect URI not provided")
msg = "redirect URI not provided"
raise InvalidInvocation(msg)
if implicit and not isinstance(self, UntrustedAuthenticator):
raise InvalidInvocation(
"Only UntrustedAuthenticator instances can "
"use the implicit grant flow."
msg = (
"Only UntrustedAuthenticator instances can use the implicit grant flow."
)
raise InvalidInvocation(msg)
if implicit and duration != "temporary":
raise InvalidInvocation(
"The implicit grant flow only supports temporary access tokens."
)
msg = "The implicit grant flow only supports temporary access tokens."
raise InvalidInvocation(msg)

params = {
"client_id": self.client_id,
Expand All @@ -107,9 +111,9 @@ def authorize_url(
}
url = self._requestor.reddit_url + const.AUTHORIZATION_PATH
request = Request("GET", url, params=params)
return request.prepare().url # type: ignore
return request.prepare().url

def revoke_token(self, token: str, token_type: Optional[str] = None) -> None:
def revoke_token(self, token: str, token_type: str | None = None) -> None:
"""Ask Reddit to revoke the provided token.

:param token: The access or refresh token to revoke.
Expand All @@ -128,7 +132,7 @@ def revoke_token(self, token: str, token_type: Optional[str] = None) -> None:
class BaseAuthorizer(ABC):
"""Superclass for OAuth2 authorization tokens and scopes."""

AUTHENTICATOR_CLASS: Union[Tuple, Type] = BaseAuthenticator
AUTHENTICATOR_CLASS: tuple | type = BaseAuthenticator

def __init__(self, authenticator: BaseAuthenticator) -> None:
"""Represent a single authorization to Reddit's API.
Expand All @@ -142,10 +146,10 @@ def __init__(self, authenticator: BaseAuthenticator) -> None:

def _clear_access_token(self) -> None:
self._expiration_timestamp: float
self.access_token: Optional[str] = None
self.scopes: Optional[Set[str]] = None
self.access_token: str | None = None
self.scopes: set[str] | None = None

def _request_token(self, **data) -> None:
def _request_token(self, **data: Any) -> None:
url = self._authenticator._requestor.reddit_url + const.ACCESS_TOKEN_PATH
pre_request_time = time.time()
response = self._authenticator._post(url=url, **data)
Expand Down Expand Up @@ -186,7 +190,8 @@ def is_valid(self) -> bool:
def revoke(self) -> None:
"""Revoke the current Authorization."""
if self.access_token is None:
raise InvalidInvocation("no token available to revoke")
msg = "no token available to revoke"
raise InvalidInvocation(msg)

self._authenticator.revoke_token(self.access_token, "access_token")
self._clear_access_token()
Expand All @@ -199,10 +204,10 @@ class TrustedAuthenticator(BaseAuthenticator):

def __init__(
self,
requestor: "Requestor",
requestor: Requestor,
client_id: str,
client_secret: str,
redirect_uri: Optional[str] = None,
redirect_uri: str | None = None,
) -> None:
"""Represent a single authentication to Reddit's API.

Expand All @@ -216,17 +221,17 @@ def __init__(
(default: ``None``).

"""
super(TrustedAuthenticator, self).__init__(requestor, client_id, redirect_uri)
super().__init__(requestor, client_id, redirect_uri)
self.client_secret = client_secret

def _auth(self) -> Tuple[str, str]:
def _auth(self) -> tuple[str, str]:
return self.client_id, self.client_secret


class UntrustedAuthenticator(BaseAuthenticator):
"""Store OAuth2 authentication credentials for installed applications."""

def _auth(self) -> Tuple[str, str]:
def _auth(self) -> tuple[str, str]:
return self.client_id, ""


Expand All @@ -237,9 +242,9 @@ def __init__(
self,
authenticator: BaseAuthenticator,
*,
post_refresh_callback: Optional[Callable[["Authorizer"], None]] = None,
pre_refresh_callback: Optional[Callable[["Authorizer"], None]] = None,
refresh_token: Optional[str] = None,
post_refresh_callback: Callable[[Authorizer], None] | None = None,
pre_refresh_callback: Callable[[Authorizer], None] | None = None,
refresh_token: str | None = None,
) -> None:
"""Represent a single authorization to Reddit's API.

Expand All @@ -257,7 +262,7 @@ def __init__(
:param refresh_token: Enables the ability to refresh the authorization.

"""
super(Authorizer, self).__init__(authenticator)
super().__init__(authenticator)
self._post_refresh_callback = post_refresh_callback
self._pre_refresh_callback = pre_refresh_callback
self.refresh_token = refresh_token
Expand All @@ -270,7 +275,8 @@ def authorize(self, code: str) -> None:

"""
if self._authenticator.redirect_uri is None:
raise InvalidInvocation("redirect URI not provided")
msg = "redirect URI not provided"
raise InvalidInvocation(msg)
self._request_token(
code=code,
grant_type="authorization_code",
Expand All @@ -282,7 +288,8 @@ def refresh(self) -> None:
if self._pre_refresh_callback:
self._pre_refresh_callback(self)
if self.refresh_token is None:
raise InvalidInvocation("refresh token not provided")
msg = "refresh token not provided"
raise InvalidInvocation(msg)
self._request_token(
grant_type="refresh_token", refresh_token=self.refresh_token
)
Expand All @@ -300,7 +307,7 @@ def revoke(self, only_access: bool = False) -> None:

"""
if only_access or self.refresh_token is None:
super(Authorizer, self).revoke()
super().revoke()
else:
self._authenticator.revoke_token(self.refresh_token, "refresh_token")
self._clear_access_token()
Expand Down Expand Up @@ -333,7 +340,7 @@ def __init__(
from Reddit in the callback to the authenticator's redirect uri.

"""
super(ImplicitAuthorizer, self).__init__(authenticator)
super().__init__(authenticator)
self._expiration_timestamp = time.time() + expires_in
self.access_token = access_token
self.scopes = set(scope.split(" "))
Expand All @@ -352,7 +359,7 @@ class ReadOnlyAuthorizer(Authorizer):
def __init__(
self,
authenticator: BaseAuthenticator,
scopes: Optional[List[str]] = None,
scopes: list[str] | None = None,
) -> None:
"""Represent a ReadOnly authorization to Reddit's API.

Expand Down Expand Up @@ -384,10 +391,10 @@ class ScriptAuthorizer(Authorizer):
def __init__(
self,
authenticator: BaseAuthenticator,
username: Optional[str],
password: Optional[str],
two_factor_callback: Optional[Callable] = None,
scopes: Optional[List[str]] = None,
username: str | None,
password: str | None,
two_factor_callback: Callable | None = None,
scopes: list[str] | None = None,
) -> None:
"""Represent a single personal-use authorization to Reddit's API.

Expand All @@ -401,7 +408,7 @@ def __init__(
``None``). The scope ``"*"`` is requested when the default argument is used.

"""
super(ScriptAuthorizer, self).__init__(authenticator)
super().__init__(authenticator)
self._password = password
self._scopes = scopes
self._two_factor_callback = two_factor_callback
Expand Down Expand Up @@ -436,8 +443,8 @@ class DeviceIDAuthorizer(BaseAuthorizer):
def __init__(
self,
authenticator: BaseAuthenticator,
device_id: Optional[str] = None,
scopes: Optional[List[str]] = None,
device_id: str | None = None,
scopes: list[str] | None = None,
) -> None:
"""Represent an app-only OAuth2 authorization for 'installed' apps.

Expand Down
12 changes: 8 additions & 4 deletions prawcore/const.py
Expand Up @@ -3,8 +3,12 @@

__version__ = "2.3.0"

ACCESS_TOKEN_PATH = "/api/v1/access_token"
AUTHORIZATION_PATH = "/api/v1/authorize"
REVOKE_TOKEN_PATH = "/api/v1/revoke_token"
TIMEOUT = float(os.environ.get("prawcore_timeout", 16))
ACCESS_TOKEN_PATH = "/api/v1/access_token" # noqa: S105
AUTHORIZATION_PATH = "/api/v1/authorize" # noqa: S105
REVOKE_TOKEN_PATH = "/api/v1/revoke_token" # noqa: S105
TIMEOUT = float(
os.environ.get(
"PRAWCORE_TIMEOUT", os.environ.get("prawcore_timeout", 16) # noqa: SIM112
)
)
WINDOW_SIZE = 600