pychemstation 0.10.5__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.
- pychemstation/analysis/__init__.py +8 -1
- pychemstation/control/README.md +1 -1
- pychemstation/control/controllers/__init__.py +2 -2
- pychemstation/control/controllers/abc_tables/__init__.py +0 -0
- pychemstation/control/controllers/abc_tables/abc_comm.py +155 -0
- pychemstation/control/controllers/abc_tables/device.py +20 -0
- pychemstation/control/controllers/{tables/table.py → abc_tables/run.py} +58 -201
- pychemstation/control/controllers/abc_tables/table.py +230 -0
- pychemstation/control/controllers/comm.py +26 -101
- pychemstation/control/controllers/{tables → data_aq}/method.py +12 -15
- pychemstation/control/controllers/{tables → data_aq}/sequence.py +168 -119
- pychemstation/control/controllers/devices/__init__.py +3 -0
- pychemstation/control/controllers/devices/injector.py +61 -28
- pychemstation/control/hplc.py +42 -26
- pychemstation/utils/injector_types.py +22 -2
- pychemstation/utils/macro.py +11 -0
- pychemstation/utils/mocking/__init__.py +0 -0
- pychemstation/utils/mocking/mock_comm.py +5 -0
- pychemstation/utils/mocking/mock_hplc.py +2 -0
- pychemstation/utils/sequence_types.py +22 -2
- pychemstation/utils/table_types.py +6 -0
- pychemstation/utils/tray_types.py +36 -1
- {pychemstation-0.10.5.dist-info → pychemstation-0.10.7.dist-info}/METADATA +3 -3
- pychemstation-0.10.7.dist-info/RECORD +42 -0
- pychemstation/control/controllers/devices/device.py +0 -74
- pychemstation-0.10.5.dist-info/RECORD +0 -36
- /pychemstation/control/controllers/{tables → data_aq}/__init__.py +0 -0
- {pychemstation-0.10.5.dist-info → pychemstation-0.10.7.dist-info}/WHEEL +0 -0
- {pychemstation-0.10.5.dist-info → pychemstation-0.10.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,230 @@
|
|
1
|
+
"""
|
2
|
+
Abstract module containing shared logic for Method and Sequence tables.
|
3
|
+
|
4
|
+
Authors: Lucy Hao
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
import abc
|
10
|
+
from typing import Optional, Union
|
11
|
+
|
12
|
+
from result import Err, Result
|
13
|
+
|
14
|
+
from ....control.controllers.comm import CommunicationController
|
15
|
+
from ....utils.macro import Command, Response
|
16
|
+
from ....utils.method_types import MethodDetails
|
17
|
+
from ....utils.sequence_types import SequenceTable
|
18
|
+
from ....utils.table_types import RegisterFlag, Table, TableOperation
|
19
|
+
|
20
|
+
TableType = Union[MethodDetails, SequenceTable]
|
21
|
+
|
22
|
+
|
23
|
+
class ABCTableController(abc.ABC):
|
24
|
+
def __init__(
|
25
|
+
self,
|
26
|
+
controller: Optional[CommunicationController],
|
27
|
+
table: Table,
|
28
|
+
):
|
29
|
+
self.controller = controller
|
30
|
+
self.table_locator = table
|
31
|
+
self.table_state: Optional[TableType] = None
|
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
|
+
|
38
|
+
def receive(self) -> Result[Response, str]:
|
39
|
+
if self.controller:
|
40
|
+
for _ in range(10):
|
41
|
+
try:
|
42
|
+
return self.controller.receive()
|
43
|
+
except IndexError:
|
44
|
+
continue
|
45
|
+
return Err("Could not parse response")
|
46
|
+
else:
|
47
|
+
raise ValueError("Controller is offline!")
|
48
|
+
|
49
|
+
def send(self, cmd: Union[Command, str]):
|
50
|
+
if not self.controller:
|
51
|
+
raise RuntimeError(
|
52
|
+
"Communication controller must be initialized before sending command. It is currently in offline mode."
|
53
|
+
)
|
54
|
+
self.controller.send(cmd)
|
55
|
+
|
56
|
+
def sleepy_send(self, cmd: Union[Command, str]):
|
57
|
+
if self.controller:
|
58
|
+
self.controller.sleepy_send(cmd)
|
59
|
+
else:
|
60
|
+
raise ValueError("Controller is offline")
|
61
|
+
|
62
|
+
def sleep(self, seconds: int):
|
63
|
+
"""
|
64
|
+
Tells the HPLC to wait for a specified number of seconds.
|
65
|
+
|
66
|
+
:param seconds: number of seconds to wait
|
67
|
+
"""
|
68
|
+
self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
|
69
|
+
|
70
|
+
def get_num(self, row: int, col_name: RegisterFlag) -> Union[int, float]:
|
71
|
+
if self.controller:
|
72
|
+
return self.controller.get_num_val(
|
73
|
+
TableOperation.GET_ROW_VAL.value.format(
|
74
|
+
register=self.table_locator.register,
|
75
|
+
table_name=self.table_locator.name,
|
76
|
+
row=row,
|
77
|
+
col_name=col_name.value,
|
78
|
+
)
|
79
|
+
)
|
80
|
+
else:
|
81
|
+
raise ValueError("Controller is offline")
|
82
|
+
|
83
|
+
def get_text(self, row: int, col_name: RegisterFlag) -> str:
|
84
|
+
if self.controller:
|
85
|
+
return self.controller.get_text_val(
|
86
|
+
TableOperation.GET_ROW_TEXT.value.format(
|
87
|
+
register=self.table_locator.register,
|
88
|
+
table_name=self.table_locator.name,
|
89
|
+
row=row,
|
90
|
+
col_name=col_name.value,
|
91
|
+
)
|
92
|
+
)
|
93
|
+
else:
|
94
|
+
raise ValueError("Controller is offline")
|
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.")
|
99
|
+
self.sleepy_send(
|
100
|
+
TableOperation.NEW_COL_VAL.value.format(
|
101
|
+
register=self.table_locator.register,
|
102
|
+
table_name=self.table_locator.name,
|
103
|
+
col_name=col_name,
|
104
|
+
val=val,
|
105
|
+
)
|
106
|
+
)
|
107
|
+
|
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.")
|
111
|
+
self.sleepy_send(
|
112
|
+
TableOperation.NEW_COL_TEXT.value.format(
|
113
|
+
register=self.table_locator.register,
|
114
|
+
table_name=self.table_locator.name,
|
115
|
+
col_name=col_name,
|
116
|
+
val=val,
|
117
|
+
)
|
118
|
+
)
|
119
|
+
|
120
|
+
def _edit_row_num(
|
121
|
+
self, col_name: RegisterFlag, val: Union[int, float], row: Optional[int] = None
|
122
|
+
):
|
123
|
+
if not (isinstance(val, int) or isinstance(val, float)):
|
124
|
+
raise ValueError(f"{val} must be an int or float.")
|
125
|
+
if row:
|
126
|
+
num_rows = self.get_num_rows()
|
127
|
+
if num_rows.is_ok():
|
128
|
+
if num_rows.ok_value.num_response < row:
|
129
|
+
raise ValueError("Not enough rows to edit!")
|
130
|
+
|
131
|
+
self.sleepy_send(
|
132
|
+
TableOperation.EDIT_ROW_VAL.value.format(
|
133
|
+
register=self.table_locator.register,
|
134
|
+
table_name=self.table_locator.name,
|
135
|
+
row=row if row is not None else "Rows",
|
136
|
+
col_name=col_name,
|
137
|
+
val=val,
|
138
|
+
)
|
139
|
+
)
|
140
|
+
|
141
|
+
def _edit_row_text(
|
142
|
+
self, col_name: RegisterFlag, val: str, row: Optional[int] = None
|
143
|
+
):
|
144
|
+
if not isinstance(val, str):
|
145
|
+
raise ValueError(f"{val} must be a str.")
|
146
|
+
if row:
|
147
|
+
num_rows = self.get_num_rows()
|
148
|
+
if num_rows.is_ok():
|
149
|
+
if num_rows.ok_value.num_response < row:
|
150
|
+
raise ValueError("Not enough rows to edit!")
|
151
|
+
|
152
|
+
self.sleepy_send(
|
153
|
+
TableOperation.EDIT_ROW_TEXT.value.format(
|
154
|
+
register=self.table_locator.register,
|
155
|
+
table_name=self.table_locator.name,
|
156
|
+
row=row if row is not None else "Rows",
|
157
|
+
col_name=col_name,
|
158
|
+
val=val,
|
159
|
+
)
|
160
|
+
)
|
161
|
+
|
162
|
+
@abc.abstractmethod
|
163
|
+
def get_row(self, row: int):
|
164
|
+
pass
|
165
|
+
|
166
|
+
def delete_row(self, row: int):
|
167
|
+
self.sleepy_send(
|
168
|
+
TableOperation.DELETE_ROW.value.format(
|
169
|
+
register=self.table_locator.register,
|
170
|
+
table_name=self.table_locator.name,
|
171
|
+
row=row,
|
172
|
+
)
|
173
|
+
)
|
174
|
+
|
175
|
+
def add_row(self):
|
176
|
+
"""
|
177
|
+
Adds a row to the provided table for currently loaded method or sequence.
|
178
|
+
"""
|
179
|
+
self.sleepy_send(
|
180
|
+
TableOperation.NEW_ROW.value.format(
|
181
|
+
register=self.table_locator.register, table_name=self.table_locator.name
|
182
|
+
)
|
183
|
+
)
|
184
|
+
|
185
|
+
def delete_table(self):
|
186
|
+
"""
|
187
|
+
Deletes the table for the current loaded method or sequence.
|
188
|
+
"""
|
189
|
+
self.sleepy_send(
|
190
|
+
TableOperation.DELETE_TABLE.value.format(
|
191
|
+
register=self.table_locator.register, table_name=self.table_locator.name
|
192
|
+
)
|
193
|
+
)
|
194
|
+
|
195
|
+
def new_table(self):
|
196
|
+
"""
|
197
|
+
Creates the table for the currently loaded method or sequence.
|
198
|
+
"""
|
199
|
+
self.send(
|
200
|
+
TableOperation.CREATE_TABLE.value.format(
|
201
|
+
register=self.table_locator.register, table_name=self.table_locator.name
|
202
|
+
)
|
203
|
+
)
|
204
|
+
|
205
|
+
def get_num_rows(self) -> Result[Response, str]:
|
206
|
+
self.send(
|
207
|
+
TableOperation.GET_NUM_ROWS.value.format(
|
208
|
+
register=self.table_locator.register,
|
209
|
+
table_name=self.table_locator.name,
|
210
|
+
col_name=RegisterFlag.NUM_ROWS,
|
211
|
+
)
|
212
|
+
)
|
213
|
+
self.send(
|
214
|
+
Command.GET_ROWS_CMD.value.format(
|
215
|
+
register=self.table_locator.register,
|
216
|
+
table_name=self.table_locator.name,
|
217
|
+
col_name=RegisterFlag.NUM_ROWS,
|
218
|
+
)
|
219
|
+
)
|
220
|
+
if self.controller:
|
221
|
+
res = self.controller.receive()
|
222
|
+
else:
|
223
|
+
raise ValueError("Controller is offline")
|
224
|
+
|
225
|
+
if res.is_ok():
|
226
|
+
self.send("Sleep 0.1")
|
227
|
+
self.send("Print Rows")
|
228
|
+
return res
|
229
|
+
else:
|
230
|
+
return Err("No rows could be read.")
|
@@ -10,30 +10,25 @@ been processed.
|
|
10
10
|
Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
|
11
11
|
"""
|
12
12
|
|
13
|
-
import os
|
14
13
|
import time
|
15
|
-
from typing import Optional, Union
|
14
|
+
from typing import Optional, Union, Tuple, List
|
16
15
|
|
17
16
|
from result import Err, Ok, Result
|
18
17
|
|
19
18
|
from ...utils.macro import (
|
20
|
-
str_to_status,
|
21
|
-
HPLCAvailStatus,
|
22
|
-
HPLCErrorStatus,
|
23
19
|
Command,
|
20
|
+
HPLCErrorStatus,
|
24
21
|
Status,
|
25
|
-
|
22
|
+
str_to_status,
|
26
23
|
)
|
24
|
+
from .abc_tables.abc_comm import ABCCommunicationController
|
27
25
|
|
28
26
|
|
29
|
-
class CommunicationController:
|
27
|
+
class CommunicationController(ABCCommunicationController):
|
30
28
|
"""
|
31
29
|
Class that communicates with Agilent using Macros
|
32
30
|
"""
|
33
31
|
|
34
|
-
# maximum command number
|
35
|
-
MAX_CMD_NO = 255
|
36
|
-
|
37
32
|
def __init__(
|
38
33
|
self,
|
39
34
|
comm_dir: str,
|
@@ -48,24 +43,7 @@ class CommunicationController:
|
|
48
43
|
:param reply_file: Name of reply file
|
49
44
|
:param debug: whether to save log of sent commands
|
50
45
|
"""
|
51
|
-
|
52
|
-
self.debug = debug
|
53
|
-
if os.path.isdir(comm_dir):
|
54
|
-
self.cmd_file = os.path.join(comm_dir, cmd_file)
|
55
|
-
self.reply_file = os.path.join(comm_dir, reply_file)
|
56
|
-
self.cmd_no = 0
|
57
|
-
else:
|
58
|
-
raise FileNotFoundError(f"comm_dir: {comm_dir} not found.")
|
59
|
-
|
60
|
-
# Create files for Chemstation to communicate with Python
|
61
|
-
open(self.cmd_file, "a").close()
|
62
|
-
open(self.reply_file, "a").close()
|
63
|
-
|
64
|
-
self.reset_cmd_counter()
|
65
|
-
|
66
|
-
# Initialize row counter for table operations
|
67
|
-
self._most_recent_hplc_status: Status = self.get_status()
|
68
|
-
self.send("Local Rows")
|
46
|
+
super().__init__(comm_dir, cmd_file, reply_file, offline, debug)
|
69
47
|
|
70
48
|
def get_num_val(self, cmd: str) -> Union[int, float]:
|
71
49
|
tries = 10
|
@@ -98,7 +76,7 @@ class CommunicationController:
|
|
98
76
|
if res.is_err():
|
99
77
|
return HPLCErrorStatus.NORESPONSE
|
100
78
|
if res.is_ok():
|
101
|
-
parsed_response = self.receive().
|
79
|
+
parsed_response = self.receive().ok_value.string_response
|
102
80
|
self._most_recent_hplc_status = str_to_status(parsed_response)
|
103
81
|
return self._most_recent_hplc_status
|
104
82
|
else:
|
@@ -108,21 +86,6 @@ class CommunicationController:
|
|
108
86
|
except IndexError:
|
109
87
|
return HPLCErrorStatus.MALFORMED
|
110
88
|
|
111
|
-
def set_status(self):
|
112
|
-
"""Updates current status of HPLC machine"""
|
113
|
-
self._most_recent_hplc_status = self.get_status()
|
114
|
-
|
115
|
-
def check_if_not_running(self) -> bool:
|
116
|
-
"""Checks if HPLC machine is in an available state, meaning a state that data is not being written.
|
117
|
-
|
118
|
-
:return: whether the HPLC machine is in a safe state to retrieve data back."""
|
119
|
-
self.set_status()
|
120
|
-
hplc_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
|
121
|
-
time.sleep(5)
|
122
|
-
self.set_status()
|
123
|
-
hplc_actually_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
|
124
|
-
return hplc_avail and hplc_actually_avail
|
125
|
-
|
126
89
|
def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
|
127
90
|
"""Low-level execution primitive. Sends a command string to HPLC.
|
128
91
|
|
@@ -186,61 +149,23 @@ class CommunicationController:
|
|
186
149
|
f"Failed to receive reply to command #{cmd_no} due to {err} caused by {err_msg}."
|
187
150
|
)
|
188
151
|
|
189
|
-
def
|
190
|
-
|
191
|
-
self.send(
|
192
|
-
self.
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
if self.debug:
|
206
|
-
f = open("out.txt", "a")
|
207
|
-
f.write(cmd_to_send + "\n")
|
208
|
-
f.close()
|
209
|
-
|
210
|
-
def receive(self) -> Result[Response, str]:
|
211
|
-
"""Returns messages received in reply file.
|
212
|
-
|
213
|
-
:return: ChemStation response
|
214
|
-
"""
|
215
|
-
num_response_prefix = "Numerical Responses:"
|
216
|
-
str_response_prefix = "String Responses:"
|
217
|
-
possible_response = self._receive(self.cmd_no)
|
218
|
-
if possible_response.is_ok():
|
219
|
-
lines = possible_response.ok_value.splitlines()
|
220
|
-
for line in lines:
|
221
|
-
if str_response_prefix in line and num_response_prefix in line:
|
222
|
-
string_responses_dirty, _, numerical_responses = line.partition(
|
223
|
-
num_response_prefix
|
224
|
-
)
|
225
|
-
_, _, string_responses = string_responses_dirty.partition(
|
226
|
-
str_response_prefix
|
227
|
-
)
|
228
|
-
return Ok(
|
229
|
-
Response(
|
230
|
-
string_response=string_responses.strip(),
|
231
|
-
num_response=float(numerical_responses.strip()),
|
232
|
-
)
|
233
|
-
)
|
234
|
-
return Err("Could not retrieve HPLC response")
|
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
|
235
168
|
else:
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
"""Resets the command counter."""
|
240
|
-
self._send(Command.RESET_COUNTER_CMD.value, cmd_no=self.MAX_CMD_NO + 1)
|
241
|
-
self._receive(cmd_no=self.MAX_CMD_NO + 1)
|
242
|
-
self.cmd_no = 0
|
243
|
-
|
244
|
-
def stop_macro(self):
|
245
|
-
"""Stops Macro execution. Connection will be lost."""
|
246
|
-
self.send(Command.STOP_MACRO_CMD)
|
169
|
+
raise ValueError(
|
170
|
+
"Please provide the method, sequence and data directories, could not be found."
|
171
|
+
)
|
@@ -3,10 +3,11 @@ 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
|
|
10
|
+
from ..abc_tables.run import RunController
|
10
11
|
from ....analysis.process_report import AgilentReport, ReportType
|
11
12
|
from ....control.controllers import CommunicationController
|
12
13
|
from pychemstation.analysis.chromatogram import (
|
@@ -24,10 +25,9 @@ from ....utils.method_types import (
|
|
24
25
|
)
|
25
26
|
from ....utils.table_types import RegisterFlag, Table, TableOperation, T
|
26
27
|
from ..devices.injector import InjectorController
|
27
|
-
from .table import TableController
|
28
28
|
|
29
29
|
|
30
|
-
class MethodController(
|
30
|
+
class MethodController(RunController):
|
31
31
|
"""
|
32
32
|
Class containing method related logic
|
33
33
|
"""
|
@@ -413,8 +413,6 @@ class MethodController(TableController):
|
|
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(TableController):
|
|
427
425
|
else experiment_name,
|
428
426
|
)
|
429
427
|
)
|
430
|
-
|
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(
|
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(TableController):
|
|
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(
|
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)
|