| 14 | |
| 15 | |
| 16 | class GitHubService: |
| 17 | def __init__(self, pat: str | None = None): |
| 18 | # Try app authentication first |
| 19 | self.client_id = os.getenv("GITHUB_CLIENT_ID") |
| 20 | self.private_key = os.getenv("GITHUB_PRIVATE_KEY") |
| 21 | self.installation_id = os.getenv("GITHUB_INSTALLATION_ID") |
| 22 | |
| 23 | # Use provided PAT if available, otherwise fallback to env PAT |
| 24 | self.github_token = pat or os.getenv("GITHUB_PAT") |
| 25 | |
| 26 | # If no credentials are provided, warn about rate limits |
| 27 | if ( |
| 28 | not all([self.client_id, self.private_key, self.installation_id]) |
| 29 | and not self.github_token |
| 30 | ): |
| 31 | logger.warning("No GitHub credentials provided. Using unauthenticated requests with rate limit of 60 requests/hour.") |
| 32 | |
| 33 | self.access_token = None |
| 34 | self.token_expires_at = None |
| 35 | |
| 36 | # autopep8: off |
| 37 | def _generate_jwt(self): |
| 38 | now = int(time.time()) |
| 39 | payload = { |
| 40 | "iat": now, |
| 41 | "exp": now + (10 * 60), # 10 minutes |
| 42 | "iss": self.client_id, |
| 43 | } |
| 44 | # Convert PEM string format to proper newlines |
| 45 | return jwt.encode(payload, self.private_key, algorithm="RS256") # type: ignore |
| 46 | |
| 47 | # autopep8: on |
| 48 | |
| 49 | def _get_installation_token(self): |
| 50 | if self.access_token and self.token_expires_at > datetime.now(): # type: ignore |
| 51 | return self.access_token |
| 52 | |
| 53 | jwt_token = self._generate_jwt() |
| 54 | response = requests.post( |
| 55 | f"https://api.github.com/app/installations/{ |
| 56 | self.installation_id}/access_tokens", |
| 57 | headers={ |
| 58 | "Authorization": f"Bearer {jwt_token}", |
| 59 | "Accept": "application/vnd.github+json", |
| 60 | }, |
| 61 | ) |
| 62 | data = response.json() |
| 63 | self.access_token = data["token"] |
| 64 | self.token_expires_at = datetime.now() + timedelta(hours=1) |
| 65 | return self.access_token |
| 66 | |
| 67 | def _get_headers(self): |
| 68 | # If no credentials are available, return basic headers |
| 69 | if ( |
| 70 | not all([self.client_id, self.private_key, self.installation_id]) |
| 71 | and not self.github_token |
| 72 | ): |
| 73 | return {"Accept": "application/vnd.github+json"} |
no outgoing calls
no test coverage detected