Initial commit

This commit is contained in:
2026-09-09 16:21:13 +03:00
commit 2067d8c298
17 changed files with 1887 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
"""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")