Files
markitect-main/markitect/spaces/events/models.py
tegwick 0a494b2011 feat(spaces): implement Phase 2 Event System
Week 4 - Event Infrastructure:
- Create SpaceEventType enum with 18 event types covering space lifecycle,
  document operations, variables, references, rendering, sync, and cache
- Create SpaceEvent dataclass with serialization/deserialization
- Create EventBus with sync/async handler support, priority ordering,
  global handlers, and optional event history
- Add event factory functions for common events

Week 5 - Event Integration:
- Wire EventBus into SpaceService as optional dependency
- Emit events for all space operations:
  - SPACE_CREATED, SPACE_UPDATED, SPACE_DELETED, SPACE_ACTIVATED, SPACE_ARCHIVED
  - DOCUMENT_ADDED, DOCUMENT_REMOVED, DOCUMENT_MOVED, DOCUMENT_CONTENT_CHANGED
  - VARIABLE_SET, VARIABLE_DELETED
- Create integration tests for event propagation patterns

Test coverage: 187 tests total
- 43 unit tests for event system
- 20 integration tests for event propagation
- 124 existing tests continue to pass

Capabilities delivered:
- CAP-010: SpaceEvent base with type, payload, timestamp
- CAP-011: EventBus with in-process publish/subscribe
- CAP-012: Event handlers registry with priority support
- CAP-013: Change detection via content hash comparison

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 07:41:47 +01:00

240 lines
6.5 KiB
Python

"""
Event models for Information Spaces.
This module defines the event types and data structures used
by the event system for space operations.
"""
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, Optional
import uuid
class SpaceEventType(Enum):
"""
Types of events that can occur in the space system.
Events are organized by category:
- Space lifecycle: creation, updates, deletion, status changes
- Document operations: add, update, remove, move
- Variable operations: set, delete
- Rendering: start, complete, fail
- Sync: start, complete, conflict
"""
# Space lifecycle events
SPACE_CREATED = "space.created"
SPACE_UPDATED = "space.updated"
SPACE_DELETED = "space.deleted"
SPACE_ACTIVATED = "space.activated"
SPACE_ARCHIVED = "space.archived"
# Document events
DOCUMENT_ADDED = "document.added"
DOCUMENT_UPDATED = "document.updated"
DOCUMENT_REMOVED = "document.removed"
DOCUMENT_MOVED = "document.moved"
DOCUMENT_CONTENT_CHANGED = "document.content_changed"
# Variable events
VARIABLE_SET = "variable.set"
VARIABLE_DELETED = "variable.deleted"
# Reference events
REFERENCE_ADDED = "reference.added"
REFERENCE_CLEARED = "reference.cleared"
# Rendering events
RENDER_STARTED = "render.started"
RENDER_COMPLETED = "render.completed"
RENDER_FAILED = "render.failed"
# Sync events
SYNC_STARTED = "sync.started"
SYNC_COMPLETED = "sync.completed"
SYNC_CONFLICT = "sync.conflict"
# Cache events
CACHE_INVALIDATED = "cache.invalidated"
@dataclass
class SpaceEvent:
"""
Represents an event in the space system.
Events are immutable records of operations that have occurred.
They carry enough information for handlers to react appropriately.
Attributes:
event_type: The type of event
space_id: The ID of the affected space
payload: Event-specific data
event_id: Unique identifier for this event
timestamp: When the event occurred
source: Optional identifier of the event source
correlation_id: Optional ID to correlate related events
"""
event_type: SpaceEventType
space_id: str
payload: Dict[str, Any] = field(default_factory=dict)
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: datetime = field(default_factory=datetime.now)
source: Optional[str] = None
correlation_id: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert the event to a dictionary for serialization."""
return {
"event_id": self.event_id,
"event_type": self.event_type.value,
"space_id": self.space_id,
"payload": self.payload,
"timestamp": self.timestamp.isoformat(),
"source": self.source,
"correlation_id": self.correlation_id,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "SpaceEvent":
"""Create an event from a dictionary."""
return cls(
event_id=data.get("event_id", str(uuid.uuid4())),
event_type=SpaceEventType(data["event_type"]),
space_id=data["space_id"],
payload=data.get("payload", {}),
timestamp=datetime.fromisoformat(data["timestamp"])
if data.get("timestamp")
else datetime.now(),
source=data.get("source"),
correlation_id=data.get("correlation_id"),
)
# Convenience factory functions for common events
def space_created_event(
space_id: str,
name: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a SPACE_CREATED event."""
return SpaceEvent(
event_type=SpaceEventType.SPACE_CREATED,
space_id=space_id,
payload={"name": name},
source=source,
)
def space_updated_event(
space_id: str,
changes: Dict[str, Any],
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a SPACE_UPDATED event."""
return SpaceEvent(
event_type=SpaceEventType.SPACE_UPDATED,
space_id=space_id,
payload={"changes": changes},
source=source,
)
def space_deleted_event(
space_id: str,
name: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a SPACE_DELETED event."""
return SpaceEvent(
event_type=SpaceEventType.SPACE_DELETED,
space_id=space_id,
payload={"name": name},
source=source,
)
def document_added_event(
space_id: str,
document_id: str,
space_path: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a DOCUMENT_ADDED event."""
return SpaceEvent(
event_type=SpaceEventType.DOCUMENT_ADDED,
space_id=space_id,
payload={"document_id": document_id, "space_path": space_path},
source=source,
)
def document_updated_event(
space_id: str,
document_id: str,
changes: Dict[str, Any],
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a DOCUMENT_UPDATED event."""
return SpaceEvent(
event_type=SpaceEventType.DOCUMENT_UPDATED,
space_id=space_id,
payload={"document_id": document_id, "changes": changes},
source=source,
)
def document_removed_event(
space_id: str,
document_id: str,
space_path: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a DOCUMENT_REMOVED event."""
return SpaceEvent(
event_type=SpaceEventType.DOCUMENT_REMOVED,
space_id=space_id,
payload={"document_id": document_id, "space_path": space_path},
source=source,
)
def document_content_changed_event(
space_id: str,
document_id: str,
old_hash: Optional[str],
new_hash: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a DOCUMENT_CONTENT_CHANGED event."""
return SpaceEvent(
event_type=SpaceEventType.DOCUMENT_CONTENT_CHANGED,
space_id=space_id,
payload={
"document_id": document_id,
"old_hash": old_hash,
"new_hash": new_hash,
},
source=source,
)
def cache_invalidated_event(
space_id: str,
document_ids: list,
reason: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a CACHE_INVALIDATED event."""
return SpaceEvent(
event_type=SpaceEventType.CACHE_INVALIDATED,
space_id=space_id,
payload={"document_ids": document_ids, "reason": reason},
source=source,
)