style: ruff format 10 python files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,9 +58,7 @@ def upgrade() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
sa.text(
|
sa.text("SELECT create_hypertable('wireless_registrations', 'time', if_not_exists => TRUE)")
|
||||||
"SELECT create_hypertable('wireless_registrations', 'time', if_not_exists => TRUE)"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Primary lookup: device + time range
|
# Primary lookup: device + time range
|
||||||
@@ -126,9 +124,7 @@ def upgrade() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
sa.text(
|
sa.text("SELECT create_hypertable('rf_monitor_stats', 'time', if_not_exists => TRUE)")
|
||||||
"SELECT create_hypertable('rf_monitor_stats', 'time', if_not_exists => TRUE)"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -157,23 +153,17 @@ def upgrade() -> None:
|
|||||||
conn.execute(sa.text("GRANT SELECT, INSERT ON rf_monitor_stats TO app_user"))
|
conn.execute(sa.text("GRANT SELECT, INSERT ON rf_monitor_stats TO app_user"))
|
||||||
conn.execute(sa.text("GRANT SELECT, INSERT ON rf_monitor_stats TO poller_user"))
|
conn.execute(sa.text("GRANT SELECT, INSERT ON rf_monitor_stats TO poller_user"))
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(sa.text("SELECT add_retention_policy('rf_monitor_stats', INTERVAL '30 days')"))
|
||||||
sa.text("SELECT add_retention_policy('rf_monitor_stats', INTERVAL '30 days')")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
|
|
||||||
# Remove retention policies before dropping tables
|
# Remove retention policies before dropping tables
|
||||||
conn.execute(
|
conn.execute(sa.text("SELECT remove_retention_policy('rf_monitor_stats', if_exists => true)"))
|
||||||
sa.text("SELECT remove_retention_policy('rf_monitor_stats', if_exists => true)")
|
|
||||||
)
|
|
||||||
conn.execute(sa.text("DROP TABLE IF EXISTS rf_monitor_stats CASCADE"))
|
conn.execute(sa.text("DROP TABLE IF EXISTS rf_monitor_stats CASCADE"))
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
sa.text(
|
sa.text("SELECT remove_retention_policy('wireless_registrations', if_exists => true)")
|
||||||
"SELECT remove_retention_policy('wireless_registrations', if_exists => true)"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
conn.execute(sa.text("DROP TABLE IF EXISTS wireless_registrations CASCADE"))
|
conn.execute(sa.text("DROP TABLE IF EXISTS wireless_registrations CASCADE"))
|
||||||
|
|||||||
@@ -183,11 +183,7 @@ def upgrade() -> None:
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(sa.text("GRANT SELECT, INSERT, UPDATE, DELETE ON site_alert_rules TO app_user"))
|
||||||
sa.text(
|
|
||||||
"GRANT SELECT, INSERT, UPDATE, DELETE ON site_alert_rules TO app_user"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# site_alert_events RLS
|
# site_alert_events RLS
|
||||||
conn.execute(sa.text("ALTER TABLE site_alert_events ENABLE ROW LEVEL SECURITY"))
|
conn.execute(sa.text("ALTER TABLE site_alert_events ENABLE ROW LEVEL SECURITY"))
|
||||||
@@ -205,23 +201,15 @@ def upgrade() -> None:
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(sa.text("GRANT SELECT, INSERT, UPDATE, DELETE ON site_alert_events TO app_user"))
|
||||||
sa.text(
|
|
||||||
"GRANT SELECT, INSERT, UPDATE, DELETE ON site_alert_events TO app_user"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
|
|
||||||
# Drop RLS policies
|
# Drop RLS policies
|
||||||
conn.execute(
|
conn.execute(sa.text("DROP POLICY IF EXISTS tenant_isolation ON site_alert_events"))
|
||||||
sa.text("DROP POLICY IF EXISTS tenant_isolation ON site_alert_events")
|
conn.execute(sa.text("DROP POLICY IF EXISTS tenant_isolation ON site_alert_rules"))
|
||||||
)
|
|
||||||
conn.execute(
|
|
||||||
sa.text("DROP POLICY IF EXISTS tenant_isolation ON site_alert_rules")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Drop tables (indexes drop automatically with tables)
|
# Drop tables (indexes drop automatically with tables)
|
||||||
op.drop_table("site_alert_events")
|
op.drop_table("site_alert_events")
|
||||||
|
|||||||
@@ -150,9 +150,7 @@ class SiteAlertEvent(Base):
|
|||||||
triggered_at: Mapped[datetime] = mapped_column(
|
triggered_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||||
)
|
)
|
||||||
resolved_at: Mapped[datetime | None] = mapped_column(
|
resolved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
DateTime(timezone=True), nullable=True
|
|
||||||
)
|
|
||||||
resolved_by: Mapped[uuid.UUID | None] = mapped_column(
|
resolved_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
UUID(as_uuid=True),
|
UUID(as_uuid=True),
|
||||||
ForeignKey("users.id", ondelete="SET NULL"),
|
ForeignKey("users.id", ondelete="SET NULL"),
|
||||||
|
|||||||
@@ -36,14 +36,18 @@ router = APIRouter(tags=["links"])
|
|||||||
)
|
)
|
||||||
async def list_links(
|
async def list_links(
|
||||||
tenant_id: uuid.UUID,
|
tenant_id: uuid.UUID,
|
||||||
state: Optional[str] = Query(None, description="Filter by link state (active, degraded, down, stale)"),
|
state: Optional[str] = Query(
|
||||||
|
None, description="Filter by link state (active, degraded, down, stale)"
|
||||||
|
),
|
||||||
device_id: Optional[uuid.UUID] = Query(None, description="Filter by device (AP or CPE side)"),
|
device_id: Optional[uuid.UUID] = Query(None, description="Filter by device (AP or CPE side)"),
|
||||||
current_user: CurrentUser = Depends(get_current_user),
|
current_user: CurrentUser = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
) -> LinkListResponse:
|
) -> LinkListResponse:
|
||||||
"""List all wireless links for a tenant with optional state and device filters."""
|
"""List all wireless links for a tenant with optional state and device filters."""
|
||||||
await _check_tenant_access(current_user, tenant_id, db)
|
await _check_tenant_access(current_user, tenant_id, db)
|
||||||
return await link_service.get_links(db=db, tenant_id=tenant_id, state=state, device_id=device_id)
|
return await link_service.get_links(
|
||||||
|
db=db, tenant_id=tenant_id, state=state, device_id=device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
@@ -91,7 +95,9 @@ async def list_device_registrations(
|
|||||||
) -> RegistrationListResponse:
|
) -> RegistrationListResponse:
|
||||||
"""Get latest wireless registration data for a device (most recent per MAC)."""
|
"""Get latest wireless registration data for a device (most recent per MAC)."""
|
||||||
await _check_tenant_access(current_user, tenant_id, db)
|
await _check_tenant_access(current_user, tenant_id, db)
|
||||||
return await link_service.get_device_registrations(db=db, tenant_id=tenant_id, device_id=device_id)
|
return await link_service.get_device_registrations(
|
||||||
|
db=db, tenant_id=tenant_id, device_id=device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
|||||||
@@ -98,9 +98,7 @@ async def get_alert_rule(
|
|||||||
db=db, tenant_id=tenant_id, site_id=site_id, rule_id=rule_id
|
db=db, tenant_id=tenant_id, site_id=site_id, rule_id=rule_id
|
||||||
)
|
)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Alert rule not found")
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Alert rule not found"
|
|
||||||
)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -124,9 +122,7 @@ async def update_alert_rule(
|
|||||||
db=db, tenant_id=tenant_id, site_id=site_id, rule_id=rule_id, data=data
|
db=db, tenant_id=tenant_id, site_id=site_id, rule_id=rule_id, data=data
|
||||||
)
|
)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Alert rule not found")
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Alert rule not found"
|
|
||||||
)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -149,9 +145,7 @@ async def delete_alert_rule(
|
|||||||
db=db, tenant_id=tenant_id, site_id=site_id, rule_id=rule_id
|
db=db, tenant_id=tenant_id, site_id=site_id, rule_id=rule_id
|
||||||
)
|
)
|
||||||
if not deleted:
|
if not deleted:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Alert rule not found")
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Alert rule not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -166,9 +166,7 @@ async def unassign_device(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Remove a device from a site. Requires operator role or above."""
|
"""Remove a device from a site. Requires operator role or above."""
|
||||||
await _check_tenant_access(current_user, tenant_id, db)
|
await _check_tenant_access(current_user, tenant_id, db)
|
||||||
await site_service.remove_device_from_site(
|
await site_service.remove_device_from_site(db=db, tenant_id=tenant_id, device_id=device_id)
|
||||||
db=db, tenant_id=tenant_id, device_id=device_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|||||||
@@ -43,8 +43,7 @@ async def _evaluate_condition(session, rule) -> bool: # noqa: ANN001
|
|||||||
|
|
||||||
offline_result = await session.execute(
|
offline_result = await session.execute(
|
||||||
text(
|
text(
|
||||||
"SELECT count(*) AS cnt FROM devices "
|
"SELECT count(*) AS cnt FROM devices WHERE site_id = :site_id AND is_online = false"
|
||||||
"WHERE site_id = :site_id AND is_online = false"
|
|
||||||
),
|
),
|
||||||
{"site_id": site_id},
|
{"site_id": site_id},
|
||||||
)
|
)
|
||||||
@@ -55,8 +54,7 @@ async def _evaluate_condition(session, rule) -> bool: # noqa: ANN001
|
|||||||
elif rule_type == "device_offline_count":
|
elif rule_type == "device_offline_count":
|
||||||
offline_result = await session.execute(
|
offline_result = await session.execute(
|
||||||
text(
|
text(
|
||||||
"SELECT count(*) AS cnt FROM devices "
|
"SELECT count(*) AS cnt FROM devices WHERE site_id = :site_id AND is_online = false"
|
||||||
"WHERE site_id = :site_id AND is_online = false"
|
|
||||||
),
|
),
|
||||||
{"site_id": site_id},
|
{"site_id": site_id},
|
||||||
)
|
)
|
||||||
@@ -171,9 +169,11 @@ async def _evaluate_rules() -> None:
|
|||||||
# Events with consecutive_hits < 2 are considered "pending"
|
# Events with consecutive_hits < 2 are considered "pending"
|
||||||
# (not yet confirmed). On next evaluation if still met,
|
# (not yet confirmed). On next evaluation if still met,
|
||||||
# consecutive_hits increments to 2 (confirmed alert).
|
# consecutive_hits increments to 2 (confirmed alert).
|
||||||
severity = "critical" if rule.rule_type in (
|
severity = (
|
||||||
"device_offline_percent", "device_offline_count"
|
"critical"
|
||||||
) else "warning"
|
if rule.rule_type in ("device_offline_percent", "device_offline_count")
|
||||||
|
else "warning"
|
||||||
|
)
|
||||||
|
|
||||||
await session.execute(
|
await session.execute(
|
||||||
text("""
|
text("""
|
||||||
|
|||||||
@@ -122,9 +122,7 @@ async def _subscribe_with_retry(js: JetStreamContext) -> None:
|
|||||||
durable="api-interface-consumer",
|
durable="api-interface-consumer",
|
||||||
stream="DEVICE_EVENTS",
|
stream="DEVICE_EVENTS",
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info("NATS: subscribed to device.interfaces.> (durable: api-interface-consumer)")
|
||||||
"NATS: subscribed to device.interfaces.> (durable: api-interface-consumer)"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if attempt < max_attempts:
|
if attempt < max_attempts:
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ _link_discovery_client: Optional[NATSClient] = None
|
|||||||
|
|
||||||
# Configurable thresholds for link state transitions
|
# Configurable thresholds for link state transitions
|
||||||
DEGRADED_SIGNAL_THRESHOLD = -80 # dBm — signals weaker than this mark link as degraded
|
DEGRADED_SIGNAL_THRESHOLD = -80 # dBm — signals weaker than this mark link as degraded
|
||||||
CONSECUTIVE_MISS_THRESHOLD = 3 # Missed polls before marking link as down
|
CONSECUTIVE_MISS_THRESHOLD = 3 # Missed polls before marking link as down
|
||||||
STALE_HOURS = 24 # Hours after down before marking link as stale
|
STALE_HOURS = 24 # Hours after down before marking link as stale
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -187,14 +187,16 @@ async def on_wireless_registration_for_links(msg) -> None:
|
|||||||
|
|
||||||
# Mark stale: any links in 'down' state where last_seen > STALE_HOURS ago
|
# Mark stale: any links in 'down' state where last_seen > STALE_HOURS ago
|
||||||
await session.execute(
|
await session.execute(
|
||||||
text("""
|
text(
|
||||||
|
"""
|
||||||
UPDATE wireless_links
|
UPDATE wireless_links
|
||||||
SET state = 'stale', updated_at = NOW()
|
SET state = 'stale', updated_at = NOW()
|
||||||
WHERE ap_device_id = :ap_device_id
|
WHERE ap_device_id = :ap_device_id
|
||||||
AND tenant_id = :tenant_id
|
AND tenant_id = :tenant_id
|
||||||
AND state = 'down'
|
AND state = 'down'
|
||||||
AND last_seen < NOW() - INTERVAL ':stale_hours hours'
|
AND last_seen < NOW() - INTERVAL ':stale_hours hours'
|
||||||
""".replace(":stale_hours", str(STALE_HOURS))),
|
""".replace(":stale_hours", str(STALE_HOURS))
|
||||||
|
),
|
||||||
{
|
{
|
||||||
"ap_device_id": device_id,
|
"ap_device_id": device_id,
|
||||||
"tenant_id": tenant_id,
|
"tenant_id": tenant_id,
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ logger = structlog.get_logger("site_service")
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _site_response(site: Site, device_count: int = 0, online_count: int = 0, alert_count: int = 0) -> SiteResponse:
|
def _site_response(
|
||||||
|
site: Site, device_count: int = 0, online_count: int = 0, alert_count: int = 0
|
||||||
|
) -> SiteResponse:
|
||||||
"""Build a SiteResponse from an ORM Site instance with health stats."""
|
"""Build a SiteResponse from an ORM Site instance with health stats."""
|
||||||
online_percent = (online_count / device_count * 100) if device_count > 0 else 0.0
|
online_percent = (online_count / device_count * 100) if device_count > 0 else 0.0
|
||||||
return SiteResponse(
|
return SiteResponse(
|
||||||
@@ -51,9 +53,7 @@ def _site_response(site: Site, device_count: int = 0, online_count: int = 0, ale
|
|||||||
|
|
||||||
async def _get_site_or_404(db: AsyncSession, tenant_id: uuid.UUID, site_id: uuid.UUID) -> Site:
|
async def _get_site_or_404(db: AsyncSession, tenant_id: uuid.UUID, site_id: uuid.UUID) -> Site:
|
||||||
"""Fetch a site by id and tenant, or raise 404."""
|
"""Fetch a site by id and tenant, or raise 404."""
|
||||||
result = await db.execute(
|
result = await db.execute(select(Site).where(Site.id == site_id, Site.tenant_id == tenant_id))
|
||||||
select(Site).where(Site.id == site_id, Site.tenant_id == tenant_id)
|
|
||||||
)
|
|
||||||
site = result.scalar_one_or_none()
|
site = result.scalar_one_or_none()
|
||||||
if not site:
|
if not site:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Site not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Site not found")
|
||||||
|
|||||||
Reference in New Issue
Block a user