|
1 | 1 | """Async GitHub API client for workflow operations.""" |
2 | 2 |
|
| 3 | +import base64 |
3 | 4 | import io |
4 | 5 | import json |
5 | 6 | import logging |
@@ -603,6 +604,69 @@ def _extract_uuid(self, text: str) -> Optional[str]: |
603 | 604 | uuid_match = re.search(uuid_pattern, text, re.IGNORECASE) |
604 | 605 | return uuid_match.group() if uuid_match else None |
605 | 606 |
|
| 607 | + async def download_file( |
| 608 | + self, repo: str, file_path: str, ref: str = "main" |
| 609 | + ) -> Optional[str]: |
| 610 | + """Download a specific file from a repository at a specific branch/ref. |
| 611 | +
|
| 612 | + Args: |
| 613 | + repo: Repository name (e.g., "redis/redis") |
| 614 | + file_path: Path to the file in the repository (e.g., "config.yaml", "src/main.py") |
| 615 | + ref: Git reference (branch, tag, or commit SHA) to download from (default: "main") |
| 616 | +
|
| 617 | + Returns: |
| 618 | + File content as a string, or None if the file is not found or an error occurs |
| 619 | + """ |
| 620 | + url = f"https://api.github.com/repos/{repo}/contents/{file_path}" |
| 621 | + headers = { |
| 622 | + "Authorization": f"Bearer {self.token}", |
| 623 | + "Accept": "application/vnd.github.v3+json", |
| 624 | + "X-GitHub-Api-Version": "2022-11-28", |
| 625 | + } |
| 626 | + params = {"ref": ref} |
| 627 | + |
| 628 | + try: |
| 629 | + logger.debug( |
| 630 | + f"[blue]Downloading file[/blue] {file_path} from {repo} at ref {ref}" |
| 631 | + ) |
| 632 | + |
| 633 | + data = await self.github_request( |
| 634 | + url=url, |
| 635 | + headers=headers, |
| 636 | + method="GET", |
| 637 | + params=params, |
| 638 | + timeout=30, |
| 639 | + error_context=f"download file {file_path}", |
| 640 | + ) |
| 641 | + |
| 642 | + # GitHub API returns file content base64-encoded |
| 643 | + if "content" in data and data.get("encoding") == "base64": |
| 644 | + content = base64.b64decode(data["content"]).decode("utf-8") |
| 645 | + logger.debug( |
| 646 | + f"[green]Successfully downloaded {file_path}[/green] ({len(content)} bytes)" |
| 647 | + ) |
| 648 | + return content |
| 649 | + else: |
| 650 | + logger.error( |
| 651 | + f"[red]Unexpected response format for file {file_path}[/red]" |
| 652 | + ) |
| 653 | + return None |
| 654 | + |
| 655 | + except aiohttp.ClientResponseError as e: |
| 656 | + if e.status == 404: |
| 657 | + logger.warning( |
| 658 | + f"[yellow]File {file_path} not found in {repo} at ref {ref}[/yellow]" |
| 659 | + ) |
| 660 | + else: |
| 661 | + logger.error(f"[red]Failed to download file {file_path}: {e}[/red]") |
| 662 | + return None |
| 663 | + except aiohttp.ClientError as e: |
| 664 | + logger.error(f"[red]Failed to download file {file_path}: {e}[/red]") |
| 665 | + return None |
| 666 | + except Exception as e: |
| 667 | + logger.error(f"[red]Error downloading file {file_path}: {e}[/red]") |
| 668 | + return None |
| 669 | + |
606 | 670 | async def list_remote_branches( |
607 | 671 | self, repo: str, pattern: Optional[str] = None |
608 | 672 | ) -> List[str]: |
|
0 commit comments