scigantic-bil 0.1.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 (28) hide show
  1. scigantic_bil-0.1.0/LICENSE +16 -0
  2. scigantic_bil-0.1.0/PKG-INFO +209 -0
  3. scigantic_bil-0.1.0/README.md +169 -0
  4. scigantic_bil-0.1.0/pyproject.toml +75 -0
  5. scigantic_bil-0.1.0/setup.cfg +4 -0
  6. scigantic_bil-0.1.0/src/scigantic_bil/__init__.py +72 -0
  7. scigantic_bil-0.1.0/src/scigantic_bil/__main__.py +3 -0
  8. scigantic_bil-0.1.0/src/scigantic_bil/_client.py +88 -0
  9. scigantic_bil-0.1.0/src/scigantic_bil/_version.py +1 -0
  10. scigantic_bil-0.1.0/src/scigantic_bil/api.py +108 -0
  11. scigantic_bil-0.1.0/src/scigantic_bil/cache.py +127 -0
  12. scigantic_bil-0.1.0/src/scigantic_bil/catalog.py +241 -0
  13. scigantic_bil-0.1.0/src/scigantic_bil/cli.py +183 -0
  14. scigantic_bil-0.1.0/src/scigantic_bil/files.py +263 -0
  15. scigantic_bil-0.1.0/src/scigantic_bil/images.py +355 -0
  16. scigantic_bil-0.1.0/src/scigantic_bil/models.py +298 -0
  17. scigantic_bil-0.1.0/src/scigantic_bil/py.typed +0 -0
  18. scigantic_bil-0.1.0/src/scigantic_bil.egg-info/PKG-INFO +209 -0
  19. scigantic_bil-0.1.0/src/scigantic_bil.egg-info/SOURCES.txt +26 -0
  20. scigantic_bil-0.1.0/src/scigantic_bil.egg-info/dependency_links.txt +1 -0
  21. scigantic_bil-0.1.0/src/scigantic_bil.egg-info/entry_points.txt +2 -0
  22. scigantic_bil-0.1.0/src/scigantic_bil.egg-info/requires.txt +20 -0
  23. scigantic_bil-0.1.0/src/scigantic_bil.egg-info/top_level.txt +1 -0
  24. scigantic_bil-0.1.0/tests/test_api.py +80 -0
  25. scigantic_bil-0.1.0/tests/test_catalog.py +90 -0
  26. scigantic_bil-0.1.0/tests/test_cli.py +34 -0
  27. scigantic_bil-0.1.0/tests/test_files.py +86 -0
  28. scigantic_bil-0.1.0/tests/test_images.py +109 -0
@@ -0,0 +1,16 @@
1
+ MIT No Attribution
2
+
3
+ Copyright 2026 Scigantic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
6
+ software and associated documentation files (the "Software"), to deal in the Software
7
+ without restriction, including without limitation the rights to use, copy, modify,
8
+ merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
12
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
13
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
14
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
15
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
16
+ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.4
2
+ Name: scigantic-bil
3
+ Version: 0.1.0
4
+ Summary: Search the Brain Image Library (14,000+ BICCN/BICAN brain microscopy datasets, 6 PB) and read light-sheet, fMOST and STPT volumes over HTTP without downloading them: a typed catalog over BIL's daily inventory and metadata API, lazy file listing, single-slice TIFF reads, thumbnails, and OME-Zarr access.
5
+ Author: Scigantic
6
+ License: MIT-0
7
+ Project-URL: Homepage, https://scigantic.com
8
+ Project-URL: Repository, https://github.com/Scigantic/scigantic-bil
9
+ Project-URL: Issues, https://github.com/Scigantic/scigantic-bil/issues
10
+ Keywords: brain-image-library,biccn,light-sheet,microscopy,neuroscience,fmost,stpt,ome-zarr,tiff,whole-brain
11
+ Classifier: License :: OSI Approved :: MIT No Attribution License (MIT-0)
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Typing :: Typed
19
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
20
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: requests<3,>=2.28
25
+ Requires-Dist: numpy<3,>=1.24
26
+ Requires-Dist: tifffile<2027,>=2023.7
27
+ Provides-Extra: zarr
28
+ Requires-Dist: zarr<4,>=3; python_version >= "3.11" and extra == "zarr"
29
+ Requires-Dist: fsspec>=2024.1; python_version >= "3.11" and extra == "zarr"
30
+ Requires-Dist: aiohttp<4,>=3.9; python_version >= "3.11" and extra == "zarr"
31
+ Provides-Extra: pandas
32
+ Requires-Dist: pandas<4,>=1.5; extra == "pandas"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7; extra == "dev"
35
+ Requires-Dist: mypy>=1.10; extra == "dev"
36
+ Requires-Dist: types-requests; extra == "dev"
37
+ Requires-Dist: pandas-stubs; extra == "dev"
38
+ Requires-Dist: pillow>=10; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ <h1 align="center">scigantic-bil</h1>
42
+
43
+ <p align="center">
44
+ <a href="https://github.com/Scigantic/scigantic-bil/actions/workflows/ci.yml">
45
+ <img alt="CI" src="https://github.com/Scigantic/scigantic-bil/actions/workflows/ci.yml/badge.svg" /></a>
46
+ <a href="https://pypi.org/project/scigantic-bil/">
47
+ <img alt="PyPI" src="https://img.shields.io/pypi/v/scigantic-bil" /></a>
48
+ <a href="https://pypi.org/project/scigantic-bil/">
49
+ <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/scigantic-bil" /></a>
50
+ <a href="https://github.com/Scigantic/scigantic-bil/blob/main/LICENSE">
51
+ <img alt="License" src="https://img.shields.io/github/license/Scigantic/scigantic-bil" /></a>
52
+ </p>
53
+
54
+ Search the [Brain Image Library](https://www.brainimagelibrary.org/) and read its light-sheet, fMOST and STPT volumes over HTTP. No download, no account, no local copy.
55
+
56
+ ```python
57
+ import scigantic_bil as bil
58
+
59
+ cat = bil.BilCatalog.load() # every BIL dataset, one 5 MB GET
60
+ for d in cat.light_sheet()[:5]: # 808 light-sheet datasets, 278 TB
61
+ print(d.bildid, d.contributor, d.species, f"{d.size_gb:.0f} GB")
62
+
63
+ img = bil.thumbnail("ace-bin-run") # middle z-slice of a 27 GB stack, one 16 MB read
64
+ ```
65
+
66
+ ## Installation
67
+
68
+ ```console
69
+ $ pip install scigantic-bil
70
+ $ pip install "scigantic-bil[zarr]" # also open OME-Zarr stores in place
71
+ ```
72
+
73
+ ## Why this exists
74
+
75
+ The Brain Image Library is the BRAIN Initiative's repository for whole-brain microscopy: 14,224 datasets and 6 PB as of its 2026-07-31 inventory, including the largest public collection of cleared-tissue light-sheet brains. Everything is served over plain HTTPS with directory listings and range requests, and the metadata API needs no key. That makes it readable in place, but nothing in the ecosystem did so.
76
+
77
+ BIL's own [`brainimagelibrary`](https://pypi.org/project/brainimagelibrary/) package (py-brain-sdk, 0.0.23, GPL-3.0) wraps the metadata API and downloads datasets, with resumable transfers and citation lookups. Its source (read, not assumed, on 2026-09-08) contains no image reading at all: the only route from a BIL id to pixels is `DatasetInventory.download()`, which fetches the files to disk. For a 27 GB light-sheet stack that is the whole stack, to look at one slice.
78
+
79
+ This package is the other half. It reads BIL in place:
80
+
81
+ - **A typed, in-memory catalog of the whole archive** from BIL's daily inventory TSV, with structured filters and a light-sheet finder that unions the inventory's technique field with BIL's fulltext index. The technique field alone says light sheet for 348 datasets; the union finds 808. The rest are tagged `other` and only say light sheet in their abstract or instrument record.
82
+ - **Single-slice TIFF reads** straight from the download server. The dominant BIL layout is one TIFF per z-plane, about 16 MB each; one slice is one request.
83
+ - **OME-Zarr stores** opened lazily through zarr's HTTP store, with pyramid levels checked against what the server actually has (one BIL store declares eight levels and serves seven).
84
+ - **Thumbnails that read as little as possible**: the middle slice of a TIFF stack, or the coarsest level of a zarr pyramid.
85
+ - **A seekable HTTP file object** (`HttpFile`) so tifffile fetches only the pages you ask for from a large multi-page TIFF.
86
+ - **BIL's per-dataset manifest** (path, size, MD5, URL for every file) as one gzipped GET, so a deep tree lists in one request.
87
+
88
+ Measured on 2026-09-08 from a residential connection, against the live archive:
89
+
90
+ | | measured |
91
+ |---|---|
92
+ | Load the full catalog (14,224 datasets) | 1.0 s cold, 0.3 s from the on-disk copy |
93
+ | Light-sheet datasets found | 348 by technique field, 808 with fulltext union |
94
+ | Read one 16 MB light-sheet slice (4501 x 3828 uint16) | 0.40 s, 45 MB/s single stream |
95
+ | Thumbnail of a 1,923-slice, 27 GB stack | 0.25 s to 0.56 s, one file read |
96
+ | Thumbnail of a 50 GB OME-Zarr store (848 x 6300 x 9600) | 0.65 s, coarsest level only |
97
+ | Eight parallel streams | 19 MB/s aggregate, slower than one stream, so reads are sequential |
98
+ | Manifest for a 1,923-file dataset | 352 KB gzipped, one request |
99
+
100
+ Dependencies are `requests`, `numpy` and `tifffile`. zarr and pandas are extras.
101
+
102
+ ## Data license
103
+
104
+ BIL data is distributed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/); some datasets additionally carry the Allen Institute Terms of Use, recorded per dataset in `DatasetDetail.rights`. This package's code is MIT-0. The permissive code license does not extend to the data: anything you derive from BIL images and redistribute needs attribution and the same license. Cite the dataset's DOI (`DatasetDetail.doi`) and its publications (`DatasetDetail.publications`).
105
+
106
+ ## Catalog
107
+
108
+ ```python
109
+ cat = bil.BilCatalog.load() # newest daily inventory
110
+ cat = bil.BilCatalog.load(date="20260731") # a specific snapshot
111
+ len(cat), cat.date # (14224, '20260731')
112
+
113
+ cat.summary() # datasets, files, TB, top modality/technique/species/extensions
114
+ cat["ace-cup-eel"] # one Dataset by id
115
+ cat.filter(technique="fMOST", species="mouse") # case-insensitive substring match, any combination
116
+ cat.filter(extension=".swc") # datasets shipping neuron reconstructions
117
+ cat.filter(max_size_gb=2) # small enough to pull whole
118
+ cat.search("iDISCO") # BIL's fulltext index, joined to inventory rows
119
+ cat.light_sheet() # technique field + fulltext, deduplicated
120
+ cat.to_dataframe(cat.filter(technique="STPT")) # pandas, with pip install "scigantic-bil[pandas]"
121
+ ```
122
+
123
+ A `Dataset` carries what the inventory indexes: contributor, affiliation, award, project, consortium, modality, technique, species, genotype, file count, size, and a per-extension file histogram (`extensions`). `Dataset.url` is the dataset's root on the download server.
124
+
125
+ ## Metadata
126
+
127
+ The full record lives on the metadata API and is fetched on demand:
128
+
129
+ ```python
130
+ d = bil.retrieve("ace-cup-eel")
131
+ d.title, d.abstract, d.rights_identifier # 'Light-sheet imaged brain ...', ..., 'CC-BY-SA-4.0'
132
+ d.microscope_type, d.species # 'Light-sheet', 'mouse'
133
+ d.instrument # {'microscopetype': 'Light-sheet', 'microscopemanufacturerandmodel': 'Zeiss Z.1', ...}
134
+ d.specimen, d.images # specimen record; per-image axes, step sizes, channels
135
+ d.contributors, d.publications, d.funders
136
+ d.is_light_sheet # checks technique, instrument and free text together
137
+
138
+ bil.retrieve_many(["ace-cup-eel", "ace-bin-run"]) # batched POST, unknown ids dropped
139
+ bil.fulltext("CLARITY") # BIL ids only
140
+ bil.query("specimen", species="mouse") # one structured element=value pair
141
+ ```
142
+
143
+ Structured queries match exactly (`query("instrument", microscopetype="Light-sheet")` found 6 datasets on 2026-09-08 where `fulltext("light sheet")` found 777), so use fulltext for discovery and the catalog's filters for structure.
144
+
145
+ ## Files
146
+
147
+ ```python
148
+ bil.list_files("ace-bin-run") # one directory: name, size, modified, url
149
+ bil.manifest("ace-bin-run") # every file, with relative path and MD5, one gzipped GET
150
+ list(bil.walk("ace-cup-eel")) # recursive; a zarr store appears once, as a directory
151
+ bil.find("ace-bin-run", suffix=".tif") # natural sort, so Z00002 follows Z00001
152
+ bil.find_zarr("ace-cup-eel") # ['https://download.brainimagelibrary.org/.../subject_5.zarr/']
153
+ bil.download(entry, "out/") # the one function that writes image bytes to disk
154
+ ```
155
+
156
+ Any of these accept a BIL id, a `Dataset`, a `/bil/data/...` path from the metadata, or a download-server URL.
157
+
158
+ ## Images
159
+
160
+ ```python
161
+ stack = bil.slices("ace-bin-run") # 1,923 FileEntry in z order, nothing fetched yet
162
+ img = bil.read_tiff(stack[961]) # (4501, 3828) uint16, one request
163
+ vol = bil.read_stack("ace-bin-run", start=900, stop=960, step=10) # (6, 4501, 3828)
164
+ bil.thumbnail("ace-bin-run", max_size=512) # middle slice, stride-downsampled
165
+ bil.thumbnail("ace-bin-run", index=100, channel="ch02")
166
+ ```
167
+
168
+ Formats this package does not decode raise `UnsupportedFormatError` naming what does: JPEG 2000 (`.jp2`, 5,787 datasets, mostly STPT and fMOST sections; use `download()` then glymur), Imaris (`.ims`, h5py), NIfTI. Reading `.jp2` in place is the obvious next addition; the format has resolution levels built in, so a thumbnail should not need the whole file.
169
+
170
+ ### OME-Zarr
171
+
172
+ ```python
173
+ g = bil.open_zarr("ace-cup-eel") # lazy; nothing read until sliced
174
+ bil.zarr_levels(g) # ['0', ..., '6'], as served, not as declared
175
+ g["6"][0, 0, 400] # one plane of the coarsest level, a few chunks
176
+ bil.zarr_thumbnail(g, max_size=512)
177
+ ```
178
+
179
+ A zarr store is a directory, so it never shows in the inventory's extension histogram; `find_zarr()` is how to know a dataset ships one. Requires `pip install "scigantic-bil[zarr]"` (zarr 3, fsspec, aiohttp) and Python 3.11 or newer, which is zarr 3's own floor; on 3.10 the extra installs nothing and `open_zarr()` raises a clear ImportError.
180
+
181
+ ## Caching
182
+
183
+ On by default. Metadata responses, directory listings and manifests are cached to `~/.cache/scigantic-bil` (macOS: `~/Library/Caches/scigantic-bil`; override with `SCIGANTIC_BIL_CACHE` or `enable_cache(cache_dir=...)`) and expire after 7 days, since BIL republishes its inventory every few days. Inventory snapshots are immutable once published and are kept as plain TSV files without expiry. Image bytes are never cached.
184
+
185
+ ```python
186
+ bil.disable_cache()
187
+ bil.enable_cache(ttl_days=1)
188
+ bil.clear_cache()
189
+ ```
190
+
191
+ ## Command line
192
+
193
+ ```console
194
+ $ scigantic-bil summary
195
+ $ scigantic-bil light-sheet --limit 20
196
+ $ scigantic-bil search "iDISCO" --json
197
+ $ scigantic-bil filter --technique fMOST --species mouse --max-gb 100
198
+ $ scigantic-bil info ace-cup-eel
199
+ $ scigantic-bil files ace-bin-run --zarr
200
+ $ scigantic-bil thumbnail ace-bin-run slice.png --size 512
201
+ ```
202
+
203
+ ## Testing
204
+
205
+ Every test runs live against BIL, no mocks, the same philosophy as the rest of the scigantic-* packages. The suite takes about 15 seconds. CI runs Python 3.10 through 3.14 plus `mypy --strict`.
206
+
207
+ ## License
208
+
209
+ MIT-0 for the code. See [Data license](#data-license) for the data.
@@ -0,0 +1,169 @@
1
+ <h1 align="center">scigantic-bil</h1>
2
+
3
+ <p align="center">
4
+ <a href="https://github.com/Scigantic/scigantic-bil/actions/workflows/ci.yml">
5
+ <img alt="CI" src="https://github.com/Scigantic/scigantic-bil/actions/workflows/ci.yml/badge.svg" /></a>
6
+ <a href="https://pypi.org/project/scigantic-bil/">
7
+ <img alt="PyPI" src="https://img.shields.io/pypi/v/scigantic-bil" /></a>
8
+ <a href="https://pypi.org/project/scigantic-bil/">
9
+ <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/scigantic-bil" /></a>
10
+ <a href="https://github.com/Scigantic/scigantic-bil/blob/main/LICENSE">
11
+ <img alt="License" src="https://img.shields.io/github/license/Scigantic/scigantic-bil" /></a>
12
+ </p>
13
+
14
+ Search the [Brain Image Library](https://www.brainimagelibrary.org/) and read its light-sheet, fMOST and STPT volumes over HTTP. No download, no account, no local copy.
15
+
16
+ ```python
17
+ import scigantic_bil as bil
18
+
19
+ cat = bil.BilCatalog.load() # every BIL dataset, one 5 MB GET
20
+ for d in cat.light_sheet()[:5]: # 808 light-sheet datasets, 278 TB
21
+ print(d.bildid, d.contributor, d.species, f"{d.size_gb:.0f} GB")
22
+
23
+ img = bil.thumbnail("ace-bin-run") # middle z-slice of a 27 GB stack, one 16 MB read
24
+ ```
25
+
26
+ ## Installation
27
+
28
+ ```console
29
+ $ pip install scigantic-bil
30
+ $ pip install "scigantic-bil[zarr]" # also open OME-Zarr stores in place
31
+ ```
32
+
33
+ ## Why this exists
34
+
35
+ The Brain Image Library is the BRAIN Initiative's repository for whole-brain microscopy: 14,224 datasets and 6 PB as of its 2026-07-31 inventory, including the largest public collection of cleared-tissue light-sheet brains. Everything is served over plain HTTPS with directory listings and range requests, and the metadata API needs no key. That makes it readable in place, but nothing in the ecosystem did so.
36
+
37
+ BIL's own [`brainimagelibrary`](https://pypi.org/project/brainimagelibrary/) package (py-brain-sdk, 0.0.23, GPL-3.0) wraps the metadata API and downloads datasets, with resumable transfers and citation lookups. Its source (read, not assumed, on 2026-09-08) contains no image reading at all: the only route from a BIL id to pixels is `DatasetInventory.download()`, which fetches the files to disk. For a 27 GB light-sheet stack that is the whole stack, to look at one slice.
38
+
39
+ This package is the other half. It reads BIL in place:
40
+
41
+ - **A typed, in-memory catalog of the whole archive** from BIL's daily inventory TSV, with structured filters and a light-sheet finder that unions the inventory's technique field with BIL's fulltext index. The technique field alone says light sheet for 348 datasets; the union finds 808. The rest are tagged `other` and only say light sheet in their abstract or instrument record.
42
+ - **Single-slice TIFF reads** straight from the download server. The dominant BIL layout is one TIFF per z-plane, about 16 MB each; one slice is one request.
43
+ - **OME-Zarr stores** opened lazily through zarr's HTTP store, with pyramid levels checked against what the server actually has (one BIL store declares eight levels and serves seven).
44
+ - **Thumbnails that read as little as possible**: the middle slice of a TIFF stack, or the coarsest level of a zarr pyramid.
45
+ - **A seekable HTTP file object** (`HttpFile`) so tifffile fetches only the pages you ask for from a large multi-page TIFF.
46
+ - **BIL's per-dataset manifest** (path, size, MD5, URL for every file) as one gzipped GET, so a deep tree lists in one request.
47
+
48
+ Measured on 2026-09-08 from a residential connection, against the live archive:
49
+
50
+ | | measured |
51
+ |---|---|
52
+ | Load the full catalog (14,224 datasets) | 1.0 s cold, 0.3 s from the on-disk copy |
53
+ | Light-sheet datasets found | 348 by technique field, 808 with fulltext union |
54
+ | Read one 16 MB light-sheet slice (4501 x 3828 uint16) | 0.40 s, 45 MB/s single stream |
55
+ | Thumbnail of a 1,923-slice, 27 GB stack | 0.25 s to 0.56 s, one file read |
56
+ | Thumbnail of a 50 GB OME-Zarr store (848 x 6300 x 9600) | 0.65 s, coarsest level only |
57
+ | Eight parallel streams | 19 MB/s aggregate, slower than one stream, so reads are sequential |
58
+ | Manifest for a 1,923-file dataset | 352 KB gzipped, one request |
59
+
60
+ Dependencies are `requests`, `numpy` and `tifffile`. zarr and pandas are extras.
61
+
62
+ ## Data license
63
+
64
+ BIL data is distributed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/); some datasets additionally carry the Allen Institute Terms of Use, recorded per dataset in `DatasetDetail.rights`. This package's code is MIT-0. The permissive code license does not extend to the data: anything you derive from BIL images and redistribute needs attribution and the same license. Cite the dataset's DOI (`DatasetDetail.doi`) and its publications (`DatasetDetail.publications`).
65
+
66
+ ## Catalog
67
+
68
+ ```python
69
+ cat = bil.BilCatalog.load() # newest daily inventory
70
+ cat = bil.BilCatalog.load(date="20260731") # a specific snapshot
71
+ len(cat), cat.date # (14224, '20260731')
72
+
73
+ cat.summary() # datasets, files, TB, top modality/technique/species/extensions
74
+ cat["ace-cup-eel"] # one Dataset by id
75
+ cat.filter(technique="fMOST", species="mouse") # case-insensitive substring match, any combination
76
+ cat.filter(extension=".swc") # datasets shipping neuron reconstructions
77
+ cat.filter(max_size_gb=2) # small enough to pull whole
78
+ cat.search("iDISCO") # BIL's fulltext index, joined to inventory rows
79
+ cat.light_sheet() # technique field + fulltext, deduplicated
80
+ cat.to_dataframe(cat.filter(technique="STPT")) # pandas, with pip install "scigantic-bil[pandas]"
81
+ ```
82
+
83
+ A `Dataset` carries what the inventory indexes: contributor, affiliation, award, project, consortium, modality, technique, species, genotype, file count, size, and a per-extension file histogram (`extensions`). `Dataset.url` is the dataset's root on the download server.
84
+
85
+ ## Metadata
86
+
87
+ The full record lives on the metadata API and is fetched on demand:
88
+
89
+ ```python
90
+ d = bil.retrieve("ace-cup-eel")
91
+ d.title, d.abstract, d.rights_identifier # 'Light-sheet imaged brain ...', ..., 'CC-BY-SA-4.0'
92
+ d.microscope_type, d.species # 'Light-sheet', 'mouse'
93
+ d.instrument # {'microscopetype': 'Light-sheet', 'microscopemanufacturerandmodel': 'Zeiss Z.1', ...}
94
+ d.specimen, d.images # specimen record; per-image axes, step sizes, channels
95
+ d.contributors, d.publications, d.funders
96
+ d.is_light_sheet # checks technique, instrument and free text together
97
+
98
+ bil.retrieve_many(["ace-cup-eel", "ace-bin-run"]) # batched POST, unknown ids dropped
99
+ bil.fulltext("CLARITY") # BIL ids only
100
+ bil.query("specimen", species="mouse") # one structured element=value pair
101
+ ```
102
+
103
+ Structured queries match exactly (`query("instrument", microscopetype="Light-sheet")` found 6 datasets on 2026-09-08 where `fulltext("light sheet")` found 777), so use fulltext for discovery and the catalog's filters for structure.
104
+
105
+ ## Files
106
+
107
+ ```python
108
+ bil.list_files("ace-bin-run") # one directory: name, size, modified, url
109
+ bil.manifest("ace-bin-run") # every file, with relative path and MD5, one gzipped GET
110
+ list(bil.walk("ace-cup-eel")) # recursive; a zarr store appears once, as a directory
111
+ bil.find("ace-bin-run", suffix=".tif") # natural sort, so Z00002 follows Z00001
112
+ bil.find_zarr("ace-cup-eel") # ['https://download.brainimagelibrary.org/.../subject_5.zarr/']
113
+ bil.download(entry, "out/") # the one function that writes image bytes to disk
114
+ ```
115
+
116
+ Any of these accept a BIL id, a `Dataset`, a `/bil/data/...` path from the metadata, or a download-server URL.
117
+
118
+ ## Images
119
+
120
+ ```python
121
+ stack = bil.slices("ace-bin-run") # 1,923 FileEntry in z order, nothing fetched yet
122
+ img = bil.read_tiff(stack[961]) # (4501, 3828) uint16, one request
123
+ vol = bil.read_stack("ace-bin-run", start=900, stop=960, step=10) # (6, 4501, 3828)
124
+ bil.thumbnail("ace-bin-run", max_size=512) # middle slice, stride-downsampled
125
+ bil.thumbnail("ace-bin-run", index=100, channel="ch02")
126
+ ```
127
+
128
+ Formats this package does not decode raise `UnsupportedFormatError` naming what does: JPEG 2000 (`.jp2`, 5,787 datasets, mostly STPT and fMOST sections; use `download()` then glymur), Imaris (`.ims`, h5py), NIfTI. Reading `.jp2` in place is the obvious next addition; the format has resolution levels built in, so a thumbnail should not need the whole file.
129
+
130
+ ### OME-Zarr
131
+
132
+ ```python
133
+ g = bil.open_zarr("ace-cup-eel") # lazy; nothing read until sliced
134
+ bil.zarr_levels(g) # ['0', ..., '6'], as served, not as declared
135
+ g["6"][0, 0, 400] # one plane of the coarsest level, a few chunks
136
+ bil.zarr_thumbnail(g, max_size=512)
137
+ ```
138
+
139
+ A zarr store is a directory, so it never shows in the inventory's extension histogram; `find_zarr()` is how to know a dataset ships one. Requires `pip install "scigantic-bil[zarr]"` (zarr 3, fsspec, aiohttp) and Python 3.11 or newer, which is zarr 3's own floor; on 3.10 the extra installs nothing and `open_zarr()` raises a clear ImportError.
140
+
141
+ ## Caching
142
+
143
+ On by default. Metadata responses, directory listings and manifests are cached to `~/.cache/scigantic-bil` (macOS: `~/Library/Caches/scigantic-bil`; override with `SCIGANTIC_BIL_CACHE` or `enable_cache(cache_dir=...)`) and expire after 7 days, since BIL republishes its inventory every few days. Inventory snapshots are immutable once published and are kept as plain TSV files without expiry. Image bytes are never cached.
144
+
145
+ ```python
146
+ bil.disable_cache()
147
+ bil.enable_cache(ttl_days=1)
148
+ bil.clear_cache()
149
+ ```
150
+
151
+ ## Command line
152
+
153
+ ```console
154
+ $ scigantic-bil summary
155
+ $ scigantic-bil light-sheet --limit 20
156
+ $ scigantic-bil search "iDISCO" --json
157
+ $ scigantic-bil filter --technique fMOST --species mouse --max-gb 100
158
+ $ scigantic-bil info ace-cup-eel
159
+ $ scigantic-bil files ace-bin-run --zarr
160
+ $ scigantic-bil thumbnail ace-bin-run slice.png --size 512
161
+ ```
162
+
163
+ ## Testing
164
+
165
+ Every test runs live against BIL, no mocks, the same philosophy as the rest of the scigantic-* packages. The suite takes about 15 seconds. CI runs Python 3.10 through 3.14 plus `mypy --strict`.
166
+
167
+ ## License
168
+
169
+ MIT-0 for the code. See [Data license](#data-license) for the data.
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "scigantic-bil"
7
+ version = "0.1.0"
8
+ description = "Search the Brain Image Library (14,000+ BICCN/BICAN brain microscopy datasets, 6 PB) and read light-sheet, fMOST and STPT volumes over HTTP without downloading them: a typed catalog over BIL's daily inventory and metadata API, lazy file listing, single-slice TIFF reads, thumbnails, and OME-Zarr access."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT-0" }
12
+ authors = [{ name = "Scigantic" }]
13
+ keywords = ["brain-image-library", "biccn", "light-sheet", "microscopy", "neuroscience", "fmost", "stpt", "ome-zarr", "tiff", "whole-brain"]
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT No Attribution License (MIT-0)",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Typing :: Typed",
23
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
24
+ "Topic :: Scientific/Engineering :: Image Processing",
25
+ ]
26
+
27
+ dependencies = [
28
+ # Upper bounds checked live against each package's current PyPI major on
29
+ # 2026-09-08 (requests 2.x, numpy 2.x, tifffile 2025.x); defensive
30
+ # ceilings, not a response to a known break in any next major.
31
+ "requests>=2.28,<3",
32
+ "numpy>=1.24,<3",
33
+ "tifffile>=2023.7,<2027",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ # zarr is only needed for open_zarr(); every other function works without
38
+ # it. zarr 3 reads HTTP stores through fsspec, which needs aiohttp for http.
39
+ # Pinned to zarr 3.x on purpose: several BIL stores are zarr v2, which
40
+ # zarr 3 still reads, and the Scigantic neuroscience notebook image asserts
41
+ # zarr stays at 3.x (ngff-zarr needs it). zarr 3 itself requires Python
42
+ # 3.11 (verified: pip on 3.10 finds no zarr>=3), so the extra is a no-op on
43
+ # 3.10 and the zarr tests skip there; CI's 3.10 leg is how the
44
+ # without-zarr path stays exercised.
45
+ zarr = [
46
+ "zarr>=3,<4; python_version >= '3.11'",
47
+ "fsspec>=2024.1; python_version >= '3.11'",
48
+ "aiohttp>=3.9,<4; python_version >= '3.11'",
49
+ ]
50
+ pandas = ["pandas>=1.5,<4"]
51
+ dev = ["pytest>=7", "mypy>=1.10", "types-requests", "pandas-stubs", "pillow>=10"]
52
+
53
+ [project.urls]
54
+ Homepage = "https://scigantic.com"
55
+ Repository = "https://github.com/Scigantic/scigantic-bil"
56
+ Issues = "https://github.com/Scigantic/scigantic-bil/issues"
57
+
58
+ [project.scripts]
59
+ scigantic-bil = "scigantic_bil.cli:main"
60
+
61
+ [tool.setuptools.packages.find]
62
+ where = ["src"]
63
+
64
+ [tool.setuptools.package-data]
65
+ scigantic_bil = ["py.typed"]
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests"]
69
+
70
+ [tool.mypy]
71
+ strict = true
72
+
73
+ [[tool.mypy.overrides]]
74
+ module = ["tifffile", "tifffile.*", "zarr", "zarr.*", "fsspec", "fsspec.*", "PIL", "PIL.*"]
75
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,72 @@
1
+ """scigantic-bil: search the Brain Image Library and read its volumes over
2
+ HTTP without downloading them.
3
+
4
+ import scigantic_bil as bil
5
+
6
+ cat = bil.BilCatalog.load() # every dataset, from BIL's daily inventory
7
+ ls = cat.light_sheet() # light-sheet datasets, technique + fulltext
8
+ d = bil.retrieve(ls[0].bildid) # full record: abstract, instrument, rights
9
+ files = bil.slices(d) # z-slice TIFFs, in order, nothing fetched
10
+ img = bil.thumbnail(d) # one middle slice, downsampled
11
+ """
12
+
13
+ from ._client import BilError, BilNotFoundError
14
+ from ._version import __version__
15
+ from .api import fulltext, query, retrieve, retrieve_many
16
+ from .cache import cache_dir, clear as clear_cache, disable_cache, enable_cache, is_cache_enabled
17
+ from .catalog import BilCatalog, available_inventory_dates
18
+ from .files import download, find, find_zarr, is_store_dir, list_files, manifest, resolve_url, walk
19
+ from .images import (
20
+ HttpFile,
21
+ UnsupportedFormatError,
22
+ downsample,
23
+ open_zarr,
24
+ read_stack,
25
+ read_tiff,
26
+ slices,
27
+ thumbnail,
28
+ zarr_levels,
29
+ zarr_thumbnail,
30
+ )
31
+ from .models import Contributor, Dataset, DatasetDetail, FileEntry, Publication, dataset_url
32
+
33
+ __all__ = [
34
+ "__version__",
35
+ "BilError",
36
+ "BilNotFoundError",
37
+ "UnsupportedFormatError",
38
+ "BilCatalog",
39
+ "available_inventory_dates",
40
+ "Dataset",
41
+ "DatasetDetail",
42
+ "Contributor",
43
+ "Publication",
44
+ "FileEntry",
45
+ "dataset_url",
46
+ "query",
47
+ "fulltext",
48
+ "retrieve",
49
+ "retrieve_many",
50
+ "list_files",
51
+ "manifest",
52
+ "walk",
53
+ "find",
54
+ "find_zarr",
55
+ "is_store_dir",
56
+ "download",
57
+ "resolve_url",
58
+ "read_tiff",
59
+ "read_stack",
60
+ "slices",
61
+ "thumbnail",
62
+ "downsample",
63
+ "open_zarr",
64
+ "zarr_levels",
65
+ "zarr_thumbnail",
66
+ "HttpFile",
67
+ "enable_cache",
68
+ "disable_cache",
69
+ "is_cache_enabled",
70
+ "cache_dir",
71
+ "clear_cache",
72
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,88 @@
1
+ """Shared HTTP plumbing: one lazily-built requests.Session, retry with
2
+ backoff on transient failures, and the two BIL hosts this package talks to.
3
+
4
+ BIL has no documented rate limit and no throttling header (checked live
5
+ 2026-09-08: responses carry only nginx defaults), so unlike
6
+ scigantic-pubchem there is no token bucket here. Retries cover 429/5xx and
7
+ connection errors only; a 404 is a real answer (a path that does not exist)
8
+ and is raised as BilNotFoundError immediately.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import threading
14
+ import time
15
+ from typing import Any
16
+
17
+ import requests
18
+
19
+ from ._version import __version__
20
+
21
+ API_BASE = "https://api.brainimagelibrary.org"
22
+ DOWNLOAD_BASE = "https://download.brainimagelibrary.org"
23
+
24
+ _USER_AGENT = f"scigantic-bil/{__version__} (+https://scigantic.com; mailto:support@scigantic.com)"
25
+
26
+ _MAX_RETRIES = 4
27
+ _RETRY_STATUS_CODES = {429, 500, 502, 503, 504}
28
+
29
+ _session: requests.Session | None = None
30
+ _session_lock = threading.Lock()
31
+
32
+
33
+ class BilError(Exception):
34
+ """Raised for an HTTP error after retries are exhausted, or for an API
35
+ response whose envelope reports failure."""
36
+
37
+
38
+ class BilNotFoundError(BilError):
39
+ """Raised for a 404: a dataset id, directory or file that does not
40
+ exist. A real outcome, not a transient failure, so never retried."""
41
+
42
+
43
+ def get_session() -> requests.Session:
44
+ global _session
45
+ if _session is None:
46
+ with _session_lock:
47
+ if _session is None:
48
+ _session = requests.Session()
49
+ _session.headers["User-Agent"] = _USER_AGENT
50
+ return _session
51
+
52
+
53
+ def send(
54
+ method: str,
55
+ url: str,
56
+ params: dict[str, Any] | None = None,
57
+ headers: dict[str, str] | None = None,
58
+ stream: bool = False,
59
+ timeout: float = 60.0,
60
+ ) -> requests.Response:
61
+ """Issue one request with retry on 429/5xx and connection errors.
62
+ Raises BilNotFoundError on 404 and BilError on any other failure."""
63
+ session = get_session()
64
+ last_error: Exception | None = None
65
+ for attempt in range(_MAX_RETRIES + 1):
66
+ try:
67
+ resp = session.request(
68
+ method, url, params=params, headers=headers, stream=stream, timeout=timeout
69
+ )
70
+ except requests.RequestException as exc:
71
+ last_error = exc
72
+ if attempt == _MAX_RETRIES:
73
+ break
74
+ time.sleep(1.5 * (2**attempt))
75
+ continue
76
+ if resp.status_code == 404:
77
+ resp.close()
78
+ raise BilNotFoundError(f"404 for {resp.url}")
79
+ if resp.status_code in _RETRY_STATUS_CODES and attempt < _MAX_RETRIES:
80
+ resp.close()
81
+ time.sleep(1.5 * (2**attempt))
82
+ continue
83
+ if resp.status_code >= 400:
84
+ body = resp.text[:300]
85
+ resp.close()
86
+ raise BilError(f"HTTP {resp.status_code} for {resp.url}: {body}")
87
+ return resp
88
+ raise BilError(f"request to {url} failed after {_MAX_RETRIES + 1} attempts: {last_error}")
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"