acecalisto3 commited on
Commit
32bc55a
·
verified ·
1 Parent(s): a2d3d1d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +70 -349
app.py CHANGED
@@ -19,8 +19,10 @@ LOGS_DIRECTORY = 'logs'
19
  RESOLUTIONS_DIRECTORY = 'resolutions'
20
  REPOS_DIRECTORY = 'repos'
21
 
22
- # Set up logging
23
  def initialize_logger() -> logging.Logger:
 
 
24
  log_file = f"{LOGS_DIRECTORY}/github_bot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
25
  logging.basicConfig(
26
  level=logging.INFO,
@@ -30,15 +32,19 @@ def initialize_logger() -> logging.Logger:
30
  logging.StreamHandler()
31
  ]
32
  )
33
- return logging.getLogger(__name__)
34
 
35
- # Initialize environment and logger
 
 
 
36
  def initialize_environment():
 
37
  directories = [LOGS_DIRECTORY, RESOLUTIONS_DIRECTORY, REPOS_DIRECTORY, INPUT_DIRECTORY, OUTPUT_DIRECTORY]
38
  for directory in directories:
39
  os.makedirs(directory, exist_ok=True)
40
 
41
- # GitHub API handler
42
  class GitHubAPI:
43
  def __init__(self, token: str):
44
  self.token = token
@@ -49,6 +55,7 @@ class GitHubAPI:
49
  self.base_url = "https://api.github.com"
50
 
51
  def _check_rate_limit(self) -> bool:
 
52
  try:
53
  response = requests.get(f"{self.base_url}/rate_limit", headers=self.headers)
54
  response.raise_for_status()
@@ -59,416 +66,130 @@ class GitHubAPI:
59
  if remaining < 10:
60
  wait_time = max(0, reset_time - int(time.time()))
61
  if wait_time > 0:
62
- logger.warning(f"Rate limit nearly exceeded. Waiting {wait_time} seconds before retrying...")
63
  time.sleep(wait_time)
64
  return False
65
  return True
66
- except requests.exceptions.RequestException as e:
67
- logger.error(f"Error checking rate limit: {str(e)}. Retrying...")
68
  return True
69
 
70
  def get_repository(self, owner: str, repo: str) -> Dict:
 
71
  try:
72
  response = requests.get(f"{self.base_url}/repos/{owner}/{repo}", headers=self.headers)
73
  response.raise_for_status()
74
  return response.json()
75
  except requests.HTTPError as e:
76
- logger.error(f"HTTP error getting repository info for {owner}/{repo}: {str(e)}. Please check the repository details.")
77
  raise
78
  except Exception as e:
79
- logger.error(f"Error getting repository info: {str(e)}")
80
  raise
81
 
82
  def get_issues(self, owner: str, repo: str, state: str = 'open') -> List[Dict]:
 
83
  if not self._check_rate_limit():
84
  return []
85
 
86
  try:
87
- response = requests.get(f"{self.base_url}/repos/{owner}/{repo}/issues", headers=self.headers, params={'state': state})
 
 
 
 
88
  response.raise_for_status()
89
- issues = response.json()
90
- return [issue for issue in issues if 'pull_request' not in issue]
91
- except Exception as e:
92
- logger.error(f"Error fetching issues for repository {owner}/{repo}: {str(e)}. Please verify the repository and token.")
93
  return []
94
 
95
- # GitHub Bot
96
  class GitHubBot:
97
  def __init__(self):
98
  self.github_api = None
99
 
100
  def initialize_api(self, token: str):
 
101
  self.github_api = GitHubAPI(token)
102
 
103
  def fetch_issues(self, token: str, owner: str, repo: str) -> List[Dict]:
104
- try:
105
- self.initialize_api(token)
106
- return self.github_api.get_issues(owner, repo)
107
- except Exception as e:
108
- logger.error(f"Error fetching issues for repository {owner}/{repo}: {str(e)}")
109
- return []
110
 
111
  def resolve_issue(self, token: str, owner: str, repo: str, issue_number: int, resolution: str, forked_repo: str) -> str:
 
112
  try:
113
  self.initialize_api(token)
114
  self.github_api.get_repository(owner, repo)
115
 
116
- # Create resolution file
117
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
118
  resolution_file = f"{RESOLUTIONS_DIRECTORY}/resolution_{issue_number}_{timestamp}.md"
119
 
120
  with open(resolution_file, "w") as f:
121
  f.write(f"# Resolution for Issue #{issue_number}\n\n{resolution}")
122
 
123
- # Clone the forked repo
124
- subprocess.run(['git', 'clone', forked_repo, '/tmp/' + forked_repo.split('/')[-1]], check=True)
 
125
 
126
- # Change to the cloned directory
127
- os.chdir('/tmp/' + forked_repo.split('/')[-1])
128
 
129
- # Assuming manual intervention now
130
- input("Apply the fix manually and stage the changes (press ENTER)? ")
131
-
132
- # Commit and push the modifications
133
  subprocess.run(['git', 'add', '.'], check=True)
134
- subprocess.run(['git', 'commit', '-m', f"Resolved issue #{issue_number} ({quote(resolution)})"], check=True)
135
  subprocess.run(['git', 'push', 'origin', 'HEAD'], check=True)
136
 
137
- # Open Pull Request page
138
- webbrowser.open(f'https://github.com/{forked_repo.split("/")[-1]}/compare/master...{owner}:{forked_repo.split("/")[-1]}_resolved_issue_{issue_number}')
139
 
140
- return f"Resolution saved: {resolution_file}"
141
 
142
  except Exception as e:
143
- error_msg = f"Error resolving issue #{issue_number} in repository {owner}/{repo}: {str(e)}"
144
- logger.error(error_msg)
145
- return error_msg
146
-
147
- def handle_issue_selection(token, owner, repo, issue_number, resolution, forked_repo):
148
- bot = GitHubBot()
149
- result = bot.resolve_issue(token, owner, repo, issue_number, resolution, forked_repo)
150
- return result
151
 
 
152
  def extract_info_from_url(url: str) -> Dict[str, Any]:
 
153
  info = {}
154
  try:
155
  response = requests.get(url)
156
  response.raise_for_status()
157
  info['status_code'] = response.status_code
158
  info['headers'] = dict(response.headers)
159
- info['content'] = response.text[:500] # Limit content to first 500 characters for brevity
160
-
161
  parsed_url = urlparse(url)
 
162
  if 'github.com' in parsed_url.netloc:
163
- parts = parsed_url.path.split('/')
164
- if len(parts) > 2:
165
- owner = parts[1]
166
- repo = parts[2]
167
  issues = bot.fetch_issues(github_token, owner, repo)
168
  info['issues'] = issues
169
- elif 'huggingface.co' in parsed_url.netloc:
170
- # Add Hugging Face specific handling if needed
171
- pass
172
-
173
- except requests.HTTPError as e:
174
- info['error'] = f"HTTP error: {str(e)}"
175
- except Exception as e:
176
- info['error'] = f"Error: {str(e)}"
177
  return info
178
 
179
- # Initialize GitHubBot globally
180
- bot = GitHubBot()
181
 
182
- # Define missing functions with validation
183
- def fetch_issues(token, repo_url):
184
- try:
185
- parts = repo_url.split('/')
186
- if len(parts) < 2:
187
- raise ValueError("Repository URL is not in the correct format. Expected format: 'owner/repo'.")
188
-
189
- owner, repo = parts[-2], parts[-1]
190
- issues = bot.fetch_issues(token, owner, repo)
191
- return issues
192
- except Exception as e:
193
- return str(e)
194
-
195
- def resolve_issue(token, repo_url, issue_number, resolution, forked_repo_url):
196
- try:
197
- parts = repo_url.split('/')
198
- if len(parts) < 2:
199
- raise ValueError("Repository URL is not in the correct format. Expected format: 'owner/repo'.")
200
-
201
- owner, repo = parts[-2], parts[-1]
202
- result = bot.resolve_issue(token, owner, repo, issue_number, resolution, forked_repo_url)
203
- return result
204
- except Exception as e:
205
- return str(e)
206
-
207
- def extract_info(url):
208
- try:
209
- info = extract_info_from_url(url)
210
- return info
211
- except Exception as e:
212
- return str(e)
213
-
214
- # HTML and CSS integration
215
- custom_html = """
216
- <!DOCTYPE html>
217
- <html>
218
- <head>
219
- <title>GitHub Issue Manager</title>
220
- <meta charset="UTF-8">
221
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
222
- <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
223
- <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.css" rel="stylesheet" type="text/css" />
224
- <style>
225
- body {
226
- background: linear-gradient(to bottom right, #121212, #303030) !important;
227
- color: #e0e0e0;
228
- font-family: 'Roboto', sans-serif;
229
- overflow-x: hidden; /* Prevent horizontal scrollbar */
230
- }
231
- .card {
232
- border-radius: 1.5rem;
233
- box-shadow: 0 15px 25px rgba(0, 0, 0, 0.2);
234
- margin-bottom: 20px;
235
- }
236
- .input, .select, .textarea {
237
- border: 2px solid #4a4a4a;
238
- border-radius: 0.75rem;
239
- padding-inline: 1.5rem;
240
- padding-block: 0.75rem;
241
- }
242
- h1, h2, h3 {
243
- color: #e0e0e0;
244
- }
245
- .output-area {
246
- padding: 1.5rem;
247
- border-radius: 0.75rem;
248
- }
249
- .btn {
250
- border-radius: 0.75rem;
251
- padding-block: 0.75rem;
252
- padding-inline: 1.5rem;
253
- }
254
- .btn-primary {
255
- background-color: #7928CA;
256
- color: white;
257
- border: 2px solid #7928CA;
258
- }
259
- .btn-primary:hover {
260
- background-color: #5e22a1;
261
- }
262
- .btn-success {
263
- background-color: #22c55e;
264
- color: white;
265
- border: 2px solid #22c55e;
266
- }
267
- .btn-success:hover {
268
- background-color: #1a9a46;
269
- }
270
- </style>
271
- </head>
272
- <body class="bg-gray-900">
273
- <div class="container mx-auto p-4">
274
- <h1 class="text-4xl md:text-5xl font-extrabold text-center mb-8">GitHub Issue Manager</h1>
275
- <!-- GitHub Token & Repo URL -->
276
- <div class="card bg-gray-800 p-8">
277
- <div class="form-control">
278
- <label class="label">
279
- <span class="label-text">GitHub Token</span>
280
- </label>
281
- <input type="password" placeholder="Enter your GitHub Personal Access Token" class="input input-bordered input-primary" id="github-token">
282
- </div>
283
- <div class="form-control">
284
- <label class="label">
285
- <span class="label-text">Repository URL</span>
286
- </label>
287
- <input type="text" placeholder="Enter the full GitHub repository URL" class="input input-bordered input-primary" id="repo-url">
288
- </div>
289
- </div>
290
- <!-- Fetch & Resolve Section -->
291
- <div class="card bg-gray-800 p-8">
292
- <div class="flex justify-between gap-4">
293
- <button class="btn btn-primary" id="fetch-issues">Fetch Issues</button>
294
- <select class="select select-primary w-full max-w-xs" id="issue-dropdown">
295
- <option disabled selected>Select Issue</option>
296
- </select>
297
- </div>
298
- <div class="form-control mt-4">
299
- <label class="label">
300
- <span class="label-text">Resolution</span>
301
- </label>
302
- <textarea class="textarea textarea-primary" placeholder="Enter the resolution details" id="resolution-textarea"></textarea>
303
- </div>
304
- <div class="form-control mt-4">
305
- <label class="label">
306
- <span class="label-text">Forked Repository URL</span>
307
- </label>
308
- <input type="text" placeholder="URL to your forked repository (Optional)" class="input input-bordered input-primary" id="forked-repo-url">
309
- </div>
310
- <div class="form-control mt-4">
311
- <button class="btn btn-success" id="resolve-issue">Resolve Issue</button>
312
- </div>
313
- </div>
314
- <!-- Output Area -->
315
- <div class="card bg-gray-800 p-8 mt-4">
316
- <label class="label">
317
- <span class="label-text">Output</span>
318
- </label>
319
- <textarea class="textarea textarea-primary h-48" id="output-textarea" readonly></textarea>
320
- </div>
321
- <!-- URL to Extract -->
322
- <div class="card bg-gray-800 p-8 mt-4">
323
- <div class="form-control">
324
- <label class="label">
325
- <span class="label-text">URL</span>
326
- </label>
327
- <input type="text" placeholder="Enter a URL to extract information" class="input input-bordered input-primary" id="url-textbox">
328
- </div>
329
- <div class="form-control">
330
- <button class="btn btn-primary" id="extract-info">Extract Info</button>
331
- </div>
332
- </div>
333
- </div>
334
- <script>
335
- const githubTokenInput = document.getElementById('github-token');
336
- const repoUrlInput = document.getElementById('repo-url');
337
- const fetchIssuesButton = document.getElementById('fetch-issues');
338
- const issueDropdown = document.getElementById('issue-dropdown');
339
- const resolutionTextarea = document.getElementById('resolution-textarea');
340
- const forkedRepoUrlInput = document.getElementById('forked-repo-url');
341
- const resolveIssueButton = document.getElementById('resolve-issue');
342
- const outputTextarea = document.getElementById('output-textarea');
343
- const urlTextbox = document.getElementById('url-textbox');
344
- const extractInfoButton = document.getElementById('extract-info');
345
-
346
- fetchIssuesButton.addEventListener('click', async () => {
347
- const token = githubTokenInput.value;
348
- const repoUrl = repoUrlInput.value;
349
- if (!token || !repoUrl) {
350
- outputTextarea.value = "Please provide both GitHub Token and Repository URL.";
351
- return;
352
- }
353
- try {
354
- const response = await fetch('/fetch-issues', {
355
- method: 'POST',
356
- headers: {
357
- 'Content-Type': 'application/json',
358
- },
359
- body: JSON.stringify({ githubToken: token, repoUrl: repoUrl }),
360
- });
361
- if (!response.ok) {
362
- throw new Error(`HTTP error! status: ${response.status}`);
363
- }
364
- const data = await response.json();
365
- if (data.error) {
366
- outputTextarea.value = data.error;
367
- } else {
368
- issueDropdown.innerHTML = '';
369
- data.issues.forEach(issue => {
370
- const option = document.createElement('option');
371
- option.value = issue.number;
372
- option.text = `${issue.number}: ${issue.title}`;
373
- issueDropdown.add(option);
374
- });
375
- }
376
- } catch (error) {
377
- outputTextarea.value = `Error fetching issues: ${error.message}`;
378
- }
379
- });
380
-
381
- resolveIssueButton.addEventListener('click', async () => {
382
- const token = githubTokenInput.value;
383
- const repoUrl = repoUrlInput.value;
384
- const issueNumber = issueDropdown.value;
385
- const resolution = resolutionTextarea.value;
386
- const forkedRepoUrl = forkedRepoUrlInput.value;
387
- if (!token || !repoUrl || !issueNumber || !resolution) {
388
- outputTextarea.value = "Please provide all required fields.";
389
- return;
390
- }
391
- try {
392
- const response = await fetch('/resolve-issue', {
393
- method: 'POST',
394
- headers: {
395
- 'Content-Type': 'application/json',
396
- },
397
- body: JSON.stringify({ githubToken: token, repoUrl: repoUrl, issueNumber: issueNumber, resolution: resolution, forkedRepoUrl: forkedRepoUrl }),
398
- });
399
- if (!response.ok) {
400
- throw new Error(`HTTP error! status: ${response.status}`);
401
- }
402
- const data = await response.json();
403
- if (data.error) {
404
- outputTextarea.value = data.error;
405
- } else {
406
- outputTextarea.value = data.output;
407
- }
408
- } catch (error) {
409
- outputTextarea.value = `Error resolving issue: ${error.message}`;
410
- }
411
- });
412
-
413
- extractInfoButton.addEventListener('click', async () => {
414
- const url = urlTextbox.value;
415
- if (!url) {
416
- outputTextarea.value = "Please provide a URL.";
417
- return;
418
- }
419
- try {
420
- const response = await fetch('/extract-info', {
421
- method: 'POST',
422
- headers: {
423
- 'Content-Type': 'application/json',
424
- },
425
- body: JSON.stringify({ url: url }),
426
- });
427
- if (!response.ok) {
428
- throw new Error(`HTTP error! status: ${response.status}`);
429
- }
430
- const data = await response.json();
431
- if (data.error) {
432
- outputTextarea.value = data.error;
433
- } else {
434
- outputTextarea.value = JSON.stringify(data, null, 2);
435
- }
436
- } catch (error) {
437
- outputTextarea.value = `Error extracting info: ${error.message}`;
438
- }
439
- });
440
- </script>
441
- </body>
442
- </html>
443
- """
444
-
445
- def create_gradio_interface():
446
- with gr.Blocks(css=None, theme=None) as demo:
447
- gr.HTML(custom_html)
448
- return demo
449
-
450
- # Cleanup function
451
- def cleanup():
452
- try:
453
- temp_dirs = [REPOS_DIRECTORY]
454
- for dir_name in temp_dirs:
455
- if os.path.exists(dir_name):
456
- shutil.rmtree(dir_name)
457
- logging.shutdown()
458
- except Exception as e:
459
- print(f"Error during cleanup: {str(e)}")
460
-
461
- def main():
462
- # Initialize environment and logger
463
- initialize_environment()
464
- logger = initialize_logger()
465
-
466
- # Register cleanup handlers
467
- atexit.register(cleanup)
468
 
469
- # Create Gradio interface
470
- demo = create_gradio_interface()
471
- demo.launch()
 
472
 
473
- if __name__ == "__main__":
474
- main()
 
 
 
 
 
 
 
 
19
  RESOLUTIONS_DIRECTORY = 'resolutions'
20
  REPOS_DIRECTORY = 'repos'
21
 
22
+ # Logger setup
23
  def initialize_logger() -> logging.Logger:
24
+ """Sets up and returns a logger instance."""
25
+ os.makedirs(LOGS_DIRECTORY, exist_ok=True)
26
  log_file = f"{LOGS_DIRECTORY}/github_bot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
27
  logging.basicConfig(
28
  level=logging.INFO,
 
32
  logging.StreamHandler()
33
  ]
34
  )
35
+ return logging.getLogger("GitHubBot")
36
 
37
+ # Global logger instance
38
+ logger = initialize_logger()
39
+
40
+ # Ensure required directories exist
41
  def initialize_environment():
42
+ """Creates necessary directories for the script."""
43
  directories = [LOGS_DIRECTORY, RESOLUTIONS_DIRECTORY, REPOS_DIRECTORY, INPUT_DIRECTORY, OUTPUT_DIRECTORY]
44
  for directory in directories:
45
  os.makedirs(directory, exist_ok=True)
46
 
47
+ # GitHub API interaction
48
  class GitHubAPI:
49
  def __init__(self, token: str):
50
  self.token = token
 
55
  self.base_url = "https://api.github.com"
56
 
57
  def _check_rate_limit(self) -> bool:
58
+ """Checks the GitHub API rate limit and waits if necessary."""
59
  try:
60
  response = requests.get(f"{self.base_url}/rate_limit", headers=self.headers)
61
  response.raise_for_status()
 
66
  if remaining < 10:
67
  wait_time = max(0, reset_time - int(time.time()))
68
  if wait_time > 0:
69
+ logger.warning(f"Rate limit nearly exceeded. Waiting {wait_time} seconds...")
70
  time.sleep(wait_time)
71
  return False
72
  return True
73
+ except requests.RequestException as e:
74
+ logger.error(f"Error checking rate limit: {str(e)}")
75
  return True
76
 
77
  def get_repository(self, owner: str, repo: str) -> Dict:
78
+ """Fetches repository details."""
79
  try:
80
  response = requests.get(f"{self.base_url}/repos/{owner}/{repo}", headers=self.headers)
81
  response.raise_for_status()
82
  return response.json()
83
  except requests.HTTPError as e:
84
+ logger.error(f"HTTP error fetching repo {owner}/{repo}: {str(e)}")
85
  raise
86
  except Exception as e:
87
+ logger.error(f"Error fetching repo: {str(e)}")
88
  raise
89
 
90
  def get_issues(self, owner: str, repo: str, state: str = 'open') -> List[Dict]:
91
+ """Fetches issues for a repository."""
92
  if not self._check_rate_limit():
93
  return []
94
 
95
  try:
96
+ response = requests.get(
97
+ f"{self.base_url}/repos/{owner}/{repo}/issues",
98
+ headers=self.headers,
99
+ params={'state': state}
100
+ )
101
  response.raise_for_status()
102
+ return [issue for issue in response.json() if 'pull_request' not in issue]
103
+ except requests.RequestException as e:
104
+ logger.error(f"Error fetching issues for {owner}/{repo}: {str(e)}")
 
105
  return []
106
 
107
+ # GitHub Bot main functionality
108
  class GitHubBot:
109
  def __init__(self):
110
  self.github_api = None
111
 
112
  def initialize_api(self, token: str):
113
+ """Initializes the GitHub API client."""
114
  self.github_api = GitHubAPI(token)
115
 
116
  def fetch_issues(self, token: str, owner: str, repo: str) -> List[Dict]:
117
+ """Fetches issues from a repository."""
118
+ self.initialize_api(token)
119
+ return self.github_api.get_issues(owner, repo)
 
 
 
120
 
121
  def resolve_issue(self, token: str, owner: str, repo: str, issue_number: int, resolution: str, forked_repo: str) -> str:
122
+ """Resolves a GitHub issue and pushes changes to a forked repository."""
123
  try:
124
  self.initialize_api(token)
125
  self.github_api.get_repository(owner, repo)
126
 
 
127
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
128
  resolution_file = f"{RESOLUTIONS_DIRECTORY}/resolution_{issue_number}_{timestamp}.md"
129
 
130
  with open(resolution_file, "w") as f:
131
  f.write(f"# Resolution for Issue #{issue_number}\n\n{resolution}")
132
 
133
+ temp_repo_dir = f"/tmp/{forked_repo.split('/')[-1]}"
134
+ subprocess.run(['git', 'clone', forked_repo, temp_repo_dir], check=True)
135
+ os.chdir(temp_repo_dir)
136
 
137
+ input("Apply the fix manually, then press ENTER to continue...")
 
138
 
 
 
 
 
139
  subprocess.run(['git', 'add', '.'], check=True)
140
+ subprocess.run(['git', 'commit', '-m', f"Resolved issue #{issue_number}: {resolution}"], check=True)
141
  subprocess.run(['git', 'push', 'origin', 'HEAD'], check=True)
142
 
143
+ pull_request_url = f"https://github.com/{forked_repo.split('/')[-1]}/compare/master...{owner}:{repo}_resolved_issue_{issue_number}"
144
+ webbrowser.open(pull_request_url)
145
 
146
+ return f"Resolution completed and saved: {resolution_file}"
147
 
148
  except Exception as e:
149
+ logger.error(f"Error resolving issue #{issue_number} for {owner}/{repo}: {str(e)}")
150
+ return f"Error resolving issue: {str(e)}"
 
 
 
 
 
 
151
 
152
+ # Utility Functions
153
  def extract_info_from_url(url: str) -> Dict[str, Any]:
154
+ """Extracts information from a URL."""
155
  info = {}
156
  try:
157
  response = requests.get(url)
158
  response.raise_for_status()
159
  info['status_code'] = response.status_code
160
  info['headers'] = dict(response.headers)
161
+ info['content'] = response.text[:500]
 
162
  parsed_url = urlparse(url)
163
+
164
  if 'github.com' in parsed_url.netloc:
165
+ parts = parsed_url.path.strip('/').split('/')
166
+ if len(parts) >= 2:
167
+ owner, repo = parts[:2]
168
+ bot = GitHubBot()
169
  issues = bot.fetch_issues(github_token, owner, repo)
170
  info['issues'] = issues
171
+ except requests.RequestException as e:
172
+ info['error'] = f"Error extracting info: {str(e)}"
 
 
 
 
 
 
173
  return info
174
 
175
+ # Initialize environment
176
+ initialize_environment()
177
 
178
+ # Example usage (placeholder for main logic)
179
+ if __name__ == "__main__":
180
+ print("GitHub Bot initialized. Add main logic as needed.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
+ # Sample interaction for testing purposes
183
+ token = input("Enter your GitHub Personal Access Token: ").strip()
184
+ owner = input("Enter the repository owner: ").strip()
185
+ repo = input("Enter the repository name: ").strip()
186
 
187
+ bot = GitHubBot()
188
+ issues = bot.fetch_issues(token, owner, repo)
189
+
190
+ if issues:
191
+ print(f"Found {len(issues)} issue(s):")
192
+ for issue in issues:
193
+ print(f"- #{issue['number']}: {issue['title']}")
194
+ else:
195
+ print("No issues found or unable to fetch issues.")