brkraw 0.5.2__py3-none-any.whl → 0.5.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. brkraw/__init__.py +1 -1
  2. brkraw/api/__init__.py +122 -0
  3. brkraw/api/types.py +39 -0
  4. brkraw/apps/loader/__init__.py +3 -6
  5. brkraw/apps/loader/core.py +128 -132
  6. brkraw/apps/loader/formatter.py +0 -2
  7. brkraw/apps/loader/helper.py +334 -114
  8. brkraw/apps/loader/info/scan.py +2 -2
  9. brkraw/apps/loader/info/transform.py +0 -1
  10. brkraw/apps/loader/types.py +56 -59
  11. brkraw/cli/commands/addon.py +1 -1
  12. brkraw/cli/commands/cache.py +82 -0
  13. brkraw/cli/commands/config.py +2 -2
  14. brkraw/cli/commands/convert.py +61 -38
  15. brkraw/cli/commands/hook.py +1 -3
  16. brkraw/cli/commands/info.py +1 -1
  17. brkraw/cli/commands/init.py +1 -1
  18. brkraw/cli/commands/params.py +1 -1
  19. brkraw/cli/commands/prune.py +2 -2
  20. brkraw/cli/commands/session.py +1 -11
  21. brkraw/cli/main.py +51 -1
  22. brkraw/cli/utils.py +1 -1
  23. brkraw/core/cache.py +87 -0
  24. brkraw/core/config.py +18 -2
  25. brkraw/core/fs.py +26 -9
  26. brkraw/core/zip.py +46 -32
  27. brkraw/dataclasses/__init__.py +3 -2
  28. brkraw/dataclasses/study.py +73 -23
  29. brkraw/resolver/datatype.py +10 -2
  30. brkraw/resolver/image.py +140 -21
  31. brkraw/resolver/nifti.py +4 -12
  32. brkraw/schema/niftiheader.yaml +0 -2
  33. brkraw/specs/meta/validator.py +0 -1
  34. brkraw/specs/rules/logic.py +1 -3
  35. {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/METADATA +8 -9
  36. {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/RECORD +39 -35
  37. {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/entry_points.txt +1 -0
  38. {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/WHEEL +0 -0
  39. {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/licenses/LICENSE +0 -0
@@ -8,9 +8,9 @@ from ....specs.remapper.validator import validate_spec
8
8
 
9
9
 
10
10
  if TYPE_CHECKING:
11
- from ..types import ScanLoader, RecoLoader
11
+ from ..types import ScanLoader
12
12
 
13
- logger = logging.getLogger("brkraw")
13
+ logger = logging.getLogger(__name__)
14
14
 
15
15
 
16
16
  def resolve(
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Union, Optional, Tuple, List, Any, cast
4
4
  from datetime import datetime, timezone, timedelta
5
- import numpy as np
6
5
  import re
7
6
 
8
7
  def strip_jcamp_string(value: Optional[str]) -> str:
@@ -5,7 +5,7 @@ Last updated: 2025-12-30
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from typing import Any, Union, Tuple, Dict, Optional, Protocol, Literal, Mapping, Callable, List, TYPE_CHECKING
8
+ from typing import Any, Union, Tuple, Dict, Optional, Protocol, Literal, Mapping, List, TYPE_CHECKING, runtime_checkable
9
9
  if TYPE_CHECKING:
10
10
  from typing_extensions import ParamSpec, TypeAlias
11
11
  else:
@@ -16,37 +16,46 @@ else:
16
16
  from ...dataclasses.study import Study
17
17
  from ...dataclasses.scan import Scan
18
18
  from ...dataclasses.reco import Reco
19
+ from ...resolver.affine import SubjectType, SubjectPose
19
20
  import numpy as np
21
+ from numpy.typing import NDArray
20
22
 
21
23
  if TYPE_CHECKING:
22
24
  from pathlib import Path
23
25
  from ...core.parameters import Parameters
24
26
  from ...resolver.image import ResolvedImage
25
- from ...resolver.affine import ResolvedAffine, SubjectType, SubjectPose
27
+ from ...resolver.affine import ResolvedAffine
26
28
  from ...resolver.nifti import Nifti1HeaderContents, XYZUNIT, TUNIT
27
- from nibabel.nifti1 import Nifti1Image
28
-
29
29
 
30
30
 
31
31
  InfoScope = Literal['full', 'study', 'scan']
32
- AffineReturn = Optional[Union[np.ndarray, Tuple[np.ndarray, ...]]]
32
+ Dataobjs = Optional[Union[NDArray, Tuple[NDArray, ...]]]
33
+ Affines = Optional[Union[NDArray, Tuple[NDArray, ...]]]
33
34
  AffineSpace = Literal["raw", "scanner", "subject_ras"]
35
+ ConvertedObj = Optional[Union["ToFilename", Tuple["ToFilename", ...]]]
36
+ Metadata = Optional[Union[Dict, Tuple[Optional[Dict], ...]]]
37
+ HookArgs = Optional[Mapping[str, Mapping[str, Any]]]
38
+
34
39
 
35
40
  P = ParamSpec("P")
36
41
 
37
42
 
43
+ @runtime_checkable
38
44
  class GetDataobjType(Protocol[P]):
39
45
  """Callable signature for get_dataobj overrides."""
40
46
  def __call__(
41
47
  self,
42
48
  scan: "Scan",
43
49
  reco_id: Optional[int],
50
+ cycle_index: Optional[int],
51
+ cycle_count: Optional[int],
44
52
  *args: P.args,
45
53
  **kwargs: P.kwargs
46
- ) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
54
+ ) -> Dataobjs:
47
55
  ...
48
56
 
49
57
 
58
+ @runtime_checkable
50
59
  class GetAffineType(Protocol):
51
60
  """Callable signature for get_affine overrides."""
52
61
  def __call__(
@@ -59,48 +68,25 @@ class GetAffineType(Protocol):
59
68
  override_subject_pose: Optional[SubjectPose],
60
69
  decimals: Optional[int] = None,
61
70
  **kwargs: Any
62
- ) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
63
- ...
64
-
65
-
66
- class GetNifti1ImageType(Protocol):
67
- """Callable signature for get_nifti1image overrides."""
68
- def __call__(
69
- self,
70
- scan: "Scan",
71
- reco_id: Optional[int] = None,
72
- *,
73
- override_header: Optional[Union[dict, "Nifti1HeaderContents"]],
74
- space: AffineSpace,
75
- override_subject_type: Optional[SubjectType],
76
- override_subject_pose: Optional[SubjectPose],
77
- flip_x: bool,
78
- flatten_fg: bool,
79
- xyz_units: XYZUNIT,
80
- t_units: TUNIT,
81
- **kwargs: Any,
82
- ) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
71
+ ) -> Affines:
83
72
  ...
84
73
 
85
74
 
75
+ @runtime_checkable
86
76
  class ConvertType(Protocol):
87
77
  """Callable signature for convert overrides."""
88
78
  def __call__(
89
79
  self,
90
80
  scan: "Scan",
91
- reco_id: Optional[int] = None,
92
- *,
93
- format: Union[Literal["nifti", "nifti1"], str],
94
- override_header: Optional[Union[dict, "Nifti1HeaderContents"]],
95
- space: AffineSpace,
96
- override_subject_type: Optional[SubjectType],
97
- override_subject_pose: Optional[SubjectPose],
98
- flip_x: bool,
99
- flatten_fg: bool,
100
- xyz_units: XYZUNIT,
101
- t_units: TUNIT,
81
+ dataobj: Union[Tuple["np.ndarray", ...], "np.ndarray"],
82
+ affine: Union[Tuple["np.ndarray", ...], "np.ndarray"],
102
83
  **kwargs: Any,
103
- ) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
84
+ ) -> ConvertedObj:
85
+ ...
86
+
87
+ class ToFilename(Protocol):
88
+ """Result object that can be written to disk."""
89
+ def to_filename(self, filename: Union[str, "Path"], *args: Any, **kwargs: Any) -> Any:
104
90
  ...
105
91
 
106
92
 
@@ -126,9 +112,12 @@ class ScanLoader(Scan, BaseLoader):
126
112
 
127
113
  image_info: Dict[int, Optional["ResolvedImage"]]
128
114
  affine_info: Dict[int, Optional["ResolvedAffine"]]
115
+ converter_func: Optional[ConvertType]
129
116
  _converter_hook: Optional[ConverterHook]
130
117
  _converter_hook_name: Optional[str]
131
-
118
+ _hook_resolved: bool = False
119
+
120
+
132
121
  def get_fid(self,
133
122
  buffer_start: Optional[int],
134
123
  buffer_size: Optional[int],
@@ -138,8 +127,12 @@ class ScanLoader(Scan, BaseLoader):
138
127
 
139
128
  def get_dataobj(
140
129
  self,
141
- reco_id: Optional[int] = None
142
- ) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
130
+ reco_id: Optional[int] = None,
131
+ *,
132
+ cycle_index: Optional[int] = None,
133
+ cycle_count: Optional[int] = None,
134
+ **kwargs: Any
135
+ ) -> Dataobjs:
143
136
  ...
144
137
 
145
138
  def get_affine(
@@ -151,39 +144,35 @@ class ScanLoader(Scan, BaseLoader):
151
144
  override_subject_pose: Optional[SubjectPose],
152
145
  decimals: Optional[int] = None,
153
146
  **kwargs: Any,
154
- ) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
147
+ ) -> Affines:
155
148
  ...
156
149
 
157
150
  def get_nifti1image(
158
- self,
159
- reco_id: Optional[int] = None,
151
+ self,
152
+ reco_id: int,
153
+ dataobjs: Tuple["np.ndarray", ...],
154
+ affines: Tuple["np.ndarray", ...],
160
155
  *,
161
156
  override_header: Optional[Union[dict, "Nifti1HeaderContents"]],
162
- space: AffineSpace = "subject_ras",
163
- override_subject_type: Optional[SubjectType],
164
- override_subject_pose: Optional[SubjectPose],
165
- flip_x: bool,
166
- flatten_fg: bool,
167
157
  xyz_units: XYZUNIT,
168
158
  t_units: TUNIT
169
- ) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
159
+ ) -> ConvertedObj:
170
160
  ...
171
161
 
172
162
  def convert(
173
163
  self,
174
164
  reco_id: Optional[int] = None,
175
165
  *,
176
- format: Literal["nifti", "nifti1"],
177
- override_header: Optional[Union[dict, "Nifti1HeaderContents"]],
178
166
  space: AffineSpace = "subject_ras",
167
+ override_header: Optional[Union[dict, "Nifti1HeaderContents"]],
179
168
  override_subject_type: Optional[SubjectType],
180
169
  override_subject_pose: Optional[SubjectPose],
181
- flip_x: bool,
182
170
  flatten_fg: bool,
183
171
  xyz_units: XYZUNIT,
184
172
  t_units: TUNIT,
185
- hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,
186
- ) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
173
+ hook_args_by_name: HookArgs = None,
174
+ **kwargs: Any,
175
+ ) -> ConvertedObj:
187
176
  ...
188
177
 
189
178
  def get_metadata(
@@ -192,7 +181,7 @@ class ScanLoader(Scan, BaseLoader):
192
181
  spec: Optional[Union[Mapping[str, Any], str, "Path"]] = None,
193
182
  context_map: Optional[Union[str, "Path"]] = None,
194
183
  return_spec: bool = False,
195
- ) -> Optional[Union[dict, Tuple[Optional[dict], Optional[dict]]]]:
184
+ ) -> Metadata:
196
185
  ...
197
186
 
198
187
 
@@ -201,19 +190,27 @@ class RecoLoader(Reco, BaseLoader):
201
190
  ...
202
191
 
203
192
 
204
- ConverterHook: TypeAlias = Mapping[str, Union[GetDataobjType[Any], GetAffineType, GetNifti1ImageType, ConvertType]]
193
+ ConverterHook: TypeAlias = Mapping[str, Union[GetDataobjType[Any], GetAffineType, ConvertType]]
205
194
  """Mapping of converter hook keys to override callables."""
206
195
 
207
196
 
208
197
  __all__ = [
209
198
  'GetDataobjType',
210
199
  'GetAffineType',
211
- 'GetNifti1ImageType',
212
200
  'ConvertType',
201
+ 'ToFilename',
213
202
  'ConverterHook',
214
203
  'StudyLoader',
215
204
  'ScanLoader',
216
205
  'RecoLoader',
206
+ 'SubjectType',
207
+ 'SubjectPose',
208
+ 'Affines',
209
+ 'Dataobjs',
210
+ 'Metadata',
211
+ 'ConvertedObj',
212
+ 'HookArgs',
213
+ 'AffineSpace',
217
214
  ]
218
215
 
219
216
  def __dir__() -> List[str]:
@@ -10,7 +10,7 @@ from brkraw.core import config as config_core
10
10
  from brkraw.core import formatter
11
11
  from brkraw.apps import addon as addon_app
12
12
 
13
- logger = logging.getLogger("brkraw")
13
+ logger = logging.getLogger(__name__)
14
14
 
15
15
 
16
16
  def cmd_addon(args: argparse.Namespace) -> int:
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ from typing import Optional
6
+
7
+ from ...core import cache
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def cmd_cache(args: argparse.Namespace) -> int:
13
+ handler = getattr(args, "cache_func", None)
14
+ if handler is None:
15
+ args.parser.print_help()
16
+ return 2
17
+ return handler(args)
18
+
19
+
20
+ def cmd_info(args: argparse.Namespace) -> int:
21
+ info = cache.get_info(root=args.root)
22
+ path = info["path"]
23
+ size = info["size"]
24
+ count = info["count"]
25
+
26
+ # Format size
27
+ unit = "B"
28
+ size_f = float(size)
29
+ for u in ["B", "KB", "MB", "GB", "TB"]:
30
+ unit = u
31
+ if size_f < 1024:
32
+ break
33
+ size_f /= 1024
34
+
35
+ print(f"Path: {path}")
36
+ print(f"Size: {size_f:.2f} {unit}")
37
+ print(f"Files: {count}")
38
+ return 0
39
+
40
+
41
+ def cmd_clear(args: argparse.Namespace) -> int:
42
+ if not args.yes:
43
+ info = cache.get_info(root=args.root)
44
+ if info["count"] == 0:
45
+ print("Cache is already empty.")
46
+ return 0
47
+ path = info["path"]
48
+ prompt = f"Clear {info['count']} files from {path}? [y/N]: "
49
+ try:
50
+ reply = input(prompt).strip().lower()
51
+ except EOFError:
52
+ reply = ""
53
+ if reply not in {"y", "yes"}:
54
+ return 1
55
+
56
+ cache.clear(root=args.root)
57
+ print("Cache cleared.")
58
+ return 0
59
+
60
+
61
+ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[name-defined]
62
+ cache_parser = subparsers.add_parser(
63
+ "cache",
64
+ help="Manage brkraw cache.",
65
+ )
66
+ cache_parser.add_argument(
67
+ "--root",
68
+ help="Override config root directory (default: BRKRAW_CONFIG_HOME or ~/.brkraw).",
69
+ )
70
+ cache_parser.set_defaults(func=cmd_cache, parser=cache_parser)
71
+ cache_sub = cache_parser.add_subparsers(dest="cache_command")
72
+
73
+ info_parser = cache_sub.add_parser("info", help="Show cache information.")
74
+ info_parser.set_defaults(cache_func=cmd_info)
75
+
76
+ clear_parser = cache_sub.add_parser("clear", help="Clear cache contents.")
77
+ clear_parser.add_argument(
78
+ "--yes", "-y",
79
+ action="store_true",
80
+ help="Do not prompt for confirmation.",
81
+ )
82
+ clear_parser.set_defaults(cache_func=cmd_clear)
@@ -11,7 +11,7 @@ import subprocess
11
11
 
12
12
  from brkraw.core import config as config_core
13
13
 
14
- logger = logging.getLogger("brkraw")
14
+ logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
17
  def cmd_config(args: argparse.Namespace) -> int:
@@ -179,7 +179,7 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[na
179
179
  path_parser = config_sub.add_parser("path", help="Print a specific config path.")
180
180
  path_parser.add_argument(
181
181
  "name",
182
- choices=["root", "config", "rules", "specs", "transforms"],
182
+ choices=["root", "config", "rules", "specs", "transforms", "cache"],
183
183
  help="Path key to print.",
184
184
  )
185
185
  path_parser.set_defaults(config_func=cmd_path)
@@ -11,9 +11,8 @@ import json
11
11
  import logging
12
12
  import os
13
13
  import re
14
- import sys
15
14
  from pathlib import Path
16
- from typing import Any, Mapping, Optional, Dict, List, Tuple, Literal, cast, get_args
15
+ from typing import Any, Mapping, Optional, Dict, List, Tuple, cast, get_args
17
16
 
18
17
  import numpy as np
19
18
  from brkraw.cli.utils import load
@@ -27,7 +26,7 @@ from brkraw.resolver.affine import SubjectPose, SubjectType
27
26
  from brkraw.apps.loader.types import AffineSpace
28
27
 
29
28
 
30
- logger = logging.getLogger("brkraw")
29
+ logger = logging.getLogger(__name__)
31
30
 
32
31
  _INVALID_CHARS = re.compile(r"[^A-Za-z0-9._-]+")
33
32
 
@@ -84,10 +83,30 @@ def cmd_convert(args: argparse.Namespace) -> int:
84
83
  if args.no_convert and not args.sidecar:
85
84
  logger.error("--no-convert requires --sidecar.")
86
85
  return 2
87
- if not args.flip_x:
88
- args.flip_x = _env_flag("BRKRAW_CONVERT_FLIP_X")
89
86
  if not args.flatten_fg:
90
87
  args.flatten_fg = _env_flag("BRKRAW_CONVERT_FLATTEN_FG")
88
+
89
+ # resolve cycle_index/cycle_count from env
90
+ if args.cycle_index is None:
91
+ value = os.environ.get("BRKRAW_CONVERT_CYCLE_INDEX")
92
+ if value:
93
+ try:
94
+ args.cycle_index = int(value)
95
+ except ValueError:
96
+ logger.error("Invalid BRKRAW_CONVERT_CYCLE_INDEX: %s", value)
97
+ return 2
98
+ if args.cycle_count is None:
99
+ value = os.environ.get("BRKRAW_CONVERT_CYCLE_COUNT")
100
+ if value:
101
+ try:
102
+ args.cycle_count = int(value)
103
+ except ValueError:
104
+ logger.error("Invalid BRKRAW_CONVERT_CYCLE_COUNT: %s", value)
105
+ return 2
106
+ # if cycle_count is set but cycle_index is not, default cycle_index to 0
107
+ if args.cycle_index is None and args.cycle_count is not None:
108
+ args.cycle_index = 0
109
+
91
110
  if args.space is None:
92
111
  args.space = os.environ.get("BRKRAW_CONVERT_SPACE")
93
112
  if args.override_subject_type is None:
@@ -119,7 +138,6 @@ def cmd_convert(args: argparse.Namespace) -> int:
119
138
  if args.space is None:
120
139
  args.space = "subject_ras"
121
140
  for attr, env_key in (
122
- ("format", "BRKRAW_CONVERT_FORMAT"),
123
141
  ("header", "BRKRAW_CONVERT_HEADER"),
124
142
  ("context_map", "BRKRAW_CONVERT_CONTEXT_MAP"),
125
143
  ):
@@ -139,13 +157,6 @@ def cmd_convert(args: argparse.Namespace) -> int:
139
157
  logger.error("Cannot use --prefix when --output is a file path.")
140
158
  return 2
141
159
 
142
- args.format = _coerce_choice(
143
- "BRKRAW_CONVERT_FORMAT",
144
- args.format or "nifti",
145
- ("nifti", "nifti1"),
146
- default="nifti",
147
- )
148
-
149
160
  try:
150
161
  render_layout_supports_counter = "counter" in inspect.signature(layout_core.render_layout).parameters
151
162
  except (TypeError, ValueError):
@@ -179,6 +190,7 @@ def cmd_convert(args: argparse.Namespace) -> int:
179
190
  hook_args_by_name = merge_hook_args(hook_args_by_name, hook_args_cli)
180
191
 
181
192
  loader = load(args.path, prefix="Loading")
193
+ logger.debug("Dataset: %s loaded", args.path)
182
194
  try:
183
195
  override_header = nifti_resolver.load_header_overrides(args.header)
184
196
  except ValueError:
@@ -234,7 +246,9 @@ def cmd_convert(args: argparse.Namespace) -> int:
234
246
  if scan_id is None:
235
247
  continue
236
248
  scan = loader.get_scan(scan_id)
249
+ logger.debug("Processing scan %s.", scan_id)
237
250
  reco_ids = [args.reco_id] if args.reco_id is not None else list(scan.avail.keys())
251
+ logger.debug("Recos: %s", reco_ids or "None")
238
252
  if not reco_ids:
239
253
  if getattr(scan, "_converter_hook", None):
240
254
  reco_ids = [None]
@@ -262,20 +276,26 @@ def cmd_convert(args: argparse.Namespace) -> int:
262
276
  nii_list: List[Any] = []
263
277
  output_count = 1
264
278
  else:
265
- nii = loader.convert(
266
- scan_id,
267
- reco_id=reco_id,
268
- format=cast(Literal["nifti", "nifti1"], args.format),
269
- space=cast(AffineSpace, args.space),
270
- override_header=cast(Nifti1HeaderContents, override_header) if override_header else None,
271
- override_subject_type=cast(Optional[SubjectType], args.override_subject_type),
272
- override_subject_pose=cast(Optional[SubjectPose], args.override_subject_pose),
273
- flip_x=args.flip_x,
274
- flatten_fg=args.flatten_fg,
275
- xyz_units=cast(XYZUNIT, args.xyz_units),
276
- t_units=cast(TUNIT, args.t_units),
277
- hook_args_by_name=hook_args_by_name,
278
- )
279
+ try:
280
+ nii = loader.convert(
281
+ scan_id,
282
+ reco_id=reco_id,
283
+ space=cast(AffineSpace, args.space),
284
+ override_header=cast(Nifti1HeaderContents, override_header) if override_header else None,
285
+ override_subject_type=cast(Optional[SubjectType], args.override_subject_type),
286
+ override_subject_pose=cast(Optional[SubjectPose], args.override_subject_pose),
287
+ flatten_fg=args.flatten_fg,
288
+ xyz_units=cast(XYZUNIT, args.xyz_units),
289
+ t_units=cast(TUNIT, args.t_units),
290
+ hook_args_by_name=hook_args_by_name,
291
+ cycle_index=args.cycle_index,
292
+ cycle_count=args.cycle_count,
293
+ )
294
+ except Exception as exc:
295
+ logger.error("Conversion failed for scan %s reco %s: %s", scan_id, reco_id, exc)
296
+ if not batch_all and args.reco_id is not None:
297
+ return 2
298
+ continue
279
299
  if nii is None:
280
300
  if not batch_all and args.reco_id is not None:
281
301
  logger.error("No NIfTI output generated for scan %s reco %s.", scan_id, reco_id)
@@ -686,7 +706,10 @@ def _parse_hook_args(values: List[str]) -> Dict[str, Dict[str, Any]]:
686
706
  key = key.strip()
687
707
  if not hook_name or not key:
688
708
  raise ValueError("Hook args must include hook name and key.")
689
- parsed.setdefault(hook_name, {})[key] = _coerce_scalar(value.strip())
709
+ coerced_value = _coerce_scalar(value.strip())
710
+ logger.debug("Parsed hook arg %s:%s=%s", hook_name, key, coerced_value)
711
+ parsed.setdefault(hook_name, {})[key] = coerced_value
712
+ logger.debug("Parsed hook args: %s", parsed)
690
713
  return parsed
691
714
 
692
715
 
@@ -781,11 +804,6 @@ def _add_convert_args(
781
804
  type=int,
782
805
  help="Reco id to convert (defaults to all recos when omitted).",
783
806
  )
784
- parser.add_argument(
785
- "--flip-x",
786
- action="store_true",
787
- help="Flip x-axis in NIfTI header.",
788
- )
789
807
  parser.add_argument(
790
808
  "--xyz-units",
791
809
  choices=list(get_args(XYZUNIT)),
@@ -854,16 +872,21 @@ def _add_convert_args(
854
872
  choices=list(get_args(SubjectPose)),
855
873
  help="Override subject pose for subject-view affines (space=subject_ras).",
856
874
  )
857
- parser.add_argument(
858
- "--format",
859
- choices=["nifti", "nifti1"],
860
- help="Output format (default: nifti).",
861
- )
862
875
  parser.add_argument(
863
876
  "--flatten-fg",
864
877
  action="store_true",
865
878
  help="Flatten frame-group dimensions to 4D when data is 5D or higher.",
866
879
  )
880
+ parser.add_argument(
881
+ "--cycle-index",
882
+ type=int,
883
+ help="Start cycle index (last axis). When set, read only a subset of cycles.",
884
+ )
885
+ parser.add_argument(
886
+ "--cycle-count",
887
+ type=int,
888
+ help="Number of cycles to read starting at --cycle-index. When omitted, reads to the end.",
889
+ )
867
890
  parser.add_argument(
868
891
  "--no-compress",
869
892
  dest="compress",
@@ -15,7 +15,7 @@ from brkraw.core import formatter
15
15
  from brkraw.specs import hook as converter_core
16
16
  import yaml
17
17
 
18
- logger = logging.getLogger("brkraw")
18
+ logger = logging.getLogger(__name__)
19
19
 
20
20
 
21
21
  def cmd_hook(args: argparse.Namespace) -> int:
@@ -178,12 +178,10 @@ _PRESET_IGNORE_PARAMS = frozenset(
178
178
  "scan",
179
179
  "scan_id",
180
180
  "reco_id",
181
- "format",
182
181
  "space",
183
182
  "override_header",
184
183
  "override_subject_type",
185
184
  "override_subject_pose",
186
- "flip_x",
187
185
  "xyz_units",
188
186
  "t_units",
189
187
  "decimals",
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from brkraw.core import config as config_core
8
8
  from brkraw.cli.utils import load
9
9
 
10
- logger = logging.getLogger("brkraw")
10
+ logger = logging.getLogger(__name__)
11
11
 
12
12
 
13
13
  def cmd_info(args: argparse.Namespace) -> int:
@@ -13,7 +13,7 @@ import yaml
13
13
  from brkraw.core import config as config_core
14
14
  from brkraw.apps import addon as addon_app
15
15
 
16
- logger = logging.getLogger("brkraw")
16
+ logger = logging.getLogger(__name__)
17
17
 
18
18
 
19
19
  def cmd_init(args: argparse.Namespace) -> int:
@@ -14,7 +14,7 @@ import numpy as np
14
14
 
15
15
  from brkraw.cli.utils import load
16
16
 
17
- logger = logging.getLogger("brkraw")
17
+ logger = logging.getLogger(__name__)
18
18
 
19
19
 
20
20
  def cmd_params(args: argparse.Namespace) -> int:
@@ -6,7 +6,7 @@ import argparse
6
6
  import logging
7
7
  from datetime import datetime
8
8
  from pathlib import Path
9
- from typing import Optional, Union
9
+ from typing import Optional
10
10
 
11
11
  import yaml
12
12
 
@@ -14,7 +14,7 @@ from brkraw.cli.utils import spinner
14
14
  from brkraw.core import config as config_core
15
15
  from brkraw.specs.pruner import prune_dataset_to_zip_from_spec
16
16
 
17
- logger = logging.getLogger("brkraw")
17
+ logger = logging.getLogger(__name__)
18
18
 
19
19
 
20
20
  def cmd_prune(args: argparse.Namespace) -> int: