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.
Files changed (37) hide show
  1. {worldarray-0.7.0 → worldarray-0.7.1}/CHANGELOG.md +10 -0
  2. {worldarray-0.7.0 → worldarray-0.7.1}/PKG-INFO +1 -1
  3. {worldarray-0.7.0 → worldarray-0.7.1}/docs/spec.md +3 -1
  4. {worldarray-0.7.0 → worldarray-0.7.1}/pyproject.toml +1 -1
  5. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/__init__.py +1 -1
  6. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/store.py +48 -17
  7. {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_access.py +43 -2
  8. {worldarray-0.7.0 → worldarray-0.7.1}/uv.lock +1 -1
  9. {worldarray-0.7.0 → worldarray-0.7.1}/.github/workflows/ci.yml +0 -0
  10. {worldarray-0.7.0 → worldarray-0.7.1}/.github/workflows/docs.yml +0 -0
  11. {worldarray-0.7.0 → worldarray-0.7.1}/.github/workflows/publish.yml +0 -0
  12. {worldarray-0.7.0 → worldarray-0.7.1}/.gitignore +0 -0
  13. {worldarray-0.7.0 → worldarray-0.7.1}/LICENSE +0 -0
  14. {worldarray-0.7.0 → worldarray-0.7.1}/README.md +0 -0
  15. {worldarray-0.7.0 → worldarray-0.7.1}/docs/changelog.md +0 -0
  16. {worldarray-0.7.0 → worldarray-0.7.1}/docs/index.md +0 -0
  17. {worldarray-0.7.0 → worldarray-0.7.1}/mkdocs.yml +0 -0
  18. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/array.py +0 -0
  19. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/context.py +0 -0
  20. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/geometry.py +0 -0
  21. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/grid.py +0 -0
  22. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/levels.py +0 -0
  23. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/placement.py +0 -0
  24. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/resample.py +0 -0
  25. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/retry.py +0 -0
  26. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/units.py +0 -0
  27. {worldarray-0.7.0 → worldarray-0.7.1}/src/worldarray/write.py +0 -0
  28. {worldarray-0.7.0 → worldarray-0.7.1}/tests/conftest.py +0 -0
  29. {worldarray-0.7.0 → worldarray-0.7.1}/tests/fixtures/metadata.json +0 -0
  30. {worldarray-0.7.0 → worldarray-0.7.1}/tests/fixtures/write.json +0 -0
  31. {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_geometry.py +0 -0
  32. {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_grid.py +0 -0
  33. {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_lost_wakeup.py +0 -0
  34. {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_metadata.py +0 -0
  35. {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_read.py +0 -0
  36. {worldarray-0.7.0 → worldarray-0.7.1}/tests/test_resample.py +0 -0
  37. {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.0
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`; when none is
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.0"
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" }
@@ -28,7 +28,7 @@ from .write import (
28
28
  parse_chunk_key,
29
29
  )
30
30
 
31
- __version__ = "0.7.0"
31
+ __version__ = "0.7.1"
32
32
 
33
33
  __all__ = [
34
34
  "ArrayMeta",
@@ -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 _read_key(spec: dict, key: str) -> bytes | None:
258
- kv = _metadata_kvstore(
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
- result = _wait(lambda: kv.read(key), spec, _where(spec, key), "read")
262
- return None if result.state == "missing" else bytes(result.value)
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 = _metadata_kvstore(
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 listing off answers a refusal for a
296
- missing key, so a refusal on one key counts as missing when another key was read
297
- under the same way; only when every key is refused is the next way tried."""
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
- try:
304
- found[key] = _read_key(spec, key)
305
- except Exception as e:
306
- if not is_refused(e):
307
- raise
308
- refusals[key] = e
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, "_read_key", fake_read_key)
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, "_read_key", fake_read_key)
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.0"
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