diff --git a/src/tracker/api/attachments.py b/src/tracker/api/attachments.py index d9fad5c..19da55a 100644 --- a/src/tracker/api/attachments.py +++ b/src/tracker/api/attachments.py @@ -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") diff --git a/src/tracker/api/members.py b/src/tracker/api/members.py index 003d47c..4062469 100644 --- a/src/tracker/api/members.py +++ b/src/tracker/api/members.py @@ -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"]) diff --git a/src/tracker/api/project_files.py b/src/tracker/api/project_files.py index f4be235..0d337df 100644 --- a/src/tracker/api/project_files.py +++ b/src/tracker/api/project_files.py @@ -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 diff --git a/src/tracker/api/projects.py b/src/tracker/api/projects.py index c9859f3..54f1c4e 100644 --- a/src/tracker/api/projects.py +++ b/src/tracker/api/projects.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]) diff --git a/src/tracker/api/schemas.py b/src/tracker/api/schemas.py index 5a0b22a..47a116a 100644 --- a/src/tracker/api/schemas.py +++ b/src/tracker/api/schemas.py @@ -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 diff --git a/src/tracker/api/steps.py b/src/tracker/api/steps.py index eca1359..888c91d 100644 --- a/src/tracker/api/steps.py +++ b/src/tracker/api/steps.py @@ -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 diff --git a/src/tracker/api/tasks.py b/src/tracker/api/tasks.py index eaca332..53e76bb 100644 --- a/src/tracker/api/tasks.py +++ b/src/tracker/api/tasks.py @@ -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") diff --git a/src/tracker/schemas.py b/src/tracker/schemas.py deleted file mode 100644 index 29e7816..0000000 --- a/src/tracker/schemas.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/tracker/ws/handler.py b/src/tracker/ws/handler.py index 2d30ebe..234cc0b 100644 --- a/src/tracker/ws/handler.py +++ b/src/tracker/ws/handler.py @@ -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")