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.
- arcade_github/__init__.py +0 -0
- arcade_github/tests/test_activity.py +55 -0
- arcade_github/tests/test_issues.py +110 -0
- arcade_github/tests/test_pull_requests.py +359 -0
- arcade_github/tests/test_repositories.py +73 -0
- arcade_github/tools/__init__.py +0 -0
- arcade_github/tools/activity.py +41 -0
- arcade_github/tools/constants.py +19 -0
- arcade_github/tools/issues.py +137 -0
- arcade_github/tools/models.py +98 -0
- arcade_github/tools/pull_requests.py +554 -0
- arcade_github/tools/repositories.py +346 -0
- arcade_github/tools/utils.py +79 -0
- arcade_github-0.0.13.dist-info/METADATA +14 -0
- arcade_github-0.0.13.dist-info/RECORD +16 -0
- arcade_github-0.0.13.dist-info/WHEEL +4 -0
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from arcade_github.tools.activity import set_starred
|
|
5
|
+
from httpx import Response
|
|
6
|
+
|
|
7
|
+
from arcade.core.errors import ToolExecutionError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_context():
|
|
12
|
+
context = AsyncMock()
|
|
13
|
+
context.authorization.token = "mock_token" # noqa: S105
|
|
14
|
+
return context
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_client():
|
|
19
|
+
with patch("arcade_github.tools.activity.httpx.AsyncClient") as client:
|
|
20
|
+
yield client.return_value.__aenter__.return_value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.asyncio
|
|
24
|
+
@pytest.mark.parametrize(
|
|
25
|
+
"starred,expected_message",
|
|
26
|
+
[
|
|
27
|
+
(True, "Successfully starred the repository owner/repo"),
|
|
28
|
+
(False, "Successfully unstarred the repository owner/repo"),
|
|
29
|
+
],
|
|
30
|
+
)
|
|
31
|
+
async def test_set_starred_success(mock_context, mock_client, starred, expected_message):
|
|
32
|
+
mock_client.put.return_value = mock_client.delete.return_value = Response(204)
|
|
33
|
+
|
|
34
|
+
result = await set_starred(mock_context, "owner", "repo", starred)
|
|
35
|
+
assert result == expected_message
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.asyncio
|
|
39
|
+
@pytest.mark.parametrize(
|
|
40
|
+
"status_code,error_message,expected_error",
|
|
41
|
+
[
|
|
42
|
+
(403, "Forbidden", "Error accessing.*: Forbidden"),
|
|
43
|
+
(404, "Not Found", "Error accessing.*: Resource not found"),
|
|
44
|
+
(500, "Internal Server Error", "Error accessing.*: Failed to process request"),
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
async def test_set_starred_errors(
|
|
48
|
+
mock_context, mock_client, status_code, error_message, expected_error
|
|
49
|
+
):
|
|
50
|
+
mock_client.put.return_value = mock_client.delete.return_value = Response(
|
|
51
|
+
status_code, json={"message": error_message}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
with pytest.raises(ToolExecutionError, match=expected_error):
|
|
55
|
+
await set_starred(mock_context, "owner", "repo", True)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from arcade_github.tools.issues import create_issue, create_issue_comment
|
|
5
|
+
from httpx import Response
|
|
6
|
+
|
|
7
|
+
from arcade.core.errors import ToolExecutionError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_context():
|
|
12
|
+
context = AsyncMock()
|
|
13
|
+
context.authorization.token = "mock_token" # noqa: S105
|
|
14
|
+
return context
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_client():
|
|
19
|
+
with patch("arcade_github.tools.issues.httpx.AsyncClient") as client:
|
|
20
|
+
yield client.return_value.__aenter__.return_value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.asyncio
|
|
24
|
+
@pytest.mark.parametrize(
|
|
25
|
+
"status_code,error_message,expected_error,func,args",
|
|
26
|
+
[
|
|
27
|
+
(
|
|
28
|
+
422,
|
|
29
|
+
"Validation Failed",
|
|
30
|
+
"Error accessing.*: Validation failed",
|
|
31
|
+
create_issue,
|
|
32
|
+
("owner", "repo", "title"),
|
|
33
|
+
),
|
|
34
|
+
(
|
|
35
|
+
401,
|
|
36
|
+
"Unauthorized",
|
|
37
|
+
"Error accessing.*: Failed to process request",
|
|
38
|
+
create_issue_comment,
|
|
39
|
+
("owner", "repo", 1, "body"),
|
|
40
|
+
),
|
|
41
|
+
(
|
|
42
|
+
403,
|
|
43
|
+
"API rate limit exceeded",
|
|
44
|
+
"Error accessing.*: Forbidden",
|
|
45
|
+
create_issue_comment,
|
|
46
|
+
("owner", "repo", 1, "body"),
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
401,
|
|
50
|
+
"Bad credentials",
|
|
51
|
+
"Error accessing.*: Failed to process request",
|
|
52
|
+
create_issue,
|
|
53
|
+
("owner", "repo", "title"),
|
|
54
|
+
),
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
async def test_issue_errors(
|
|
58
|
+
mock_context, mock_client, status_code, error_message, expected_error, func, args
|
|
59
|
+
):
|
|
60
|
+
mock_client.post.return_value = Response(status_code, json={"message": error_message})
|
|
61
|
+
|
|
62
|
+
with pytest.raises(ToolExecutionError, match=expected_error):
|
|
63
|
+
await func(mock_context, *args)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
@pytest.mark.parametrize(
|
|
68
|
+
"func,args,response_json,expected_assertions",
|
|
69
|
+
[
|
|
70
|
+
(
|
|
71
|
+
create_issue,
|
|
72
|
+
("owner", "repo", "Test Issue", "This is a test issue"),
|
|
73
|
+
{
|
|
74
|
+
"id": 1,
|
|
75
|
+
"url": "https://api.github.com/repos/owner/repo/issues/1",
|
|
76
|
+
"title": "Test Issue",
|
|
77
|
+
"body": "This is a test issue",
|
|
78
|
+
"state": "open",
|
|
79
|
+
"html_url": "https://github.com/owner/repo/issues/1",
|
|
80
|
+
"created_at": "2023-05-01T12:00:00Z",
|
|
81
|
+
"updated_at": "2023-05-01T12:00:00Z",
|
|
82
|
+
"user": {"login": "testuser"},
|
|
83
|
+
"assignees": [],
|
|
84
|
+
"labels": [],
|
|
85
|
+
},
|
|
86
|
+
["Test Issue", "https://github.com/owner/repo/issues/1"],
|
|
87
|
+
),
|
|
88
|
+
(
|
|
89
|
+
create_issue_comment,
|
|
90
|
+
("owner", "repo", 1, "This is a test comment"),
|
|
91
|
+
{
|
|
92
|
+
"id": 1,
|
|
93
|
+
"url": "https://api.github.com/repos/owner/repo/issues/comments/1",
|
|
94
|
+
"body": "This is a test comment",
|
|
95
|
+
"user": {"login": "testuser"},
|
|
96
|
+
"created_at": "2023-05-01T12:00:00Z",
|
|
97
|
+
"updated_at": "2023-05-01T12:00:00Z",
|
|
98
|
+
},
|
|
99
|
+
["This is a test comment", "https://api.github.com/repos/owner/repo/issues/comments/1"],
|
|
100
|
+
),
|
|
101
|
+
],
|
|
102
|
+
)
|
|
103
|
+
async def test_issue_success(
|
|
104
|
+
mock_context, mock_client, func, args, response_json, expected_assertions
|
|
105
|
+
):
|
|
106
|
+
mock_client.post.return_value = Response(201, json=response_json)
|
|
107
|
+
|
|
108
|
+
result = await func(mock_context, *args)
|
|
109
|
+
for assertion in expected_assertions:
|
|
110
|
+
assert assertion in result
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from arcade_github.tools.models import (
|
|
5
|
+
DiffSide,
|
|
6
|
+
ReviewCommentSubjectType,
|
|
7
|
+
)
|
|
8
|
+
from arcade_github.tools.pull_requests import (
|
|
9
|
+
create_reply_for_review_comment,
|
|
10
|
+
create_review_comment,
|
|
11
|
+
get_pull_request,
|
|
12
|
+
list_pull_request_commits,
|
|
13
|
+
list_pull_requests,
|
|
14
|
+
list_review_comments_on_pull_request,
|
|
15
|
+
update_pull_request,
|
|
16
|
+
)
|
|
17
|
+
from httpx import Response
|
|
18
|
+
|
|
19
|
+
from arcade.core.errors import RetryableToolError, ToolExecutionError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def mock_context():
|
|
24
|
+
context = AsyncMock()
|
|
25
|
+
context.authorization.token = "mock_token" # noqa: S105
|
|
26
|
+
return context
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def mock_client():
|
|
31
|
+
with patch("arcade_github.tools.pull_requests.httpx.AsyncClient") as client:
|
|
32
|
+
yield client.return_value.__aenter__.return_value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
@pytest.mark.parametrize(
|
|
37
|
+
"func,args,status_code,json_response,expected_result,error_message",
|
|
38
|
+
[
|
|
39
|
+
(list_pull_requests, ("owner", "repo"), 200, [], '{"pull_requests": []}', None),
|
|
40
|
+
(
|
|
41
|
+
get_pull_request,
|
|
42
|
+
("owner", "repo", 1),
|
|
43
|
+
404,
|
|
44
|
+
{"message": "Not Found"},
|
|
45
|
+
None,
|
|
46
|
+
"Error accessing.*: Resource not found",
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
update_pull_request,
|
|
50
|
+
("owner", "repo", 1, "New Title"),
|
|
51
|
+
409,
|
|
52
|
+
{"message": "Conflict"},
|
|
53
|
+
None,
|
|
54
|
+
"Error accessing.*: Failed to process request",
|
|
55
|
+
),
|
|
56
|
+
(
|
|
57
|
+
list_pull_request_commits,
|
|
58
|
+
("owner", "repo", 1),
|
|
59
|
+
500,
|
|
60
|
+
{"message": "Internal Server Error"},
|
|
61
|
+
None,
|
|
62
|
+
"Error accessing.*: Failed to process request",
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
list_review_comments_on_pull_request,
|
|
66
|
+
("owner", "repo", 1),
|
|
67
|
+
403,
|
|
68
|
+
{"message": "API rate limit exceeded"},
|
|
69
|
+
None,
|
|
70
|
+
"Error accessing.*: Forbidden",
|
|
71
|
+
),
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
async def test_pull_request_functions(
|
|
75
|
+
mock_context,
|
|
76
|
+
mock_client,
|
|
77
|
+
func,
|
|
78
|
+
args,
|
|
79
|
+
status_code,
|
|
80
|
+
json_response,
|
|
81
|
+
expected_result,
|
|
82
|
+
error_message,
|
|
83
|
+
):
|
|
84
|
+
mock_client.get.return_value = mock_client.post.return_value = (
|
|
85
|
+
mock_client.patch.return_value
|
|
86
|
+
) = Response(status_code, json=json_response)
|
|
87
|
+
|
|
88
|
+
if error_message:
|
|
89
|
+
with pytest.raises(ToolExecutionError, match=error_message):
|
|
90
|
+
await func(mock_context, *args)
|
|
91
|
+
else:
|
|
92
|
+
result = await func(mock_context, *args)
|
|
93
|
+
assert result == expected_result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.mark.asyncio
|
|
97
|
+
@pytest.mark.parametrize(
|
|
98
|
+
"func,args,json_response,expected_assertions",
|
|
99
|
+
[
|
|
100
|
+
(
|
|
101
|
+
list_pull_requests,
|
|
102
|
+
("owner", "repo"),
|
|
103
|
+
[
|
|
104
|
+
{
|
|
105
|
+
"number": 1,
|
|
106
|
+
"title": "Test PR",
|
|
107
|
+
"body": "This is a test PR",
|
|
108
|
+
"state": "open",
|
|
109
|
+
"html_url": "https://github.com/owner/repo/pull/1",
|
|
110
|
+
"created_at": "2023-05-01T12:00:00Z",
|
|
111
|
+
"updated_at": "2023-05-01T12:00:00Z",
|
|
112
|
+
"user": {"login": "testuser"},
|
|
113
|
+
"base": {"ref": "main"},
|
|
114
|
+
"head": {"ref": "feature-branch"},
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
["Test PR", "https://github.com/owner/repo/pull/1"],
|
|
118
|
+
),
|
|
119
|
+
(
|
|
120
|
+
update_pull_request,
|
|
121
|
+
("owner", "repo", 1, "Updated PR Title", "Updated PR body"),
|
|
122
|
+
{
|
|
123
|
+
"number": 1,
|
|
124
|
+
"title": "Updated PR Title",
|
|
125
|
+
"body": "Updated PR body",
|
|
126
|
+
"state": "open",
|
|
127
|
+
"html_url": "https://github.com/owner/repo/pull/1",
|
|
128
|
+
"created_at": "2023-05-01T12:00:00Z",
|
|
129
|
+
"updated_at": "2023-05-02T12:00:00Z",
|
|
130
|
+
"user": {"login": "testuser"},
|
|
131
|
+
},
|
|
132
|
+
["Updated PR Title", "Updated PR body"],
|
|
133
|
+
),
|
|
134
|
+
(
|
|
135
|
+
list_pull_request_commits,
|
|
136
|
+
("owner", "repo", 1),
|
|
137
|
+
[
|
|
138
|
+
{
|
|
139
|
+
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
140
|
+
"commit": {
|
|
141
|
+
"author": {
|
|
142
|
+
"name": "Test Author",
|
|
143
|
+
"email": "author@example.com",
|
|
144
|
+
"date": "2023-05-01T12:00:00Z",
|
|
145
|
+
},
|
|
146
|
+
"message": "Test commit message",
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
["6dcb09b5b57875f334f61aebed695e2e4193db5e", "Test commit message"],
|
|
151
|
+
),
|
|
152
|
+
(
|
|
153
|
+
create_reply_for_review_comment,
|
|
154
|
+
("owner", "repo", 1, 42, "Thanks for the suggestion."),
|
|
155
|
+
{
|
|
156
|
+
"id": 123,
|
|
157
|
+
"body": "Thanks for the suggestion.",
|
|
158
|
+
"user": {"login": "testuser"},
|
|
159
|
+
"created_at": "2023-05-02T12:00:00Z",
|
|
160
|
+
"updated_at": "2023-05-02T12:00:00Z",
|
|
161
|
+
},
|
|
162
|
+
["Thanks for the suggestion.", "testuser"],
|
|
163
|
+
),
|
|
164
|
+
(
|
|
165
|
+
list_review_comments_on_pull_request,
|
|
166
|
+
("owner", "repo", 1),
|
|
167
|
+
[
|
|
168
|
+
{
|
|
169
|
+
"id": 1,
|
|
170
|
+
"body": "Great changes!",
|
|
171
|
+
"user": {"login": "reviewer1"},
|
|
172
|
+
"created_at": "2023-05-01T12:00:00Z",
|
|
173
|
+
"updated_at": "2023-05-01T12:00:00Z",
|
|
174
|
+
"path": "file1.txt",
|
|
175
|
+
"line": 5,
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
["Great changes!", "reviewer1", "file1.txt"],
|
|
179
|
+
),
|
|
180
|
+
(
|
|
181
|
+
get_pull_request,
|
|
182
|
+
("owner", "repo", 1, False, False),
|
|
183
|
+
{
|
|
184
|
+
"number": 1,
|
|
185
|
+
"title": "Test PR",
|
|
186
|
+
"body": "This is a test PR",
|
|
187
|
+
"state": "open",
|
|
188
|
+
"html_url": "https://github.com/owner/repo/pull/1",
|
|
189
|
+
"created_at": "2023-05-01T12:00:00Z",
|
|
190
|
+
"updated_at": "2023-05-01T12:00:00Z",
|
|
191
|
+
"user": {"login": "testuser"},
|
|
192
|
+
"base": {"ref": "main"},
|
|
193
|
+
"head": {"ref": "feature-branch"},
|
|
194
|
+
},
|
|
195
|
+
["Test PR", "https://github.com/owner/repo/pull/1"],
|
|
196
|
+
),
|
|
197
|
+
(
|
|
198
|
+
get_pull_request,
|
|
199
|
+
("owner", "repo", 1, True, False),
|
|
200
|
+
{
|
|
201
|
+
"number": 1,
|
|
202
|
+
"title": "Test PR",
|
|
203
|
+
"body": "This is a test PR",
|
|
204
|
+
"state": "open",
|
|
205
|
+
"html_url": "https://github.com/owner/repo/pull/1",
|
|
206
|
+
"created_at": "2023-05-01T12:00:00Z",
|
|
207
|
+
"updated_at": "2023-05-01T12:00:00Z",
|
|
208
|
+
"user": {"login": "testuser"},
|
|
209
|
+
"base": {"ref": "main"},
|
|
210
|
+
"head": {"ref": "feature-branch"},
|
|
211
|
+
"diff_content": "Sample diff content",
|
|
212
|
+
},
|
|
213
|
+
["Test PR", "https://github.com/owner/repo/pull/1", "diff_content"],
|
|
214
|
+
),
|
|
215
|
+
(
|
|
216
|
+
create_review_comment,
|
|
217
|
+
(
|
|
218
|
+
"owner",
|
|
219
|
+
"repo",
|
|
220
|
+
1,
|
|
221
|
+
"Great changes!",
|
|
222
|
+
"file1.txt",
|
|
223
|
+
"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
224
|
+
1,
|
|
225
|
+
2,
|
|
226
|
+
DiffSide.RIGHT,
|
|
227
|
+
None,
|
|
228
|
+
ReviewCommentSubjectType.LINE,
|
|
229
|
+
),
|
|
230
|
+
{
|
|
231
|
+
"id": 1,
|
|
232
|
+
"body": "Great changes!",
|
|
233
|
+
"path": "file1.txt",
|
|
234
|
+
"line": 2,
|
|
235
|
+
"side": "RIGHT",
|
|
236
|
+
"commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
237
|
+
"user": {"login": "testuser"},
|
|
238
|
+
"created_at": "2023-05-01T12:00:00Z",
|
|
239
|
+
"updated_at": "2023-05-01T12:00:00Z",
|
|
240
|
+
"html_url": "https://github.com/owner/repo/pull/1#discussion_r1",
|
|
241
|
+
},
|
|
242
|
+
["Great changes!", "file1.txt", "6dcb09b5b57875f334f61aebed695e2e4193db5e"],
|
|
243
|
+
),
|
|
244
|
+
],
|
|
245
|
+
)
|
|
246
|
+
async def test_pull_request_functions_success(
|
|
247
|
+
mock_context, mock_client, func, args, json_response, expected_assertions
|
|
248
|
+
):
|
|
249
|
+
mock_client.get.return_value = mock_client.post.return_value = (
|
|
250
|
+
mock_client.patch.return_value
|
|
251
|
+
) = Response(200, json=json_response)
|
|
252
|
+
|
|
253
|
+
result = await func(mock_context, *args)
|
|
254
|
+
for assertion in expected_assertions:
|
|
255
|
+
assert assertion in result
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@pytest.mark.asyncio
|
|
259
|
+
async def test_create_review_comment_file_subject_type(mock_context, mock_client):
|
|
260
|
+
mock_client.post.return_value = Response(
|
|
261
|
+
200,
|
|
262
|
+
json={
|
|
263
|
+
"id": 1,
|
|
264
|
+
"body": "File comment",
|
|
265
|
+
"path": "file1.txt",
|
|
266
|
+
"commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
267
|
+
"user": {"login": "testuser"},
|
|
268
|
+
"created_at": "2023-05-01T12:00:00Z",
|
|
269
|
+
"updated_at": "2023-05-01T12:00:00Z",
|
|
270
|
+
"html_url": "https://github.com/owner/repo/pull/1#discussion_r1",
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
result = await create_review_comment(
|
|
275
|
+
mock_context,
|
|
276
|
+
"owner",
|
|
277
|
+
"repo",
|
|
278
|
+
1,
|
|
279
|
+
"File comment",
|
|
280
|
+
"file1.txt",
|
|
281
|
+
"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
282
|
+
subject_type=ReviewCommentSubjectType.FILE,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
assert "File comment" in result
|
|
286
|
+
assert "file1.txt" in result
|
|
287
|
+
assert "6dcb09b5b57875f334f61aebed695e2e4193db5e" in result
|
|
288
|
+
assert "start_line" not in mock_client.post.call_args[1]["json"]
|
|
289
|
+
assert "end_line" not in mock_client.post.call_args[1]["json"]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@pytest.mark.asyncio
|
|
293
|
+
async def test_create_review_comment_missing_commit_id(mock_context, mock_client):
|
|
294
|
+
mock_client.get.return_value = Response(
|
|
295
|
+
200,
|
|
296
|
+
json=[{"sha": "latest_commit_sha"}],
|
|
297
|
+
)
|
|
298
|
+
mock_client.post.return_value = Response(
|
|
299
|
+
200,
|
|
300
|
+
json={
|
|
301
|
+
"id": 1,
|
|
302
|
+
"body": "Comment with auto-fetched commit ID",
|
|
303
|
+
"path": "file1.txt",
|
|
304
|
+
"commit_id": "latest_commit_sha",
|
|
305
|
+
"user": {"login": "testuser"},
|
|
306
|
+
"created_at": "2023-05-01T12:00:00Z",
|
|
307
|
+
"updated_at": "2023-05-01T12:00:00Z",
|
|
308
|
+
"html_url": "https://github.com/owner/repo/pull/1#discussion_r1",
|
|
309
|
+
},
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
result = await create_review_comment(
|
|
313
|
+
mock_context,
|
|
314
|
+
"owner",
|
|
315
|
+
"repo",
|
|
316
|
+
1,
|
|
317
|
+
"Comment with auto-fetched commit ID",
|
|
318
|
+
"file1.txt",
|
|
319
|
+
start_line=1,
|
|
320
|
+
end_line=2,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
assert "Comment with auto-fetched commit ID" in result
|
|
324
|
+
assert "latest_commit_sha" in result
|
|
325
|
+
assert mock_client.get.called
|
|
326
|
+
assert mock_client.post.called
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@pytest.mark.asyncio
|
|
330
|
+
async def test_create_review_comment_invalid_input(mock_context, mock_client):
|
|
331
|
+
with pytest.raises(
|
|
332
|
+
RetryableToolError, match="'start_line' and 'end_line' parameters are required"
|
|
333
|
+
):
|
|
334
|
+
await create_review_comment(
|
|
335
|
+
mock_context,
|
|
336
|
+
"owner",
|
|
337
|
+
"repo",
|
|
338
|
+
1,
|
|
339
|
+
"Invalid comment",
|
|
340
|
+
"file1.txt",
|
|
341
|
+
subject_type=ReviewCommentSubjectType.LINE,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@pytest.mark.asyncio
|
|
346
|
+
async def test_create_review_comment_no_commits(mock_context, mock_client):
|
|
347
|
+
mock_client.get.return_value = Response(200, json=[])
|
|
348
|
+
|
|
349
|
+
with pytest.raises(RetryableToolError, match="Failed to get the latest commit SHA"):
|
|
350
|
+
await create_review_comment(
|
|
351
|
+
mock_context,
|
|
352
|
+
"owner",
|
|
353
|
+
"repo",
|
|
354
|
+
1,
|
|
355
|
+
"Comment with no commits",
|
|
356
|
+
"file1.txt",
|
|
357
|
+
start_line=1,
|
|
358
|
+
end_line=2,
|
|
359
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from arcade_github.tools.models import RepoType
|
|
5
|
+
from arcade_github.tools.repositories import (
|
|
6
|
+
count_stargazers,
|
|
7
|
+
get_repository,
|
|
8
|
+
list_org_repositories,
|
|
9
|
+
list_repository_activities,
|
|
10
|
+
list_review_comments_in_a_repository,
|
|
11
|
+
)
|
|
12
|
+
from httpx import Response
|
|
13
|
+
|
|
14
|
+
from arcade.core.errors import ToolExecutionError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_context():
|
|
19
|
+
context = AsyncMock()
|
|
20
|
+
context.authorization.token = "mock_token" # noqa: S105
|
|
21
|
+
return context
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def mock_client():
|
|
26
|
+
with patch("arcade_github.tools.repositories.httpx.AsyncClient") as client:
|
|
27
|
+
yield client.return_value.__aenter__.return_value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
@pytest.mark.parametrize(
|
|
32
|
+
"status_code,error_message,expected_error",
|
|
33
|
+
[
|
|
34
|
+
(422, "Validation Failed", "Error accessing.*: Validation failed"),
|
|
35
|
+
(301, "Moved Permanently", "Error accessing.*: Moved permanently"),
|
|
36
|
+
(404, "Not Found", "Error accessing.*: Resource not found"),
|
|
37
|
+
(503, "Service Unavailable", "Error accessing.*: Service unavailable"),
|
|
38
|
+
(410, "Gone", "Error accessing.*: Gone"),
|
|
39
|
+
],
|
|
40
|
+
)
|
|
41
|
+
async def test_error_responses(
|
|
42
|
+
mock_context, mock_client, status_code, error_message, expected_error
|
|
43
|
+
):
|
|
44
|
+
mock_client.get.return_value = Response(status_code, json={"message": error_message})
|
|
45
|
+
mock_client.post.return_value = Response(status_code, json={"message": error_message})
|
|
46
|
+
|
|
47
|
+
with pytest.raises(ToolExecutionError, match=expected_error):
|
|
48
|
+
if status_code == 422:
|
|
49
|
+
await list_org_repositories(mock_context, "org", repo_type=RepoType.ALL)
|
|
50
|
+
elif status_code == 301:
|
|
51
|
+
await count_stargazers(mock_context, "owner", "repo")
|
|
52
|
+
elif status_code == 404:
|
|
53
|
+
await list_org_repositories(mock_context, "non_existent_org")
|
|
54
|
+
elif status_code == 503:
|
|
55
|
+
await get_repository(mock_context, "owner", "repo")
|
|
56
|
+
elif status_code == 410:
|
|
57
|
+
await list_review_comments_in_a_repository(mock_context, "owner", "repo")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_list_repository_activities_invalid_cursor(mock_context, mock_client):
|
|
62
|
+
mock_client.get.return_value = Response(422, json={"message": "Validation Failed"})
|
|
63
|
+
|
|
64
|
+
with pytest.raises(ToolExecutionError, match="Error accessing.*: Validation failed"):
|
|
65
|
+
await list_repository_activities(mock_context, "owner", "repo", before="invalid_cursor")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.asyncio
|
|
69
|
+
async def test_count_stargazers_success(mock_context, mock_client):
|
|
70
|
+
mock_client.get.return_value = Response(200, json={"stargazers_count": 42})
|
|
71
|
+
|
|
72
|
+
result = await count_stargazers(mock_context, "owner", "repo")
|
|
73
|
+
assert result == 42
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from arcade.core.schema import ToolContext
|
|
6
|
+
from arcade.sdk import tool
|
|
7
|
+
from arcade.sdk.auth import GitHub
|
|
8
|
+
from arcade_github.tools.utils import get_github_json_headers, get_url, handle_github_response
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Implements https://docs.github.com/en/rest/activity/starring?apiVersion=2022-11-28#star-a-repository-for-the-authenticated-user and https://docs.github.com/en/rest/activity/starring?apiVersion=2022-11-28#unstar-a-repository-for-the-authenticated-user
|
|
12
|
+
# Example `arcade chat` usage: "star the vscode repo owned by microsoft"
|
|
13
|
+
@tool(requires_auth=GitHub())
|
|
14
|
+
async def set_starred(
|
|
15
|
+
context: ToolContext,
|
|
16
|
+
owner: Annotated[str, "The owner of the repository"],
|
|
17
|
+
name: Annotated[str, "The name of the repository"],
|
|
18
|
+
starred: Annotated[bool, "Whether to star the repository or not"],
|
|
19
|
+
) -> Annotated[
|
|
20
|
+
str, "A message indicating whether the repository was successfully starred or unstarred"
|
|
21
|
+
]:
|
|
22
|
+
"""
|
|
23
|
+
Star or un-star a GitHub repository.
|
|
24
|
+
For example, to star microsoft/vscode, you would use:
|
|
25
|
+
```
|
|
26
|
+
set_starred(owner="microsoft", name="vscode", starred=True)
|
|
27
|
+
```
|
|
28
|
+
"""
|
|
29
|
+
url = get_url("user_starred", owner=owner, repo=name)
|
|
30
|
+
headers = get_github_json_headers(context.authorization.token)
|
|
31
|
+
|
|
32
|
+
async with httpx.AsyncClient() as client:
|
|
33
|
+
if starred:
|
|
34
|
+
response = await client.put(url, headers=headers)
|
|
35
|
+
else:
|
|
36
|
+
response = await client.delete(url, headers=headers)
|
|
37
|
+
|
|
38
|
+
handle_github_response(response, url)
|
|
39
|
+
|
|
40
|
+
action = "starred" if starred else "unstarred"
|
|
41
|
+
return f"Successfully {action} the repository {owner}/{name}"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Base URL for GitHub API
|
|
2
|
+
GITHUB_API_BASE_URL = "https://api.github.com"
|
|
3
|
+
|
|
4
|
+
# Endpoint patterns
|
|
5
|
+
ENDPOINTS = {
|
|
6
|
+
"repo": "/repos/{owner}/{repo}",
|
|
7
|
+
"org_repos": "/orgs/{org}/repos",
|
|
8
|
+
"repo_activity": "/repos/{owner}/{repo}/activity",
|
|
9
|
+
"repo_pulls_comments": "/repos/{owner}/{repo}/pulls/comments",
|
|
10
|
+
"repo_issues": "/repos/{owner}/{repo}/issues",
|
|
11
|
+
"repo_issue_comments": "/repos/{owner}/{repo}/issues/{issue_number}/comments",
|
|
12
|
+
"repo_pulls": "/repos/{owner}/{repo}/pulls",
|
|
13
|
+
"repo_pull": "/repos/{owner}/{repo}/pulls/{pull_number}",
|
|
14
|
+
"repo_pull_commits": "/repos/{owner}/{repo}/pulls/{pull_number}/commits",
|
|
15
|
+
"repo_pull_comments": "/repos/{owner}/{repo}/pulls/{pull_number}/comments",
|
|
16
|
+
"repo_pull_comment_replies": "/repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies",
|
|
17
|
+
"user_starred": "/user/starred/{owner}/{repo}",
|
|
18
|
+
"repo_stargazers": "/repos/{owner}/{repo}/stargazers",
|
|
19
|
+
}
|