# Copyright (C) 2026 Hector van der Aa # Copyright (C) 2026 Association Exergie # 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, }, )