типизация: единые схемы везде, убраны ручные dict-ы из broadcasts, удалён дублирующий schemas.py
Some checks failed
Deploy Tracker / deploy (push) Failing after 3s

This commit is contained in:
markov 2026-02-25 14:14:29 +01:00
parent be5fc6e99e
commit d0c0c87120
9 changed files with 50 additions and 154 deletions

View File

@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from tracker.database import get_db
from tracker.models import Attachment, Message, Member
from tracker.api.schemas import UploadOut
router = APIRouter(tags=["attachments"])
@ -50,13 +51,13 @@ async def upload_file(
mime = file.content_type or mimetypes.guess_type(file.filename or "")[0]
return {
"file_id": str(file_id),
"filename": file.filename,
"mime_type": mime,
"size": len(content),
"storage_name": storage_name,
}
return UploadOut(
file_id=str(file_id),
filename=file.filename or "",
mime_type=mime,
size=len(content),
storage_name=storage_name,
)
@router.get("/attachments/{attachment_id}/download")

View File

@ -12,8 +12,7 @@ from sqlalchemy.orm import selectinload
from tracker.database import get_db
from tracker.enums import AuthMethod, ListenMode, MemberRole, MemberStatus, MemberType
from tracker.models import Member, AgentConfig
from tracker.schemas import MemberOut, AgentConfigOut
from tracker.api.schemas import AgentConfigOut, MemberOut
from tracker.api.schemas import MemberOut, AgentConfigOut, OkResponse
from tracker.api.converters import member_out
router = APIRouter(tags=["members"])

View File

@ -14,7 +14,7 @@ from sqlalchemy.orm import selectinload
from tracker.database import get_db
from tracker.models import ProjectFile, Project, Member
from tracker.schemas import ProjectFileOut, MemberBrief
from tracker.api.schemas import ProjectFileOut, MemberBrief
from tracker.api.schemas import ProjectFileOut
# Using unified schemas from schemas.py

View File

@ -43,22 +43,21 @@ class ProjectMemberOut(BaseModel):
from_attributes = True
async def _project_out(p: Project, db: AsyncSession) -> dict:
# Get project chat
async def _project_out(p: Project, db: AsyncSession) -> ProjectOut:
chat_result = await db.execute(
select(Chat).where(Chat.project_id == p.id, Chat.kind == ChatKind.PROJECT)
)
chat = chat_result.scalar_one_or_none()
return {
"id": str(p.id),
"name": p.name,
"slug": p.slug,
"description": p.description,
"repo_urls": p.repo_urls or [],
"status": p.status,
"task_counter": p.task_counter,
"chat_id": str(chat.id) if chat else None,
}
return ProjectOut(
id=str(p.id),
name=p.name,
slug=p.slug,
description=p.description,
repo_urls=p.repo_urls or [],
status=p.status,
task_counter=p.task_counter,
chat_id=str(chat.id) if chat else None,
)
@router.get("/projects", response_model=list[ProjectOut])

View File

@ -104,6 +104,19 @@ class ProjectOut(BaseModel):
from_attributes = True
class UploadOut(BaseModel):
"""Response from file upload endpoint."""
file_id: str
filename: str
mime_type: str | None = None
size: int
storage_name: str
class OkResponse(BaseModel):
ok: bool = True
class ProjectFileOut(BaseModel):
id: str
filename: str

View File

@ -9,7 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from tracker.database import get_db
from tracker.models import Step, Task
from tracker.schemas import StepOut
from tracker.api.schemas import StepOut
from tracker.api.schemas import StepOut
from tracker.api.converters import step_out

View File

@ -211,13 +211,13 @@ async def list_tasks(
q = q.where(Task.labels.contains([label]))
q = q.order_by(Task.position, Task.created_at)
result = await db.execute(q)
return [_to_task_out(t, t.project.slug if t.project else "") for t in result.scalars()]
return [task_out(t, t.project.slug if t.project else "") for t in result.scalars()]
@router.get("/tasks/{task_id}", response_model=TaskOut)
async def get_task(task_id: str, db: AsyncSession = Depends(get_db)):
task = await _get_task(task_id, db)
return _to_task_out(task, task.project.slug if task.project else "")
return task_out(task, task.project.slug if task.project else "")
@router.post("/tasks", response_model=TaskOut)
@ -263,18 +263,9 @@ async def create_task(
# Broadcast using TaskOut schema
from tracker.ws.manager import manager
task_schema = _to_task_out(task_full, project.slug)
task_schema = task_out(task_full, project.slug)
key = f"{project.slug[:2].upper()}-{task.number}"
await manager.broadcast_task_event(str(project.id), WSEventType.TASK_CREATED, {
"id": str(task.id),
"project_id": str(project.id),
"number": task.number,
"key": key,
"title": task.title,
"status": task.status,
"assignee_id": str(assignee_id) if assignee_id else None,
"assignee": _to_member_brief(task_full.assignee).model_dump() if task_full.assignee else None,
})
await manager.broadcast_task_event(str(project.id), WSEventType.TASK_CREATED, task_schema.model_dump())
# System message
chat_text = f"{key} создана: {task.title}"
@ -286,7 +277,7 @@ async def create_task(
project_slug=project.slug, actor=current_member)
await db.commit()
return _to_task_out(task_full, project.slug)
return task_out(task_full, project.slug)
@router.patch("/tasks/{task_id}", response_model=TaskOut)
@ -383,14 +374,10 @@ async def update_task(
# Broadcast simplified update
from tracker.ws.manager import manager
task_full = await _get_task(task_id, db)
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_UPDATED, {
"id": str(task.id),
"status": task.status,
"assignee_id": str(task.assignee_id) if task.assignee_id else None,
"assignee": _to_member_brief(task_full.assignee).model_dump() if task_full.assignee else None,
})
task_schema = task_out(task_full, slug)
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_UPDATED, task_schema.model_dump())
return _to_task_out(task_full, slug)
return task_schema
@router.delete("/tasks/{task_id}")
@ -448,13 +435,10 @@ async def take_task(
from tracker.ws.manager import manager
task_full = await _get_task(task_id, db)
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_ASSIGNED, {
"id": str(task.id),
"assignee_id": str(current_member.id),
"assignee": _to_member_brief(current_member).model_dump(),
})
task_schema = task_out(task_full, proj_slug)
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_ASSIGNED, task_schema.model_dump())
return _to_task_out(task_full, proj_slug)
return task_schema
@router.post("/tasks/{task_id}/reject")
@ -525,13 +509,10 @@ async def assign_task(
from tracker.ws.manager import manager
task_full = await _get_task(str(task.id), db)
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_ASSIGNED, {
"id": str(task.id),
"assignee_id": str(assignee.id),
"assignee": _to_member_brief(assignee).model_dump(),
})
task_schema = task_out(task_full, proj_slug)
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_ASSIGNED, task_schema.model_dump())
return _to_task_out(task_full, proj_slug)
return task_schema
@router.post("/tasks/{task_id}/watch")

View File

@ -1,97 +0,0 @@
"""Unified Pydantic schemas for consistent data serialization across REST API and WebSocket broadcasts."""
from pydantic import BaseModel
class MemberBrief(BaseModel):
"""Краткий member для вложенных объектов (author, assignee, reviewer)"""
id: str
slug: str
name: str
class AgentConfigOut(BaseModel):
capabilities: list[str] = []
chat_listen: str = "mentions"
task_listen: str = "mentions"
prompt: str | None = None
model: str | None = None
class MemberOut(BaseModel):
id: str
slug: str
name: str
type: str
role: str
status: str
avatar_url: str | None = None
is_active: bool
token: str | None = None # Показывается только для агентов при создании
agent_config: AgentConfigOut | None = None
class MessageOut(BaseModel):
id: str
chat_id: str | None = None
task_id: str | None = None
parent_id: str | None = None
author_type: str
author_id: str | None = None
author: MemberBrief | None = None
content: str
mentions: list[str] = []
voice_url: str | None = None
attachments: list[dict] = []
created_at: str
class StepOut(BaseModel):
id: str
task_id: str
title: str
done: bool
position: int
created_at: str
class TaskOut(BaseModel):
id: str
project_id: str
number: int
key: str
title: str
description: str | None = None
type: str
status: str
priority: str
labels: list[str] = []
assignee: MemberBrief | None = None
reviewer: MemberBrief | None = None
assignee_id: str | None = None
reviewer_id: str | None = None
watcher_ids: list[str] = []
depends_on: list[str] = []
position: int
time_spent: int = 0
steps: list[StepOut] = []
created_at: str
updated_at: str
class AttachmentOut(BaseModel):
id: str
filename: str
mime_type: str | None = None
size: int
class ProjectFileOut(BaseModel):
id: str
filename: str
description: str | None = None
mime_type: str | None = None
size: int
uploaded_by: MemberBrief | None = None
created_at: str
updated_at: str

View File

@ -13,7 +13,7 @@ from tracker.enums import (
ProjectStatus, WSEventType,
)
from tracker.models import Member, AgentConfig, Chat, Message, Project, ProjectMember
from tracker.schemas import MessageOut, MemberBrief
from tracker.api.schemas import MessageOut, MemberBrief
from tracker.ws.manager import ConnectedClient, manager
logger = logging.getLogger("tracker.ws")