Skip to content

Commit ac86411

Browse files
tomcodgentkfoss
tomcodgen
andauthored
[CG-10837] feat: Linear tools error handling, extra test, request retry (#653)
# Motivation <!-- Why is this change necessary? --> # Content <!-- 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: tomcodgen <[email protected]>
1 parent f26ed85 commit ac86411

File tree

5 files changed

+229
-14
lines changed

5 files changed

+229
-14
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ dependencies = [
7373
"lox>=0.12.0",
7474
"httpx>=0.28.1",
7575
"docker>=6.1.3",
76+
"urllib3>=2.0.0",
7677
]
7778

7879
license = { text = "Apache-2.0" }

src/codegen/extensions/linear/linear_client.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import json
21
import logging
32
import os
43
from typing import Optional
54

65
import requests
6+
from requests.adapters import HTTPAdapter
7+
from urllib3.util.retry import Retry
78

89
from codegen.extensions.linear.types import LinearComment, LinearIssue, LinearTeam, LinearUser
910

@@ -14,7 +15,7 @@ class LinearClient:
1415
api_headers: dict
1516
api_endpoint = "https://api.linear.app/graphql"
1617

17-
def __init__(self, access_token: Optional[str] = None, team_id: Optional[str] = None):
18+
def __init__(self, access_token: Optional[str] = None, team_id: Optional[str] = None, max_retries: int = 3, backoff_factor: float = 0.5):
1819
if not access_token:
1920
access_token = os.getenv("LINEAR_ACCESS_TOKEN")
2021
if not access_token:
@@ -31,6 +32,18 @@ def __init__(self, access_token: Optional[str] = None, team_id: Optional[str] =
3132
"Authorization": self.access_token,
3233
}
3334

35+
# Set up a session with retry logic
36+
self.session = requests.Session()
37+
retry_strategy = Retry(
38+
total=max_retries,
39+
backoff_factor=backoff_factor,
40+
status_forcelist=[429, 500, 502, 503, 504],
41+
allowed_methods=["POST", "GET"], # POST is important for GraphQL
42+
)
43+
adapter = HTTPAdapter(max_retries=retry_strategy)
44+
self.session.mount("https://", adapter)
45+
self.session.mount("http://", adapter)
46+
3447
def get_issue(self, issue_id: str) -> LinearIssue:
3548
query = """
3649
query getIssue($issueId: String!) {
@@ -42,7 +55,7 @@ def get_issue(self, issue_id: str) -> LinearIssue:
4255
}
4356
"""
4457
variables = {"issueId": issue_id}
45-
response = requests.post(self.api_endpoint, headers=self.api_headers, json={"query": query, "variables": variables})
58+
response = self.session.post(self.api_endpoint, headers=self.api_headers, json={"query": query, "variables": variables})
4659
data = response.json()
4760
issue_data = data["data"]["issue"]
4861
return LinearIssue(id=issue_data["id"], title=issue_data["title"], description=issue_data["description"])
@@ -66,7 +79,7 @@ def get_issue_comments(self, issue_id: str) -> list[LinearComment]:
6679
}
6780
"""
6881
variables = {"issueId": issue_id}
69-
response = requests.post(self.api_endpoint, headers=self.api_headers, json={"query": query, "variables": variables})
82+
response = self.session.post(self.api_endpoint, headers=self.api_headers, json={"query": query, "variables": variables})
7083
data = response.json()
7184
comments = data["data"]["issue"]["comments"]["nodes"]
7285

@@ -80,8 +93,8 @@ def get_issue_comments(self, issue_id: str) -> list[LinearComment]:
8093
# Convert raw comments to LinearComment objects
8194
return parsed_comments
8295

83-
def comment_on_issue(self, issue_id: str, body: str) -> dict:
84-
"""issue_id is our internal issue ID"""
96+
def comment_on_issue(self, issue_id: str, body: str) -> LinearComment:
97+
"""Add a comment to an issue."""
8598
query = """mutation makeComment($issueId: String!, $body: String!) {
8699
commentCreate(input: {issueId: $issueId, body: $body}) {
87100
comment {
@@ -97,19 +110,21 @@ def comment_on_issue(self, issue_id: str, body: str) -> dict:
97110
}
98111
"""
99112
variables = {"issueId": issue_id, "body": body}
100-
response = requests.post(
113+
response = self.session.post(
101114
self.api_endpoint,
102115
headers=self.api_headers,
103-
data=json.dumps({"query": query, "variables": variables}),
116+
json={"query": query, "variables": variables},
104117
)
105118
data = response.json()
106119
try:
107120
comment_data = data["data"]["commentCreate"]["comment"]
121+
user_data = comment_data.get("user", None)
122+
user = LinearUser(id=user_data["id"], name=user_data["name"]) if user_data else None
108123

109-
return comment_data
124+
return LinearComment(id=comment_data["id"], body=comment_data["body"], user=user)
110125
except Exception as e:
111126
msg = f"Error creating comment\n{data}\n{e}"
112-
raise Exception(msg)
127+
raise ValueError(msg)
113128

114129
def register_webhook(self, webhook_url: str, team_id: str, secret: str, enabled: bool, resource_types: list[str]):
115130
mutation = """
@@ -134,7 +149,7 @@ def register_webhook(self, webhook_url: str, team_id: str, secret: str, enabled:
134149
}
135150
}
136151

137-
response = requests.post(self.api_endpoint, headers=self.api_headers, json={"query": mutation, "variables": variables})
152+
response = self.session.post(self.api_endpoint, headers=self.api_headers, json={"query": mutation, "variables": variables})
138153
body = response.json()
139154
return body
140155

@@ -160,7 +175,7 @@ def search_issues(self, query: str, limit: int = 10) -> list[LinearIssue]:
160175
}
161176
"""
162177
variables = {"query": query, "limit": limit}
163-
response = requests.post(
178+
response = self.session.post(
164179
self.api_endpoint,
165180
headers=self.api_headers,
166181
json={"query": graphql_query, "variables": variables},
@@ -222,7 +237,7 @@ def create_issue(self, title: str, description: str | None = None, team_id: str
222237
}
223238
}
224239

225-
response = requests.post(
240+
response = self.session.post(
226241
self.api_endpoint,
227242
headers=self.api_headers,
228243
json={"query": mutation, "variables": variables},
@@ -258,7 +273,7 @@ def get_teams(self) -> list[LinearTeam]:
258273
}
259274
"""
260275

261-
response = requests.post(
276+
response = self.session.post(
262277
self.api_endpoint,
263278
headers=self.api_headers,
264279
json={"query": query},

src/codegen/extensions/tools/linear/linear.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from typing import ClassVar
44

5+
import requests
56
from pydantic import Field
67

78
from codegen.extensions.linear.linear_client import LinearClient
@@ -97,7 +98,32 @@ def linear_get_issue_tool(client: LinearClient, issue_id: str) -> LinearIssueObs
9798
issue_id=issue_id,
9899
issue_data=issue.dict(),
99100
)
101+
except requests.exceptions.RequestException as e:
102+
# Network-related errors
103+
return LinearIssueObservation(
104+
status="error",
105+
error=f"Network error when fetching issue: {e!s}",
106+
issue_id=issue_id,
107+
issue_data={},
108+
)
109+
except ValueError as e:
110+
# Input validation errors
111+
return LinearIssueObservation(
112+
status="error",
113+
error=f"Invalid input: {e!s}",
114+
issue_id=issue_id,
115+
issue_data={},
116+
)
117+
except KeyError as e:
118+
# Missing data in response
119+
return LinearIssueObservation(
120+
status="error",
121+
error=f"Unexpected API response format: {e!s}",
122+
issue_id=issue_id,
123+
issue_data={},
124+
)
100125
except Exception as e:
126+
# Catch-all for other errors
101127
return LinearIssueObservation(
102128
status="error",
103129
error=f"Failed to get issue: {e!s}",
@@ -115,7 +141,32 @@ def linear_get_issue_comments_tool(client: LinearClient, issue_id: str) -> Linea
115141
issue_id=issue_id,
116142
comments=[comment.dict() for comment in comments],
117143
)
144+
except requests.exceptions.RequestException as e:
145+
# Network-related errors
146+
return LinearCommentsObservation(
147+
status="error",
148+
error=f"Network error when fetching comments: {e!s}",
149+
issue_id=issue_id,
150+
comments=[],
151+
)
152+
except ValueError as e:
153+
# Input validation errors
154+
return LinearCommentsObservation(
155+
status="error",
156+
error=f"Invalid input: {e!s}",
157+
issue_id=issue_id,
158+
comments=[],
159+
)
160+
except KeyError as e:
161+
# Missing data in response
162+
return LinearCommentsObservation(
163+
status="error",
164+
error=f"Unexpected API response format: {e!s}",
165+
issue_id=issue_id,
166+
comments=[],
167+
)
118168
except Exception as e:
169+
# Catch-all for other errors
119170
return LinearCommentsObservation(
120171
status="error",
121172
error=f"Failed to get issue comments: {e!s}",
@@ -133,7 +184,32 @@ def linear_comment_on_issue_tool(client: LinearClient, issue_id: str, body: str)
133184
issue_id=issue_id,
134185
comment=comment,
135186
)
187+
except requests.exceptions.RequestException as e:
188+
# Network-related errors
189+
return LinearCommentObservation(
190+
status="error",
191+
error=f"Network error when adding comment: {e!s}",
192+
issue_id=issue_id,
193+
comment={},
194+
)
195+
except ValueError as e:
196+
# Input validation errors
197+
return LinearCommentObservation(
198+
status="error",
199+
error=f"Invalid input: {e!s}",
200+
issue_id=issue_id,
201+
comment={},
202+
)
203+
except KeyError as e:
204+
# Missing data in response
205+
return LinearCommentObservation(
206+
status="error",
207+
error=f"Unexpected API response format: {e!s}",
208+
issue_id=issue_id,
209+
comment={},
210+
)
136211
except Exception as e:
212+
# Catch-all for other errors
137213
return LinearCommentObservation(
138214
status="error",
139215
error=f"Failed to comment on issue: {e!s}",
@@ -159,7 +235,35 @@ def linear_register_webhook_tool(
159235
team_id=team_id,
160236
response=response,
161237
)
238+
except requests.exceptions.RequestException as e:
239+
# Network-related errors
240+
return LinearWebhookObservation(
241+
status="error",
242+
error=f"Network error when registering webhook: {e!s}",
243+
webhook_url=webhook_url,
244+
team_id=team_id,
245+
response={},
246+
)
247+
except ValueError as e:
248+
# Input validation errors
249+
return LinearWebhookObservation(
250+
status="error",
251+
error=f"Invalid input: {e!s}",
252+
webhook_url=webhook_url,
253+
team_id=team_id,
254+
response={},
255+
)
256+
except KeyError as e:
257+
# Missing data in response
258+
return LinearWebhookObservation(
259+
status="error",
260+
error=f"Unexpected API response format: {e!s}",
261+
webhook_url=webhook_url,
262+
team_id=team_id,
263+
response={},
264+
)
162265
except Exception as e:
266+
# Catch-all for other errors
163267
return LinearWebhookObservation(
164268
status="error",
165269
error=f"Failed to register webhook: {e!s}",
@@ -178,7 +282,32 @@ def linear_search_issues_tool(client: LinearClient, query: str, limit: int = 10)
178282
query=query,
179283
issues=[issue.dict() for issue in issues],
180284
)
285+
except requests.exceptions.RequestException as e:
286+
# Network-related errors
287+
return LinearSearchObservation(
288+
status="error",
289+
error=f"Network error when searching issues: {e!s}",
290+
query=query,
291+
issues=[],
292+
)
293+
except ValueError as e:
294+
# Input validation errors
295+
return LinearSearchObservation(
296+
status="error",
297+
error=f"Invalid input: {e!s}",
298+
query=query,
299+
issues=[],
300+
)
301+
except KeyError as e:
302+
# Missing data in response
303+
return LinearSearchObservation(
304+
status="error",
305+
error=f"Unexpected API response format: {e!s}",
306+
query=query,
307+
issues=[],
308+
)
181309
except Exception as e:
310+
# Catch-all for other errors
182311
return LinearSearchObservation(
183312
status="error",
184313
error=f"Failed to search issues: {e!s}",
@@ -197,7 +326,35 @@ def linear_create_issue_tool(client: LinearClient, title: str, description: str
197326
team_id=team_id,
198327
issue_data=issue.dict(),
199328
)
329+
except requests.exceptions.RequestException as e:
330+
# Network-related errors
331+
return LinearCreateIssueObservation(
332+
status="error",
333+
error=f"Network error when creating issue: {e!s}",
334+
title=title,
335+
team_id=team_id,
336+
issue_data={},
337+
)
338+
except ValueError as e:
339+
# Input validation errors
340+
return LinearCreateIssueObservation(
341+
status="error",
342+
error=f"Invalid input: {e!s}",
343+
title=title,
344+
team_id=team_id,
345+
issue_data={},
346+
)
347+
except KeyError as e:
348+
# Missing data in response
349+
return LinearCreateIssueObservation(
350+
status="error",
351+
error=f"Unexpected API response format: {e!s}",
352+
title=title,
353+
team_id=team_id,
354+
issue_data={},
355+
)
200356
except Exception as e:
357+
# Catch-all for other errors
201358
return LinearCreateIssueObservation(
202359
status="error",
203360
error=f"Failed to create issue: {e!s}",
@@ -215,7 +372,29 @@ def linear_get_teams_tool(client: LinearClient) -> LinearTeamsObservation:
215372
status="success",
216373
teams=[team.dict() for team in teams],
217374
)
375+
except requests.exceptions.RequestException as e:
376+
# Network-related errors
377+
return LinearTeamsObservation(
378+
status="error",
379+
error=f"Network error when fetching teams: {e!s}",
380+
teams=[],
381+
)
382+
except ValueError as e:
383+
# Input validation errors
384+
return LinearTeamsObservation(
385+
status="error",
386+
error=f"Invalid input: {e!s}",
387+
teams=[],
388+
)
389+
except KeyError as e:
390+
# Missing data in response
391+
return LinearTeamsObservation(
392+
status="error",
393+
error=f"Unexpected API response format: {e!s}",
394+
teams=[],
395+
)
218396
except Exception as e:
397+
# Catch-all for other errors
219398
return LinearTeamsObservation(
220399
status="error",
221400
error=f"Failed to get teams: {e!s}",

0 commit comments

Comments
 (0)