Skip to content

Commit 05bd66b

Browse files
name photos
1 parent 10d185c commit 05bd66b

4 files changed

Lines changed: 71 additions & 7 deletions

File tree

core/pioreactor/actions/camera_snapshot.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pioreactor import types as pt
44
from pioreactor import whoami
55
from pioreactor.camera import camera_still_image_path
6+
from pioreactor.camera import camera_storage_name_is_safe
67
from pioreactor.camera import CameraCaptureError
78
from pioreactor.camera import CameraStillMetadata
89
from pioreactor.camera import CameraUnavailableError
@@ -13,20 +14,30 @@
1314
def camera_snapshot(
1415
unit: pt.Unit | None = None,
1516
experiment: pt.Experiment | None = None,
17+
name: str | None = None,
1618
) -> CameraStillMetadata:
1719
"""Capture and store a still for the unit's assigned experiment."""
20+
if name is not None and not camera_storage_name_is_safe(name):
21+
raise ValueError(f"Unsafe camera image name: {name}")
22+
1823
unit = unit or whoami.get_unit_name()
1924
experiment = experiment or whoami.get_assigned_experiment_name(unit)
2025

2126
with managed_lifecycle(unit, experiment, "camera_snapshot"):
22-
return capture_camera_still(unit, experiment=experiment, capture_reason="manual")
27+
return capture_camera_still(
28+
unit,
29+
experiment=experiment,
30+
capture_reason="manual",
31+
image_id=name,
32+
)
2333

2434

2535
@click.command(name="camera_snapshot")
26-
def click_camera_snapshot() -> None:
36+
@click.option("--name", help="Photo name without the .jpg extension.")
37+
def click_camera_snapshot(name: str | None) -> None:
2738
"""Take a camera snapshot for the current experiment."""
2839
try:
29-
metadata = camera_snapshot()
40+
metadata = camera_snapshot(name=name)
3041
except (CameraUnavailableError, CameraCaptureError, ValueError) as error:
3142
raise click.ClickException(str(error)) from error
3243

core/pioreactor/camera.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ def capture_camera_still(
811811
*,
812812
experiment: pt.Experiment | None,
813813
capture_reason: CameraCaptureReason,
814+
image_id: str | None = None,
814815
timeout: float = 20.0,
815816
dot_pioreactor: Path | None = None,
816817
) -> CameraStillMetadata:
@@ -826,6 +827,7 @@ def capture_camera_still(
826827
unit,
827828
experiment=experiment,
828829
capture_reason=capture_reason,
830+
image_id=image_id,
829831
dot_pioreactor=dot_pioreactor,
830832
)
831833

core/tests/test_camera_snapshot.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,17 @@ def test_camera_snapshot_uses_current_unit_and_experiment(monkeypatch: pytest.Mo
2626
)
2727
monkeypatch.setattr(camera_snapshot, "managed_lifecycle", lifecycle)
2828

29-
def capture(unit: str, *, experiment: str | None, capture_reason: str) -> CameraStillMetadata:
29+
def capture(
30+
unit: str,
31+
*,
32+
experiment: str | None,
33+
capture_reason: str,
34+
image_id: str | None,
35+
) -> CameraStillMetadata:
3036
captured["unit"] = unit
3137
captured["experiment"] = experiment
3238
captured["capture_reason"] = capture_reason
39+
captured["image_id"] = image_id
3340
return metadata
3441

3542
monkeypatch.setattr(camera_snapshot, "capture_camera_still", capture)
@@ -39,5 +46,42 @@ def capture(unit: str, *, experiment: str | None, capture_reason: str) -> Camera
3946
"unit": "unit-a",
4047
"experiment": "experiment-a",
4148
"capture_reason": "manual",
49+
"image_id": None,
4250
}
4351
lifecycle.assert_called_once_with("unit-a", "experiment-a", "camera_snapshot")
52+
53+
54+
def test_camera_snapshot_passes_name_as_image_id(monkeypatch: pytest.MonkeyPatch) -> None:
55+
metadata = CameraStillMetadata(
56+
experiment="experiment-a",
57+
captured_at=datetime(2026, 7, 13, 12, 0, tzinfo=UTC),
58+
image_id="inoculation",
59+
)
60+
captured: dict[str, str | None] = {}
61+
62+
monkeypatch.setattr(camera_snapshot, "managed_lifecycle", lambda *_args: nullcontext())
63+
64+
def capture(
65+
unit: str,
66+
*,
67+
experiment: str | None,
68+
capture_reason: str,
69+
image_id: str | None,
70+
) -> CameraStillMetadata:
71+
captured["image_id"] = image_id
72+
return metadata
73+
74+
monkeypatch.setattr(camera_snapshot, "capture_camera_still", capture)
75+
76+
assert camera_snapshot.camera_snapshot("unit-a", "experiment-a", name="inoculation") == metadata
77+
assert captured["image_id"] == "inoculation"
78+
79+
80+
def test_camera_snapshot_rejects_unsafe_name_before_capture(monkeypatch: pytest.MonkeyPatch) -> None:
81+
capture = MagicMock()
82+
monkeypatch.setattr(camera_snapshot, "capture_camera_still", capture)
83+
84+
with pytest.raises(ValueError, match="Unsafe camera image name"):
85+
camera_snapshot.camera_snapshot("unit-a", "experiment-a", name="../inoculation")
86+
87+
capture.assert_not_called()

core/tests/test_cli.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,11 +1142,18 @@ def test_camera_snapshot(monkeypatch: pytest.MonkeyPatch) -> None:
11421142
captured_at=datetime(2026, 7, 13, 12, 0, tzinfo=UTC),
11431143
image_id="image-a",
11441144
)
1145-
monkeypatch.setattr(camera_snapshot, "camera_snapshot", lambda: metadata)
1145+
supplied_names: list[str | None] = []
11461146

1147-
result = CliRunner().invoke(pio, ["run", "camera_snapshot"])
1147+
def capture(name: str | None) -> CameraStillMetadata:
1148+
supplied_names.append(name)
1149+
return metadata
1150+
1151+
monkeypatch.setattr(camera_snapshot, "camera_snapshot", capture)
1152+
1153+
result = CliRunner().invoke(pio, ["run", "camera_snapshot", "--name", "inoculation"])
11481154

11491155
assert result.exit_code == 0
1156+
assert supplied_names == ["inoculation"]
11501157
expected_path = camera_snapshot.camera_still_image_path(metadata)
11511158
assert result.output == f"Captured camera snapshot image-a, at {expected_path}\n"
11521159

@@ -1155,7 +1162,7 @@ def test_camera_snapshot_reports_capture_error(monkeypatch: pytest.MonkeyPatch)
11551162
from pioreactor.actions import camera_snapshot
11561163
from pioreactor.camera import CameraUnavailableError
11571164

1158-
def raise_camera_unavailable() -> None:
1165+
def raise_camera_unavailable(name: str | None) -> None:
11591166
raise CameraUnavailableError("No camera detected.")
11601167

11611168
monkeypatch.setattr(camera_snapshot, "camera_snapshot", raise_camera_unavailable)

0 commit comments

Comments
 (0)