Skip to content

Commit 5f5a3c0

Browse files
cosmicBboyclaude
andcommitted
feat(github): give GitHubClient both a sync and an async call form
Wraps the 25 client methods with flyte's @Syncify, matching how the SDK itself exposes Run.listall, flyte.run and flyte.serve. Each method now has two forms: `client.foo(...)` blocks, `await client.foo.aio(...)` does not. - Adds __enter__/__exit__ so the blocking form is actually usable. They run __aenter__/__aexit__ on syncify's background loop -- the same loop the syncified methods run on -- so the httpx.AsyncClient is created and used on a single loop. - Internal self-calls use .aio(). The blocking form would deadlock when called from syncify's own loop thread. - The MCP tool bridge uses .aio(). `await getattr(client, name)(...)` would otherwise raise TypeError on the returned value, and would stall the MCP server's event loop for the duration of every tool call. - Tests, examples and READMEs use .aio() on async paths, and document both forms plus when not to reach for the blocking one. The syncified client type-checks clean: mypy resolves methods to SyncFunction[...] and the error count on the package is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VKZrTNjjWVzTZUxDFbn4Nk
1 parent 0ec85b5 commit 5f5a3c0

9 files changed

Lines changed: 163 additions & 63 deletions

File tree

plugins/github/README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,32 @@ from flyteplugins.github import GitHubClient
4545
@env.task
4646
async def summarize_pr(repo: str, number: int) -> str:
4747
async with GitHubClient() as client:
48-
pr = await client.get_pull_request(repo, number)
49-
files = await client.get_pull_request_files(repo, number)
48+
pr = await client.get_pull_request.aio(repo, number)
49+
files = await client.get_pull_request_files.aio(repo, number)
5050
return f"{pr['title']}: {len(files)} files changed"
5151
```
5252

5353
The client covers repositories, files, commits, issues, pull requests,
5454
reviews, branches, check runs, and merging — see `flyteplugins.github.GitHubClient`.
5555

56+
### Both call forms
57+
58+
Every client method is available two ways. `await client.get_pull_request.aio(...)` is the
59+
async form — use it in `async def` tasks and anywhere on an app's event loop.
60+
`client.get_pull_request(...)` is the blocking form, for plain `def` tasks and scripts:
61+
62+
```python
63+
@env.task
64+
def summarize(...) -> str:
65+
with GitHubClient() as client: # note: `with`, not `async with`
66+
pr = client.get_pull_request(repo, number)
67+
...
68+
```
69+
70+
The blocking form parks the calling thread until the call returns, so never
71+
reach for it inside an `async def` task or a webhook handler — it would stall
72+
the event loop and everything else waiting on it.
73+
5674
## Human review gate (condition with a JSON payload)
5775

5876
`review_pr` parks a run on a `flyte.new_condition` whose markdown prompt
@@ -69,7 +87,7 @@ async def gated_merge(repo: str, number: int) -> str:
6987
if not decision.is_approved:
7088
return f"blocked: {decision.summary}"
7189
async with GitHubClient() as client:
72-
await client.merge_pull_request(repo, number, merge_method="squash")
90+
await client.merge_pull_request.aio(repo, number, merge_method="squash")
7391
return "merged"
7492
```
7593

plugins/github/examples/pr_review_gate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ async def gated_merge(repo: str, number: int) -> str:
3737
if not decision.is_approved:
3838
# Post the reviewer's feedback back to the PR before bailing out.
3939
async with GitHubClient() as client:
40-
await client.create_issue_comment(
40+
await client.create_issue_comment.aio(
4141
repo,
4242
number,
4343
f"Review gate blocked this merge: {decision.summary}",
4444
)
4545
return f"blocked: {decision.summary}"
4646

4747
async with GitHubClient() as client:
48-
result = await client.merge_pull_request(repo, number, merge_method="squash")
48+
result = await client.merge_pull_request.aio(repo, number, merge_method="squash")
4949
return f"merged {result.get('sha', '')}"
5050

5151

plugins/github/examples/react_to_pr_events.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async def label_new_issues(event):
7171
if event.repository is None or event.number is None:
7272
return None
7373
async with GitHubClient() as client:
74-
await client.add_labels(event.repository, event.number, ["flyte-triage"])
74+
await client.add_labels.aio(event.repository, event.number, ["flyte-triage"])
7575
return {"labeled": event.number}
7676

7777

plugins/github/examples/read_write_pr.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
async def summarize_pr(repo: str, number: int) -> str:
3333
"""Read a pull request and summarize what it changes."""
3434
async with GitHubClient() as client:
35-
pr = await client.get_pull_request(repo, number)
36-
files = await client.get_pull_request_files(repo, number)
35+
pr = await client.get_pull_request.aio(repo, number)
36+
files = await client.get_pull_request_files.aio(repo, number)
3737
summary = "\n".join(f"- {f['filename']} (+{f['additions']}/-{f['deletions']})" for f in files[:20])
3838
return f"{pr['title']} ({pr['head']} -> {pr['base']})\n{summary}"
3939

@@ -45,9 +45,9 @@ async def triage_pr(repo: str, number: int) -> str:
4545
This is the task the webhook example launches for every newly opened PR.
4646
"""
4747
async with GitHubClient() as client:
48-
pr = await client.get_pull_request(repo, number)
49-
await client.add_labels(repo, number, ["flyte-triage"])
50-
await client.create_issue_comment(
48+
pr = await client.get_pull_request.aio(repo, number)
49+
await client.add_labels.aio(repo, number, ["flyte-triage"])
50+
await client.create_issue_comment.aio(
5151
repo,
5252
number,
5353
f"Flyte triage: this PR touches {pr.get('changed_files', '?')} files "
@@ -56,7 +56,7 @@ async def triage_pr(repo: str, number: int) -> str:
5656

5757
head_sha = pr.get("head_sha")
5858
if head_sha:
59-
await client.create_check_run(
59+
await client.create_check_run.aio(
6060
repo,
6161
name="flyte-triage",
6262
head_sha=head_sha,

plugins/github/src/flyteplugins/github/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,16 @@
2525
@env.task
2626
async def summarize_pr(repo: str, number: int) -> str:
2727
async with GitHubClient() as client:
28-
pr = await client.get_pull_request(repo, number)
29-
files = await client.get_pull_request_files(repo, number)
28+
pr = await client.get_pull_request.aio(repo, number)
29+
files = await client.get_pull_request_files.aio(repo, number)
3030
return f"{pr['title']}: {len(files)} files changed"
3131
```
3232
33+
Every client method has two call forms: `await client.get_pull_request.aio(...)` for
34+
async tasks and app handlers, and `client.get_pull_request(...)` (under a plain `with`)
35+
for sync tasks and scripts. The blocking form stalls the calling thread, so never
36+
use it on an event loop.
37+
3338
## Human review gate (condition with a JSON payload)
3439
3540
```python
@@ -40,7 +45,7 @@ async def gated_merge(repo: str, number: int) -> str:
4045
decision = await review_pr(repo, number)
4146
if decision.is_approved:
4247
async with GitHubClient() as client:
43-
await client.merge_pull_request(repo, number, merge_method="squash")
48+
await client.merge_pull_request.aio(repo, number, merge_method="squash")
4449
return "merged"
4550
return f"blocked: {decision.summary}"
4651
```

0 commit comments

Comments
 (0)