ghpsolutions/quadro
Quadro: team boards with roles, history and an MCP endpoint for Claude and ChatGPT
A remix template · 0 remixes · Remix ghpsolutions/quadro
app/boards/infrastructure/memory.py
"""An in-memory repository with the same contract as the DataK3 one.
Used by the tests and by `STORE=memory` for running the app on a laptop without a bucket.
"""
from __future__ import annotations
import copy
import threading
from typing import Optional, Sequence
from boards.application.ports import NumberTaken
from boards.domain.model import Board, BoardList, Card, Role
class MemoryBoards:
def __init__(self):
self._lock = threading.RLock()
self.boards, self.members, self.lists, self.cards = {}, {}, {}, {}
self.archived, self.comments, self.events, self.vectors = set(), [], [], {}
def save_board(self, board: Board) -> None:
with self._lock:
self.boards[board.id] = copy.deepcopy(board)
def get_board(self, board_id: str) -> Optional[Board]:
with self._lock:
b = self.boards.get(board_id)
if not b:
return None
b = copy.deepcopy(b)
b.members = {e: r for (bid, e), r in self.members.items()
if bid == board_id and r is not Role.OWNER}
return b
def boards_for(self, email: str) -> list:
with self._lock:
ids = {bid for (bid, e) in self.members if e == email}
return [self.get_board(i) for i in ids if i in self.boards]
def save_member(self, board_id: str, email: str, role: Role) -> None:
with self._lock:
self.members[(board_id, email)] = role
def remove_member(self, board_id: str, email: str) -> None:
with self._lock:
self.members.pop((board_id, email), None)
def save_list(self, lst: BoardList) -> None:
with self._lock:
self.lists[lst.id] = copy.deepcopy(lst)
def lists_on(self, board_id: str) -> list:
with self._lock:
return [copy.deepcopy(l) for l in self.lists.values() if l.board_id == board_id]
def delete_list(self, list_id: str) -> None:
with self._lock:
self.lists.pop(list_id, None)
def next_number(self, board_id: str) -> int:
with self._lock:
return 1 + max((c.number for c in self.cards.values() if c.board_id == board_id),
default=0)
def insert_card(self, card: Card) -> None:
with self._lock:
if card.id in self.cards:
raise NumberTaken(card.id)
self.cards[card.id] = copy.deepcopy(card)
def update_card(self, card: Card, expected_version: int) -> bool:
with self._lock:
current = self.cards.get(card.id)
if current is None or current.version != expected_version or card.id in self.archived:
return False
self.cards[card.id] = copy.deepcopy(card)
return True
def get_card(self, card_id: str) -> Optional[Card]:
with self._lock:
c = self.cards.get(card_id)
return copy.deepcopy(c) if c and card_id not in self.archived else None
def cards_on(self, board_ids: Sequence[str]) -> list:
with self._lock:
ids = set(board_ids)
return [copy.deepcopy(c) for c in self.cards.values()
if c.board_id in ids and c.id not in self.archived]
def delete_card(self, card_id: str) -> None:
with self._lock:
self.archived.add(card_id)
def add_comment(self, comment: dict) -> None:
with self._lock:
self.comments.append(dict(comment))
def comments_for(self, card_id: str) -> list:
with self._lock:
return sorted((dict(c) for c in self.comments if c["card_id"] == card_id),
key=lambda c: c["created_at"])
def comment_counts(self, board_id: str) -> dict:
with self._lock:
out = {}
for c in self.comments:
if c["board_id"] == board_id:
out[c["card_id"]] = out.get(c["card_id"], 0) + 1
return out
def record(self, event: dict) -> None:
with self._lock:
self.events.append(copy.deepcopy(event))
def activity(self, board_id: str, card_id: Optional[str] = None, limit: int = 50) -> list:
with self._lock:
rows = [copy.deepcopy(e) for e in reversed(self.events) if e["board_id"] == board_id
and (card_id is None or e["card_id"] == card_id)]
return rows[:limit]
def save_embedding(self, card_id: str, board_id: str, digest: str, vector: list) -> None:
with self._lock:
self.vectors[card_id] = (board_id, digest, list(vector))
def embeddings_on(self, board_id: str) -> dict:
with self._lock:
return {cid: (d, v) for cid, (bid, d, v) in self.vectors.items() if bid == board_id}