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.
Files changed (108) hide show
  1. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/PKG-INFO +1 -1
  2. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/equipment_adapter.py +1 -1
  3. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/mqtt.py +59 -41
  4. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/output_module.py +1 -1
  5. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/utils.py +64 -26
  6. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/pyproject.toml +1 -1
  7. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/LICENSE +0 -0
  8. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/README.md +0 -0
  9. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/__init__.py +0 -0
  10. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/__init__.py +0 -0
  11. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/__init__.py +0 -0
  12. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/continuous_experiment_adapter.py +0 -0
  13. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/discrete_experiment_adapter.py +0 -0
  14. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/streaming_adapter.py +0 -0
  15. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/adapters/core_adapters/upload_adapter.py +0 -0
  16. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/config/configuration.yaml +0 -0
  17. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/data/marketplace.yaml +0 -0
  18. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/error_handler/__init__.py +0 -0
  19. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/error_handler/error_holder.py +0 -0
  20. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/error_handler/exceptions.py +0 -0
  21. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/measurement_handler/__init__.py +0 -0
  22. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/measurement_handler/handler.py +0 -0
  23. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/measurement_handler/measurements.yaml +0 -0
  24. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/measurement_handler/terms.py +0 -0
  25. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/.gitkeep +0 -0
  26. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/__init__.py +0 -0
  27. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/__init__.py +0 -0
  28. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/ble_watcher.py +0 -0
  29. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/db_watcher.py +0 -0
  30. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/event_watcher.py +0 -0
  31. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/external_api_watcher.py +0 -0
  32. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/external_event_watcher.py +0 -0
  33. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/file_watcher.py +0 -0
  34. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/http_watcher.py +0 -0
  35. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/lads_watcher.py +0 -0
  36. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/modbus_watcher.py +0 -0
  37. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/mqtt_external_event_watcher.py +0 -0
  38. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/mqtt_watcher.py +0 -0
  39. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/opc_watcher.py +0 -0
  40. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/polling_watcher.py +0 -0
  41. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/input_modules/simple_watcher.py +0 -0
  42. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/__init__.py +0 -0
  43. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/carbon_dioxide.py +0 -0
  44. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/dissolved_oxygen.py +0 -0
  45. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/fluorescence.py +0 -0
  46. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/measurement_module.py +0 -0
  47. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/optical_density.py +0 -0
  48. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/ph.py +0 -0
  49. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/measurement_modules/temperature.py +0 -0
  50. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/__init__.py +0 -0
  51. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/file.py +0 -0
  52. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/keydb.py +0 -0
  53. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/sql.py +0 -0
  54. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/output_modules/topic_store.py +0 -0
  55. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/__init__.py +0 -0
  56. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/control.py +0 -0
  57. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/error.py +0 -0
  58. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/external_event_phase.py +0 -0
  59. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/initialisation.py +0 -0
  60. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/measure.py +0 -0
  61. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/phase.py +0 -0
  62. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/start.py +0 -0
  63. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/phase_modules/stop.py +0 -0
  64. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/__init__.py +0 -0
  65. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/continous_module.py +0 -0
  66. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/discrete_module.py +0 -0
  67. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/external_event_process.py +0 -0
  68. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/process_module.py +0 -0
  69. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/modules/process_modules/upload_module.py +0 -0
  70. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/__init__.py +0 -0
  71. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/abstract_leaf.py +0 -0
  72. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/equipment_actions.yaml +0 -0
  73. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/equipment_data.yaml +0 -0
  74. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/metadata.py +0 -0
  75. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/py.typed +0 -0
  76. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/register/topic_utilities.py +0 -0
  77. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/registry/discovery.py +0 -0
  78. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/registry/loader.py +0 -0
  79. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/registry/registry.py +0 -0
  80. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/registry/utils.py +0 -0
  81. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/start.py +0 -0
  82. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/__init__.py +0 -0
  83. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/adapter_ui_extension.py +0 -0
  84. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/components/__init__.py +0 -0
  85. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/components/header.py +0 -0
  86. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/components/update_notice.py +0 -0
  87. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/constants.py +0 -0
  88. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/desktop_launcher.py +0 -0
  89. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/images/icon.icns +0 -0
  90. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/images/icon.ico +0 -0
  91. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/images/icon.svg +0 -0
  92. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/interface.py +0 -0
  93. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/markdown/configuration_help.md +0 -0
  94. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/markdown/documentation_main.md +0 -0
  95. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/markdown/documentation_protips.md +0 -0
  96. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/scripts.js +0 -0
  97. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/styles.css +0 -0
  98. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/__init__.py +0 -0
  99. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/adapters.py +0 -0
  100. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/configuration.py +0 -0
  101. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/documentation.py +0 -0
  102. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/logs.py +0 -0
  103. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/tabs/monitoring.py +0 -0
  104. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/ui/update_checker.py +0 -0
  105. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/utility/config_utils.py +0 -0
  106. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/utility/logger/__init__.py +0 -0
  107. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/utility/logger/logger_utils.py +0 -0
  108. {leaf_framework-0.10.20 → leaf_framework-0.10.22}/leaf/utility/running_utilities.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: leaf-framework
3
- Version: 0.10.20
3
+ Version: 0.10.22
4
4
  Summary: Lightweight Adapter Framework (LEAF)
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -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
- heartbeat_topic (Optional[str]): Topic to publish heartbeat messages on. If not set,
71
- heartbeat and LWT are disabled.
72
- heartbeat_interval (float): Seconds between heartbeat publishes. Defaults to 30.
73
- Set to 0 to disable.
74
- heartbeat_payload (Optional[str | dict]): Payload for heartbeat messages. Defaults to
75
- ``{"status": "alive", "timestamp": <epoch>, "datetime": <datetime>}`` with the timestamp filled in at publish time.
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
- self._heartbeat_topic: str | None = heartbeat_topic
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
- self._heartbeat_payload: str | dict | None = heartbeat_payload
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._heartbeat_topic and self._heartbeat_interval > 0:
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 on '{self._heartbeat_topic}' every {self._heartbeat_interval}s")
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 message at regular intervals until stopped."""
546
- while True:
547
- if self._heartbeat_payload is not None:
548
- payload = self._heartbeat_payload
549
- else:
550
- payload = {
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
  """
@@ -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 install_adapter(adapter: dict[Any, Any]) -> None:
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
- result = subprocess.run(
60
- [sys.executable, "-m", "pip", "install", "--upgrade", "--force-reinstall", f"git+{repository}"],
61
- stdout=subprocess.PIPE,
62
- stderr=subprocess.PIPE,
63
- text=True,
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
- print(f"Cannot uninstall adapter without package name: {installed_adapter}")
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
- result = subprocess.run(
106
- [sys.executable, "-m", "pip", "uninstall", "-y", package_name],
107
- stdout=subprocess.PIPE,
108
- stderr=subprocess.PIPE,
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"Successfully uninstalled {package_name}! Click the Refresh button to update the list.",
117
- type="positive",
143
+ f"Could not start the removal of {package_name}",
144
+ type="negative",
118
145
  position="top",
119
146
  close_button=True,
120
- timeout=10000,
147
+ timeout=8000,
121
148
  )
122
- else:
123
- print(f"Failed to uninstall {package_name}.\nError: {result.stderr}")
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
+ )
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "leaf-framework"
3
- version = "0.10.20"
3
+ version = "0.10.22"
4
4
  description = "Lightweight Adapter Framework (LEAF)"
5
5
  authors = [
6
6
  "Matthew Crowther <nmc215@ncl.ac.uk>",