spatialscape 0.3.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.
Files changed (83) hide show
  1. spatialscape-0.3.0/.gitignore +28 -0
  2. spatialscape-0.3.0/LICENSE +21 -0
  3. spatialscape-0.3.0/PKG-INFO +68 -0
  4. spatialscape-0.3.0/README.md +19 -0
  5. spatialscape-0.3.0/pyproject.toml +81 -0
  6. spatialscape-0.3.0/scripts/bundle_app.py +39 -0
  7. spatialscape-0.3.0/scripts/gen_brand.py +83 -0
  8. spatialscape-0.3.0/scripts/gen_cli_docs.py +247 -0
  9. spatialscape-0.3.0/src/spatialscape/__init__.py +13 -0
  10. spatialscape-0.3.0/src/spatialscape/_app/assets/array-utils-flat-BL8Cekj1.js +13 -0
  11. spatialscape-0.3.0/src/spatialscape/_app/assets/blosc-DEOJs6D5.js +18 -0
  12. spatialscape-0.3.0/src/spatialscape/_app/assets/blosc-Df7TknHH.js +18 -0
  13. spatialscape-0.3.0/src/spatialscape/_app/assets/chunk-INHXZS53-DAOLzMPX.js +2 -0
  14. spatialscape-0.3.0/src/spatialscape/_app/assets/chunk-INHXZS53-DfBSTIxr.js +2 -0
  15. spatialscape-0.3.0/src/spatialscape/_app/assets/deflate-B2AbjNo_.js +2 -0
  16. spatialscape-0.3.0/src/spatialscape/_app/assets/expression-E359vKZW.js +368 -0
  17. spatialscape-0.3.0/src/spatialscape/_app/assets/get-attribute-from-layouts-Dd2gixpo.js +2 -0
  18. spatialscape-0.3.0/src/spatialscape/_app/assets/index-BKPnXJSt.css +1 -0
  19. spatialscape-0.3.0/src/spatialscape/_app/assets/index-Gxpj1e1n.js +8454 -0
  20. spatialscape-0.3.0/src/spatialscape/_app/assets/jpeg-b0MwBR_9.js +2 -0
  21. spatialscape-0.3.0/src/spatialscape/_app/assets/lerc-ujbbHc0C.js +2 -0
  22. spatialscape-0.3.0/src/spatialscape/_app/assets/lz4-BFJkNc3o.js +16 -0
  23. spatialscape-0.3.0/src/spatialscape/_app/assets/lz4-DNE3IovK.js +16 -0
  24. spatialscape-0.3.0/src/spatialscape/_app/assets/lzw-DfRkoFB0.js +2 -0
  25. spatialscape-0.3.0/src/spatialscape/_app/assets/packbits-BDCGuk_Z.js +2 -0
  26. spatialscape-0.3.0/src/spatialscape/_app/assets/pako.esm-Ch1S5qKo.js +2 -0
  27. spatialscape-0.3.0/src/spatialscape/_app/assets/raw-vNZ-QFXZ.js +2 -0
  28. spatialscape-0.3.0/src/spatialscape/_app/assets/webgl-D2Fkccwd.js +345 -0
  29. spatialscape-0.3.0/src/spatialscape/_app/assets/webimage-DDU0IC05.js +2 -0
  30. spatialscape-0.3.0/src/spatialscape/_app/assets/worker-CUy87eWB.js +8 -0
  31. spatialscape-0.3.0/src/spatialscape/_app/assets/zstd-CGD_7wnU.js +16 -0
  32. spatialscape-0.3.0/src/spatialscape/_app/assets/zstd-qXhZ_1DN.js +16 -0
  33. spatialscape-0.3.0/src/spatialscape/_app/favicon.svg +161 -0
  34. spatialscape-0.3.0/src/spatialscape/_app/index.html +23 -0
  35. spatialscape-0.3.0/src/spatialscape/build.py +630 -0
  36. spatialscape-0.3.0/src/spatialscape/cli.py +399 -0
  37. spatialscape-0.3.0/src/spatialscape/config.py +570 -0
  38. spatialscape-0.3.0/src/spatialscape/convert.py +321 -0
  39. spatialscape-0.3.0/src/spatialscape/discover.py +98 -0
  40. spatialscape-0.3.0/src/spatialscape/expression.py +300 -0
  41. spatialscape-0.3.0/src/spatialscape/fingerprint.py +152 -0
  42. spatialscape-0.3.0/src/spatialscape/genes.py +251 -0
  43. spatialscape-0.3.0/src/spatialscape/geometry.py +132 -0
  44. spatialscape-0.3.0/src/spatialscape/images.py +240 -0
  45. spatialscape-0.3.0/src/spatialscape/init.py +578 -0
  46. spatialscape-0.3.0/src/spatialscape/manifest.py +162 -0
  47. spatialscape-0.3.0/src/spatialscape/obs.py +48 -0
  48. spatialscape-0.3.0/src/spatialscape/outlines.py +145 -0
  49. spatialscape-0.3.0/src/spatialscape/polygons.py +85 -0
  50. spatialscape-0.3.0/src/spatialscape/rds.py +609 -0
  51. spatialscape-0.3.0/src/spatialscape/rdsread.py +556 -0
  52. spatialscape-0.3.0/src/spatialscape/readers.py +195 -0
  53. spatialscape-0.3.0/src/spatialscape/registry.py +83 -0
  54. spatialscape-0.3.0/src/spatialscape/serve.py +262 -0
  55. spatialscape-0.3.0/src/spatialscape/session.py +474 -0
  56. spatialscape-0.3.0/src/spatialscape/site.py +274 -0
  57. spatialscape-0.3.0/src/spatialscape/synthetic.py +142 -0
  58. spatialscape-0.3.0/src/spatialscape/thumbnail.py +95 -0
  59. spatialscape-0.3.0/src/spatialscape/tracks.py +551 -0
  60. spatialscape-0.3.0/src/spatialscape/validate.py +193 -0
  61. spatialscape-0.3.0/src/spatialscape/vocab.py +188 -0
  62. spatialscape-0.3.0/tests/conftest.py +25 -0
  63. spatialscape-0.3.0/tests/fixtures/make_tiny_seurat.R +39 -0
  64. spatialscape-0.3.0/tests/fixtures/tiny_sce.rds +0 -0
  65. spatialscape-0.3.0/tests/fixtures/tiny_seurat.rds +0 -0
  66. spatialscape-0.3.0/tests/fixtures/tiny_spe.rda +0 -0
  67. spatialscape-0.3.0/tests/fixtures/tiny_spe.rds +0 -0
  68. spatialscape-0.3.0/tests/test_build.py +215 -0
  69. spatialscape-0.3.0/tests/test_cli.py +161 -0
  70. spatialscape-0.3.0/tests/test_config.py +103 -0
  71. spatialscape-0.3.0/tests/test_expression.py +83 -0
  72. spatialscape-0.3.0/tests/test_fast_build.py +156 -0
  73. spatialscape-0.3.0/tests/test_genes.py +118 -0
  74. spatialscape-0.3.0/tests/test_geometry.py +33 -0
  75. spatialscape-0.3.0/tests/test_incremental.py +127 -0
  76. spatialscape-0.3.0/tests/test_init_menu.py +180 -0
  77. spatialscape-0.3.0/tests/test_outlines.py +25 -0
  78. spatialscape-0.3.0/tests/test_polygons.py +76 -0
  79. spatialscape-0.3.0/tests/test_rds.py +233 -0
  80. spatialscape-0.3.0/tests/test_review_fixes.py +221 -0
  81. spatialscape-0.3.0/tests/test_serve_site.py +233 -0
  82. spatialscape-0.3.0/tests/test_show.py +173 -0
  83. spatialscape-0.3.0/tests/test_tracks.py +271 -0
@@ -0,0 +1,28 @@
1
+ node_modules/
2
+ dist/
3
+ .venv/
4
+ __pycache__/
5
+ *.pyc
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ *.egg-info/
9
+ build/
10
+ app/test-results/
11
+ app/playwright-report/
12
+ .DS_Store
13
+ .npmrc
14
+ .claude/
15
+ docs/.vitepress/cache/
16
+ docs/.vitepress/dist/
17
+ app/tsconfig.tsbuildinfo
18
+ # real-data bundles and their source exports stay local (large, derived from cluster data)
19
+ examples/*/
20
+ !examples/synthetic/
21
+ !examples/synthetic-src/
22
+ !examples/scripts/
23
+ !examples/palettes/
24
+ # viewer build copied into the package by python/scripts/bundle_app.py
25
+ python/src/spatialscape/_app/
26
+ python/dist/
27
+ # per-machine registry overlay used by the vite dev server
28
+ app/public/datasets.local.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Totty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.5
2
+ Name: spatialscape
3
+ Version: 0.3.0
4
+ Summary: Build static spatialscape viewer bundles from AnnData / SpatialData
5
+ Project-URL: Homepage, https://mictott.github.io/spatialscape/docs/
6
+ Project-URL: Documentation, https://mictott.github.io/spatialscape/docs/
7
+ Project-URL: Repository, https://github.com/mictott/spatialscape
8
+ Project-URL: Issues, https://github.com/mictott/spatialscape/issues
9
+ Project-URL: Changelog, https://github.com/mictott/spatialscape/blob/main/CHANGELOG.md
10
+ Author-email: Michael Totty <mictott@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: deck.gl,single-cell,spatial-transcriptomics,visium,visualization,xenium,zarr
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Web Environment
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
23
+ Classifier: Topic :: Scientific/Engineering :: Visualization
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: anndata>=0.12
26
+ Requires-Dist: imageio>=2.34
27
+ Requires-Dist: numcodecs>=0.15
28
+ Requires-Dist: numpy>=1.26
29
+ Requires-Dist: pandas>=2.0
30
+ Requires-Dist: pillow>=10
31
+ Requires-Dist: pyarrow>=15
32
+ Requires-Dist: pydantic>=2.6
33
+ Requires-Dist: pyyaml>=6
34
+ Requires-Dist: scikit-image>=0.22
35
+ Requires-Dist: scipy>=1.11
36
+ Requires-Dist: tifffile>=2024.1
37
+ Requires-Dist: typer>=0.12
38
+ Requires-Dist: zarr>=3.0
39
+ Provides-Extra: dev
40
+ Requires-Dist: build>=1.2; extra == 'dev'
41
+ Requires-Dist: h5py>=3.10; extra == 'dev'
42
+ Requires-Dist: pytest>=8; extra == 'dev'
43
+ Requires-Dist: ruff>=0.5; extra == 'dev'
44
+ Provides-Extra: spatialdata
45
+ Requires-Dist: spatialdata>=0.4; extra == 'spatialdata'
46
+ Provides-Extra: tracks
47
+ Requires-Dist: pybigwig>=0.3.18; extra == 'tracks'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # spatialscape (Python)
51
+
52
+ The command-line tool behind **spatialscape**, a static, reactive browser for spatial transcriptomics and
53
+ snRNA-seq. It turns a saved R object (`SpatialExperiment` / `SingleCellExperiment` as `.rds` or `.rda`, parsed in
54
+ Python) or AnnData / SpatialData files into viewer bundles (sharded Zarr v3 expression, observation arrays in
55
+ microns, OME-Zarr images, domain outlines, cell polygons), and ships the viewer itself so you can open them locally
56
+ or publish them as a website.
57
+
58
+ ```bash
59
+ pip install spatialscape
60
+ spatialscape init analysis/spe_dlpfc.rds --id dlpfc # writes dlpfc.yaml, a menu of what the data contains
61
+ # edit dlpfc.yaml: keep the annotations you want, name them, pick colors and the opening gene
62
+ spatialscape build dlpfc.yaml # -> bundles/dlpfc; rerun after any edit
63
+ spatialscape serve bundles --open # viewer + data at http://127.0.0.1:8787/
64
+ spatialscape site build bundles -o site # folder to upload to any static host
65
+ ```
66
+
67
+ `sscape` is a short alias for the same command. Documentation: https://mictott.github.io/spatialscape/docs/
68
+ Source and issues: https://github.com/MicTott/spatialscape
@@ -0,0 +1,19 @@
1
+ # spatialscape (Python)
2
+
3
+ The command-line tool behind **spatialscape**, a static, reactive browser for spatial transcriptomics and
4
+ snRNA-seq. It turns a saved R object (`SpatialExperiment` / `SingleCellExperiment` as `.rds` or `.rda`, parsed in
5
+ Python) or AnnData / SpatialData files into viewer bundles (sharded Zarr v3 expression, observation arrays in
6
+ microns, OME-Zarr images, domain outlines, cell polygons), and ships the viewer itself so you can open them locally
7
+ or publish them as a website.
8
+
9
+ ```bash
10
+ pip install spatialscape
11
+ spatialscape init analysis/spe_dlpfc.rds --id dlpfc # writes dlpfc.yaml, a menu of what the data contains
12
+ # edit dlpfc.yaml: keep the annotations you want, name them, pick colors and the opening gene
13
+ spatialscape build dlpfc.yaml # -> bundles/dlpfc; rerun after any edit
14
+ spatialscape serve bundles --open # viewer + data at http://127.0.0.1:8787/
15
+ spatialscape site build bundles -o site # folder to upload to any static host
16
+ ```
17
+
18
+ `sscape` is a short alias for the same command. Documentation: https://mictott.github.io/spatialscape/docs/
19
+ Source and issues: https://github.com/MicTott/spatialscape
@@ -0,0 +1,81 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "spatialscape"
7
+ dynamic = ["version"]
8
+ description = "Build static spatialscape viewer bundles from AnnData / SpatialData"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "Michael Totty", email = "mictott@gmail.com" }]
13
+ keywords = ["spatial-transcriptomics", "visium", "xenium", "single-cell", "visualization", "zarr", "deck.gl"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Science/Research",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
23
+ "Topic :: Scientific/Engineering :: Visualization",
24
+ "Environment :: Web Environment",
25
+ ]
26
+ dependencies = [
27
+ "anndata>=0.12",
28
+ "zarr>=3.0",
29
+ "numcodecs>=0.15",
30
+ "numpy>=1.26",
31
+ "scipy>=1.11",
32
+ "pandas>=2.0",
33
+ "pyarrow>=15",
34
+ "typer>=0.12",
35
+ "pydantic>=2.6",
36
+ "pyyaml>=6",
37
+ "imageio>=2.34",
38
+ "tifffile>=2024.1",
39
+ "scikit-image>=0.22",
40
+ "pillow>=10",
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://mictott.github.io/spatialscape/docs/"
45
+ Documentation = "https://mictott.github.io/spatialscape/docs/"
46
+ Repository = "https://github.com/mictott/spatialscape"
47
+ Issues = "https://github.com/mictott/spatialscape/issues"
48
+ Changelog = "https://github.com/mictott/spatialscape/blob/main/CHANGELOG.md"
49
+
50
+ [project.optional-dependencies]
51
+ spatialdata = ["spatialdata>=0.4"]
52
+ tracks = ["pyBigWig>=0.3.18"]
53
+ dev = ["pytest>=8", "ruff>=0.5", "h5py>=3.10", "build>=1.2"]
54
+
55
+ [project.scripts]
56
+ spatialscape = "spatialscape.cli:main"
57
+ sscape = "spatialscape.cli:main"
58
+
59
+ [tool.hatch.version]
60
+ path = "src/spatialscape/__init__.py"
61
+
62
+ [tool.hatch.build]
63
+ # the viewer build is generated (python/scripts/bundle_app.py) and gitignored, so list it explicitly
64
+ artifacts = ["src/spatialscape/_app/**"]
65
+
66
+ [tool.hatch.build.targets.wheel]
67
+ packages = ["src/spatialscape"]
68
+
69
+ [tool.ruff]
70
+ line-length = 120
71
+ target-version = "py311"
72
+
73
+ [tool.ruff.lint]
74
+ select = ["E", "F", "I", "B", "UP", "SIM", "PIE", "C4", "RUF"]
75
+ ignore = ["E501", "B008", "SIM108", "RUF012", "RUF005"]
76
+
77
+ [tool.ruff.lint.per-file-ignores]
78
+ "tests/*" = ["B011"]
79
+
80
+ [tool.pytest.ini_options]
81
+ testpaths = ["tests"]
@@ -0,0 +1,39 @@
1
+ """Copy the built viewer (app/dist) into the Python package so the wheel ships it.
2
+
3
+ npm run build && python python/scripts/bundle_app.py
4
+
5
+ Only index.html and assets/ are copied: no source maps, no registry, no site drafts. The copy lives at
6
+ src/spatialscape/_app (gitignored) and is picked up by `spatialscape serve` and `spatialscape site build`.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ ROOT = Path(__file__).resolve().parents[2]
15
+ DIST = ROOT / "app" / "dist"
16
+ DEST = ROOT / "python" / "src" / "spatialscape" / "_app"
17
+
18
+
19
+ def main() -> None:
20
+ if not (DIST / "index.html").exists():
21
+ sys.exit(f"{DIST} has no build; run `npm run build` first")
22
+ if DEST.exists():
23
+ shutil.rmtree(DEST)
24
+ (DEST / "assets").mkdir(parents=True)
25
+ shutil.copy2(DIST / "index.html", DEST / "index.html")
26
+ if (DIST / "favicon.svg").exists():
27
+ shutil.copy2(DIST / "favicon.svg", DEST / "favicon.svg")
28
+ n = 0
29
+ for f in (DIST / "assets").iterdir():
30
+ if f.suffix == ".map":
31
+ continue
32
+ shutil.copy2(f, DEST / "assets" / f.name)
33
+ n += 1
34
+ size = sum(p.stat().st_size for p in DEST.rglob("*") if p.is_file())
35
+ print(f"packaged viewer: index.html + {n} assets, {size / 1e6:.1f} MB -> {DEST}")
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -0,0 +1,83 @@
1
+ """The spatialscape mark: a tissue section made of spots whose colored domains roll like hills.
2
+
3
+ python/.venv/bin/python python/scripts/gen_brand.py
4
+
5
+ Writes docs/public/brand/{mark,banner-dark,banner-light}.svg (also copied to docs/public/logo.svg and app/public/favicon.svg).
6
+ """
7
+ import math
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ ROOT = Path(__file__).resolve().parents[2]
12
+ out = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "docs" / "public" / "brand"
13
+ out.mkdir(parents=True, exist_ok=True)
14
+ CX = CY = 128
15
+ R_DISC = 118
16
+ R_FILL = 104 # dots live inside this radius
17
+ PITCH = 15.5
18
+ DOT = 5.6
19
+ DISC = "#141a26"
20
+ SKY = "#2a3449"
21
+ SUN = "#ff8a5b"
22
+ BANDS = ["#f5d324", "#35c46a", "#3d7fe0", "#8b5cf6"] # yellow, green, blue, violet from top to bottom
23
+
24
+ def curve(k, x):
25
+ """boundary k between band k-1 and band k, as y(x): a hill rising on the left, strata following it"""
26
+ t = (x - CX) / R_FILL
27
+ hill = -26 * math.exp(-((t + 0.35) / 0.55) ** 2) + 10 * math.sin(t * 2.2 + 2.4) # one crest left, a dip under the sun
28
+ base = [112, 146, 178, 206][k]
29
+ follow = [1.0, 0.75, 0.5, 0.3][k]
30
+ return base + follow * hill + 3 * math.sin(t * 6.1 + k * 1.7)
31
+
32
+
33
+ def band_of(x, y):
34
+ for k in range(4):
35
+ if y < curve(k, x):
36
+ return k - 1 # -1 = sky
37
+ return 3
38
+
39
+ def dots():
40
+ rows = int(2 * R_FILL / (PITCH * math.sqrt(3) / 2)) + 3
41
+ out = []
42
+ for j in range(-rows, rows + 1):
43
+ y = CY + j * PITCH * math.sqrt(3) / 2
44
+ off = PITCH / 2 if j % 2 else 0
45
+ cols = int(2 * R_FILL / PITCH) + 3
46
+ for i in range(-cols, cols + 1):
47
+ x = CX + i * PITCH + off
48
+ if math.hypot(x - CX, y - CY) > R_FILL:
49
+ continue
50
+ out.append((x, y))
51
+ return out
52
+
53
+ def mark_svg():
54
+ parts = [f'<circle cx="{CX}" cy="{CY}" r="{R_DISC}" fill="{DISC}"/>']
55
+ sun = (CX + 52, CY - 58)
56
+ parts.append(f'<circle cx="{sun[0]}" cy="{sun[1]}" r="13" fill="{SUN}"/>')
57
+ for x, y in dots():
58
+ if math.hypot(x - sun[0], y - sun[1]) < 20:
59
+ continue
60
+ b = band_of(x, y)
61
+ if b < 0:
62
+ parts.append(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{DOT*0.55:.1f}" fill="{SKY}"/>')
63
+ else:
64
+ parts.append(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{DOT}" fill="{BANDS[b]}"/>')
65
+ return "\n".join(parts)
66
+
67
+ inner = mark_svg()
68
+ mark = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">\n{inner}\n</svg>\n'
69
+ (out / "mark.svg").write_text(mark)
70
+
71
+ def banner(fill, name):
72
+ txt = (f'<text x="76" y="63" font-family="Inter, \'Helvetica Neue\', Helvetica, Arial, sans-serif" font-size="46" '
73
+ f'font-weight="600" letter-spacing="-1.2" fill="{fill}" textLength="286" lengthAdjust="spacingAndGlyphs">spatialscape</text>')
74
+ svg = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 372 96" width="372" height="96">\n'
75
+ f'<g transform="translate(8 16) scale(0.25)">\n{inner}\n</g>\n{txt}\n</svg>\n')
76
+ (out / name).write_text(svg)
77
+
78
+ banner("#e6edf3", "banner-dark.svg")
79
+ banner("#1f2328", "banner-light.svg")
80
+
81
+ for copy in (ROOT / "docs" / "public" / "logo.svg", ROOT / "app" / "public" / "favicon.svg"):
82
+ copy.write_text(mark)
83
+ print("wrote", sorted(p.name for p in out.iterdir()))
@@ -0,0 +1,247 @@
1
+ """Generate the CLI reference pages for the docs site from the Typer app itself.
2
+
3
+ python/.venv/bin/python python/scripts/gen_cli_docs.py
4
+
5
+ Writes docs/reference/cli/index.md, one page per command, and sidebar.json used by the VitePress config.
6
+ Help strings in spatialscape/cli.py are the single source of truth; examples live in EXAMPLES below.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ from pathlib import Path
13
+
14
+ import typer.main
15
+
16
+ from spatialscape.cli import app
17
+
18
+ ROOT = Path(__file__).resolve().parents[2]
19
+ OUT = ROOT / "docs" / "reference" / "cli"
20
+
21
+ EXAMPLES: dict[str, list[tuple[str, str]]] = {
22
+ "init": [
23
+ ("A SpatialExperiment saved from R (one sample per sample_id)", 'spatialscape init analysis/spe_visium.rds --id dlpfc --name "Human DLPFC"'),
24
+ ("Xenium donors in sibling folders", 'spatialscape init "xenium/*/adata.zarr" --platform xenium --id amygdala --name "Human amygdala"'),
25
+ ("A spatial object and the snRNA-seq reference behind its labels, one dataset", "spatialscape init analysis/spe_visium.rds analysis/sce_snrnaseq.rds --id dlpfc"),
26
+ ],
27
+ "build": [
28
+ ("Build, or update after editing the yaml (default output: `bundles/<id>` next to it)", "spatialscape build dlpfc.yaml"),
29
+ ("See what would be rebuilt and why, without writing anything", "spatialscape build dlpfc.yaml --dry-run"),
30
+ ("For GitHub Pages or other hosts without Range support", "spatialscape build dlpfc.yaml --no-shard"),
31
+ ("Rebuild everything", "spatialscape build dlpfc.yaml --force"),
32
+ ],
33
+ "serve": [
34
+ ("Open everything under a folder in the viewer", "spatialscape serve bundles --open"),
35
+ ("Reachable from another machine on the network", "spatialscape serve bundles --host 0.0.0.0 --port 8787"),
36
+ ("Preview a site written by `site build`", "spatialscape serve site"),
37
+ ],
38
+ "site init": [
39
+ ("List two datasets and start a gallery yaml", 'spatialscape site init dlpfc.yaml amygdala.yaml --title "Our lab\'s spatial data"'),
40
+ ("Pick up every dataset yaml and bundle in the current folder", "spatialscape site init"),
41
+ ],
42
+ "site build": [
43
+ ("The gallery described in site.yaml (default output: site/ next to it)", "spatialscape site build site.yaml"),
44
+ ("One dataset, no settings: viewer + bundles from a folder", "spatialscape site build bundles -o site"),
45
+ ("Local preview of large bundles without copying them", "spatialscape site build site.yaml --link && spatialscape serve site"),
46
+ ],
47
+ "inspect": [
48
+ ("A SpatialExperiment saved from R", "spatialscape inspect analysis/spe_visium.rds"),
49
+ ("An h5ad written from R", "spatialscape inspect Br2743.h5ad"),
50
+ ("A SpatialData store with several tables", "spatialscape inspect store.zarr --table cells"),
51
+ ],
52
+ "validate": [
53
+ ("Before upload", "spatialscape validate bundles/dlpfc"),
54
+ ("After upload", "spatialscape validate https://data.example.org/dlpfc"),
55
+ ],
56
+ "outlines": [
57
+ ("All spatial samples, all categorical fields", "spatialscape outlines dlpfc.yaml"),
58
+ ("Finer outlines for one Xenium donor", "spatialscape outlines amygdala.yaml --sample xen_Br9280 --field domain --smooth-um 80 --min-feature-um 150"),
59
+ ],
60
+ "thumbnails": [("Lead with a particular section", "spatialscape thumbnails bundles/dlpfc --hero vis_Br8325")],
61
+ "synth": [("Make inputs, build them, serve them", "spatialscape synth demo-src && spatialscape build demo-src/dataset.yaml && spatialscape serve demo-src/bundles --open")],
62
+ }
63
+ RELATED: dict[str, list[str]] = {
64
+ "init": ["build", "inspect"],
65
+ "build": ["serve", "validate", "site build"],
66
+ "serve": ["build", "site build"],
67
+ "site init": ["site build"],
68
+ "site build": ["site init", "serve", "build"],
69
+ "inspect": ["init"],
70
+ "validate": ["build", "serve"],
71
+ "outlines": ["build"],
72
+ "thumbnails": ["build"],
73
+ "synth": ["build"],
74
+ }
75
+ GUIDE_LINKS: dict[str, tuple[str, str]] = {
76
+ "init": ("Writing dataset.yaml", "/guide/dataset-yaml"),
77
+ "build": ("Building and validating", "/guide/building"),
78
+ "serve": ("Getting started", "/guide/getting-started"),
79
+ "site init": ("The gallery: site.yaml", "/guide/registry"),
80
+ "site build": ("Publish your own site", "/guide/publish"),
81
+ "inspect": ("Inputs: what the CLI reads", "/guide/preparing-data"),
82
+ "validate": ("Hosting", "/guide/hosting"),
83
+ "outlines": ("Using the viewer: outlines", "/guide/viewer#outlines"),
84
+ "thumbnails": ("The gallery: site.yaml", "/guide/registry"),
85
+ "synth": ("Getting started", "/guide/getting-started#try-it-in-two-minutes"),
86
+ }
87
+
88
+
89
+ def is_arg(p) -> bool:
90
+ return p.param_type_name == "argument"
91
+
92
+
93
+ def is_opt(p) -> bool:
94
+ return p.param_type_name == "option"
95
+
96
+
97
+ def md_inline(s: str) -> str:
98
+ """Help strings use backticks already; just escape table pipes."""
99
+ return (s or "").replace("|", "\\|").replace("\n", " ")
100
+
101
+
102
+ def type_name(p) -> str:
103
+ t = p.type
104
+ if is_opt(p) and p.is_flag:
105
+ return "flag"
106
+ name = getattr(t, "name", str(t)).upper()
107
+ if name in ("TEXT", "STR", "STRING"):
108
+ name = "text"
109
+ elif name in ("INTEGER",):
110
+ name = "int"
111
+ elif name in ("FLOAT",):
112
+ name = "float"
113
+ elif name in ("PATH",):
114
+ name = "path"
115
+ else:
116
+ name = name.lower()
117
+ return name + (", repeatable" if p.multiple else "")
118
+
119
+
120
+ def default_str(p) -> str:
121
+ if p.required:
122
+ return "*required*"
123
+ d = p.default
124
+ if is_opt(p) and p.is_flag:
125
+ return "`off`" if not d else "`on`"
126
+ if d is None or d == () or d == []:
127
+ return "—"
128
+ if callable(d):
129
+ return "—"
130
+ return f"`{d}`"
131
+
132
+
133
+ def usage(cmd_name: str, cmd) -> str:
134
+ parts = [f"spatialscape {cmd_name}"]
135
+ for p in cmd.params:
136
+ if is_arg(p):
137
+ nm = p.human_readable_name.upper()
138
+ parts.append(f"<{nm}>" + ("..." if p.nargs == -1 else "") if p.required else f"[{nm}]")
139
+ if any(is_opt(p) for p in cmd.params):
140
+ parts.append("[OPTIONS]")
141
+ return " ".join(parts)
142
+
143
+
144
+ def page(cmd_name: str, cmd) -> str:
145
+ doc = (cmd.help or "").strip()
146
+ summary, _, rest = doc.partition("\n\n")
147
+ rest = re.sub(r"\n(?!\n)", " ", rest).strip()
148
+ args = [p for p in cmd.params if is_arg(p)]
149
+ opts = [p for p in cmd.params if is_opt(p) and p.name != "help"]
150
+ out = [f"# `spatialscape {cmd_name}`", "", summary, ""]
151
+ if rest:
152
+ out += [rest, ""]
153
+ out += ["## Usage", "", "```bash", usage(cmd_name, cmd), "```", ""]
154
+ if args:
155
+ out += ["## Arguments", "", "| Argument | Type | Default | Description |", "|---|---|---|---|"]
156
+ for p in args:
157
+ out.append(f"| `{p.human_readable_name.upper()}` | {type_name(p)} | {default_str(p)} | {md_inline(getattr(p, 'help', '') or '')} |")
158
+ out.append("")
159
+ if opts:
160
+ out += ["## Options", "", "| Option | Type | Default | Description |", "|---|---|---|---|"]
161
+ for p in opts:
162
+ names = ", ".join(f"`{o}`" for o in (p.opts + p.secondary_opts))
163
+ out.append(f"| {names} | {type_name(p)} | {default_str(p)} | {md_inline(p.help or '')} |")
164
+ out.append("")
165
+ ex = EXAMPLES.get(cmd_name, [])
166
+ if ex:
167
+ out += ["## Examples", ""]
168
+ for title, code in ex:
169
+ if "`" not in title:
170
+ title = title.replace("<", "&lt;").replace(">", "&gt;")
171
+ out += [f"**{title}**", "", "```bash", code, "```", ""]
172
+ rel = RELATED.get(cmd_name, [])
173
+ g = GUIDE_LINKS.get(cmd_name)
174
+ if rel or g:
175
+ out += ["## See also", ""]
176
+ for r in rel:
177
+ out.append(f"- [`spatialscape {r}`](./{r.replace(' ', '-')})")
178
+ if g:
179
+ out.append(f"- Guide: [{g[0]}]({g[1]})")
180
+ out.append("")
181
+ return "\n".join(out)
182
+
183
+
184
+ def main() -> None:
185
+ group = typer.main.get_command(app)
186
+ assert hasattr(group, "commands"), "expected a command group"
187
+ OUT.mkdir(parents=True, exist_ok=True)
188
+ commands: dict[str, object] = {}
189
+ for n, cmd in group.commands.items():
190
+ if hasattr(cmd, "commands"): # nested group such as `site`
191
+ for sub, subcmd in cmd.commands.items():
192
+ commands[f"{n} {sub}"] = subcmd
193
+ else:
194
+ commands[n] = cmd
195
+ names = [n for n in EXAMPLES if n in commands] # documented order
196
+ names += [n for n in commands if n not in names]
197
+ rows = []
198
+ sidebar = []
199
+ for n in names:
200
+ cmd = commands[n]
201
+ slug = n.replace(" ", "-")
202
+ (OUT / f"{slug}.md").write_text(page(n, cmd))
203
+ summary = (cmd.help or "").strip().partition("\n\n")[0]
204
+ rows.append(f"| [`{n}`](./{slug}) | {md_inline(summary)} |")
205
+ sidebar.append({"text": n, "link": f"/reference/cli/{slug}"})
206
+ index = [
207
+ "# CLI commands",
208
+ "",
209
+ "Four commands cover the normal path: `init` writes a dataset yaml from your data, `build` turns it into a bundle (and updates the bundle after every edit), `serve` opens it, `site build` writes the folder to publish. Every command is also available as `sscape <command>`, and `spatialscape <command> --help` prints the same information as these pages.",
210
+ "",
211
+ "```bash",
212
+ "pip install spatialscape",
213
+ "spatialscape --help",
214
+ "```",
215
+ "",
216
+ "| Command | Purpose |",
217
+ "|---|---|",
218
+ *rows,
219
+ "",
220
+ "## Typical sequence",
221
+ "",
222
+ "```bash",
223
+ "spatialscape init analysis/spe_visium.rds --id my_atlas # writes my_atlas.yaml, a menu of the data",
224
+ "# edit my_atlas.yaml: annotations, names, colors, the opening gene",
225
+ "spatialscape build my_atlas.yaml # -> bundles/my_atlas; rerun after any edit",
226
+ "spatialscape serve bundles --open",
227
+ "spatialscape site init my_atlas.yaml && spatialscape site build site.yaml # only for a gallery with settings",
228
+ "# ... upload site/ (or the bundles alone) ...",
229
+ "spatialscape validate https://data.example.org/my_atlas",
230
+ "```",
231
+ "",
232
+ "## Exit codes",
233
+ "",
234
+ "`build` and `validate` exit with status 1 when validation reports problems, so they can gate a deployment script. Every other command exits 0 on success and prints a one-line error otherwise (`SPATIALSCAPE_DEBUG=1` shows the traceback).",
235
+ "",
236
+ "::: tip Generated reference",
237
+ "These pages are generated from the CLI's own help text by `python/scripts/gen_cli_docs.py`. If a page and `--help` ever disagree, `--help` is right and the generator needs re-running.",
238
+ ":::",
239
+ "",
240
+ ]
241
+ (OUT / "index.md").write_text("\n".join(index))
242
+ (OUT / "sidebar.json").write_text(json.dumps(sidebar, indent=2))
243
+ print(f"wrote {len(names)} command pages to {OUT}")
244
+
245
+
246
+ if __name__ == "__main__":
247
+ main()
@@ -0,0 +1,13 @@
1
+ """spatialscape: build static spatialscape viewer bundles, or show an object from the session you are in."""
2
+
3
+ from importlib import import_module
4
+
5
+ __version__ = "0.3.0"
6
+
7
+ __all__ = ["Session", "show", "stop"]
8
+
9
+
10
+ def __getattr__(name: str): # lazy: the CLI must not pay for importing the build stack at startup
11
+ if name in ("show", "stop", "Session"):
12
+ return getattr(import_module(".session", __name__), name)
13
+ raise AttributeError(name)