from __future__ import annotations import json import os import uuid from dataclasses import dataclass, asdict, field from datetime import datetime from typing import List, Optional import random import math STORE_DIR = os.path.join(os.path.dirname(__file__), "data") STORE_PATH = os.path.join(STORE_DIR, "kpi_records.jsonl") @dataclass class KPIRecord: revenue: float cogs: float inventory_turns: float lead_time: float cac: float ltv: float region: str = "global" industry: str = "unknown" anon_id: str = field(default_factory=lambda: uuid.uuid4().hex) timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat()) class LocalStore: """Simple offline-first store for KPI records (JSONL).""" def __init__(self, path: Optional[str] = None) -> None: self.path = path or STORE_PATH os.makedirs(os.path.dirname(self.path), exist_ok=True) def add_kpi(self, record: KPIRecord) -> None: with open(self.path, "a", encoding="utf-8") as f: f.write(json.dumps(asdict(record)) + "\n") def get_all(self) -> List[KPIRecord]: records: List[KPIRecord] = [] if not os.path.exists(self.path): return records with open(self.path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue data = json.loads(line) # Guard against missing fields in legacy lines records.append(KPIRecord(**data)) return records class SecureAggregator: """Privacy-aware aggregate over KPI records. By default, returns the plain mean. When anonymize=True, adds Laplace noise to the result to preserve differential privacy characteristics. Noise is deterministic for unit tests by allowing a fixed seed on the RNG via Python's random module; in production, a robust rng should be used. """ @staticmethod def _laplace_sample(b: float) -> float: # Laplace sampling via the difference of two exponentials: ~ Laplace(0, b) u1 = random.random() u2 = random.random() if u1 == 0 else random.random() # ensure positive values and stable logs return -b * (math.log(u1 + 1e-12) - math.log(u2 + 1e-12)) @staticmethod def aggregate(records: List[KPIRecord], metric: str, anonymize: bool = False, epsilon: float = 1.0) -> float: if not records: return 0.0 if not hasattr(KPIRecord, '__annotations__') or metric not in KPIRecord.__annotations__: raise ValueError(f"Unknown metric '{metric}' for KPIRecord.") values = [getattr(r, metric) for r in records] mean = sum(values) / len(values) if values else 0.0 if anonymize: b = 1.0 / max(epsilon, 1e-9) noise = SecureAggregator._laplace_sample(b) mean += noise return mean class GrowthCalculator: """Derived-growth helpers for KPI records.""" @staticmethod def roi(record: KPIRecord) -> float: # Simple return on investment proxy: (LTV - CAC) / CAC if record.cac == 0: return float('inf') return (record.ltv - record.cac) / record.cac @staticmethod def growth_index(record: KPIRecord) -> float: # A lightweight composite growth index using revenue and LTV return (record.revenue + record.ltv) / max(1.0, record.cogs) __all__ = ["KPIRecord", "LocalStore", "SecureAggregator", "GrowthCalculator"]