Skip to content

Commit

Permalink
Make the GithubClient a singleton
Browse files Browse the repository at this point in the history
This should make adding new screens and views easier since we won't have
to thread the existing GithubClient through the entire UI

This addresses #6
  • Loading branch information
gizmo385 committed Sep 9, 2024
1 parent 049fddb commit 16dda47
Show file tree
Hide file tree
Showing 15 changed files with 146 additions and 153 deletions.
3 changes: 1 addition & 2 deletions lazy_github/lib/github/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

import httpx

from lazy_github.lib.constants import CONFIG_FOLDER
from lazy_github.lib.constants import DEVICE_CODE_GRANT_TYPE, LAZY_GITHUB_CLIENT_ID
from lazy_github.lib.constants import CONFIG_FOLDER, DEVICE_CODE_GRANT_TYPE, LAZY_GITHUB_CLIENT_ID

# Auth and client globals
_AUTHENTICATION_CACHE_LOCATION = CONFIG_FOLDER / "auth.text"
Expand Down
43 changes: 42 additions & 1 deletion lazy_github/lib/github/client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from typing import Optional
from collections.abc import Coroutine
from typing import Any, Optional

import hishel
from httpx import URL, Response
from httpx._types import HeaderTypes, QueryParamTypes

from lazy_github.lib.config import Config
from lazy_github.lib.constants import JSON_CONTENT_ACCEPT_TYPE
from lazy_github.lib.github.auth import token
from lazy_github.models.github import User


Expand All @@ -30,3 +34,40 @@ async def user(self) -> User:
response = await self.get("/user", headers=self.headers_with_auth_accept())
self._user = User(**response.json())
return self._user


_GITHUB_CLIENT: GithubClient | None = None


def _get_client() -> GithubClient:
global _GITHUB_CLIENT
if not _GITHUB_CLIENT:
_GITHUB_CLIENT = GithubClient(Config.load_config(), token())
return _GITHUB_CLIENT


def get(
url: URL | str,
headers: HeaderTypes | None = None,
params: QueryParamTypes | None = None,
follow_redirects: bool = True,
) -> Coroutine[Any, Any, Response]:
return _get_client().get(url, headers=headers, params=params, follow_redirects=follow_redirects)


def post(url: URL | str, json: Any | None = None, headers: HeaderTypes | None = None) -> Coroutine[Any, Any, Response]:
return _get_client().post(url, json=json, headers=headers)


def patch(url: URL | str, json: Any | None, headers: HeaderTypes | None = None) -> Coroutine[Any, Any, Response]:
return _get_client().patch(url, json=json, headers=headers)


def headers_with_auth_accept(
accept: str = JSON_CONTENT_ACCEPT_TYPE, cache_duration: Optional[int] = None
) -> dict[str, str]:
return _get_client().headers_with_auth_accept(accept, cache_duration)


async def user() -> User:
return await _get_client().user()
28 changes: 13 additions & 15 deletions lazy_github/lib/github/issues.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import TypedDict, Unpack

import lazy_github.lib.github.client as github
from lazy_github.lib.config import Config
from lazy_github.lib.constants import IssueOwnerFilter, IssueStateFilter
from lazy_github.lib.github.client import GithubClient
from lazy_github.models.github import Issue, IssueComment, PartialPullRequest, Repository


Expand All @@ -10,17 +11,16 @@ class UpdateIssuePayload(TypedDict):
body: str | None


async def list_issues(
client: GithubClient, repo: Repository, state: IssueStateFilter, owner: IssueOwnerFilter
) -> list[Issue]:
async def list_issues(repo: Repository, state: IssueStateFilter, owner: IssueOwnerFilter) -> list[Issue]:
"""Fetch issues (included pull requests) from the repo matching the state/owner filters"""
query_params = {"state": str(state).lower()}
if owner == IssueOwnerFilter.MINE:
user = await client.user()
user = await github.user()
query_params["creator"] = user.login

headers = client.headers_with_auth_accept(cache_duration=client.config.cache.list_issues_ttl)
response = await client.get(f"/repos/{repo.owner.login}/{repo.name}/issues", headers=headers, params=query_params)
config = Config.load_config()
headers = github.headers_with_auth_accept(cache_duration=config.cache.list_issues_ttl)
response = await github.get(f"/repos/{repo.owner.login}/{repo.name}/issues", headers=headers, params=query_params)
response.raise_for_status()
result: list[Issue] = []
for issue in response.json():
Expand All @@ -31,25 +31,23 @@ async def list_issues(
return result


async def get_comments(client: GithubClient, issue: Issue) -> list[IssueComment]:
response = await client.get(issue.comments_url, headers=client.headers_with_auth_accept())
async def get_comments(issue: Issue) -> list[IssueComment]:
response = await github.get(issue.comments_url, headers=github.headers_with_auth_accept())
response.raise_for_status()
return [IssueComment(**i) for i in response.json()]


async def create_comment(client: GithubClient, issue: Issue, comment_body: str) -> IssueComment:
async def create_comment(issue: Issue, comment_body: str) -> IssueComment:
url = f"/repos/{issue.repo.owner.login}/{issue.repo.name}/issues/{issue.number}/comments"
body = {"body": comment_body}
response = await client.post(url, json=body, headers=client.headers_with_auth_accept())
response = await github.post(url, json=body, headers=github.headers_with_auth_accept())
response.raise_for_status()
return IssueComment(**response.json())


async def update_issue(
client: GithubClient, issue_to_update: Issue, **updated_fields: Unpack[UpdateIssuePayload]
) -> Issue:
async def update_issue(issue_to_update: Issue, **updated_fields: Unpack[UpdateIssuePayload]) -> Issue:
repo = issue_to_update.repo
url = f"/repos/{repo.owner.login}/{repo.name}/issues/{issue_to_update.number}"
response = await client.patch(url, json=updated_fields, headers=client.headers_with_auth_accept())
response = await github.patch(url, json=updated_fields, headers=github.headers_with_auth_accept())
response.raise_for_status()
return Issue(**response.json(), repo=repo)
36 changes: 19 additions & 17 deletions lazy_github/lib/github/pull_requests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from lazy_github.lib.config import Config
from lazy_github.lib.constants import DIFF_CONTENT_ACCEPT_TYPE
from lazy_github.lib.github.client import GithubClient
import lazy_github.lib.github.client as github
from lazy_github.lib.github.issues import list_issues
from lazy_github.models.github import (
FullPullRequest,
Expand All @@ -11,55 +12,56 @@
)


async def list_for_repo(client: GithubClient, repo: Repository) -> list[PartialPullRequest]:
async def list_for_repo(repo: Repository) -> list[PartialPullRequest]:
"""Lists the pull requests associated with the specified repo"""
state_filter = client.config.pull_requests.state_filter
owner_filter = client.config.pull_requests.owner_filter
issues = await list_issues(client, repo, state_filter, owner_filter)
config = Config.load_config()
state_filter = config.pull_requests.state_filter
owner_filter = config.pull_requests.owner_filter
issues = await list_issues(repo, state_filter, owner_filter)
return [i for i in issues if isinstance(i, PartialPullRequest)]


async def get_full_pull_request(client: GithubClient, partial_pr: PartialPullRequest) -> FullPullRequest:
async def get_full_pull_request(partial_pr: PartialPullRequest) -> FullPullRequest:
"""Converts a partial pull request into a full pull request"""
url = f"/repos/{partial_pr.repo.owner.login}/{partial_pr.repo.name}/pulls/{partial_pr.number}"
response = await client.get(url, headers=client.headers_with_auth_accept())
response = await github.get(url, headers=github.headers_with_auth_accept())
response.raise_for_status()
return FullPullRequest(**response.json(), repo=partial_pr.repo)


async def get_diff(client: GithubClient, pr: FullPullRequest) -> str:
async def get_diff(pr: FullPullRequest) -> str:
"""Fetches the raw diff for an individual pull request"""
headers = client.headers_with_auth_accept(DIFF_CONTENT_ACCEPT_TYPE)
response = await client.get(pr.diff_url, headers=headers, follow_redirects=True)
headers = github.headers_with_auth_accept(DIFF_CONTENT_ACCEPT_TYPE)
response = await github.get(pr.diff_url, headers=headers, follow_redirects=True)
response.raise_for_status()
return response.text


async def get_review_comments(client: GithubClient, pr: FullPullRequest, review: Review) -> list[ReviewComment]:
async def get_review_comments(pr: FullPullRequest, review: Review) -> list[ReviewComment]:
url = f"/repos/{pr.repo.owner.login}/{pr.repo.name}/pulls/{pr.number}/reviews/{review.id}/comments"
response = await client.get(url, headers=client.headers_with_auth_accept())
response = await github.get(url, headers=github.headers_with_auth_accept())
response.raise_for_status()
return [ReviewComment(**c) for c in response.json()]


async def get_reviews(client: GithubClient, pr: FullPullRequest, with_comments: bool = True) -> list[Review]:
async def get_reviews(pr: FullPullRequest, with_comments: bool = True) -> list[Review]:
url = url = f"/repos/{pr.repo.owner.login}/{pr.repo.name}/pulls/{pr.number}/reviews"
response = await client.get(url, headers=client.headers_with_auth_accept())
response = await github.get(url, headers=github.headers_with_auth_accept())
response.raise_for_status()
reviews: list[Review] = []
for raw_review in response.json():
review = Review(**raw_review)
if with_comments:
review.comments = await get_review_comments(client, pr, review)
review.comments = await get_review_comments(pr, review)
reviews.append(review)
return reviews


async def reply_to_review_comment(
client: GithubClient, repo: Repository, issue: Issue, comment: ReviewComment, comment_body: str
repo: Repository, issue: Issue, comment: ReviewComment, comment_body: str
) -> ReviewComment:
url = f"/repos/{repo.owner.login}/{repo.name}/pulls/{issue.number}/comments/{comment.id}/replies"
response = await client.post(url, headers=client.headers_with_auth_accept(), json={"body": comment_body})
response = await github.post(url, headers=github.headers_with_auth_accept(), json={"body": comment_body})
response.raise_for_status()
return ReviewComment(**response.json())

Expand Down
13 changes: 7 additions & 6 deletions lazy_github/lib/github/repositories.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from functools import partial
from typing import Literal

from lazy_github.lib.github.client import GithubClient
import lazy_github.lib.github.client as github
from lazy_github.lib.config import Config
from lazy_github.models.github import Repository

RepoTypeFilter = Literal["all"] | Literal["owner"] | Literal["member"]
Expand All @@ -13,17 +14,18 @@


async def _list_for_page(
client: GithubClient,
repo_types: RepoTypeFilter,
sort: RepositorySortKey,
direction: SortDirection,
per_page: int,
page: int,
) -> tuple[list[Repository], bool]:
"""Retrieves a single page of Github repos matching the specified criteria"""
headers = client.headers_with_auth_accept(cache_duration=client.config.cache.list_repos_ttl)
config = Config.load_config()
headers = github.headers_with_auth_accept(cache_duration=config.cache.list_repos_ttl)
query_params = {"type": repo_types, "direction": direction, "sort": sort, "page": page, "per_page": per_page}
response = await client.get("/user/repos", headers=headers, params=query_params)

response = await github.get("/user/repos", headers=headers, params=query_params)
response.raise_for_status()

link_header = response.headers.get("link")
Expand All @@ -33,7 +35,6 @@ async def _list_for_page(


async def _list(
client: GithubClient,
repo_types: RepoTypeFilter,
sort: RepositorySortKey = "full_name",
direction: SortDirection = "asc",
Expand All @@ -43,7 +44,7 @@ async def _list(
repositories: list[Repository] = []

for page in range(1, MAX_PAGES):
repos_in_page, more_pages = await _list_for_page(client, repo_types, sort, direction, per_page, page)
repos_in_page, more_pages = await _list_for_page(repo_types, sort, direction, per_page, page)
repositories.extend(repos_in_page)
if not more_pages:
break
Expand Down
12 changes: 5 additions & 7 deletions lazy_github/ui/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from textual import log
from textual.app import App

from lazy_github.lib.config import Config
from lazy_github.lib.github.auth import GithubAuthenticationRequired, token
from lazy_github.lib.github.client import GithubClient
from lazy_github.lib.github.auth import GithubAuthenticationRequired
import lazy_github.lib.github.client as github
from lazy_github.ui.screens.auth import AuthenticationModal
from lazy_github.ui.screens.primary import LazyGithubMainScreen

Expand All @@ -12,11 +11,10 @@ class LazyGithub(App):
BINDINGS = [("d", "toggle_dark", "Toggle dark mode"), ("q", "quit", "Quit"), ("ctrl+p", "command_palette")]

async def authenticate_with_github(self):
config = Config.load_config()
try:
access_token = token()
client = GithubClient(config, access_token)
self.push_screen(LazyGithubMainScreen(client))
# We pull the user here to validate auth
_ = await github.user()
self.push_screen(LazyGithubMainScreen())
except GithubAuthenticationRequired:
log("Triggering auth with github")
self.push_screen(AuthenticationModal(id="auth-modal"))
Expand Down
4 changes: 1 addition & 3 deletions lazy_github/ui/screens/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from textual.widgets import Footer

import lazy_github.lib.github.auth as auth
from lazy_github.lib.config import Config
from lazy_github.lib.github.client import GithubClient
from lazy_github.ui.screens.primary import LazyGithubMainScreen


Expand Down Expand Up @@ -71,7 +69,7 @@ async def check_access_token(self, device_code: auth.DeviceCodeResponse):
log("Successfully authenticated!")
auth.save_access_token(access_token)
self.access_token_timer.stop()
self.app.switch_screen(LazyGithubMainScreen(GithubClient(Config.load_config(), auth.token())))
self.app.switch_screen(LazyGithubMainScreen())

@work
async def get_device_token(self):
Expand Down
11 changes: 4 additions & 7 deletions lazy_github/ui/screens/edit_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from textual.widgets import Button, Input, Label, Rule, TextArea

from lazy_github.lib.github import issues
from lazy_github.lib.github.client import GithubClient
from lazy_github.models.github import Issue


Expand All @@ -20,9 +19,8 @@ class EditIssueContainer(Container):
}
"""

def __init__(self, client: GithubClient, issue: Issue, *args, **kwargs) -> None:
def __init__(self, issue: Issue, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.client = client
self.issue = issue

def compose(self) -> ComposeResult:
Expand All @@ -49,7 +47,7 @@ async def submit_updated_issue(self, save_button: Button) -> None:
updated_title = self.query_one("#updated_issue_title", Input).value
updated_body = self.query_one("#updated_issue_body", TextArea).text
self.notify(f"Updating issue #{self.issue.number}...", timeout=1)
await issues.update_issue(self.client, self.issue, title=updated_title, body=updated_body)
await issues.update_issue(self.issue, title=updated_title, body=updated_body)
self.notify(f"Successfully updated issue #{self.issue.number}")
self.app.pop_screen()

Expand All @@ -69,10 +67,9 @@ class EditIssueModal(ModalScreen):
}
"""

def __init__(self, client: GithubClient, issue: Issue, *args, **kwargs):
def __init__(self, issue: Issue, *args, **kwargs):
super().__init__(*args, **kwargs)
self.issue = issue
self.client = client

def compose(self) -> ComposeResult:
yield EditIssueContainer(self.client, self.issue)
yield EditIssueContainer(self.issue)
Loading

0 comments on commit 16dda47

Please sign in to comment.