Files
library-desk/src/apis/alphavantage.py
T
jpmschweitzerandClaude Opus 4.5 5d4a8dba95 feat: add external API providers and central settings database
- Add central settings database client (system_settings on postgres-shared)
  - User-scoped settings with global fallback
  - API config storage with enabled/disabled toggle
  - Per-source category filtering for news

- Add modular external API providers in src/apis/:
  - OpenMeteoProvider: weather with geocoding (free, no key)
  - NOSProvider: Dutch news RSS feeds
  - BBCProvider: English news RSS feeds
  - AggregatedNewsProvider: merges sources with category filtering
  - AlphaVantageProvider: financial quotes (API key from settings DB)

- Add provider dependencies and lifecycle management
- Add requirements-dev.txt with pip-audit for security auditing
- Add MEMORY_REMEMBER_PLAN.md documenting volatile/document memory architecture

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 12:07:58 +01:00

228 lines
7.2 KiB
Python

"""
Alpha Vantage financial API client.
Stock and cryptocurrency quotes.
https://www.alphavantage.co/documentation/
Requires API key (free tier available).
"""
import httpx
import logging
from datetime import datetime
from typing import Optional
from .base import FinancialProvider, StockQuote
logger = logging.getLogger(__name__)
class AlphaVantageProvider(FinancialProvider):
"""Alpha Vantage financial API implementation."""
BASE_URL = "https://www.alphavantage.co/query"
def __init__(self, api_key: str, timeout: int = 10):
"""
Initialize Alpha Vantage client.
Args:
api_key: Alpha Vantage API key
timeout: HTTP request timeout in seconds
"""
self.api_key = api_key
self.timeout = timeout
self._client: Optional[httpx.AsyncClient] = None
@property
def client(self) -> httpx.AsyncClient:
"""Lazy-initialize HTTP client."""
if self._client is None or self._client.is_closed:
self._client = httpx.AsyncClient(timeout=self.timeout)
return self._client
async def close(self):
"""Close HTTP client."""
if self._client and not self._client.is_closed:
await self._client.aclose()
self._client = None
async def get_quote(self, symbol: str) -> Optional[StockQuote]:
"""
Get current quote for a stock symbol.
Args:
symbol: Stock ticker symbol (e.g., "AAPL", "MSFT")
Returns:
StockQuote with current price info or None if not found
"""
try:
response = await self.client.get(
self.BASE_URL,
params={
"function": "GLOBAL_QUOTE",
"symbol": symbol.upper(),
"apikey": self.api_key
}
)
response.raise_for_status()
data = response.json()
# Check for API errors
if "Error Message" in data:
logger.warning(f"Alpha Vantage error for {symbol}: {data['Error Message']}")
return None
if "Note" in data:
# Rate limit warning
logger.warning(f"Alpha Vantage rate limit: {data['Note']}")
return None
quote = data.get("Global Quote", {})
if not quote:
logger.warning(f"No quote data for symbol: {symbol}")
return None
# Parse quote data
price = float(quote.get("05. price", 0))
change = float(quote.get("09. change", 0))
change_percent_str = quote.get("10. change percent", "0%")
change_percent = float(change_percent_str.rstrip('%'))
return StockQuote(
symbol=symbol.upper(),
name=None, # Global Quote doesn't include company name
price=price,
currency="USD", # Alpha Vantage returns USD for US stocks
change=change,
change_percent=change_percent,
timestamp=datetime.now()
)
except httpx.HTTPError as e:
logger.error(f"Alpha Vantage request failed for {symbol}: {e}")
return None
except (KeyError, ValueError) as e:
logger.error(f"Failed to parse Alpha Vantage response for {symbol}: {e}")
return None
async def get_quotes(self, symbols: list[str]) -> list[StockQuote]:
"""
Get quotes for multiple stock symbols.
Note: Alpha Vantage free tier has rate limits (5 calls/min, 500 calls/day).
Consider using batch endpoints or caching for production use.
Args:
symbols: List of stock ticker symbols
Returns:
List of StockQuote objects (may be less than input if some fail)
"""
quotes = []
for symbol in symbols:
quote = await self.get_quote(symbol)
if quote:
quotes.append(quote)
return quotes
async def get_crypto_quote(
self,
symbol: str,
market: str = "USD"
) -> Optional[StockQuote]:
"""
Get current quote for a cryptocurrency.
Args:
symbol: Crypto symbol (e.g., "BTC", "ETH")
market: Market currency (default: USD)
Returns:
StockQuote with current price info or None if not found
"""
try:
response = await self.client.get(
self.BASE_URL,
params={
"function": "CURRENCY_EXCHANGE_RATE",
"from_currency": symbol.upper(),
"to_currency": market.upper(),
"apikey": self.api_key
}
)
response.raise_for_status()
data = response.json()
# Check for API errors
if "Error Message" in data:
logger.warning(f"Alpha Vantage error for {symbol}: {data['Error Message']}")
return None
if "Note" in data:
logger.warning(f"Alpha Vantage rate limit: {data['Note']}")
return None
rate_data = data.get("Realtime Currency Exchange Rate", {})
if not rate_data:
logger.warning(f"No exchange rate data for: {symbol}/{market}")
return None
price = float(rate_data.get("5. Exchange Rate", 0))
return StockQuote(
symbol=f"{symbol.upper()}/{market.upper()}",
name=rate_data.get("2. From_Currency Name"),
price=price,
currency=market.upper(),
change=None, # Exchange rate endpoint doesn't provide change
change_percent=None,
timestamp=datetime.now()
)
except httpx.HTTPError as e:
logger.error(f"Alpha Vantage crypto request failed for {symbol}: {e}")
return None
except (KeyError, ValueError) as e:
logger.error(f"Failed to parse Alpha Vantage crypto response for {symbol}: {e}")
return None
async def search_symbol(self, keywords: str) -> list[dict]:
"""
Search for stock symbols by keywords.
Args:
keywords: Search keywords (company name or partial symbol)
Returns:
List of matching symbols with metadata
"""
try:
response = await self.client.get(
self.BASE_URL,
params={
"function": "SYMBOL_SEARCH",
"keywords": keywords,
"apikey": self.api_key
}
)
response.raise_for_status()
data = response.json()
matches = data.get("bestMatches", [])
return [
{
"symbol": m.get("1. symbol"),
"name": m.get("2. name"),
"type": m.get("3. type"),
"region": m.get("4. region"),
"currency": m.get("8. currency"),
}
for m in matches
]
except httpx.HTTPError as e:
logger.error(f"Alpha Vantage search failed for '{keywords}': {e}")
return []