- Add cancel_sprint and delete_sprint endpoints to sprints.py - Add unassign_issue endpoint to issues.py - Add remove_issue_from_sprint endpoint to sprints.py - Add CRUD methods: remove_sprint_from_issues, unassign, remove_from_sprint - Add validation to prevent closed issues in active/planned sprints - Add authorization tests for SSE events endpoint - Fix IDOR vulnerabilities in agents.py and projects.py - Add Syndarix models migration (0004) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
132 lines
3.9 KiB
Python
132 lines
3.9 KiB
Python
# app/schemas/syndarix/project.py
|
|
"""
|
|
Pydantic schemas for Project entity.
|
|
"""
|
|
|
|
import re
|
|
from datetime import datetime
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
from .enums import AutonomyLevel, ProjectStatus
|
|
|
|
|
|
class ProjectBase(BaseModel):
|
|
"""Base project schema with common fields."""
|
|
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
slug: str | None = Field(None, min_length=1, max_length=255)
|
|
description: str | None = None
|
|
autonomy_level: AutonomyLevel = AutonomyLevel.MILESTONE
|
|
status: ProjectStatus = ProjectStatus.ACTIVE
|
|
settings: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
@field_validator("slug")
|
|
@classmethod
|
|
def validate_slug(cls, v: str | None) -> str | None:
|
|
"""Validate slug format: lowercase, alphanumeric, hyphens only."""
|
|
if v is None:
|
|
return v
|
|
if not re.match(r"^[a-z0-9-]+$", v):
|
|
raise ValueError(
|
|
"Slug must contain only lowercase letters, numbers, and hyphens"
|
|
)
|
|
if v.startswith("-") or v.endswith("-"):
|
|
raise ValueError("Slug cannot start or end with a hyphen")
|
|
if "--" in v:
|
|
raise ValueError("Slug cannot contain consecutive hyphens")
|
|
return v
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def validate_name(cls, v: str) -> str:
|
|
"""Validate project name."""
|
|
if not v or v.strip() == "":
|
|
raise ValueError("Project name cannot be empty")
|
|
return v.strip()
|
|
|
|
|
|
class ProjectCreate(ProjectBase):
|
|
"""Schema for creating a new project."""
|
|
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
slug: str = Field(..., min_length=1, max_length=255)
|
|
owner_id: UUID | None = None
|
|
|
|
|
|
class ProjectUpdate(BaseModel):
|
|
"""Schema for updating a project.
|
|
|
|
Note: owner_id is intentionally excluded to prevent IDOR vulnerabilities.
|
|
Project ownership transfer should be done via a dedicated endpoint with
|
|
proper authorization checks.
|
|
"""
|
|
|
|
name: str | None = Field(None, min_length=1, max_length=255)
|
|
slug: str | None = Field(None, min_length=1, max_length=255)
|
|
description: str | None = None
|
|
autonomy_level: AutonomyLevel | None = None
|
|
status: ProjectStatus | None = None
|
|
settings: dict[str, Any] | None = None
|
|
|
|
@field_validator("slug")
|
|
@classmethod
|
|
def validate_slug(cls, v: str | None) -> str | None:
|
|
"""Validate slug format."""
|
|
if v is None:
|
|
return v
|
|
if not re.match(r"^[a-z0-9-]+$", v):
|
|
raise ValueError(
|
|
"Slug must contain only lowercase letters, numbers, and hyphens"
|
|
)
|
|
if v.startswith("-") or v.endswith("-"):
|
|
raise ValueError("Slug cannot start or end with a hyphen")
|
|
if "--" in v:
|
|
raise ValueError("Slug cannot contain consecutive hyphens")
|
|
return v
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def validate_name(cls, v: str | None) -> str | None:
|
|
"""Validate project name."""
|
|
if v is not None and (not v or v.strip() == ""):
|
|
raise ValueError("Project name cannot be empty")
|
|
return v.strip() if v else v
|
|
|
|
|
|
class ProjectInDB(ProjectBase):
|
|
"""Schema for project in database."""
|
|
|
|
id: UUID
|
|
owner_id: UUID | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class ProjectResponse(ProjectBase):
|
|
"""Schema for project API responses."""
|
|
|
|
id: UUID
|
|
owner_id: UUID | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
agent_count: int | None = 0
|
|
issue_count: int | None = 0
|
|
active_sprint_name: str | None = None
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class ProjectListResponse(BaseModel):
|
|
"""Schema for paginated project list responses."""
|
|
|
|
projects: list[ProjectResponse]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
pages: int
|