from __future__ import annotations import json import os import time from collections.abc import Callable from dataclasses import dataclass from pathlib import Path from collections.abc import Mapping IDENTITY_TTL_SECONDS = 60.0 @dataclass(frozen=True) class TelemetryIdentity: install_id: str | None enabled: bool path: Path _cache: tuple[float, Path, TelemetryIdentity] | None = None def _telemetry_path(home_dir: Path | None = None) -> Path: return (home_dir or Path.home()) / ".ktx" / "telemetry.json" def _env_disables(env: Mapping[str, str] | None = None) -> bool: source = os.environ if env is None else env return bool( source.get("KTX_TELEMETRY_DISABLED") or source.get("DO_NOT_TRACK") or source.get("CI") ) def _read_identity(path: Path) -> TelemetryIdentity: try: raw = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return TelemetryIdentity(install_id=None, enabled=False, path=path) install_id = raw.get("installId") enabled = raw.get("enabled") if not isinstance(install_id, str) or enabled is not True: return TelemetryIdentity( install_id=install_id if isinstance(install_id, str) else None, enabled=False, path=path, ) return TelemetryIdentity(install_id=install_id, enabled=True, path=path) def load_telemetry_identity( *, home_dir: Path | None = None, env: Mapping[str, str] | None = None, now: Callable[[], float] | None = None, ) -> TelemetryIdentity: global _cache path = _telemetry_path(home_dir) clock = now or time.monotonic current = float(clock()) if _cache and _cache[1] == path and current - _cache[0] < IDENTITY_TTL_SECONDS: cached = _cache[2] else: cached = _read_identity(path) _cache = (current, path, cached) if _env_disables(env): return TelemetryIdentity(install_id=cached.install_id, enabled=False, path=path) return cached def reset_identity_cache() -> None: global _cache _cache = None