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.
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/PKG-INFO +1 -1
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/control_spec.py +7 -1
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/equipment_adapter.py +2 -1
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/ble_watcher.py +0 -1
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/mqtt_external_event_watcher.py +0 -19
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/measure.py +22 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/metadata.py +10 -1
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/registry/discovery.py +1 -2
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/registry/registry.py +0 -7
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/start.py +1 -1
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/constants.py +1 -6
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/interface.py +0 -6
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/adapters.py +41 -3
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/utils.py +12 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/pyproject.toml +1 -1
- leaf_framework-0.11.0/leaf/measurement_handler/handler.py +0 -11
- leaf_framework-0.11.0/leaf/modules/input_modules/db_watcher.py +0 -15
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/LICENSE +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/README.md +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/continuous_experiment_adapter.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/discrete_experiment_adapter.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/streaming_adapter.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/upload_adapter.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/config/configuration.yaml +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/data/marketplace.yaml +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/error_handler/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/error_handler/error_holder.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/error_handler/exceptions.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/measurement_handler/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/measurement_handler/measurements.yaml +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/measurement_handler/terms.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/.gitkeep +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/event_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/external_api_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/external_event_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/file_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/http_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/lads_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/modbus_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/mqtt_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/opc_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/polling_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/simple_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/carbon_dioxide.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/dissolved_oxygen.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/fluorescence.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/measurement_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/optical_density.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/ph.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/temperature.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/file.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/mqtt.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/output_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/sql.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/topic_store.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/control.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/error.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/external_event_phase.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/initialisation.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/phase.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/start.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/stop.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/continous_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/discrete_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/external_event_process.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/process_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/upload_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/abstract_leaf.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/equipment_actions.yaml +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/equipment_data.yaml +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/py.typed +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/register/topic_utilities.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/registry/loader.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/registry/utils.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/adapter_ui_extension.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/auth.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/components/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/components/app_control.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/components/header.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/components/update_notice.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/desktop_launcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/images/icon.icns +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/images/icon.ico +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/images/icon.svg +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/markdown/configuration_help.md +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/markdown/documentation_main.md +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/markdown/documentation_protips.md +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/scripts.js +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/styles.css +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/configuration.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/control.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/documentation.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/logs.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/tabs/monitoring.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/ui/update_checker.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/utility/config_utils.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/utility/logger/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/utility/logger/logger_utils.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/utility/running_adapters.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/utility/running_utilities.py +0 -0
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/adapters/core_adapters/streaming_adapter.py
RENAMED
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/external_api_watcher.py
RENAMED
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/external_event_watcher.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/modbus_watcher.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/polling_watcher.py
RENAMED
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/input_modules/simple_watcher.py
RENAMED
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/__init__.py
RENAMED
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/carbon_dioxide.py
RENAMED
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/dissolved_oxygen.py
RENAMED
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/fluorescence.py
RENAMED
|
File without changes
|
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/optical_density.py
RENAMED
|
File without changes
|
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/measurement_modules/temperature.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/output_modules/output_module.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/external_event_phase.py
RENAMED
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/phase_modules/initialisation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/continous_module.py
RENAMED
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/discrete_module.py
RENAMED
|
File without changes
|
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/leaf/modules/process_modules/process_module.py
RENAMED
|
File without changes
|
{leaf_framework-0.11.0 → leaf_framework-0.11.1}/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
|