Skip to content

Commit 35b039d

Browse files
valentin-pinkauvalentin-pinkau
andauthored
enforce read_only on remote datasets (#1383)
* enforce read_only on remote datasets * reset state to server state when change was tried. * add tests for changing properties on remote datasets. fix test.py * format * add changelog --------- Co-authored-by: valentin-pinkau <[email protected]>
1 parent 4712a0a commit 35b039d

File tree

8 files changed

+500
-13
lines changed

8 files changed

+500
-13
lines changed

webknossos/Changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ For upgrade instructions, please check the respective _Breaking Changes_ section
4040
- Refactored the architecture, by introducing RemoteLayers, RemoteSegmentationLayers and their abstract base classes. [#1371](https://github.com/scalableminds/webknossos-libs/pull/1371])
4141
- Updated the api version of the webknossos-api to 12. [#1371](https://github.com/scalableminds/webknossos-libs/pull/1371])
4242
- Allowing RemoteDataset to align mags, when down- or upsampling [#1382](https://github.com/scalableminds/webknossos-libs/pull/1382)
43+
- RemoteDatasets that use zarr streaming are no longer read-only. [#1383](https://github.com/scalableminds/webknossos-libs/pull/1383)
4344

4445
### Fixed
45-
46+
- Fixed test.py to parse the command line arguments correctly. [#1383](https://github.com/scalableminds/webknossos-libs/pull/1383)
4647

4748
## [2.5.0](https://github.com/scalableminds/webknossos-libs/releases/tag/v2.5.0) - 2025-10-06
4849
[Commits](https://github.com/scalableminds/webknossos-libs/compare/v2.4.12...v2.5.0)

webknossos/test.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,17 @@ def local_test_wk() -> Iterator[None]:
106106

107107
# Fetch current version of webknossos.org this can be replaced with a fixed version for testing
108108
wk_version = requests.get(
109-
"https://webknossos.org/api/v{WK_API_VERSION}/buildinfo"
109+
f"https://webknossos.org/api/v{WK_API_VERSION}/buildinfo"
110110
).json()["webknossos"]["version"]
111111
wk_docker_tag = f"master__${wk_version}"
112112
os.environ["DOCKER_TAG"] = wk_docker_tag
113113
wk_docker_dir = Path("tests")
114+
tear_down_wk = False
114115

115116
try:
116117
if not requests.get(f"{WK_URL}/api/v{WK_API_VERSION}/health").ok:
117118
start_wk_via_docker()
119+
tear_down_wk = True
118120
else:
119121
print(
120122
f"Using the already running webknossos at {WK_URL}. Make sure l4_sample exists and is set to public first!",
@@ -142,7 +144,8 @@ def local_test_wk() -> Iterator[None]:
142144
)
143145
yield
144146
finally:
145-
subprocess.check_call(["docker", "compose", "down"], cwd=wk_docker_dir)
147+
if tear_down_wk:
148+
subprocess.check_call(["docker", "compose", "down"], cwd=wk_docker_dir)
146149

147150

148151
@contextmanager
@@ -259,7 +262,7 @@ def main(snapshot_command: Literal["refresh", "add"] | None, args: list[str]) ->
259262
snapshot_command = None
260263
args = sys.argv[1:]
261264
if len(args) > 0 and args[0] in ["--refresh-snapshots", "--add-snapshots"]:
262-
snapshot_command = args[0][3:-10]
265+
snapshot_command = args[0][2:-10]
263266
args = args[1:]
264267

265268
main(snapshot_command, args)

webknossos/tests/cassettes/test_dataset_add_remote_mag_and_layer/test_changing_properties_on_remote_dataset.yml

Lines changed: 436 additions & 0 deletions
Large diffs are not rendered by default.

webknossos/tests/dataset/test_dataset_add_remote_mag_and_layer.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
import pytest
77
from upath import UPath
88

9-
from webknossos import COLOR_CATEGORY, Dataset, RemoteDataset
9+
from webknossos import (
10+
COLOR_CATEGORY,
11+
Dataset,
12+
LayerViewConfiguration,
13+
RemoteDataset,
14+
)
1015
from webknossos.geometry import BoundingBox
1116
from webknossos.utils import is_remote_path
1217

@@ -158,3 +163,39 @@ def test_add_mag_ref_from_local_path(tmp_upath: UPath) -> None:
158163
assert layer2_mag1._properties.path == str(
159164
(tmp_upath / "origin" / "color" / "1").resolve()
160165
)
166+
167+
168+
def test_changing_properties_on_remote_dataset() -> None:
169+
remote_dataset = RemoteDataset.open(dataset_id="59e9cfbdba632ac2ab8b23b5")
170+
remote_dataset.description = "This is a test description"
171+
assert remote_dataset.description == "This is a test description"
172+
largest_segment_id_before_change_attempt = remote_dataset.get_segmentation_layer(
173+
"segmentation"
174+
).largest_segment_id
175+
with pytest.raises(RuntimeError):
176+
remote_dataset.get_segmentation_layer("segmentation").largest_segment_id = 10
177+
assert (
178+
remote_dataset.get_segmentation_layer("segmentation").largest_segment_id
179+
== largest_segment_id_before_change_attempt
180+
)
181+
default_view_configuration_before_change_attempt = remote_dataset.get_layer(
182+
"color"
183+
).default_view_configuration
184+
with pytest.raises(RuntimeError):
185+
remote_dataset.get_layer(
186+
"color"
187+
).default_view_configuration = LayerViewConfiguration(alpha=0.3)
188+
assert (
189+
remote_dataset.get_layer("color").default_view_configuration
190+
== default_view_configuration_before_change_attempt
191+
)
192+
193+
194+
def test_changing_properties_on_read_only_remote_dataset() -> None:
195+
remote_dataset = RemoteDataset.open(
196+
dataset_id="59e9cfbdba632ac2ab8b23b5", read_only=True
197+
)
198+
description_before_change_attempt = remote_dataset.description
199+
with pytest.raises(RuntimeError):
200+
remote_dataset.description = "This is a test description"
201+
assert remote_dataset.description == description_before_change_attempt

webknossos/tests/dataset/test_dataset_download_upload_remote.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ def test_remote_dataset(tmp_upath: UPath) -> None:
131131
sample_dataset.get_color_layers()[0].get_finest_mag().read(),
132132
)
133133

134-
assert remote_ds.read_only
135-
assert remote_ds.get_color_layers()[0].read_only
134+
assert not remote_ds.read_only
135+
assert not remote_ds.get_color_layers()[0].read_only
136136
assert remote_ds.get_color_layers()[0].get_finest_mag().read_only
137137

138138
assert remote_ds.name == "test_remote_metadata"

webknossos/webknossos/annotation/annotation.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -982,7 +982,6 @@ def get_remote_annotation_dataset(self) -> RemoteDataset:
982982
dataset_id=self.dataset_id,
983983
annotation_id_or_url=self.annotation_id,
984984
use_zarr_streaming=True,
985-
read_only=True,
986985
)
987986

988987
def get_remote_base_dataset(

webknossos/webknossos/dataset/dataset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,7 @@ def upload(
701701
self, new_dataset_name, converted_layers_to_link, jobs
702702
)
703703

704-
return RemoteDataset.open(dataset_id=new_dataset_id, read_only=True)
704+
return RemoteDataset.open(dataset_id=new_dataset_id)
705705

706706
def _copy_or_symlink_dataset_to_paths(
707707
self, data_source: DatasetProperties, symlink_data_instead_of_copy: bool

webknossos/webknossos/dataset/remote_dataset.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class RemoteDataset(AbstractDataset[RemoteLayer, RemoteSegmentationLayer]):
4949
This class is returned from `RemoteDataset.open()` and provides read-only access to
5050
image data streamed from the webknossos server. It uses the same interface as `Dataset`
5151
but additionally allows metadata manipulation through properties.
52+
In case of zarr streaming, an even smaller subset of metadata manipulation is possible.
5253
5354
Properties:
5455
metadata: Dataset metadata as key-value pairs
@@ -112,7 +113,6 @@ def __init__(
112113
self.zarr_streaming_path = zarr_streaming_path
113114
self._use_zarr_streaming = zarr_streaming_path is not None
114115
if self._use_zarr_streaming:
115-
assert read_only, "zarr streaming is only supported in read-only mode"
116116
dataset_properties = self._load_dataset_properties()
117117

118118
assert dataset_properties is not None
@@ -187,8 +187,6 @@ def open(
187187
url_prefix = wk_context.get_datastore_api_client(datastore_url).url_prefix
188188

189189
if use_zarr_streaming:
190-
if not read_only:
191-
logger.warning("zarr streaming is supported in read-only mode only")
192190
if annotation_id is not None:
193191
zarr_path = UPath(
194192
f"{url_prefix}/annotations/zarr/{annotation_id}/",
@@ -207,7 +205,7 @@ def open(
207205
dataset_id,
208206
annotation_id,
209207
context_manager,
210-
read_only=True,
208+
read_only=read_only,
211209
)
212210
else:
213211
if isinstance(api_dataset_info.data_source, ApiUnusableDataSource):
@@ -246,9 +244,16 @@ def _save_dataset_properties_impl(self) -> None:
246244
Exports the current dataset properties to the server.
247245
Note that some edits will not be accepted by the server.
248246
The client-side RemoteDataset is reinitialized to the new server state.
247+
Does not work with zarr streaming, as the remote datasource-properties.json is not writable.
249248
"""
250249
from ..client.context import _get_api_client
251250

251+
if self._use_zarr_streaming:
252+
# reset the dataset properties to the server state
253+
data_source = self._load_dataset_properties()
254+
self._init_from_properties(data_source, read_only=self.read_only)
255+
raise RuntimeError("zarr streaming does not support updating this property")
256+
252257
with self._context:
253258
client = _get_api_client()
254259
client.dataset_update(
@@ -325,6 +330,7 @@ def _update_dataset_info(
325330
tags: list[str] = _UNSET,
326331
metadata: list[ApiMetadata] | None = _UNSET,
327332
) -> None:
333+
self._ensure_writable()
328334
from ..client.context import _get_api_client
329335

330336
# Atm, the wk backend needs to get previous parameters passed
@@ -578,6 +584,7 @@ def allowed_teams(self) -> tuple["Team", ...]:
578584
@allowed_teams.setter
579585
def allowed_teams(self, allowed_teams: Sequence[Union[str, "Team"]]) -> None:
580586
"""Assign the teams that are allowed to access the dataset. Specify the teams like this `[Team.get_by_name("Lab_A"), ...]`."""
587+
self._ensure_writable()
581588
from ..administration.team import Team
582589
from ..client.context import _get_api_client
583590

0 commit comments

Comments
 (0)