Skip to content

Commit 95e8400

Browse files
authored
add linear webhook example (#501)
# Motivation <!-- Why is this change necessary? --> # Content Add an example to codegen-examples show how to use the `linear.event` decorator <!-- Please include a summary of the change --> # Testing <!-- How was the change tested? --> # Please check the following before marking your PR as ready for review - [ ] I have added tests for my changes - [ ] I have updated the documentation or added new documentation as needed --------- Co-authored-by: rushilpatel0 <[email protected]>
1 parent 371dabd commit 95e8400

File tree

6 files changed

+2445
-11
lines changed

6 files changed

+2445
-11
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
LINEAR_ACCESS_TOKEN="..."
2+
LINEAR_SIGNING_SECRET="..."
3+
LINEAR_TEAM_ID="..."
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[project]
2+
name = "linear-webhook-example"
3+
version = "0.1.0"
4+
description = "Linear webhook example for Codegen"
5+
requires-python = ">=3.13"
6+
dependencies = ["codegen>=0.22.2", "modal>=0.73.25"]

codegen-examples/examples/linear_webhooks/uv.lock

Lines changed: 2395 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import modal.running_app
2+
from codegen.extensions.events.app import CodegenApp
3+
import modal
4+
5+
image = modal.Image.debian_slim(python_version="3.13").apt_install("git").pip_install("fastapi[standard]", "codegen>=v0.22.2")
6+
app = CodegenApp(name="test-linear", modal_api_key="", image=image)
7+
8+
# Here is an example implementation of setting up an endpoint for receiving webhook events from Linear.
9+
# The @app.linear.event() decorator takes care of subscribing to the webhook and also unsubscribing when the deployment spun
10+
# Load environment variables from .env file
11+
12+
13+
@app.cls(secrets=[modal.Secret.from_dotenv()], keep_warm=1)
14+
class LinearEventHandlers:
15+
@modal.enter()
16+
def enter(self):
17+
app.linear.subscribe_all_handlers()
18+
19+
@modal.exit()
20+
def exit(self):
21+
app.linear.unsubscribe_all_handlers()
22+
23+
@modal.web_endpoint(method="POST")
24+
@app.linear.event("Issue")
25+
def test(self, data: dict):
26+
# handle webhook event
27+
# data is the payload of the webhook event
28+
print(data)

src/codegen/extensions/clients/linear.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def unregister_webhook(self, webhook_id: str):
130130
response = requests.post(self.api_endpoint, headers=self.api_headers, json={"query": mutation, "variables": variables})
131131
return response.json()
132132

133-
def register_webhook(self, webhook_url: str, team_id: str, secret: str, enabled: bool, resource_types: list[str]):
133+
def register_webhook(self, webhook_url: str, team_id: str, secret: str, enabled: bool, resource_types: list[str]) -> str | None:
134134
mutation = """
135135
mutation createWebhook($input: WebhookCreateInput!) {
136136
webhookCreate(input: $input) {

src/codegen/extensions/events/linear.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
class RegisteredWebhookHandler(BaseModel):
1616
webhook_id: str | None = None
1717
handler_func: Callable
18+
event_name: str
1819

1920

2021
class Linear(EventHandlerManagerProtocol):
@@ -24,13 +25,20 @@ def __init__(self, app: modal.App):
2425
self.signing_secret = os.environ["LINEAR_SIGNING_SECRET"]
2526
self.linear_team_id = os.environ["LINEAR_TEAM_ID"]
2627
self.registered_handlers = {}
28+
self._webhook_url = None
2729

28-
def subscribe_handler_to_webhook(self, web_url: str, event_name: str):
30+
def subscribe_handler_to_webhook(self, handler: RegisteredWebhookHandler):
2931
client = LinearClient(access_token=self.access_token)
30-
31-
result = client.register_webhook(team_id=self.linear_team_id, webhook_url=web_url, enabled=True, resource_types=[event_name], secret=self.signing_secret)
32+
web_url = modal.Function.from_name(app_name=self.app.name, name=handler.handler_func.__qualname__).web_url
33+
result = client.register_webhook(team_id=self.linear_team_id, webhook_url=web_url, enabled=True, resource_types=[handler.event_name], secret=self.signing_secret)
3234
return result
3335

36+
def subscribe_all_handlers(self):
37+
for handler_key in self.registered_handlers:
38+
handler = self.registered_handlers[handler_key]
39+
result = self.subscribe_handler_to_webhook(handler=self.registered_handlers[handler_key])
40+
handler.webhook_id = result
41+
3442
def unsubscribe_handler_to_webhook(self, registered_handler: RegisteredWebhookHandler):
3543
webhook_id = registered_handler.webhook_id
3644

@@ -59,13 +67,7 @@ def decorator(func):
5967
# Register the handler with the app's registry.
6068
modal_ready_func = func
6169
func_name = func.__qualname__
62-
app_name = self.app.name
63-
web_url = modal.Function.from_name(app_name=app_name, name=func_name).web_url
64-
65-
self.registered_handlers[func_name] = RegisteredWebhookHandler(handler_func=modal_ready_func)
66-
67-
webhook_id = self.subscribe_handler_to_webhook(web_url=web_url, event_name=event_name)
68-
self.registered_handlers[func_name].webhook_id = webhook_id
70+
self.registered_handlers[func_name] = RegisteredWebhookHandler(handler_func=modal_ready_func, event_name=event_name)
6971

7072
@functools.wraps(func)
7173
def wrapper(*args, **kwargs):

0 commit comments

Comments
 (0)