worldarray 0.6.0__tar.gz → 0.7.0__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.6.0 → worldarray-0.7.0}/CHANGELOG.md +13 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/PKG-INFO +5 -4
- {worldarray-0.6.0 → worldarray-0.7.0}/README.md +4 -3
- {worldarray-0.6.0 → worldarray-0.7.0}/docs/spec.md +37 -16
- {worldarray-0.6.0 → worldarray-0.7.0}/pyproject.toml +1 -1
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/__init__.py +1 -1
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/array.py +23 -7
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/store.py +63 -6
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/test_access.py +37 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/uv.lock +1 -1
- {worldarray-0.6.0 → worldarray-0.7.0}/.github/workflows/ci.yml +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/.github/workflows/docs.yml +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/.github/workflows/publish.yml +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/.gitignore +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/LICENSE +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/docs/changelog.md +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/docs/index.md +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/mkdocs.yml +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/context.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/geometry.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/grid.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/levels.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/placement.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/resample.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/retry.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/units.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/src/worldarray/write.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/conftest.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/fixtures/metadata.json +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/fixtures/write.json +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/test_geometry.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/test_grid.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/test_lost_wakeup.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/test_metadata.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/test_read.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/test_resample.py +0 -0
- {worldarray-0.6.0 → worldarray-0.7.0}/tests/test_write.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
Opens time out. No placement change and no unit table change.
|
|
6
|
+
|
|
7
|
+
- Every metadata document read or written and every store opened is timed out and
|
|
8
|
+
retried under `Retry`, as data reads are (Behaviour 6): by default 5 s times the attempt,
|
|
9
|
+
ten attempts. A server that accepts a connection and never answers held `wa.open` and
|
|
10
|
+
`wa.metadata` forever before; it now raises `TimeoutError` naming the URL and the
|
|
11
|
+
attempts (cellmap-flow, 2026-10-23). The policy is the call's `retries`, which
|
|
12
|
+
`wa.metadata`, `wa.detect` and `wa.open_tensorstore` now take as `wa.open`,
|
|
13
|
+
`wa.open_pyramid` and `wa.create` already did. Listings and the open that creates an
|
|
14
|
+
array are not timed out.
|
|
15
|
+
|
|
3
16
|
## 0.6.0 (2026-10-23)
|
|
4
17
|
|
|
5
18
|
One new argument, at chunkmirage's request. 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.
|
|
3
|
+
Version: 0.7.0
|
|
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/
|
|
@@ -205,9 +205,10 @@ wa.open(path, *, level=None, voxel_size=None, level_mode="floor", on_mismatch="e
|
|
|
205
205
|
metadata_conflicts="error", base_voxel_size=None, base_units=None, anchor=None,
|
|
206
206
|
transform=None, strict_units=True) -> WorldArray
|
|
207
207
|
wa.open_pyramid(path, ...) -> Pyramid # .names .levels .by_name() .select() .placements() .metas(): no opens needed
|
|
208
|
-
wa.metadata(path, level=None, units=None, open=True) -> ArrayMeta # open=False: from documents alone
|
|
209
|
-
wa.detect(path, missing_ok=False) -> Node # format, array or group, raw attrs
|
|
210
|
-
wa.open_tensorstore(path, level=None, context=None)
|
|
208
|
+
wa.metadata(path, level=None, units=None, open=True, retries=None) -> ArrayMeta # open=False: from documents alone
|
|
209
|
+
wa.detect(path, missing_ok=False, retries=None) -> Node # format, array or group, raw attrs
|
|
210
|
+
wa.open_tensorstore(path, level=None, context=None, retries=None)
|
|
211
|
+
# retries= times out and retries document reads and the open, as it does data reads
|
|
211
212
|
wa.shared_context(cache_bytes=0, threads=None) ; wa.capped_context(cache_bytes=0, concurrency=1, network=True) ; wa.Retry(...)
|
|
212
213
|
wa.thread_budget() # the shared context's default thread limit: OMP_NUM_THREADS, the job's slots, or CPUs;
|
|
213
214
|
# threads=N limits it to N, threads=0 leaves tensorstore's own pool sizes
|
|
@@ -146,9 +146,10 @@ wa.open(path, *, level=None, voxel_size=None, level_mode="floor", on_mismatch="e
|
|
|
146
146
|
metadata_conflicts="error", base_voxel_size=None, base_units=None, anchor=None,
|
|
147
147
|
transform=None, strict_units=True) -> WorldArray
|
|
148
148
|
wa.open_pyramid(path, ...) -> Pyramid # .names .levels .by_name() .select() .placements() .metas(): no opens needed
|
|
149
|
-
wa.metadata(path, level=None, units=None, open=True) -> ArrayMeta # open=False: from documents alone
|
|
150
|
-
wa.detect(path, missing_ok=False) -> Node # format, array or group, raw attrs
|
|
151
|
-
wa.open_tensorstore(path, level=None, context=None)
|
|
149
|
+
wa.metadata(path, level=None, units=None, open=True, retries=None) -> ArrayMeta # open=False: from documents alone
|
|
150
|
+
wa.detect(path, missing_ok=False, retries=None) -> Node # format, array or group, raw attrs
|
|
151
|
+
wa.open_tensorstore(path, level=None, context=None, retries=None)
|
|
152
|
+
# retries= times out and retries document reads and the open, as it does data reads
|
|
152
153
|
wa.shared_context(cache_bytes=0, threads=None) ; wa.capped_context(cache_bytes=0, concurrency=1, network=True) ; wa.Retry(...)
|
|
153
154
|
wa.thread_budget() # the shared context's default thread limit: OMP_NUM_THREADS, the job's slots, or CPUs;
|
|
154
155
|
# threads=N limits it to N, threads=0 leaves tensorstore's own pool sizes
|
|
@@ -177,10 +177,10 @@ pyr.voxel_sizes() ; pyr.translations() # where it lists the levels (OME-NGFF
|
|
|
177
177
|
# choosing or placing a level opens nothing, whether the pyramid was opened by its
|
|
178
178
|
# group path or by one of its level paths; else by opening each level
|
|
179
179
|
|
|
180
|
-
wa.metadata(path, level=None, units=None, open=True) -> ArrayMeta # open=False: from the documents alone
|
|
181
|
-
wa.detect(path, missing_ok=False) -> Node | None # format, kind (array|group), path, docs, scale_key; .attrs (raw user
|
|
180
|
+
wa.metadata(path, level=None, units=None, open=True, retries=None) -> ArrayMeta # open=False: from the documents alone
|
|
181
|
+
wa.detect(path, missing_ok=False, retries=None) -> Node | None # format, kind (array|group), path, docs, scale_key; .attrs (raw user
|
|
182
182
|
# attributes), .multiscales (OME 0.5's `ome` unwrapped)
|
|
183
|
-
wa.open_tensorstore(path, level=None, context=None) -> tensorstore.TensorStore # reads no world metadata, so an
|
|
183
|
+
wa.open_tensorstore(path, level=None, context=None, retries=None) -> tensorstore.TensorStore # reads no world metadata, so an
|
|
184
184
|
# array with odd placement attributes (a coordinate array) still opens
|
|
185
185
|
wa.corner_to_centre(corner, voxel_size, axes=None) ; wa.default_axes(ndim) ; wa.index_range(begin, end, voxel_size, translation)
|
|
186
186
|
wa.units.TABLE # the unit tables, public: {"length": {...metres per unit}, "time": {...seconds per unit}}
|
|
@@ -521,7 +521,8 @@ this document and that one differ, fix one of them.
|
|
|
521
521
|
one probe key counts as "missing" when another probe key was read under the same way,
|
|
522
522
|
and only when every probe key is refused is the next way tried (see Open question 4).
|
|
523
523
|
Metadata reads retry for about five seconds in total (`max_retries 5, initial_delay
|
|
524
|
-
0.2s, max_delay 2s`); data reads keep tensorstore's defaults plus `Retry
|
|
524
|
+
0.2s, max_delay 2s`); data reads keep tensorstore's defaults plus `Retry`, and each
|
|
525
|
+
document read and store open is also timed out under `Retry` (0.7, Behaviour 6).
|
|
525
526
|
|
|
526
527
|
6. **Writing is reading inverted, in one format.** The package writes OME-NGFF zarr only:
|
|
527
528
|
zarr v2 with OME 0.4 (`.zgroup`, and `.zattrs` holding `multiscales`, each entry with
|
|
@@ -649,9 +650,21 @@ this document and that one differ, fix one of them.
|
|
|
649
650
|
still land after its cancel, with the same bytes the next attempt writes. Each retry is
|
|
650
651
|
logged as a warning naming the error's first line, or its type when the message is
|
|
651
652
|
empty (tensorstore's timeout is `TimeoutError()`); when every attempt times out the
|
|
652
|
-
`TimeoutError` names the operation, the path and the attempts.
|
|
653
|
-
|
|
654
|
-
|
|
653
|
+
`TimeoutError` names the operation, the path and the attempts. The policy is a
|
|
654
|
+
per-handle attribute and a per-read and per-write argument, never only constructor
|
|
655
|
+
state.
|
|
656
|
+
|
|
657
|
+
Documents and opens (0.7): every metadata document read or written and every store
|
|
658
|
+
opened is timed out and retried the same way, as an operation of no bytes (by default
|
|
659
|
+
5 s times the attempt, ten attempts), with tensorstore's own request retries
|
|
660
|
+
(Convention 5) running inside each attempt. A server that accepts a connection and
|
|
661
|
+
never answers used to hold `wa.open` and `wa.metadata` forever: cellmap-flow's picker
|
|
662
|
+
kept a 5 s probe of its own in front of them (2026-10-23). The policy is the `retries`
|
|
663
|
+
of the public call doing the reading: `open`, `open_pyramid`, `create`, `metadata`,
|
|
664
|
+
`detect` and `open_tensorstore`, which all take it; a document read later outside such
|
|
665
|
+
a call (a zarr v2 `.zattrs` read on demand, a pyramid placing its levels) waits under
|
|
666
|
+
`Retry()`. Listings are not timed out, their size being unknown, and neither is the
|
|
667
|
+
open that creates an array, which a second attempt would find already made.
|
|
655
668
|
|
|
656
669
|
The watchdog (0.5): tensorstore up to at least 0.1.85 can strand a queued task. A pool
|
|
657
670
|
worker idle for 20 ms exits, and a task queued as it exits gets no thread while the pool
|
|
@@ -1019,7 +1032,8 @@ kicked by the watchdog, a dead context no longer watched, and a kick landing on
|
|
|
1019
1032
|
context's own pools; `thread_budget` from each variable, a bad value skipped, and the
|
|
1020
1033
|
shared context's limits. 0.6: `shared_context(threads=)` at `None`, `N` and `0`, one
|
|
1021
1034
|
context per pair, a negative count refused, and a pickled handle coming back on the same
|
|
1022
|
-
shared context.
|
|
1035
|
+
shared context. 0.7: `metadata`, `open` and `detect` against a server
|
|
1036
|
+
that accepts and never answers raising `TimeoutError` after the attempts. Opt-in (`WORLDARRAY_STRESS=1`, about a minute,
|
|
1023
1037
|
`tests/test_lost_wakeup.py`): worker processes aim tasks at the moment a one-thread pool's
|
|
1024
1038
|
worker exits idle, so a plain context strands some for good (13 of about 7,000 on
|
|
1025
1039
|
tensorstore 0.1.78; 40 to 44 of 12,000 to 15,000 with 16 workers on 0.1.75 and 0.1.85)
|
|
@@ -1160,13 +1174,20 @@ over funlib geometry, by this spec's own assignment.
|
|
|
1160
1174
|
a real bucket with listing disabled. One network test before 0.1.0 may change the rule.
|
|
1161
1175
|
4. **The network test** against a non-integer-voxel OpenOrganelle volume (Tests) is not yet
|
|
1162
1176
|
written; it needs the dataset chosen from the catalog.
|
|
1163
|
-
5. **Legacy attributes on new outputs.**
|
|
1164
|
-
`
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1177
|
+
5. **Legacy attributes on new outputs.** Resolved 2026-10-23: nobody needs them, so there
|
|
1178
|
+
is no `funlib_attrs`. cellmap-analyze stopped writing funlib `voxel_size` and `offset`
|
|
1179
|
+
(which held the centre) in 0.6.0, whose outputs carry only the OME documents `create`
|
|
1180
|
+
writes. cellmap-flow writes them through funlib's own `prepare_ds`, `offset` as the
|
|
1181
|
+
corner beside an agreeing OME 0.4 translation, and is their only reader. dacapo reads
|
|
1182
|
+
funlib attributes through `open_ds` (`ome_metadata=False` by default), but nothing feeds
|
|
1183
|
+
it these outputs, and on a zarr v3 one funlib 0.7 silently reads voxel size 1 and
|
|
1184
|
+
offset 0. cellmap-data and fibsem-tools read OME only, and both pin `zarr<3`. funlib
|
|
1185
|
+
0.6 and 0.7 also require integer offsets and voxel sizes, so a float helper here would
|
|
1186
|
+
either refuse placements or write numbers funlib truncates; a repo that wants the keys
|
|
1187
|
+
passes them through `create(attrs=)`. Found on the way, outside this package:
|
|
1188
|
+
cellmap-data (main 356c3ac, `image.py`) takes an OME translation as voxel 0's corner,
|
|
1189
|
+
half a voxel off on every OME-correct store, and funlib's `open_ome_ds` puts the
|
|
1190
|
+
translation straight into its corner `offset`.
|
|
1170
1191
|
6. **The block iterator's shape** (Behaviour 17). Resolved 2026-10-22: both sessions
|
|
1171
1192
|
confirmed it. mesh-n-bone asked that `box` may reach past the array and that `clip`
|
|
1172
1193
|
clips read boxes only, and derives its per-level boxes from one grid itself;
|
|
@@ -1197,7 +1218,7 @@ over funlib geometry, by this spec's own assignment.
|
|
|
1197
1218
|
unlisted has its placement nowhere until it is listed, so a `create(listed=False)`
|
|
1198
1219
|
handle, and its pickles, would carry the placement from its arguments alone. Decide
|
|
1199
1220
|
whether that is worth having, or whether an unlisted level should describe itself on
|
|
1200
|
-
the array (
|
|
1221
|
+
the array (an OME-shaped array attribute; funlib keys are out, open question 5).
|
|
1201
1222
|
12. **Per-open object-store credentials.** cellmap-flow's MinIO writers open with a
|
|
1202
1223
|
per-session key, secret and endpoint while the same process reads public S3
|
|
1203
1224
|
anonymously. tensorstore 0.1.85's `aws_credentials` takes no inline key and secret, and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "worldarray"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
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" }
|
|
@@ -44,6 +44,7 @@ from .store import (
|
|
|
44
44
|
normalize_path,
|
|
45
45
|
open_array,
|
|
46
46
|
precomputed_scale_index,
|
|
47
|
+
retried_documents,
|
|
47
48
|
)
|
|
48
49
|
|
|
49
50
|
logger = logging.getLogger(__name__)
|
|
@@ -68,6 +69,7 @@ class WorldArray:
|
|
|
68
69
|
``voxel_size`` picks the level; a group with neither raises, listing its levels).
|
|
69
70
|
"""
|
|
70
71
|
|
|
72
|
+
@retried_documents
|
|
71
73
|
def __init__(
|
|
72
74
|
self,
|
|
73
75
|
path: str,
|
|
@@ -1002,6 +1004,7 @@ class Pyramid:
|
|
|
1002
1004
|
own metadata where it lists the levels' scales (OME-NGFF, COSEM N5, precomputed), and
|
|
1003
1005
|
opens the levels only when it does not."""
|
|
1004
1006
|
|
|
1007
|
+
@retried_documents
|
|
1005
1008
|
def __init__(
|
|
1006
1009
|
self,
|
|
1007
1010
|
path: str,
|
|
@@ -1201,6 +1204,7 @@ class Pyramid:
|
|
|
1201
1204
|
# Functions
|
|
1202
1205
|
|
|
1203
1206
|
|
|
1207
|
+
@retried_documents
|
|
1204
1208
|
def create(
|
|
1205
1209
|
path: str,
|
|
1206
1210
|
shape: Sequence[int] | None = None,
|
|
@@ -1345,14 +1349,21 @@ def open_pyramid(path: str, **kwargs) -> Pyramid:
|
|
|
1345
1349
|
return Pyramid(path, **kwargs)
|
|
1346
1350
|
|
|
1347
1351
|
|
|
1352
|
+
@retried_documents
|
|
1348
1353
|
def metadata(
|
|
1349
|
-
path: str,
|
|
1354
|
+
path: str,
|
|
1355
|
+
*,
|
|
1356
|
+
level: str | int | None = None,
|
|
1357
|
+
units: Units = None,
|
|
1358
|
+
open: bool = True,
|
|
1359
|
+
retries: Retry | None = None,
|
|
1350
1360
|
) -> ArrayMeta:
|
|
1351
1361
|
"""The ``ArrayMeta`` of an array (or of ``level`` under a group), in native units unless
|
|
1352
1362
|
``units`` says otherwise. ``open=False`` reads it from the documents alone, with no
|
|
1353
|
-
tensorstore opened.
|
|
1363
|
+
tensorstore opened. ``retries`` times out and retries each document read and the open
|
|
1364
|
+
(Behaviour 6)."""
|
|
1354
1365
|
if open:
|
|
1355
|
-
vol = WorldArray(path, level=level, units=units)
|
|
1366
|
+
vol = WorldArray(path, level=level, units=units, retries=retries)
|
|
1356
1367
|
return vol.meta if units is None else vol.grid
|
|
1357
1368
|
node = detect(path)
|
|
1358
1369
|
if level == "first":
|
|
@@ -1386,13 +1397,18 @@ def metadata(
|
|
|
1386
1397
|
return meta.in_units(space=space, time=time)
|
|
1387
1398
|
|
|
1388
1399
|
|
|
1400
|
+
@retried_documents
|
|
1389
1401
|
def open_tensorstore(
|
|
1390
|
-
path: str,
|
|
1402
|
+
path: str,
|
|
1403
|
+
*,
|
|
1404
|
+
level: str | int | None = None,
|
|
1405
|
+
context: ts.Context | None = None,
|
|
1406
|
+
retries: Retry | None = None,
|
|
1391
1407
|
) -> ts.TensorStore:
|
|
1392
1408
|
"""The raw tensorstore of an array or level: C order, origin 0, a precomputed single
|
|
1393
|
-
channel dropped. The access fallbacks and shared context apply
|
|
1394
|
-
read
|
|
1395
|
-
opens."""
|
|
1409
|
+
channel dropped. The access fallbacks and shared context apply, and ``retries`` to each
|
|
1410
|
+
document read and the open (Behaviour 6). No world metadata is read, so an array whose
|
|
1411
|
+
placement attributes are odd (a coordinate array, a field) still opens."""
|
|
1396
1412
|
context = context if context is not None else shared_context(0)
|
|
1397
1413
|
node = detect(path)
|
|
1398
1414
|
if level == "first":
|
|
@@ -8,6 +8,8 @@ one in, with credentials only when a public read is refused (spec, Conventions 5
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import contextlib
|
|
12
|
+
import contextvars
|
|
11
13
|
import functools
|
|
12
14
|
import json
|
|
13
15
|
import logging
|
|
@@ -21,6 +23,7 @@ from typing import Any, TypeVar
|
|
|
21
23
|
import tensorstore as ts
|
|
22
24
|
|
|
23
25
|
from .context import watch
|
|
26
|
+
from .retry import Retry, with_retries
|
|
24
27
|
|
|
25
28
|
logger = logging.getLogger(__name__)
|
|
26
29
|
T = TypeVar("T")
|
|
@@ -33,6 +36,11 @@ PROBE_KEYS = ("zarr.json", ".zarray", ".zgroup", "info", "attributes.json")
|
|
|
33
36
|
_SCHEME = re.compile(r"^(s3|gs|http|https|file|memory)://")
|
|
34
37
|
_REMOTE = ("s3://", "gs://", "http://", "https://")
|
|
35
38
|
|
|
39
|
+
# the Retry that documents and opens wait under: the policy of the public call they serve
|
|
40
|
+
_documents_retry: contextvars.ContextVar[Retry | None] = contextvars.ContextVar(
|
|
41
|
+
"worldarray_documents_retry", default=None
|
|
42
|
+
)
|
|
43
|
+
|
|
36
44
|
|
|
37
45
|
# ---------------------------------------------------------------------------
|
|
38
46
|
# Paths
|
|
@@ -189,6 +197,47 @@ def with_access(specs: Sequence[dict], attempt: Callable[[dict], T]) -> T:
|
|
|
189
197
|
return result
|
|
190
198
|
|
|
191
199
|
|
|
200
|
+
@contextlib.contextmanager
|
|
201
|
+
def documents_retried(retry: Retry | None):
|
|
202
|
+
"""Metadata documents read or written, and stores opened, inside this block are timed
|
|
203
|
+
out and retried per ``retry`` as reads of no bytes; ``None`` keeps the enclosing
|
|
204
|
+
policy, else ``Retry()`` (spec, Behaviour 6)."""
|
|
205
|
+
token = _documents_retry.set(retry) if retry is not None else None
|
|
206
|
+
try:
|
|
207
|
+
yield
|
|
208
|
+
finally:
|
|
209
|
+
if token is not None:
|
|
210
|
+
_documents_retry.reset(token)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def retried_documents(fn: Callable[..., T]) -> Callable[..., T]:
|
|
214
|
+
"""``fn`` run under ``documents_retried(retries)``, ``retries`` being its keyword."""
|
|
215
|
+
|
|
216
|
+
@functools.wraps(fn)
|
|
217
|
+
def run(*args, **kwargs):
|
|
218
|
+
with documents_retried(kwargs.get("retries")):
|
|
219
|
+
return fn(*args, **kwargs)
|
|
220
|
+
|
|
221
|
+
return run
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _where(spec: dict, key: str = "") -> str:
|
|
225
|
+
if spec["driver"] == "file":
|
|
226
|
+
return spec["path"] + key
|
|
227
|
+
if spec["driver"] == "http":
|
|
228
|
+
return spec["base_url"].rstrip("/") + "/" + spec.get("path", "").lstrip("/") + key
|
|
229
|
+
return f"{spec['driver']}://{spec.get('bucket', '')}/{spec.get('path', '')}{key}"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _wait(start: Callable[[], Any], spec: dict, what: str, verb: str) -> Any:
|
|
233
|
+
"""``start().result()`` for a document or an open: a server that accepts and never
|
|
234
|
+
answers would otherwise hold it forever (tensorstore's own request retries run inside
|
|
235
|
+
each attempt)."""
|
|
236
|
+
retry = _documents_retry.get() or Retry()
|
|
237
|
+
remote = spec["driver"] in ("http", "gcs", "s3")
|
|
238
|
+
return with_retries(start, 0, retry, remote=remote, what=what, verb=verb)
|
|
239
|
+
|
|
240
|
+
|
|
192
241
|
def metadata_spec(spec: dict) -> dict:
|
|
193
242
|
"""``spec`` with its requests' retries capped, so a mistyped host fails in seconds."""
|
|
194
243
|
if spec["driver"] not in ("s3", "gcs", "http"):
|
|
@@ -201,14 +250,15 @@ def _metadata_kvstore(spec_json: str, gcs_url: str | None) -> ts.KvStore:
|
|
|
201
250
|
"""One opened kvstore per spec, and per ``TENSORSTORE_GCS_HTTP_URL``: tensorstore's gcs
|
|
202
251
|
driver reads that variable when the store is opened, so a cached store would keep
|
|
203
252
|
talking to the host the variable named then."""
|
|
204
|
-
|
|
253
|
+
spec = json.loads(spec_json)
|
|
254
|
+
return _wait(lambda: ts.KvStore.open(metadata_spec(spec)), spec, _where(spec), "open")
|
|
205
255
|
|
|
206
256
|
|
|
207
257
|
def _read_key(spec: dict, key: str) -> bytes | None:
|
|
208
258
|
kv = _metadata_kvstore(
|
|
209
259
|
json.dumps(spec, sort_keys=True), os.environ.get("TENSORSTORE_GCS_HTTP_URL")
|
|
210
260
|
)
|
|
211
|
-
result = kv.read(key)
|
|
261
|
+
result = _wait(lambda: kv.read(key), spec, _where(spec, key), "read")
|
|
212
262
|
return None if result.state == "missing" else bytes(result.value)
|
|
213
263
|
|
|
214
264
|
|
|
@@ -216,7 +266,7 @@ def _write_key(spec: dict, key: str, data: bytes) -> None:
|
|
|
216
266
|
kv = _metadata_kvstore(
|
|
217
267
|
json.dumps(spec, sort_keys=True), os.environ.get("TENSORSTORE_GCS_HTTP_URL")
|
|
218
268
|
)
|
|
219
|
-
kv.write(key, data)
|
|
269
|
+
_wait(lambda: kv.write(key, data), spec, _where(spec, key), "write")
|
|
220
270
|
|
|
221
271
|
|
|
222
272
|
class Location:
|
|
@@ -323,12 +373,14 @@ def _decode(docs: dict[str, bytes | None]) -> dict[str, Any]:
|
|
|
323
373
|
return {k: json.loads(v) for k, v in docs.items() if v is not None}
|
|
324
374
|
|
|
325
375
|
|
|
326
|
-
|
|
376
|
+
@retried_documents
|
|
377
|
+
def detect(path: str, *, missing_ok: bool = False, retries: Retry | None = None) -> Node | None:
|
|
327
378
|
"""Probe ``zarr.json``, ``.zarray``, ``.zgroup``, ``info``, ``attributes.json`` in that
|
|
328
379
|
order; failing all, probe the parent for ``info`` and take the last path segment as a
|
|
329
380
|
precomputed scale key, then probe ``s0`` for a group that has no document of its own.
|
|
330
381
|
The ``.n5`` or ``.zarr`` suffix is never consulted. Nothing found raises
|
|
331
|
-
``FileNotFoundError``, or returns None with ``missing_ok``.
|
|
382
|
+
``FileNotFoundError``, or returns None with ``missing_ok``. ``retries`` times out and
|
|
383
|
+
retries each document read (Behaviour 6)."""
|
|
332
384
|
path = normalize_path(path)
|
|
333
385
|
docs = _decode(Location(path).read_many(PROBE_KEYS))
|
|
334
386
|
if "zarr.json" in docs:
|
|
@@ -440,7 +492,12 @@ def open_array(
|
|
|
440
492
|
spec["assume_metadata"] = True
|
|
441
493
|
if node.format == "precomputed" and scale_index is not None:
|
|
442
494
|
spec["scale_index"] = scale_index
|
|
443
|
-
return
|
|
495
|
+
return _wait(
|
|
496
|
+
lambda: ts.open(spec, read=True, write=write, context=watch(context)),
|
|
497
|
+
kvstore,
|
|
498
|
+
node.path,
|
|
499
|
+
"open",
|
|
500
|
+
)
|
|
444
501
|
|
|
445
502
|
store = with_access(kvstore_specs(node.path), attempt)
|
|
446
503
|
if node.format in ("n5", "precomputed"):
|
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import gc
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
|
+
import socket
|
|
9
|
+
import threading
|
|
8
10
|
import time
|
|
9
11
|
import weakref
|
|
10
12
|
|
|
@@ -321,3 +323,38 @@ def test_a_shared_context_takes_the_callers_thread_count(monkeypatch):
|
|
|
321
323
|
assert wa.context.from_recipe(("shared", 7)) is default
|
|
322
324
|
with pytest.raises(ValueError):
|
|
323
325
|
wa.shared_context(threads=-1)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@pytest.fixture
|
|
329
|
+
def silent_server():
|
|
330
|
+
"""An http address whose server accepts every connection and never answers."""
|
|
331
|
+
srv = socket.socket()
|
|
332
|
+
srv.bind(("127.0.0.1", 0))
|
|
333
|
+
srv.listen(16)
|
|
334
|
+
held = []
|
|
335
|
+
|
|
336
|
+
def accept():
|
|
337
|
+
while True:
|
|
338
|
+
try:
|
|
339
|
+
held.append(srv.accept()[0])
|
|
340
|
+
except OSError:
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
threading.Thread(target=accept, daemon=True).start()
|
|
344
|
+
yield f"http://127.0.0.1:{srv.getsockname()[1]}"
|
|
345
|
+
srv.close()
|
|
346
|
+
for c in held:
|
|
347
|
+
c.close()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_a_server_that_never_answers_times_out_the_open(silent_server):
|
|
351
|
+
quick = wa.Retry(timeout=0.3, max_attempts=2, base_delay=0.0)
|
|
352
|
+
for open_it in (
|
|
353
|
+
lambda url: wa.metadata(url, open=False, retries=quick),
|
|
354
|
+
lambda url: wa.open(url, retries=quick),
|
|
355
|
+
lambda url: wa.detect(url, retries=quick),
|
|
356
|
+
):
|
|
357
|
+
start = time.monotonic()
|
|
358
|
+
with pytest.raises(TimeoutError, match="127.0.0.1.*2 attempts"):
|
|
359
|
+
open_it(f"{silent_server}/vol.zarr/s0")
|
|
360
|
+
assert time.monotonic() - start < 10
|
|
@@ -1247,7 +1247,7 @@ wheels = [
|
|
|
1247
1247
|
|
|
1248
1248
|
[[package]]
|
|
1249
1249
|
name = "worldarray"
|
|
1250
|
-
version = "0.
|
|
1250
|
+
version = "0.7.0"
|
|
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
|