From 793001fc3a0060c192f0a4a3fa05ce5954100ad6 Mon Sep 17 00:00:00 2001 From: Constantinos Eleftheriou Date: Wed, 23 Sep 2026 15:48:59 +0100 Subject: [PATCH 1/4] add function to read session start time --- src/mesoscopy/io.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/mesoscopy/io.py b/src/mesoscopy/io.py index 5d953a4..6074b46 100644 --- a/src/mesoscopy/io.py +++ b/src/mesoscopy/io.py @@ -248,6 +248,30 @@ def write_timestamps_aligned(path: str, timestamps: npt.ArrayLike, attrs: dict[s return path +def read_start_time(path: str) -> str: + """Read a recording's start time, as stored in the file. + + For HDF5 files this is the first `/timestamps` entry; for NWB files it is `/session_start_time`. + + Args: + path (str): Path to the HDF5 or NWB file. + + Returns: + str: The start time. + + Raises: + ValueError: If the file has no start time. + """ + key = "/session_start_time" if path.endswith(".nwb") else "/timestamps" + with h5py.File(path, "r") as f: + if key not in f or f[key].size == 0: + msg = f"{path} has no {key} to read a start time from." + raise ValueError(msg) + dataset = f[key] + value = dataset[()] if dataset.ndim == 0 else dataset[0] + return value.decode("utf-8") if isinstance(value, bytes) else str(value) + + def read_points(path: str) -> dict[str, tuple[float, float]]: """Read a landmark points file. From 91b96325b9e77ea4efe147e74259944afe8656f7 Mon Sep 17 00:00:00 2001 From: Constantinos Eleftheriou Date: Wed, 23 Sep 2026 15:49:16 +0100 Subject: [PATCH 2/4] add top-level CLI command to print session start time --- src/mesoscopy/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/mesoscopy/__init__.py b/src/mesoscopy/__init__.py index 5c93c65..14b457a 100644 --- a/src/mesoscopy/__init__.py +++ b/src/mesoscopy/__init__.py @@ -42,6 +42,7 @@ "register": ("mesoscopy.register:register_cmd", "Register recordings to an anatomical template."), "report": ("mesoscopy.report:report_cmd", "Generate an HTML report for a mesoscopy processing step."), "sample": ("mesoscopy:sample", "Sample an image frame from an HDF5 file and export it as a PNG."), + "start-time": ("mesoscopy:start_time", "Print a recording's start time."), } @@ -146,3 +147,18 @@ def sample(path, out_dir, index, crop=0, vmin=0, vmax=255, key="frames"): else: plt.imsave(outpath, d[index], vmin=vmin, vmax=vmax, cmap="jet") click.echo(f"Saved sample at {outpath}") + + +@click.command("start-time") +@click.argument("path", type=click.Path(exists=True, dir_okay=False)) +def start_time(path: str) -> None: + """Print a recording's start time. + + This is the first /timestamps entry of an HDF5 file, or the session_start_time of an NWB file, as stored. + """ # noqa: DOC501 + from mesoscopy import io + + try: + click.echo(io.read_start_time(path)) + except ValueError as err: + raise click.ClickException(str(err)) from err From bea3b9a294fdcfccc59e12fd4825102cfc485336 Mon Sep 17 00:00:00 2001 From: Constantinos Eleftheriou Date: Wed, 23 Sep 2026 15:49:32 +0100 Subject: [PATCH 3/4] add tests for reading session start time --- tests/test_cli.py | 30 ++++++++++++++++++++++++++++++ tests/test_io.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 76c3607..c707aea 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -25,6 +25,7 @@ import sys import click +import h5py import pytest from click.testing import CliRunner @@ -88,3 +89,32 @@ def test_no_eager_subcommands(): def test_unknown_subcommand_is_none(): assert mesoscopy.cli.get_command(click.Context(mesoscopy.cli), "nope") is None + + +def test_start_time_prints_first_timestamp(tmp_path): + path = str(tmp_path / "rec.h5") + with h5py.File(path, "w") as f: + f.create_dataset("/timestamps", data=[b"2025-05-20T14:42:39.6422784", b"2025-05-20T14:42:39.6622784"]) + + result = CliRunner().invoke(mesoscopy.cli, ["start-time", path]) + + assert result.exit_code == 0 + assert result.output == "2025-05-20T14:42:39.6422784\n" + + +def test_start_time_nwb(nwbfile): + result = CliRunner().invoke(mesoscopy.cli, ["start-time", nwbfile]) + + assert result.exit_code == 0 + assert result.output.startswith("2024-01-01T14:00:00") + + +def test_start_time_missing_timestamps_fails(tmp_path): + path = str(tmp_path / "rec.h5") + with h5py.File(path, "w") as f: + f.create_dataset("/F", data=[0.0]) + + result = CliRunner().invoke(mesoscopy.cli, ["start-time", path]) + + assert result.exit_code == 1 + assert "no /timestamps" in result.output diff --git a/tests/test_io.py b/tests/test_io.py index fab1533..c76e2b4 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -3,6 +3,7 @@ from uuid import uuid4 import dask.array as da +import h5py import imageio.v2 as iio import numpy as np import pytest @@ -26,6 +27,43 @@ def test_h5_write(tmp_path): assert io.read_h5(path)["dataset2"][0][0] == 1 +def test_read_start_time_raw_h5(raw_h5): + with h5py.File(raw_h5, "r") as f: + expected = f["/timestamps"][0].decode("utf-8") + + assert io.read_start_time(raw_h5) == expected + + +def test_read_start_time_keeps_stored_string(tmp_path): + path = str(tmp_path / "rec_preprocessed.h5") + with h5py.File(path, "w") as f: + f.create_dataset("/timestamps", data=[b"2025-05-20T14:42:39.64227", b"2025-05-20T14:42:39.68227"], dtype="S25") + + assert io.read_start_time(path) == "2025-05-20T14:42:39.64227" + + +def test_read_start_time_nwb(nwbfile): + assert io.read_start_time(nwbfile).startswith("2024-01-01T14:00:00") + + +def test_read_start_time_missing_timestamps_raises(tmp_path): + path = str(tmp_path / "rec.h5") + with h5py.File(path, "w") as f: + f.create_dataset("/F", data=np.zeros((2, 2, 2))) + + with pytest.raises(ValueError, match="/timestamps"): + io.read_start_time(path) + + +def test_read_start_time_empty_timestamps_raises(tmp_path): + path = str(tmp_path / "rec.h5") + with h5py.File(path, "w") as f: + f.create_dataset("/timestamps", shape=(0,), dtype="S25") + + with pytest.raises(ValueError, match="/timestamps"): + io.read_start_time(path) + + def test_read_nwb(nwbfile): _ = io.read_nwb(nwbfile) From 7e184434897513ceea192bf39b8537bf4d828ca9 Mon Sep 17 00:00:00 2001 From: Constantinos Eleftheriou Date: Wed, 23 Sep 2026 15:49:38 +0100 Subject: [PATCH 4/4] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24786d1..fbc8e60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Versions follow [Semantic Versioning](https://semver.org) (`.._report.html`, an interactive peri-event report: per-region traces as mean ± 95% CI or individual trials, filtered by `sdt_type` and optionally split by trial type, a clickable Allen CCF top view for choosing the region, and an all-regions grid. When the `*_metrics.csv` and `*_metrics-session.csv` from `process metrics` are stored together, the report also marks the onset, peak and offset of the mean trace with the per-trial mean ± SD or median and IQR of each time alongside (per-trial markers in the individual-trials view), plots per-trial metrics for the selected region, colours the atlas by a session metric and lists the session table. `-t/--trials` supplies `sdt_type` for peri-event files written without it ([#162](https://github.com/DuguidLab/mesoscopy/issues/162)). - [Reports](https://docs.mesoscopy.org/how-to/reports/) how-to page covering the preprocessing, registration and peri-event reports ([#162](https://github.com/DuguidLab/mesoscopy/issues/162)). - `report.perievent` module with `report_payload`, `perievent_cube`, `trial_types`, `atlas_outlines`, `metrics_paths`, `pack_float32` and `unpack_float32` ([#162](https://github.com/DuguidLab/mesoscopy/issues/162)). +- `start-time` command and `io.read_start_time`, printing a recording's start time as stored: the first `/timestamps` entry of an HDF5 file, or `session_start_time` of an NWB file ([#170](https://github.com/DuguidLab/mesoscopy/issues/170)). ## [0.13.1] - 2026-09-18