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.
@@ -1,9 +1,8 @@
1
-
2
- __author__ = 'Niklas Rosenstein <nrosenstein@palantir.com>'
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__ = ['GithubApp', 'Event', 'Webhook']
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 # type: ignore[import]
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('T')
18
+ T = t.TypeVar("T")
21
19
  logger = logging.getLogger(__name__)
22
- user_agent = f'python/{sys.version.split()[0]} github-bot-api/{__version__}'
20
+ user_agent = f"python/{sys.version.split()[0]} github-bot-api/{__version__}"
23
21
 
24
22
  if t.TYPE_CHECKING:
25
- import github
23
+ import github
26
24
 
27
25
 
28
26
  @dataclasses.dataclass
29
27
  class GithubClientSettings:
30
- """
31
- Settings for constructing a #github.Github client object.
32
- """
33
-
34
- base_url: t.Optional[str] = None
35
- user_agent: t.Optional[str] = None
36
- timeout: t.Optional[float] = None
37
- per_page: t.Optional[int] = None
38
- verify: t.Optional[bool] = None
39
- retry: t.Optional[urllib3.Retry] = None
40
-
41
- def update(self, other: 'GithubClientSettings') -> 'GithubClientSettings':
42
- result = GithubClientSettings()
43
- for field in dataclasses.fields(self):
44
- value = getattr(other, field.name)
45
- if value is None:
46
- value = getattr(self, field.name)
47
- setattr(result, field.name, value)
48
- return result
49
-
50
- def make_client(self, login_or_token: t.Optional[str] = None, jwt: t.Optional[str] = None) -> 'github.Github':
51
- import github, github.MainClass
52
- return github.Github(
53
- login_or_token=login_or_token,
54
- jwt=jwt,
55
- base_url=self.base_url or github.MainClass.DEFAULT_BASE_URL, # type: ignore[attr-defined]
56
- user_agent=self.user_agent or "PyGithub/Python",
57
- timeout=coalesce(self.timeout, github.MainClass.DEFAULT_TIMEOUT), # type: ignore[attr-defined]
58
- per_page=coalesce(self.per_page, github.MainClass.DEFAULT_PER_PAGE), # type: ignore[attr-defined]
59
- verify=coalesce(self.verify, True),
60
- retry=self.retry)
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
- Create a user agent string for the PyGithub client, including the installation if specified.
67
+ Represents a GitHub application and all the required details.
111
68
  """
112
69
 
113
- user_agent = f'{self.user_agent} PyGithub/python (app_id={self.app_id}'
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
- @property
119
- def jwt(self) -> TokenInfo:
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
- return self._jwt_supplier()
75
+ app_id: int
76
+ """GitHub Application ID."""
125
77
 
126
- @property
127
- def jwt_supplier(self) -> JwtSupplier:
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
- return JwtSupplier(self.app_id, self.private_key)
81
+ v3_api_url: str = PUBLIC_GITHUB_V3_API_URL
82
+ """GitHub API base URL. Defaults to the public GitHub API."""
133
83
 
134
- @property # type: ignore[misc]
135
- @deprecated.deprecated(reason='use GithubApp.app_client() instead', version='0.4.0')
136
- def client(self) -> 'github.Github':
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
- return self.app_client()
89
+ def _get_base_github_client_settings(self) -> GithubClientSettings:
90
+ return GithubClientSettings(self.v3_api_url, self.get_user_agent())
142
91
 
143
- def app_client(self, settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None) -> 'github.Github':
144
- """
145
- Returns a PyGithub client for your GitHub application.
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
- Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
148
- client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
149
- operation you perform.
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
- This requires you to install `PyGithub`.
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
- if isinstance(settings, dict):
155
- settings = GithubClientSettings(**settings)
156
- elif settings is None:
157
- settings = GithubClientSettings()
108
+ return self._jwt_supplier()
158
109
 
159
- settings = self._get_base_github_client_settings().update(settings)
160
- return settings.make_client(jwt=self.jwt.value)
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
- def __requestor(self, auth_header: str, installation_id: int) -> t.Dict[str, str]:
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
- def get_installation_token_supplier(self, installation_id: int) -> InstallationTokenSupplier:
169
- """
170
- Create an #InstallationTokenSupplier for your GitHub application to act within the scope of the given
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
- with self._lock:
175
- return self._installation_tokens.setdefault(
176
- installation_id,
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
- def installation_token(self, installation_id: int) -> TokenInfo:
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
- return self.get_installation_token_supplier(installation_id)()
129
+ if isinstance(settings, dict):
130
+ settings = GithubClientSettings(**settings)
131
+ elif settings is None:
132
+ settings = GithubClientSettings()
190
133
 
191
- def installation_client(
192
- self,
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
- Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
200
- client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
201
- operation you perform.
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
- This requires you to install `PyGithub`.
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
- if isinstance(settings, dict):
207
- settings = GithubClientSettings(**settings)
208
- elif settings is None:
209
- settings = GithubClientSettings()
164
+ return self.get_installation_token_supplier(installation_id)()
210
165
 
211
- token = self.installation_token(installation_id).value
212
- settings = self._get_base_github_client_settings().update(settings)
213
- return settings.make_client(login_or_token=token)
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
- Represents a GitHub webhook event.
22
- """
20
+ """
21
+ Represents a GitHub webhook event.
22
+ """
23
23
 
24
- #: The name of the event. Could be `pull_request`, for example.
25
- name: str
24
+ name: str
25
+ """ The name of the event. Could be `pull_request`, for example."""
26
26
 
27
- #: The delivery ID of the event.
28
- delivery_id: str
27
+ delivery_id: str
28
+ """The delivery ID of the event."""
29
29
 
30
- #: The signature of the event. Will only be set if a Webhook-secret is configured on the
31
- #: client side (e.g. in #Webhook.secret / if the *webhook_secret* parameter is passed to
32
- #: #accept_event()).
33
- signature: t.Optional[str]
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
- #: The user agent invoking the webhook.
36
- user_agent: str
35
+ user_agent: str
36
+ """The user agent invoking the webhook."""
37
37
 
38
- #: The event payload.
39
- payload: t.Dict[str, t.Any]
38
+ payload: t.Dict[str, t.Any]
39
+ """The event payload."""
40
40
 
41
41
 
42
42
  def accept_event(
43
- headers: t.Mapping[str, str],
44
- raw_body: bytes,
45
- webhook_secret: t.Optional[str] = None,
43
+ headers: t.Mapping[str, str],
44
+ raw_body: bytes,
45
+ webhook_secret: t.Optional[str] = None,
46
46
  ) -> Event:
47
- """
48
- Converts thee HTTP *headers* and the *raw_body* to an #Event object.
49
-
50
- # Arguments
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 or 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
- )
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
- Raised when an invalid request is passed to #accept_event().
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
- Creates an event handler flask view that interprets the received HTTP request as a GitHub application
35
- event and dispatches it via #webhook.dispatch().
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
- def event_handler():
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
- name: str,
50
- webhook: Webhook,
51
- path: str = '/event-handler',
50
+ name: str,
51
+ webhook: Webhook,
52
+ path: str = "/event-handler",
52
53
  ) -> flask.Flask:
53
- """
54
- Creates a new #flask.Flask application with a `POST` event handler under the given *path* (defaulting
55
- to `/event-handler`). This is a useful shorthand to attach your #Webhook to an HTTP server.
56
- """
57
-
58
- flask_app = flask.Flask(name)
59
- flask_app.route(path, methods=['POST'])(create_event_handler(webhook))
60
- return flask_app
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