chromparser 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.
- chromparser/__init__.py +183 -0
- chromparser/__main__.py +7 -0
- chromparser/_netcdf3.py +548 -0
- chromparser/_util.py +98 -0
- chromparser/cli.py +192 -0
- chromparser/errors.py +49 -0
- chromparser/hdf5.py +428 -0
- chromparser/model.py +254 -0
- chromparser/py.typed +0 -0
- chromparser/sniff.py +80 -0
- chromparser/vendors/__init__.py +55 -0
- chromparser/vendors/agilent/__init__.py +17 -0
- chromparser/vendors/agilent/ch.py +344 -0
- chromparser/vendors/agilent/dot_d.py +69 -0
- chromparser/vendors/agilent/dx.py +147 -0
- chromparser/vendors/andi/__init__.py +15 -0
- chromparser/vendors/andi/chrom.py +181 -0
- chromparser/vendors/andi/common.py +232 -0
- chromparser/vendors/andi/ms.py +358 -0
- chromparser/vendors/andi/write.py +320 -0
- chromparser/vendors/chromeleon/__init__.py +19 -0
- chromparser/vendors/chromeleon/archive.py +310 -0
- chromparser/vendors/chromeleon/raw.py +407 -0
- chromparser/vendors/chromeleon/sequence.py +177 -0
- chromparser/vendors/chromeleon/txt.py +319 -0
- chromparser/vendors/shimadzu/__init__.py +8 -0
- chromparser/vendors/shimadzu/gcd.py +146 -0
- chromparser/vendors/shimadzu/ole.py +84 -0
- chromparser/vendors/shimadzu/qgd.py +196 -0
- chromparser/vendors/thermo/__init__.py +7 -0
- chromparser/vendors/thermo/raw.py +377 -0
- chromparser-0.1.0.dist-info/METADATA +162 -0
- chromparser-0.1.0.dist-info/RECORD +36 -0
- chromparser-0.1.0.dist-info/WHEEL +4 -0
- chromparser-0.1.0.dist-info/entry_points.txt +3 -0
- chromparser-0.1.0.dist-info/licenses/LICENSE +51 -0
chromparser/model.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Data model returned by all readers: `Injection`, `Signal` and `MSData`.
|
|
2
|
+
|
|
3
|
+
See `docs/model.md` for the reader contract. Metadata values are JSON types
|
|
4
|
+
(str, int, float, bool, lists and dicts of them); timestamps in metadata are
|
|
5
|
+
ISO 8601 strings. Unknown values are left out rather than stored as `None` or
|
|
6
|
+
`""`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field, fields
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
SPECTRUM_TYPES = ("centroid", "profile")
|
|
18
|
+
|
|
19
|
+
#: Bit of `MSData.flags` set for peaks recorded with a saturated detector.
|
|
20
|
+
SATURATED = 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _equal(a: Any, b: Any) -> bool:
|
|
24
|
+
"""Compare values; arrays must agree in dtype, shape and content."""
|
|
25
|
+
if isinstance(a, np.ndarray) or isinstance(b, np.ndarray):
|
|
26
|
+
if not (isinstance(a, np.ndarray) and isinstance(b, np.ndarray)):
|
|
27
|
+
return False
|
|
28
|
+
return (
|
|
29
|
+
a.dtype == b.dtype
|
|
30
|
+
and a.shape == b.shape
|
|
31
|
+
and bool(np.array_equal(a, b, equal_nan=a.dtype.kind in "fc"))
|
|
32
|
+
)
|
|
33
|
+
if isinstance(a, dict) and isinstance(b, dict):
|
|
34
|
+
return a.keys() == b.keys() and all(_equal(a[k], b[k]) for k in a)
|
|
35
|
+
if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
|
|
36
|
+
return len(a) == len(b) and all(map(_equal, a, b))
|
|
37
|
+
return bool(a == b)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _ArrayEq:
|
|
41
|
+
"""Dataclass mixin: `==` compares all fields, arrays with `_equal`."""
|
|
42
|
+
|
|
43
|
+
def __eq__(self, other: object) -> bool:
|
|
44
|
+
if type(other) is not type(self):
|
|
45
|
+
return NotImplemented
|
|
46
|
+
return all(
|
|
47
|
+
_equal(getattr(self, f.name), getattr(other, f.name))
|
|
48
|
+
for f in fields(self) # type: ignore[arg-type]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__hash__ = None # type: ignore[assignment]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(eq=False)
|
|
55
|
+
class Signal(_ArrayEq):
|
|
56
|
+
"""One detector trace of one injection (FID, TCD, single-wavelength UV, ...).
|
|
57
|
+
|
|
58
|
+
Attributes
|
|
59
|
+
----------
|
|
60
|
+
channel : str
|
|
61
|
+
Channel name, e.g. ``"FID_left"``.
|
|
62
|
+
time : numpy.ndarray
|
|
63
|
+
Retention time in minutes (float64).
|
|
64
|
+
values : numpy.ndarray
|
|
65
|
+
Signal values in `unit`, with the dtype the file stores.
|
|
66
|
+
unit : str or None
|
|
67
|
+
Signal unit, e.g. ``"pA"``; `None` when the file does not say.
|
|
68
|
+
acquisition_start : datetime or None
|
|
69
|
+
Start of the data acquisition (timezone-aware, UTC).
|
|
70
|
+
metadata : dict
|
|
71
|
+
Further metadata, e.g. ``detector``, ``driver``.
|
|
72
|
+
sampling_interval : float or None
|
|
73
|
+
Time between two points in seconds, for regularly sampled signals.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
channel: str
|
|
77
|
+
time: np.ndarray
|
|
78
|
+
values: np.ndarray
|
|
79
|
+
unit: str | None = None
|
|
80
|
+
acquisition_start: datetime | None = None
|
|
81
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
82
|
+
sampling_interval: float | None = None
|
|
83
|
+
|
|
84
|
+
def __len__(self) -> int:
|
|
85
|
+
return len(self.time)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(eq=False)
|
|
89
|
+
class MSData(_ArrayEq):
|
|
90
|
+
"""Mass spectra of one injection, stored back to back (CSR layout).
|
|
91
|
+
|
|
92
|
+
The peaks of the scan at index ``i`` are
|
|
93
|
+
``mz[peak_offset[i]:peak_offset[i + 1]]`` (likewise `intensity` and
|
|
94
|
+
`flags`). Use `spectrum` for one scan and `xic` for extracted-ion
|
|
95
|
+
chromatograms.
|
|
96
|
+
|
|
97
|
+
Attributes
|
|
98
|
+
----------
|
|
99
|
+
name : str
|
|
100
|
+
Name of the MS data, e.g. ``"MSDevice"``.
|
|
101
|
+
scan_number : numpy.ndarray
|
|
102
|
+
Scan numbers (int32) as the file stores them (Thermo counts from 1,
|
|
103
|
+
Shimadzu from 0).
|
|
104
|
+
retention_time : numpy.ndarray
|
|
105
|
+
Retention time of each scan in minutes (float64).
|
|
106
|
+
tic : numpy.ndarray
|
|
107
|
+
Total ion current of each scan (float64).
|
|
108
|
+
base_peak_mz, base_peak_intensity : numpy.ndarray or None
|
|
109
|
+
m/z and intensity of the most intense peak of each scan; `None` when
|
|
110
|
+
the file has no such field. When computed by the reader, their names
|
|
111
|
+
are listed in ``metadata["computed"]``.
|
|
112
|
+
filters : list of str or None
|
|
113
|
+
Scan filter of each scan, e.g. ``"+ c EI Full ms [33.00-500.00]"``.
|
|
114
|
+
mz : numpy.ndarray
|
|
115
|
+
m/z of all peaks, as stored (float32 for Thermo, float64 for Shimadzu).
|
|
116
|
+
intensity : numpy.ndarray
|
|
117
|
+
Intensity of all peaks, as stored (float32 for Thermo, uint32 for
|
|
118
|
+
Shimadzu).
|
|
119
|
+
peak_offset : numpy.ndarray
|
|
120
|
+
Start of each scan's peaks, plus the total number of peaks (int64,
|
|
121
|
+
one more entry than scans).
|
|
122
|
+
acquisition_start : datetime or None
|
|
123
|
+
Start of the MS acquisition (timezone-aware, UTC).
|
|
124
|
+
metadata : dict
|
|
125
|
+
Further metadata, e.g. ``instrument_model``, ``ionisation``.
|
|
126
|
+
ms_level : numpy.ndarray or None
|
|
127
|
+
MS level of each scan (uint8).
|
|
128
|
+
flags : numpy.ndarray or None
|
|
129
|
+
Per-peak flags (uint8); bit 0 (`SATURATED`) marks a saturated detector.
|
|
130
|
+
spectrum_type : str
|
|
131
|
+
``"centroid"`` or ``"profile"``.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
name: str
|
|
135
|
+
scan_number: np.ndarray
|
|
136
|
+
retention_time: np.ndarray
|
|
137
|
+
tic: np.ndarray
|
|
138
|
+
base_peak_mz: np.ndarray | None
|
|
139
|
+
base_peak_intensity: np.ndarray | None
|
|
140
|
+
filters: list[str] | None
|
|
141
|
+
mz: np.ndarray
|
|
142
|
+
intensity: np.ndarray
|
|
143
|
+
peak_offset: np.ndarray
|
|
144
|
+
acquisition_start: datetime | None = None
|
|
145
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
146
|
+
ms_level: np.ndarray | None = None
|
|
147
|
+
flags: np.ndarray | None = None
|
|
148
|
+
spectrum_type: str = "centroid"
|
|
149
|
+
|
|
150
|
+
def __post_init__(self) -> None:
|
|
151
|
+
n = len(self.scan_number)
|
|
152
|
+
per_scan = {
|
|
153
|
+
"retention_time": self.retention_time,
|
|
154
|
+
"tic": self.tic,
|
|
155
|
+
"base_peak_mz": self.base_peak_mz,
|
|
156
|
+
"base_peak_intensity": self.base_peak_intensity,
|
|
157
|
+
"filters": self.filters,
|
|
158
|
+
"ms_level": self.ms_level,
|
|
159
|
+
}
|
|
160
|
+
for name, values in per_scan.items():
|
|
161
|
+
if values is not None and len(values) != n:
|
|
162
|
+
raise ValueError(f"MSData.{name} has {len(values)} entries, not {n}.")
|
|
163
|
+
if len(self.peak_offset) != n + 1:
|
|
164
|
+
raise ValueError("MSData.peak_offset must have one entry more than scans.")
|
|
165
|
+
n_peaks = int(self.peak_offset[-1])
|
|
166
|
+
for name in ("mz", "intensity", "flags"):
|
|
167
|
+
values = getattr(self, name)
|
|
168
|
+
if values is not None and len(values) != n_peaks:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"MSData.{name} has {len(values)} peaks, peak_offset says {n_peaks}."
|
|
171
|
+
)
|
|
172
|
+
if self.spectrum_type not in SPECTRUM_TYPES:
|
|
173
|
+
raise ValueError(f"Unknown spectrum type {self.spectrum_type!r}.")
|
|
174
|
+
|
|
175
|
+
def __len__(self) -> int:
|
|
176
|
+
return len(self.scan_number)
|
|
177
|
+
|
|
178
|
+
def spectrum(self, scan_number: int) -> tuple[np.ndarray, np.ndarray]:
|
|
179
|
+
"""Return the m/z and intensity arrays of one scan.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
scan_number : int
|
|
184
|
+
Scan number as stored in `scan_number`.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
mz, intensity : numpy.ndarray
|
|
189
|
+
Views into `mz` and `intensity`.
|
|
190
|
+
|
|
191
|
+
Raises
|
|
192
|
+
------
|
|
193
|
+
KeyError
|
|
194
|
+
If there is no scan with this number.
|
|
195
|
+
"""
|
|
196
|
+
index = int(np.searchsorted(self.scan_number, scan_number))
|
|
197
|
+
if index == len(self) or self.scan_number[index] != scan_number:
|
|
198
|
+
raise KeyError(f"No scan {scan_number}.")
|
|
199
|
+
peaks = slice(self.peak_offset[index], self.peak_offset[index + 1])
|
|
200
|
+
return self.mz[peaks], self.intensity[peaks]
|
|
201
|
+
|
|
202
|
+
def xic(self, mz: float, tolerance: float = 0.5) -> np.ndarray:
|
|
203
|
+
"""Extracted-ion chromatogram.
|
|
204
|
+
|
|
205
|
+
Sums the intensity of each scan within
|
|
206
|
+
``mz - tolerance <= m/z <= mz + tolerance``.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
mz : float
|
|
211
|
+
Centre of the m/z window.
|
|
212
|
+
tolerance : float
|
|
213
|
+
Half width of the window.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
numpy.ndarray
|
|
218
|
+
One intensity (float64) per scan, to plot against `retention_time`.
|
|
219
|
+
"""
|
|
220
|
+
selected = (self.mz >= mz - tolerance) & (self.mz <= mz + tolerance)
|
|
221
|
+
scan_index = np.repeat(np.arange(len(self)), np.diff(self.peak_offset))
|
|
222
|
+
return np.bincount(
|
|
223
|
+
scan_index[selected],
|
|
224
|
+
weights=self.intensity[selected].astype(np.float64),
|
|
225
|
+
minlength=len(self),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass(eq=False)
|
|
230
|
+
class Injection(_ArrayEq):
|
|
231
|
+
"""One injection with its signals and MS data.
|
|
232
|
+
|
|
233
|
+
Attributes
|
|
234
|
+
----------
|
|
235
|
+
name : str
|
|
236
|
+
Injection name (``""`` when unnamed).
|
|
237
|
+
inject_time : datetime or None
|
|
238
|
+
Injection time as local time of the instrument: timezone-aware when the
|
|
239
|
+
file stores the UTC offset, naive when it stores only the wall time
|
|
240
|
+
(Agilent ``.ch``, some Chromeleon text exports), `None` when unknown.
|
|
241
|
+
signals : dict of str to Signal
|
|
242
|
+
Signals by channel name.
|
|
243
|
+
ms : dict of str to MSData
|
|
244
|
+
MS data by name.
|
|
245
|
+
metadata : dict
|
|
246
|
+
Further metadata, e.g. ``sample_name``, ``sequence``,
|
|
247
|
+
``instrument_method``.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
name: str
|
|
251
|
+
inject_time: datetime | None = None
|
|
252
|
+
signals: dict[str, Signal] = field(default_factory=dict)
|
|
253
|
+
ms: dict[str, MSData] = field(default_factory=dict)
|
|
254
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
chromparser/py.typed
ADDED
|
File without changes
|
chromparser/sniff.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Detect the format of a file from its content.
|
|
2
|
+
|
|
3
|
+
Magic bytes, OLE2 storages, zip members and the content of `.D` folders
|
|
4
|
+
decide; the file extension is only used in error messages.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ._util import head
|
|
12
|
+
from .errors import UnsupportedFormat
|
|
13
|
+
|
|
14
|
+
HDF5 = "hdf5"
|
|
15
|
+
|
|
16
|
+
_HINTS = {
|
|
17
|
+
".cmbx": "chromeleon-cmbx",
|
|
18
|
+
".dx": "agilent-dx",
|
|
19
|
+
".raw": "chromeleon-raw or thermo-raw",
|
|
20
|
+
".qgd": "shimadzu-qgd",
|
|
21
|
+
".gcd": "shimadzu-gcd",
|
|
22
|
+
".d": "agilent-d",
|
|
23
|
+
".ch": "agilent-ch",
|
|
24
|
+
".txt": "chromeleon-txt",
|
|
25
|
+
".cdf": "andi-ms or andi-chrom",
|
|
26
|
+
".h5": HDF5,
|
|
27
|
+
".hdf5": HDF5,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_RECOGNISED_UNSUPPORTED = {
|
|
31
|
+
b"CpAu": "a Chromeleon audit trail (CpAu), which holds no chromatogram",
|
|
32
|
+
b"CDF\x01": "a netCDF file without ANDI MS or chromatography data",
|
|
33
|
+
b"CDF\x02": "a netCDF file without ANDI MS or chromatography data",
|
|
34
|
+
b"CDF\x05": "a CDF-5 netCDF file, which chromparser does not read",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def sniff(path: str | Path) -> str:
|
|
39
|
+
"""Return the format id of a file or folder.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
path : str or Path
|
|
44
|
+
File or folder (Agilent `.D`).
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
str
|
|
49
|
+
One of the ids in `chromparser.vendors.FORMATS` (e.g.
|
|
50
|
+
``"shimadzu-qgd"``), or ``"hdf5"`` for files written by chromparser
|
|
51
|
+
or cmbx-reader.
|
|
52
|
+
|
|
53
|
+
Raises
|
|
54
|
+
------
|
|
55
|
+
FileNotFoundError
|
|
56
|
+
If `path` does not exist.
|
|
57
|
+
UnsupportedFormat
|
|
58
|
+
If the content is not a supported format.
|
|
59
|
+
"""
|
|
60
|
+
from .hdf5 import sniff_hdf5
|
|
61
|
+
from .vendors import FORMATS
|
|
62
|
+
|
|
63
|
+
path = Path(path)
|
|
64
|
+
if not path.exists():
|
|
65
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
66
|
+
if sniff_hdf5(path):
|
|
67
|
+
return HDF5
|
|
68
|
+
for fmt in FORMATS:
|
|
69
|
+
if fmt.sniff(path):
|
|
70
|
+
return fmt.id
|
|
71
|
+
if path.is_file():
|
|
72
|
+
start = head(path, 8)
|
|
73
|
+
for magic, what in _RECOGNISED_UNSUPPORTED.items():
|
|
74
|
+
if start.startswith(magic):
|
|
75
|
+
raise UnsupportedFormat(f"{path.name} is {what}.")
|
|
76
|
+
hint = _HINTS.get(path.suffix.lower())
|
|
77
|
+
detail = f" (the extension suggests {hint})" if hint else ""
|
|
78
|
+
raise UnsupportedFormat(
|
|
79
|
+
f"{path.name} is not a chromatography file chromparser can read{detail}."
|
|
80
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Readers of the vendor formats and the registry used by `chromparser.read`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..model import Injection
|
|
10
|
+
from .agilent import (
|
|
11
|
+
read_agilent_ch,
|
|
12
|
+
read_agilent_d,
|
|
13
|
+
read_agilent_dx,
|
|
14
|
+
sniff_agilent_ch,
|
|
15
|
+
sniff_agilent_d,
|
|
16
|
+
sniff_agilent_dx,
|
|
17
|
+
)
|
|
18
|
+
from .andi import read_andi_chrom, read_andi_ms, sniff_andi_chrom, sniff_andi_ms
|
|
19
|
+
from .chromeleon import (
|
|
20
|
+
read_chromeleon_raw,
|
|
21
|
+
read_chromeleon_txt,
|
|
22
|
+
read_cmbx,
|
|
23
|
+
sniff_chromeleon_raw,
|
|
24
|
+
sniff_chromeleon_txt,
|
|
25
|
+
sniff_cmbx,
|
|
26
|
+
)
|
|
27
|
+
from .shimadzu import read_gcd, read_qgd, sniff_gcd, sniff_qgd
|
|
28
|
+
from .thermo import read_thermo_raw, sniff_thermo_raw
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Format:
|
|
33
|
+
"""A file format: id, description, content sniffer and reader."""
|
|
34
|
+
|
|
35
|
+
id: str
|
|
36
|
+
description: str
|
|
37
|
+
sniff: Callable[[str | Path], bool]
|
|
38
|
+
read: Callable[..., list[Injection]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#: Vendor formats in the order they are tried by `sniff`: formats with a
|
|
42
|
+
#: specific signature first, the heuristic ones (Agilent `.ch`, text) last.
|
|
43
|
+
FORMATS = [
|
|
44
|
+
Format("chromeleon-cmbx", "Chromeleon 7 archive (.cmbx)", sniff_cmbx, read_cmbx),
|
|
45
|
+
Format("agilent-dx", "Agilent OpenLab archive (.dx)", sniff_agilent_dx, read_agilent_dx),
|
|
46
|
+
Format("chromeleon-raw", "Chromeleon 7 raw signal file (.raw)", sniff_chromeleon_raw, read_chromeleon_raw),
|
|
47
|
+
Format("thermo-raw", "Thermo Finnigan MS file (.raw)", sniff_thermo_raw, read_thermo_raw),
|
|
48
|
+
Format("shimadzu-qgd", "Shimadzu GCMSsolution GC-MS file (.qgd)", sniff_qgd, read_qgd),
|
|
49
|
+
Format("shimadzu-gcd", "Shimadzu GC file (.gcd)", sniff_gcd, read_gcd),
|
|
50
|
+
Format("andi-ms", "ANDI-MS netCDF GC-MS file (.cdf)", sniff_andi_ms, read_andi_ms),
|
|
51
|
+
Format("andi-chrom", "ANDI chromatography (AIA) netCDF file (.cdf)", sniff_andi_chrom, read_andi_chrom),
|
|
52
|
+
Format("agilent-d", "Agilent data folder (.D)", sniff_agilent_d, read_agilent_d),
|
|
53
|
+
Format("agilent-ch", "Agilent chromatogram (.ch)", sniff_agilent_ch, read_agilent_ch),
|
|
54
|
+
Format("chromeleon-txt", "Chromeleon text export (.txt)", sniff_chromeleon_txt, read_chromeleon_txt),
|
|
55
|
+
] # fmt: skip
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Agilent ChemStation / OpenLab: `.ch` files, `.D` folders and `.dx` archives."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .ch import decode_ch, read_agilent_ch, sniff_agilent_ch
|
|
6
|
+
from .dot_d import read_agilent_d, sniff_agilent_d
|
|
7
|
+
from .dx import read_agilent_dx, sniff_agilent_dx
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"decode_ch",
|
|
11
|
+
"read_agilent_ch",
|
|
12
|
+
"read_agilent_d",
|
|
13
|
+
"read_agilent_dx",
|
|
14
|
+
"sniff_agilent_ch",
|
|
15
|
+
"sniff_agilent_d",
|
|
16
|
+
"sniff_agilent_dx",
|
|
17
|
+
]
|