"""Environment-backed configuration shared by every service client.""" from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path from urllib.parse import urlsplit from dotenv import load_dotenv from .errors import ConfigurationError DEFAULT_ENV_FILE = Path(__file__).resolve().parent.parent / ".env" def _required(name: str) -> str: value = os.getenv(name, "").strip() if not value: raise ConfigurationError(f".env içinde {name} eksik veya boş.") if value.lower() == "change-me": raise ConfigurationError(f".env içindeki {name} placeholder değeri değiştirilmemiş.") return value def _positive_float(name: str, default: str) -> float: raw_value = os.getenv(name, default).strip() try: value = float(raw_value) except ValueError as exc: raise ConfigurationError(f"{name} sayısal bir değer olmalıdır.") from exc if value <= 0: raise ConfigurationError(f"{name} sıfırdan büyük olmalıdır.") return value def _non_negative_int(name: str, default: str) -> int: raw_value = os.getenv(name, default).strip() try: value = int(raw_value) except ValueError as exc: raise ConfigurationError(f"{name} tam sayı olmalıdır.") from exc if value < 0: raise ConfigurationError(f"{name} negatif olamaz.") return value def env_bool(name: str, default: bool = False) -> bool: raw_value = os.getenv(name) if raw_value is None or not raw_value.strip(): return default normalized = raw_value.strip().lower() if normalized in {"1", "true", "yes", "on"}: return True if normalized in {"0", "false", "no", "off"}: return False raise ConfigurationError(f"{name} true veya false olmalıdır.") @dataclass(frozen=True) class Settings: env_file: Path base_url: str hmac_secret: str accept_language: str timeout_seconds: float token_expiry_skew_seconds: int max_retries: int retry_base_seconds: float @classmethod def from_env(cls, env_file: Path = DEFAULT_ENV_FILE) -> "Settings": env_file = Path(env_file).resolve() if not env_file.is_file(): raise ConfigurationError(f".env dosyası bulunamadı: {env_file}") load_dotenv(env_file, override=True) environment = _required("IKLIM_API_ENV").lower() url_variables = { "test": "IKLIM_API_TEST_URL", "prod": "IKLIM_API_PROD_URL", } if environment not in url_variables: raise ConfigurationError("IKLIM_API_ENV yalnızca test veya prod olabilir.") base_url = _required(url_variables[environment]).rstrip("/") parsed_url = urlsplit(base_url) if parsed_url.scheme != "https" or not parsed_url.netloc: raise ConfigurationError("Seçilen API adresi geçerli bir HTTPS adresi olmalıdır.") language = os.getenv("IKLIM_ACCEPT_LANGUAGE", "en").strip().lower() or "en" if language not in {"tr", "en"}: raise ConfigurationError("IKLIM_ACCEPT_LANGUAGE yalnızca tr veya en olabilir.") return cls( env_file=env_file, base_url=base_url, hmac_secret=_required("IKLIM_HMAC_SECRET"), accept_language=language, timeout_seconds=_positive_float("IKLIM_TIMEOUT_SECONDS", "30"), token_expiry_skew_seconds=_non_negative_int( "IKLIM_TOKEN_EXPIRY_SKEW_SECONDS", "60" ), max_retries=_non_negative_int("IKLIM_MAX_RETRIES", "3"), retry_base_seconds=_positive_float("IKLIM_RETRY_BASE_SECONDS", "2"), ) def credentials(self) -> tuple[str, str]: return _required("IKLIM_USERNAME"), _required("IKLIM_PASSWORD")