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.
Files changed (40) hide show
  1. spatialdata_js_util/__init__.py +107 -0
  2. spatialdata_js_util/cli.py +803 -0
  3. spatialdata_js_util/codecs/__init__.py +67 -0
  4. spatialdata_js_util/codecs/backends.py +250 -0
  5. spatialdata_js_util/codecs/chunks.py +40 -0
  6. spatialdata_js_util/codecs/encoding.py +154 -0
  7. spatialdata_js_util/codecs/htj2k_wasm.py +266 -0
  8. spatialdata_js_util/codecs/names.py +26 -0
  9. spatialdata_js_util/codecs/probe/multicomponent.j2c +0 -0
  10. spatialdata_js_util/codecs/probe/multicomponent.json +3 -0
  11. spatialdata_js_util/codecs/probe/multicomponent.npy +0 -0
  12. spatialdata_js_util/codecs/vendor/__init__.py +1 -0
  13. spatialdata_js_util/codecs/vendor/encode-plane.mjs +164 -0
  14. spatialdata_js_util/codecs/vendor/openjph/index.mjs +131 -0
  15. spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph.mjs +2 -0
  16. spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph.wasm +0 -0
  17. spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph_simd.mjs +2 -0
  18. spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph_simd.wasm +0 -0
  19. spatialdata_js_util/codecs/zarr_codec.py +149 -0
  20. spatialdata_js_util/errors.py +5 -0
  21. spatialdata_js_util/images.py +849 -0
  22. spatialdata_js_util/index_permutations.py +311 -0
  23. spatialdata_js_util/points.py +445 -0
  24. spatialdata_js_util/points_zarr.py +799 -0
  25. spatialdata_js_util/provenance.py +17 -0
  26. spatialdata_js_util/pyramids.py +318 -0
  27. spatialdata_js_util/runners.py +458 -0
  28. spatialdata_js_util/store.py +305 -0
  29. spatialdata_js_util/tables.py +310 -0
  30. spatialdata_js_util/tui/__init__.py +0 -0
  31. spatialdata_js_util/tui/app.py +112 -0
  32. spatialdata_js_util/tui/models.py +45 -0
  33. spatialdata_js_util/tui/screens.py +1255 -0
  34. spatialdata_js_util/tui/widgets.py +50 -0
  35. spatialdata_js_util/verify.py +338 -0
  36. spatialdata_js_util-0.1.0.dist-info/METADATA +412 -0
  37. spatialdata_js_util-0.1.0.dist-info/RECORD +40 -0
  38. spatialdata_js_util-0.1.0.dist-info/WHEEL +5 -0
  39. spatialdata_js_util-0.1.0.dist-info/entry_points.txt +7 -0
  40. spatialdata_js_util-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,458 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import sys
6
+ from collections.abc import Callable, Iterator
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any, Sequence, TypeVar
10
+
11
+ import pandas as pd
12
+ import pyarrow.dataset as ds
13
+
14
+ from .errors import WriterCommandError
15
+ from .index_permutations import (
16
+ DEFAULT_CONDITIONS,
17
+ FEATURE_SELECTIVITY_CONDITIONS,
18
+ IndexCondition,
19
+ write_index_permutations,
20
+ )
21
+ from .points import EncodingPolicy
22
+ from .points_zarr import (
23
+ DEFAULT_CHUNK_ROWS,
24
+ DEFAULT_SHARD_CHUNKS,
25
+ SIDECAR_GROUP,
26
+ PointsSource,
27
+ add_points_zarr_to_store,
28
+ write_points_zarr_store,
29
+ )
30
+ from .points import (
31
+ build_spatialdata_multiscale_metadata,
32
+ write_morton_points_parquet,
33
+ write_multiscale_points_parquet,
34
+ )
35
+ from .store import (
36
+ copy_points_element_metadata,
37
+ experimental_points_output_path,
38
+ list_points_keys,
39
+ points_parquet_path,
40
+ read_points_dataframe,
41
+ read_points_element_attrs,
42
+ register_points_elements_in_consolidated_metadata,
43
+ validate_points_key,
44
+ )
45
+
46
+ _T = TypeVar("_T")
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class MortonZarrOutput:
51
+ source_parquet: Path
52
+ output_parquet: Path
53
+ in_place: bool
54
+ output_points_key: str | None
55
+ collection: str
56
+
57
+
58
+ def _as_command_error(action: Callable[[], _T]) -> _T:
59
+ try:
60
+ return action()
61
+ except WriterCommandError:
62
+ raise
63
+ except (FileNotFoundError, ValueError) as exc:
64
+ raise WriterCommandError(str(exc)) from exc
65
+
66
+
67
+ def read_input_dataframe(path: str | Path) -> pd.DataFrame:
68
+ input_path = Path(path)
69
+ if input_path.is_dir():
70
+ return read_points_dataframe(input_path)
71
+ suffix = input_path.suffix.lower()
72
+ if suffix == ".csv":
73
+ return pd.read_csv(input_path)
74
+ if suffix in {".parquet", ".pq"}:
75
+ return pd.read_parquet(input_path)
76
+ raise WriterCommandError(
77
+ f"Unsupported input: {path}\n"
78
+ "Expected a .csv file, .parquet file, or a directory of Parquet parts."
79
+ )
80
+
81
+
82
+ def run_list_points(zarr: str | Path) -> dict[str, Any]:
83
+ keys = list_points_keys(zarr)
84
+ if not keys:
85
+ raise WriterCommandError(f"No Points elements found under {Path(zarr) / 'points'}")
86
+ return {"zarr": str(zarr), "points_keys": keys}
87
+
88
+
89
+ def run_morton_points(
90
+ input_path: str | Path,
91
+ output_path: str | Path,
92
+ *,
93
+ feature_key: str | None = None,
94
+ row_group_size: int = 50_000,
95
+ compression: str = "zstd",
96
+ ) -> dict[str, Any]:
97
+ df = _as_command_error(lambda: read_input_dataframe(input_path))
98
+ sorted_df = _as_command_error(
99
+ lambda: write_morton_points_parquet(
100
+ df,
101
+ output_path,
102
+ feature_key=feature_key,
103
+ row_group_size=row_group_size,
104
+ compression=compression,
105
+ )
106
+ )
107
+ return {
108
+ "format": "morton-points",
109
+ "rows": int(len(sorted_df)),
110
+ "output": str(output_path),
111
+ "row_group_size": row_group_size,
112
+ }
113
+
114
+
115
+ def run_points_zarr(
116
+ input_path: str | Path,
117
+ output_store: str | Path,
118
+ *,
119
+ feature_key: str,
120
+ coordinates: Sequence[str] = ("x", "y"),
121
+ points_key: str = "transcripts",
122
+ tile_size: float = 100.0,
123
+ n_levels: int | None = None,
124
+ seed: int = 0,
125
+ chunk_rows: int = DEFAULT_CHUNK_ROWS,
126
+ shard_chunks: int = DEFAULT_SHARD_CHUNKS,
127
+ overwrite: bool = False,
128
+ ) -> dict[str, Any]:
129
+ if Path(output_store).exists() and not overwrite:
130
+ raise WriterCommandError(
131
+ f"Output already exists: {output_store}\nChoose another path or enable overwrite."
132
+ )
133
+ source = _as_command_error(
134
+ lambda: _points_source(input_path, [*coordinates, feature_key])
135
+ )
136
+ return _as_command_error(
137
+ lambda: write_points_zarr_store(
138
+ source,
139
+ output_store,
140
+ points_key=points_key,
141
+ overwrite=overwrite,
142
+ feature_key=feature_key,
143
+ coordinates=coordinates,
144
+ tile_size=tile_size,
145
+ n_levels=n_levels,
146
+ seed=seed,
147
+ chunk_rows=chunk_rows,
148
+ shard_chunks=shard_chunks,
149
+ progress=lambda message: print(message, file=sys.stderr, flush=True),
150
+ )
151
+ )
152
+
153
+
154
+ def run_add_points_zarr(
155
+ store: str | Path,
156
+ *,
157
+ points_key: str,
158
+ tile_size: float = 100.0,
159
+ n_levels: int | None = None,
160
+ seed: int = 0,
161
+ chunk_rows: int = DEFAULT_CHUNK_ROWS,
162
+ shard_chunks: int = DEFAULT_SHARD_CHUNKS,
163
+ overwrite: bool = False,
164
+ ) -> dict[str, Any]:
165
+ existing = Path(store) / "points" / points_key / SIDECAR_GROUP
166
+ if existing.exists() and not overwrite:
167
+ raise WriterCommandError(
168
+ f"Output already exists: {existing}\nChoose another element or enable overwrite."
169
+ )
170
+ return _as_command_error(
171
+ lambda: add_points_zarr_to_store(
172
+ None,
173
+ store,
174
+ points_key=points_key,
175
+ overwrite=overwrite,
176
+ tile_size=tile_size,
177
+ n_levels=n_levels,
178
+ seed=seed,
179
+ chunk_rows=chunk_rows,
180
+ shard_chunks=shard_chunks,
181
+ progress=lambda message: print(message, file=sys.stderr, flush=True),
182
+ )
183
+ )
184
+
185
+
186
+ def _points_source(path: str | Path, columns: list[str]) -> pd.DataFrame | PointsSource:
187
+ """Parquet is streamed a batch at a time, so it need not fit in memory; CSV is read whole."""
188
+ input_path = Path(path)
189
+ if input_path.suffix.lower() == ".csv":
190
+ return read_input_dataframe(input_path)
191
+ if not input_path.exists():
192
+ raise FileNotFoundError(f"Input not found: {input_path}")
193
+ dataset = ds.dataset(input_path, format="parquet")
194
+
195
+ def batches() -> Iterator[pd.DataFrame]:
196
+ for batch in dataset.to_batches(columns=columns, batch_size=4_000_000):
197
+ yield batch.to_pandas()
198
+
199
+ return batches
200
+
201
+
202
+ def run_multiscale_points(
203
+ input_path: str | Path,
204
+ output_path: str | Path,
205
+ *,
206
+ metadata_json: str | Path | None = None,
207
+ row_group_size: int = 50_000,
208
+ compression: str = "zstd",
209
+ ) -> dict[str, Any]:
210
+ df = _as_command_error(lambda: read_input_dataframe(input_path))
211
+ if metadata_json:
212
+ metadata = _as_command_error(lambda: json.loads(Path(metadata_json).read_text()))
213
+ else:
214
+ metadata = _as_command_error(lambda: build_spatialdata_multiscale_metadata(df))
215
+ _as_command_error(
216
+ lambda: write_multiscale_points_parquet(
217
+ df,
218
+ output_path,
219
+ metadata=metadata,
220
+ row_group_size=row_group_size,
221
+ compression=compression,
222
+ )
223
+ )
224
+ return {
225
+ "format": "spatialdata_multiscale_points",
226
+ "rows": int(len(df)),
227
+ "output": str(output_path),
228
+ "row_group_size": row_group_size,
229
+ }
230
+
231
+
232
+ def resolve_morton_from_zarr_output(
233
+ zarr_path: Path,
234
+ points_key: str,
235
+ *,
236
+ output: str | Path | None = None,
237
+ output_points_key: str | None = None,
238
+ experimental: bool = False,
239
+ ) -> MortonZarrOutput:
240
+ if output is not None and output_points_key is not None:
241
+ raise WriterCommandError("Pass either output or output_points_key, not both.")
242
+ source_parquet = points_parquet_path(zarr_path, points_key)
243
+ if output:
244
+ resolved_output = Path(output)
245
+ return MortonZarrOutput(
246
+ source_parquet=source_parquet,
247
+ output_parquet=resolved_output,
248
+ in_place=resolved_output == source_parquet,
249
+ output_points_key=None,
250
+ collection="path",
251
+ )
252
+ if experimental:
253
+ resolved_output_key = validate_points_key(output_points_key or points_key)
254
+ resolved_output = experimental_points_output_path(zarr_path, resolved_output_key)
255
+ return MortonZarrOutput(
256
+ source_parquet=source_parquet,
257
+ output_parquet=resolved_output,
258
+ in_place=False,
259
+ output_points_key=resolved_output_key,
260
+ collection="points.experimental",
261
+ )
262
+ resolved_output_key = validate_points_key(output_points_key or points_key)
263
+ resolved_output = points_parquet_path(zarr_path, resolved_output_key)
264
+ return MortonZarrOutput(
265
+ source_parquet=source_parquet,
266
+ output_parquet=resolved_output,
267
+ in_place=resolved_output == source_parquet,
268
+ output_points_key=resolved_output_key,
269
+ collection="points",
270
+ )
271
+
272
+
273
+ def run_morton_points_from_zarr(
274
+ zarr: str | Path,
275
+ *,
276
+ points_key: str | None = None,
277
+ experimental: bool = False,
278
+ output: str | Path | None = None,
279
+ output_points_key: str | None = None,
280
+ feature_key: str | None = None,
281
+ overwrite: bool = False,
282
+ row_group_size: int = 50_000,
283
+ compression: str = "zstd",
284
+ ) -> dict[str, Any]:
285
+ zarr_path = Path(zarr)
286
+ keys = list_points_keys(zarr_path)
287
+ if not keys:
288
+ raise WriterCommandError(f"No Points elements found under {zarr_path / 'points'}")
289
+
290
+ resolved_key = points_key
291
+ if resolved_key is None:
292
+ if len(keys) == 1:
293
+ resolved_key = keys[0]
294
+ else:
295
+ raise WriterCommandError(
296
+ "Multiple Points elements found; pass points_key. "
297
+ f"Available keys: {', '.join(keys)}"
298
+ )
299
+ if resolved_key not in keys:
300
+ raise WriterCommandError(
301
+ f"Unknown Points element {resolved_key!r}. Available: {', '.join(keys)}"
302
+ )
303
+
304
+ attrs = _as_command_error(lambda: read_points_element_attrs(zarr_path, resolved_key))
305
+ resolved_feature_key = feature_key or attrs.get("feature_key")
306
+ output_spec = resolve_morton_from_zarr_output(
307
+ zarr_path,
308
+ resolved_key,
309
+ output=output,
310
+ output_points_key=output_points_key,
311
+ experimental=experimental,
312
+ )
313
+ if (
314
+ output_spec.output_parquet.exists()
315
+ and not output_spec.in_place
316
+ and not overwrite
317
+ ):
318
+ raise WriterCommandError(
319
+ f"Output already exists: {output_spec.output_parquet}\n"
320
+ "Choose another element name/path or enable overwrite."
321
+ )
322
+ if (
323
+ output_spec.collection == "points"
324
+ and output_spec.output_points_key is not None
325
+ and output_spec.output_points_key != resolved_key
326
+ ):
327
+ target_element_dir = zarr_path / "points" / output_spec.output_points_key
328
+ if target_element_dir.exists() and not overwrite:
329
+ raise WriterCommandError(
330
+ f"Points element already exists: points/{output_spec.output_points_key}\n"
331
+ "Choose another element name or enable overwrite."
332
+ )
333
+
334
+ df = _as_command_error(lambda: read_points_dataframe(output_spec.source_parquet))
335
+ if output_spec.output_parquet.exists():
336
+ if output_spec.output_parquet.is_dir():
337
+ shutil.rmtree(output_spec.output_parquet)
338
+ else:
339
+ output_spec.output_parquet.unlink()
340
+ if (
341
+ output_spec.collection == "points"
342
+ and output_spec.output_points_key is not None
343
+ and output_spec.output_points_key != resolved_key
344
+ ):
345
+ _as_command_error(
346
+ lambda: copy_points_element_metadata(
347
+ zarr_path,
348
+ source_key=resolved_key,
349
+ dest_key=output_spec.output_points_key,
350
+ )
351
+ )
352
+ sorted_df = _as_command_error(
353
+ lambda: write_morton_points_parquet(
354
+ df,
355
+ output_spec.output_parquet,
356
+ feature_key=resolved_feature_key,
357
+ row_group_size=row_group_size,
358
+ compression=compression,
359
+ )
360
+ )
361
+ if (
362
+ output_spec.collection == "points"
363
+ and output_spec.output_points_key is not None
364
+ and output_spec.output_points_key != resolved_key
365
+ ):
366
+ _as_command_error(
367
+ lambda: register_points_elements_in_consolidated_metadata(
368
+ zarr_path,
369
+ [resolved_key, output_spec.output_points_key],
370
+ template_key=resolved_key,
371
+ )
372
+ )
373
+ return {
374
+ "format": "morton-points",
375
+ "zarr": str(zarr_path),
376
+ "points_key": resolved_key,
377
+ "output_points_key": output_spec.output_points_key,
378
+ "output_collection": output_spec.collection,
379
+ "source": str(output_spec.source_parquet),
380
+ "output": str(output_spec.output_parquet),
381
+ "in_place": output_spec.in_place,
382
+ "feature_key": resolved_feature_key,
383
+ "rows": int(len(sorted_df)),
384
+ "row_group_size": row_group_size,
385
+ }
386
+
387
+
388
+ def run_write_index_permutations(
389
+ source_zarr: str | Path,
390
+ dest_zarr: str | Path,
391
+ *,
392
+ points_key: str | None = None,
393
+ max_rows: int | None = None,
394
+ condition_ids: Sequence[str] | None = None,
395
+ overwrite: bool = False,
396
+ row_group_size: int = 50_000,
397
+ compression: str = "zstd",
398
+ encodings: EncodingPolicy = "auto",
399
+ write_page_index: bool = True,
400
+ ) -> dict[str, Any]:
401
+ selected: tuple[IndexCondition, ...] | None = None
402
+ if condition_ids:
403
+ by_id = {
404
+ condition.id: condition
405
+ for condition in (*DEFAULT_CONDITIONS, *FEATURE_SELECTIVITY_CONDITIONS)
406
+ }
407
+ missing = [value for value in condition_ids if value not in by_id]
408
+ if missing:
409
+ raise WriterCommandError(f"Unknown conditions: {', '.join(missing)}")
410
+ selected = tuple(by_id[value] for value in condition_ids)
411
+
412
+ return _as_command_error(
413
+ lambda: write_index_permutations(
414
+ source_zarr,
415
+ dest_zarr,
416
+ points_key=points_key,
417
+ max_rows=max_rows,
418
+ conditions=selected,
419
+ overwrite=overwrite,
420
+ row_group_size=row_group_size,
421
+ compression=compression,
422
+ encodings=encodings,
423
+ write_page_index=write_page_index,
424
+ )
425
+ )
426
+
427
+
428
+ def run_tables_to_csc(
429
+ source: str | Path,
430
+ dest: str | Path | None = None,
431
+ *,
432
+ tables: Sequence[str] | None = None,
433
+ layers: bool = True,
434
+ densify: bool = False,
435
+ overwrite: bool = False,
436
+ ) -> dict[str, Any]:
437
+ from .tables import convert_store_tables_to_csc
438
+
439
+ result = _as_command_error(
440
+ lambda: convert_store_tables_to_csc(
441
+ source,
442
+ dest,
443
+ tables=tables,
444
+ layers=layers,
445
+ densify=densify,
446
+ overwrite=overwrite,
447
+ )
448
+ )
449
+ return result.manifest
450
+
451
+
452
+ def run_list_tables(zarr: str | Path) -> dict[str, Any]:
453
+ from .store import list_element_keys
454
+
455
+ keys = list_element_keys(zarr, "tables")
456
+ if not keys:
457
+ raise WriterCommandError(f"No table elements found under {Path(zarr) / 'tables'}")
458
+ return {"zarr": str(zarr), "table_keys": keys}