forked from cardosofelipe/pragma-stack
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
|