dcccpy 0.3.0a3__tar.gz → 0.3.0a4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dcccpy
3
- Version: 0.3.0a3
3
+ Version: 0.3.0a4
4
4
  Summary: Python wrapper for the DCCCcore PET biomarker command-line tool
5
5
  Author: DCCCSlicer contributors
6
6
  License-Expression: CC-BY-NC-ND-4.0
@@ -17,20 +17,50 @@ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown
18
18
  Requires-Dist: nibabel>=5
19
19
  Provides-Extra: linux-runtime
20
- Requires-Dist: dcccpy-linux-runtime==0.2.0a2; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "linux-runtime"
21
- Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a2; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "linux-runtime"
20
+ Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "linux-runtime"
21
+ Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "linux-runtime"
22
22
  Provides-Extra: windows-runtime
23
- Requires-Dist: dcccpy-windows-runtime==0.2.0a2; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "windows-runtime"
23
+ Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "windows-runtime"
24
24
  Provides-Extra: macos-runtime
25
- Requires-Dist: dcccpy-macos-runtime==0.2.0a2; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos-runtime"
25
+ Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos-runtime"
26
26
  Provides-Extra: runtime
27
- Requires-Dist: dcccpy-linux-runtime==0.2.0a2; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "runtime"
28
- Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a2; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "runtime"
29
- Requires-Dist: dcccpy-windows-runtime==0.2.0a2; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "runtime"
30
- Requires-Dist: dcccpy-macos-runtime==0.2.0a2; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "runtime"
27
+ Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "runtime"
28
+ Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "runtime"
29
+ Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "runtime"
30
+ Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "runtime"
31
31
 
32
32
  # dcccpy
33
33
 
34
+ ### Native PET segmentation
35
+
36
+ ```python
37
+ import dcccpy
38
+ result = dcccpy.seg("pet.nii.gz", "labels.nii.gz")
39
+ labels = result.load_output()
40
+ ```
41
+
42
+ Requires a DCCCcore build containing the `seg` command and its bundled model.
43
+ The signed integer output matches the input PET's native shape and geometry.
44
+ The default `mode="freesurfer"` returns raw DKT/aseg IDs; `mode="training"`
45
+ returns IDs 0–95. `mode="neurosegmenter"` provides partial regional compatibility:
46
+ brainstem/pons IDs 45/46 are unavailable, so pons SUVR is unsupported.
47
+ Mirror TTA is enabled by default; `tta=False` explicitly changes that protocol.
48
+ Use `threads=4` to select the CPU thread count, `bundle=...` to override the model
49
+ directory, and `debug=True` to save intermediate grids and a transform receipt.
50
+ The approximately 120 MB segmentation model is excluded from PyPI wheels.
51
+ With a slim runtime, the first `seg()` call downloads the separately published
52
+ `DCCCseg-4.6.0-alpha.zip` Release asset, verifies the model SHA-256 and caches it
53
+ under the dcccpy cache directory. A full native installation reuses its bundled
54
+ model. Existing `DCCCPY_AUTO_DOWNLOAD=0` disables implicit downloads; provision
55
+ offline use with `dcccpy.download_seg_model()` or pass `bundle=` pointing to
56
+ the extracted `seg` directory. `DCCCPY_SEG_MODEL_URL` selects a mirror while
57
+ retaining checksum verification.
58
+ An explicit `config=` delegates model selection to that configuration and
59
+ suppresses default-model provisioning. With a slim runtime and a configuration
60
+ that only changes other settings, also pass `bundle=dcccpy.download_seg_model()`.
61
+ An optional `rigid_header=...` must retain the exact native PET values and record
62
+ exactly two rigid steps. Existing output images are refused by the core.
63
+
34
64
  `dcccpy` is a Python wrapper for the `DCCCcore` command-line tool from
35
65
  DCCCSlicer. It keeps the native C++ core as the execution engine and provides a
36
66
  small Python API for running common PET biomarker workflows.
@@ -212,7 +242,7 @@ the calculation again.
212
242
  Release wheels should vendor the matching `DCCCcore` runtime tree before build:
213
243
 
214
244
  ```bash
215
- python scripts/vendor_dccccore.py --version 4.5.1-alpha --release-platform ubuntu-latest-x64
245
+ python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-x64
216
246
  python -m build --wheel
217
247
  ```
218
248
 
@@ -220,7 +250,7 @@ The Linux ARM64 runtime package uses the corresponding release asset:
220
250
 
221
251
  ```bash
222
252
  cd python/dcccpy-linux-arm64-runtime
223
- python scripts/vendor_dccccore.py --version 4.5.1-alpha --release-platform ubuntu-latest-arm64
253
+ python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-arm64
224
254
  python -m build --wheel
225
255
  ```
226
256
 
@@ -238,7 +268,7 @@ The preferred distribution layout is:
238
268
 
239
269
  ## Packaging note
240
270
 
241
- The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.5.1-alpha. They omit the
271
+ The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.6.0-alpha. They omit the
242
272
  `fast_and_acc` registration model/config and the ADAD decoupler ONNX ensemble,
243
273
  while keeping the default spatial normalization model and assets needed by
244
274
  common Centiloid/CenTauR/CenTauRz workflows.
@@ -1,5 +1,35 @@
1
1
  # dcccpy
2
2
 
3
+ ### Native PET segmentation
4
+
5
+ ```python
6
+ import dcccpy
7
+ result = dcccpy.seg("pet.nii.gz", "labels.nii.gz")
8
+ labels = result.load_output()
9
+ ```
10
+
11
+ Requires a DCCCcore build containing the `seg` command and its bundled model.
12
+ The signed integer output matches the input PET's native shape and geometry.
13
+ The default `mode="freesurfer"` returns raw DKT/aseg IDs; `mode="training"`
14
+ returns IDs 0–95. `mode="neurosegmenter"` provides partial regional compatibility:
15
+ brainstem/pons IDs 45/46 are unavailable, so pons SUVR is unsupported.
16
+ Mirror TTA is enabled by default; `tta=False` explicitly changes that protocol.
17
+ Use `threads=4` to select the CPU thread count, `bundle=...` to override the model
18
+ directory, and `debug=True` to save intermediate grids and a transform receipt.
19
+ The approximately 120 MB segmentation model is excluded from PyPI wheels.
20
+ With a slim runtime, the first `seg()` call downloads the separately published
21
+ `DCCCseg-4.6.0-alpha.zip` Release asset, verifies the model SHA-256 and caches it
22
+ under the dcccpy cache directory. A full native installation reuses its bundled
23
+ model. Existing `DCCCPY_AUTO_DOWNLOAD=0` disables implicit downloads; provision
24
+ offline use with `dcccpy.download_seg_model()` or pass `bundle=` pointing to
25
+ the extracted `seg` directory. `DCCCPY_SEG_MODEL_URL` selects a mirror while
26
+ retaining checksum verification.
27
+ An explicit `config=` delegates model selection to that configuration and
28
+ suppresses default-model provisioning. With a slim runtime and a configuration
29
+ that only changes other settings, also pass `bundle=dcccpy.download_seg_model()`.
30
+ An optional `rigid_header=...` must retain the exact native PET values and record
31
+ exactly two rigid steps. Existing output images are refused by the core.
32
+
3
33
  `dcccpy` is a Python wrapper for the `DCCCcore` command-line tool from
4
34
  DCCCSlicer. It keeps the native C++ core as the execution engine and provides a
5
35
  small Python API for running common PET biomarker workflows.
@@ -181,7 +211,7 @@ the calculation again.
181
211
  Release wheels should vendor the matching `DCCCcore` runtime tree before build:
182
212
 
183
213
  ```bash
184
- python scripts/vendor_dccccore.py --version 4.5.1-alpha --release-platform ubuntu-latest-x64
214
+ python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-x64
185
215
  python -m build --wheel
186
216
  ```
187
217
 
@@ -189,7 +219,7 @@ The Linux ARM64 runtime package uses the corresponding release asset:
189
219
 
190
220
  ```bash
191
221
  cd python/dcccpy-linux-arm64-runtime
192
- python scripts/vendor_dccccore.py --version 4.5.1-alpha --release-platform ubuntu-latest-arm64
222
+ python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-arm64
193
223
  python -m build --wheel
194
224
  ```
195
225
 
@@ -207,7 +237,7 @@ The preferred distribution layout is:
207
237
 
208
238
  ## Packaging note
209
239
 
210
- The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.5.1-alpha. They omit the
240
+ The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.6.0-alpha. They omit the
211
241
  `fast_and_acc` registration model/config and the ADAD decoupler ONNX ensemble,
212
242
  while keeping the default spatial normalization model and assets needed by
213
243
  common Centiloid/CenTauR/CenTauRz workflows.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dcccpy"
7
- version = "0.3.0a3"
7
+ version = "0.3.0a4"
8
8
  description = "Python wrapper for the DCCCcore PET biomarker command-line tool"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -31,16 +31,16 @@ Issues = "https://github.com/tctco/DCCCSlicer/issues"
31
31
 
32
32
  [project.optional-dependencies]
33
33
  linux-runtime = [
34
- "dcccpy-linux-runtime==0.2.0a2; sys_platform == 'linux' and platform_machine == 'x86_64'",
35
- "dcccpy-linux-arm64-runtime==0.2.0a2; sys_platform == 'linux' and platform_machine == 'aarch64'",
34
+ "dcccpy-linux-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'x86_64'",
35
+ "dcccpy-linux-arm64-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'aarch64'",
36
36
  ]
37
- windows-runtime = ["dcccpy-windows-runtime==0.2.0a2; sys_platform == 'win32' and platform_machine == 'AMD64'"]
38
- macos-runtime = ["dcccpy-macos-runtime==0.2.0a2; sys_platform == 'darwin' and platform_machine == 'arm64'"]
37
+ windows-runtime = ["dcccpy-windows-runtime==0.2.0a3; sys_platform == 'win32' and platform_machine == 'AMD64'"]
38
+ macos-runtime = ["dcccpy-macos-runtime==0.2.0a3; sys_platform == 'darwin' and platform_machine == 'arm64'"]
39
39
  runtime = [
40
- "dcccpy-linux-runtime==0.2.0a2; sys_platform == 'linux' and platform_machine == 'x86_64'",
41
- "dcccpy-linux-arm64-runtime==0.2.0a2; sys_platform == 'linux' and platform_machine == 'aarch64'",
42
- "dcccpy-windows-runtime==0.2.0a2; sys_platform == 'win32' and platform_machine == 'AMD64'",
43
- "dcccpy-macos-runtime==0.2.0a2; sys_platform == 'darwin' and platform_machine == 'arm64'",
40
+ "dcccpy-linux-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'x86_64'",
41
+ "dcccpy-linux-arm64-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'aarch64'",
42
+ "dcccpy-windows-runtime==0.2.0a3; sys_platform == 'win32' and platform_machine == 'AMD64'",
43
+ "dcccpy-macos-runtime==0.2.0a3; sys_platform == 'darwin' and platform_machine == 'arm64'",
44
44
  ]
45
45
 
46
46
  [project.scripts]
@@ -15,12 +15,16 @@ from .core import (
15
15
  parse_metrics,
16
16
  pet_motion_correct,
17
17
  rigid,
18
+ seg,
18
19
  run,
19
20
  suvr,
20
21
  )
21
22
  from .runtime import DCCCCORE_VERSION, DCCCcoreDownloadError, DCCCcoreNotFoundError, download_dccccore
23
+ from .segmentation import SegModelDownloadError, download_seg_model
22
24
 
23
25
  __all__ = [
26
+ "SegModelDownloadError",
27
+ "download_seg_model",
24
28
  "DCCCResult",
25
29
  "DCCCCORE_VERSION",
26
30
  "DCCCcoreDownloadError",
@@ -39,6 +43,7 @@ __all__ = [
39
43
  "parse_metrics",
40
44
  "pet_motion_correct",
41
45
  "rigid",
46
+ "seg",
42
47
  "run",
43
48
  "suvr",
44
49
  ]
@@ -443,6 +443,48 @@ def rigid(
443
443
  return _spatial_command("rigid", input, output, **kwargs)
444
444
 
445
445
 
446
+ def seg(
447
+ input: object,
448
+ output: str | os.PathLike[str] | None = None,
449
+ *,
450
+ mode: str = "freesurfer",
451
+ threads: int = 4,
452
+ tta: bool = True,
453
+ bundle: str | os.PathLike[str] | None = None,
454
+ rigid_header: str | os.PathLike[str] | None = None,
455
+ debug: bool = False,
456
+ config: str | os.PathLike[str] | None = None,
457
+ check: bool = True,
458
+ executable: str | os.PathLike[str] | None = None,
459
+ ) -> DCCCResult:
460
+ """Segment one 3-D PET into native-space categorical labels.
461
+
462
+ ``freesurfer`` emits raw DKT/aseg IDs, ``training`` IDs 0–95, and
463
+ ``neurosegmenter`` a partial aggregation without brainstem/pons IDs 45/46.
464
+ Mirror TTA is enabled by default. ``rigid_header``, when supplied, must
465
+ retain the input's voxel values after exactly two rigid predictions.
466
+ """
467
+ if mode not in ("freesurfer", "training", "neurosegmenter"):
468
+ raise ValueError("mode must be freesurfer, training, or neurosegmenter")
469
+ if isinstance(threads, bool) or not isinstance(threads, int) or threads < 1:
470
+ raise ValueError("threads must be a positive integer")
471
+ if bundle is None and config is None:
472
+ from .segmentation import default_seg_bundle
473
+ executable = dccccore_path(executable=executable)
474
+ bundle = default_seg_bundle(executable)
475
+ args: list[object] = ["--mode", mode, "--threads", threads]
476
+ if not tta:
477
+ args.append("--no-tta")
478
+ if debug:
479
+ args.append("--debug")
480
+ for flag, path in (("--bundle", bundle), ("--rigid-header", rigid_header),
481
+ ("--config", config)):
482
+ if path is not None:
483
+ args.extend([flag, _resolve_user_path(path)])
484
+ return _spatial_command("seg", input, output, extra_args=args,
485
+ check=check, executable=executable)
486
+
487
+
446
488
  def pet_motion_correct(
447
489
  input: object,
448
490
  output: str | os.PathLike[str] | None = None,
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
  from typing import Iterable
13
13
 
14
14
 
15
- DCCCCORE_VERSION = "4.5.1-alpha"
15
+ DCCCCORE_VERSION = "4.6.0-alpha"
16
16
  RELEASE_REPO = "tctco/DCCCSlicer"
17
17
 
18
18
 
@@ -0,0 +1,123 @@
1
+ """Optional DCCC Seg model delivery, kept outside PyPI wheels."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import os
6
+ from pathlib import Path, PurePosixPath
7
+ import re
8
+ import shutil
9
+ import stat
10
+ import tempfile
11
+ import urllib.request
12
+ import zipfile
13
+
14
+ from .runtime import DCCCCORE_VERSION, RELEASE_REPO, auto_download_enabled, cache_root
15
+
16
+
17
+ SEG_MODEL_SHA256 = "8ea46dc8eff44e7a6c55b93ec9a353eb676692741d56a47b04c755a3c956faf6"
18
+ MODEL_FILES = ("dccc_seg.onnx", "manifest.json", "grid.json", "plans.json", "dataset.json",
19
+ "labels.json", "neurosegmenter_mapping.json", "validation.json")
20
+
21
+
22
+ class SegModelDownloadError(RuntimeError):
23
+ """The optional segmentation model could not be provisioned."""
24
+
25
+
26
+ def seg_model_dir(version: str = DCCCCORE_VERSION) -> Path:
27
+ if not re.fullmatch(r"\d+\.\d+\.\d+(?:-[A-Za-z0-9.]+)?", version):
28
+ raise ValueError("Invalid segmentation model version")
29
+ return cache_root() / "models" / "seg" / version
30
+
31
+
32
+ def _complete(directory: Path) -> bool:
33
+ return all((directory / name).is_file() for name in MODEL_FILES)
34
+
35
+
36
+ def _verified(directory: Path) -> bool:
37
+ if not _complete(directory):
38
+ return False
39
+ digest = hashlib.sha256()
40
+ with (directory / "dccc_seg.onnx").open("rb") as source:
41
+ for chunk in iter(lambda: source.read(1024 * 1024), b""):
42
+ digest.update(chunk)
43
+ return digest.hexdigest() == SEG_MODEL_SHA256
44
+
45
+
46
+ def download_seg_model(*, version: str = DCCCCORE_VERSION, force: bool = False) -> Path:
47
+ """Download and verify the optional model; return its cached bundle directory.
48
+
49
+ An explicit call provisions offline use even with DCCCPY_AUTO_DOWNLOAD=0.
50
+ DCCCPY_SEG_MODEL_URL can select a mirrored archive; the model SHA-256 is
51
+ still enforced. force=True replaces this version's model cache only.
52
+ """
53
+ destination = seg_model_dir(version)
54
+ if not force and _verified(destination):
55
+ return destination
56
+ if destination.exists() and not force:
57
+ raise SegModelDownloadError("Cached segmentation model is incomplete or corrupt; "
58
+ "call download_seg_model(force=True) to repair it")
59
+ repo = os.environ.get("DCCCPY_RELEASE_REPO", RELEASE_REPO)
60
+ url = os.environ.get("DCCCPY_SEG_MODEL_URL") or (
61
+ f"https://github.com/{repo}/releases/download/v{version}/DCCCseg-{version}.zip")
62
+ destination.parent.mkdir(parents=True, exist_ok=True)
63
+ try:
64
+ with tempfile.TemporaryDirectory(prefix=".seg-download-", dir=destination.parent) as temporary:
65
+ temporary = Path(temporary)
66
+ archive = temporary / "model.zip"
67
+ with urllib.request.urlopen(url, timeout=120) as response, archive.open("wb") as output:
68
+ # Bound compressed downloads as well as extracted size.
69
+ total = 0
70
+ while True:
71
+ chunk = response.read(1024 * 1024)
72
+ if not chunk:
73
+ break
74
+ total += len(chunk)
75
+ if total > 200 * 1024 * 1024:
76
+ raise SegModelDownloadError("Segmentation model archive exceeds size limit")
77
+ output.write(chunk)
78
+ with zipfile.ZipFile(archive) as source:
79
+ total = 0
80
+ for member in source.infolist():
81
+ path = PurePosixPath(member.filename)
82
+ if (path.is_absolute() or ".." in path.parts or "\\" in member.filename
83
+ or not path.parts or path.parts[0] != "seg"
84
+ or stat.S_ISLNK(member.external_attr >> 16)):
85
+ raise SegModelDownloadError("Unsafe segmentation model archive path")
86
+ total += member.file_size
87
+ if total > 200 * 1024 * 1024:
88
+ raise SegModelDownloadError("Segmentation model expands beyond size limit")
89
+ for name in MODEL_FILES:
90
+ output = temporary / "seg" / name
91
+ output.parent.mkdir(parents=True, exist_ok=True)
92
+ with source.open("seg/" + name) as handle, output.open("xb") as target:
93
+ shutil.copyfileobj(handle, target)
94
+ staged = temporary / "seg"
95
+ if not _verified(staged):
96
+ raise SegModelDownloadError("Segmentation model SHA-256 mismatch")
97
+ if force and destination.exists():
98
+ shutil.rmtree(destination)
99
+ try:
100
+ staged.rename(destination)
101
+ except OSError:
102
+ # Another process may have finished the same version concurrently.
103
+ if not _verified(destination):
104
+ raise
105
+ return destination
106
+ except SegModelDownloadError:
107
+ raise
108
+ except Exception as exc:
109
+ raise SegModelDownloadError("Cannot download the DCCC Seg model; provide bundle= with "
110
+ "an extracted DCCCseg Release asset for offline use") from exc
111
+
112
+
113
+ def default_seg_bundle(executable: Path) -> Path | None:
114
+ """Return None for a full native installation, otherwise provision its model."""
115
+ if _complete(Path(executable).resolve().parent / "assets/models/seg"):
116
+ return None
117
+ destination = seg_model_dir()
118
+ if _verified(destination):
119
+ return destination
120
+ if not auto_download_enabled():
121
+ raise SegModelDownloadError("Segmentation model is absent and automatic download is disabled; "
122
+ "pass bundle= or call download_seg_model() explicitly")
123
+ return download_seg_model()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dcccpy
3
- Version: 0.3.0a3
3
+ Version: 0.3.0a4
4
4
  Summary: Python wrapper for the DCCCcore PET biomarker command-line tool
5
5
  Author: DCCCSlicer contributors
6
6
  License-Expression: CC-BY-NC-ND-4.0
@@ -17,20 +17,50 @@ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown
18
18
  Requires-Dist: nibabel>=5
19
19
  Provides-Extra: linux-runtime
20
- Requires-Dist: dcccpy-linux-runtime==0.2.0a2; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "linux-runtime"
21
- Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a2; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "linux-runtime"
20
+ Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "linux-runtime"
21
+ Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "linux-runtime"
22
22
  Provides-Extra: windows-runtime
23
- Requires-Dist: dcccpy-windows-runtime==0.2.0a2; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "windows-runtime"
23
+ Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "windows-runtime"
24
24
  Provides-Extra: macos-runtime
25
- Requires-Dist: dcccpy-macos-runtime==0.2.0a2; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos-runtime"
25
+ Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos-runtime"
26
26
  Provides-Extra: runtime
27
- Requires-Dist: dcccpy-linux-runtime==0.2.0a2; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "runtime"
28
- Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a2; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "runtime"
29
- Requires-Dist: dcccpy-windows-runtime==0.2.0a2; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "runtime"
30
- Requires-Dist: dcccpy-macos-runtime==0.2.0a2; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "runtime"
27
+ Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "runtime"
28
+ Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "runtime"
29
+ Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "runtime"
30
+ Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "runtime"
31
31
 
32
32
  # dcccpy
33
33
 
34
+ ### Native PET segmentation
35
+
36
+ ```python
37
+ import dcccpy
38
+ result = dcccpy.seg("pet.nii.gz", "labels.nii.gz")
39
+ labels = result.load_output()
40
+ ```
41
+
42
+ Requires a DCCCcore build containing the `seg` command and its bundled model.
43
+ The signed integer output matches the input PET's native shape and geometry.
44
+ The default `mode="freesurfer"` returns raw DKT/aseg IDs; `mode="training"`
45
+ returns IDs 0–95. `mode="neurosegmenter"` provides partial regional compatibility:
46
+ brainstem/pons IDs 45/46 are unavailable, so pons SUVR is unsupported.
47
+ Mirror TTA is enabled by default; `tta=False` explicitly changes that protocol.
48
+ Use `threads=4` to select the CPU thread count, `bundle=...` to override the model
49
+ directory, and `debug=True` to save intermediate grids and a transform receipt.
50
+ The approximately 120 MB segmentation model is excluded from PyPI wheels.
51
+ With a slim runtime, the first `seg()` call downloads the separately published
52
+ `DCCCseg-4.6.0-alpha.zip` Release asset, verifies the model SHA-256 and caches it
53
+ under the dcccpy cache directory. A full native installation reuses its bundled
54
+ model. Existing `DCCCPY_AUTO_DOWNLOAD=0` disables implicit downloads; provision
55
+ offline use with `dcccpy.download_seg_model()` or pass `bundle=` pointing to
56
+ the extracted `seg` directory. `DCCCPY_SEG_MODEL_URL` selects a mirror while
57
+ retaining checksum verification.
58
+ An explicit `config=` delegates model selection to that configuration and
59
+ suppresses default-model provisioning. With a slim runtime and a configuration
60
+ that only changes other settings, also pass `bundle=dcccpy.download_seg_model()`.
61
+ An optional `rigid_header=...` must retain the exact native PET values and record
62
+ exactly two rigid steps. Existing output images are refused by the core.
63
+
34
64
  `dcccpy` is a Python wrapper for the `DCCCcore` command-line tool from
35
65
  DCCCSlicer. It keeps the native C++ core as the execution engine and provides a
36
66
  small Python API for running common PET biomarker workflows.
@@ -212,7 +242,7 @@ the calculation again.
212
242
  Release wheels should vendor the matching `DCCCcore` runtime tree before build:
213
243
 
214
244
  ```bash
215
- python scripts/vendor_dccccore.py --version 4.5.1-alpha --release-platform ubuntu-latest-x64
245
+ python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-x64
216
246
  python -m build --wheel
217
247
  ```
218
248
 
@@ -220,7 +250,7 @@ The Linux ARM64 runtime package uses the corresponding release asset:
220
250
 
221
251
  ```bash
222
252
  cd python/dcccpy-linux-arm64-runtime
223
- python scripts/vendor_dccccore.py --version 4.5.1-alpha --release-platform ubuntu-latest-arm64
253
+ python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-arm64
224
254
  python -m build --wheel
225
255
  ```
226
256
 
@@ -238,7 +268,7 @@ The preferred distribution layout is:
238
268
 
239
269
  ## Packaging note
240
270
 
241
- The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.5.1-alpha. They omit the
271
+ The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.6.0-alpha. They omit the
242
272
  `fast_and_acc` registration model/config and the ADAD decoupler ONNX ensemble,
243
273
  while keeping the default spatial normalization model and assets needed by
244
274
  common Centiloid/CenTauR/CenTauRz workflows.
@@ -7,6 +7,7 @@ src/dcccpy/cli.py
7
7
  src/dcccpy/core.py
8
8
  src/dcccpy/py.typed
9
9
  src/dcccpy/runtime.py
10
+ src/dcccpy/segmentation.py
10
11
  src/dcccpy.egg-info/PKG-INFO
11
12
  src/dcccpy.egg-info/SOURCES.txt
12
13
  src/dcccpy.egg-info/dependency_links.txt
@@ -14,4 +15,5 @@ src/dcccpy.egg-info/entry_points.txt
14
15
  src/dcccpy.egg-info/requires.txt
15
16
  src/dcccpy.egg-info/top_level.txt
16
17
  tests/test_core.py
18
+ tests/test_seg_distribution.py
17
19
  tests/test_testpypi_centiloid.py
@@ -3,31 +3,31 @@ nibabel>=5
3
3
  [linux-runtime]
4
4
 
5
5
  [linux-runtime:sys_platform == "linux" and platform_machine == "aarch64"]
6
- dcccpy-linux-arm64-runtime==0.2.0a2
6
+ dcccpy-linux-arm64-runtime==0.2.0a3
7
7
 
8
8
  [linux-runtime:sys_platform == "linux" and platform_machine == "x86_64"]
9
- dcccpy-linux-runtime==0.2.0a2
9
+ dcccpy-linux-runtime==0.2.0a3
10
10
 
11
11
  [macos-runtime]
12
12
 
13
13
  [macos-runtime:sys_platform == "darwin" and platform_machine == "arm64"]
14
- dcccpy-macos-runtime==0.2.0a2
14
+ dcccpy-macos-runtime==0.2.0a3
15
15
 
16
16
  [runtime]
17
17
 
18
18
  [runtime:sys_platform == "darwin" and platform_machine == "arm64"]
19
- dcccpy-macos-runtime==0.2.0a2
19
+ dcccpy-macos-runtime==0.2.0a3
20
20
 
21
21
  [runtime:sys_platform == "linux" and platform_machine == "aarch64"]
22
- dcccpy-linux-arm64-runtime==0.2.0a2
22
+ dcccpy-linux-arm64-runtime==0.2.0a3
23
23
 
24
24
  [runtime:sys_platform == "linux" and platform_machine == "x86_64"]
25
- dcccpy-linux-runtime==0.2.0a2
25
+ dcccpy-linux-runtime==0.2.0a3
26
26
 
27
27
  [runtime:sys_platform == "win32" and platform_machine == "AMD64"]
28
- dcccpy-windows-runtime==0.2.0a2
28
+ dcccpy-windows-runtime==0.2.0a3
29
29
 
30
30
  [windows-runtime]
31
31
 
32
32
  [windows-runtime:sys_platform == "win32" and platform_machine == "AMD64"]
33
- dcccpy-windows-runtime==0.2.0a2
33
+ dcccpy-windows-runtime==0.2.0a3
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import subprocess
4
5
  from pathlib import Path
5
6
  import sys
6
7
  import types
@@ -20,6 +21,11 @@ from dcccpy.runtime import (
20
21
 
21
22
 
22
23
  def make_fake_dccccore(tmp_path: Path) -> Path:
24
+ bundle = tmp_path / "assets/models/seg"
25
+ bundle.mkdir(parents=True, exist_ok=True)
26
+ for name in ("dccc_seg.onnx", "manifest.json", "grid.json", "plans.json", "dataset.json",
27
+ "labels.json", "neurosegmenter_mapping.json", "validation.json"):
28
+ (bundle / name).write_bytes(b"test runtime resource")
23
29
  exe = tmp_path / "DCCCcore"
24
30
  exe.write_text(
25
31
  "#!/usr/bin/env sh\n"
@@ -46,6 +52,66 @@ def test_parse_metrics() -> None:
46
52
  assert metrics == {"fbp": 12.5, "SUVr": 1.23, "ADAD score": -0.2}
47
53
 
48
54
 
55
+ def test_seg_defaults_and_explicit_options(tmp_path, monkeypatch):
56
+ fake_args = tmp_path / "args.txt"
57
+ fake_exe = make_fake_dccccore(tmp_path)
58
+ monkeypatch.setenv("DCCCPY_FAKE_ARGS", str(fake_args))
59
+ result = dcccpy.seg("pet.nii", executable=fake_exe)
60
+ args = fake_args.read_text().splitlines()
61
+ assert args[0] == "seg"
62
+ assert args[args.index("--mode") + 1] == "freesurfer"
63
+ assert args[args.index("--threads") + 1] == "4"
64
+ assert "--no-tta" not in args
65
+ assert result.output.exists()
66
+ result = dcccpy.seg("pet.nii", tmp_path / "labels.nii.gz", mode="training",
67
+ threads=2, tta=False, bundle="model bundle",
68
+ rigid_header="rigid header.nii.gz", config="config.toml",
69
+ debug=True, executable=fake_exe)
70
+ args = fake_args.read_text().splitlines()
71
+ assert args[args.index("--mode") + 1] == "training"
72
+ assert args[args.index("--threads") + 1] == "2"
73
+ assert args[args.index("--bundle") + 1] == str(Path("model bundle").resolve())
74
+ assert args[args.index("--rigid-header") + 1] == str(Path("rigid header.nii.gz").resolve())
75
+ assert args[args.index("--config") + 1] == str(Path("config.toml").resolve())
76
+ assert "--debug" in args and "--no-tta" in args
77
+
78
+
79
+ @pytest.mark.parametrize("kwargs", [{"mode": "bogus"}, {"threads": 0},
80
+ {"threads": 1.5}, {"threads": True}])
81
+ def test_seg_rejects_bad_options(kwargs):
82
+ with pytest.raises(ValueError):
83
+ dcccpy.seg("pet.nii", **kwargs)
84
+
85
+
86
+ def test_seg_preserves_native_failure(tmp_path):
87
+ exe = make_fake_dccccore(tmp_path)
88
+ exe.write_text("#!/bin/sh\nprintf 'invalid segmentation model' >&2\nexit 7\n")
89
+ exe.chmod(0o755)
90
+ result = dcccpy.seg("pet.nii", check=False, executable=exe)
91
+ assert result.returncode == 7
92
+ assert "invalid segmentation model" in result.stderr
93
+ assert not result.output.exists()
94
+ with pytest.raises(subprocess.CalledProcessError) as error:
95
+ dcccpy.seg("pet.nii", executable=exe)
96
+ assert error.value.returncode == 7
97
+
98
+
99
+ def test_seg_custom_config_does_not_provision_or_override_model(tmp_path, monkeypatch):
100
+ exe = make_fake_dccccore(tmp_path)
101
+ (tmp_path / "assets/models/seg/dccc_seg.onnx").unlink()
102
+ args_file = tmp_path / "args.txt"
103
+ monkeypatch.setenv("DCCCPY_FAKE_ARGS", str(args_file))
104
+ monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "0")
105
+ monkeypatch.setenv("DCCCPY_CACHE_DIR", str(tmp_path / "empty-cache"))
106
+ config = tmp_path / "custom.toml"
107
+ config.write_text('[models]\nseg = "offline/custom/dccc_seg.onnx"\n')
108
+ result = dcccpy.seg("pet.nii", config=config, executable=exe)
109
+ assert result.returncode == 0
110
+ args = args_file.read_text().splitlines()
111
+ assert args[args.index("--config") + 1] == str(config)
112
+ assert "--bundle" not in args
113
+
114
+
49
115
  def test_centiloid_omits_output_by_creating_temp_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
50
116
  fake_args = tmp_path / "args.txt"
51
117
  fake_exe = make_fake_dccccore(tmp_path)
@@ -142,7 +208,7 @@ def test_dccccore_path_can_disable_auto_download(tmp_path: Path, monkeypatch: py
142
208
 
143
209
 
144
210
  def test_dccccore_path_auto_downloads_to_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
145
- asset_root = tmp_path / "asset" / "DCCCcore-4.5.1-alpha-ubuntu-latest-x64"
211
+ asset_root = tmp_path / "asset" / "DCCCcore-4.6.0-alpha-ubuntu-latest-x64"
146
212
  asset_root.mkdir(parents=True)
147
213
  exe = asset_root / "DCCCcore"
148
214
  exe.write_text("#!/usr/bin/env sh\nprintf 'fake dccccore\\n'\n")
@@ -150,7 +216,7 @@ def test_dccccore_path_auto_downloads_to_cache(tmp_path: Path, monkeypatch: pyte
150
216
 
151
217
  archive = tmp_path / "DCCCcore.zip"
152
218
  with zipfile.ZipFile(archive, "w") as zf:
153
- zf.write(exe, "DCCCcore-4.5.1-alpha-ubuntu-latest-x64/DCCCcore")
219
+ zf.write(exe, "DCCCcore-4.6.0-alpha-ubuntu-latest-x64/DCCCcore")
154
220
 
155
221
  monkeypatch.delenv("DCCCPY_DCCCCORE", raising=False)
156
222
  monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "1")
@@ -175,7 +241,7 @@ def test_find_existing_dccccore_checks_macos_runtime_package(
175
241
  exe.write_text("#!/usr/bin/env sh\n")
176
242
  exe.chmod(0o755)
177
243
 
178
- module = types.SimpleNamespace(DCCCCORE_VERSION="4.5.1-alpha", dccccore_root=lambda: root)
244
+ module = types.SimpleNamespace(DCCCCORE_VERSION="4.6.0-alpha", dccccore_root=lambda: root)
179
245
  monkeypatch.setitem(sys.modules, "dcccpy_macos_runtime", module)
180
246
  monkeypatch.setenv("PATH", str(tmp_path / "empty-path"))
181
247
 
@@ -186,10 +252,10 @@ def test_linux_arm64_release_platform() -> None:
186
252
  assert release_platform("linux-arm64") == "ubuntu-latest-arm64"
187
253
 
188
254
 
189
- def test_default_release_targets_dccccore_450_alpha() -> None:
190
- assert DCCCCORE_VERSION == "4.5.1-alpha"
255
+ def test_default_release_targets_dccccore_460_alpha() -> None:
256
+ assert DCCCCORE_VERSION == "4.6.0-alpha"
191
257
  assert release_url(platform_name="ubuntu-latest-x64").endswith(
192
- "/releases/download/v4.5.1-alpha/DCCCcore-4.5.1-alpha-ubuntu-latest-x64.zip"
258
+ "/releases/download/v4.6.0-alpha/DCCCcore-4.6.0-alpha-ubuntu-latest-x64.zip"
193
259
  )
194
260
 
195
261
 
@@ -220,7 +286,7 @@ def test_find_existing_dccccore_checks_linux_arm64_runtime_package(
220
286
  exe.write_text("#!/usr/bin/env sh\n")
221
287
  exe.chmod(0o755)
222
288
 
223
- module = types.SimpleNamespace(DCCCCORE_VERSION="4.5.1-alpha", dccccore_root=lambda: root)
289
+ module = types.SimpleNamespace(DCCCCORE_VERSION="4.6.0-alpha", dccccore_root=lambda: root)
224
290
  monkeypatch.setitem(sys.modules, "dcccpy_linux_arm64_runtime", module)
225
291
  monkeypatch.setenv("PATH", str(tmp_path / "empty-path"))
226
292
 
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import importlib.util
5
+ from pathlib import Path
6
+ import zipfile
7
+
8
+ import pytest
9
+
10
+
11
+ FILES = ("dccc_seg.onnx", "manifest.json", "grid.json", "plans.json", "dataset.json",
12
+ "labels.json", "neurosegmenter_mapping.json", "validation.json")
13
+
14
+
15
+ def import_script(path):
16
+ spec = importlib.util.spec_from_file_location("release_script", path)
17
+ module = importlib.util.module_from_spec(spec)
18
+ spec.loader.exec_module(module)
19
+ return module
20
+
21
+
22
+ @pytest.fixture
23
+ def distribution(tmp_path, monkeypatch):
24
+ import dcccpy.segmentation as model
25
+ archive = tmp_path / "model.zip"
26
+ with zipfile.ZipFile(archive, "w") as z:
27
+ for name in FILES:
28
+ z.writestr("seg/" + name, b"model" if name.endswith(".onnx") else b"{}")
29
+ monkeypatch.setenv("DCCCPY_CACHE_DIR", str(tmp_path / "cache"))
30
+ monkeypatch.setenv("DCCCPY_SEG_MODEL_URL", archive.as_uri())
31
+ monkeypatch.setattr(model, "SEG_MODEL_SHA256", hashlib.sha256(b"model").hexdigest())
32
+ return model, archive
33
+
34
+
35
+ def test_model_download_is_cached_outside_package(distribution):
36
+ model, archive = distribution
37
+ directory = model.download_seg_model()
38
+ assert directory.name == "4.6.0-alpha"
39
+ assert (directory / "dccc_seg.onnx").read_bytes() == b"model"
40
+ archive.unlink()
41
+ assert model.download_seg_model() == directory
42
+
43
+
44
+ @pytest.mark.parametrize("kind", ["digest", "traversal", "missing"])
45
+ def test_model_download_rejects_bad_assets(distribution, kind, tmp_path):
46
+ model, archive = distribution
47
+ with zipfile.ZipFile(archive, "w") as z:
48
+ for name in FILES:
49
+ if kind == "missing" and name == "labels.json":
50
+ continue
51
+ z.writestr("seg/" + name, b"wrong" if kind == "digest" else
52
+ (b"model" if name.endswith(".onnx") else b"{}"))
53
+ if kind == "traversal":
54
+ z.writestr("../escaped", b"bad")
55
+ with pytest.raises(model.SegModelDownloadError):
56
+ model.download_seg_model()
57
+ assert not (tmp_path / "escaped").exists()
58
+ assert not model.seg_model_dir().exists()
59
+
60
+
61
+ def test_bundled_model_and_download_opt_out(distribution, monkeypatch, tmp_path):
62
+ model, archive = distribution
63
+ exe = tmp_path / "native" / "DCCCcore"
64
+ bundle = exe.parent / "assets/models/seg"
65
+ bundle.mkdir(parents=True)
66
+ for name in FILES:
67
+ (bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
68
+ monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "0")
69
+ assert model.default_seg_bundle(exe) is None
70
+ (bundle / "dccc_seg.onnx").unlink()
71
+ with pytest.raises(model.SegModelDownloadError, match="download"):
72
+ model.default_seg_bundle(exe)
73
+ cached = model.download_seg_model() # Explicit provisioning honors user's call.
74
+ assert model.default_seg_bundle(exe) == cached
75
+
76
+
77
+ def test_native_symlink_reuses_real_bundled_model(distribution, monkeypatch, tmp_path):
78
+ model, _ = distribution
79
+ native = tmp_path / "native/DCCCcore"
80
+ bundle = native.parent / "assets/models/seg"
81
+ bundle.mkdir(parents=True)
82
+ native.write_text("native executable")
83
+ for name in FILES:
84
+ (bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
85
+ link = tmp_path / "bin/DCCCcore"
86
+ link.parent.mkdir()
87
+ link.symlink_to(native)
88
+ monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "0")
89
+ assert model.default_seg_bundle(link) is None
90
+
91
+
92
+ @pytest.mark.parametrize("package", ["linux", "linux-arm64", "macos", "windows"])
93
+ def test_every_runtime_profile_excludes_seg(tmp_path, package):
94
+ root = Path(__file__).resolve().parents[3]
95
+ vendor = import_script(root / f"python/dcccpy-{package}-runtime/scripts/vendor_dccccore.py")
96
+ for profile in vendor.EXCLUDE_PROFILES:
97
+ dest = tmp_path / profile
98
+ model = dest / "assets/models/seg/dccc_seg.onnx"
99
+ rigid = dest / "assets/models/registration/rigid.onnx"
100
+ model.parent.mkdir(parents=True)
101
+ rigid.parent.mkdir(parents=True)
102
+ model.write_bytes(b"bulky optional segmentation")
103
+ rigid.write_bytes(b"required rigid")
104
+ vendor.prune_profile(dest, profile)
105
+ assert not model.exists()
106
+ assert rigid.exists()
107
+
108
+
109
+ def test_model_release_zip_contains_runtime_controls(tmp_path):
110
+ root = Path(__file__).resolve().parents[3]
111
+ packer = import_script(root / "scripts/package-seg-model.py")
112
+ bundle = tmp_path / "source"
113
+ bundle.mkdir()
114
+ for name in FILES:
115
+ (bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
116
+ archive = packer.package_model(bundle, tmp_path / "dist", "4.6.0-alpha")
117
+ assert archive.name == "DCCCseg-4.6.0-alpha.zip"
118
+ with zipfile.ZipFile(archive) as z:
119
+ assert set(z.namelist()) == {"seg/" + name for name in FILES}
120
+
121
+
122
+ def test_model_release_preserves_reference_entrypoint(tmp_path):
123
+ root = Path(__file__).resolve().parents[3]
124
+ packer = import_script(root / "scripts/package-seg-model.py")
125
+ bundle = tmp_path / "source"
126
+ bundle.mkdir()
127
+ for name in FILES:
128
+ (bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
129
+ (bundle / "README.md").write_text("See dccc_seg.py for reference inference")
130
+ (bundle / "dccc_seg.py").write_text("# shipped reference entry point\n")
131
+ archive = packer.package_model(bundle, tmp_path / "dist", "4.6.0-alpha")
132
+ with zipfile.ZipFile(archive) as z:
133
+ assert z.read("seg/dccc_seg.py") == b"# shipped reference entry point\n"
134
+
135
+
136
+ def test_release_version_must_match_source():
137
+ root = Path(__file__).resolve().parents[3]
138
+ metadata = import_script(root / "scripts/release-metadata.py")
139
+ cmake = 'project(DCCCcore VERSION 4.6.0)\nset(DCCCCORE_VERSION_SUFFIX "-alpha")\n'
140
+ assert metadata.release_version(cmake, requested="4.6.0-alpha") == "4.6.0-alpha"
141
+ assert metadata.release_version(cmake, tag="v4.6.0-alpha") == "4.6.0-alpha"
142
+ with pytest.raises(ValueError):
143
+ metadata.release_version(cmake, requested="4.5.1-alpha")
144
+ with pytest.raises(ValueError):
145
+ metadata.release_version(cmake, tag="v4.6.0")
File without changes
File without changes
File without changes
File without changes
File without changes