типизация: единые схемы везде, убраны ручные dict-ы из broadcasts, удалён дублирующий schemas.py
Some checks failed
Deploy Tracker / deploy (push) Failing after 3s
Some checks failed
Deploy Tracker / deploy (push) Failing after 3s
This commit is contained in:
parent
be5fc6e99e
commit
d0c0c87120
@ -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")
|
||||
|
||||
@ -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"])
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user