or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

actions.mdapi-client.mdapps.mdexceptions.mdhttp-implementations.mdindex.mdrouting.mdsansio.md

routing.mddocs/

0

# Webhook Routing

1

2

Event routing system for handling GitHub webhook events with pattern matching and automatic dispatch to registered callback functions. The router supports both simple event type matching and data-based filtering for precise event handling.

3

4

## Capabilities

5

6

### Router Class

7

8

Central routing system for webhook event dispatch.

9

10

```python { .api }

11

from typing import Any, Awaitable, Callable, Dict, List, FrozenSet

12

13

AsyncCallback = Callable[..., Awaitable[None]]

14

15

class Router:

16

"""Route webhook events to registered functions."""

17

18

def __init__(self, *other_routers: "Router") -> None:

19

"""

20

Instantiate a new router, optionally inheriting from other routers.

21

22

Parameters:

23

- *other_routers: Existing routers to inherit routes from

24

"""

25

26

def add(self, func: AsyncCallback, event_type: str, **data_detail: Any) -> None:

27

"""

28

Add a new route for an event type and optional data filtering.

29

30

Parameters:

31

- func: Async callback function to handle the event

32

- event_type: GitHub event type (e.g., "push", "pull_request")

33

- **data_detail: Optional data-based filtering (max one level deep)

34

35

Raises:

36

- TypeError: If more than one data detail level is specified

37

"""

38

39

def register(

40

self,

41

event_type: str,

42

**data_detail: Any

43

) -> Callable[[AsyncCallback], AsyncCallback]:

44

"""

45

Decorator to register a function for an event type.

46

47

Parameters:

48

- event_type: GitHub event type to register for

49

- **data_detail: Optional data-based filtering

50

51

Returns:

52

- Decorator function that registers and returns the callback

53

"""

54

55

def fetch(self, event: sansio.Event) -> FrozenSet[AsyncCallback]:

56

"""

57

Return a set of functions registered for the given event.

58

59

Parameters:

60

- event: GitHub webhook event to match against

61

62

Returns:

63

- Frozen set of callback functions that match the event

64

"""

65

66

async def dispatch(self, event: sansio.Event, *args: Any, **kwargs: Any) -> None:

67

"""

68

Dispatch an event to all registered functions.

69

70

Parameters:

71

- event: GitHub webhook event

72

- *args: Additional arguments to pass to callbacks

73

- **kwargs: Additional keyword arguments to pass to callbacks

74

"""

75

```

76

77

## Usage Examples

78

79

### Basic Event Routing

80

81

```python

82

import asyncio

83

from gidgethub.routing import Router

84

from gidgethub.sansio import Event

85

86

router = Router()

87

88

# Register for all push events

89

@router.register("push")

90

async def handle_push(event):

91

print(f"Push to {event.data['repository']['name']}")

92

print(f"Commits: {len(event.data['commits'])}")

93

94

# Register for pull request events

95

@router.register("pull_request")

96

async def handle_pull_request(event):

97

action = event.data['action']

98

pr_number = event.data['number']

99

print(f"Pull request #{pr_number} was {action}")

100

101

# Manual registration (equivalent to decorator)

102

async def handle_issues(event):

103

print(f"Issue event: {event.data['action']}")

104

105

router.add(handle_issues, "issues")

106

107

# Dispatch events

108

async def process_webhook(event_data):

109

event = Event(event_data, event=event_data['event_type'],

110

delivery_id="12345")

111

await router.dispatch(event)

112

```

113

114

### Data-Based Event Filtering

115

116

```python

117

router = Router()

118

119

# Handle only pull request "opened" actions

120

@router.register("pull_request", action="opened")

121

async def handle_pr_opened(event):

122

pr = event.data['pull_request']

123

print(f"New PR: {pr['title']}")

124

print(f"Author: {pr['user']['login']}")

125

126

# Handle only pull request "closed" actions

127

@router.register("pull_request", action="closed")

128

async def handle_pr_closed(event):

129

pr = event.data['pull_request']

130

if pr['merged']:

131

print(f"PR merged: {pr['title']}")

132

else:

133

print(f"PR closed without merge: {pr['title']}")

134

135

# Handle push events to main branch only

136

@router.register("push", ref="refs/heads/main")

137

async def handle_main_push(event):

138

repo = event.data['repository']['name']

139

commits = len(event.data['commits'])

140

print(f"Push to main branch of {repo}: {commits} commits")

141

142

# Handle issue events with specific labels

143

@router.register("issues", action="labeled")

144

async def handle_issue_labeled(event):

145

issue = event.data['issue']

146

label = event.data['label']['name']

147

print(f"Issue #{issue['number']} labeled with '{label}'")

148

```

149

150

### Router Composition

151

152

```python

153

# Create specialized routers

154

pr_router = Router()

155

issue_router = Router()

156

157

@pr_router.register("pull_request", action="opened")

158

async def handle_new_pr(event):

159

print("New PR opened")

160

161

@issue_router.register("issues", action="opened")

162

async def handle_new_issue(event):

163

print("New issue opened")

164

165

# Combine routers

166

main_router = Router(pr_router, issue_router)

167

168

# Add additional routes to combined router

169

@main_router.register("push")

170

async def handle_push(event):

171

print("Push event")

172

173

# The main router now has all routes from all routers

174

```

175

176

### Advanced Event Processing

177

178

```python

179

import asyncio

180

from gidgethub.routing import Router

181

from gidgethub.aiohttp import GitHubAPI

182

import aiohttp

183

184

router = Router()

185

186

@router.register("pull_request", action="opened")

187

async def auto_review_pr(event, gh: GitHubAPI):

188

"""Automatically request review for new PRs."""

189

pr = event.data['pull_request']

190

repo_name = event.data['repository']['full_name']

191

pr_number = pr['number']

192

193

# Request review from team

194

await gh.post(

195

f"/repos/{repo_name}/pulls/{pr_number}/requested_reviewers",

196

data={"team_reviewers": ["core-team"]}

197

)

198

199

# Add labels based on file changes

200

files = await gh.getiter(f"/repos/{repo_name}/pulls/{pr_number}/files")

201

labels = []

202

203

async for file in files:

204

if file['filename'].endswith('.py'):

205

labels.append("python")

206

elif file['filename'].endswith(('.js', '.ts')):

207

labels.append("javascript")

208

209

if labels:

210

await gh.post(

211

f"/repos/{repo_name}/issues/{pr_number}/labels",

212

data={"labels": labels}

213

)

214

215

@router.register("issues", action="opened")

216

async def triage_issue(event, gh: GitHubAPI):

217

"""Auto-triage new issues."""

218

issue = event.data['issue']

219

repo_name = event.data['repository']['full_name']

220

issue_number = issue['number']

221

222

# Add triage label

223

await gh.post(

224

f"/repos/{repo_name}/issues/{issue_number}/labels",

225

data={"labels": ["needs-triage"]}

226

)

227

228

# Assign to triage team if it's a bug report

229

if "bug" in issue['title'].lower():

230

await gh.post(

231

f"/repos/{repo_name}/issues/{issue_number}/assignees",

232

data={"assignees": ["triage-bot"]}

233

)

234

235

# Process webhook with GitHub API client

236

async def handle_webhook(webhook_data, oauth_token):

237

async with aiohttp.ClientSession() as session:

238

gh = GitHubAPI(session, "webhook-bot/1.0", oauth_token=oauth_token)

239

240

event = Event(

241

webhook_data,

242

event=webhook_data['event_type'],

243

delivery_id=webhook_data['delivery_id']

244

)

245

246

# Dispatch with GitHub API client

247

await router.dispatch(event, gh)

248

```

249

250

### Testing Event Routing

251

252

```python

253

import asyncio

254

from gidgethub.routing import Router

255

from gidgethub.sansio import Event

256

257

async def test_routing():

258

router = Router()

259

260

# Track which callbacks were called

261

called_callbacks = []

262

263

@router.register("push")

264

async def handle_push(event):

265

called_callbacks.append("push")

266

267

@router.register("pull_request", action="opened")

268

async def handle_pr_opened(event):

269

called_callbacks.append("pr_opened")

270

271

@router.register("pull_request", action="closed")

272

async def handle_pr_closed(event):

273

called_callbacks.append("pr_closed")

274

275

# Test push event

276

push_event = Event(

277

{"repository": {"name": "test-repo"}},

278

event="push",

279

delivery_id="1"

280

)

281

await router.dispatch(push_event)

282

assert "push" in called_callbacks

283

284

# Test PR opened event

285

pr_event = Event(

286

{"action": "opened", "number": 1},

287

event="pull_request",

288

delivery_id="2"

289

)

290

await router.dispatch(pr_event)

291

assert "pr_opened" in called_callbacks

292

assert "pr_closed" not in called_callbacks

293

294

# Test event filtering

295

callbacks = router.fetch(pr_event)

296

assert len(callbacks) == 1 # Only pr_opened should match

297

298

asyncio.run(test_routing())

299

```

300

301

### Web Framework Integration

302

303

```python

304

# Flask example

305

from flask import Flask, request

306

import gidgethub.sansio

307

from gidgethub.routing import Router

308

309

app = Flask(__name__)

310

router = Router()

311

312

@router.register("push")

313

async def handle_push(event):

314

print(f"Push to {event.data['repository']['name']}")

315

316

@app.route('/webhook', methods=['POST'])

317

def webhook():

318

# Validate and parse webhook

319

event = gidgethub.sansio.Event.from_http(

320

dict(request.headers),

321

request.data,

322

secret=app.config['WEBHOOK_SECRET']

323

)

324

325

# Dispatch in background (Flask doesn't support async views directly)

326

import threading

327

def dispatch_async():

328

asyncio.run(router.dispatch(event))

329

330

threading.Thread(target=dispatch_async).start()

331

return '', 200

332

333

# FastAPI example (native async support)

334

from fastapi import FastAPI, Request

335

import gidgethub.sansio

336

337

app = FastAPI()

338

router = Router()

339

340

@app.post("/webhook")

341

async def webhook(request: Request):

342

body = await request.body()

343

headers = dict(request.headers)

344

345

event = gidgethub.sansio.Event.from_http(

346

headers, body, secret="webhook_secret"

347

)

348

349

await router.dispatch(event)

350

return {"status": "ok"}

351

```

352

353

## Event Types

354

355

Common GitHub webhook event types that can be registered:

356

357

- `"push"` - Repository push events

358

- `"pull_request"` - Pull request events (actions: opened, closed, synchronize, etc.)

359

- `"issues"` - Issue events (actions: opened, closed, labeled, etc.)

360

- `"issue_comment"` - Issue and PR comments

361

- `"pull_request_review"` - PR review events

362

- `"release"` - Release events

363

- `"fork"` - Repository fork events

364

- `"star"` - Repository star events

365

- `"watch"` - Repository watch events

366

- `"deployment"` - Deployment events

367

- `"status"` - Commit status updates

368

- `"check_run"` - Check run events

369

- `"workflow_run"` - GitHub Actions workflow events

370

371

## Types

372

373

```python { .api }

374

from typing import Any, Awaitable, Callable, Dict, List, FrozenSet

375

from gidgethub.sansio import Event

376

377

# Callback function type for webhook handlers

378

AsyncCallback = Callable[..., Awaitable[None]]

379

380

# Router internal types

381

_ShallowRoutes = Dict[str, List[AsyncCallback]]

382

_DeepRoutes = Dict[str, Dict[str, Dict[Any, List[AsyncCallback]]]]

383

```