Skip to content

Commit 80b621b

Browse files
allow for passing Installation instance to GitHub API clients (#30)
1 parent 12edffc commit 80b621b

File tree

4 files changed

+92
-5
lines changed

4 files changed

+92
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Changed
22+
23+
- `AsyncGitHubAPI` and `SyncGitHubAPI` clients can now take an instance of `Installation` using the `installation` kwarg, in addition to the previous behavior of providing the `installation_id`. One or the other must be used for authenticated requests, not both.
24+
2125
## [0.3.0]
2226

2327
### Added

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,14 @@ async def create_comment(repo_full_name: str):
266266
f"/repos/{repo_full_name}/issues/1/comments",
267267
data={"body": "Hello!"}
268268
)
269+
270+
# You can either provide the `installation_id` as above, or the `Installation` instance
271+
# itself
272+
async with AsyncGitHubAPI(installation=installation) as gh:
273+
await gh.post(
274+
f"/repos/{repo_full_name}/issues/1/comments",
275+
data={"body": "World!"}
276+
)
269277
```
270278
271279
#### `SyncGitHubAPI`
@@ -291,6 +299,14 @@ def create_comment_sync(repo_full_name: str):
291299
f"/repos/{repo_full_name}/issues/1/comments",
292300
data={"body": "Hello!"}
293301
)
302+
303+
# You can either provide the `installation_id` as above, or the `Installation` instance
304+
# itself
305+
with SyncGitHubAPI(installation=installation) as gh:
306+
gh.post(
307+
f"/repos/{repo_full_name}/issues/1/comments",
308+
data={"body": "World!"}
309+
)
294310
```
295311
296312
### Models

src/django_github_app/github.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from dataclasses import dataclass
77
from enum import Enum
88
from types import TracebackType
9+
from typing import TYPE_CHECKING
910
from typing import Any
1011
from urllib.parse import urlencode
1112

@@ -20,6 +21,9 @@
2021
from ._sync import async_to_sync_method
2122
from ._typing import override
2223

24+
if TYPE_CHECKING:
25+
from .models import Installation
26+
2327
cache: cachetools.LRUCache[Any, Any] = cachetools.LRUCache(maxsize=500)
2428
# need to create an ssl_context in the main thread, see:
2529
# - https://github.com/pallets/flask/discussions/5387#discussioncomment-10835348
@@ -32,24 +36,31 @@ class AsyncGitHubAPI(gh_abc.GitHubAPI):
3236
def __init__(
3337
self,
3438
*args: Any,
39+
installation: Installation | None = None,
3540
installation_id: int | None = None,
3641
**kwargs: Any,
3742
) -> None:
43+
if installation is not None and installation_id is not None:
44+
raise ValueError("Must use only one of installation or installation_id")
45+
46+
self.installation = installation
3847
self.installation_id = installation_id
48+
self.oauth_token = None
3949
self._client = httpx.AsyncClient(verify=ssl_context)
4050
super().__init__(*args, cache=cache, **kwargs)
4151

4252
async def __aenter__(self) -> AsyncGitHubAPI:
4353
from .models import Installation
4454

45-
if self.installation_id:
55+
if self.installation or self.installation_id:
4656
try:
47-
installation = await Installation.objects.aget(
57+
installation = self.installation or await Installation.objects.aget(
4858
installation_id=self.installation_id
4959
)
5060
self.oauth_token = await installation.aget_access_token(self)
5161
except (Installation.DoesNotExist, gidgethub.BadRequest):
52-
self.oauth_token = None
62+
pass
63+
5364
return self
5465

5566
async def __aexit__(

tests/test_github.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,35 @@
1313
@pytest.mark.asyncio
1414
@pytest.mark.django_db
1515
class TestAsyncGitHubAPI:
16+
async def test_init_with_two_installation_kwargs(self, ainstallation):
17+
installation = await ainstallation
18+
19+
with pytest.raises(ValueError):
20+
AsyncGitHubAPI(
21+
"test",
22+
installation=installation,
23+
installation_id=installation.installation_id,
24+
)
25+
1626
async def test_request(self, httpx_mock):
1727
httpx_mock.add_response(json={"foo": "bar"})
1828

1929
async with AsyncGitHubAPI("test") as gh:
2030
response = await gh.getitem("/foo")
2131
assert response == {"foo": "bar"}
2232

23-
async def test_oauth_token(self, ainstallation, monkeypatch):
33+
async def test_oauth_token_installation(self, ainstallation, monkeypatch):
34+
async def mock_aget_access_token(*args, **kwargs):
35+
return "ABC123"
36+
37+
monkeypatch.setattr(Installation, "aget_access_token", mock_aget_access_token)
38+
39+
installation = await ainstallation
40+
41+
async with AsyncGitHubAPI("test", installation=installation) as gh:
42+
assert gh.oauth_token == "ABC123"
43+
44+
async def test_oauth_token_installation_id(self, ainstallation, monkeypatch):
2445
async def mock_aget_access_token(*args, **kwargs):
2546
return "ABC123"
2647

@@ -37,7 +58,7 @@ async def test_oauth_token_installation_doesnotexist(self):
3758
async with AsyncGitHubAPI("test", installation_id=1234) as gh:
3859
assert gh.oauth_token is None
3960

40-
async def test_oauth_token_no_installation_id(self):
61+
async def test_oauth_token_no_kwargs(self):
4162
async with AsyncGitHubAPI("test") as gh:
4263
assert gh.oauth_token is None
4364

@@ -52,7 +73,42 @@ async def test_sleep(self):
5273
assert (stop - start) > datetime.timedelta(seconds=delay)
5374

5475

76+
@pytest.mark.django_db
5577
class TestSyncGitHubAPI:
78+
def test_init_with_two_installation_kwargs(self, installation):
79+
with pytest.raises(ValueError):
80+
SyncGitHubAPI(
81+
"test",
82+
installation=installation,
83+
installation_id=installation.installation_id,
84+
)
85+
86+
def test_oauth_token_installation(self, installation, monkeypatch):
87+
async def mock_aget_access_token(*args, **kwargs):
88+
return "ABC123"
89+
90+
monkeypatch.setattr(Installation, "aget_access_token", mock_aget_access_token)
91+
92+
with SyncGitHubAPI("test", installation=installation) as gh:
93+
assert gh.oauth_token == "ABC123"
94+
95+
def test_oauth_token_installation_id(self, installation, monkeypatch):
96+
async def mock_aget_access_token(*args, **kwargs):
97+
return "ABC123"
98+
99+
monkeypatch.setattr(Installation, "aget_access_token", mock_aget_access_token)
100+
101+
with SyncGitHubAPI("test", installation_id=installation.installation_id) as gh:
102+
assert gh.oauth_token == "ABC123"
103+
104+
def test_oauth_token_installation_doesnotexist(self):
105+
with SyncGitHubAPI("test", installation_id=1234) as gh:
106+
assert gh.oauth_token is None
107+
108+
def test_oauth_token_no_kwargs(self):
109+
with SyncGitHubAPI("test") as gh:
110+
assert gh.oauth_token is None
111+
56112
def test_getitem(self, httpx_mock):
57113
httpx_mock.add_response(json={"foo": "bar"})
58114

0 commit comments

Comments
 (0)