spatialdata-js-util 0.1.0__py3-none-any.whl
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.
- spatialdata_js_util/__init__.py +107 -0
- spatialdata_js_util/cli.py +803 -0
- spatialdata_js_util/codecs/__init__.py +67 -0
- spatialdata_js_util/codecs/backends.py +250 -0
- spatialdata_js_util/codecs/chunks.py +40 -0
- spatialdata_js_util/codecs/encoding.py +154 -0
- spatialdata_js_util/codecs/htj2k_wasm.py +266 -0
- spatialdata_js_util/codecs/names.py +26 -0
- spatialdata_js_util/codecs/probe/multicomponent.j2c +0 -0
- spatialdata_js_util/codecs/probe/multicomponent.json +3 -0
- spatialdata_js_util/codecs/probe/multicomponent.npy +0 -0
- spatialdata_js_util/codecs/vendor/__init__.py +1 -0
- spatialdata_js_util/codecs/vendor/encode-plane.mjs +164 -0
- spatialdata_js_util/codecs/vendor/openjph/index.mjs +131 -0
- spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph.mjs +2 -0
- spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph.wasm +0 -0
- spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph_simd.mjs +2 -0
- spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph_simd.wasm +0 -0
- spatialdata_js_util/codecs/zarr_codec.py +149 -0
- spatialdata_js_util/errors.py +5 -0
- spatialdata_js_util/images.py +849 -0
- spatialdata_js_util/index_permutations.py +311 -0
- spatialdata_js_util/points.py +445 -0
- spatialdata_js_util/points_zarr.py +799 -0
- spatialdata_js_util/provenance.py +17 -0
- spatialdata_js_util/pyramids.py +318 -0
- spatialdata_js_util/runners.py +458 -0
- spatialdata_js_util/store.py +305 -0
- spatialdata_js_util/tables.py +310 -0
- spatialdata_js_util/tui/__init__.py +0 -0
- spatialdata_js_util/tui/app.py +112 -0
- spatialdata_js_util/tui/models.py +45 -0
- spatialdata_js_util/tui/screens.py +1255 -0
- spatialdata_js_util/tui/widgets.py +50 -0
- spatialdata_js_util/verify.py +338 -0
- spatialdata_js_util-0.1.0.dist-info/METADATA +412 -0
- spatialdata_js_util-0.1.0.dist-info/RECORD +40 -0
- spatialdata_js_util-0.1.0.dist-info/WHEEL +5 -0
- spatialdata_js_util-0.1.0.dist-info/entry_points.txt +7 -0
- spatialdata_js_util-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Widgets that work around defects in the installed Textual version."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.css.query import NoMatches
|
|
6
|
+
from textual.dom import NoScreen
|
|
7
|
+
from textual.events import Mount
|
|
8
|
+
from textual.widgets import Header
|
|
9
|
+
|
|
10
|
+
# `HeaderTitle` is not re-exported from `textual.widgets`; the override below
|
|
11
|
+
# needs the same class the base implementation queries for.
|
|
12
|
+
from textual.widgets._header import HeaderTitle
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SafeHeader(Header):
|
|
16
|
+
"""`Header` that tolerates its title watcher running before compose mounts.
|
|
17
|
+
|
|
18
|
+
Textual's `Header._on_mount` registers title watchers with `init=True`, and
|
|
19
|
+
their callback does `query_one(HeaderTitle)` guarded only against
|
|
20
|
+
`NoScreen`. `HeaderTitle` is a child yielded by `Header.compose()`, so when
|
|
21
|
+
the watcher runs before that child has mounted the query raises `NoMatches`
|
|
22
|
+
— which escapes into the app's message pump and fails whatever is driving
|
|
23
|
+
the app.
|
|
24
|
+
|
|
25
|
+
It is timing-dependent: it surfaced on loaded CI runners and never
|
|
26
|
+
reproduced locally, including under CPU contention. Verified against textual
|
|
27
|
+
8.2.8, the latest release when this was written.
|
|
28
|
+
|
|
29
|
+
Delete this once upstream catches `NoMatches` as well;
|
|
30
|
+
`test_tui.py::TestSafeHeader` asserts the stock `Header` still has the
|
|
31
|
+
defect, so it will fail and prompt the removal when that lands.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def _on_mount(self, event: Mount) -> None:
|
|
35
|
+
# Textual dispatches a handler for every class in the MRO, so defining
|
|
36
|
+
# `_on_mount` here does not replace the base one — it runs first, and
|
|
37
|
+
# the unguarded original would still follow. `prevent_default` is what
|
|
38
|
+
# actually suppresses it.
|
|
39
|
+
event.prevent_default()
|
|
40
|
+
|
|
41
|
+
async def set_title() -> None:
|
|
42
|
+
try:
|
|
43
|
+
self.query_one(HeaderTitle).update(self.format_title())
|
|
44
|
+
except (NoScreen, NoMatches):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
self.watch(self.app, "title", set_title)
|
|
48
|
+
self.watch(self.app, "sub_title", set_title)
|
|
49
|
+
self.watch(self.screen, "title", set_title)
|
|
50
|
+
self.watch(self.screen, "sub_title", set_title)
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import pyarrow.parquet as pq
|
|
10
|
+
|
|
11
|
+
from .points import MORTON_CODE_2D_COLUMN
|
|
12
|
+
|
|
13
|
+
_UINT_INTERMEDIATE_SUFFIXES = ("_uint",)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _dataframe_column(df: pd.DataFrame, name: str) -> pd.Series:
|
|
17
|
+
column = df[name]
|
|
18
|
+
if isinstance(column, pd.DataFrame):
|
|
19
|
+
raise TypeError(f"expected column {name!r} to be a Series, got DataFrame")
|
|
20
|
+
return column
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _series_extrema(series: pd.Series) -> tuple[float, float]:
|
|
24
|
+
return float(series.min()), float(series.max())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class VerifyCheck:
|
|
29
|
+
id: str
|
|
30
|
+
passed: bool
|
|
31
|
+
detail: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _check(id: str, passed: bool, detail: str) -> VerifyCheck:
|
|
35
|
+
return VerifyCheck(id=id, passed=passed, detail=detail)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _count_sentinel_prefix(morton_values: pd.Series) -> int:
|
|
39
|
+
count = 0
|
|
40
|
+
for value in morton_values.head(4):
|
|
41
|
+
if int(value) != 0:
|
|
42
|
+
break
|
|
43
|
+
count += 1
|
|
44
|
+
return count
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _sentinel_count_from_row_group(
|
|
48
|
+
parquet: pq.ParquetFile,
|
|
49
|
+
morton_values: pd.Series,
|
|
50
|
+
) -> int | None:
|
|
51
|
+
if parquet.num_row_groups == 0:
|
|
52
|
+
return None
|
|
53
|
+
first_group_rows = parquet.metadata.row_group(0).num_rows
|
|
54
|
+
if not 2 <= first_group_rows <= 4:
|
|
55
|
+
return None
|
|
56
|
+
first_values = morton_values.iloc[:first_group_rows]
|
|
57
|
+
if first_values.astype("int64").eq(0).all():
|
|
58
|
+
return int(first_group_rows)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def verify_morton_parquet(path: str | Path) -> list[VerifyCheck]:
|
|
63
|
+
parquet_path = Path(path)
|
|
64
|
+
checks: list[VerifyCheck] = []
|
|
65
|
+
|
|
66
|
+
if not parquet_path.is_file():
|
|
67
|
+
return [_check("file_exists", False, f"Parquet file not found: {parquet_path}")]
|
|
68
|
+
|
|
69
|
+
checks.append(_check("file_exists", True, str(parquet_path)))
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
parquet = pq.ParquetFile(parquet_path)
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
checks.append(
|
|
75
|
+
_check("parquet_readable", False, f"failed to read Parquet metadata: {exc}")
|
|
76
|
+
)
|
|
77
|
+
return checks
|
|
78
|
+
|
|
79
|
+
columns = parquet.schema_arrow.names
|
|
80
|
+
checks.append(
|
|
81
|
+
_check(
|
|
82
|
+
"column_present",
|
|
83
|
+
MORTON_CODE_2D_COLUMN in columns,
|
|
84
|
+
f"expected column {MORTON_CODE_2D_COLUMN!r} in schema",
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
checks.append(
|
|
88
|
+
_check("x_column_present", "x" in columns, "expected column 'x' in schema")
|
|
89
|
+
)
|
|
90
|
+
checks.append(
|
|
91
|
+
_check("y_column_present", "y" in columns, "expected column 'y' in schema")
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
uint_columns = [
|
|
95
|
+
name
|
|
96
|
+
for name in columns
|
|
97
|
+
if any(name.endswith(suffix) for suffix in _UINT_INTERMEDIATE_SUFFIXES)
|
|
98
|
+
]
|
|
99
|
+
checks.append(
|
|
100
|
+
_check(
|
|
101
|
+
"no_uint_intermediates",
|
|
102
|
+
not uint_columns,
|
|
103
|
+
"no uint staging columns"
|
|
104
|
+
if not uint_columns
|
|
105
|
+
else f"unexpected uint columns: {', '.join(uint_columns)}",
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
MORTON_CODE_2D_COLUMN not in columns
|
|
111
|
+
or "x" not in columns
|
|
112
|
+
or "y" not in columns
|
|
113
|
+
):
|
|
114
|
+
return checks
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
df = pd.read_parquet(parquet_path, columns=[MORTON_CODE_2D_COLUMN, "x", "y"])
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
checks.append(
|
|
120
|
+
_check(
|
|
121
|
+
"required_columns_readable",
|
|
122
|
+
False,
|
|
123
|
+
f"failed to read required columns: {exc}",
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return checks
|
|
127
|
+
|
|
128
|
+
morton_column = _dataframe_column(df, MORTON_CODE_2D_COLUMN)
|
|
129
|
+
x_column = _dataframe_column(df, "x")
|
|
130
|
+
y_column = _dataframe_column(df, "y")
|
|
131
|
+
sentinel_count = _sentinel_count_from_row_group(parquet, morton_column)
|
|
132
|
+
if sentinel_count is None:
|
|
133
|
+
sentinel_count = _count_sentinel_prefix(morton_column)
|
|
134
|
+
checks.append(
|
|
135
|
+
_check(
|
|
136
|
+
"sentinel_prefix",
|
|
137
|
+
2 <= sentinel_count <= 4,
|
|
138
|
+
f"sentinel prefix rows: {sentinel_count} (expected 2–4)",
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if sentinel_count > 0:
|
|
143
|
+
sentinel_x = x_column.iloc[:sentinel_count]
|
|
144
|
+
sentinel_y = y_column.iloc[:sentinel_count]
|
|
145
|
+
sentinel_x_min, sentinel_x_max = _series_extrema(sentinel_x)
|
|
146
|
+
dataset_x_min, dataset_x_max = _series_extrema(x_column)
|
|
147
|
+
sentinel_y_min, sentinel_y_max = _series_extrema(sentinel_y)
|
|
148
|
+
dataset_y_min, dataset_y_max = _series_extrema(y_column)
|
|
149
|
+
bbox_match = (
|
|
150
|
+
sentinel_x_min == dataset_x_min
|
|
151
|
+
and sentinel_x_max == dataset_x_max
|
|
152
|
+
and sentinel_y_min == dataset_y_min
|
|
153
|
+
and sentinel_y_max == dataset_y_max
|
|
154
|
+
)
|
|
155
|
+
checks.append(
|
|
156
|
+
_check(
|
|
157
|
+
"sentinel_bbox",
|
|
158
|
+
bbox_match,
|
|
159
|
+
"sentinel rows encode full x/y bounds"
|
|
160
|
+
if bbox_match
|
|
161
|
+
else "sentinel x/y extrema do not match dataset bounds",
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if sentinel_count < len(df):
|
|
166
|
+
tail = morton_column.iloc[sentinel_count:].astype("int64")
|
|
167
|
+
monotonic = bool((tail.diff().dropna() >= 0).all())
|
|
168
|
+
checks.append(
|
|
169
|
+
_check(
|
|
170
|
+
"morton_monotonic",
|
|
171
|
+
monotonic,
|
|
172
|
+
"morton_code_2d non-decreasing after sentinels"
|
|
173
|
+
if monotonic
|
|
174
|
+
else "morton_code_2d decreases after sentinel prefix",
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
checks.append(
|
|
179
|
+
_check(
|
|
180
|
+
"morton_monotonic",
|
|
181
|
+
True,
|
|
182
|
+
"all rows are sentinel prefix (small dataset)",
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if parquet.num_row_groups > 0:
|
|
187
|
+
first_group_rows = parquet.metadata.row_group(0).num_rows
|
|
188
|
+
sentinel_only = first_group_rows <= 4 and first_group_rows == sentinel_count
|
|
189
|
+
checks.append(
|
|
190
|
+
_check(
|
|
191
|
+
"row_group_sentinels",
|
|
192
|
+
sentinel_only,
|
|
193
|
+
f"row group 0 has {first_group_rows} rows (sentinel count {sentinel_count})",
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
checks.append(_check("row_group_sentinels", False, "parquet has no row groups"))
|
|
198
|
+
|
|
199
|
+
return checks
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def verify_multiscale_parquet(path: str | Path) -> list[VerifyCheck]:
|
|
203
|
+
parquet_path = Path(path)
|
|
204
|
+
checks: list[VerifyCheck] = []
|
|
205
|
+
|
|
206
|
+
if not parquet_path.is_file():
|
|
207
|
+
return [_check("file_exists", False, f"Parquet file not found: {parquet_path}")]
|
|
208
|
+
|
|
209
|
+
checks.append(_check("file_exists", True, str(parquet_path)))
|
|
210
|
+
|
|
211
|
+
# A verifier that raises on a corrupt file reports nothing about it; the
|
|
212
|
+
# unreadable file is itself the finding, as in `verify_morton_parquet`.
|
|
213
|
+
try:
|
|
214
|
+
schema_metadata = pq.ParquetFile(parquet_path).schema_arrow.metadata
|
|
215
|
+
except Exception as exc:
|
|
216
|
+
checks.append(_check("parquet_readable", False, f"failed to read parquet: {exc}"))
|
|
217
|
+
return checks
|
|
218
|
+
|
|
219
|
+
if schema_metadata is None or b"spatialdata_multiscale" not in schema_metadata:
|
|
220
|
+
checks.append(
|
|
221
|
+
_check(
|
|
222
|
+
"multiscale_metadata",
|
|
223
|
+
False,
|
|
224
|
+
"missing spatialdata_multiscale schema metadata",
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
return checks
|
|
228
|
+
|
|
229
|
+
stored = json.loads(schema_metadata[b"spatialdata_multiscale"])
|
|
230
|
+
checks.append(
|
|
231
|
+
_check(
|
|
232
|
+
"multiscale_metadata",
|
|
233
|
+
stored.get("format") == "spatialdata_multiscale_points",
|
|
234
|
+
f"format={stored.get('format')!r}",
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
bbox = stored.get("bounding_box")
|
|
239
|
+
has_bbox = (
|
|
240
|
+
isinstance(bbox, dict)
|
|
241
|
+
and isinstance(bbox.get("min"), list)
|
|
242
|
+
and isinstance(bbox.get("max"), list)
|
|
243
|
+
and len(bbox.get("min", [])) > 0
|
|
244
|
+
and len(bbox.get("max", [])) > 0
|
|
245
|
+
)
|
|
246
|
+
checks.append(
|
|
247
|
+
_check(
|
|
248
|
+
"multiscale_bbox",
|
|
249
|
+
has_bbox,
|
|
250
|
+
"bounding_box min/max present"
|
|
251
|
+
if has_bbox
|
|
252
|
+
else "bounding_box missing or incomplete",
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
return checks
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def verify_index_permutations_manifest(dest_zarr: str | Path) -> list[VerifyCheck]:
|
|
259
|
+
dest_path = Path(dest_zarr)
|
|
260
|
+
manifest_path = dest_path / "index-manifest.json"
|
|
261
|
+
checks: list[VerifyCheck] = []
|
|
262
|
+
|
|
263
|
+
if not manifest_path.is_file():
|
|
264
|
+
return [
|
|
265
|
+
_check(
|
|
266
|
+
"manifest_exists",
|
|
267
|
+
False,
|
|
268
|
+
f"index-manifest.json not found under {dest_path}",
|
|
269
|
+
)
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
checks.append(_check("manifest_exists", True, str(manifest_path)))
|
|
273
|
+
manifest: dict[str, Any] = json.loads(manifest_path.read_text())
|
|
274
|
+
conditions = manifest.get("conditions", [])
|
|
275
|
+
if not isinstance(conditions, list) or not conditions:
|
|
276
|
+
checks.append(_check("manifest_conditions", False, "no conditions in manifest"))
|
|
277
|
+
return checks
|
|
278
|
+
|
|
279
|
+
checks.append(
|
|
280
|
+
_check("manifest_conditions", True, f"{len(conditions)} condition(s) listed")
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
for condition in conditions:
|
|
284
|
+
if not isinstance(condition, dict):
|
|
285
|
+
continue
|
|
286
|
+
condition_id = condition.get("id", "unknown")
|
|
287
|
+
element_path = condition.get("element_path")
|
|
288
|
+
if not isinstance(element_path, str):
|
|
289
|
+
checks.append(
|
|
290
|
+
_check(
|
|
291
|
+
f"path_{condition_id}",
|
|
292
|
+
False,
|
|
293
|
+
"condition missing element_path",
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
parquet_path = dest_path / element_path / "points.parquet"
|
|
299
|
+
if not parquet_path.is_file() and not parquet_path.is_dir():
|
|
300
|
+
checks.append(
|
|
301
|
+
_check(
|
|
302
|
+
f"path_{condition_id}",
|
|
303
|
+
False,
|
|
304
|
+
f"missing output: {parquet_path}",
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
checks.append(_check(f"path_{condition_id}", True, str(parquet_path)))
|
|
310
|
+
|
|
311
|
+
tiling_kind = condition.get("tiling_kind")
|
|
312
|
+
if tiling_kind == "morton-points":
|
|
313
|
+
if parquet_path.is_file():
|
|
314
|
+
for morton_check in verify_morton_parquet(parquet_path):
|
|
315
|
+
checks.append(
|
|
316
|
+
VerifyCheck(
|
|
317
|
+
id=f"{condition_id}_{morton_check.id}",
|
|
318
|
+
passed=morton_check.passed,
|
|
319
|
+
detail=morton_check.detail,
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
# A directory dataset passes the path check above, so without this
|
|
324
|
+
# the manifest would report `all_passed` for a Morton condition
|
|
325
|
+
# whose ordering was never looked at.
|
|
326
|
+
checks.append(
|
|
327
|
+
_check(
|
|
328
|
+
f"{condition_id}_morton_verified",
|
|
329
|
+
False,
|
|
330
|
+
f"morton ordering not verified: {parquet_path} is a directory dataset",
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return checks
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def all_passed(checks: list[VerifyCheck]) -> bool:
|
|
338
|
+
return bool(checks) and all(check.passed for check in checks)
|