Spaces:
Configuration error
Configuration error
Added function to get details for github repo from its url
Browse files- functions/github.py +383 -0
- 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()
|