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

chore: Replace black, isort and pyupgrade with Ruff #292

Merged
merged 1 commit into from
Aug 15, 2024
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
18 changes: 5 additions & 13 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,13 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace

- repo: https://github.com/asottile/pyupgrade
rev: v3.17.0
hooks:
- id: pyupgrade
args: [--py37-plus]

- repo: https://github.com/psf/black
rev: 24.8.0
hooks:
- id: black

- repo: https://github.com/pycqa/isort
rev: 5.13.2
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.0
hooks:
- id: isort
- id: ruff
args: [ --fix ]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.1
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,11 @@ markers = [
"repo_list: mark a test as using a list of repos in config",
"username_list: mark a test as using a list of usernames in config",
]

[tool.ruff.lint]
ignore = []
select = [
"I", # isort
"UP", # pyupgrade
"FA", # flake8-future-annotations
]
44 changes: 21 additions & 23 deletions tap_github/authenticator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Classes to assist in authenticating to the GitHub API."""

from __future__ import annotations

import logging
import time
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
from os import environ
from random import choice, shuffle
from typing import Any, Dict, List, Optional, Set, Tuple
Expand All @@ -27,16 +29,16 @@ class TokenManager:

def __init__(
self,
token: Optional[str],
rate_limit_buffer: Optional[int] = None,
logger: Optional[Any] = None,
token: str | None,
rate_limit_buffer: int | None = None,
logger: Any | None = None,
):
"""Init TokenManager info."""
self.token = token
self.logger = logger
self.rate_limit = self.DEFAULT_RATE_LIMIT
self.rate_limit_remaining = self.DEFAULT_RATE_LIMIT
self.rate_limit_reset: Optional[datetime] = None
self.rate_limit_reset: datetime | None = None
self.rate_limit_used = 0
self.rate_limit_buffer = (
rate_limit_buffer
Expand Down Expand Up @@ -95,7 +97,7 @@ def has_calls_remaining(self) -> bool:
class PersonalTokenManager(TokenManager):
"""A class to store token rate limiting information."""

def __init__(self, token: str, rate_limit_buffer: Optional[int] = None, **kwargs):
def __init__(self, token: str, rate_limit_buffer: int | None = None, **kwargs):
"""Init PersonalTokenRateLimit info."""
super().__init__(token, rate_limit_buffer=rate_limit_buffer, **kwargs)

Expand Down Expand Up @@ -124,8 +126,8 @@ def generate_jwt_token(
def generate_app_access_token(
github_app_id: str,
github_private_key: str,
github_installation_id: Optional[str] = None,
) -> Tuple[str, datetime]:
github_installation_id: str | None = None,
) -> tuple[str, datetime]:
produced_at = datetime.now()
jwt_token = generate_jwt_token(github_app_id, github_private_key)

Expand All @@ -143,9 +145,7 @@ def generate_app_access_token(

github_installation_id = choice(list_installations)["id"]

url = "https://api.github.com/app/installations/{}/access_tokens".format(
github_installation_id
)
url = f"https://api.github.com/app/installations/{github_installation_id}/access_tokens"
resp = requests.post(url, headers=headers)

if resp.status_code != 201:
Expand All @@ -164,8 +164,8 @@ class AppTokenManager(TokenManager):
def __init__(
self,
env_key: str,
rate_limit_buffer: Optional[int] = None,
expiry_time_buffer: Optional[int] = None,
rate_limit_buffer: int | None = None,
expiry_time_buffer: int | None = None,
**kwargs,
):
if rate_limit_buffer is None:
Expand All @@ -175,15 +175,13 @@ def __init__(
parts = env_key.split(";;")
self.github_app_id = parts[0]
self.github_private_key = (parts[1:2] or [""])[0].replace("\\n", "\n")
self.github_installation_id: Optional[str] = (
parts[2] if len(parts) >= 3 else None
)
self.github_installation_id: str | None = parts[2] if len(parts) >= 3 else None

if expiry_time_buffer is None:
expiry_time_buffer = self.DEFAULT_EXPIRY_BUFFER_MINS
self.expiry_time_buffer = expiry_time_buffer

self.token_expires_at: Optional[datetime] = None
self.token_expires_at: datetime | None = None
self.claim_token()

def claim_token(self):
Expand Down Expand Up @@ -247,14 +245,14 @@ class GitHubTokenAuthenticator(APIAuthenticatorBase):
def get_env():
return dict(environ)

def prepare_tokens(self) -> List[TokenManager]:
def prepare_tokens(self) -> list[TokenManager]:
"""Prep GitHub tokens"""

env_dict = self.get_env()
rate_limit_buffer = self._config.get("rate_limit_buffer", None)
expiry_time_buffer = self._config.get("expiry_time_buffer", None)

personal_tokens: Set[str] = set()
personal_tokens: set[str] = set()
if "auth_token" in self._config:
personal_tokens.add(self._config["auth_token"])
if "additional_auth_tokens" in self._config:
Expand All @@ -274,7 +272,7 @@ def prepare_tokens(self) -> List[TokenManager]:
)
personal_tokens = personal_tokens.union(env_tokens)

token_managers: List[TokenManager] = []
token_managers: list[TokenManager] = []
for token in personal_tokens:
token_manager = PersonalTokenManager(
token, rate_limit_buffer=rate_limit_buffer, logger=self.logger
Expand Down Expand Up @@ -315,9 +313,9 @@ def __init__(self, stream: RESTStream) -> None:
super().__init__(stream=stream)
self.logger: logging.Logger = stream.logger
self.tap_name: str = stream.tap_name
self._config: Dict[str, Any] = dict(stream.config)
self._config: dict[str, Any] = dict(stream.config)
self.token_managers = self.prepare_tokens()
self.active_token: Optional[TokenManager] = (
self.active_token: TokenManager | None = (
choice(self.token_managers) if self.token_managers else None
)

Expand Down Expand Up @@ -348,7 +346,7 @@ def update_rate_limit(
self.active_token.update_rate_limit(response_headers)

@property
def auth_headers(self) -> Dict[str, str]:
def auth_headers(self) -> dict[str, str]:
"""Return a dictionary of auth headers to be applied.

These will be merged with any `http_headers` specified in the stream.
Expand Down
42 changes: 22 additions & 20 deletions tap_github/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""REST client handling, including GitHubStream base class."""

from __future__ import annotations

import collections
import email.utils
import inspect
Expand Down Expand Up @@ -27,7 +29,7 @@ class GitHubRestStream(RESTStream):
"""GitHub Rest stream class."""

MAX_PER_PAGE = 100 # GitHub's limit is 100.
MAX_RESULTS_LIMIT: Optional[int] = None
MAX_RESULTS_LIMIT: int | None = None
DEFAULT_API_BASE_URL = "https://api.github.com"
LOG_REQUEST_METRIC_URLS = True

Expand All @@ -37,7 +39,7 @@ class GitHubRestStream(RESTStream):
# This only has effect on streams whose `replication_key` is `updated_at`.
use_fake_since_parameter = False

_authenticator: Optional[GitHubTokenAuthenticator] = None
_authenticator: GitHubTokenAuthenticator | None = None

@property
def authenticator(self) -> GitHubTokenAuthenticator:
Expand All @@ -50,19 +52,19 @@ def url_base(self) -> str:
return self.config.get("api_url_base", self.DEFAULT_API_BASE_URL)

primary_keys = ["id"]
replication_key: Optional[str] = None
tolerated_http_errors: List[int] = []
replication_key: str | None = None
tolerated_http_errors: list[int] = []

@property
def http_headers(self) -> Dict[str, str]:
def http_headers(self) -> dict[str, str]:
"""Return the http headers needed."""
headers = {"Accept": "application/vnd.github.v3+json"}
headers["User-Agent"] = cast(str, self.config.get("user_agent", "tap-github"))
return headers

def get_next_page_token(
self, response: requests.Response, previous_token: Optional[Any]
) -> Optional[Any]:
self, response: requests.Response, previous_token: Any | None
) -> Any | None:
"""Return a token for identifying next page or None if no more pages."""
if (
previous_token
Expand Down Expand Up @@ -135,8 +137,8 @@ def get_next_page_token(
return (previous_token or 1) + 1

def get_url_params(
self, context: Optional[Dict], next_page_token: Optional[Any]
) -> Dict[str, Any]:
self, context: dict | None, next_page_token: Any | None
) -> dict[str, Any]:
"""Return a dictionary of values to be used in URL parameterization."""
params: dict = {"per_page": self.MAX_PER_PAGE}
if next_page_token:
Expand Down Expand Up @@ -265,7 +267,7 @@ def parse_response(self, response: requests.Response) -> Iterable[dict]:

yield from results

def post_process(self, row: dict, context: Optional[Dict[str, str]] = None) -> dict:
def post_process(self, row: dict, context: dict[str, str] | None = None) -> dict:
"""Add `repo_id` by default to all streams."""
if context is not None and "repo_id" in context:
row["repo_id"] = context["repo_id"]
Expand Down Expand Up @@ -295,8 +297,8 @@ def calculate_sync_cost(
self,
request: requests.PreparedRequest,
response: requests.Response,
context: Optional[dict],
) -> Dict[str, int]:
context: dict | None,
) -> dict[str, int]:
"""Return the cost of the last REST API call."""
return {"rest": 1, "graphql": 0, "search": 0}

Expand Down Expand Up @@ -327,8 +329,8 @@ def parse_response(self, response: requests.Response) -> Iterable[dict]:
yield from extract_jsonpath(self.query_jsonpath, input=resp_json)

def get_next_page_token(
self, response: requests.Response, previous_token: Optional[Any]
) -> Optional[Any]:
self, response: requests.Response, previous_token: Any | None
) -> Any | None:
"""
Return a dict of cursors for identifying next page or None if no more pages.

Expand All @@ -352,7 +354,7 @@ def get_next_page_token(
with_keys=True,
)

has_next_page_indices: List[int] = []
has_next_page_indices: list[int] = []
# Iterate over all the items and filter items with hasNextPage = True.
for key, value in next_page_results.items():
# Check if key is even then add pair to new dictionary
Expand All @@ -369,7 +371,7 @@ def get_next_page_token(

# We leverage previous_token to remember the pagination cursors
# for indices below max_pagination_index.
next_page_cursors: Dict[str, str] = dict()
next_page_cursors: dict[str, str] = dict()
for key, value in (previous_token or {}).items():
# Only keep pagination info for indices below max_pagination_index.
pagination_index = int(str(key).split("_")[1])
Expand All @@ -391,8 +393,8 @@ def get_next_page_token(
return next_page_cursors

def get_url_params(
self, context: Optional[Dict], next_page_token: Optional[Any]
) -> Dict[str, Any]:
self, context: dict | None, next_page_token: Any | None
) -> dict[str, Any]:
"""Return a dictionary of values to be used in URL parameterization."""
params = context.copy() if context else dict()
params["per_page"] = self.MAX_PER_PAGE
Expand All @@ -409,8 +411,8 @@ def calculate_sync_cost(
self,
request: requests.PreparedRequest,
response: requests.Response,
context: Optional[dict],
) -> Dict[str, int]:
context: dict | None,
) -> dict[str, int]:
"""Return the cost of the last graphql API call."""
costgen = extract_jsonpath("$.data.rateLimit.cost", input=response.json())
# calculate_sync_cost is called before the main response parsing.
Expand Down
12 changes: 7 additions & 5 deletions tap_github/organization_streams.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""User Stream types classes for tap-github."""

from __future__ import annotations

from typing import Any, Dict, Iterable, List, Optional

from singer_sdk import typing as th # JSON Schema typing helpers
Expand All @@ -16,15 +18,15 @@ class OrganizationStream(GitHubRestStream):
path = "/orgs/{org}"

@property
def partitions(self) -> Optional[List[Dict]]:
def partitions(self) -> list[dict] | None:
return [{"org": org} for org in self.config["organizations"]]

def get_child_context(self, record: Dict, context: Optional[Dict]) -> dict:
def get_child_context(self, record: dict, context: dict | None) -> dict:
return {
"org": record["login"],
}

def get_records(self, context: Optional[Dict]) -> Iterable[Dict[str, Any]]:
def get_records(self, context: dict | None) -> Iterable[dict[str, Any]]:
"""
Override the parent method to allow skipping API calls
if the stream is deselected and skip_parent_streams is True in config.
Expand Down Expand Up @@ -74,7 +76,7 @@ class TeamsStream(GitHubRestStream):
parent_stream_type = OrganizationStream
state_partitioning_keys = ["org"]

def get_child_context(self, record: Dict, context: Optional[Dict]) -> dict:
def get_child_context(self, record: dict, context: dict | None) -> dict:
new_context = {"team_slug": record["slug"]}
if context:
return {
Expand Down Expand Up @@ -129,7 +131,7 @@ class TeamMembersStream(GitHubRestStream):
parent_stream_type = TeamsStream
state_partitioning_keys = ["team_slug", "org"]

def get_child_context(self, record: Dict, context: Optional[Dict]) -> dict:
def get_child_context(self, record: dict, context: dict | None) -> dict:
new_context = {"username": record["login"]}
if context:
return {
Expand Down
Loading