mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Add a per-project `quality_command` (model + ORM + API + panel + migration 029) that the pre-submit gate prefers over the lint/typecheck pair. Pointed at the new `make gate` target (ruff format --check + ruff check + mypy + xenon, no tests), i_am_done now catches lint, type AND complexity failures in the developer's workspace before QA — closing the gap where over-complex code only failed in CI. Falls back to lint+typecheck when unset; a no-op when no commands are configured.
196 lines
5.7 KiB
Python
196 lines
5.7 KiB
Python
"""
|
|
Project API Schemas
|
|
|
|
Request/response models for project endpoints.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING
|
|
from typing import cast as typing_cast
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from roboco.models.base import Team
|
|
|
|
if TYPE_CHECKING:
|
|
from roboco.db.tables import ProjectTable
|
|
|
|
|
|
# =============================================================================
|
|
# RESPONSE MODELS
|
|
# =============================================================================
|
|
|
|
|
|
class ProjectResponse(BaseModel):
|
|
"""Response model for project information."""
|
|
|
|
id: UUID
|
|
name: str
|
|
slug: str
|
|
git_url: str
|
|
default_branch: str
|
|
protected_branches: list[str]
|
|
assigned_cell: Team
|
|
|
|
# Git authentication status (token never exposed, only boolean)
|
|
has_git_token: bool = False
|
|
|
|
# Optional commands
|
|
test_command: str | None = None
|
|
lint_command: str | None = None
|
|
format_command: str | None = None
|
|
typecheck_command: str | None = None
|
|
build_command: str | None = None
|
|
quality_command: str | None = None
|
|
|
|
# Runtime state
|
|
workspace_path: str | None = None
|
|
last_synced_at: datetime | None = None
|
|
head_commit: str | None = None
|
|
|
|
# Metadata
|
|
created_by: UUID
|
|
is_active: bool
|
|
created_at: datetime
|
|
updated_at: datetime | None = None
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class ProjectSummaryResponse(BaseModel):
|
|
"""Compact project response for list views."""
|
|
|
|
id: UUID
|
|
name: str
|
|
slug: str
|
|
git_url: str
|
|
default_branch: str
|
|
assigned_cell: Team
|
|
is_active: bool
|
|
has_workspace: bool = False
|
|
has_git_token: bool = False
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
# =============================================================================
|
|
# REQUEST MODELS
|
|
# =============================================================================
|
|
|
|
|
|
class ProjectCreateRequest(BaseModel):
|
|
"""Request model for creating a project."""
|
|
|
|
name: str = Field(..., min_length=1, max_length=100)
|
|
slug: str = Field(..., min_length=1, max_length=50, pattern=r"^[a-z0-9-]+$")
|
|
git_url: str
|
|
default_branch: str = "master"
|
|
protected_branches: list[str] | None = Field(
|
|
default=None,
|
|
description="Branches to protect. Defaults to [default_branch].",
|
|
)
|
|
assigned_cell: Team
|
|
|
|
# Git authentication (will be encrypted and stored securely)
|
|
git_token: str | None = Field(
|
|
default=None,
|
|
description="GitHub PAT for clone/push/PR (stored encrypted, never returned)",
|
|
)
|
|
|
|
# Optional commands
|
|
test_command: str | None = None
|
|
lint_command: str | None = None
|
|
format_command: str | None = None
|
|
typecheck_command: str | None = None
|
|
build_command: str | None = None
|
|
quality_command: str | None = None
|
|
|
|
|
|
class ProjectUpdateRequest(BaseModel):
|
|
"""Request model for updating a project."""
|
|
|
|
name: str | None = None
|
|
git_url: str | None = None
|
|
default_branch: str | None = None
|
|
protected_branches: list[str] | None = None
|
|
assigned_cell: Team | None = None
|
|
|
|
# Git authentication (empty string clears token, None leaves unchanged)
|
|
git_token: str | None = Field(
|
|
default=None,
|
|
description="GitHub PAT (empty string clears, None leaves unchanged)",
|
|
)
|
|
|
|
# Commands
|
|
test_command: str | None = None
|
|
lint_command: str | None = None
|
|
format_command: str | None = None
|
|
typecheck_command: str | None = None
|
|
build_command: str | None = None
|
|
quality_command: str | None = None
|
|
|
|
# State
|
|
is_active: bool | None = None
|
|
|
|
|
|
class SetWorkspaceRequest(BaseModel):
|
|
"""Request to set project workspace path."""
|
|
|
|
workspace_path: str
|
|
|
|
|
|
class SyncStateRequest(BaseModel):
|
|
"""Request to update sync state."""
|
|
|
|
head_commit: str
|
|
|
|
|
|
# =============================================================================
|
|
# CONVERTERS
|
|
# =============================================================================
|
|
|
|
|
|
def project_to_response(project: "ProjectTable") -> ProjectResponse:
|
|
"""Convert a ProjectTable to ProjectResponse."""
|
|
default_branch = project.default_branch
|
|
return ProjectResponse(
|
|
id=typing_cast("UUID", project.id),
|
|
name=str(project.name),
|
|
slug=str(project.slug),
|
|
git_url=str(project.git_url),
|
|
default_branch=str(default_branch) if default_branch else "master",
|
|
protected_branches=list(project.protected_branches or []),
|
|
assigned_cell=project.assigned_cell,
|
|
has_git_token=bool(project.git_token_encrypted),
|
|
test_command=project.test_command,
|
|
lint_command=project.lint_command,
|
|
format_command=project.format_command,
|
|
typecheck_command=project.typecheck_command,
|
|
build_command=project.build_command,
|
|
quality_command=project.quality_command,
|
|
workspace_path=project.workspace_path,
|
|
last_synced_at=project.last_synced_at,
|
|
head_commit=project.head_commit,
|
|
created_by=typing_cast("UUID", project.created_by),
|
|
is_active=bool(project.is_active),
|
|
created_at=project.created_at,
|
|
updated_at=project.updated_at,
|
|
)
|
|
|
|
|
|
def project_to_summary(project: "ProjectTable") -> ProjectSummaryResponse:
|
|
"""Convert a ProjectTable to ProjectSummaryResponse."""
|
|
default_branch = project.default_branch
|
|
return ProjectSummaryResponse(
|
|
id=typing_cast("UUID", project.id),
|
|
name=str(project.name),
|
|
slug=str(project.slug),
|
|
git_url=str(project.git_url),
|
|
default_branch=str(default_branch) if default_branch else "master",
|
|
assigned_cell=project.assigned_cell,
|
|
is_active=bool(project.is_active),
|
|
has_workspace=bool(project.workspace_path),
|
|
has_git_token=bool(project.git_token_encrypted),
|
|
)
|