gperdrizet commited on
Commit
ad17523
·
verified ·
1 Parent(s): 5d93a4f

Added function to get details for github repo from its url

Browse files
Files changed (2) hide show
  1. functions/github.py +383 -0
  2. tests/test_github.py +275 -1
functions/github.py CHANGED
@@ -350,3 +350,386 @@ def format_repositories_for_llm(github_result: Dict) -> str:
350
  formatted_parts.append("\n=== END GITHUB REPOSITORIES ===")
351
 
352
  return '\n'.join(formatted_parts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  formatted_parts.append("\n=== END GITHUB REPOSITORIES ===")
351
 
352
  return '\n'.join(formatted_parts)
353
+
354
+
355
+ def get_repository_details(repo_url: str) -> Dict:
356
+ """
357
+ Get detailed information about a specific GitHub repository.
358
+
359
+ Args:
360
+ repo_url (str): GitHub repository URL (e.g., https://github.com/user/repo)
361
+
362
+ Returns:
363
+ dict: Dictionary containing comprehensive repository information
364
+
365
+ Example:
366
+ {
367
+ "status": "success",
368
+ "repository": {
369
+ "name": "repo-name",
370
+ "full_name": "user/repo-name",
371
+ "description": "Repository description",
372
+ "language": "Python",
373
+ "languages": {"Python": 85.5, "JavaScript": 14.5},
374
+ "stars": 100,
375
+ "forks": 25,
376
+ "watchers": 50,
377
+ "size": 1024,
378
+ "created_at": "2024-01-01T00:00:00Z",
379
+ "updated_at": "2024-01-15T00:00:00Z",
380
+ "pushed_at": "2024-01-15T00:00:00Z",
381
+ "html_url": "https://github.com/user/repo",
382
+ "clone_url": "https://github.com/user/repo.git",
383
+ "topics": ["python", "api", "web"],
384
+ "license": {"name": "MIT License", "spdx_id": "MIT"},
385
+ "readme": "README content here...",
386
+ "file_structure": ["src/", "tests/", "README.md", "setup.py"],
387
+ "releases": [{"tag_name": "v1.0.0", "name": "Release 1.0.0"}],
388
+ "contributors": [{"login": "user1", "contributions": 50}],
389
+ "is_fork": false,
390
+ "is_archived": false,
391
+ "is_private": false,
392
+ "default_branch": "main",
393
+ "open_issues": 5,
394
+ "has_issues": true,
395
+ "has_wiki": true,
396
+ "has_pages": false
397
+ },
398
+ "message": "Successfully retrieved repository details"
399
+ }
400
+ """
401
+ if not repo_url or not repo_url.strip():
402
+ return {"status": "error", "message": "No repository URL provided"}
403
+
404
+ try:
405
+ # Extract owner and repo name from URL
406
+ owner, repo_name = _extract_repo_info(repo_url)
407
+
408
+ if not owner or not repo_name:
409
+ return {"status": "error", "message": "Invalid GitHub repository URL format"}
410
+
411
+ logger.info("Fetching detailed information for repository: %s/%s", owner, repo_name)
412
+
413
+ # Get basic repository information
414
+ repo_info = _get_repository_info(owner, repo_name)
415
+ if repo_info["status"] != "success":
416
+ return repo_info
417
+
418
+ repo_data = repo_info["data"]
419
+
420
+ # Get additional repository details
421
+ additional_data = {}
422
+
423
+ # Get languages
424
+ languages_result = _get_repository_languages(owner, repo_name)
425
+ if languages_result["status"] == "success":
426
+ additional_data["languages"] = languages_result["data"]
427
+
428
+ # Get README content
429
+ readme_result = _get_repository_readme(owner, repo_name)
430
+ if readme_result["status"] == "success":
431
+ additional_data["readme"] = readme_result["data"]
432
+
433
+ # Get file structure (root directory)
434
+ file_structure_result = _get_repository_contents(owner, repo_name)
435
+ if file_structure_result["status"] == "success":
436
+ additional_data["file_structure"] = file_structure_result["data"]
437
+
438
+ # Get releases
439
+ releases_result = _get_repository_releases(owner, repo_name)
440
+ if releases_result["status"] == "success":
441
+ additional_data["releases"] = releases_result["data"]
442
+
443
+ # Get contributors
444
+ contributors_result = _get_repository_contributors(owner, repo_name)
445
+ if contributors_result["status"] == "success":
446
+ additional_data["contributors"] = contributors_result["data"]
447
+
448
+ # Combine all data
449
+ repository_details = {
450
+ "name": repo_data.get("name", ""),
451
+ "full_name": repo_data.get("full_name", ""),
452
+ "description": repo_data.get("description", ""),
453
+ "language": repo_data.get("language", ""),
454
+ "languages": additional_data.get("languages", {}),
455
+ "stars": repo_data.get("stargazers_count", 0),
456
+ "forks": repo_data.get("forks_count", 0),
457
+ "watchers": repo_data.get("watchers_count", 0),
458
+ "size": repo_data.get("size", 0),
459
+ "created_at": repo_data.get("created_at", ""),
460
+ "updated_at": repo_data.get("updated_at", ""),
461
+ "pushed_at": repo_data.get("pushed_at", ""),
462
+ "html_url": repo_data.get("html_url", ""),
463
+ "clone_url": repo_data.get("clone_url", ""),
464
+ "ssh_url": repo_data.get("ssh_url", ""),
465
+ "topics": repo_data.get("topics", []),
466
+ "license": repo_data.get("license", {}),
467
+ "readme": additional_data.get("readme", ""),
468
+ "file_structure": additional_data.get("file_structure", []),
469
+ "releases": additional_data.get("releases", []),
470
+ "contributors": additional_data.get("contributors", []),
471
+ "is_fork": repo_data.get("fork", False),
472
+ "is_archived": repo_data.get("archived", False),
473
+ "is_private": repo_data.get("private", False),
474
+ "default_branch": repo_data.get("default_branch", "main"),
475
+ "open_issues": repo_data.get("open_issues_count", 0),
476
+ "has_issues": repo_data.get("has_issues", False),
477
+ "has_wiki": repo_data.get("has_wiki", False),
478
+ "has_pages": repo_data.get("has_pages", False),
479
+ "has_projects": repo_data.get("has_projects", False),
480
+ "visibility": repo_data.get("visibility", "public")
481
+ }
482
+
483
+ result = {
484
+ "status": "success",
485
+ "repository": repository_details,
486
+ "message": f"Successfully retrieved details for {owner}/{repo_name}"
487
+ }
488
+
489
+ # Save results to JSON file
490
+ try:
491
+ data_dir = Path(__file__).parent.parent / "data"
492
+ data_dir.mkdir(exist_ok=True)
493
+
494
+ output_file = data_dir / f"repo_details_{owner}_{repo_name}.json"
495
+ with open(output_file, 'w', encoding='utf-8') as f:
496
+ json.dump(result, f, indent=2, ensure_ascii=False)
497
+
498
+ logger.info("Repository details saved to %s", output_file)
499
+ except Exception as save_error:
500
+ logger.warning("Failed to save repository details to file: %s", str(save_error))
501
+
502
+ return result
503
+
504
+ except Exception as e:
505
+ logger.error("Error retrieving repository details: %s", str(e))
506
+ return {
507
+ "status": "error",
508
+ "message": f"Failed to retrieve repository details: {str(e)}"
509
+ }
510
+
511
+
512
+ def _extract_repo_info(repo_url: str) -> tuple:
513
+ """
514
+ Extract owner and repository name from GitHub repository URL.
515
+
516
+ Args:
517
+ repo_url (str): GitHub repository URL
518
+
519
+ Returns:
520
+ tuple: (owner, repo_name) if valid URL, (None, None) otherwise
521
+ """
522
+ try:
523
+ # Clean up the URL
524
+ url = repo_url.strip().rstrip('/')
525
+
526
+ # Handle various GitHub repository URL formats
527
+ patterns = [
528
+ r'github\.com/([^/]+)/([^/]+)/?$', # https://github.com/owner/repo
529
+ r'github\.com/([^/]+)/([^/]+)/.*', # https://github.com/owner/repo/anything
530
+ ]
531
+
532
+ for pattern in patterns:
533
+ match = re.search(pattern, url)
534
+ if match:
535
+ owner = match.group(1)
536
+ repo_name = match.group(2)
537
+
538
+ # Remove .git suffix if present
539
+ if repo_name.endswith('.git'):
540
+ repo_name = repo_name[:-4]
541
+
542
+ # Validate format
543
+ if (re.match(r'^[a-zA-Z0-9\-_\.]+$', owner) and
544
+ re.match(r'^[a-zA-Z0-9\-_\.]+$', repo_name)):
545
+ return owner, repo_name
546
+
547
+ return None, None
548
+
549
+ except Exception as e:
550
+ logger.warning("Error extracting repo info from URL %s: %s", repo_url, str(e))
551
+ return None, None
552
+
553
+
554
+ def _get_repository_info(owner: str, repo_name: str) -> Dict:
555
+ """Get basic repository information from GitHub API."""
556
+ try:
557
+ url = f"https://api.github.com/repos/{owner}/{repo_name}"
558
+ headers = {
559
+ "Accept": "application/vnd.github.v3+json",
560
+ "User-Agent": "Resumate-App/1.0"
561
+ }
562
+
563
+ response = requests.get(url, headers=headers, timeout=10)
564
+
565
+ if response.status_code == 404:
566
+ return {"status": "error", "message": f"Repository '{owner}/{repo_name}' not found"}
567
+ elif response.status_code == 403:
568
+ return {"status": "error", "message": "GitHub API rate limit exceeded"}
569
+ elif response.status_code != 200:
570
+ return {"status": "error", "message": f"GitHub API error: {response.status_code}"}
571
+
572
+ return {"status": "success", "data": response.json()}
573
+
574
+ except requests.RequestException as e:
575
+ logger.error("Network error fetching repository info: %s", str(e))
576
+ return {"status": "error", "message": f"Network error: {str(e)}"}
577
+
578
+
579
+ def _get_repository_languages(owner: str, repo_name: str) -> Dict:
580
+ """Get repository languages from GitHub API."""
581
+ try:
582
+ url = f"https://api.github.com/repos/{owner}/{repo_name}/languages"
583
+ headers = {
584
+ "Accept": "application/vnd.github.v3+json",
585
+ "User-Agent": "Resumate-App/1.0"
586
+ }
587
+
588
+ response = requests.get(url, headers=headers, timeout=10)
589
+
590
+ if response.status_code == 200:
591
+ # Convert byte counts to percentages
592
+ languages = response.json()
593
+ total_bytes = sum(languages.values())
594
+
595
+ if total_bytes > 0:
596
+ language_percentages = {
597
+ lang: round((bytes_count / total_bytes) * 100, 1)
598
+ for lang, bytes_count in languages.items()
599
+ }
600
+ return {"status": "success", "data": language_percentages}
601
+
602
+ return {"status": "error", "message": "Could not retrieve languages"}
603
+
604
+ except Exception as e:
605
+ logger.warning("Error fetching repository languages: %s", str(e))
606
+ return {"status": "error", "message": str(e)}
607
+
608
+
609
+ def _get_repository_readme(owner: str, repo_name: str) -> Dict:
610
+ """Get repository README content from GitHub API."""
611
+ try:
612
+ url = f"https://api.github.com/repos/{owner}/{repo_name}/readme"
613
+ headers = {
614
+ "Accept": "application/vnd.github.v3+json",
615
+ "User-Agent": "Resumate-App/1.0"
616
+ }
617
+
618
+ response = requests.get(url, headers=headers, timeout=10)
619
+
620
+ if response.status_code == 200:
621
+ readme_data = response.json()
622
+
623
+ # Get the raw content URL and fetch it
624
+ download_url = readme_data.get("download_url")
625
+ if download_url:
626
+ content_response = requests.get(download_url, timeout=10)
627
+ if content_response.status_code == 200:
628
+ return {"status": "success", "data": content_response.text}
629
+
630
+ return {"status": "error", "message": "README not found"}
631
+
632
+ except Exception as e:
633
+ logger.warning("Error fetching README: %s", str(e))
634
+ return {"status": "error", "message": str(e)}
635
+
636
+
637
+ def _get_repository_contents(owner: str, repo_name: str, path: str = "") -> Dict:
638
+ """Get repository contents (file structure) from GitHub API."""
639
+ try:
640
+ url = f"https://api.github.com/repos/{owner}/{repo_name}/contents/{path}"
641
+ headers = {
642
+ "Accept": "application/vnd.github.v3+json",
643
+ "User-Agent": "Resumate-App/1.0"
644
+ }
645
+
646
+ response = requests.get(url, headers=headers, timeout=10)
647
+
648
+ if response.status_code == 200:
649
+ contents = response.json()
650
+
651
+ # Extract file and directory names
652
+ file_structure = []
653
+ for item in contents:
654
+ name = item.get("name", "")
655
+ if item.get("type") == "dir":
656
+ name += "/"
657
+ file_structure.append(name)
658
+
659
+ # Sort with directories first
660
+ file_structure.sort(key=lambda x: (not x.endswith("/"), x.lower()))
661
+
662
+ return {"status": "success", "data": file_structure}
663
+
664
+ return {"status": "error", "message": "Could not retrieve file structure"}
665
+
666
+ except Exception as e:
667
+ logger.warning("Error fetching repository contents: %s", str(e))
668
+ return {"status": "error", "message": str(e)}
669
+
670
+
671
+ def _get_repository_releases(owner: str, repo_name: str) -> Dict:
672
+ """Get repository releases from GitHub API."""
673
+ try:
674
+ url = f"https://api.github.com/repos/{owner}/{repo_name}/releases"
675
+ headers = {
676
+ "Accept": "application/vnd.github.v3+json",
677
+ "User-Agent": "Resumate-App/1.0"
678
+ }
679
+
680
+ response = requests.get(url, headers=headers, timeout=10)
681
+
682
+ if response.status_code == 200:
683
+ releases = response.json()
684
+
685
+ # Extract key release information
686
+ release_info = []
687
+ for release in releases[:10]: # Limit to 10 most recent
688
+ release_info.append({
689
+ "tag_name": release.get("tag_name", ""),
690
+ "name": release.get("name", ""),
691
+ "published_at": release.get("published_at", ""),
692
+ "prerelease": release.get("prerelease", False),
693
+ "draft": release.get("draft", False)
694
+ })
695
+
696
+ return {"status": "success", "data": release_info}
697
+
698
+ return {"status": "error", "message": "Could not retrieve releases"}
699
+
700
+ except Exception as e:
701
+ logger.warning("Error fetching repository releases: %s", str(e))
702
+ return {"status": "error", "message": str(e)}
703
+
704
+
705
+ def _get_repository_contributors(owner: str, repo_name: str) -> Dict:
706
+ """Get repository contributors from GitHub API."""
707
+ try:
708
+ url = f"https://api.github.com/repos/{owner}/{repo_name}/contributors"
709
+ headers = {
710
+ "Accept": "application/vnd.github.v3+json",
711
+ "User-Agent": "Resumate-App/1.0"
712
+ }
713
+
714
+ response = requests.get(url, headers=headers, timeout=10)
715
+
716
+ if response.status_code == 200:
717
+ contributors = response.json()
718
+
719
+ # Extract key contributor information
720
+ contributor_info = []
721
+ for contributor in contributors[:20]: # Limit to top 20 contributors
722
+ contributor_info.append({
723
+ "login": contributor.get("login", ""),
724
+ "contributions": contributor.get("contributions", 0),
725
+ "html_url": contributor.get("html_url", ""),
726
+ "type": contributor.get("type", "")
727
+ })
728
+
729
+ return {"status": "success", "data": contributor_info}
730
+
731
+ return {"status": "error", "message": "Could not retrieve contributors"}
732
+
733
+ except Exception as e:
734
+ logger.warning("Error fetching repository contributors: %s", str(e))
735
+ return {"status": "error", "message": str(e)}
tests/test_github.py CHANGED
@@ -7,10 +7,11 @@ from unittest.mock import patch, MagicMock
7
  import requests
8
  from functions import github
9
 
 
10
 
11
  class TestExtractGitHubUsername(unittest.TestCase):
12
  """Test cases for the _extract_github_username function."""
13
-
14
  def test_valid_github_urls(self):
15
  """Test extraction from valid GitHub URLs."""
16
  test_cases = [
@@ -381,5 +382,278 @@ class TestFormatRepositoriesForLLM(unittest.TestCase):
381
  self.assertNotIn("repo-20", result)
382
 
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  if __name__ == '__main__':
385
  unittest.main()
 
7
  import requests
8
  from functions import github
9
 
10
+ # pylint: disable=protected-access
11
 
12
  class TestExtractGitHubUsername(unittest.TestCase):
13
  """Test cases for the _extract_github_username function."""
14
+
15
  def test_valid_github_urls(self):
16
  """Test extraction from valid GitHub URLs."""
17
  test_cases = [
 
382
  self.assertNotIn("repo-20", result)
383
 
384
 
385
+ class TestGetRepositoryDetails(unittest.TestCase):
386
+ """Test cases for the get_repository_details function."""
387
+
388
+ def test_invalid_repository_url(self):
389
+ """Test handling of invalid repository URLs."""
390
+ invalid_urls = [
391
+ "",
392
+ None,
393
+ "https://gitlab.com/user/repo",
394
+ "https://github.com/",
395
+ "https://github.com/user",
396
+ "not-a-url",
397
+ ]
398
+
399
+ for url in invalid_urls:
400
+ with self.subTest(url=url):
401
+ result = github.get_repository_details(url)
402
+ self.assertEqual(result["status"], "error")
403
+ self.assertIn("message", result)
404
+
405
+ @patch('functions.github._get_repository_info')
406
+ @patch('functions.github._extract_repo_info')
407
+ def test_repository_not_found(self, mock_extract, mock_get_info):
408
+ """Test handling of non-existent repository."""
409
+ mock_extract.return_value = ("user", "nonexistent-repo")
410
+ mock_get_info.return_value = {
411
+ "status": "error",
412
+ "message": "Repository 'user/nonexistent-repo' not found"
413
+ }
414
+
415
+ result = github.get_repository_details("https://github.com/user/nonexistent-repo")
416
+
417
+ self.assertEqual(result["status"], "error")
418
+ self.assertIn("not found", result["message"])
419
+
420
+ @patch('functions.github._get_repository_contributors')
421
+ @patch('functions.github._get_repository_releases')
422
+ @patch('functions.github._get_repository_contents')
423
+ @patch('functions.github._get_repository_readme')
424
+ @patch('functions.github._get_repository_languages')
425
+ @patch('functions.github._get_repository_info')
426
+ @patch('functions.github._extract_repo_info')
427
+ def test_successful_repository_details(self, mock_extract, mock_get_info,
428
+ mock_languages, mock_readme, mock_contents,
429
+ mock_releases, mock_contributors):
430
+ """Test successful repository details retrieval."""
431
+ # Mock URL extraction
432
+ mock_extract.return_value = ("octocat", "Hello-World")
433
+
434
+ # Mock basic repository info
435
+ mock_get_info.return_value = {
436
+ "status": "success",
437
+ "data": {
438
+ "name": "Hello-World",
439
+ "full_name": "octocat/Hello-World",
440
+ "description": "This your first repo!",
441
+ "language": "C",
442
+ "stargazers_count": 80,
443
+ "forks_count": 9,
444
+ "watchers_count": 80,
445
+ "size": 108,
446
+ "created_at": "2011-01-26T19:01:12Z",
447
+ "updated_at": "2011-01-26T19:14:43Z",
448
+ "pushed_at": "2011-01-26T19:06:43Z",
449
+ "html_url": "https://github.com/octocat/Hello-World",
450
+ "clone_url": "https://github.com/octocat/Hello-World.git",
451
+ "ssh_url": "[email protected]:octocat/Hello-World.git",
452
+ "topics": ["example", "tutorial"],
453
+ "license": {"name": "MIT License", "spdx_id": "MIT"},
454
+ "fork": False,
455
+ "archived": False,
456
+ "private": False,
457
+ "default_branch": "master",
458
+ "open_issues_count": 0,
459
+ "has_issues": True,
460
+ "has_wiki": True,
461
+ "has_pages": False,
462
+ "has_projects": True,
463
+ "visibility": "public"
464
+ }
465
+ }
466
+
467
+ # Mock additional data
468
+ mock_languages.return_value = {
469
+ "status": "success",
470
+ "data": {"C": 78.1, "Makefile": 21.9}
471
+ }
472
+
473
+ mock_readme.return_value = {
474
+ "status": "success",
475
+ "data": "# Hello World\n\nThis is a test repository."
476
+ }
477
+
478
+ mock_contents.return_value = {
479
+ "status": "success",
480
+ "data": ["README.md", "hello.c", "Makefile"]
481
+ }
482
+
483
+ mock_releases.return_value = {
484
+ "status": "success",
485
+ "data": [
486
+ {
487
+ "tag_name": "v1.0.0",
488
+ "name": "First Release",
489
+ "published_at": "2011-01-26T19:14:43Z",
490
+ "prerelease": False,
491
+ "draft": False
492
+ }
493
+ ]
494
+ }
495
+
496
+ mock_contributors.return_value = {
497
+ "status": "success",
498
+ "data": [
499
+ {
500
+ "login": "octocat",
501
+ "contributions": 32,
502
+ "html_url": "https://github.com/octocat",
503
+ "type": "User"
504
+ }
505
+ ]
506
+ }
507
+
508
+ result = github.get_repository_details("https://github.com/octocat/Hello-World")
509
+
510
+ # Verify success
511
+ self.assertEqual(result["status"], "success")
512
+ self.assertIn("repository", result)
513
+
514
+ repo = result["repository"]
515
+
516
+ # Verify basic info
517
+ self.assertEqual(repo["name"], "Hello-World")
518
+ self.assertEqual(repo["full_name"], "octocat/Hello-World")
519
+ self.assertEqual(repo["description"], "This your first repo!")
520
+ self.assertEqual(repo["language"], "C")
521
+ self.assertEqual(repo["stars"], 80)
522
+ self.assertEqual(repo["forks"], 9)
523
+
524
+ # Verify additional data
525
+ self.assertEqual(repo["languages"], {"C": 78.1, "Makefile": 21.9})
526
+ self.assertIn("Hello World", repo["readme"])
527
+ self.assertEqual(repo["file_structure"], ["README.md", "hello.c", "Makefile"])
528
+ self.assertEqual(len(repo["releases"]), 1)
529
+ self.assertEqual(repo["releases"][0]["tag_name"], "v1.0.0")
530
+ self.assertEqual(len(repo["contributors"]), 1)
531
+ self.assertEqual(repo["contributors"][0]["login"], "octocat")
532
+
533
+ # Verify boolean flags
534
+ self.assertFalse(repo["is_fork"])
535
+ self.assertFalse(repo["is_archived"])
536
+ self.assertFalse(repo["is_private"])
537
+ self.assertTrue(repo["has_issues"])
538
+
539
+ def test_extract_repo_info_valid_urls(self):
540
+ """Test _extract_repo_info with valid repository URLs."""
541
+ test_cases = [
542
+ ("https://github.com/octocat/Hello-World", ("octocat", "Hello-World")),
543
+ ("https://github.com/user/repo.git", ("user", "repo")),
544
+ ("https://github.com/org/project/", ("org", "project")),
545
+ ("github.com/test/example", ("test", "example")),
546
+ ("https://github.com/user/repo/issues", ("user", "repo")),
547
+ ]
548
+
549
+ for url, expected in test_cases:
550
+ with self.subTest(url=url):
551
+ result = github._extract_repo_info(url)
552
+ self.assertEqual(result, expected)
553
+
554
+ def test_extract_repo_info_invalid_urls(self):
555
+ """Test _extract_repo_info with invalid repository URLs."""
556
+ invalid_urls = [
557
+ "",
558
+ "https://gitlab.com/user/repo",
559
+ "https://github.com/user",
560
+ "https://github.com/",
561
+ "not-a-url",
562
+ ]
563
+
564
+ for url in invalid_urls:
565
+ with self.subTest(url=url):
566
+ result = github._extract_repo_info(url)
567
+ self.assertEqual(result, (None, None))
568
+
569
+ @patch('requests.get')
570
+ def test_get_repository_info_success(self, mock_get):
571
+ """Test _get_repository_info with successful response."""
572
+ mock_response = MagicMock()
573
+ mock_response.status_code = 200
574
+ mock_response.json.return_value = {
575
+ "name": "test-repo",
576
+ "full_name": "user/test-repo"
577
+ }
578
+ mock_get.return_value = mock_response
579
+
580
+ result = github._get_repository_info("user", "test-repo")
581
+
582
+ self.assertEqual(result["status"], "success")
583
+ self.assertIn("data", result)
584
+ self.assertEqual(result["data"]["name"], "test-repo")
585
+
586
+ @patch('requests.get')
587
+ def test_get_repository_info_not_found(self, mock_get):
588
+ """Test _get_repository_info with 404 response."""
589
+ mock_response = MagicMock()
590
+ mock_response.status_code = 404
591
+ mock_get.return_value = mock_response
592
+
593
+ result = github._get_repository_info("user", "nonexistent")
594
+
595
+ self.assertEqual(result["status"], "error")
596
+ self.assertIn("not found", result["message"])
597
+
598
+ @patch('requests.get')
599
+ def test_get_repository_languages_success(self, mock_get):
600
+ """Test _get_repository_languages with successful response."""
601
+ mock_response = MagicMock()
602
+ mock_response.status_code = 200
603
+ mock_response.json.return_value = {
604
+ "Python": 50000,
605
+ "JavaScript": 25000,
606
+ "CSS": 25000
607
+ }
608
+ mock_get.return_value = mock_response
609
+
610
+ result = github._get_repository_languages("user", "repo")
611
+
612
+ self.assertEqual(result["status"], "success")
613
+ expected_percentages = {"Python": 50.0, "JavaScript": 25.0, "CSS": 25.0}
614
+ self.assertEqual(result["data"], expected_percentages)
615
+
616
+ @patch('requests.get')
617
+ def test_get_repository_readme_success(self, mock_get):
618
+ """Test _get_repository_readme with successful response."""
619
+ # Mock the README metadata response
620
+ readme_response = MagicMock()
621
+ readme_response.status_code = 200
622
+ readme_response.json.return_value = {
623
+ "download_url": "https://raw.githubusercontent.com/user/repo/main/README.md"
624
+ }
625
+
626
+ # Mock the README content response
627
+ content_response = MagicMock()
628
+ content_response.status_code = 200
629
+ content_response.text = "# Test Repository\n\nThis is a test."
630
+
631
+ mock_get.side_effect = [readme_response, content_response]
632
+
633
+ result = github._get_repository_readme("user", "repo")
634
+
635
+ self.assertEqual(result["status"], "success")
636
+ self.assertIn("Test Repository", result["data"])
637
+
638
+ @patch('requests.get')
639
+ def test_get_repository_contents_success(self, mock_get):
640
+ """Test _get_repository_contents with successful response."""
641
+ mock_response = MagicMock()
642
+ mock_response.status_code = 200
643
+ mock_response.json.return_value = [
644
+ {"name": "src", "type": "dir"},
645
+ {"name": "README.md", "type": "file"},
646
+ {"name": "setup.py", "type": "file"},
647
+ {"name": "tests", "type": "dir"}
648
+ ]
649
+ mock_get.return_value = mock_response
650
+
651
+ result = github._get_repository_contents("user", "repo")
652
+
653
+ self.assertEqual(result["status"], "success")
654
+ expected_structure = ["src/", "tests/", "README.md", "setup.py"]
655
+ self.assertEqual(result["data"], expected_structure)
656
+
657
+
658
  if __name__ == '__main__':
659
  unittest.main()