pychemstation 0.10.6__py3-none-any.whl → 0.10.7__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.
@@ -10,7 +10,7 @@ DATA_DIR_2 = "C:\\Users\\Public\\Documents\\ChemStation\\2\\Data"
10
10
  DATA_DIR_3 = "C:\\Users\\Public\\Documents\\ChemStation\\3\\Data"
11
11
 
12
12
  # Initialize HPLC Controller
13
- hplc_controller = HPLCController(data_dirs=[DATA_DIR_2, DATA_DIR_3],
13
+ hplc_controller = HPLCController(extra_data_dirs=[DATA_DIR_2, DATA_DIR_3],
14
14
  comm_dir=DEFAULT_COMMAND_PATH,
15
15
  method_dir=DEFAULT_METHOD_DIR,
16
16
  sequence_dir=SEQUENCE_DIR)
@@ -18,12 +18,7 @@ from typing import Union
18
18
 
19
19
  from result import Err, Ok, Result
20
20
 
21
- from ...utils.macro import (
22
- HPLCAvailStatus,
23
- Command,
24
- Status,
25
- Response,
26
- )
21
+ from ....utils.macro import Status, HPLCAvailStatus, Command, Response
27
22
 
28
23
 
29
24
  class ABCCommunicationController(abc.ABC):
@@ -13,3 +13,8 @@ class DeviceController(ABCTableController, ABC):
13
13
  ):
14
14
  super().__init__(controller=controller, table=table)
15
15
  self.offline = offline
16
+
17
+ def __new__(cls, *args, **kwargs):
18
+ if cls is ABCTableController:
19
+ raise TypeError(f"only children of '{cls.__name__}' may be instantiated")
20
+ return object.__new__(cls)
@@ -11,13 +11,17 @@ import math
11
11
  import os
12
12
  import time
13
13
  import warnings
14
- from typing import Dict, List, Optional, Tuple, Union
14
+ from typing import Dict, List, Optional, Tuple, Union, Set
15
15
 
16
16
  import polling
17
17
  import rainbow as rb
18
- from result import Err, Result, Ok
18
+ from result import Err, Ok, Result
19
+
20
+ from pychemstation.analysis.chromatogram import (
21
+ AgilentChannelChromatogramData,
22
+ AgilentHPLCChromatogram,
23
+ )
19
24
 
20
- from .table import ABCTableController
21
25
  from ....analysis.process_report import (
22
26
  AgilentReport,
23
27
  CSVProcessor,
@@ -25,14 +29,11 @@ from ....analysis.process_report import (
25
29
  TXTProcessor,
26
30
  )
27
31
  from ....control.controllers.comm import CommunicationController
28
- from pychemstation.analysis.chromatogram import (
29
- AgilentChannelChromatogramData,
30
- AgilentHPLCChromatogram,
31
- )
32
- from ....utils.macro import HPLCRunningStatus
32
+ from ....utils.macro import Command, HPLCRunningStatus
33
33
  from ....utils.method_types import MethodDetails
34
34
  from ....utils.sequence_types import SequenceTable
35
- from ....utils.table_types import Table, T
35
+ from ....utils.table_types import T, Table
36
+ from .table import ABCTableController
36
37
 
37
38
  TableType = Union[MethodDetails, SequenceTable]
38
39
 
@@ -47,12 +48,10 @@ class RunController(ABCTableController, abc.ABC):
47
48
  offline: bool = False,
48
49
  ):
49
50
  super().__init__(controller=controller, table=table)
50
- warnings.warn(
51
- "This abstract class is not meant to be initialized. Use MethodController or SequenceController."
52
- )
53
51
  self.table_state: Optional[TableType] = None
54
52
  self.curr_run_starting_time: Optional[float] = None
55
53
  self.timeout: Optional[float] = None
54
+ self.current_run_child_files: Set[str] = set()
56
55
 
57
56
  if not offline:
58
57
  if src and not os.path.isdir(src):
@@ -79,8 +78,15 @@ class RunController(ABCTableController, abc.ABC):
79
78
  self.uv: Dict[int, AgilentHPLCChromatogram] = {}
80
79
  self.data_files: List = []
81
80
 
81
+ def __new__(cls, *args, **kwargs):
82
+ if cls is RunController:
83
+ raise TypeError(f"only children of '{cls.__name__}' may be instantiated")
84
+ return object.__new__(cls)
85
+
82
86
  @abc.abstractmethod
83
- def fuzzy_match_most_recent_folder(self, most_recent_folder: T) -> Result[T, str]:
87
+ def fuzzy_match_most_recent_folder(
88
+ self, most_recent_folder: T, child_files: Set[str]
89
+ ) -> Result[T, str]:
84
90
  pass
85
91
 
86
92
  @abc.abstractmethod
@@ -120,6 +126,12 @@ class RunController(ABCTableController, abc.ABC):
120
126
 
121
127
  def check_hplc_run_finished(self) -> Tuple[float, bool]:
122
128
  if self.controller:
129
+ try:
130
+ _, current_run_file = self.get_current_run_data_dir_file()
131
+ sample_file, extension, _ = current_run_file.partition(".D")
132
+ self.current_run_child_files.add(sample_file)
133
+ except Exception:
134
+ pass
123
135
  done_running = self.controller.check_if_not_running()
124
136
  if self.curr_run_starting_time and self.timeout:
125
137
  time_passed = time.time() - self.curr_run_starting_time
@@ -141,6 +153,7 @@ class RunController(ABCTableController, abc.ABC):
141
153
 
142
154
  :return: Data file object containing most recent run file information.
143
155
  """
156
+ self.current_run_child_files = set()
144
157
  if self.timeout is not None:
145
158
  finished_run = False
146
159
  minutes = math.ceil(self.timeout / 60)
@@ -170,7 +183,9 @@ class RunController(ABCTableController, abc.ABC):
170
183
  else:
171
184
  raise ValueError("Timeout value is None, no comparison can be made.")
172
185
 
173
- check_folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
186
+ check_folder = self.fuzzy_match_most_recent_folder(
187
+ self.data_files[-1], self.current_run_child_files
188
+ )
174
189
  if check_folder.is_ok() and finished_run:
175
190
  return check_folder
176
191
  elif check_folder.is_ok():
@@ -226,3 +241,16 @@ class RunController(ABCTableController, abc.ABC):
226
241
  def _reset_time(self):
227
242
  self.curr_run_starting_time = None
228
243
  self.timeout = None
244
+
245
+ def get_current_run_data_dir_file(self) -> Tuple[str, str]:
246
+ self.send(Command.GET_CURRENT_RUN_DATA_DIR)
247
+ full_path_name = self.receive()
248
+ self.send(Command.GET_CURRENT_RUN_DATA_FILE)
249
+ current_sample_file = self.receive()
250
+ if full_path_name.is_ok() and current_sample_file.is_ok():
251
+ return (
252
+ full_path_name.ok_value.string_response,
253
+ current_sample_file.ok_value.string_response,
254
+ )
255
+ else:
256
+ raise ValueError("Couldn't read data dir and file.")
@@ -7,7 +7,6 @@ Authors: Lucy Hao
7
7
  from __future__ import annotations
8
8
 
9
9
  import abc
10
- import warnings
11
10
  from typing import Optional, Union
12
11
 
13
12
  from result import Err, Result
@@ -27,13 +26,15 @@ class ABCTableController(abc.ABC):
27
26
  controller: Optional[CommunicationController],
28
27
  table: Table,
29
28
  ):
30
- warnings.warn(
31
- "This abstract class is not meant to be initialized. Use MethodController or SequenceController."
32
- )
33
29
  self.controller = controller
34
30
  self.table_locator = table
35
31
  self.table_state: Optional[TableType] = None
36
32
 
33
+ def __new__(cls, *args, **kwargs):
34
+ if cls is ABCTableController:
35
+ raise TypeError(f"only children of '{cls.__name__}' may be instantiated")
36
+ return object.__new__(cls, *args, **kwargs)
37
+
37
38
  def receive(self) -> Result[Response, str]:
38
39
  if self.controller:
39
40
  for _ in range(10):
@@ -93,6 +94,8 @@ class ABCTableController(abc.ABC):
93
94
  raise ValueError("Controller is offline")
94
95
 
95
96
  def add_new_col_num(self, col_name: RegisterFlag, val: Union[int, float]):
97
+ if not (isinstance(val, int) or isinstance(val, float)):
98
+ raise ValueError(f"{val} must be an int or float.")
96
99
  self.sleepy_send(
97
100
  TableOperation.NEW_COL_VAL.value.format(
98
101
  register=self.table_locator.register,
@@ -103,6 +106,8 @@ class ABCTableController(abc.ABC):
103
106
  )
104
107
 
105
108
  def add_new_col_text(self, col_name: RegisterFlag, val: str):
109
+ if not isinstance(val, str):
110
+ raise ValueError(f"{val} must be a str.")
106
111
  self.sleepy_send(
107
112
  TableOperation.NEW_COL_TEXT.value.format(
108
113
  register=self.table_locator.register,
@@ -115,10 +120,12 @@ class ABCTableController(abc.ABC):
115
120
  def _edit_row_num(
116
121
  self, col_name: RegisterFlag, val: Union[int, float], row: Optional[int] = None
117
122
  ):
123
+ if not (isinstance(val, int) or isinstance(val, float)):
124
+ raise ValueError(f"{val} must be an int or float.")
118
125
  if row:
119
126
  num_rows = self.get_num_rows()
120
127
  if num_rows.is_ok():
121
- if num_rows.value.num_response < row:
128
+ if num_rows.ok_value.num_response < row:
122
129
  raise ValueError("Not enough rows to edit!")
123
130
 
124
131
  self.sleepy_send(
@@ -134,10 +141,12 @@ class ABCTableController(abc.ABC):
134
141
  def _edit_row_text(
135
142
  self, col_name: RegisterFlag, val: str, row: Optional[int] = None
136
143
  ):
144
+ if not isinstance(val, str):
145
+ raise ValueError(f"{val} must be a str.")
137
146
  if row:
138
147
  num_rows = self.get_num_rows()
139
148
  if num_rows.is_ok():
140
- if num_rows.value.num_response < row:
149
+ if num_rows.ok_value.num_response < row:
141
150
  raise ValueError("Not enough rows to edit!")
142
151
 
143
152
  self.sleepy_send(
@@ -11,17 +11,17 @@ Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
11
11
  """
12
12
 
13
13
  import time
14
- from typing import Optional, Union
14
+ from typing import Optional, Union, Tuple, List
15
15
 
16
16
  from result import Err, Ok, Result
17
17
 
18
18
  from ...utils.macro import (
19
- str_to_status,
20
- HPLCErrorStatus,
21
19
  Command,
20
+ HPLCErrorStatus,
22
21
  Status,
22
+ str_to_status,
23
23
  )
24
- from ...utils.mocking.abc_comm import ABCCommunicationController
24
+ from .abc_tables.abc_comm import ABCCommunicationController
25
25
 
26
26
 
27
27
  class CommunicationController(ABCCommunicationController):
@@ -76,7 +76,7 @@ class CommunicationController(ABCCommunicationController):
76
76
  if res.is_err():
77
77
  return HPLCErrorStatus.NORESPONSE
78
78
  if res.is_ok():
79
- parsed_response = self.receive().value.string_response
79
+ parsed_response = self.receive().ok_value.string_response
80
80
  self._most_recent_hplc_status = str_to_status(parsed_response)
81
81
  return self._most_recent_hplc_status
82
82
  else:
@@ -148,3 +148,24 @@ class CommunicationController(ABCCommunicationController):
148
148
  return Err(
149
149
  f"Failed to receive reply to command #{cmd_no} due to {err} caused by {err_msg}."
150
150
  )
151
+
152
+ def get_chemstation_dirs(self) -> Tuple[str, str, List[str]]:
153
+ method_dir, sequence_dir, data_dirs = None, None, None
154
+ self.send(Command.GET_METHOD_DIR)
155
+ res = self.receive()
156
+ if res.is_ok():
157
+ method_dir = res.ok_value.string_response
158
+ self.send(Command.GET_SEQUENCE_DIR)
159
+ res = self.receive()
160
+ if res.is_ok():
161
+ sequence_dir = res.ok_value.string_response
162
+ self.send(Command.GET_DATA_DIRS)
163
+ res = self.receive()
164
+ if res.is_ok():
165
+ data_dirs = res.ok().string_response.split("|")
166
+ if method_dir and sequence_dir and data_dirs:
167
+ return method_dir, sequence_dir, data_dirs
168
+ else:
169
+ raise ValueError(
170
+ "Please provide the method, sequence and data directories, could not be found."
171
+ )
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import os
4
4
  import time
5
5
  import warnings
6
- from typing import List, Optional, Union, Dict
6
+ from typing import List, Optional, Union, Dict, Set
7
7
 
8
8
  from result import Err, Ok, Result
9
9
 
@@ -413,8 +413,6 @@ class MethodController(RunController):
413
413
  :param stall_while_running: whether to stall or immediately return
414
414
  :param add_timestamp: if should add timestamp to experiment name
415
415
  """
416
-
417
- folder_name = ""
418
416
  hplc_is_running = False
419
417
  tries = 0
420
418
  while tries < 10 and not hplc_is_running:
@@ -427,18 +425,15 @@ class MethodController(RunController):
427
425
  else experiment_name,
428
426
  )
429
427
  )
430
- folder_name = (
431
- f"{experiment_name}_{timestamp}.D"
432
- if add_timestamp
433
- else f"{experiment_name}.D"
434
- )
428
+
435
429
  hplc_is_running = self.check_hplc_is_running()
436
430
  tries += 1
437
431
 
432
+ data_dir, data_file = self.get_current_run_data_dir_file()
438
433
  if not hplc_is_running:
439
434
  raise RuntimeError("Method failed to start.")
440
435
 
441
- self.data_files.append(os.path.join(self.data_dirs[0], folder_name))
436
+ self.data_files.append(os.path.join(os.path.normpath(data_dir), data_file))
442
437
  self.timeout = (self.get_total_runtime()) * 60
443
438
 
444
439
  if stall_while_running:
@@ -446,18 +441,20 @@ class MethodController(RunController):
446
441
  if run_completed.is_ok():
447
442
  self.data_files[-1] = run_completed.ok_value
448
443
  else:
449
- raise RuntimeError("Run error has occurred.")
444
+ raise RuntimeError(f"Run error has occurred:{run_completed.err_value}.")
450
445
  else:
451
- folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
446
+ folder = self.fuzzy_match_most_recent_folder(self.data_files[-1], None)
452
447
  while folder.is_err():
453
- folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
448
+ folder = self.fuzzy_match_most_recent_folder(self.data_files[-1], None)
454
449
  if folder.is_ok():
455
450
  self.data_files[-1] = folder.ok_value
456
451
  else:
457
452
  warning = f"Data folder {self.data_files[-1]} may not exist, returning and will check again after run is done."
458
453
  warnings.warn(warning)
459
454
 
460
- def fuzzy_match_most_recent_folder(self, most_recent_folder: T) -> Result[str, str]:
455
+ def fuzzy_match_most_recent_folder(
456
+ self, most_recent_folder: T, child_dirs: Optional[Set[str]]
457
+ ) -> Result[str, str]:
461
458
  if isinstance(most_recent_folder, str) or isinstance(most_recent_folder, bytes):
462
459
  if os.path.exists(most_recent_folder):
463
460
  return Ok(most_recent_folder)
@@ -1,13 +1,12 @@
1
1
  import os
2
2
  import time
3
3
  import warnings
4
- from typing import Any, Dict, List, Optional, Union
4
+ from typing import Any, Dict, List, Optional, Union, Set
5
5
 
6
6
  from result import Err, Ok, Result
7
7
  from typing_extensions import override
8
8
 
9
9
  from pychemstation.analysis.chromatogram import (
10
- SEQUENCE_TIME_FORMAT,
11
10
  AgilentChannelChromatogramData,
12
11
  AgilentHPLCChromatogram,
13
12
  )
@@ -92,6 +91,7 @@ class SequenceController(RunController):
92
91
  def get_row(self, row: int) -> SequenceEntry:
93
92
  sample_name = self.get_text(row, RegisterFlag.NAME)
94
93
  vial_location = self.try_int(self.get_num(row, RegisterFlag.VIAL_LOCATION))
94
+ data_file = self.get_text(row, RegisterFlag.DATA_FILE)
95
95
  method = self.get_text(row, RegisterFlag.METHOD)
96
96
  num_inj = self.try_int(self.get_num(row, RegisterFlag.NUM_INJ))
97
97
  inj_vol = self.try_float(self.get_text(row, RegisterFlag.INJ_VOL))
@@ -106,6 +106,7 @@ class SequenceController(RunController):
106
106
  inj_vol=inj_vol,
107
107
  inj_source=inj_source,
108
108
  sample_type=sample_type,
109
+ data_file=data_file,
109
110
  )
110
111
 
111
112
  def check(self) -> str:
@@ -127,13 +128,17 @@ class SequenceController(RunController):
127
128
  self.send(f'_SeqPath$ = "{self.src}"')
128
129
  self.send(Command.SWITCH_SEQUENCE_CMD)
129
130
  time.sleep(2)
130
- self.send(Command.GET_SEQUENCE_CMD)
131
- time.sleep(2)
132
- parsed_response = self.receive().ok_value.string_response
131
+ parsed_response = self.get_current_sequence_name()
133
132
 
134
133
  assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
135
134
  self.table_state = self.load()
136
135
 
136
+ def get_current_sequence_name(self):
137
+ self.send(Command.GET_SEQUENCE_CMD)
138
+ time.sleep(2)
139
+ parsed_response = self.receive().ok_value.string_response
140
+ return parsed_response
141
+
137
142
  def edit(self, sequence_table: SequenceTable):
138
143
  """
139
144
  Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
@@ -151,13 +156,13 @@ class SequenceController(RunController):
151
156
  self.send(Command.SAVE_SEQUENCE_CMD)
152
157
  for i in range(int(wanted_row_num)):
153
158
  self.add_row()
154
- self.send(Command.SAVE_SEQUENCE_CMD)
159
+ self.save()
155
160
  self.send(Command.SWITCH_SEQUENCE_CMD)
156
161
 
157
162
  for i, row in enumerate(sequence_table.rows):
158
163
  self._edit_row(row=row, row_num=i + 1)
159
164
  self.sleep(1)
160
- self.send(Command.SAVE_SEQUENCE_CMD)
165
+ self.save()
161
166
  self.send(Command.SWITCH_SEQUENCE_CMD)
162
167
 
163
168
  def _edit_row(self, row: SequenceEntry, row_num: int):
@@ -171,62 +176,78 @@ class SequenceController(RunController):
171
176
  if num_rows.is_ok():
172
177
  while num_rows.ok_value.num_response < row_num:
173
178
  self.add_row()
174
- self.send(Command.SAVE_SEQUENCE_CMD)
179
+ self.save()
175
180
  num_rows = self.get_num_rows()
176
181
  if row.vial_location:
177
- self.edit_vial_location(row.vial_location, row_num)
182
+ self.edit_vial_location(row.vial_location, row_num, save=False)
178
183
  if row.method:
179
- self.edit_method_name(row.method, row_num)
184
+ self.edit_method_name(row.method, row_num, save=False)
180
185
  if row.num_inj:
181
- self.edit_num_injections(row.num_inj, row_num)
186
+ self.edit_num_injections(row.num_inj, row_num, save=False)
182
187
  if row.inj_vol:
183
- self.edit_injection_volume(row.inj_vol, row_num)
188
+ self.edit_injection_volume(row.inj_vol, row_num, save=False)
184
189
  if row.inj_source:
185
- self.edit_injection_source(row.inj_source, row_num)
190
+ self.edit_injection_source(row.inj_source, row_num, save=False)
186
191
  if row.sample_name:
187
- self.edit_sample_name(row.sample_name, row_num)
192
+ self.edit_sample_name(row.sample_name, row_num, save=False)
188
193
  if row.data_file:
189
- self.edit_data_file(row.data_file, row_num)
190
- elif row.sample_name and not row.data_file:
191
- self.edit_data_file(row.sample_name, row_num)
194
+ self.edit_data_file(row.data_file, row_num, save=False)
192
195
  if row.sample_type:
193
- self.edit_sample_type(row.sample_type, row_num)
194
- self.send(Command.SAVE_SEQUENCE_CMD)
196
+ self.edit_sample_type(row.sample_type, row_num, save=False)
197
+ self.save()
195
198
 
196
- def edit_sample_type(self, sample_type: SampleType, row_num: int):
199
+ def edit_sample_type(
200
+ self, sample_type: SampleType, row_num: int, save: bool = True
201
+ ):
197
202
  self._edit_row_num(
198
203
  row=row_num,
199
204
  col_name=RegisterFlag.SAMPLE_TYPE,
200
205
  val=sample_type.value,
201
206
  )
207
+ if save:
208
+ self.save()
202
209
 
203
- def edit_data_file(self, data_file: str, row_num: int):
210
+ def edit_data_file(self, data_file: str, row_num: int, save: bool = True):
204
211
  self._edit_row_text(row=row_num, col_name=RegisterFlag.DATA_FILE, val=data_file)
212
+ if save:
213
+ self.save()
205
214
 
206
- def edit_sample_name(self, sample_name: str, row_num: int):
215
+ def edit_sample_name(self, sample_name: str, row_num: int, save: bool = True):
207
216
  self._edit_row_text(row=row_num, col_name=RegisterFlag.NAME, val=sample_name)
217
+ if save:
218
+ self.save()
208
219
 
209
- def edit_injection_source(self, inj_source: InjectionSource, row_num: int):
220
+ def edit_injection_source(
221
+ self, inj_source: InjectionSource, row_num: int, save: bool = True
222
+ ):
210
223
  self._edit_row_text(
211
224
  row=row_num, col_name=RegisterFlag.INJ_SOR, val=inj_source.value
212
225
  )
226
+ if save:
227
+ self.save()
213
228
 
214
- def edit_injection_volume(self, inj_vol: Union[int, float], row_num: int):
229
+ def edit_injection_volume(
230
+ self, inj_vol: Union[int, float], row_num: int, save: bool = True
231
+ ):
215
232
  self._edit_row_text(
216
233
  row=row_num, col_name=RegisterFlag.INJ_VOL, val=str(inj_vol)
217
234
  )
235
+ if save:
236
+ self.save()
218
237
 
219
- def edit_num_injections(self, num_inj: int, row_num: int):
238
+ def edit_num_injections(self, num_inj: int, row_num: int, save: bool = True):
220
239
  self._edit_row_num(row=row_num, col_name=RegisterFlag.NUM_INJ, val=num_inj)
221
240
 
222
- def edit_method_name(self, method: str, row_num: int):
241
+ def edit_method_name(self, method: str, row_num: int, save: bool = True):
223
242
  method_dir = self.method_controller.src
224
243
  possible_path = os.path.join(method_dir, method) + ".M\\"
225
244
  if os.path.exists(possible_path):
226
245
  method = os.path.join(method_dir, method)
227
246
  self._edit_row_text(row=row_num, col_name=RegisterFlag.METHOD, val=method)
247
+ if save:
248
+ self.save()
228
249
 
229
- def edit_vial_location(self, loc: Tray, row_num: int):
250
+ def edit_vial_location(self, loc: Tray, row_num: int, save: bool = True):
230
251
  loc_num = -1
231
252
  if isinstance(loc, TenVialColumn):
232
253
  loc_num = loc.value
@@ -235,6 +256,11 @@ class SequenceController(RunController):
235
256
  self._edit_row_num(
236
257
  row=row_num, col_name=RegisterFlag.VIAL_LOCATION, val=loc_num
237
258
  )
259
+ if save:
260
+ self.save()
261
+
262
+ def save(self):
263
+ self.send(Command.SAVE_SEQUENCE_CMD)
238
264
 
239
265
  def run(self, stall_while_running: bool = True):
240
266
  """
@@ -248,7 +274,8 @@ class SequenceController(RunController):
248
274
  else:
249
275
  raise ValueError("Controller is offline!")
250
276
 
251
- if not self.table_state:
277
+ current_sequence_name = self.get_current_sequence_name()
278
+ if not self.table_state or self.table_state.name not in current_sequence_name:
252
279
  self.table_state = self.load()
253
280
 
254
281
  total_runtime = 0.0
@@ -270,7 +297,6 @@ class SequenceController(RunController):
270
297
  curr_method_runtime = self.method_controller.get_total_runtime()
271
298
  total_runtime += curr_method_runtime
272
299
 
273
- timestamp = time.strftime(SEQUENCE_TIME_FORMAT)
274
300
  self.send(Command.RUN_SEQUENCE_CMD.value)
275
301
  self.timeout = total_runtime * 60
276
302
 
@@ -282,11 +308,24 @@ class SequenceController(RunController):
282
308
  break
283
309
 
284
310
  if hplc_running:
285
- folder_name = f"{self.table_state.name} {timestamp}"
286
- data_file = SequenceDataFiles(
287
- dir=folder_name, sequence_name=self.table_state.name
288
- )
289
- self.data_files.append(data_file)
311
+ full_path_name, current_sample_file = None, None
312
+ for _ in range(5):
313
+ try:
314
+ full_path_name, current_sample_file = (
315
+ self.get_current_run_data_dir_file()
316
+ )
317
+ break
318
+ except ValueError:
319
+ pass
320
+ if full_path_name and current_sample_file:
321
+ data_file = SequenceDataFiles(
322
+ sequence_name=self.table_state.name,
323
+ dir=full_path_name,
324
+ _data_files=[r.data_file for r in self.table_state.rows],
325
+ )
326
+ self.data_files.append(data_file)
327
+ else:
328
+ raise ValueError("Data directory for sequence was not found.")
290
329
 
291
330
  if stall_while_running:
292
331
  run_completed = self.check_hplc_done_running()
@@ -299,54 +338,39 @@ class SequenceController(RunController):
299
338
 
300
339
  @override
301
340
  def fuzzy_match_most_recent_folder(
302
- self, most_recent_folder: T
341
+ self, most_recent_folder: T, child_dirs: Optional[Set[str]]
303
342
  ) -> Result[SequenceDataFiles, str]:
304
343
  if isinstance(most_recent_folder, SequenceDataFiles):
305
- if os.path.isdir(most_recent_folder.dir):
306
- subdirs = [x[0] for x in os.walk(most_recent_folder.dir)]
307
- potential_folders = sorted(
308
- list(filter(lambda d: most_recent_folder.dir in d, subdirs))
309
- )
310
- most_recent_folder.child_dirs = [
311
- f
312
- for f in potential_folders
313
- if most_recent_folder.dir in f and ".M" not in f and ".D" in f
314
- ]
315
- return Ok(most_recent_folder)
316
-
317
344
  try:
318
- potential_folders = []
319
- for d in self.data_dirs:
320
- subdirs = [x[0] for x in os.walk(d)]
321
- potential_folders = sorted(
322
- list(filter(lambda d: most_recent_folder.dir in d, subdirs))
323
- )
324
- if len(potential_folders) > 0:
325
- break
326
- assert len(potential_folders) > 0
327
- parent_dirs = []
328
- for folder in potential_folders:
329
- path = os.path.normpath(folder)
330
- split_folder = path.split(os.sep)
331
- if most_recent_folder.dir in split_folder[-1]:
332
- parent_dirs.append(folder)
333
- parent_dir = sorted(parent_dirs, reverse=True)[0]
334
-
335
- potential_folders = []
336
- for d in self.data_dirs:
337
- subdirs = [x[0] for x in os.walk(d)]
338
- potential_folders = sorted(
339
- list(filter(lambda d: parent_dir in d, subdirs))
340
- )
341
- if len(potential_folders) > 0:
342
- break
343
- assert len(potential_folders) > 0
344
- most_recent_folder.child_dirs = [
345
- f
346
- for f in potential_folders
347
- if parent_dir in f and ".M" not in f and ".D" in f
348
- ]
349
- return Ok(most_recent_folder)
345
+ if most_recent_folder.dir and os.path.isdir(most_recent_folder.dir):
346
+ subdirs = [x[0] for x in os.walk(most_recent_folder.dir)]
347
+ if (
348
+ child_dirs
349
+ and len(child_dirs) > 0
350
+ and set(most_recent_folder._data_files) == child_dirs
351
+ ):
352
+ most_recent_folder.child_dirs = [
353
+ os.path.join(most_recent_folder.dir, f) for f in child_dirs
354
+ ]
355
+ else:
356
+ potential_folders: List[str] = sorted(
357
+ list(
358
+ filter(
359
+ lambda d: most_recent_folder.dir in d,
360
+ subdirs,
361
+ )
362
+ )
363
+ )
364
+ most_recent_folder.child_dirs = [
365
+ f
366
+ for f in potential_folders
367
+ if most_recent_folder.dir in f
368
+ and ".M" not in f
369
+ and ".D" in f
370
+ ]
371
+ return Ok(most_recent_folder)
372
+ else:
373
+ return Err("No sequence folder found, please give the full path.")
350
374
  except Exception as e:
351
375
  error = f"Failed to get sequence folder: {e}"
352
376
  return Err(error)
@@ -354,16 +378,20 @@ class SequenceController(RunController):
354
378
 
355
379
  def get_data_mult_uv(self, custom_path: Optional[str] = None):
356
380
  seq_data_dir = (
357
- SequenceDataFiles(dir=custom_path, child_dirs=[], sequence_name="")
381
+ SequenceDataFiles(dir=custom_path, sequence_name="")
358
382
  if custom_path
359
383
  else self.data_files[-1]
360
384
  )
361
385
  if len(seq_data_dir.child_dirs) == 0:
362
- self.data_files[-1] = self.fuzzy_match_most_recent_folder(
363
- seq_data_dir
364
- ).ok_value
386
+ search_folder = self.fuzzy_match_most_recent_folder(
387
+ seq_data_dir, set(seq_data_dir.child_dirs)
388
+ )
389
+ if search_folder.is_ok():
390
+ seq_data_dir = search_folder.ok_value
391
+ else:
392
+ raise FileNotFoundError(search_folder.err_value)
365
393
  all_w_spectra: List[Dict[int, AgilentHPLCChromatogram]] = []
366
- for row in self.data_files[-1].child_dirs:
394
+ for row in seq_data_dir.child_dirs:
367
395
  all_w_spectra.append(self.get_data_uv(custom_path=row))
368
396
  return all_w_spectra
369
397
 
@@ -381,13 +409,13 @@ class SequenceController(RunController):
381
409
  self, custom_path: Optional[str] = None
382
410
  ) -> List[AgilentChannelChromatogramData]:
383
411
  seq_file_dir = (
384
- SequenceDataFiles(dir=custom_path, child_dirs=[], sequence_name="")
412
+ SequenceDataFiles(dir=custom_path, sequence_name="")
385
413
  if custom_path
386
414
  else self.data_files[-1]
387
415
  )
388
416
  if len(seq_file_dir.child_dirs) == 0:
389
417
  self.data_files[-1] = self.fuzzy_match_most_recent_folder(
390
- seq_file_dir
418
+ seq_file_dir, set(seq_file_dir.child_dirs)
391
419
  ).ok_value
392
420
  spectra: List[AgilentChannelChromatogramData] = []
393
421
  for row in self.data_files[-1].child_dirs:
@@ -404,8 +432,10 @@ class SequenceController(RunController):
404
432
  self.data_files.append(
405
433
  self.fuzzy_match_most_recent_folder(
406
434
  most_recent_folder=SequenceDataFiles(
407
- dir=custom_path, child_dirs=[], sequence_name="NA"
408
- )
435
+ dir=custom_path,
436
+ sequence_name="NA",
437
+ ),
438
+ child_dirs=None,
409
439
  ).ok_value
410
440
  )
411
441
  parent_dir = self.data_files[-1]
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from ..abc_tables.device import DeviceController
2
4
  from ....control.controllers import CommunicationController
3
5
  from ....utils.injector_types import (
@@ -90,7 +92,7 @@ class InjectorController(DeviceController):
90
92
  )
91
93
  raise ValueError("No valid function found.")
92
94
 
93
- def load(self) -> InjectorTable:
95
+ def load(self) -> InjectorTable | None:
94
96
  rows = self.get_num_rows()
95
97
  if rows.is_ok():
96
98
  row_response = rows.value
@@ -101,6 +103,4 @@ class InjectorController(DeviceController):
101
103
  for i in range(int(row_response.num_response))
102
104
  ]
103
105
  )
104
- elif rows.is_err():
105
- return InjectorTable(functions=[])
106
- raise ValueError("Unexpected error")
106
+ raise ValueError("Couldn't read injector table rows.")
@@ -6,6 +6,7 @@ Authors: Lucy Hao
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import os.path
9
10
  from typing import Dict, List, Optional, Tuple, Union
10
11
 
11
12
  from pychemstation.analysis.chromatogram import AgilentChannelChromatogramData
@@ -31,14 +32,16 @@ class HPLCController:
31
32
 
32
33
  INJECTOR_TABLE = Table(register="RCWLS1Pretreatment[1]", name="InstructionTable")
33
34
 
35
+ DAD_TABLE = Table(register="RCDAD1Method[1]", name="Timetable")
36
+
34
37
  MSD_TABLE = Table(register="MSACQINFO[1]", name="SprayChamber")
35
38
 
36
39
  def __init__(
37
40
  self,
38
41
  comm_dir: str,
39
- method_dir: str,
40
- sequence_dir: str,
41
- data_dirs: List[str],
42
+ method_dir: Optional[str] = None,
43
+ sequence_dir: Optional[str] = None,
44
+ extra_data_dirs: Optional[List[str]] = None,
42
45
  offline: bool = False,
43
46
  debug: bool = False,
44
47
  ):
@@ -47,33 +50,42 @@ class HPLCController:
47
50
  double escaped: "C:\\my_folder\\"
48
51
 
49
52
  :param comm_dir: Name of directory for communication, where ChemStation will read and write from. Can be any existing directory.
50
- :param data_dirs: Name of directories for storing data after method or sequence runs. Method data dir is default
51
53
  the first one in the list. In other words, the first dir in the list is highest prio. Must be "normal" strings and not r-strings.
52
- :param method_dir: Name of directory where method files are stored.
53
- :param sequence_dir: Name of directory where sequence files are stored.
54
54
  :raises FileNotFoundError: If either `data_dir`, `method_dir`, `sequence_dir`, `sequence_data_dir`or `comm_dir` is not a valid directory.
55
55
  """
56
56
  self.comm: CommunicationController = CommunicationController(
57
- comm_dir=comm_dir, debug=debug
58
- )
59
- self.method_controller: MethodController = MethodController(
60
- controller=self.comm,
61
- src=method_dir,
62
- data_dirs=data_dirs,
63
- table=self.METHOD_TIMETABLE,
64
- offline=offline,
65
- injector_controller=InjectorController(
66
- controller=self.comm, table=self.INJECTOR_TABLE, offline=offline
67
- ),
68
- )
69
- self.sequence_controller: SequenceController = SequenceController(
70
- controller=self.comm,
71
- src=sequence_dir,
72
- data_dirs=data_dirs,
73
- table=self.SEQUENCE_TABLE,
74
- method_controller=self.method_controller,
75
- offline=offline,
57
+ comm_dir=comm_dir, debug=debug, offline=offline
76
58
  )
59
+ data_dirs: List[str] = []
60
+ if not offline:
61
+ if not method_dir or not sequence_dir or not extra_data_dirs:
62
+ method_dir, sequence_dir, data_dirs = self.comm.get_chemstation_dirs()
63
+ if extra_data_dirs:
64
+ data_dirs.extend(extra_data_dirs)
65
+ data_dirs = list(set([os.path.normpath(p) for p in data_dirs]))
66
+ if method_dir and sequence_dir and data_dirs:
67
+ self.method_controller: MethodController = MethodController(
68
+ controller=self.comm,
69
+ src=method_dir,
70
+ data_dirs=data_dirs,
71
+ table=self.METHOD_TIMETABLE,
72
+ offline=offline,
73
+ injector_controller=InjectorController(
74
+ controller=self.comm, table=self.INJECTOR_TABLE, offline=offline
75
+ ),
76
+ )
77
+ self.sequence_controller: SequenceController = SequenceController(
78
+ controller=self.comm,
79
+ src=sequence_dir,
80
+ data_dirs=data_dirs,
81
+ table=self.SEQUENCE_TABLE,
82
+ method_controller=self.method_controller,
83
+ offline=offline,
84
+ )
85
+ else:
86
+ raise ValueError(
87
+ "Expected a method dir, sequence dir and data dirs but they were None."
88
+ )
77
89
 
78
90
  def send(self, cmd: Union[Command, str]):
79
91
  """
@@ -279,7 +291,7 @@ class HPLCController:
279
291
  """Returns the currently loaded sequence."""
280
292
  return self.sequence_controller.load()
281
293
 
282
- def load_injector_program(self) -> InjectorTable:
294
+ def load_injector_program(self) -> InjectorTable | None:
283
295
  return self.method_controller.injector_controller.load()
284
296
 
285
297
  def standby(self):
@@ -48,6 +48,17 @@ class Command(Enum):
48
48
  SAVE_METHOD_CMD = 'SaveMethod _MethPath$, _MethFile$, "{commit_msg}"'
49
49
  GET_SEQUENCE_CMD = "response$ = _SeqFile$"
50
50
  RUN_SEQUENCE_CMD = "RunSequence"
51
+ CURRENT_RUNNING_SEQ_LINE = "response_num = _SEQCURRLINE1"
52
+
53
+ # Get directories
54
+ GET_METHOD_DIR = "response$ = _METHPATH$"
55
+ GET_SEQUENCE_DIR = "response$ = _SEQUENCEPATHS$"
56
+ GET_DATA_DIRS = "response$ = _DATAPATHS$"
57
+ GET_CURRENT_RUN_DATA_DIR = "response$ = _DATAPath$"
58
+ GET_CURRENT_RUN_DATA_FILE = "response$ = _DATAFILE1$"
59
+
60
+ # Debuggng
61
+ ERROR = "response$ = _ERROR$"
51
62
 
52
63
 
53
64
  class HPLCRunningStatus(Enum):
@@ -1,4 +1,4 @@
1
- from pychemstation.utils.mocking.abc_comm import ABCCommunicationController
1
+ from ...control.controllers.abc_tables.abc_comm import ABCCommunicationController
2
2
 
3
3
 
4
4
  class MockCommunicationController(ABCCommunicationController):
@@ -11,6 +11,7 @@ from pychemstation.utils.tray_types import Tray
11
11
  class SequenceDataFiles:
12
12
  sequence_name: str
13
13
  dir: str
14
+ _data_files: List[str] = field(default_factory=list)
14
15
  child_dirs: List[str] = field(default_factory=list)
15
16
 
16
17
 
@@ -38,9 +39,9 @@ class InjectionSource(Enum):
38
39
 
39
40
  @dataclass
40
41
  class SequenceEntry:
41
- sample_name: str
42
+ data_file: str
42
43
  vial_location: Tray
43
- data_file: Optional[str] = None
44
+ sample_name: Optional[str] = None
44
45
  method: Optional[str] = None
45
46
  num_inj: Optional[int] = 1
46
47
  inj_vol: Optional[float] = 2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pychemstation
3
- Version: 0.10.6
3
+ Version: 0.10.7
4
4
  Summary: Library to interact with Chemstation software, primarily used in Hein lab
5
5
  Project-URL: Documentation, https://pychemstation-e5a086.gitlab.io/pychemstation.html
6
6
  Project-URL: Repository, https://gitlab.com/heingroup/device-api/pychemstation
@@ -18,7 +18,7 @@ Requires-Dist: matplotlib>=3.7.5
18
18
  Requires-Dist: pandas>=2.0.3
19
19
  Requires-Dist: pdoc>=14.7.0
20
20
  Requires-Dist: polling>=0.3.2
21
- Requires-Dist: pre-commit>=4.2.0
21
+ Requires-Dist: pre-commit>=3.5.0
22
22
  Requires-Dist: pytest>=7.3.5
23
23
  Requires-Dist: rainbow-api>=1.0.10
24
24
  Requires-Dist: result>=0.17.0
@@ -90,7 +90,7 @@ DATA_DIR_2 = "C:\\Users\\Public\\Documents\\ChemStation\\2\\Data"
90
90
  DATA_DIR_3 = "C:\\Users\\Public\\Documents\\ChemStation\\3\\Data"
91
91
 
92
92
  # Initialize HPLC Controller
93
- hplc_controller = HPLCController(data_dirs=[DATA_DIR_2, DATA_DIR_3],
93
+ hplc_controller = HPLCController(extra_data_dirs=[DATA_DIR_2, DATA_DIR_3],
94
94
  comm_dir=DEFAULT_COMMAND_PATH,
95
95
  method_dir=DEFAULT_METHOD_DIR,
96
96
  sequence_dir=SEQUENCE_DIR)
@@ -3,40 +3,40 @@ pychemstation/analysis/__init__.py,sha256=mPNnp0TmkoUxrTGcT6wNKMyCiOar5vC0cTPmFL
3
3
  pychemstation/analysis/base_spectrum.py,sha256=t_VoxAtBph1V7S4fOsziERHiOBkYP0_nH7LTwbTEvcE,16529
4
4
  pychemstation/analysis/chromatogram.py,sha256=cHxPd5-miA6L3FjwN5cSFyq4xEeZoHWFFk8gU6tCgg4,3946
5
5
  pychemstation/analysis/process_report.py,sha256=xckcpqnYfzFTIo1nhgwP5A4GPJsW3PQ_qzuy5Z1KOuI,14714
6
- pychemstation/control/README.md,sha256=_7ITj4hD17YIwci6UY6xBebC9gPCBpzBFTB_Gx0eJBc,3124
6
+ pychemstation/control/README.md,sha256=yVMhIOq3YL-8EzqZDWGXt36GzSjzqiI4fwkJ35bAzD0,3130
7
7
  pychemstation/control/__init__.py,sha256=7lSkY7Qa7Ikdz82-2FESc_oqktv7JndsjltCkiMqnMI,147
8
- pychemstation/control/hplc.py,sha256=C4cnjsSsjgK_VcySGhbG1OMGEVoY3pYSA69dnvqwEL0,12526
8
+ pychemstation/control/hplc.py,sha256=PY6P_GTnVxL3rAwPHtJmiAm5iTw9X6xjYghyKpBpYFM,13063
9
9
  pychemstation/control/controllers/README.md,sha256=S5cd4NJmPjs6TUH98BtPJJhiS1Lu-mxLCNS786ogOrQ,32
10
10
  pychemstation/control/controllers/__init__.py,sha256=EWvvITwY6RID5b1ilVsPowP85uzmIt3LYW0rvyN-3x0,146
11
- pychemstation/control/controllers/comm.py,sha256=saTyM86kUZ7mevWPpna7LoMC-Q0KR4Pq0cURhCoiW8k,5018
11
+ pychemstation/control/controllers/comm.py,sha256=a3qp_u2vm_UO3VTDwKEEkJfHIWgF4Gxne0h03dQe0A8,5878
12
12
  pychemstation/control/controllers/abc_tables/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- pychemstation/control/controllers/abc_tables/device.py,sha256=HRea-dRwHf7u1aA1m2C9TmIBhhKKifmqVdPfZxmwoyk,442
14
- pychemstation/control/controllers/abc_tables/run.py,sha256=8RGvQ3NrXPidoGOilxyFoGHxaM1r1qn2d3TLLyOe4po,8202
15
- pychemstation/control/controllers/abc_tables/table.py,sha256=XriB53rn1YOAMCS4AGARKoniaXOQi5qUsBSNuLCn8ew,7196
13
+ pychemstation/control/controllers/abc_tables/abc_comm.py,sha256=G2JSCNwLrLYPYBdcf_WuIl6dvJORmmQe5wih6TdvPiU,5226
14
+ pychemstation/control/controllers/abc_tables/device.py,sha256=ZJQID_M53bUwVSleBL5MjFJFrExZO1xdYKRFn5VToug,641
15
+ pychemstation/control/controllers/abc_tables/run.py,sha256=L958yy3vBxN0qxHVfMb7wgxxHAvSQq4zggWlon6vPs4,9294
16
+ pychemstation/control/controllers/abc_tables/table.py,sha256=4c4W_9OzrY-SfH_oV6oBZkfn1Vp5XvNYbWUvbGY3G2s,7699
16
17
  pychemstation/control/controllers/data_aq/__init__.py,sha256=w-Zgbit10niOQfz780ZmRHjUFxV1hMkdui7fOMPqeLA,132
17
- pychemstation/control/controllers/data_aq/method.py,sha256=HnVMg-qwKSY7mTmw0AerMlVWHeu72YPfPBviTnzavA4,18214
18
- pychemstation/control/controllers/data_aq/sequence.py,sha256=Q77YkDTkIxUbsRYyN5OJt1a0HgD9TxHW-LiFRQDV8Kk,16516
18
+ pychemstation/control/controllers/data_aq/method.py,sha256=BqAW2XdlJJQj08P282h_NPTJofdBKiNJgdis9j0c9lc,18183
19
+ pychemstation/control/controllers/data_aq/sequence.py,sha256=here444uwwIH9m08ls8_wapA9088uvqX5DYCHFsarcU,17422
19
20
  pychemstation/control/controllers/devices/__init__.py,sha256=QpgGnLXyWiB96KIB98wMccEi8oOUUaLxvBCyevJzcOg,75
20
- pychemstation/control/controllers/devices/injector.py,sha256=NXJEKDGu-XJoQDbaJav6tD73S7UqFZqXJd5bcwdvwBQ,4047
21
+ pychemstation/control/controllers/devices/injector.py,sha256=OxrF-SbV006bCh_j9o-clavc_d8Loh3myhFerbvjLg4,4033
21
22
  pychemstation/generated/__init__.py,sha256=xnEs0QTjeuGYO3tVUIy8GDo95GqTV1peEjosGckpOu0,977
22
23
  pychemstation/generated/dad_method.py,sha256=xTUiSCvkXcxBUhjVm1YZKu-tHs16k23pF-0xYrQSwWA,8408
23
24
  pychemstation/generated/pump_method.py,sha256=s3MckKDw2-nZUC5lHrJVvXYdneWP8-9UvblNuGryPHY,12092
24
25
  pychemstation/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
26
  pychemstation/utils/injector_types.py,sha256=z2iWwTklGm0GRDCL9pnPCovQrwyRwxv8w5w5Xh7Pj3U,1152
26
- pychemstation/utils/macro.py,sha256=Y_0CwTHcDQKoxYrNHqdF14-ciwtx56Y65SzNtOFPhNk,2848
27
+ pychemstation/utils/macro.py,sha256=4Tn__0EeoDT3exbzTKde5epd7Oj-rhUh-l9EkXTHn7c,3242
27
28
  pychemstation/utils/method_types.py,sha256=_2djFz_uWFc9aoZcyPMIjC5KYBs003WPoQGx7ZhMiOg,1649
28
29
  pychemstation/utils/num_utils.py,sha256=dDs8sLZ_SdtvDKhyhF3IkljiVf16IYqpMTO5tEk9vMk,2079
29
30
  pychemstation/utils/parsing.py,sha256=mzdpxrH5ux4-_i4CwZvnIYnIwAnRnOptKb3fZyYJcx0,9307
30
31
  pychemstation/utils/pump_types.py,sha256=HWQHxscGn19NTrfYBwQRCO2VcYfwyko7YfBO5uDhEm4,93
31
- pychemstation/utils/sequence_types.py,sha256=XVAi1MCQrNqwbmzrBrVbapjD_AQg6A-l0nwjNsNAbKs,1924
32
+ pychemstation/utils/sequence_types.py,sha256=Bf_oGm-adRejDUmYrzV2GEx16NIO6jlH_ZMIYVKl8gg,1981
32
33
  pychemstation/utils/spec_utils.py,sha256=lS27Xi4mFNDWBfmBqOoxTcVchPAkLK2mSdoaWDOfaPI,10211
33
34
  pychemstation/utils/table_types.py,sha256=I5xy7tpmMWOMb6tFfWVE1r4wnSsgky5sZU9oNtHU9BE,3451
34
35
  pychemstation/utils/tray_types.py,sha256=FUjCUnozt5ZjCl5Vg9FioPHGTdKa43SQI1QPVkoBmV0,6078
35
36
  pychemstation/utils/mocking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- pychemstation/utils/mocking/abc_comm.py,sha256=zt1_6IBme6FiRjZ3zvUvlSdToBb85u3th8NKKGprn_Q,5246
37
- pychemstation/utils/mocking/mock_comm.py,sha256=nYSK2S85iDACIY73rX56yyqyaWo6Oaj3H7gvMsrkRJA,150
37
+ pychemstation/utils/mocking/mock_comm.py,sha256=4DcKmUxp-LYNXjywT_za1_GpqKa4sFTj7F2V3r_qsA0,156
38
38
  pychemstation/utils/mocking/mock_hplc.py,sha256=Hx6127C7d3miYGCZYxbN-Q3PU8kpMgXYX2n6we2Twgw,25
39
- pychemstation-0.10.6.dist-info/METADATA,sha256=yfxKdWhVOQkClZFBqiluL41D6PH3XkVZ9SoXxKQlY8Q,5933
40
- pychemstation-0.10.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
41
- pychemstation-0.10.6.dist-info/licenses/LICENSE,sha256=9bdF75gIf1MecZ7oymqWgJREVz7McXPG-mjqrTmzzD8,18658
42
- pychemstation-0.10.6.dist-info/RECORD,,
39
+ pychemstation-0.10.7.dist-info/METADATA,sha256=oDYpd4ohtIgRyHstF4Pdnn-EP5YSN0ozESFeAQLu_CE,5939
40
+ pychemstation-0.10.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
41
+ pychemstation-0.10.7.dist-info/licenses/LICENSE,sha256=9bdF75gIf1MecZ7oymqWgJREVz7McXPG-mjqrTmzzD8,18658
42
+ pychemstation-0.10.7.dist-info/RECORD,,