-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub_api.py
More file actions
430 lines (364 loc) · 13.8 KB
/
github_api.py
File metadata and controls
430 lines (364 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# Labels is a CLI tool to audit, sync, and manage Repository labels.
#
# Copyright (C) 2025 TheSyscall
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.
import json
import os
import re
import sys
from typing import Any
from typing import Optional
from typing import Tuple
from typing import Union
import requests
from label_diff import Label
def fetch_json(
endpoint: str,
params: dict[str, Any] = {},
body: dict[str, Any] = {},
method: str = "GET",
) -> Tuple[
Union[dict[str, Any], list[dict[str, Any]], str],
int,
dict[str, Any],
]:
"""
Fetches JSON data from a specified API endpoint using a given HTTP method.
Supports GET, POST, PATCH, and DELETE methods. The function accepts query
parameters, a request body, and authorization headers, and returns the
decoded JSON response, the HTTP status code, and the response headers.
If the response cannot be decoded as JSON, the program exits with an error
message.
Args:
endpoint (str): The API endpoint to fetch data from. Can be a full URL
including the protocol, host, and port, or a relative path appended
to the base GitHub API URL.
params (dict[str, Any], optional): A dictionary of query parameters to
include in the request. Defaults to an empty dictionary.
body (dict[str, Any], optional): A dictionary of data to include in the
request body for POST or PATCH requests. Defaults to an empty
dictionary.
method (str, optional): The HTTP method to use for the request.
Accepts 'GET', 'POST', 'PATCH', or 'DELETE'. Defaults to 'GET'.
Returns:
Tuple[Union[dict[str, Any], list[dict[str, Any]]], int, dict[str, Any]]
A tuple containing:
- the parsed JSON response as a dictionary or list of
dictionaries
- the HTTP status code as an integer
- the response headers as a dictionary
"""
base_url = "https://api.github.com"
url = base_url + endpoint
if endpoint.startswith("http"):
url = endpoint
headers = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
token = os.getenv("GITHUB_ACCESS_TOKEN")
if token is not None and token != "":
headers["Authorization"] = "token " + token
match method:
case "GET":
response = requests.get(
url,
params=params,
headers=headers,
)
case "POST":
response = requests.post(
url,
params=params,
json=body,
headers=headers,
)
case "PATCH":
response = requests.patch(
url,
params=params,
json=body,
headers=headers,
)
case "DELETE":
response = requests.delete(
url,
params=params,
headers=headers,
)
result = response.text
if response.text != "":
try:
result = response.json()
except json.JSONDecodeError as e:
print(f"Failed to decode API response! {e.msg}", file=sys.stderr)
exit(1)
return result, response.status_code, dict(response.headers)
def fetch_paginated_json(
endpoint: str,
params: dict[str, Any] = {},
body: dict[str, Any] = {},
method: str = "GET",
) -> Tuple[list[dict[str, Any]], Optional[str]]:
"""
Fetches paginated JSON data from a given endpoint and returns the combined
content along with an optional error message if applicable.
Supports handling of HTTP pagination through link headers and can process
GET or other HTTP methods.
Parameters:
endpoint (str): The URL endpoint to fetch data from.
params (dict[str, Any]): Query parameters to include in the request.
Defaults to an empty dictionary.
body (dict[str, Any]): Payload to send for non-GET requests, such
as POST or PUT. Defaults to an empty dictionary.
method (str): The HTTP method to use for the request. Defaults
to "GET".
Returns:
Tuple[list[dict[str, Any]], Optional[str]] A tuple containing:
- the combined content of all paginated responses as a list of
dictionaries
- An optional string containing an error message, or None if no
error occurs.
"""
content: list[dict[str, Any]] = []
params["per_page"] = 100
while True:
(data, code, headers) = fetch_json(endpoint, params, body, method)
if code >= 200 and code < 300:
pass # Nothing to do
elif code == 404:
return [], "Resource not found"
else:
return [], "Unknown error"
assert isinstance(data, list)
content += data
if "Link" not in headers:
break
links = parse_link_header(headers["Link"])
if "next" not in links:
break
endpoint = links["next"]
return content, None
def fetch_labels(
namespace: str,
repository: str,
) -> Tuple[list[Label], Optional[str]]:
"""
Fetches a list of labels for a specified repository.
This function retrieves labels associated with a specific repository within
a given namespace. The labels are extracted from the API response and
returned as a list of label objects. It returns an error string in case the
API request fails.
Arguments:
namespace (str): The namespace or organization of the repository.
repository (str): The repository name for which labels are to be
fetched.
Returns:
Tuple[list[Label], Optional[str]] A tuple containing:
- A list of Label objects if the operation is successful;
otherwise, an empty list.
- An optional string containing an error message, or None if no
error occurs.
"""
(data, err) = fetch_paginated_json(
f"/repos/{namespace}/{repository}/labels",
)
if err:
return [], err
result = []
for jdata in data:
result.append(Label.from_dict(jdata))
return result, None
def delete_label(
namespace: str,
repository: str,
label: str,
) -> Tuple[bool, Optional[str]]:
"""
Deletes a specific label from a repository in the given namespace.
This function sends an HTTP DELETE request to remove a label from
a specified repository. The function returns a tuple indicating
whether the operation was successful and an optional error message
if it was not.
Parameters:
namespace (str): The namespace under which the repository is located.
repository (str): The name of the repository from which the label is to
be deleted.
label (str): The name of the label to be deleted.
Returns:
Tuple[bool, Optional[str]] A tuple containing:
- a boolean indicating success (True) or failure (False) of the
operation.
- an optional string containing an error message if the operation
failed.
"""
(data, code, _) = fetch_json(
f"/repos/{namespace}/{repository}/labels/{label}",
method="DELETE",
)
if code >= 200 and code < 300:
return True, None
elif code == 404:
return False, "Not found"
else:
return False, "Unknown error"
def create_label(
namespace: str,
repository: str,
name: str,
description: Optional[str] = None,
color: Optional[str] = None,
) -> Tuple[bool, Optional[str]]:
"""
Create a new label in the specified repository within a given namespace.
This function sends a POST request to the repository API to create a label
with the specified name, and optionally, a description and color.
It returns a tuple indicating the success status of the operation and an
optional error message if the operation was not successful.
Arguments:
namespace (str): The namespace or organization owning the repository.
repository (str): The repository name where the label will be created.
name (str): The name of the label to be created.
description (Optional[str]): The optional description of the label.
color (Optional[str]): The RGB hex code of the label color
(e.g., 'ff5733').
Returns:
Tuple[bool, Optional[str]]: A tuple containing:
- a boolean indicating success (True) or failure (False) of the
operation.
- an optional string containing an error message if the
operation failed.
"""
body = {
"name": name,
}
if description is not None:
body["description"] = description
if color is not None:
body["color"] = color
(data, code, _) = fetch_json(
f"/repos/{namespace}/{repository}/labels",
body=body,
method="POST",
)
if code >= 200 and code < 300:
return True, None
elif code == 404:
return False, "Resource not found"
elif code == 422:
return False, "Validation failed"
else:
return False, "Unknown error"
def update_label(
namespace: str,
repository: str,
old_name: str,
new_name: Optional[str] = None,
description: Optional[str] = None,
color: Optional[str] = None,
) -> Tuple[bool, Optional[str]]:
"""
Updates a label in a specified repository with new values for its
properties.
Allows changing the name, description, and color of the label.
Returns the result of the update operation.
Arguments:
namespace (str): The namespace of the repository
(e.g., organization or username).
repository (str): The name of the repository containing the label to
update.
old_name (str): The current name of the label to be updated.
new_name (Optional[str]): The new name for the label (if provided).
Defaults to None.
description (Optional[str]): A description to update the label with
(if provided). Defaults to None.
color (Optional[str]): A new color for the label in hexadecimal format
(if provided). Defaults to None.
Returns:
Tuple[bool, Optional[str]] A tuple containing:
- a boolean indicating success (True) or failure (False) of the
operation.
- an optional string containing an error message if the
operation failed.
"""
body = {}
if new_name is not None:
body["new_name"] = new_name
if description is not None:
body["description"] = description
if color is not None:
body["color"] = color
(data, code, _) = fetch_json(
f"/repos/{namespace}/{repository}/labels/{old_name}",
body=body,
method="PATCH",
)
if code >= 200 and code < 300:
return True, None
elif code == 404:
return False, "Not found"
elif code == 422:
return False, "Validation failed"
else:
return False, "Unknown error"
def fetch_repositories(
namespace: str,
) -> Tuple[list[dict[str, Any]], Optional[str]]:
"""
Fetch a list of repositories for the given namespace.
This function retrieves all repositories associated with a specific
namespace, which can be a user or an organization.
If the namespace is "-", it fetches the repositories for the authenticated
user. It uses pagination to ensure that all repositories are retrieved.
An error, if occurred during either fetch attempt, is also returned.
Parameters:
namespace (str): The namespace for which the repositories should be
fetched. This can be the name of a user, an organization, or "-"
for the authenticated user.
Returns:
tuple[list[dict[str, Any]], Optional[str]]
A tuple containing:
- A list of dictionaries, where each dictionary represents a
single repository and its details.
- An optional string representing any error message encountered
while attempting to fetch the repositories.
"""
if namespace == "-":
(data, err) = fetch_paginated_json("/user/repos")
else:
(data, err) = fetch_paginated_json(f"/orgs/{namespace}/repos")
if err is not None:
(data, err) = fetch_paginated_json(f"/users/{namespace}/repos")
return data, err
def parse_link_header(header: str) -> dict[str, str]:
"""
Parses the provided HTTP Link header into a dictionary where the keys
are the "rel" attributes and the values are the corresponding URLs.
The function uses a regular expression to find all link entries and
extracts the "rel" attribute and the corresponding URL. It stores this
information in a dictionary and returns the result.
Arguments:
header (str): The HTTP Link header string to be parsed.
Returns:
dict[str, str]: A dictionary mapping "rel" attributes to their
corresponding URLs.
"""
regex = re.compile(r'<(.*?)>(?:.*?)rel="([A-z\s]*)"')
links = {}
for match in regex.finditer(header):
link = match.group(1)
rel = match.group(2)
links[rel] = link
return links