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()
|