VibecoderMcSwaggins commited on
Commit
4eeba46
Β·
unverified Β·
1 Parent(s): da08b3c

feat(phase-0): bootstrap project with TDD structure (#1)

Browse files

* feat(phase-0): bootstrap project with TDD structure

Phase 0 complete - repo bootstrap with 2025 Python best practices:

- pyproject.toml with uv + hatchling backend
- Tobias's datasets fork for BIDS/NIfTI support
- src/stroke_deepisles_demo/ package structure
- core/ module with config, types, exceptions stubs
- data/, inference/, ui/ module stubs
- tests/ with pytest smoke tests (TDD - written first!)
- ruff for linting + formatting
- mypy strict mode on src/ and tests/
- pre-commit hooks configured

All "done" criteria verified:
- uv sync βœ“
- pytest (3 passed) βœ“
- ruff check βœ“
- ruff format --check βœ“
- mypy src/ tests/ βœ“
- pre-commit run --all-files βœ“
- package import βœ“

* fix: address CodeRabbit review - ironclad Gucci banger status

Fixes all nitpicks from CodeRabbit review:

1. Python version alignment:
- ruff target-version: py311 β†’ py312
- mypy python_version: 3.11 β†’ 3.12
- Now aligned with .python-version (3.12)

2. Author info updated:
- Removed placeholder "Your Name" β†’ "stroke-deepisles-demo contributors"

3. Consolidated dev deps:
- Removed duplicate [tool.uv].dev-dependencies section
- Single source of truth: [dependency-groups].dev

4. Pre-commit versions aligned:
- ruff: v0.8.0 β†’ v0.14.8
- mypy: v1.8.0 β†’ v1.19.0
- pre-commit-hooks: v4.5.0 β†’ v6.0.0
- pyproject.toml minimums updated to match

5. Pydantic v2 modern style:
- Replaced nested `class Config` with `model_config = SettingsConfigDict(...)`

6. TypedDict improved:
- Changed flair/ground_truth from `Path | None` (required key, optional value)
- To `NotRequired[Path]` (optional key entirely)
- Added comprehensive docstring explaining required vs optional keys

All checks pass: pytest βœ“ ruff βœ“ mypy βœ“ pre-commit βœ“

.gitignore CHANGED
@@ -182,9 +182,9 @@ cython_debug/
182
  .abstra/
183
 
184
  # Visual Studio Code
185
- # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
  # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
- # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
  # you could uncomment the following to ignore the entire vscode folder
189
  # .vscode/
190
 
 
182
  .abstra/
183
 
184
  # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
  # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
  # you could uncomment the following to ignore the entire vscode folder
189
  # .vscode/
190
 
.pre-commit-config.yaml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.14.8
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+
9
+ - repo: https://github.com/pre-commit/mirrors-mypy
10
+ rev: v1.19.0
11
+ hooks:
12
+ - id: mypy
13
+ additional_dependencies:
14
+ - pydantic>=2.5.0
15
+ - pydantic-settings>=2.1.0
16
+ args: [--config-file=pyproject.toml]
17
+
18
+ - repo: https://github.com/pre-commit/pre-commit-hooks
19
+ rev: v6.0.0
20
+ hooks:
21
+ - id: trailing-whitespace
22
+ - id: end-of-file-fixer
23
+ - id: check-yaml
24
+ - id: check-added-large-files
25
+ args: [--maxkb=1000]
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.12
pyproject.toml ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "stroke-deepisles-demo"
3
+ version = "0.1.0"
4
+ description = "Demo: HF datasets + DeepISLES stroke segmentation + Gradio visualization"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.11"
8
+ authors = [
9
+ { name = "stroke-deepisles-demo contributors" }
10
+ ]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Science/Research",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
18
+ ]
19
+ keywords = ["stroke", "neuroimaging", "segmentation", "BIDS", "NIfTI", "deep-learning"]
20
+
21
+ dependencies = [
22
+ # Core - pinned to Tobias's fork for BIDS + NIfTI lazy loading
23
+ "datasets @ git+https://github.com/CloseChoice/datasets.git@feat/bids-loader-streaming-upload-fix",
24
+ "huggingface-hub>=0.25.0",
25
+
26
+ # NIfTI handling
27
+ "nibabel>=5.2.0",
28
+ "numpy>=1.26.0",
29
+
30
+ # Configuration
31
+ "pydantic>=2.5.0",
32
+ "pydantic-settings>=2.1.0",
33
+
34
+ # UI (Gradio 5.x)
35
+ "gradio>=5.0.0",
36
+ ]
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "pytest>=8.0.0",
41
+ "pytest-cov>=4.1.0",
42
+ "pytest-mock>=3.12.0",
43
+ "mypy>=1.19.0",
44
+ "ruff>=0.14.0",
45
+ "pre-commit>=3.6.0",
46
+ # Type stubs
47
+ "types-requests",
48
+ ]
49
+
50
+ [build-system]
51
+ requires = ["hatchling"]
52
+ build-backend = "hatchling.build"
53
+
54
+ [tool.hatch.metadata]
55
+ allow-direct-references = true
56
+
57
+ [tool.hatch.build.targets.wheel]
58
+ packages = ["src/stroke_deepisles_demo"]
59
+
60
+ # ─────────────────────────────────────────────────────────────────
61
+ # Tool configurations
62
+ # ─────────────────────────────────────────────────────────────────
63
+
64
+ [tool.ruff]
65
+ target-version = "py312"
66
+ line-length = 100
67
+ src = ["src", "tests"]
68
+
69
+ [tool.ruff.lint]
70
+ select = [
71
+ "E", # pycodestyle errors
72
+ "W", # pycodestyle warnings
73
+ "F", # pyflakes
74
+ "I", # isort
75
+ "B", # flake8-bugbear
76
+ "C4", # flake8-comprehensions
77
+ "UP", # pyupgrade
78
+ "ARG", # flake8-unused-arguments
79
+ "SIM", # flake8-simplify
80
+ "TCH", # flake8-type-checking
81
+ "PTH", # flake8-use-pathlib
82
+ "RUF", # ruff-specific
83
+ ]
84
+ ignore = [
85
+ "E501", # line too long (handled by formatter)
86
+ ]
87
+
88
+ [tool.ruff.lint.isort]
89
+ known-first-party = ["stroke_deepisles_demo"]
90
+
91
+ [tool.mypy]
92
+ python_version = "3.12"
93
+ strict = true
94
+ warn_return_any = true
95
+ warn_unused_ignores = true
96
+ disallow_untyped_defs = true
97
+ plugins = ["pydantic.mypy"]
98
+
99
+ [[tool.mypy.overrides]]
100
+ module = [
101
+ "nibabel.*",
102
+ "gradio.*",
103
+ "datasets.*",
104
+ "niivue.*",
105
+ ]
106
+ ignore_missing_imports = true
107
+
108
+ [tool.pytest.ini_options]
109
+ testpaths = ["tests"]
110
+ python_files = ["test_*.py"]
111
+ python_functions = ["test_*"]
112
+ addopts = [
113
+ "-v",
114
+ "--tb=short",
115
+ "--strict-markers",
116
+ ]
117
+ markers = [
118
+ "integration: marks tests requiring external resources (Docker, network)",
119
+ "slow: marks tests that take >10s to run",
120
+ ]
121
+ filterwarnings = [
122
+ "ignore::DeprecationWarning",
123
+ ]
124
+
125
+ [tool.coverage.run]
126
+ source = ["src/stroke_deepisles_demo"]
127
+ branch = true
128
+
129
+ [tool.coverage.report]
130
+ exclude_lines = [
131
+ "pragma: no cover",
132
+ "if TYPE_CHECKING:",
133
+ "raise NotImplementedError",
134
+ ]
src/stroke_deepisles_demo/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """stroke-deepisles-demo: HF datasets + DeepISLES + Gradio visualization."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
src/stroke_deepisles_demo/core/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Core utilities for stroke-deepisles-demo."""
2
+
3
+ from stroke_deepisles_demo.core.config import Settings, settings
4
+ from stroke_deepisles_demo.core.exceptions import (
5
+ DataLoadError,
6
+ DeepISLESError,
7
+ DockerNotAvailableError,
8
+ MissingInputError,
9
+ StrokeDemoError,
10
+ )
11
+ from stroke_deepisles_demo.core.types import CaseFiles, InferenceResult
12
+
13
+ __all__ = [
14
+ "CaseFiles",
15
+ "DataLoadError",
16
+ "DeepISLESError",
17
+ "DockerNotAvailableError",
18
+ "InferenceResult",
19
+ "MissingInputError",
20
+ "Settings",
21
+ "StrokeDemoError",
22
+ "settings",
23
+ ]
src/stroke_deepisles_demo/core/config.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Application configuration using pydantic-settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic_settings import BaseSettings, SettingsConfigDict
6
+
7
+
8
+ class Settings(BaseSettings):
9
+ """Application settings loaded from environment variables."""
10
+
11
+ model_config = SettingsConfigDict(
12
+ env_prefix="STROKE_DEMO_",
13
+ env_file=".env",
14
+ env_file_encoding="utf-8",
15
+ )
16
+
17
+ # HuggingFace
18
+ hf_dataset_id: str = "YongchengYAO/ISLES24-MR-Lite"
19
+ hf_cache_dir: str | None = None
20
+
21
+ # DeepISLES
22
+ deepisles_docker_image: str = "isleschallenge/deepisles"
23
+ deepisles_fast_mode: bool = True
24
+
25
+ # Paths
26
+ temp_dir: str | None = None
27
+
28
+
29
+ settings = Settings()
src/stroke_deepisles_demo/core/exceptions.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Custom exceptions for stroke-deepisles-demo."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class StrokeDemoError(Exception):
7
+ """Base exception for stroke-deepisles-demo."""
8
+
9
+
10
+ class DataLoadError(StrokeDemoError):
11
+ """Failed to load data from HuggingFace Hub."""
12
+
13
+
14
+ class DockerNotAvailableError(StrokeDemoError):
15
+ """Docker is not installed or not running."""
16
+
17
+
18
+ class DeepISLESError(StrokeDemoError):
19
+ """DeepISLES inference failed."""
20
+
21
+
22
+ class MissingInputError(StrokeDemoError):
23
+ """Required input files are missing."""
src/stroke_deepisles_demo/core/types.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared type definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, NotRequired, TypedDict
7
+
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
10
+
11
+
12
+ class CaseFiles(TypedDict):
13
+ """Paths to NIfTI files for a single case.
14
+
15
+ Required keys:
16
+ dwi: Path to DWI NIfTI file
17
+ adc: Path to ADC NIfTI file
18
+
19
+ Optional keys (may be absent):
20
+ flair: Path to FLAIR NIfTI file (not all cases have FLAIR)
21
+ ground_truth: Path to ground truth mask (not available during inference)
22
+ """
23
+
24
+ dwi: Path
25
+ adc: Path
26
+ flair: NotRequired[Path]
27
+ ground_truth: NotRequired[Path]
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class InferenceResult:
32
+ """Result of running DeepISLES on a case."""
33
+
34
+ case_id: str
35
+ input_files: CaseFiles
36
+ prediction_mask: Path
37
+ elapsed_seconds: float
src/stroke_deepisles_demo/data/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Data loading module for stroke-deepisles-demo."""
src/stroke_deepisles_demo/inference/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """DeepISLES inference module for stroke-deepisles-demo."""
src/stroke_deepisles_demo/py.typed ADDED
File without changes
src/stroke_deepisles_demo/ui/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Gradio UI module for stroke-deepisles-demo."""
tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Tests for stroke-deepisles-demo."""
tests/conftest.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Shared pytest fixtures for stroke-deepisles-demo tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # No fixtures needed for Phase 0 - pure import tests
tests/test_package.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Smoke tests for package structure."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def test_package_imports() -> None:
7
+ """Verify the package can be imported."""
8
+ import stroke_deepisles_demo
9
+
10
+ assert stroke_deepisles_demo.__version__ == "0.1.0"
11
+
12
+
13
+ def test_core_modules_import() -> None:
14
+ """Verify core modules can be imported without side effects."""
15
+ from stroke_deepisles_demo.core import config, exceptions, types
16
+
17
+ assert config.settings is not None
18
+ assert types.CaseFiles is not None
19
+ assert exceptions.StrokeDemoError is not None
20
+
21
+
22
+ def test_subpackages_exist() -> None:
23
+ """Verify subpackage structure exists."""
24
+ from stroke_deepisles_demo import data, inference, ui
25
+
26
+ # These are stubs, just verify they exist
27
+ assert data is not None
28
+ assert inference is not None
29
+ assert ui is not None
uv.lock ADDED
The diff for this file is too large to render. See raw diff