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.
- leaf/__init__.py +0 -0
- leaf/adapters/__init__.py +0 -0
- leaf/adapters/core_adapters/__init__.py +0 -0
- leaf/adapters/core_adapters/continuous_experiment_adapter.py +100 -0
- leaf/adapters/core_adapters/discrete_experiment_adapter.py +74 -0
- leaf/adapters/core_adapters/upload_adapter.py +80 -0
- leaf/adapters/equipment_adapter.py +355 -0
- leaf/error_handler/__init__.py +0 -0
- leaf/error_handler/error_holder.py +52 -0
- leaf/error_handler/exceptions.py +109 -0
- leaf/measurement_handler/__init__.py +0 -0
- leaf/measurement_handler/handler.py +11 -0
- leaf/measurement_handler/measurements.yaml +7 -0
- leaf/measurement_handler/terms.py +94 -0
- leaf/modules/.gitkeep +0 -0
- leaf/modules/__init__.py +0 -0
- leaf/modules/input_modules/__init__.py +0 -0
- leaf/modules/input_modules/csv_watcher.py +103 -0
- leaf/modules/input_modules/db_watcher.py +17 -0
- leaf/modules/input_modules/event_watcher.py +109 -0
- leaf/modules/input_modules/external_api_watcher.py +123 -0
- leaf/modules/input_modules/external_event_watcher.py +23 -0
- leaf/modules/input_modules/file_watcher.py +241 -0
- leaf/modules/input_modules/http_watcher.py +157 -0
- leaf/modules/input_modules/mqtt_external_event_watcher.py +296 -0
- leaf/modules/input_modules/opc_watcher.py +131 -0
- leaf/modules/input_modules/polling_watcher.py +125 -0
- leaf/modules/input_modules/simple_watcher.py +55 -0
- leaf/modules/measurement_modules/__init__.py +0 -0
- leaf/modules/measurement_modules/carbon_dioxide.py +31 -0
- leaf/modules/measurement_modules/dissolved_oxygen.py +32 -0
- leaf/modules/measurement_modules/fluorescence.py +34 -0
- leaf/modules/measurement_modules/measurement_module.py +46 -0
- leaf/modules/measurement_modules/optical_density.py +32 -0
- leaf/modules/measurement_modules/ph.py +32 -0
- leaf/modules/measurement_modules/temperature.py +34 -0
- leaf/modules/output_modules/__init__.py +0 -0
- leaf/modules/output_modules/file.py +178 -0
- leaf/modules/output_modules/keydb.py +236 -0
- leaf/modules/output_modules/mqtt.py +462 -0
- leaf/modules/output_modules/output_module.py +202 -0
- leaf/modules/phase_modules/__init__.py +0 -0
- leaf/modules/phase_modules/control.py +47 -0
- leaf/modules/phase_modules/external_event_phase.py +35 -0
- leaf/modules/phase_modules/initialisation.py +40 -0
- leaf/modules/phase_modules/measure.py +135 -0
- leaf/modules/phase_modules/phase.py +114 -0
- leaf/modules/phase_modules/start.py +54 -0
- leaf/modules/phase_modules/stop.py +54 -0
- leaf/modules/process_modules/__init__.py +0 -0
- leaf/modules/process_modules/continous_module.py +45 -0
- leaf/modules/process_modules/discrete_module.py +47 -0
- leaf/modules/process_modules/external_event_process.py +25 -0
- leaf/modules/process_modules/process_module.py +116 -0
- leaf/modules/process_modules/upload_module.py +63 -0
- leaf/registry/discovery.py +161 -0
- leaf/registry/loader.py +62 -0
- leaf/registry/registry.py +117 -0
- leaf/registry/utils.py +27 -0
- leaf/start.py +348 -0
- leaf/utility/logger/__init__.py +0 -0
- leaf/utility/logger/logger_utils.py +57 -0
- leaf/utility/running_utilities.py +198 -0
- leaf_framework-0.1.0.dist-info/LICENSE +201 -0
- leaf_framework-0.1.0.dist-info/METADATA +32 -0
- leaf_framework-0.1.0.dist-info/RECORD +68 -0
- leaf_framework-0.1.0.dist-info/WHEEL +4 -0
- 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
|