LoveLogicAI's picture
Upload folder using huggingface_hub
587a933 verified
Raw History Blame Contribute Delete
3.99 kB
"""
Thin async Postgres access layer + card (de)serialization.
Kept separate so mcp_server, conductor, and reaper share one pool + one mapper.
"""
from __future__ import annotations
import json
import os
from datetime import datetime, timedelta, timezone
from typing import Optional
import asyncpg
from .contract import Card, Column, Role, ThreadEntry
_POOL: Optional[asyncpg.Pool] = None
async def pool() -> asyncpg.Pool:
global _POOL
if _POOL is None:
dsn = os.environ.get(
"DATABASE_URL",
"postgresql://kanbot:kanbot@localhost:5432/kanbot",
)
_POOL = await asyncpg.create_pool(dsn, min_size=1, max_size=10)
return _POOL
def _row_to_card(row: asyncpg.Record) -> Card:
return Card(
id=row["id"],
title=row["title"],
column=Column(row["column"]),
agent_role=Role(row["agent_role"]),
spec=row["spec"],
acceptance_criteria=json.loads(row["acceptance_criteria"]),
assigned_worker=row["assigned_worker"],
parent=row["parent"],
children=json.loads(row["children"]),
thread=[ThreadEntry(**e) for e in json.loads(row["thread"])],
lease_expires_at=row["lease_expires_at"].isoformat()
if row["lease_expires_at"] else None,
attempt=row["attempt"],
requires_human=row["requires_human"],
tags=json.loads(row["tags"]),
created_at=row["created_at"].isoformat(),
updated_at=row["updated_at"].isoformat(),
)
async def get_card(card_id: str) -> Optional[Card]:
async with (await pool()).acquire() as c:
row = await c.fetchrow("SELECT * FROM cards WHERE id = $1", card_id)
return _row_to_card(row) if row else None
async def list_column(column: Column) -> list[Card]:
async with (await pool()).acquire() as c:
rows = await c.fetch(
'SELECT * FROM cards WHERE "column" = $1 ORDER BY created_at', column.value
)
return [_row_to_card(r) for r in rows]
async def next_card_id() -> str:
async with (await pool()).acquire() as c:
n = await c.fetchval("SELECT count(*) FROM cards")
return f"CARD-{n + 1}"
async def insert_card(card: Card) -> Card:
async with (await pool()).acquire() as c:
await c.execute(
"""INSERT INTO cards
(id,title,"column",agent_role,spec,acceptance_criteria,parent,
children,thread,tags,requires_human,attempt)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)""",
card.id, card.title, card.column.value, card.agent_role.value,
card.spec, json.dumps(card.acceptance_criteria), card.parent,
json.dumps(card.children), json.dumps([e.__dict__ for e in card.thread]),
json.dumps(card.tags), card.requires_human, card.attempt,
)
return card
async def append_thread(card_id: str, entry: ThreadEntry) -> None:
async with (await pool()).acquire() as c:
await c.execute(
"UPDATE cards SET thread = thread || $2::jsonb WHERE id = $1",
card_id, json.dumps([entry.__dict__]),
)
async def set_lease(card_id: str, worker: str, seconds: int) -> None:
expiry = datetime.now(timezone.utc) + timedelta(seconds=seconds)
async with (await pool()).acquire() as c:
await c.execute(
"UPDATE cards SET assigned_worker=$2, lease_expires_at=$3 WHERE id=$1",
card_id, worker, expiry,
)
async def clear_lease(card_id: str) -> None:
async with (await pool()).acquire() as c:
await c.execute(
"UPDATE cards SET assigned_worker=NULL, lease_expires_at=NULL WHERE id=$1",
card_id,
)
async def add_child(parent_id: str, child_id: str) -> None:
async with (await pool()).acquire() as c:
await c.execute(
"UPDATE cards SET children = children || $2::jsonb WHERE id = $1",
parent_id, json.dumps([child_id]),
)