| 13 | |
| 14 | |
| 15 | class GitHubManager: |
| 16 | def __init__(self): |
| 17 | self._session: aiohttp.ClientSession | None = None |
| 18 | self._connector: aiohttp.TCPConnector | None = None |
| 19 | |
| 20 | @property |
| 21 | def repo(self) -> str: |
| 22 | """Get the configured repository.""" |
| 23 | return config.github_repo |
| 24 | |
| 25 | async def get_session(self) -> aiohttp.ClientSession: |
| 26 | """Get or create the aiohttp session with connection pooling.""" |
| 27 | if self._session is None or self._session.closed: |
| 28 | # Connection pooling for faster subsequent requests |
| 29 | self._connector = aiohttp.TCPConnector( |
| 30 | limit=50, # Max total connections |
| 31 | limit_per_host=30, # Max per host (GitHub) |
| 32 | keepalive_timeout=60, # Keep connections alive longer |
| 33 | enable_cleanup_closed=True, |
| 34 | ttl_dns_cache=300, # Cache DNS for 5 mins |
| 35 | use_dns_cache=True, |
| 36 | ) |
| 37 | self._session = aiohttp.ClientSession( |
| 38 | connector=self._connector, |
| 39 | timeout=aiohttp.ClientTimeout(total=60, connect=10), |
| 40 | ) |
| 41 | return self._session |
| 42 | |
| 43 | async def close(self): |
| 44 | """Close the aiohttp session.""" |
| 45 | if self._session and not self._session.closed: |
| 46 | await self._session.close() |
| 47 | self._session = None |
| 48 | if self._connector: |
| 49 | await self._connector.close() |
| 50 | self._connector = None |
| 51 | |
| 52 | async def _get_token(self) -> str | None: |
| 53 | """Get GitHub token (from App or PAT).""" |
| 54 | if github_auth.github_app_auth: |
| 55 | token = await github_auth.github_app_auth.get_token() |
| 56 | if token: |
| 57 | return token |
| 58 | # Fallback to PAT |
| 59 | return config.github_token if config.github_token else None |
| 60 | |
| 61 | def _has_auth(self) -> bool: |
| 62 | """Check if any auth method is available (sync check).""" |
| 63 | has_app = github_auth.github_app_auth is not None |
| 64 | has_pat = bool(config.github_token) |
| 65 | logger.debug(f"_has_auth check: app={has_app}, pat={has_pat}") |
| 66 | return has_app or has_pat |
| 67 | |
| 68 | async def _get_headers(self) -> dict | None: |
| 69 | """Get standard GitHub API headers.""" |
| 70 | token = await self._get_token() |
| 71 | if not token: |
| 72 | return None |