arcade-github 0.0.13__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.
@@ -0,0 +1,346 @@
1
+ import json
2
+ from typing import Annotated, Optional
3
+
4
+ import httpx
5
+
6
+ from arcade.core.schema import ToolContext
7
+ from arcade.sdk import tool
8
+ from arcade.sdk.auth import GitHub
9
+ from arcade_github.tools.models import (
10
+ ActivityType,
11
+ RepoSortProperty,
12
+ RepoTimePeriod,
13
+ RepoType,
14
+ ReviewCommentSortProperty,
15
+ SortDirection,
16
+ )
17
+ from arcade_github.tools.utils import (
18
+ get_github_json_headers,
19
+ get_url,
20
+ handle_github_response,
21
+ remove_none_values,
22
+ )
23
+
24
+
25
+ # Implements https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#get-a-repository and returns only the stargazers_count field.
26
+ # Example arcade chat usage: "How many stargazers does the <OWNER>/<REPO> repo have?"
27
+ @tool(requires_auth=GitHub())
28
+ async def count_stargazers(
29
+ context: ToolContext,
30
+ owner: Annotated[str, "The owner of the repository"],
31
+ name: Annotated[str, "The name of the repository"],
32
+ ) -> Annotated[int, "The number of stargazers (stars) for the specified repository"]:
33
+ """Count the number of stargazers (stars) for a GitHub repository.
34
+ For example, to count the number of stars for microsoft/vscode, you would use:
35
+ ```
36
+ count_stargazers(owner="microsoft", name="vscode")
37
+ ```
38
+ """
39
+
40
+ headers = get_github_json_headers(context.authorization.token)
41
+
42
+ url = get_url("repo", owner=owner, repo=name)
43
+ async with httpx.AsyncClient() as client:
44
+ response = await client.get(url, headers=headers)
45
+
46
+ handle_github_response(response, url)
47
+
48
+ data = response.json()
49
+ stargazers_count = data.get("stargazers_count", 0)
50
+ return stargazers_count
51
+
52
+
53
+ # Implements https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-organization-repositories
54
+ # Example arcade chat usage: "List all repositories for the <ORG> organization. Sort by creation date in descending order."
55
+ @tool(requires_auth=GitHub())
56
+ async def list_org_repositories(
57
+ context: ToolContext,
58
+ org: Annotated[str, "The organization name. The name is not case sensitive"],
59
+ repo_type: Annotated[RepoType, "The types of repositories you want returned."] = RepoType.ALL,
60
+ sort: Annotated[
61
+ RepoSortProperty, "The property to sort the results by"
62
+ ] = RepoSortProperty.CREATED,
63
+ sort_direction: Annotated[SortDirection, "The order to sort by"] = SortDirection.ASC,
64
+ per_page: Annotated[Optional[int], "The number of results per page"] = 30,
65
+ page: Annotated[Optional[int], "The page number of the results to fetch"] = 1,
66
+ include_extra_data: Annotated[
67
+ bool,
68
+ "If true, return all the data available about the pull requests. This is a large payload and may impact performance - use with caution.",
69
+ ] = False,
70
+ ) -> Annotated[
71
+ dict[str, list[dict]],
72
+ "A dictionary with key 'repositories' containing a list of repositories, each with details such as name, full_name, html_url, description, clone_url, private status, creation/update/push timestamps, and star/watcher/fork counts",
73
+ ]:
74
+ """List repositories for the specified organization."""
75
+ url = get_url("org_repos", org=org)
76
+ params = {
77
+ "type": repo_type.value,
78
+ "sort": sort.value,
79
+ "direction": sort_direction.value,
80
+ "per_page": per_page,
81
+ "page": page,
82
+ }
83
+
84
+ headers = get_github_json_headers(context.authorization.token)
85
+
86
+ async with httpx.AsyncClient() as client:
87
+ response = await client.get(url, headers=headers, params=params)
88
+
89
+ handle_github_response(response, url)
90
+
91
+ repos = response.json()
92
+ if include_extra_data:
93
+ return {"repositories": repos}
94
+
95
+ results = []
96
+ for repo in repos:
97
+ results.append({
98
+ "name": repo["name"],
99
+ "full_name": repo["full_name"],
100
+ "html_url": repo["html_url"],
101
+ "description": repo["description"],
102
+ "clone_url": repo["clone_url"],
103
+ "private": repo["private"],
104
+ "created_at": repo["created_at"],
105
+ "updated_at": repo["updated_at"],
106
+ "pushed_at": repo["pushed_at"],
107
+ "stargazers_count": repo["stargazers_count"],
108
+ "watchers_count": repo["watchers_count"],
109
+ "forks_count": repo["forks_count"],
110
+ })
111
+
112
+ return {"repositories": results}
113
+
114
+
115
+ # Implements https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#get-a-repository
116
+ # Example arcade chat usage: "Tell me about the <OWNER>/<REPO> repo."
117
+ @tool(requires_auth=GitHub())
118
+ async def get_repository(
119
+ context: ToolContext,
120
+ owner: Annotated[str, "The account owner of the repository. The name is not case sensitive."],
121
+ repo: Annotated[
122
+ str,
123
+ "The name of the repository without the .git extension. The name is not case sensitive.",
124
+ ],
125
+ include_extra_data: Annotated[
126
+ bool,
127
+ "If true, return all the data available about the pull requests. This is a large payload and may impact performance - use with caution.",
128
+ ] = False,
129
+ ) -> Annotated[
130
+ dict,
131
+ "A dictionary containing repository details such as name, full_name, html_url, description, clone_url, private status, creation/update/push timestamps, and star/watcher/fork counts",
132
+ ]:
133
+ """Get a repository.
134
+
135
+ Retrieves detailed information about a repository using the GitHub API.
136
+
137
+ Example:
138
+ ```
139
+ get_repository(owner="octocat", repo="Hello-World")
140
+ ```
141
+ """
142
+ url = get_url("repo", owner=owner, repo=repo)
143
+ headers = get_github_json_headers(context.authorization.token)
144
+
145
+ async with httpx.AsyncClient() as client:
146
+ response = await client.get(url, headers=headers)
147
+
148
+ handle_github_response(response, url)
149
+
150
+ repo_data = response.json()
151
+ if include_extra_data:
152
+ return json.dumps(repo_data)
153
+
154
+ return {
155
+ "name": repo_data["name"],
156
+ "full_name": repo_data["full_name"],
157
+ "html_url": repo_data["html_url"],
158
+ "description": repo_data["description"],
159
+ "clone_url": repo_data["clone_url"],
160
+ "private": repo_data["private"],
161
+ "created_at": repo_data["created_at"],
162
+ "updated_at": repo_data["updated_at"],
163
+ "pushed_at": repo_data["pushed_at"],
164
+ "stargazers_count": repo_data["stargazers_count"],
165
+ "watchers_count": repo_data["watchers_count"],
166
+ "forks_count": repo_data["forks_count"],
167
+ }
168
+
169
+
170
+ # Implements https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repository-activities
171
+ # Example arcade chat usage: "List all merges into main for the <OWNER>/<REPO> repo in the last week by <USER>"
172
+ @tool(requires_auth=GitHub())
173
+ async def list_repository_activities(
174
+ context: ToolContext,
175
+ owner: Annotated[str, "The account owner of the repository. The name is not case sensitive."],
176
+ repo: Annotated[
177
+ str,
178
+ "The name of the repository without the .git extension. The name is not case sensitive.",
179
+ ],
180
+ direction: Annotated[
181
+ Optional[SortDirection], "The direction to sort the results by."
182
+ ] = SortDirection.DESC,
183
+ per_page: Annotated[Optional[int], "The number of results per page (max 100)."] = 30,
184
+ before: Annotated[
185
+ Optional[str],
186
+ "A cursor (unique identifier, e.g., a SHA of a commit) to search for results before this cursor.",
187
+ ] = None,
188
+ after: Annotated[
189
+ Optional[str],
190
+ "A cursor (unique identifier, e.g., a SHA of a commit) to search for results after this cursor.",
191
+ ] = None,
192
+ ref: Annotated[
193
+ Optional[str],
194
+ "The Git reference for the activities you want to list. The ref for a branch can be formatted either as refs/heads/BRANCH_NAME or BRANCH_NAME, where BRANCH_NAME is the name of your branch.",
195
+ ] = None,
196
+ actor: Annotated[
197
+ Optional[str], "The GitHub username to filter by the actor who performed the activity."
198
+ ] = None,
199
+ time_period: Annotated[Optional[RepoTimePeriod], "The time period to filter by."] = None,
200
+ activity_type: Annotated[Optional[ActivityType], "The activity type to filter by."] = None,
201
+ include_extra_data: Annotated[
202
+ bool,
203
+ "If true, return all the data available about the pull requests. This is a large payload and may impact performance - use with caution.",
204
+ ] = False,
205
+ ) -> Annotated[
206
+ str,
207
+ "A JSON string containing a dictionary with key 'activities', which is a list of repository activities. Each activity includes id, node_id, before and after states, ref, timestamp, activity_type, and actor information",
208
+ ]:
209
+ """List repository activities.
210
+
211
+ Retrieves a detailed history of changes to a repository, such as pushes, merges, force pushes, and branch changes,
212
+ and associates these changes with commits and users.
213
+
214
+ Example:
215
+ ```
216
+ list_repository_activities(
217
+ owner="octocat",
218
+ repo="Hello-World",
219
+ per_page=10,
220
+ activity_type="force_push"
221
+ )
222
+ ```
223
+ """
224
+ url = get_url("repo_activity", owner=owner, repo=repo)
225
+ params = {
226
+ "direction": direction.value,
227
+ "per_page": min(100, per_page), # The API only allows up to 100 per page
228
+ "before": before,
229
+ "after": after,
230
+ "ref": ref,
231
+ "actor": actor,
232
+ "time_period": time_period,
233
+ "activity_type": activity_type,
234
+ }
235
+ params = remove_none_values(params)
236
+
237
+ headers = get_github_json_headers(context.authorization.token)
238
+
239
+ async with httpx.AsyncClient() as client:
240
+ response = await client.get(url, headers=headers, params=params)
241
+
242
+ handle_github_response(response, url)
243
+
244
+ activities = response.json()
245
+ if include_extra_data:
246
+ return json.dumps({"activities": activities})
247
+
248
+ results = []
249
+ for activity in activities:
250
+ results.append({
251
+ "id": activity["id"],
252
+ "node_id": activity["node_id"],
253
+ "before": activity.get("before"),
254
+ "after": activity.get("after"),
255
+ "ref": activity.get("ref"),
256
+ "timestamp": activity.get("timestamp"),
257
+ "activity_type": activity.get("activity_type"),
258
+ "actor": activity.get("actor", {}).get("login") if activity.get("actor") else None,
259
+ })
260
+ return json.dumps({"activities": results})
261
+
262
+
263
+ # Implements https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#list-review-comments-in-a-repository
264
+ # Example arcade chat usage: "List all review comments for the <OWNER>/<REPO> repo. Sort by update date in descending order."
265
+ # TODO: Improve the 'since' input parameter such that language model can more easily specify a valid date/time.
266
+ @tool(requires_auth=GitHub())
267
+ async def list_review_comments_in_a_repository(
268
+ context: ToolContext,
269
+ owner: Annotated[str, "The account owner of the repository. The name is not case sensitive."],
270
+ repo: Annotated[
271
+ str,
272
+ "The name of the repository without the .git extension. The name is not case sensitive.",
273
+ ],
274
+ sort: Annotated[
275
+ Optional[ReviewCommentSortProperty], "Can be one of: created, updated."
276
+ ] = ReviewCommentSortProperty.CREATED,
277
+ direction: Annotated[
278
+ Optional[SortDirection],
279
+ "The direction to sort results. Ignored without sort parameter. Can be one of: asc, desc.",
280
+ ] = SortDirection.DESC,
281
+ since: Annotated[
282
+ Optional[str],
283
+ "Only show results that were last updated after the given time. This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ.",
284
+ ] = None,
285
+ per_page: Annotated[Optional[int], "The number of results per page (max 100)."] = 30,
286
+ page: Annotated[Optional[int], "The page number of the results to fetch."] = 1,
287
+ include_extra_data: Annotated[
288
+ bool,
289
+ "If true, return all the data available about the pull requests. This is a large payload and may impact performance - use with caution.",
290
+ ] = False,
291
+ ) -> Annotated[
292
+ str,
293
+ "A JSON string containing a dictionary with key 'review_comments', which is a list of review comments. Each comment includes id, url, diff_hunk, path, position details, commit information, user, body, timestamps, and related URLs",
294
+ ]:
295
+ """
296
+ List review comments in a GitHub repository.
297
+
298
+ Example:
299
+ ```
300
+ list_review_comments(owner="octocat", repo="Hello-World", sort="created", direction="asc")
301
+ ```
302
+ """
303
+ url = get_url("repo_pulls_comments", owner=owner, repo=repo)
304
+
305
+ params = {
306
+ "per_page": min(max(1, per_page), 100), # clamp per_page to 1-100
307
+ "page": page,
308
+ "sort": sort,
309
+ "direction": direction,
310
+ "since": since,
311
+ }
312
+ params = remove_none_values(params)
313
+ headers = get_github_json_headers(context.authorization.token)
314
+
315
+ async with httpx.AsyncClient() as client:
316
+ response = await client.get(url, headers=headers, params=params)
317
+
318
+ handle_github_response(response, url)
319
+
320
+ review_comments = response.json()
321
+ if include_extra_data:
322
+ return json.dumps({"review_comments": review_comments})
323
+ else:
324
+ important_info = [
325
+ {
326
+ "id": comment["id"],
327
+ "url": comment["url"],
328
+ "diff_hunk": comment["diff_hunk"],
329
+ "path": comment["path"],
330
+ "position": comment["position"],
331
+ "original_position": comment["original_position"],
332
+ "commit_id": comment["commit_id"],
333
+ "original_commit_id": comment["original_commit_id"],
334
+ "in_reply_to_id": comment.get("in_reply_to_id"),
335
+ "user": comment["user"]["login"],
336
+ "body": comment["body"],
337
+ "created_at": comment["created_at"],
338
+ "updated_at": comment["updated_at"],
339
+ "html_url": comment["html_url"],
340
+ "line": comment["line"],
341
+ "side": comment["side"],
342
+ "pull_request_url": comment["pull_request_url"],
343
+ }
344
+ for comment in review_comments
345
+ ]
346
+ return json.dumps({"review_comments": important_info})
@@ -0,0 +1,79 @@
1
+ from arcade.core.errors import ToolExecutionError
2
+ from arcade_github.tools.constants import ENDPOINTS, GITHUB_API_BASE_URL
3
+
4
+
5
+ def handle_github_response(response: dict, url: str) -> None:
6
+ """
7
+ Handle GitHub API response and raise appropriate exceptions for non-200 status codes.
8
+
9
+ :param response: The response object from the GitHub API
10
+ :param url: The URL of the API endpoint
11
+ :raises ToolExecutionError: If the response status code is not 200
12
+ """
13
+ if 200 <= response.status_code < 300:
14
+ return
15
+
16
+ error_messages = {
17
+ 301: "Moved permanently. The repository has moved.",
18
+ 304: "Not modified. The requested resource hasn't been modified since the last request.",
19
+ 403: "Forbidden. You do not have access to this resource.",
20
+ 404: "Resource not found. The requested resource does not exist.",
21
+ 410: "Gone. The requested resource is no longer available.",
22
+ 422: "Validation failed or the endpoint has been spammed.",
23
+ 503: "Service unavailable. The server is temporarily unable to handle the request.",
24
+ }
25
+
26
+ error_message = error_messages.get(
27
+ response.status_code, f"Failed to process request. Status code: {response.status_code}"
28
+ )
29
+
30
+ raise ToolExecutionError(f"Error accessing '{url}': {error_message}")
31
+
32
+
33
+ def get_github_json_headers(token: str) -> dict:
34
+ """
35
+ Generate common headers for GitHub API requests.
36
+
37
+ :param token: The authorization token
38
+ :return: A dictionary of headers
39
+ """
40
+ return {
41
+ "Accept": "application/vnd.github+json",
42
+ "Authorization": f"Bearer {token}",
43
+ "X-GitHub-Api-Version": "2022-11-28",
44
+ }
45
+
46
+
47
+ def get_github_diff_headers(token: str) -> dict:
48
+ """
49
+ Generate headers for GitHub API requests for diff content.
50
+
51
+ :param token: The authorization token
52
+ :return: A dictionary of headers
53
+ """
54
+ return {
55
+ "Accept": "application/vnd.github.diff",
56
+ "Authorization": f"Bearer {token}",
57
+ "X-GitHub-Api-Version": "2022-11-28",
58
+ }
59
+
60
+
61
+ def remove_none_values(params: dict) -> dict:
62
+ """
63
+ Remove None values from a dictionary.
64
+
65
+ :param params: The dictionary to clean
66
+ :return: A new dictionary with None values removed
67
+ """
68
+ return {k: v for k, v in params.items() if v is not None}
69
+
70
+
71
+ def get_url(endpoint: str, **kwargs) -> str:
72
+ """
73
+ Get the full URL for a given endpoint.
74
+
75
+ :param endpoint: The endpoint key from ENDPOINTS
76
+ :param kwargs: The parameters to format the URL with
77
+ :return: The full URL
78
+ """
79
+ return f"{GITHUB_API_BASE_URL}{ENDPOINTS[endpoint].format(**kwargs)}"
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.1
2
+ Name: arcade_github
3
+ Version: 0.0.13
4
+ Summary: LLM tools for interacting with Github
5
+ Author: Arcade AI
6
+ Author-email: dev@arcade-ai.com
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: arcade-ai (==0.0.13)
14
+ Requires-Dist: httpx (>=0.27.2,<0.28.0)
@@ -0,0 +1,16 @@
1
+ arcade_github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ arcade_github/tests/test_activity.py,sha256=L5kgm3DGo_vkWsLIysbUbhKIxC66T_OdDLNGBBVjQ6I,1727
3
+ arcade_github/tests/test_issues.py,sha256=MgH6lT8a9Qy2aY_IAVEi8WVW_sEbeTwNYK9iTei-TBs,3445
4
+ arcade_github/tests/test_pull_requests.py,sha256=E2kJIOsaiRNsmUoVGOr0lpgRe7rqjm8OwxrQM8peQIw,11521
5
+ arcade_github/tests/test_repositories.py,sha256=Zc306DjfNa6o9EhJJFAdsEDmxTb1f_4usNQpxSW9oc8,2700
6
+ arcade_github/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ arcade_github/tools/activity.py,sha256=RrJ8H12qCxYuvkcE3e7iXz5XozrFp8uS4BoKKgnHYIY,1638
8
+ arcade_github/tools/constants.py,sha256=uwYChJziZnRt0NP1_AhHcEo5V7wive4_JbZeYyrUl-U,929
9
+ arcade_github/tools/issues.py,sha256=hywyzzbBOP4NVbJ8QNqSH_LJL1UWy86oq22XkCFMobQ,5475
10
+ arcade_github/tools/models.py,sha256=Vo0eihZM2lgdy6c8VTUmYSVGQ45b3M5AyH825pI5Goo,2039
11
+ arcade_github/tools/pull_requests.py,sha256=5wKXTkazwgrJBsqRnpEEtPwIUjrvYVp3AkYUij6wRJw,23583
12
+ arcade_github/tools/repositories.py,sha256=589FWTcJuHqMpbqDEOP4hqCd5HZ_-JC2B5IhnGk07sQ,13896
13
+ arcade_github/tools/utils.py,sha256=n90OuiHr_iYZtSMSXkWslZHoMM3pqZf5m0lwXQv-y68,2617
14
+ arcade_github-0.0.13.dist-info/METADATA,sha256=LR_JS_rp4dDdk_SzpdM2GkCmTSDGX7e6PtOQpqtfNwE,512
15
+ arcade_github-0.0.13.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
16
+ arcade_github-0.0.13.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any