brkraw 0.3.11__py3-none-any.whl → 0.5.0__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.
- brkraw/__init__.py +9 -3
- brkraw/apps/__init__.py +12 -0
- brkraw/apps/addon/__init__.py +30 -0
- brkraw/apps/addon/core.py +35 -0
- brkraw/apps/addon/dependencies.py +402 -0
- brkraw/apps/addon/installation.py +500 -0
- brkraw/apps/addon/io.py +21 -0
- brkraw/apps/hook/__init__.py +25 -0
- brkraw/apps/hook/core.py +636 -0
- brkraw/apps/loader/__init__.py +10 -0
- brkraw/apps/loader/core.py +622 -0
- brkraw/apps/loader/formatter.py +288 -0
- brkraw/apps/loader/helper.py +797 -0
- brkraw/apps/loader/info/__init__.py +11 -0
- brkraw/apps/loader/info/scan.py +85 -0
- brkraw/apps/loader/info/scan.yaml +90 -0
- brkraw/apps/loader/info/study.py +69 -0
- brkraw/apps/loader/info/study.yaml +156 -0
- brkraw/apps/loader/info/transform.py +92 -0
- brkraw/apps/loader/types.py +220 -0
- brkraw/cli/__init__.py +5 -0
- brkraw/cli/commands/__init__.py +2 -0
- brkraw/cli/commands/addon.py +327 -0
- brkraw/cli/commands/config.py +205 -0
- brkraw/cli/commands/convert.py +903 -0
- brkraw/cli/commands/hook.py +348 -0
- brkraw/cli/commands/info.py +74 -0
- brkraw/cli/commands/init.py +214 -0
- brkraw/cli/commands/params.py +106 -0
- brkraw/cli/commands/prune.py +288 -0
- brkraw/cli/commands/session.py +371 -0
- brkraw/cli/hook_args.py +80 -0
- brkraw/cli/main.py +83 -0
- brkraw/cli/utils.py +60 -0
- brkraw/core/__init__.py +13 -0
- brkraw/core/config.py +380 -0
- brkraw/core/entrypoints.py +25 -0
- brkraw/core/formatter.py +367 -0
- brkraw/core/fs.py +495 -0
- brkraw/core/jcamp.py +600 -0
- brkraw/core/layout.py +451 -0
- brkraw/core/parameters.py +781 -0
- brkraw/core/zip.py +1121 -0
- brkraw/dataclasses/__init__.py +14 -0
- brkraw/dataclasses/node.py +139 -0
- brkraw/dataclasses/reco.py +33 -0
- brkraw/dataclasses/scan.py +61 -0
- brkraw/dataclasses/study.py +131 -0
- brkraw/default/__init__.py +3 -0
- brkraw/default/pruner_specs/deid4share.yaml +42 -0
- brkraw/default/rules/00_default.yaml +4 -0
- brkraw/default/specs/metadata_dicom.yaml +236 -0
- brkraw/default/specs/metadata_transforms.py +92 -0
- brkraw/resolver/__init__.py +7 -0
- brkraw/resolver/affine.py +539 -0
- brkraw/resolver/datatype.py +69 -0
- brkraw/resolver/fid.py +90 -0
- brkraw/resolver/helpers.py +36 -0
- brkraw/resolver/image.py +188 -0
- brkraw/resolver/nifti.py +370 -0
- brkraw/resolver/shape.py +235 -0
- brkraw/schema/__init__.py +3 -0
- brkraw/schema/context_map.yaml +62 -0
- brkraw/schema/meta.yaml +57 -0
- brkraw/schema/niftiheader.yaml +95 -0
- brkraw/schema/pruner.yaml +55 -0
- brkraw/schema/remapper.yaml +128 -0
- brkraw/schema/rules.yaml +154 -0
- brkraw/specs/__init__.py +10 -0
- brkraw/specs/hook/__init__.py +12 -0
- brkraw/specs/hook/logic.py +31 -0
- brkraw/specs/hook/validator.py +22 -0
- brkraw/specs/meta/__init__.py +5 -0
- brkraw/specs/meta/validator.py +156 -0
- brkraw/specs/pruner/__init__.py +15 -0
- brkraw/specs/pruner/logic.py +361 -0
- brkraw/specs/pruner/validator.py +119 -0
- brkraw/specs/remapper/__init__.py +27 -0
- brkraw/specs/remapper/logic.py +924 -0
- brkraw/specs/remapper/validator.py +314 -0
- brkraw/specs/rules/__init__.py +6 -0
- brkraw/specs/rules/logic.py +263 -0
- brkraw/specs/rules/validator.py +103 -0
- brkraw-0.5.0.dist-info/METADATA +81 -0
- brkraw-0.5.0.dist-info/RECORD +88 -0
- {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info}/WHEEL +1 -2
- brkraw-0.5.0.dist-info/entry_points.txt +13 -0
- brkraw/lib/__init__.py +0 -4
- brkraw/lib/backup.py +0 -641
- brkraw/lib/bids.py +0 -0
- brkraw/lib/errors.py +0 -125
- brkraw/lib/loader.py +0 -1220
- brkraw/lib/orient.py +0 -194
- brkraw/lib/parser.py +0 -48
- brkraw/lib/pvobj.py +0 -301
- brkraw/lib/reference.py +0 -245
- brkraw/lib/utils.py +0 -471
- brkraw/scripts/__init__.py +0 -0
- brkraw/scripts/brk_backup.py +0 -106
- brkraw/scripts/brkraw.py +0 -744
- brkraw/ui/__init__.py +0 -0
- brkraw/ui/config.py +0 -17
- brkraw/ui/main_win.py +0 -214
- brkraw/ui/previewer.py +0 -225
- brkraw/ui/scan_info.py +0 -72
- brkraw/ui/scan_list.py +0 -73
- brkraw/ui/subj_info.py +0 -128
- brkraw-0.3.11.dist-info/METADATA +0 -25
- brkraw-0.3.11.dist-info/RECORD +0 -28
- brkraw-0.3.11.dist-info/entry_points.txt +0 -3
- brkraw-0.3.11.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast, TYPE_CHECKING, Dict, Optional, Union
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import logging
|
|
6
|
+
from ....specs.remapper import load_spec, map_parameters
|
|
7
|
+
from ....specs.remapper.validator import validate_spec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..types import ScanLoader, RecoLoader
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("brkraw")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve(
|
|
17
|
+
scan: "ScanLoader",
|
|
18
|
+
*,
|
|
19
|
+
spec: Optional[Dict[str, Any]] = None,
|
|
20
|
+
transforms: Optional[Dict[str, Any]] = None,
|
|
21
|
+
spec_source: Optional[Union[str, Path]] = None,
|
|
22
|
+
spec_filename: Optional[str] = None,
|
|
23
|
+
validate: bool = True,
|
|
24
|
+
) -> Dict[str, Any]:
|
|
25
|
+
"""Resolve scan-level metadata using a remapper spec.
|
|
26
|
+
|
|
27
|
+
The spec/transform sources are resolved in the following priority order:
|
|
28
|
+
1) ``spec``/``transforms`` mappings passed directly.
|
|
29
|
+
2) ``spec_source``.
|
|
30
|
+
3) Package default ``scan.yaml``, optionally overridden by ``spec_filename``.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
scan: ScanLoader instance to resolve metadata from.
|
|
34
|
+
spec: Spec mapping to use directly (highest priority).
|
|
35
|
+
transforms: Transform mapping to use with ``spec``.
|
|
36
|
+
spec_source: Path for a YAML spec.
|
|
37
|
+
spec_filename: Package-relative spec filename (default ``scan.yaml``).
|
|
38
|
+
validate: When True, validate the spec before mapping.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A dictionary of resolved scan metadata.
|
|
42
|
+
"""
|
|
43
|
+
scan = cast("ScanLoader", scan)
|
|
44
|
+
|
|
45
|
+
if spec is not None:
|
|
46
|
+
spec_data = spec
|
|
47
|
+
transforms_data = transforms or {}
|
|
48
|
+
if validate:
|
|
49
|
+
validate_spec(spec_data)
|
|
50
|
+
elif spec_source is not None:
|
|
51
|
+
spec_data, transforms_data = load_spec(
|
|
52
|
+
spec_source,
|
|
53
|
+
validate=validate,
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
spec_file = Path(__file__).with_name(spec_filename or "scan.yaml")
|
|
57
|
+
spec_data, transforms_data = load_spec(
|
|
58
|
+
spec_file,
|
|
59
|
+
validate=validate,
|
|
60
|
+
)
|
|
61
|
+
results = map_parameters(scan, spec_data, transforms_data)
|
|
62
|
+
if len(scan.avail):
|
|
63
|
+
results['Reco(s)'] = {}
|
|
64
|
+
for reco_id in scan.avail.keys():
|
|
65
|
+
reco_spec = {
|
|
66
|
+
"Type": {
|
|
67
|
+
"sources": [
|
|
68
|
+
{
|
|
69
|
+
"file": "visu_pars",
|
|
70
|
+
"key": "VisuCoreFrameType",
|
|
71
|
+
"reco_id": reco_id,
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
try:
|
|
77
|
+
results["Reco(s)"][reco_id] = map_parameters(scan, reco_spec)
|
|
78
|
+
except (FileNotFoundError, AttributeError) as exc:
|
|
79
|
+
logger.warning(
|
|
80
|
+
"visu_pars missing for scan %s reco %s; skipping reco entry: %s",
|
|
81
|
+
getattr(scan, "scan_id", "unknown"),
|
|
82
|
+
reco_id,
|
|
83
|
+
exc,
|
|
84
|
+
)
|
|
85
|
+
return results
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
__meta__:
|
|
2
|
+
name: "scan_info"
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
description: "Scan-level info mapping for Bruker datasets."
|
|
5
|
+
category: "info_spec"
|
|
6
|
+
transforms_source: "transform.py"
|
|
7
|
+
|
|
8
|
+
Method:
|
|
9
|
+
sources:
|
|
10
|
+
- file: acqp
|
|
11
|
+
key: ACQ_method
|
|
12
|
+
transform: strip_jcamp_string
|
|
13
|
+
Protocol:
|
|
14
|
+
sources:
|
|
15
|
+
- file: acqp
|
|
16
|
+
key: ACQ_protocol_name
|
|
17
|
+
transform: strip_jcamp_string
|
|
18
|
+
Dim:
|
|
19
|
+
sources:
|
|
20
|
+
- file: acqp
|
|
21
|
+
key: ACQ_dim
|
|
22
|
+
DimDesc:
|
|
23
|
+
sources:
|
|
24
|
+
- file: acqp
|
|
25
|
+
key: ACQ_dim_desc
|
|
26
|
+
transform: convert_to_list
|
|
27
|
+
NumEchoes:
|
|
28
|
+
sources:
|
|
29
|
+
- file: acqp
|
|
30
|
+
key: NECHOES
|
|
31
|
+
TE (ms):
|
|
32
|
+
sources:
|
|
33
|
+
- file: method
|
|
34
|
+
key: PVM_EchoTime
|
|
35
|
+
- file: acqp
|
|
36
|
+
key: ACQ_echo_time
|
|
37
|
+
transform: convert_to_list
|
|
38
|
+
TR (ms):
|
|
39
|
+
sources:
|
|
40
|
+
- file: method
|
|
41
|
+
key: PVM_RepetitionTime
|
|
42
|
+
- file: acqp
|
|
43
|
+
key: ACQ_repetition_time
|
|
44
|
+
transform: convert_to_list
|
|
45
|
+
FlipAngle (degree):
|
|
46
|
+
sources:
|
|
47
|
+
- file: acqp
|
|
48
|
+
key: ACQ_flip_angle
|
|
49
|
+
Shape:
|
|
50
|
+
sources:
|
|
51
|
+
- file: method
|
|
52
|
+
key: PVM_Matrix
|
|
53
|
+
transform: convert_to_list
|
|
54
|
+
FOV (mm):
|
|
55
|
+
sources:
|
|
56
|
+
- file: method
|
|
57
|
+
key: PVM_Fov
|
|
58
|
+
transform: convert_to_list
|
|
59
|
+
NumSlicePack:
|
|
60
|
+
sources:
|
|
61
|
+
- file: method
|
|
62
|
+
key: PVM_NSPacks
|
|
63
|
+
SliceOrient:
|
|
64
|
+
sources:
|
|
65
|
+
- file: method
|
|
66
|
+
key: PVM_SPackArrSliceOrient
|
|
67
|
+
transform: convert_to_list
|
|
68
|
+
ReadOrient:
|
|
69
|
+
sources:
|
|
70
|
+
- file: method
|
|
71
|
+
key: PVM_SPackArrReadOrient
|
|
72
|
+
transform: convert_to_list
|
|
73
|
+
SliceDistance (mm):
|
|
74
|
+
sources:
|
|
75
|
+
- file: method
|
|
76
|
+
key: PVM_SPackArrSliceDistance
|
|
77
|
+
transform: convert_to_list
|
|
78
|
+
SliceGap (mm):
|
|
79
|
+
sources:
|
|
80
|
+
- file: method
|
|
81
|
+
key: PVM_SPackArrSliceGap
|
|
82
|
+
transform: convert_to_list
|
|
83
|
+
NumAverage:
|
|
84
|
+
sources:
|
|
85
|
+
- file: method
|
|
86
|
+
key: PVM_NAverages
|
|
87
|
+
NumRepeat:
|
|
88
|
+
sources:
|
|
89
|
+
- file: method
|
|
90
|
+
key: PVM_NRepetitions
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Resolve study-level metadata using remapping rules.
|
|
4
|
+
|
|
5
|
+
This module maps Paravision JCAMP parameters into a normalized dictionary.
|
|
6
|
+
It prefers subject-level parameters when available, and falls back to scan-level
|
|
7
|
+
visu_pars data when subject metadata is missing. Scan fallback selects the
|
|
8
|
+
first scan that contains a reco with a readable visu_pars object.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any, cast, TYPE_CHECKING, Dict, Optional
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from ....core.parameters import Parameters
|
|
14
|
+
from ....specs.remapper import load_spec, map_parameters
|
|
15
|
+
from ....specs.remapper.validator import validate_spec
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .. import BrukerLoader
|
|
20
|
+
from ..types import StudyLoader
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve(loader: "BrukerLoader") -> Dict[str, Any]:
|
|
24
|
+
"""Resolve study/subject metadata into a normalized mapping.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
loader: BrukerLoader instance providing access to Study/Scan/Reco nodes.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A dictionary containing study and subject metadata fields.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If no scans are available or no scan has a readable
|
|
34
|
+
visu_pars when subject metadata is missing.
|
|
35
|
+
"""
|
|
36
|
+
def _resolve_section_transforms_path(section: Dict[str, Any]) -> Optional[Path]:
|
|
37
|
+
meta = section.get("__meta__")
|
|
38
|
+
if not isinstance(meta, dict) or not meta.get("transforms_source"):
|
|
39
|
+
return None
|
|
40
|
+
path = Path(meta["transforms_source"])
|
|
41
|
+
if not path.is_absolute():
|
|
42
|
+
path = (spec_path.parent / path).resolve()
|
|
43
|
+
return path
|
|
44
|
+
|
|
45
|
+
spec_path = Path(__file__).with_name("study.yaml")
|
|
46
|
+
spec, transforms = load_spec(
|
|
47
|
+
spec_path,
|
|
48
|
+
validate=False,
|
|
49
|
+
)
|
|
50
|
+
validate_spec(spec["study"], transforms_source=_resolve_section_transforms_path(spec["study"]))
|
|
51
|
+
validate_spec(spec["scan"], transforms_source=_resolve_section_transforms_path(spec["scan"]))
|
|
52
|
+
study = cast("StudyLoader", loader._study)
|
|
53
|
+
if study.has_subject:
|
|
54
|
+
return map_parameters(study, spec["study"], transforms=transforms, validate=True)
|
|
55
|
+
else:
|
|
56
|
+
if not study.avail:
|
|
57
|
+
raise ValueError("No scans available to resolve study info from visu_pars.")
|
|
58
|
+
scan_with_visu = None
|
|
59
|
+
for scan in study.avail.values():
|
|
60
|
+
for reco in scan.avail.values():
|
|
61
|
+
visu = getattr(reco, "visu_pars", None)
|
|
62
|
+
if isinstance(visu, Parameters):
|
|
63
|
+
scan_with_visu = scan
|
|
64
|
+
break
|
|
65
|
+
if scan_with_visu is not None:
|
|
66
|
+
break
|
|
67
|
+
if scan_with_visu is None:
|
|
68
|
+
raise ValueError("No scan contains a reco with readable visu_pars for study info.")
|
|
69
|
+
return map_parameters(scan_with_visu, spec["scan"], transforms=transforms)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
study:
|
|
2
|
+
__meta__:
|
|
3
|
+
name: "study_info"
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
description: "Study-level info mapping for Bruker datasets."
|
|
6
|
+
category: "info_spec"
|
|
7
|
+
transforms_source: "transform.py"
|
|
8
|
+
Study.Opperator:
|
|
9
|
+
sources:
|
|
10
|
+
- file: subject
|
|
11
|
+
key: SUBJECT_referral
|
|
12
|
+
- file: subject
|
|
13
|
+
key: SUBJECT_study_operator
|
|
14
|
+
transform: strip_jcamp_string
|
|
15
|
+
Study.Date:
|
|
16
|
+
sources:
|
|
17
|
+
- file: subject
|
|
18
|
+
key: SUBJECT_abs_date
|
|
19
|
+
- file: subject
|
|
20
|
+
key: SUBJECT_study_date
|
|
21
|
+
transform:
|
|
22
|
+
- unixtime_to_datetime
|
|
23
|
+
Study.ID:
|
|
24
|
+
sources:
|
|
25
|
+
- file: subject
|
|
26
|
+
key: SUBJECT_study_name
|
|
27
|
+
transform: strip_jcamp_string
|
|
28
|
+
Study.Number:
|
|
29
|
+
sources:
|
|
30
|
+
- file: subject
|
|
31
|
+
key: SUBJECT_study_nr
|
|
32
|
+
transform: strip_jcamp_string
|
|
33
|
+
Study.InstanceUID:
|
|
34
|
+
sources:
|
|
35
|
+
- file: subject
|
|
36
|
+
key: SUBJECT_study_instance_uid
|
|
37
|
+
transform: strip_jcamp_string
|
|
38
|
+
Subject.ID:
|
|
39
|
+
sources:
|
|
40
|
+
- file: subject
|
|
41
|
+
key: SUBJECT_id
|
|
42
|
+
transform: strip_jcamp_string
|
|
43
|
+
Subject.Name:
|
|
44
|
+
sources:
|
|
45
|
+
- file: subject
|
|
46
|
+
key: SUBJECT_name_string
|
|
47
|
+
transform: strip_jcamp_string
|
|
48
|
+
Subject.Type:
|
|
49
|
+
sources:
|
|
50
|
+
- file: subject
|
|
51
|
+
key: SUBJECT_type
|
|
52
|
+
Subject.Sex:
|
|
53
|
+
sources:
|
|
54
|
+
- file: subject
|
|
55
|
+
key: SUBJECT_sex
|
|
56
|
+
- file: subject
|
|
57
|
+
key: SUBJECT_gender
|
|
58
|
+
transform: strip_jcamp_string
|
|
59
|
+
Subject.DateOfBirth:
|
|
60
|
+
sources:
|
|
61
|
+
- file: subject
|
|
62
|
+
key: SUBJECT_dbirth
|
|
63
|
+
transform:
|
|
64
|
+
- strip_jcamp_string
|
|
65
|
+
- stringtime_to_datetime
|
|
66
|
+
Subject.Weight:
|
|
67
|
+
sources:
|
|
68
|
+
- file: subject
|
|
69
|
+
key: SUBJECT_weight
|
|
70
|
+
transform: strip_jcamp_string
|
|
71
|
+
Subject.Position:
|
|
72
|
+
sources:
|
|
73
|
+
- file: subject
|
|
74
|
+
key: SUBJECT_study_instrument_position
|
|
75
|
+
- inputs:
|
|
76
|
+
entry:
|
|
77
|
+
sources:
|
|
78
|
+
- file: subject
|
|
79
|
+
key: SUBJECT_entry
|
|
80
|
+
position:
|
|
81
|
+
sources:
|
|
82
|
+
- file: subject
|
|
83
|
+
key: SUBJECT_position
|
|
84
|
+
transform: merge_entry_and_position
|
|
85
|
+
transform: strip_jcamp_string
|
|
86
|
+
|
|
87
|
+
scan:
|
|
88
|
+
__meta__:
|
|
89
|
+
name: "scan_info_fallback"
|
|
90
|
+
version: "1.0.0"
|
|
91
|
+
description: "Scan-level fallback info mapping for Bruker datasets."
|
|
92
|
+
category: "info_spec"
|
|
93
|
+
transforms_source: "transform.py"
|
|
94
|
+
Study.Opperator:
|
|
95
|
+
sources:
|
|
96
|
+
- file: visu_pars
|
|
97
|
+
key: VisuStudyReferringPhysician
|
|
98
|
+
transform: strip_jcamp_string
|
|
99
|
+
Study.Date:
|
|
100
|
+
sources:
|
|
101
|
+
- file: visu_pars
|
|
102
|
+
key: VisuStudyDate
|
|
103
|
+
transform:
|
|
104
|
+
- strip_jcamp_string
|
|
105
|
+
- stringtime_to_datetime
|
|
106
|
+
Study.ID:
|
|
107
|
+
sources:
|
|
108
|
+
- file: visu_pars
|
|
109
|
+
key: VisuStudyId
|
|
110
|
+
transform: strip_jcamp_string
|
|
111
|
+
Study.Number:
|
|
112
|
+
sources:
|
|
113
|
+
- file: visu_pars
|
|
114
|
+
key: VisuStudyNumber
|
|
115
|
+
transform: strip_jcamp_string
|
|
116
|
+
Study.InstanceUID:
|
|
117
|
+
sources:
|
|
118
|
+
- file: visu_pars
|
|
119
|
+
key: VisuStudyUid
|
|
120
|
+
transform: strip_jcamp_string
|
|
121
|
+
Subject.ID:
|
|
122
|
+
sources:
|
|
123
|
+
- file: visu_pars
|
|
124
|
+
key: VisuSubjectId
|
|
125
|
+
transform: strip_jcamp_string
|
|
126
|
+
Subject.Name:
|
|
127
|
+
sources:
|
|
128
|
+
- file: visu_pars
|
|
129
|
+
key: VisuSubjectName
|
|
130
|
+
transform: strip_jcamp_string
|
|
131
|
+
Subject.Type:
|
|
132
|
+
sources:
|
|
133
|
+
- file: visu_pars
|
|
134
|
+
key: VisuSubjectType
|
|
135
|
+
transform: strip_jcamp_string
|
|
136
|
+
Subject.Sex:
|
|
137
|
+
sources:
|
|
138
|
+
- file: visu_pars
|
|
139
|
+
key: VisuSubjectSex
|
|
140
|
+
transform: strip_jcamp_string
|
|
141
|
+
Subject.DateOfBirth:
|
|
142
|
+
sources:
|
|
143
|
+
- file: visu_pars
|
|
144
|
+
key: VisuSubjectBirthDate
|
|
145
|
+
transform:
|
|
146
|
+
- strip_jcamp_string
|
|
147
|
+
- stringtime_to_datetime
|
|
148
|
+
Subject.Weight:
|
|
149
|
+
sources:
|
|
150
|
+
- file: visu_pars
|
|
151
|
+
key: VisuSubjectWeight
|
|
152
|
+
transform: strip_jcamp_string
|
|
153
|
+
Subject.Position:
|
|
154
|
+
sources:
|
|
155
|
+
- file: visu_pars
|
|
156
|
+
key: VisuSubjectPosition
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Union, Optional, Tuple, List, Any, cast
|
|
4
|
+
from datetime import datetime, timezone, timedelta
|
|
5
|
+
import numpy as np
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
def strip_jcamp_string(value: Optional[str]) -> str:
|
|
9
|
+
if value is None:
|
|
10
|
+
return "Unknown"
|
|
11
|
+
text = str(value).strip()
|
|
12
|
+
if text.startswith("<") and text.endswith(">"):
|
|
13
|
+
text = text[1:-1]
|
|
14
|
+
text = re.sub(r"\^+", " ", text)
|
|
15
|
+
return " ".join(text.split())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def unixtime_to_datetime(value: Union[int, float, Tuple[Union[int, float], ...]]) -> datetime:
|
|
19
|
+
"""Convert unix time value to timezone-aware datetime.
|
|
20
|
+
|
|
21
|
+
Accepts:
|
|
22
|
+
- int/float: epoch seconds (local timezone)
|
|
23
|
+
- tuple: (sec,), (sec, ms), or (sec, ms, offset_min)
|
|
24
|
+
|
|
25
|
+
If offset_min is missing, local timezone is used.
|
|
26
|
+
"""
|
|
27
|
+
local_tz = datetime.now().astimezone().tzinfo
|
|
28
|
+
|
|
29
|
+
if isinstance(value, (int, float)):
|
|
30
|
+
return datetime.fromtimestamp(value, tz=local_tz)
|
|
31
|
+
|
|
32
|
+
if not isinstance(value, tuple) or not value:
|
|
33
|
+
raise TypeError(f"Unsupported value: {value!r}")
|
|
34
|
+
|
|
35
|
+
sec = value[0]
|
|
36
|
+
ms = value[1] if len(value) > 1 else 0
|
|
37
|
+
offset_min = value[2] if len(value) > 2 else None
|
|
38
|
+
|
|
39
|
+
tz = timezone(timedelta(minutes=offset_min)) if offset_min is not None else local_tz
|
|
40
|
+
return datetime.fromtimestamp(sec, tz=tz).replace(microsecond=int(ms) * 1000)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def stringtime_to_datetime(value: str) -> Union[datetime, str]:
|
|
44
|
+
"""Parse PV time strings into datetime.
|
|
45
|
+
|
|
46
|
+
Supported formats:
|
|
47
|
+
- 2026-01-01T12:00:00,873-0500
|
|
48
|
+
- 12:00:00 1 Jan 2026
|
|
49
|
+
- 1 Jan 2026
|
|
50
|
+
- 20260101
|
|
51
|
+
"""
|
|
52
|
+
_FORMATS = (
|
|
53
|
+
"%Y-%m-%dT%H:%M:%S,%f%z",
|
|
54
|
+
"%H:%M:%S %d %b %Y",
|
|
55
|
+
"%d %b %Y",
|
|
56
|
+
"%Y%m%d",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
value = value.strip()
|
|
60
|
+
if len(value) == 0:
|
|
61
|
+
return "Unknown"
|
|
62
|
+
for fmt in _FORMATS:
|
|
63
|
+
try:
|
|
64
|
+
return datetime.strptime(value, fmt)
|
|
65
|
+
except ValueError:
|
|
66
|
+
continue
|
|
67
|
+
raise ValueError(f"Unsupported time format: {value!r}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def merge_entry_and_position(entry: str, position: str):
|
|
71
|
+
entry = entry.split('_')[-1].replace("First", "")
|
|
72
|
+
position = position.split('_')[-1]
|
|
73
|
+
return f'{entry}_{position}'
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def convert_to_list(value: Any) -> List[Any]:
|
|
77
|
+
if value is None:
|
|
78
|
+
return []
|
|
79
|
+
if hasattr(value, "tolist"):
|
|
80
|
+
return cast(Any, value).tolist()
|
|
81
|
+
if isinstance(value, (list, tuple)):
|
|
82
|
+
return list(value)
|
|
83
|
+
return [value]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
__all__ = [
|
|
87
|
+
'strip_jcamp_string',
|
|
88
|
+
'unixtime_to_datetime',
|
|
89
|
+
'stringtime_to_datetime',
|
|
90
|
+
'merge_entry_and_position',
|
|
91
|
+
'convert_to_list'
|
|
92
|
+
]
|