Added range API, fixed offline processing and DLPak saving
Saving a buffer can now speicfy the buffer used. Offline processing uses a per signal binary search to hugely reduce processing time that was mostly spent on an O(n) linear search API now has range fetching API's for graphs etc
This commit is contained in:
6 files changed
+388
-160
No files matched your search
@@ -6,8 +6,11 @@ from __future__ import annotations
|
||||
from copy import copy
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Literal, Self
|
||||
from uuid import UUID
|
||||
from zipfile import ZIP_DEFLATED, ZipFile
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -23,6 +26,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RecordManifest(BaseModel):
|
||||
format_version: Literal[1] = 1
|
||||
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
signals: list[JsonSignalDescriptor] = Field(default_factory=list)
|
||||
comments: str = ""
|
||||
@@ -108,6 +112,36 @@ class DLPak:
|
||||
def get_all_signal_descriptors(self) -> list[SignalDescriptor]:
|
||||
return copy([signal.to_pure() for signal in self._manifest.signals])
|
||||
|
||||
def get_data_range(
|
||||
self,
|
||||
signal_ids: list[UUID] | None = None,
|
||||
start_ns: int | None = None,
|
||||
end_ns: int | None = None,
|
||||
) -> dict[UUID, list[tuple[int, float]]]:
|
||||
if self._data is None:
|
||||
raise DLPakError("No data is available", code="no_data")
|
||||
return self._data.get_data_range(signal_ids, start_ns, end_ns)
|
||||
|
||||
def get_time_range(self) -> tuple[int, int] | None:
|
||||
if self._data is None:
|
||||
raise DLPakError("No data is available", code="no_data")
|
||||
return self._data.get_time_range()
|
||||
|
||||
@staticmethod
|
||||
def _validate_contents(data: ValueBuffer, manifest: RecordManifest) -> None:
|
||||
signal_ids = [signal.id for signal in manifest.signals]
|
||||
if len(signal_ids) != len(set(signal_ids)):
|
||||
raise DLPakError(
|
||||
"The manifest contains duplicate signal IDs", code="invalid_manifest"
|
||||
)
|
||||
missing = set(data.get_signal_ids()) - set(signal_ids)
|
||||
if missing:
|
||||
raise DLPakError(
|
||||
"The manifest is missing descriptors for signals: "
|
||||
+ ", ".join(sorted(str(uuid) for uuid in missing)),
|
||||
code="invalid_manifest",
|
||||
)
|
||||
|
||||
def read(self, file: str | Path) -> Self:
|
||||
input_path = Path(file)
|
||||
|
||||
@@ -118,6 +152,7 @@ class DLPak:
|
||||
)
|
||||
data = ValueBuffer()
|
||||
data.import_csv(archive.read("data.csv").decode("utf-8"))
|
||||
self._validate_contents(data, manifest)
|
||||
except Exception as error:
|
||||
log.exception(
|
||||
"Failed to read DLPak archive from %s",
|
||||
@@ -174,12 +209,26 @@ class DLPak:
|
||||
|
||||
output_path = output_dir / file_path
|
||||
|
||||
temporary_path: Path | None = None
|
||||
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("data.csv", self._data.export_csv())
|
||||
self._validate_contents(self._data, self._manifest)
|
||||
with NamedTemporaryFile(
|
||||
dir=output_path.parent,
|
||||
prefix=f".{output_path.name}.",
|
||||
suffix=".tmp",
|
||||
delete=False,
|
||||
) as temporary_file:
|
||||
temporary_path = Path(temporary_file.name)
|
||||
with ZipFile(
|
||||
temporary_file, mode="w", compression=ZIP_DEFLATED
|
||||
) as archive:
|
||||
archive.writestr(
|
||||
"manifest.json", self._manifest.model_dump_json(indent=2)
|
||||
)
|
||||
archive.writestr("data.csv", self._data.export_csv())
|
||||
temporary_file.flush()
|
||||
os.fsync(temporary_file.fileno())
|
||||
temporary_path.replace(output_path)
|
||||
except Exception as error:
|
||||
log.exception(
|
||||
"Failed to write DLPak archive to %s",
|
||||
@@ -194,6 +243,9 @@ class DLPak:
|
||||
f"Could not write DLPak archive {output_path}: {error}",
|
||||
code="write_failed",
|
||||
) from error
|
||||
finally:
|
||||
if temporary_path is not None:
|
||||
temporary_path.unlink(missing_ok=True)
|
||||
|
||||
log.info(
|
||||
"Wrote DLPak archive to %s",
|
||||
|
||||
Reference in new issue
Block a user