Files
DinQuant/backend_api_python/app/utils/config_loader.py
T

222 lines
7.4 KiB
Python
Raw Normal View History

2025-12-29 03:06:49 +08:00
"""
Config loader (local-only).
This project is fully localized: all sensitive configuration should come from
`backend_api_python/.env` (or OS environment variables).
We keep the return shape compatible with the old PHP `loadConfig`:
flat keys like `openrouter.api_key` become nested dicts like:
{
"openrouter": {"api_key": "..."}
}
"""
2025-12-29 03:06:49 +08:00
import os
from typing import Any, Dict, List, Optional, Tuple
2025-12-29 03:06:49 +08:00
from app.utils.logger import get_logger
logger = get_logger(__name__)
# Configure cache
2025-12-29 03:06:49 +08:00
_config_cache: Optional[Dict[str, Any]] = None
def load_addon_config() -> Dict[str, Any]:
"""
Build config from environment variables (.env / OS env).
NOTE: We intentionally do NOT load secrets from the database.
Returns:
Nested config dict (PHP-compatible shape)
"""
global _config_cache
# If the cache exists, return directly
2025-12-29 03:06:49 +08:00
if _config_cache is not None:
return _config_cache
2025-12-29 03:06:49 +08:00
config: Dict[str, Any] = {}
def set_nested(cfg: Dict[str, Any], dotted_key: str, value: Any) -> None:
keys = dotted_key.split(".")
2025-12-29 03:06:49 +08:00
ref = cfg
for i, k in enumerate(keys):
if i == len(keys) - 1:
ref[k] = value
else:
if k not in ref or not isinstance(ref[k], dict):
ref[k] = {}
ref = ref[k]
def env_get(name: str) -> Optional[str]:
val = os.getenv(name)
if val is None:
return None
val = str(val).strip()
return val if val != "" else None
2025-12-29 03:06:49 +08:00
# Map env vars to PHP-style dotted keys.
mappings: List[Tuple[str, str, str]] = [
# internal
("INTERNAL_API_KEY", "internal_api.key", "string"),
2025-12-29 03:06:49 +08:00
# OpenRouter / LLM
("OPENROUTER_API_KEY", "openrouter.api_key", "string"),
("OPENROUTER_API_URL", "openrouter.api_url", "string"),
("OPENROUTER_MODEL", "openrouter.model", "string"),
("OPENROUTER_TEMPERATURE", "openrouter.temperature", "float"),
("OPENROUTER_MAX_TOKENS", "openrouter.max_tokens", "int"),
("OPENROUTER_TIMEOUT", "openrouter.timeout", "int"),
("OPENROUTER_CONNECT_TIMEOUT", "openrouter.connect_timeout", "int"),
2026-01-24 03:22:14 +08:00
# OpenAI Direct
("OPENAI_API_KEY", "openai.api_key", "string"),
("OPENAI_BASE_URL", "openai.base_url", "string"),
("OPENAI_MODEL", "openai.model", "string"),
2026-01-24 03:22:14 +08:00
# Google Gemini
("GOOGLE_API_KEY", "google.api_key", "string"),
("GOOGLE_MODEL", "google.model", "string"),
2026-01-24 03:22:14 +08:00
# DeepSeek
("DEEPSEEK_API_KEY", "deepseek.api_key", "string"),
("DEEPSEEK_BASE_URL", "deepseek.base_url", "string"),
("DEEPSEEK_MODEL", "deepseek.model", "string"),
2026-01-24 03:22:14 +08:00
# xAI Grok
("GROK_API_KEY", "grok.api_key", "string"),
("GROK_BASE_URL", "grok.base_url", "string"),
("GROK_MODEL", "grok.model", "string"),
2026-01-24 03:22:14 +08:00
# LLM Provider Selection
("LLM_PROVIDER", "llm.provider", "string"),
2025-12-29 03:06:49 +08:00
# App
("RATE_LIMIT", "app.rate_limit", "int"),
("ENABLE_CACHE", "app.enable_cache", "bool"),
("ENABLE_REQUEST_LOG", "app.enable_request_log", "bool"),
2025-12-29 03:06:49 +08:00
# Data source common
("DATA_SOURCE_TIMEOUT", "data_source.timeout", "int"),
("DATA_SOURCE_RETRY", "data_source.retry_count", "int"),
("DATA_SOURCE_RETRY_BACKOFF", "data_source.retry_backoff", "float"),
2025-12-29 03:06:49 +08:00
# Finnhub
("FINNHUB_API_KEY", "finnhub.api_key", "string"),
("FINNHUB_TIMEOUT", "finnhub.timeout", "int"),
("FINNHUB_RATE_LIMIT", "finnhub.rate_limit", "int"),
2025-12-29 03:06:49 +08:00
# CCXT
("CCXT_DEFAULT_EXCHANGE", "ccxt.default_exchange", "string"),
("CCXT_TIMEOUT", "ccxt.timeout", "int"),
2025-12-29 03:06:49 +08:00
# Other sources
("YFINANCE_TIMEOUT", "yfinance.timeout", "int"),
("TIINGO_API_KEY", "tiingo.api_key", "string"),
("TIINGO_TIMEOUT", "tiingo.timeout", "int"),
2025-12-29 03:06:49 +08:00
# Search (Google CSE / Bing)
("SEARCH_PROVIDER", "search.provider", "string"),
("SEARCH_MAX_RESULTS", "search.max_results", "int"),
("SEARCH_GOOGLE_API_KEY", "search.google.api_key", "string"),
("SEARCH_GOOGLE_CX", "search.google.cx", "string"),
("SEARCH_BING_API_KEY", "search.bing.api_key", "string"),
2026-02-05 00:25:38 +08:00
# Tavily (AI-optimized search)
("TAVILY_API_KEYS", "tavily.api_keys", "string"),
2026-02-05 00:25:38 +08:00
# SerpAPI (Google/Bing scraper)
("SERPAPI_KEYS", "serpapi.api_keys", "string"),
2025-12-29 03:06:49 +08:00
]
for env_name, dotted_key, value_type in mappings:
raw = env_get(env_name)
if raw is None:
continue
try:
value = _convert_config_value(raw, value_type)
set_nested(config, dotted_key, value)
except Exception as e:
logger.warning(f"Config env parse failed: {env_name} -> {dotted_key}: {e}")
_config_cache = config
return config
def _convert_config_value(value: str, value_type: str) -> Any:
"""
Convert configuration values according to type (consistent with the convertConfigValue method on the PHP side)
2025-12-29 03:06:49 +08:00
Args:
value: configuration value string (may be None)
value_type: configuration type
2025-12-29 03:06:49 +08:00
Returns:
Converted configuration value
2025-12-29 03:06:49 +08:00
"""
# Handling None or null values
if value is None or value == "":
if value_type == "int":
2025-12-29 03:06:49 +08:00
return 0
elif value_type == "float":
2025-12-29 03:06:49 +08:00
return 0.0
elif value_type == "bool":
2025-12-29 03:06:49 +08:00
return False
elif value_type == "json":
2025-12-29 03:06:49 +08:00
return {}
else:
return ""
2025-12-29 03:06:49 +08:00
try:
if value_type == "int":
2025-12-29 03:06:49 +08:00
return int(value)
elif value_type == "float":
2025-12-29 03:06:49 +08:00
return float(value)
elif value_type == "bool":
return bool(value) or value == "1" or value == "true" or value == "True"
elif value_type == "json":
2025-12-29 03:06:49 +08:00
import json
2025-12-29 03:06:49 +08:00
try:
return json.loads(value) if value else {}
except (json.JSONDecodeError, TypeError):
return {}
else:
return str(value) if value is not None else ""
2025-12-29 03:06:49 +08:00
except (ValueError, TypeError) as e:
logger.warning(f"Config value type conversion failed: value={value}, type={value_type}, error={str(e)}")
# Returns default value if conversion fails
if value_type == "int":
2025-12-29 03:06:49 +08:00
return 0
elif value_type == "float":
2025-12-29 03:06:49 +08:00
return 0.0
elif value_type == "bool":
2025-12-29 03:06:49 +08:00
return False
elif value_type == "json":
2025-12-29 03:06:49 +08:00
return {}
else:
return str(value) if value is not None else ""
2025-12-29 03:06:49 +08:00
def get_internal_api_key() -> Optional[str]:
"""
Get the internal API key (preferably read from environment variables)
2025-12-29 03:06:49 +08:00
Returns:
Internal API key, returns None if not configured
2025-12-29 03:06:49 +08:00
"""
try:
env_val = os.getenv("INTERNAL_API_KEY", "").strip()
2025-12-29 03:06:49 +08:00
if env_val:
return env_val
config = load_addon_config()
api_key = config.get("internal_api", {}).get("key")
2025-12-29 03:06:49 +08:00
if api_key:
logger.debug(f"Loaded INTERNAL_API_KEY from env-config shape, length: {len(api_key)}")
else:
logger.warning("Missing INTERNAL_API_KEY (env).")
2025-12-29 03:06:49 +08:00
return api_key
except Exception as e:
logger.error(f"Failed to load internal API key: {str(e)}")
return None
def clear_config_cache():
"""
Clear configuration cache (called after configuration update)
2025-12-29 03:06:49 +08:00
"""
global _config_cache
_config_cache = None
logger.debug("Addon config cache cleared")