leaf-framework 0.11.0__tar.gz → 0.11.1__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 (112) hide show
  1. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/PKG-INFO +1 -1
  2. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/control_spec.py +7 -1
  3. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/equipment_adapter.py +2 -1
  4. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/ble_watcher.py +0 -1
  5. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/mqtt_external_event_watcher.py +0 -19
  6. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/measure.py +22 -0
  7. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/metadata.py +10 -1
  8. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/registry/discovery.py +1 -2
  9. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/registry/registry.py +0 -7
  10. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/start.py +1 -1
  11. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/constants.py +1 -6
  12. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/interface.py +0 -6
  13. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/adapters.py +41 -3
  14. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/utils.py +12 -0
  15. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/pyproject.toml +1 -1
  16. leaf_framework-0.11.0/leaf/measurement_handler/handler.py +0 -11
  17. leaf_framework-0.11.0/leaf/modules/input_modules/db_watcher.py +0 -15
  18. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/LICENSE +0 -0
  19. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/README.md +0 -0
  20. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/__init__.py +0 -0
  21. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/__init__.py +0 -0
  22. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/__init__.py +0 -0
  23. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/continuous_experiment_adapter.py +0 -0
  24. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/discrete_experiment_adapter.py +0 -0
  25. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/streaming_adapter.py +0 -0
  26. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/upload_adapter.py +0 -0
  27. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/config/configuration.yaml +0 -0
  28. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/data/marketplace.yaml +0 -0
  29. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/error_handler/__init__.py +0 -0
  30. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/error_handler/error_holder.py +0 -0
  31. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/error_handler/exceptions.py +0 -0
  32. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/measurement_handler/__init__.py +0 -0
  33. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/measurement_handler/measurements.yaml +0 -0
  34. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/measurement_handler/terms.py +0 -0
  35. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/.gitkeep +0 -0
  36. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/__init__.py +0 -0
  37. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/__init__.py +0 -0
  38. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/event_watcher.py +0 -0
  39. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/external_api_watcher.py +0 -0
  40. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/external_event_watcher.py +0 -0
  41. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/file_watcher.py +0 -0
  42. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/http_watcher.py +0 -0
  43. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/lads_watcher.py +0 -0
  44. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/modbus_watcher.py +0 -0
  45. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/mqtt_watcher.py +0 -0
  46. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/opc_watcher.py +0 -0
  47. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/polling_watcher.py +0 -0
  48. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/simple_watcher.py +0 -0
  49. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/__init__.py +0 -0
  50. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/carbon_dioxide.py +0 -0
  51. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/dissolved_oxygen.py +0 -0
  52. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/fluorescence.py +0 -0
  53. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/measurement_module.py +0 -0
  54. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/optical_density.py +0 -0
  55. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/ph.py +0 -0
  56. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/temperature.py +0 -0
  57. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/__init__.py +0 -0
  58. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/file.py +0 -0
  59. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/mqtt.py +0 -0
  60. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/output_module.py +0 -0
  61. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/sql.py +0 -0
  62. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/topic_store.py +0 -0
  63. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/__init__.py +0 -0
  64. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/control.py +0 -0
  65. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/error.py +0 -0
  66. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/external_event_phase.py +0 -0
  67. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/initialisation.py +0 -0
  68. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/phase.py +0 -0
  69. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/start.py +0 -0
  70. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/stop.py +0 -0
  71. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/__init__.py +0 -0
  72. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/continous_module.py +0 -0
  73. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/discrete_module.py +0 -0
  74. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/external_event_process.py +0 -0
  75. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/process_module.py +0 -0
  76. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/upload_module.py +0 -0
  77. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/__init__.py +0 -0
  78. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/abstract_leaf.py +0 -0
  79. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/equipment_actions.yaml +0 -0
  80. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/equipment_data.yaml +0 -0
  81. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/py.typed +0 -0
  82. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/topic_utilities.py +0 -0
  83. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/registry/loader.py +0 -0
  84. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/registry/utils.py +0 -0
  85. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/__init__.py +0 -0
  86. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/adapter_ui_extension.py +0 -0
  87. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/auth.py +0 -0
  88. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/components/__init__.py +0 -0
  89. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/components/app_control.py +0 -0
  90. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/components/header.py +0 -0
  91. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/components/update_notice.py +0 -0
  92. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/desktop_launcher.py +0 -0
  93. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/images/icon.icns +0 -0
  94. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/images/icon.ico +0 -0
  95. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/images/icon.svg +0 -0
  96. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/markdown/configuration_help.md +0 -0
  97. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/markdown/documentation_main.md +0 -0
  98. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/markdown/documentation_protips.md +0 -0
  99. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/scripts.js +0 -0
  100. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/styles.css +0 -0
  101. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/__init__.py +0 -0
  102. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/configuration.py +0 -0
  103. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/control.py +0 -0
  104. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/documentation.py +0 -0
  105. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/logs.py +0 -0
  106. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/monitoring.py +0 -0
  107. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/update_checker.py +0 -0
  108. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/utility/config_utils.py +0 -0
  109. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/utility/logger/__init__.py +0 -0
  110. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/utility/logger/logger_utils.py +0 -0
  111. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/utility/running_adapters.py +0 -0
  112. {leaf_framework-0.11.0 → leaf_framework-0.11.1}/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.11.0
3
+ Version: 0.11.1
4
4
  Summary: Lightweight Adapter Framework (LEAF)
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -140,7 +140,9 @@ def _field_value(value: object) -> int | float | str:
140
140
  return json.dumps(value)
141
141
 
142
142
 
143
- def result_record(data: str, source: str, error: Exception | None = None) -> dict[str, object]:
143
+ def result_record(
144
+ data: str, source: str, error: Exception | None = None, instance_tags: dict[str, str] | None = None
145
+ ) -> dict[str, object]:
144
146
  """
145
147
  A control message as the LeafPoint LEAF publishes on <control topic>/result, so that what was
146
148
  sent to the equipment is stored with the measurements.
@@ -159,6 +161,10 @@ def result_record(data: str, source: str, error: Exception | None = None) -> dic
159
161
  tags: dict[str, str] = {}
160
162
  for key, value in _part(point, "tags").items():
161
163
  tags[key] = str(value)
164
+ # The instance's organisation and department, where the message didn't set them
165
+ if instance_tags is not None:
166
+ for key, value in instance_tags.items():
167
+ _ = tags.setdefault(key, value)
162
168
  fields: dict[str, int | float | str] = {}
163
169
  for key, value in _part(point, "fields").items():
164
170
  fields[key] = _field_value(value)
@@ -293,7 +293,8 @@ class EquipmentAdapter(ABC):
293
293
  """Publish what happened to a control message on <control topic>/result, so it is stored with the data."""
294
294
  # Never let the record break the command it describes
295
295
  try:
296
- _ = self._output.transmit(f"{topic}/result", result_record(data, source, error))
296
+ record = result_record(data, source, error, self._metadata_manager.instance_tags())
297
+ _ = self._output.transmit(f"{topic}/result", record)
297
298
  except Exception as e:
298
299
  self._logger.warning(f"Could not publish the control record for {topic}: {e}")
299
300
 
@@ -21,7 +21,6 @@ logger = get_logger(__name__, log_file="input_module.log")
21
21
  FIRST_RECONNECT_DELAY = 1
22
22
  RECONNECT_RATE = 2
23
23
  MAX_RECONNECT_DELAY = 60
24
- MAX_RECONNECT_COUNT = 12
25
24
 
26
25
 
27
26
  class BLEEventWatcher(EventWatcher):
@@ -241,22 +241,3 @@ class MQTTExternalEventWatcher(ExternalEventWatcher):
241
241
  bool: True if the client is connected, False otherwise.
242
242
  """
243
243
  return self.client.is_connected()
244
-
245
- def _handle_return_code(self, return_code: int) -> ClientUnreachableError | None:
246
- """
247
- Handle MQTT return codes.
248
-
249
- Args:
250
- return_code (int): The return code to handle.
251
-
252
- Returns:
253
- Optional[ClientUnreachableError]: An error if one occurred, None otherwise.
254
- """
255
- if return_code == mqtt.MQTT_ERR_SUCCESS:
256
- return None
257
- message = {
258
- mqtt.MQTT_ERR_NO_CONN: "Can't connect to broker",
259
- mqtt.MQTT_ERR_QUEUE_SIZE: "Message queue size limit reached",
260
- }.get(return_code, f"Unknown error with return code {return_code}")
261
-
262
- return ClientUnreachableError(message, output_module=self, severity=SeverityLevel.INFO)
@@ -43,6 +43,7 @@ class MeasurePhase(PhaseModule):
43
43
 
44
44
  super().__init__(term_builder, metadata_manager=metadata_manager, error_holder=error_holder)
45
45
  self._maximum_message_size: int = maximum_message_size
46
+ self._tag_mismatches: set[tuple[str, str]] = set()
46
47
  # Guards against a second measurement running concurrently with one still
47
48
  # draining (e.g. a generator-based interpreter that streams for a long time,
48
49
  # or forever). Without this, an overlapping event (e.g. FileWatcher's
@@ -231,6 +232,7 @@ class MeasurePhase(PhaseModule):
231
232
  list[tuple]: A list of (action, result) tuples.
232
233
  """
233
234
  if isinstance(result, dict):
235
+ self._add_instance_tags(cast(object, result))
234
236
  measurement = self._resolve_measurement(result)
235
237
  action = self._term_builder(experiment_id=experiment_id, measurement=measurement)
236
238
  return [(action, result)]
@@ -239,6 +241,7 @@ class MeasurePhase(PhaseModule):
239
241
  point_json = point_json_of(payload)
240
242
  if point_json is not None:
241
243
  result = point_json
244
+ self._add_instance_tags(cast(object, result))
242
245
  measurement = self._resolve_measurement(result)
243
246
  action = self._term_builder(experiment_id=experiment_id, measurement=measurement)
244
247
  return [(action, result)]
@@ -254,6 +257,7 @@ class MeasurePhase(PhaseModule):
254
257
 
255
258
  groups: dict[str, list] = {}
256
259
  for item in converted:
260
+ self._add_instance_tags(cast(object, item))
257
261
  if isinstance(item, dict):
258
262
  key = self._resolve_measurement(item)
259
263
  else:
@@ -270,6 +274,24 @@ class MeasurePhase(PhaseModule):
270
274
  self._handle_exception(excp)
271
275
  return []
272
276
 
277
+ def _add_instance_tags(self, item: object) -> None:
278
+ """Tag a point with its instance's organisation and department, unless the adapter already did."""
279
+ if self._metadata_manager is None or not isinstance(item, dict) or "fields" not in item:
280
+ return
281
+ tags = cast(dict[str, object], item).setdefault("tags", {})
282
+ if not isinstance(tags, dict):
283
+ return
284
+ for key, value in self._metadata_manager.instance_tags().items():
285
+ current = cast(dict[str, object], tags).get(key)
286
+ if current is None:
287
+ tags[key] = value
288
+ elif str(current) != value and (key, str(current)) not in self._tag_mismatches:
289
+ # Kept as the adapter set it; warned once so every measurement doesn't repeat it
290
+ self._tag_mismatches.add((key, str(current)))
291
+ logger.warning(
292
+ f"Tag {key}={current!r} differs from the configured {key} {value!r}; keeping {current!r}"
293
+ )
294
+
273
295
  def _resolve_measurement(self, item: dict) -> str:
274
296
  """
275
297
  Resolve the measurement label used for the topic's last segment.
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import os
3
- from typing import Any
3
+ from typing import Any, cast
4
4
 
5
5
  import yaml # type: ignore
6
6
 
@@ -60,6 +60,15 @@ class MetadataManager:
60
60
  return self._instance_data
61
61
  return self._instance_data.get(key)
62
62
 
63
+ def instance_tags(self) -> dict[str, str]:
64
+ """The organisation and department tags every point of this instance carries."""
65
+ tags: dict[str, str] = {}
66
+ for key in ("organisation", "department"):
67
+ value = cast(object, self._instance_data.get(key)) # pyright: ignore[reportUnknownMemberType]
68
+ if value:
69
+ tags[key] = str(value)
70
+ return tags
71
+
63
72
  def get_instance_id(self) -> str:
64
73
  """Convenience accessor for 'instance_id' in equipment."""
65
74
  return self._instance_data.get("instance_id", "")
@@ -16,8 +16,7 @@ from leaf.utility.logger.logger_utils import get_logger
16
16
  root_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
17
17
  adapter_dir = os.path.join(root_dir, "adapters")
18
18
  core_adapter_dir = os.path.join(adapter_dir, "core_adapters")
19
- functional_adapter_dir = os.path.join(adapter_dir, "functional_adapters")
20
- default_equipment_locations = [core_adapter_dir, functional_adapter_dir]
19
+ default_equipment_locations = [core_adapter_dir]
21
20
 
22
21
  output_module_dir = os.path.join(root_dir, "modules", "output_modules")
23
22
  input_module_dir = os.path.join(root_dir, "modules", "input_modules")
@@ -56,13 +56,6 @@ def get_external_input(code: str) -> type[ExternalEventWatcher]:
56
56
  return cls
57
57
 
58
58
 
59
- def all_registered(plugin_type: PluginType) -> dict[str, type[Any]]:
60
- """
61
- Return all registered classes of a given plugin type.
62
- """
63
- return dict(_registry[plugin_type])
64
-
65
-
66
59
  def discover_from_config(config: dict[str, Any], external_path: str = None) -> None:
67
60
  """
68
61
  Discover and register only the plugins referenced in the given configuration.
@@ -111,7 +111,7 @@ def parse_args(args: list[str] | None = None) -> argparse.Namespace:
111
111
  "--port",
112
112
  type=int,
113
113
  default=4242,
114
- help="Port to run the NiceGUI interface on. Default is 8080.",
114
+ help="Port to run the NiceGUI interface on. Default is 4242.",
115
115
  )
116
116
 
117
117
  parser.add_argument(
@@ -1,16 +1,11 @@
1
1
  """Constants for LEAF UI."""
2
2
 
3
3
  # Server constants
4
- DEFAULT_PORT = 8080
4
+ DEFAULT_PORT = 4242
5
5
  MARKETPLACE_URL = "https://gitlab.com/leaf-framework/leaf-marketplace/-/raw/main/public/adapter.yaml"
6
6
  MAX_LOG_LINES = 1000
7
- CONFIGURATION_FILENAME = "configuration.yaml"
8
7
 
9
8
  # UI Constants
10
9
  CARD_WIDTH_CLASS = "w-64"
11
- HEADER_CLASSES = "leaf-header"
12
10
  CARD_CLASSES = "leaf-card"
13
11
  TAB_CLASSES = "leaf-tab"
14
-
15
- # Color constants
16
- STATUS_COLORS = {"online": "status-online", "offline": "status-offline", "warning": "status-warning"}
@@ -38,12 +38,6 @@ def get_startup_error() -> str | None:
38
38
  return _startup_error
39
39
 
40
40
 
41
- def clear_startup_error() -> None:
42
- """Clear the startup error."""
43
- global _startup_error
44
- _startup_error = None
45
-
46
-
47
41
  def start_nicegui(port: int = DEFAULT_PORT, config_path: Path | None = None, auto_open_browser: bool = True) -> None:
48
42
  """
49
43
  Start the LEAF NiceGUI web interface.
@@ -9,7 +9,7 @@ from nicegui import ui
9
9
 
10
10
  from leaf.registry.discovery import get_all_adapter_codes
11
11
  from leaf.ui.constants import CARD_CLASSES, CARD_WIDTH_CLASS, MARKETPLACE_URL
12
- from leaf.ui.utils import adapter_install_disabled, install_adapter, uninstall_adapter
12
+ from leaf.ui.utils import adapter_install_disabled, check_repository_url, install_adapter, uninstall_adapter
13
13
  from leaf.utility.logger.logger_utils import get_logger
14
14
 
15
15
  logger = get_logger(__name__, log_file="input_module.log")
@@ -182,8 +182,8 @@ def _token_dialog(adapter: Mapping[str, object]) -> None:
182
182
  with ui.dialog() as dialog, ui.card().classes("w-96"):
183
183
  _ = ui.label(f"{name} is available on request").classes("text-h6")
184
184
  _ = ui.label("Enter the username and token you received, e.g. a GitLab deploy token.").classes("text-body")
185
- username = ui.input("Username")
186
- token = ui.input("Token", password=True, password_toggle_button=True)
185
+ username = ui.input("Username").classes("w-full")
186
+ token = ui.input("Token", password=True, password_toggle_button=True).classes("w-full")
187
187
  request = adapter.get("request_access")
188
188
  if request:
189
189
  _ = ui.link("Request access", str(request), new_tab=True)
@@ -198,6 +198,43 @@ def _token_dialog(adapter: Mapping[str, object]) -> None:
198
198
  _ = dialog.open()
199
199
 
200
200
 
201
+ def _install_from_url_button() -> None:
202
+ """Installs an adapter that is not in the marketplace, unless adapter installs are disabled."""
203
+ if adapter_install_disabled():
204
+ return
205
+ _ = ui.button("Install from git URL", icon="link", on_click=_url_dialog).classes("btn-secondary mb-4")
206
+
207
+
208
+ def _url_dialog() -> None:
209
+ """Asks for the git URL of an adapter, plus a username and token if its repository is private."""
210
+ with ui.dialog() as dialog, ui.card().classes("w-96"):
211
+ _ = ui.label("Install from a git URL").classes("text-h6")
212
+ _ = ui.label("Only install adapters from sources you trust: they run on this machine.").classes("text-body")
213
+ url = ui.input("Git URL", placeholder="https://gitlab.com/group/leaf-adapter.git").classes("w-full")
214
+ username = ui.input("Username (private repositories only)").classes("w-full")
215
+ token = ui.input("Token (private repositories only)", password=True, password_toggle_button=True).classes(
216
+ "w-full"
217
+ )
218
+
219
+ async def install() -> None:
220
+ repository = str(url.value).strip()
221
+ credentials = None
222
+ if username.value or token.value:
223
+ credentials = (str(username.value), str(token.value))
224
+ problem = check_repository_url(repository, credentials is not None)
225
+ if problem:
226
+ ui.notify(problem, type="negative", position="top")
227
+ return
228
+ _ = dialog.close()
229
+ name = repository.rstrip("/").rsplit("/", 1)[-1].removesuffix(".git")
230
+ await install_adapter({"name": name, "repo_url": repository}, credentials)
231
+
232
+ with ui.row().classes("w-full justify-end"):
233
+ _ = ui.button("Cancel", on_click=dialog.close).classes("btn-tertiary")
234
+ _ = ui.button("Install", icon="download", on_click=install).classes("btn-secondary")
235
+ _ = dialog.open()
236
+
237
+
201
238
  def _uninstall_button(adapter: Mapping[str, object]) -> None:
202
239
  """The installed adapter card's Uninstall button, unless adapter installs are disabled."""
203
240
  if adapter_install_disabled():
@@ -296,6 +333,7 @@ def build_adapters_content(container: ui.column) -> None:
296
333
  ui.icon("store", size="1.5rem").classes("text-icon")
297
334
  ui.label("Adapter Marketplace").classes("text-xl font-semibold text-subheading ml-2")
298
335
  _install_disabled_note()
336
+ _install_from_url_button()
299
337
 
300
338
  # Container for marketplace adapters
301
339
  marketplace_container = ui.column().classes("w-full")
@@ -88,6 +88,18 @@ def with_credentials(repository: str, username: str, token: str) -> str:
88
88
  return urlunsplit(parts._replace(netloc=f"{quote(username, safe='')}:{quote(token, safe='')}@{host}"))
89
89
 
90
90
 
91
+ def check_repository_url(url: str, with_token: bool) -> str | None:
92
+ """Why an adapter can't be installed from url (typed in the UI), or None when it can."""
93
+ parts = urlsplit(url)
94
+ if parts.scheme not in ("http", "https") or not parts.hostname:
95
+ return "Enter an http(s) URL, e.g. https://gitlab.com/group/leaf-adapter.git"
96
+ if parts.username or parts.password:
97
+ return "Put the username and token in their own fields, not in the URL"
98
+ if with_token and parts.scheme != "https":
99
+ return "A token is only sent over https"
100
+ return None
101
+
102
+
91
103
  async def install_adapter(adapter: dict[Any, Any], credentials: tuple[str, str] | None = None) -> None:
92
104
  """
93
105
  Install a LEAF adapter from a Git repository.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "leaf-framework"
3
- version = "0.11.0"
3
+ version = "0.11.1"
4
4
  description = "Lightweight Adapter Framework (LEAF)"
5
5
  authors = [
6
6
  "Matthew Crowther <nmc215@ncl.ac.uk>",
@@ -1,11 +0,0 @@
1
- """
2
- Unused for now.
3
- """
4
-
5
-
6
- class MeasurementHandler:
7
- def __init__(self):
8
- pass
9
-
10
- def add_measurement(self, term, value):
11
- pass
@@ -1,15 +0,0 @@
1
- from leaf.modules.input_modules.event_watcher import EventWatcher
2
- from leaf.register.metadata import MetadataManager
3
- from leaf.utility.logger.logger_utils import get_logger
4
-
5
- logger = get_logger(__name__, log_file="input_module.log")
6
-
7
-
8
- class DBWatcher(EventWatcher):
9
- def __init__(self, metadata_manager: MetadataManager, callbacks=None, error_holder=None):
10
- raise NotImplementedError()
11
- term_map = {}
12
- super().__init__(term_map, metadata_manager, callbacks=callbacks, error_holder=error_holder)
13
-
14
- def start(self):
15
- raise NotImplementedError()
File without changes