feat(workbench): add runtime GIS and sample integrations
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
"""Monotonic, idempotent state machine for task-produced alerts."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.domain.schemas.task_alert import TaskAlertEvent, TaskAlertState
|
||||
from app.domain.domain_errors import require
|
||||
from app.services.content_digest import digest
|
||||
|
||||
|
||||
class TaskAlertService:
|
||||
def __init__(
|
||||
self,
|
||||
repository,
|
||||
context,
|
||||
*,
|
||||
clock=None,
|
||||
internal_producer_id: str | None = None,
|
||||
):
|
||||
self.repo = repository
|
||||
self.context = context
|
||||
self.clock = clock or (lambda: datetime.now(timezone.utc))
|
||||
self.internal_producer_id = internal_producer_id
|
||||
|
||||
async def push(self, command: TaskAlertEvent) -> TaskAlertState:
|
||||
if self.internal_producer_id is None:
|
||||
require(
|
||||
self.context is not None and self.context.project_role == "member",
|
||||
"PERMISSION_DENIED",
|
||||
"Alert events require project write permission",
|
||||
status=403,
|
||||
)
|
||||
require(
|
||||
command.producer_id == f"principal:{self.context.user_id}",
|
||||
"PERMISSION_DENIED",
|
||||
"Alert producer must match the authenticated project principal",
|
||||
status=403,
|
||||
)
|
||||
else:
|
||||
require(
|
||||
command.producer_id == self.internal_producer_id,
|
||||
"PERMISSION_DENIED",
|
||||
"Internal alert producer identity does not match",
|
||||
status=403,
|
||||
)
|
||||
event = command.model_dump(mode="json")
|
||||
async with self.repo.transaction():
|
||||
await self.repo.lock_alert(command.alert_id)
|
||||
prior_event = await self.repo.get_event(
|
||||
command.producer_id, str(command.event_id)
|
||||
)
|
||||
if prior_event is not None:
|
||||
prior_command = {
|
||||
key: value for key, value in prior_event.items()
|
||||
if key != "resulting_state"
|
||||
}
|
||||
require(
|
||||
digest(prior_command) == digest(event),
|
||||
"IDEMPOTENCY_CONFLICT",
|
||||
"event_id was used with a different alert event",
|
||||
status=409,
|
||||
)
|
||||
return TaskAlertState.model_validate(prior_event["resulting_state"])
|
||||
|
||||
current = await self.repo.get_alert(command.alert_id)
|
||||
state = self._transition(command, current)
|
||||
state_document = state.model_dump(mode="json")
|
||||
await self.repo.save_event(event, state_document)
|
||||
await self.repo.save_alert(state_document)
|
||||
return state
|
||||
|
||||
def _transition(
|
||||
self, command: TaskAlertEvent, current: tuple[int, dict] | None
|
||||
) -> TaskAlertState:
|
||||
now = self.clock()
|
||||
require(now.tzinfo is not None, "SCHEMA_INVALID", "Alert clock must be timezone-aware")
|
||||
if current is None:
|
||||
require(command.event_type == "raised", "INVALID_TRANSITION",
|
||||
"An alert must be raised before it can change state", status=409)
|
||||
revision = 1
|
||||
status = "active"
|
||||
else:
|
||||
revision, document = current
|
||||
require(document.get("producer_id") == command.producer_id,
|
||||
"PERMISSION_DENIED", "Only the originating producer can update an alert", status=403)
|
||||
require(document.get("source") == command.source.model_dump(mode="json"),
|
||||
"SCOPE_MISMATCH", "Alert source cannot change", status=409)
|
||||
require(
|
||||
all(
|
||||
document.get(field) == event_value
|
||||
for field, event_value in {
|
||||
"condition_id": command.condition_id,
|
||||
"title": command.title,
|
||||
"description": command.description,
|
||||
"occurred_at": command.model_dump(mode="json")["occurred_at"],
|
||||
"severity": command.severity,
|
||||
}.items()
|
||||
),
|
||||
"IDEMPOTENCY_CONFLICT",
|
||||
"Alert facts cannot change after the alert is raised",
|
||||
status=409,
|
||||
)
|
||||
allowed = {
|
||||
("active", "acknowledged"): "acknowledged",
|
||||
("active", "resolved"): "resolved",
|
||||
("acknowledged", "resolved"): "resolved",
|
||||
}
|
||||
status = allowed.get((document["status"], command.event_type))
|
||||
require(status is not None, "INVALID_TRANSITION",
|
||||
"Alert state transition is not permitted", status=409)
|
||||
revision += 1
|
||||
return TaskAlertState(
|
||||
alert_id=command.alert_id,
|
||||
revision=revision,
|
||||
producer_id=command.producer_id,
|
||||
condition_id=command.condition_id,
|
||||
source=command.source,
|
||||
title=command.title,
|
||||
description=command.description,
|
||||
occurred_at=command.occurred_at,
|
||||
severity=command.severity,
|
||||
status=status,
|
||||
updated_at=now,
|
||||
)
|
||||
Reference in New Issue
Block a user