Skip to content

Commit 9027c5e

Browse files
committed
linear integration
1 parent b370518 commit 9027c5e

File tree

5 files changed

+139
-203
lines changed

5 files changed

+139
-203
lines changed

src/codegen/extensions/clients/linear.py

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import json
22
import logging
3-
import requests
43

4+
import requests
55
from pydantic import BaseModel
66

77
logger = logging.getLogger(__name__)
88

99

1010
# --- TYPES
1111

12+
1213
class LinearUser(BaseModel):
1314
id: str
1415
name: str
1516

17+
1618
class LinearComment(BaseModel):
1719
id: str
1820
body: str
1921
user: LinearUser | None = None
2022

23+
2124
class LinearIssue(BaseModel):
2225
id: str
2326
title: str
@@ -49,12 +52,7 @@ def get_issue(self, issue_id: str) -> LinearIssue:
4952
response = requests.post(self.api_endpoint, headers=self.api_headers, json={"query": query, "variables": variables})
5053
data = response.json()
5154
issue_data = data["data"]["issue"]
52-
return LinearIssue(
53-
id=issue_data["id"],
54-
title=issue_data["title"],
55-
description=issue_data["description"]
56-
)
57-
55+
return LinearIssue(id=issue_data["id"], title=issue_data["title"], description=issue_data["description"])
5856

5957
def get_issue_comments(self, issue_id: str) -> list[LinearComment]:
6058
query = """
@@ -69,7 +67,7 @@ def get_issue_comments(self, issue_id: str) -> list[LinearComment]:
6967
name
7068
}
7169
}
72-
70+
7371
}
7472
}
7573
}
@@ -83,21 +81,13 @@ def get_issue_comments(self, issue_id: str) -> list[LinearComment]:
8381
parsed_comments = []
8482
for comment in comments:
8583
user = comment.get("user", None)
86-
parsed_comment = LinearComment(
87-
id=comment["id"],
88-
body=comment["body"],
89-
user=LinearUser(
90-
id=user.get("id"),
91-
name=user.get("name")
92-
) if user else None
93-
)
84+
parsed_comment = LinearComment(id=comment["id"], body=comment["body"], user=LinearUser(id=user.get("id"), name=user.get("name")) if user else None)
9485
parsed_comments.append(parsed_comment)
9586

9687
# Convert raw comments to LinearComment objects
9788
return parsed_comments
9889

99-
100-
def comment_on_issue(self, issue_id: str, body: str) -> dict:
90+
def comment_on_issue(self, issue_id: str, body: str) -> dict:
10191
"""issue_id is our internal issue ID"""
10292
query = """mutation makeComment($issueId: String!, $body: String!) {
10393
commentCreate(input: {issueId: $issueId, body: $body}) {
@@ -106,7 +96,7 @@ def comment_on_issue(self, issue_id: str, body: str) -> dict:
10696
body
10797
url
10898
user {
109-
id
99+
id
110100
name
111101
}
112102
}
@@ -125,30 +115,48 @@ def comment_on_issue(self, issue_id: str, body: str) -> dict:
125115

126116
return comment_data
127117
except:
128-
raise Exception(f"Error creating comment\n{data}")
118+
msg = f"Error creating comment\n{data}"
119+
raise Exception(msg)
129120

121+
def unregister_webhook(self, webhook_id: str):
122+
mutation = """
123+
mutation deleteWebhook($id: String!) {
124+
webhookDelete(id: $id) {
125+
success
126+
}
127+
}
128+
"""
129+
variables = {"id": webhook_id}
130+
response = requests.post(self.api_endpoint, headers=self.api_headers, json={"query": mutation, "variables": variables})
131+
return response.json()
130132

131133
def register_webhook(self, webhook_url: str, team_id: str, secret: str, enabled: bool, resource_types: list[str]):
132134
mutation = """
133135
mutation createWebhook($input: WebhookCreateInput!) {
134136
webhookCreate(input: $input) {
135137
success
136138
webhook {
137-
id
138-
enabled
139+
id
140+
enabled
139141
}
140142
}
141143
}
142144
"""
143145

144-
variables = {"input": {
145-
"url": webhook_url,
146-
"teamId": team_id,
147-
"resourceTypes": resource_types,
148-
"enabled": enabled,
149-
"secret": secret,
150-
}}
146+
variables = {
147+
"input": {
148+
"url": webhook_url,
149+
"teamId": team_id,
150+
"resourceTypes": resource_types,
151+
"enabled": enabled,
152+
"secret": secret,
153+
}
154+
}
151155

152156
response = requests.post(self.api_endpoint, headers=self.api_headers, json={"query": mutation, "variables": variables})
157+
if response.status_code != 200:
158+
return None
159+
153160
body = response.json()
161+
body = body["data"]["webhookCreate"]["webhook"]["id"]
154162
return body

src/codegen/extensions/events/app.py

Lines changed: 12 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,21 @@
1-
import functools
2-
import os
3-
from typing import Callable, Optional
1+
import logging
2+
43
import modal
5-
import modal.runner
64

7-
from codegen.extensions.clients.linear import LinearClient
5+
from codegen.extensions.events.linear import Linear
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class CodegenApp(modal.App):
11+
linear: Linear
812

9-
class CodegenApp:
1013
def __init__(self, name, modal_api_key, image: modal.Image):
1114
self._modal_api_key = modal_api_key
1215
self._image = image
1316
self._name = name
1417

15-
self.modal_app = modal.App(name=name, image=image, )
16-
# Internal registry for event handlers, keyed by event name.
17-
self._registry = {}
18-
# Expose a linear attribute that provides the event decorator.
19-
self.linear = Linear(self)
20-
21-
def _register_event_handler(self, event_name, handler):
22-
"""Registers a handler for a given event."""
23-
self._registry.setdefault(event_name, []).append(handler)
24-
print(f"Registered handler '{handler}' for event '{event_name}'.")
25-
26-
def dispatch_event(self, event_name, request):
27-
"""
28-
Dispatches an event to all registered handlers for the event.
29-
"""
30-
handlers = self._registry.get(event_name, [])
31-
if not handlers:
32-
print(f"No handlers registered for event '{event_name}'.")
33-
return
34-
for handler in handlers:
35-
print(f"Dispatching event '{event_name}' to {handler.__name__}.")
36-
handler(request)
37-
38-
39-
40-
class Linear:
41-
def __init__(self, app: CodegenApp):
42-
self.app = app
43-
self.access_token= os.environ["LINEAR_ACCESS_TOKEN"] # move to extensions config.
44-
self.signing_secret = os.environ["LINEAR_SIGNING_SECRET"]
45-
self.linear_team_id = os.environ["LINEAR_TEAM_ID"]
18+
super().__init__(name=name, image=image)
4619

47-
48-
def register_handler_to_webhook(self, func_name: str, modal_app: modal.App, event_name):
49-
app_name = modal_app.name
50-
web_url = modal.Function.from_name(app_name=app_name,name=func_name)
51-
client = LinearClient(access_token=self.access_token)
52-
client.register_webhook(
53-
team_id=self.linear_team_id,
54-
webhook_url=web_url,
55-
enabled=True,
56-
resource_types=[event_name],
57-
secret=self.signing_secret)
58-
59-
# def unregister_handler(self):
60-
# TODO
61-
# pass
62-
63-
64-
def event(self, event_name, register_hook: Optional[Callable]=None):
65-
"""
66-
Decorator for registering an event handler.
67-
68-
:param event_name: The name of the event to handle.
69-
:param register_hook: An optional function to call during registration,
70-
e.g., to make an API call to register the webhook.
71-
"""
72-
def decorator(func):
73-
# Register the handler with the app's registry.
74-
75-
modal_ready_func = apply_decorators(func=func, decorators=[modal.web_endpoint(method="POST"), self.app.modal_app.function()])
76-
self.app._register_event_handler(event_name, modal_ready_func)
77-
78-
# If a custom registration hook is provided, execute it.
79-
if callable(register_hook):
80-
register_hook(event_name, func, self.app)
81-
82-
@functools.wraps(func)
83-
def wrapper(*args, **kwargs):
84-
return func(*args, **kwargs)
85-
86-
result = modal.runner.deploy_app(self.app.modal_app)
87-
print(result)
88-
return wrapper
89-
90-
91-
return decorator
92-
93-
94-
95-
96-
def apply_decorators(func, decorators):
97-
for decorator in decorators:
98-
func = decorator(func)
99-
return func
20+
# Expose a attribute that provides the event decorator for different providers.
21+
self.linear = Linear(self)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from typing import Protocol
2+
3+
import modal
4+
5+
6+
class EventHandlerManagerProtocol(Protocol):
7+
def subscribe_handler_to_webhook(self, func_name: str, modal_app: modal.App, event_name):
8+
pass
9+
10+
def unsubscribe_handler_to_webhook(self, func_name: str, modal_app: modal.App, event_name):
11+
pass
12+
13+
def unsubscribe_all_handlers(self):
14+
pass
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import functools
2+
import logging
3+
import os
4+
from typing import Callable
5+
6+
import modal
7+
from anthropic import BaseModel
8+
9+
from codegen.extensions.clients.linear import LinearClient
10+
from codegen.extensions.events.interface import EventHandlerManagerProtocol
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class RegisteredWebhookHandler(BaseModel):
16+
webhook_id: str | None = None
17+
handler_func: Callable
18+
19+
20+
class Linear(EventHandlerManagerProtocol):
21+
def __init__(self, app: modal.App):
22+
self.app = app
23+
self.access_token = os.environ["LINEAR_ACCESS_TOKEN"] # move to extensions config.
24+
self.signing_secret = os.environ["LINEAR_SIGNING_SECRET"]
25+
self.linear_team_id = os.environ["LINEAR_TEAM_ID"]
26+
self.registered_handlers = {}
27+
28+
def subscribe_handler_to_webhook(self, web_url: str, event_name: str):
29+
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+
return result
33+
34+
def unsubscribe_handler_to_webhook(self, registered_handler: RegisteredWebhookHandler):
35+
webhook_id = registered_handler.webhook_id
36+
37+
client = LinearClient(access_token=self.access_token)
38+
if webhook_id:
39+
print(f"Unsubscribing from webhook {webhook_id}")
40+
result = client.unregister_webhook(webhook_id)
41+
return result
42+
else:
43+
print("No webhook id found for handler")
44+
return None
45+
46+
def unsubscribe_all_handlers(self):
47+
for handler in self.registered_handlers:
48+
self.unsubscribe_handler_to_webhook(self.registered_handlers[handler])
49+
50+
def event(self, event_name):
51+
"""Decorator for registering an event handler.
52+
53+
:param event_name: The name of the event to handle.
54+
:param register_hook: An optional function to call during registration,
55+
e.g., to make an API call to register the webhook.
56+
"""
57+
58+
def decorator(func):
59+
# Register the handler with the app's registry.
60+
modal_ready_func = func
61+
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
69+
70+
@functools.wraps(func)
71+
def wrapper(*args, **kwargs):
72+
return func(*args, **kwargs)
73+
74+
return wrapper
75+
76+
return decorator

0 commit comments

Comments
 (0)