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.
@@ -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()