-
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathapp.py
More file actions
232 lines (192 loc) · 7.93 KB
/
app.py
File metadata and controls
232 lines (192 loc) · 7.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
from functools import partial
import sys
import os
import signal
import trio
from glom import glom
import hypercorn
import hypercorn.trio
import quart
from quart import request
from quart_trio import QuartTrio
import gidgethub
from .db import PersistentStringSet
from .gh import GithubApp
# we should stash the delivery id in a contextvar and include it in logging
# also maybe structlog? eh print is so handy for now
# Maybe:
# send message on first PR, with basic background info – volunteer project,
# super appreciate their contribution, also means we're sometimes slow.
# how to ping?
#
# send message with invitation giving info
#
# after they accept, post a closed issue to welcome them, and invite them to
# ask any questions and introduce themselves there? and also highlight it in
# the chat? (include link to search for their contributions, as part of the
# introduction?)
#
# in the future would be nice if bot could do some pings, both ways
#
# include some specific suggestions on how to get help? assign a mentor from a
# list of volunteers?
#
# I almost wonder if it would be better to give membership on like, the 3rd
# merged PR
# with the bot keeping track, and posting an encouraging countdown on each
# merged PR, so it feels more like an incremental process, where you can see
# the milestone coming and then when you get there it *is* a milestone.
quart_app = QuartTrio(__name__)
github_app = GithubApp()
if "SENTRY_DSN" in os.environ:
import sentry_sdk
sentry_sdk.init(os.environ["SENTRY_DSN"])
@quart.got_request_exception.connect
async def error_handler(_, *, exception):
if isinstance(exception, Exception):
print(f"Logging error to sentry: {exception!r}")
sentry_sdk.capture_exception(exception)
else:
print(f"NOT logging error to sentry: {exception!r}")
@quart_app.route("/")
async def index():
return "Hi! 🐍🐍🐍"
@quart_app.route("/webhook/github", methods=["POST"])
async def webhook_github():
body = await request.get_data()
await github_app.dispatch_webhook(request.headers, body)
return ""
SENT_INVITATION = PersistentStringSet("SENT_INVITATION")
# dedent, remove single newlines (but not double-newlines), remove
# leading/trailing newlines
def _fix_markdown(s):
import textwrap
s = s.strip("\n")
s = textwrap.dedent(s)
s = s.replace("\n\n", "__PARAGRAPH_BREAK__")
s = s.replace("\n", " ")
s = s.replace("__PARAGRAPH_BREAK__", "\n\n")
return s
invite_message = _fix_markdown(
"""
Hey @{username}, it looks like that was the first time we merged one of
your PRs! Thanks so much! :tada: :birthday:
If you want to keep contributing, we'd love to have you. So, I just sent
you an invitation to join the python-trio organization on Github! If you
accept, then here's what will happen:
* Github will automatically subscribe you to notifications on all our
repositories. (But you can unsubscribe again if you don't want the
spam.)
* You'll be able to help us manage issues (add labels, close them, etc.)
* You'll be able to review and merge other people's pull requests
* You'll get a [member] badge next to your name when participating in the
Trio repos, and you'll have the option of adding your name to our
[member's page](https://github.com/orgs/python-trio/people) and putting
our icon on your Github profile
([details](https://help.github.com/en/articles/publicizing-or-hiding-organization-membership))
If you want to read more, [here's the relevant section in our contributing
guide](https://trio.readthedocs.io/en/latest/contributing.html#joining-the-team).
Alternatively, you're free to decline or ignore the invitation. You'll
still be able to contribute as much or as little as you like, and I won't
hassle you about joining again. But if you ever change your mind, just let
us know and we'll send another invitation. We'd love to have you, but more
importantly we want you to do whatever's best for you.
If you have any questions, well... I am just a [humble Python
script](https://github.com/python-trio/snekomatic), so I probably can't
help. But please do post a comment here, or [in our
chat](https://gitter.im/python-trio/general), or [on our
forum](https://trio.discourse.group/c/help-and-advice), whatever's
easiest, and someone will help you out!
"""
)
async def _member_state(gh_client, org, member):
# Returns "active" (they're a member), "pending" (they're not a member,
# but they have an invitation they haven't responded to yet), or None
# (they're not a member and don't have a pending invitation)
try:
response = await gh_client.getitem(
"/orgs/{org}/memberships/{username}",
url_vars={"org": org, "username": member},
)
except gidgethub.BadRequest as exc:
if exc.status_code == 404:
return None
else:
raise
else:
return glom(response, "state")
# This is used for testing; it never actually happens
@github_app.route("pull_request", action="rotated")
async def pull_request_rotated(event_type, payload, gh_client):
print("PR rotated (I guess you're running the test suite)")
try:
await trio.sleep(glom(payload, "rotation_time"))
except BaseException as exc:
print(f"rotation interrupted by {exc!r}")
raise
print("rotation complete")
# There's no "merged" event; instead you get action=closed + merged=True
@github_app.route("pull_request", action="closed")
async def pull_request_merged(event_type, payload, gh_client):
print("PR closed")
if not glom(payload, "pull_request.merged"):
print("but not merged, so never mind")
return
creator = glom(payload, "pull_request.user.login")
org = glom(payload, "organization.login")
print(f"PR by {creator} was merged!")
if creator in SENT_INVITATION:
print("The database says we already sent an invitation")
return
state = await _member_state(gh_client, org, creator)
if state is not None:
# Remember for later so we don't keep checking the Github API over and
# over.
SENT_INVITATION.add(creator)
print(f"They already have member state {state}; not inviting")
return
print("Inviting! Woohoo!")
# Send an invitation
await gh_client.put(
"/orgs/{org}/memberships/{username}",
url_vars={"org": org, "username": creator},
data={"role": "member"},
)
# Record that we did
SENT_INVITATION.add(creator)
# Welcome them
await gh_client.post(
glom(payload, "pull_request.comments_url"),
data={"body": invite_message.format(username=creator)},
)
async def main(*, task_status=trio.TASK_STATUS_IGNORED):
shutdown_event = trio.Event()
async def listen_for_sigterm(*, task_status=trio.TASK_STATUS_IGNORED):
with trio.open_signal_receiver(signal.SIGTERM) as signal_aiter:
task_status.started()
async for signum in signal_aiter:
print(f"shutdown on signal signum: {signum}")
shutdown_event.set()
return
print("~~~ Starting up! ~~~")
# On Heroku, have to bind to whatever $PORT says:
# https://devcenter.heroku.com/articles/dynos#local-environment-variables
port = os.environ.get("PORT", 8000)
async with trio.open_nursery() as nursery:
config = hypercorn.Config.from_mapping(
bind=[f"0.0.0.0:{port}"],
# Log to stdout
accesslog="-",
errorlog="-",
# Setting this just silences a warning:
worker_class="trio",
)
await nursery.start(listen_for_sigterm)
urls = await nursery.start(partial(
hypercorn.trio.serve,
quart_app,
config,
shutdown_trigger=shutdown_event.wait
))
print("Accepting HTTP requests at:", urls)
task_status.started(urls)