- Create Alembic migration 035 with site_alert_rules and site_alert_events tables, RLS policies, and GRANT - Add SiteAlertRule/SiteAlertEvent ORM models with enums for rule_type, severity, state - Add Pydantic schemas for rule/event CRUD and signal history points - Add SIGNAL_DEGRADATION_THRESHOLD_DB, ALERT_EVALUATION_INTERVAL_SECONDS, TREND_DETECTION_INTERVAL_SECONDS to Settings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
4.6 KiB
Python
169 lines
4.6 KiB
Python
"""Pydantic schemas for site alert rules, events, and signal history."""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
|
|
|
|
class SiteAlertRuleCreate(BaseModel):
|
|
"""Schema for creating a new site alert rule."""
|
|
|
|
name: str
|
|
rule_type: str
|
|
threshold_value: float
|
|
threshold_unit: str
|
|
sector_id: Optional[uuid.UUID] = None
|
|
description: Optional[str] = None
|
|
enabled: bool = True
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def validate_name(cls, v: str) -> str:
|
|
v = v.strip()
|
|
if len(v) < 1 or len(v) > 255:
|
|
raise ValueError("Rule name must be 1-255 characters")
|
|
return v
|
|
|
|
@field_validator("rule_type")
|
|
@classmethod
|
|
def validate_rule_type(cls, v: str) -> str:
|
|
allowed = {
|
|
"device_offline_percent",
|
|
"device_offline_count",
|
|
"sector_signal_avg",
|
|
"sector_client_drop",
|
|
"signal_degradation",
|
|
}
|
|
if v not in allowed:
|
|
raise ValueError(f"rule_type must be one of: {', '.join(sorted(allowed))}")
|
|
return v
|
|
|
|
@field_validator("threshold_unit")
|
|
@classmethod
|
|
def validate_threshold_unit(cls, v: str) -> str:
|
|
allowed = {"percent", "count", "dBm"}
|
|
if v not in allowed:
|
|
raise ValueError(f"threshold_unit must be one of: {', '.join(sorted(allowed))}")
|
|
return v
|
|
|
|
|
|
class SiteAlertRuleUpdate(BaseModel):
|
|
"""Schema for updating an existing site alert rule. All fields optional."""
|
|
|
|
name: Optional[str] = None
|
|
rule_type: Optional[str] = None
|
|
threshold_value: Optional[float] = None
|
|
threshold_unit: Optional[str] = None
|
|
sector_id: Optional[uuid.UUID] = None
|
|
description: Optional[str] = None
|
|
enabled: Optional[bool] = None
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def validate_name(cls, v: Optional[str]) -> Optional[str]:
|
|
if v is None:
|
|
return v
|
|
v = v.strip()
|
|
if len(v) < 1 or len(v) > 255:
|
|
raise ValueError("Rule name must be 1-255 characters")
|
|
return v
|
|
|
|
@field_validator("rule_type")
|
|
@classmethod
|
|
def validate_rule_type(cls, v: Optional[str]) -> Optional[str]:
|
|
if v is None:
|
|
return v
|
|
allowed = {
|
|
"device_offline_percent",
|
|
"device_offline_count",
|
|
"sector_signal_avg",
|
|
"sector_client_drop",
|
|
"signal_degradation",
|
|
}
|
|
if v not in allowed:
|
|
raise ValueError(f"rule_type must be one of: {', '.join(sorted(allowed))}")
|
|
return v
|
|
|
|
@field_validator("threshold_unit")
|
|
@classmethod
|
|
def validate_threshold_unit(cls, v: Optional[str]) -> Optional[str]:
|
|
if v is None:
|
|
return v
|
|
allowed = {"percent", "count", "dBm"}
|
|
if v not in allowed:
|
|
raise ValueError(f"threshold_unit must be one of: {', '.join(sorted(allowed))}")
|
|
return v
|
|
|
|
|
|
class SiteAlertRuleResponse(BaseModel):
|
|
"""Site alert rule response schema."""
|
|
|
|
id: uuid.UUID
|
|
tenant_id: uuid.UUID
|
|
site_id: uuid.UUID
|
|
sector_id: Optional[uuid.UUID] = None
|
|
rule_type: str
|
|
name: str
|
|
description: Optional[str] = None
|
|
threshold_value: float
|
|
threshold_unit: str
|
|
enabled: bool
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class SiteAlertRuleListResponse(BaseModel):
|
|
"""List of site alert rules with total count."""
|
|
|
|
items: list[SiteAlertRuleResponse]
|
|
total: int
|
|
|
|
|
|
class SiteAlertEventResponse(BaseModel):
|
|
"""Site alert event response schema."""
|
|
|
|
id: uuid.UUID
|
|
tenant_id: uuid.UUID
|
|
site_id: uuid.UUID
|
|
sector_id: Optional[uuid.UUID] = None
|
|
rule_id: Optional[uuid.UUID] = None
|
|
device_id: Optional[uuid.UUID] = None
|
|
link_id: Optional[uuid.UUID] = None
|
|
severity: str
|
|
message: str
|
|
state: str
|
|
consecutive_hits: int
|
|
triggered_at: datetime
|
|
resolved_at: Optional[datetime] = None
|
|
resolved_by: Optional[uuid.UUID] = None
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class SiteAlertEventListResponse(BaseModel):
|
|
"""List of site alert events with total count."""
|
|
|
|
items: list[SiteAlertEventResponse]
|
|
total: int
|
|
|
|
|
|
class SignalHistoryPoint(BaseModel):
|
|
"""A single time-bucketed signal history data point."""
|
|
|
|
timestamp: datetime
|
|
signal_avg: int
|
|
signal_min: int
|
|
signal_max: int
|
|
|
|
|
|
class SignalHistoryResponse(BaseModel):
|
|
"""Signal history response with time-bucketed data points."""
|
|
|
|
items: list[SignalHistoryPoint]
|
|
mac_address: str
|
|
range: str
|