github-bot-api 0.7.0__tar.gz → 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/PKG-INFO +19 -8
  2. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/README.md +16 -4
  3. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/pyproject.toml +16 -5
  4. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/__init__.py +1 -1
  5. github_bot_api-0.7.1/src/github_bot_api/app.py +380 -0
  6. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/token.py +1 -1
  7. github_bot_api-0.7.0/src/github_bot_api/app.py +0 -188
  8. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/LICENSE +0 -0
  9. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/app_test.py +0 -0
  10. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/event.py +0 -0
  11. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/flask.py +0 -0
  12. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/py.typed +0 -0
  13. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/signature.py +0 -0
  14. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/tests/test_import.py +0 -0
  15. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/utils/__init__.py +0 -0
  16. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/utils/functions.py +0 -0
  17. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/utils/mime.py +0 -0
  18. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/utils/types.py +0 -0
  19. {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/webhook.py +0 -0
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: github-bot-api
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: API for creating GitHub bots and webhooks in Python.
5
5
  Author-Email: Niklas Rosenstein <rosensteinniklas@gmail.com>
6
6
  License: MIT
7
7
  Classifier: Intended Audience :: Developers
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.9
11
10
  Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
@@ -15,15 +14,14 @@ Classifier: Programming Language :: Python :: 3.13
15
14
  Project-URL: Bug Tracker, https://github.com/NiklasRosenstein/python-github-bot-api/issues
16
15
  Project-URL: Documentation, https://niklasrosenstein.github.io/python-github-bot-api/
17
16
  Project-URL: Repository, https://github.com/NiklasRosenstein/python-github-bot-api
18
- Requires-Python: <4.0,>=3.9
17
+ Requires-Python: <4.0,>=3.10
19
18
  Requires-Dist: cryptography<44.0.1,>=44.0.0
20
19
  Requires-Dist: pygithub>=2.5.0
21
20
  Requires-Dist: PyJWT<3.0.0,>=2.6.0
22
21
  Requires-Dist: requests<3.0.0,>=2.28.2
23
- Requires-Dist: urllib3<2.4.0,>=2.3.0
22
+ Requires-Dist: urllib3<2.6.4,>=2.6.3
24
23
  Description-Content-Type: text/markdown
25
24
 
26
- <p align="center"><img src="https://i.imgur.com/5SiDsz8.png"></p>
27
25
  <h1 align="center">python-github-bot-api</h1>
28
26
  <p align="center">
29
27
  <a href="https://pypi.org/project/github-bot-api"><img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/github-bot-api"></a></p>
@@ -32,18 +30,31 @@ Description-Content-Type: text/markdown
32
30
 
33
31
  A thin Python library for creating GitHub bots and webhooks in Python with [PyGithub].
34
32
 
33
+ ## Quickstart
34
+
35
35
  ```python
36
- from github import Github
37
36
  from github_bot_api import GithubApp
38
37
  from pathlib import Path
39
38
 
40
39
  app = GithubApp(
41
40
  user_agent='my-bot/0.0.0',
42
- app_id="67890",
41
+ app_id="12345",
43
42
  private_key=Path("app-private.key").read_text(),
44
43
  )
44
+ ```
45
45
 
46
- client: Github = app.installation_client(12345)
46
+ Create a PyGithub client for the app itself:
47
+
48
+ ```python
49
+ from github import Github
50
+ client: Github = app.app_client()
51
+ ```
52
+
53
+ Create a PyGithub client for an app's installation scope:
54
+
55
+ ```python
56
+ from github import Github
57
+ client: Github = app.installation_client(45678)
47
58
  ```
48
59
 
49
60
  For more examples, check out the [documentation](https://niklasrosenstein.github.io/python-github-bot-api/).
@@ -1,4 +1,3 @@
1
- <p align="center"><img src="https://i.imgur.com/5SiDsz8.png"></p>
2
1
  <h1 align="center">python-github-bot-api</h1>
3
2
  <p align="center">
4
3
  <a href="https://pypi.org/project/github-bot-api"><img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/github-bot-api"></a></p>
@@ -7,18 +6,31 @@
7
6
 
8
7
  A thin Python library for creating GitHub bots and webhooks in Python with [PyGithub].
9
8
 
9
+ ## Quickstart
10
+
10
11
  ```python
11
- from github import Github
12
12
  from github_bot_api import GithubApp
13
13
  from pathlib import Path
14
14
 
15
15
  app = GithubApp(
16
16
  user_agent='my-bot/0.0.0',
17
- app_id="67890",
17
+ app_id="12345",
18
18
  private_key=Path("app-private.key").read_text(),
19
19
  )
20
+ ```
20
21
 
21
- client: Github = app.installation_client(12345)
22
+ Create a PyGithub client for the app itself:
23
+
24
+ ```python
25
+ from github import Github
26
+ client: Github = app.app_client()
27
+ ```
28
+
29
+ Create a PyGithub client for an app's installation scope:
30
+
31
+ ```python
32
+ from github import Github
33
+ client: Github = app.installation_client(45678)
22
34
  ```
23
35
 
24
36
  For more examples, check out the [documentation](https://niklasrosenstein.github.io/python-github-bot-api/).
@@ -8,23 +8,22 @@ build-backend = "pdm.backend"
8
8
  authors = [
9
9
  { name = "Niklas Rosenstein", email = "rosensteinniklas@gmail.com" },
10
10
  ]
11
- requires-python = "<4.0,>=3.9"
11
+ requires-python = "<4.0,>=3.10"
12
12
  dependencies = [
13
13
  "cryptography<44.0.1,>=44.0.0",
14
14
  "pygithub>=2.5.0",
15
15
  "PyJWT<3.0.0,>=2.6.0",
16
16
  "requests<3.0.0,>=2.28.2",
17
- "urllib3<2.4.0,>=2.3.0",
17
+ "urllib3<2.6.4,>=2.6.3",
18
18
  ]
19
19
  name = "github-bot-api"
20
- version = "0.7.0"
20
+ version = "0.7.1"
21
21
  description = "API for creating GitHub bots and webhooks in Python."
22
22
  readme = "README.md"
23
23
  classifiers = [
24
24
  "Intended Audience :: Developers",
25
25
  "License :: OSI Approved :: MIT License",
26
26
  "Programming Language :: Python :: 3",
27
- "Programming Language :: Python :: 3.9",
28
27
  "Programming Language :: Python :: 3.10",
29
28
  "Programming Language :: Python :: 3.11",
30
29
  "Programming Language :: Python :: 3.12",
@@ -41,7 +40,7 @@ Documentation = "https://niklasrosenstein.github.io/python-github-bot-api/"
41
40
  Repository = "https://github.com/NiklasRosenstein/python-github-bot-api"
42
41
 
43
42
  [tool.mypy]
44
- python_version = "3.8"
43
+ python_version = "3.10"
45
44
  explicit_package_bases = true
46
45
  mypy_path = [
47
46
  "src",
@@ -64,6 +63,18 @@ dev-dependencies = [
64
63
  "mypy",
65
64
  "pytest",
66
65
  "ruff>=0.8.5",
66
+ "types-deprecated>=1.2.15.20241117",
67
67
  "types-flask",
68
68
  "types-requests",
69
69
  ]
70
+
71
+ [tool.slap.test]
72
+ ruff-fmt = "ruff format --check ."
73
+ ruff-check = "ruff check ."
74
+ mypy = "mypy ."
75
+ pytest = "pytest ."
76
+
77
+ [tool.slap.run]
78
+ fmt = "ruff format ."
79
+ docs-serve = "slap changelog format --all --markdown > docs/content/changelog.md && cd docs && uv run mkdocs serve"
80
+ docs-build = "slap changelog format --all --markdown > docs/content/changelog.md && cd docs && uv run mkdocs build"
@@ -1,5 +1,5 @@
1
1
  __author__ = "Niklas Rosenstein <nrosenstein@palantir.com>"
2
- __version__ = "0.7.0"
2
+ __version__ = "0.7.1"
3
3
 
4
4
  from .app import GithubApp
5
5
  from .event import Event, accept_event
@@ -0,0 +1,380 @@
1
+ """
2
+ Registry for GitHub event handlers.
3
+ """
4
+
5
+ import dataclasses
6
+ import logging
7
+ import sys
8
+ import threading
9
+ import time
10
+ import typing as t
11
+ from urllib.parse import parse_qs, urlencode
12
+
13
+ import deprecated
14
+ import requests
15
+ import urllib3
16
+
17
+ from . import __version__
18
+ from .token import InstallationTokenSupplier, JwtSupplier, TokenInfo
19
+ from .utils.functions import coalesce
20
+
21
+ T = t.TypeVar("T")
22
+ logger = logging.getLogger(__name__)
23
+ user_agent = f"python/{sys.version.split()[0]} github-bot-api/{__version__}"
24
+
25
+ if t.TYPE_CHECKING:
26
+ import github
27
+
28
+
29
+ @dataclasses.dataclass
30
+ class GithubClientSettings:
31
+ """
32
+ Settings for constructing a #github.Github client object.
33
+ """
34
+
35
+ base_url: t.Optional[str] = None
36
+ user_agent: t.Optional[str] = None
37
+ timeout: t.Optional[int] = None
38
+ per_page: t.Optional[int] = None
39
+ verify: t.Optional[bool] = None
40
+ retry: t.Optional[urllib3.Retry] = None
41
+
42
+ def update(self, other: "GithubClientSettings") -> "GithubClientSettings":
43
+ result = GithubClientSettings()
44
+ for field in dataclasses.fields(self):
45
+ value = getattr(other, field.name)
46
+ if value is None:
47
+ value = getattr(self, field.name)
48
+ setattr(result, field.name, value)
49
+ return result
50
+
51
+ def make_client(self, login_or_token: t.Optional[str] = None, jwt: t.Optional[str] = None) -> "github.Github":
52
+ import github
53
+ import github.Consts
54
+
55
+ return github.Github(
56
+ login_or_token=login_or_token,
57
+ jwt=jwt,
58
+ base_url=self.base_url or github.Consts.DEFAULT_BASE_URL,
59
+ user_agent=self.user_agent or "PyGithub/Python",
60
+ timeout=coalesce(self.timeout, github.Consts.DEFAULT_TIMEOUT),
61
+ per_page=coalesce(self.per_page, github.Consts.DEFAULT_PER_PAGE),
62
+ verify=coalesce(self.verify, True),
63
+ retry=self.retry,
64
+ )
65
+
66
+
67
+ @dataclasses.dataclass
68
+ class GithubApp:
69
+ """
70
+ Represents a GitHub application and all the required details.
71
+ """
72
+
73
+ PUBLIC_GITHUB_V3_API_URL = "https://api.github.com"
74
+
75
+ user_agent: str
76
+ """User agent of the application. This will be respected in #get_user_agent()."""
77
+
78
+ app_id: int
79
+ """GitHub Application ID."""
80
+
81
+ private_key: str = dataclasses.field(repr=False)
82
+ """RSA private key to sign the JWT with."""
83
+
84
+ client_id: t.Optional[str] = None
85
+ """The GitHub App's OAuth client ID. This is required for OAuth2 authorization URL generation. Can be omitted
86
+ if the app does not use OAuth2."""
87
+
88
+ client_secret: t.Optional[str] = dataclasses.field(default=None, repr=False)
89
+ """The GitHub App's OAuth client secret. This is required for OAuth2 authorization URL generation. Can be omitted
90
+ if the app does not use OAuth2. Note that this must be specified if #client_id is specified."""
91
+
92
+ redirect_uri: t.Optional[str] = None
93
+ """The GitHub App's OAuth redirect URI. This is required for OAuth2 authorization URL generation. Can be omitted
94
+ if the app does not use OAuth2. This field is optional, but required for the web authorization flow with
95
+ #oauth2_web_application_flow_url()."""
96
+
97
+ v3_api_url: str = PUBLIC_GITHUB_V3_API_URL
98
+ """GitHub API base URL. Defaults to the public GitHub API."""
99
+
100
+ def __post_init__(self):
101
+ self._jwt_supplier = JwtSupplier(self.app_id, self.private_key)
102
+ self._lock = threading.Lock()
103
+ self._installation_tokens: t.Dict[int, InstallationTokenSupplier] = {}
104
+
105
+ if self.client_id is not None:
106
+ if self.client_secret is None:
107
+ raise ValueError("client_secret must be specified if client_id is specified.")
108
+ if self.redirect_uri is not None:
109
+ if self.client_id is None:
110
+ raise ValueError("redirect_uri does not make sense without client_id.")
111
+
112
+ def _get_base_github_client_settings(self) -> GithubClientSettings:
113
+ return GithubClientSettings(self.v3_api_url, self.get_user_agent())
114
+
115
+ def get_user_agent(self, installation_id: t.Optional[int] = None) -> str:
116
+ """
117
+ Create a user agent string for the PyGithub client, including the installation if specified.
118
+ """
119
+
120
+ user_agent = f"{self.user_agent} PyGithub/python (app_id={self.app_id}"
121
+ if installation_id:
122
+ user_agent += f", installation_id={installation_id})"
123
+ return user_agent
124
+
125
+ @property
126
+ def jwt(self) -> TokenInfo:
127
+ """
128
+ Returns the JWT for your GitHub application. The JWT is the token to use with GitHub application APIs.
129
+ """
130
+
131
+ return self._jwt_supplier()
132
+
133
+ @property
134
+ def jwt_supplier(self) -> JwtSupplier:
135
+ """
136
+ Returns a new #JwtSupplier that is used for generating JWT tokens for your GitHub application.
137
+ """
138
+
139
+ return JwtSupplier(self.app_id, self.private_key)
140
+
141
+ def app_client(self, settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None) -> "github.Github":
142
+ """
143
+ Returns a PyGithub client for your GitHub application.
144
+
145
+ Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
146
+ client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
147
+ operation you perform.
148
+
149
+ This requires you to install `PyGithub>=1.58`.
150
+ """
151
+
152
+ if isinstance(settings, dict):
153
+ settings = GithubClientSettings(**settings)
154
+ elif settings is None:
155
+ settings = GithubClientSettings()
156
+
157
+ settings = self._get_base_github_client_settings().update(settings)
158
+ return settings.make_client(jwt=self.jwt.value)
159
+
160
+ def __requestor(self, auth_header: str, installation_id: int) -> t.Dict[str, str]:
161
+ return requests.post(
162
+ self.v3_api_url.rstrip("/") + f"/app/installations/{installation_id}/access_tokens",
163
+ headers={"Authorization": auth_header, "User-Agent": user_agent},
164
+ ).json()
165
+
166
+ @deprecated.deprecated(reason="Use .installation_token_supplier() instead.", version="0.8.0")
167
+ def get_installation_token_supplier(self, installation_id: int) -> InstallationTokenSupplier:
168
+ return self.installation_token_supplier(installation_id)
169
+
170
+ def installation_token_supplier(self, installation_id: int) -> InstallationTokenSupplier:
171
+ """
172
+ Create an #InstallationTokenSupplier for your GitHub application to act within the scope of the given
173
+ *installation_id*.
174
+ """
175
+
176
+ with self._lock:
177
+ return self._installation_tokens.setdefault(
178
+ installation_id,
179
+ InstallationTokenSupplier(
180
+ self._jwt_supplier,
181
+ installation_id,
182
+ self.__requestor,
183
+ ),
184
+ )
185
+
186
+ def installation_token(self, installation_id: int) -> TokenInfo:
187
+ """
188
+ A short-hand to retrieve a new installation token for the given *installation_id*.
189
+ """
190
+
191
+ return self.get_installation_token_supplier(installation_id)()
192
+
193
+ def installation_client(
194
+ self,
195
+ installation_id: int,
196
+ settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None,
197
+ ) -> "github.Github":
198
+ """
199
+ Returns a PyGithub client for your GitHub application to act in the scope of the given *installation_id*.
200
+
201
+ Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
202
+ client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
203
+ operation you perform.
204
+
205
+ This requires you to install `PyGithub>=1.58`.
206
+ """
207
+
208
+ if isinstance(settings, dict):
209
+ settings = GithubClientSettings(**settings)
210
+ elif settings is None:
211
+ settings = GithubClientSettings()
212
+
213
+ token = self.installation_token(installation_id).value
214
+ settings = self._get_base_github_client_settings().update(settings)
215
+ return settings.make_client(login_or_token=token)
216
+
217
+ def oauth2_web_application_flow_url(self, state: t.Optional[str] = None) -> str:
218
+ """
219
+ Returns the URL for a user to begin the OAuth2 web authorization flow.
220
+
221
+ Preconditions:
222
+ - You must have provided a `client_id` when constructing the #GithubApp instance.
223
+
224
+ Documentation: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-web-application-flow-to-generate-a-user-access-token
225
+ """
226
+
227
+ if self.client_id is None:
228
+ raise ValueError("client_id must be specified to generate OAuth2 authorization URL.")
229
+ if self.client_secret is None:
230
+ raise ValueError("client_secret must be specified to generate OAuth2 authorization URL.")
231
+ if not self.redirect_uri:
232
+ raise ValueError("redirect_uri must be specified to generate OAuth2 authorization URL.")
233
+
234
+ params = {
235
+ "client_id": self.client_id,
236
+ "redirect_uri": self.redirect_uri,
237
+ }
238
+ if state is not None:
239
+ params["state"] = state
240
+
241
+ url = self.v3_api_url.replace("api.", "").replace("/api/v3", "").rstrip("/")
242
+ return f"{url}/login/oauth/authorize?" + urlencode(params)
243
+
244
+ def oauth2_device_flow(self) -> "OAuth2DeviceCodeFlow":
245
+ """
246
+ Makes a request to GitHub to request a device code for the OAuth2 device flow.
247
+
248
+ Prerequisites:
249
+
250
+ - "Enable Device Flow" must be checked in your GitHub app's settings.
251
+ - You must have provided a `client_id` when constructing the #GithubApp instance.
252
+
253
+ Documentation: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-device-flow-to-generate-a-user-access-token
254
+ """
255
+
256
+ params = {"client_id": self.client_id}
257
+ url = self.v3_api_url.replace("api.", "").replace("/api/v3", "").rstrip("/")
258
+ response = requests.post(f"{url}/login/device/code", params=params)
259
+ response.raise_for_status()
260
+ payload = {k: v[0] for k, v in parse_qs(response.text).items()}
261
+ return OAuth2DeviceCodeFlow(
262
+ device_code=payload["device_code"],
263
+ user_code=payload["user_code"],
264
+ verification_uri=payload["verification_uri"],
265
+ expires_in=int(payload["expires_in"]),
266
+ interval=int(payload["interval"]),
267
+ app=self,
268
+ )
269
+
270
+ def oauth2_access_token(
271
+ self, *, code: t.Optional[str] = None, device_code: t.Optional[str] = None
272
+ ) -> t.Optional["OAuth2TokenInfo"]:
273
+ """
274
+ Makes a request to GitHub to exchange an OAuth2 code for an access token.
275
+
276
+ Important: You must provide the correct `code` or `device_code` parameter, but not both.
277
+
278
+ Documentation: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#generating-a-user-access-token-when-a-user-installs-your-app
279
+
280
+ Returns None if the token is not yet available (in the case of the device flow). Otherwise, returns a
281
+ #TokenInfo object. If an error occurs, an #AccessTokenError is raised.
282
+ """
283
+
284
+ params = {"client_id": self.client_id, "client_secret": self.client_secret}
285
+ if code is not None:
286
+ params["code"] = code
287
+ elif device_code is not None:
288
+ params["device_code"] = device_code
289
+ params["grant_type"] = "urn:ietf:params:oauth:grant-type:device_code"
290
+ else:
291
+ raise ValueError("You must provide either a code or a device_code.")
292
+
293
+ url = self.v3_api_url.replace("api.", "").replace("/api/v3", "").rstrip("/")
294
+ response = requests.post(f"{url}/login/oauth/access_token", params=params)
295
+ payload = {k: v[0] for k, v in parse_qs(response.text).items()}
296
+
297
+ if (error := payload.get("error")) == "authorization_pending":
298
+ return None
299
+ elif error is not None:
300
+ return OAuth2TokenInfo(
301
+ access_token=payload["access_token"],
302
+ expires_in=int(payload["expires_in"]),
303
+ scope=payload.get("scope"),
304
+ token_type=payload["token_type"],
305
+ refresh_token=payload.get("refresh_token"),
306
+ refresh_token_expires_in=int(payload["refresh_token_expires_in"])
307
+ if "refresh_token_expires_in" in payload
308
+ else None,
309
+ )
310
+ else:
311
+ raise AccessTokenError(error) # type: ignore[arg-type]
312
+
313
+
314
+ @dataclasses.dataclass
315
+ class OAuth2DeviceCodeFlow:
316
+ device_code: str
317
+ user_code: str
318
+ verification_uri: str
319
+ expires_in: int
320
+ interval: int
321
+
322
+ # If you want to use this object to poll for the token, you need the GitHub App.
323
+ app: t.Optional[GithubApp] = None
324
+
325
+ def wait_for_token(
326
+ self, aborted: t.Optional[threading.Event] = None, max_duration: t.Optional[float] = None
327
+ ) -> "OAuth2TokenInfo":
328
+ """
329
+ Polls GitHub for the access token until it is available.
330
+
331
+ If you pass in a threading.Event object, it will be used to abort the polling when the
332
+ event is set. If *max_duration* is specified, the polling will stop after that many seconds and
333
+ raise a #TimeoutError if the token is not available by then.
334
+ """
335
+
336
+ assert self.app is not None, "You must set the 'app' attribute to use this method."
337
+
338
+ tstart = time.perf_counter()
339
+
340
+ while True:
341
+ if max_duration is not None and time.perf_counter() - tstart > max_duration:
342
+ raise TimeoutError("Timed out waiting for token.")
343
+ if aborted and aborted.is_set():
344
+ raise RuntimeError("Polling for token was aborted.")
345
+ try:
346
+ if token := self.app.oauth2_access_token(device_code=self.device_code):
347
+ return token
348
+ except AccessTokenError as e:
349
+ if e.error == "slow_down":
350
+ time.sleep(5)
351
+ else:
352
+ raise
353
+ time.sleep(self.interval)
354
+
355
+
356
+ @dataclasses.dataclass
357
+ class OAuth2TokenInfo:
358
+ access_token: str
359
+ expires_in: int
360
+ token_type: str
361
+ scope: t.Optional[str] = None
362
+ refresh_token: t.Optional[str] = None
363
+ refresh_token_expires_in: t.Optional[int] = None
364
+
365
+ @property
366
+ def auth_header(self) -> str:
367
+ return f"{self.token_type} {self.access_token}"
368
+
369
+
370
+ @dataclasses.dataclass
371
+ class AccessTokenError(Exception):
372
+ error: t.Literal[
373
+ "slow_down",
374
+ "expired_token",
375
+ "unsupported_grant_type",
376
+ "incorrect_client_credentials",
377
+ "incorrect_device_code",
378
+ "access_denied",
379
+ "device_flow_disabled",
380
+ ]
@@ -59,7 +59,7 @@ def create_jwt(app_id: int, expires_in: int, private_key: str) -> TokenInfo:
59
59
 
60
60
  now = int(time.time())
61
61
  exp = now + expires_in
62
- payload = {"iss": app_id, "iat": now, "exp": exp}
62
+ payload = {"iss": str(app_id), "iat": now, "exp": exp}
63
63
  token = jwt.encode(payload, private_key, algorithm="RS256")
64
64
  return TokenInfo(exp, "Bearer", token)
65
65
 
@@ -1,188 +0,0 @@
1
- """
2
- Registry for GitHub event handlers.
3
- """
4
-
5
- import dataclasses
6
- import logging
7
- import sys
8
- import threading
9
- import typing as t
10
-
11
- import requests
12
- import urllib3
13
-
14
- from . import __version__
15
- from .token import InstallationTokenSupplier, JwtSupplier, TokenInfo
16
- from .utils.functions import coalesce
17
-
18
- T = t.TypeVar("T")
19
- logger = logging.getLogger(__name__)
20
- user_agent = f"python/{sys.version.split()[0]} github-bot-api/{__version__}"
21
-
22
- if t.TYPE_CHECKING:
23
- import github
24
-
25
-
26
- @dataclasses.dataclass
27
- class GithubClientSettings:
28
- """
29
- Settings for constructing a #github.Github client object.
30
- """
31
-
32
- base_url: t.Optional[str] = None
33
- user_agent: t.Optional[str] = None
34
- timeout: t.Optional[int] = None
35
- per_page: t.Optional[int] = None
36
- verify: t.Optional[bool] = None
37
- retry: t.Optional[urllib3.Retry] = None
38
-
39
- def update(self, other: "GithubClientSettings") -> "GithubClientSettings":
40
- result = GithubClientSettings()
41
- for field in dataclasses.fields(self):
42
- value = getattr(other, field.name)
43
- if value is None:
44
- value = getattr(self, field.name)
45
- setattr(result, field.name, value)
46
- return result
47
-
48
- def make_client(self, login_or_token: t.Optional[str] = None, jwt: t.Optional[str] = None) -> "github.Github":
49
- import github
50
- import github.Consts
51
-
52
- return github.Github(
53
- login_or_token=login_or_token,
54
- jwt=jwt,
55
- base_url=self.base_url or github.Consts.DEFAULT_BASE_URL,
56
- user_agent=self.user_agent or "PyGithub/Python",
57
- timeout=coalesce(self.timeout, github.Consts.DEFAULT_TIMEOUT),
58
- per_page=coalesce(self.per_page, github.Consts.DEFAULT_PER_PAGE),
59
- verify=coalesce(self.verify, True),
60
- retry=self.retry,
61
- )
62
-
63
-
64
- @dataclasses.dataclass
65
- class GithubApp:
66
- """
67
- Represents a GitHub application and all the required details.
68
- """
69
-
70
- PUBLIC_GITHUB_V3_API_URL = "https://api.github.com"
71
-
72
- user_agent: str
73
- """User agent of the application. This will be respected in #get_user_agent()."""
74
-
75
- app_id: int
76
- """GitHub Application ID."""
77
-
78
- private_key: str
79
- """RSA private key to sign the JWT with."""
80
-
81
- v3_api_url: str = PUBLIC_GITHUB_V3_API_URL
82
- """GitHub API base URL. Defaults to the public GitHub API."""
83
-
84
- def __post_init__(self):
85
- self._jwt_supplier = JwtSupplier(self.app_id, self.private_key)
86
- self._lock = threading.Lock()
87
- self._installation_tokens: t.Dict[int, InstallationTokenSupplier] = {}
88
-
89
- def _get_base_github_client_settings(self) -> GithubClientSettings:
90
- return GithubClientSettings(self.v3_api_url, self.get_user_agent())
91
-
92
- def get_user_agent(self, installation_id: t.Optional[int] = None) -> str:
93
- """
94
- Create a user agent string for the PyGithub client, including the installation if specified.
95
- """
96
-
97
- user_agent = f"{self.user_agent} PyGithub/python (app_id={self.app_id}"
98
- if installation_id:
99
- user_agent += f", installation_id={installation_id})"
100
- return user_agent
101
-
102
- @property
103
- def jwt(self) -> TokenInfo:
104
- """
105
- Returns the JWT for your GitHub application. The JWT is the token to use with GitHub application APIs.
106
- """
107
-
108
- return self._jwt_supplier()
109
-
110
- @property
111
- def jwt_supplier(self) -> JwtSupplier:
112
- """
113
- Returns a new #JwtSupplier that is used for generating JWT tokens for your GitHub application.
114
- """
115
-
116
- return JwtSupplier(self.app_id, self.private_key)
117
-
118
- def app_client(self, settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None) -> "github.Github":
119
- """
120
- Returns a PyGithub client for your GitHub application.
121
-
122
- Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
123
- client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
124
- operation you perform.
125
-
126
- This requires you to install `PyGithub>=1.58`.
127
- """
128
-
129
- if isinstance(settings, dict):
130
- settings = GithubClientSettings(**settings)
131
- elif settings is None:
132
- settings = GithubClientSettings()
133
-
134
- settings = self._get_base_github_client_settings().update(settings)
135
- return settings.make_client(jwt=self.jwt.value)
136
-
137
- def __requestor(self, auth_header: str, installation_id: int) -> t.Dict[str, str]:
138
- return requests.post(
139
- self.v3_api_url.rstrip("/") + f"/app/installations/{installation_id}/access_tokens",
140
- headers={"Authorization": auth_header, "User-Agent": user_agent},
141
- ).json()
142
-
143
- def get_installation_token_supplier(self, installation_id: int) -> InstallationTokenSupplier:
144
- """
145
- Create an #InstallationTokenSupplier for your GitHub application to act within the scope of the given
146
- *installation_id*.
147
- """
148
-
149
- with self._lock:
150
- return self._installation_tokens.setdefault(
151
- installation_id,
152
- InstallationTokenSupplier(
153
- self._jwt_supplier,
154
- installation_id,
155
- self.__requestor,
156
- ),
157
- )
158
-
159
- def installation_token(self, installation_id: int) -> TokenInfo:
160
- """
161
- A short-hand to retrieve a new installation token for the given *installation_id*.
162
- """
163
-
164
- return self.get_installation_token_supplier(installation_id)()
165
-
166
- def installation_client(
167
- self,
168
- installation_id: int,
169
- settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None,
170
- ) -> "github.Github":
171
- """
172
- Returns a PyGithub client for your GitHub application to act in the scope of the given *installation_id*.
173
-
174
- Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
175
- client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
176
- operation you perform.
177
-
178
- This requires you to install `PyGithub>=1.58`.
179
- """
180
-
181
- if isinstance(settings, dict):
182
- settings = GithubClientSettings(**settings)
183
- elif settings is None:
184
- settings = GithubClientSettings()
185
-
186
- token = self.installation_token(installation_id).value
187
- settings = self._get_base_github_client_settings().update(settings)
188
- return settings.make_client(login_or_token=token)
File without changes