fix: resolve curl_cffi TLS errors and fix FRED/upgrades endpoints
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- Pin curl_cffi==0.7.4 to avoid BoringSSL bug in 0.12-0.14 - Patch curl_cffi Session to use safari TLS fingerprint instead of chrome, which triggers SSL_ERROR_SYSCALL on some networks - Register FRED API key with OpenBB credentials at startup - Fix macro overview to return latest data instead of oldest, and extract values by FRED series ID key - Replace Finnhub upgrades endpoint (premium-only) with yfinance upgrades_downgrades which includes price target changes - Remove redundant curl_cffi upgrade from Dockerfile
This commit is contained in:
@@ -12,7 +12,6 @@ COPY pyproject.toml ./
|
||||
|
||||
RUN pip install --no-cache-dir . && \
|
||||
pip install --no-cache-dir openbb-quantitative openbb-econometrics openbb-technical && \
|
||||
pip install --no-cache-dir --upgrade curl_cffi && \
|
||||
apt-get purge -y gcc g++ libssl-dev && \
|
||||
apt-get autoremove -y
|
||||
|
||||
|
||||
@@ -36,19 +36,31 @@ def _to_dicts(result: Any) -> list[dict[str, Any]]:
|
||||
return [vars(result.results)]
|
||||
|
||||
|
||||
async def get_series(series_id: str, limit: int = 10) -> list[dict[str, Any]]:
|
||||
async def get_series(
|
||||
series_id: str, limit: int = 10, latest: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get a FRED time series by ID."""
|
||||
try:
|
||||
fetch_limit = limit if not latest else None
|
||||
kwargs: dict[str, Any] = {
|
||||
"symbol": series_id,
|
||||
"provider": PROVIDER,
|
||||
}
|
||||
if fetch_limit is not None:
|
||||
kwargs["limit"] = fetch_limit
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.fred_series,
|
||||
symbol=series_id,
|
||||
limit=limit,
|
||||
provider=PROVIDER,
|
||||
**kwargs,
|
||||
)
|
||||
items = _to_dicts(result)
|
||||
for item in items:
|
||||
if "date" in item and not isinstance(item["date"], str):
|
||||
item = {**item, "date": str(item["date"])}
|
||||
items = [
|
||||
{**item, "date": str(item["date"])}
|
||||
if "date" in item and not isinstance(item["date"], str)
|
||||
else item
|
||||
for item in items
|
||||
]
|
||||
if latest:
|
||||
items = items[-limit:]
|
||||
return items
|
||||
except Exception:
|
||||
logger.warning("Failed to fetch FRED series %s", series_id, exc_info=True)
|
||||
@@ -58,20 +70,22 @@ async def get_series(series_id: str, limit: int = 10) -> list[dict[str, Any]]:
|
||||
async def get_macro_overview() -> dict[str, Any]:
|
||||
"""Get a summary of key macro indicators."""
|
||||
tasks = {
|
||||
name: get_series(series_id, limit=1)
|
||||
name: get_series(series_id, limit=1, latest=True)
|
||||
for name, series_id in SERIES.items()
|
||||
}
|
||||
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
||||
|
||||
overview: dict[str, Any] = {}
|
||||
for name, result in zip(tasks.keys(), results):
|
||||
for (name, series_id), result in zip(SERIES.items(), results):
|
||||
if isinstance(result, BaseException):
|
||||
logger.warning("Failed to fetch %s: %s", name, result)
|
||||
overview[name] = None
|
||||
elif result and len(result) > 0:
|
||||
entry = result[0]
|
||||
entry = result[-1]
|
||||
# FRED returns values keyed by series ID, not "value"
|
||||
value = entry.get(series_id) or entry.get("value")
|
||||
overview[name] = {
|
||||
"value": entry.get("value"),
|
||||
"value": value,
|
||||
"date": str(entry.get("date", "")),
|
||||
}
|
||||
else:
|
||||
|
||||
20
main.py
20
main.py
@@ -4,7 +4,27 @@ import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
# Patch curl_cffi to use safari TLS fingerprint instead of chrome.
|
||||
# curl_cffi's chrome impersonation triggers BoringSSL SSL_ERROR_SYSCALL on
|
||||
# some networks; safari works reliably. This must happen before any import
|
||||
# that creates a curl_cffi Session (yfinance, openbb).
|
||||
import curl_cffi.requests as _cffi_requests
|
||||
|
||||
_orig_session_init = _cffi_requests.Session.__init__
|
||||
|
||||
def _patched_session_init(self, *args, **kwargs):
|
||||
if kwargs.get("impersonate") == "chrome":
|
||||
kwargs["impersonate"] = "safari"
|
||||
_orig_session_init(self, *args, **kwargs)
|
||||
|
||||
_cffi_requests.Session.__init__ = _patched_session_init
|
||||
|
||||
from openbb import obb
|
||||
from config import settings
|
||||
|
||||
# Register optional provider credentials with OpenBB
|
||||
if settings.fred_api_key:
|
||||
obb.user.credentials.fred_api_key = settings.fred_api_key
|
||||
from routes import router
|
||||
from routes_sentiment import router as sentiment_router
|
||||
from routes_macro import router as macro_router
|
||||
|
||||
@@ -171,3 +171,30 @@ async def get_growth() -> list[dict]:
|
||||
obb.equity.discovery.growth_tech, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
|
||||
|
||||
async def get_upgrades_downgrades(symbol: str, limit: int = 20) -> list[dict]:
|
||||
"""Get analyst upgrades/downgrades via yfinance."""
|
||||
import yfinance as yf
|
||||
|
||||
def _fetch() -> list[dict[str, Any]]:
|
||||
t = yf.Ticker(symbol)
|
||||
df = t.upgrades_downgrades
|
||||
if df is None or df.empty:
|
||||
return []
|
||||
df = df.head(limit).reset_index()
|
||||
return [
|
||||
{
|
||||
"date": str(row.get("GradeDate", "")),
|
||||
"company": row.get("Firm"),
|
||||
"action": row.get("Action"),
|
||||
"from_grade": row.get("FromGrade"),
|
||||
"to_grade": row.get("ToGrade"),
|
||||
"price_target_action": row.get("priceTargetAction"),
|
||||
"current_price_target": row.get("currentPriceTarget"),
|
||||
"prior_price_target": row.get("priorPriceTarget"),
|
||||
}
|
||||
for _, row in df.iterrows()
|
||||
]
|
||||
|
||||
return await asyncio.to_thread(_fetch)
|
||||
|
||||
@@ -9,6 +9,7 @@ dependencies = [
|
||||
"openbb[yfinance]",
|
||||
"pydantic-settings",
|
||||
"httpx",
|
||||
"curl_cffi==0.7.4",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -8,6 +8,7 @@ from models import ApiResponse
|
||||
from route_utils import safe, validate_symbol
|
||||
import alphavantage_service
|
||||
import finnhub_service
|
||||
import openbb_service
|
||||
|
||||
import logging
|
||||
|
||||
@@ -96,17 +97,7 @@ async def stock_recommendations(symbol: str = Path(..., min_length=1, max_length
|
||||
@router.get("/stock/{symbol}/upgrades", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_upgrades(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get recent analyst upgrades and downgrades."""
|
||||
"""Get recent analyst upgrades and downgrades (via yfinance)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
raw = await finnhub_service.get_upgrade_downgrade(symbol)
|
||||
upgrades = [
|
||||
{
|
||||
"company": u.get("company"),
|
||||
"action": u.get("action"),
|
||||
"from_grade": u.get("fromGrade"),
|
||||
"to_grade": u.get("toGrade"),
|
||||
"date": u.get("gradeTime"),
|
||||
}
|
||||
for u in raw[:20]
|
||||
]
|
||||
return ApiResponse(data=upgrades)
|
||||
data = await openbb_service.get_upgrades_downgrades(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
Reference in New Issue
Block a user