From 142646396e6c0253520bedc27e244b05d6290229 Mon Sep 17 00:00:00 2001 From: Hector van der Aa Date: Fri, 18 Sep 2026 22:21:31 +0200 Subject: [PATCH] Added API guardrails with copy API has been update to return copies when returning immutable objects to avoid passing internal references to the user that could then modify them, breaking the internal state. Even if the copy operation is obsolete in some cases, it is kept in as a safety should the API functions be modified which would cause a return reference issue, the concerned API methods are not to be requested at high frequency either way so the performance hit is estimated to be minimal, the only API method that may be queried at a high freqency is fetching live values which are returned as floats which are immutable and therefore not concerned --- src/dynalab_core/__init__.py | 22 ++++++++++------------ src/dynalab_core/dlpak.py | 11 +++++++---- src/dynalab_core/dlunit.py | 16 +++++++++++----- src/dynalab_core/types.py | 2 +- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/dynalab_core/__init__.py b/src/dynalab_core/__init__.py index db8d46a..0b4c8c8 100644 --- a/src/dynalab_core/__init__.py +++ b/src/dynalab_core/__init__.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later +from copy import copy from datetime import datetime, timezone import logging from pathlib import Path @@ -225,11 +226,6 @@ class Core: return stopped def set_mode(self, mode: CoreMode) -> None: - if mode not in ("realtime", "playback"): - raise CoreError( - f"Unknown core mode {mode!r}; expected 'realtime' or 'playback'", - code="invalid_mode", - ) if self._recording.is_set(): raise CoreError( "Cannot change core mode while a recording is active", @@ -241,7 +237,7 @@ class Core: self._live_values = {} def get_mode(self) -> CoreMode: - return self._mode.value + return copy(self._mode.value) def load_playback_engine(self, input: Path | None = None) -> None: if self._mode.value != "playback": @@ -255,11 +251,14 @@ class Core: "Cannot load playback because no recording is available", code="no_playback_data", ) - loaded_data = self._processing_buffer + loaded_data = copy(self._processing_buffer) else: loaded_data = DLPak().read(input) self._playback_engine.set_data(loaded_data) + def load_processing_buffer(self, input: Path | None = None) -> None: + pass + # def unload_playback_engine def start_recording(self) -> None: @@ -299,8 +298,7 @@ class Core: time.sleep(1) self._recording_buffer.normalize() self._recording_buffer.sort() - self._processing_buffer = DLPak() - self._processing_buffer.set_data(self._recording_buffer) + self._processing_buffer = DLPak().set_data(copy(self._recording_buffer)) connector_signal_descriptors = ( self._connector_registry.get_all_signal_descriptors() ) @@ -343,7 +341,7 @@ class Core: if signal is None and self._core_config.playback_mode == "reprocess": signal = self._derive_registry.get_signal_descriptor(signal_id) - return signal + return copy(signal) def get_all_signal_descriptors(self) -> list[SignalDescriptor]: root_signals: list[SignalDescriptor] = [] @@ -358,7 +356,7 @@ class Core: or self._mode.value == "realtime" ): derive_signals = self._derive_registry.get_all_signal_descriptors() - return root_signals + derive_signals + return copy(root_signals + derive_signals) def get_live_value(self, signal_id: UUID) -> float | None: with self._live_values_lock: @@ -391,7 +389,7 @@ class Core: ) unit = self._derive_registry.register(dlunit) - return unit.uuid() + return copy(unit.uuid()) def unbind_derive_unit(self, unit_id: UUID) -> None: self._derive_registry.unregister(unit_id) diff --git a/src/dynalab_core/dlpak.py b/src/dynalab_core/dlpak.py index a40efe2..23f206f 100644 --- a/src/dynalab_core/dlpak.py +++ b/src/dynalab_core/dlpak.py @@ -12,14 +12,17 @@ from pydantic import BaseModel, Field from dynalab_core.buffer import ValueBuffer from dynalab_core.errors import DLPakError -from dynalab_core.protocols.packets.handshake import SignalDescriptor +from dynalab_core.protocols.packets.handshake import ( + JsonSignalDescriptor, + SignalDescriptor, +) log = logging.getLogger(__name__) class RecordManifest(BaseModel): timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) - signals: list[SignalDescriptor] = Field(default_factory=list) + signals: list[JsonSignalDescriptor] = Field(default_factory=list) comments: str = "" @@ -45,11 +48,11 @@ class DLPak: data_signal_ids = self._data.get_signal_ids() - manifest_signals: list[SignalDescriptor] = [] + manifest_signals: list[JsonSignalDescriptor] = [] for signal in all_signals: if signal.id in data_signal_ids: - manifest_signals.append(signal) + manifest_signals.append(signal.to_json()) self._manifest = RecordManifest( timestamp=timestamp, signals=manifest_signals, comments="" diff --git a/src/dynalab_core/dlunit.py b/src/dynalab_core/dlunit.py index 22ab478..4c5b27a 100644 --- a/src/dynalab_core/dlunit.py +++ b/src/dynalab_core/dlunit.py @@ -11,7 +11,10 @@ from zipfile import ZIP_DEFLATED, ZipFile from pydantic import BaseModel, Field from dynalab_core.errors import DLPakError, DLUnitError -from dynalab_core.protocols.packets.handshake import SignalDescriptor +from dynalab_core.protocols.packets.handshake import ( + JsonSignalDescriptor, + SignalDescriptor, +) log = logging.getLogger(__name__) @@ -19,8 +22,8 @@ log = logging.getLogger(__name__) class DeriveManifest(BaseModel): unit_uuid: UUID - input_signals: list[SignalDescriptor] = Field(default_factory=list) - return_signal: SignalDescriptor + input_signals: list[JsonSignalDescriptor] = Field(default_factory=list) + return_signal: JsonSignalDescriptor comments: str = "" @@ -42,10 +45,13 @@ class DLUnit: return_signal: SignalDescriptor, ) -> None: + json_input_signals = [signal.to_json() for signal in input_signals] + json_return_signal = return_signal.to_json() + self._manifest = DeriveManifest( unit_uuid=unit_uuid, - input_signals=input_signals, - return_signal=return_signal, + input_signals=json_input_signals, + return_signal=json_return_signal, ) def read(self, file: str | Path) -> Self: diff --git a/src/dynalab_core/types.py b/src/dynalab_core/types.py index 7812ffa..f4d563f 100644 --- a/src/dynalab_core/types.py +++ b/src/dynalab_core/types.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from typing import Literal -CoreMode = Literal["realtime", "playback"] +CoreMode = Literal["realtime", "playback", "offline"] @dataclass