feat: add validation for minimum length on various fields and update type definitions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
13
backend/app/schemas/common.py
Normal file
13
backend/app/schemas/common.py
Normal 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
|
||||
47
backend/app/schemas/gateway_api.py
Normal file
47
backend/app/schemas/gateway_api.py
Normal 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]
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user