- Add JSON schema at schemas/state.json with full validation rules - Implement State, StateType, StateMetadata dataclasses in models/state.py - Support immutable state objects with versioning and TTL - Include serialization/deserialization (JSON and dict) - Add next_version() for state history chaining - Comprehensive test suite with 19 tests (100% pass) - Full documentation at docs/state-schema.md Features: - UUID validation for all ID fields - Optimistic locking via version numbers - Flexible payload structure - Metadata support (source, provenance, tags) - Expiration checking via TTL - Complete type hints Closes Issue #3
234 lines
7.3 KiB
Python
234 lines
7.3 KiB
Python
"""
|
|
State Model for SEED Architecture
|
|
|
|
This module defines the State dataclass and related utilities for the
|
|
State component of the SEED (State-Event-Entity-Domain) architecture.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import uuid
|
|
from dataclasses import dataclass, field, asdict
|
|
from datetime import datetime, timezone
|
|
from enum import Enum, auto
|
|
from typing import Any, Optional, Dict, List
|
|
from pathlib import Path
|
|
|
|
|
|
class StateType(Enum):
|
|
"""Enumeration of valid state types."""
|
|
ACTIVE = "active"
|
|
INACTIVE = "inactive"
|
|
PENDING = "pending"
|
|
ARCHIVED = "archived"
|
|
DELETED = "deleted"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StateMetadata:
|
|
"""Metadata associated with a state record."""
|
|
source: str = "unknown"
|
|
provenance: Optional[str] = None
|
|
created_by: Optional[str] = None
|
|
tags: List[str] = field(default_factory=list)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert metadata to dictionary."""
|
|
return {
|
|
"source": self.source,
|
|
"provenance": self.provenance,
|
|
"created_by": self.created_by,
|
|
"tags": self.tags
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> StateMetadata:
|
|
"""Create metadata from dictionary."""
|
|
return cls(
|
|
source=data.get("source", "unknown"),
|
|
provenance=data.get("provenance"),
|
|
created_by=data.get("created_by"),
|
|
tags=data.get("tags", [])
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class State:
|
|
"""
|
|
Immutable State record for SEED architecture.
|
|
|
|
Attributes:
|
|
id: Unique identifier for this state record (UUID v4)
|
|
entity_id: Reference to the entity this state belongs to
|
|
state_type: The type/category of this state
|
|
payload: Flexible JSON payload containing state-specific data
|
|
timestamp: ISO 8601 timestamp when this state was recorded
|
|
version: Version number for optimistic locking
|
|
metadata: Additional metadata about this state record
|
|
previous_state_id: Reference to the previous state (None if first)
|
|
ttl: Time-to-live in seconds (None for no expiration)
|
|
"""
|
|
id: str
|
|
entity_id: str
|
|
state_type: StateType
|
|
payload: Dict[str, Any]
|
|
timestamp: datetime
|
|
version: int
|
|
metadata: StateMetadata = field(default_factory=StateMetadata)
|
|
previous_state_id: Optional[str] = None
|
|
ttl: Optional[int] = None
|
|
|
|
def __post_init__(self):
|
|
"""Validate state after initialization."""
|
|
# Validate UUIDs
|
|
try:
|
|
uuid.UUID(self.id)
|
|
uuid.UUID(self.entity_id)
|
|
if self.previous_state_id:
|
|
uuid.UUID(self.previous_state_id)
|
|
except ValueError as e:
|
|
raise ValueError(f"Invalid UUID format: {e}")
|
|
|
|
# Validate version
|
|
if self.version < 1:
|
|
raise ValueError("Version must be >= 1")
|
|
|
|
# Validate TTL
|
|
if self.ttl is not None and self.ttl < 0:
|
|
raise ValueError("TTL must be non-negative")
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert state to dictionary representation."""
|
|
return {
|
|
"id": self.id,
|
|
"entity_id": self.entity_id,
|
|
"state_type": self.state_type.value,
|
|
"payload": self.payload,
|
|
"timestamp": self.timestamp.isoformat(),
|
|
"version": self.version,
|
|
"metadata": self.metadata.to_dict(),
|
|
"previous_state_id": self.previous_state_id,
|
|
"ttl": self.ttl
|
|
}
|
|
|
|
def to_json(self, indent: Optional[int] = None) -> str:
|
|
"""Serialize state to JSON string."""
|
|
return json.dumps(self.to_dict(), indent=indent, default=str)
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
entity_id: str,
|
|
state_type: StateType,
|
|
payload: Dict[str, Any],
|
|
version: int = 1,
|
|
metadata: Optional[StateMetadata] = None,
|
|
previous_state_id: Optional[str] = None,
|
|
ttl: Optional[int] = None
|
|
) -> State:
|
|
"""
|
|
Factory method to create a new State with auto-generated ID and timestamp.
|
|
|
|
Args:
|
|
entity_id: The entity this state belongs to
|
|
state_type: Type of state
|
|
payload: State data payload
|
|
version: Version number (default: 1)
|
|
metadata: Optional metadata
|
|
previous_state_id: Link to previous state
|
|
ttl: Optional time-to-live in seconds
|
|
|
|
Returns:
|
|
New State instance
|
|
"""
|
|
return cls(
|
|
id=str(uuid.uuid4()),
|
|
entity_id=entity_id,
|
|
state_type=state_type,
|
|
payload=payload,
|
|
timestamp=datetime.now(timezone.utc),
|
|
version=version,
|
|
metadata=metadata or StateMetadata(),
|
|
previous_state_id=previous_state_id,
|
|
ttl=ttl
|
|
)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> State:
|
|
"""Create State from dictionary representation."""
|
|
return cls(
|
|
id=data["id"],
|
|
entity_id=data["entity_id"],
|
|
state_type=StateType(data["state_type"]),
|
|
payload=data["payload"],
|
|
timestamp=datetime.fromisoformat(data["timestamp"].replace('Z', '+00:00')),
|
|
version=data["version"],
|
|
metadata=StateMetadata.from_dict(data.get("metadata", {})),
|
|
previous_state_id=data.get("previous_state_id"),
|
|
ttl=data.get("ttl")
|
|
)
|
|
|
|
@classmethod
|
|
def from_json(cls, json_str: str) -> State:
|
|
"""Deserialize State from JSON string."""
|
|
return cls.from_dict(json.loads(json_str))
|
|
|
|
def is_expired(self) -> bool:
|
|
"""Check if this state has expired based on TTL."""
|
|
if self.ttl is None:
|
|
return False
|
|
age = (datetime.now(timezone.utc) - self.timestamp).total_seconds()
|
|
return age > self.ttl
|
|
|
|
def next_version(self, new_payload: Dict[str, Any]) -> State:
|
|
"""
|
|
Create a new State representing the next version of this state.
|
|
|
|
Args:
|
|
new_payload: Updated payload for the new state
|
|
|
|
Returns:
|
|
New State instance with incremented version
|
|
"""
|
|
return State.create(
|
|
entity_id=self.entity_id,
|
|
state_type=self.state_type,
|
|
payload=new_payload,
|
|
version=self.version + 1,
|
|
metadata=self.metadata,
|
|
previous_state_id=self.id,
|
|
ttl=self.ttl
|
|
)
|
|
|
|
|
|
def load_schema() -> Dict[str, Any]:
|
|
"""Load the JSON schema for validation."""
|
|
schema_path = Path(__file__).parent.parent / "schemas" / "state.json"
|
|
with open(schema_path, 'r') as f:
|
|
return json.load(f)
|
|
|
|
|
|
# Example usage
|
|
if __name__ == "__main__":
|
|
# Create a sample state
|
|
state = State.create(
|
|
entity_id=str(uuid.uuid4()),
|
|
state_type=StateType.ACTIVE,
|
|
payload={"status": "running", "progress": 75},
|
|
metadata=StateMetadata(
|
|
source="electra-archon",
|
|
created_by="electra",
|
|
tags=["seed", "priority"]
|
|
)
|
|
)
|
|
|
|
print("Created State:")
|
|
print(state.to_json(indent=2))
|
|
print(f"\nIs expired: {state.is_expired()}")
|
|
|
|
# Create next version
|
|
next_state = state.next_version({"status": "running", "progress": 90})
|
|
print(f"\nNext version: {next_state.version}")
|
|
print(f"Previous state ID: {next_state.previous_state_id}")
|