"""Carga de config.json sin depender de argparse. El ``config_parser`` original invoca ``ArgParser().parse()`` en tiempo de import, lo que obliga a que haya argumentos de linea de comandos validos. El daemon y las herramientas necesitan leer la configuracion sin eso. """ from __future__ import annotations import json import os ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DEFAULT_CONFIG = os.path.join(ROOT, "config.json") DEFAULTS = { "api_url": "https://timenet-wcp.gpisoftware.com/api/", "timezone": "Europe/Madrid", "jitter": { # margen, en segundos, con el que se ficha respecto de la hora teorica. # Entradas ANTES (Timenet no acredita nada fuera de la ventana, pero si # debita las entradas tardias). Salidas DESPUES. # Lognormal: la moda queda pegada a la hora teorica y la cola va hacia # mas margen. Da mas minutos distintos por menos presencia extra que una # uniforme (ver la nota de cabecera de app/jitter.py). # floor = margen minimo, median = mediana, sigma = longitud de la cola, # cap = tope duro. # perfil "natural": 5 minutos distintos en la entrada, 4,1 min/dia de # presencia extra. Ver la tabla de perfiles en el README. "entry_floor": 12, "entry_median": 58, "entry_sigma": 0.95, "entry_cap": 330, "exit_floor": 10, "exit_median": 42, "exit_sigma": 0.95, "exit_cap": 300, # Deriva lenta de la hora habitual, en unidades LOGARITMICAS (multiplica # el centro). Mueve la moda de una semana a otra, lo que ensancha lo que se # ve a lo largo del mes con coste medio practicamente nulo. "drift_sigma": 0.30, "drift_clamp": 0.75, # margen de seguridad: nunca fichar una entrada a menos de X s de la hora "entry_guard": 12, }, "geo_jitter_meters": 12.0, "telegram": {}, } def _merge(base: dict, over: dict) -> dict: out = dict(base) for k, v in (over or {}).items(): out[k] = _merge(base[k], v) if isinstance(v, dict) and isinstance(base.get(k), dict) else v return out def load(path: str | None = None) -> dict: path = path or os.environ.get("AUTOFICHER_CONFIG") or DEFAULT_CONFIG if not os.path.exists(path): raise FileNotFoundError(f"No se encuentra el archivo de configuracion: {path}") with open(path, encoding="utf-8") as f: cfg = json.load(f) if not isinstance(cfg, dict): raise ValueError("config.json no contiene un objeto JSON") cfg = _merge(DEFAULTS, cfg) for req in ("api_url", "user"): if not cfg.get(req): raise ValueError(f"Falta '{req}' en {path}") return cfg def pin(cfg: dict | None = None) -> str: """PIN desde el entorno, la config o por teclado. Nunca se escribe a disco.""" import getpass p = os.environ.get("TIMENET_PIN") or (cfg or {}).get("pin") return str(p) if p else getpass.getpass("PIN de Timenet: ")