leaf-framework 0.10.20__tar.gz → 0.10.22__tar.gz
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_framework-0.10.20 → leaf_framework-0.10.22}/PKG-INFO +1 -1
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/equipment_adapter.py +1 -1
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/mqtt.py +59 -41
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/output_module.py +1 -1
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/utils.py +64 -26
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/pyproject.toml +1 -1
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/LICENSE +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/README.md +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/continuous_experiment_adapter.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/discrete_experiment_adapter.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/streaming_adapter.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/upload_adapter.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/config/configuration.yaml +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/data/marketplace.yaml +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/error_handler/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/error_handler/error_holder.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/error_handler/exceptions.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/measurement_handler/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/measurement_handler/handler.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/measurement_handler/measurements.yaml +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/measurement_handler/terms.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/.gitkeep +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/ble_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/db_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/event_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/external_api_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/external_event_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/file_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/http_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/lads_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/modbus_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/mqtt_external_event_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/mqtt_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/opc_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/polling_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/simple_watcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/carbon_dioxide.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/dissolved_oxygen.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/fluorescence.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/measurement_module.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/optical_density.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/ph.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/temperature.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/file.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/keydb.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/sql.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/topic_store.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/control.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/error.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/external_event_phase.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/initialisation.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/measure.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/phase.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/start.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/stop.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/continous_module.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/discrete_module.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/external_event_process.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/process_module.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/upload_module.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/abstract_leaf.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/equipment_actions.yaml +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/equipment_data.yaml +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/metadata.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/py.typed +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/topic_utilities.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/registry/discovery.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/registry/loader.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/registry/registry.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/registry/utils.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/start.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/adapter_ui_extension.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/components/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/components/header.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/components/update_notice.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/constants.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/desktop_launcher.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/images/icon.icns +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/images/icon.ico +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/images/icon.svg +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/interface.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/markdown/configuration_help.md +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/markdown/documentation_main.md +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/markdown/documentation_protips.md +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/scripts.js +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/styles.css +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/adapters.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/configuration.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/documentation.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/logs.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/monitoring.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/update_checker.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/utility/config_utils.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/utility/logger/__init__.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/utility/logger/logger_utils.py +0 -0
- {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/utility/running_utilities.py +0 -0
|
@@ -315,7 +315,7 @@ class EquipmentAdapter(ABC):
|
|
|
315
315
|
Stop the adapter and all associated processes and watchers.
|
|
316
316
|
"""
|
|
317
317
|
self._stop_event.set()
|
|
318
|
-
self._output.on_adapter_stop()
|
|
318
|
+
self._output.on_adapter_stop(self._metadata_manager)
|
|
319
319
|
if self._watcher.is_running():
|
|
320
320
|
self._watcher.stop()
|
|
321
321
|
|
|
@@ -48,8 +48,9 @@ class MQTT(OutputModule):
|
|
|
48
48
|
error_holder: ErrorHolder | None = None,
|
|
49
49
|
name: str | None = None,
|
|
50
50
|
# Heartbeat
|
|
51
|
+
heartbeat_interval: float = 300.0,
|
|
52
|
+
# Removed options, still accepted so existing configs keep loading.
|
|
51
53
|
heartbeat_topic: str | None = None,
|
|
52
|
-
heartbeat_interval: float = 30.0,
|
|
53
54
|
heartbeat_payload: str | dict | None = None,
|
|
54
55
|
) -> None:
|
|
55
56
|
"""
|
|
@@ -67,12 +68,12 @@ class MQTT(OutputModule):
|
|
|
67
68
|
transport (Literal['tcp', 'websockets', 'unix']): The transport method, either TCP, WebSockets, or Unix socket.
|
|
68
69
|
tls (bool): Boolean flag to enable or disable TLS encryption.
|
|
69
70
|
error_holder (Optional[ErrorHolder]): An instance of ErrorHolder for tracking errors.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
heartbeat_interval (float): Seconds between heartbeat publishes. Defaults to 300.
|
|
72
|
+
Set to 0 to disable. Each equipment instance gets its own heartbeat on
|
|
73
|
+
<root>/<organisation>/<department>/<adapter_id>/<instance_id>/heartbeat.
|
|
74
|
+
The payload is always ``{"status": "alive", "interval": <seconds>,
|
|
75
|
+
"timestamp": <epoch>, "datetime": <datetime>}``.
|
|
76
|
+
heartbeat_topic, heartbeat_payload: Removed; ignored with a warning.
|
|
76
77
|
"""
|
|
77
78
|
|
|
78
79
|
super().__init__(fallback=fallback, error_holder=error_holder, name=name)
|
|
@@ -110,10 +111,16 @@ class MQTT(OutputModule):
|
|
|
110
111
|
# still False, tripping into the fallback path unnecessarily.
|
|
111
112
|
self._connected_event: threading.Event = threading.Event()
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
# One output module is shared by all equipment instances, so each
|
|
115
|
+
# adapter registers its own heartbeat topic via on_adapter_ready.
|
|
116
|
+
self._heartbeat_topics: set[str] = set()
|
|
117
|
+
self._heartbeat_lock: threading.Lock = threading.Lock()
|
|
115
118
|
self._heartbeat_interval: float = heartbeat_interval
|
|
116
|
-
|
|
119
|
+
if heartbeat_topic is not None or heartbeat_payload is not None:
|
|
120
|
+
logger.warning(
|
|
121
|
+
"heartbeat_topic and heartbeat_payload are no longer supported and are ignored; "
|
|
122
|
+
"each instance publishes on its own .../<instance_id>/heartbeat topic."
|
|
123
|
+
)
|
|
117
124
|
self._heartbeat_stop: threading.Event = threading.Event()
|
|
118
125
|
self._heartbeat_thread: threading.Thread | None = None
|
|
119
126
|
|
|
@@ -147,12 +154,6 @@ class MQTT(OutputModule):
|
|
|
147
154
|
except Exception as e:
|
|
148
155
|
raise AdapterBuildError(f"Failed to set up TLS: {e}")
|
|
149
156
|
|
|
150
|
-
if self._heartbeat_topic:
|
|
151
|
-
self.client.will_set(
|
|
152
|
-
self._heartbeat_topic, payload=json.dumps({"status": "disconnected"}), qos=1, retain=True
|
|
153
|
-
)
|
|
154
|
-
logger.info(f"LWT registered on '{self._heartbeat_topic}'")
|
|
155
|
-
|
|
156
157
|
self.connect()
|
|
157
158
|
|
|
158
159
|
def connect(self) -> None:
|
|
@@ -167,7 +168,7 @@ class MQTT(OutputModule):
|
|
|
167
168
|
self._connected_event.clear()
|
|
168
169
|
self.client.connect(self._broker, self._port, 20)
|
|
169
170
|
self.client.loop_start()
|
|
170
|
-
if self.
|
|
171
|
+
if self._heartbeat_interval > 0:
|
|
171
172
|
self._start_heartbeat()
|
|
172
173
|
except (gaierror, OSError) as e:
|
|
173
174
|
self._handle_exception(ClientUnreachableError(f"Error connecting to broker: {e}", output_module=self))
|
|
@@ -531,7 +532,7 @@ class MQTT(OutputModule):
|
|
|
531
532
|
target=self._heartbeat_loop, name=f"HeartbeatThread-{self._name}", daemon=True
|
|
532
533
|
)
|
|
533
534
|
self._heartbeat_thread.start()
|
|
534
|
-
logger.info(f"Heartbeat started
|
|
535
|
+
logger.info(f"Heartbeat started every {self._heartbeat_interval}s")
|
|
535
536
|
|
|
536
537
|
def _stop_heartbeat(self) -> None:
|
|
537
538
|
"""Signal the heartbeat thread to stop and wait for it to exit."""
|
|
@@ -541,30 +542,47 @@ class MQTT(OutputModule):
|
|
|
541
542
|
self._heartbeat_thread = None
|
|
542
543
|
logger.info("Heartbeat stopped.")
|
|
543
544
|
|
|
545
|
+
def on_adapter_ready(self, metadata_manager: Any) -> None:
|
|
546
|
+
"""Register the instance's heartbeat topic and publish a first beat right away."""
|
|
547
|
+
topic = metadata_manager.heartbeat()
|
|
548
|
+
with self._heartbeat_lock:
|
|
549
|
+
self._heartbeat_topics.add(topic)
|
|
550
|
+
self._publish_heartbeat(topic)
|
|
551
|
+
|
|
552
|
+
def on_adapter_stop(self, metadata_manager: Any) -> None:
|
|
553
|
+
"""Unregister the instance's heartbeat topic and mark it as stopped."""
|
|
554
|
+
topic = metadata_manager.heartbeat()
|
|
555
|
+
with self._heartbeat_lock:
|
|
556
|
+
self._heartbeat_topics.discard(topic)
|
|
557
|
+
self._publish_heartbeat(topic, "stopped")
|
|
558
|
+
|
|
559
|
+
def _publish_heartbeat(self, topic: str, status: str = "alive") -> None:
|
|
560
|
+
"""Publish a single heartbeat, never raising."""
|
|
561
|
+
payload = {
|
|
562
|
+
"status": status,
|
|
563
|
+
# Lets consumers judge staleness without knowing the config.
|
|
564
|
+
"interval": self._heartbeat_interval,
|
|
565
|
+
"timestamp": time.time(),
|
|
566
|
+
"datetime": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime()),
|
|
567
|
+
}
|
|
568
|
+
logger.debug(f"Publishing heartbeat to '{topic}': {payload}")
|
|
569
|
+
try:
|
|
570
|
+
# use_fallback=False: a heartbeat is only meaningful live, so a
|
|
571
|
+
# failed publish is dropped rather than buffered in the fallback
|
|
572
|
+
# and replayed as a stale "alive" once the broker is back.
|
|
573
|
+
self.transmit(topic, payload, retain=True, use_fallback=False)
|
|
574
|
+
except LEAFError as e:
|
|
575
|
+
# Must not propagate - an unhandled exception in the heartbeat thread
|
|
576
|
+
# would silently kill it and stop all future heartbeats.
|
|
577
|
+
logger.debug(f"Heartbeat publish failed, will retry next interval: {e}")
|
|
578
|
+
|
|
544
579
|
def _heartbeat_loop(self) -> None:
|
|
545
|
-
"""Publish a heartbeat
|
|
546
|
-
while
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
"status": "alive",
|
|
552
|
-
"timestamp": time.time(),
|
|
553
|
-
"datetime": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime()),
|
|
554
|
-
}
|
|
555
|
-
logger.debug(f"Publishing heartbeat to '{self._heartbeat_topic}': {payload}")
|
|
556
|
-
try:
|
|
557
|
-
# use_fallback=False: a heartbeat is only meaningful live, so a
|
|
558
|
-
# failed publish is dropped rather than buffered in the fallback
|
|
559
|
-
# and replayed as a stale "alive" once the broker is back.
|
|
560
|
-
self.transmit(self._heartbeat_topic, payload, use_fallback=False)
|
|
561
|
-
except LEAFError as e:
|
|
562
|
-
# Must not propagate - an unhandled exception here (e.g. the broker
|
|
563
|
-
# connection not being fully up yet) would silently kill this daemon
|
|
564
|
-
# thread and stop all future heartbeats.
|
|
565
|
-
logger.debug(f"Heartbeat publish failed, will retry next interval: {e}")
|
|
566
|
-
if self._heartbeat_stop.wait(self._heartbeat_interval):
|
|
567
|
-
break
|
|
580
|
+
"""Publish a heartbeat for every registered instance at regular intervals until stopped."""
|
|
581
|
+
while not self._heartbeat_stop.wait(self._heartbeat_interval):
|
|
582
|
+
with self._heartbeat_lock:
|
|
583
|
+
topics = list(self._heartbeat_topics)
|
|
584
|
+
for topic in topics:
|
|
585
|
+
self._publish_heartbeat(topic)
|
|
568
586
|
|
|
569
587
|
def _handle_return_code(self, return_code: int) -> ClientUnreachableError | None:
|
|
570
588
|
"""
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/output_module.py
RENAMED
|
@@ -367,7 +367,7 @@ class OutputModule(ABC):
|
|
|
367
367
|
Output modules that need instance identity (e.g. MQTT heartbeat) override this.
|
|
368
368
|
"""
|
|
369
369
|
|
|
370
|
-
def on_adapter_stop(self) -> None: # noqa: B027
|
|
370
|
+
def on_adapter_stop(self, metadata_manager: Any) -> None: # noqa: B027
|
|
371
371
|
"""
|
|
372
372
|
Called by EquipmentAdapter when the adapter is stopping.
|
|
373
373
|
Output modules that started background tasks in on_adapter_ready override this.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Utility functions and classes for LEAF UI."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
@@ -37,7 +38,28 @@ class LogElementHandler(logging.Handler):
|
|
|
37
38
|
self._emitting = False
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
def
|
|
41
|
+
def _run_pip(*args: str) -> subprocess.CompletedProcess[str]:
|
|
42
|
+
"""
|
|
43
|
+
Run pip in the interpreter currently running LEAF.
|
|
44
|
+
|
|
45
|
+
Blocking; call it off the event loop (asyncio.to_thread) so the UI keeps
|
|
46
|
+
responding while pip works.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
*args: Arguments passed to pip, e.g. "install", "--upgrade", "git+<url>"
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The completed pip process, for the caller to check returncode/stderr
|
|
53
|
+
"""
|
|
54
|
+
return subprocess.run(
|
|
55
|
+
[sys.executable, "-m", "pip", *args],
|
|
56
|
+
stdout=subprocess.PIPE,
|
|
57
|
+
stderr=subprocess.PIPE,
|
|
58
|
+
text=True,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def install_adapter(adapter: dict[Any, Any]) -> None:
|
|
41
63
|
"""
|
|
42
64
|
Install a LEAF adapter from a Git repository.
|
|
43
65
|
|
|
@@ -55,13 +77,25 @@ def install_adapter(adapter: dict[Any, Any]) -> None:
|
|
|
55
77
|
# repo's default branch HEAD -- these adapters' pyproject.toml versions
|
|
56
78
|
# don't get bumped per commit, so a plain `pip install` would see the
|
|
57
79
|
# already-installed version as satisfying the requirement and skip
|
|
58
|
-
# fetching the newer commit entirely.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
# fetching the newer commit entirely. --no-deps keeps the force away from
|
|
81
|
+
# the dependency tree (which may include LEAF itself); the second, plain
|
|
82
|
+
# install then only adds dependencies that are still missing.
|
|
83
|
+
try:
|
|
84
|
+
result = await asyncio.to_thread(
|
|
85
|
+
_run_pip, "install", "--upgrade", "--force-reinstall", "--no-deps", f"git+{repository}"
|
|
86
|
+
)
|
|
87
|
+
if result.returncode == 0:
|
|
88
|
+
result = await asyncio.to_thread(_run_pip, "install", f"git+{repository}")
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Could not run pip to install {adapter_name}: {e}", exc_info=True)
|
|
91
|
+
ui.notify(
|
|
92
|
+
f"Could not start the installation of {adapter_name}",
|
|
93
|
+
type="negative",
|
|
94
|
+
position="top",
|
|
95
|
+
close_button=True,
|
|
96
|
+
timeout=8000,
|
|
97
|
+
)
|
|
98
|
+
return
|
|
65
99
|
|
|
66
100
|
if result.returncode != 0:
|
|
67
101
|
logger.error(f"Failed to install {adapter_name}: {result.stderr}")
|
|
@@ -78,18 +112,17 @@ def install_adapter(adapter: dict[Any, Any]) -> None:
|
|
|
78
112
|
)
|
|
79
113
|
|
|
80
114
|
|
|
81
|
-
def uninstall_adapter(installed_adapter: dict) -> None:
|
|
115
|
+
async def uninstall_adapter(installed_adapter: dict[Any, Any]) -> None:
|
|
82
116
|
"""
|
|
83
117
|
Uninstall a LEAF adapter package.
|
|
84
118
|
|
|
85
119
|
Args:
|
|
86
120
|
installed_adapter: Dictionary containing adapter metadata with 'name' field
|
|
87
121
|
"""
|
|
88
|
-
print(f"Uninstalling {installed_adapter}...")
|
|
89
122
|
package_name = installed_adapter.get("name")
|
|
90
123
|
|
|
91
124
|
if not package_name:
|
|
92
|
-
|
|
125
|
+
logger.error(f"Cannot uninstall adapter without package name: {installed_adapter}")
|
|
93
126
|
ui.notify(
|
|
94
127
|
"Cannot uninstall adapter without package name",
|
|
95
128
|
type="negative",
|
|
@@ -102,26 +135,31 @@ def uninstall_adapter(installed_adapter: dict) -> None:
|
|
|
102
135
|
logger.info(f"Uninstalling {package_name}...")
|
|
103
136
|
ui.notify(f"Uninstalling {package_name}... This may take a moment.", type="info", position="top", timeout=3000)
|
|
104
137
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
text=True,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
if result.returncode == 0:
|
|
113
|
-
print(f"Uninstalled {package_name} successfully.")
|
|
114
|
-
logger.info(f"Uninstalled {package_name} successfully.")
|
|
138
|
+
try:
|
|
139
|
+
result = await asyncio.to_thread(_run_pip, "uninstall", "-y", package_name)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Could not run pip to uninstall {package_name}: {e}", exc_info=True)
|
|
115
142
|
ui.notify(
|
|
116
|
-
f"
|
|
117
|
-
type="
|
|
143
|
+
f"Could not start the removal of {package_name}",
|
|
144
|
+
type="negative",
|
|
118
145
|
position="top",
|
|
119
146
|
close_button=True,
|
|
120
|
-
timeout=
|
|
147
|
+
timeout=8000,
|
|
121
148
|
)
|
|
122
|
-
|
|
123
|
-
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
if result.returncode != 0:
|
|
124
152
|
logger.error(f"Failed to uninstall {package_name}: {result.stderr}")
|
|
125
153
|
ui.notify(
|
|
126
154
|
f"Failed to uninstall {package_name}", type="negative", position="top", close_button=True, timeout=8000
|
|
127
155
|
)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
logger.info(f"Uninstalled {package_name} successfully.")
|
|
159
|
+
ui.notify(
|
|
160
|
+
f"Successfully uninstalled {package_name}! Click the Refresh button to update the list.",
|
|
161
|
+
type="positive",
|
|
162
|
+
position="top",
|
|
163
|
+
close_button=True,
|
|
164
|
+
timeout=10000,
|
|
165
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/streaming_adapter.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/upload_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/measurement_handler/measurements.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/event_watcher.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/external_api_watcher.py
RENAMED
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/file_watcher.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/http_watcher.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/lads_watcher.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/modbus_watcher.py
RENAMED
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/mqtt_watcher.py
RENAMED
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/polling_watcher.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/simple_watcher.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/__init__.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/carbon_dioxide.py
RENAMED
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/fluorescence.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/temperature.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/topic_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/external_event_phase.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/initialisation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/continous_module.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/discrete_module.py
RENAMED
|
File without changes
|
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/process_module.py
RENAMED
|
File without changes
|
{leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/upload_module.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|