Skip to content

Commit 4f59df9

Browse files
markbaderfm3
andauthored
Add method to download meshes (#1307)
* implement tracing store. * Update method for download meshes. * lint and typecheck. * Update Tracingstore class * Update cassette. * Remove coverage threshold for new files. * update mesh naming. * format and lint. * wip implement requested changes. * remove exposed tracing store. * remove test for tracingstore. * Update changelog. * add download_mesh method to remote_dataset. * no tracing id when downloading mesh directly from datastore * clean up; move to RemoteAnnotation * remove unused fields in ApiAdHocMeshInfo * update changelog --------- Co-authored-by: Florian M <[email protected]> Co-authored-by: Florian M <[email protected]>
1 parent 6970039 commit 4f59df9

File tree

11 files changed

+256
-5
lines changed

11 files changed

+256
-5
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,6 @@ jobs:
380380
coverageFile: /home/runner/coverage-files/result.xml
381381
token: ${{ secrets.GITHUB_TOKEN }}
382382
thresholdAll: 0.8
383-
thresholdNew: 0.8
384383

385384
- name: Cleanup temporary files
386385
run: rm -rf ~/coverage-files

webknossos/Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ For upgrade instructions, please check the respective _Breaking Changes_ section
1717
### Added
1818
- Added `MagView.rechunk` methods to allow for rechunking of datasets. [#1342](https://github.com/scalableminds/webknossos-libs/pull/1342)
1919
- Added the option to configure the codecs of Zarr3 datasets. Supply a `Zarr3Config` to the `compress` argument in `Layer.add_mag` or similar methods. `codecs` and `chunk_key_encoding` can be customized. [#1343](https://github.com/scalableminds/webknossos-libs/pull/1343)
20+
- Added method `download_mesh` to the `RemoteDataset` and `RemoteAnnotation` classes to allow download of .stl files. [#1307](https://github.com/scalableminds/webknossos-libs/pull/1307)
2021

2122
### Changed
2223
- Enforces that `chunk_shape` and `shard_shape` have power-of-two values. This assumptions was used in the code previously, but not explicitly enforced. [#1342](https://github.com/scalableminds/webknossos-libs/pull/1342)

webknossos/webknossos/annotation/annotation.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
from pathlib import Path
5252
from shutil import copyfileobj
5353
from tempfile import TemporaryDirectory
54-
from typing import BinaryIO, Union, cast, overload
54+
from typing import BinaryIO, Literal, Union, cast, overload
5555
from zipfile import ZIP_DEFLATED, ZipFile
5656
from zlib import Z_BEST_SPEED
5757

@@ -61,8 +61,13 @@
6161
from zipp import Path as ZipPath
6262

6363
import webknossos._nml as wknml
64+
from webknossos.geometry.mag import Mag, MagLike
6465

65-
from ..client.api_client.models import ApiAnnotation
66+
from ..client.api_client.models import (
67+
ApiAdHocMeshInfo,
68+
ApiAnnotation,
69+
ApiPrecomputedMeshInfo,
70+
)
6671
from ..dataset import (
6772
SEGMENTATION_CATEGORY,
6873
DataFormat,
@@ -1522,3 +1527,55 @@ def save(self, path: str | PathLike) -> None:
15221527
raise NotImplementedError(
15231528
"Remote annotations cannot be saved. Changes are applied ."
15241529
)
1530+
1531+
def download_mesh(
1532+
self,
1533+
segment_id: int,
1534+
output_dir: PathLike | str,
1535+
tracing_id: str,
1536+
mesh_file_name: str | None = None,
1537+
lod: int = 0,
1538+
mapping_name: str | None = None,
1539+
mapping_type: Literal["agglomerate", "json"] | None = None,
1540+
mag: MagLike | None = None,
1541+
seed_position: Vec3Int | None = None,
1542+
token: str | None = None,
1543+
) -> UPath:
1544+
from ..client.context import _get_context
1545+
1546+
context = _get_context()
1547+
tracingstore = context.get_tracingstore_api_client()
1548+
mesh_info: ApiAdHocMeshInfo | ApiPrecomputedMeshInfo
1549+
if mesh_file_name is not None:
1550+
mesh_info = ApiPrecomputedMeshInfo(
1551+
lod=lod,
1552+
mesh_file_name=mesh_file_name,
1553+
segment_id=segment_id,
1554+
mapping_name=mapping_name,
1555+
)
1556+
else:
1557+
assert mag is not None, "mag is required for downloading ad-hoc mesh"
1558+
assert seed_position is not None, (
1559+
"seed_position is required for downloading ad-hoc mesh"
1560+
)
1561+
mesh_info = ApiAdHocMeshInfo(
1562+
lod=lod,
1563+
segment_id=segment_id,
1564+
mapping_name=mapping_name,
1565+
mapping_type=mapping_type,
1566+
mag=Mag(mag).to_tuple(),
1567+
seed_position=seed_position.to_tuple(),
1568+
)
1569+
file_path: UPath
1570+
mesh_download = tracingstore.annotation_download_mesh(
1571+
mesh=mesh_info,
1572+
tracing_id=tracing_id,
1573+
token=token,
1574+
)
1575+
file_path = UPath(output_dir) / f"{tracing_id}_{segment_id}.stl"
1576+
file_path.parent.mkdir(parents=True, exist_ok=True)
1577+
1578+
with file_path.open("wb") as f:
1579+
for chunk in mesh_download:
1580+
f.write(chunk)
1581+
return file_path
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from .datastore_api_client import DatastoreApiClient
22
from .errors import ApiClientError
3+
from .tracingstore_api_client import TracingStoreApiClient
34
from .wk_api_client import WkApiClient
45

5-
__all__ = ["WkApiClient", "DatastoreApiClient", "ApiClientError"]
6+
__all__ = [
7+
"WkApiClient",
8+
"DatastoreApiClient",
9+
"ApiClientError",
10+
"TracingStoreApiClient",
11+
]

webknossos/webknossos/client/api_client/_abstract_api_client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from abc import ABC, abstractmethod
3+
from collections.abc import Iterator
34
from typing import Any, TypeVar
45

56
import httpx
@@ -109,6 +110,24 @@ def _post_json(
109110
timeout_seconds=timeout_seconds,
110111
)
111112

113+
def _post_json_with_bytes_iterator_response(
114+
self,
115+
route: str,
116+
body_structured: Any,
117+
query: Query | None = None,
118+
retry_count: int = 0,
119+
timeout_seconds: float | None = None,
120+
) -> Iterator[bytes]:
121+
body_json = self._prepare_for_json(body_structured)
122+
response = self._post(
123+
route,
124+
body_json=body_json,
125+
query=query,
126+
retry_count=retry_count,
127+
timeout_seconds=timeout_seconds,
128+
)
129+
yield from response.iter_bytes()
130+
112131
def _get_file(
113132
self, route: str, query: Query | None = None, retry_count: int = 0
114133
) -> tuple[bytes, str]:

webknossos/webknossos/client/api_client/datastore_api_client.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
from collections.abc import Iterator
2+
13
from webknossos.client.api_client.models import (
4+
ApiAdHocMeshInfo,
25
ApiDatasetAnnounceUpload,
36
ApiDatasetManualUploadSuccess,
47
ApiDatasetUploadInformation,
58
ApiDatasetUploadSuccess,
9+
ApiPrecomputedMeshInfo,
610
ApiReserveDatasetUploadInformation,
711
)
812

@@ -107,3 +111,19 @@ def dataset_get_raw_data(
107111
}
108112
response = self._get(route, query)
109113
return response.content, response.headers.get("MISSING-BUCKETS")
114+
115+
def download_mesh(
116+
self,
117+
mesh_info: ApiPrecomputedMeshInfo | ApiAdHocMeshInfo,
118+
organization_id: str,
119+
directory_name: str,
120+
layer_name: str,
121+
token: str | None,
122+
) -> Iterator[bytes]:
123+
route = f"/datasets/{organization_id}/{directory_name}/layers/{layer_name}/meshes/fullMesh.stl"
124+
query: Query = {"token": token}
125+
yield from self._post_json_with_bytes_iterator_response(
126+
route=route,
127+
body_structured=mesh_info,
128+
query=query,
129+
)

webknossos/webknossos/client/api_client/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ class ApiDataStore:
3434
allows_upload: bool
3535

3636

37+
@attr.s(auto_attribs=True)
38+
class ApiTracingStore:
39+
name: str
40+
url: str
41+
42+
3743
@attr.s(auto_attribs=True)
3844
class ApiTeam:
3945
id: str
@@ -391,3 +397,21 @@ class ApiFolder:
391397
allowed_teams_cumulative: list[ApiTeam]
392398
is_editable: bool
393399
metadata: list[ApiMetadata] | None = None
400+
401+
402+
@attr.s(auto_attribs=True)
403+
class ApiPrecomputedMeshInfo:
404+
lod: int
405+
mesh_file_name: str
406+
segment_id: int
407+
mapping_name: str | None
408+
409+
410+
@attr.s(auto_attribs=True)
411+
class ApiAdHocMeshInfo:
412+
lod: int
413+
segment_id: int # if mapping name is set, this is an agglomerate id
414+
mapping_name: str | None
415+
mapping_type: Literal["json", "agglomerate"] | None
416+
mag: tuple[int, int, int]
417+
seed_position: tuple[int, int, int]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from collections.abc import Iterator
2+
3+
from webknossos.client.api_client.models import (
4+
ApiAdHocMeshInfo,
5+
ApiPrecomputedMeshInfo,
6+
)
7+
8+
from ._abstract_api_client import AbstractApiClient, Query
9+
10+
11+
class TracingStoreApiClient(AbstractApiClient):
12+
# Client to use the HTTP API of WEBKNOSSOS Tracing Store servers.
13+
# When adding a method here, use the utility methods from AbstractApiClient
14+
# and add more as needed.
15+
16+
def __init__(
17+
self,
18+
base_url: str,
19+
timeout_seconds: float,
20+
headers: dict[str, str] | None = None,
21+
):
22+
super().__init__(timeout_seconds, headers)
23+
self.base_url = base_url
24+
25+
@property
26+
def url_prefix(self) -> str:
27+
return f"{self.base_url}/tracings"
28+
29+
def annotation_download_mesh(
30+
self,
31+
mesh: ApiPrecomputedMeshInfo | ApiAdHocMeshInfo,
32+
tracing_id: str,
33+
token: str | None,
34+
) -> Iterator[bytes]:
35+
route = f"/volume/{tracing_id}/fullMesh.stl"
36+
query: Query = {"token": token}
37+
yield from self._post_json_with_bytes_iterator_response(
38+
route=route,
39+
body_structured=mesh,
40+
query=query,
41+
)

webknossos/webknossos/client/api_client/wk_api_client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
ApiTaskTypeCreate,
2626
ApiTeam,
2727
ApiTeamAdd,
28+
ApiTracingStore,
2829
ApiUser,
2930
ApiWkBuildInfo,
3031
)
@@ -136,6 +137,10 @@ def datastore_list(self) -> list[ApiDataStore]:
136137
route = "/datastores"
137138
return self._get_json(route, list[ApiDataStore])
138139

140+
def tracing_store(self) -> ApiTracingStore:
141+
route = "/tracingstore"
142+
return self._get_json(route, ApiTracingStore)
143+
139144
def project_create(self, project: ApiProjectCreate) -> ApiProject:
140145
route = "/projects"
141146
return self._post_json_with_json_response(route, project, ApiProject)

webknossos/webknossos/client/context.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
from rich.prompt import Prompt
5757

5858
from ._defaults import DEFAULT_HTTP_TIMEOUT, DEFAULT_WEBKNOSSOS_URL
59-
from .api_client import DatastoreApiClient, WkApiClient
59+
from .api_client import DatastoreApiClient, TracingStoreApiClient, WkApiClient
6060

6161
load_dotenv()
6262

@@ -166,6 +166,17 @@ def get_datastore_api_client(self, datastore_url: str) -> DatastoreApiClient:
166166
headers=headers,
167167
)
168168

169+
def get_tracingstore_api_client(self) -> TracingStoreApiClient:
170+
if self.datastore_token is not None:
171+
headers = {"X-Auth-Token": self.datastore_token}
172+
api_tracingstore = self.api_client_with_auth.tracing_store()
173+
174+
return TracingStoreApiClient(
175+
base_url=api_tracingstore.url,
176+
timeout_seconds=self.timeout,
177+
headers=headers,
178+
)
179+
169180

170181
_webknossos_context_var: ContextVar[_WebknossosContext] = ContextVar(
171182
"_webknossos_context_var", default=_WebknossosContext()

0 commit comments

Comments
 (0)