Spacelab 0.28.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.
- spacelab/__init__.py +6 -0
- spacelab/__main__.py +3 -0
- spacelab/cli.py +97 -0
- spacelab/hlod/__init__.py +8 -0
- spacelab/hlod/__main__.py +5 -0
- spacelab/hlod/build.py +349 -0
- spacelab/hlod/cells.py +82 -0
- spacelab/hlod/cli.py +90 -0
- spacelab/hlod/hashing.py +38 -0
- spacelab/hlod/hilbert.py +91 -0
- spacelab/hlod/read.py +150 -0
- spacelab/hlod/verify.py +142 -0
- spacelab/prep_spatialdata.py +700 -0
- spacelab/server.py +130 -0
- spacelab/static/AI_lens_black.png +0 -0
- spacelab/static/ai-new-logo.png +0 -0
- spacelab/static/aifi_logo.png +0 -0
- spacelab/static/assets/AllenInstitutePlusText-Bold-DxLtQ4oE.woff2 +0 -0
- spacelab/static/assets/AllenInstitutePlusText-BoldItalic-mxzHQhQO.woff2 +0 -0
- spacelab/static/assets/AllenInstitutePlusText-Italic-CaKmxpsW.woff2 +0 -0
- spacelab/static/assets/AllenInstitutePlusText-Light-DUThXl1m.woff2 +0 -0
- spacelab/static/assets/AllenInstitutePlusText-LightItalic-CxfKknzG.woff2 +0 -0
- spacelab/static/assets/AllenInstitutePlusText-Regular-B0ryviHQ.woff2 +0 -0
- spacelab/static/assets/AllenInstitutePlusText-Semibold-3PoiDF1d.woff2 +0 -0
- spacelab/static/assets/AllenInstitutePlusText-SemiboldItalic-uXaVqnFo.woff2 +0 -0
- spacelab/static/assets/blosc-BKQbfHyB.js +18 -0
- spacelab/static/assets/blosc-j6ygecpn.js +18 -0
- spacelab/static/assets/chunk-INHXZS53-D3tQiqtZ.js +2 -0
- spacelab/static/assets/chunk-INHXZS53-QjxmivJp.js +2 -0
- spacelab/static/assets/deflate-BRVR_fNk.js +2 -0
- spacelab/static/assets/hlodTranscripts.worker-CjhNLk56.js +2 -0
- spacelab/static/assets/index-BT-mDgg2.js +5025 -0
- spacelab/static/assets/index-CNIqsj6m.css +1 -0
- spacelab/static/assets/jpeg-JUDfHHDo.js +2 -0
- spacelab/static/assets/lerc-CXQdKTps.js +2 -0
- spacelab/static/assets/lz4-C2MQfzIA.js +16 -0
- spacelab/static/assets/lz4-rzy_Siro.js +16 -0
- spacelab/static/assets/lzw-DBCCrYEo.js +2 -0
- spacelab/static/assets/packbits-LrtZ2mhj.js +2 -0
- spacelab/static/assets/pako.esm-C69tvH9t.js +2 -0
- spacelab/static/assets/polygons.worker-ClOPdWlW.js +2 -0
- spacelab/static/assets/raw-lPmjnySZ.js +2 -0
- spacelab/static/assets/spatialdataExpression.worker-D4W_OHS3.js +4 -0
- spacelab/static/assets/spatialdataShapes.worker-DQV1zeTt.js +974 -0
- spacelab/static/assets/spatialdataTranscripts.worker-DH9L-MwZ.js +974 -0
- spacelab/static/assets/transcripts.worker-CtxyO4n3.js +2 -0
- spacelab/static/assets/umap.worker-BPHuoLPV.js +2 -0
- spacelab/static/assets/webimage-xg0Pp5Jv.js +2 -0
- spacelab/static/assets/zstd-Cp7BEDDH.js +16 -0
- spacelab/static/assets/zstd-WlDjm0Vu.js +16 -0
- spacelab/static/blurred-bg.jpg +0 -0
- spacelab/static/favicon.ico +0 -0
- spacelab/static/index.html +22 -0
- spacelab/static/logo192.png +0 -0
- spacelab/static/logo512.png +0 -0
- spacelab/static/manifest.json +15 -0
- spacelab/static/robots.txt +3 -0
- spacelab-0.28.0.dist-info/METADATA +30 -0
- spacelab-0.28.0.dist-info/RECORD +62 -0
- spacelab-0.28.0.dist-info/WHEEL +4 -0
- spacelab-0.28.0.dist-info/entry_points.txt +2 -0
- spacelab-0.28.0.dist-info/licenses/LICENSE +21 -0
spacelab/server.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Static server mirroring server.js: the bundled frontend first, then the data dir."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import mimetypes
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from functools import partial
|
|
10
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
14
|
+
ZARR_METADATA = (".zarray", ".zattrs", ".zgroup")
|
|
15
|
+
# One range only (what the app sends); a multi-range request gets the whole file.
|
|
16
|
+
_RANGE = re.compile(r"bytes=(\d*)-(\d*)$")
|
|
17
|
+
_COPY_CHUNK = 64 * 1024
|
|
18
|
+
_START = time.time()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Handler(SimpleHTTPRequestHandler):
|
|
22
|
+
def __init__(self, *args, data_dir: str, **kwargs):
|
|
23
|
+
self.data_dir = data_dir
|
|
24
|
+
super().__init__(*args, directory=str(STATIC_DIR), **kwargs)
|
|
25
|
+
|
|
26
|
+
def translate_path(self, path):
|
|
27
|
+
# Frontend assets win; anything else resolves against the data dir.
|
|
28
|
+
static = super().translate_path(path)
|
|
29
|
+
if os.path.exists(static):
|
|
30
|
+
return static
|
|
31
|
+
rel = os.path.relpath(static, STATIC_DIR)
|
|
32
|
+
return os.path.join(self.data_dir, rel)
|
|
33
|
+
|
|
34
|
+
def guess_type(self, path):
|
|
35
|
+
if str(path).endswith(ZARR_METADATA):
|
|
36
|
+
return "application/json"
|
|
37
|
+
return super().guess_type(path)
|
|
38
|
+
|
|
39
|
+
def end_headers(self):
|
|
40
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
41
|
+
self.send_header("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
|
|
42
|
+
self.send_header(
|
|
43
|
+
"Access-Control-Allow-Headers",
|
|
44
|
+
"Origin, X-Requested-With, Content-Type, Accept, Range",
|
|
45
|
+
)
|
|
46
|
+
self.send_header("Access-Control-Expose-Headers", "Content-Length, Content-Range")
|
|
47
|
+
self.send_header("Cache-Control", "no-cache")
|
|
48
|
+
super().end_headers()
|
|
49
|
+
|
|
50
|
+
def do_OPTIONS(self):
|
|
51
|
+
self.send_response(204)
|
|
52
|
+
self.end_headers()
|
|
53
|
+
|
|
54
|
+
# Byte ranges, as express.static serves them: the SpatialData reader fetches
|
|
55
|
+
# parquet footers and single row groups by range, and a 200 with the whole
|
|
56
|
+
# file instead makes it pull a multi-GB transcripts part into the browser.
|
|
57
|
+
def send_head(self):
|
|
58
|
+
self._range_remaining = None
|
|
59
|
+
header = self.headers.get("Range")
|
|
60
|
+
path = self.translate_path(self.path)
|
|
61
|
+
match = _RANGE.match(header.strip()) if header else None
|
|
62
|
+
if not match or not (match[1] or match[2]) or not os.path.isfile(path):
|
|
63
|
+
return super().send_head()
|
|
64
|
+
|
|
65
|
+
size = os.path.getsize(path)
|
|
66
|
+
if match[1]:
|
|
67
|
+
start = int(match[1])
|
|
68
|
+
end = min(int(match[2]), size - 1) if match[2] else size - 1
|
|
69
|
+
else: # suffix range: the last N bytes
|
|
70
|
+
start, end = max(0, size - int(match[2])), size - 1
|
|
71
|
+
if start >= size or start > end:
|
|
72
|
+
self.send_response(416)
|
|
73
|
+
self.send_header("Content-Range", f"bytes */{size}")
|
|
74
|
+
self.send_header("Content-Length", "0")
|
|
75
|
+
self.end_headers()
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
f = open(path, "rb")
|
|
79
|
+
f.seek(start)
|
|
80
|
+
self.send_response(206)
|
|
81
|
+
self.send_header("Content-Type", self.guess_type(path))
|
|
82
|
+
self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
|
|
83
|
+
self.send_header("Content-Length", str(end - start + 1))
|
|
84
|
+
self.send_header("Accept-Ranges", "bytes")
|
|
85
|
+
self.end_headers()
|
|
86
|
+
self._range_remaining = end - start + 1
|
|
87
|
+
return f
|
|
88
|
+
|
|
89
|
+
def copyfile(self, source, outputfile):
|
|
90
|
+
remaining = getattr(self, "_range_remaining", None)
|
|
91
|
+
if remaining is None:
|
|
92
|
+
return super().copyfile(source, outputfile)
|
|
93
|
+
while remaining > 0:
|
|
94
|
+
buf = source.read(min(_COPY_CHUNK, remaining))
|
|
95
|
+
if not buf:
|
|
96
|
+
break
|
|
97
|
+
outputfile.write(buf)
|
|
98
|
+
remaining -= len(buf)
|
|
99
|
+
|
|
100
|
+
def do_GET(self):
|
|
101
|
+
if self.path == "/health":
|
|
102
|
+
body = json.dumps({"status": "healthy", "uptime": time.time() - _START}).encode()
|
|
103
|
+
self.send_response(200)
|
|
104
|
+
self.send_header("Content-Type", "application/json")
|
|
105
|
+
self.send_header("Content-Length", str(len(body)))
|
|
106
|
+
self.end_headers()
|
|
107
|
+
self.wfile.write(body)
|
|
108
|
+
return
|
|
109
|
+
super().do_GET()
|
|
110
|
+
|
|
111
|
+
def log_message(self, format, *args):
|
|
112
|
+
pass # per-chunk request logs would flood the terminal
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Server(ThreadingHTTPServer):
|
|
116
|
+
daemon_threads = True
|
|
117
|
+
|
|
118
|
+
def handle_error(self, request, client_address):
|
|
119
|
+
# The app aborts in-flight fetches (tiles on pan/zoom, chunks on tissue
|
|
120
|
+
# switch); the resulting resets are routine, not worth a traceback.
|
|
121
|
+
if isinstance(sys.exc_info()[1], (ConnectionResetError, BrokenPipeError)):
|
|
122
|
+
return
|
|
123
|
+
super().handle_error(request, client_address)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def make_server(data_dir: str, host: str, port: int) -> ThreadingHTTPServer:
|
|
127
|
+
mimetypes.add_type("application/javascript", ".js")
|
|
128
|
+
mimetypes.add_type("application/wasm", ".wasm")
|
|
129
|
+
handler = partial(Handler, data_dir=os.path.abspath(data_dir))
|
|
130
|
+
return Server((host, port), handler)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|