ref(backend): Centralize deps and add mypy

Extract reusable API dependencies and activity logging helpers.\nAdd mypy configuration and dev dependency for type checking.\n\nCo-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Abhimanyu Saharan
2026-02-04 03:57:19 +05:30
parent 7fd079e4f1
commit b24e3e1dcd
12 changed files with 205 additions and 189 deletions

View File

@@ -1,13 +1,14 @@
from __future__ import annotations from __future__ import annotations
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlmodel import Session, select from sqlalchemy import desc
from sqlmodel import Session, col, select
from app.core.auth import get_auth_context from app.api.deps import require_admin_auth
from app.core.auth import AuthContext
from app.db.session import get_session from app.db.session import get_session
from app.models.activity_events import ActivityEvent from app.models.activity_events import ActivityEvent
from app.schemas.activity_events import ActivityEventRead from app.schemas.activity_events import ActivityEventRead
from app.services.admin_access import require_admin
router = APIRouter(prefix="/activity", tags=["activity"]) router = APIRouter(prefix="/activity", tags=["activity"])
@@ -17,10 +18,12 @@ def list_activity(
limit: int = Query(50, ge=1, le=200), limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> list[ActivityEvent]: ) -> list[ActivityEvent]:
require_admin(auth)
statement = ( statement = (
select(ActivityEvent).order_by(ActivityEvent.created_at.desc()).offset(offset).limit(limit) select(ActivityEvent)
.order_by(desc(col(ActivityEvent.created_at)))
.offset(offset)
.limit(limit)
) )
return list(session.exec(statement)) return list(session.exec(statement))

View File

@@ -7,10 +7,10 @@ from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select from sqlmodel import Session, select
from app.core.auth import get_auth_context from app.api.deps import require_admin_auth
from app.core.auth import AuthContext
from app.db.session import get_session from app.db.session import get_session
from app.integrations.openclaw_gateway import OpenClawGatewayError, openclaw_call from app.integrations.openclaw_gateway import OpenClawGatewayError, openclaw_call
from app.models.activity_events import ActivityEvent
from app.models.agents import Agent from app.models.agents import Agent
from app.schemas.agents import ( from app.schemas.agents import (
AgentCreate, AgentCreate,
@@ -19,7 +19,7 @@ from app.schemas.agents import (
AgentRead, AgentRead,
AgentUpdate, AgentUpdate,
) )
from app.services.admin_access import require_admin from app.services.activity_log import record_activity
from app.services.agent_provisioning import send_provisioning_message from app.services.agent_provisioning import send_provisioning_message
router = APIRouter(prefix="/agents", tags=["agents"]) router = APIRouter(prefix="/agents", tags=["agents"])
@@ -54,20 +54,28 @@ def _with_computed_status(agent: Agent) -> Agent:
def _record_heartbeat(session: Session, agent: Agent) -> None: def _record_heartbeat(session: Session, agent: Agent) -> None:
event = ActivityEvent( record_activity(
session,
event_type="agent.heartbeat", event_type="agent.heartbeat",
message=f"Heartbeat received from {agent.name}.", message=f"Heartbeat received from {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.add(event)
def _record_provisioning_failure(session: Session, agent: Agent, error: str) -> None:
record_activity(
session,
event_type="agent.provision.failed",
message=f"Provisioning message failed: {error}",
agent_id=agent.id,
)
@router.get("", response_model=list[AgentRead]) @router.get("", response_model=list[AgentRead])
def list_agents( def list_agents(
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> list[Agent]: ) -> list[Agent]:
require_admin(auth)
agents = list(session.exec(select(Agent))) agents = list(session.exec(select(Agent)))
return [_with_computed_status(agent) for agent in agents] return [_with_computed_status(agent) for agent in agents]
@@ -76,9 +84,8 @@ def list_agents(
async def create_agent( async def create_agent(
payload: AgentCreate, payload: AgentCreate,
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> Agent: ) -> Agent:
require_admin(auth)
agent = Agent.model_validate(payload) agent = Agent.model_validate(payload)
session_key, session_error = await _ensure_gateway_session(agent.name) session_key, session_error = await _ensure_gateway_session(agent.name)
agent.openclaw_session_id = session_key agent.openclaw_session_id = session_key
@@ -86,41 +93,27 @@ async def create_agent(
session.commit() session.commit()
session.refresh(agent) session.refresh(agent)
if session_error: if session_error:
session.add( record_activity(
ActivityEvent( session,
event_type="agent.session.failed", event_type="agent.session.failed",
message=f"Session sync failed for {agent.name}: {session_error}", message=f"Session sync failed for {agent.name}: {session_error}",
agent_id=agent.id, agent_id=agent.id,
)
) )
else: else:
session.add( record_activity(
ActivityEvent( session,
event_type="agent.session.created", event_type="agent.session.created",
message=f"Session created for {agent.name}.", message=f"Session created for {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
)
) )
session.commit() session.commit()
try: try:
await send_provisioning_message(agent) await send_provisioning_message(agent)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
session.add( _record_provisioning_failure(session, agent, str(exc))
ActivityEvent(
event_type="agent.provision.failed",
message=f"Provisioning message failed: {exc}",
agent_id=agent.id,
)
)
session.commit() session.commit()
except Exception as exc: # pragma: no cover - unexpected provisioning errors except Exception as exc: # pragma: no cover - unexpected provisioning errors
session.add( _record_provisioning_failure(session, agent, str(exc))
ActivityEvent(
event_type="agent.provision.failed",
message=f"Provisioning message failed: {exc}",
agent_id=agent.id,
)
)
session.commit() session.commit()
return agent return agent
@@ -129,9 +122,8 @@ async def create_agent(
def get_agent( def get_agent(
agent_id: str, agent_id: str,
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> Agent: ) -> Agent:
require_admin(auth)
agent = session.get(Agent, agent_id) agent = session.get(Agent, agent_id)
if agent is None: if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@@ -143,9 +135,8 @@ def update_agent(
agent_id: str, agent_id: str,
payload: AgentUpdate, payload: AgentUpdate,
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> Agent: ) -> Agent:
require_admin(auth)
agent = session.get(Agent, agent_id) agent = session.get(Agent, agent_id)
if agent is None: if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@@ -164,9 +155,8 @@ def heartbeat_agent(
agent_id: str, agent_id: str,
payload: AgentHeartbeat, payload: AgentHeartbeat,
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> Agent: ) -> Agent:
require_admin(auth)
agent = session.get(Agent, agent_id) agent = session.get(Agent, agent_id)
if agent is None: if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@@ -185,9 +175,8 @@ def heartbeat_agent(
async def heartbeat_or_create_agent( async def heartbeat_or_create_agent(
payload: AgentHeartbeatCreate, payload: AgentHeartbeatCreate,
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> Agent: ) -> Agent:
require_admin(auth)
agent = session.exec(select(Agent).where(Agent.name == payload.name)).first() agent = session.exec(select(Agent).where(Agent.name == payload.name)).first()
if agent is None: if agent is None:
agent = Agent(name=payload.name, status=payload.status or "online") agent = Agent(name=payload.name, status=payload.status or "online")
@@ -197,81 +186,53 @@ async def heartbeat_or_create_agent(
session.commit() session.commit()
session.refresh(agent) session.refresh(agent)
if session_error: if session_error:
session.add( record_activity(
ActivityEvent( session,
event_type="agent.session.failed", event_type="agent.session.failed",
message=f"Session sync failed for {agent.name}: {session_error}", message=f"Session sync failed for {agent.name}: {session_error}",
agent_id=agent.id, agent_id=agent.id,
)
) )
else: else:
session.add( record_activity(
ActivityEvent( session,
event_type="agent.session.created", event_type="agent.session.created",
message=f"Session created for {agent.name}.", message=f"Session created for {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
)
) )
session.commit() session.commit()
try: try:
await send_provisioning_message(agent) await send_provisioning_message(agent)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
session.add( _record_provisioning_failure(session, agent, str(exc))
ActivityEvent(
event_type="agent.provision.failed",
message=f"Provisioning message failed: {exc}",
agent_id=agent.id,
)
)
session.commit() session.commit()
except Exception as exc: # pragma: no cover - unexpected provisioning errors except Exception as exc: # pragma: no cover - unexpected provisioning errors
session.add( _record_provisioning_failure(session, agent, str(exc))
ActivityEvent(
event_type="agent.provision.failed",
message=f"Provisioning message failed: {exc}",
agent_id=agent.id,
)
)
session.commit() session.commit()
elif not agent.openclaw_session_id: elif not agent.openclaw_session_id:
session_key, session_error = await _ensure_gateway_session(agent.name) session_key, session_error = await _ensure_gateway_session(agent.name)
agent.openclaw_session_id = session_key agent.openclaw_session_id = session_key
if session_error: if session_error:
session.add( record_activity(
ActivityEvent( session,
event_type="agent.session.failed", event_type="agent.session.failed",
message=f"Session sync failed for {agent.name}: {session_error}", message=f"Session sync failed for {agent.name}: {session_error}",
agent_id=agent.id, agent_id=agent.id,
)
) )
else: else:
session.add( record_activity(
ActivityEvent( session,
event_type="agent.session.created", event_type="agent.session.created",
message=f"Session created for {agent.name}.", message=f"Session created for {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
)
) )
session.commit() session.commit()
try: try:
await send_provisioning_message(agent) await send_provisioning_message(agent)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
session.add( _record_provisioning_failure(session, agent, str(exc))
ActivityEvent(
event_type="agent.provision.failed",
message=f"Provisioning message failed: {exc}",
agent_id=agent.id,
)
)
session.commit() session.commit()
except Exception as exc: # pragma: no cover - unexpected provisioning errors except Exception as exc: # pragma: no cover - unexpected provisioning errors
session.add( _record_provisioning_failure(session, agent, str(exc))
ActivityEvent(
event_type="agent.provision.failed",
message=f"Provisioning message failed: {exc}",
agent_id=agent.id,
)
)
session.commit() session.commit()
if payload.status: if payload.status:
agent.status = payload.status agent.status = payload.status
@@ -288,9 +249,8 @@ async def heartbeat_or_create_agent(
def delete_agent( def delete_agent(
agent_id: str, agent_id: str,
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, bool]: ) -> dict[str, bool]:
require_admin(auth)
agent = session.get(Agent, agent_id) agent = session.get(Agent, agent_id)
if agent: if agent:
session.delete(agent) session.delete(agent)

View File

@@ -2,14 +2,14 @@ from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from app.core.auth import get_auth_context from app.core.auth import AuthContext, get_auth_context
from app.schemas.users import UserRead from app.schemas.users import UserRead
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/bootstrap", response_model=UserRead) @router.post("/bootstrap", response_model=UserRead)
async def bootstrap_user(auth=Depends(get_auth_context)) -> UserRead: async def bootstrap_user(auth: AuthContext = Depends(get_auth_context)) -> UserRead:
if auth.actor_type != "user" or auth.user is None: if auth.actor_type != "user" or auth.user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return auth.user return UserRead.model_validate(auth.user)

View File

@@ -1,13 +1,13 @@
from __future__ import annotations from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends
from sqlmodel import Session, select from sqlmodel import Session, select
from app.core.auth import get_auth_context from app.api.deps import get_board_or_404, require_admin_auth
from app.core.auth import AuthContext
from app.db.session import get_session from app.db.session import get_session
from app.models.boards import Board from app.models.boards import Board
from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate
from app.services.admin_access import require_admin
router = APIRouter(prefix="/boards", tags=["boards"]) router = APIRouter(prefix="/boards", tags=["boards"])
@@ -15,9 +15,8 @@ router = APIRouter(prefix="/boards", tags=["boards"])
@router.get("", response_model=list[BoardRead]) @router.get("", response_model=list[BoardRead])
def list_boards( def list_boards(
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> list[Board]: ) -> list[Board]:
require_admin(auth)
return list(session.exec(select(Board))) return list(session.exec(select(Board)))
@@ -25,9 +24,8 @@ def list_boards(
def create_board( def create_board(
payload: BoardCreate, payload: BoardCreate,
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> Board: ) -> Board:
require_admin(auth)
board = Board.model_validate(payload) board = Board.model_validate(payload)
session.add(board) session.add(board)
session.commit() session.commit()
@@ -37,28 +35,19 @@ def create_board(
@router.get("/{board_id}", response_model=BoardRead) @router.get("/{board_id}", response_model=BoardRead)
def get_board( def get_board(
board_id: str, board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), auth: AuthContext = Depends(require_admin_auth),
auth=Depends(get_auth_context),
) -> Board: ) -> Board:
require_admin(auth)
board = session.get(Board, board_id)
if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return board return board
@router.patch("/{board_id}", response_model=BoardRead) @router.patch("/{board_id}", response_model=BoardRead)
def update_board( def update_board(
board_id: str,
payload: BoardUpdate, payload: BoardUpdate,
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), board: Board = Depends(get_board_or_404),
auth: AuthContext = Depends(require_admin_auth),
) -> Board: ) -> Board:
require_admin(auth)
board = session.get(Board, board_id)
if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
updates = payload.model_dump(exclude_unset=True) updates = payload.model_dump(exclude_unset=True)
for key, value in updates.items(): for key, value in updates.items():
setattr(board, key, value) setattr(board, key, value)
@@ -70,13 +59,10 @@ def update_board(
@router.delete("/{board_id}") @router.delete("/{board_id}")
def delete_board( def delete_board(
board_id: str,
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), board: Board = Depends(get_board_or_404),
auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, bool]: ) -> dict[str, bool]:
require_admin(auth) session.delete(board)
board = session.get(Board, board_id) session.commit()
if board:
session.delete(board)
session.commit()
return {"ok": True} return {"ok": True}

36
backend/app/api/deps.py Normal file
View File

@@ -0,0 +1,36 @@
from __future__ import annotations
from fastapi import Depends, HTTPException, status
from sqlmodel import Session
from app.core.auth import AuthContext, get_auth_context
from app.db.session import get_session
from app.models.boards import Board
from app.models.tasks import Task
from app.services.admin_access import require_admin
def require_admin_auth(auth: AuthContext = Depends(get_auth_context)) -> AuthContext:
require_admin(auth)
return auth
def get_board_or_404(
board_id: str,
session: Session = Depends(get_session),
) -> Board:
board = session.get(Board, board_id)
if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return board
def get_task_or_404(
task_id: str,
board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session),
) -> Task:
task = session.get(Task, task_id)
if task is None or task.board_id != board.id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return task

View File

@@ -2,7 +2,8 @@ from __future__ import annotations
from fastapi import APIRouter, Body, Depends, HTTPException, status from fastapi import APIRouter, Body, Depends, HTTPException, status
from app.core.auth import get_auth_context from app.api.deps import require_admin_auth
from app.core.auth import AuthContext
from app.core.config import settings from app.core.config import settings
from app.integrations.openclaw_gateway import ( from app.integrations.openclaw_gateway import (
OpenClawGatewayError, OpenClawGatewayError,
@@ -10,14 +11,12 @@ from app.integrations.openclaw_gateway import (
openclaw_call, openclaw_call,
send_message, send_message,
) )
from app.services.admin_access import require_admin
router = APIRouter(prefix="/gateway", tags=["gateway"]) router = APIRouter(prefix="/gateway", tags=["gateway"])
@router.get("/status") @router.get("/status")
async def gateway_status(auth=Depends(get_auth_context)) -> dict[str, object]: async def gateway_status(auth: AuthContext = Depends(require_admin_auth)) -> dict[str, object]:
require_admin(auth)
gateway_url = settings.openclaw_gateway_url or "ws://127.0.0.1:18789" gateway_url = settings.openclaw_gateway_url or "ws://127.0.0.1:18789"
try: try:
sessions = await openclaw_call("sessions.list") sessions = await openclaw_call("sessions.list")
@@ -40,8 +39,7 @@ async def gateway_status(auth=Depends(get_auth_context)) -> dict[str, object]:
@router.get("/sessions") @router.get("/sessions")
async def list_sessions(auth=Depends(get_auth_context)) -> dict[str, object]: async def list_sessions(auth: AuthContext = Depends(require_admin_auth)) -> dict[str, object]:
require_admin(auth)
try: try:
sessions = await openclaw_call("sessions.list") sessions = await openclaw_call("sessions.list")
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
@@ -52,8 +50,9 @@ async def list_sessions(auth=Depends(get_auth_context)) -> dict[str, object]:
@router.get("/sessions/{session_id}") @router.get("/sessions/{session_id}")
async def get_session(session_id: str, auth=Depends(get_auth_context)) -> dict[str, object]: async def get_session(
require_admin(auth) session_id: str, auth: AuthContext = Depends(require_admin_auth)
) -> dict[str, object]:
try: try:
sessions = await openclaw_call("sessions.list") sessions = await openclaw_call("sessions.list")
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
@@ -69,8 +68,9 @@ async def get_session(session_id: str, auth=Depends(get_auth_context)) -> dict[s
@router.get("/sessions/{session_id}/history") @router.get("/sessions/{session_id}/history")
async def get_session_history(session_id: str, auth=Depends(get_auth_context)) -> dict[str, object]: async def get_session_history(
require_admin(auth) session_id: str, auth: AuthContext = Depends(require_admin_auth)
) -> dict[str, object]:
try: try:
history = await get_chat_history(session_id) history = await get_chat_history(session_id)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
@@ -84,9 +84,8 @@ async def get_session_history(session_id: str, auth=Depends(get_auth_context)) -
async def send_session_message( async def send_session_message(
session_id: str, session_id: str,
payload: dict = Body(...), payload: dict = Body(...),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, bool]: ) -> dict[str, bool]:
require_admin(auth)
content = payload.get("content") content = payload.get("content")
if not content: if not content:
raise HTTPException( raise HTTPException(

View File

@@ -2,45 +2,36 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends
from sqlmodel import Session, select from sqlmodel import Session, select
from app.core.auth import get_auth_context from app.api.deps import get_board_or_404, get_task_or_404, require_admin_auth
from app.core.auth import AuthContext
from app.db.session import get_session from app.db.session import get_session
from app.models.activity_events import ActivityEvent
from app.models.boards import Board from app.models.boards import Board
from app.models.tasks import Task from app.models.tasks import Task
from app.schemas.tasks import TaskCreate, TaskRead, TaskUpdate from app.schemas.tasks import TaskCreate, TaskRead, TaskUpdate
from app.services.admin_access import require_admin from app.services.activity_log import record_activity
router = APIRouter(prefix="/boards/{board_id}/tasks", tags=["tasks"]) router = APIRouter(prefix="/boards/{board_id}/tasks", tags=["tasks"])
@router.get("", response_model=list[TaskRead]) @router.get("", response_model=list[TaskRead])
def list_tasks( def list_tasks(
board_id: str, board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> list[Task]: ) -> list[Task]:
require_admin(auth)
board = session.get(Board, board_id)
if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return list(session.exec(select(Task).where(Task.board_id == board.id))) return list(session.exec(select(Task).where(Task.board_id == board.id)))
@router.post("", response_model=TaskRead) @router.post("", response_model=TaskRead)
def create_task( def create_task(
board_id: str,
payload: TaskCreate, payload: TaskCreate,
board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> Task: ) -> Task:
require_admin(auth)
board = session.get(Board, board_id)
if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
task = Task.model_validate(payload) task = Task.model_validate(payload)
task.board_id = board.id task.board_id = board.id
if task.created_by_user_id is None and auth.user is not None: if task.created_by_user_id is None and auth.user is not None:
@@ -49,32 +40,23 @@ def create_task(
session.commit() session.commit()
session.refresh(task) session.refresh(task)
event = ActivityEvent( record_activity(
session,
event_type="task.created", event_type="task.created",
task_id=task.id, task_id=task.id,
message=f"Task created: {task.title}.", message=f"Task created: {task.title}.",
) )
session.add(event)
session.commit() session.commit()
return task return task
@router.patch("/{task_id}", response_model=TaskRead) @router.patch("/{task_id}", response_model=TaskRead)
def update_task( def update_task(
board_id: str,
task_id: str,
payload: TaskUpdate, payload: TaskUpdate,
task: Task = Depends(get_task_or_404),
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), auth: AuthContext = Depends(require_admin_auth),
) -> Task: ) -> Task:
require_admin(auth)
board = session.get(Board, board_id)
if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
task = session.get(Task, task_id)
if task is None or task.board_id != board.id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
previous_status = task.status previous_status = task.status
updates = payload.model_dump(exclude_unset=True) updates = payload.model_dump(exclude_unset=True)
for key, value in updates.items(): for key, value in updates.items():
@@ -91,25 +73,17 @@ def update_task(
else: else:
event_type = "task.updated" event_type = "task.updated"
message = f"Task updated: {task.title}." message = f"Task updated: {task.title}."
event = ActivityEvent(event_type=event_type, task_id=task.id, message=message) record_activity(session, event_type=event_type, task_id=task.id, message=message)
session.add(event)
session.commit() session.commit()
return task return task
@router.delete("/{task_id}") @router.delete("/{task_id}")
def delete_task( def delete_task(
board_id: str,
task_id: str,
session: Session = Depends(get_session), session: Session = Depends(get_session),
auth=Depends(get_auth_context), task: Task = Depends(get_task_or_404),
auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, bool]: ) -> dict[str, bool]:
require_admin(auth) session.delete(task)
board = session.get(Board, board_id) session.commit()
if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
task = session.get(Task, task_id)
if task and task.board_id == board.id:
session.delete(task)
session.commit()
return {"ok": True} return {"ok": True}

View File

@@ -58,10 +58,12 @@ async def _send_request(
async def _handle_challenge( async def _handle_challenge(
ws: websockets.WebSocketClientProtocol, first_message: str | None ws: websockets.WebSocketClientProtocol, first_message: str | bytes | None
) -> None: ) -> None:
if not first_message: if not first_message:
return return
if isinstance(first_message, bytes):
first_message = first_message.decode("utf-8")
data = json.loads(first_message) data = json.loads(first_message)
if data.get("type") != "event" or data.get("event") != "connect.challenge": if data.get("type") != "event" or data.get("event") != "connect.challenge":
return return

View File

@@ -0,0 +1,25 @@
from __future__ import annotations
from uuid import UUID
from sqlmodel import Session
from app.models.activity_events import ActivityEvent
def record_activity(
session: Session,
*,
event_type: str,
message: str,
agent_id: UUID | None = None,
task_id: UUID | None = None,
) -> ActivityEvent:
event = ActivityEvent(
event_type=event_type,
message=message,
agent_id=agent_id,
task_id=task_id,
)
session.add(event)
return event

View File

@@ -33,7 +33,17 @@ dev = [
"black==24.10.0", "black==24.10.0",
"flake8==7.1.1", "flake8==7.1.1",
"isort==5.13.2", "isort==5.13.2",
"mypy==1.11.2",
"pytest==8.3.3", "pytest==8.3.3",
"pytest-asyncio==0.24.0", "pytest-asyncio==0.24.0",
"ruff==0.6.9", "ruff==0.6.9",
] ]
[tool.mypy]
python_version = "3.12"
ignore_missing_imports = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unused_configs = true
check_untyped_defs = true
plugins = ["pydantic.mypy"]

View File

@@ -1,3 +1,4 @@
pytest==8.3.3 pytest==8.3.3
pytest-asyncio==0.24.0 pytest-asyncio==0.24.0
ruff==0.6.9 ruff==0.6.9
mypy==1.11.2

20
backend/uv.lock generated
View File

@@ -437,6 +437,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
] ]
[[package]]
name = "mypy"
version = "1.11.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806, upload-time = "2024-08-24T22:50:11.357Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/3a/ed7b12ecc3f6db2f664ccf85cb2e004d3e90bec928e9d7be6aa2f16b7cdf/mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", size = 10990335, upload-time = "2024-08-24T22:49:54.245Z" },
{ url = "https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", size = 10007119, upload-time = "2024-08-24T22:49:03.451Z" },
{ url = "https://files.pythonhosted.org/packages/f3/3c/350a9da895f8a7e87ade0028b962be0252d152e0c2fbaafa6f0658b4d0d4/mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", size = 12506856, upload-time = "2024-08-24T22:50:08.804Z" },
{ url = "https://files.pythonhosted.org/packages/b6/49/ee5adf6a49ff13f4202d949544d3d08abb0ea1f3e7f2a6d5b4c10ba0360a/mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", size = 12952066, upload-time = "2024-08-24T22:50:03.89Z" },
{ url = "https://files.pythonhosted.org/packages/27/c0/b19d709a42b24004d720db37446a42abadf844d5c46a2c442e2a074d70d9/mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", size = 9664000, upload-time = "2024-08-24T22:49:59.703Z" },
{ url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625, upload-time = "2024-08-24T22:50:01.842Z" },
]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "1.1.0" version = "1.1.0"
@@ -472,6 +490,7 @@ dev = [
{ name = "black" }, { name = "black" },
{ name = "flake8" }, { name = "flake8" },
{ name = "isort" }, { name = "isort" },
{ name = "mypy" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "ruff" }, { name = "ruff" },
@@ -486,6 +505,7 @@ requires-dist = [
{ name = "flake8", marker = "extra == 'dev'", specifier = "==7.1.1" }, { name = "flake8", marker = "extra == 'dev'", specifier = "==7.1.1" },
{ name = "httpx", specifier = "==0.27.2" }, { name = "httpx", specifier = "==0.27.2" },
{ name = "isort", marker = "extra == 'dev'", specifier = "==5.13.2" }, { name = "isort", marker = "extra == 'dev'", specifier = "==5.13.2" },
{ name = "mypy", marker = "extra == 'dev'", specifier = "==1.11.2" },
{ name = "psycopg", extras = ["binary"], specifier = "==3.2.1" }, { name = "psycopg", extras = ["binary"], specifier = "==3.2.1" },
{ name = "pydantic-settings", specifier = "==2.5.2" }, { name = "pydantic-settings", specifier = "==2.5.2" },
{ name = "pytest", marker = "extra == 'dev'", specifier = "==8.3.3" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==8.3.3" },