parallelazy 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- parallelazy/__init__.py +43 -0
- parallelazy/environment_manager.py +55 -0
- parallelazy/examples/__init__.py +0 -0
- parallelazy/examples/example_configuration.py +194 -0
- parallelazy/examples/example_count.py +84 -0
- parallelazy/examples/example_data_pipeline.py +226 -0
- parallelazy/examples/example_env_variables.py +158 -0
- parallelazy/examples/example_error_handling.py +266 -0
- parallelazy/examples/example_graceful_shutdown.py +139 -0
- parallelazy/examples/example_ml_training.py +280 -0
- parallelazy/examples/example_pubsub.py +137 -0
- parallelazy/examples/example_rpc_calls.py +182 -0
- parallelazy/examples/example_worker_pool.py +188 -0
- parallelazy/plugin.py +223 -0
- parallelazy/plugin_data.py +466 -0
- parallelazy/plugin_manager.py +617 -0
- parallelazy/plugin_rpc.py +511 -0
- parallelazy/plugin_transport.py +347 -0
- parallelazy/plugin_worker.py +700 -0
- parallelazy/tests/__init__.py +0 -0
- parallelazy/utils.py +71 -0
- parallelazy-0.0.1.dist-info/METADATA +17 -0
- parallelazy-0.0.1.dist-info/RECORD +26 -0
- parallelazy-0.0.1.dist-info/WHEEL +5 -0
- parallelazy-0.0.1.dist-info/licenses/LICENSE +339 -0
- parallelazy-0.0.1.dist-info/top_level.txt +1 -0
parallelazy/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ModularNet Plugin System: Lightweight multiprocessing-based plugin architecture.
|
|
3
|
+
|
|
4
|
+
Provides modular plugin execution with pub/sub messaging, environment variables,
|
|
5
|
+
and service invocation.
|
|
6
|
+
|
|
7
|
+
Main Components:
|
|
8
|
+
- PluginDescriptor: Serializable plugin metadata
|
|
9
|
+
- Plugin: Abstract base class for plugins
|
|
10
|
+
- PluginManager: Plugin orchestration and management
|
|
11
|
+
- PluginWorker: Plugin subprocess worker
|
|
12
|
+
- PluginMessages: Unified IPC message protocol (Request, Response)
|
|
13
|
+
- DataExchange: Unified subscription model (topics + env vars)
|
|
14
|
+
- ManagerProxy: Client-side proxy for direct manager access from plugins
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from parallelazy.plugin import Plugin, PluginState, PluginDescriptor
|
|
18
|
+
from parallelazy.environment_manager import EnvironmentManager
|
|
19
|
+
from parallelazy.plugin_manager import PluginManager
|
|
20
|
+
|
|
21
|
+
from parallelazy.plugin_data import (
|
|
22
|
+
Request, Response,
|
|
23
|
+
Subscription,
|
|
24
|
+
PluginStatus, ProcessStatus
|
|
25
|
+
)
|
|
26
|
+
from parallelazy.plugin_rpc import PluginRPC
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"PluginDescriptor",
|
|
30
|
+
"Plugin",
|
|
31
|
+
"PluginState",
|
|
32
|
+
"EnvironmentManager",
|
|
33
|
+
"EnvUpdate",
|
|
34
|
+
"PluginManager",
|
|
35
|
+
"Request",
|
|
36
|
+
"Response",
|
|
37
|
+
"Channel",
|
|
38
|
+
"Subscription",
|
|
39
|
+
"SubscriptionType",
|
|
40
|
+
"PluginStatus",
|
|
41
|
+
"ProcessStatus",
|
|
42
|
+
"PluginRPC",
|
|
43
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Environment State System: Manages shared environment variables across plugins.
|
|
3
|
+
|
|
4
|
+
Simple thread-safe system for reading/writing global environment variables
|
|
5
|
+
and broadcasting updates to interested plugins.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
from threading import Lock
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EnvironmentManager:
|
|
17
|
+
"""Thread-safe manager for shared environment variables."""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""Initialize environment manager."""
|
|
21
|
+
self._env: Dict[str, Any] = {}
|
|
22
|
+
self._lock = Lock()
|
|
23
|
+
|
|
24
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
25
|
+
"""Get environment variable value."""
|
|
26
|
+
with self._lock:
|
|
27
|
+
return self._env.get(key, default)
|
|
28
|
+
|
|
29
|
+
def set(self, key: str, value: Any) -> None:
|
|
30
|
+
"""Set environment variable."""
|
|
31
|
+
with self._lock:
|
|
32
|
+
self._env[key] = value
|
|
33
|
+
|
|
34
|
+
def get_all(self) -> Dict[str, Any]:
|
|
35
|
+
"""Get all environment variables."""
|
|
36
|
+
with self._lock:
|
|
37
|
+
return self._env.copy()
|
|
38
|
+
|
|
39
|
+
def set_all(self, env_dict: Dict[str, Any]) -> None:
|
|
40
|
+
"""Set multiple environment variables."""
|
|
41
|
+
for key, value in env_dict.items():
|
|
42
|
+
self.set(key, value)
|
|
43
|
+
|
|
44
|
+
def clear(self, key: str=None) -> None:
|
|
45
|
+
"""Clear a specific environment variable."""
|
|
46
|
+
with self._lock:
|
|
47
|
+
if key in self._env:
|
|
48
|
+
del self._env[key]
|
|
49
|
+
else:
|
|
50
|
+
self._env.clear()
|
|
51
|
+
|
|
52
|
+
def __contains__(self, key: str) -> bool:
|
|
53
|
+
"""Check if key exists in environment."""
|
|
54
|
+
with self._lock:
|
|
55
|
+
return key in self._env
|
|
File without changes
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration & Options Example: Plugin customization at load time.
|
|
3
|
+
|
|
4
|
+
This example shows how plugins can:
|
|
5
|
+
1. Accept configuration options when loaded
|
|
6
|
+
2. Use options to customize behavior
|
|
7
|
+
3. Override configuration dynamically
|
|
8
|
+
4. Validate configuration options
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
|
|
15
|
+
from parallelazy.plugin import Plugin, PluginDescriptor
|
|
16
|
+
from parallelazy.plugin_worker import PluginWorker
|
|
17
|
+
from parallelazy.plugin_manager import PluginManager
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Sensor(Plugin):
|
|
23
|
+
"""
|
|
24
|
+
Plugin that simulates a sensor with configurable sampling.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
29
|
+
default_options = {
|
|
30
|
+
"sample_rate": 10, # Hz
|
|
31
|
+
"noise_level": 0.1,
|
|
32
|
+
"sensor_type": "temperature",
|
|
33
|
+
}
|
|
34
|
+
if options:
|
|
35
|
+
default_options.update(options)
|
|
36
|
+
|
|
37
|
+
return super().Descriptor(
|
|
38
|
+
version="1.0.0",
|
|
39
|
+
author="ModularNet Team",
|
|
40
|
+
description="Configurable sensor plugin",
|
|
41
|
+
options=default_options
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
45
|
+
super().__init__(worker, plugin_id, options)
|
|
46
|
+
self.sample_count = 0
|
|
47
|
+
self.sample_rate = options.get("sample_rate", 10)
|
|
48
|
+
self.noise_level = options.get("noise_level", 0.1)
|
|
49
|
+
self.sensor_type = options.get("sensor_type", "temperature")
|
|
50
|
+
|
|
51
|
+
def on_start(self) -> None:
|
|
52
|
+
logger.info(f"[{self.plugin_id}] Sensor started with options:")
|
|
53
|
+
logger.info(f" - type: {self.sensor_type}")
|
|
54
|
+
logger.info(f" - sample_rate: {self.sample_rate} Hz")
|
|
55
|
+
logger.info(f" - noise_level: {self.noise_level}")
|
|
56
|
+
self.env_set("sensor.config", {
|
|
57
|
+
"type": self.sensor_type,
|
|
58
|
+
"sample_rate": self.sample_rate,
|
|
59
|
+
"noise_level": self.noise_level
|
|
60
|
+
})
|
|
61
|
+
self.env_set("sensor.samples", 0)
|
|
62
|
+
|
|
63
|
+
def main(self):
|
|
64
|
+
self.sample_count += 1
|
|
65
|
+
# Generate simulated sensor reading
|
|
66
|
+
base_value = 20 + (self.sample_count % 10) # Temperature 20-30°C
|
|
67
|
+
import random
|
|
68
|
+
noise = random.gauss(0, self.noise_level)
|
|
69
|
+
reading = base_value + noise
|
|
70
|
+
|
|
71
|
+
if self.sample_count % 5 == 0:
|
|
72
|
+
logger.info(f"[{self.plugin_id}] Sample #{self.sample_count}: {reading:.2f}")
|
|
73
|
+
self.topic_publish("sensor.readings", {
|
|
74
|
+
"sample": self.sample_count,
|
|
75
|
+
"value": reading,
|
|
76
|
+
"type": self.sensor_type
|
|
77
|
+
})
|
|
78
|
+
self.env_set("sensor.samples", self.sample_count, wait=False)
|
|
79
|
+
|
|
80
|
+
def on_stop(self) -> None:
|
|
81
|
+
logger.info(f"[{self.plugin_id}] Sensor stopped, collected {self.sample_count} samples")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SensorLogger(Plugin):
|
|
85
|
+
"""
|
|
86
|
+
Plugin that logs sensor readings with configurable verbosity.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
91
|
+
default_options = {
|
|
92
|
+
"verbose": False,
|
|
93
|
+
"log_every_n": 5,
|
|
94
|
+
}
|
|
95
|
+
if options:
|
|
96
|
+
default_options.update(options)
|
|
97
|
+
|
|
98
|
+
return super().Descriptor(
|
|
99
|
+
version="1.0.0",
|
|
100
|
+
author="ModularNet Team",
|
|
101
|
+
description="Sensor logger with configurable output",
|
|
102
|
+
options=default_options
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
106
|
+
super().__init__(worker, plugin_id, options)
|
|
107
|
+
self.verbose = options.get("verbose", False)
|
|
108
|
+
self.log_every_n = options.get("log_every_n", 5)
|
|
109
|
+
self.readings = []
|
|
110
|
+
|
|
111
|
+
def on_start(self) -> None:
|
|
112
|
+
logger.info(f"[{self.plugin_id}] SensorLogger started (verbose={self.verbose})")
|
|
113
|
+
self.topic_subscribe("sensor.readings", self.on_reading)
|
|
114
|
+
|
|
115
|
+
def on_reading(self, reading: Dict[str, Any]):
|
|
116
|
+
self.readings.append(reading)
|
|
117
|
+
if self.verbose:
|
|
118
|
+
logger.info(f"[{self.plugin_id}] Received: {reading}")
|
|
119
|
+
elif len(self.readings) % self.log_every_n == 0:
|
|
120
|
+
avg_value = sum(r["value"] for r in self.readings) / len(self.readings)
|
|
121
|
+
logger.info(f"[{self.plugin_id}] {len(self.readings)} readings, avg={avg_value:.2f}")
|
|
122
|
+
self.env_set("logger.avg_value", avg_value, wait=False)
|
|
123
|
+
|
|
124
|
+
def main(self):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
def on_stop(self) -> None:
|
|
128
|
+
logger.info(f"[{self.plugin_id}] SensorLogger stopped, logged {len(self.readings)} readings")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main():
|
|
132
|
+
"""Run the configuration example."""
|
|
133
|
+
manager = PluginManager()
|
|
134
|
+
|
|
135
|
+
# Load sensor with custom options
|
|
136
|
+
sensor_desc = Sensor.Descriptor(options={
|
|
137
|
+
"sample_rate": 20,
|
|
138
|
+
"noise_level": 0.5,
|
|
139
|
+
"sensor_type": "humidity"
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
# Load logger with verbose mode
|
|
143
|
+
logger_desc = SensorLogger.Descriptor(options={
|
|
144
|
+
"verbose": False,
|
|
145
|
+
"log_every_n": 3
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
manager.load_plugin(sensor_desc)
|
|
149
|
+
manager.load_plugin(logger_desc)
|
|
150
|
+
|
|
151
|
+
manager.start_plugin(sensor_desc.name)
|
|
152
|
+
manager.start_plugin(logger_desc.name)
|
|
153
|
+
|
|
154
|
+
print("Sensor and logger running... Press Ctrl+C to stop.")
|
|
155
|
+
time.sleep(5)
|
|
156
|
+
print("Stopping plugins...")
|
|
157
|
+
|
|
158
|
+
manager.stop_plugin(logger_desc.name)
|
|
159
|
+
manager.stop_plugin(sensor_desc.name)
|
|
160
|
+
|
|
161
|
+
# Show configuration
|
|
162
|
+
print("\nFinal state:")
|
|
163
|
+
all_vars = manager.env_get()
|
|
164
|
+
for name, value in all_vars.items():
|
|
165
|
+
if name.startswith("sensor.") or name.startswith("logger."):
|
|
166
|
+
print(f" {name}: {value}")
|
|
167
|
+
|
|
168
|
+
# VERIFICATION
|
|
169
|
+
print("\n=== VERIFICATION ===")
|
|
170
|
+
print("PREDICTED BEHAVIOR:")
|
|
171
|
+
print("1. Sensor loads with custom options (sample_rate=20, noise_level=0.5, type=humidity)")
|
|
172
|
+
print("2. Logger loads with verbose=False, log_every_n=3")
|
|
173
|
+
print("3. Sensor publishes readings, logger subscribes and aggregates")
|
|
174
|
+
print("4. Config persists in environment\n")
|
|
175
|
+
|
|
176
|
+
sensor_config = all_vars.get("sensor.config")
|
|
177
|
+
logger_avg = all_vars.get("logger.avg_value")
|
|
178
|
+
|
|
179
|
+
assert sensor_config is not None, "sensor.config should be set"
|
|
180
|
+
assert sensor_config.get("type") == "humidity", f"Sensor type should be humidity, got {sensor_config.get('type')}"
|
|
181
|
+
assert sensor_config.get("sample_rate") == 20, f"Sample rate should be 20, got {sensor_config.get('sample_rate')}"
|
|
182
|
+
assert sensor_config.get("noise_level") == 0.5, f"Noise level should be 0.5, got {sensor_config.get('noise_level')}"
|
|
183
|
+
|
|
184
|
+
print(f"✓ Sensor config: type={sensor_config['type']}, rate={sensor_config['sample_rate']}Hz, noise={sensor_config['noise_level']}")
|
|
185
|
+
if logger_avg is not None:
|
|
186
|
+
print(f"✓ Logger computed average: {logger_avg:.2f}")
|
|
187
|
+
print("✓ All configuration assertions passed!")
|
|
188
|
+
|
|
189
|
+
manager.unload_plugin(sensor_desc.name)
|
|
190
|
+
manager.unload_plugin(logger_desc.name)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
main()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic test plugin for lifecycle testing.
|
|
3
|
+
|
|
4
|
+
A minimal plugin that exercises the load/unload/start/stop/status lifecycle
|
|
5
|
+
without any external dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
|
|
11
|
+
from parallelazy.plugin import Plugin, PluginDescriptor
|
|
12
|
+
from parallelazy.plugin_worker import PluginWorker
|
|
13
|
+
import time
|
|
14
|
+
from parallelazy.plugin_manager import PluginManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
class ExamplePluginCount(Plugin):
|
|
21
|
+
"""
|
|
22
|
+
Minimal test plugin that exercises core lifecycle.
|
|
23
|
+
|
|
24
|
+
Features:
|
|
25
|
+
- Simple on_start/on_stop implementation
|
|
26
|
+
- Tracks call count
|
|
27
|
+
- Can be used to verify manager lifecycle
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
32
|
+
"""Create plugin descriptor."""
|
|
33
|
+
return super().Descriptor(
|
|
34
|
+
version="1.0.0",
|
|
35
|
+
author="ModularNet Team",
|
|
36
|
+
description="Basic test plugin for lifecycle verification",
|
|
37
|
+
options=options or {}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
worker: PluginWorker,
|
|
43
|
+
plugin_id: str,
|
|
44
|
+
options: Dict[str, Any],
|
|
45
|
+
):
|
|
46
|
+
super().__init__(worker, plugin_id, options)
|
|
47
|
+
self.count = None
|
|
48
|
+
|
|
49
|
+
def main(self):
|
|
50
|
+
self.count +=1
|
|
51
|
+
self.env_set("count", self.count, wait=False)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def on_start(self) -> None:
|
|
55
|
+
logger.info(f"[{self.plugin_id}] ExamplePluginCount starting...")
|
|
56
|
+
self.count = 0
|
|
57
|
+
self.env_set("count", self.count)
|
|
58
|
+
|
|
59
|
+
def on_stop(self) -> None:
|
|
60
|
+
logger.info(f"[{self.plugin_id}] Total loop: {self.count}")
|
|
61
|
+
self.env_set("count", self.count)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
"""Example main function to run the plugin standalone for testing."""
|
|
66
|
+
manager = PluginManager()
|
|
67
|
+
descriptor = ExamplePluginCount.Descriptor()
|
|
68
|
+
manager.load_plugin(descriptor)
|
|
69
|
+
manager.start_plugin(descriptor.name)
|
|
70
|
+
|
|
71
|
+
# Let it run for a few seconds
|
|
72
|
+
time.sleep(3)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
manager.unload_plugin(descriptor.name)
|
|
76
|
+
|
|
77
|
+
all_vars = manager.env_get()
|
|
78
|
+
|
|
79
|
+
print("All environment variables:")
|
|
80
|
+
for name, value in all_vars.items():
|
|
81
|
+
print(f" {name}: {value}")
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__": main()
|
|
84
|
+
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data Pipeline Example: Multi-stage processing with pub/sub.
|
|
3
|
+
|
|
4
|
+
This example shows how plugins can:
|
|
5
|
+
1. Form a data pipeline using pub/sub topics
|
|
6
|
+
2. Process data in stages
|
|
7
|
+
3. Coordinate using environment variables
|
|
8
|
+
4. Track progress across the pipeline
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
|
|
15
|
+
from parallelazy.plugin import Plugin, PluginDescriptor
|
|
16
|
+
from parallelazy.plugin_worker import PluginWorker
|
|
17
|
+
from parallelazy.plugin_manager import PluginManager
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataSource(Plugin):
|
|
23
|
+
"""
|
|
24
|
+
Stage 1: Generate data and publish to topic.from parallelazy.plugin import Plugin, PluginDescriptor
|
|
25
|
+
from parallelazy.plugin_worker import PluginWorker
|
|
26
|
+
from parallelazy.plugin_manager import PluginManager
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
32
|
+
return super().Descriptor(
|
|
33
|
+
version="1.0.0",
|
|
34
|
+
author="ModularNet Team",
|
|
35
|
+
description="Data source for pipeline",
|
|
36
|
+
options=options or {}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
40
|
+
super().__init__(worker, plugin_id, options)
|
|
41
|
+
self.count = 0
|
|
42
|
+
|
|
43
|
+
def on_start(self) -> None:
|
|
44
|
+
logger.info(f"[{self.plugin_id}] DataSource started")
|
|
45
|
+
self.env_set("pipeline.stage1.count", 0)
|
|
46
|
+
|
|
47
|
+
def main(self):
|
|
48
|
+
self.count += 1
|
|
49
|
+
# Generate raw data
|
|
50
|
+
raw_data = {
|
|
51
|
+
"id": self.count,
|
|
52
|
+
"raw_value": self.count * 10
|
|
53
|
+
}
|
|
54
|
+
# Publish to pipeline topic
|
|
55
|
+
self.topic_publish("pipeline.raw", raw_data)
|
|
56
|
+
self.env_set("pipeline.stage1.count", self.count, wait=False)
|
|
57
|
+
|
|
58
|
+
def on_stop(self) -> None:
|
|
59
|
+
logger.info(f"[{self.plugin_id}] DataSource stopped, produced {self.count} items")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DataProcessor(Plugin):
|
|
63
|
+
"""
|
|
64
|
+
Stage 2: Process data from raw topic and publish to processed topic.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
69
|
+
return super().Descriptor(
|
|
70
|
+
version="1.0.0",
|
|
71
|
+
author="ModularNet Team",
|
|
72
|
+
description="Data processor for pipeline",
|
|
73
|
+
options=options or {}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
77
|
+
super().__init__(worker, plugin_id, options)
|
|
78
|
+
self.count = 0
|
|
79
|
+
|
|
80
|
+
def on_start(self) -> None:
|
|
81
|
+
logger.info(f"[{self.plugin_id}] DataProcessor started")
|
|
82
|
+
self.topic_subscribe("pipeline.raw", self.on_raw_data)
|
|
83
|
+
self.env_set("pipeline.stage2.count", 0)
|
|
84
|
+
|
|
85
|
+
def on_raw_data(self, data: Dict[str, Any]):
|
|
86
|
+
self.count += 1
|
|
87
|
+
# Process the raw data
|
|
88
|
+
processed = {
|
|
89
|
+
"id": data["id"],
|
|
90
|
+
"raw_value": data["raw_value"],
|
|
91
|
+
"processed_value": data["raw_value"] * 2 + 5
|
|
92
|
+
}
|
|
93
|
+
logger.info(f"[{self.plugin_id}] Processed item #{data['id']}: {processed['processed_value']}")
|
|
94
|
+
# Publish processed data
|
|
95
|
+
self.topic_publish("pipeline.processed", processed)
|
|
96
|
+
self.env_set("pipeline.stage2.count", self.count, wait=False)
|
|
97
|
+
|
|
98
|
+
def main(self):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
def on_stop(self) -> None:
|
|
102
|
+
logger.info(f"[{self.plugin_id}] DataProcessor stopped, processed {self.count} items")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DataAggregator(Plugin):
|
|
106
|
+
"""
|
|
107
|
+
Stage 3: Aggregate processed data and store statistics.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
112
|
+
return super().Descriptor(
|
|
113
|
+
version="1.0.0",
|
|
114
|
+
author="ModularNet Team",
|
|
115
|
+
description="Data aggregator for pipeline",
|
|
116
|
+
options=options or {}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
120
|
+
super().__init__(worker, plugin_id, options)
|
|
121
|
+
self.count = 0
|
|
122
|
+
self.sum_value = 0
|
|
123
|
+
self.max_value = 0
|
|
124
|
+
|
|
125
|
+
def on_start(self) -> None:
|
|
126
|
+
logger.info(f"[{self.plugin_id}] DataAggregator started")
|
|
127
|
+
self.topic_subscribe("pipeline.processed", self.on_processed_data)
|
|
128
|
+
self.env_set("pipeline.stage3.count", 0)
|
|
129
|
+
self.env_set("pipeline.stats.sum", 0)
|
|
130
|
+
self.env_set("pipeline.stats.max", 0)
|
|
131
|
+
|
|
132
|
+
def on_processed_data(self, data: Dict[str, Any]):
|
|
133
|
+
self.count += 1
|
|
134
|
+
value = data["processed_value"]
|
|
135
|
+
self.sum_value += value
|
|
136
|
+
self.max_value = max(self.max_value, value)
|
|
137
|
+
|
|
138
|
+
if self.count % 5 == 0:
|
|
139
|
+
logger.info(
|
|
140
|
+
f"[{self.plugin_id}] Aggregated {self.count} items | "
|
|
141
|
+
f"Sum: {self.sum_value} | Max: {self.max_value}"
|
|
142
|
+
)
|
|
143
|
+
self.env_set("pipeline.stage3.count", self.count, wait=False)
|
|
144
|
+
self.env_set("pipeline.stats.sum", self.sum_value, wait=False)
|
|
145
|
+
self.env_set("pipeline.stats.max", self.max_value, wait=False)
|
|
146
|
+
|
|
147
|
+
def main(self):
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def on_stop(self) -> None:
|
|
151
|
+
logger.info(f"[{self.plugin_id}] DataAggregator stopped")
|
|
152
|
+
logger.info(f"[{self.plugin_id}] Final stats: count={self.count}, sum={self.sum_value}, max={self.max_value}")
|
|
153
|
+
self.env_set("pipeline.stats.final_count", self.count)
|
|
154
|
+
self.env_set("pipeline.stats.final_sum", self.sum_value)
|
|
155
|
+
self.env_set("pipeline.stats.final_max", self.max_value)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main():
|
|
159
|
+
"""Run the data pipeline example."""
|
|
160
|
+
manager = PluginManager()
|
|
161
|
+
|
|
162
|
+
# Create and load pipeline plugins in order
|
|
163
|
+
source_desc = DataSource.Descriptor()
|
|
164
|
+
processor_desc = DataProcessor.Descriptor()
|
|
165
|
+
aggregator_desc = DataAggregator.Descriptor()
|
|
166
|
+
|
|
167
|
+
manager.load_plugin(source_desc)
|
|
168
|
+
manager.load_plugin(processor_desc)
|
|
169
|
+
manager.load_plugin(aggregator_desc)
|
|
170
|
+
|
|
171
|
+
manager.start_plugin(source_desc.name)
|
|
172
|
+
manager.start_plugin(processor_desc.name)
|
|
173
|
+
manager.start_plugin(aggregator_desc.name)
|
|
174
|
+
|
|
175
|
+
# Let the pipeline run
|
|
176
|
+
print("Data pipeline running... Press Ctrl+C to stop.")
|
|
177
|
+
time.sleep(3)
|
|
178
|
+
print("Stopping pipeline...")
|
|
179
|
+
|
|
180
|
+
manager.stop_plugin(source_desc.name)
|
|
181
|
+
manager.stop_plugin(processor_desc.name)
|
|
182
|
+
manager.stop_plugin(aggregator_desc.name)
|
|
183
|
+
|
|
184
|
+
time.sleep(1)
|
|
185
|
+
|
|
186
|
+
# Show final state
|
|
187
|
+
print("\nFinal pipeline statistics:")
|
|
188
|
+
all_vars = manager.env_get()
|
|
189
|
+
for name, value in all_vars.items():
|
|
190
|
+
if name.startswith("pipeline."):
|
|
191
|
+
print(f" {name}: {value}")
|
|
192
|
+
|
|
193
|
+
# VERIFICATION
|
|
194
|
+
print("\n=== VERIFICATION ===")
|
|
195
|
+
print("PREDICTED BEHAVIOR:")
|
|
196
|
+
print("1. DataSource generates ~3 items in 3 seconds")
|
|
197
|
+
print("2. DataProcessor receives all items and doubles+offsets values")
|
|
198
|
+
print("3. DataAggregator aggregates: sum and max of processed values")
|
|
199
|
+
print("4. All 3 stages see the same count\n")
|
|
200
|
+
|
|
201
|
+
count1 = all_vars.get("pipeline.stage1.count", 0)
|
|
202
|
+
count2 = all_vars.get("pipeline.stage2.count", 0)
|
|
203
|
+
count3 = all_vars.get("pipeline.stage3.count", 0)
|
|
204
|
+
stats_sum = all_vars.get("pipeline.stats.sum", 0)
|
|
205
|
+
stats_max = all_vars.get("pipeline.stats.max", 0)
|
|
206
|
+
|
|
207
|
+
assert count1 > 0, "Stage 1 should generate items"
|
|
208
|
+
assert count2 > 0, "Stage 2 should process items"
|
|
209
|
+
assert count3 > 0, "Stage 3 should aggregate items"
|
|
210
|
+
assert count1 == count2, f"Stage counts should match: stage1={count1}, stage2={count2}"
|
|
211
|
+
assert count1 == count3, f"Stage counts should match: stage1={count1}, stage3={count3}"
|
|
212
|
+
assert stats_sum > 0, "Sum should be positive"
|
|
213
|
+
assert stats_max > 0, "Max should be positive"
|
|
214
|
+
|
|
215
|
+
print(f"✓ Items generated: {count1}")
|
|
216
|
+
print(f"✓ Items processed through all 3 stages: {count1} == {count2} == {count3}")
|
|
217
|
+
print(f"✓ Aggregator computed: sum={stats_sum}, max={stats_max}")
|
|
218
|
+
print("✓ All pipeline assertions passed!")
|
|
219
|
+
|
|
220
|
+
manager.unload_plugin(source_desc.name)
|
|
221
|
+
manager.unload_plugin(processor_desc.name)
|
|
222
|
+
manager.unload_plugin(aggregator_desc.name)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
main()
|