|
8 | 8 | - Unauthenticated: Without GITHUB_PAT - public repos only, 60 requests/hour |
9 | 9 | """ |
10 | 10 |
|
| 11 | +import time |
11 | 12 | from dataclasses import dataclass |
12 | | -from datetime import datetime |
| 13 | +from datetime import datetime, timezone |
13 | 14 | from typing import Generator, Optional, Union |
14 | 15 |
|
15 | | -from github import Github, GithubException |
| 16 | +from github import Github, GithubException, RateLimitExceededException |
16 | 17 | from github.Repository import Repository |
17 | 18 | from github.Organization import Organization |
18 | 19 | from github.AuthenticatedUser import AuthenticatedUser |
19 | 20 | from github.NamedUser import NamedUser |
| 21 | +from urllib3.util.retry import Retry |
20 | 22 |
|
21 | 23 | from config import Settings |
22 | 24 | from ui.console import backup_logger |
@@ -71,12 +73,15 @@ def __init__(self, settings: Settings): |
71 | 73 | self.settings = settings |
72 | 74 | self._authenticated = settings.is_authenticated |
73 | 75 |
|
| 76 | + # Retry on transient server errors (502, 503, 504) |
| 77 | + retry = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504]) |
| 78 | + |
74 | 79 | if self._authenticated: |
75 | 80 | # Use per_page=100 for better performance with large orgs |
76 | | - self.gh = Github(settings.github_pat, per_page=100) |
| 81 | + self.gh = Github(settings.github_pat, per_page=100, retry=retry) |
77 | 82 | backup_logger.debug("GitHub client initialized with authentication (5000 req/hour)") |
78 | 83 | else: |
79 | | - self.gh = Github(per_page=100) # Unauthenticated |
| 84 | + self.gh = Github(per_page=100, retry=retry) # Unauthenticated |
80 | 85 | backup_logger.debug( |
81 | 86 | "GitHub client initialized WITHOUT authentication. " |
82 | 87 | "Only public repositories accessible (60 req/hour rate limit). " |
@@ -110,6 +115,27 @@ def get_rate_limit_info(self) -> dict: |
110 | 115 | "reset": rate.core.reset.isoformat() if rate.core.reset else None, |
111 | 116 | } |
112 | 117 |
|
| 118 | + def wait_for_rate_limit(self, min_remaining: int = 100) -> None: |
| 119 | + """Check rate limit and wait if near exhaustion. |
| 120 | +
|
| 121 | + Args: |
| 122 | + min_remaining: Minimum remaining requests before waiting. |
| 123 | + """ |
| 124 | + try: |
| 125 | + rate = self.gh.get_rate_limit() |
| 126 | + remaining = rate.core.remaining |
| 127 | + if remaining < min_remaining: |
| 128 | + reset_time = rate.core.reset |
| 129 | + wait_seconds = (reset_time - datetime.now(timezone.utc)).total_seconds() + 5 |
| 130 | + if wait_seconds > 0: |
| 131 | + backup_logger.warning( |
| 132 | + f"Rate limit low ({remaining}/{rate.core.limit} remaining), " |
| 133 | + f"waiting {wait_seconds:.0f}s until reset" |
| 134 | + ) |
| 135 | + time.sleep(min(wait_seconds, 3600)) # Cap at 1 hour |
| 136 | + except GithubException: |
| 137 | + pass # Don't fail backup over rate limit check itself |
| 138 | + |
113 | 139 | def _resolve_owner(self) -> OwnerType: |
114 | 140 | """Resolve the owner name to an Organization or User object. |
115 | 141 |
|
|
0 commit comments