agentr 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
agentr/application.py CHANGED
@@ -1,5 +1,4 @@
1
- from abc import ABC, abstractmethod
2
-
1
+ from abc import ABC
3
2
  from loguru import logger
4
3
  from agentr.exceptions import NotAuthorizedError
5
4
  from agentr.integration import Integration
@@ -11,11 +10,25 @@ class Application(ABC):
11
10
  """
12
11
  def __init__(self, name: str, **kwargs):
13
12
  self.name = name
13
+ self.tools = []
14
14
 
15
- @abstractmethod
15
+ def tool(self):
16
+ """Decorator to register a method as a tool.
17
+
18
+ This decorator adds the decorated method to the application's tools list
19
+ and returns the original method unchanged.
20
+
21
+ Returns:
22
+ The decorated method
23
+ """
24
+ def decorator(func):
25
+ if func not in self.tools:
26
+ self.tools.append(func)
27
+ return func
28
+ return decorator
29
+
16
30
  def list_tools(self):
17
- pass
18
-
31
+ return self.tools
19
32
 
20
33
  class APIApplication(Application):
21
34
  """
@@ -35,8 +48,8 @@ class APIApplication(Application):
35
48
  response.raise_for_status()
36
49
  return response
37
50
  except NotAuthorizedError as e:
38
- logger.warning(f"Reddit authorization needed: {e.message}")
39
- return e.message
51
+ logger.warning(f"Authorization needed: {e.message}")
52
+ raise e
40
53
  except Exception as e:
41
54
  logger.error(f"Error getting {url}: {e}")
42
55
  raise e
@@ -49,8 +62,8 @@ class APIApplication(Application):
49
62
  response.raise_for_status()
50
63
  return response
51
64
  except NotAuthorizedError as e:
52
- logger.warning(f"Reddit authorization needed: {e.message}")
53
- return e.message
65
+ logger.warning(f"Authorization needed: {e.message}")
66
+ raise e
54
67
  except httpx.HTTPStatusError as e:
55
68
  if e.response.status_code == 429:
56
69
  return e.response.text or "Rate limit exceeded. Please try again later."
@@ -67,8 +80,8 @@ class APIApplication(Application):
67
80
  response.raise_for_status()
68
81
  return response
69
82
  except NotAuthorizedError as e:
70
- logger.warning(f"Reddit authorization needed: {e.message}")
71
- return e.message
83
+ logger.warning(f"Authorization needed: {e.message}")
84
+ raise e
72
85
  except Exception as e:
73
86
  logger.error(f"Error posting {url}: {e}")
74
87
  raise e
@@ -80,14 +93,24 @@ class APIApplication(Application):
80
93
  response.raise_for_status()
81
94
  return response
82
95
  except NotAuthorizedError as e:
83
- logger.warning(f"Reddit authorization needed: {e.message}")
84
- return e.message
96
+ logger.warning(f"Authorization needed: {e.message}")
97
+ raise e
85
98
  except Exception as e:
86
99
  logger.error(f"Error posting {url}: {e}")
100
+ raise e
101
+
102
+ def _patch(self, url, data):
103
+ try:
104
+ headers = self._get_headers()
105
+ response = httpx.patch(url, headers=headers, json=data)
106
+ response.raise_for_status()
107
+ return response
108
+ except NotAuthorizedError as e:
109
+ logger.warning(f"Authorization needed: {e.message}")
110
+ raise e
111
+ except Exception as e:
112
+ logger.error(f"Error patching {url}: {e}")
113
+ raise e
87
114
 
88
115
  def validate(self):
89
- pass
90
-
91
- @abstractmethod
92
- def list_tools(self):
93
116
  pass
@@ -1,7 +1,7 @@
1
1
  from agentr.integration import Integration
2
2
  from agentr.application import APIApplication
3
3
  from loguru import logger
4
- from agentr.exceptions import NotAuthorizedError
4
+ from typing import List, Dict, Any
5
5
 
6
6
  class GithubApp(APIApplication):
7
7
  def __init__(self, integration: Integration) -> None:
@@ -12,10 +12,6 @@ class GithubApp(APIApplication):
12
12
  if not self.integration:
13
13
  raise ValueError("Integration not configured")
14
14
  credentials = self.integration.get_credentials()
15
- if not credentials:
16
- logger.warning("No credentials found")
17
- action = self.integration.authorize()
18
- raise NotAuthorizedError(action)
19
15
  if "headers" in credentials:
20
16
  return credentials["headers"]
21
17
  return {
@@ -55,35 +51,24 @@ class GithubApp(APIApplication):
55
51
  Returns:
56
52
  A formatted list of recent commits
57
53
  """
58
- try:
59
- repo_full_name = repo_full_name.strip()
60
- url = f"{self.base_api_url}/{repo_full_name}/commits"
61
- response = self._get(url)
54
+ repo_full_name = repo_full_name.strip()
55
+ url = f"{self.base_api_url}/{repo_full_name}/commits"
56
+ response = self._get(url)
57
+ response.raise_for_status()
58
+
59
+ commits = response.json()
60
+ if not commits:
61
+ return f"No commits found for repository {repo_full_name}"
62
+
63
+ result = f"Recent commits for {repo_full_name}:\n\n"
64
+ for commit in commits[:12]: # Limit to 12 commits
65
+ sha = commit.get("sha", "")[:7]
66
+ message = commit.get("commit", {}).get("message", "").split('\n')[0]
67
+ author = commit.get("commit", {}).get("author", {}).get("name", "Unknown")
62
68
 
63
- if response.status_code == 200:
64
- commits = response.json()
65
- if not commits:
66
- return f"No commits found for repository {repo_full_name}"
67
-
68
- result = f"Recent commits for {repo_full_name}:\n\n"
69
- for commit in commits[:12]: # Limit to 12 commits
70
- sha = commit.get("sha", "")[:7]
71
- message = commit.get("commit", {}).get("message", "").split('\n')[0]
72
- author = commit.get("commit", {}).get("author", {}).get("name", "Unknown")
73
-
74
- result += f"- {sha}: {message} (by {author})\n"
75
-
76
- return result
77
- elif response.status_code == 404:
78
- return f"Repository {repo_full_name} not found"
79
- else:
80
- logger.error(response.text)
81
- return f"Error retrieving commits: {response.status_code} - {response.text}"
82
- except NotAuthorizedError as e:
83
- return e.message
84
- except Exception as e:
85
- logger.error(e)
86
- return f"Error retrieving commits: {str(e)}"
69
+ result += f"- {sha}: {message} (by {author})\n"
70
+
71
+ return result
87
72
 
88
73
  def list_branches(self, repo_full_name: str) -> str:
89
74
  """List branches for a GitHub repository
@@ -94,32 +79,21 @@ class GithubApp(APIApplication):
94
79
  Returns:
95
80
  A formatted list of branches
96
81
  """
97
- try:
98
- repo_full_name = repo_full_name.strip()
99
- url = f"{self.base_api_url}/{repo_full_name}/branches"
100
- response = self._get(url)
101
-
102
- if response.status_code == 200:
103
- branches = response.json()
104
- if not branches:
105
- return f"No branches found for repository {repo_full_name}"
106
-
107
- result = f"Branches for {repo_full_name}:\n\n"
108
- for branch in branches:
109
- branch_name = branch.get("name", "Unknown")
110
- result += f"- {branch_name}\n"
111
-
112
- return result
113
- elif response.status_code == 404:
114
- return f"Repository {repo_full_name} not found"
115
- else:
116
- logger.error(response.text)
117
- return f"Error retrieving branches: {response.status_code} - {response.text}"
118
- except NotAuthorizedError as e:
119
- return e.message
120
- except Exception as e:
121
- logger.error(e)
122
- return f"Error retrieving branches: {str(e)}"
82
+ repo_full_name = repo_full_name.strip()
83
+ url = f"{self.base_api_url}/{repo_full_name}/branches"
84
+ response = self._get(url)
85
+ response.raise_for_status()
86
+
87
+ branches = response.json()
88
+ if not branches:
89
+ return f"No branches found for repository {repo_full_name}"
90
+
91
+ result = f"Branches for {repo_full_name}:\n\n"
92
+ for branch in branches:
93
+ branch_name = branch.get("name", "Unknown")
94
+ result += f"- {branch_name}\n"
95
+
96
+ return result
123
97
 
124
98
  def list_pull_requests(self, repo_full_name: str, state: str = "open") -> str:
125
99
  """List pull requests for a GitHub repository
@@ -131,83 +105,59 @@ class GithubApp(APIApplication):
131
105
  Returns:
132
106
  A formatted list of pull requests
133
107
  """
134
- try:
135
- repo_full_name = repo_full_name.strip()
136
- url = f"{self.base_api_url}/{repo_full_name}/pulls"
137
- params = {"state": state}
138
- response = self._get(url, params=params)
108
+ repo_full_name = repo_full_name.strip()
109
+ url = f"{self.base_api_url}/{repo_full_name}/pulls"
110
+ params = {"state": state}
111
+ response = self._get(url, params=params)
112
+ response.raise_for_status()
113
+
114
+ pull_requests = response.json()
115
+ if not pull_requests:
116
+ return f"No pull requests found for repository {repo_full_name} with state '{state}'"
117
+
118
+ result = f"Pull requests for {repo_full_name} (State: {state}):\n\n"
119
+ for pr in pull_requests:
120
+ pr_title = pr.get("title", "No Title")
121
+ pr_number = pr.get("number", "Unknown")
122
+ pr_state = pr.get("state", "Unknown")
123
+ pr_user = pr.get("user", {}).get("login", "Unknown")
139
124
 
140
- if response.status_code == 200:
141
- pull_requests = response.json()
142
- if not pull_requests:
143
- return f"No pull requests found for repository {repo_full_name} with state '{state}'"
144
-
145
- result = f"Pull requests for {repo_full_name} (State: {state}):\n\n"
146
- for pr in pull_requests:
147
- pr_title = pr.get("title", "No Title")
148
- pr_number = pr.get("number", "Unknown")
149
- pr_state = pr.get("state", "Unknown")
150
- pr_user = pr.get("user", {}).get("login", "Unknown")
151
-
152
- result += f"- PR #{pr_number}: {pr_title} (by {pr_user}, Status: {pr_state})\n"
153
-
154
- return result
155
- elif response.status_code == 404:
156
- return f"Repository {repo_full_name} not found"
157
- else:
158
- logger.error(response.text)
159
- return f"Error retrieving pull requests: {response.status_code} - {response.text}"
160
- except NotAuthorizedError as e:
161
- return e.message
162
- except Exception as e:
163
- logger.error(e)
164
- return f"Error retrieving pull requests: {str(e)}"
125
+ result += f"- PR #{pr_number}: {pr_title} (by {pr_user}, Status: {pr_state})\n"
126
+
127
+ return result
165
128
 
166
- def list_issues(self, repo_full_name: str, per_page: int = 30, page: int = 1) -> str:
129
+ def list_issues(self, repo_full_name: str, state: str = "open", assignee: str = None,
130
+ labels: str = None, per_page: int = 30, page: int = 1) -> List[Dict[str, Any]]:
167
131
  """List issues for a GitHub repository
168
132
 
169
133
  Args:
170
134
  repo_full_name: The full name of the repository (e.g. 'owner/repo')
135
+ state: State of issues to return (open, closed, all). Default: open
136
+ assignee: Filter by assignee. Use 'none' for no assignee, '*' for any assignee
137
+ labels: Comma-separated list of label names (e.g. "bug,ui,@high")
171
138
  per_page: The number of results per page (max 100)
172
139
  page: The page number of the results to fetch
173
140
 
174
141
  Returns:
175
- A formatted list of issues
142
+ The complete GitHub API response
176
143
  """
177
- try:
178
- repo_full_name = repo_full_name.strip()
179
- url = f"{self.base_api_url}/{repo_full_name}/issues/events"
180
- params = {
181
- "per_page": per_page,
182
- "page": page
183
- }
184
- response = self._get(url, params=params)
185
-
186
- if response.status_code == 200:
187
- issues = response.json()
188
- if not issues:
189
- return f"No issues found for repository {repo_full_name}"
190
-
191
- result = f"Issues for {repo_full_name} (Page {page}):\n\n"
192
- for issue in issues:
193
- issue_title = issue.get("issue", {}).get("title", "No Title")
194
- issue_number = issue.get("issue", {}).get("number", "Unknown")
195
- issue_state = issue.get("issue", {}).get("state", "Unknown")
196
- issue_user = issue.get("issue", {}).get("user", {}).get("login", "Unknown")
197
-
198
- result += f"- Issue #{issue_number}: {issue_title} (by {issue_user}, Status: {issue_state})\n"
199
-
200
- return result
201
- elif response.status_code == 404:
202
- return f"Repository {repo_full_name} not found"
203
- else:
204
- logger.error(response.text)
205
- return f"Error retrieving issues: {response.status_code} - {response.text}"
206
- except NotAuthorizedError as e:
207
- return e.message
208
- except Exception as e:
209
- logger.error(e)
210
- return f"Error retrieving issues: {str(e)}"
144
+ repo_full_name = repo_full_name.strip()
145
+ url = f"{self.base_api_url}/{repo_full_name}/issues"
146
+
147
+ params = {
148
+ "state": state,
149
+ "per_page": per_page,
150
+ "page": page
151
+ }
152
+
153
+ if assignee:
154
+ params["assignee"] = assignee
155
+ if labels:
156
+ params["labels"] = labels
157
+
158
+ response = self._get(url, params=params)
159
+ response.raise_for_status()
160
+ return response.json()
211
161
 
212
162
  def get_pull_request(self, repo_full_name: str, pull_number: int) -> str:
213
163
  """Get a specific pull request for a GitHub repository
@@ -219,86 +169,68 @@ class GithubApp(APIApplication):
219
169
  Returns:
220
170
  A formatted string with pull request details
221
171
  """
222
- try:
223
- repo_full_name = repo_full_name.strip()
224
- url = f"{self.base_api_url}/{repo_full_name}/pulls/{pull_number}"
225
- response = self._get(url)
226
-
227
- if response.status_code == 200:
228
- pr = response.json()
229
- pr_title = pr.get("title", "No Title")
230
- pr_number = pr.get("number", "Unknown")
231
- pr_state = pr.get("state", "Unknown")
232
- pr_user = pr.get("user", {}).get("login", "Unknown")
233
- pr_body = pr.get("body", "No description provided.")
234
-
235
- result = (
236
- f"Pull Request #{pr_number}: {pr_title}\n"
237
- f"Created by: {pr_user}\n"
238
- f"Status: {pr_state}\n"
239
- f"Description: {pr_body}\n"
240
- )
241
-
242
- return result
243
- elif response.status_code == 404:
244
- return f"Pull request #{pull_number} not found in repository {repo_full_name}"
245
- else:
246
- logger.error(response.text)
247
- return f"Error retrieving pull request: {response.status_code} - {response.text}"
248
- except NotAuthorizedError as e:
249
- return e.message
250
- except Exception as e:
251
- logger.error(e)
252
- return f"Error retrieving pull request: {str(e)}"
172
+ repo_full_name = repo_full_name.strip()
173
+ url = f"{self.base_api_url}/{repo_full_name}/pulls/{pull_number}"
174
+ response = self._get(url)
175
+ response.raise_for_status()
176
+
177
+ pr = response.json()
178
+ pr_title = pr.get("title", "No Title")
179
+ pr_number = pr.get("number", "Unknown")
180
+ pr_state = pr.get("state", "Unknown")
181
+ pr_user = pr.get("user", {}).get("login", "Unknown")
182
+ pr_body = pr.get("body", "No description provided.")
183
+
184
+ result = (
185
+ f"Pull Request #{pr_number}: {pr_title}\n"
186
+ f"Created by: {pr_user}\n"
187
+ f"Status: {pr_state}\n"
188
+ f"Description: {pr_body}\n"
189
+ )
190
+
191
+ return result
253
192
 
254
- def create_pull_request(self, repo_full_name: str, title: str, head: str, base: str, body: str = "",
255
- maintainer_can_modify: bool = True, draft: bool = False) -> str:
193
+ def create_pull_request(self, repo_full_name: str, head: str, base: str, title: str = None,
194
+ body: str = None, issue: int = None, maintainer_can_modify: bool = True,
195
+ draft: bool = False) -> Dict[str, Any]:
256
196
  """Create a new pull request for a GitHub repository
257
197
 
258
198
  Args:
259
199
  repo_full_name: The full name of the repository (e.g. 'owner/repo')
260
- title: The title of the new pull request
261
200
  head: The name of the branch where your changes are implemented
262
201
  base: The name of the branch you want the changes pulled into
202
+ title: The title of the new pull request (required if issue is not specified)
263
203
  body: The contents of the pull request
204
+ issue: An issue number to convert to a pull request. If specified, the issue's
205
+ title, body, and comments will be used for the pull request
264
206
  maintainer_can_modify: Indicates whether maintainers can modify the pull request
265
207
  draft: Indicates whether the pull request is a draft
266
208
 
267
209
  Returns:
268
- A confirmation message with the new pull request details
210
+ The complete GitHub API response
269
211
  """
270
- try:
271
- repo_full_name = repo_full_name.strip()
272
- url = f"{self.base_api_url}/{repo_full_name}/pulls"
273
-
274
- pull_request_data = {
275
- "title": title,
276
- "head": head,
277
- "base": base,
278
- "body": body,
279
- "maintainer_can_modify": maintainer_can_modify,
280
- "draft": draft
281
- }
282
-
283
- response = self._post(url, pull_request_data)
284
-
285
- if response.status_code in [200, 201]:
286
- pr = response.json()
287
- pr_number = pr.get("number", "Unknown")
288
- pr_url = pr.get("html_url", "")
289
-
290
- return f"Successfully created pull request #{pr_number}:\n" \
291
- f"Title: {title}\n" \
292
- f"From: {head} → To: {base}\n" \
293
- f"URL: {pr_url}"
294
- else:
295
- logger.error(response.text)
296
- return f"Error creating pull request: {response.status_code} - {response.text}"
297
- except NotAuthorizedError as e:
298
- return e.message
299
- except Exception as e:
300
- logger.error(e)
301
- return f"Error creating pull request: {str(e)}"
212
+ repo_full_name = repo_full_name.strip()
213
+ url = f"{self.base_api_url}/{repo_full_name}/pulls"
214
+
215
+ pull_request_data = {
216
+ "head": head,
217
+ "base": base,
218
+ "maintainer_can_modify": maintainer_can_modify,
219
+ "draft": draft
220
+ }
221
+
222
+ if issue is not None:
223
+ pull_request_data["issue"] = issue
224
+ else:
225
+ if title is None:
226
+ raise ValueError("Either 'title' or 'issue' must be specified")
227
+ pull_request_data["title"] = title
228
+ if body is not None:
229
+ pull_request_data["body"] = body
230
+
231
+ response = self._post(url, pull_request_data)
232
+ response.raise_for_status()
233
+ return response.json()
302
234
 
303
235
  def create_issue(self, repo_full_name: str, title: str, body: str = "", labels = None) -> str:
304
236
  """Create a new issue in a GitHub repository
@@ -315,40 +247,31 @@ class GithubApp(APIApplication):
315
247
  Returns:
316
248
  A confirmation message with the new issue details
317
249
  """
318
- try:
319
- repo_full_name = repo_full_name.strip()
320
- url = f"{self.base_api_url}/{repo_full_name}/issues"
321
-
322
- issue_data = {
323
- "title": title,
324
- "body": body
325
- }
326
-
327
- if labels:
328
- if isinstance(labels, str):
329
- labels_list = [label.strip() for label in labels.split(",") if label.strip()]
330
- issue_data["labels"] = labels_list
331
- else:
332
- issue_data["labels"] = labels
333
-
334
- response = self._post(url, issue_data)
335
-
336
- if response.status_code in [200, 201]:
337
- issue = response.json()
338
- issue_number = issue.get("number", "Unknown")
339
- issue_url = issue.get("html_url", "")
340
-
341
- return f"Successfully created issue #{issue_number}:\n" \
342
- f"Title: {title}\n" \
343
- f"URL: {issue_url}"
250
+ repo_full_name = repo_full_name.strip()
251
+ url = f"{self.base_api_url}/{repo_full_name}/issues"
252
+
253
+ issue_data = {
254
+ "title": title,
255
+ "body": body
256
+ }
257
+
258
+ if labels:
259
+ if isinstance(labels, str):
260
+ labels_list = [label.strip() for label in labels.split(",") if label.strip()]
261
+ issue_data["labels"] = labels_list
344
262
  else:
345
- logger.error(response.text)
346
- return f"Error creating issue: {response.status_code} - {response.text}"
347
- except NotAuthorizedError as e:
348
- return e.message
349
- except Exception as e:
350
- logger.error(e)
351
- return f"Error creating issue: {str(e)}"
263
+ issue_data["labels"] = labels
264
+
265
+ response = self._post(url, issue_data)
266
+ response.raise_for_status()
267
+
268
+ issue = response.json()
269
+ issue_number = issue.get("number", "Unknown")
270
+ issue_url = issue.get("html_url", "")
271
+
272
+ return f"Successfully created issue #{issue_number}:\n" \
273
+ f"Title: {title}\n" \
274
+ f"URL: {issue_url}"
352
275
 
353
276
  def list_repo_activities(self, repo_full_name: str, direction: str = "desc", per_page: int = 30) -> str:
354
277
  """List activities for a GitHub repository
@@ -361,48 +284,73 @@ class GithubApp(APIApplication):
361
284
  Returns:
362
285
  A formatted list of repository activities
363
286
  """
364
- try:
365
- repo_full_name = repo_full_name.strip()
366
- url = f"{self.base_api_url}/{repo_full_name}/activity"
367
-
368
- # Build query parameters
369
- params = {
370
- "direction": direction,
371
- "per_page": per_page
372
- }
287
+ repo_full_name = repo_full_name.strip()
288
+ url = f"{self.base_api_url}/{repo_full_name}/activity"
289
+
290
+ # Build query parameters
291
+ params = {
292
+ "direction": direction,
293
+ "per_page": per_page
294
+ }
295
+
296
+ response = self._get(url, params=params)
297
+ response.raise_for_status()
298
+
299
+ activities = response.json()
300
+ if not activities:
301
+ return f"No activities found for repository {repo_full_name}"
302
+
303
+ result = f"Repository activities for {repo_full_name}:\n\n"
304
+
305
+ for activity in activities:
306
+ # Extract common fields
307
+ timestamp = activity.get("timestamp", "Unknown time")
308
+ actor_name = "Unknown user"
309
+ if "actor" in activity and activity["actor"]:
310
+ actor_name = activity["actor"].get("login", "Unknown user")
373
311
 
374
- response = self._get(url, params=params)
312
+ # Create a simple description of the activity
313
+ result += f"- {actor_name} performed an activity at {timestamp}\n"
314
+
315
+ return result
316
+
317
+ def update_issue(self, repo_full_name: str, issue_number: int, title: str = None,
318
+ body: str = None, assignee: str = None, state: str = None,
319
+ state_reason: str = None) -> Dict[str, Any]:
320
+ """Update an issue in a GitHub repository
321
+
322
+ Args:
323
+ repo_full_name: The full name of the repository (e.g. 'owner/repo')
324
+ issue_number: The number that identifies the issue
325
+ title: The title of the issue
326
+ body: The contents of the issue
327
+ assignee: Username to assign to this issue
328
+ state: State of the issue (open or closed)
329
+ state_reason: Reason for state change (completed, not_planned, reopened, null)
375
330
 
376
- if response.status_code == 200:
377
- activities = response.json()
378
- if not activities:
379
- return f"No activities found for repository {repo_full_name}"
380
-
381
- result = f"Repository activities for {repo_full_name}:\n\n"
382
-
383
- for activity in activities:
384
- # Extract common fields
385
- timestamp = activity.get("timestamp", "Unknown time")
386
- actor_name = "Unknown user"
387
- if "actor" in activity and activity["actor"]:
388
- actor_name = activity["actor"].get("login", "Unknown user")
389
-
390
- # Create a simple description of the activity
391
- result += f"- {actor_name} performed an activity at {timestamp}\n"
392
-
393
- return result
394
- elif response.status_code == 404:
395
- return f"Repository {repo_full_name} not found"
396
- else:
397
- logger.error(response.text)
398
- return f"Error retrieving activities: {response.status_code} - {response.text}"
399
- except NotAuthorizedError as e:
400
- return e.message
401
- except Exception as e:
402
- logger.error(e)
403
- return f"Error retrieving activities: {str(e)}"
331
+ Returns:
332
+ The complete GitHub API response
333
+ """
334
+ url = f"{self.base_api_url}/{repo_full_name}/issues/{issue_number}"
335
+
336
+ update_data = {}
337
+ if title is not None:
338
+ update_data["title"] = title
339
+ if body is not None:
340
+ update_data["body"] = body
341
+ if assignee is not None:
342
+ update_data["assignee"] = assignee
343
+ if state is not None:
344
+ update_data["state"] = state
345
+ if state_reason is not None:
346
+ update_data["state_reason"] = state_reason
347
+
348
+ response = self._patch(url, update_data)
349
+ response.raise_for_status()
350
+ return response.json()
404
351
 
405
352
  def list_tools(self):
406
353
  return [self.star_repository, self.list_commits, self.list_branches,
407
354
  self.list_pull_requests, self.list_issues, self.get_pull_request,
408
- self.create_pull_request, self.create_issue, self.list_repo_activities]
355
+ self.create_pull_request, self.create_issue, self.update_issue,
356
+ self.list_repo_activities]