pymiox 0.1.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.

Potentially problematic release.


This version of pymiox might be problematic. Click here for more details.

Files changed (123) hide show
  1. miox/__init__.py +79 -0
  2. miox/__main__.py +22 -0
  3. miox/audit.py +143 -0
  4. miox/cli.py +292 -0
  5. miox/data.py +1260 -0
  6. miox/matfile.py +94 -0
  7. miox/meta/__init__.py +22 -0
  8. miox/meta/keys.py +408 -0
  9. miox/meta/meta.py +61 -0
  10. miox/preview.py +454 -0
  11. miox/processing.py +766 -0
  12. miox/quicklook.py +1195 -0
  13. miox/quicklook_pages.py +1307 -0
  14. miox/readers/__init__.py +84 -0
  15. miox/readers/bart_cfl.py +113 -0
  16. miox/readers/base.py +380 -0
  17. miox/readers/basis_folder.py +110 -0
  18. miox/readers/biopac_acq.py +380 -0
  19. miox/readers/brainvoyager.py +485 -0
  20. miox/readers/brainvoyager_text.py +425 -0
  21. miox/readers/bruker.py +563 -0
  22. miox/readers/bruker_2dseq.py +150 -0
  23. miox/readers/bruker_raw.py +368 -0
  24. miox/readers/csdm.py +135 -0
  25. miox/readers/dicom_derived.py +1102 -0
  26. miox/readers/dicom_image.py +595 -0
  27. miox/readers/dicom_mrs.py +528 -0
  28. miox/readers/dmiwizard_map.py +66 -0
  29. miox/readers/dsi_studio.py +102 -0
  30. miox/readers/fastmri.py +103 -0
  31. miox/readers/fida_mat.py +109 -0
  32. miox/readers/fsl_mrs_basis.py +70 -0
  33. miox/readers/fsl_mrs_dynamic.py +107 -0
  34. miox/readers/fsl_vest.py +78 -0
  35. miox/readers/gadgetron_array.py +66 -0
  36. miox/readers/gava_midas_prior.py +90 -0
  37. miox/readers/ge_pfile.py +162 -0
  38. miox/readers/ge_pfile_raw.py +151 -0
  39. miox/readers/ge_rdb.py +429 -0
  40. miox/readers/ge_scanarchive.py +372 -0
  41. miox/readers/ge_signa.py +516 -0
  42. miox/readers/gipl.py +93 -0
  43. miox/readers/inspector_basis.py +72 -0
  44. miox/readers/ismrmrd.py +421 -0
  45. miox/readers/jcamp.py +324 -0
  46. miox/readers/jeol.py +242 -0
  47. miox/readers/jmrui.py +109 -0
  48. miox/readers/jnifti.py +244 -0
  49. miox/readers/lcmodel_basis.py +116 -0
  50. miox/readers/lcmodel_control.py +76 -0
  51. miox/readers/lcmodel_output.py +257 -0
  52. miox/readers/lcmodel_raw.py +201 -0
  53. miox/readers/loader.py +391 -0
  54. miox/readers/marss_basis.py +70 -0
  55. miox/readers/metaimage.py +172 -0
  56. miox/readers/mrd.py +514 -0
  57. miox/readers/mrs_sim_basis.py +87 -0
  58. miox/readers/mrtrix.py +205 -0
  59. miox/readers/nibabel_image.py +272 -0
  60. miox/readers/nifti_mrs.py +101 -0
  61. miox/readers/nifti_sidecar_mrs.py +111 -0
  62. miox/readers/nifti_zarr.py +80 -0
  63. miox/readers/nmrml.py +227 -0
  64. miox/readers/nmrpipe.py +164 -0
  65. miox/readers/nrrd.py +275 -0
  66. miox/readers/osprey_basis.py +79 -0
  67. miox/readers/osprey_container.py +81 -0
  68. miox/readers/philips_data_list.py +209 -0
  69. miox/readers/philips_dicom_raw.py +137 -0
  70. miox/readers/philips_examcard.py +194 -0
  71. miox/readers/philips_list_raw.py +150 -0
  72. miox/readers/philips_raw_lab.py +603 -0
  73. miox/readers/philips_spar_sdat.py +170 -0
  74. miox/readers/physio.py +453 -0
  75. miox/readers/profit_basis.py +72 -0
  76. miox/readers/pulseq.py +272 -0
  77. miox/readers/pyamares_prior.py +88 -0
  78. miox/readers/pyamares_result.py +57 -0
  79. miox/readers/rf_pulse.py +166 -0
  80. miox/readers/rnmrtk.py +82 -0
  81. miox/readers/rs2d.py +196 -0
  82. miox/readers/siemens_dicom_protocol.py +88 -0
  83. miox/readers/siemens_protocol.py +352 -0
  84. miox/readers/siemens_rda.py +84 -0
  85. miox/readers/siemens_twix.py +277 -0
  86. miox/readers/siemens_twix_raw.py +358 -0
  87. miox/readers/simpson.py +98 -0
  88. miox/readers/simulation.py +269 -0
  89. miox/readers/sivic_ddf.py +145 -0
  90. miox/readers/spant_rds.py +418 -0
  91. miox/readers/sparky.py +104 -0
  92. miox/readers/spinsolve.py +187 -0
  93. miox/readers/spinwizard_basis.py +87 -0
  94. miox/readers/spm_mat.py +110 -0
  95. miox/readers/stimulate.py +156 -0
  96. miox/readers/surfaces.py +262 -0
  97. miox/readers/tarquin_dpt.py +86 -0
  98. miox/readers/tarquin_output.py +182 -0
  99. miox/readers/tecmag.py +111 -0
  100. miox/readers/text_fid.py +66 -0
  101. miox/readers/toppe.py +187 -0
  102. miox/readers/topspin.py +306 -0
  103. miox/readers/tractograms.py +285 -0
  104. miox/readers/transforms.py +301 -0
  105. miox/readers/varian.py +170 -0
  106. miox/readers/varian_fdf.py +244 -0
  107. miox/readers/varian_raw.py +339 -0
  108. miox/readers/vasf.py +104 -0
  109. miox/readers/vespa_prior.py +77 -0
  110. miox/readers/vespa_viff.py +123 -0
  111. miox/readers/vista.py +138 -0
  112. miox/readers/vtk_legacy.py +294 -0
  113. miox/readers/vtk_xml.py +161 -0
  114. miox/readers/yarra.py +60 -0
  115. miox/sniff.py +2044 -0
  116. miox/viewer.py +990 -0
  117. miox/yardl.py +604 -0
  118. pymiox-0.1.0.dist-info/METADATA +87 -0
  119. pymiox-0.1.0.dist-info/RECORD +123 -0
  120. pymiox-0.1.0.dist-info/WHEEL +5 -0
  121. pymiox-0.1.0.dist-info/entry_points.txt +2 -0
  122. pymiox-0.1.0.dist-info/licenses/LICENSE +28 -0
  123. pymiox-0.1.0.dist-info/top_level.txt +1 -0
miox/__init__.py ADDED
@@ -0,0 +1,79 @@
1
+ ####################################################################################################
2
+ # __init__.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
6
+ # #
7
+ # Created: 01/10/26 #
8
+ # #
9
+ # Purpose: Package entry: the version and the top-level API. #
10
+ # #
11
+ ####################################################################################################
12
+
13
+
14
+ #*************#
15
+ # imports #
16
+ #*************#
17
+
18
+ # own
19
+ from miox.data import (
20
+ BasisSet,
21
+ FitResult,
22
+ MRImage,
23
+ MRSData,
24
+ NoiseCovariance,
25
+ Phantom,
26
+ PhysioData,
27
+ PriorSet,
28
+ Protocol,
29
+ Provenance,
30
+ PulseSequence,
31
+ RawData,
32
+ RelaxationData,
33
+ RFPulse,
34
+ Surface,
35
+ Tractogram,
36
+ Transform,
37
+ )
38
+ from miox.readers import IncompleteError, Loader, ReaderError, load
39
+ from miox.sniff import Format, Kind, Sniff, Sniffer, sniff
40
+
41
+ __version__ = "0.1.0"
42
+ __all__ = [
43
+ "BasisSet",
44
+ "FitResult",
45
+ "Format",
46
+ "IncompleteError",
47
+ "Kind",
48
+ "Loader",
49
+ "MRImage",
50
+ "MRSData",
51
+ "NoiseCovariance",
52
+ "Phantom",
53
+ "PhysioData",
54
+ "PriorSet",
55
+ "Protocol",
56
+ "Provenance",
57
+ "PulseSequence",
58
+ "RawData",
59
+ "ReaderError",
60
+ "RelaxationData",
61
+ "RFPulse",
62
+ "Sniff",
63
+ "Sniffer",
64
+ "Surface",
65
+ "Tractogram",
66
+ "Transform",
67
+ "__version__",
68
+ "load",
69
+ "sniff",
70
+ "view",
71
+ ]
72
+
73
+
74
+ def __getattr__(name: str):
75
+ if name == "view": # on first use: matplotlib adds about 0.3 s to every import
76
+ from miox.viewer import view
77
+
78
+ return view
79
+ raise AttributeError(f"module 'miox' has no attribute {name!r}")
miox/__main__.py ADDED
@@ -0,0 +1,22 @@
1
+ ####################################################################################################
2
+ # __main__.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
6
+ # #
7
+ # Created: 12/10/26 #
8
+ # #
9
+ # Purpose: ``python -m miox``: the command line, as the ``miox`` console script. #
10
+ # #
11
+ ####################################################################################################
12
+
13
+
14
+ #*************#
15
+ # imports #
16
+ #*************#
17
+ import sys
18
+
19
+ # own
20
+ from miox.cli import main
21
+
22
+ sys.exit(main())
miox/audit.py ADDED
@@ -0,0 +1,143 @@
1
+ ####################################################################################################
2
+ # audit.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
6
+ # #
7
+ # Created: 01/10/26 #
8
+ # #
9
+ # Purpose: Coverage audit: every field of a file's vendor header, and whether a standard key, some #
10
+ # other key, or nothing is read from it. #
11
+ # #
12
+ ####################################################################################################
13
+
14
+
15
+ #*************#
16
+ # imports #
17
+ #*************#
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ from dataclasses import dataclass
22
+ from enum import StrEnum
23
+ from pathlib import Path
24
+
25
+ # own
26
+ from miox.meta.keys import KeyTable
27
+ from miox.readers import Loader
28
+ from miox.sniff import Format
29
+
30
+
31
+ #**************************************************************************************************#
32
+ # Status #
33
+ #**************************************************************************************************#
34
+ # #
35
+ # What is read from a header field. #
36
+ # #
37
+ #**************************************************************************************************#
38
+ class Status(StrEnum):
39
+ STANDARD = "standard" # a key of the key table
40
+ OTHER = "other" # a key outside the key table, e.g. a user-defined NIfTI-MRS key
41
+ UNUSED = "unused" # nothing
42
+
43
+
44
+ #**************************************************************************************************#
45
+ # Field #
46
+ #**************************************************************************************************#
47
+ # #
48
+ # One field of an audited vendor header. #
49
+ # #
50
+ #**************************************************************************************************#
51
+ @dataclass(frozen=True)
52
+ class Field:
53
+ path: str
54
+ status: Status
55
+ key: str
56
+ """The key read from this field, empty if none."""
57
+ value: str
58
+ """Short preview of the value."""
59
+
60
+
61
+ #**************************************************************************************************#
62
+ # Coverage #
63
+ #**************************************************************************************************#
64
+ # #
65
+ # The audit of one file: all fields of its vendor header, with counts and a Markdown table. #
66
+ # #
67
+ #**************************************************************************************************#
68
+ @dataclass(frozen=True)
69
+ class Coverage:
70
+ path: Path
71
+ format: Format
72
+ fields: tuple[Field, ...]
73
+
74
+ def summary(self) -> dict[Status, int]:
75
+ counts = dict.fromkeys(Status, 0)
76
+ for f in self.fields:
77
+ counts[f.status] += 1
78
+ return counts
79
+
80
+ def to_markdown(self) -> str:
81
+ s = self.summary()
82
+ lines = [
83
+ f"# Coverage: `{self.path.name}` ({self.format})",
84
+ "",
85
+ f"{len(self.fields)} header fields: {s[Status.STANDARD]} read into standard keys, "
86
+ f"{s[Status.OTHER]} into other keys, {s[Status.UNUSED]} unused.",
87
+ "",
88
+ "| field | status | key | value |",
89
+ "|---|---|---|---|",
90
+ ]
91
+ for f in self.fields:
92
+ lines.append(f"| `{f.path}` | {f.status} | {f.key} | {f.value} |")
93
+ return "\n".join(lines) + "\n"
94
+
95
+
96
+ #**************************************************************************************************#
97
+ # Auditor #
98
+ #**************************************************************************************************#
99
+ # #
100
+ # Loads a file and matches its vendor header against the fields its keys were read from. A key #
101
+ # read from a nested block covers every field inside it. #
102
+ # #
103
+ #**************************************************************************************************#
104
+ class Auditor:
105
+ PREVIEW_LIMIT = 60 # characters of a value shown in the table
106
+
107
+ def __init__(self):
108
+ self.loader = Loader()
109
+ self.keys = KeyTable()
110
+
111
+ def audit(self, path: str | Path) -> Coverage:
112
+ """Audit the metadata coverage of one file (all datasets in it)."""
113
+ path = Path(path)
114
+ datasets = self.loader.load(path)
115
+ read_from = {} # vendor field -> key
116
+ for d in datasets:
117
+ read_from.update({field: key for key, field in d.meta.fields.items()})
118
+ fields = []
119
+ for field, value in datasets[0].meta.vendor_fields().items():
120
+ key = self._key(field, read_from)
121
+ if not key:
122
+ status = Status.UNUSED
123
+ elif key in self.keys:
124
+ status = Status.STANDARD
125
+ else:
126
+ status = Status.OTHER
127
+ fields.append(Field(field, status, key, self._preview(value)))
128
+ return Coverage(path, datasets[0].provenance.format, tuple(fields))
129
+
130
+ @staticmethod
131
+ def _key(field: str, read_from: dict[str, str]) -> str:
132
+ parts = field.split("/")
133
+ for n in range(len(parts), 0, -1):
134
+ key = read_from.get("/".join(parts[:n]))
135
+ if key:
136
+ return key
137
+ return ""
138
+
139
+ def _preview(self, value) -> str:
140
+ text = json.dumps(value, default=str)
141
+ if len(text) > self.PREVIEW_LIMIT:
142
+ text = text[: self.PREVIEW_LIMIT - 1] + "…"
143
+ return text.replace("|", "\\|")
miox/cli.py ADDED
@@ -0,0 +1,292 @@
1
+ ####################################################################################################
2
+ # cli.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
6
+ # #
7
+ # Created: 01/10/26 #
8
+ # #
9
+ # Purpose: Console entry point for the ``miox`` command: ``info`` (what a file holds: data #
10
+ # shape, standard keys and their sources), ``sniff`` (format of files or directories), #
11
+ # ``audit`` (metadata coverage table of a file) and ``view`` (a quick look at what a #
12
+ # file holds, in a window or saved to a file). #
13
+ # #
14
+ ####################################################################################################
15
+
16
+
17
+ #*************#
18
+ # imports #
19
+ #*************#
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import io
24
+ import json
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ # own
29
+ from miox import __version__
30
+ from miox.audit import Auditor
31
+ from miox.data import (
32
+ FitResult,
33
+ MRImage,
34
+ MRSData,
35
+ NoiseCovariance,
36
+ Phantom,
37
+ PhysioData,
38
+ PriorSet,
39
+ Protocol,
40
+ PulseSequence,
41
+ RawData,
42
+ RelaxationData,
43
+ RFPulse,
44
+ Surface,
45
+ Tractogram,
46
+ Transform,
47
+ )
48
+ from miox.readers import Loader, ReaderError
49
+ from miox.sniff import Format, Kind, Sniffer
50
+
51
+
52
+ #**************************************************************************************************#
53
+ # CLI #
54
+ #**************************************************************************************************#
55
+ # #
56
+ # The ``miox`` command line: one method per subcommand, each returning the exit status. #
57
+ # #
58
+ #**************************************************************************************************#
59
+ class CLI:
60
+ NOT_SPECTROSCOPY = (Kind.IMAGING, Kind.TRACTOGRAM, Kind.SURFACE, Kind.TRANSFORM, Kind.PHYSIO,
61
+ Kind.SEQUENCE, Kind.PROTOCOL, Kind.PHANTOM, Kind.REPORT)
62
+ def __init__(self):
63
+ self.parser = argparse.ArgumentParser(prog="miox", description="Open any MR file.")
64
+ self.parser.add_argument("--version", action="version", version=f"miox {__version__}")
65
+ commands = self.parser.add_subparsers(dest="command", required=True)
66
+
67
+ info = commands.add_parser("info", help="data shape, standard keys and their sources")
68
+ info.add_argument("paths", nargs="+", type=Path)
69
+ info.add_argument("--json", action="store_true", help="one JSON object per dataset")
70
+ info.set_defaults(func=self.info)
71
+
72
+ sniff = commands.add_parser("sniff", help="identify the format of files or directories")
73
+ sniff.add_argument("paths", nargs="+", type=Path)
74
+ sniff.add_argument("--json", action="store_true", help="one JSON object per line")
75
+ sniff.set_defaults(func=self.sniff)
76
+
77
+ audit = commands.add_parser("audit", help="metadata coverage table for a file (Markdown)")
78
+ audit.add_argument("path", type=Path)
79
+ audit.set_defaults(func=self.audit)
80
+
81
+ view = commands.add_parser("view", help="quick look in a window or saved to a file")
82
+ view.add_argument("paths", nargs="+", type=Path)
83
+ view.add_argument("--save", type=Path, help="save to this file instead of a window "
84
+ "(several datasets: name-1.png, name-2.png, ...)")
85
+ view.set_defaults(func=self.view)
86
+
87
+ preview = commands.add_parser("preview", help="one picture of a spectroscopy file; with "
88
+ "--to x.html the Quick Look page of a file of any kind")
89
+ preview.add_argument("path", type=Path)
90
+ preview.add_argument("--to", default=None, help="file to write (format by suffix: html "
91
+ "for the Quick Look page, pdf, png, svg), or - for PDF on standard "
92
+ "output; default: a window")
93
+ preview.set_defaults(func=self.preview)
94
+
95
+ def run(self, argv: list[str] | None = None) -> int:
96
+ args = self.parser.parse_args(argv)
97
+ return args.func(args)
98
+
99
+ def info(self, args: argparse.Namespace) -> int:
100
+ """Summary of every dataset in every path; status 1 if any path cannot be read."""
101
+ loader = Loader()
102
+ status = 0
103
+ for path in args.paths:
104
+ try:
105
+ datasets = loader.load(path)
106
+ except ReaderError as e:
107
+ print(f"miox info: {e}", file=sys.stderr)
108
+ status = 1
109
+ continue
110
+ for d in datasets:
111
+ print(json.dumps(d.to_dict(), default=str) if args.json else self._describe(d))
112
+ return status
113
+
114
+ def _describe(self, d) -> str:
115
+ lines = [f"{d.provenance.paths[0]}: {d.provenance.format} ({d.provenance.reader})"]
116
+ if isinstance(d, RawData):
117
+ trajectory = ", with trajectory" if d.trajectory is not None else ""
118
+ waveforms = f", {len(d.waveforms)} waveforms" if d.waveforms else ""
119
+ lines.append(
120
+ f" {d.name}: {d.n_readouts} readouts x {d.n_coils} coils x "
121
+ f"{d.data.shape[-1]} samples, {d.data.dtype}{trajectory}{waveforms}"
122
+ )
123
+ elif isinstance(d, MRImage):
124
+ dims = " ".join(f"{name}={n}" for name, n in zip(d.dims, d.shape, strict=True))
125
+ sizes = " x ".join(f"{v:.4g}" for v in d.voxel_sizes)
126
+ lines.append(f" {d.name}: {dims}, {d.data.dtype}, voxels {sizes} mm")
127
+ elif isinstance(d, PriorSet):
128
+ lines.append(f" {d.name}: {len(d.metabolites)} metabolites, {len(d.lines)} lines")
129
+ elif isinstance(d, Protocol):
130
+ where = f" in {' / '.join(d.location)}" if d.location else ""
131
+ lines.append(f" protocol {d.name}{where}")
132
+ elif isinstance(d, Phantom):
133
+ lines.append(f" {d.name}: {d.n_spins} spins, {', '.join(d.properties)}")
134
+ elif isinstance(d, Tractogram):
135
+ lines.append(f" {d.name}: {d.n_streamlines} streamlines, {d.n_points} "
136
+ f"points ({d.space})")
137
+ elif isinstance(d, Surface):
138
+ faces = f", {d.faces.shape[0]} faces" if d.faces is not None else ""
139
+ data = ", ".join(list(d.vertex_data) + list(d.labels))
140
+ lines.append(f" {d.name}: {d.n_vertices} vertices{faces}{', ' + data if data else ''}")
141
+ elif isinstance(d, Transform):
142
+ lines.append(f" {d.name}: {d.kind} transform ({d.convention})")
143
+ elif isinstance(d, PhysioData):
144
+ rate = f" at {d.sampling_frequency:g} Hz" if d.sampling_frequency else ""
145
+ events = "".join(f", {len(v)} {k}" for k, v in d.events.items())
146
+ channels = ", ".join(d.channels)
147
+ lines.append(f" {d.name}: {d.n_samples} samples of {channels}{rate}{events}")
148
+ elif isinstance(d, RFPulse):
149
+ duration = f", {d.duration * 1e3:.4g} ms" if d.duration is not None else ""
150
+ lines.append(f" {d.name}: RF pulse, {d.n_samples} samples{duration}, "
151
+ f"amplitude in {d.amplitude_unit}")
152
+ elif isinstance(d, RelaxationData):
153
+ curves = f" x {d.n_curves} curves" if d.n_curves > 1 else ""
154
+ lines.append(f" {d.name}: {d.sequence} relaxation, {d.n_points} points{curves}")
155
+ elif isinstance(d, NoiseCovariance):
156
+ lines.append(
157
+ f" {d.name}: noise covariance of {d.n_coils} channels, {d.sample_count} samples"
158
+ )
159
+ elif isinstance(d, PulseSequence):
160
+ events = ", ".join(f"{len(t)} {n}" for n, t in d.events.items())
161
+ duration = f", {d.duration:.4g} s" if d.duration is not None else ""
162
+ lines.append(f" {d.name}: Pulseq {d.version}, {d.n_blocks} blocks{duration}; {events}")
163
+ elif isinstance(d, FitResult):
164
+ voxel = f", voxel {d.voxel}" if d.voxel else ""
165
+ curves = f", curves: {', '.join(d.curves)}" if d.curves else ""
166
+ lines.append(f" {d.name}: {len(d.rows)} metabolites{voxel}{curves}")
167
+ lines.extend(f" {name}: {value}" for name, value in d.diagnostics.items())
168
+ else:
169
+ dims = " ".join(f"{name}={n}" for name, n in zip(d.dims, d.shape, strict=True))
170
+ width = "" if d.spectral_width is None else f" ({d.spectral_width:.6g} Hz)"
171
+ lines += [f" {d.name}: {dims}", f" {d.acquisition()}{width}, {d.n_points} points"]
172
+ width = max((len(key) for key in d.meta), default=0)
173
+ for key, value in d.meta.items():
174
+ text = json.dumps(value, default=str)
175
+ if len(text) > 50:
176
+ text = text[:49] + "…"
177
+ lines.append(f" {key:<{width}} {text:<50} {d.meta.source[key]}")
178
+ lines.append(f" vendor header: {len(d.meta.vendor_fields())} fields")
179
+ lines.extend(f" note: {note}" for note in d.provenance.notes)
180
+ return "\n".join(lines)
181
+
182
+ def sniff(self, args: argparse.Namespace) -> int:
183
+ """One line (or JSON object) per path; status 1 if any path is unknown."""
184
+ sniffer = Sniffer()
185
+ status = 0
186
+ for path in args.paths:
187
+ result = sniffer.sniff(path)
188
+ if result.format == Format.UNKNOWN:
189
+ status = 1
190
+ print(json.dumps(result.to_dict()) if args.json else result)
191
+ return status
192
+
193
+ def audit(self, args: argparse.Namespace) -> int:
194
+ """Coverage table as Markdown; status 2 if the file cannot be read."""
195
+ try:
196
+ print(Auditor().audit(args.path).to_markdown(), end="")
197
+ except ReaderError as e:
198
+ print(f"miox audit: {e}", file=sys.stderr)
199
+ return 2
200
+ return 0
201
+
202
+ def view(self, args: argparse.Namespace) -> int:
203
+ """Every dataset that has a view; status 1 if a path cannot be read or nothing is shown."""
204
+ from miox.viewer import VIEWS, show, view # matplotlib only when viewing
205
+
206
+ loader = Loader()
207
+ status, datasets = 0, []
208
+ for path in args.paths:
209
+ try:
210
+ loaded = loader.load(path)
211
+ except ReaderError as e:
212
+ print(f"miox view: {e}", file=sys.stderr)
213
+ status = 1
214
+ continue
215
+ for d in loaded:
216
+ if isinstance(d, tuple(VIEWS)):
217
+ datasets.append(d)
218
+ else:
219
+ print(f"miox view: no view for {type(d).__name__} yet ({d.name})",
220
+ file=sys.stderr)
221
+ if not datasets:
222
+ return 1
223
+ if args.save is None:
224
+ show(datasets)
225
+ elif len(datasets) == 1:
226
+ view(datasets[0], args.save)
227
+ else:
228
+ for n, d in enumerate(datasets, 1):
229
+ view(d, args.save.with_name(f"{args.save.stem}-{n}{args.save.suffix}"))
230
+ return status
231
+
232
+
233
+ def preview(self, args: argparse.Namespace) -> int:
234
+ """Status 2 when the file holds no spectroscopy data (files whose format cannot hold any
235
+ are declined before loading), 1 when it cannot be read."""
236
+ from miox.preview import Preview, preview # matplotlib only when previewing
237
+
238
+ sniffed = Sniffer().sniff(args.path)
239
+ page = bool(args.to) and str(args.to).endswith(".html") # the Quick Look page: every kind
240
+ declined = {Kind.REPORT} if page else set(self.NOT_SPECTROSCOPY)
241
+ if sniffed.format == Format.UNKNOWN or sniffed.kind in declined:
242
+ what = "has no page" if page else "is not spectroscopy"
243
+ print(f"miox preview: {args.path.name} {what} ({sniffed.format})", file=sys.stderr)
244
+ return 2
245
+ if page:
246
+ from miox.quicklook import load, write
247
+
248
+ try:
249
+ datasets, notes = load(args.path)
250
+ write(datasets, args.path.name, args.to, source=args.path, notes=notes)
251
+ except (ReaderError, ValueError) as e:
252
+ print(f"miox preview: {e}", file=sys.stderr)
253
+ return 1 if isinstance(e, ReaderError) else 2
254
+ return 0
255
+ window = None
256
+ if args.to is None:
257
+ import matplotlib.pyplot as plt
258
+
259
+ window = plt.figure(figsize=Preview.SIZE)
260
+ try:
261
+ datasets = [d for d in Loader().load(args.path) if isinstance(d, MRSData)]
262
+ except ReaderError as e:
263
+ print(f"miox preview: {e}", file=sys.stderr)
264
+ return 1
265
+ if not datasets:
266
+ print(f"miox preview: {args.path.name} holds no spectroscopy data", file=sys.stderr)
267
+ return 2
268
+ figure = preview(datasets, title=args.path.name, figure=window)
269
+ if window is not None:
270
+ plt.show()
271
+ elif args.to == "-":
272
+ buffer = io.BytesIO()
273
+ figure.savefig(buffer, format="pdf")
274
+ sys.stdout.buffer.write(buffer.getvalue())
275
+ else:
276
+ figure.savefig(args.to, dpi=150)
277
+ return 0
278
+
279
+
280
+ #**************************************************************************************************#
281
+ # main #
282
+ #**************************************************************************************************#
283
+ # #
284
+ # Console-script entry point. #
285
+ # #
286
+ #**************************************************************************************************#
287
+ def main(argv: list[str] | None = None) -> int:
288
+ return CLI().run(argv)
289
+
290
+
291
+ if __name__ == "__main__":
292
+ sys.exit(main())