""" Flare – ConfigProvider (şifreli cloud_token desteği) """ from __future__ import annotations import json, os from pathlib import Path from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field, HttpUrl, ValidationError from utils import log from encryption_utils import decrypt class GlobalConfig(BaseModel): work_mode: str = Field("hfcloud", pattern=r"^(hfcloud|cloud|on-premise)$") cloud_token: Optional[str] = None spark_endpoint: HttpUrl users: List["UserConfig"] = [] def get_plain_token(self) -> Optional[str]: return decrypt(self.cloud_token) if self.cloud_token else None # ---------------- Global ----------------- class UserConfig(BaseModel): username: str password_hash: str salt: str # ---------------- Retry / Proxy ---------- class RetryConfig(BaseModel): retry_count: int = Field(3, alias="max_attempts") backoff_seconds: int = 2 strategy: str = Field("static", pattern=r"^(static|exponential)$") class ProxyConfig(BaseModel): enabled: bool = True url: HttpUrl # ---------------- API & Auth ------------- class APIAuthConfig(BaseModel): enabled: bool = False token_endpoint: Optional[HttpUrl] = None response_token_path: str = "access_token" token_request_body: Dict[str, Any] = Field({}, alias="body_template") token_refresh_endpoint: Optional[HttpUrl] = None token_refresh_body: Dict[str, Any] = {} class Config: extra = "allow" class APIConfig(BaseModel): name: str url: HttpUrl method: str = Field("GET", pattern=r"^(GET|POST|PUT|PATCH|DELETE)$") headers: Dict[str, Any] = {} body_template: Dict[str, Any] = {} timeout_seconds: int = 10 retry: RetryConfig = RetryConfig() proxy: Optional[str | ProxyConfig] = None auth: Optional[APIAuthConfig] = None response_prompt: Optional[str] = None class Config: extra = "allow" # ---------------- Intent / Param --------- class ParameterConfig(BaseModel): name: str caption: Optional[str] = "" type: str = Field(..., pattern=r"^(int|float|bool|str|string)$") required: bool = True extraction_prompt: Optional[str] = None validation_regex: Optional[str] = None invalid_prompt: Optional[str] = None type_error_prompt: Optional[str] = None def canonical_type(self) -> str: return "str" if self.type == "string" else self.type class IntentConfig(BaseModel): name: str caption: Optional[str] = "" locale: str = "tr-TR" dependencies: List[str] = [] examples: List[str] = [] detection_prompt: Optional[str] = None parameters: List[ParameterConfig] = [] action: str fallback_timeout_prompt: Optional[str] = None fallback_error_prompt: Optional[str] = None class Config: extra = "allow" # ---------------- Version / Project ------ class LLMConfig(BaseModel): repo_id: str generation_config: Dict[str, Any] = {} use_fine_tune: bool = False fine_tune_zip: str = "" class VersionConfig(BaseModel): id: int = Field(..., alias="version_number") caption: Optional[str] = "" published: bool = False general_prompt: str llm: "LLMConfig" intents: List["IntentConfig"] class Config: extra = "allow" populate_by_name = True class ProjectConfig(BaseModel): id: Optional[int] = None name: str caption: Optional[str] = "" enabled: bool = True last_version_number: Optional[int] = None versions: List[VersionConfig] class Config: extra = "allow" # ---------------- Service Config --------- class ServiceConfig(BaseModel): global_config: GlobalConfig = Field(..., alias="config") projects: List[ProjectConfig] apis: List[APIConfig] # runtime helpers (skip validation) _api_by_name: Dict[str, APIConfig] = {} def build_index(self): self._api_by_name = {a.name: a for a in self.apis} def get_api(self, name: str) -> APIConfig | None: return self._api_by_name.get(name) # ---------------- Provider Singleton ----- class ConfigProvider: _instance: Optional[ServiceConfig] = None _CONFIG_PATH = Path(__file__).parent / "service_config.jsonc" @classmethod def get(cls) -> ServiceConfig: if cls._instance is None: cls._instance = cls._load() cls._instance.build_index() return cls._instance # -------- Internal helpers ------------ @classmethod def _load(cls) -> ServiceConfig: log(f"📥 Loading service config from {cls._CONFIG_PATH.name} …") raw = cls._CONFIG_PATH.read_text(encoding="utf-8") json_str = cls._strip_jsonc(raw) try: data = json.loads(json_str) cfg = ServiceConfig.model_validate(data) log("✅ Service config loaded.") return cfg except (json.JSONDecodeError, ValidationError) as exc: log(f"❌ Config validation error: {exc}") raise @staticmethod def _strip_jsonc(text: str) -> str: """Remove // and /* */ comments (string-aware).""" OUT, STR, ESC, SLASH, BLOCK = 0, 1, 2, 3, 4 state, res, i = OUT, [], 0 while i < len(text): ch = text[i] if state == OUT: if ch == '"': state, res = STR, res + [ch] elif ch == '/': nxt = text[i + 1] if i + 1 < len(text) else "" if nxt == '/': state, i = SLASH, i + 1 elif nxt == '*': state, i = BLOCK, i + 1 else: res.append(ch) else: res.append(ch) elif state == STR: res.append(ch) state = ESC if ch == '\\' else (OUT if ch == '"' else STR) elif state == ESC: res.append(ch) state = STR elif state == SLASH: if ch == '\n': res.append(ch) state = OUT elif state == BLOCK: if ch == '*' and i + 1 < len(text) and text[i + 1] == '/': i, state = i + 1, OUT i += 1 return ''.join(res)