llama-github 0.1.0__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.
- llama_github/__init__.py +3 -0
- llama_github/config/__init__.py +0 -0
- llama_github/config/config.json +20 -0
- llama_github/config/config.py +30 -0
- llama_github/data_retrieval/__init__.py +0 -0
- llama_github/data_retrieval/github_api.py +296 -0
- llama_github/data_retrieval/github_entities.py +301 -0
- llama_github/features/__init__.py +0 -0
- llama_github/features/feature_flags.py +0 -0
- llama_github/features/insider_features.py +0 -0
- llama_github/github_integration/__init__.py +7 -0
- llama_github/github_integration/github_auth_manager.py +266 -0
- llama_github/github_rag.py +348 -0
- llama_github/llm_integration/__init__.py +0 -0
- llama_github/llm_integration/initial_load.py +101 -0
- llama_github/llm_integration/llm_handler.py +131 -0
- llama_github/logger.py +28 -0
- llama_github/rag_processing/__init__.py +0 -0
- llama_github/rag_processing/rag_processor.py +490 -0
- llama_github/utils.py +98 -0
- llama_github/version.py +1 -0
- llama_github-0.1.0.dist-info/LICENSE +201 -0
- llama_github-0.1.0.dist-info/METADATA +100 -0
- llama_github-0.1.0.dist-info/RECORD +26 -0
- llama_github-0.1.0.dist-info/WHEEL +5 -0
- llama_github-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# To do list:
|
|
2
|
+
# 1. Add issues to the Repository class.
|
|
3
|
+
# 2. Add a method to get issues for a repository.
|
|
4
|
+
|
|
5
|
+
from github import GithubException
|
|
6
|
+
from threading import Lock, Event, Thread
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
import time
|
|
9
|
+
from llama_github.logger import logger
|
|
10
|
+
from llama_github.github_integration.github_auth_manager import ExtendedGithub
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Repository:
|
|
15
|
+
def __init__(self, full_name, github_instance: ExtendedGithub, **kwargs):
|
|
16
|
+
"""
|
|
17
|
+
Initializes a Repository instance with details and a GitHub instance for API calls.
|
|
18
|
+
|
|
19
|
+
:param id: The ID of the repository.
|
|
20
|
+
:param name: The name of the repository.
|
|
21
|
+
:param full_name: The full name of the repository (e.g., 'octocat/Hello-World').
|
|
22
|
+
:param description: The description of the repository.
|
|
23
|
+
:param html_url: The HTML URL of the repository.
|
|
24
|
+
:param stargazers_count: The count of stargazers of the repository.
|
|
25
|
+
:param watchers_count: The count of watchers of the repository.
|
|
26
|
+
:param language: The primary language of the repository.
|
|
27
|
+
:param forks_count: The count of forks of the repository.
|
|
28
|
+
:param github_instance: Authenticated instance of a Github client.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
self._github = github_instance
|
|
32
|
+
self.full_name = full_name
|
|
33
|
+
|
|
34
|
+
self.id = kwargs.get("id")
|
|
35
|
+
self.name = kwargs.get("name")
|
|
36
|
+
self.description = kwargs.get("description")
|
|
37
|
+
self.html_url = kwargs.get("html_url")
|
|
38
|
+
self.stargazers_count = kwargs.get("stargazers_count")
|
|
39
|
+
self.language = kwargs.get("language")
|
|
40
|
+
self.default_branch = kwargs.get("default_branch")
|
|
41
|
+
self.updated_at = kwargs.get("updated_at")
|
|
42
|
+
self.watchers_count = None
|
|
43
|
+
|
|
44
|
+
self.creation_time = datetime.now(timezone.utc)
|
|
45
|
+
self.last_read_time = datetime.now(timezone.utc)
|
|
46
|
+
|
|
47
|
+
self._repo = None
|
|
48
|
+
|
|
49
|
+
# Locking for write action
|
|
50
|
+
self._structure_lock = Lock()
|
|
51
|
+
self._file_contents_lock = Lock()
|
|
52
|
+
self._issue_lock = Lock()
|
|
53
|
+
self._readme_lock = Lock()
|
|
54
|
+
self._repo_lock = Lock()
|
|
55
|
+
|
|
56
|
+
if (self.id is None) or \
|
|
57
|
+
(self.name is None) or \
|
|
58
|
+
(self.full_name is None) or \
|
|
59
|
+
(self.html_url is None) or \
|
|
60
|
+
(self.stargazers_count is None) or \
|
|
61
|
+
(self.default_branch is None) or \
|
|
62
|
+
(self.updated_at is None):
|
|
63
|
+
self.get_repo()
|
|
64
|
+
|
|
65
|
+
self._structure = None # Singleton pattern for repository structure
|
|
66
|
+
self._file_contents = {} # Singleton pattern for file contents
|
|
67
|
+
self._issues = {} # Singleton pattern for file contents
|
|
68
|
+
self._readme = None # Singleton pattern for README content
|
|
69
|
+
|
|
70
|
+
def update_last_read_time(self):
|
|
71
|
+
self.last_read_time = datetime.now(timezone.utc)
|
|
72
|
+
|
|
73
|
+
def get_readme(self) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Retrieves the README content of the repository using a singleton design pattern.
|
|
76
|
+
"""
|
|
77
|
+
if self._readme is None: # Check if README content has already been fetched
|
|
78
|
+
with self._readme_lock: # Locking for write action
|
|
79
|
+
if self._readme is None: # Check if README content has already been fetched after get lock
|
|
80
|
+
try:
|
|
81
|
+
readme = self.repo.get_readme()
|
|
82
|
+
self._readme = readme.decoded_content.decode("utf-8")
|
|
83
|
+
except GithubException as e:
|
|
84
|
+
logger.exception(
|
|
85
|
+
f"Error getting README for repository {self.full_name}:")
|
|
86
|
+
self._readme = None
|
|
87
|
+
self.update_last_read_time()
|
|
88
|
+
return self._readme
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def repo(self):
|
|
92
|
+
return self.get_repo()
|
|
93
|
+
|
|
94
|
+
def get_repo(self):
|
|
95
|
+
"""
|
|
96
|
+
Retrieves the Github Repo object of the repository using a singleton design pattern.
|
|
97
|
+
"""
|
|
98
|
+
if self._repo is None: # Check if repo object has already been fetched
|
|
99
|
+
with self._repo_lock: # Locking for write action
|
|
100
|
+
if self._repo is None:
|
|
101
|
+
try:
|
|
102
|
+
self._repo = self._github.get_repo(self.full_name)
|
|
103
|
+
self.id = self._repo.id
|
|
104
|
+
self.name = self._repo.name
|
|
105
|
+
self.description = self._repo.description
|
|
106
|
+
self.html_url = self._repo.html_url
|
|
107
|
+
self.stargazers_count = self._repo.stargazers_count
|
|
108
|
+
self.watchers_count = self._repo.subscribers_count
|
|
109
|
+
self.language = self._repo.language
|
|
110
|
+
self.default_branch = self._repo.default_branch
|
|
111
|
+
self.updated_at = self._repo.updated_at
|
|
112
|
+
except GithubException as e:
|
|
113
|
+
logger.exception(
|
|
114
|
+
f"Error retrieving repository '{self.full_name}':")
|
|
115
|
+
return None
|
|
116
|
+
self.update_last_read_time()
|
|
117
|
+
return self._repo
|
|
118
|
+
|
|
119
|
+
def get_structure(self, path="/") -> dict:
|
|
120
|
+
"""
|
|
121
|
+
Retrieves the structure of the repository using a singleton design pattern.
|
|
122
|
+
"""
|
|
123
|
+
if self._structure is None: # Check if structure has already been fetched
|
|
124
|
+
with self._structure_lock: # Locking for write action
|
|
125
|
+
if self._structure is None: # Check if structure has already been fetched after get lock
|
|
126
|
+
try:
|
|
127
|
+
self._structure = self._github.get_repo_structure(
|
|
128
|
+
self.full_name, self.default_branch)
|
|
129
|
+
except GithubException as e:
|
|
130
|
+
logger.exception(
|
|
131
|
+
f"Error getting structure for repository {self.full_name}:")
|
|
132
|
+
self._structure = None
|
|
133
|
+
self.update_last_read_time()
|
|
134
|
+
return self._structure
|
|
135
|
+
|
|
136
|
+
def get_file_content(self, file_path) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Retrieves the content of a file using a singleton design pattern.
|
|
139
|
+
"""
|
|
140
|
+
if file_path not in self._file_contents: # Check if file content has already been fetched
|
|
141
|
+
with self._file_contents_lock: # Locking for write action
|
|
142
|
+
if file_path not in self._file_contents: # Check if file content has already been fetched after get lock
|
|
143
|
+
try:
|
|
144
|
+
file_content = self.repo.get_contents(file_path)
|
|
145
|
+
self._file_contents[file_path] = file_content.decoded_content.decode(
|
|
146
|
+
"utf-8")
|
|
147
|
+
except GithubException as e:
|
|
148
|
+
logger.exception(
|
|
149
|
+
f"Error getting file content for {file_path} in repository {self.full_name}:")
|
|
150
|
+
return None
|
|
151
|
+
self.update_last_read_time()
|
|
152
|
+
return self._file_contents[file_path]
|
|
153
|
+
|
|
154
|
+
def get_issue_content(self, number, issue=None) -> str:
|
|
155
|
+
"""
|
|
156
|
+
Retrieves the content of a issue using a singleton design pattern.
|
|
157
|
+
"""
|
|
158
|
+
if number not in self._issues: # Check if issue has already been fetched
|
|
159
|
+
with self._issue_lock: # Locking for write action
|
|
160
|
+
if number not in self._issues: # Check if issue has already been fetched after get lock
|
|
161
|
+
try:
|
|
162
|
+
if issue is None:
|
|
163
|
+
issue = self.repo.get_issue(number=number)
|
|
164
|
+
comments_amount = issue.comments
|
|
165
|
+
body_content = "This is a Github Issue related to repo \"" + (self.full_name or "") + "\". Repo description:" + (self.description or "") +\
|
|
166
|
+
"\n\nIssue Title: "+issue.title+"\nIssue state: "+issue.state + \
|
|
167
|
+
"\nIssue last updated at: "+str(issue.updated_at) + \
|
|
168
|
+
"\nTotal favour: " + \
|
|
169
|
+
str(issue.reactions['total_count']) + \
|
|
170
|
+
"\nComment amount: " + \
|
|
171
|
+
str(comments_amount) + \
|
|
172
|
+
"\nIssue body:\n" + \
|
|
173
|
+
re.sub(r'\n+\s*\n+', '\n',
|
|
174
|
+
issue.body.replace('\r', '\n').strip())
|
|
175
|
+
else:
|
|
176
|
+
comments_amount = issue['comments']
|
|
177
|
+
body_content = "This is a Github Issue related to repo \"" + (self.full_name or "") + "\". Repo description:" + (self.description or "") +\
|
|
178
|
+
"\n\nIssue title: "+issue['title']+"\nIssue state: "+issue['state'] + \
|
|
179
|
+
"\nIssue last updated at: "+str(issue['updated_at']) + \
|
|
180
|
+
"\nTotal favour: " + \
|
|
181
|
+
str(issue['reactions']['total_count']) + \
|
|
182
|
+
"\nComment amount: " + \
|
|
183
|
+
str(comments_amount) + \
|
|
184
|
+
"\nIssue body:\n" + \
|
|
185
|
+
re.sub(
|
|
186
|
+
r'\n+\s*\n+', '\n', issue['body'].replace('\r', '\n').strip())
|
|
187
|
+
if comments_amount > 0:
|
|
188
|
+
comments = self._github.get_issue_comments(
|
|
189
|
+
repo_full_name=self.full_name, issue_number=number)
|
|
190
|
+
comments_text_list = []
|
|
191
|
+
for index, comment in enumerate(comments, start=1):
|
|
192
|
+
cleaned_body = re.sub(
|
|
193
|
+
r'\n+\s*\n+', '\n', comment['body'].replace('\r', '\n').strip())
|
|
194
|
+
comment_text = (
|
|
195
|
+
f"Comment No.{index} - comment author type: {comment['user']['type']}; "
|
|
196
|
+
f"comment total favour: {comment['reactions']['total_count']}\n"
|
|
197
|
+
f"Comment body:\n{cleaned_body}\n"
|
|
198
|
+
)
|
|
199
|
+
comments_text_list.append(comment_text)
|
|
200
|
+
comments_text = "\n".join(comments_text_list)
|
|
201
|
+
issue_content = body_content + "\n\nIssue Comments:\n" + comments_text
|
|
202
|
+
else:
|
|
203
|
+
issue_content = body_content
|
|
204
|
+
self._issues[number] = issue_content.strip()
|
|
205
|
+
except GithubException as e:
|
|
206
|
+
logger.exception(
|
|
207
|
+
f"Error getting issue content for {number} in repository {self.full_name}:")
|
|
208
|
+
return None
|
|
209
|
+
self.update_last_read_time()
|
|
210
|
+
return self._issues[number]
|
|
211
|
+
|
|
212
|
+
def __str__(self):
|
|
213
|
+
"""
|
|
214
|
+
String representation of the Repository instance.
|
|
215
|
+
|
|
216
|
+
:return: A string describing the repository.
|
|
217
|
+
"""
|
|
218
|
+
self.update_last_read_time()
|
|
219
|
+
return f"{self.full_name} - {self.description}"
|
|
220
|
+
|
|
221
|
+
def clear_cache(self):
|
|
222
|
+
"""
|
|
223
|
+
Clears the cached data of the repository, including structure, file contents, and README content.
|
|
224
|
+
"""
|
|
225
|
+
with self._structure_lock:
|
|
226
|
+
self._structure = None # Reset the repository structure cache
|
|
227
|
+
with self._file_contents_lock:
|
|
228
|
+
self._file_contents = {} # Reset the file contents cache
|
|
229
|
+
with self._issue_lock:
|
|
230
|
+
self._issues = {} # Reset the issue contents cache
|
|
231
|
+
with self._readme_lock:
|
|
232
|
+
self._readme = None # Reset the README content cache
|
|
233
|
+
with self._repo_lock:
|
|
234
|
+
self._repo = None # Reset the Repo cache
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class RepositoryPool:
|
|
238
|
+
_instance_lock = Lock()
|
|
239
|
+
_instance = None
|
|
240
|
+
|
|
241
|
+
def __new__(cls, *args, **kwargs):
|
|
242
|
+
if cls._instance is None: # First check (unlocked)
|
|
243
|
+
with cls._instance_lock: # Acquire lock
|
|
244
|
+
if cls._instance is None: # Second check (locked)
|
|
245
|
+
cls._instance = super(RepositoryPool, cls).__new__(cls)
|
|
246
|
+
return cls._instance
|
|
247
|
+
|
|
248
|
+
def __init__(self, github_instance, cleanup_interval=3600, max_idle_time=86400):
|
|
249
|
+
if not hasattr(self, 'initialized'):
|
|
250
|
+
with self._instance_lock: # Prevent re-initialization
|
|
251
|
+
if not hasattr(self, 'initialized'):
|
|
252
|
+
self.initialized = True
|
|
253
|
+
self._locks_registry = {} # A registry for repository-specific locks
|
|
254
|
+
self._registry_lock = Lock() # A lock to protect the locks registry
|
|
255
|
+
self._pool = {} # The repository pool
|
|
256
|
+
self.github_instance = github_instance
|
|
257
|
+
self.cleanup_interval = cleanup_interval # How often to run cleanup in seconds
|
|
258
|
+
self.max_idle_time = max_idle_time # Maximum idle time in seconds
|
|
259
|
+
self._cleanup_thread = Thread(
|
|
260
|
+
target=self._cleanup, daemon=True)
|
|
261
|
+
self._stop_event = Event()
|
|
262
|
+
self._cleanup_thread.start()
|
|
263
|
+
|
|
264
|
+
def _cleanup(self): # Internal method for cleaning up idle repository objects' cache content, not real delete repository objects
|
|
265
|
+
"""Periodically checks and removes idle repository objects."""
|
|
266
|
+
while not self._stop_event.is_set():
|
|
267
|
+
with self._registry_lock:
|
|
268
|
+
current_time = datetime.now(timezone.utc)
|
|
269
|
+
for full_name in list(self._pool.keys()):
|
|
270
|
+
repo = self._pool[full_name]
|
|
271
|
+
if ((current_time - repo.last_read_time).total_seconds() > self.max_idle_time) or \
|
|
272
|
+
((current_time - repo.creation_time).total_seconds() > 7 * self.max_idle_time and
|
|
273
|
+
(current_time - repo.last_read_time).total_seconds() > (self.max_idle_time // 4) + 1):
|
|
274
|
+
# release the lock due to object already created
|
|
275
|
+
del self._locks_registry[full_name]
|
|
276
|
+
# clear the cache content of repository object
|
|
277
|
+
self._pool[full_name].clear_cache()
|
|
278
|
+
time.sleep(self.cleanup_interval)
|
|
279
|
+
|
|
280
|
+
def stop_cleanup(self):
|
|
281
|
+
"""Stops the cleanup thread."""
|
|
282
|
+
self._stop_event.set()
|
|
283
|
+
|
|
284
|
+
def _get_repo_lock(self, full_name):
|
|
285
|
+
"""Retrieve or create a lock for a specific repository."""
|
|
286
|
+
with self._registry_lock:
|
|
287
|
+
if full_name not in self._locks_registry:
|
|
288
|
+
# Create a new lock for the repository creation
|
|
289
|
+
self._locks_registry[full_name] = Lock()
|
|
290
|
+
return self._locks_registry[full_name]
|
|
291
|
+
|
|
292
|
+
def get_repository(self, full_name, **kwargs) -> Repository:
|
|
293
|
+
"""Retrieve a repository from the pool or create a new one if it doesn't exist."""
|
|
294
|
+
if full_name in self._pool:
|
|
295
|
+
return self._pool[full_name]
|
|
296
|
+
repo_lock = self._get_repo_lock(full_name)
|
|
297
|
+
with repo_lock:
|
|
298
|
+
if full_name not in self._pool:
|
|
299
|
+
self._pool[full_name] = Repository(
|
|
300
|
+
full_name, self.github_instance, **kwargs)
|
|
301
|
+
return self._pool[full_name]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# To do list:
|
|
2
|
+
# 1. add the mechanism for installation_access_token and github_instance refreshment in authenticate_with_app model
|
|
3
|
+
# 2. add re-try mechanism for the API calls
|
|
4
|
+
# 3. add the mechanism for the rate limit handling
|
|
5
|
+
# 4. add the mechanism for the error handling
|
|
6
|
+
# 5. add the mechanism for the logging
|
|
7
|
+
# 6. add search issues functionality
|
|
8
|
+
# 7. add search discussions functionality through Github GraphQL API
|
|
9
|
+
|
|
10
|
+
from github import Github, GithubIntegration
|
|
11
|
+
import requests
|
|
12
|
+
from requests.adapters import HTTPAdapter
|
|
13
|
+
from requests.exceptions import HTTPError, RequestException
|
|
14
|
+
from urllib3.util.retry import Retry
|
|
15
|
+
from llama_github.logger import logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GitHubAuthManager:
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.github_instance = None
|
|
21
|
+
self.access_token = None
|
|
22
|
+
self.app_id = None
|
|
23
|
+
self.private_key = None
|
|
24
|
+
self.installation_id = None
|
|
25
|
+
|
|
26
|
+
def authenticate_with_token(self, access_token):
|
|
27
|
+
"""
|
|
28
|
+
Authenticate using a personal access token or an OAuth token.
|
|
29
|
+
Suitable for individual developers and applications using OAuth for authorization.
|
|
30
|
+
"""
|
|
31
|
+
self.access_token = access_token
|
|
32
|
+
self.github_instance = ExtendedGithub(login_or_token=access_token)
|
|
33
|
+
return self.github_instance
|
|
34
|
+
|
|
35
|
+
def authenticate_with_app(self, app_id, private_key, installation_id):
|
|
36
|
+
"""
|
|
37
|
+
Authenticate using a GitHub App.
|
|
38
|
+
Suitable for integrations in organizational or enterprise environments.
|
|
39
|
+
"""
|
|
40
|
+
self.app_id = app_id
|
|
41
|
+
self.private_key = private_key
|
|
42
|
+
self.installation_id = installation_id
|
|
43
|
+
integration = GithubIntegration(app_id, private_key)
|
|
44
|
+
installation_access_token = integration.get_access_token(
|
|
45
|
+
installation_id).token
|
|
46
|
+
self.access_token = installation_access_token
|
|
47
|
+
self.github_instance = ExtendedGithub(
|
|
48
|
+
login_or_token=installation_access_token)
|
|
49
|
+
return self.github_instance
|
|
50
|
+
|
|
51
|
+
def close_connection(self):
|
|
52
|
+
"""
|
|
53
|
+
Close the connection to GitHub to free up resources.
|
|
54
|
+
"""
|
|
55
|
+
if self.github_instance:
|
|
56
|
+
self.github_instance = None
|
|
57
|
+
|
|
58
|
+
# Extended Github Class for powerful API calls - e.g. recursive call to get repo structure
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ExtendedGithub(Github):
|
|
62
|
+
def __init__(self, login_or_token):
|
|
63
|
+
self.access_token = login_or_token
|
|
64
|
+
super().__init__(login_or_token=login_or_token)
|
|
65
|
+
|
|
66
|
+
def get_repo_structure(self, repo_full_name, branch='main') -> dict:
|
|
67
|
+
"""
|
|
68
|
+
Get the structure of a repository (files and directories) recursively.
|
|
69
|
+
"""
|
|
70
|
+
owner, repo_name = repo_full_name.split('/')
|
|
71
|
+
headers = {'Authorization': f'token {self.access_token}'}
|
|
72
|
+
|
|
73
|
+
# Function to convert the flat list to a hierarchical structure
|
|
74
|
+
def list_to_tree(items):
|
|
75
|
+
"""
|
|
76
|
+
Convert the flat list to a hierarchical structure with full paths.
|
|
77
|
+
Include size metadata for files and remove 'type' attributes.
|
|
78
|
+
"""
|
|
79
|
+
tree = {}
|
|
80
|
+
for item in items:
|
|
81
|
+
path_parts = item['path'].split('/')
|
|
82
|
+
current_level = tree
|
|
83
|
+
for part in path_parts[:-1]:
|
|
84
|
+
# Ensure 'children' dictionary exists for directories without explicitly adding 'type'
|
|
85
|
+
current_level = current_level.setdefault(
|
|
86
|
+
part, {'children': {}})
|
|
87
|
+
# Ensure we don't inadvertently create a 'type' key for directories
|
|
88
|
+
current_level = current_level.get('children')
|
|
89
|
+
|
|
90
|
+
# For the last part of the path, decide if it's a file or directory and add appropriate information
|
|
91
|
+
if item['type'] == 'blob': # It's a file
|
|
92
|
+
current_level[path_parts[-1]] = {
|
|
93
|
+
'path': item['path'], # Include full path
|
|
94
|
+
# Include size if available
|
|
95
|
+
'size': item.get('size', 0)
|
|
96
|
+
}
|
|
97
|
+
else: # It's a directory
|
|
98
|
+
# Initialize the directory if not already present, without adding 'type'
|
|
99
|
+
if path_parts[-1] not in current_level:
|
|
100
|
+
current_level[path_parts[-1]] = {'children': {}}
|
|
101
|
+
return tree
|
|
102
|
+
|
|
103
|
+
# Directly use the Trees API to get the full directory structure of the "main" branch
|
|
104
|
+
tree_url = f'https://api.github.com/repos/{owner}/{repo_name}/git/trees/{branch}?recursive=1'
|
|
105
|
+
tree_response = requests.get(tree_url, headers=headers)
|
|
106
|
+
|
|
107
|
+
# Check if the request was successful
|
|
108
|
+
if tree_response.status_code == 200:
|
|
109
|
+
tree_data = tree_response.json()
|
|
110
|
+
# Convert the flat list of items to a hierarchical tree structure
|
|
111
|
+
repo_structure = list_to_tree(tree_data['tree'])
|
|
112
|
+
return repo_structure
|
|
113
|
+
else:
|
|
114
|
+
print(
|
|
115
|
+
f"Error fetching tree structure: {tree_response.status_code}")
|
|
116
|
+
print("Details:", tree_response.json())
|
|
117
|
+
|
|
118
|
+
def search_code(self, query: str, per_page: int = 30) -> dict:
|
|
119
|
+
"""
|
|
120
|
+
Search for code on GitHub using the GitHub API.
|
|
121
|
+
|
|
122
|
+
Parameters:
|
|
123
|
+
query (str): The search query.
|
|
124
|
+
per_page (int): The number of results per page.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
dict: The search result in dict format.
|
|
128
|
+
"""
|
|
129
|
+
url = 'https://api.github.com/search/code'
|
|
130
|
+
headers = {
|
|
131
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
132
|
+
'Authorization': f'token {self.access_token}'
|
|
133
|
+
}
|
|
134
|
+
params = {
|
|
135
|
+
'q': query,
|
|
136
|
+
'per_page': per_page
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Retry strategy
|
|
140
|
+
retry_strategy = Retry(
|
|
141
|
+
total=3, # Total number of retries
|
|
142
|
+
# Retry on these HTTP status codes
|
|
143
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
144
|
+
# Retry on these HTTP methods
|
|
145
|
+
allowed_methods=["HEAD", "GET", "OPTIONS"],
|
|
146
|
+
backoff_factor=1 # Exponential backoff factor
|
|
147
|
+
)
|
|
148
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
149
|
+
http = requests.Session()
|
|
150
|
+
http.mount("https://", adapter)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
response = http.get(url, headers=headers, params=params)
|
|
154
|
+
response.raise_for_status() # Raise HTTPError for bad responses
|
|
155
|
+
return response.json().get('items', [])
|
|
156
|
+
except HTTPError as http_err:
|
|
157
|
+
logger.error(f"HTTP error occurred: {http_err}")
|
|
158
|
+
except RequestException as req_err:
|
|
159
|
+
logger.error(f"Request error occurred: {req_err}")
|
|
160
|
+
except Exception as err:
|
|
161
|
+
logger.error(f"An error occurred: {err}")
|
|
162
|
+
|
|
163
|
+
def search_issues(self, query: str, per_page: int = 30) -> dict:
|
|
164
|
+
"""
|
|
165
|
+
Search for code on GitHub using the GitHub API.
|
|
166
|
+
|
|
167
|
+
Parameters:
|
|
168
|
+
query (str): The search query.
|
|
169
|
+
per_page (int): The number of results per page.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
dict: The search result in dict format.
|
|
173
|
+
"""
|
|
174
|
+
url = 'https://api.github.com/search/issues'
|
|
175
|
+
headers = {
|
|
176
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
177
|
+
'Authorization': f'token {self.access_token}'
|
|
178
|
+
}
|
|
179
|
+
params = {
|
|
180
|
+
'q': query,
|
|
181
|
+
'per_page': per_page
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Retry strategy
|
|
185
|
+
retry_strategy = Retry(
|
|
186
|
+
total=3, # Total number of retries
|
|
187
|
+
# Retry on these HTTP status codes
|
|
188
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
189
|
+
# Retry on these HTTP methods
|
|
190
|
+
allowed_methods=["HEAD", "GET", "OPTIONS"],
|
|
191
|
+
backoff_factor=1 # Exponential backoff factor
|
|
192
|
+
)
|
|
193
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
194
|
+
http = requests.Session()
|
|
195
|
+
http.mount("https://", adapter)
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
response = http.get(url, headers=headers, params=params)
|
|
199
|
+
response.raise_for_status() # Raise HTTPError for bad responses
|
|
200
|
+
return response.json().get('items', [])
|
|
201
|
+
except HTTPError as http_err:
|
|
202
|
+
logger.error(f"HTTP error occurred: {http_err}")
|
|
203
|
+
except RequestException as req_err:
|
|
204
|
+
logger.error(f"Request error occurred: {req_err}")
|
|
205
|
+
except Exception as err:
|
|
206
|
+
logger.error(f"An error occurred: {err}")
|
|
207
|
+
|
|
208
|
+
def get_issue_comments(self, repo_full_name: str, issue_number: int) -> dict:
|
|
209
|
+
"""
|
|
210
|
+
Get comments of an issue on GitHub using the GitHub API.
|
|
211
|
+
|
|
212
|
+
Parameters:
|
|
213
|
+
repo_full_name (str): The full name of the repository (e.g., 'octocat/Hello-World').
|
|
214
|
+
issue_number (int): The issue number.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
dict: The comments of the issue in dict format.
|
|
218
|
+
"""
|
|
219
|
+
url = f'https://api.github.com/repos/{repo_full_name}/issues/{issue_number}/comments'
|
|
220
|
+
headers = {
|
|
221
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
222
|
+
'Authorization': f'token {self.access_token}'
|
|
223
|
+
}
|
|
224
|
+
# Retry strategy
|
|
225
|
+
retry_strategy = Retry(
|
|
226
|
+
total=3, # Total number of retries
|
|
227
|
+
# Retry on these HTTP status codes
|
|
228
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
229
|
+
# Retry on these HTTP methods
|
|
230
|
+
allowed_methods=["HEAD", "GET", "OPTIONS"],
|
|
231
|
+
backoff_factor=1 # Exponential backoff factor
|
|
232
|
+
)
|
|
233
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
234
|
+
http = requests.Session()
|
|
235
|
+
http.mount("https://", adapter)
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
response = http.get(url, headers=headers)
|
|
239
|
+
response.raise_for_status() # Raise HTTPError for bad responses
|
|
240
|
+
return response.json()
|
|
241
|
+
except HTTPError as http_err:
|
|
242
|
+
logger.error(f"HTTP error occurred: {http_err}")
|
|
243
|
+
except RequestException as req_err:
|
|
244
|
+
logger.error(f"Request error occurred: {req_err}")
|
|
245
|
+
except Exception as err:
|
|
246
|
+
logger.error(f"An error occurred: {err}")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# Example usage:
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
auth_manager = GitHubAuthManager()
|
|
252
|
+
|
|
253
|
+
# For developers using a personal access token or an OAuth token
|
|
254
|
+
github_instance = auth_manager.authenticate_with_token(
|
|
255
|
+
"your_personal_access_token_or_oauth_token_here")
|
|
256
|
+
|
|
257
|
+
# For organizational or enterprise environments using GitHub App
|
|
258
|
+
# github_instance = auth_manager.authenticate_with_app("app_id", "private_key", "installation_id")
|
|
259
|
+
|
|
260
|
+
# Example action: List all repositories for the authenticated user
|
|
261
|
+
if github_instance:
|
|
262
|
+
for repo in github_instance.get_user().get_repos():
|
|
263
|
+
print(repo.name)
|
|
264
|
+
|
|
265
|
+
# Close the connection when done
|
|
266
|
+
auth_manager.close_connection()
|