worldarray 0.10.0__tar.gz → 0.10.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.10.0 → worldarray-0.10.1}/CHANGELOG.md +13 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/PKG-INFO +1 -1
- {worldarray-0.10.0 → worldarray-0.10.1}/docs/spec.md +12 -2
- {worldarray-0.10.0 → worldarray-0.10.1}/pyproject.toml +1 -1
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/__init__.py +1 -1
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/store.py +28 -8
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/test_access.py +153 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/uv.lock +1 -1
- {worldarray-0.10.0 → worldarray-0.10.1}/.github/workflows/ci.yml +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/.github/workflows/docs.yml +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/.github/workflows/publish.yml +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/.gitignore +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/LICENSE +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/README.md +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/docs/changelog.md +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/docs/index.md +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/mkdocs.yml +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/array.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/context.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/geometry.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/grid.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/levels.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/placement.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/resample.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/retry.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/units.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/src/worldarray/write.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/conftest.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/fixtures/metadata.json +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/fixtures/write.json +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/test_axes_order.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/test_geometry.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/test_grid.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/test_lost_wakeup.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/test_metadata.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/test_read.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/test_resample.py +0 -0
- {worldarray-0.10.0 → worldarray-0.10.1}/tests/test_write.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.1 (2026-11-13)
|
|
4
|
+
|
|
5
|
+
A fix for `gs://` with a stale Google login. No placement change, no unit table change,
|
|
6
|
+
and no default changes.
|
|
7
|
+
|
|
8
|
+
- A `gs://` open falls back to the bucket's public https URL when Google's default
|
|
9
|
+
credentials fail to refresh. An expired gcloud login (a Workspace reauth policy) is
|
|
10
|
+
refused with HTTP 400 `invalid_grant`, which tensorstore does not report as a refusal,
|
|
11
|
+
so the open raised instead of reading a public bucket. Any error from getting the
|
|
12
|
+
access token now counts as a refusal, and so does a token endpoint out of reach.
|
|
13
|
+
- When every way in fails after a refusal, the error raised carries the first refusal
|
|
14
|
+
as its cause, so a stale login stays in the traceback of a private bucket's 403.
|
|
15
|
+
|
|
3
16
|
## 0.10.0 (2026-11-03)
|
|
4
17
|
|
|
5
18
|
Axes in another order, on request. No placement change, no unit table change, and no
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: worldarray
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.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/
|
|
@@ -539,11 +539,18 @@ this document and that one differ, fix one of them.
|
|
|
539
539
|
changes it between opens reads the right host. An N5 `units` written once as a string
|
|
540
540
|
is read as that unit on every axis; tensorstore is given only the array members of
|
|
541
541
|
`attributes.json`, so attributes it would reject do not stop the open. The next way is tried only when the error
|
|
542
|
-
starts `PERMISSION_DENIED` or `UNAUTHENTICATED
|
|
542
|
+
starts `PERMISSION_DENIED` or `UNAUTHENTICATED`, or came from getting an access token,
|
|
543
|
+
which tensorstore marks with its `internal/oauth2/` code among the error's source
|
|
544
|
+
locations (0.10.1): Google refuses an expired gcloud login with HTTP 400 `invalid_grant`,
|
|
545
|
+
which starts neither (cellmap-flow, on David's Mac), and a token endpoint out of reach
|
|
546
|
+
leaves no token either, so the public URL is worth trying. When the last way fails too
|
|
547
|
+
after a refusal, its error carries the first refusal as its cause, so a stale login is
|
|
548
|
+
still in the traceback of a private bucket's 403. The way that worked is remembered per
|
|
543
549
|
(driver, bucket) for the process. Format probing runs all probe keys under one way before
|
|
544
550
|
moving to the next: a bucket with listing off answers 403 for a missing key, so a 403 on
|
|
545
551
|
one probe key counts as "missing" when another probe key was read under the same way,
|
|
546
|
-
and only when every probe key is refused is the next way tried (see Open question 4)
|
|
552
|
+
and only when every probe key is refused is the next way tried (see Open question 4);
|
|
553
|
+
a failed login answers no key, so it moves on at once.
|
|
547
554
|
Metadata reads retry for about five seconds in total (`max_retries 5, initial_delay
|
|
548
555
|
0.2s, max_delay 2s`); data reads keep tensorstore's defaults plus `Retry`, and each
|
|
549
556
|
document read and store open is also timed out under `Retry` (0.7, Behaviour 6).
|
|
@@ -1049,6 +1056,9 @@ chunkmirage derives from raw attributes carries a `layered` note beside the pack
|
|
|
1049
1056
|
any other error raises at once, the way that worked is tried first next time for that
|
|
1050
1057
|
bucket, and a 403 on one probe key with another probe key readable does not trigger the
|
|
1051
1058
|
fallback; detection ignores the directory suffix and finds a precomputed scale key.
|
|
1059
|
+
0.10.1: a failed login counts as a refusal and is the cause of a later failure; end to
|
|
1060
|
+
end, a `gs://` open whose token refresh is answered 400 `invalid_grant` (an https proxy
|
|
1061
|
+
playing www.googleapis.com under a test CA) reads through the public URL.
|
|
1052
1062
|
|
|
1053
1063
|
Reads: every statement in Behaviour 1, 2, 3 and 9; a document-less group probed through
|
|
1054
1064
|
`s0`, integer and `"first"` levels, `detect(missing_ok=True)`, `index_box`; funlib keys on
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "worldarray"
|
|
3
|
-
version = "0.10.
|
|
3
|
+
version = "0.10.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" }
|
|
@@ -158,9 +158,19 @@ class Refused(Exception):
|
|
|
158
158
|
"""A read was refused for want of credentials (not a missing key, not a network error)."""
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
def login_failed(error: BaseException) -> bool:
|
|
162
|
+
"""``error`` came from getting an access token, before any request reached the store: a
|
|
163
|
+
stale Google login (HTTP 400 ``invalid_grant``, which starts neither refusal code), a
|
|
164
|
+
rejected client, or a token endpoint out of reach. tensorstore names its oauth2 code
|
|
165
|
+
among the error's source locations."""
|
|
166
|
+
return "tensorstore/internal/oauth2/" in str(error)
|
|
167
|
+
|
|
168
|
+
|
|
161
169
|
def is_refused(error: BaseException) -> bool:
|
|
162
|
-
return
|
|
163
|
-
(
|
|
170
|
+
return (
|
|
171
|
+
isinstance(error, Refused)
|
|
172
|
+
or str(error).startswith(("PERMISSION_DENIED", "UNAUTHENTICATED"))
|
|
173
|
+
or login_failed(error)
|
|
164
174
|
)
|
|
165
175
|
|
|
166
176
|
|
|
@@ -171,19 +181,23 @@ _admitted_lock = threading.Lock()
|
|
|
171
181
|
def with_access(specs: Sequence[dict], attempt: Callable[[dict], T]) -> T:
|
|
172
182
|
"""``attempt(spec)`` with each spec in turn, moving on only while access is refused; the
|
|
173
183
|
way that worked is tried first next time, for its bucket. Any other error, and the last
|
|
174
|
-
refusal, is raised
|
|
184
|
+
refusal, is raised; after a refusal, with the first refusal as its cause, since that
|
|
185
|
+
can be the reason the read failed (a stale login passed over for a public URL that
|
|
186
|
+
refuses too)."""
|
|
175
187
|
if len(specs) == 1:
|
|
176
188
|
return attempt(specs[0])
|
|
177
189
|
key = (specs[0]["driver"], specs[0].get("bucket", ""))
|
|
178
190
|
with _admitted_lock:
|
|
179
191
|
first = _admitted.get(key, 0)
|
|
180
192
|
order = [first] + [i for i in range(len(specs)) if i != first]
|
|
193
|
+
refusal: BaseException | None = None
|
|
181
194
|
for i in order[:-1]:
|
|
182
195
|
try:
|
|
183
196
|
result = attempt(specs[i])
|
|
184
197
|
except Exception as e:
|
|
185
198
|
if not is_refused(e):
|
|
186
199
|
raise
|
|
200
|
+
refusal = refusal or e
|
|
187
201
|
logger.info(
|
|
188
202
|
"%s refused a read (%s); trying the next way in", key, str(e).splitlines()[0][:200]
|
|
189
203
|
)
|
|
@@ -191,7 +205,12 @@ def with_access(specs: Sequence[dict], attempt: Callable[[dict], T]) -> T:
|
|
|
191
205
|
with _admitted_lock:
|
|
192
206
|
_admitted[key] = i
|
|
193
207
|
return result
|
|
194
|
-
|
|
208
|
+
try:
|
|
209
|
+
result = attempt(specs[order[-1]])
|
|
210
|
+
except Exception as e:
|
|
211
|
+
if refusal is None or e.__cause__ is not None:
|
|
212
|
+
raise
|
|
213
|
+
raise e from refusal
|
|
195
214
|
with _admitted_lock:
|
|
196
215
|
_admitted[key] = order[-1]
|
|
197
216
|
return result
|
|
@@ -264,7 +283,8 @@ def _read_keys(spec: dict, keys: Sequence[str]) -> dict[str, bytes | None | Base
|
|
|
264
283
|
"""``keys`` under one way in, their reads begun at once: one round trip, not one per key
|
|
265
284
|
(an http open probes five). Each value is the document, None when it is missing, or the
|
|
266
285
|
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
|
|
286
|
+
left out, so a server that never answers costs one key's attempts, not five, and so
|
|
287
|
+
does a failed login, which every key would meet."""
|
|
268
288
|
kv = _metadata_kv(spec)
|
|
269
289
|
started = {key: kv.read(key) for key in keys}
|
|
270
290
|
out: dict[str, bytes | None | BaseException] = {}
|
|
@@ -278,7 +298,7 @@ def _read_keys(spec: dict, keys: Sequence[str]) -> dict[str, bytes | None | Base
|
|
|
278
298
|
result = _wait(start, spec, _where(spec, key), "read")
|
|
279
299
|
except Exception as e: # noqa: BLE001 the caller decides what a failed key means
|
|
280
300
|
out[key] = e
|
|
281
|
-
if not is_refused(e):
|
|
301
|
+
if not is_refused(e) or login_failed(e):
|
|
282
302
|
for future in started.values():
|
|
283
303
|
future.cancel()
|
|
284
304
|
return out
|
|
@@ -325,14 +345,14 @@ class Location:
|
|
|
325
345
|
"""All of ``keys`` under one way in, read at once (``_read_keys``). A bucket with
|
|
326
346
|
listing off answers a refusal for a missing key, so a refusal on one key counts as
|
|
327
347
|
missing when another key was read under the same way; only when every key is refused
|
|
328
|
-
is the next way tried."""
|
|
348
|
+
is the next way tried, and at once after a failed login, which answers no key."""
|
|
329
349
|
|
|
330
350
|
def attempt(spec: dict) -> dict[str, bytes | None]:
|
|
331
351
|
found: dict[str, bytes | None] = {}
|
|
332
352
|
refusals: dict[str, BaseException] = {}
|
|
333
353
|
for key, value in _read_keys(spec, keys).items():
|
|
334
354
|
if isinstance(value, BaseException):
|
|
335
|
-
if not is_refused(value):
|
|
355
|
+
if not is_refused(value) or login_failed(value):
|
|
336
356
|
raise value
|
|
337
357
|
refusals[key] = value
|
|
338
358
|
else:
|
|
@@ -5,9 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
import functools
|
|
6
6
|
import gc
|
|
7
7
|
import http.server
|
|
8
|
+
import json
|
|
8
9
|
import logging
|
|
9
10
|
import os
|
|
11
|
+
import shutil
|
|
10
12
|
import socket
|
|
13
|
+
import socketserver
|
|
14
|
+
import ssl
|
|
11
15
|
import subprocess
|
|
12
16
|
import sys
|
|
13
17
|
import textwrap
|
|
@@ -104,6 +108,40 @@ def test_all_probe_keys_refused_falls_back(monkeypatch):
|
|
|
104
108
|
assert found[".zarray"] == b"{}" and found["zarr.json"] is None
|
|
105
109
|
|
|
106
110
|
|
|
111
|
+
_STALE_LOGIN = ( # tensorstore's error for gcloud's login after a reauth policy expired it
|
|
112
|
+
'Unknown [HTTP code 400] [http_response_body=\'{"error": "invalid_grant"}\'] '
|
|
113
|
+
"[source locations='tensorstore/internal/oauth2/oauth2_auth_provider.cc:83\n"
|
|
114
|
+
"tensorstore/internal/oauth2/auth_provider.cc:30']"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_a_failed_login_is_a_refusal_and_the_cause_of_a_later_one():
|
|
119
|
+
unreachable = (
|
|
120
|
+
"UNAVAILABLE: CURL error Could not resolve host [source locations='"
|
|
121
|
+
"tensorstore/internal/curl/curl_transport.cc:475\n"
|
|
122
|
+
"tensorstore/internal/oauth2/oauth2_auth_provider.cc:81']"
|
|
123
|
+
)
|
|
124
|
+
assert store.is_refused(ValueError(_STALE_LOGIN)) and store.is_refused(ValueError(unreachable))
|
|
125
|
+
assert not store.is_refused(ValueError("Unknown [HTTP code 400] [source locations='x.cc:1']"))
|
|
126
|
+
specs = [
|
|
127
|
+
{"driver": "gcs", "bucket": "b"},
|
|
128
|
+
{"driver": "http", "base_url": "https://storage.googleapis.com", "path": "/b/"},
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
def attempt(spec, public=True):
|
|
132
|
+
if spec["driver"] == "gcs":
|
|
133
|
+
raise ValueError(_STALE_LOGIN)
|
|
134
|
+
if public:
|
|
135
|
+
return "ok"
|
|
136
|
+
raise ValueError("PERMISSION_DENIED: Forbidden [http_response_code='403']")
|
|
137
|
+
|
|
138
|
+
assert store.with_access(specs, attempt) == "ok"
|
|
139
|
+
store._admitted.clear()
|
|
140
|
+
with pytest.raises(ValueError, match="PERMISSION_DENIED") as raised:
|
|
141
|
+
store.with_access(specs, functools.partial(attempt, public=False))
|
|
142
|
+
assert "invalid_grant" in str(raised.value.__cause__) # the stale login is in the traceback
|
|
143
|
+
|
|
144
|
+
|
|
107
145
|
def test_detection_ignores_the_suffix_and_finds_precomputed_scale_keys(tmp_path):
|
|
108
146
|
misnamed = tmp_path / "actually_zarr.n5"
|
|
109
147
|
write_zarr2_group(misnamed)
|
|
@@ -481,6 +519,121 @@ def test_an_http_open_probes_its_keys_at_once(tmp_path):
|
|
|
481
519
|
server.shutdown()
|
|
482
520
|
|
|
483
521
|
|
|
522
|
+
def _google_tls(tmp_path):
|
|
523
|
+
"""A test CA's certificate, and a TLS context serving a certificate it signed for
|
|
524
|
+
Google's token hosts, with the extensions macOS requires of a TLS server's."""
|
|
525
|
+
(tmp_path / "test.cnf").write_text(
|
|
526
|
+
textwrap.dedent("""
|
|
527
|
+
[req]
|
|
528
|
+
distinguished_name = dn
|
|
529
|
+
[dn]
|
|
530
|
+
[ca]
|
|
531
|
+
basicConstraints = critical, CA:TRUE
|
|
532
|
+
keyUsage = critical, keyCertSign, cRLSign
|
|
533
|
+
[leaf]
|
|
534
|
+
basicConstraints = critical, CA:FALSE
|
|
535
|
+
keyUsage = critical, digitalSignature, keyEncipherment
|
|
536
|
+
extendedKeyUsage = serverAuth
|
|
537
|
+
subjectAltName = DNS:www.googleapis.com, DNS:oauth2.googleapis.com
|
|
538
|
+
""")
|
|
539
|
+
)
|
|
540
|
+
for args in (
|
|
541
|
+
"req -x509 -config test.cnf -extensions ca -newkey rsa:2048 -nodes -keyout ca.key"
|
|
542
|
+
" -out ca.pem -subj /CN=ca -days 1 -sha256",
|
|
543
|
+
"req -new -config test.cnf -newkey rsa:2048 -nodes -keyout leaf.key -out leaf.csr"
|
|
544
|
+
" -subj /CN=www.googleapis.com -sha256",
|
|
545
|
+
"x509 -req -in leaf.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out leaf.pem -days 1"
|
|
546
|
+
" -sha256 -extfile test.cnf -extensions leaf",
|
|
547
|
+
):
|
|
548
|
+
subprocess.run(["openssl", *args.split()], cwd=tmp_path, check=True, capture_output=True)
|
|
549
|
+
tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
550
|
+
tls.load_cert_chain(tmp_path / "leaf.pem", tmp_path / "leaf.key")
|
|
551
|
+
return tmp_path / "ca.pem", tls
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@pytest.mark.skipif(shutil.which("openssl") is None, reason="the test CA is made with openssl")
|
|
555
|
+
def test_a_stale_google_login_falls_back_to_the_public_url(tmp_path):
|
|
556
|
+
"""Google answers the token refresh of an expired gcloud login 400 invalid_grant, which
|
|
557
|
+
starts neither refusal code (cellmap-flow, on David's Mac). An https proxy plays
|
|
558
|
+
www.googleapis.com, under a CA tensorstore is told to trust, and a local http server is
|
|
559
|
+
the bucket's public URL."""
|
|
560
|
+
public = tmp_path / "public"
|
|
561
|
+
write_zarr2(public / "bucket" / "vol.zarr" / "s0", pattern((4, 4)))
|
|
562
|
+
ca, tls = _google_tls(tmp_path)
|
|
563
|
+
refreshes = []
|
|
564
|
+
|
|
565
|
+
class Google(socketserver.StreamRequestHandler):
|
|
566
|
+
def handle(self):
|
|
567
|
+
target = self.rfile.readline().split()[1]
|
|
568
|
+
while self.rfile.readline() not in (b"\r\n", b""):
|
|
569
|
+
pass
|
|
570
|
+
if target != b"www.googleapis.com:443":
|
|
571
|
+
self.wfile.write(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
|
|
572
|
+
return
|
|
573
|
+
self.wfile.write(b"HTTP/1.1 200 Connection established\r\n\r\n")
|
|
574
|
+
with tls.wrap_socket(self.connection, server_side=True) as conn:
|
|
575
|
+
request = conn.makefile("rb")
|
|
576
|
+
headers = []
|
|
577
|
+
while (line := request.readline()) not in (b"\r\n", b""):
|
|
578
|
+
headers.append(line.decode())
|
|
579
|
+
sizes = [h.split(":")[1] for h in headers if h.lower().startswith("content-length")]
|
|
580
|
+
request.read(int(sizes[0]) if sizes else 0)
|
|
581
|
+
refreshes.append(headers[0].split()[1])
|
|
582
|
+
body = b'{"error": "invalid_grant", "error_description": "reauth related error"}'
|
|
583
|
+
conn.sendall(
|
|
584
|
+
b"HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\n"
|
|
585
|
+
b"Connection: close\r\nContent-Length: %d\r\n\r\n%s" % (len(body), body)
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
class Files(http.server.SimpleHTTPRequestHandler):
|
|
589
|
+
def log_message(self, *args):
|
|
590
|
+
pass
|
|
591
|
+
|
|
592
|
+
proxy = socketserver.ThreadingTCPServer(("127.0.0.1", 0), Google)
|
|
593
|
+
proxy.daemon_threads = True
|
|
594
|
+
files = http.server.ThreadingHTTPServer(
|
|
595
|
+
("127.0.0.1", 0), functools.partial(Files, directory=str(public))
|
|
596
|
+
)
|
|
597
|
+
for server in (proxy, files):
|
|
598
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
599
|
+
login = tmp_path / "adc.json" # gcloud's application default credentials, for a user
|
|
600
|
+
login.write_text(
|
|
601
|
+
json.dumps(
|
|
602
|
+
{
|
|
603
|
+
"type": "authorized_user",
|
|
604
|
+
"client_id": "c",
|
|
605
|
+
"client_secret": "s",
|
|
606
|
+
"refresh_token": "r",
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
env = {k: v for k, v in os.environ.items() if "proxy" not in k.lower()}
|
|
611
|
+
env.update(
|
|
612
|
+
HTTPS_PROXY=f"http://127.0.0.1:{proxy.server_address[1]}",
|
|
613
|
+
NO_PROXY="127.0.0.1",
|
|
614
|
+
TENSORSTORE_CA_BUNDLE=str(ca),
|
|
615
|
+
GOOGLE_APPLICATION_CREDENTIALS=str(login),
|
|
616
|
+
TENSORSTORE_GCS_HTTP_URL=f"http://127.0.0.1:{files.server_address[1]}",
|
|
617
|
+
TENSORSTORE_CURL_VERBOSE="1", # why a handshake with the proxy failed, if one does
|
|
618
|
+
)
|
|
619
|
+
read = (
|
|
620
|
+
"import json, worldarray as wa; "
|
|
621
|
+
"vol = wa.open('gs://bucket/vol.zarr/s0', units=None); "
|
|
622
|
+
"print(json.dumps(vol.ts.read().result().tolist()))"
|
|
623
|
+
)
|
|
624
|
+
try:
|
|
625
|
+
done = subprocess.run(
|
|
626
|
+
[sys.executable, "-c", read], env=env, capture_output=True, text=True, timeout=120
|
|
627
|
+
)
|
|
628
|
+
finally:
|
|
629
|
+
proxy.shutdown()
|
|
630
|
+
files.shutdown()
|
|
631
|
+
assert done.returncode == 0, done.stderr
|
|
632
|
+
assert json.loads(done.stdout) == pattern((4, 4)).tolist()
|
|
633
|
+
# the login was tried, and refused
|
|
634
|
+
assert refreshes and set(refreshes) == {"/oauth2/v3/token"}, done.stderr[-4000:]
|
|
635
|
+
|
|
636
|
+
|
|
484
637
|
def test_capped_contexts_size_file_io_apart_and_write_without_sync_or_locks():
|
|
485
638
|
spec = wa.capped_context_spec(concurrency=1, network=False)
|
|
486
639
|
assert spec["data_copy_concurrency"] == spec["file_io_concurrency"] == {"limit": 1}
|
|
@@ -1247,7 +1247,7 @@ wheels = [
|
|
|
1247
1247
|
|
|
1248
1248
|
[[package]]
|
|
1249
1249
|
name = "worldarray"
|
|
1250
|
-
version = "0.10.
|
|
1250
|
+
version = "0.10.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
|
|
File without changes
|