leaf-framework 0.1.0__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.
Files changed (68) hide show
  1. leaf/__init__.py +0 -0
  2. leaf/adapters/__init__.py +0 -0
  3. leaf/adapters/core_adapters/__init__.py +0 -0
  4. leaf/adapters/core_adapters/continuous_experiment_adapter.py +100 -0
  5. leaf/adapters/core_adapters/discrete_experiment_adapter.py +74 -0
  6. leaf/adapters/core_adapters/upload_adapter.py +80 -0
  7. leaf/adapters/equipment_adapter.py +355 -0
  8. leaf/error_handler/__init__.py +0 -0
  9. leaf/error_handler/error_holder.py +52 -0
  10. leaf/error_handler/exceptions.py +109 -0
  11. leaf/measurement_handler/__init__.py +0 -0
  12. leaf/measurement_handler/handler.py +11 -0
  13. leaf/measurement_handler/measurements.yaml +7 -0
  14. leaf/measurement_handler/terms.py +94 -0
  15. leaf/modules/.gitkeep +0 -0
  16. leaf/modules/__init__.py +0 -0
  17. leaf/modules/input_modules/__init__.py +0 -0
  18. leaf/modules/input_modules/csv_watcher.py +103 -0
  19. leaf/modules/input_modules/db_watcher.py +17 -0
  20. leaf/modules/input_modules/event_watcher.py +109 -0
  21. leaf/modules/input_modules/external_api_watcher.py +123 -0
  22. leaf/modules/input_modules/external_event_watcher.py +23 -0
  23. leaf/modules/input_modules/file_watcher.py +241 -0
  24. leaf/modules/input_modules/http_watcher.py +157 -0
  25. leaf/modules/input_modules/mqtt_external_event_watcher.py +296 -0
  26. leaf/modules/input_modules/opc_watcher.py +131 -0
  27. leaf/modules/input_modules/polling_watcher.py +125 -0
  28. leaf/modules/input_modules/simple_watcher.py +55 -0
  29. leaf/modules/measurement_modules/__init__.py +0 -0
  30. leaf/modules/measurement_modules/carbon_dioxide.py +31 -0
  31. leaf/modules/measurement_modules/dissolved_oxygen.py +32 -0
  32. leaf/modules/measurement_modules/fluorescence.py +34 -0
  33. leaf/modules/measurement_modules/measurement_module.py +46 -0
  34. leaf/modules/measurement_modules/optical_density.py +32 -0
  35. leaf/modules/measurement_modules/ph.py +32 -0
  36. leaf/modules/measurement_modules/temperature.py +34 -0
  37. leaf/modules/output_modules/__init__.py +0 -0
  38. leaf/modules/output_modules/file.py +178 -0
  39. leaf/modules/output_modules/keydb.py +236 -0
  40. leaf/modules/output_modules/mqtt.py +462 -0
  41. leaf/modules/output_modules/output_module.py +202 -0
  42. leaf/modules/phase_modules/__init__.py +0 -0
  43. leaf/modules/phase_modules/control.py +47 -0
  44. leaf/modules/phase_modules/external_event_phase.py +35 -0
  45. leaf/modules/phase_modules/initialisation.py +40 -0
  46. leaf/modules/phase_modules/measure.py +135 -0
  47. leaf/modules/phase_modules/phase.py +114 -0
  48. leaf/modules/phase_modules/start.py +54 -0
  49. leaf/modules/phase_modules/stop.py +54 -0
  50. leaf/modules/process_modules/__init__.py +0 -0
  51. leaf/modules/process_modules/continous_module.py +45 -0
  52. leaf/modules/process_modules/discrete_module.py +47 -0
  53. leaf/modules/process_modules/external_event_process.py +25 -0
  54. leaf/modules/process_modules/process_module.py +116 -0
  55. leaf/modules/process_modules/upload_module.py +63 -0
  56. leaf/registry/discovery.py +161 -0
  57. leaf/registry/loader.py +62 -0
  58. leaf/registry/registry.py +117 -0
  59. leaf/registry/utils.py +27 -0
  60. leaf/start.py +348 -0
  61. leaf/utility/logger/__init__.py +0 -0
  62. leaf/utility/logger/logger_utils.py +57 -0
  63. leaf/utility/running_utilities.py +198 -0
  64. leaf_framework-0.1.0.dist-info/LICENSE +201 -0
  65. leaf_framework-0.1.0.dist-info/METADATA +32 -0
  66. leaf_framework-0.1.0.dist-info/RECORD +68 -0
  67. leaf_framework-0.1.0.dist-info/WHEEL +4 -0
  68. leaf_framework-0.1.0.dist-info/entry_points.txt +3 -0
leaf/__init__.py ADDED
File without changes
File without changes
File without changes
@@ -0,0 +1,100 @@
1
+ from typing import Optional, Any
2
+
3
+ from leaf.modules.process_modules.continous_module import ContinousProcess
4
+ from leaf.modules.process_modules.process_module import ProcessModule
5
+ from leaf.modules.phase_modules.start import StartPhase
6
+ from leaf.modules.phase_modules.stop import StopPhase
7
+ from leaf.modules.phase_modules.measure import MeasurePhase
8
+ from leaf.modules.phase_modules.initialisation import InitialisationPhase
9
+
10
+ from leaf_register.metadata import MetadataManager
11
+ from leaf.error_handler.error_holder import ErrorHolder
12
+
13
+ from leaf.modules.input_modules.event_watcher import EventWatcher
14
+ from leaf.modules.input_modules.external_event_watcher import ExternalEventWatcher
15
+ from leaf.modules.output_modules.output_module import OutputModule
16
+
17
+ from leaf.adapters.equipment_adapter import EquipmentAdapter
18
+ from leaf.adapters.equipment_adapter import AbstractInterpreter
19
+
20
+
21
+ class ContinuousExperimentAdapter(EquipmentAdapter):
22
+ """
23
+ Adapter that implements a continuous process workflow for equipment
24
+ without defined experiment start/stop boundaries.
25
+
26
+ It runs the measurement process continuously and wraps start/stop/details
27
+ in a parallel control process.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ instance_data: dict[str, Any],
33
+ watcher: EventWatcher,
34
+ output: OutputModule,
35
+ interpreter: AbstractInterpreter,
36
+ maximum_message_size: Optional[int] = 1,
37
+ error_holder: Optional[ErrorHolder] = None,
38
+ metadata_manager: Optional[MetadataManager] = None,
39
+ experiment_timeout: Optional[int] = None,
40
+ external_watcher: Optional[ExternalEventWatcher] = None,
41
+ ):
42
+ """
43
+ Initialise the ContinuousExperimentAdapter.
44
+
45
+ Args:
46
+ instance_data (dict): Configuration and metadata for this adapter.
47
+ watcher (EventWatcher): Input module that monitors raw data/events.
48
+ output (OutputModule): Output module to transmit processed data.
49
+ interpreter (AbstractInterpreter): Parses and transforms raw inputs.
50
+ maximum_message_size (Optional[int]): Limit for batching measurement messages.
51
+ error_holder (Optional[ErrorHolder]): Shared error collector.
52
+ metadata_manager (Optional[MetadataManager]): Metadata manager instance.
53
+ experiment_timeout (Optional[int]): Optional timeout between measurements.
54
+ external_watcher (Optional[ExternalEventWatcher]): Optional external input module.
55
+ """
56
+ # Initialize phases
57
+ start_p = StartPhase(metadata_manager)
58
+ stop_p = StopPhase(metadata_manager)
59
+ measure_p = MeasurePhase(metadata_manager, maximum_message_size=maximum_message_size)
60
+ details_p = InitialisationPhase(metadata_manager)
61
+
62
+ # Set up continuous and control processes
63
+ measurement_process = ContinousProcess(
64
+ output,
65
+ measure_p,
66
+ metadata_manager=metadata_manager,
67
+ error_holder=error_holder
68
+ )
69
+
70
+ self._control_process = ProcessModule(
71
+ output,
72
+ [start_p, stop_p, details_p],
73
+ metadata_manager=metadata_manager,
74
+ error_holder=error_holder
75
+ )
76
+
77
+ processes = [measurement_process, self._control_process]
78
+
79
+ super().__init__(
80
+ instance_data,
81
+ watcher,
82
+ output,
83
+ processes,
84
+ interpreter,
85
+ metadata_manager=metadata_manager,
86
+ error_holder=error_holder,
87
+ experiment_timeout=experiment_timeout,
88
+ external_watcher=external_watcher
89
+ )
90
+
91
+ def start(self) -> None:
92
+ """
93
+ Trigger the start phase and launch the adapter loop.
94
+
95
+ Injects a synthetic start message via the control process
96
+ to initialise any start-specific logic or output.
97
+ """
98
+ start_topic = self._metadata_manager.experiment.start
99
+ self._control_process.process_input(start_topic, {})
100
+ super().start()
@@ -0,0 +1,74 @@
1
+ from typing import Optional, Any
2
+
3
+ from leaf.modules.process_modules.discrete_module import DiscreteProcess
4
+ from leaf.modules.phase_modules.start import StartPhase
5
+ from leaf.modules.phase_modules.stop import StopPhase
6
+ from leaf.modules.phase_modules.measure import MeasurePhase
7
+ from leaf.modules.phase_modules.initialisation import InitialisationPhase
8
+
9
+ from leaf_register.metadata import MetadataManager
10
+ from leaf.error_handler.error_holder import ErrorHolder
11
+
12
+ from leaf.modules.input_modules.event_watcher import EventWatcher
13
+ from leaf.modules.input_modules.external_event_watcher import ExternalEventWatcher
14
+ from leaf.modules.output_modules.output_module import OutputModule
15
+
16
+ from leaf.adapters.equipment_adapter import EquipmentAdapter, AbstractInterpreter
17
+
18
+
19
+ class DiscreteExperimentAdapter(EquipmentAdapter):
20
+ """
21
+ Adapter that implements a discrete start-stop process workflow.
22
+
23
+ It sets up individual phases for start, stop, measure, and details.
24
+ The process is triggered by incoming events, not continuous monitoring.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ instance_data: dict[str, Any],
30
+ watcher: EventWatcher,
31
+ output: OutputModule,
32
+ interpreter: AbstractInterpreter,
33
+ maximum_message_size: Optional[int] = 1,
34
+ error_holder: Optional[ErrorHolder] = None,
35
+ metadata_manager: Optional[MetadataManager] = None,
36
+ experiment_timeout: Optional[int] = None,
37
+ external_watcher: Optional[ExternalEventWatcher] = None,
38
+ ):
39
+ """
40
+ Initialise the DiscreteExperimentAdapter.
41
+
42
+ Args:
43
+ instance_data (dict): Configuration and instance-level metadata.
44
+ watcher (EventWatcher): Monitors input events to trigger phase execution.
45
+ output (OutputModule): Module to transmit processed data externally.
46
+ interpreter (AbstractInterpreter): Translates raw data into structured output.
47
+ maximum_message_size (Optional[int]): Max batch size for messages in MeasurePhase.
48
+ error_holder (Optional[ErrorHolder]): Shared error container.
49
+ metadata_manager (Optional[MetadataManager]): Optional metadata manager.
50
+ experiment_timeout (Optional[int]): Max time allowed between measurements.
51
+ external_watcher (Optional[ExternalEventWatcher]): Input for out-of-band events (optional).
52
+ """
53
+ # Initialize phase modules
54
+ start_p = StartPhase(metadata_manager)
55
+ stop_p = StopPhase(metadata_manager)
56
+ measure_p = MeasurePhase(metadata_manager,
57
+ maximum_message_size=maximum_message_size)
58
+ details_p = InitialisationPhase(metadata_manager)
59
+
60
+ # Combine into a discrete process
61
+ phases = [start_p, measure_p, stop_p, details_p]
62
+ processes = [DiscreteProcess(output, phases)]
63
+
64
+ super().__init__(
65
+ instance_data,
66
+ watcher,
67
+ output,
68
+ processes,
69
+ interpreter,
70
+ metadata_manager=metadata_manager,
71
+ error_holder=error_holder,
72
+ experiment_timeout=experiment_timeout,
73
+ external_watcher=external_watcher
74
+ )
@@ -0,0 +1,80 @@
1
+ from typing import Optional, Any
2
+
3
+ from leaf.modules.process_modules.upload_module import UploadProcess
4
+ from leaf.modules.phase_modules.start import StartPhase
5
+ from leaf.modules.phase_modules.stop import StopPhase
6
+ from leaf.modules.phase_modules.measure import MeasurePhase
7
+ from leaf.modules.phase_modules.initialisation import InitialisationPhase
8
+
9
+ from leaf.modules.input_modules.file_watcher import FileWatcher
10
+ from leaf.modules.input_modules.external_event_watcher import ExternalEventWatcher
11
+ from leaf.modules.output_modules.output_module import OutputModule
12
+
13
+ from leaf_register.metadata import MetadataManager
14
+ from leaf.error_handler.error_holder import ErrorHolder
15
+
16
+ from leaf.adapters.equipment_adapter import EquipmentAdapter
17
+ from leaf.adapters.equipment_adapter import AbstractInterpreter
18
+
19
+
20
+ class UploadAdapter(EquipmentAdapter):
21
+ """
22
+ Adapter for equipment that stores data to a file all at once,
23
+ or where data must be manually uploaded into a watched directory.
24
+
25
+ This adapter uses a file watcher to detect new uploads and processes
26
+ the data through an artificial start-measure-stop sequence.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ instance_data: dict[str, Any],
32
+ output: OutputModule,
33
+ interpreter: AbstractInterpreter,
34
+ watch_dir: Optional[str] = None,
35
+ maximum_message_size: Optional[int] = 1,
36
+ error_holder: Optional[ErrorHolder] = None,
37
+ metadata_manager: Optional[MetadataManager] = None,
38
+ experiment_timeout: Optional[int] = None,
39
+ external_watcher: Optional[ExternalEventWatcher] = None,
40
+ ):
41
+ """
42
+ Initialise the UploadAdapter.
43
+
44
+ Args:
45
+ instance_data (dict): Instance-level configuration and metadata.
46
+ output (OutputModule): The output module used to transmit parsed data.
47
+ interpreter (AbstractInterpreter): Parses and translates raw data.
48
+ watch_dir (Optional[str]): Directory path to monitor for uploaded files.
49
+ maximum_message_size (Optional[int]): Max number of messages per transmission.
50
+ error_holder (Optional[ErrorHolder]): Error tracking object.
51
+ metadata_manager (Optional[MetadataManager]): Optional metadata manager.
52
+ experiment_timeout (Optional[int]): Timeout between measurements (not used here).
53
+ external_watcher (Optional[ExternalEventWatcher]): Optional listener for external inputs.
54
+ """
55
+ # Input module: watches a local directory
56
+ watcher = FileWatcher(watch_dir, metadata_manager,
57
+ error_holder=error_holder)
58
+
59
+ # Initialize discrete phases
60
+ start_p = StartPhase(metadata_manager)
61
+ stop_p = StopPhase(metadata_manager)
62
+ measure_p = MeasurePhase(metadata_manager,
63
+ maximum_message_size=maximum_message_size)
64
+ details_p = InitialisationPhase(metadata_manager)
65
+
66
+ # Combine phases into a process
67
+ phases = [start_p, measure_p, stop_p, details_p]
68
+ processes = [UploadProcess(output, phases)]
69
+
70
+ super().__init__(
71
+ instance_data,
72
+ watcher,
73
+ output,
74
+ processes,
75
+ interpreter,
76
+ metadata_manager=metadata_manager,
77
+ error_holder=error_holder,
78
+ experiment_timeout=experiment_timeout,
79
+ external_watcher=external_watcher,
80
+ )
@@ -0,0 +1,355 @@
1
+ import logging
2
+ import time
3
+ from abc import ABC, abstractmethod
4
+ from threading import Event
5
+ from typing import Any, Optional
6
+
7
+ from leaf_register.metadata import MetadataManager
8
+
9
+ from leaf.error_handler import exceptions
10
+ from leaf.error_handler.exceptions import LEAFError
11
+ from leaf.error_handler.error_holder import ErrorHolder
12
+
13
+ from leaf.modules.input_modules.event_watcher import EventWatcher
14
+ from leaf.modules.input_modules.external_event_watcher import ExternalEventWatcher
15
+ from leaf.modules.output_modules.output_module import OutputModule
16
+ from leaf.utility.logger.logger_utils import get_logger
17
+ from leaf.modules.process_modules.process_module import ProcessModule
18
+ from leaf.modules.process_modules.external_event_process import ExternalEventProcess
19
+
20
+
21
+ class AbstractInterpreter(ABC):
22
+ """
23
+ Abstract base class for interpreters.
24
+
25
+ Interpreters are responsible for transforming raw input data into a
26
+ structured format suitable for processing and output. Each adapter
27
+ uses one interpreter instance to convert metadata and measurement
28
+ input into a standardised structure.
29
+ """
30
+
31
+ def __init__(self, error_holder: Optional[ErrorHolder] = None):
32
+ """
33
+ Initialise the interpreter.
34
+
35
+ Args:
36
+ error_holder (Optional[ErrorHolder]): Optional holder for logging
37
+ or deferring error handling.
38
+ """
39
+ self.id: str = "undefined"
40
+ self.TIMESTAMP_KEY: str = "timestamp"
41
+ self.EXPERIMENT_ID_KEY: str = "experiment_id"
42
+ self.MEASUREMENT_HEADING_KEY: str = "measurement_types"
43
+ self._error_holder = error_holder
44
+ self._last_measurement = None
45
+ self._is_running = False
46
+
47
+ def set_error_holder(self, error_holder: Optional[ErrorHolder]) -> None:
48
+ """Assign an ErrorHolder instance to the interpreter."""
49
+ self._error_holder = error_holder
50
+
51
+ @abstractmethod
52
+ def metadata(self, data: Any) -> dict[str, Any]:
53
+ """
54
+ Parse metadata input and return it in structured form.
55
+
56
+ Args:
57
+ data (Any): Raw metadata from input module.
58
+
59
+ Returns:
60
+ dict[str, Any]: Parsed metadata dictionary.
61
+ """
62
+ return {self.TIMESTAMP_KEY: time.time()}
63
+
64
+ @abstractmethod
65
+ def measurement(self, data: Any) -> Any:
66
+ """
67
+ Parse a measurement payload.
68
+
69
+ Args:
70
+ data (Any): Raw measurement data.
71
+
72
+ Returns:
73
+ Any: Parsed or transformed measurement data.
74
+ """
75
+ self._last_measurement = time.time()
76
+ return data
77
+
78
+ def get_last_measurement_time(self) -> Optional[float]:
79
+ """Return the timestamp of the last successful measurement."""
80
+ return self._last_measurement
81
+
82
+ def experiment_stop(self, data: Any = None) -> Any:
83
+ """
84
+ Clear internal state for stopping experiment tracking.
85
+
86
+ Args:
87
+ data (Any): Optional additional data for stop logic.
88
+
89
+ Returns:
90
+ Any: Forwarded or cleaned-up stop payload.
91
+ """
92
+ self._last_measurement = None
93
+ return data
94
+
95
+ def _handle_exception(self, exception: LEAFError) -> None:
96
+ """
97
+ Route exceptions to the error handler or raise directly.
98
+
99
+ Args:
100
+ exception (LEAFError): Exception to handle or raise.
101
+ """
102
+ if self._error_holder is not None:
103
+ self._error_holder.add_error(exception)
104
+ else:
105
+ raise exception
106
+
107
+
108
+ class EquipmentAdapter(ABC):
109
+ """
110
+ Base class for all equipment adapters.
111
+
112
+ Handles coordination of:
113
+ - data input (via watchers),
114
+ - processing (via processes),
115
+ - output (via output modules),
116
+ - and error handling.
117
+
118
+ Subclasses implement concrete workflows (e.g., continuous, discrete).
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ instance_data: dict[str, Any],
124
+ watcher: EventWatcher,
125
+ output: OutputModule,
126
+ process_adapters: ProcessModule | list[ProcessModule],
127
+ interpreter: AbstractInterpreter,
128
+ metadata_manager: Optional[MetadataManager] = None,
129
+ error_holder: Optional[ErrorHolder] = None,
130
+ experiment_timeout: Optional[int] = None,
131
+ external_watcher: Optional[ExternalEventWatcher] = None
132
+ ):
133
+ """
134
+ Initialise the equipment adapter.
135
+
136
+ Args:
137
+ instance_data (dict): Configuration and metadata for the adapter instance.
138
+ watcher (EventWatcher): Input module responsible for receiving input events.
139
+ output (OutputModule): Output module to send processed data.
140
+ process_adapters (ProcessModule | list): Phase processors to handle logic.
141
+ interpreter (AbstractInterpreter): Logic for transforming raw input to usable data.
142
+ metadata_manager (Optional[MetadataManager]): Metadata coordinator.
143
+ error_holder (Optional[ErrorHolder]): Centralised error recording and dispatch.
144
+ experiment_timeout (Optional[int]): Optional timeout in seconds for experiment stall detection.
145
+ external_watcher (Optional[ExternalEventWatcher]): Optional secondary input for external events.
146
+ """
147
+ self._output = output
148
+ self._error_holder = error_holder
149
+
150
+ if not isinstance(process_adapters, (list, tuple, set)):
151
+ process_adapters = [process_adapters]
152
+ self._processes: list[ProcessModule] = list(process_adapters)
153
+ for p in self._processes:
154
+ p.set_error_holder(error_holder)
155
+
156
+ self._interpreter = interpreter
157
+ for p in self._processes:
158
+ p.set_interpreter(interpreter)
159
+ interpreter.set_error_holder(error_holder)
160
+
161
+ self._metadata_manager = metadata_manager or MetadataManager()
162
+ self._metadata_manager.add_instance_data(instance_data)
163
+
164
+ self._watcher = watcher
165
+ for p in self._processes:
166
+ self._watcher.add_callback(p.process_input)
167
+ p.set_metadata_manager(self._metadata_manager)
168
+ self._watcher.set_error_holder(error_holder)
169
+
170
+ ins_id = self._metadata_manager.get_instance_id()
171
+ self._logger = get_logger(
172
+ name=f"{__name__}.{ins_id}",
173
+ log_file=f"{ins_id}.log",
174
+ error_log_file=f"{ins_id}_error.log",
175
+ log_level=logging.INFO,
176
+ )
177
+
178
+ self._stop_event = Event()
179
+ self._stop_event.set()
180
+ self._experiment_timeout = experiment_timeout
181
+
182
+ self._external_watcher = external_watcher
183
+ if self._external_watcher is not None:
184
+ self._external_watcher.set_metadata_manager(self._metadata_manager)
185
+ self._external_process = ExternalEventProcess(
186
+ output, self._metadata_manager, self._error_holder
187
+ )
188
+ self._external_process.set_error_holder(self._error_holder)
189
+ self._external_process.set_interpreter(interpreter)
190
+ self._external_watcher.add_callback(self._external_process.process_input)
191
+
192
+ def is_running(self) -> bool:
193
+ """
194
+ Check if the adapter is actively running.
195
+
196
+ Returns:
197
+ bool: True if input/output are active and adapter is not stopped.
198
+ """
199
+ return (self._watcher.is_running() and
200
+ self._output.is_connected() and
201
+ not self._stop_event.is_set())
202
+
203
+ def start(self) -> None:
204
+ """
205
+ Begin running the adapter.
206
+
207
+ Starts input watchers, begins monitoring for errors,
208
+ and invokes appropriate error-driven control logic.
209
+ """
210
+ if not self._metadata_manager.is_valid():
211
+ ins_id = self._metadata_manager.get_instance_id()
212
+ missing_data = self._metadata_manager.get_missing_metadata()
213
+ excp = exceptions.AdapterLogicError(
214
+ f"{ins_id} is missing data: {missing_data}",
215
+ severity=exceptions.SeverityLevel.CRITICAL,
216
+ )
217
+ self._logger.error("Critical error, shutting down adapter", exc_info=excp)
218
+ self._handle_exception(excp)
219
+ return self.stop()
220
+
221
+ try:
222
+ self._watcher.start()
223
+ if self._external_watcher:
224
+ self._external_watcher.start()
225
+
226
+ self._stop_event.clear()
227
+ # Expose adapter on all channels.
228
+ for process in self._processes:
229
+ process.process_input(self._metadata_manager.details,
230
+ self._metadata_manager.get_data())
231
+ while not self._stop_event.is_set():
232
+ time.sleep(1)
233
+ if not self._error_holder:
234
+ continue
235
+
236
+ cur_errors = self._error_holder.get_unseen_errors()
237
+ self.transmit_errors(cur_errors)
238
+
239
+ for error, _ in cur_errors:
240
+ if error.severity == exceptions.SeverityLevel.CRITICAL:
241
+ self._logger.error("Critical error, stopping adapter", exc_info=error)
242
+ self._stop_event.set()
243
+ self.stop()
244
+
245
+ elif error.severity == exceptions.SeverityLevel.ERROR:
246
+ self._logger.error("Error, restarting adapter", exc_info=error)
247
+ self.stop()
248
+ return self.start()
249
+
250
+ elif error.severity == exceptions.SeverityLevel.WARNING:
251
+ self._handle_warning(error)
252
+
253
+ elif error.severity == exceptions.SeverityLevel.INFO:
254
+ self._logger.info("Info: %s", error, exc_info=error)
255
+
256
+ if self._experiment_timeout:
257
+ lmt = self._interpreter.get_last_measurement_time()
258
+ if lmt and (time.time() - lmt > self._experiment_timeout):
259
+ self._handle_exception(exceptions.HardwareStalledError("Experiment timeout"))
260
+
261
+ except KeyboardInterrupt:
262
+ self._logger.info("Keyboard interrupt received.")
263
+ self._stop_event.set()
264
+
265
+ except Exception as e:
266
+ self._logger.error(f"Unexpected error: {e}", exc_info=True)
267
+ self._stop_event.set()
268
+
269
+ finally:
270
+ self._logger.info("Stopping adapter.")
271
+ self._watcher.stop()
272
+ self.stop()
273
+
274
+ def _handle_warning(self, error: LEAFError) -> None:
275
+ """
276
+ Handle warnings by attempting recovery actions (restart input, etc).
277
+
278
+ Args:
279
+ error (LEAFError): The warning-level exception to evaluate.
280
+ """
281
+ if isinstance(error, exceptions.InputError):
282
+ self._logger.warning("Input error, restarting watcher", exc_info=error)
283
+ if self._watcher.is_running():
284
+ self._watcher.stop()
285
+ self._watcher.start()
286
+
287
+ elif isinstance(error, exceptions.HardwareStalledError):
288
+ self._logger.warning("Hardware stalled, stopping processes", exc_info=error)
289
+ for p in self._processes:
290
+ p.stop()
291
+ self._interpreter.experiment_stop()
292
+ if self._watcher.is_running():
293
+ self._watcher.stop()
294
+ self._watcher.start()
295
+
296
+ elif isinstance(error, exceptions.ClientUnreachableError):
297
+ self._logger.warning("Client unreachable", exc_info=error)
298
+ if error.client:
299
+ error.client.disconnect()
300
+ time.sleep(1)
301
+ error.client.connect()
302
+
303
+ def stop(self) -> None:
304
+ """
305
+ Stop the adapter and all associated processes and watchers.
306
+ """
307
+ self._stop_event.set()
308
+ if self._watcher.is_running():
309
+ self._watcher.stop()
310
+
311
+ for p in self._processes:
312
+ p.stop()
313
+
314
+ if self._external_watcher:
315
+ self._external_watcher.stop()
316
+
317
+ def withdraw(self) -> None:
318
+ """
319
+ Withdraw the adapter from being visible, but leave it running.
320
+
321
+ Calls withdraw on each active process.
322
+ """
323
+ for process in self._processes:
324
+ process.withdraw()
325
+
326
+ def transmit_errors(self, errors: list[tuple[LEAFError, str]] = None) -> None:
327
+ """
328
+ Push errors to the output module(s) via each process.
329
+
330
+ Args:
331
+ errors (list[LEAFError], optional): A list of errors to transmit.
332
+ """
333
+ errors = errors or self._error_holder.get_unseen_errors()
334
+ for error, _ in errors:
335
+ if not isinstance(error, LEAFError):
336
+ self._logger.error("Non-LEAF error added to error holder",
337
+ exc_info=error)
338
+ return self.stop()
339
+
340
+ error_json = error.to_json()
341
+ for process in self._processes:
342
+ process.transmit_error(error_json)
343
+ time.sleep(0.1)
344
+
345
+ def _handle_exception(self, exception: Exception) -> None:
346
+ """
347
+ Record or raise an exception.
348
+
349
+ Args:
350
+ exception (Exception): Exception to handle.
351
+ """
352
+ if self._error_holder:
353
+ self._error_holder.add_error(exception)
354
+ else:
355
+ raise exception
File without changes
@@ -0,0 +1,52 @@
1
+ import threading
2
+ import traceback
3
+ from typing import Optional, List, Tuple
4
+ from leaf.error_handler.exceptions import LEAFError
5
+ class ErrorHolder:
6
+ """
7
+ A simplified error manager that stores errors once
8
+ and lets you retrieve (and clear) all of them at once.
9
+ """
10
+
11
+ def __init__(self, adapter_id: Optional[str] = None):
12
+ """
13
+ Initialize the ErrorHolder instance.
14
+
15
+ Args:
16
+ adapter_id (Optional[str]): Optional identifier
17
+ for the adapter.
18
+ """
19
+ self._errors: List[dict] = []
20
+ self.lock = threading.Lock()
21
+ self._adapter_id = adapter_id
22
+
23
+ def add_error(self, exc: LEAFError) -> None:
24
+ """
25
+ Add an error entry with its traceback.
26
+
27
+ Args:
28
+ exc (LEAFError): The exception to add.
29
+ """
30
+ if not isinstance(exc, LEAFError):
31
+ raise TypeError("ErrorHolder only accepts LEAFError exceptions.")
32
+ with self.lock:
33
+ tb = traceback.format_exc()
34
+ error_entry = {
35
+ "error": exc,
36
+ "traceback": tb
37
+ }
38
+ self._errors.append(error_entry)
39
+
40
+ def get_unseen_errors(self) -> List[Tuple[LEAFError, str]]:
41
+ """
42
+ Retrieve all stored errors and clear the list.
43
+
44
+ Returns:
45
+ List[Tuple[Exception, str]]:
46
+ A list of (exception, traceback) tuples.
47
+ """
48
+ with self.lock:
49
+ all_errors = [(err["error"], err["traceback"])
50
+ for err in self._errors]
51
+ self._errors.clear()
52
+ return all_errors