Skip to content

Commit

Permalink
Use custom github library in most places
Browse files Browse the repository at this point in the history
  • Loading branch information
gizmo385 committed Jun 24, 2024
1 parent d064ca8 commit 66ae470
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 110 deletions.
43 changes: 23 additions & 20 deletions lazy_github/lib/github_v2/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,40 +31,43 @@ class GithubAuthenticationRequired(Exception):
pass


def get_device_code() -> DeviceCodeResponse:
async def get_device_code() -> DeviceCodeResponse:
"""
Authenticates this device with the Github API. This will require the user to go enter the provided device code on
the Github UI to authenticate the LazyGithub app.
"""
response = (
httpx.post(
async with httpx.AsyncClient() as client:
response = await client.post(
"https://github.com/login/device/code",
data={"client_id": LAZY_GITHUB_CLIENT_ID},
headers={"Accept": "application/json"},
)
.raise_for_status()
.json()
)
expires_at = time.time() + response["expires_in"]

response.raise_for_status()
body = response.json()
expires_at = time.time() + body["expires_in"]
return DeviceCodeResponse(
response["device_code"],
response["verification_uri"],
response["user_code"],
response["interval"],
body["device_code"],
body["verification_uri"],
body["user_code"],
body["interval"],
expires_at,
)


def get_access_token(device_code: DeviceCodeResponse) -> AccessTokenResponse:
async def get_access_token(device_code: DeviceCodeResponse) -> AccessTokenResponse:
"""Given a device code, retrieves the oauth access token that can be used to send requests to the GIthub API"""
access_token_res = httpx.post(
"https://github.com/login/oauth/access_token",
data={
"client_id": LAZY_GITHUB_CLIENT_ID,
"grant_type": DEVICE_CODE_GRANT_TYPE,
"device_code": device_code.device_code,
},
).raise_for_status()
async with httpx.AsyncClient() as client:
# TODO: This should specify an accept
access_token_res = await client.post(
"https://github.com/login/oauth/access_token",
data={
"client_id": LAZY_GITHUB_CLIENT_ID,
"grant_type": DEVICE_CODE_GRANT_TYPE,
"device_code": device_code.device_code,
},
)
access_token_res.raise_for_status()
pairs = access_token_res.text.split("&")
access_token_data = dict(pair.split("=") for pair in pairs)
return AccessTokenResponse(
Expand Down
3 changes: 0 additions & 3 deletions lazy_github/lib/github_v2/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from functools import cached_property

import httpx

from lazy_github.lib.config import Config
Expand All @@ -18,7 +16,6 @@ def headers_with_auth_accept(self, accept: str = JSON_CONTENT_ACCEPT_TYPE) -> di
"""Helper function to build a request with specific headers"""
return {"Accept": accept, "Authorization": f"Bearer {self.access_token}"}

@cached_property
async def user(self) -> User:
"""Returns the authed user for this client"""
if self._user is None:
Expand Down
8 changes: 7 additions & 1 deletion lazy_github/lib/github_v2/issues.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from functools import partial
from typing import Literal

from lazy_github.lib.github_v2.client import GithubClient
Expand All @@ -8,7 +9,7 @@

async def _list(client: GithubClient, repo: Repository, state: IssueStateFilter) -> list[Issue]:
query_params = {"state": state}
user = await client.user
user = await client.user()
response = await client.get(
f"/repos/{user.login}/{repo.name}/issues", headers=client.headers_with_auth_accept(), params=query_params
)
Expand All @@ -21,6 +22,11 @@ async def _list(client: GithubClient, repo: Repository, state: IssueStateFilter)
return result


list_open_issues = partial(_list, state="open")
list_closed_issues = partial(_list, state="closed")
list_all_issues = partial(_list, state="all")


if __name__ == "__main__":
import asyncio

Expand Down
15 changes: 5 additions & 10 deletions lazy_github/lib/github_v2/pull_requests.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
from typing import Self

from lazy_github.lib.github_v2.client import GithubClient
from lazy_github.lib.github_v2.issues import list_all_issues
from lazy_github.models.core import PullRequest, Repository


class PullRequest:
def __init__(self, client: GithubClient, raw_data) -> None:
self.client = client
self._raw_data = raw_data

@classmethod
async def list_for_repo(cls, client: GithubClient, repo: str) -> list[Self]:
return [cls(client, {})]
async def list_for_repo(client: GithubClient, repo: Repository) -> list[PullRequest]:
issues = await list_all_issues(client, repo)
return [i for i in issues if isinstance(i, PullRequest)]
2 changes: 1 addition & 1 deletion lazy_github/lib/github_v2/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async def _list(
) -> list[Repository]:
"""Retrieves Github repos matching the specified criteria"""
query_params = {"type": repo_types, "direction": direction, "sort": sort, "page": page, "per_page": per_page}
user = await client.user
user = await client.user()
response = await client.get(
f"/users/{user.login}/repos", headers=client.headers_with_auth_accept(), params=query_params
)
Expand Down
29 changes: 27 additions & 2 deletions lazy_github/lib/messages.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from github.PullRequest import PullRequest
from github.Repository import Repository
from functools import cached_property

from textual.message import Message

from lazy_github.models.core import Issue, PullRequest, Repository


class RepoSelected(Message):
"""
Expand All @@ -24,3 +26,26 @@ class PullRequestSelected(Message):
def __init__(self, pr: PullRequest) -> None:
self.pr = pr
super().__init__()


class IssuesAndPullRequestsFetched(Message):
"""
Since issues and pull requests are both represented on the Github API as issues, we want to pull issues once and
then send that message to both sections of the UI.
"""

def __init__(self, issues_and_pull_requests: list[Issue]) -> None:
self.issues_and_pull_requests = issues_and_pull_requests
super().__init__()

@cached_property
def pull_requests(self) -> list[PullRequest]:
return [pr for pr in self.issues_and_pull_requests if isinstance(pr, PullRequest)]

@cached_property
def issues(self) -> list[Issue]:
return [
issue
for issue in self.issues_and_pull_requests
if isinstance(issue, Issue) and not isinstance(issue, PullRequest)
]
13 changes: 9 additions & 4 deletions lazy_github/ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from textual.app import App

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

Expand All @@ -13,15 +16,17 @@ class LazyGithub(App):
]

async def authenticate_with_github(self):
config = Config.load_config()
try:
_github = g.github_client()
self.push_screen(LazyGithubMainScreen())
access_token = token()
client = GithubClient(config, access_token)
self.push_screen(LazyGithubMainScreen(client))
except g.GithubAuthenticationRequired:
log("Triggering auth with github")
self.push_screen(AuthenticationModal(id="auth-modal"))

def on_ready(self):
self.run_worker(self.authenticate_with_github)
async def on_ready(self):
await self.authenticate_with_github()


app = LazyGithub()
14 changes: 8 additions & 6 deletions lazy_github/ui/screens/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from textual.widget import Widget
from textual.widgets import Footer

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


Expand Down Expand Up @@ -45,8 +47,8 @@ def compose(self) -> ComposeResult:
yield Footer()

@work
async def check_access_token(self, device_code: g.DeviceCodeResponse):
access_token = g.get_access_token(device_code)
async def check_access_token(self, device_code: auth.DeviceCodeResponse):
access_token = await auth.get_access_token(device_code)
match access_token.error:
case "authorization_pending":
log("Continuing to wait for auth...")
Expand All @@ -67,14 +69,14 @@ async def check_access_token(self, device_code: g.DeviceCodeResponse):
self.access_token_timer.stop()
case _:
log("Successfully authenticated!")
g.save_access_token(access_token)
auth.save_access_token(access_token)
self.access_token_timer.stop()
self.app.switch_screen(LazyGithubMainScreen())
self.app.switch_screen(LazyGithubMainScreen(GithubClient(Config.load_config(), auth.token())))

@work
async def get_device_token(self):
log("Attempting to get device code...")
device_code = g.get_device_code()
device_code = await auth.get_device_code()
log(f"Device code: {device_code}")
self.query_one(UserTokenDisplay).user_code = device_code.user_code

Expand Down
40 changes: 30 additions & 10 deletions lazy_github/ui/screens/primary.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from textual.widget import Widget
from textual.widgets import Footer, TabbedContent

from lazy_github.lib.messages import PullRequestSelected, RepoSelected
from lazy_github.lib.github_v2.client import GithubClient
from lazy_github.lib.github_v2.issues import list_all_issues
from lazy_github.lib.messages import IssuesAndPullRequestsFetched, PullRequestSelected, RepoSelected
from lazy_github.ui.widgets.actions import ActionsContainer
from lazy_github.ui.widgets.command_log import CommandLogSection
from lazy_github.ui.widgets.common import LazyGithubContainer
Expand Down Expand Up @@ -86,9 +88,13 @@ class SelectionsPane(Container):
}
"""

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

def compose(self) -> ComposeResult:
yield ReposContainer(id="repos")
yield PullRequestsContainer(id="pull_requests")
yield ReposContainer(self.client, id="repos")
yield PullRequestsContainer(self.client, id="pull_requests")
yield IssuesContainer(id="issues")
yield ActionsContainer(id="actions")

Expand All @@ -105,12 +111,18 @@ def actions(self) -> ActionsContainer:
return self.query_one("#actions", ActionsContainer)

async def on_repo_selected(self, message: RepoSelected) -> None:
self.pull_requests.post_message(message)
self.issues.post_message(message)
self.actions.post_message(message)
# self.actions.post_message(message)
issues_and_pull_requests = await list_all_issues(self.client, message.repo)
issue_and_pr_message = IssuesAndPullRequestsFetched(issues_and_pull_requests)
self.pull_requests.post_message(issue_and_pr_message)
self.issues.post_message(issue_and_pr_message)


class SelectionDetailsPane(Container):
def __init__(self, client: GithubClient, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.client = client

def compose(self) -> ComposeResult:
yield SelectionDetailsContainer(id="selection_details")
yield CommandLogSection()
Expand All @@ -126,15 +138,19 @@ class MainViewPane(Container):
("6", "focus_section('LazyGithubCommandLog')"),
]

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

def action_focus_section(self, selector: str) -> None:
self.query_one(selector).focus()

def compose(self) -> ComposeResult:
yield SelectionsPane()
yield SelectionDetailsPane()
yield SelectionsPane(self.client)
yield SelectionDetailsPane(self.client)

def on_pull_request_selected(self, message: PullRequestSelected) -> None:
log(f"raw PR = {message.pr.raw_data}")
log(f"PR = {message.pr}")
tabbed_content = self.query_one("#selection_detail_tabs", TabbedContent)
tabbed_content.clear_panes()
tabbed_content.add_pane(PrOverviewTabPane(message.pr))
Expand All @@ -146,8 +162,12 @@ def on_pull_request_selected(self, message: PullRequestSelected) -> None:
class LazyGithubMainScreen(Screen):
BINDINGS = [("r", "refresh_repos", "Refresh global repo state")]

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

def compose(self):
with Container():
yield LazyGithubStatusSummary()
yield MainViewPane()
yield MainViewPane(self.client)
yield LazyGithubFooter()
13 changes: 5 additions & 8 deletions lazy_github/ui/widgets/issues.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import Dict

from github.Issue import Issue
from textual.app import ComposeResult

from lazy_github.lib.messages import RepoSelected
from lazy_github.lib.messages import IssuesAndPullRequestsFetched
from lazy_github.models.core import Issue
from lazy_github.ui.widgets.common import LazyGithubContainer, LazyGithubDataTable


Expand Down Expand Up @@ -32,16 +32,13 @@ def on_mount(self) -> None:
self.number_column_index = self.table.get_column_index("number")
self.title_column_index = self.table.get_column_index("title")

async def on_repo_selected(self, message: RepoSelected) -> None:
async def on_issues_and_pull_requests_fetched(self, message: IssuesAndPullRequestsFetched) -> None:
message.stop()
self.table.clear()
self.issues = {}
issues = message.repo.get_issues(state="all", sort="updated", direction="desc")

rows = []
for issue in issues:
# TODO: Currently, this also includes PRs because the Github API classifies all PRs as issues. The PyGithub
# library makes this even more problematic because there seemingly isn't a way to distinguish between an
# issue and a PR without making an additional API request. This is quite slow >:(
for issue in message.issues:
self.issues[issue.number] = issue
rows.append((issue.state, issue.number, issue.user.login, issue.title))
self.table.add_rows(rows)
Loading

0 comments on commit 66ae470

Please sign in to comment.