Spaces:
Runtime error
feat(phase-4): Gradio UI with NiiVue visualization (#5)
Browse files* feat(phase-4): Gradio UI with NiiVue visualization
Implements Phase 4 of the stroke-deepisles-demo project:
UI Components:
- Gradio 5 application with case selection and segmentation
- NiiVue WebGL viewer with base64 data URLs for 3D visualization
- Matplotlib slice comparison views (axial/coronal/sagittal)
- Settings accordion for fast mode and ground truth toggle
Data Path Refactoring:
- Renamed data/scratch/ to data/isles24/ for clarity
- Added data/discovery/ for schema reports
- Updated all specs and code to use new paths
- Added data/README.md documenting folder structure
Files Added:
- src/stroke_deepisles_demo/ui/{app,viewer,components}.py
- app.py (HF Spaces entry point)
- tests/ui/{test_app,test_viewer}.py
- data/README.md
Quality:
- 88 tests passing (15 new UI tests)
- ruff + mypy clean
- Lazy initialization pattern to avoid import-time side effects
* fix: address CodeRabbit review feedback
- data/README.md: Add `text` language to code blocks (markdownlint MD040)
- test_viewer.py: Move matplotlib.use("Agg") before pyplot import (real bug)
- components.py: Add logger.warning for exception handling (debugging aid)
- app.py: Add explicit noqa comment explaining runtime import need
- .gitignore +6 -3
- app.py +11 -0
- data/README.md +39 -0
- data/scratch/.gitkeep +0 -0
- docs/specs/00-context.md +5 -5
- docs/specs/02-phase-1-data-access.md +4 -4
- docs/specs/data-discovery.md +7 -8
- pyproject.toml +1 -0
- scripts/discovery/inspect_isles24.py +2 -2
- src/stroke_deepisles_demo/data/loader.py +1 -1
- src/stroke_deepisles_demo/ui/__init__.py +5 -1
- src/stroke_deepisles_demo/ui/app.py +170 -0
- src/stroke_deepisles_demo/ui/components.py +84 -0
- src/stroke_deepisles_demo/ui/viewer.py +324 -0
- tests/data/test_integration_real_data.py +3 -3
- tests/ui/test_app.py +75 -0
- tests/ui/test_viewer.py +146 -0
- uv.lock +305 -0
|
@@ -206,6 +206,9 @@ marimo/_static/
|
|
| 206 |
marimo/_lsp/
|
| 207 |
__marimo__/
|
| 208 |
|
| 209 |
-
#
|
| 210 |
-
data/
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
marimo/_lsp/
|
| 207 |
__marimo__/
|
| 208 |
|
| 209 |
+
# Local ISLES24 data (git-ignored, download separately)
|
| 210 |
+
data/isles24/
|
| 211 |
+
|
| 212 |
+
# Discovery artifacts (schema reports, samples)
|
| 213 |
+
data/discovery/
|
| 214 |
+
data/scratch/
|
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Entry point for Hugging Face Spaces deployment."""
|
| 2 |
+
|
| 3 |
+
import gradio as gr
|
| 4 |
+
|
| 5 |
+
from stroke_deepisles_demo.ui.app import get_demo
|
| 6 |
+
|
| 7 |
+
# Create the demo instance at module level for Gradio
|
| 8 |
+
demo = get_demo()
|
| 9 |
+
|
| 10 |
+
if __name__ == "__main__":
|
| 11 |
+
demo.launch(theme=gr.themes.Soft(), css="footer {visibility: hidden}")
|
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Data Directory
|
| 2 |
+
|
| 3 |
+
This folder contains local neuroimaging data for the stroke-deepisles-demo project.
|
| 4 |
+
|
| 5 |
+
## Structure
|
| 6 |
+
|
| 7 |
+
```text
|
| 8 |
+
data/
|
| 9 |
+
├── README.md # This file (tracked)
|
| 10 |
+
├── isles24/ # ISLES24 NIfTI files (gitignored)
|
| 11 |
+
│ ├── Images-DWI/ # DWI volumes (149 files)
|
| 12 |
+
│ ├── Images-ADC/ # ADC maps (149 files)
|
| 13 |
+
│ └── Masks/ # Ground truth lesion masks (149 files)
|
| 14 |
+
└── discovery/ # Schema reports (gitignored)
|
| 15 |
+
└── isles24_schema_report.txt
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
## Setup
|
| 19 |
+
|
| 20 |
+
1. Download ISLES24-MR-Lite from [HuggingFace](https://huggingface.co/datasets/YongchengYAO/ISLES24-MR-Lite)
|
| 21 |
+
2. Extract the ZIP files into `data/isles24/`:
|
| 22 |
+
- `Images-DWI.zip` → `data/isles24/Images-DWI/`
|
| 23 |
+
- `Images-ADC.zip` → `data/isles24/Images-ADC/`
|
| 24 |
+
- `Masks.zip` → `data/isles24/Masks/`
|
| 25 |
+
|
| 26 |
+
## File Naming Convention
|
| 27 |
+
|
| 28 |
+
Files follow BIDS-like naming:
|
| 29 |
+
```text
|
| 30 |
+
sub-stroke{XXXX}_ses-02_{modality}.nii.gz
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
Example: `sub-stroke0005_ses-02_dwi.nii.gz`
|
| 34 |
+
|
| 35 |
+
## Notes
|
| 36 |
+
|
| 37 |
+
- All data files are gitignored to avoid committing large binaries
|
| 38 |
+
- The `discovery/` folder contains schema reports from data exploration scripts
|
| 39 |
+
- See `docs/specs/02-phase-1-data-access.md` for detailed data loading documentation
|
|
File without changes
|
|
@@ -40,9 +40,9 @@ This showcases that:
|
|
| 40 |
|
| 41 |
**The original ISLES24-MR-Lite dataset is NOT properly uploaded to HuggingFace.**
|
| 42 |
|
| 43 |
-
It's just raw ZIP files dumped on HF, not a proper Dataset with parquet/Arrow format. This means `load_dataset()` fails. See `data/
|
| 44 |
|
| 45 |
-
**Workaround**: We extracted the ZIPs locally to `data/
|
| 46 |
|
| 47 |
## why we need tobias's datasets fork
|
| 48 |
|
|
@@ -75,20 +75,20 @@ We pin to this branch until upstream merges the PRs.
|
|
| 75 |
### 1. data source: ISLES24-MR-Lite
|
| 76 |
|
| 77 |
- **HF Dataset**: [YongchengYAO/ISLES24-MR-Lite](https://huggingface.co/datasets/YongchengYAO/ISLES24-MR-Lite) (**BROKEN** - raw ZIPs, not proper dataset)
|
| 78 |
-
- **Local extracted**: `data/
|
| 79 |
- **Content**: 149 acute stroke MRI cases with DWI, ADC, and manual infarct masks
|
| 80 |
- **Origin**: Subset of ISLES 2024 challenge data
|
| 81 |
- **Why suitable**: DeepISLES was trained on ISLES 2022, so ISLES24 is an **external** test set (no data leakage)
|
| 82 |
|
| 83 |
**File structure** (after extraction):
|
| 84 |
```
|
| 85 |
-
data/
|
| 86 |
├── Images-DWI/sub-stroke{XXXX}_ses-02_dwi.nii.gz # 149 files
|
| 87 |
├── Images-ADC/sub-stroke{XXXX}_ses-02_adc.nii.gz # 149 files
|
| 88 |
└── Masks/sub-stroke{XXXX}_ses-02_lesion-msk.nii.gz # 149 files
|
| 89 |
```
|
| 90 |
|
| 91 |
-
**Schema reference**: `data/
|
| 92 |
|
| 93 |
### 2. model: DeepISLES
|
| 94 |
|
|
|
|
| 40 |
|
| 41 |
**The original ISLES24-MR-Lite dataset is NOT properly uploaded to HuggingFace.**
|
| 42 |
|
| 43 |
+
It's just raw ZIP files dumped on HF, not a proper Dataset with parquet/Arrow format. This means `load_dataset()` fails. See `data/discovery/isles24_schema_report.txt` for full details.
|
| 44 |
|
| 45 |
+
**Workaround**: We extracted the ZIPs locally to `data/isles24/` (git-ignored) and will implement a file-based loader first. Later, we'll re-upload properly and verify full HF consumption.
|
| 46 |
|
| 47 |
## why we need tobias's datasets fork
|
| 48 |
|
|
|
|
| 75 |
### 1. data source: ISLES24-MR-Lite
|
| 76 |
|
| 77 |
- **HF Dataset**: [YongchengYAO/ISLES24-MR-Lite](https://huggingface.co/datasets/YongchengYAO/ISLES24-MR-Lite) (**BROKEN** - raw ZIPs, not proper dataset)
|
| 78 |
+
- **Local extracted**: `data/isles24/` (git-ignored)
|
| 79 |
- **Content**: 149 acute stroke MRI cases with DWI, ADC, and manual infarct masks
|
| 80 |
- **Origin**: Subset of ISLES 2024 challenge data
|
| 81 |
- **Why suitable**: DeepISLES was trained on ISLES 2022, so ISLES24 is an **external** test set (no data leakage)
|
| 82 |
|
| 83 |
**File structure** (after extraction):
|
| 84 |
```
|
| 85 |
+
data/isles24/
|
| 86 |
├── Images-DWI/sub-stroke{XXXX}_ses-02_dwi.nii.gz # 149 files
|
| 87 |
├── Images-ADC/sub-stroke{XXXX}_ses-02_adc.nii.gz # 149 files
|
| 88 |
└── Masks/sub-stroke{XXXX}_ses-02_lesion-msk.nii.gz # 149 files
|
| 89 |
```
|
| 90 |
|
| 91 |
+
**Schema reference**: `data/discovery/isles24_schema_report.txt`
|
| 92 |
|
| 93 |
### 2. model: DeepISLES
|
| 94 |
|
|
@@ -14,7 +14,7 @@ Implement a data loading layer that provides typed access to ISLES24 neuroimagin
|
|
| 14 |
| Columns: `dwi`, `adc`, `mask`, `participant_id` | No columns - just raw ZIP files |
|
| 15 |
| Parquet/Arrow format | Three ZIP archives dumped on HF |
|
| 16 |
|
| 17 |
-
**Evidence**: `data/
|
| 18 |
|
| 19 |
This means the demo must be built in phases:
|
| 20 |
1. **Phase 1A**: Local file loader (works NOW with extracted data)
|
|
@@ -29,7 +29,7 @@ This means the demo must be built in phases:
|
|
| 29 |
### data location
|
| 30 |
|
| 31 |
```
|
| 32 |
-
data/
|
| 33 |
├── Images-DWI/ # 149 files
|
| 34 |
│ └── sub-stroke{XXXX}_ses-02_dwi.nii.gz
|
| 35 |
├── Images-ADC/ # 149 files
|
|
@@ -87,7 +87,7 @@ class DatasetInfo:
|
|
| 87 |
|
| 88 |
|
| 89 |
def load_isles_dataset(
|
| 90 |
-
source: str | Path = "data/
|
| 91 |
*,
|
| 92 |
local_mode: bool = True, # Default to local for now
|
| 93 |
) -> LocalDataset:
|
|
@@ -288,7 +288,7 @@ def test_get_case_returns_case_files(synthetic_isles_dir):
|
|
| 288 |
### done criteria (phase 1a)
|
| 289 |
|
| 290 |
- [ ] `uv run pytest tests/data/ -v` passes
|
| 291 |
-
- [ ] Can load all 149 cases from `data/
|
| 292 |
- [ ] `list_case_ids()` returns 149 subject IDs
|
| 293 |
- [ ] `get_case("sub-stroke0005")` returns valid CaseFiles
|
| 294 |
- [ ] Type checking passes: `uv run mypy src/stroke_deepisles_demo/data/`
|
|
|
|
| 14 |
| Columns: `dwi`, `adc`, `mask`, `participant_id` | No columns - just raw ZIP files |
|
| 15 |
| Parquet/Arrow format | Three ZIP archives dumped on HF |
|
| 16 |
|
| 17 |
+
**Evidence**: `data/discovery/isles24_schema_report.txt`
|
| 18 |
|
| 19 |
This means the demo must be built in phases:
|
| 20 |
1. **Phase 1A**: Local file loader (works NOW with extracted data)
|
|
|
|
| 29 |
### data location
|
| 30 |
|
| 31 |
```
|
| 32 |
+
data/isles24/ # Git-ignored
|
| 33 |
├── Images-DWI/ # 149 files
|
| 34 |
│ └── sub-stroke{XXXX}_ses-02_dwi.nii.gz
|
| 35 |
├── Images-ADC/ # 149 files
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
def load_isles_dataset(
|
| 90 |
+
source: str | Path = "data/isles24",
|
| 91 |
*,
|
| 92 |
local_mode: bool = True, # Default to local for now
|
| 93 |
) -> LocalDataset:
|
|
|
|
| 288 |
### done criteria (phase 1a)
|
| 289 |
|
| 290 |
- [ ] `uv run pytest tests/data/ -v` passes
|
| 291 |
+
- [ ] Can load all 149 cases from `data/isles24/`
|
| 292 |
- [ ] `list_case_ids()` returns 149 subject IDs
|
| 293 |
- [ ] `get_case("sub-stroke0005")` returns valid CaseFiles
|
| 294 |
- [ ] Type checking passes: `uv run mypy src/stroke_deepisles_demo/data/`
|
|
@@ -23,10 +23,9 @@ scripts/discovery/
|
|
| 23 |
### data & artifacts
|
| 24 |
All downloaded samples, temporary outputs, and schema reports reside in:
|
| 25 |
```
|
| 26 |
-
data/
|
| 27 |
-
├──
|
| 28 |
-
|
| 29 |
-
└── samples/ # Raw data samples (IGNORED)
|
| 30 |
```
|
| 31 |
|
| 32 |
## discovery workflow
|
|
@@ -44,11 +43,11 @@ Write a focused script in `scripts/discovery/` that:
|
|
| 44 |
### 2. execution
|
| 45 |
Run the script from the project root:
|
| 46 |
```bash
|
| 47 |
-
uv run scripts/discovery/inspect_hf_dataset.py > data/
|
| 48 |
```
|
| 49 |
|
| 50 |
### 3. verification
|
| 51 |
-
Manually review `data/
|
| 52 |
- **Check**: Do column names match `CaseAdapter` expectations?
|
| 53 |
- **Check**: Are file paths strings or objects?
|
| 54 |
- **Check**: Are required fields (DWI, ADC) actually present?
|
|
@@ -62,6 +61,6 @@ If the report contradicts the code/specs:
|
|
| 62 |
## git configuration
|
| 63 |
Ensure `.gitignore` includes:
|
| 64 |
```gitignore
|
| 65 |
-
data/
|
| 66 |
-
|
| 67 |
```
|
|
|
|
| 23 |
### data & artifacts
|
| 24 |
All downloaded samples, temporary outputs, and schema reports reside in:
|
| 25 |
```
|
| 26 |
+
data/
|
| 27 |
+
├── isles24/ # Extracted ISLES24 data (IGNORED)
|
| 28 |
+
└── discovery/ # Schema reports, samples (IGNORED)
|
|
|
|
| 29 |
```
|
| 30 |
|
| 31 |
## discovery workflow
|
|
|
|
| 43 |
### 2. execution
|
| 44 |
Run the script from the project root:
|
| 45 |
```bash
|
| 46 |
+
uv run scripts/discovery/inspect_hf_dataset.py > data/discovery/schema_report.txt
|
| 47 |
```
|
| 48 |
|
| 49 |
### 3. verification
|
| 50 |
+
Manually review `data/discovery/schema_report.txt`.
|
| 51 |
- **Check**: Do column names match `CaseAdapter` expectations?
|
| 52 |
- **Check**: Are file paths strings or objects?
|
| 53 |
- **Check**: Are required fields (DWI, ADC) actually present?
|
|
|
|
| 61 |
## git configuration
|
| 62 |
Ensure `.gitignore` includes:
|
| 63 |
```gitignore
|
| 64 |
+
data/isles24/
|
| 65 |
+
data/discovery/
|
| 66 |
```
|
|
@@ -33,6 +33,7 @@ dependencies = [
|
|
| 33 |
|
| 34 |
# UI (Gradio 5.x)
|
| 35 |
"gradio>=5.0.0",
|
|
|
|
| 36 |
|
| 37 |
# Networking
|
| 38 |
"requests>=2.0.0",
|
|
|
|
| 33 |
|
| 34 |
# UI (Gradio 5.x)
|
| 35 |
"gradio>=5.0.0",
|
| 36 |
+
"matplotlib>=3.8.0",
|
| 37 |
|
| 38 |
# Networking
|
| 39 |
"requests>=2.0.0",
|
|
@@ -7,7 +7,7 @@ to document its exact schema before building adapters.
|
|
| 7 |
|
| 8 |
Per: docs/specs/data-discovery.md
|
| 9 |
|
| 10 |
-
Output: data/
|
| 11 |
"""
|
| 12 |
|
| 13 |
from __future__ import annotations
|
|
@@ -20,7 +20,7 @@ from typing import Any
|
|
| 20 |
|
| 21 |
# Constants
|
| 22 |
DATASET_ID = "YongchengYAO/ISLES24-MR-Lite"
|
| 23 |
-
OUTPUT_DIR = Path(__file__).parent.parent.parent / "data" / "
|
| 24 |
REPORT_FILE = OUTPUT_DIR / "isles24_schema_report.txt"
|
| 25 |
|
| 26 |
|
|
|
|
| 7 |
|
| 8 |
Per: docs/specs/data-discovery.md
|
| 9 |
|
| 10 |
+
Output: data/discovery/isles24_schema_report.txt
|
| 11 |
"""
|
| 12 |
|
| 13 |
from __future__ import annotations
|
|
|
|
| 20 |
|
| 21 |
# Constants
|
| 22 |
DATASET_ID = "YongchengYAO/ISLES24-MR-Lite"
|
| 23 |
+
OUTPUT_DIR = Path(__file__).parent.parent.parent / "data" / "discovery"
|
| 24 |
REPORT_FILE = OUTPUT_DIR / "isles24_schema_report.txt"
|
| 25 |
|
| 26 |
|
|
@@ -21,7 +21,7 @@ class DatasetInfo:
|
|
| 21 |
|
| 22 |
|
| 23 |
def load_isles_dataset(
|
| 24 |
-
source: str | Path = "data/
|
| 25 |
*,
|
| 26 |
local_mode: bool = True, # Default to local for now
|
| 27 |
) -> LocalDataset:
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
def load_isles_dataset(
|
| 24 |
+
source: str | Path = "data/isles24",
|
| 25 |
*,
|
| 26 |
local_mode: bool = True, # Default to local for now
|
| 27 |
) -> LocalDataset:
|
|
@@ -1 +1,5 @@
|
|
| 1 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""UI module for stroke-deepisles-demo."""
|
| 2 |
+
|
| 3 |
+
from stroke_deepisles_demo.ui.app import create_app, get_demo
|
| 4 |
+
|
| 5 |
+
__all__ = ["create_app", "get_demo"]
|
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Main Gradio application for stroke-deepisles-demo."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
from matplotlib.figure import Figure # noqa: TC002 - needed at runtime for Gradio
|
| 10 |
+
|
| 11 |
+
from stroke_deepisles_demo.pipeline import run_pipeline_on_case
|
| 12 |
+
from stroke_deepisles_demo.ui.components import (
|
| 13 |
+
create_case_selector,
|
| 14 |
+
create_results_display,
|
| 15 |
+
create_settings_accordion,
|
| 16 |
+
)
|
| 17 |
+
from stroke_deepisles_demo.ui.viewer import (
|
| 18 |
+
create_niivue_html,
|
| 19 |
+
nifti_to_data_url,
|
| 20 |
+
render_slice_comparison,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def run_segmentation(
|
| 27 |
+
case_id: str, fast_mode: bool, show_ground_truth: bool
|
| 28 |
+
) -> tuple[str, Figure | None, dict[str, Any], str | None, str]:
|
| 29 |
+
"""
|
| 30 |
+
Run segmentation and return results for display.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
case_id: Selected case identifier
|
| 34 |
+
fast_mode: Whether to use fast mode (SEALS)
|
| 35 |
+
show_ground_truth: Whether to show ground truth in plots
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
Tuple of (niivue_html, slice_fig, metrics_dict, download_path, status_msg)
|
| 39 |
+
"""
|
| 40 |
+
if not case_id:
|
| 41 |
+
return (
|
| 42 |
+
"",
|
| 43 |
+
None,
|
| 44 |
+
{},
|
| 45 |
+
None,
|
| 46 |
+
"Please select a case first.",
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
logger.info("Running segmentation for %s", case_id)
|
| 51 |
+
result = run_pipeline_on_case(case_id, fast=fast_mode, compute_dice=True)
|
| 52 |
+
|
| 53 |
+
# 1. NiiVue Visualization
|
| 54 |
+
# We need data URLs for the browser
|
| 55 |
+
# Note: This reads the file content into memory (base64)
|
| 56 |
+
# For large datasets, this might be heavy, but for ISLES24-MR-Lite (cropped) it's fine.
|
| 57 |
+
# Assuming DWI is the background
|
| 58 |
+
dwi_path = result.input_files["dwi"]
|
| 59 |
+
dwi_url = nifti_to_data_url(dwi_path)
|
| 60 |
+
|
| 61 |
+
mask_url = None
|
| 62 |
+
if result.prediction_mask and result.prediction_mask.exists():
|
| 63 |
+
mask_url = nifti_to_data_url(result.prediction_mask)
|
| 64 |
+
|
| 65 |
+
niivue_html = create_niivue_html(
|
| 66 |
+
dwi_url,
|
| 67 |
+
mask_url,
|
| 68 |
+
height=500,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# 2. Slice Comparison (Static Plot)
|
| 72 |
+
gt_path = result.ground_truth if show_ground_truth else None
|
| 73 |
+
slice_fig = render_slice_comparison(
|
| 74 |
+
dwi_path=dwi_path,
|
| 75 |
+
prediction_path=result.prediction_mask,
|
| 76 |
+
ground_truth_path=gt_path,
|
| 77 |
+
orientation="axial",
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
# 3. Metrics
|
| 81 |
+
metrics = {
|
| 82 |
+
"case_id": result.case_id,
|
| 83 |
+
"dice_score": result.dice_score,
|
| 84 |
+
"elapsed_seconds": round(result.elapsed_seconds, 2),
|
| 85 |
+
"model": "SEALS (Fast)" if fast_mode else "Ensemble",
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# 4. Download
|
| 89 |
+
download_path = str(result.prediction_mask)
|
| 90 |
+
|
| 91 |
+
status_msg = (
|
| 92 |
+
f"Success! Dice: {result.dice_score:.3f}"
|
| 93 |
+
if result.dice_score is not None
|
| 94 |
+
else "Success!"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
return niivue_html, slice_fig, metrics, download_path, status_msg
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
logger.exception("Error running segmentation")
|
| 101 |
+
return "", None, {}, None, f"Error: {e!s}"
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def create_app() -> gr.Blocks:
|
| 105 |
+
"""
|
| 106 |
+
Create the Gradio application.
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Configured gr.Blocks application
|
| 110 |
+
"""
|
| 111 |
+
with gr.Blocks(
|
| 112 |
+
title="Stroke Lesion Segmentation Demo",
|
| 113 |
+
) as demo:
|
| 114 |
+
# Header
|
| 115 |
+
gr.Markdown("""
|
| 116 |
+
# 🧠 Stroke Lesion Segmentation Demo
|
| 117 |
+
|
| 118 |
+
This demo runs [DeepISLES](https://github.com/ezequieldlrosa/DeepIsles)
|
| 119 |
+
stroke segmentation on cases from
|
| 120 |
+
[ISLES24-MR-Lite](https://huggingface.co/datasets/YongchengYAO/ISLES24-MR-Lite).
|
| 121 |
+
|
| 122 |
+
**Note:** The pipeline runs inside a Docker container. First run might be slow (pulling image).
|
| 123 |
+
""")
|
| 124 |
+
|
| 125 |
+
with gr.Row():
|
| 126 |
+
# Left column: Controls
|
| 127 |
+
with gr.Column(scale=1):
|
| 128 |
+
case_selector = create_case_selector()
|
| 129 |
+
settings = create_settings_accordion()
|
| 130 |
+
run_btn = gr.Button("Run Segmentation", variant="primary")
|
| 131 |
+
status = gr.Textbox(label="Status", interactive=False)
|
| 132 |
+
|
| 133 |
+
# Right column: Results
|
| 134 |
+
with gr.Column(scale=2):
|
| 135 |
+
results = create_results_display()
|
| 136 |
+
|
| 137 |
+
# Event handlers
|
| 138 |
+
run_btn.click(
|
| 139 |
+
fn=run_segmentation,
|
| 140 |
+
inputs=[
|
| 141 |
+
case_selector,
|
| 142 |
+
settings["fast_mode"],
|
| 143 |
+
settings["show_ground_truth"],
|
| 144 |
+
],
|
| 145 |
+
outputs=[
|
| 146 |
+
results["niivue_viewer"],
|
| 147 |
+
results["slice_plot"],
|
| 148 |
+
results["metrics"],
|
| 149 |
+
results["download"],
|
| 150 |
+
status,
|
| 151 |
+
],
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
return demo # type: ignore[no-any-return]
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# Lazy initialization pattern
|
| 158 |
+
_demo: gr.Blocks | None = None
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def get_demo() -> gr.Blocks:
|
| 162 |
+
"""Get the global demo instance, creating it if necessary."""
|
| 163 |
+
global _demo
|
| 164 |
+
if _demo is None:
|
| 165 |
+
_demo = create_app()
|
| 166 |
+
return _demo
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
if __name__ == "__main__":
|
| 170 |
+
get_demo().launch(theme=gr.themes.Soft(), css="footer {visibility: hidden}")
|
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reusable UI components."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
|
| 9 |
+
from stroke_deepisles_demo.data import list_case_ids
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def create_case_selector() -> gr.Dropdown:
|
| 15 |
+
"""
|
| 16 |
+
Create a dropdown for selecting cases.
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
Configured gr.Dropdown component
|
| 20 |
+
"""
|
| 21 |
+
try:
|
| 22 |
+
case_ids = list_case_ids()
|
| 23 |
+
except Exception:
|
| 24 |
+
logger.warning("Failed to load case IDs, using fallback", exc_info=True)
|
| 25 |
+
case_ids = ["Error loading cases"]
|
| 26 |
+
|
| 27 |
+
return gr.Dropdown(
|
| 28 |
+
choices=case_ids,
|
| 29 |
+
value=case_ids[0] if case_ids else None,
|
| 30 |
+
label="Select Case",
|
| 31 |
+
info="Choose a case from ISLES24-MR-Lite",
|
| 32 |
+
filterable=True,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def create_results_display() -> dict[str, gr.components.Component]:
|
| 37 |
+
"""
|
| 38 |
+
Create results display components.
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
Dictionary of component name -> gr.Component
|
| 42 |
+
"""
|
| 43 |
+
# Using gr.Group to group them visually
|
| 44 |
+
with gr.Group():
|
| 45 |
+
# NiiVue visualization uses HTML
|
| 46 |
+
niivue_viewer = gr.HTML(label="Interactive 3D Viewer")
|
| 47 |
+
|
| 48 |
+
# Slice comparisons (Matplotlib)
|
| 49 |
+
slice_plot = gr.Plot(label="Slice Comparison")
|
| 50 |
+
|
| 51 |
+
metrics = gr.JSON(label="Metrics")
|
| 52 |
+
download = gr.File(label="Download Prediction")
|
| 53 |
+
|
| 54 |
+
return {
|
| 55 |
+
"niivue_viewer": niivue_viewer,
|
| 56 |
+
"slice_plot": slice_plot,
|
| 57 |
+
"metrics": metrics,
|
| 58 |
+
"download": download,
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def create_settings_accordion() -> dict[str, gr.components.Component]:
|
| 63 |
+
"""
|
| 64 |
+
Create expandable settings section.
|
| 65 |
+
|
| 66 |
+
Returns:
|
| 67 |
+
Dictionary of setting name -> gr.Component
|
| 68 |
+
"""
|
| 69 |
+
with gr.Accordion("Advanced Settings", open=False):
|
| 70 |
+
fast_mode = gr.Checkbox(
|
| 71 |
+
value=True,
|
| 72 |
+
label="Fast Mode (SEALS)",
|
| 73 |
+
info="Run SEALS only (ISLES'22 winner, requires DWI+ADC). Disable for full ensemble (requires FLAIR).",
|
| 74 |
+
)
|
| 75 |
+
show_ground_truth = gr.Checkbox(
|
| 76 |
+
value=True,
|
| 77 |
+
label="Show Ground Truth",
|
| 78 |
+
info="Display ground truth mask if available",
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
return {
|
| 82 |
+
"fast_mode": fast_mode,
|
| 83 |
+
"show_ground_truth": show_ground_truth,
|
| 84 |
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Neuroimaging visualization for Gradio."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import base64
|
| 6 |
+
from typing import TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
import matplotlib.pyplot as plt
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
from stroke_deepisles_demo.metrics import load_nifti_as_array
|
| 12 |
+
|
| 13 |
+
if TYPE_CHECKING:
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
from matplotlib.figure import Figure
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def nifti_to_data_url(nifti_path: Path) -> str:
|
| 20 |
+
"""
|
| 21 |
+
Convert NIfTI file to base64 data URL for NiiVue.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
nifti_path: Path to NIfTI file
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
Data URL string
|
| 28 |
+
"""
|
| 29 |
+
# We load the raw bytes directly to avoid re-serialization overhead if possible
|
| 30 |
+
# But nibabel might be safer to ensure valid nifti if we were manipulating
|
| 31 |
+
# Here we just want the file content.
|
| 32 |
+
with nifti_path.open("rb") as f:
|
| 33 |
+
nifti_bytes = f.read()
|
| 34 |
+
|
| 35 |
+
nifti_b64 = base64.b64encode(nifti_bytes).decode("utf-8")
|
| 36 |
+
return f"data:application/octet-stream;base64,{nifti_b64}"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def get_slice_at_max_lesion(
|
| 40 |
+
mask_path: Path,
|
| 41 |
+
orientation: str = "axial",
|
| 42 |
+
) -> int:
|
| 43 |
+
"""
|
| 44 |
+
Find slice index with maximum lesion area.
|
| 45 |
+
|
| 46 |
+
Useful for displaying the most informative slice.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
mask_path: Path to lesion mask NIfTI
|
| 50 |
+
orientation: Slice orientation ("axial", "coronal", "sagittal")
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
Slice index with maximum lesion area
|
| 54 |
+
"""
|
| 55 |
+
data, _ = load_nifti_as_array(mask_path)
|
| 56 |
+
|
| 57 |
+
# Determine axes to sum over
|
| 58 |
+
# Default NIfTI (RAS+): x=sagittal, y=coronal, z=axial
|
| 59 |
+
# array indices: [x, y, z]
|
| 60 |
+
if orientation == "sagittal":
|
| 61 |
+
# Sum over y and z (axes 1, 2) -> result shape [x]
|
| 62 |
+
lesion_counts = np.sum(data > 0, axis=(1, 2))
|
| 63 |
+
elif orientation == "coronal":
|
| 64 |
+
# Sum over x and z (axes 0, 2) -> result shape [y]
|
| 65 |
+
lesion_counts = np.sum(data > 0, axis=(0, 2))
|
| 66 |
+
else: # axial
|
| 67 |
+
# Sum over x and y (axes 0, 1) -> result shape [z]
|
| 68 |
+
lesion_counts = np.sum(data > 0, axis=(0, 1))
|
| 69 |
+
|
| 70 |
+
max_slice = int(np.argmax(lesion_counts))
|
| 71 |
+
|
| 72 |
+
# If mask is empty, return middle slice
|
| 73 |
+
if np.max(lesion_counts) == 0:
|
| 74 |
+
if orientation == "sagittal":
|
| 75 |
+
return int(data.shape[0] // 2)
|
| 76 |
+
elif orientation == "coronal":
|
| 77 |
+
return int(data.shape[1] // 2)
|
| 78 |
+
else:
|
| 79 |
+
return int(data.shape[2] // 2)
|
| 80 |
+
|
| 81 |
+
return max_slice
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def render_3panel_view(
|
| 85 |
+
nifti_path: Path,
|
| 86 |
+
mask_path: Path | None = None,
|
| 87 |
+
*,
|
| 88 |
+
mask_alpha: float = 0.5,
|
| 89 |
+
) -> Figure:
|
| 90 |
+
"""
|
| 91 |
+
Render axial/coronal/sagittal slices with optional mask overlay.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
nifti_path: Path to base NIfTI volume
|
| 95 |
+
mask_path: Optional path to mask for overlay
|
| 96 |
+
mask_alpha: Transparency of mask overlay
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
Matplotlib figure with 3-panel view
|
| 100 |
+
"""
|
| 101 |
+
data, _ = load_nifti_as_array(nifti_path)
|
| 102 |
+
mask_data = None
|
| 103 |
+
if mask_path:
|
| 104 |
+
mask_data, _ = load_nifti_as_array(mask_path)
|
| 105 |
+
|
| 106 |
+
# Get slices (middle by default, or max lesion if mask exists)
|
| 107 |
+
mid_x, mid_y, mid_z = data.shape[0] // 2, data.shape[1] // 2, data.shape[2] // 2
|
| 108 |
+
|
| 109 |
+
if mask_data is not None and np.any(mask_data > 0):
|
| 110 |
+
# Try to find a slice that intersects the lesion best
|
| 111 |
+
# Simplified: use center of mass of lesion
|
| 112 |
+
coords = np.argwhere(mask_data > 0)
|
| 113 |
+
center = coords.mean(axis=0).astype(int)
|
| 114 |
+
mid_x, mid_y, mid_z = center[0], center[1], center[2]
|
| 115 |
+
|
| 116 |
+
# Create figure
|
| 117 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
| 118 |
+
fig.patch.set_facecolor("black")
|
| 119 |
+
|
| 120 |
+
# Axial (XY plane, Z fixed) - often needs rotation 90 deg
|
| 121 |
+
# NIfTI data[x, y, z]. To display standard axial:
|
| 122 |
+
# usually imshow(data[:, :, z].T, origin='lower')
|
| 123 |
+
ax_slice = np.rot90(data[:, :, mid_z])
|
| 124 |
+
axes[0].imshow(ax_slice, cmap="gray")
|
| 125 |
+
axes[0].set_title(f"Axial (z={mid_z})", color="white")
|
| 126 |
+
if mask_data is not None:
|
| 127 |
+
m_slice = np.rot90(mask_data[:, :, mid_z])
|
| 128 |
+
axes[0].imshow(
|
| 129 |
+
np.ma.masked_where(m_slice == 0, m_slice), # type: ignore[no-untyped-call]
|
| 130 |
+
cmap="Reds",
|
| 131 |
+
alpha=mask_alpha,
|
| 132 |
+
vmin=0,
|
| 133 |
+
vmax=1,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
# Coronal (XZ plane, Y fixed)
|
| 137 |
+
cor_slice = np.rot90(data[:, mid_y, :])
|
| 138 |
+
axes[1].imshow(cor_slice, cmap="gray")
|
| 139 |
+
axes[1].set_title(f"Coronal (y={mid_y})", color="white")
|
| 140 |
+
if mask_data is not None:
|
| 141 |
+
m_slice = np.rot90(mask_data[:, mid_y, :])
|
| 142 |
+
axes[1].imshow(
|
| 143 |
+
np.ma.masked_where(m_slice == 0, m_slice), # type: ignore[no-untyped-call]
|
| 144 |
+
cmap="Reds",
|
| 145 |
+
alpha=mask_alpha,
|
| 146 |
+
vmin=0,
|
| 147 |
+
vmax=1,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
# Sagittal (YZ plane, X fixed)
|
| 151 |
+
sag_slice = np.rot90(data[mid_x, :, :])
|
| 152 |
+
axes[2].imshow(sag_slice, cmap="gray")
|
| 153 |
+
axes[2].set_title(f"Sagittal (x={mid_x})", color="white")
|
| 154 |
+
if mask_data is not None:
|
| 155 |
+
m_slice = np.rot90(mask_data[mid_x, :, :])
|
| 156 |
+
axes[2].imshow(
|
| 157 |
+
np.ma.masked_where(m_slice == 0, m_slice), # type: ignore[no-untyped-call]
|
| 158 |
+
cmap="Reds",
|
| 159 |
+
alpha=mask_alpha,
|
| 160 |
+
vmin=0,
|
| 161 |
+
vmax=1,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
for ax in axes:
|
| 165 |
+
ax.axis("off")
|
| 166 |
+
|
| 167 |
+
plt.tight_layout()
|
| 168 |
+
return fig
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def render_slice_comparison(
|
| 172 |
+
dwi_path: Path,
|
| 173 |
+
prediction_path: Path,
|
| 174 |
+
ground_truth_path: Path | None = None,
|
| 175 |
+
*,
|
| 176 |
+
slice_idx: int | None = None,
|
| 177 |
+
orientation: str = "axial",
|
| 178 |
+
) -> Figure:
|
| 179 |
+
"""
|
| 180 |
+
Render side-by-side comparison of DWI, prediction, and ground truth.
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
dwi_path: Path to DWI NIfTI
|
| 184 |
+
prediction_path: Path to predicted mask NIfTI
|
| 185 |
+
ground_truth_path: Optional path to ground truth mask
|
| 186 |
+
slice_idx: Slice index (default: max lesion or middle)
|
| 187 |
+
orientation: One of "axial", "coronal", "sagittal"
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
Matplotlib figure with comparison view
|
| 191 |
+
"""
|
| 192 |
+
dwi_data, _ = load_nifti_as_array(dwi_path)
|
| 193 |
+
pred_data, _ = load_nifti_as_array(prediction_path)
|
| 194 |
+
gt_data = None
|
| 195 |
+
if ground_truth_path:
|
| 196 |
+
gt_data, _ = load_nifti_as_array(ground_truth_path)
|
| 197 |
+
|
| 198 |
+
# Determine slice index
|
| 199 |
+
if slice_idx is None:
|
| 200 |
+
# Use prediction to find best slice
|
| 201 |
+
slice_idx = get_slice_at_max_lesion(prediction_path, orientation)
|
| 202 |
+
|
| 203 |
+
# Extract slices based on orientation
|
| 204 |
+
# Assuming data[x, y, z]
|
| 205 |
+
if orientation == "sagittal":
|
| 206 |
+
# X fixed
|
| 207 |
+
d_slice = np.rot90(dwi_data[slice_idx, :, :])
|
| 208 |
+
p_slice = np.rot90(pred_data[slice_idx, :, :])
|
| 209 |
+
g_slice = np.rot90(gt_data[slice_idx, :, :]) if gt_data is not None else None
|
| 210 |
+
elif orientation == "coronal":
|
| 211 |
+
# Y fixed
|
| 212 |
+
d_slice = np.rot90(dwi_data[:, slice_idx, :])
|
| 213 |
+
p_slice = np.rot90(pred_data[:, slice_idx, :])
|
| 214 |
+
g_slice = np.rot90(gt_data[:, slice_idx, :]) if gt_data is not None else None
|
| 215 |
+
else:
|
| 216 |
+
# Z fixed (axial)
|
| 217 |
+
d_slice = np.rot90(dwi_data[:, :, slice_idx])
|
| 218 |
+
p_slice = np.rot90(pred_data[:, :, slice_idx])
|
| 219 |
+
g_slice = np.rot90(gt_data[:, :, slice_idx]) if gt_data is not None else None
|
| 220 |
+
|
| 221 |
+
# Plotting
|
| 222 |
+
num_plots = 3 if gt_data is not None else 2
|
| 223 |
+
fig, axes = plt.subplots(1, num_plots, figsize=(5 * num_plots, 5))
|
| 224 |
+
fig.patch.set_facecolor("black")
|
| 225 |
+
if num_plots == 2:
|
| 226 |
+
axes = np.array(axes) # handle single case if needed, but subplots(1,2) returns array
|
| 227 |
+
|
| 228 |
+
# 1. DWI
|
| 229 |
+
axes[0].imshow(d_slice, cmap="gray")
|
| 230 |
+
axes[0].set_title("DWI Input", color="white")
|
| 231 |
+
|
| 232 |
+
# 2. Prediction
|
| 233 |
+
axes[1].imshow(d_slice, cmap="gray")
|
| 234 |
+
axes[1].imshow(
|
| 235 |
+
np.ma.masked_where(p_slice == 0, p_slice), # type: ignore[no-untyped-call]
|
| 236 |
+
cmap="Reds",
|
| 237 |
+
alpha=0.5,
|
| 238 |
+
vmin=0,
|
| 239 |
+
vmax=1,
|
| 240 |
+
)
|
| 241 |
+
axes[1].set_title("Prediction", color="white")
|
| 242 |
+
|
| 243 |
+
# 3. GT (if available)
|
| 244 |
+
if gt_data is not None:
|
| 245 |
+
axes[2].imshow(d_slice, cmap="gray")
|
| 246 |
+
axes[2].imshow(
|
| 247 |
+
np.ma.masked_where(g_slice == 0, g_slice), # type: ignore[no-untyped-call]
|
| 248 |
+
cmap="Greens",
|
| 249 |
+
alpha=0.5,
|
| 250 |
+
vmin=0,
|
| 251 |
+
vmax=1,
|
| 252 |
+
)
|
| 253 |
+
axes[2].set_title("Ground Truth", color="white")
|
| 254 |
+
|
| 255 |
+
for ax in axes:
|
| 256 |
+
ax.axis("off")
|
| 257 |
+
|
| 258 |
+
plt.tight_layout()
|
| 259 |
+
return fig
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def create_niivue_html(
|
| 263 |
+
volume_url: str,
|
| 264 |
+
mask_url: str | None = None,
|
| 265 |
+
*,
|
| 266 |
+
height: int = 400,
|
| 267 |
+
) -> str:
|
| 268 |
+
"""
|
| 269 |
+
Create HTML/JS for NiiVue viewer.
|
| 270 |
+
|
| 271 |
+
Args:
|
| 272 |
+
volume_url: URL to volume NIfTI file
|
| 273 |
+
mask_url: Optional URL to mask NIfTI file
|
| 274 |
+
height: Viewer height in pixels
|
| 275 |
+
|
| 276 |
+
Returns:
|
| 277 |
+
HTML string with embedded NiiVue viewer
|
| 278 |
+
"""
|
| 279 |
+
mask_js = ""
|
| 280 |
+
if mask_url:
|
| 281 |
+
mask_js = f"""
|
| 282 |
+
volumes.push({{
|
| 283 |
+
url: '{mask_url}',
|
| 284 |
+
colorMap: 'red',
|
| 285 |
+
opacity: 0.5
|
| 286 |
+
}});
|
| 287 |
+
"""
|
| 288 |
+
|
| 289 |
+
return f"""
|
| 290 |
+
<div style="width:100%; height:{height}px; background:#000; border-radius:8px; position: relative;">
|
| 291 |
+
<canvas id="niivue-canvas" style="width:100%; height:100%;"></canvas>
|
| 292 |
+
</div>
|
| 293 |
+
<script type="module">
|
| 294 |
+
const niivueModule = await import('https://unpkg.com/@niivue/niivue@0.57.0/dist/index.js');
|
| 295 |
+
const Niivue = niivueModule.Niivue;
|
| 296 |
+
|
| 297 |
+
const nv = new Niivue({{
|
| 298 |
+
logging: false,
|
| 299 |
+
show3Dcrosshair: true,
|
| 300 |
+
textHeight: 0.04,
|
| 301 |
+
backColor: [0, 0, 0, 1]
|
| 302 |
+
}});
|
| 303 |
+
|
| 304 |
+
await nv.attachTo('niivue-canvas');
|
| 305 |
+
|
| 306 |
+
const volumes = [{{
|
| 307 |
+
url: '{volume_url}',
|
| 308 |
+
name: 'input.nii.gz'
|
| 309 |
+
}}];
|
| 310 |
+
{mask_js}
|
| 311 |
+
|
| 312 |
+
await nv.loadVolumes(volumes);
|
| 313 |
+
|
| 314 |
+
// Multiplanar + 3D view
|
| 315 |
+
nv.setSliceType(nv.sliceTypeMultiplanar);
|
| 316 |
+
// Check if setMultiplanarLayout exists (added in newer versions, 0.57 has it)
|
| 317 |
+
if (nv.setMultiplanarLayout) {{
|
| 318 |
+
nv.setMultiplanarLayout(2);
|
| 319 |
+
}}
|
| 320 |
+
nv.opts.show3Dcrosshair = true;
|
| 321 |
+
nv.setRenderAzimuthElevation(120, 10);
|
| 322 |
+
nv.drawScene();
|
| 323 |
+
</script>
|
| 324 |
+
"""
|
|
@@ -8,10 +8,10 @@ import pytest
|
|
| 8 |
|
| 9 |
from stroke_deepisles_demo.data.loader import load_isles_dataset
|
| 10 |
|
| 11 |
-
REAL_DATA_PATH = Path("data/
|
| 12 |
|
| 13 |
|
| 14 |
-
@pytest.mark.skipif(not REAL_DATA_PATH.exists(), reason="Real data not found in data/
|
| 15 |
def test_load_real_data_count() -> None:
|
| 16 |
"""Verify that we can load the expected number of cases from real data."""
|
| 17 |
dataset = load_isles_dataset(source=REAL_DATA_PATH)
|
|
@@ -28,7 +28,7 @@ def test_load_real_data_count() -> None:
|
|
| 28 |
assert case["ground_truth"].exists()
|
| 29 |
|
| 30 |
|
| 31 |
-
@pytest.mark.skipif(not REAL_DATA_PATH.exists(), reason="Real data not found in data/
|
| 32 |
def test_real_data_subject_ids() -> None:
|
| 33 |
"""Verify subject ID formatting on real data."""
|
| 34 |
dataset = load_isles_dataset(source=REAL_DATA_PATH)
|
|
|
|
| 8 |
|
| 9 |
from stroke_deepisles_demo.data.loader import load_isles_dataset
|
| 10 |
|
| 11 |
+
REAL_DATA_PATH = Path("data/isles24")
|
| 12 |
|
| 13 |
|
| 14 |
+
@pytest.mark.skipif(not REAL_DATA_PATH.exists(), reason="Real data not found in data/isles24")
|
| 15 |
def test_load_real_data_count() -> None:
|
| 16 |
"""Verify that we can load the expected number of cases from real data."""
|
| 17 |
dataset = load_isles_dataset(source=REAL_DATA_PATH)
|
|
|
|
| 28 |
assert case["ground_truth"].exists()
|
| 29 |
|
| 30 |
|
| 31 |
+
@pytest.mark.skipif(not REAL_DATA_PATH.exists(), reason="Real data not found in data/isles24")
|
| 32 |
def test_real_data_subject_ids() -> None:
|
| 33 |
"""Verify subject ID formatting on real data."""
|
| 34 |
dataset = load_isles_dataset(source=REAL_DATA_PATH)
|
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smoke tests for Gradio app."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from unittest.mock import MagicMock, patch
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_app_module_imports() -> None:
|
| 9 |
+
"""App module imports without side effects."""
|
| 10 |
+
# This should not launch the app or make network calls
|
| 11 |
+
from stroke_deepisles_demo.ui import app
|
| 12 |
+
|
| 13 |
+
assert hasattr(app, "create_app")
|
| 14 |
+
assert hasattr(app, "get_demo")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_create_app_returns_blocks() -> None:
|
| 18 |
+
"""create_app returns a gr.Blocks instance."""
|
| 19 |
+
import gradio as gr
|
| 20 |
+
|
| 21 |
+
# Mock list_case_ids to avoid network call
|
| 22 |
+
with patch("stroke_deepisles_demo.ui.components.list_case_ids", return_value=["sub-001"]):
|
| 23 |
+
from stroke_deepisles_demo.ui.app import create_app
|
| 24 |
+
|
| 25 |
+
app = create_app()
|
| 26 |
+
|
| 27 |
+
assert isinstance(app, gr.Blocks)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_viewer_module_imports() -> None:
|
| 31 |
+
"""Viewer module imports without errors."""
|
| 32 |
+
from stroke_deepisles_demo.ui import viewer
|
| 33 |
+
|
| 34 |
+
assert hasattr(viewer, "render_3panel_view")
|
| 35 |
+
assert hasattr(viewer, "create_niivue_html")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_components_module_imports() -> None:
|
| 39 |
+
"""Components module imports without errors."""
|
| 40 |
+
from stroke_deepisles_demo.ui import components
|
| 41 |
+
|
| 42 |
+
assert hasattr(components, "create_case_selector")
|
| 43 |
+
assert hasattr(components, "create_results_display")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_run_segmentation_logic() -> None:
|
| 47 |
+
"""Test run_segmentation logic with mocks."""
|
| 48 |
+
from stroke_deepisles_demo.pipeline import PipelineResult
|
| 49 |
+
from stroke_deepisles_demo.ui.app import run_segmentation
|
| 50 |
+
|
| 51 |
+
mock_result = PipelineResult(
|
| 52 |
+
case_id="sub-001",
|
| 53 |
+
input_files={"dwi": MagicMock(), "adc": MagicMock()},
|
| 54 |
+
staged_dir=MagicMock(),
|
| 55 |
+
prediction_mask=MagicMock(),
|
| 56 |
+
ground_truth=MagicMock(),
|
| 57 |
+
dice_score=0.85,
|
| 58 |
+
elapsed_seconds=10.5,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Mock everything that touches files/network
|
| 62 |
+
with (
|
| 63 |
+
patch("stroke_deepisles_demo.ui.app.run_pipeline_on_case", return_value=mock_result),
|
| 64 |
+
patch("stroke_deepisles_demo.ui.app.nifti_to_data_url", return_value="data:image..."),
|
| 65 |
+
patch("stroke_deepisles_demo.ui.app.create_niivue_html", return_value="<div></div>"),
|
| 66 |
+
patch("stroke_deepisles_demo.ui.app.render_slice_comparison", return_value=MagicMock()),
|
| 67 |
+
):
|
| 68 |
+
html, _fig, metrics, _dl_path, status = run_segmentation(
|
| 69 |
+
"sub-001", fast_mode=True, show_ground_truth=True
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
assert html == "<div></div>"
|
| 73 |
+
assert metrics["case_id"] == "sub-001"
|
| 74 |
+
assert metrics["dice_score"] == 0.85
|
| 75 |
+
assert "Success" in status
|
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for viewer module."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
import matplotlib
|
| 8 |
+
|
| 9 |
+
# Non-interactive backend for tests - must be before pyplot import
|
| 10 |
+
matplotlib.use("Agg")
|
| 11 |
+
|
| 12 |
+
import matplotlib.pyplot as plt
|
| 13 |
+
import numpy as np
|
| 14 |
+
from matplotlib.figure import Figure
|
| 15 |
+
|
| 16 |
+
from stroke_deepisles_demo.ui.viewer import (
|
| 17 |
+
create_niivue_html,
|
| 18 |
+
get_slice_at_max_lesion,
|
| 19 |
+
render_3panel_view,
|
| 20 |
+
render_slice_comparison,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
if TYPE_CHECKING:
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class TestRender3PanelView:
|
| 28 |
+
"""Tests for render_3panel_view."""
|
| 29 |
+
|
| 30 |
+
def test_returns_matplotlib_figure(self, synthetic_nifti_3d: Path) -> None:
|
| 31 |
+
"""Returns a matplotlib Figure object."""
|
| 32 |
+
fig = render_3panel_view(synthetic_nifti_3d)
|
| 33 |
+
|
| 34 |
+
assert isinstance(fig, Figure)
|
| 35 |
+
plt.close(fig)
|
| 36 |
+
|
| 37 |
+
def test_has_three_axes(self, synthetic_nifti_3d: Path) -> None:
|
| 38 |
+
"""Figure has 3 subplots (axial, coronal, sagittal)."""
|
| 39 |
+
fig = render_3panel_view(synthetic_nifti_3d)
|
| 40 |
+
|
| 41 |
+
assert len(fig.axes) == 3
|
| 42 |
+
plt.close(fig)
|
| 43 |
+
|
| 44 |
+
def test_overlay_mask_when_provided(self, synthetic_nifti_3d: Path, temp_dir: Path) -> None:
|
| 45 |
+
"""Overlays mask when mask_path provided."""
|
| 46 |
+
# Create a simple mask
|
| 47 |
+
import nibabel as nib
|
| 48 |
+
|
| 49 |
+
mask_data = np.zeros((10, 10, 10), dtype=np.uint8)
|
| 50 |
+
mask_data[4:6, 4:6, 4:6] = 1
|
| 51 |
+
mask_img = nib.Nifti1Image(mask_data, np.eye(4)) # type: ignore
|
| 52 |
+
mask_path = temp_dir / "mask.nii.gz"
|
| 53 |
+
nib.save(mask_img, mask_path) # type: ignore
|
| 54 |
+
|
| 55 |
+
fig = render_3panel_view(synthetic_nifti_3d, mask_path=mask_path)
|
| 56 |
+
|
| 57 |
+
# Should not raise
|
| 58 |
+
assert fig is not None
|
| 59 |
+
plt.close(fig)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class TestRenderSliceComparison:
|
| 63 |
+
"""Tests for render_slice_comparison."""
|
| 64 |
+
|
| 65 |
+
def test_comparison_without_ground_truth(self, synthetic_nifti_3d: Path) -> None:
|
| 66 |
+
"""Works when ground truth is None."""
|
| 67 |
+
fig = render_slice_comparison(
|
| 68 |
+
synthetic_nifti_3d,
|
| 69 |
+
synthetic_nifti_3d, # Use same as prediction for test
|
| 70 |
+
ground_truth_path=None,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
assert isinstance(fig, Figure)
|
| 74 |
+
plt.close(fig)
|
| 75 |
+
|
| 76 |
+
def test_comparison_with_ground_truth(self, synthetic_nifti_3d: Path) -> None:
|
| 77 |
+
"""Works when ground truth is provided."""
|
| 78 |
+
fig = render_slice_comparison(
|
| 79 |
+
synthetic_nifti_3d,
|
| 80 |
+
synthetic_nifti_3d,
|
| 81 |
+
ground_truth_path=synthetic_nifti_3d,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
assert isinstance(fig, Figure)
|
| 85 |
+
plt.close(fig)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class TestGetSliceAtMaxLesion:
|
| 89 |
+
"""Tests for get_slice_at_max_lesion."""
|
| 90 |
+
|
| 91 |
+
def test_finds_slice_with_lesion(self, temp_dir: Path) -> None:
|
| 92 |
+
"""Returns slice index where lesion is largest."""
|
| 93 |
+
import nibabel as nib
|
| 94 |
+
|
| 95 |
+
# Create mask with lesion at slice 7
|
| 96 |
+
mask_data = np.zeros((10, 10, 10), dtype=np.uint8)
|
| 97 |
+
mask_data[:, :, 7] = 1 # Full slice 7 is lesion
|
| 98 |
+
|
| 99 |
+
mask_img = nib.Nifti1Image(mask_data, np.eye(4)) # type: ignore
|
| 100 |
+
mask_path = temp_dir / "mask.nii.gz"
|
| 101 |
+
nib.save(mask_img, mask_path) # type: ignore
|
| 102 |
+
|
| 103 |
+
slice_idx = get_slice_at_max_lesion(mask_path, orientation="axial")
|
| 104 |
+
|
| 105 |
+
assert slice_idx == 7
|
| 106 |
+
|
| 107 |
+
def test_returns_middle_for_empty_mask(self, temp_dir: Path) -> None:
|
| 108 |
+
"""Returns middle slice when mask is empty."""
|
| 109 |
+
import nibabel as nib
|
| 110 |
+
|
| 111 |
+
mask_data = np.zeros((10, 10, 20), dtype=np.uint8)
|
| 112 |
+
mask_img = nib.Nifti1Image(mask_data, np.eye(4)) # type: ignore
|
| 113 |
+
mask_path = temp_dir / "mask.nii.gz"
|
| 114 |
+
nib.save(mask_img, mask_path) # type: ignore
|
| 115 |
+
|
| 116 |
+
slice_idx = get_slice_at_max_lesion(mask_path, orientation="axial")
|
| 117 |
+
|
| 118 |
+
assert slice_idx == 10 # Middle of 20
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class TestCreateNiivueHtml:
|
| 122 |
+
"""Tests for create_niivue_html."""
|
| 123 |
+
|
| 124 |
+
def test_includes_volume_url(self) -> None:
|
| 125 |
+
"""Generated HTML includes the volume URL."""
|
| 126 |
+
html = create_niivue_html("http://example.com/brain.nii.gz")
|
| 127 |
+
|
| 128 |
+
assert "http://example.com/brain.nii.gz" in html
|
| 129 |
+
|
| 130 |
+
def test_includes_mask_when_provided(self) -> None:
|
| 131 |
+
"""Generated HTML includes mask URL when provided."""
|
| 132 |
+
html = create_niivue_html(
|
| 133 |
+
"http://example.com/brain.nii.gz",
|
| 134 |
+
mask_url="http://example.com/mask.nii.gz",
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
assert "http://example.com/mask.nii.gz" in html
|
| 138 |
+
|
| 139 |
+
def test_sets_height(self) -> None:
|
| 140 |
+
"""Generated HTML respects height parameter."""
|
| 141 |
+
html = create_niivue_html(
|
| 142 |
+
"http://example.com/brain.nii.gz",
|
| 143 |
+
height=600,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
assert "height:600px" in html
|
|
@@ -396,6 +396,88 @@ wheels = [
|
|
| 396 |
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
| 397 |
]
|
| 398 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
[[package]]
|
| 400 |
name = "coverage"
|
| 401 |
version = "7.12.0"
|
|
@@ -488,6 +570,15 @@ toml = [
|
|
| 488 |
{ name = "tomli", marker = "python_full_version <= '3.11'" },
|
| 489 |
]
|
| 490 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
[[package]]
|
| 492 |
name = "datasets"
|
| 493 |
version = "4.4.2.dev0"
|
|
@@ -560,6 +651,55 @@ wheels = [
|
|
| 560 |
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 },
|
| 561 |
]
|
| 562 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
[[package]]
|
| 564 |
name = "frozenlist"
|
| 565 |
version = "1.8.0"
|
|
@@ -878,6 +1018,96 @@ wheels = [
|
|
| 878 |
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
|
| 879 |
]
|
| 880 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
[[package]]
|
| 882 |
name = "librt"
|
| 883 |
version = "0.6.3"
|
|
@@ -1027,6 +1257,70 @@ wheels = [
|
|
| 1027 |
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 },
|
| 1028 |
]
|
| 1029 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1030 |
[[package]]
|
| 1031 |
name = "mdurl"
|
| 1032 |
version = "0.1.2"
|
|
@@ -1880,6 +2174,15 @@ wheels = [
|
|
| 1880 |
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
|
| 1881 |
]
|
| 1882 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1883 |
[[package]]
|
| 1884 |
name = "pytest"
|
| 1885 |
version = "9.0.1"
|
|
@@ -2130,6 +2433,7 @@ dependencies = [
|
|
| 2130 |
{ name = "datasets" },
|
| 2131 |
{ name = "gradio" },
|
| 2132 |
{ name = "huggingface-hub" },
|
|
|
|
| 2133 |
{ name = "nibabel" },
|
| 2134 |
{ name = "numpy" },
|
| 2135 |
{ name = "pydantic" },
|
|
@@ -2153,6 +2457,7 @@ requires-dist = [
|
|
| 2153 |
{ name = "datasets", git = "https://github.com/CloseChoice/datasets.git?rev=feat%2Fbids-loader-streaming-upload-fix" },
|
| 2154 |
{ name = "gradio", specifier = ">=5.0.0" },
|
| 2155 |
{ name = "huggingface-hub", specifier = ">=0.25.0" },
|
|
|
|
| 2156 |
{ name = "nibabel", specifier = ">=5.2.0" },
|
| 2157 |
{ name = "numpy", specifier = ">=1.26.0" },
|
| 2158 |
{ name = "pydantic", specifier = ">=2.5.0" },
|
|
|
|
| 396 |
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
| 397 |
]
|
| 398 |
|
| 399 |
+
[[package]]
|
| 400 |
+
name = "contourpy"
|
| 401 |
+
version = "1.3.3"
|
| 402 |
+
source = { registry = "https://pypi.org/simple" }
|
| 403 |
+
dependencies = [
|
| 404 |
+
{ name = "numpy" },
|
| 405 |
+
]
|
| 406 |
+
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 }
|
| 407 |
+
wheels = [
|
| 408 |
+
{ url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773 },
|
| 409 |
+
{ url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149 },
|
| 410 |
+
{ url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222 },
|
| 411 |
+
{ url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234 },
|
| 412 |
+
{ url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555 },
|
| 413 |
+
{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238 },
|
| 414 |
+
{ url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218 },
|
| 415 |
+
{ url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867 },
|
| 416 |
+
{ url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677 },
|
| 417 |
+
{ url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234 },
|
| 418 |
+
{ url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123 },
|
| 419 |
+
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419 },
|
| 420 |
+
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979 },
|
| 421 |
+
{ url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653 },
|
| 422 |
+
{ url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536 },
|
| 423 |
+
{ url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397 },
|
| 424 |
+
{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601 },
|
| 425 |
+
{ url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288 },
|
| 426 |
+
{ url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386 },
|
| 427 |
+
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018 },
|
| 428 |
+
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567 },
|
| 429 |
+
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655 },
|
| 430 |
+
{ url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257 },
|
| 431 |
+
{ url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034 },
|
| 432 |
+
{ url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672 },
|
| 433 |
+
{ url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234 },
|
| 434 |
+
{ url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169 },
|
| 435 |
+
{ url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859 },
|
| 436 |
+
{ url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062 },
|
| 437 |
+
{ url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932 },
|
| 438 |
+
{ url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024 },
|
| 439 |
+
{ url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578 },
|
| 440 |
+
{ url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524 },
|
| 441 |
+
{ url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730 },
|
| 442 |
+
{ url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897 },
|
| 443 |
+
{ url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751 },
|
| 444 |
+
{ url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486 },
|
| 445 |
+
{ url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106 },
|
| 446 |
+
{ url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548 },
|
| 447 |
+
{ url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297 },
|
| 448 |
+
{ url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023 },
|
| 449 |
+
{ url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157 },
|
| 450 |
+
{ url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570 },
|
| 451 |
+
{ url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713 },
|
| 452 |
+
{ url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189 },
|
| 453 |
+
{ url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251 },
|
| 454 |
+
{ url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810 },
|
| 455 |
+
{ url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871 },
|
| 456 |
+
{ url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264 },
|
| 457 |
+
{ url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819 },
|
| 458 |
+
{ url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650 },
|
| 459 |
+
{ url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833 },
|
| 460 |
+
{ url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692 },
|
| 461 |
+
{ url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424 },
|
| 462 |
+
{ url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300 },
|
| 463 |
+
{ url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769 },
|
| 464 |
+
{ url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892 },
|
| 465 |
+
{ url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748 },
|
| 466 |
+
{ url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554 },
|
| 467 |
+
{ url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118 },
|
| 468 |
+
{ url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555 },
|
| 469 |
+
{ url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295 },
|
| 470 |
+
{ url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027 },
|
| 471 |
+
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428 },
|
| 472 |
+
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331 },
|
| 473 |
+
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831 },
|
| 474 |
+
{ url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809 },
|
| 475 |
+
{ url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593 },
|
| 476 |
+
{ url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202 },
|
| 477 |
+
{ url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207 },
|
| 478 |
+
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315 },
|
| 479 |
+
]
|
| 480 |
+
|
| 481 |
[[package]]
|
| 482 |
name = "coverage"
|
| 483 |
version = "7.12.0"
|
|
|
|
| 570 |
{ name = "tomli", marker = "python_full_version <= '3.11'" },
|
| 571 |
]
|
| 572 |
|
| 573 |
+
[[package]]
|
| 574 |
+
name = "cycler"
|
| 575 |
+
version = "0.12.1"
|
| 576 |
+
source = { registry = "https://pypi.org/simple" }
|
| 577 |
+
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 }
|
| 578 |
+
wheels = [
|
| 579 |
+
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 },
|
| 580 |
+
]
|
| 581 |
+
|
| 582 |
[[package]]
|
| 583 |
name = "datasets"
|
| 584 |
version = "4.4.2.dev0"
|
|
|
|
| 651 |
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 },
|
| 652 |
]
|
| 653 |
|
| 654 |
+
[[package]]
|
| 655 |
+
name = "fonttools"
|
| 656 |
+
version = "4.61.0"
|
| 657 |
+
source = { registry = "https://pypi.org/simple" }
|
| 658 |
+
sdist = { url = "https://files.pythonhosted.org/packages/33/f9/0e84d593c0e12244150280a630999835a64f2852276161b62a0f98318de0/fonttools-4.61.0.tar.gz", hash = "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7", size = 3561884 }
|
| 659 |
+
wheels = [
|
| 660 |
+
{ url = "https://files.pythonhosted.org/packages/fd/be/5aa89cdddf2863d8afbdc19eb8ec5d8d35d40eeeb8e6cf52c5ff1c2dbd33/fonttools-4.61.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a32a16951cbf113d38f1dd8551b277b6e06e0f6f776fece0f99f746d739e1be3", size = 2847553 },
|
| 661 |
+
{ url = "https://files.pythonhosted.org/packages/0d/3e/6ff643b07cead1236a534f51291ae2981721cf419135af5b740c002a66dd/fonttools-4.61.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:328a9c227984bebaf69f3ac9062265f8f6acc7ddf2e4e344c63358579af0aa3d", size = 2388298 },
|
| 662 |
+
{ url = "https://files.pythonhosted.org/packages/c3/15/fca8dfbe7b482e6f240b1aad0ed7c6e2e75e7a28efa3d3a03b570617b5e5/fonttools-4.61.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0bafc8a3b3749c69cc610e5aa3da832d39c2a37a68f03d18ec9a02ecaac04a", size = 5054133 },
|
| 663 |
+
{ url = "https://files.pythonhosted.org/packages/6a/a2/821c61c691b21fd09e07528a9a499cc2b075ac83ddb644aa16c9875a64bc/fonttools-4.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5ca59b7417d149cf24e4c1933c9f44b2957424fc03536f132346d5242e0ebe5", size = 5031410 },
|
| 664 |
+
{ url = "https://files.pythonhosted.org/packages/e8/f6/8b16339e93d03c732c8a23edefe3061b17a5f9107ddc47a3215ecd054cac/fonttools-4.61.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:df8cbce85cf482eb01f4551edca978c719f099c623277bda8332e5dbe7dba09d", size = 5030005 },
|
| 665 |
+
{ url = "https://files.pythonhosted.org/packages/ac/eb/d4e150427bdaa147755239c931bbce829a88149ade5bfd8a327afe565567/fonttools-4.61.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7fb5b84f48a6a733ca3d7f41aa9551908ccabe8669ffe79586560abcc00a9cfd", size = 5154026 },
|
| 666 |
+
{ url = "https://files.pythonhosted.org/packages/7f/5f/3dd00ce0dba6759943c707b1830af8c0bcf6f8f1a9fe46cb82e7ac2aaa74/fonttools-4.61.0-cp311-cp311-win32.whl", hash = "sha256:787ef9dfd1ea9fe49573c272412ae5f479d78e671981819538143bec65863865", size = 2276035 },
|
| 667 |
+
{ url = "https://files.pythonhosted.org/packages/4e/44/798c472f096ddf12955eddb98f4f7c906e7497695d04ce073ddf7161d134/fonttools-4.61.0-cp311-cp311-win_amd64.whl", hash = "sha256:14fafda386377b6131d9e448af42d0926bad47e038de0e5ba1d58c25d621f028", size = 2327290 },
|
| 668 |
+
{ url = "https://files.pythonhosted.org/packages/00/5d/19e5939f773c7cb05480fe2e881d63870b63ee2b4bdb9a77d55b1d36c7b9/fonttools-4.61.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", size = 2846930 },
|
| 669 |
+
{ url = "https://files.pythonhosted.org/packages/25/b2/0658faf66f705293bd7e739a4f038302d188d424926be9c59bdad945664b/fonttools-4.61.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", size = 2383016 },
|
| 670 |
+
{ url = "https://files.pythonhosted.org/packages/29/a3/1fa90b95b690f0d7541f48850adc40e9019374d896c1b8148d15012b2458/fonttools-4.61.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", size = 4949425 },
|
| 671 |
+
{ url = "https://files.pythonhosted.org/packages/af/00/acf18c00f6c501bd6e05ee930f926186f8a8e268265407065688820f1c94/fonttools-4.61.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6cd0d9051b8ddaf7385f99dd82ec2a058e2b46cf1f1961e68e1ff20fcbb61af", size = 4999632 },
|
| 672 |
+
{ url = "https://files.pythonhosted.org/packages/5f/e0/19a2b86e54109b1d2ee8743c96a1d297238ae03243897bc5345c0365f34d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e074bc07c31406f45c418e17c1722e83560f181d122c412fa9e815df0ff74810", size = 4939438 },
|
| 673 |
+
{ url = "https://files.pythonhosted.org/packages/04/35/7b57a5f57d46286360355eff8d6b88c64ab6331107f37a273a71c803798d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a9b78da5d5faa17e63b2404b77feeae105c1b7e75f26020ab7a27b76e02039f", size = 5088960 },
|
| 674 |
+
{ url = "https://files.pythonhosted.org/packages/3e/0e/6c5023eb2e0fe5d1ababc7e221e44acd3ff668781489cc1937a6f83d620a/fonttools-4.61.0-cp312-cp312-win32.whl", hash = "sha256:9821ed77bb676736b88fa87a737c97b6af06e8109667e625a4f00158540ce044", size = 2264404 },
|
| 675 |
+
{ url = "https://files.pythonhosted.org/packages/36/0b/63273128c7c5df19b1e4cd92e0a1e6ea5bb74a400c4905054c96ad60a675/fonttools-4.61.0-cp312-cp312-win_amd64.whl", hash = "sha256:0011d640afa61053bc6590f9a3394bd222de7cfde19346588beabac374e9d8ac", size = 2314427 },
|
| 676 |
+
{ url = "https://files.pythonhosted.org/packages/17/45/334f0d7f181e5473cfb757e1b60f4e60e7fc64f28d406e5d364a952718c0/fonttools-4.61.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba774b8cbd8754f54b8eb58124e8bd45f736b2743325ab1a5229698942b9b433", size = 2841801 },
|
| 677 |
+
{ url = "https://files.pythonhosted.org/packages/cc/63/97b9c78e1f79bc741d4efe6e51f13872d8edb2b36e1b9fb2bab0d4491bb7/fonttools-4.61.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c84b430616ed73ce46e9cafd0bf0800e366a3e02fb7e1ad7c1e214dbe3862b1f", size = 2379024 },
|
| 678 |
+
{ url = "https://files.pythonhosted.org/packages/4e/80/c87bc524a90dbeb2a390eea23eae448286983da59b7e02c67fa0ca96a8c5/fonttools-4.61.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2b734d8391afe3c682320840c8191de9bd24e7eb85768dd4dc06ed1b63dbb1b", size = 4923706 },
|
| 679 |
+
{ url = "https://files.pythonhosted.org/packages/6d/f6/a3b0374811a1de8c3f9207ec88f61ad1bb96f938ed89babae26c065c2e46/fonttools-4.61.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5c5fff72bf31b0e558ed085e4fd7ed96eb85881404ecc39ed2a779e7cf724eb", size = 4979751 },
|
| 680 |
+
{ url = "https://files.pythonhosted.org/packages/a5/3b/30f63b4308b449091573285f9d27619563a84f399946bca3eadc9554afbe/fonttools-4.61.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14a290c5c93fcab76b7f451e6a4b7721b712d90b3b5ed6908f1abcf794e90d6d", size = 4921113 },
|
| 681 |
+
{ url = "https://files.pythonhosted.org/packages/41/6c/58e6e9b7d9d8bf2d7010bd7bb493060b39b02a12d1cda64a8bfb116ce760/fonttools-4.61.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:13e3e20a5463bfeb77b3557d04b30bd6a96a6bb5c15c7b2e7908903e69d437a0", size = 5063183 },
|
| 682 |
+
{ url = "https://files.pythonhosted.org/packages/3f/e3/52c790ab2b07492df059947a1fd7778e105aac5848c0473029a4d20481a2/fonttools-4.61.0-cp313-cp313-win32.whl", hash = "sha256:6781e7a4bb010be1cd69a29927b0305c86b843395f2613bdabe115f7d6ea7f34", size = 2263159 },
|
| 683 |
+
{ url = "https://files.pythonhosted.org/packages/e9/1f/116013b200fbeba871046554d5d2a45fefa69a05c40e9cdfd0d4fff53edc/fonttools-4.61.0-cp313-cp313-win_amd64.whl", hash = "sha256:c53b47834ae41e8e4829171cc44fec0fdf125545a15f6da41776b926b9645a9a", size = 2313530 },
|
| 684 |
+
{ url = "https://files.pythonhosted.org/packages/d3/99/59b1e25987787cb714aa9457cee4c9301b7c2153f0b673e2b8679d37669d/fonttools-4.61.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:96dfc9bc1f2302224e48e6ee37e656eddbab810b724b52e9d9c13a57a6abad01", size = 2841429 },
|
| 685 |
+
{ url = "https://files.pythonhosted.org/packages/2b/b2/4c1911d4332c8a144bb3b44416e274ccca0e297157c971ea1b3fbb855590/fonttools-4.61.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3b2065d94e5d63aafc2591c8b6ccbdb511001d9619f1bca8ad39b745ebeb5efa", size = 2378987 },
|
| 686 |
+
{ url = "https://files.pythonhosted.org/packages/24/b0/f442e90fde5d2af2ae0cb54008ab6411edc557ee33b824e13e1d04925ac9/fonttools-4.61.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e0d87e81e4d869549585ba0beb3f033718501c1095004f5e6aef598d13ebc216", size = 4873270 },
|
| 687 |
+
{ url = "https://files.pythonhosted.org/packages/bb/04/f5d5990e33053c8a59b90b1d7e10ad9b97a73f42c745304da0e709635fab/fonttools-4.61.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cfa2eb9bae650e58f0e8ad53c49d19a844d6034d6b259f30f197238abc1ccee", size = 4968270 },
|
| 688 |
+
{ url = "https://files.pythonhosted.org/packages/94/9f/2091402e0d27c9c8c4bab5de0e5cd146d9609a2d7d1c666bbb75c0011c1a/fonttools-4.61.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4238120002e68296d55e091411c09eab94e111c8ce64716d17df53fd0eb3bb3d", size = 4919799 },
|
| 689 |
+
{ url = "https://files.pythonhosted.org/packages/a8/72/86adab22fde710b829f8ffbc8f264df01928e5b7a8f6177fa29979ebf256/fonttools-4.61.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6ceac262cc62bec01b3bb59abccf41b24ef6580869e306a4e88b7e56bb4bdda", size = 5030966 },
|
| 690 |
+
{ url = "https://files.pythonhosted.org/packages/e8/a7/7c8e31b003349e845b853f5e0a67b95ff6b052fa4f5224f8b72624f5ac69/fonttools-4.61.0-cp314-cp314-win32.whl", hash = "sha256:adbb4ecee1a779469a77377bbe490565effe8fce6fb2e6f95f064de58f8bac85", size = 2267243 },
|
| 691 |
+
{ url = "https://files.pythonhosted.org/packages/20/ee/f434fe7749360497c52b7dcbcfdbccdaab0a71c59f19d572576066717122/fonttools-4.61.0-cp314-cp314-win_amd64.whl", hash = "sha256:02bdf8e04d1a70476564b8640380f04bb4ac74edc1fc71f1bacb840b3e398ee9", size = 2318822 },
|
| 692 |
+
{ url = "https://files.pythonhosted.org/packages/33/b3/c16255320255e5c1863ca2b2599bb61a46e2f566db0bbb9948615a8fe692/fonttools-4.61.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:627216062d90ab0d98215176d8b9562c4dd5b61271d35f130bcd30f6a8aaa33a", size = 2924917 },
|
| 693 |
+
{ url = "https://files.pythonhosted.org/packages/e2/b8/08067ae21de705a817777c02ef36ab0b953cbe91d8adf134f9c2da75ed6d/fonttools-4.61.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b446623c9cd5f14a59493818eaa80255eec2468c27d2c01b56e05357c263195", size = 2413576 },
|
| 694 |
+
{ url = "https://files.pythonhosted.org/packages/42/f1/96ff43f92addce2356780fdc203f2966206f3d22ea20e242c27826fd7442/fonttools-4.61.0-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:70e2a0c0182ee75e493ef33061bfebf140ea57e035481d2f95aa03b66c7a0e05", size = 4877447 },
|
| 695 |
+
{ url = "https://files.pythonhosted.org/packages/d0/1e/a3d8e51ed9ccfd7385e239ae374b78d258a0fb82d82cab99160a014a45d1/fonttools-4.61.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9064b0f55b947e929ac669af5311ab1f26f750214db6dd9a0c97e091e918f486", size = 5095681 },
|
| 696 |
+
{ url = "https://files.pythonhosted.org/packages/eb/f6/d256bd6c1065c146a0bdddf1c62f542e08ae5b3405dbf3fcc52be272f674/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cb5e45a824ce14b90510024d0d39dae51bd4fbb54c42a9334ea8c8cf4d95cbe", size = 4974140 },
|
| 697 |
+
{ url = "https://files.pythonhosted.org/packages/5d/0c/96633eb4b26f138cc48561c6e0c44b4ea48acea56b20b507d6b14f8e80ce/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e5ca8c62efdec7972dfdfd454415c4db49b89aeaefaaacada432f3b7eea9866", size = 5001741 },
|
| 698 |
+
{ url = "https://files.pythonhosted.org/packages/6f/9a/3b536bad3be4f26186f296e749ff17bad3e6d57232c104d752d24b2e265b/fonttools-4.61.0-cp314-cp314t-win32.whl", hash = "sha256:63c7125d31abe3e61d7bb917329b5543c5b3448db95f24081a13aaf064360fc8", size = 2330707 },
|
| 699 |
+
{ url = "https://files.pythonhosted.org/packages/18/ea/e6b9ac610451ee9f04477c311ad126de971f6112cb579fa391d2a8edb00b/fonttools-4.61.0-cp314-cp314t-win_amd64.whl", hash = "sha256:67d841aa272be5500de7f447c40d1d8452783af33b4c3599899319f6ef9ad3c1", size = 2395950 },
|
| 700 |
+
{ url = "https://files.pythonhosted.org/packages/0c/14/634f7daea5ffe6a5f7a0322ba8e1a0e23c9257b80aa91458107896d1dfc7/fonttools-4.61.0-py3-none-any.whl", hash = "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", size = 1144485 },
|
| 701 |
+
]
|
| 702 |
+
|
| 703 |
[[package]]
|
| 704 |
name = "frozenlist"
|
| 705 |
version = "1.8.0"
|
|
|
|
| 1018 |
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
|
| 1019 |
]
|
| 1020 |
|
| 1021 |
+
[[package]]
|
| 1022 |
+
name = "kiwisolver"
|
| 1023 |
+
version = "1.4.9"
|
| 1024 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1025 |
+
sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564 }
|
| 1026 |
+
wheels = [
|
| 1027 |
+
{ url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167 },
|
| 1028 |
+
{ url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579 },
|
| 1029 |
+
{ url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309 },
|
| 1030 |
+
{ url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596 },
|
| 1031 |
+
{ url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548 },
|
| 1032 |
+
{ url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618 },
|
| 1033 |
+
{ url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437 },
|
| 1034 |
+
{ url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742 },
|
| 1035 |
+
{ url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810 },
|
| 1036 |
+
{ url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579 },
|
| 1037 |
+
{ url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071 },
|
| 1038 |
+
{ url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840 },
|
| 1039 |
+
{ url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159 },
|
| 1040 |
+
{ url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686 },
|
| 1041 |
+
{ url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460 },
|
| 1042 |
+
{ url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952 },
|
| 1043 |
+
{ url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756 },
|
| 1044 |
+
{ url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404 },
|
| 1045 |
+
{ url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410 },
|
| 1046 |
+
{ url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631 },
|
| 1047 |
+
{ url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963 },
|
| 1048 |
+
{ url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295 },
|
| 1049 |
+
{ url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987 },
|
| 1050 |
+
{ url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817 },
|
| 1051 |
+
{ url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895 },
|
| 1052 |
+
{ url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992 },
|
| 1053 |
+
{ url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681 },
|
| 1054 |
+
{ url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464 },
|
| 1055 |
+
{ url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961 },
|
| 1056 |
+
{ url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607 },
|
| 1057 |
+
{ url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546 },
|
| 1058 |
+
{ url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482 },
|
| 1059 |
+
{ url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720 },
|
| 1060 |
+
{ url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907 },
|
| 1061 |
+
{ url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334 },
|
| 1062 |
+
{ url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313 },
|
| 1063 |
+
{ url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970 },
|
| 1064 |
+
{ url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894 },
|
| 1065 |
+
{ url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995 },
|
| 1066 |
+
{ url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510 },
|
| 1067 |
+
{ url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903 },
|
| 1068 |
+
{ url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402 },
|
| 1069 |
+
{ url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135 },
|
| 1070 |
+
{ url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409 },
|
| 1071 |
+
{ url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763 },
|
| 1072 |
+
{ url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643 },
|
| 1073 |
+
{ url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818 },
|
| 1074 |
+
{ url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963 },
|
| 1075 |
+
{ url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639 },
|
| 1076 |
+
{ url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741 },
|
| 1077 |
+
{ url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646 },
|
| 1078 |
+
{ url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806 },
|
| 1079 |
+
{ url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605 },
|
| 1080 |
+
{ url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925 },
|
| 1081 |
+
{ url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414 },
|
| 1082 |
+
{ url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272 },
|
| 1083 |
+
{ url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578 },
|
| 1084 |
+
{ url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607 },
|
| 1085 |
+
{ url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150 },
|
| 1086 |
+
{ url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979 },
|
| 1087 |
+
{ url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456 },
|
| 1088 |
+
{ url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621 },
|
| 1089 |
+
{ url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417 },
|
| 1090 |
+
{ url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582 },
|
| 1091 |
+
{ url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514 },
|
| 1092 |
+
{ url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905 },
|
| 1093 |
+
{ url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399 },
|
| 1094 |
+
{ url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197 },
|
| 1095 |
+
{ url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125 },
|
| 1096 |
+
{ url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612 },
|
| 1097 |
+
{ url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990 },
|
| 1098 |
+
{ url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601 },
|
| 1099 |
+
{ url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041 },
|
| 1100 |
+
{ url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897 },
|
| 1101 |
+
{ url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835 },
|
| 1102 |
+
{ url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988 },
|
| 1103 |
+
{ url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260 },
|
| 1104 |
+
{ url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104 },
|
| 1105 |
+
{ url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592 },
|
| 1106 |
+
{ url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281 },
|
| 1107 |
+
{ url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009 },
|
| 1108 |
+
{ url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929 },
|
| 1109 |
+
]
|
| 1110 |
+
|
| 1111 |
[[package]]
|
| 1112 |
name = "librt"
|
| 1113 |
version = "0.6.3"
|
|
|
|
| 1257 |
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 },
|
| 1258 |
]
|
| 1259 |
|
| 1260 |
+
[[package]]
|
| 1261 |
+
name = "matplotlib"
|
| 1262 |
+
version = "3.10.7"
|
| 1263 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1264 |
+
dependencies = [
|
| 1265 |
+
{ name = "contourpy" },
|
| 1266 |
+
{ name = "cycler" },
|
| 1267 |
+
{ name = "fonttools" },
|
| 1268 |
+
{ name = "kiwisolver" },
|
| 1269 |
+
{ name = "numpy" },
|
| 1270 |
+
{ name = "packaging" },
|
| 1271 |
+
{ name = "pillow" },
|
| 1272 |
+
{ name = "pyparsing" },
|
| 1273 |
+
{ name = "python-dateutil" },
|
| 1274 |
+
]
|
| 1275 |
+
sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865 }
|
| 1276 |
+
wheels = [
|
| 1277 |
+
{ url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507 },
|
| 1278 |
+
{ url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565 },
|
| 1279 |
+
{ url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668 },
|
| 1280 |
+
{ url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051 },
|
| 1281 |
+
{ url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878 },
|
| 1282 |
+
{ url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142 },
|
| 1283 |
+
{ url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439 },
|
| 1284 |
+
{ url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389 },
|
| 1285 |
+
{ url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247 },
|
| 1286 |
+
{ url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996 },
|
| 1287 |
+
{ url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153 },
|
| 1288 |
+
{ url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093 },
|
| 1289 |
+
{ url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771 },
|
| 1290 |
+
{ url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812 },
|
| 1291 |
+
{ url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212 },
|
| 1292 |
+
{ url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713 },
|
| 1293 |
+
{ url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527 },
|
| 1294 |
+
{ url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690 },
|
| 1295 |
+
{ url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732 },
|
| 1296 |
+
{ url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727 },
|
| 1297 |
+
{ url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958 },
|
| 1298 |
+
{ url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849 },
|
| 1299 |
+
{ url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225 },
|
| 1300 |
+
{ url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708 },
|
| 1301 |
+
{ url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409 },
|
| 1302 |
+
{ url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054 },
|
| 1303 |
+
{ url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100 },
|
| 1304 |
+
{ url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131 },
|
| 1305 |
+
{ url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787 },
|
| 1306 |
+
{ url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348 },
|
| 1307 |
+
{ url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949 },
|
| 1308 |
+
{ url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247 },
|
| 1309 |
+
{ url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497 },
|
| 1310 |
+
{ url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732 },
|
| 1311 |
+
{ url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240 },
|
| 1312 |
+
{ url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938 },
|
| 1313 |
+
{ url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245 },
|
| 1314 |
+
{ url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411 },
|
| 1315 |
+
{ url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664 },
|
| 1316 |
+
{ url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066 },
|
| 1317 |
+
{ url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832 },
|
| 1318 |
+
{ url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585 },
|
| 1319 |
+
{ url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283 },
|
| 1320 |
+
{ url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733 },
|
| 1321 |
+
{ url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919 },
|
| 1322 |
+
]
|
| 1323 |
+
|
| 1324 |
[[package]]
|
| 1325 |
name = "mdurl"
|
| 1326 |
version = "0.1.2"
|
|
|
|
| 2174 |
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
|
| 2175 |
]
|
| 2176 |
|
| 2177 |
+
[[package]]
|
| 2178 |
+
name = "pyparsing"
|
| 2179 |
+
version = "3.2.5"
|
| 2180 |
+
source = { registry = "https://pypi.org/simple" }
|
| 2181 |
+
sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274 }
|
| 2182 |
+
wheels = [
|
| 2183 |
+
{ url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890 },
|
| 2184 |
+
]
|
| 2185 |
+
|
| 2186 |
[[package]]
|
| 2187 |
name = "pytest"
|
| 2188 |
version = "9.0.1"
|
|
|
|
| 2433 |
{ name = "datasets" },
|
| 2434 |
{ name = "gradio" },
|
| 2435 |
{ name = "huggingface-hub" },
|
| 2436 |
+
{ name = "matplotlib" },
|
| 2437 |
{ name = "nibabel" },
|
| 2438 |
{ name = "numpy" },
|
| 2439 |
{ name = "pydantic" },
|
|
|
|
| 2457 |
{ name = "datasets", git = "https://github.com/CloseChoice/datasets.git?rev=feat%2Fbids-loader-streaming-upload-fix" },
|
| 2458 |
{ name = "gradio", specifier = ">=5.0.0" },
|
| 2459 |
{ name = "huggingface-hub", specifier = ">=0.25.0" },
|
| 2460 |
+
{ name = "matplotlib", specifier = ">=3.8.0" },
|
| 2461 |
{ name = "nibabel", specifier = ">=5.2.0" },
|
| 2462 |
{ name = "numpy", specifier = ">=1.26.0" },
|
| 2463 |
{ name = "pydantic", specifier = ">=2.5.0" },
|