Skip to content

Commit d895825

Browse files
committed
linear integration
1 parent ca74b8f commit d895825

File tree

5 files changed

+134
-220
lines changed

5 files changed

+134
-220
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: 7 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,21 @@
1-
import functools
2-
import os
3-
from typing import Callable, Optional, Protocol
4-
import modal
5-
from codegen.extensions.clients.linear import LinearClient
61
import logging
72

8-
logger = logging.getLogger(__name__)
9-
10-
11-
12-
class EventHandlerManagerProtocol(Protocol):
13-
def subscribe_handler_to_webhook(self, func_name: str, modal_app: modal.App, event_name):
14-
pass
15-
def unsubscribe_handler_to_webhook(self, func_name: str, modal_app: modal.App, event_name):
16-
pass
3+
import modal
174

18-
def unsubscribe_all_handlers(self):
19-
pass
5+
from codegen.extensions.events.linear import Linear
206

7+
logger = logging.getLogger(__name__)
218

229

2310
class CodegenApp(modal.App):
11+
linear: Linear
12+
2413
def __init__(self, name, modal_api_key, image: modal.Image):
2514
self._modal_api_key = modal_api_key
2615
self._image = image
2716
self._name = name
2817

2918
super().__init__(name=name, image=image)
30-
# Expose a linear attribute that provides the event decorator.
31-
self.linear = Linear(self)
32-
33-
# unsubscribe all other handlers
34-
3519

36-
class Linear(EventHandlerManagerProtocol):
37-
def __init__(self, app: CodegenApp):
38-
self.app = app
39-
self.access_token= "test" or os.environ["LINEAR_ACCESS_TOKEN"] # move to extensions config.
40-
self.signing_secret = "test" or os.environ["LINEAR_SIGNING_SECRET"]
41-
self.linear_team_id = "test" or os.environ["LINEAR_TEAM_ID"]
42-
self.registered_handlers = {}
43-
44-
def subscribe_handler_to_webhook(self, func_name: str, modal_app: modal.App, event_name):
45-
app_name = modal_app.name
46-
web_url = modal.Function.from_name(app_name=app_name, name=func_name).web_url
47-
client = LinearClient(access_token=self.access_token)
48-
print(f"Subscribing to {event_name} for {app_name} at {web_url}")
49-
client.register_webhook(
50-
team_id=self.linear_team_id,
51-
webhook_url=web_url,
52-
enabled=True,
53-
resource_types=[event_name],
54-
secret=self.signing_secret)
55-
56-
def unsubscribe_handler_to_webhook(self, func_name: str, modal_app: modal.App, event_name):
57-
app_name = modal_app.name
58-
web_url = modal.Function.from_name(app_name=app_name, name=func_name).web_url
59-
client = LinearClient(access_token=self.access_token)
60-
print(f"Unsubscribing from {event_name} for {app_name} at {web_url}")
61-
client.register_webhook(
62-
team_id=self.linear_team_id,
63-
webhook_url=web_url,
64-
enabled=False,
65-
resource_types=[event_name],
66-
secret=self.signing_secret)
67-
68-
def unsubscribe_all_handlers(self):
69-
for handler in self.registered_handlers:
70-
self.unsubscribe_handler_to_webhook(handler, self.app, self.registered_handlers[handler])
71-
72-
73-
74-
def event(self, event_name):
75-
"""
76-
Decorator for registering an event handler.
77-
78-
:param event_name: The name of the event to handle.
79-
:param register_hook: An optional function to call during registration,
80-
e.g., to make an API call to register the webhook.
81-
"""
82-
print("debugging event decorator 1")
83-
def decorator(func):
84-
# Register the handler with the app's registry.
85-
# modal_ready_func = applzy_decorators(func=func, decorators=[modal.web_endpoint(method="POST")])
86-
modal_ready_func = func
87-
self.registered_handlers[event_name] = modal_ready_func
88-
89-
self.subscribe_handler_to_webhook(func_name=func.__name__, modal_app=self.app, event_name=event_name)
90-
print("debugging decorator 2",)
91-
92-
@functools.wraps(func)
93-
def wrapper(*args, **kwargs):
94-
print("wrapper called")
95-
return func(*args, **kwargs)
96-
return wrapper
97-
98-
99-
return decorator
100-
101-
102-
103-
104-
def apply_decorators(func, decorators):
105-
for decorator in decorators:
106-
func = decorator(func)
107-
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)