Skip to content

Commit 4d0321a

Browse files
asottile-sentryandrewshie-sentry
authored andcommitted
ref: fix typing for integrations.github.client (#97501)
<!-- Describe your PR here. -->
1 parent ab68219 commit 4d0321a

File tree

4 files changed

+23
-14
lines changed

4 files changed

+23
-14
lines changed

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ module = [
113113
"sentry.api.paginator",
114114
"sentry.db.postgres.base",
115115
"sentry.eventstore.models",
116-
"sentry.integrations.github.client",
117116
"sentry.integrations.pagerduty.actions.form",
118117
"sentry.integrations.slack.message_builder.notifications.issues",
119118
"sentry.integrations.slack.webhooks.event",

src/sentry/integrations/github/blame.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from collections.abc import Mapping, Sequence
55
from dataclasses import asdict
66
from datetime import datetime, timezone
7-
from typing import Any, TypedDict
7+
from typing import Any, TypedDict, TypeIs
88

99
from django.utils.datastructures import OrderedSet
1010

@@ -13,6 +13,7 @@
1313
FileBlameInfo,
1414
SourceLineInfo,
1515
)
16+
from sentry.shared_integrations.response.mapping import MappingApiResponse
1617

1718
logger = logging.getLogger("sentry.integrations.github")
1819

@@ -49,6 +50,11 @@ class GitHubGraphQlResponse(TypedDict):
4950
errors: list[dict[str, str]]
5051

5152

53+
def is_graphql_response(response: object) -> TypeIs[GitHubGraphQlResponse]:
54+
# TODO: should probably narrow this better
55+
return isinstance(response, MappingApiResponse) and ("data" in response or "errors" in response)
56+
57+
5258
FilePathMapping = dict[str, dict[str, OrderedSet]]
5359
GitHubRepositoryResponse = dict[str, GitHubRefResponse]
5460

src/sentry/integrations/github/client.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
create_blame_query,
1515
extract_commits_from_blame_response,
1616
generate_file_path_mapping,
17+
is_graphql_response,
1718
)
1819
from sentry.integrations.github.utils import get_jwt, get_next_link
1920
from sentry.integrations.models.integration import Integration
@@ -30,7 +31,6 @@
3031
from sentry.models.repository import Repository
3132
from sentry.shared_integrations.client.proxy import IntegrationProxyClient
3233
from sentry.shared_integrations.exceptions import ApiError, ApiRateLimitedError
33-
from sentry.shared_integrations.response.mapping import MappingApiResponse
3434
from sentry.silo.base import control_silo_function
3535
from sentry.utils import metrics
3636

@@ -210,7 +210,7 @@ def get_access_token(self) -> AccessTokenData | None:
210210
access_token: str | None = self.integration.metadata.get("access_token")
211211
expires_at: str | None = self.integration.metadata.get("expires_at")
212212
is_expired = (
213-
bool(expires_at) and datetime.fromisoformat(expires_at).replace(tzinfo=None) < now
213+
expires_at is not None and datetime.fromisoformat(expires_at).replace(tzinfo=None) < now
214214
)
215215
should_refresh = not access_token or not expires_at or is_expired
216216

@@ -230,7 +230,7 @@ def authorize_request(self, prepared_request: PreparedRequest) -> PreparedReques
230230
integration: RpcIntegration | Integration | None = None
231231
if hasattr(self, "integration"):
232232
integration = self.integration
233-
elif hasattr(self, "org_integration_id"):
233+
elif self.org_integration_id is not None:
234234
integration = Integration.objects.filter(
235235
organizationintegration__id=self.org_integration_id,
236236
provider=EXTERNAL_PROVIDERS[ExternalProviders.GITHUB],
@@ -430,7 +430,7 @@ def get_repos(self) -> list[dict[str, Any]]:
430430
It uses page_size from the base class to specify how many items per page.
431431
The upper bound of requests is controlled with self.page_number_limit to prevent infinite requests.
432432
"""
433-
return self.get_with_pagination("/installation/repositories", response_key="repositories")
433+
return self._get_with_pagination("/installation/repositories", response_key="repositories")
434434

435435
def search_repositories(self, query: bytes) -> Mapping[str, Sequence[Any]]:
436436
"""
@@ -445,9 +445,9 @@ def get_assignees(self, repo: str) -> Sequence[Any]:
445445
"""
446446
https://docs.github.com/en/rest/issues/assignees#list-assignees
447447
"""
448-
return self.get_with_pagination(f"/repos/{repo}/assignees")
448+
return self._get_with_pagination(f"/repos/{repo}/assignees")
449449

450-
def get_with_pagination(
450+
def _get_with_pagination(
451451
self, path: str, response_key: str | None = None, page_number_limit: int | None = None
452452
) -> list[Any]:
453453
"""
@@ -549,7 +549,7 @@ def get_labels(self, owner: str, repo: str) -> list[Any]:
549549
Fetches all labels for a repository.
550550
https://docs.github.com/en/rest/issues/labels#list-labels-for-a-repository
551551
"""
552-
return self.get_with_pagination(f"/repos/{owner}/{repo}/labels")
552+
return self._get_with_pagination(f"/repos/{owner}/{repo}/labels")
553553

554554
def check_file(self, repo: Repository, path: str, version: str | None) -> object | None:
555555
return self.head_cached(path=f"/repos/{repo.name}/contents/{path}", params={"ref": version})
@@ -580,7 +580,7 @@ def get_file(
580580

581581
def get_blame_for_files(
582582
self, files: Sequence[SourceLineInfo], extra: dict[str, Any]
583-
) -> Sequence[FileBlameInfo]:
583+
) -> list[FileBlameInfo]:
584584
log_info = {
585585
**extra,
586586
"provider": IntegrationProviderSlug.GITHUB,
@@ -633,7 +633,7 @@ def get_blame_for_files(
633633
else:
634634
self.set_cache(cache_key, response, 60)
635635

636-
if not isinstance(response, MappingApiResponse):
636+
if not is_graphql_response(response):
637637
raise ApiError("Response is not JSON")
638638

639639
errors = response.get("errors", [])
@@ -662,6 +662,10 @@ def get_blame_for_files(
662662
)
663663

664664

665+
class _IntegrationIdParams(TypedDict, total=False):
666+
integration_id: int
667+
668+
665669
class GitHubApiClient(GitHubBaseClient):
666670
def __init__(
667671
self,
@@ -671,7 +675,7 @@ def __init__(
671675
logging_context: Mapping[str, Any] | None = None,
672676
) -> None:
673677
self.integration = integration
674-
kwargs = {}
678+
kwargs: _IntegrationIdParams = {}
675679
if hasattr(self.integration, "id"):
676680
kwargs["integration_id"] = integration.id
677681

tests/sentry/integrations/github/test_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def test_get_with_pagination(self, get_jwt) -> None:
196196
# The code only cares about the `next` value which is not included here
197197
headers={"link": f'<{url}&page=1>; rel="first", <{url}&page=3>; rel="prev"'},
198198
)
199-
self.github_client.get_with_pagination(f"/repos/{self.repo.name}/assignees")
199+
self.github_client._get_with_pagination(f"/repos/{self.repo.name}/assignees")
200200
assert len(responses.calls) == 4
201201
assert responses.calls[0].response.status_code == 200
202202

@@ -207,7 +207,7 @@ def test_get_with_pagination_only_one_page(self, get_jwt) -> None:
207207

208208
# No link in the headers because there are no more pages
209209
responses.add(method=responses.GET, url=url, json={}, headers={})
210-
self.github_client.get_with_pagination(f"/repos/{self.repo.name}/assignees")
210+
self.github_client._get_with_pagination(f"/repos/{self.repo.name}/assignees")
211211
assert len(responses.calls) == 1
212212
assert responses.calls[0].response.status_code == 200
213213

0 commit comments

Comments
 (0)