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.
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/PKG-INFO +19 -8
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/README.md +16 -4
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/pyproject.toml +16 -5
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/__init__.py +1 -1
- github_bot_api-0.7.1/src/github_bot_api/app.py +380 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/token.py +1 -1
- github_bot_api-0.7.0/src/github_bot_api/app.py +0 -188
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/LICENSE +0 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/app_test.py +0 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/event.py +0 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/flask.py +0 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/py.typed +0 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/signature.py +0 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/tests/test_import.py +0 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/utils/__init__.py +0 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/utils/functions.py +0 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/utils/mime.py +0 -0
- {github_bot_api-0.7.0 → github_bot_api-0.7.1}/src/github_bot_api/utils/types.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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="
|
|
41
|
+
app_id="12345",
|
|
43
42
|
private_key=Path("app-private.key").read_text(),
|
|
44
43
|
)
|
|
44
|
+
```
|
|
45
45
|
|
|
46
|
-
client
|
|
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="
|
|
17
|
+
app_id="12345",
|
|
18
18
|
private_key=Path("app-private.key").read_text(),
|
|
19
19
|
)
|
|
20
|
+
```
|
|
20
21
|
|
|
21
|
-
client
|
|
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.
|
|
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
|
|
17
|
+
"urllib3<2.6.4,>=2.6.3",
|
|
18
18
|
]
|
|
19
19
|
name = "github-bot-api"
|
|
20
|
-
version = "0.7.
|
|
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.
|
|
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"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|