111 lines
3.7 KiB
Python
111 lines
3.7 KiB
Python
"""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")
|