Vscode / app /config.py
Erinaldorodrigues's picture
Release Safe Bet AI v2.2 Precision
888ef7f
Raw
History Blame Contribute Delete
4.12 kB
from __future__ import annotations
from dataclasses import dataclass, field
import os
from pathlib import Path
from app.core.competitions import COMPETITIONS
DEFAULT_SPORT_KEYS = tuple(COMPETITIONS.keys())
MODEL_VERSION = "2.2-precision"
VALID_ODDS_REGIONS = frozenset({"us", "us2", "uk", "au", "eu"})
def _default_data_dir() -> Path:
"""Prefer the persistent volume mounted by Hugging Face Spaces."""
configured = os.getenv("DATA_DIR", "").strip()
if configured:
return Path(configured).expanduser()
persistent_root = Path("/data")
if persistent_root.is_dir():
return persistent_root / "safe-bet-ai"
return Path("/app/data") if Path("/app").is_dir() else Path("./data")
def _int(name: str, default: int, minimum: int, maximum: int) -> int:
try:
value = int(os.getenv(name, str(default)))
except ValueError:
value = default
return max(minimum, min(maximum, value))
def _float(name: str, default: float, minimum: float, maximum: float) -> float:
try:
value = float(os.getenv(name, str(default)))
except ValueError:
value = default
return max(minimum, min(maximum, value))
def _odds_regions() -> str:
raw = os.getenv("ODDS_REGIONS")
if raw is None:
return "eu"
unique = list(dict.fromkeys(
part.strip().lower() for part in raw.split(",") if part.strip()
))
if not unique or any(region not in VALID_ODDS_REGIONS for region in unique):
return ""
return ",".join(unique)
@dataclass(frozen=True)
class Settings:
football_data_token: str = field(default_factory=lambda: os.getenv("FOOTBALL_DATA_TOKEN", "").strip())
odds_api_key: str = field(default_factory=lambda: os.getenv("ODDS_API_KEY", "").strip())
cron_secret: str = field(default_factory=lambda: os.getenv("CRON_SECRET", "").strip())
odds_regions: str = field(default_factory=_odds_regions)
history_days: int = field(default_factory=lambda: _int("HISTORY_DAYS", 240, 120, 730))
scan_horizon_hours: int = field(default_factory=lambda: _int("SCAN_HORIZON_HOURS", 36, 6, 96))
min_scan_interval_minutes: int = field(default_factory=lambda: _int("MIN_SCAN_INTERVAL_MINUTES", 180, 15, 1440))
min_safe_score: float = field(default_factory=lambda: _float("MIN_SAFE_SCORE", 76.0, 55.0, 95.0))
min_probability: float = field(default_factory=lambda: _float("MIN_PROBABILITY", 0.64, 0.55, 0.90))
min_conservative_probability: float = field(default_factory=lambda: _float("MIN_CONSERVATIVE_PROBABILITY", 0.57, 0.50, 0.85))
min_bookmakers: int = field(default_factory=lambda: _int("MIN_BOOKMAKERS", 3, 1, 10))
min_name_score: float = field(default_factory=lambda: _float("MIN_NAME_SCORE", 82.0, 70.0, 98.0))
top_picks_limit: int = field(default_factory=lambda: _int("TOP_PICKS_LIMIT", 10, 1, 20))
tz_display: str = field(default_factory=lambda: os.getenv("TZ_DISPLAY", "America/Sao_Paulo").strip() or "America/Sao_Paulo")
hf_token: str = field(default_factory=lambda: (os.getenv("HF_WRITE_TOKEN", "").strip() or os.getenv("HF_TOKEN", "").strip()))
hf_dataset_repo: str = field(default_factory=lambda: os.getenv("HF_DATASET_REPO", "").strip())
data_dir: Path = field(default_factory=_default_data_dir)
@property
def sport_keys(self) -> tuple[str, ...]:
raw = os.getenv("ODDS_SPORT_KEYS", "").strip()
if not raw:
return DEFAULT_SPORT_KEYS
keys = tuple(dict.fromkeys(x.strip() for x in raw.split(",") if x.strip()))
# Fail closed when any requested league cannot be joined safely to
# football-data.org history; silently falling back would waste quota.
if not keys or any(key not in COMPETITIONS for key in keys):
return ()
return keys
@property
def required_ready(self) -> bool:
return bool(
self.football_data_token
and self.odds_api_key
and self.cron_secret
and self.odds_regions
and self.sport_keys
)
settings = Settings()
settings.data_dir.mkdir(parents=True, exist_ok=True)