"""Redis client for DevOpsDash — reads the same keys as DevOpsMCP.""" from __future__ import annotations import json import os from typing import Any, Dict, List, Optional import redis # ── Key prefixes (must match DevOpsMCP exactly) ─────────────────────────────── BOARD_KEY_PREFIX = "devops-mcp:taskboard:" TASK_KEY_PREFIX = "devops-mcp:task:" BOARD_INDEX_KEY = "devops-mcp:taskboard:_index" ADR_KEY_PREFIX = "devops-mcp:adr:" ADR_INDEX_KEY = "devops-mcp:adr:_index" MEMORY_KEY_PREFIX = "devops-mcp:memory:" PROJECT_KEY_PREFIX = "devops-mcp:projects:" PROJECT_INDEX_KEY = "devops-mcp:projects:_index" WORKCONTEXT_KEY_PREFIX = "devops-mcp:workcontext:" def get_redis() -> redis.Redis: url = os.environ.get("REDIS_URL", "redis://localhost:6379") return redis.from_url(url, decode_responses=True, socket_connect_timeout=3) # ── Task boards ─────────────────────────────────────────────────────────────── def list_boards(r: redis.Redis, project: Optional[str] = None, status: Optional[str] = None) -> List[Dict[str, Any]]: index: List[str] = json.loads(r.get(BOARD_INDEX_KEY) or "[]") boards = [] for bid in index: raw = r.get(f"{BOARD_KEY_PREFIX}{bid}") if raw: board = json.loads(raw) if project and project.lower() not in board.get("project", "").lower(): continue if status and board.get("status") != status: continue boards.append(board) boards.sort(key=lambda b: b.get("updated_at", ""), reverse=True) return boards def get_board(r: redis.Redis, board_id: str) -> Optional[Dict[str, Any]]: raw = r.get(f"{BOARD_KEY_PREFIX}{board_id}") return json.loads(raw) if raw else None def save_board(r: redis.Redis, board: Dict[str, Any]) -> None: bid = board["board_id"] r.set(f"{BOARD_KEY_PREFIX}{bid}", json.dumps(board), ex=86400 * 365 * 3) index: List[str] = json.loads(r.get(BOARD_INDEX_KEY) or "[]") if bid not in index: index.append(bid) r.set(BOARD_INDEX_KEY, json.dumps(index), ex=86400 * 365 * 3) def save_task(r: redis.Redis, task: Dict[str, Any]) -> None: tid = task["task_id"] r.set(f"{TASK_KEY_PREFIX}{tid}", json.dumps(task), ex=86400 * 365 * 3) board = get_board(r, task["board_id"]) if board: tasks = board.setdefault("tasks", []) existing = next((i for i, t in enumerate(tasks) if t["task_id"] == tid), None) if existing is not None: tasks[existing] = task else: tasks.append(task) from datetime import datetime, timezone board["updated_at"] = datetime.now(timezone.utc).isoformat() save_board(r, board) # ── ADRs ────────────────────────────────────────────────────────────────────── def list_adrs(r: redis.Redis, status_filter: Optional[str] = None) -> List[Dict[str, Any]]: index: List[str] = json.loads(r.get(ADR_INDEX_KEY) or "[]") adrs = [] for adr_id in index: raw = r.get(f"{ADR_KEY_PREFIX}{adr_id}") if raw: adr = json.loads(raw) if status_filter and adr.get("status", "").lower() != status_filter.lower(): continue adrs.append(adr) adrs.sort(key=lambda a: a.get("number", 0)) return adrs def get_adr(r: redis.Redis, adr_id: str) -> Optional[Dict[str, Any]]: raw = r.get(f"{ADR_KEY_PREFIX}{adr_id}") return json.loads(raw) if raw else None # ── Memories ────────────────────────────────────────────────────────────────── def list_memories(r: redis.Redis, entity_type: Optional[str] = None, limit: int = 50) -> List[Dict[str, Any]]: keys = r.keys(f"{MEMORY_KEY_PREFIX}*") memories = [] for key in keys: if key.endswith("_index"): continue raw = r.get(key) if raw: try: mem = json.loads(raw) if entity_type and mem.get("entity_type", "").lower() != entity_type.lower(): continue memories.append(mem) except json.JSONDecodeError: pass memories.sort(key=lambda m: m.get("created_at", ""), reverse=True) return memories[:limit] # ── Projects ────────────────────────────────────────────────────────────────── def list_projects(r: redis.Redis, context: Optional[str] = None) -> List[Dict[str, Any]]: index: List[str] = json.loads(r.get(PROJECT_INDEX_KEY) or "[]") projects = [] for path in index: raw = r.get(f"{PROJECT_KEY_PREFIX}{path}") if raw: proj = json.loads(raw) if context and proj.get("context", "").lower() != context.lower(): continue projects.append(proj) return projects def list_workcontexts(r: redis.Redis) -> List[Dict[str, Any]]: keys = r.keys(f"{WORKCONTEXT_KEY_PREFIX}*") contexts = [] for key in keys: raw = r.get(key) if raw: try: ctx = json.loads(raw) ctx["path"] = key[len(WORKCONTEXT_KEY_PREFIX):] contexts.append(ctx) except json.JSONDecodeError: pass contexts.sort(key=lambda c: c.get("saved_at", ""), reverse=True) return contexts