pychemstation 0.10.3__py3-none-any.whl → 0.10.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 (31) hide show
  1. pychemstation/__init__.py +1 -1
  2. pychemstation/analysis/__init__.py +1 -6
  3. pychemstation/analysis/base_spectrum.py +7 -7
  4. pychemstation/{utils → analysis}/chromatogram.py +24 -4
  5. pychemstation/analysis/process_report.py +189 -90
  6. pychemstation/control/__init__.py +5 -2
  7. pychemstation/control/controllers/__init__.py +2 -7
  8. pychemstation/control/controllers/comm.py +56 -32
  9. pychemstation/control/controllers/devices/device.py +59 -24
  10. pychemstation/control/controllers/devices/injector.py +33 -10
  11. pychemstation/control/controllers/tables/__init__.py +4 -0
  12. pychemstation/control/controllers/tables/method.py +241 -151
  13. pychemstation/control/controllers/tables/sequence.py +226 -107
  14. pychemstation/control/controllers/tables/table.py +216 -132
  15. pychemstation/control/hplc.py +89 -75
  16. pychemstation/generated/__init__.py +0 -2
  17. pychemstation/generated/pump_method.py +15 -19
  18. pychemstation/utils/injector_types.py +1 -1
  19. pychemstation/utils/macro.py +11 -10
  20. pychemstation/utils/method_types.py +2 -1
  21. pychemstation/utils/parsing.py +0 -11
  22. pychemstation/utils/sequence_types.py +2 -3
  23. pychemstation/utils/spec_utils.py +2 -3
  24. pychemstation/utils/table_types.py +10 -9
  25. pychemstation/utils/tray_types.py +45 -36
  26. {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/METADATA +5 -4
  27. pychemstation-0.10.5.dist-info/RECORD +36 -0
  28. pychemstation/control/controllers/tables/ms.py +0 -21
  29. pychemstation-0.10.3.dist-info/RECORD +0 -37
  30. {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/WHEEL +0 -0
  31. {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/licenses/LICENSE +0 -0
pychemstation/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """
2
2
  .. include:: ../README.md
3
- """
3
+ """
@@ -1,9 +1,4 @@
1
- from .base_spectrum import AbstractSpectrum
2
1
  from .process_report import CSVProcessor
3
2
  from .process_report import TXTProcessor
4
3
 
5
- __all__ = [
6
- 'AbstractSpectrum',
7
- 'CSVProcessor',
8
- 'TXTProcessor'
9
- ]
4
+ __all__ = ["CSVProcessor", "TXTProcessor"]
@@ -197,10 +197,10 @@ class AbstractSpectrum(ABC):
197
197
  return (self.x.copy()[full_mask], self.y.copy()[full_mask])
198
198
 
199
199
  def show_spectrum(
200
- self,
201
- filename=None,
202
- title=None,
203
- label=None,
200
+ self,
201
+ filename=None,
202
+ title=None,
203
+ label=None,
204
204
  ):
205
205
  """Plots the spectral data using matplotlib.pyplot module.
206
206
 
@@ -249,7 +249,7 @@ class AbstractSpectrum(ABC):
249
249
  os.makedirs(path, exist_ok=True)
250
250
  fig.savefig(os.path.join(path, f"{filename}.png"), dpi=150)
251
251
 
252
- def find_peaks(self, threshold=1, min_width=.1, min_dist=None, area=None):
252
+ def find_peaks(self, threshold=1, min_width=0.1, min_dist=None, area=None):
253
253
  """Finds all peaks above the threshold with at least min_width width.
254
254
 
255
255
  Args:
@@ -385,12 +385,12 @@ class AbstractSpectrum(ABC):
385
385
 
386
386
  if rule == "trapz":
387
387
  return integrate.trapz(
388
- self.y[left_idx: right_idx + 1], self.x[left_idx: right_idx + 1]
388
+ self.y[left_idx : right_idx + 1], self.x[left_idx : right_idx + 1]
389
389
  )
390
390
 
391
391
  elif rule == "simps":
392
392
  return integrate.simps(
393
- self.y[left_idx: right_idx + 1], self.x[left_idx: right_idx + 1]
393
+ self.y[left_idx : right_idx + 1], self.x[left_idx : right_idx + 1]
394
394
  )
395
395
 
396
396
  else:
@@ -3,11 +3,13 @@
3
3
  import os
4
4
  import time
5
5
  from dataclasses import dataclass
6
+ from typing import Dict
6
7
 
7
8
  import numpy as np
8
9
 
9
- from ..analysis import AbstractSpectrum
10
- from .parsing import CHFile
10
+
11
+ from ..utils.parsing import CHFile
12
+ from ..analysis.base_spectrum import AbstractSpectrum
11
13
 
12
14
  ACQUISITION_PARAMETERS = "acq.txt"
13
15
 
@@ -36,12 +38,11 @@ class AgilentHPLCChromatogram(AbstractSpectrum):
36
38
  }
37
39
 
38
40
  def __init__(self, path=None, autosaving=False):
39
-
40
41
  if path is not None:
41
42
  os.makedirs(path, exist_ok=True)
42
43
  self.path = path
43
44
  else:
44
- self.path = os.path.join(".", "hplc_data")
45
+ self.path = os.path.join("../utils", "hplc_data")
45
46
  os.makedirs(self.path, exist_ok=True)
46
47
 
47
48
  super().__init__(path=path, autosaving=autosaving)
@@ -114,3 +115,22 @@ class AgilentChannelChromatogramData:
114
115
  F: AgilentHPLCChromatogram
115
116
  G: AgilentHPLCChromatogram
116
117
  H: AgilentHPLCChromatogram
118
+
119
+ @classmethod
120
+ def from_dict(cls, chroms: Dict[str, AgilentHPLCChromatogram]):
121
+ keys = chroms.keys()
122
+ class_keys = vars(AgilentChannelChromatogramData)["__annotations__"].keys()
123
+ if set(class_keys) == set(keys):
124
+ return AgilentChannelChromatogramData(
125
+ A=chroms["A"],
126
+ B=chroms["B"],
127
+ C=chroms["C"],
128
+ D=chroms["D"],
129
+ E=chroms["E"],
130
+ F=chroms["F"],
131
+ G=chroms["G"],
132
+ H=chroms["H"],
133
+ )
134
+ else:
135
+ err = f"{keys} don't match {class_keys}"
136
+ raise KeyError(err)
@@ -1,10 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  import abc
2
4
  import os
3
5
  import re
4
6
  from abc import abstractmethod
5
7
  from dataclasses import dataclass
6
8
  from enum import Enum
7
- from typing import AnyStr, Dict, List, Optional, Pattern
9
+ from typing import AnyStr, Dict, List, Optional, Pattern, Union
8
10
 
9
11
  import pandas as pd
10
12
  from aghplctools.ingestion.text import (
@@ -15,10 +17,11 @@ from aghplctools.ingestion.text import (
15
17
  _signal_table_re,
16
18
  chunk_string,
17
19
  )
20
+ from pandas.errors import EmptyDataError
18
21
  from result import Err, Ok, Result
19
22
 
20
- from pychemstation.utils.chromatogram import AgilentHPLCChromatogram
21
- from pychemstation.utils.tray_types import FiftyFourVialPlate, Tray
23
+ from ..analysis.chromatogram import AgilentHPLCChromatogram
24
+ from ..utils.tray_types import FiftyFourVialPlate, Tray
22
25
 
23
26
 
24
27
  @dataclass
@@ -43,7 +46,7 @@ class Signals:
43
46
  class AgilentReport:
44
47
  vial_location: Optional[Tray]
45
48
  signals: List[Signals]
46
- solvents: Optional[Dict[AnyStr, AnyStr]]
49
+ solvents: Optional[Dict[str, str]]
47
50
 
48
51
 
49
52
  class ReportType(Enum):
@@ -69,6 +72,37 @@ class CSVProcessor(ReportProcessor):
69
72
  """
70
73
  super().__init__(path)
71
74
 
75
+ def find_csv_prefix(self) -> str:
76
+ files = [
77
+ f
78
+ for f in os.listdir(self.path)
79
+ if os.path.isfile(os.path.join(self.path, f))
80
+ ]
81
+ for file in files:
82
+ if "00" in file:
83
+ name, _, file_extension = file.partition(".")
84
+ if "00" in name and file_extension.lower() == "csv":
85
+ prefix, _, _ = name.partition("00")
86
+ return prefix
87
+ raise FileNotFoundError("Couldn't find the prefix for CSV")
88
+
89
+ def report_contains(self, labels: List[str], want: List[str]):
90
+ for label in labels:
91
+ if label in want:
92
+ want.remove(label)
93
+
94
+ all_labels_seen = False
95
+ if len(want) != 0:
96
+ for want_label in want:
97
+ label_seen = False
98
+ for label in labels:
99
+ if want_label in label or want_label == label:
100
+ label_seen = True
101
+ all_labels_seen = label_seen
102
+ else:
103
+ return True
104
+ return all_labels_seen
105
+
72
106
  def process_report(self) -> Result[AgilentReport, AnyStr]:
73
107
  """
74
108
  Method to parse details from CSV report.
@@ -76,13 +110,30 @@ class CSVProcessor(ReportProcessor):
76
110
  :return: subset of complete report details, specifically the sample location, solvents in pumps,
77
111
  and list of peaks at each wavelength channel.
78
112
  """
79
- labels = os.path.join(self.path, 'REPORT00.CSV')
80
- if os.path.exists(labels):
81
- df_labels: Dict[int, Dict[int: AnyStr]] = pd.read_csv(labels, encoding="utf-16", header=None).to_dict()
82
- vial_location = []
83
- signals = {}
84
- solvents = {}
85
- for pos, val in df_labels[0].items():
113
+ prefix = self.find_csv_prefix()
114
+ labels = os.path.join(self.path, f"{prefix}00.CSV")
115
+ if not os.path.exists(labels):
116
+ raise ValueError(
117
+ "CSV reports do not exist, make sure to turn on the post run CSV report option!"
118
+ )
119
+ elif os.path.exists(labels):
120
+ LOCATION = "Location"
121
+ NUM_SIGNALS = "Number of Signals"
122
+ SOLVENT = "Solvent"
123
+ df_labels: Dict[int, Dict[int, str]] = pd.read_csv(
124
+ labels, encoding="utf-16", header=None
125
+ ).to_dict()
126
+ vial_location: str = ""
127
+ signals: Dict[int, list[AgilentPeak]] = {}
128
+ solvents: Dict[str, str] = {}
129
+ report_labels: Dict[int, str] = df_labels[0]
130
+
131
+ if not self.report_contains(
132
+ list(report_labels.values()), [LOCATION, NUM_SIGNALS, SOLVENT]
133
+ ):
134
+ return Err(f"Missing one of: {LOCATION}, {NUM_SIGNALS}, {SOLVENT}")
135
+
136
+ for pos, val in report_labels.items():
86
137
  if val == "Location":
87
138
  vial_location = df_labels[1][pos]
88
139
  elif "Solvent" in val:
@@ -91,18 +142,35 @@ class CSVProcessor(ReportProcessor):
91
142
  elif val == "Number of Signals":
92
143
  num_signals = int(df_labels[1][pos])
93
144
  for s in range(1, num_signals + 1):
94
- df = pd.read_csv(os.path.join(self.path, f'REPORT0{s}.CSV'),
95
- encoding="utf-16", header=None)
96
- peaks = df.apply(lambda row: AgilentPeak(*row), axis=1)
97
- wavelength = df_labels[1][pos + s].partition(",4 Ref=off")[0][-3:]
98
- signals[wavelength] = list(peaks)
145
+ try:
146
+ df = pd.read_csv(
147
+ os.path.join(self.path, f"{prefix}0{s}.CSV"),
148
+ encoding="utf-16",
149
+ header=None,
150
+ )
151
+ peaks = df.apply(lambda row: AgilentPeak(*row), axis=1)
152
+ except EmptyDataError:
153
+ peaks = []
154
+ try:
155
+ wavelength = df_labels[1][pos + s].partition(",4 Ref=off")[
156
+ 0
157
+ ][-3:]
158
+ signals[int(wavelength)] = list(peaks)
159
+ except (IndexError, ValueError):
160
+ # TODO: Ask about the MS signals
161
+ pass
99
162
  break
100
163
 
101
- return Ok(AgilentReport(
102
- signals=[Signals(wavelength=int(w), peaks=s, data=None) for w, s in signals.items()],
103
- vial_location=FiftyFourVialPlate.from_int(int(vial_location)),
104
- solvents=solvents
105
- ))
164
+ return Ok(
165
+ AgilentReport(
166
+ signals=[
167
+ Signals(wavelength=w, peaks=s, data=None)
168
+ for w, s in signals.items()
169
+ ],
170
+ vial_location=FiftyFourVialPlate.from_int(int(vial_location)),
171
+ solvents=solvents,
172
+ )
173
+ )
106
174
 
107
175
  return Err("No report found")
108
176
 
@@ -111,34 +179,39 @@ class TXTProcessor(ReportProcessor):
111
179
  """
112
180
  Regex matches for column and unit combinations, courtesy of Veronica Lai.
113
181
  """
182
+
114
183
  _column_re_dictionary = {
115
- 'Peak': { # peak index
116
- '#': '[ ]+(?P<Peak>[\d]+)', # number
184
+ "Peak": { # peak index
185
+ "#": "[ ]+(?P<Peak>[\d]+)", # number
117
186
  },
118
- 'RetTime': { # retention time
119
- '[min]': '(?P<RetTime>[\d]+.[\d]+)', # minutes
187
+ "RetTime": { # retention time
188
+ "[min]": "(?P<RetTime>[\d]+.[\d]+)", # minutes
120
189
  },
121
- 'Type': { # peak type
122
- '': '(?P<Type>[A-Z]{1,3}(?: [A-Z]{1,2})*)', # todo this is different from <4.8.8 aghplc tools
190
+ "Type": { # peak type
191
+ "": "(?P<Type>[A-Z]{1,3}(?: [A-Z]{1,2})*)", # todo this is different from <4.8.8 aghplc tools
123
192
  },
124
- 'Width': { # peak width
125
- '[min]': '(?P<Width>[\d]+.[\d]+[e+-]*[\d]+)',
193
+ "Width": { # peak width
194
+ "[min]": "(?P<Width>[\d]+.[\d]+[e+-]*[\d]+)",
126
195
  },
127
- 'Area': { # peak area
128
- '[mAU*s]': '(?P<Area>[\d]+.[\d]+[e+-]*[\d]+)', # area units
129
- '%': '(?P<percent>[\d]+.[\d]+[e+-]*[\d]+)', # percent
196
+ "Area": { # peak area
197
+ "[mAU*s]": "(?P<Area>[\d]+.[\d]+[e+-]*[\d]+)", # area units
198
+ "%": "(?P<percent>[\d]+.[\d]+[e+-]*[\d]+)", # percent
130
199
  },
131
- 'Height': { # peak height
132
- '[mAU]': '(?P<Height>[\d]+.[\d]+[e+-]*[\d]+)',
200
+ "Height": { # peak height
201
+ "[mAU]": "(?P<Height>[\d]+.[\d]+[e+-]*[\d]+)",
133
202
  },
134
- 'Name': {
135
- '': '(?P<Name>[^\s]+(?:\s[^\s]+)*)', # peak name
203
+ "Name": {
204
+ "": "(?P<Name>[^\s]+(?:\s[^\s]+)*)", # peak name
136
205
  },
137
206
  }
138
207
 
139
- def __init__(self, path: str, min_ret_time: int = 0,
140
- max_ret_time: int = 999,
141
- target_wavelength_range: List[int] = range(200, 300)):
208
+ def __init__(
209
+ self,
210
+ path: str,
211
+ min_ret_time: int = 0,
212
+ max_ret_time: int = 999,
213
+ target_wavelength_range=None,
214
+ ):
142
215
  """
143
216
  Class to process reports in CSV form.
144
217
 
@@ -147,12 +220,14 @@ class TXTProcessor(ReportProcessor):
147
220
  :param max_ret_time: peaks will only be returned up to this time (min)
148
221
  :param target_wavelength_range: range of wavelengths to return
149
222
  """
223
+ if target_wavelength_range is None:
224
+ target_wavelength_range = list(range(200, 300))
150
225
  self.target_wavelength_range = target_wavelength_range
151
226
  self.min_ret_time = min_ret_time
152
227
  self.max_ret_time = max_ret_time
153
228
  super().__init__(path)
154
229
 
155
- def process_report(self) -> Result[AgilentReport, AnyStr]:
230
+ def process_report(self) -> Result[AgilentReport, Union[AnyStr, Exception]]:
156
231
  """
157
232
  Method to parse details from CSV report.
158
233
  If you want more functionality, use `aghplctools`.
@@ -162,34 +237,48 @@ class TXTProcessor(ReportProcessor):
162
237
  :return: subset of complete report details, specifically the sample location, solvents in pumps,
163
238
  and list of peaks at each wavelength channel.
164
239
  """
165
-
166
- with open(os.path.join(self.path, "REPORT.TXT"), 'r', encoding='utf-16') as openfile:
167
- text = openfile.read()
168
-
169
240
  try:
170
- signals = self.parse_area_report(text)
171
- except ValueError as e:
172
- return Err("No peaks found: " + str(e))
173
-
174
- signals = {key: signals[key] for key in self.target_wavelength_range if key in signals}
175
-
176
- parsed_signals = []
177
- for wavelength, wavelength_dict in signals.items():
178
- current_wavelength_signals = Signals(wavelength=int(wavelength), peaks=[], data=None)
179
- for ret_time, ret_time_dict in wavelength_dict.items():
180
- if self.min_ret_time <= ret_time <= self.max_ret_time:
181
- current_wavelength_signals.peaks.append(AgilentPeak(retention_time=ret_time,
182
- area=ret_time_dict['Area'],
183
- width=ret_time_dict['Width'],
184
- height=ret_time_dict['Height'],
185
- peak_number=None,
186
- peak_type=ret_time_dict['Type'],
187
- area_percent=None))
188
- parsed_signals.append(current_wavelength_signals)
189
-
190
- return Ok(AgilentReport(vial_location=None,
191
- solvents=None,
192
- signals=parsed_signals))
241
+ with open(
242
+ os.path.join(self.path, "REPORT.TXT"), "r", encoding="utf-16"
243
+ ) as openfile:
244
+ text = openfile.read()
245
+
246
+ try:
247
+ signals = self.parse_area_report(text)
248
+ except ValueError as e:
249
+ return Err("No peaks found: " + str(e))
250
+
251
+ signals = {
252
+ key: signals[key]
253
+ for key in self.target_wavelength_range
254
+ if key in signals
255
+ }
256
+
257
+ parsed_signals = []
258
+ for wavelength, wavelength_dict in signals.items():
259
+ current_wavelength_signals = Signals(
260
+ wavelength=int(wavelength), peaks=[], data=None
261
+ )
262
+ for ret_time, ret_time_dict in wavelength_dict.items():
263
+ if self.min_ret_time <= ret_time <= self.max_ret_time:
264
+ current_wavelength_signals.peaks.append(
265
+ AgilentPeak(
266
+ retention_time=ret_time,
267
+ area=ret_time_dict["Area"],
268
+ width=ret_time_dict["Width"],
269
+ height=ret_time_dict["Height"],
270
+ peak_number=None,
271
+ peak_type=ret_time_dict["Type"],
272
+ area_percent=None,
273
+ )
274
+ )
275
+ parsed_signals.append(current_wavelength_signals)
276
+
277
+ return Ok(
278
+ AgilentReport(vial_location=None, solvents=None, signals=parsed_signals)
279
+ )
280
+ except Exception as e:
281
+ return Err(e)
193
282
 
194
283
  def parse_area_report(self, report_text: str) -> Dict:
195
284
  """
@@ -205,9 +294,9 @@ class TXTProcessor(ReportProcessor):
205
294
  should be able to use the `parse_area_report` method of aghplctools v4.8.8
206
295
  """
207
296
  if re.search(_no_peaks_re, report_text): # There are no peaks in Report.txt
208
- raise ValueError('No peaks found in Report.txt')
297
+ raise ValueError("No peaks found in Report.txt")
209
298
  blocks = _header_block_re.split(report_text)
210
- signals = {} # output dictionary
299
+ signals: Dict[int, dict] = {} # output dictionary
211
300
  for ind, block in enumerate(blocks):
212
301
  # area report block
213
302
  if _area_report_re.match(block): # match area report block
@@ -218,65 +307,75 @@ class TXTProcessor(ReportProcessor):
218
307
  si = _signal_info_re.match(table)
219
308
  if si is not None:
220
309
  # some error state (e.g. 'not found')
221
- if si.group('error') != '':
310
+ if si.group("error") != "":
222
311
  continue
223
- wavelength = float(si.group('wavelength'))
312
+ wavelength = int(si.group("wavelength"))
224
313
  if wavelength in signals:
225
314
  # placeholder error raise just in case (this probably won't happen)
226
315
  raise KeyError(
227
- f'The wavelength {float(si.group("wavelength"))} is already in the signals dictionary')
316
+ f"The wavelength {float(si.group('wavelength'))} is already in the signals dictionary"
317
+ )
228
318
  signals[wavelength] = {}
229
319
  # build peak regex
230
320
  peak_re = self.build_peak_regex(table)
231
- if peak_re is None: # if there are no columns (empty table), continue
321
+ if (
322
+ peak_re is None
323
+ ): # if there are no columns (empty table), continue
232
324
  continue
233
- for line in table.split('\n'):
325
+ for line in table.split("\n"):
234
326
  peak = peak_re.match(line)
235
327
  if peak is not None:
236
- signals[wavelength][float(peak.group('RetTime'))] = {}
237
- current = signals[wavelength][float(peak.group('RetTime'))]
328
+ signals[wavelength][float(peak.group("RetTime"))] = {}
329
+ current = signals[wavelength][
330
+ float(peak.group("RetTime"))
331
+ ]
238
332
  for key in self._column_re_dictionary:
239
333
  if key in peak.re.groupindex:
240
334
  try: # try float conversion, otherwise continue
241
- value = float(peak.group(key))
335
+ current[key] = float(peak.group(key))
242
336
  except ValueError:
243
- value = peak.group(key)
244
- current[key] = value
337
+ current[key] = peak.group(key)
245
338
  else: # ensures defined
246
339
  current[key] = None
247
340
  return signals
248
341
 
249
- def build_peak_regex(self, signal_table: str) -> Pattern[AnyStr]:
342
+ def build_peak_regex(self, signal_table: str) -> Pattern[str] | None:
250
343
  """
251
344
  Builds a peak regex from a signal table. Courtesy of Veronica Lai.
252
345
 
253
346
  :param signal_table: block of lines associated with an area table
254
347
  :return: peak line regex object (<=3.6 _sre.SRE_PATTERN, >=3.7 re.Pattern)
255
348
  """
256
- split_table = signal_table.split('\n')
349
+ split_table = signal_table.split("\n")
257
350
  if len(split_table) <= 4: # catch peak table with no values
258
351
  return None
259
352
  # todo verify that these indicies are always true
260
353
  column_line = split_table[2] # table column line
261
354
  unit_line = split_table[3] # column unit line
262
- length_line = [len(val) + 1 for val in split_table[4].split('|')] # length line
355
+ length_line = [len(val) + 1 for val in split_table[4].split("|")] # length line
263
356
 
264
357
  # iterate over header values and units to build peak table regex
265
358
  peak_re_string = []
266
359
  for header, unit in zip(
267
- chunk_string(column_line, length_line),
268
- chunk_string(unit_line, length_line)
360
+ chunk_string(column_line, length_line), chunk_string(unit_line, length_line)
269
361
  ):
270
- if header == '': # todo create a better catch for an undefined header
362
+ if header == "": # todo create a better catch for an undefined header
271
363
  continue
272
364
  try:
273
365
  peak_re_string.append(
274
- self._column_re_dictionary[header][unit] # append the appropriate regex
366
+ self._column_re_dictionary[header][
367
+ unit
368
+ ] # append the appropriate regex
275
369
  )
276
370
  except KeyError: # catch for undefined regexes (need to be built)
277
- raise KeyError(f'The header/unit combination "{header}" "{unit}" is not defined in the peak regex '
278
- f'dictionary. Let Lars know.')
371
+ raise KeyError(
372
+ f'The header/unit combination "{header}" "{unit}" is not defined in the peak regex '
373
+ f"dictionary. Let Lars know."
374
+ )
375
+
279
376
  return re.compile(
280
- '[ ]+'.join(peak_re_string) # constructed string delimited by 1 or more spaces
281
- + '[\s]*' # and any remaining white space
377
+ "[ ]+".join(
378
+ peak_re_string
379
+ ) # constructed string delimited by 1 or more spaces
380
+ + "[\s]*" # and any remaining white space
282
381
  )
@@ -1,8 +1,11 @@
1
1
  """
2
2
  .. include:: README.md
3
3
  """
4
+
4
5
  from .hplc import HPLCController
6
+ from . import controllers
5
7
 
6
8
  __all__ = [
7
- 'HPLCController',
8
- ]
9
+ "HPLCController",
10
+ "controllers",
11
+ ]
@@ -3,11 +3,6 @@
3
3
  """
4
4
 
5
5
  from .comm import CommunicationController
6
- from .tables.method import MethodController
7
- from .tables.sequence import SequenceController
6
+ from . import tables
8
7
 
9
- __all__ = [
10
- 'CommunicationController',
11
- 'MethodController',
12
- 'SequenceController'
13
- ]
8
+ __all__ = ["CommunicationController", "tables"]