Infrastructure: - Add Redis and Celery workers to all docker-compose files - Fix celery migration race condition in entrypoint.sh - Add healthchecks and resource limits to dev compose - Update .env.template with Redis/Celery variables Backend Models & Schemas: - Rename Sprint.completed_points to velocity (per requirements) - Add AgentInstance.name as required field - Rename Issue external tracker fields for consistency - Add IssueSource and TrackerType enums - Add Project.default_tracker_type field Backend Fixes: - Add Celery retry configuration with exponential backoff - Remove unused sequence counter from EventBus - Add mypy overrides for test dependencies - Fix test file using wrong schema (UserUpdate -> dict) Frontend Fixes: - Fix memory leak in useProjectEvents (proper cleanup) - Fix race condition with stale closure in reconnection - Sync TokenWithUser type with regenerated API client - Fix expires_in null handling in useAuth - Clean up unused imports in prototype pages - Add ESLint relaxed rules for prototype files CI/CD: - Add E2E testing stage with Testcontainers - Add security scanning with Trivy and pip-audit - Add dependency caching for faster builds Tests: - Update all tests to use renamed fields (velocity, name, etc.) - Fix 14 schema test failures - All 1500 tests pass with 91% coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
194 lines
5.7 KiB
Python
194 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
|