Getting Started
This guide walks you through installing PyRapide and building your first causal event-driven architecture: a temperature monitoring system that detects high readings, fires alerts, and tracks the full causal chain from sensor to notification.
Prerequisites
- Python 3.11+. PyRapide uses modern Python features including
asynciotask groups and type parameter syntax. - pip or uv. Any standard Python package manager will work.
PyRapide has zero required dependencies beyond the Python standard library. Optional extras pull in additional packages for visualization, prediction, and MCP integration.
Installation
Basic Install
$ pip install pyrapide
Optional Extras
Install additional capabilities as needed:
# Visualization: render causal graphs as interactive DAG diagrams
pip install pyrapide[viz]
# Prediction: forecast future events from observed causal patterns
pip install pyrapide[prediction]
# MCP: Model Context Protocol server integration
pip install pyrapide[mcp]
# Everything
pip install pyrapide[viz,prediction,mcp]
If you are using uv, replace pip install with uv add in the commands above.
Your First Causal Event Architecture
We will build a MonitoringSystem with two components: a temperature sensor that emits readings and an alerter that fires when the temperature exceeds a threshold. PyRapide will automatically track the causal chain from each sensor reading to the alert it triggers.
Step 1: Define Interfaces
Interfaces declare the behavioral contract of a component: what actions it can perform and what data those actions carry. They are abstract: no implementation yet.
1from pyrapide import interface, action
2
3@interface
4class Sensor:
5 """A component that produces temperature readings."""
6 @action
7 async def reading(self, temperature: float) -> None: ...
8
9@interface
10class Alerter:
11 """A component that emits alerts with a message and severity."""
12 @action
13 async def alert(self, message: str, severity: str) -> None: ...
The @interface decorator registers the class with PyRapide's type system. Each @action method becomes a typed event source. When invoked during execution, it generates a traceable event in the causal graph.
Step 2: Implement Modules
Modules are the concrete implementations of interfaces. This is where your application logic
lives. The TemperatureSensor emits a reading, and the ThresholdAlerter fires an alert if the temperature is too high.
1from pyrapide import module
2
3@module(implements=Sensor)
4class TemperatureSensor:
5 async def reading(self, temperature: float) -> None:
6 print(f"Sensor recorded {temperature}°C")
7
8@module(implements=Alerter)
9class ThresholdAlerter:
10 threshold: float = 100.0
11
12 async def alert(self, message: str, severity: str) -> None:
13 print(f"[{severity.upper()}] {message}")
Step 3: Compose the Architecture
An architecture wires components together. The connections method declares that
whenever the sensor emits a reading event, it should be routed to the alerter.
1from pyrapide import architecture, connect, Pattern
2
3@architecture
4class MonitoringSystem:
5 sensor: Sensor
6 alerter: Alerter
7
8 def connections(self):
9 return [
10 connect(Pattern.match("Sensor.reading"), "alerter")
11 ]
Pattern.match("Sensor.reading") matches any event produced by the reading action on the Sensor interface. You can add attribute filters like Pattern.match("Sensor.reading", temperature=lambda t: t > 100) to route selectively.
Step 4: Run the Engine
The Engine executes the architecture asynchronously. Feed it a sequence of stimuli
(initial events) and it runs the full causal chain to completion, returning a computation, the complete causal event graph.
1from pyrapide import Engine
2import asyncio
3
4async def main():
5 engine = Engine(MonitoringSystem)
6
7 # Stimulate the system with temperature readings
8 computation = await engine.run(stimuli=[
9 ("sensor", "reading", {"temperature": 72.0}),
10 ("sensor", "reading", {"temperature": 150.0}),
11 ("sensor", "reading", {"temperature": 95.0}),
12 ])
13
14 print(f"Total events: {len(computation.events)}")
15
16asyncio.run(main())
Expected output:
$ Sensor recorded 72.0°C
Sensor recorded 150.0°C
[CRITICAL] Temperature 150.0°C exceeds threshold
Sensor recorded 95.0°C
Total events: 4
Notice that the 150.0 reading produces two events: the reading itself and the alert it causes. PyRapide automatically tracks that the alert was caused by the reading. This causal link is stored in the computation, not just in your log output.
Step 5: Inspect Results
The computation object is a queryable causal event graph. You can traverse causal
relationships, find root causes, and compute impact sets.
1# Find all events and their causal relationships
2for event in computation.events:
3 print(f"{event.action} | causes: {[e.action for e in event.causes]}")
4
5# Root-cause analysis: why did the alert fire?
6alert_event = computation.find("Alerter.alert")[0]
7root_causes = computation.root_causes(alert_event)
8print(f"Alert root causes: {root_causes}")
9
10# Forward slice: what did the 150° reading cause?
11hot_reading = computation.find("Sensor.reading", temperature=150.0)[0]
12impact = computation.forward_slice(hot_reading)
13print(f"Impact of hot reading: {impact}")
Expected output:
$ Sensor.reading | causes: []
Sensor.reading | causes: []
Alerter.alert | causes: [Sensor.reading]
Sensor.reading | causes: []
Alert root causes: [Event(Sensor.reading, temperature=150.0)]
Impact of hot reading: [Event(Alerter.alert, message='Temperature 150.0°C exceeds threshold')]
Step 6: Enforce Constraints
Constraints are declarative rules that a computation must satisfy. They let you specify what must happen and what must never happen in your system.
1from pyrapide import must_match, never
2
3# Every reading above the threshold MUST trigger an alert
4must_match(
5 Pattern.seq(
6 Pattern.match("Sensor.reading", temperature=lambda t: t > 100),
7 Pattern.match("Alerter.alert")
8 ),
9 name="high_temp_must_alert"
10)
11
12# An alert must NEVER fire without a preceding high reading
13never(
14 Pattern.match("Alerter.alert").without_cause("Sensor.reading"),
15 name="no_spurious_alerts"
16)
17
18# Validate the computation against all constraints
19results = computation.check_constraints()
20for result in results:
21 status = "PASS" if result.satisfied else "FAIL"
22 print(f"[{status}] {result.name}")
Expected output:
$ [PASS] high_temp_must_alert
[PASS] no_spurious_alerts
Constraints are checked against the causal structure, not just event presence. The without_cause filter checks whether an event has a specific causal ancestor in the graph. This is something impossible with flat logs.
Complete Example
Here is the full working example in a single file:
1from pyrapide import (
2 interface, action, module, architecture,
3 connect, Pattern, Engine, must_match, never,
4)
5import asyncio
6
7# ── Interfaces ──────────────────────────────────────────
8
9@interface
10class Sensor:
11 @action
12 async def reading(self, temperature: float) -> None: ...
13
14@interface
15class Alerter:
16 @action
17 async def alert(self, message: str, severity: str) -> None: ...
18
19# ── Modules ─────────────────────────────────────────────
20
21@module(implements=Sensor)
22class TemperatureSensor:
23 async def reading(self, temperature: float) -> None:
24 print(f"Sensor recorded {temperature}°C")
25
26@module(implements=Alerter)
27class ThresholdAlerter:
28 threshold: float = 100.0
29
30 async def alert(self, message: str, severity: str) -> None:
31 print(f"[{severity.upper()}] {message}")
32
33# ── Architecture ────────────────────────────────────────
34
35@architecture
36class MonitoringSystem:
37 sensor: Sensor
38 alerter: Alerter
39
40 def connections(self):
41 return [connect(Pattern.match("Sensor.reading"), "alerter")]
42
43# ── Constraints ─────────────────────────────────────────
44
45must_match(
46 Pattern.seq(
47 Pattern.match("Sensor.reading", temperature=lambda t: t > 100),
48 Pattern.match("Alerter.alert"),
49 ),
50 name="high_temp_must_alert",
51)
52
53never(
54 Pattern.match("Alerter.alert").without_cause("Sensor.reading"),
55 name="no_spurious_alerts",
56)
57
58# ── Run ─────────────────────────────────────────────────
59
60async def main():
61 engine = Engine(MonitoringSystem)
62 computation = await engine.run(stimuli=[
63 ("sensor", "reading", {"temperature": 72.0}),
64 ("sensor", "reading", {"temperature": 150.0}),
65 ("sensor", "reading", {"temperature": 95.0}),
66 ])
67
68 # Inspect causal relationships
69 for event in computation.events:
70 print(f"{event.action} | causes: {[e.action for e in event.causes]}")
71
72 # Validate constraints
73 for result in computation.check_constraints():
74 status = "PASS" if result.satisfied else "FAIL"
75 print(f"[{status}] {result.name}")
76
77asyncio.run(main())
Next Steps
Now that you have a working causal event architecture, explore the rest of PyRapide:
- Core Concepts: Understand events, posets, computations, and the full conceptual model.
- Architectures: Learn about connection semantics: pipe, broadcast, and filter.
- Patterns: Write advanced event matching expressions with sequences, guards, and combinators.
- Constraints: Define richer behavioral rules for safety-critical systems.
- Streaming: Process events from live sources in real time with sliding windows.
- MCP Integration: Connect PyRapide to Model Context Protocol servers for agentic AI observability.
- Analysis: Root-cause analysis, forward/backward slicing, and causal path queries.
- Visualization: Render your causal graphs as interactive diagrams.
Having trouble? Check the GitHub repository for issues and discussions, or explore the examples/ directory for more complete use cases.