Files
dynalab-core/src/dynalab_core/dlunit.py
T
h3cx 142646396e 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
2026-09-18 22:21:31 +02:00

149 lines
4.5 KiB
Python

# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
from pathlib import Path
from typing import Literal, Self
from uuid import UUID
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 (
JsonSignalDescriptor,
SignalDescriptor,
)
log = logging.getLogger(__name__)
class DeriveManifest(BaseModel):
unit_uuid: UUID
input_signals: list[JsonSignalDescriptor] = Field(default_factory=list)
return_signal: JsonSignalDescriptor
comments: str = ""
class DLUnit:
def __init__(self) -> None:
self._dir: Path | None = None
self._filename: str | None = None
self._savetype: Literal["zip", "folder"] = "zip"
self._data: str | None = None
self._manifest: DeriveManifest | None = None
def set_data(self, data: str) -> None:
self._data = data
def set_manifest(
self,
unit_uuid: UUID,
input_signals: list[SignalDescriptor],
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=json_input_signals,
return_signal=json_return_signal,
)
def read(self, file: str | Path) -> Self:
input_path = Path(file)
try:
with ZipFile(input_path, mode="r") as archive:
manifest = DeriveManifest.model_validate_json(
archive.read("manifest.json")
)
data: str = archive.read("unit.py").decode("utf-8")
except Exception as error:
log.exception(
"Failed to read DLUnit archive from %s",
input_path.name,
extra={
"event": "dlunit.read_failed",
"input_filename": input_path.name,
"exception_type": type(error).__name__,
},
)
raise DLUnitError(
f"Could not read DLUnit archive {input_path}: {error}",
code="read_failed",
) from error
self._dir = input_path.parent
self._filename = input_path.name
self._data = data
self._manifest = manifest
log.info(
"Read DLUnit archive from %s",
input_path.name,
extra={
"event": "dlunit.read",
"input_filename": input_path.name,
},
)
return self
def write(self, dir: str | Path, filename: str) -> None:
output_dir = Path(dir)
if self._data is None:
raise DLUnitError(
"Cannot write a DLUnit archive before setting its data",
code="no_data",
)
if self._manifest is None:
raise DLUnitError(
"Cannot write a DLUnit archive before creating its manifest",
code="no_manifest",
)
if output_dir.is_file():
output_dir = output_dir.parent
file_path = Path(filename)
if file_path.suffix.lower() != ".dlunit":
file_path = file_path.with_suffix(".dlunit")
output_path = output_dir / file_path
try:
with ZipFile(output_path, mode="w", compression=ZIP_DEFLATED) as archive:
archive.writestr(
"manifest.json", self._manifest.model_dump_json(indent=2)
)
archive.writestr("unit.py", self._data.export_csv())
except Exception as error:
log.exception(
"Failed to write DLUnit archive to %s",
output_path.name,
extra={
"event": "dlunit.write_failed",
"output_filename": output_path.name,
"exception_type": type(error).__name__,
},
)
raise DLUnitError(
f"Could not write DLUnit archive {output_path}: {error}",
code="write_failed",
) from error
log.info(
"Wrote DLUnit archive to %s",
output_path.name,
extra={
"event": "dlunit.written",
"output_filename": output_path.name,
},
)