| import logging | |
| import httpx | |
| from ingestion.alphavantage import AV_BASE, _next_key, _key_count, _is_rate_limited | |
| logger = logging.getLogger(__name__) | |
| def _parse_transcript(data: dict) -> str: | |
| raw = data.get("transcript", "") | |
| if isinstance(raw, list): | |
| lines = [] | |
| for item in raw: | |
| speaker = item.get("speaker", "") | |
| title = item.get("title", "") | |
| label = f"{speaker} ({title})" if title else speaker | |
| lines.append(f"{label}: {item.get('content', '')}") | |
| return "\n".join(lines) | |
| if raw is None: | |
| return "" | |
| return str(raw) | |
| def fetch_transcript(ticker: str, quarter: str) -> str: | |
| """Fetch earnings call transcript from Alpha Vantage. | |
| Args: | |
| ticker: Stock symbol e.g. "AAPL" | |
| quarter: Alpha Vantage format e.g. "2024Q4" | |
| Returns: | |
| Transcript text, or empty string if unavailable. | |
| """ | |
| rate_limited_keys = 0 | |
| n_keys = max(_key_count(), 1) | |
| for attempt in range(n_keys): | |
| key = _next_key() | |
| key_label = f"key[...{key[-4:]}]" | |
| try: | |
| resp = httpx.get( | |
| AV_BASE, | |
| params={ | |
| "function": "EARNINGS_CALL_TRANSCRIPT", | |
| "symbol": ticker.upper(), | |
| "quarter": quarter, | |
| "apikey": key, | |
| }, | |
| timeout=30, | |
| ) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| if "Information" in data: | |
| rate_limited_keys += 1 | |
| logger.warning( | |
| "[transcript] %s %s %s β rate limit hit (attempt %d/%d): %s", | |
| ticker, quarter, key_label, attempt + 1, n_keys, data["Information"], | |
| ) | |
| continue | |
| if "Note" in data: | |
| rate_limited_keys += 1 | |
| logger.warning( | |
| "[transcript] %s %s %s β API note/rate limit (attempt %d/%d): %s", | |
| ticker, quarter, key_label, attempt + 1, n_keys, data["Note"], | |
| ) | |
| continue | |
| if "Error Message" in data: | |
| logger.warning( | |
| "[transcript] %s %s %s β API error: %s", | |
| ticker, quarter, key_label, data["Error Message"], | |
| ) | |
| return "" | |
| text = _parse_transcript(data) | |
| if text: | |
| logger.info( | |
| "[transcript] %s %s β fetched %d chars via %s", | |
| ticker, quarter, len(text), key_label, | |
| ) | |
| else: | |
| logger.info( | |
| "[transcript] %s %s β API returned empty transcript (no content available) via %s", | |
| ticker, quarter, key_label, | |
| ) | |
| return text | |
| except ValueError: | |
| raise | |
| except Exception as e: | |
| logger.warning( | |
| "[transcript] %s %s %s β request error (attempt %d/%d): %s", | |
| ticker, quarter, key_label, attempt + 1, n_keys, e, | |
| ) | |
| continue | |
| if rate_limited_keys == n_keys: | |
| logger.error( | |
| "[transcript] %s %s β ALL %d keys exhausted by rate limits. No transcript fetched.", | |
| ticker, quarter, n_keys, | |
| ) | |
| else: | |
| logger.error( | |
| "[transcript] %s %s β failed after %d attempt(s). No transcript fetched.", | |
| ticker, quarter, n_keys, | |
| ) | |
| return "" | |