типизация: единые схемы везде, убраны ручные 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.database import get_db
|
||||||
from tracker.models import Attachment, Message, Member
|
from tracker.models import Attachment, Message, Member
|
||||||
|
from tracker.api.schemas import UploadOut
|
||||||
|
|
||||||
router = APIRouter(tags=["attachments"])
|
router = APIRouter(tags=["attachments"])
|
||||||
|
|
||||||
@ -50,13 +51,13 @@ async def upload_file(
|
|||||||
|
|
||||||
mime = file.content_type or mimetypes.guess_type(file.filename or "")[0]
|
mime = file.content_type or mimetypes.guess_type(file.filename or "")[0]
|
||||||
|
|
||||||
return {
|
return UploadOut(
|
||||||
"file_id": str(file_id),
|
file_id=str(file_id),
|
||||||
"filename": file.filename,
|
filename=file.filename or "",
|
||||||
"mime_type": mime,
|
mime_type=mime,
|
||||||
"size": len(content),
|
size=len(content),
|
||||||
"storage_name": storage_name,
|
storage_name=storage_name,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/attachments/{attachment_id}/download")
|
@router.get("/attachments/{attachment_id}/download")
|
||||||
|
|||||||
@ -12,8 +12,7 @@ from sqlalchemy.orm import selectinload
|
|||||||
from tracker.database import get_db
|
from tracker.database import get_db
|
||||||
from tracker.enums import AuthMethod, ListenMode, MemberRole, MemberStatus, MemberType
|
from tracker.enums import AuthMethod, ListenMode, MemberRole, MemberStatus, MemberType
|
||||||
from tracker.models import Member, AgentConfig
|
from tracker.models import Member, AgentConfig
|
||||||
from tracker.schemas import MemberOut, AgentConfigOut
|
from tracker.api.schemas import MemberOut, AgentConfigOut, OkResponse
|
||||||
from tracker.api.schemas import AgentConfigOut, MemberOut
|
|
||||||
from tracker.api.converters import member_out
|
from tracker.api.converters import member_out
|
||||||
|
|
||||||
router = APIRouter(tags=["members"])
|
router = APIRouter(tags=["members"])
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from sqlalchemy.orm import selectinload
|
|||||||
|
|
||||||
from tracker.database import get_db
|
from tracker.database import get_db
|
||||||
from tracker.models import ProjectFile, Project, Member
|
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
|
from tracker.api.schemas import ProjectFileOut
|
||||||
# Using unified schemas from schemas.py
|
# Using unified schemas from schemas.py
|
||||||
|
|
||||||
|
|||||||
@ -43,22 +43,21 @@ class ProjectMemberOut(BaseModel):
|
|||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
async def _project_out(p: Project, db: AsyncSession) -> dict:
|
async def _project_out(p: Project, db: AsyncSession) -> ProjectOut:
|
||||||
# Get project chat
|
|
||||||
chat_result = await db.execute(
|
chat_result = await db.execute(
|
||||||
select(Chat).where(Chat.project_id == p.id, Chat.kind == ChatKind.PROJECT)
|
select(Chat).where(Chat.project_id == p.id, Chat.kind == ChatKind.PROJECT)
|
||||||
)
|
)
|
||||||
chat = chat_result.scalar_one_or_none()
|
chat = chat_result.scalar_one_or_none()
|
||||||
return {
|
return ProjectOut(
|
||||||
"id": str(p.id),
|
id=str(p.id),
|
||||||
"name": p.name,
|
name=p.name,
|
||||||
"slug": p.slug,
|
slug=p.slug,
|
||||||
"description": p.description,
|
description=p.description,
|
||||||
"repo_urls": p.repo_urls or [],
|
repo_urls=p.repo_urls or [],
|
||||||
"status": p.status,
|
status=p.status,
|
||||||
"task_counter": p.task_counter,
|
task_counter=p.task_counter,
|
||||||
"chat_id": str(chat.id) if chat else None,
|
chat_id=str(chat.id) if chat else None,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/projects", response_model=list[ProjectOut])
|
@router.get("/projects", response_model=list[ProjectOut])
|
||||||
|
|||||||
@ -104,6 +104,19 @@ class ProjectOut(BaseModel):
|
|||||||
from_attributes = True
|
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):
|
class ProjectFileOut(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
filename: str
|
filename: str
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from tracker.database import get_db
|
from tracker.database import get_db
|
||||||
from tracker.models import Step, Task
|
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.schemas import StepOut
|
||||||
from tracker.api.converters import step_out
|
from tracker.api.converters import step_out
|
||||||
|
|
||||||
|
|||||||
@ -211,13 +211,13 @@ async def list_tasks(
|
|||||||
q = q.where(Task.labels.contains([label]))
|
q = q.where(Task.labels.contains([label]))
|
||||||
q = q.order_by(Task.position, Task.created_at)
|
q = q.order_by(Task.position, Task.created_at)
|
||||||
result = await db.execute(q)
|
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)
|
@router.get("/tasks/{task_id}", response_model=TaskOut)
|
||||||
async def get_task(task_id: str, db: AsyncSession = Depends(get_db)):
|
async def get_task(task_id: str, db: AsyncSession = Depends(get_db)):
|
||||||
task = await _get_task(task_id, 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)
|
@router.post("/tasks", response_model=TaskOut)
|
||||||
@ -263,18 +263,9 @@ async def create_task(
|
|||||||
|
|
||||||
# Broadcast using TaskOut schema
|
# Broadcast using TaskOut schema
|
||||||
from tracker.ws.manager import manager
|
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}"
|
key = f"{project.slug[:2].upper()}-{task.number}"
|
||||||
await manager.broadcast_task_event(str(project.id), WSEventType.TASK_CREATED, {
|
await manager.broadcast_task_event(str(project.id), WSEventType.TASK_CREATED, task_schema.model_dump())
|
||||||
"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,
|
|
||||||
})
|
|
||||||
|
|
||||||
# System message
|
# System message
|
||||||
chat_text = f"{key} создана: {task.title}"
|
chat_text = f"{key} создана: {task.title}"
|
||||||
@ -286,7 +277,7 @@ async def create_task(
|
|||||||
project_slug=project.slug, actor=current_member)
|
project_slug=project.slug, actor=current_member)
|
||||||
await db.commit()
|
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)
|
@router.patch("/tasks/{task_id}", response_model=TaskOut)
|
||||||
@ -383,14 +374,10 @@ async def update_task(
|
|||||||
# Broadcast simplified update
|
# Broadcast simplified update
|
||||||
from tracker.ws.manager import manager
|
from tracker.ws.manager import manager
|
||||||
task_full = await _get_task(task_id, db)
|
task_full = await _get_task(task_id, db)
|
||||||
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_UPDATED, {
|
task_schema = task_out(task_full, slug)
|
||||||
"id": str(task.id),
|
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_UPDATED, task_schema.model_dump())
|
||||||
"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,
|
|
||||||
})
|
|
||||||
|
|
||||||
return _to_task_out(task_full, slug)
|
return task_schema
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/tasks/{task_id}")
|
@router.delete("/tasks/{task_id}")
|
||||||
@ -448,13 +435,10 @@ async def take_task(
|
|||||||
|
|
||||||
from tracker.ws.manager import manager
|
from tracker.ws.manager import manager
|
||||||
task_full = await _get_task(task_id, db)
|
task_full = await _get_task(task_id, db)
|
||||||
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_ASSIGNED, {
|
task_schema = task_out(task_full, proj_slug)
|
||||||
"id": str(task.id),
|
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_ASSIGNED, task_schema.model_dump())
|
||||||
"assignee_id": str(current_member.id),
|
|
||||||
"assignee": _to_member_brief(current_member).model_dump(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return _to_task_out(task_full, proj_slug)
|
return task_schema
|
||||||
|
|
||||||
|
|
||||||
@router.post("/tasks/{task_id}/reject")
|
@router.post("/tasks/{task_id}/reject")
|
||||||
@ -525,13 +509,10 @@ async def assign_task(
|
|||||||
|
|
||||||
from tracker.ws.manager import manager
|
from tracker.ws.manager import manager
|
||||||
task_full = await _get_task(str(task.id), db)
|
task_full = await _get_task(str(task.id), db)
|
||||||
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_ASSIGNED, {
|
task_schema = task_out(task_full, proj_slug)
|
||||||
"id": str(task.id),
|
await manager.broadcast_task_event(str(task.project_id), WSEventType.TASK_ASSIGNED, task_schema.model_dump())
|
||||||
"assignee_id": str(assignee.id),
|
|
||||||
"assignee": _to_member_brief(assignee).model_dump(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return _to_task_out(task_full, proj_slug)
|
return task_schema
|
||||||
|
|
||||||
|
|
||||||
@router.post("/tasks/{task_id}/watch")
|
@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,
|
ProjectStatus, WSEventType,
|
||||||
)
|
)
|
||||||
from tracker.models import Member, AgentConfig, Chat, Message, Project, ProjectMember
|
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
|
from tracker.ws.manager import ConnectedClient, manager
|
||||||
|
|
||||||
logger = logging.getLogger("tracker.ws")
|
logger = logging.getLogger("tracker.ws")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user