Spaces:
Running
Running
File size: 5,492 Bytes
ed8fc9e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | """Adapter between the Gradio app and the external stock-analysis project.
The external project is not hard-coded. Set ``EXTERNAL_API_URL`` to its HTTP
endpoint when it is ready. The app sends a small JSON request, keeps the
upstream body, and passes JSON through unchanged. Non-JSON bodies are wrapped
so the Gradio JSON output always has a stable shape.
This module is the only place to change when the external request or response
contract changes.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Any
import requests
DEFAULT_TIMEOUT_SECONDS = 120.0
_settings: dict[str, Any] = {
"url": "",
"timeout": DEFAULT_TIMEOUT_SECONDS,
"model_id": "",
}
@dataclass(frozen=True)
class ExternalResult:
"""Normalized result returned by :func:`query_external`."""
payload: Any
raw: str
http_status: int | None = None
content_type: str | None = None
is_json: bool = False
error: str | None = None
def configure(
*,
url: str | None = None,
timeout: float | None = None,
model_id: str | None = None,
) -> dict[str, Any]:
"""Apply settings from ``app.py``. All values are optional."""
if url is not None:
_settings["url"] = str(url).strip()
if timeout is not None:
_settings["timeout"] = float(timeout)
if model_id is not None:
_settings["model_id"] = str(model_id).strip()
return settings()
def settings() -> dict[str, Any]:
"""Return a copy of the current settings, without any credentials."""
return {
"configured": is_configured(),
"timeout": float(_settings["timeout"]),
"model_id": _resolved_model_id(),
}
def is_configured() -> bool:
return bool(_resolved_url())
def model_id() -> str:
return _resolved_model_id()
def _resolved_url() -> str:
return str(_settings["url"] or os.getenv("EXTERNAL_API_URL", "")).strip()
def _resolved_model_id() -> str:
return str(_settings["model_id"] or os.getenv("STOCK_MODEL_ID", "")).strip()
def _token() -> str | None:
return os.getenv("HF_TOKEN") or None
def _redact(text: str, token: str | None) -> str:
if token:
text = text.replace(token, "hf_***")
return text
def _headers(token: str | None) -> dict[str, str]:
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "StockAnalysis-Space/1.0",
}
if token:
headers["Authorization"] = f"Bearer {token}"
return headers
def _not_configured(ticker: str) -> ExternalResult:
message = "Set EXTERNAL_API_URL to the stock-analysis project endpoint."
return ExternalResult(
payload={
"status": "not_configured",
"ticker": ticker,
"message": message,
},
raw="",
error=message,
)
def query_external(
ticker: str,
horizon: str,
risk_profile: str,
question: str,
) -> ExternalResult:
"""Send one analysis request to the external project.
The request body is intentionally small and easy to map onto another
service. If ``STOCK_MODEL_ID`` is set, it is included as ``model`` so the
external project can select the uploaded custom model.
"""
url = _resolved_url()
if not url:
return _not_configured(ticker)
token = _token()
payload: dict[str, Any] = {
"ticker": ticker,
"horizon": horizon,
"risk_profile": risk_profile,
"question": question,
}
resolved_model = _resolved_model_id()
if resolved_model:
payload["model"] = resolved_model
try:
response = requests.post(
url,
json=payload,
headers=_headers(token),
timeout=float(_settings["timeout"]),
)
except requests.RequestException as exc:
message = _redact(f"{type(exc).__name__}: {exc}", token)
return ExternalResult(
payload={
"status": "error",
"ticker": ticker,
"error": message,
},
raw="",
error=message,
)
raw = response.text or ""
content_type = response.headers.get("content-type", "").split(";")[0].strip() or None
try:
parsed = response.json()
except ValueError:
parsed = None
if not response.ok:
message = _redact(f"HTTP {response.status_code}: {raw[:500]}", token)
return ExternalResult(
payload={
"status": "error",
"ticker": ticker,
"http_status": response.status_code,
"content_type": content_type,
"error": message,
"raw": raw,
},
raw=raw,
http_status=response.status_code,
content_type=content_type,
error=message,
)
if parsed is not None:
return ExternalResult(
payload=parsed,
raw=raw,
http_status=response.status_code,
content_type=content_type,
is_json=True,
)
return ExternalResult(
payload={
"status": "ok",
"ticker": ticker,
"http_status": response.status_code,
"content_type": content_type,
"raw": raw,
},
raw=raw,
http_status=response.status_code,
content_type=content_type,
is_json=False,
)
|