# Copyright (c) 2026 Patrick Motsch # All rights reserved. """Workflow action: fetch aggregated Redmine statistics from the mirror.""" import logging from typing import Any, Dict, List, Optional from modules.datamodels.datamodelChat import ActionResult from modules.features.redmine.serviceRedmineStats import getStats from ._shared import resolveInstanceContext logger = logging.getLogger(__name__) def _normalizeIntList(value: Any) -> Optional[List[int]]: """Accept ``None | int | "1,2,3" | [..]`` and return a list of ints.""" if value is None or value == "": return None if isinstance(value, int): return [value] if isinstance(value, str): value = [v.strip() for v in value.split(",") if v.strip()] if isinstance(value, list): ids: List[int] = [] for v in value: try: ids.append(int(v)) except (TypeError, ValueError): continue return ids or None return None async def getStatsAction(self, parameters: Dict[str, Any]) -> ActionResult: """Return the same DTO as the ``/stats`` endpoint, cached per filter.""" try: user, mandateId, featureInstanceId = resolveInstanceContext(self.services, parameters) except ValueError as exc: return ActionResult.isFailure(error=str(exc)) bucket = (parameters.get("bucket") or "week").lower() if bucket not in {"day", "week", "month"}: bucket = "week" status_filter = (parameters.get("statusFilter") or "*").lower() if status_filter not in {"*", "open", "closed"}: status_filter = "*" try: stats = await getStats( user, mandateId, featureInstanceId, dateFrom=parameters.get("dateFrom") or None, dateTo=parameters.get("dateTo") or None, bucket=bucket, trackerIds=_normalizeIntList(parameters.get("trackerIds")), categoryIds=_normalizeIntList(parameters.get("categoryIds")), statusFilter=status_filter, ) except Exception as exc: logger.exception("redmine.getStats failed") return ActionResult.isFailure(error=f"Stats failed: {exc}") return ActionResult.isSuccess(data={"stats": stats.model_dump(exclude_none=True)})