Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/check-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ jobs:
run: |
task test-container
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_WATCHER_INTEGRATION_TEST_TOKEN }}
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
18 changes: 9 additions & 9 deletions backend/lib/github/clients/gql.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def to_dataclass(self) -> typing.Any:

@dataclasses.dataclass(frozen=True)
class GetRepositoriesRequest(BaseRequest):
owner: str
owner: github_models.OwnerName
limit: int = 100
after: str | None = None

Expand Down Expand Up @@ -78,9 +78,9 @@ class GetRepositoriesResponse(BaseResponse):
class Search(BaseModel):
class Repository(BaseModel):
class Owner(BaseModel):
login: str
login: github_models.OwnerName

name: str
name: github_models.RepositoryName
owner: Owner

class PageInfo(BaseModel):
Expand All @@ -104,8 +104,8 @@ def to_dataclass(self) -> list[github_models.Repository]:

@dataclasses.dataclass(frozen=True)
class GetRepositoryIssuesRequest(BaseRequest):
owner: str
repository: str
owner: github_models.OwnerName
repository: github_models.RepositoryName
created_after: datetime.datetime
limit: int = 100

Expand Down Expand Up @@ -150,7 +150,7 @@ class GetRepositoryIssuesResponse(BaseResponse):
class Search(BaseModel):
class Issue(BaseModel):
class Author(BaseModel):
login: str
login: github_models.UserLogin

id: str
url: str
Expand Down Expand Up @@ -179,8 +179,8 @@ def to_dataclass(self) -> list[github_models.Issue]:

@dataclasses.dataclass(frozen=True)
class GetRepositoryPRsRequest(BaseRequest):
owner: str
repository: str
owner: github_models.OwnerName
repository: github_models.RepositoryName
created_after: datetime.datetime
limit: int = 100

Expand Down Expand Up @@ -225,7 +225,7 @@ class GetRepositoryPRsResponse(BaseResponse):
class Search(BaseModel):
class PR(BaseModel):
class Author(BaseModel):
login: str
login: github_models.UserLogin

id: str
url: str
Expand Down
100 changes: 94 additions & 6 deletions backend/lib/github/clients/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import typing

import aiohttp
import pydantic

import lib.github.models as github_models
import lib.utils.pydantic as pydantic_utils
Expand All @@ -31,6 +32,7 @@ def to_dataclass(self) -> typing.Any:
raise NotImplementedError


# https://docs.github.com/en/rest/actions/workflow-runs#list-workflow-runs-for-a-repository
@dataclasses.dataclass(frozen=True)
class GetRepositoryWorkflowRunsRequest(BaseRequest):
owner: str
Expand Down Expand Up @@ -84,11 +86,55 @@ def to_dataclass(self) -> list[github_models.WorkflowRun]:
]


# https://docs.github.com/en/rest/teams/members#list-team-members
@dataclasses.dataclass(frozen=True)
class GetOrganizationTeamMembersRequest(BaseRequest):
owner: github_models.OwnerName
team_slug: github_models.TeamSlug

role: typing.Literal["member", "maintainer", "all"] = "all"
per_page: int = 100
page: int = 1

@property
def method(self) -> str:
return "GET"

@property
def url(self) -> str:
return f"https://api.github.com/orgs/{self.owner}/teams/{self.team_slug}/members"

@property
def params(self) -> dict[str, typing.Any]:
return {
"role": self.role,
"per_page": self.per_page,
"page": self.page,
}


class _TeamMember(pydantic_utils.BaseModel):
login: github_models.UserLogin


class GetOrganizationTeamMembersResponse(BaseResponse, pydantic.RootModel[list[_TeamMember]]):
root: list[_TeamMember]

def to_dataclass(self) -> list[github_models.UserLogin]:
return [member.login for member in self.root]


@dataclasses.dataclass(frozen=True)
class RestGithubClient:
aiohttp_client: aiohttp.ClientSession
token: str

class BaseError(Exception): ...

class NotFoundError(BaseError): ...

class UnknownResponseError(BaseError): ...

@classmethod
def from_token(cls, token: str) -> typing.Self:
aiohttp_client = aiohttp.ClientSession()
Expand All @@ -97,11 +143,10 @@ def from_token(cls, token: str) -> typing.Self:
async def dispose(self) -> None:
await self.aiohttp_client.close()

async def _request[ResponseT: BaseResponse](
async def _request(
self,
request: BaseRequest,
response_model: type[ResponseT],
) -> ResponseT:
) -> typing.Any:
headers = {
"Authorization": f"Bearer {self.token}",
"Accept": "application/vnd.github.v3+json",
Expand All @@ -114,14 +159,19 @@ async def _request[ResponseT: BaseResponse](
headers=headers,
) as response:
response.raise_for_status()
data = await response.json()
return response_model.model_validate(data)
return await response.json()

async def _get_repository_workflow_runs(
self,
request: GetRepositoryWorkflowRunsRequest,
) -> GetRepositoryWorkflowRunsResponse:
return await self._request(request, GetRepositoryWorkflowRunsResponse)
try:
raw_data = await self._request(request)
except aiohttp.ClientResponseError as e: # pragma: no cover
logger.exception("Unknown response error")
raise self.UnknownResponseError from e

return GetRepositoryWorkflowRunsResponse.model_validate(raw_data)

async def get_repository_workflow_runs(
self,
Expand All @@ -147,8 +197,46 @@ async def get_repository_workflow_runs(

page += 1

async def _get_organization_team_members(
self,
request: GetOrganizationTeamMembersRequest,
) -> GetOrganizationTeamMembersResponse:
try:
raw_data = await self._request(request)
return GetOrganizationTeamMembersResponse.model_validate(raw_data)
except aiohttp.ClientResponseError as e:
if e.status == 404:
logger.info(e.message)
raise self.NotFoundError from e

logger.exception("Unknown response error") # pragma: no cover
raise self.UnknownResponseError from e # pragma: no cover

async def get_organization_team_members(
self,
request: GetOrganizationTeamMembersRequest,
) -> typing.AsyncGenerator[github_models.UserLogin, None]:
page = request.page
while True:
response = await self._get_organization_team_members(
request=GetOrganizationTeamMembersRequest(
owner=request.owner,
team_slug=request.team_slug,
page=page,
per_page=request.per_page,
),
)
if not response.root:
return

for member in response.to_dataclass():
yield member

page += 1


__all__ = [
"GetOrganizationTeamMembersRequest",
"GetRepositoryWorkflowRunsRequest",
"RestGithubClient",
]
28 changes: 22 additions & 6 deletions backend/lib/github/models.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import dataclasses
import datetime

type UserLogin = str
type OrganizationName = str
type OwnerName = UserLogin | OrganizationName
type RepositoryName = str
type TeamSlug = str

type WorkflowRunStatus = str
type WorkflowRunConclusion = str


@dataclasses.dataclass(frozen=True)
class Repository:
owner: str
name: str
owner: OwnerName
name: RepositoryName


@dataclasses.dataclass(frozen=True)
class Issue:
id: str
author: str | None
author: UserLogin | None
url: str
title: str
body: str
Expand All @@ -21,7 +30,7 @@ class Issue:
@dataclasses.dataclass(frozen=True)
class PullRequest:
id: str
author: str | None
author: UserLogin | None
url: str
title: str
body: str
Expand All @@ -33,14 +42,21 @@ class WorkflowRun:
id: int
name: str
url: str
status: str
conclusion: str | None
status: WorkflowRunStatus
conclusion: WorkflowRunConclusion | None
created_at: datetime.datetime


__all__ = [
"Issue",
"OrganizationName",
"OwnerName",
"PullRequest",
"Repository",
"RepositoryName",
"TeamSlug",
"UserLogin",
"WorkflowRun",
"WorkflowRunConclusion",
"WorkflowRunStatus",
]
Loading