github-bot-api 0.5.1__py3-none-any.whl → 0.6.0__py3-none-any.whl
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/__init__.py +4 -5
- github_bot_api/app.py +138 -163
- github_bot_api/app_test.py +16 -0
- github_bot_api/event.py +66 -66
- github_bot_api/flask.py +26 -25
- github_bot_api/signature.py +29 -30
- github_bot_api/tests/test_import.py +2 -0
- github_bot_api/token.py +89 -87
- github_bot_api/utils/functions.py +29 -0
- github_bot_api/utils/mime.py +16 -17
- github_bot_api/utils/types.py +6 -7
- github_bot_api/webhook.py +49 -49
- github_bot_api-0.5.1.dist-info/LICENSE.txt → github_bot_api-0.6.0.dist-info/LICENSE +2 -2
- github_bot_api-0.6.0.dist-info/METADATA +52 -0
- github_bot_api-0.6.0.dist-info/RECORD +18 -0
- {github_bot_api-0.5.1.dist-info → github_bot_api-0.6.0.dist-info}/WHEEL +1 -2
- github_bot_api-0.5.1.dist-info/METADATA +0 -94
- github_bot_api-0.5.1.dist-info/RECORD +0 -16
- github_bot_api-0.5.1.dist-info/top_level.txt +0 -1
github_bot_api/__init__.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
__version__ = '0.5.1'
|
|
1
|
+
__author__ = "Niklas Rosenstein <nrosenstein@palantir.com>"
|
|
2
|
+
__version__ = "0.6.0"
|
|
4
3
|
|
|
5
4
|
from .app import GithubApp
|
|
6
|
-
from .event import Event
|
|
5
|
+
from .event import Event, accept_event
|
|
7
6
|
from .webhook import Webhook
|
|
8
7
|
|
|
9
|
-
__all__ = [
|
|
8
|
+
__all__ = ["GithubApp", "Event", "accept_event", "Webhook"]
|
github_bot_api/app.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
"""
|
|
3
2
|
Registry for GitHub event handlers.
|
|
4
3
|
"""
|
|
@@ -9,205 +8,181 @@ import sys
|
|
|
9
8
|
import threading
|
|
10
9
|
import typing as t
|
|
11
10
|
|
|
12
|
-
import deprecated
|
|
13
11
|
import requests
|
|
14
|
-
import urllib3
|
|
15
|
-
from nr.functional import coalesce
|
|
12
|
+
import urllib3
|
|
16
13
|
|
|
17
14
|
from . import __version__
|
|
18
15
|
from .token import InstallationTokenSupplier, JwtSupplier, TokenInfo
|
|
16
|
+
from .utils.functions import coalesce
|
|
19
17
|
|
|
20
|
-
T = t.TypeVar(
|
|
18
|
+
T = t.TypeVar("T")
|
|
21
19
|
logger = logging.getLogger(__name__)
|
|
22
|
-
user_agent = f
|
|
20
|
+
user_agent = f"python/{sys.version.split()[0]} github-bot-api/{__version__}"
|
|
23
21
|
|
|
24
22
|
if t.TYPE_CHECKING:
|
|
25
|
-
|
|
23
|
+
import github
|
|
26
24
|
|
|
27
25
|
|
|
28
26
|
@dataclasses.dataclass
|
|
29
27
|
class GithubClientSettings:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
)
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
@dataclasses.dataclass
|
|
64
65
|
class GithubApp:
|
|
65
|
-
"""
|
|
66
|
-
Represents a GitHub application and all the required details.
|
|
67
|
-
|
|
68
|
-
# Example
|
|
69
|
-
|
|
70
|
-
```py
|
|
71
|
-
import os
|
|
72
|
-
from github_bot_api import GithubApp
|
|
73
|
-
|
|
74
|
-
with open(os.environ['PRIVATE_KEY_FILE']) as fp:
|
|
75
|
-
private_key = fp.read()
|
|
76
|
-
|
|
77
|
-
app = GithubApp(
|
|
78
|
-
user_agent='my-bot/0.0.0',
|
|
79
|
-
app_id=int(os.environ['APP_ID']),
|
|
80
|
-
private_key=private_key)
|
|
81
|
-
|
|
82
|
-
print(app.app_client().get_app().owner)
|
|
83
|
-
```
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
PUBLIC_GITHUB_V3_API_URL = 'https://api.github.com'
|
|
87
|
-
|
|
88
|
-
#: User agent of the application. This will be respected in #get_user_agent().
|
|
89
|
-
user_agent: str
|
|
90
|
-
|
|
91
|
-
#: GitHub Application ID.
|
|
92
|
-
app_id: int
|
|
93
|
-
|
|
94
|
-
#: RSA private key to sign the JWT with.
|
|
95
|
-
private_key: str
|
|
96
|
-
|
|
97
|
-
#: GitHub API base URL. Defaults to the public GitHub API.
|
|
98
|
-
v3_api_url: str = PUBLIC_GITHUB_V3_API_URL
|
|
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
|
-
def _get_base_github_client_settings(self) -> GithubClientSettings:
|
|
106
|
-
return GithubClientSettings(self.v3_api_url, self.get_user_agent())
|
|
107
|
-
|
|
108
|
-
def get_user_agent(self, installation_id: t.Optional[int] = None) -> str:
|
|
109
66
|
"""
|
|
110
|
-
|
|
67
|
+
Represents a GitHub application and all the required details.
|
|
111
68
|
"""
|
|
112
69
|
|
|
113
|
-
|
|
114
|
-
if installation_id:
|
|
115
|
-
user_agent += f', installation_id={installation_id})'
|
|
116
|
-
return user_agent
|
|
70
|
+
PUBLIC_GITHUB_V3_API_URL = "https://api.github.com"
|
|
117
71
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"""
|
|
121
|
-
Returns the JWT for your GitHub application. The JWT is the token to use with GitHub application APIs.
|
|
122
|
-
"""
|
|
72
|
+
user_agent: str
|
|
73
|
+
"""User agent of the application. This will be respected in #get_user_agent()."""
|
|
123
74
|
|
|
124
|
-
|
|
75
|
+
app_id: int
|
|
76
|
+
"""GitHub Application ID."""
|
|
125
77
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"""
|
|
129
|
-
Returns a new #JwtSupplier that is used for generating JWT tokens for your GitHub application.
|
|
130
|
-
"""
|
|
78
|
+
private_key: str
|
|
79
|
+
"""RSA private key to sign the JWT with."""
|
|
131
80
|
|
|
132
|
-
|
|
81
|
+
v3_api_url: str = PUBLIC_GITHUB_V3_API_URL
|
|
82
|
+
"""GitHub API base URL. Defaults to the public GitHub API."""
|
|
133
83
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Use #app_client() instead.
|
|
139
|
-
"""
|
|
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] = {}
|
|
140
88
|
|
|
141
|
-
|
|
89
|
+
def _get_base_github_client_settings(self) -> GithubClientSettings:
|
|
90
|
+
return GithubClientSettings(self.v3_api_url, self.get_user_agent())
|
|
142
91
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
"""
|
|
146
96
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
150
101
|
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
"""
|
|
153
107
|
|
|
154
|
-
|
|
155
|
-
settings = GithubClientSettings(**settings)
|
|
156
|
-
elif settings is None:
|
|
157
|
-
settings = GithubClientSettings()
|
|
108
|
+
return self._jwt_supplier()
|
|
158
109
|
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
"""
|
|
161
115
|
|
|
162
|
-
|
|
163
|
-
return requests.post(
|
|
164
|
-
self.v3_api_url.rstrip('/') + f'/app/installations/{installation_id}/access_tokens',
|
|
165
|
-
headers={'Authorization': auth_header, 'User-Agent': user_agent},
|
|
166
|
-
).json()
|
|
116
|
+
return JwtSupplier(self.app_id, self.private_key)
|
|
167
117
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
*installation_id*.
|
|
172
|
-
"""
|
|
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.
|
|
173
121
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
InstallationTokenSupplier(
|
|
178
|
-
self._jwt_supplier,
|
|
179
|
-
installation_id,
|
|
180
|
-
self.__requestor,
|
|
181
|
-
)
|
|
182
|
-
)
|
|
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.
|
|
183
125
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
A short-hand to retrieve a new installation token for the given *installation_id*.
|
|
187
|
-
"""
|
|
126
|
+
This requires you to install `PyGithub>=1.58`.
|
|
127
|
+
"""
|
|
188
128
|
|
|
189
|
-
|
|
129
|
+
if isinstance(settings, dict):
|
|
130
|
+
settings = GithubClientSettings(**settings)
|
|
131
|
+
elif settings is None:
|
|
132
|
+
settings = GithubClientSettings()
|
|
190
133
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
installation_id: int,
|
|
194
|
-
settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None,
|
|
195
|
-
) -> 'github.Github':
|
|
196
|
-
"""
|
|
197
|
-
Returns a PyGithub client for your GitHub application to act in the scope of the given *installation_id*.
|
|
134
|
+
settings = self._get_base_github_client_settings().update(settings)
|
|
135
|
+
return settings.make_client(jwt=self.jwt.value)
|
|
198
136
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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()
|
|
202
142
|
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
"""
|
|
205
163
|
|
|
206
|
-
|
|
207
|
-
settings = GithubClientSettings(**settings)
|
|
208
|
-
elif settings is None:
|
|
209
|
-
settings = GithubClientSettings()
|
|
164
|
+
return self.get_installation_token_supplier(installation_id)()
|
|
210
165
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
from github_bot_api.app import GithubApp
|
|
4
|
+
from github_bot_api.token import TokenInfo
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_simple_app():
|
|
8
|
+
"""
|
|
9
|
+
Regression test for PyGithub 1.58+ where such code failed on:
|
|
10
|
+
|
|
11
|
+
AttributeError: module 'github.MainClass' has no attribute 'DEFAULT_TIMEOUT'
|
|
12
|
+
"""
|
|
13
|
+
token = TokenInfo(1, "Bearer", "token")
|
|
14
|
+
with patch("github_bot_api.token.InstallationTokenSupplier._new_token", return_value=token):
|
|
15
|
+
app = GithubApp("UA/0.0.0", 42, "private_key")
|
|
16
|
+
app.installation_client(1)
|
github_bot_api/event.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
|
|
2
1
|
"""
|
|
3
2
|
Abstraction of a GitHub Webhook event.
|
|
4
3
|
|
|
5
4
|
Reference: https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/webhook-events-and-payloads
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
|
-
import logging
|
|
9
7
|
import json
|
|
8
|
+
import logging
|
|
10
9
|
import typing as t
|
|
11
10
|
from dataclasses import dataclass
|
|
11
|
+
|
|
12
12
|
from .signature import check_signature
|
|
13
13
|
from .utils.mime import get_mime_components
|
|
14
14
|
|
|
@@ -17,79 +17,79 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
|
|
18
18
|
@dataclass
|
|
19
19
|
class Event:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
"""
|
|
21
|
+
Represents a GitHub webhook event.
|
|
22
|
+
"""
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
name: str
|
|
25
|
+
""" The name of the event. Could be `pull_request`, for example."""
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
delivery_id: str
|
|
28
|
+
"""The delivery ID of the event."""
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
signature: t.Optional[str]
|
|
31
|
+
"""The signature of the event. Will only be set if a Webhook-secret is configured on the
|
|
32
|
+
client side (e.g. in [`Webhook.secret`][github_bot_api.webhook.Webhook] / if the *webhook_secret* parameter is
|
|
33
|
+
passed to [`accept_event()`][github_bot_api.event.accept_event])."""
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
user_agent: str
|
|
36
|
+
"""The user agent invoking the webhook."""
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
payload: t.Dict[str, t.Any]
|
|
39
|
+
"""The event payload."""
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def accept_event(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
headers: t.Mapping[str, str],
|
|
44
|
+
raw_body: bytes,
|
|
45
|
+
webhook_secret: t.Optional[str] = None,
|
|
46
46
|
) -> Event:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
47
|
+
"""
|
|
48
|
+
Converts thee HTTP *headers* and the *raw_body* to an #Event object.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
headers: The HTTP headers. Must have `X-Github-Event`, `X-Github-Delivery`, `User-Agent`, `Content-Type`.
|
|
52
|
+
May have `X-Hub-Signature` or `X-Hub-Signature-256`.
|
|
53
|
+
raw_body: The raw request body for the event. This is converted into a JSON payload.
|
|
54
|
+
webhook_secret: If specified, the `X-Hub-Signature` or `X-Hub-Signature-256` headers are used to verify
|
|
55
|
+
the signature of the payload. If not specified, the client does not validate the signature.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
event_name = headers.get("X-GitHub-Event")
|
|
59
|
+
delivery_id = headers.get("X-GitHub-Delivery")
|
|
60
|
+
signature_1 = headers.get("X-Hub-Signature")
|
|
61
|
+
signature_256 = headers.get("X-Hub-Signature-256")
|
|
62
|
+
user_agent = headers.get("User-Agent")
|
|
63
|
+
content_type = headers.get("Content-Type")
|
|
64
|
+
|
|
65
|
+
if not event_name or not delivery_id or not user_agent or not content_type:
|
|
66
|
+
raise InvalidRequest("missing required headers")
|
|
67
|
+
if webhook_secret is not None and not signature_1 and not signature_256:
|
|
68
|
+
raise InvalidRequest("webhook secret is configured but no signature header was received")
|
|
69
|
+
|
|
70
|
+
mime_type, parameters = get_mime_components(content_type)
|
|
71
|
+
if mime_type != "application/json":
|
|
72
|
+
raise InvalidRequest(f"expected Content-Type: application/json, got {content_type}")
|
|
73
|
+
encoding = dict(parameters).get("encoding", "UTF-8")
|
|
74
|
+
|
|
75
|
+
if webhook_secret is not None:
|
|
76
|
+
if signature_256:
|
|
77
|
+
check_signature(signature_256, raw_body, webhook_secret.encode("ascii"), algo="sha256")
|
|
78
|
+
elif signature_1:
|
|
79
|
+
check_signature(signature_1, raw_body, webhook_secret.encode("ascii"), algo="sha1")
|
|
80
|
+
else:
|
|
81
|
+
raise RuntimeError
|
|
82
|
+
|
|
83
|
+
return Event(
|
|
84
|
+
event_name,
|
|
85
|
+
delivery_id,
|
|
86
|
+
signature_256 or signature_1,
|
|
87
|
+
user_agent,
|
|
88
|
+
json.loads(raw_body.decode(encoding)),
|
|
89
|
+
)
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
class InvalidRequest(Exception):
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
"""
|
|
94
|
+
Raised when an invalid request is passed to #accept_event().
|
|
95
|
+
"""
|
github_bot_api/flask.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
"""
|
|
3
2
|
Flask binding for handling GitHub webhook events.
|
|
4
3
|
|
|
@@ -23,38 +22,40 @@ flask_app.run()
|
|
|
23
22
|
```
|
|
24
23
|
"""
|
|
25
24
|
|
|
26
|
-
import flask
|
|
27
25
|
import typing as t
|
|
26
|
+
|
|
27
|
+
import flask
|
|
28
|
+
|
|
28
29
|
from .event import accept_event
|
|
29
30
|
from .webhook import Webhook
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def create_event_handler(webhook: Webhook) -> t.Callable[[], t.Tuple[t.Text, int, t.Dict[str, str]]]:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
"""
|
|
35
|
+
Creates an event handler flask view that interprets the received HTTP request as a GitHub application
|
|
36
|
+
event and dispatches it via #webhook.dispatch().
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def event_handler():
|
|
40
|
+
event = accept_event(
|
|
41
|
+
t.cast(t.Mapping[str, str], flask.request.headers), flask.request.get_data(), webhook.secret
|
|
42
|
+
)
|
|
43
|
+
webhook.dispatch(event)
|
|
44
|
+
return "", 202, {}
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
event = accept_event(
|
|
40
|
-
t.cast(t.Mapping[str, str], flask.request.headers),
|
|
41
|
-
flask.request.get_data(),
|
|
42
|
-
webhook.secret)
|
|
43
|
-
webhook.dispatch(event)
|
|
44
|
-
return '', 202, {}
|
|
45
|
-
return event_handler
|
|
46
|
+
return event_handler
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
def create_flask_app(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
name: str,
|
|
51
|
+
webhook: Webhook,
|
|
52
|
+
path: str = "/event-handler",
|
|
52
53
|
) -> flask.Flask:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
"""
|
|
55
|
+
Creates a new #flask.Flask application with a `POST` event handler under the given *path* (defaulting
|
|
56
|
+
to `/event-handler`). This is a useful shorthand to attach your #Webhook to an HTTP server.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
flask_app = flask.Flask(name)
|
|
60
|
+
flask_app.route(path, methods=["POST"])(create_event_handler(webhook))
|
|
61
|
+
return flask_app
|