Skip to content

Commit f874eae

Browse files
add webknossos-client with examples (#452)
* add v1 of user facing rest client with examples * only allow binary mode with zipfile * Undo global_offset change when setting bounding_box on layer, fix in example * 🙈 * fix roundtrip for int zoomLevels * Merge remote-tracking branch 'origin/master' into user-facing-rest-api * Update webknossos/tests/conftest.py Co-authored-by: Philipp Otto <[email protected]> * undo typecheck change from merge * Merge branch 'master' into user-facing-rest-api * apply feedback part 1 * Merge remote-tracking branch 'origin/master' into user-facing-rest-api * rm unused imports * Merge remote-tracking branch 'origin/master' into user-facing-rest-api * apply PR feedback part 2 & speed up learned_segmenter test * fix chunk for single list entry * remove unset handling in generated client responses * PR feedback part 3 * formatting * formatting * formatting * add PR feedback & lint everything * add changelog entries * apply PR feedback feedback :-) * Merge remote-tracking branch 'origin/master' into user-facing-rest-api * remove unused import * remove more unused imports
1 parent 95f0078 commit f874eae

File tree

77 files changed

+134530
-928
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+134530
-928
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,19 @@ jobs:
4545
poetry install
4646
4747
- name: Check formatting
48-
run: poetry run ./format.sh check
48+
run: ./format.sh check
4949

5050
- name: Lint code
51-
run: poetry run ./lint.sh
51+
run: ./lint.sh
5252

5353
- name: Check typing
5454
run: |
55-
poetry run ./typecheck.sh
55+
./typecheck.sh
5656
5757
- name: Python tests
5858
env:
5959
WK_TOKEN: ${{ secrets.WK_TOKEN }}
60-
run: poetry run pytest tests
60+
run: ./test.sh
6161

6262
- name: Check if git is dirty
6363
run: |

webknossos/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
tests/.webknossos-server
2+
13
.idea
24
*.iml
35

webknossos/Changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ For upgrade instructions, please check the respective *Breaking Changes* section
1111
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.18...HEAD)
1212

1313
### Breaking Changes
14+
- `BoundingBox.chunk()`'s 2nd parameter `chunk_border_alignments` now does not accept a list with a single `int` anymore. [#437](https://github.com/scalableminds/webknossos-libs/pull/452)
15+
1416
### Added
17+
- Added a new Annotation class which includes skeletons as well as volume-annotations. [#437](https://github.com/scalableminds/webknossos-libs/pull/452)
18+
- Added dataset down- and upload as well as annotation download, see the examples `learned_segmenter.py` and `upload_image_data.py`. [#437](https://github.com/scalableminds/webknossos-libs/pull/452)
19+
1520
### Changed
1621
### Fixed
1722

webknossos/__generate_client.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime
44
from pathlib import Path
55
from tempfile import NamedTemporaryFile
6-
from typing import Dict, Iterable, Tuple
6+
from typing import Any, Dict, Iterable, Tuple
77

88
import httpx
99
from inducoapi import build_openapi
@@ -13,9 +13,8 @@
1313
Project,
1414
_get_project_for_url_or_path,
1515
)
16-
from openapi_python_client.cli import handle_errors
1716

18-
from webknossos.client import _get_generated_client
17+
from webknossos.client.context import get_generated_client
1918
from webknossos.utils import snake_to_camel_case
2019

2120
SCHEMA_URL = "https://converter.swagger.io/api/convert?url=https%3A%2F%2Fwebknossos.org%2Fswagger.json"
@@ -39,9 +38,10 @@ def generate_client(openapi_schema: Dict) -> None:
3938
meta=MetaType.POETRY,
4039
config=generator_config,
4140
)
42-
assert isinstance(generator_project, Project)
43-
errors = generator_project.update()
44-
# handle_errors(errors) # prints warnings
41+
assert isinstance(generator_project, Project), generator_project.detail
42+
_errors = generator_project.update() # pylint: disable=no-member
43+
# from openapi_python_client.cli import handle_errors
44+
# handle_errors(_errors) # prints warnings
4545

4646

4747
def add_api_prefix_for_non_data_paths(openapi_schema: Dict) -> None:
@@ -68,7 +68,7 @@ def iterate_request_ids_with_responses() -> Iterable[Tuple[str, bytes]]:
6868

6969
d = datetime.utcnow()
7070
unixtime = calendar.timegm(d.utctimetuple())
71-
client = _get_generated_client(enforce_token=True)
71+
client = get_generated_client(enforce_auth=True)
7272

7373
annotation_info_response = annotation_info.sync_detailed(
7474
typ="Explorational",
@@ -91,9 +91,30 @@ def iterate_request_ids_with_responses() -> Iterable[Tuple[str, bytes]]:
9191
api_endpoint_name = api_endpoint.__name__.split(".")[-1]
9292
api_endpoint_name = snake_to_camel_case(api_endpoint_name)
9393

94-
response = api_endpoint.sync_detailed(client=client)
95-
assert response.status_code == 200
96-
yield api_endpoint_name, response.content
94+
api_endpoint_response = api_endpoint.sync_detailed(client=client)
95+
assert api_endpoint_response.status_code == 200
96+
yield api_endpoint_name, api_endpoint_response.content
97+
98+
99+
FIELDS_WITH_VARYING_CONTENT = ["adminViewConfiguration"]
100+
101+
102+
def make_properties_required(x: Any) -> None:
103+
if isinstance(x, dict):
104+
for key, value in x.items():
105+
# do not recurse into objects where the contents might be varying
106+
if key in FIELDS_WITH_VARYING_CONTENT:
107+
continue
108+
make_properties_required(value)
109+
elif isinstance(x, list):
110+
for i in x:
111+
make_properties_required(i)
112+
113+
if isinstance(x, dict) and "properties" in x:
114+
properties = x["properties"]
115+
if isinstance(properties, dict) and len(properties) > 0:
116+
assert "required" not in x
117+
x["required"] = list(properties.keys())
97118

98119

99120
def set_response_schema_by_example(
@@ -120,16 +141,17 @@ def set_response_schema_by_example(
120141
for path_method in path.values()
121142
if path_method["operationId"] == operation_id
122143
][0]
144+
make_properties_required(recorded_response_schema)
123145
request_schema["responses"]["200"]["content"] = recorded_response_schema
124146

125147

126148
def bootstrap_response_schemas(openapi_schema: Dict) -> None:
127149
"""Inserts the response schemas into openapi_schema (in-place),
128150
as recorded by example requests."""
129151
assert_valid_schema(openapi_schema)
130-
for operation_id, response in iterate_request_ids_with_responses():
152+
for operation_id, example_response in iterate_request_ids_with_responses():
131153
set_response_schema_by_example(
132-
openapi_schema, example_response=response, operation_id=operation_id
154+
openapi_schema, example_response=example_response, operation_id=operation_id
133155
)
134156

135157

webknossos/_helpers/pylint_plugins/attrs_ng_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
def register(linter):
99
# Needed for registering the plugin.
10-
pass
10+
del linter
1111

1212

1313
ATTRIB_NAMES = frozenset(("attr.ib", "attrib", "attr.attrib", "attr.field", "field"))

webknossos/examples/WIP/learned_segmenter.py

Lines changed: 0 additions & 30 deletions
This file was deleted.

webknossos/examples/WIP/merge_trees_at_closest_nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# type: ignore
2+
# pylint: skip-file
23
from itertools import combinations
34

45
import networkx as nx

webknossos/examples/WIP/offline_merger_mode.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# type: ignore
2+
# pylint: skip-file
23
import numpy as np
34

45
import webknossos as wk
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from functools import partial
2+
from time import gmtime, strftime
3+
4+
import numpy as np
5+
from skimage import feature
6+
from skimage.future import TrainableSegmenter
7+
8+
import webknossos as wk
9+
from webknossos.client.context import webknossos_context
10+
from webknossos.geometry import BoundingBox, Mag
11+
12+
annotation = wk.open_annotation(
13+
"https://webknossos.org/annotations/Explorational/616457c2010000870032ced4"
14+
)
15+
training_data_bbox = BoundingBox.from_tuple6(
16+
annotation.skeleton.user_bounding_boxes[0] # pylint: disable=unsubscriptable-object
17+
)
18+
time_str = strftime("%Y-%m-%d_%H-%M-%S", gmtime())
19+
new_dataset_name = annotation.dataset_name + f"_segmented_{time_str}"
20+
dataset = wk.download_dataset(
21+
annotation.dataset_name,
22+
"scalable_minds",
23+
path=new_dataset_name,
24+
)
25+
dataset.name = new_dataset_name
26+
27+
volume_annotation = annotation.save_volume_annotation(dataset)
28+
volume_annotation.bounding_box = training_data_bbox
29+
30+
mag = Mag(1)
31+
mag_view = dataset.layers["color"].mags[mag]
32+
33+
features_func = partial(
34+
feature.multiscale_basic_features, multichannel=True, edges=False
35+
)
36+
segmenter = TrainableSegmenter(features_func=features_func)
37+
38+
print("Starting training…")
39+
img_data_train = mag_view.read(
40+
training_data_bbox.in_mag(mag).topleft, training_data_bbox.in_mag(mag).size
41+
)
42+
# move channels to last dimension, remove z dimension
43+
X_train = np.moveaxis(np.squeeze(img_data_train), 0, -1)
44+
Y_train = np.squeeze(volume_annotation.mags[mag].get_view().read())
45+
segmenter.fit(X_train, Y_train)
46+
47+
48+
print("Starting prediction…")
49+
X_predict = np.moveaxis(np.squeeze(mag_view.read()), 0, -1)
50+
Y_predicted = segmenter.predict(X_predict)
51+
segmentation = Y_predicted[:, :, None] # adds z dimension
52+
assert segmentation.max() < 256
53+
segmentation = segmentation.astype("uint8")
54+
55+
segmentation_layer = dataset.add_layer(
56+
"segmentation",
57+
"segmentation",
58+
segmentation.dtype,
59+
compressed=True,
60+
largest_segment_id=int(segmentation.max()),
61+
)
62+
segmentation_layer.bounding_box = dataset.layers["color"].bounding_box
63+
segmentation_layer.add_mag(mag, compress=True).write(segmentation)
64+
65+
with webknossos_context(url="http://localhost:9000", token="secretScmBoyToken"):
66+
url = dataset.upload()
67+
print(f"Successfully uploaded {url}")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from time import gmtime, strftime
2+
3+
from skimage import data
4+
5+
from webknossos.client.context import webknossos_context
6+
from webknossos.dataset.dataset import Dataset
7+
8+
with webknossos_context(url="http://localhost:9000", token="secretScmBoyToken"):
9+
img = data.cell()
10+
time_str = strftime("%Y-%m-%d_%H-%M-%S", gmtime())
11+
name = f"cell_{time_str}"
12+
ds = Dataset.create(name, scale=(107, 107, 107))
13+
layer = ds.add_layer(
14+
"color",
15+
"color",
16+
dtype_per_layer=img.dtype,
17+
)
18+
# add channel and z dimensions and put X before Y,
19+
# resulting dimensions are C, X, Y, Z.
20+
layer.add_mag(1, compress=True).write(img.T[None, :, :, None])
21+
url = ds.upload()
22+
print(f"Successfully uploaded {url}")

0 commit comments

Comments
 (0)