Files
fast-next-template/backend/app/schemas/syndarix/issue.py
Felipe Cardoso acd18ff694 chore(backend): standardize multiline formatting across modules
Reformatted multiline function calls, object definitions, and queries for improved code readability and consistency. Adjusted imports and constraints where necessary.
2026-01-03 01:35:18 +01:00

192 lines
5.7 KiB
Python

# app/schemas/syndarix/issue.py
"""
Pydantic schemas for Issue entity.
"""
from datetime import datetime
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from .enums import IssuePriority, IssueStatus, SyncStatus
class IssueBase(BaseModel):
"""Base issue schema with common fields."""
title: str = Field(..., min_length=1, max_length=500)
body: str = ""
status: IssueStatus = IssueStatus.OPEN
priority: IssuePriority = IssuePriority.MEDIUM
labels: list[str] = Field(default_factory=list)
story_points: int | None = Field(None, ge=0, le=100)
@field_validator("title")
@classmethod
def validate_title(cls, v: str) -> str:
"""Validate issue title."""
if not v or v.strip() == "":
raise ValueError("Issue title cannot be empty")
return v.strip()
@field_validator("labels")
@classmethod
def validate_labels(cls, v: list[str]) -> list[str]:
"""Validate and normalize labels."""
return [label.strip().lower() for label in v if label.strip()]
class IssueCreate(IssueBase):
"""Schema for creating a new issue."""
project_id: UUID
assigned_agent_id: UUID | None = None
human_assignee: str | None = Field(None, max_length=255)
sprint_id: UUID | None = None
# External tracker fields (optional, for importing from external systems)
external_tracker_type: Literal["gitea", "github", "gitlab"] | None = None
external_issue_id: str | None = Field(None, max_length=255)
remote_url: str | None = Field(None, max_length=1000)
external_issue_number: int | None = None
class IssueUpdate(BaseModel):
"""Schema for updating an issue."""
title: str | None = Field(None, min_length=1, max_length=500)
body: str | None = None
status: IssueStatus | None = None
priority: IssuePriority | None = None
labels: list[str] | None = None
assigned_agent_id: UUID | None = None
human_assignee: str | None = Field(None, max_length=255)
sprint_id: UUID | None = None
story_points: int | None = Field(None, ge=0, le=100)
sync_status: SyncStatus | None = None
@field_validator("title")
@classmethod
def validate_title(cls, v: str | None) -> str | None:
"""Validate issue title."""
if v is not None and (not v or v.strip() == ""):
raise ValueError("Issue title cannot be empty")
return v.strip() if v else v
@field_validator("labels")
@classmethod
def validate_labels(cls, v: list[str] | None) -> list[str] | None:
"""Validate and normalize labels."""
if v is None:
return v
return [label.strip().lower() for label in v if label.strip()]
class IssueClose(BaseModel):
"""Schema for closing an issue."""
resolution: str | None = None # Optional resolution note
class IssueAssign(BaseModel):
"""Schema for assigning an issue."""
assigned_agent_id: UUID | None = None
human_assignee: str | None = Field(None, max_length=255)
@model_validator(mode="after")
def validate_assignment(self) -> "IssueAssign":
"""Ensure only one type of assignee is set."""
if self.assigned_agent_id and self.human_assignee:
raise ValueError("Cannot assign to both an agent and a human. Choose one.")
return self
class IssueSyncUpdate(BaseModel):
"""Schema for updating sync-related fields."""
sync_status: SyncStatus
last_synced_at: datetime | None = None
external_updated_at: datetime | None = None
class IssueInDB(IssueBase):
"""Schema for issue in database."""
id: UUID
project_id: UUID
assigned_agent_id: UUID | None = None
human_assignee: str | None = None
sprint_id: UUID | None = None
external_tracker_type: str | None = None
external_issue_id: str | None = None
remote_url: str | None = None
external_issue_number: int | None = None
sync_status: SyncStatus = SyncStatus.SYNCED
last_synced_at: datetime | None = None
external_updated_at: datetime | None = None
closed_at: datetime | None = None
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class IssueResponse(BaseModel):
"""Schema for issue API responses."""
id: UUID
project_id: UUID
title: str
body: str
status: IssueStatus
priority: IssuePriority
labels: list[str] = Field(default_factory=list)
assigned_agent_id: UUID | None = None
human_assignee: str | None = None
sprint_id: UUID | None = None
story_points: int | None = None
external_tracker_type: str | None = None
external_issue_id: str | None = None
remote_url: str | None = None
external_issue_number: int | None = None
sync_status: SyncStatus = SyncStatus.SYNCED
last_synced_at: datetime | None = None
external_updated_at: datetime | None = None
closed_at: datetime | None = None
created_at: datetime
updated_at: datetime
# Expanded fields from relationships
project_name: str | None = None
project_slug: str | None = None
sprint_name: str | None = None
assigned_agent_type_name: str | None = None
model_config = ConfigDict(from_attributes=True)
class IssueListResponse(BaseModel):
"""Schema for paginated issue list responses."""
issues: list[IssueResponse]
total: int
page: int
page_size: int
pages: int
class IssueStats(BaseModel):
"""Schema for issue statistics."""
total: int
open: int
in_progress: int
in_review: int
blocked: int
closed: int
by_priority: dict[str, int]
total_story_points: int | None = None
completed_story_points: int | None = None