feat(workbench): add runtime GIS and sample integrations
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
from __future__ import annotations
|
||||
from typing import Annotated, Literal
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, model_validator
|
||||
|
||||
from app.domain.schemas.shared import ObjectRef, TimeRange, WireDateTime
|
||||
|
||||
Text = Annotated[str, Field(min_length=1, max_length=4000)]
|
||||
PositiveInt = Annotated[StrictInt, Field(ge=1)]
|
||||
TaskStatus = Literal["pending", "processing", "pending_review", "completed"]
|
||||
Scenario = Literal["normal", "missing_data", "unavailable", "response_lost", "duplicate_feedback", "conflicting_feedback", "late_state"]
|
||||
|
||||
|
||||
class Model(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", allow_inf_nan=False)
|
||||
|
||||
|
||||
class SourceLink(Model):
|
||||
system: Literal["next"]
|
||||
task_id: UUID
|
||||
object_type: Literal["validation", "artifact"]
|
||||
object_id: UUID
|
||||
revision: PositiveInt
|
||||
|
||||
|
||||
class CreateTask(Model):
|
||||
request_id: UUID
|
||||
title: Annotated[str, Field(min_length=1, max_length=200)]
|
||||
summary: Text
|
||||
source: SourceLink
|
||||
asset_ids: Annotated[list[Annotated[str, Field(min_length=1, max_length=128)]], Field(min_length=1, max_length=100)]
|
||||
analysis_window: TimeRange
|
||||
evidence_refs: Annotated[list[ObjectRef], Field(max_length=100)]
|
||||
requested_check: Text
|
||||
|
||||
@model_validator(mode="after")
|
||||
def unique_assets(self):
|
||||
if len(set(self.asset_ids)) != len(self.asset_ids):
|
||||
raise ValueError("asset_ids must be unique")
|
||||
return self
|
||||
|
||||
|
||||
class Transition(Model):
|
||||
request_id: UUID
|
||||
expected_revision: PositiveInt
|
||||
target_status: TaskStatus
|
||||
reason: Text
|
||||
|
||||
|
||||
class AddFeedback(Model):
|
||||
request_id: UUID
|
||||
expected_revision: PositiveInt
|
||||
feedback_id: UUID
|
||||
occurred_at: WireDateTime
|
||||
applicable_time: TimeRange
|
||||
asset_ids: Annotated[list[Annotated[str, Field(min_length=1, max_length=128)]], Field(min_length=1, max_length=100)]
|
||||
finding: Literal["instrument_fault", "no_instrument_fault", "uncertain"]
|
||||
notes: Text
|
||||
attachment_refs: Annotated[list[Annotated[str, Field(min_length=1, max_length=500)]], Field(max_length=20)]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def aware_observation(self):
|
||||
if self.occurred_at.tzinfo is None:
|
||||
raise ValueError("occurred_at must include timezone")
|
||||
if len(set(self.asset_ids)) != len(self.asset_ids):
|
||||
raise ValueError("asset_ids must be unique")
|
||||
return self
|
||||
|
||||
|
||||
class Feedback(Model):
|
||||
feedback_id: UUID
|
||||
task_id: UUID
|
||||
recorded_by: str
|
||||
recorded_at: WireDateTime
|
||||
occurred_at: WireDateTime
|
||||
applicable_time: TimeRange
|
||||
asset_ids: list[str]
|
||||
finding: Literal["instrument_fault", "no_instrument_fault", "uncertain"]
|
||||
notes: str
|
||||
attachment_refs: list[str]
|
||||
origin: Literal["synthetic"]
|
||||
|
||||
|
||||
class HistoryEntry(Model):
|
||||
sequence: PositiveInt
|
||||
from_status: TaskStatus | None
|
||||
to_status: TaskStatus
|
||||
actor: str
|
||||
at: WireDateTime
|
||||
reason: str
|
||||
|
||||
|
||||
class Task(Model):
|
||||
task_id: UUID
|
||||
project_id: UUID
|
||||
revision: PositiveInt
|
||||
title: str
|
||||
summary: str
|
||||
source: SourceLink
|
||||
asset_ids: list[str]
|
||||
analysis_window: TimeRange
|
||||
evidence_refs: list[ObjectRef]
|
||||
requested_check: str
|
||||
status: TaskStatus
|
||||
origin: Literal["synthetic"]
|
||||
created_at: WireDateTime
|
||||
updated_at: WireDateTime
|
||||
created_by: str
|
||||
history: list[HistoryEntry]
|
||||
feedback: list[Feedback]
|
||||
|
||||
|
||||
class TaskPage(Model):
|
||||
items: list[Task]
|
||||
next_cursor: UUID | None
|
||||
|
||||
|
||||
WorkOrderStatus = Literal["draft", "open", "in_progress", "completed", "cancelled"]
|
||||
Priority = Literal["low", "normal", "high", "urgent"]
|
||||
|
||||
|
||||
class CreateWorkOrder(Model):
|
||||
request_id: UUID
|
||||
title: Annotated[str, Field(min_length=1, max_length=200)]
|
||||
summary: Text
|
||||
status: WorkOrderStatus = "draft"
|
||||
priority: Priority = "normal"
|
||||
assignee: Annotated[str, Field(min_length=1, max_length=200)] | None = None
|
||||
scheduled_at: WireDateTime | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def aware_schedule(self):
|
||||
if self.scheduled_at is not None and self.scheduled_at.tzinfo is None:
|
||||
raise ValueError("scheduled_at must include timezone")
|
||||
return self
|
||||
|
||||
|
||||
class UpdateWorkOrder(Model):
|
||||
request_id: UUID
|
||||
expected_revision: PositiveInt
|
||||
title: Annotated[str, Field(min_length=1, max_length=200)] | None = None
|
||||
summary: Text | None = None
|
||||
status: WorkOrderStatus | None = None
|
||||
priority: Priority | None = None
|
||||
assignee: Annotated[str, Field(min_length=1, max_length=200)] | None = None
|
||||
clear_assignee: StrictBool = False
|
||||
scheduled_at: WireDateTime | None = None
|
||||
clear_scheduled_at: StrictBool = False
|
||||
|
||||
@model_validator(mode="after")
|
||||
def valid_update(self):
|
||||
changed = set(self.model_fields_set) - {"request_id", "expected_revision"}
|
||||
if not changed:
|
||||
raise ValueError("at least one work order field must be updated")
|
||||
if self.clear_assignee and self.assignee is not None:
|
||||
raise ValueError("clear_assignee conflicts with assignee")
|
||||
if self.clear_scheduled_at and self.scheduled_at is not None:
|
||||
raise ValueError("clear_scheduled_at conflicts with scheduled_at")
|
||||
if self.scheduled_at is not None and self.scheduled_at.tzinfo is None:
|
||||
raise ValueError("scheduled_at must include timezone")
|
||||
return self
|
||||
|
||||
|
||||
class DeleteResource(Model):
|
||||
request_id: UUID
|
||||
expected_revision: PositiveInt
|
||||
|
||||
|
||||
class WorkOrder(Model):
|
||||
work_order_id: UUID
|
||||
project_id: UUID
|
||||
revision: PositiveInt
|
||||
title: str
|
||||
summary: str
|
||||
status: WorkOrderStatus
|
||||
priority: Priority
|
||||
assignee: str | None
|
||||
scheduled_at: WireDateTime | None
|
||||
origin: Literal["synthetic"]
|
||||
created_at: WireDateTime
|
||||
updated_at: WireDateTime
|
||||
created_by: str
|
||||
updated_by: str
|
||||
|
||||
|
||||
class WorkOrderPage(Model):
|
||||
items: list[WorkOrder]
|
||||
next_cursor: UUID | None
|
||||
|
||||
|
||||
class CreateTestDataRecord(Model):
|
||||
request_id: UUID
|
||||
label: Annotated[str, Field(min_length=1, max_length=200)]
|
||||
point_id: Annotated[str, Field(min_length=1, max_length=128)]
|
||||
observed_at: WireDateTime
|
||||
value: StrictFloat | StrictInt | None
|
||||
unit: Annotated[str, Field(min_length=1, max_length=32)]
|
||||
quality: Literal["valid", "unknown", "missing"] = "valid"
|
||||
notes: Annotated[str, Field(max_length=4000)] = ""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def aware_observation(self):
|
||||
if self.observed_at.tzinfo is None:
|
||||
raise ValueError("observed_at must include timezone")
|
||||
return self
|
||||
|
||||
|
||||
class UpdateTestDataRecord(Model):
|
||||
request_id: UUID
|
||||
expected_revision: PositiveInt
|
||||
label: Annotated[str, Field(min_length=1, max_length=200)] | None = None
|
||||
point_id: Annotated[str, Field(min_length=1, max_length=128)] | None = None
|
||||
observed_at: WireDateTime | None = None
|
||||
value: StrictFloat | StrictInt | None = None
|
||||
clear_value: StrictBool = False
|
||||
unit: Annotated[str, Field(min_length=1, max_length=32)] | None = None
|
||||
quality: Literal["valid", "unknown", "missing"] | None = None
|
||||
notes: Annotated[str, Field(max_length=4000)] | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def valid_update(self):
|
||||
changed = set(self.model_fields_set) - {"request_id", "expected_revision"}
|
||||
if not changed:
|
||||
raise ValueError("at least one test data field must be updated")
|
||||
if self.clear_value and self.value is not None:
|
||||
raise ValueError("clear_value conflicts with value")
|
||||
if self.observed_at is not None and self.observed_at.tzinfo is None:
|
||||
raise ValueError("observed_at must include timezone")
|
||||
return self
|
||||
|
||||
|
||||
class TestDataRecord(Model):
|
||||
record_id: UUID
|
||||
project_id: UUID
|
||||
revision: PositiveInt
|
||||
label: str
|
||||
point_id: str
|
||||
observed_at: WireDateTime
|
||||
value: float | int | None
|
||||
unit: str
|
||||
quality: Literal["valid", "unknown", "missing"]
|
||||
notes: str
|
||||
origin: Literal["synthetic"]
|
||||
created_at: WireDateTime
|
||||
updated_at: WireDateTime
|
||||
created_by: str
|
||||
updated_by: str
|
||||
|
||||
|
||||
class TestDataRecordPage(Model):
|
||||
items: list[TestDataRecord]
|
||||
next_cursor: UUID | None
|
||||
|
||||
|
||||
class DeletedResource(Model):
|
||||
resource_type: Literal["work_order", "test_data_record"]
|
||||
resource_id: UUID
|
||||
revision: Annotated[StrictInt, Field(ge=2)]
|
||||
deleted: Literal[True]
|
||||
|
||||
|
||||
class Receipt(Model):
|
||||
request_id: UUID
|
||||
task_id: UUID | None
|
||||
result: Task | ScenarioState | WorkOrder | TestDataRecord | DeletedResource
|
||||
|
||||
|
||||
class ScenarioCommand(Model):
|
||||
request_id: UUID
|
||||
scenario: Scenario
|
||||
|
||||
|
||||
class ScenarioState(Model):
|
||||
scenario: Scenario
|
||||
revision: PositiveInt
|
||||
|
||||
|
||||
ResourceAvailability = Literal["available", "not_configured"]
|
||||
|
||||
|
||||
class ManagedResourceCapabilities(Model):
|
||||
work_orders: ResourceAvailability
|
||||
test_data: ResourceAvailability
|
||||
|
||||
|
||||
class Context(Model):
|
||||
protocol_version: Literal["tjwater-peripheral-sample@1"]
|
||||
origin: Literal["synthetic"]
|
||||
project_id: UUID
|
||||
user_id: UUID
|
||||
can_write: StrictBool
|
||||
scenarios: list[Scenario]
|
||||
state_transitions: dict[str, list[str]]
|
||||
managed_resources: ManagedResourceCapabilities
|
||||
|
||||
|
||||
class Point(Model):
|
||||
point_id: str
|
||||
label: str
|
||||
unit: str
|
||||
status: str
|
||||
origin: Literal["historical_replay", "synthetic"]
|
||||
|
||||
|
||||
class PointPage(Model):
|
||||
items: list[Point]
|
||||
source_id: str
|
||||
origin: Literal["historical_replay", "synthetic"]
|
||||
next_cursor: str | None
|
||||
|
||||
|
||||
class Observation(Model):
|
||||
record_id: str
|
||||
point_id: str
|
||||
observed_at: WireDateTime
|
||||
value: float | None
|
||||
quality: Literal["valid", "unknown", "missing"]
|
||||
unit: str
|
||||
|
||||
|
||||
class Observations(Model):
|
||||
origin: Literal["historical_replay", "synthetic"]
|
||||
source_id: str
|
||||
source_revision: str
|
||||
items: list[Observation]
|
||||
truncated: StrictBool
|
||||
|
||||
|
||||
class Alert(Model):
|
||||
alert_id: str
|
||||
point_id: str
|
||||
occurred_at: WireDateTime
|
||||
message: str
|
||||
origin: Literal["historical_replay", "synthetic"]
|
||||
|
||||
|
||||
class AlertPage(Model):
|
||||
items: list[Alert]
|
||||
source_id: str
|
||||
origin: Literal["historical_replay", "synthetic"]
|
||||
availability: Literal["available", "not_configured"]
|
||||
reason: str | None
|
||||
|
||||
|
||||
class SampleError(Model):
|
||||
code: str
|
||||
message: str
|
||||
request_id: UUID | None
|
||||
effect: Literal["none", "unknown"]
|
||||
|
||||
|
||||
class DemoDeviceState(Model):
|
||||
record_id: UUID
|
||||
asset_id: str
|
||||
asset_type: Literal["pressure_sensor", "pump", "valve"]
|
||||
observed_at: WireDateTime
|
||||
state: Literal["online", "offline", "running", "stopped", "open", "closed"]
|
||||
|
||||
|
||||
class DemoAlert(Model):
|
||||
alert_id: UUID
|
||||
asset_id: str
|
||||
occurred_at: WireDateTime
|
||||
cleared_at: WireDateTime | None
|
||||
code: Literal["communication_lost", "low_level"]
|
||||
message: str
|
||||
|
||||
|
||||
class DemoOperation(Model):
|
||||
record_id: UUID
|
||||
asset_id: str
|
||||
occurred_at: WireDateTime
|
||||
operation: Literal["pump_start", "pump_stop", "valve_open", "valve_close"]
|
||||
actor: Literal["synthetic-generator"]
|
||||
note: str
|
||||
|
||||
|
||||
class DemoContent(Model):
|
||||
generator_version: Literal["water-demo@2"]
|
||||
seed: str
|
||||
origin: Literal["synthetic"]
|
||||
window: TimeRange
|
||||
points: list[Point]
|
||||
observations: list[Observation]
|
||||
device_states: list[DemoDeviceState]
|
||||
alerts: list[DemoAlert]
|
||||
operations: list[DemoOperation]
|
||||
|
||||
|
||||
class DemoDataset(DemoContent):
|
||||
project_id: UUID
|
||||
source_id: str
|
||||
source_revision: str
|
||||
|
||||
|
||||
Receipt.model_rebuild()
|
||||
Reference in New Issue
Block a user