"""Helpers shared by the nowcast alarm clients (geo and point).""" from __future__ import annotations import csv import json import os import tempfile from copy import deepcopy from pathlib import Path from typing import Any from urllib.parse import urlsplit from uuid import UUID from common import ApiError, ConfigurationError, IklimClient AUTHENTICATION_FIELDS = { "BASIC": ("username", "password"), "JWT_TOKEN": ("jwtToken",), "API_KEY": ("apiKey", "asQueryParameter", "asHeader"), "HMAC_SIGNATURE": ("clientId", "secret", "algorithm"), } def read_json_object(path: Path, description: str) -> dict[str, Any]: try: data = json.loads(path.read_text(encoding="utf-8")) except FileNotFoundError as exc: raise ConfigurationError(f"{description} dosyası bulunamadı: {path}") from exc except json.JSONDecodeError as exc: raise ConfigurationError( f"{description} geçerli JSON değil ({path}:{exc.lineno})." ) from exc if not isinstance(data, dict): raise ConfigurationError(f"{description} kök değeri JSON nesnesi olmalıdır.") return data def required_config_value(data: dict[str, Any], key: str, location: str) -> Any: value = data.get(key) if value is None or (isinstance(value, str) and not value.strip()): raise ConfigurationError(f"{location}.{key} eksik veya boş.") if isinstance(value, str) and "change-me" in value.strip().lower(): raise ConfigurationError(f"{location}.{key} placeholder değeri değiştirilmemiş.") return value def load_webhook_config(path: Path) -> dict[str, Any]: config = read_json_object(path, "Webhook config") url = str(required_config_value(config, "url", "webhook")) parsed_url = urlsplit(url) if parsed_url.scheme != "https" or not parsed_url.netloc: raise ConfigurationError("webhook.url geçerli bir HTTPS adresi olmalıdır.") http_method = str(required_config_value(config, "httpMethod", "webhook")).upper() if http_method not in {"POST", "PUT", "GET"}: raise ConfigurationError("webhook.httpMethod POST, PUT veya GET olmalıdır.") authentication = config.get("authentication") if not isinstance(authentication, dict): raise ConfigurationError("webhook.authentication JSON nesnesi olmalıdır.") selected = str( required_config_value(authentication, "selected", "webhook.authentication") ).upper() if selected not in AUTHENTICATION_FIELDS: allowed = ", ".join(AUTHENTICATION_FIELDS) raise ConfigurationError( f"webhook.authentication.selected şu değerlerden biri olmalıdır: {allowed}." ) options = authentication.get("options") if not isinstance(options, dict) or not isinstance(options.get(selected), dict): raise ConfigurationError( f"webhook.authentication.options.{selected} JSON nesnesi olmalıdır." ) selected_options = options[selected] auth_payload: dict[str, Any] = {"type": selected} for field in AUTHENTICATION_FIELDS[selected]: auth_payload[field] = required_config_value( selected_options, field, f"webhook.authentication.options.{selected}" ) payload: dict[str, Any] = { "url": url, "httpMethod": http_method, "contentType": str( required_config_value(config, "contentType", "webhook") ), "authentication": auth_payload, } for optional_field in ("requestHeaders", "deliveryPolicy"): if optional_field in config: if not isinstance(config[optional_field], dict): raise ConfigurationError( f"webhook.{optional_field} JSON nesnesi olmalıdır." ) payload[optional_field] = deepcopy(config[optional_field]) return payload def as_uuid(value: Any, *, version: int | None = None) -> str | None: if not isinstance(value, str): return None try: parsed = UUID(value) except ValueError: return None if version is not None and parsed.version != version: return None return value def response_warnings(*objects: Any) -> list[str]: warnings: list[str] = [] for data in objects: if not isinstance(data, dict): continue for key in ("warning", "warnings"): value = data.get(key) if isinstance(value, str) and value.strip(): warnings.append(value.strip()) elif isinstance(value, list): warnings.extend(str(item) for item in value if str(item).strip()) return warnings def write_csv_atomic( path: Path, rows: list[dict[str, Any]], fieldnames: list[str] ) -> None: """Rewrite a semicolon separated CSV through a temp file in the same directory.""" temp_path: Path | None = None try: with tempfile.NamedTemporaryFile( "w", encoding="utf-8-sig", newline="", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", delete=False, ) as temp_file: temp_path = Path(temp_file.name) writer = csv.DictWriter( temp_file, fieldnames=fieldnames, delimiter=";", extrasaction="ignore", ) writer.writeheader() writer.writerows(rows) temp_file.flush() os.fsync(temp_file.fileno()) os.replace(temp_path, path) finally: if temp_path and temp_path.exists(): temp_path.unlink() def fetch_account_id(client: IklimClient) -> str: """Return the caller's account id from /v1/users/me.""" response = client.request_json("GET", "/v1/users/me") data = response.data account = data.get("account") if isinstance(data, dict) else None account_id = account.get("id") if isinstance(account, dict) else None if not as_uuid(account_id): raise ApiError("/v1/users/me yanıtında geçerli account.id bulunamadı.") return account_id