leaf-framework 0.11.0__tar.gz → 0.12.0__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.12.0}/PKG-INFO +2 -2
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/adapters/control_spec.py +7 -1
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/adapters/core_adapters/continuous_experiment_adapter.py +1 -1
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/adapters/core_adapters/streaming_adapter.py +2 -2
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/adapters/equipment_adapter.py +7 -1
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/ble_watcher.py +1 -2
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/file_watcher.py +3 -3
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/lads_watcher.py +2 -2
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/modbus_watcher.py +1 -1
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/mqtt_external_event_watcher.py +0 -19
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/mqtt_watcher.py +3 -3
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/opc_watcher.py +2 -2
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/polling_watcher.py +8 -5
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/output_modules/topic_store.py +2 -17
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/phase_modules/measure.py +87 -31
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/phase_modules/start.py +2 -2
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/phase_modules/stop.py +2 -2
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/process_modules/upload_module.py +3 -3
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/register/equipment_actions.yaml +5 -4
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/register/metadata.py +23 -11
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/register/topic_utilities.py +1 -1
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/registry/discovery.py +1 -2
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/registry/registry.py +0 -7
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/start.py +1 -1
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/constants.py +1 -6
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/interface.py +0 -6
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/tabs/adapters.py +41 -3
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/tabs/control.py +12 -6
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/tabs/monitoring.py +50 -39
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/utils.py +12 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/pyproject.toml +2 -2
- 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.12.0}/LICENSE +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/README.md +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/adapters/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/adapters/core_adapters/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/adapters/core_adapters/discrete_experiment_adapter.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/adapters/core_adapters/upload_adapter.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/config/configuration.yaml +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/data/marketplace.yaml +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/error_handler/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/error_handler/error_holder.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/error_handler/exceptions.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/measurement_handler/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/measurement_handler/measurements.yaml +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/measurement_handler/terms.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/.gitkeep +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/event_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/external_api_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/external_event_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/http_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/simple_watcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/measurement_modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/measurement_modules/carbon_dioxide.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/measurement_modules/dissolved_oxygen.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/measurement_modules/fluorescence.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/measurement_modules/measurement_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/measurement_modules/optical_density.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/measurement_modules/ph.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/measurement_modules/temperature.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/output_modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/output_modules/file.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/output_modules/mqtt.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/output_modules/output_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/output_modules/sql.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/phase_modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/phase_modules/control.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/phase_modules/error.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/phase_modules/external_event_phase.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/phase_modules/initialisation.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/phase_modules/phase.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/process_modules/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/process_modules/continous_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/process_modules/discrete_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/process_modules/external_event_process.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/process_modules/process_module.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/register/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/register/abstract_leaf.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/register/equipment_data.yaml +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/register/py.typed +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/registry/loader.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/registry/utils.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/adapter_ui_extension.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/auth.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/components/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/components/app_control.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/components/header.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/components/update_notice.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/desktop_launcher.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/images/icon.icns +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/images/icon.ico +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/images/icon.svg +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/markdown/configuration_help.md +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/markdown/documentation_main.md +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/markdown/documentation_protips.md +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/scripts.js +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/styles.css +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/tabs/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/tabs/configuration.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/tabs/documentation.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/tabs/logs.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/ui/update_checker.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/utility/config_utils.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/utility/logger/__init__.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/utility/logger/logger_utils.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/utility/running_adapters.py +0 -0
- {leaf_framework-0.11.0 → leaf_framework-0.12.0}/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.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Lightweight Adapter Framework (LEAF)
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
15
15
|
Requires-Dist: cairosvg (>=2.9.1,<3.0.0)
|
|
16
16
|
Requires-Dist: dateparser (>=1.4.3,<1.5.0)
|
|
17
17
|
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
18
|
-
Requires-Dist: leafobject (>=0.0
|
|
18
|
+
Requires-Dist: leafobject (>=0.1.0,<0.2.0)
|
|
19
19
|
Requires-Dist: nicegui (>=3.18.0,<3.19.0)
|
|
20
20
|
Requires-Dist: paho-mqtt (>=2.1.0,<2.2.0)
|
|
21
21
|
Requires-Dist: pillow (>=13.0.0,<14.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)
|
|
@@ -87,6 +87,6 @@ class ContinuousExperimentAdapter(EquipmentAdapter):
|
|
|
87
87
|
Injects a synthetic start message via the control process
|
|
88
88
|
to initialise any start-specific logic or output.
|
|
89
89
|
"""
|
|
90
|
-
start_topic = self._metadata_manager.
|
|
90
|
+
start_topic = self._metadata_manager.start
|
|
91
91
|
self._control_process.process_input(start_topic, {})
|
|
92
92
|
super().start()
|
{leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/adapters/core_adapters/streaming_adapter.py
RENAMED
|
@@ -159,9 +159,9 @@ class StreamingAdapter(EquipmentAdapter):
|
|
|
159
159
|
is active for a filename, further non-start/stop events for it are
|
|
160
160
|
no-ops; the running stream() call handles tailing on its own.
|
|
161
161
|
"""
|
|
162
|
-
if topic() == self._metadata_manager.
|
|
162
|
+
if topic() == self._metadata_manager.start():
|
|
163
163
|
self._launch_stream(data)
|
|
164
|
-
elif topic() == self._metadata_manager.
|
|
164
|
+
elif topic() == self._metadata_manager.stop():
|
|
165
165
|
self._stop_stream(data)
|
|
166
166
|
else:
|
|
167
167
|
with self._streams_guard:
|
|
@@ -293,7 +293,13 @@ 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
|
+
# A control acts on the instance itself, so that is its entity unless the message names one
|
|
297
|
+
instance_tags = {
|
|
298
|
+
**self._metadata_manager.instance_tags(),
|
|
299
|
+
"entity": self._metadata_manager.get_instance_id(),
|
|
300
|
+
}
|
|
301
|
+
record = result_record(data, source, error, instance_tags)
|
|
302
|
+
_ = self._output.transmit(f"{topic}/result", record)
|
|
297
303
|
except Exception as e:
|
|
298
304
|
self._logger.warning(f"Could not publish the control record for {topic}: {e}")
|
|
299
305
|
|
|
@@ -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):
|
|
@@ -232,7 +231,7 @@ class BLEEventWatcher(EventWatcher):
|
|
|
232
231
|
|
|
233
232
|
# Dispatch to callbacks using the measurement event
|
|
234
233
|
if self._metadata_manager:
|
|
235
|
-
self._dispatch_callback(self._metadata_manager.
|
|
234
|
+
self._dispatch_callback(self._metadata_manager.measurement, event_data)
|
|
236
235
|
else:
|
|
237
236
|
# Fallback: call callbacks directly with "measurement" event
|
|
238
237
|
for callback in self._callbacks:
|
|
@@ -131,9 +131,9 @@ class FileWatcher(FileSystemEventHandler, EventWatcher):
|
|
|
131
131
|
self._pending_timers: dict = {} # Track pending trailing timers per file path
|
|
132
132
|
|
|
133
133
|
self._term_map = {
|
|
134
|
-
self.on_created: metadata_manager.
|
|
135
|
-
self.on_modified: metadata_manager.
|
|
136
|
-
self.on_deleted: metadata_manager.
|
|
134
|
+
self.on_created: metadata_manager.start,
|
|
135
|
+
self.on_modified: metadata_manager.measurement,
|
|
136
|
+
self.on_deleted: metadata_manager.stop,
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
def start(self) -> None:
|
|
@@ -100,7 +100,7 @@ class LADSWatcher(EventWatcher):
|
|
|
100
100
|
self.logger.debug(f"LADS datachange_notification: node={nodeid}, value={val}")
|
|
101
101
|
info = self._variables.get(nodeid, {})
|
|
102
102
|
self._dispatch_callback(
|
|
103
|
-
self._metadata_manager.
|
|
103
|
+
self._metadata_manager.measurement,
|
|
104
104
|
{
|
|
105
105
|
"functional_unit": info.get("functional_unit"),
|
|
106
106
|
"node": node.nodeid.Identifier,
|
|
@@ -321,7 +321,7 @@ class LADSWatcher(EventWatcher):
|
|
|
321
321
|
self.logger.info(f"Subscribed to: {info['functional_unit']}/{info['browse_name']} ({nodeid})")
|
|
322
322
|
# Send a dummy value to trigger the callback
|
|
323
323
|
self._dispatch_callback(
|
|
324
|
-
self._metadata_manager.
|
|
324
|
+
self._metadata_manager.measurement,
|
|
325
325
|
{
|
|
326
326
|
"functional_unit": info["functional_unit"],
|
|
327
327
|
"node": node.nodeid.Identifier,
|
{leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/modbus_watcher.py
RENAMED
|
@@ -170,7 +170,7 @@ class ModbusWatcher(ExternalEventWatcher):
|
|
|
170
170
|
|
|
171
171
|
def _emit(self, data: dict[str, Any]) -> None:
|
|
172
172
|
if self._metadata_manager:
|
|
173
|
-
self._dispatch_callback(self._metadata_manager.
|
|
173
|
+
self._dispatch_callback(self._metadata_manager.measurement, data)
|
|
174
174
|
else:
|
|
175
175
|
for callback in self._callbacks:
|
|
176
176
|
try:
|
|
@@ -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)
|
|
@@ -92,9 +92,9 @@ class MQTTEventWatcher(EventWatcher):
|
|
|
92
92
|
|
|
93
93
|
# topic -> list of events
|
|
94
94
|
self._topic_event_map: dict[str, list[str]] = {}
|
|
95
|
-
self._register_topics(start_topics, metadata_manager.
|
|
96
|
-
self._register_topics(measurement_topics, metadata_manager.
|
|
97
|
-
self._register_topics(stop_topics, metadata_manager.
|
|
95
|
+
self._register_topics(start_topics, metadata_manager.start)
|
|
96
|
+
self._register_topics(measurement_topics, metadata_manager.measurement)
|
|
97
|
+
self._register_topics(stop_topics, metadata_manager.stop)
|
|
98
98
|
self._register_topics(error_topics, metadata_manager.error)
|
|
99
99
|
|
|
100
100
|
# Set when on_connect fires so start() can block until subscriptions are active
|
|
@@ -77,7 +77,7 @@ class OPCWatcher(EventWatcher):
|
|
|
77
77
|
def datachange_notification(self, node: Node, val: int | str | float, data: DataChangeNotification) -> None:
|
|
78
78
|
self.logger.debug(f"OPC datachange_notification: node={node.nodeid.Identifier}, value={val}")
|
|
79
79
|
self._dispatch_callback(
|
|
80
|
-
self._metadata_manager.
|
|
80
|
+
self._metadata_manager.measurement,
|
|
81
81
|
{
|
|
82
82
|
"node": node.nodeid.Identifier,
|
|
83
83
|
"value": val,
|
|
@@ -233,7 +233,7 @@ class OPCWatcher(EventWatcher):
|
|
|
233
233
|
self.logger.info(f"Subscribed to: {node.nodeid.to_string()}")
|
|
234
234
|
# Send a dummy value to trigger the callback
|
|
235
235
|
self._dispatch_callback(
|
|
236
|
-
self._metadata_manager.
|
|
236
|
+
self._metadata_manager.measurement,
|
|
237
237
|
{
|
|
238
238
|
"node": node.nodeid.Identifier,
|
|
239
239
|
"value": node.get_value(),
|
{leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/input_modules/polling_watcher.py
RENAMED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import threading
|
|
2
|
-
import time
|
|
3
2
|
from abc import abstractmethod
|
|
4
3
|
from collections.abc import Callable
|
|
5
4
|
from typing import Any
|
|
@@ -40,10 +39,12 @@ class PollingWatcher(EventWatcher):
|
|
|
40
39
|
|
|
41
40
|
self._interval: int = interval
|
|
42
41
|
self._thread: threading.Thread | None = None
|
|
42
|
+
# Set by stop() so the poll loop wakes at once instead of finishing a full interval.
|
|
43
|
+
self._stop_event: threading.Event = threading.Event()
|
|
43
44
|
self._term_map = {
|
|
44
|
-
self.start_message: metadata_manager.
|
|
45
|
-
self.stop_message: metadata_manager.
|
|
46
|
-
self.measurement_message: metadata_manager.
|
|
45
|
+
self.start_message: metadata_manager.start,
|
|
46
|
+
self.stop_message: metadata_manager.stop,
|
|
47
|
+
self.measurement_message: metadata_manager.measurement,
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
def start_message(self, data: Any) -> None:
|
|
@@ -99,7 +100,7 @@ class PollingWatcher(EventWatcher):
|
|
|
99
100
|
# reporting data.
|
|
100
101
|
pass
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
_ = self._stop_event.wait(self._interval)
|
|
103
104
|
|
|
104
105
|
def start(self) -> None:
|
|
105
106
|
"""
|
|
@@ -112,6 +113,7 @@ class PollingWatcher(EventWatcher):
|
|
|
112
113
|
return
|
|
113
114
|
|
|
114
115
|
self._running = True
|
|
116
|
+
self._stop_event.clear()
|
|
115
117
|
self._thread = threading.Thread(target=self._poll, daemon=True)
|
|
116
118
|
self._thread.start()
|
|
117
119
|
|
|
@@ -127,6 +129,7 @@ class PollingWatcher(EventWatcher):
|
|
|
127
129
|
return
|
|
128
130
|
|
|
129
131
|
self._running = False
|
|
132
|
+
self._stop_event.set()
|
|
130
133
|
if self._thread:
|
|
131
134
|
self._thread.join()
|
|
132
135
|
logger.info("PollingWatcher stopped.")
|
|
@@ -46,13 +46,12 @@ class TopicStore:
|
|
|
46
46
|
sample = _point_sample(point)
|
|
47
47
|
if sample is None:
|
|
48
48
|
continue
|
|
49
|
-
key = history_key(topic, point)
|
|
50
49
|
with self._history_lock:
|
|
51
|
-
history = self._history.setdefault(
|
|
50
|
+
history = self._history.setdefault(topic, deque(maxlen=HISTORY_LENGTH))
|
|
52
51
|
history.append(sample)
|
|
53
52
|
|
|
54
53
|
def get_history(self) -> dict[str, list[tuple[float, float]]]:
|
|
55
|
-
"""(timestamp, value) pairs per
|
|
54
|
+
"""(timestamp, value) pairs per topic, oldest first."""
|
|
56
55
|
with self._history_lock:
|
|
57
56
|
return {topic: list(points) for topic, points in self._history.items()}
|
|
58
57
|
|
|
@@ -83,20 +82,6 @@ class TopicStore:
|
|
|
83
82
|
return self._store
|
|
84
83
|
|
|
85
84
|
|
|
86
|
-
# Separates the topic from the point's entity tag in a history key
|
|
87
|
-
ENTITY_SEPARATOR = "#"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def history_key(topic: str, point: object) -> str:
|
|
91
|
-
"""Topic plus the point's "entity" tag, since one topic can carry several entities (e.g. the wells of a plate)."""
|
|
92
|
-
tags = cast(dict[str, object], point).get("tags")
|
|
93
|
-
if isinstance(tags, dict):
|
|
94
|
-
entity = cast(dict[str, object], tags).get("entity")
|
|
95
|
-
if isinstance(entity, str) and entity:
|
|
96
|
-
return f"{topic}{ENTITY_SEPARATOR}{entity}"
|
|
97
|
-
return topic
|
|
98
|
-
|
|
99
|
-
|
|
100
85
|
def _point_sample(point: object) -> tuple[float, float] | None:
|
|
101
86
|
"""(timestamp, value) of a LeafPoint JSON dict with a numeric fields.value, or None."""
|
|
102
87
|
if not isinstance(point, dict):
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import re
|
|
2
3
|
import threading
|
|
3
4
|
import time
|
|
4
5
|
from typing import TYPE_CHECKING, Any, cast
|
|
@@ -35,14 +36,14 @@ class MeasurePhase(PhaseModule):
|
|
|
35
36
|
maximum_message_size (Optional[int]): The maximum number of measurements in a single message.
|
|
36
37
|
error_holder (Optional[ErrorHolder]): Optional, an error holder to manage phase errors.
|
|
37
38
|
"""
|
|
38
|
-
|
|
39
|
-
metadata_manager.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
39
|
+
if metadata_manager is not None:
|
|
40
|
+
term_builder = metadata_manager.measurement
|
|
41
|
+
else:
|
|
42
|
+
term_builder = "metadata_manager.measurement"
|
|
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
|
|
@@ -146,10 +147,10 @@ class MeasurePhase(PhaseModule):
|
|
|
146
147
|
finally:
|
|
147
148
|
self._measurement_lock.release()
|
|
148
149
|
else:
|
|
149
|
-
experiment_id = kwargs.get("experiment_id", "unknown")
|
|
150
150
|
measurement = kwargs.get("measurement", "unknown")
|
|
151
|
+
entity = kwargs.get("entity", self._instance_entity())
|
|
151
152
|
|
|
152
|
-
action = self._term_builder(
|
|
153
|
+
action = self._term_builder(entity=entity, measurement=measurement)
|
|
153
154
|
return [(action, data)]
|
|
154
155
|
|
|
155
156
|
def stream(self, output: "OutputModule", filename: Any) -> None:
|
|
@@ -217,11 +218,9 @@ class MeasurePhase(PhaseModule):
|
|
|
217
218
|
"""
|
|
218
219
|
Formulate one or more (topic, data) messages for a measurement result.
|
|
219
220
|
|
|
220
|
-
A list/set of points can mix several
|
|
221
|
-
(e.g.
|
|
222
|
-
encodes
|
|
223
|
-
the batch is split out into its own message rather than all being
|
|
224
|
-
published under whichever type happened to come first.
|
|
221
|
+
A list/set of points can mix several measurement types (e.g. pH,
|
|
222
|
+
temperature) and entities (e.g. reactors) in one batch. The topic
|
|
223
|
+
encodes one of each, so every distinct pair gets its own message.
|
|
225
224
|
|
|
226
225
|
Args:
|
|
227
226
|
experiment_id (str): The ID of the experiment.
|
|
@@ -231,45 +230,102 @@ class MeasurePhase(PhaseModule):
|
|
|
231
230
|
list[tuple]: A list of (action, result) tuples.
|
|
232
231
|
"""
|
|
233
232
|
if isinstance(result, dict):
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return [(action, result)]
|
|
233
|
+
self._add_instance_tags(cast(object, result), experiment_id)
|
|
234
|
+
return [(self._topic_of(result), result)]
|
|
237
235
|
|
|
238
236
|
payload = cast(object, result)
|
|
239
|
-
|
|
237
|
+
try:
|
|
238
|
+
point_json = point_json_of(self._with_entity(payload))
|
|
239
|
+
except ValueError as ex:
|
|
240
|
+
# A LeafPoint that fails validation, e.g. one without an entity tag
|
|
241
|
+
self._handle_exception(InterpreterError(str(ex)))
|
|
242
|
+
return []
|
|
240
243
|
if point_json is not None:
|
|
241
244
|
result = point_json
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return [(action, result)]
|
|
245
|
+
self._add_instance_tags(cast(object, result), experiment_id)
|
|
246
|
+
return [(self._topic_of(result), result)]
|
|
245
247
|
|
|
246
248
|
if isinstance(result, list):
|
|
247
249
|
converted = []
|
|
248
250
|
for item in cast(list[object], result):
|
|
249
|
-
|
|
251
|
+
try:
|
|
252
|
+
point_json = point_json_of(self._with_entity(item))
|
|
253
|
+
except ValueError as ex:
|
|
254
|
+
# Only the invalid point is dropped, the rest of the batch is still sent
|
|
255
|
+
self._handle_exception(InterpreterError(str(ex)))
|
|
256
|
+
continue
|
|
250
257
|
if point_json is not None:
|
|
251
258
|
converted.append(point_json)
|
|
252
259
|
else:
|
|
253
260
|
converted.append(item)
|
|
254
261
|
|
|
262
|
+
# One message per topic, so per metric and entity
|
|
255
263
|
groups: dict[str, list] = {}
|
|
256
264
|
for item in converted:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
key = "unknown"
|
|
261
|
-
groups.setdefault(key, []).append(item)
|
|
262
|
-
|
|
263
|
-
messages = []
|
|
264
|
-
for key, group in groups.items():
|
|
265
|
-
action = self._term_builder(experiment_id=experiment_id, measurement=key)
|
|
266
|
-
messages.append((action, group))
|
|
267
|
-
return messages
|
|
265
|
+
self._add_instance_tags(cast(object, item), experiment_id)
|
|
266
|
+
groups.setdefault(self._topic_of(item), []).append(item)
|
|
267
|
+
return list(groups.items())
|
|
268
268
|
|
|
269
269
|
excp = AdapterLogicError(f"Unknown measurement data type: {type(result)}")
|
|
270
270
|
self._handle_exception(excp)
|
|
271
271
|
return []
|
|
272
272
|
|
|
273
|
+
def _add_instance_tags(self, item: object, experiment_id: str | None = None) -> None:
|
|
274
|
+
"""Tag a point with its instance's organisation, department, experiment id and entity, unless the adapter already did."""
|
|
275
|
+
if self._metadata_manager is None or not isinstance(item, dict) or "fields" not in item:
|
|
276
|
+
return
|
|
277
|
+
tags = cast(dict[str, object], item).setdefault("tags", {})
|
|
278
|
+
if not isinstance(tags, dict):
|
|
279
|
+
return
|
|
280
|
+
expected = self._metadata_manager.instance_tags()
|
|
281
|
+
# These defaults mean the adapter never set an experiment id
|
|
282
|
+
if experiment_id and experiment_id not in ("undefined", "invalid_id"):
|
|
283
|
+
expected["experiment_id"] = experiment_id
|
|
284
|
+
for key, value in expected.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
|
+
# Every point has an entity; one inside the instance (e.g. a reactor) differs from it by design, so no warning
|
|
295
|
+
if not tags.get("entity"):
|
|
296
|
+
tags["entity"] = self._instance_entity()
|
|
297
|
+
|
|
298
|
+
def _with_entity(self, item: object) -> object:
|
|
299
|
+
"""Name the instance as the entity of a LeafPoint that names none."""
|
|
300
|
+
# Before to_json(), which rejects a point without an entity
|
|
301
|
+
tags: object = getattr(item, "tags", None)
|
|
302
|
+
if not isinstance(tags, dict):
|
|
303
|
+
return item
|
|
304
|
+
point_tags = cast(dict[str, object], tags)
|
|
305
|
+
if not point_tags.get("entity"):
|
|
306
|
+
point_tags["entity"] = self._instance_entity()
|
|
307
|
+
return item
|
|
308
|
+
|
|
309
|
+
def _topic_of(self, item: object) -> str:
|
|
310
|
+
"""Measurement topic of a point: .../measurement/<entity>/<metric>."""
|
|
311
|
+
if not isinstance(item, dict):
|
|
312
|
+
return self._term_builder(entity=self._instance_entity(), measurement="unknown")
|
|
313
|
+
return self._term_builder(entity=self._resolve_entity(item), measurement=self._resolve_measurement(item))
|
|
314
|
+
|
|
315
|
+
def _instance_entity(self) -> str:
|
|
316
|
+
"""Entity of a point that names none: the instance itself."""
|
|
317
|
+
if self._metadata_manager is None:
|
|
318
|
+
return "unknown"
|
|
319
|
+
return self._metadata_manager.get_instance_id() or "unknown"
|
|
320
|
+
|
|
321
|
+
def _resolve_entity(self, item: dict[str, Any]) -> str:
|
|
322
|
+
"""The point's "entity" tag, else the instance."""
|
|
323
|
+
tags = item.get("tags")
|
|
324
|
+
if not isinstance(tags, dict) or not tags.get("entity"):
|
|
325
|
+
return self._instance_entity()
|
|
326
|
+
# Level separators and wildcards would break the topic
|
|
327
|
+
return re.sub(r"[/+#]", "_", str(tags["entity"]))
|
|
328
|
+
|
|
273
329
|
def _resolve_measurement(self, item: dict) -> str:
|
|
274
330
|
"""
|
|
275
331
|
Resolve the measurement label used for the topic's last segment.
|
|
@@ -25,7 +25,7 @@ class StartPhase(ControlPhase):
|
|
|
25
25
|
metadata_manager (MetadataManager): Manages metadata associated with the phase.
|
|
26
26
|
error_holder (Optional[ErrorHolder]): Optional, an error holder to manage phase errors.
|
|
27
27
|
"""
|
|
28
|
-
term_builder = metadata_manager.
|
|
28
|
+
term_builder = metadata_manager.start
|
|
29
29
|
super().__init__(term_builder, metadata_manager=metadata_manager, error_holder=error_holder)
|
|
30
30
|
|
|
31
31
|
def update(self, data: Any) -> list:
|
|
@@ -48,5 +48,5 @@ class StartPhase(ControlPhase):
|
|
|
48
48
|
|
|
49
49
|
data = super().update(data)
|
|
50
50
|
data += [(self._metadata_manager.running(), True)]
|
|
51
|
-
data += [(self._metadata_manager.
|
|
51
|
+
data += [(self._metadata_manager.stop(), None)]
|
|
52
52
|
return data
|
|
@@ -25,7 +25,7 @@ class StopPhase(ControlPhase):
|
|
|
25
25
|
metadata_manager (MetadataManager): Manages metadata associated with the phase.
|
|
26
26
|
error_holder (Optional[ErrorHolder]): Optional, an error holder to manage phase errors.
|
|
27
27
|
"""
|
|
28
|
-
term_builder = metadata_manager.
|
|
28
|
+
term_builder = metadata_manager.stop
|
|
29
29
|
super().__init__(term_builder, metadata_manager=metadata_manager, error_holder=error_holder)
|
|
30
30
|
|
|
31
31
|
def update(self, data: Any) -> list:
|
|
@@ -47,5 +47,5 @@ class StopPhase(ControlPhase):
|
|
|
47
47
|
self._handle_exception(leaf_exp)
|
|
48
48
|
data = super().update(data)
|
|
49
49
|
data += [(self._metadata_manager.running(), False)]
|
|
50
|
-
data += [(self._metadata_manager.
|
|
50
|
+
data += [(self._metadata_manager.start(), None)]
|
|
51
51
|
return data
|
{leaf_framework-0.11.0 → leaf_framework-0.12.0}/leaf/modules/process_modules/upload_module.py
RENAMED
|
@@ -43,7 +43,7 @@ class UploadProcess(DiscreteProcess):
|
|
|
43
43
|
topic (str): The topic to activate a specific phase.
|
|
44
44
|
data (dict): The data to be processed by the phase.
|
|
45
45
|
"""
|
|
46
|
-
if topic() != self._metadata_manager.
|
|
46
|
+
if topic() != self._metadata_manager.start():
|
|
47
47
|
assert topic() == self._metadata_manager.details()
|
|
48
48
|
return super().process_input(topic, data)
|
|
49
49
|
|
|
@@ -52,11 +52,11 @@ class UploadProcess(DiscreteProcess):
|
|
|
52
52
|
time.sleep(1)
|
|
53
53
|
|
|
54
54
|
# Measurements
|
|
55
|
-
measure_builder = self._metadata_manager.
|
|
55
|
+
measure_builder = self._metadata_manager.measurement
|
|
56
56
|
super().process_input(measure_builder, data)
|
|
57
57
|
time.sleep(1)
|
|
58
58
|
|
|
59
59
|
# Stop
|
|
60
|
-
stop_builder = self._metadata_manager.
|
|
60
|
+
stop_builder = self._metadata_manager.stop
|
|
61
61
|
super().process_input(stop_builder, data)
|
|
62
62
|
time.sleep(1)
|
|
@@ -4,7 +4,8 @@ heartbeat: <root>/<organisation>/<department>/<adapter_id>/<instance_id>/heartbe
|
|
|
4
4
|
error: <root>/<organisation>/<department>/<adapter_id>/<instance_id>/error
|
|
5
5
|
control: <root>/<organisation>/<department>/<adapter_id>/<instance_id>/control
|
|
6
6
|
control_result: <root>/<organisation>/<department>/<adapter_id>/<instance_id>/control/result
|
|
7
|
-
experiment
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
# A run (experiment, batch, upload) starts and stops on the instance itself; its id is a point tag
|
|
8
|
+
start: <root>/<organisation>/<department>/<adapter_id>/<instance_id>/start
|
|
9
|
+
stop: <root>/<organisation>/<department>/<adapter_id>/<instance_id>/stop
|
|
10
|
+
# The entity is what a point is about: the instance itself, or something inside it such as a reactor
|
|
11
|
+
measurement: <root>/<organisation>/<department>/<adapter_id>/<instance_id>/measurement/<entity>/<measurement>
|
|
@@ -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", "")
|
|
@@ -108,8 +117,8 @@ class MetadataManager:
|
|
|
108
117
|
|
|
109
118
|
def __getattr__(self, name: str) -> Any:
|
|
110
119
|
"""
|
|
111
|
-
If the user does 'metadata_manager.
|
|
112
|
-
'
|
|
120
|
+
If the user does 'metadata_manager.start()', we look up
|
|
121
|
+
'start' in the TopicUtility's root group, and wrap it to
|
|
113
122
|
auto-fill placeholders with equipment metadata.
|
|
114
123
|
"""
|
|
115
124
|
try:
|
|
@@ -120,10 +129,7 @@ class MetadataManager:
|
|
|
120
129
|
if callable(sub):
|
|
121
130
|
|
|
122
131
|
def _wrapper(**overrides) -> str:
|
|
123
|
-
|
|
124
|
-
data.update(self.get_instance_data())
|
|
125
|
-
placeholders = {**data, **overrides}
|
|
126
|
-
return sub(**placeholders)
|
|
132
|
+
return cast(str, sub(**self._placeholders(overrides)))
|
|
127
133
|
|
|
128
134
|
return _wrapper
|
|
129
135
|
if isinstance(sub, _TemplateGroup):
|
|
@@ -131,6 +137,15 @@ class MetadataManager:
|
|
|
131
137
|
|
|
132
138
|
return sub
|
|
133
139
|
|
|
140
|
+
def _placeholders(self, overrides: dict[str, Any]) -> dict[str, Any]:
|
|
141
|
+
"""Template placeholders from the equipment and instance data, then the caller's overrides."""
|
|
142
|
+
data = self.get_equipment_data().copy()
|
|
143
|
+
data.update(self.get_instance_data())
|
|
144
|
+
# A point that names no entity is about the instance itself
|
|
145
|
+
if data.get("instance_id"):
|
|
146
|
+
data.setdefault("entity", data["instance_id"])
|
|
147
|
+
return {**data, **overrides}
|
|
148
|
+
|
|
134
149
|
|
|
135
150
|
class _ManagerGroup:
|
|
136
151
|
"""
|
|
@@ -147,10 +162,7 @@ class _ManagerGroup:
|
|
|
147
162
|
if callable(sub):
|
|
148
163
|
|
|
149
164
|
def _wrapper(**overrides) -> str:
|
|
150
|
-
|
|
151
|
-
data.update(self._manager.get_instance_data())
|
|
152
|
-
placeholders = {**data, **overrides}
|
|
153
|
-
return sub(**placeholders)
|
|
165
|
+
return cast(str, sub(**self._manager._placeholders(overrides))) # pyright: ignore[reportPrivateUsage]
|
|
154
166
|
|
|
155
167
|
return _wrapper
|
|
156
168
|
if isinstance(sub, _TemplateGroup):
|
|
@@ -68,7 +68,7 @@ class TopicUtility:
|
|
|
68
68
|
self._collect_templates(self._raw_dict)
|
|
69
69
|
self.root = _TemplateGroup(self._raw_dict, self)
|
|
70
70
|
|
|
71
|
-
def __getattr__(self, name: str):
|
|
71
|
+
def __getattr__(self, name: str) -> Any:
|
|
72
72
|
"""
|
|
73
73
|
Handle dynamic attribute access:
|
|
74
74
|
- If it's a required field from the YAML, return its name.
|
|
@@ -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(
|