monalisten 0.1.0__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.
- monalisten-0.1.0/PKG-INFO +247 -0
- monalisten-0.1.0/README.md +235 -0
- monalisten-0.1.0/pyproject.toml +35 -0
- monalisten-0.1.0/src/monalisten/__init__.py +4 -0
- monalisten-0.1.0/src/monalisten/_core.py +123 -0
- monalisten-0.1.0/src/monalisten/py.typed +0 -0
- monalisten-0.1.0/src/monalisten/types.py +157 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: monalisten
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Monalisten is an async library for handling GitHub webhook events in an easy way
|
|
5
|
+
Author: trag1c
|
|
6
|
+
Author-email: trag1c <dev@jakubr.me>
|
|
7
|
+
Requires-Dist: githubkit~=0.13
|
|
8
|
+
Requires-Dist: httpx>=0.28.1
|
|
9
|
+
Requires-Dist: httpx-sse>=0.4.1
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
[](https://github.com/astral-sh/uv)
|
|
14
|
+
[](https://github.com/astral-sh/ruff)
|
|
15
|
+
|
|
16
|
+
# Monalisten
|
|
17
|
+
|
|
18
|
+
Monalisten is a Python 3.9+ asynchronous library that helps you handle webhook
|
|
19
|
+
events received from GitHub in an easy way. It is built on top of the amazing
|
|
20
|
+
[`githubkit`][githubkit] and [`httpx`][httpx] libraries and relies on [SSE]
|
|
21
|
+
(with [`httpx-sse`][httpx-sse]) to stream events without exposing any endpoints.
|
|
22
|
+
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Usage](#usage)
|
|
25
|
+
- [Foreword on how this works](#foreword-on-how-this-works)
|
|
26
|
+
- [Basic example](#basic-example)
|
|
27
|
+
- [One event, multiple hooks](#one-event-multiple-hooks)
|
|
28
|
+
- [One hook, multiple events](#one-hook-multiple-events)
|
|
29
|
+
- [Wildcard hooks](#wildcard-hooks)
|
|
30
|
+
- [Warnings & authentication behavior](#warnings--authentication-behavior)
|
|
31
|
+
- [API reference](#api-reference)
|
|
32
|
+
- [GitHub event type reference](#github-event-type-reference)
|
|
33
|
+
- [`monalisten.types` reference](#monalistentypes-reference)
|
|
34
|
+
- [License](#license)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
`monalisten` is available on PyPI and can be installed with any package manager:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
pip install monalisten
|
|
42
|
+
# or
|
|
43
|
+
poetry add monalisten
|
|
44
|
+
# or
|
|
45
|
+
uv add monalisten
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You can also install it from source:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pip install git+https://github.com/trag1c/monalisten.git
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Foreword on how this works
|
|
58
|
+
|
|
59
|
+
GitHub webhooks can only send event data to publicly accessible HTTP endpoints.
|
|
60
|
+
If your environment is behind a firewall, or a NAT, or you simply don't want to
|
|
61
|
+
set up a server, you can use a relay service, like [smee.io]. It generates a
|
|
62
|
+
unique relay URL to which GitHub sends requests to, and the relay then streams
|
|
63
|
+
them to your local client via SSE. Monalisten connects to the relay's SSE URL
|
|
64
|
+
and receives events as they arrive without any direct incoming connection to
|
|
65
|
+
your machine.
|
|
66
|
+
|
|
67
|
+
> [!warning]
|
|
68
|
+
> Relay URLs are essentially private endpoints. Anyone who knows your relay URL
|
|
69
|
+
> can send forged events. To mitigate this, configure a **webhook secret** in
|
|
70
|
+
> your GitHub repository or organization webhook settings. Pass the same secret
|
|
71
|
+
> to Monalisten through the `token` parameter. Now, Monalisten will validate
|
|
72
|
+
> incoming payloads and discard invalid ones.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### Basic example
|
|
76
|
+
|
|
77
|
+
```py
|
|
78
|
+
import asyncio
|
|
79
|
+
|
|
80
|
+
from monalisten import Monalisten
|
|
81
|
+
from monalisten.types import PushEvent
|
|
82
|
+
|
|
83
|
+
client = Monalisten("https://smee.io/aBCDef1gHijKLM2N", token="foobar")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@client.on("push")
|
|
87
|
+
async def log_push(event: PushEvent) -> None:
|
|
88
|
+
actor = event.sender.login if event.sender else "Someone"
|
|
89
|
+
print(f"{actor} pushed to the repo!")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
asyncio.run(client.listen())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Monalisten heavily relies on the [`githubkit`][githubkit] SDK for parsing and
|
|
96
|
+
verifying payloads. The `monalisten.types` module (meant for type annotations)
|
|
97
|
+
is actually a re-export of the `githubkit.versions.v2022_11_28.webhooks` module!
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
### One event, multiple hooks
|
|
101
|
+
|
|
102
|
+
You can decorate several functions with the same event passed to
|
|
103
|
+
`Monalisten.on`, and both of them will be registered:
|
|
104
|
+
|
|
105
|
+
```py
|
|
106
|
+
@client.on("pull_request")
|
|
107
|
+
async def log_opened_pr(event: PullRequestEvent) -> None:
|
|
108
|
+
if event.action != "opened":
|
|
109
|
+
return
|
|
110
|
+
print(f"New PR: #{event.number}")
|
|
111
|
+
|
|
112
|
+
@client.on("pull_request")
|
|
113
|
+
async def log_pr_action(event: PullRequestEvent) -> None:
|
|
114
|
+
print(f"Something happened to PR #{event.number}!")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
When an event type has several hooks attached, they're all run concurrently.
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
### One hook, multiple events
|
|
121
|
+
|
|
122
|
+
You can decorate the same function with `Monalisten.on` several times:
|
|
123
|
+
|
|
124
|
+
```py
|
|
125
|
+
@client.on("pull_request")
|
|
126
|
+
@client.on("push")
|
|
127
|
+
async def log_things(event: PullRequestEvent | PushEvent) -> None:
|
|
128
|
+
if "PullRequest" in type(event).__name__:
|
|
129
|
+
print("Something happened to a PR!")
|
|
130
|
+
else:
|
|
131
|
+
print("Someone pushed!")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
### Wildcard hooks
|
|
136
|
+
|
|
137
|
+
You can define a hook to be triggered for ALL events by setting the event name
|
|
138
|
+
to `*`:
|
|
139
|
+
|
|
140
|
+
```py
|
|
141
|
+
@client.on("*")
|
|
142
|
+
async def log(event: WebhookEvent) -> None:
|
|
143
|
+
print(f"Something definitely happened... a {type(event).__name__} perhaps")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
### Warnings & authentication behavior
|
|
148
|
+
|
|
149
|
+
During its authentication step, Monalisten can issue warnings for unexpected
|
|
150
|
+
state if you pass `log_auth_warnings=True` when creating a client.
|
|
151
|
+
|
|
152
|
+
Monalisten will issue warnings in the following cases:
|
|
153
|
+
|
|
154
|
+
* the client sets a token, but:
|
|
155
|
+
* the received event doesn't have a signature header
|
|
156
|
+
* the received event's signature cannot be validated with the client's token
|
|
157
|
+
|
|
158
|
+
(the event is not processed in both cases)
|
|
159
|
+
|
|
160
|
+
* the client doesn't set a token, but the received event has a signature header
|
|
161
|
+
(the event is still processed)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
### API reference
|
|
165
|
+
|
|
166
|
+
#### `Monalisten`
|
|
167
|
+
|
|
168
|
+
```py
|
|
169
|
+
class Monalisten:
|
|
170
|
+
def __init__(
|
|
171
|
+
self,
|
|
172
|
+
source: str,
|
|
173
|
+
*,
|
|
174
|
+
token: str | None = None,
|
|
175
|
+
log_auth_warnings: bool = False,
|
|
176
|
+
) -> None: ...
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Creates a Monalisten client streaming events from `source`, optionally secured
|
|
180
|
+
by the secret `token`. For details on `log_auth_warnings`, see the
|
|
181
|
+
[Warnings & authentication behavior](#warnings--authentication-behavior)
|
|
182
|
+
section.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
#### `Monalisten.listen`
|
|
186
|
+
|
|
187
|
+
```py
|
|
188
|
+
class Monalisten:
|
|
189
|
+
async def listen(self) -> None: ...
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Instantiates an internal HTTP client and starts streaming events from `source`.
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
#### `Monalisten.on`
|
|
196
|
+
|
|
197
|
+
```py
|
|
198
|
+
class Monalisten:
|
|
199
|
+
def on(self, event: HookTrigger) -> Callable[[Hook[H]], Hook[H]]: ...
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Meant to be used as a decorator. Registers the decorated function as a hook for
|
|
203
|
+
the `event` event. Raises an error if an invalid event name is provided.
|
|
204
|
+
`HookTrigger` is either a [GitHub event name](#github-event-type-reference) or
|
|
205
|
+
the [wildcard hook `"*"`](#wildcard-hooks).
|
|
206
|
+
|
|
207
|
+
In technical terms, this returns a function that registers the function passed
|
|
208
|
+
in as a Monalisten hook.
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
#### `MonalistenError`
|
|
212
|
+
|
|
213
|
+
```py
|
|
214
|
+
class MonalistenError(Exception): ...
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
An exception for errors encountered by the Monalisten client (e.g. invalid event
|
|
218
|
+
name or missing payload data).
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
### GitHub event name reference
|
|
222
|
+
|
|
223
|
+
For a list of event names that can be passed to `Monalisten.on`, see GitHub's
|
|
224
|
+
documentation page on [Webhook events and payloads][gh-events].
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
### `monalisten.types` reference
|
|
228
|
+
|
|
229
|
+
For a list of type names that can be used as event annotations, see the
|
|
230
|
+
[src/monalisten/types.py][githubkit-types] file, or, if you use one,
|
|
231
|
+
rely on your LSP's autocomplete!
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
`monalisten` is licensed under the [MIT License].
|
|
236
|
+
© [trag1c], 2025
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
[githubkit]: https://github.com/yanyongyu/githubkit
|
|
240
|
+
[httpx]: https://github.com/encode/httpx
|
|
241
|
+
[SSE]: https://en.wikipedia.org/wiki/Server-sent_events
|
|
242
|
+
[httpx-sse]: https://github.com/florimondmanca/httpx-sse
|
|
243
|
+
[smee.io]: https://smee.io/
|
|
244
|
+
[gh-events]: https://docs.github.com/en/webhooks/webhook-events-and-payloads
|
|
245
|
+
[githubkit-types]: https://github.com/trag1c/monalisten/blob/main/src/monalisten/types.py
|
|
246
|
+
[MIT License]: https://opensource.org/license/mit
|
|
247
|
+
[trag1c]: https://github.com/trag1c
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
[](https://github.com/astral-sh/uv)
|
|
2
|
+
[](https://github.com/astral-sh/ruff)
|
|
3
|
+
|
|
4
|
+
# Monalisten
|
|
5
|
+
|
|
6
|
+
Monalisten is a Python 3.9+ asynchronous library that helps you handle webhook
|
|
7
|
+
events received from GitHub in an easy way. It is built on top of the amazing
|
|
8
|
+
[`githubkit`][githubkit] and [`httpx`][httpx] libraries and relies on [SSE]
|
|
9
|
+
(with [`httpx-sse`][httpx-sse]) to stream events without exposing any endpoints.
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Usage](#usage)
|
|
13
|
+
- [Foreword on how this works](#foreword-on-how-this-works)
|
|
14
|
+
- [Basic example](#basic-example)
|
|
15
|
+
- [One event, multiple hooks](#one-event-multiple-hooks)
|
|
16
|
+
- [One hook, multiple events](#one-hook-multiple-events)
|
|
17
|
+
- [Wildcard hooks](#wildcard-hooks)
|
|
18
|
+
- [Warnings & authentication behavior](#warnings--authentication-behavior)
|
|
19
|
+
- [API reference](#api-reference)
|
|
20
|
+
- [GitHub event type reference](#github-event-type-reference)
|
|
21
|
+
- [`monalisten.types` reference](#monalistentypes-reference)
|
|
22
|
+
- [License](#license)
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
`monalisten` is available on PyPI and can be installed with any package manager:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pip install monalisten
|
|
30
|
+
# or
|
|
31
|
+
poetry add monalisten
|
|
32
|
+
# or
|
|
33
|
+
uv add monalisten
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
You can also install it from source:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pip install git+https://github.com/trag1c/monalisten.git
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Foreword on how this works
|
|
46
|
+
|
|
47
|
+
GitHub webhooks can only send event data to publicly accessible HTTP endpoints.
|
|
48
|
+
If your environment is behind a firewall, or a NAT, or you simply don't want to
|
|
49
|
+
set up a server, you can use a relay service, like [smee.io]. It generates a
|
|
50
|
+
unique relay URL to which GitHub sends requests to, and the relay then streams
|
|
51
|
+
them to your local client via SSE. Monalisten connects to the relay's SSE URL
|
|
52
|
+
and receives events as they arrive without any direct incoming connection to
|
|
53
|
+
your machine.
|
|
54
|
+
|
|
55
|
+
> [!warning]
|
|
56
|
+
> Relay URLs are essentially private endpoints. Anyone who knows your relay URL
|
|
57
|
+
> can send forged events. To mitigate this, configure a **webhook secret** in
|
|
58
|
+
> your GitHub repository or organization webhook settings. Pass the same secret
|
|
59
|
+
> to Monalisten through the `token` parameter. Now, Monalisten will validate
|
|
60
|
+
> incoming payloads and discard invalid ones.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Basic example
|
|
64
|
+
|
|
65
|
+
```py
|
|
66
|
+
import asyncio
|
|
67
|
+
|
|
68
|
+
from monalisten import Monalisten
|
|
69
|
+
from monalisten.types import PushEvent
|
|
70
|
+
|
|
71
|
+
client = Monalisten("https://smee.io/aBCDef1gHijKLM2N", token="foobar")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@client.on("push")
|
|
75
|
+
async def log_push(event: PushEvent) -> None:
|
|
76
|
+
actor = event.sender.login if event.sender else "Someone"
|
|
77
|
+
print(f"{actor} pushed to the repo!")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
asyncio.run(client.listen())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Monalisten heavily relies on the [`githubkit`][githubkit] SDK for parsing and
|
|
84
|
+
verifying payloads. The `monalisten.types` module (meant for type annotations)
|
|
85
|
+
is actually a re-export of the `githubkit.versions.v2022_11_28.webhooks` module!
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
### One event, multiple hooks
|
|
89
|
+
|
|
90
|
+
You can decorate several functions with the same event passed to
|
|
91
|
+
`Monalisten.on`, and both of them will be registered:
|
|
92
|
+
|
|
93
|
+
```py
|
|
94
|
+
@client.on("pull_request")
|
|
95
|
+
async def log_opened_pr(event: PullRequestEvent) -> None:
|
|
96
|
+
if event.action != "opened":
|
|
97
|
+
return
|
|
98
|
+
print(f"New PR: #{event.number}")
|
|
99
|
+
|
|
100
|
+
@client.on("pull_request")
|
|
101
|
+
async def log_pr_action(event: PullRequestEvent) -> None:
|
|
102
|
+
print(f"Something happened to PR #{event.number}!")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
When an event type has several hooks attached, they're all run concurrently.
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
### One hook, multiple events
|
|
109
|
+
|
|
110
|
+
You can decorate the same function with `Monalisten.on` several times:
|
|
111
|
+
|
|
112
|
+
```py
|
|
113
|
+
@client.on("pull_request")
|
|
114
|
+
@client.on("push")
|
|
115
|
+
async def log_things(event: PullRequestEvent | PushEvent) -> None:
|
|
116
|
+
if "PullRequest" in type(event).__name__:
|
|
117
|
+
print("Something happened to a PR!")
|
|
118
|
+
else:
|
|
119
|
+
print("Someone pushed!")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
### Wildcard hooks
|
|
124
|
+
|
|
125
|
+
You can define a hook to be triggered for ALL events by setting the event name
|
|
126
|
+
to `*`:
|
|
127
|
+
|
|
128
|
+
```py
|
|
129
|
+
@client.on("*")
|
|
130
|
+
async def log(event: WebhookEvent) -> None:
|
|
131
|
+
print(f"Something definitely happened... a {type(event).__name__} perhaps")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
### Warnings & authentication behavior
|
|
136
|
+
|
|
137
|
+
During its authentication step, Monalisten can issue warnings for unexpected
|
|
138
|
+
state if you pass `log_auth_warnings=True` when creating a client.
|
|
139
|
+
|
|
140
|
+
Monalisten will issue warnings in the following cases:
|
|
141
|
+
|
|
142
|
+
* the client sets a token, but:
|
|
143
|
+
* the received event doesn't have a signature header
|
|
144
|
+
* the received event's signature cannot be validated with the client's token
|
|
145
|
+
|
|
146
|
+
(the event is not processed in both cases)
|
|
147
|
+
|
|
148
|
+
* the client doesn't set a token, but the received event has a signature header
|
|
149
|
+
(the event is still processed)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
### API reference
|
|
153
|
+
|
|
154
|
+
#### `Monalisten`
|
|
155
|
+
|
|
156
|
+
```py
|
|
157
|
+
class Monalisten:
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
source: str,
|
|
161
|
+
*,
|
|
162
|
+
token: str | None = None,
|
|
163
|
+
log_auth_warnings: bool = False,
|
|
164
|
+
) -> None: ...
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Creates a Monalisten client streaming events from `source`, optionally secured
|
|
168
|
+
by the secret `token`. For details on `log_auth_warnings`, see the
|
|
169
|
+
[Warnings & authentication behavior](#warnings--authentication-behavior)
|
|
170
|
+
section.
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
#### `Monalisten.listen`
|
|
174
|
+
|
|
175
|
+
```py
|
|
176
|
+
class Monalisten:
|
|
177
|
+
async def listen(self) -> None: ...
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Instantiates an internal HTTP client and starts streaming events from `source`.
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
#### `Monalisten.on`
|
|
184
|
+
|
|
185
|
+
```py
|
|
186
|
+
class Monalisten:
|
|
187
|
+
def on(self, event: HookTrigger) -> Callable[[Hook[H]], Hook[H]]: ...
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Meant to be used as a decorator. Registers the decorated function as a hook for
|
|
191
|
+
the `event` event. Raises an error if an invalid event name is provided.
|
|
192
|
+
`HookTrigger` is either a [GitHub event name](#github-event-type-reference) or
|
|
193
|
+
the [wildcard hook `"*"`](#wildcard-hooks).
|
|
194
|
+
|
|
195
|
+
In technical terms, this returns a function that registers the function passed
|
|
196
|
+
in as a Monalisten hook.
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
#### `MonalistenError`
|
|
200
|
+
|
|
201
|
+
```py
|
|
202
|
+
class MonalistenError(Exception): ...
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
An exception for errors encountered by the Monalisten client (e.g. invalid event
|
|
206
|
+
name or missing payload data).
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
### GitHub event name reference
|
|
210
|
+
|
|
211
|
+
For a list of event names that can be passed to `Monalisten.on`, see GitHub's
|
|
212
|
+
documentation page on [Webhook events and payloads][gh-events].
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
### `monalisten.types` reference
|
|
216
|
+
|
|
217
|
+
For a list of type names that can be used as event annotations, see the
|
|
218
|
+
[src/monalisten/types.py][githubkit-types] file, or, if you use one,
|
|
219
|
+
rely on your LSP's autocomplete!
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
`monalisten` is licensed under the [MIT License].
|
|
224
|
+
© [trag1c], 2025
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
[githubkit]: https://github.com/yanyongyu/githubkit
|
|
228
|
+
[httpx]: https://github.com/encode/httpx
|
|
229
|
+
[SSE]: https://en.wikipedia.org/wiki/Server-sent_events
|
|
230
|
+
[httpx-sse]: https://github.com/florimondmanca/httpx-sse
|
|
231
|
+
[smee.io]: https://smee.io/
|
|
232
|
+
[gh-events]: https://docs.github.com/en/webhooks/webhook-events-and-payloads
|
|
233
|
+
[githubkit-types]: https://github.com/trag1c/monalisten/blob/main/src/monalisten/types.py
|
|
234
|
+
[MIT License]: https://opensource.org/license/mit
|
|
235
|
+
[trag1c]: https://github.com/trag1c
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "monalisten"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Monalisten is an async library for handling GitHub webhook events in an easy way"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "trag1c", email = "dev@jakubr.me" }]
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
dependencies = ["githubkit~=0.13", "httpx>=0.28.1", "httpx-sse>=0.4.1"]
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["uv_build>=0.8.3,<0.9.0"]
|
|
12
|
+
build-backend = "uv_build"
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = [
|
|
16
|
+
"aiohttp>=3.12.15",
|
|
17
|
+
"pyright>=1.1.403",
|
|
18
|
+
"pytest>=8.4.1",
|
|
19
|
+
"pytest-asyncio>=1.1.0",
|
|
20
|
+
"ruff>=0.12.8",
|
|
21
|
+
"taplo>=0.9.3",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
asyncio_mode = "auto"
|
|
26
|
+
|
|
27
|
+
[tool.ruff]
|
|
28
|
+
target-version = "py39"
|
|
29
|
+
|
|
30
|
+
[tool.ruff.lint]
|
|
31
|
+
select = ["ALL"]
|
|
32
|
+
ignore = ["COM", "D", "S", "PLR2004"]
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint.per-file-ignores]
|
|
35
|
+
"tests/*" = ["FBT", "T201", "SLF001"]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import warnings
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from itertools import chain
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from githubkit import webhooks
|
|
11
|
+
from githubkit.versions.v2022_11_28.webhooks import VALID_EVENT_NAMES
|
|
12
|
+
from httpx_sse import ServerSentEvent, aconnect_sse
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Awaitable, Callable
|
|
17
|
+
|
|
18
|
+
from githubkit.versions.v2022_11_28.webhooks import EventNameType, WebhookEvent
|
|
19
|
+
from typing_extensions import TypeAlias
|
|
20
|
+
|
|
21
|
+
H = TypeVar("H", bound="WebhookEvent")
|
|
22
|
+
Hook: TypeAlias = "Callable[[H], Awaitable[None]]"
|
|
23
|
+
HookTrigger: TypeAlias = 'Literal[EventNameType, "*"]'
|
|
24
|
+
|
|
25
|
+
GUID_HEADER = "x-github-delivery"
|
|
26
|
+
EVENT_HEADER = "x-github-event"
|
|
27
|
+
SIG_HEADER = "x-hub-signature-256"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MonalistenError(Exception):
|
|
31
|
+
"""Exception for errors encountered by the Monalisten client."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Monalisten:
|
|
35
|
+
"""
|
|
36
|
+
A Monalisten client streaming events from `source`, optionally secured by the secret
|
|
37
|
+
`token`. For details on `log_auth_warnings`, see the "Warnings & authentication
|
|
38
|
+
behavior" section of the documentation.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self, source: str, *, token: str | None = None, log_auth_warnings: bool = False
|
|
43
|
+
) -> None:
|
|
44
|
+
self._source = source
|
|
45
|
+
self._token = token
|
|
46
|
+
self._log = log_auth_warnings
|
|
47
|
+
self._hooks = defaultdict[HookTrigger, list[Hook]](list)
|
|
48
|
+
|
|
49
|
+
def _passes_auth(self, event_data: dict[str, Any]) -> bool:
|
|
50
|
+
if not self._token:
|
|
51
|
+
if SIG_HEADER in event_data:
|
|
52
|
+
self._warn(
|
|
53
|
+
event_data, f"Received {SIG_HEADER} header, but no token was set"
|
|
54
|
+
)
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
if not (signature := event_data.get(SIG_HEADER)):
|
|
58
|
+
self._warn(event_data, f"Missing {SIG_HEADER} header")
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
if webhooks.verify(self._token, event_data["body"], signature):
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
self._warn(event_data, f"{SIG_HEADER} header does not match set token")
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
def _warn(self, event_data: dict[str, Any], message: str) -> None:
|
|
68
|
+
if not self._log:
|
|
69
|
+
return
|
|
70
|
+
name = event_data.get(EVENT_HEADER, "unknown")
|
|
71
|
+
guid = event_data.get(GUID_HEADER, "unknown")
|
|
72
|
+
warnings.warn(f"Event {name} ({guid}): {message}", stacklevel=3)
|
|
73
|
+
|
|
74
|
+
def on(self, event: HookTrigger) -> Callable[[Hook[H]], Hook[H]]:
|
|
75
|
+
"""Register the decorated function as a hook for the `event` event."""
|
|
76
|
+
if event not in VALID_EVENT_NAMES and event != "*":
|
|
77
|
+
msg = f"Invalid event name: {event!r}"
|
|
78
|
+
raise MonalistenError(msg)
|
|
79
|
+
|
|
80
|
+
def wrapper(hook: Hook[H]) -> Hook[H]:
|
|
81
|
+
self._hooks[event].append(hook)
|
|
82
|
+
return hook
|
|
83
|
+
|
|
84
|
+
return wrapper
|
|
85
|
+
|
|
86
|
+
async def _handle_event(self, event: ServerSentEvent) -> None:
|
|
87
|
+
if event.data == "{}":
|
|
88
|
+
return
|
|
89
|
+
data = event.json()
|
|
90
|
+
|
|
91
|
+
if not self._passes_auth(data):
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if not (event_name := data.get(EVENT_HEADER)):
|
|
95
|
+
msg = f"received data is missing the {EVENT_HEADER} header"
|
|
96
|
+
raise MonalistenError(msg)
|
|
97
|
+
|
|
98
|
+
if not (body := data.get("body")):
|
|
99
|
+
msg = "received data doesn't contain a body"
|
|
100
|
+
raise MonalistenError(msg)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
webhook_event = cast("WebhookEvent", webhooks.parse_obj(event_name, body))
|
|
104
|
+
except ValidationError as pydantic_exc:
|
|
105
|
+
msg = "the received payload could not be parsed as an event"
|
|
106
|
+
raise MonalistenError(msg) from pydantic_exc
|
|
107
|
+
|
|
108
|
+
coros = (
|
|
109
|
+
hook(webhook_event)
|
|
110
|
+
for hook in chain.from_iterable(
|
|
111
|
+
self._hooks.get(hook_kind, []) for hook_kind in (event_name, "*")
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
await asyncio.gather(*coros)
|
|
115
|
+
|
|
116
|
+
async def listen(self) -> None:
|
|
117
|
+
"""Start an internal HTTP client and stream events from `source`."""
|
|
118
|
+
async with (
|
|
119
|
+
httpx.AsyncClient(timeout=None) as client,
|
|
120
|
+
aconnect_sse(client, "GET", self._source) as sse,
|
|
121
|
+
):
|
|
122
|
+
async for event in sse.aiter_sse():
|
|
123
|
+
await self._handle_event(event)
|
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from githubkit.versions.v2022_11_28.webhooks import (
|
|
2
|
+
BranchProtectionConfigurationEvent,
|
|
3
|
+
BranchProtectionRuleEvent,
|
|
4
|
+
CheckRunEvent,
|
|
5
|
+
CheckSuiteEvent,
|
|
6
|
+
CodeScanningAlertEvent,
|
|
7
|
+
CommitCommentEvent,
|
|
8
|
+
CreateEvent,
|
|
9
|
+
CustomPropertyEvent,
|
|
10
|
+
CustomPropertyValuesEvent,
|
|
11
|
+
DeleteEvent,
|
|
12
|
+
DependabotAlertEvent,
|
|
13
|
+
DeployKeyEvent,
|
|
14
|
+
DeploymentEvent,
|
|
15
|
+
DeploymentProtectionRuleEvent,
|
|
16
|
+
DeploymentReviewEvent,
|
|
17
|
+
DeploymentStatusEvent,
|
|
18
|
+
DiscussionCommentEvent,
|
|
19
|
+
DiscussionEvent,
|
|
20
|
+
ForkEvent,
|
|
21
|
+
GithubAppAuthorizationEvent,
|
|
22
|
+
GollumEvent,
|
|
23
|
+
InstallationEvent,
|
|
24
|
+
InstallationRepositoriesEvent,
|
|
25
|
+
InstallationTargetEvent,
|
|
26
|
+
IssueCommentEvent,
|
|
27
|
+
IssueDependenciesEvent,
|
|
28
|
+
IssuesEvent,
|
|
29
|
+
LabelEvent,
|
|
30
|
+
MarketplacePurchaseEvent,
|
|
31
|
+
MemberEvent,
|
|
32
|
+
MembershipEvent,
|
|
33
|
+
MergeGroupEvent,
|
|
34
|
+
MetaEvent,
|
|
35
|
+
MilestoneEvent,
|
|
36
|
+
OrganizationEvent,
|
|
37
|
+
OrgBlockEvent,
|
|
38
|
+
PackageEvent,
|
|
39
|
+
PageBuildEvent,
|
|
40
|
+
PersonalAccessTokenRequestEvent,
|
|
41
|
+
PingEvent,
|
|
42
|
+
ProjectCardEvent,
|
|
43
|
+
ProjectColumnEvent,
|
|
44
|
+
ProjectEvent,
|
|
45
|
+
ProjectsV2Event,
|
|
46
|
+
ProjectsV2ItemEvent,
|
|
47
|
+
ProjectsV2StatusUpdateEvent,
|
|
48
|
+
PublicEvent,
|
|
49
|
+
PullRequestEvent,
|
|
50
|
+
PullRequestReviewCommentEvent,
|
|
51
|
+
PullRequestReviewEvent,
|
|
52
|
+
PullRequestReviewThreadEvent,
|
|
53
|
+
PushEvent,
|
|
54
|
+
RegistryPackageEvent,
|
|
55
|
+
ReleaseEvent,
|
|
56
|
+
RepositoryAdvisoryEvent,
|
|
57
|
+
RepositoryDispatchEvent,
|
|
58
|
+
RepositoryEvent,
|
|
59
|
+
RepositoryImportEvent,
|
|
60
|
+
RepositoryRulesetEvent,
|
|
61
|
+
RepositoryVulnerabilityAlertEvent,
|
|
62
|
+
SecretScanningAlertEvent,
|
|
63
|
+
SecretScanningAlertLocationEvent,
|
|
64
|
+
SecretScanningScanEvent,
|
|
65
|
+
SecurityAdvisoryEvent,
|
|
66
|
+
SecurityAndAnalysisEvent,
|
|
67
|
+
SponsorshipEvent,
|
|
68
|
+
StarEvent,
|
|
69
|
+
StatusEvent,
|
|
70
|
+
SubIssuesEvent,
|
|
71
|
+
TeamAddEvent,
|
|
72
|
+
TeamEvent,
|
|
73
|
+
WatchEvent,
|
|
74
|
+
WebhookEvent,
|
|
75
|
+
WorkflowDispatchEvent,
|
|
76
|
+
WorkflowJobEvent,
|
|
77
|
+
WorkflowRunEvent,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
__all__ = (
|
|
81
|
+
"BranchProtectionConfigurationEvent",
|
|
82
|
+
"BranchProtectionRuleEvent",
|
|
83
|
+
"CheckRunEvent",
|
|
84
|
+
"CheckSuiteEvent",
|
|
85
|
+
"CodeScanningAlertEvent",
|
|
86
|
+
"CommitCommentEvent",
|
|
87
|
+
"CreateEvent",
|
|
88
|
+
"CustomPropertyEvent",
|
|
89
|
+
"CustomPropertyValuesEvent",
|
|
90
|
+
"DeleteEvent",
|
|
91
|
+
"DependabotAlertEvent",
|
|
92
|
+
"DeployKeyEvent",
|
|
93
|
+
"DeploymentEvent",
|
|
94
|
+
"DeploymentProtectionRuleEvent",
|
|
95
|
+
"DeploymentReviewEvent",
|
|
96
|
+
"DeploymentStatusEvent",
|
|
97
|
+
"DiscussionCommentEvent",
|
|
98
|
+
"DiscussionEvent",
|
|
99
|
+
"ForkEvent",
|
|
100
|
+
"GithubAppAuthorizationEvent",
|
|
101
|
+
"GollumEvent",
|
|
102
|
+
"InstallationEvent",
|
|
103
|
+
"InstallationRepositoriesEvent",
|
|
104
|
+
"InstallationTargetEvent",
|
|
105
|
+
"IssueCommentEvent",
|
|
106
|
+
"IssueDependenciesEvent",
|
|
107
|
+
"IssuesEvent",
|
|
108
|
+
"LabelEvent",
|
|
109
|
+
"MarketplacePurchaseEvent",
|
|
110
|
+
"MemberEvent",
|
|
111
|
+
"MembershipEvent",
|
|
112
|
+
"MergeGroupEvent",
|
|
113
|
+
"MetaEvent",
|
|
114
|
+
"MilestoneEvent",
|
|
115
|
+
"OrgBlockEvent",
|
|
116
|
+
"OrganizationEvent",
|
|
117
|
+
"PackageEvent",
|
|
118
|
+
"PageBuildEvent",
|
|
119
|
+
"PersonalAccessTokenRequestEvent",
|
|
120
|
+
"PingEvent",
|
|
121
|
+
"ProjectCardEvent",
|
|
122
|
+
"ProjectColumnEvent",
|
|
123
|
+
"ProjectEvent",
|
|
124
|
+
"ProjectsV2Event",
|
|
125
|
+
"ProjectsV2ItemEvent",
|
|
126
|
+
"ProjectsV2StatusUpdateEvent",
|
|
127
|
+
"PublicEvent",
|
|
128
|
+
"PullRequestEvent",
|
|
129
|
+
"PullRequestReviewCommentEvent",
|
|
130
|
+
"PullRequestReviewEvent",
|
|
131
|
+
"PullRequestReviewThreadEvent",
|
|
132
|
+
"PushEvent",
|
|
133
|
+
"RegistryPackageEvent",
|
|
134
|
+
"ReleaseEvent",
|
|
135
|
+
"RepositoryAdvisoryEvent",
|
|
136
|
+
"RepositoryDispatchEvent",
|
|
137
|
+
"RepositoryEvent",
|
|
138
|
+
"RepositoryImportEvent",
|
|
139
|
+
"RepositoryRulesetEvent",
|
|
140
|
+
"RepositoryVulnerabilityAlertEvent",
|
|
141
|
+
"SecretScanningAlertEvent",
|
|
142
|
+
"SecretScanningAlertLocationEvent",
|
|
143
|
+
"SecretScanningScanEvent",
|
|
144
|
+
"SecurityAdvisoryEvent",
|
|
145
|
+
"SecurityAndAnalysisEvent",
|
|
146
|
+
"SponsorshipEvent",
|
|
147
|
+
"StarEvent",
|
|
148
|
+
"StatusEvent",
|
|
149
|
+
"SubIssuesEvent",
|
|
150
|
+
"TeamAddEvent",
|
|
151
|
+
"TeamEvent",
|
|
152
|
+
"WatchEvent",
|
|
153
|
+
"WebhookEvent",
|
|
154
|
+
"WorkflowDispatchEvent",
|
|
155
|
+
"WorkflowJobEvent",
|
|
156
|
+
"WorkflowRunEvent",
|
|
157
|
+
)
|