Reformatted multiline function calls, object definitions, and queries for improved code readability and consistency. Adjusted imports and constraints where necessary.
192 lines
5.7 KiB
Python
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
|