worldarray 0.7.0__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {worldarray-0.7.0 → worldarray-0.7.1}/CHANGELOG.md +10 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/PKG-INFO +1 -1
- {worldarray-0.7.0 → worldarray-0.7.1}/docs/spec.md +3 -1
- {worldarray-0.7.0 → worldarray-0.7.1}/pyproject.toml +1 -1
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/__init__.py +1 -1
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/store.py +48 -17
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_access.py +43 -2
- {worldarray-0.7.0 → worldarray-0.7.1}/uv.lock +1 -1
- {worldarray-0.7.0 → worldarray-0.7.1}/.github/workflows/ci.yml +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/.github/workflows/docs.yml +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/.github/workflows/publish.yml +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/.gitignore +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/LICENSE +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/README.md +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/docs/changelog.md +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/docs/index.md +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/mkdocs.yml +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/array.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/context.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/geometry.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/grid.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/levels.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/placement.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/resample.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/retry.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/units.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/write.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/conftest.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/fixtures/metadata.json +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/fixtures/write.json +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_geometry.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_grid.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_lost_wakeup.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_metadata.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_read.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_resample.py +0 -0
- {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_write.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.1 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
Faster opens over http. No placement change, no unit table change, no API change.
|
|
6
|
+
|
|
7
|
+
- The documents a call needs from one directory are read at once instead of in turn. An
|
|
8
|
+
http open of a zarr v2 level made its eight requests one after another (five probe keys,
|
|
9
|
+
the level's `.zattrs`, the parent's `zarr.json` and `.zattrs`); now they go in three
|
|
10
|
+
rounds. At 10 ms an answer, `wa.open` took 98 ms and takes 52 (cellmap-flow's
|
|
11
|
+
measurement). A server that never answers still costs one key's attempts, not five.
|
|
12
|
+
|
|
3
13
|
## 0.7.0 (2026-10-23)
|
|
4
14
|
|
|
5
15
|
Opens time out. No placement change and no unit table change.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: worldarray
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Read zarr, N5 and Neuroglancer precomputed volumes in world coordinates through tensorstore: correct voxel placement, pyramid levels, padded region reads, resampling. numpy + tensorstore only.
|
|
5
5
|
Project-URL: Homepage, https://janeliascicomp.github.io/worldarray/
|
|
6
6
|
Project-URL: Documentation, https://janeliascicomp.github.io/worldarray/
|
|
@@ -490,7 +490,9 @@ this document and that one differ, fix one of them.
|
|
|
490
490
|
|
|
491
491
|
5. **Stores are read through tensorstore's kvstore only**, never zarr-python: `zarr.json`,
|
|
492
492
|
`.zarray`, `.zattrs`, `.zgroup`, `attributes.json`, `info`. Format detection probes keys
|
|
493
|
-
in this order: `zarr.json`, `.zarray`, `.zgroup`, `info`, `attributes.json
|
|
493
|
+
in this order: `zarr.json`, `.zarray`, `.zgroup`, `info`, `attributes.json`, the reads
|
|
494
|
+
begun all at once and the order deciding which document wins (0.7.1: one round trip
|
|
495
|
+
where an http open made five in a row, cellmap-flow's measurement); when none is
|
|
494
496
|
found, the parent is probed for `info` and the last path segment is taken as a
|
|
495
497
|
precomputed scale key (mesh-n-bone's case), then `s0` is probed so a group with no
|
|
496
498
|
document of its own (an N5 directory holding `s0, s1, ...`) is still a group. The
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "worldarray"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.1"
|
|
4
4
|
description = "Read zarr, N5 and Neuroglancer precomputed volumes in world coordinates through tensorstore: correct voxel placement, pyramid levels, padded region reads, resampling. numpy + tensorstore only."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -254,18 +254,48 @@ def _metadata_kvstore(spec_json: str, gcs_url: str | None) -> ts.KvStore:
|
|
|
254
254
|
return _wait(lambda: ts.KvStore.open(metadata_spec(spec)), spec, _where(spec), "open")
|
|
255
255
|
|
|
256
256
|
|
|
257
|
-
def
|
|
258
|
-
|
|
257
|
+
def _metadata_kv(spec: dict) -> ts.KvStore:
|
|
258
|
+
return _metadata_kvstore(
|
|
259
259
|
json.dumps(spec, sort_keys=True), os.environ.get("TENSORSTORE_GCS_HTTP_URL")
|
|
260
260
|
)
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _read_keys(spec: dict, keys: Sequence[str]) -> dict[str, bytes | None | BaseException]:
|
|
264
|
+
"""``keys`` under one way in, their reads begun at once: one round trip, not one per key
|
|
265
|
+
(an http open probes five). Each value is the document, None when it is missing, or the
|
|
266
|
+
error its read raised; after an error other than a refusal the rest are cancelled and
|
|
267
|
+
left out, so a server that never answers costs one key's attempts, not five."""
|
|
268
|
+
kv = _metadata_kv(spec)
|
|
269
|
+
started = {key: kv.read(key) for key in keys}
|
|
270
|
+
out: dict[str, bytes | None | BaseException] = {}
|
|
271
|
+
for key in keys:
|
|
272
|
+
first = [started[key]]
|
|
273
|
+
|
|
274
|
+
def start(key=key, first=first):
|
|
275
|
+
return first.pop() if first else kv.read(key)
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
result = _wait(start, spec, _where(spec, key), "read")
|
|
279
|
+
except Exception as e: # noqa: BLE001 the caller decides what a failed key means
|
|
280
|
+
out[key] = e
|
|
281
|
+
if not is_refused(e):
|
|
282
|
+
for future in started.values():
|
|
283
|
+
future.cancel()
|
|
284
|
+
return out
|
|
285
|
+
continue
|
|
286
|
+
out[key] = None if result.state == "missing" else bytes(result.value)
|
|
287
|
+
return out
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _read_key(spec: dict, key: str) -> bytes | None:
|
|
291
|
+
value = _read_keys(spec, [key])[key]
|
|
292
|
+
if isinstance(value, BaseException):
|
|
293
|
+
raise value
|
|
294
|
+
return value
|
|
263
295
|
|
|
264
296
|
|
|
265
297
|
def _write_key(spec: dict, key: str, data: bytes) -> None:
|
|
266
|
-
kv =
|
|
267
|
-
json.dumps(spec, sort_keys=True), os.environ.get("TENSORSTORE_GCS_HTTP_URL")
|
|
268
|
-
)
|
|
298
|
+
kv = _metadata_kv(spec)
|
|
269
299
|
_wait(lambda: kv.write(key, data), spec, _where(spec, key), "write")
|
|
270
300
|
|
|
271
301
|
|
|
@@ -292,20 +322,21 @@ class Location:
|
|
|
292
322
|
self.write(key, (json.dumps(doc, indent=2) + "\n").encode())
|
|
293
323
|
|
|
294
324
|
def read_many(self, keys: Sequence[str]) -> dict[str, bytes | None]:
|
|
295
|
-
"""All of ``keys`` under one way in. A bucket with
|
|
296
|
-
missing key, so a refusal on one key counts as
|
|
297
|
-
under the same way; only when every key is refused
|
|
325
|
+
"""All of ``keys`` under one way in, read at once (``_read_keys``). A bucket with
|
|
326
|
+
listing off answers a refusal for a missing key, so a refusal on one key counts as
|
|
327
|
+
missing when another key was read under the same way; only when every key is refused
|
|
328
|
+
is the next way tried."""
|
|
298
329
|
|
|
299
330
|
def attempt(spec: dict) -> dict[str, bytes | None]:
|
|
300
331
|
found: dict[str, bytes | None] = {}
|
|
301
332
|
refusals: dict[str, BaseException] = {}
|
|
302
|
-
for key in keys:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
333
|
+
for key, value in _read_keys(spec, keys).items():
|
|
334
|
+
if isinstance(value, BaseException):
|
|
335
|
+
if not is_refused(value):
|
|
336
|
+
raise value
|
|
337
|
+
refusals[key] = value
|
|
338
|
+
else:
|
|
339
|
+
found[key] = value
|
|
309
340
|
if refusals and len(refusals) == len(keys):
|
|
310
341
|
raise Refused(next(iter(refusals.values())).args[0] if refusals else "refused")
|
|
311
342
|
for key in refusals:
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import functools
|
|
5
6
|
import gc
|
|
7
|
+
import http.server
|
|
6
8
|
import logging
|
|
7
9
|
import os
|
|
8
10
|
import socket
|
|
@@ -56,6 +58,21 @@ def test_other_errors_raise_at_once():
|
|
|
56
58
|
store.with_access(specs, attempt)
|
|
57
59
|
|
|
58
60
|
|
|
61
|
+
def _per_key(read_key):
|
|
62
|
+
"""A ``store._read_keys`` stand-in built from a one-key fake."""
|
|
63
|
+
|
|
64
|
+
def read_keys(spec, keys):
|
|
65
|
+
out = {}
|
|
66
|
+
for key in keys:
|
|
67
|
+
try:
|
|
68
|
+
out[key] = read_key(spec, key)
|
|
69
|
+
except Exception as e: # noqa: BLE001
|
|
70
|
+
out[key] = e
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
return read_keys
|
|
74
|
+
|
|
75
|
+
|
|
59
76
|
def test_a_refused_probe_key_counts_as_missing_when_another_key_was_read(monkeypatch):
|
|
60
77
|
present = {"zarr.json": b'{"node_type": "array"}'}
|
|
61
78
|
|
|
@@ -66,7 +83,7 @@ def test_a_refused_probe_key_counts_as_missing_when_another_key_was_read(monkeyp
|
|
|
66
83
|
return present[key]
|
|
67
84
|
raise RuntimeError("PERMISSION_DENIED: listing is off, so a missing key is a 403")
|
|
68
85
|
|
|
69
|
-
monkeypatch.setattr(store, "
|
|
86
|
+
monkeypatch.setattr(store, "_read_keys", _per_key(fake_read_key))
|
|
70
87
|
loc = store.Location("s3://bucket/vol.zarr/s0")
|
|
71
88
|
found = loc.read_many(store.PROBE_KEYS)
|
|
72
89
|
assert found["zarr.json"] == present["zarr.json"]
|
|
@@ -79,7 +96,7 @@ def test_all_probe_keys_refused_falls_back(monkeypatch):
|
|
|
79
96
|
raise RuntimeError("PERMISSION_DENIED")
|
|
80
97
|
return b"{}" if key == ".zarray" else None
|
|
81
98
|
|
|
82
|
-
monkeypatch.setattr(store, "
|
|
99
|
+
monkeypatch.setattr(store, "_read_keys", _per_key(fake_read_key))
|
|
83
100
|
found = store.Location("s3://bucket/vol.zarr/s0").read_many(store.PROBE_KEYS)
|
|
84
101
|
assert found[".zarray"] == b"{}" and found["zarr.json"] is None
|
|
85
102
|
|
|
@@ -358,3 +375,27 @@ def test_a_server_that_never_answers_times_out_the_open(silent_server):
|
|
|
358
375
|
with pytest.raises(TimeoutError, match="127.0.0.1.*2 attempts"):
|
|
359
376
|
open_it(f"{silent_server}/vol.zarr/s0")
|
|
360
377
|
assert time.monotonic() - start < 10
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_an_http_open_probes_its_keys_at_once(tmp_path):
|
|
381
|
+
"""Five probe keys, one round trip: each answer here takes 0.3 s."""
|
|
382
|
+
write_zarr2(tmp_path / "vol.zarr" / "s0", pattern((4, 4, 4)))
|
|
383
|
+
|
|
384
|
+
class Slow(http.server.SimpleHTTPRequestHandler):
|
|
385
|
+
def do_GET(self):
|
|
386
|
+
time.sleep(0.3)
|
|
387
|
+
super().do_GET()
|
|
388
|
+
|
|
389
|
+
def log_message(self, *args):
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
handler = functools.partial(Slow, directory=str(tmp_path))
|
|
393
|
+
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
|
394
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
395
|
+
try:
|
|
396
|
+
start = time.monotonic()
|
|
397
|
+
node = wa.detect(f"http://127.0.0.1:{server.server_address[1]}/vol.zarr/s0")
|
|
398
|
+
assert node.format == "zarr2" and node.kind == "array"
|
|
399
|
+
assert time.monotonic() - start < 1.0 # in turn: 1.5 s
|
|
400
|
+
finally:
|
|
401
|
+
server.shutdown()
|
|
@@ -1247,7 +1247,7 @@ wheels = [
|
|
|
1247
1247
|
|
|
1248
1248
|
[[package]]
|
|
1249
1249
|
name = "worldarray"
|
|
1250
|
-
version = "0.7.
|
|
1250
|
+
version = "0.7.1"
|
|
1251
1251
|
source = { editable = "." }
|
|
1252
1252
|
dependencies = [
|
|
1253
1253
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|