stroke-viewer-frontend / docs /specs /01-phase-0-repo-bootstrap.md
VibecoderMcSwaggins's picture
docs: document SEALS model selection and scientific rationale
211e2f6
|
raw
history blame
11 kB
# phase 0: repo bootstrap
## purpose
Set up the foundational project structure with 2025 Python best practices. At the end of this phase, we have a working skeleton that can be installed, linted, type-checked, and tested (even if tests are empty).
## deliverables
- [ ] `pyproject.toml` with uv + hatchling backend
- [ ] `src/stroke_deepisles_demo/` package structure
- [ ] `tests/` directory with pytest configuration
- [ ] Development tooling: ruff, mypy, pre-commit
- [ ] Basic `README.md` with clinical disclaimer
- [ ] `.gitignore` updates if needed
## repo structure
```
stroke-deepisles-demo/
β”œβ”€β”€ pyproject.toml # Project metadata, deps, tool config
β”œβ”€β”€ uv.lock # Locked dependencies (auto-generated)
β”œβ”€β”€ .python-version # Python version (3.12)
β”œβ”€β”€ README.md # Project overview + disclaimer
β”œβ”€β”€ .gitignore # Standard Python ignores
β”œβ”€β”€ .pre-commit-config.yaml # Pre-commit hooks
β”‚
β”œβ”€β”€ src/
β”‚ └── stroke_deepisles_demo/
β”‚ β”œβ”€β”€ __init__.py # Package version, exports
β”‚ β”œβ”€β”€ py.typed # PEP 561 marker
β”‚ β”‚
β”‚ β”œβ”€β”€ core/ # Shared utilities
β”‚ β”‚ β”œβ”€β”€ __init__.py
β”‚ β”‚ β”œβ”€β”€ config.py # Pydantic settings (stub)
β”‚ β”‚ β”œβ”€β”€ types.py # Shared type definitions (stub)
β”‚ β”‚ └── exceptions.py # Custom exceptions (stub)
β”‚ β”‚
β”‚ β”œβ”€β”€ data/ # Data loading (stub)
β”‚ β”‚ └── __init__.py
β”‚ β”‚
β”‚ β”œβ”€β”€ inference/ # DeepISLES integration (stub)
β”‚ β”‚ └── __init__.py
β”‚ β”‚
β”‚ └── ui/ # Gradio app (stub)
β”‚ └── __init__.py
β”‚
β”œβ”€β”€ tests/
β”‚ β”œβ”€β”€ __init__.py
β”‚ β”œβ”€β”€ conftest.py # Shared fixtures
β”‚ └── test_package.py # Smoke test: package imports
β”‚
└── docs/
└── specs/ # These spec documents
β”œβ”€β”€ 00-context.md
β”œβ”€β”€ 01-phase-0-repo-bootstrap.md
└── ...
```
## pyproject.toml specification
```toml
[project]
name = "stroke-deepisles-demo"
version = "0.1.0"
description = "Demo: HF datasets + DeepISLES stroke segmentation + Gradio visualization"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.11"
authors = [
{ name = "Your Name", email = "you@example.com" }
]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering :: Medical Science Apps.",
]
keywords = ["stroke", "neuroimaging", "segmentation", "BIDS", "NIfTI", "deep-learning"]
dependencies = [
# Core - pinned to Tobias's fork for BIDS + NIfTI lazy loading
"datasets @ git+https://github.com/CloseChoice/datasets.git@feat/bids-loader-streaming-upload-fix",
"huggingface-hub>=0.25.0",
# NIfTI handling
"nibabel>=5.2.0",
"numpy>=1.26.0",
# Configuration
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
# UI (Gradio 5.x)
"gradio>=5.0.0",
]
[dependency-groups]
dev = [
"pytest>=8.0.0",
"pytest-cov>=4.1.0",
"pytest-mock>=3.12.0",
"mypy>=1.8.0",
"ruff>=0.8.0",
"pre-commit>=3.6.0",
# Type stubs
"types-requests",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/stroke_deepisles_demo"]
[tool.uv]
dev-dependencies = [
"pytest>=8.0.0",
"pytest-cov>=4.1.0",
"pytest-mock>=3.12.0",
"mypy>=1.8.0",
"ruff>=0.8.0",
"pre-commit>=3.6.0",
]
# ─────────────────────────────────────────────────────────────────
# Tool configurations
# ─────────────────────────────────────────────────────────────────
[tool.ruff]
target-version = "py311"
line-length = 100
src = ["src", "tests"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG", # flake8-unused-arguments
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"PTH", # flake8-use-pathlib
"RUF", # ruff-specific
]
ignore = [
"E501", # line too long (handled by formatter)
]
[tool.ruff.lint.isort]
known-first-party = ["stroke_deepisles_demo"]
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
plugins = ["pydantic.mypy"]
[[tool.mypy.overrides]]
module = [
"nibabel.*",
"gradio.*",
"datasets.*",
"niivue.*",
]
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = [
"-v",
"--tb=short",
"--strict-markers",
]
markers = [
"integration: marks tests requiring external resources (Docker, network)",
"slow: marks tests that take >10s to run",
]
filterwarnings = [
"ignore::DeprecationWarning",
]
[tool.coverage.run]
source = ["src/stroke_deepisles_demo"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
```
## module stubs
### `src/stroke_deepisles_demo/__init__.py`
```python
"""stroke-deepisles-demo: HF datasets + DeepISLES + Gradio visualization."""
__version__ = "0.1.0"
__all__ = ["__version__"]
```
### `src/stroke_deepisles_demo/core/config.py`
```python
"""Application configuration using pydantic-settings."""
from __future__ import annotations
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Application settings loaded from environment variables."""
# HuggingFace
hf_dataset_id: str = "YongchengYAO/ISLES24-MR-Lite"
hf_cache_dir: str | None = None
# DeepISLES
deepisles_docker_image: str = "isleschallenge/deepisles"
deepisles_fast_mode: bool = True # SEALS-only (ISLES'22 winner, no FLAIR needed)
# Paths
temp_dir: str | None = None
class Config:
env_prefix = "STROKE_DEMO_"
env_file = ".env"
settings = Settings()
```
### `src/stroke_deepisles_demo/core/types.py`
```python
"""Shared type definitions."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import TypedDict
class CaseFiles(TypedDict):
"""Paths to NIfTI files for a single case."""
dwi: Path
adc: Path
flair: Path | None
ground_truth: Path | None
@dataclass(frozen=True)
class InferenceResult:
"""Result of running DeepISLES on a case."""
case_id: str
input_files: CaseFiles
prediction_mask: Path
elapsed_seconds: float
```
### `src/stroke_deepisles_demo/core/exceptions.py`
```python
"""Custom exceptions for stroke-deepisles-demo."""
from __future__ import annotations
class StrokeDemoError(Exception):
"""Base exception for stroke-deepisles-demo."""
class DataLoadError(StrokeDemoError):
"""Failed to load data from HuggingFace Hub."""
class DockerNotAvailableError(StrokeDemoError):
"""Docker is not installed or not running."""
class DeepISLESError(StrokeDemoError):
"""DeepISLES inference failed."""
class MissingInputError(StrokeDemoError):
"""Required input files are missing."""
```
## pre-commit configuration
### `.pre-commit-config.yaml`
```yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies:
- pydantic>=2.5.0
- pydantic-settings>=2.1.0
args: [--config-file=pyproject.toml]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
args: [--maxkb=1000]
```
## tdd plan
### tests to write first
1. **`tests/test_package.py`** - Smoke test that package imports work
```python
"""Smoke tests for package structure."""
from __future__ import annotations
def test_package_imports() -> None:
"""Verify the package can be imported."""
import stroke_deepisles_demo
assert stroke_deepisles_demo.__version__ == "0.1.0"
def test_core_modules_import() -> None:
"""Verify core modules can be imported without side effects."""
from stroke_deepisles_demo.core import config, exceptions, types
assert config.settings is not None
assert types.CaseFiles is not None
assert exceptions.StrokeDemoError is not None
def test_subpackages_exist() -> None:
"""Verify subpackage structure exists."""
from stroke_deepisles_demo import data, inference, ui
# These are stubs, just verify they exist
assert data is not None
assert inference is not None
assert ui is not None
```
### what to mock
- Nothing needed for Phase 0 - these are pure import tests
### what to test for real
- Package imports
- Module structure
- Type definitions load correctly
- Pydantic settings initialize with defaults
## "done" criteria
Phase 0 is complete when:
1. `uv sync` succeeds and creates virtual environment
2. `uv run pytest` passes all smoke tests
3. `uv run ruff check .` reports no errors
4. `uv run ruff format --check .` reports no changes needed
5. `uv run mypy src/` passes with no errors
6. `uv run pre-commit run --all-files` passes
7. Package can be imported: `uv run python -c "import stroke_deepisles_demo"`
## commands cheatsheet
```bash
# Initialize (if starting fresh)
uv init --package stroke-deepisles-demo
# Install dependencies
uv sync
# Run tests
uv run pytest
# Run tests with coverage
uv run pytest --cov
# Lint
uv run ruff check .
# Format
uv run ruff format .
# Type check
uv run mypy src/
# Install pre-commit hooks
uv run pre-commit install
# Run all pre-commit hooks
uv run pre-commit run --all-files
```
## notes
- We use `hatchling` as the build backend (current uv default, stable)
- `uv_build` is newer but `hatchling` is battle-tested
- The `datasets` dependency is pinned to Tobias's fork via git URL
- Gradio 5.x for latest features (SSR, improved components)
- Python 3.11+ for modern typing features (`X | None` syntax)