A powerful event-driven framework for simulations, games, and complex systems with comprehensive event sourcing, querying, and analysis capabilities.
-
Event Driven
- Decouple components through publish-subscribe patterns
- EventBus with flexible wildcard subscription support
- Immutable events with type, data, and metadata
- JSON serialization for persistence and network transmission
-
Event Sourcing
- Events as the single source of truth for all state changes
- Complete event history with EventLog as the persistent store
- State reconstruction at any point in time
- Parent-child relationships for complete cascade tracking
-
Tick-Based System
- Deterministic event processing with precise ordering
- Discrete time increments for simulations and games
- Easy state snapshots at specific tick boundaries
- Simplified debugging and testing through reproducibility
-
Advanced Event Querying
- Filter events by type, data content, or relationships
- Analyze event cascades and parent-child structures
- Count, group, and visualize events with detailed formatting
- Historical state access through tick-based event queries
-
Ready-to-Use Examples
- Cryptocurrency Trading Bot: Financial simulation with market events
- Adventure Game: Complete game state management through events
-
Developer-Friendly
- Type-safe API with comprehensive type hints
- Zero dependencies (pure Python implementation)
- Extensive test coverage
- Detailed documentation with practical examples
Eventure implements a classic event-driven architecture pattern, providing:
-
Publish-Subscribe Model
- Publishers emit events without knowledge of subscribers
- Subscribers register interest in specific event types
- Complete decoupling between system components
-
EventBus Implementation
- Central event dispatcher connecting publishers and subscribers
- Flexible wildcard subscription system (
prefix.*
,*.suffix
,*
) - Type-based routing with predictable delivery order
-
Practical Benefits
- Highly modular and extensible system design
- Components can be developed, tested, and deployed independently
- Easy to add new functionality without modifying existing code
- Reduced complexity through clear separation of concerns
# Creating an event bus
bus = EventBus()
# Subscribing to events
def on_market_update(event):
print(f"Market updated: {event.data['price']}")
unsubscribe = bus.subscribe("market.update", on_market_update)
# Publishing an event
bus.publish("market.update", {"price": 100.25})
# Later: unsubscribe when no longer needed
unsubscribe()
Eventure is built on the event sourcing architectural pattern, which means:
-
Events as the Source of Truth
- All state changes are represented as immutable events
- The sequence of events becomes the authoritative record
- Current state is derived by replaying the event history
-
Implementation in Eventure
EventLog
: Serves as the event store, maintaining the complete event historyEvent
objects: Immutable records with type, data, tick, and relationship information- State derivation: Game or application state is reconstructed by replaying events
EventQuery
: Provides powerful tools to analyze and understand the event history
-
Benefits in Practice
- Complete audit trail for debugging and analysis
- Time-travel capabilities through historical state reconstruction
- Natural support for undo/redo and save/load features
- Perfect reproducibility of any system state
# Example: State reconstruction from events
events = log.get_events_until_tick(5)
state_at_tick_5 = derive_game_state(events)
# Restore game to a previous state
historical_state = derive_game_state(events_until_save_point)
Eventure uses a tick-based system that provides several key advantages:
-
Deterministic Execution
- Events within a tick are processed in a consistent, predictable order
- Eliminates race conditions and timing-related bugs
- Makes debugging significantly easier with reproducible scenarios
-
Discrete Time Steps
- Game or simulation time advances in well-defined increments
- Each tick represents a clear, isolated state transition
- Multiple events can be processed within a single tick
-
Performance Optimization
- Events can be batched and processed efficiently within ticks
- Reduced overhead compared to continuous time systems
-
Flexible Simulation Control
- Easily implement pause, step-forward, or fast-forward functionality
- Run simulations at different speeds without affecting logic
- Perfect for debugging complex scenarios or analyzing specific moments
# Example: Using ticks for deterministic ordering
log.add_event("market.price_update", {"price": 100}) # Tick 1
log.advance_tick()
log.add_event("trading.buy_order", {"amount": 10}) # Tick 2
Eventure's event cascade system tracks relationships between events, providing several powerful capabilities:
-
Causal Tracking
- Every event can have a parent event that triggered it
- Complete traceability from cause to effect
- Understand complex chains of interactions
-
Debugging and Analysis
- Trace the root cause of any system behavior
- Visualize complete event cascades
- Identify unexpected side effects or event chains
-
Rich Query Capabilities
- Filter events by their relationships
- Find all events triggered by a specific root event
- Analyze patterns in event propagation
-
System Understanding
- Map complex interactions between system components
- Document emergent behaviors
- Improve system design through relationship analysis
# Creating related events
def on_enemy_defeated(event):
# These events will be linked to the parent event
bus.publish("treasure.drop", {"item": "gold_coins"}, parent_event=event)
bus.publish("experience.gain", {"amount": 100}, parent_event=event)
# Querying related events
root_event = query.get_event_by_id("evt_123")
cascade_events = query.get_cascade_events(root_event)
# Using pip
pip install eventure
# Using uv (recommended)
uv add eventure
The fundamental unit representing something that happened:
from eventure import Event
# Create an event
event = Event(
tick=0,
timestamp=time.time(),
type="user.login",
data={"user_id": 123, "ip": "192.168.1.1"}
)
# Events have unique IDs and can be serialized
print(f"Event ID: {event.id}") # Format: {tick}-{typeHash}-{sequence}
json_str = event.to_json()
Tracks, stores, and manages events in a time-ordered sequence:
from eventure import EventLog
# Create an event log
log = EventLog()
# Add events to the log
event = log.add_event("user.login", {"user_id": 123})
log.advance_tick() # Move to next discrete time step
log.add_event("user.action", {"user_id": 123, "action": "view_profile"})
# Create child events (establishing causal relationships)
parent = log.add_event("combat.start", {"player": "hero", "enemy": "dragon"})
child = log.add_event("combat.attack", {"damage": 15}, parent_event=parent)
# Save and load event history
log.save_to_file("game_events.json")
new_log = EventLog.load_from_file("game_events.json")
Manages event publication and subscription:
from eventure import EventBus
# Create an event bus connected to a log
bus = EventBus(log)
# Subscribe to specific events
def on_login(event):
print(f"User {event.data['user_id']} logged in")
unsubscribe = bus.subscribe("user.login", on_login)
# Subscribe with wildcards
bus.subscribe("user.*", lambda e: print(f"User event: {e.type}"))
bus.subscribe("*.error", lambda e: print(f"Error: {e.data['message']}"))
bus.subscribe("*", lambda e: print(f"Any event: {e.type}"))
# Publish events
bus.publish("user.login", {"user_id": 456})
# Unsubscribe when done
unsubscribe()
Eventure supports three powerful wildcard subscription patterns that allow handlers to receive multiple types of events:
# Exact match subscription
bus.subscribe("player.move", on_player_move)
# Prefix wildcard - receives all events with a specific prefix
bus.subscribe("player.*", on_any_player_event) # player.move, player.attack, etc.
# Suffix wildcard - receives all events with a specific suffix
bus.subscribe("*.error", on_any_error_event) # network.error, auth.error, etc.
# Global wildcard - receives ALL events
bus.subscribe("*", on_any_event)
When multiple handlers match an event, they are called in order of specificity:
- Exact match handlers
- Prefix wildcard handlers
- Suffix wildcard handlers
- Global wildcard handlers
This hierarchical dispatch system allows for both specific and general event handling, making it easy to implement logging, debugging, or cross-cutting concerns.
Powerful API for querying, analyzing, and visualizing events:
from eventure import EventQuery
# Create a query interface for an event log
query = EventQuery(log)
# Filter events
combat_events = query.get_events_by_type("combat.attack")
dragon_events = query.get_events_by_data("enemy", "dragon")
# Analyze relationships
child_events = query.get_child_events(parent_event)
cascade = query.get_cascade_events(root_event)
# Count and group
type_counts = query.count_events_by_type()
print(f"Combat events: {type_counts.get('combat.attack', 0)}")
# Get events by tick or relationship
tick5_events = query.get_events_at_tick(5)
root_events = query.get_root_events()
# Visualize events and cascades
query.print_event_cascade() # All events organized by tick
query.print_single_cascade(root_event) # Show a specific cascade
query.print_event_details(event) # Show details of a single event
Eventure includes two complete example applications demonstrating real-world usage:
A simulated trading system showing market events, trading signals, and order execution:
from examples.crypto_trading_bot import CryptoTradingBot
# Create and run a trading simulation
bot = CryptoTradingBot()
bot.run_simulation()
# Query interesting patterns from the event log
query = EventQuery(bot.event_log)
buy_signals = query.get_events_by_data("signal", "BUY")
Key features demonstrated:
- Market simulation with price and volume updates
- Trading strategy implementation via events
- Order creation and execution
- Portfolio tracking
- Event-based system analysis
A text-based adventure game showing game state management:
from examples.adventure_game import AdventureGame
# Create and run a game
game = AdventureGame()
game.run_game()
# Analyze game events
query = EventQuery(game.event_log)
combat_events = query.get_events_by_type("combat.start")
treasure_events = query.get_events_by_type("treasure.found")
Key features demonstrated:
- Room navigation and discovery
- Item collection and inventory management
- Enemy encounters and combat
- Event cascades (e.g., entering a room triggers discoveries)
- Game state derivation from events
The EventQuery API provides a consistent set of methods for analyzing and visualizing events:
# By event type
strategy_signals = query.get_events_by_type("strategy.signal")
# By data content
buy_signals = query.get_events_by_data("signal", "BUY")
dragon_encounters = query.get_events_by_data("enemy", "dragon")
# By tick
tick_3_events = query.get_events_at_tick(3)
# Root events (with no parent or parent in previous tick)
root_events = query.get_root_events()
# Direct children of an event
children = query.get_child_events(parent_event)
# Complete cascade (parent, children, grandchildren, etc.)
full_cascade = query.get_cascade_events(root_event)
# Count events by type
counts = query.count_events_by_type()
print(f"Total combat events: {sum(counts.get(t, 0) for t in counts if t.startswith('combat.'))}")
# Print all events organized by tick
query.print_event_cascade()
# Print a specific event cascade
query.print_single_cascade(root_event)
# Print details of a specific event
query.print_event_details(event)
The EventQuery API provides powerful visualization capabilities for event cascades. Here are some examples from the included Adventure Game example:
┌─── TICK 4 ───┐
│
↓ room.enter (caused by: player.move @ tick 3)
ID: 4-FADA-1
Data:
room: treasury
description: Glittering treasures fill this heavily guarded room.
│
└─ enemy.encounter
│ ID: 4-ADCF-1
│ Data:
│ enemy: dragon
│ message: A dragon appears before you!
└───────────────┘
┌─── TICK 5 ───┐
│
↓ combat.start (caused by: enemy.encounter @ tick 4)
ID: 5-ABAA-1
Data:
enemy: dragon
enemy_health: 100
message: Combat with dragon begins!
│
↓ combat.round (caused by: enemy.encounter @ tick 4)
ID: 5-BDBB-1
Data:
enemy: dragon
round: 1
message: Round 1 against dragon!
│
└─ combat.damage_dealt
│ ID: 5-DDDF-1
│ Data:
│ enemy: dragon
│ amount: 18
│ message: You hit the dragon for 18 damage!
└───────────────┘
This visualization shows:
- Events organized by tick
- Parent-child relationships with indentation
- Causal connections between events (shown with arrows)
- Complete event data for analysis
To generate these visualizations in your code:
# Print all events in the log organized by tick
query.print_event_cascade()
# Print a specific cascade starting from a root event
query.print_single_cascade(root_event)
# Print details of a single event
query.print_event_details(event)
One of Eventure's most powerful features is the ability to reconstruct state by replaying events:
# State can be derived entirely from events
def derive_game_state(events):
state = {"health": 100, "inventory": [], "location": "start"}
for event in events:
if event.type == "player.damage":
state["health"] -= event.data["amount"]
elif event.type == "item.pickup":
state["inventory"].append(event.data["item"])
elif event.type == "player.move":
state["location"] = event.data["destination"]
return state
# Current state from all events
current_state = derive_game_state(log.events)
# Historical state (state at tick 5)
tick_5_events = [e for e in log.events if e.tick <= 5]
historical_state = derive_game_state(tick_5_events)
# Create a handler that triggers follow-up events
def combat_handler(event):
if event.data.get("enemy_health", 0) <= 0:
# Generate a cascade event based on the original event
bus.publish("enemy.defeated",
{"enemy": event.data["enemy"]},
parent_event=event) # Parent reference maintains event chain
# Subscribe to combat events
bus.subscribe("combat.attack", combat_handler)
For the complete API documentation, see the API Reference.
# Clone the repository
git clone https://github.com/yourusername/eventure.git
cd eventure
# Install development dependencies
uv sync --all-extras
# Run all tests
just test