feat: add validation for minimum length on various fields and update type definitions

This commit is contained in:
Abhimanyu Saharan
2026-02-06 16:12:04 +05:30
parent ca614328ac
commit d86fe0a7a6
157 changed files with 12340 additions and 2977 deletions

View File

@@ -1,21 +1,64 @@
from __future__ import annotations
from collections.abc import Mapping
from datetime import datetime
from typing import Any
from uuid import UUID
from pydantic import field_validator
from sqlmodel import SQLModel
from app.schemas.common import NonEmptyStr
def _normalize_identity_profile(
profile: object,
) -> dict[str, str] | None:
if not isinstance(profile, Mapping):
return None
normalized: dict[str, str] = {}
for raw_key, raw in profile.items():
if raw is None:
continue
key = str(raw_key).strip()
if not key:
continue
if isinstance(raw, list):
parts = [str(item).strip() for item in raw if str(item).strip()]
if not parts:
continue
normalized[key] = ", ".join(parts)
continue
value = str(raw).strip()
if value:
normalized[key] = value
return normalized or None
class AgentBase(SQLModel):
board_id: UUID | None = None
name: str
name: NonEmptyStr
status: str = "provisioning"
heartbeat_config: dict[str, Any] | None = None
identity_profile: dict[str, Any] | None = None
identity_template: str | None = None
soul_template: str | None = None
@field_validator("identity_template", "soul_template", mode="before")
@classmethod
def normalize_templates(cls, value: Any) -> Any:
if value is None:
return None
if isinstance(value, str):
value = value.strip()
return value or None
return value
@field_validator("identity_profile", mode="before")
@classmethod
def normalize_identity_profile(cls, value: Any) -> Any:
return _normalize_identity_profile(value)
class AgentCreate(AgentBase):
pass
@@ -24,13 +67,28 @@ class AgentCreate(AgentBase):
class AgentUpdate(SQLModel):
board_id: UUID | None = None
is_gateway_main: bool | None = None
name: str | None = None
name: NonEmptyStr | None = None
status: str | None = None
heartbeat_config: dict[str, Any] | None = None
identity_profile: dict[str, Any] | None = None
identity_template: str | None = None
soul_template: str | None = None
@field_validator("identity_template", "soul_template", mode="before")
@classmethod
def normalize_templates(cls, value: Any) -> Any:
if value is None:
return None
if isinstance(value, str):
value = value.strip()
return value or None
return value
@field_validator("identity_profile", mode="before")
@classmethod
def normalize_identity_profile(cls, value: Any) -> Any:
return _normalize_identity_profile(value)
class AgentRead(AgentBase):
id: UUID
@@ -47,9 +105,9 @@ class AgentHeartbeat(SQLModel):
class AgentHeartbeatCreate(AgentHeartbeat):
name: str
name: NonEmptyStr
board_id: UUID | None = None
class AgentNudge(SQLModel):
message: str
message: NonEmptyStr

View File

@@ -1,17 +1,22 @@
from __future__ import annotations
from datetime import datetime
from typing import Literal, Self
from uuid import UUID
from pydantic import model_validator
from sqlmodel import SQLModel
ApprovalStatus = Literal["pending", "approved", "rejected"]
class ApprovalBase(SQLModel):
action_type: str
payload: dict[str, object] | None = None
confidence: int
rubric_scores: dict[str, int] | None = None
status: str = "pending"
status: ApprovalStatus = "pending"
class ApprovalCreate(ApprovalBase):
@@ -19,7 +24,13 @@ class ApprovalCreate(ApprovalBase):
class ApprovalUpdate(SQLModel):
status: str | None = None
status: ApprovalStatus | None = None
@model_validator(mode="after")
def validate_status(self) -> Self:
if "status" in self.model_fields_set and self.status is None:
raise ValueError("status is required")
return self
class ApprovalRead(ApprovalBase):

View File

@@ -5,9 +5,11 @@ from uuid import UUID
from sqlmodel import SQLModel
from app.schemas.common import NonEmptyStr
class BoardMemoryCreate(SQLModel):
content: str
content: NonEmptyStr
tags: list[str] | None = None
source: str | None = None

View File

@@ -1,18 +1,21 @@
from __future__ import annotations
from datetime import datetime
from typing import Literal, Self
from uuid import UUID
from pydantic import model_validator
from pydantic import Field, model_validator
from sqlmodel import SQLModel
from app.schemas.common import NonEmptyStr
class BoardOnboardingStart(SQLModel):
pass
class BoardOnboardingAnswer(SQLModel):
answer: str
answer: NonEmptyStr
other_text: str | None = None
@@ -23,13 +26,30 @@ class BoardOnboardingConfirm(SQLModel):
target_date: datetime | None = None
@model_validator(mode="after")
def validate_goal_fields(self):
def validate_goal_fields(self) -> Self:
if self.board_type == "goal":
if not self.objective or not self.success_metrics:
raise ValueError("Confirmed goal boards require objective and success_metrics")
return self
class BoardOnboardingQuestionOption(SQLModel):
id: NonEmptyStr
label: NonEmptyStr
class BoardOnboardingAgentQuestion(SQLModel):
question: NonEmptyStr
options: list[BoardOnboardingQuestionOption] = Field(min_length=1)
class BoardOnboardingAgentComplete(BoardOnboardingConfirm):
status: Literal["complete"]
BoardOnboardingAgentUpdate = BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion
class BoardOnboardingRead(SQLModel):
id: UUID
board_id: UUID

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from datetime import datetime
from typing import Self
from uuid import UUID
from pydantic import model_validator
@@ -20,8 +21,10 @@ class BoardBase(SQLModel):
class BoardCreate(BoardBase):
gateway_id: UUID
@model_validator(mode="after")
def validate_goal_fields(self):
def validate_goal_fields(self) -> Self:
if self.board_type == "goal" and self.goal_confirmed:
if not self.objective or not self.success_metrics:
raise ValueError("Confirmed goal boards require objective and success_metrics")
@@ -39,6 +42,13 @@ class BoardUpdate(SQLModel):
goal_confirmed: bool | None = None
goal_source: str | None = None
@model_validator(mode="after")
def validate_gateway_id(self) -> Self:
# Treat explicit null like "unset" is invalid for patch updates.
if "gateway_id" in self.model_fields_set and self.gateway_id is None:
raise ValueError("gateway_id is required")
return self
class BoardRead(BoardBase):
id: UUID

View File

@@ -0,0 +1,13 @@
from __future__ import annotations
from typing import Annotated
from pydantic import StringConstraints
from sqlmodel import SQLModel
# Reusable string type for request payloads where blank/whitespace-only values are invalid.
NonEmptyStr = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
class OkResponse(SQLModel):
ok: bool = True

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
from sqlmodel import SQLModel
from app.schemas.common import NonEmptyStr
class GatewaySessionMessageRequest(SQLModel):
content: NonEmptyStr
class GatewayResolveQuery(SQLModel):
board_id: str | None = None
gateway_url: str | None = None
gateway_token: str | None = None
gateway_main_session_key: str | None = None
class GatewaysStatusResponse(SQLModel):
connected: bool
gateway_url: str
sessions_count: int | None = None
sessions: list[object] | None = None
main_session_key: str | None = None
main_session: object | None = None
main_session_error: str | None = None
error: str | None = None
class GatewaySessionsResponse(SQLModel):
sessions: list[object]
main_session_key: str | None = None
main_session: object | None = None
class GatewaySessionResponse(SQLModel):
session: object
class GatewaySessionHistoryResponse(SQLModel):
history: list[object]
class GatewayCommandsResponse(SQLModel):
protocol_version: int
methods: list[str]
events: list[str]

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from uuid import UUID
from pydantic import field_validator
from sqlmodel import SQLModel
@@ -17,6 +19,16 @@ class GatewayBase(SQLModel):
class GatewayCreate(GatewayBase):
token: str | None = None
@field_validator("token", mode="before")
@classmethod
def normalize_token(cls, value: Any) -> Any:
if value is None:
return None
if isinstance(value, str):
value = value.strip()
return value or None
return value
class GatewayUpdate(SQLModel):
name: str | None = None
@@ -26,6 +38,16 @@ class GatewayUpdate(SQLModel):
workspace_root: str | None = None
skyll_enabled: bool | None = None
@field_validator("token", mode="before")
@classmethod
def normalize_token(cls, value: Any) -> Any:
if value is None:
return None
if isinstance(value, str):
value = value.strip()
return value or None
return value
class GatewayRead(GatewayBase):
id: UUID

View File

@@ -1,15 +1,22 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Literal, Self
from uuid import UUID
from pydantic import field_validator, model_validator
from sqlmodel import SQLModel
from app.schemas.common import NonEmptyStr
TaskStatus = Literal["inbox", "in_progress", "review", "done"]
class TaskBase(SQLModel):
title: str
description: str | None = None
status: str = "inbox"
status: TaskStatus = "inbox"
priority: str = "medium"
due_at: datetime | None = None
assigned_agent_id: UUID | None = None
@@ -22,11 +29,26 @@ class TaskCreate(TaskBase):
class TaskUpdate(SQLModel):
title: str | None = None
description: str | None = None
status: str | None = None
status: TaskStatus | None = None
priority: str | None = None
due_at: datetime | None = None
assigned_agent_id: UUID | None = None
comment: str | None = None
comment: NonEmptyStr | None = None
@field_validator("comment", mode="before")
@classmethod
def normalize_comment(cls, value: Any) -> Any:
if value is None:
return None
if isinstance(value, str) and not value.strip():
return None
return value
@model_validator(mode="after")
def validate_status(self) -> Self:
if "status" in self.model_fields_set and self.status is None:
raise ValueError("status is required")
return self
class TaskRead(TaskBase):
@@ -39,7 +61,7 @@ class TaskRead(TaskBase):
class TaskCommentCreate(SQLModel):
message: str
message: NonEmptyStr
class TaskCommentRead(SQLModel):