sl-shared-assets 2.0.1__py3-none-any.whl → 3.0.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sl-shared-assets might be problematic. Click here for more details.
- sl_shared_assets/__init__.py +9 -5
- sl_shared_assets/__init__.pyi +4 -4
- sl_shared_assets/cli.py +262 -21
- sl_shared_assets/cli.pyi +48 -5
- sl_shared_assets/data_classes/configuration_data.py +20 -0
- sl_shared_assets/data_classes/configuration_data.pyi +14 -0
- sl_shared_assets/data_classes/runtime_data.py +17 -2
- sl_shared_assets/data_classes/runtime_data.pyi +6 -2
- sl_shared_assets/data_classes/session_data.py +7 -11
- sl_shared_assets/data_classes/session_data.pyi +1 -2
- sl_shared_assets/server/__init__.py +2 -2
- sl_shared_assets/server/__init__.pyi +5 -2
- sl_shared_assets/server/job.py +229 -1
- sl_shared_assets/server/job.pyi +111 -0
- sl_shared_assets/server/server.py +365 -31
- sl_shared_assets/server/server.pyi +144 -15
- sl_shared_assets/tools/__init__.py +2 -1
- sl_shared_assets/tools/__init__.pyi +2 -0
- sl_shared_assets/tools/packaging_tools.py +1 -2
- sl_shared_assets/tools/project_management_tools.py +150 -34
- sl_shared_assets/tools/project_management_tools.pyi +46 -3
- {sl_shared_assets-2.0.1.dist-info → sl_shared_assets-3.0.0rc2.dist-info}/METADATA +5 -6
- sl_shared_assets-3.0.0rc2.dist-info/RECORD +36 -0
- {sl_shared_assets-2.0.1.dist-info → sl_shared_assets-3.0.0rc2.dist-info}/entry_points.txt +2 -0
- sl_shared_assets-2.0.1.dist-info/RECORD +0 -36
- {sl_shared_assets-2.0.1.dist-info → sl_shared_assets-3.0.0rc2.dist-info}/WHEEL +0 -0
- {sl_shared_assets-2.0.1.dist-info → sl_shared_assets-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import field, dataclass
|
|
2
2
|
|
|
3
|
+
from _typeshed import Incomplete
|
|
3
4
|
from ataraxis_data_structures import YamlConfig
|
|
4
5
|
|
|
5
6
|
@dataclass()
|
|
@@ -50,11 +51,12 @@ class LickTrainingDescriptor(YamlConfig):
|
|
|
50
51
|
experimenter: str
|
|
51
52
|
mouse_weight_g: float
|
|
52
53
|
dispensed_water_volume_ml: float
|
|
53
|
-
|
|
54
|
+
minimum_reward_delay_s: int
|
|
54
55
|
maximum_reward_delay_s: int
|
|
55
56
|
maximum_water_volume_ml: float
|
|
56
57
|
maximum_training_time_m: int
|
|
57
58
|
maximum_unconsumed_rewards: int = ...
|
|
59
|
+
system_state_codes: dict[str, int] = field(default_factory=Incomplete)
|
|
58
60
|
experimenter_notes: str = ...
|
|
59
61
|
experimenter_given_water_volume_ml: float = ...
|
|
60
62
|
incomplete: bool = ...
|
|
@@ -77,6 +79,7 @@ class RunTrainingDescriptor(YamlConfig):
|
|
|
77
79
|
maximum_training_time_m: int
|
|
78
80
|
maximum_unconsumed_rewards: int = ...
|
|
79
81
|
maximum_idle_time_s: float = ...
|
|
82
|
+
system_state_codes: dict[str, int] = field(default_factory=Incomplete)
|
|
80
83
|
experimenter_notes: str = ...
|
|
81
84
|
experimenter_given_water_volume_ml: float = ...
|
|
82
85
|
incomplete: bool = ...
|
|
@@ -89,6 +92,7 @@ class MesoscopeExperimentDescriptor(YamlConfig):
|
|
|
89
92
|
mouse_weight_g: float
|
|
90
93
|
dispensed_water_volume_ml: float
|
|
91
94
|
maximum_unconsumed_rewards: int = ...
|
|
95
|
+
system_state_codes: dict[str, int] = field(default_factory=Incomplete)
|
|
92
96
|
experimenter_notes: str = ...
|
|
93
97
|
experimenter_given_water_volume_ml: float = ...
|
|
94
98
|
incomplete: bool = ...
|
|
@@ -299,23 +299,21 @@ class ProcessedData:
|
|
|
299
299
|
behavior_data_path: Path = Path()
|
|
300
300
|
"""Stores the path to the directory that contains the non-video and non-brain-activity data extracted from
|
|
301
301
|
.npz log files by our in-house log parsing pipeline."""
|
|
302
|
-
job_logs_path: Path = Path()
|
|
303
|
-
"""Stores the path to the directory that stores the standard output and standard error data collected during
|
|
304
|
-
server-side data processing pipeline runtimes. This directory is primarily used when running data processing jobs
|
|
305
|
-
on the remote server. However, it is possible to configure local runtimes to also redirect log data to files
|
|
306
|
-
stored in this directory (by editing ataraxis-base-utilities 'console' variable)."""
|
|
307
302
|
suite2p_processing_tracker_path: Path = Path()
|
|
308
303
|
"""Stores the path to the suite2p_processing_tracker.yaml tracker file. This file stores the current state of the
|
|
309
304
|
sl-suite2p single-day data processing pipeline."""
|
|
310
|
-
dataset_formation_tracker_path: Path = Path()
|
|
311
|
-
"""Same as suite2p_processing_tracker_path, but stores the current state of the dataset formation process that
|
|
312
|
-
includes this session (communicates whether the session has been successfully added to any dataset(s))."""
|
|
313
305
|
behavior_processing_tracker_path: Path = Path()
|
|
314
306
|
"""Stores the path to the behavior_processing_tracker.yaml file. This file stores the current state of the
|
|
315
307
|
behavior (log) data processing pipeline."""
|
|
316
308
|
video_processing_tracker_path: Path = Path()
|
|
317
309
|
"""Stores the path to the video_processing_tracker.yaml file. This file stores the current state of the video
|
|
318
310
|
tracking (DeepLabCut) processing pipeline."""
|
|
311
|
+
p53_path: Path = Path()
|
|
312
|
+
"""Stores the path to the p53.bin file. This file serves as a lock-in marker that determines whether the session is
|
|
313
|
+
in the processing or dataset mode. Specifically, if the file does not exist, the session data cannot be integrated
|
|
314
|
+
into any dataset, as it may be actively worked on by processing pipelines. Conversely, if the marker exists,
|
|
315
|
+
processing pipelines are not allowed to work with the session, as it may be actively integrated into one or more
|
|
316
|
+
datasets."""
|
|
319
317
|
|
|
320
318
|
def resolve_paths(self, root_directory_path: Path) -> None:
|
|
321
319
|
"""Resolves all paths managed by the class instance based on the input root directory path.
|
|
@@ -333,11 +331,10 @@ class ProcessedData:
|
|
|
333
331
|
self.camera_data_path = self.processed_data_path.joinpath("camera_data")
|
|
334
332
|
self.mesoscope_data_path = self.processed_data_path.joinpath("mesoscope_data")
|
|
335
333
|
self.behavior_data_path = self.processed_data_path.joinpath("behavior_data")
|
|
336
|
-
self.job_logs_path = self.processed_data_path.joinpath("job_logs")
|
|
337
334
|
self.suite2p_processing_tracker_path = self.processed_data_path.joinpath("suite2p_processing_tracker.yaml")
|
|
338
|
-
self.dataset_formation_tracker_path = self.processed_data_path.joinpath("dataset_formation_tracker.yaml")
|
|
339
335
|
self.behavior_processing_tracker_path = self.processed_data_path.joinpath("behavior_processing_tracker.yaml")
|
|
340
336
|
self.video_processing_tracker_path = self.processed_data_path.joinpath("video_processing_tracker.yaml")
|
|
337
|
+
self.p53_path = self.processed_data_path.joinpath("p53.bin")
|
|
341
338
|
|
|
342
339
|
def make_directories(self) -> None:
|
|
343
340
|
"""Ensures that all major subdirectories and the root directory exist, creating any missing directories."""
|
|
@@ -345,7 +342,6 @@ class ProcessedData:
|
|
|
345
342
|
ensure_directory_exists(self.processed_data_path)
|
|
346
343
|
ensure_directory_exists(self.camera_data_path)
|
|
347
344
|
ensure_directory_exists(self.behavior_data_path)
|
|
348
|
-
ensure_directory_exists(self.job_logs_path)
|
|
349
345
|
|
|
350
346
|
|
|
351
347
|
@dataclass
|
|
@@ -132,11 +132,10 @@ class ProcessedData:
|
|
|
132
132
|
camera_data_path: Path = ...
|
|
133
133
|
mesoscope_data_path: Path = ...
|
|
134
134
|
behavior_data_path: Path = ...
|
|
135
|
-
job_logs_path: Path = ...
|
|
136
135
|
suite2p_processing_tracker_path: Path = ...
|
|
137
|
-
dataset_formation_tracker_path: Path = ...
|
|
138
136
|
behavior_processing_tracker_path: Path = ...
|
|
139
137
|
video_processing_tracker_path: Path = ...
|
|
138
|
+
p53_path: Path = ...
|
|
140
139
|
def resolve_paths(self, root_directory_path: Path) -> None:
|
|
141
140
|
"""Resolves all paths managed by the class instance based on the input root directory path.
|
|
142
141
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
and other compute servers. This package is also used across all Sun lab members private code to interface with the
|
|
3
3
|
shared server."""
|
|
4
4
|
|
|
5
|
-
from .job import Job
|
|
5
|
+
from .job import Job, JupyterJob
|
|
6
6
|
from .server import Server, ServerCredentials, generate_server_credentials
|
|
7
7
|
|
|
8
|
-
__all__ = ["Server", "ServerCredentials", "generate_server_credentials", "Job"]
|
|
8
|
+
__all__ = ["Server", "ServerCredentials", "generate_server_credentials", "Job", "JupyterJob"]
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
from .job import
|
|
1
|
+
from .job import (
|
|
2
|
+
Job as Job,
|
|
3
|
+
JupyterJob as JupyterJob,
|
|
4
|
+
)
|
|
2
5
|
from .server import (
|
|
3
6
|
Server as Server,
|
|
4
7
|
ServerCredentials as ServerCredentials,
|
|
5
8
|
generate_server_credentials as generate_server_credentials,
|
|
6
9
|
)
|
|
7
10
|
|
|
8
|
-
__all__ = ["Server", "ServerCredentials", "generate_server_credentials", "Job"]
|
|
11
|
+
__all__ = ["Server", "ServerCredentials", "generate_server_credentials", "Job", "JupyterJob"]
|
sl_shared_assets/server/job.py
CHANGED
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
"""This module provides the core Job class, used as the starting point for all SLURM-managed job executed on lab compute
|
|
2
2
|
server(s). Specifically, the Job class acts as a wrapper around the SLURM configuration and specific logic of each
|
|
3
3
|
job. During runtime, Server class interacts with input job objects to manage their transfer and execution on the
|
|
4
|
-
remote servers.
|
|
4
|
+
remote servers.
|
|
5
|
+
|
|
6
|
+
Since version 3.0.0, this module also provides the specialized JupyterJob class used to launch remote Jupyter
|
|
7
|
+
notebook servers.
|
|
8
|
+
"""
|
|
5
9
|
|
|
6
10
|
# noinspection PyProtectedMember
|
|
11
|
+
import re
|
|
7
12
|
from pathlib import Path
|
|
8
13
|
import datetime
|
|
14
|
+
from dataclasses import dataclass
|
|
9
15
|
|
|
16
|
+
# noinspection PyProtectedMember
|
|
10
17
|
from simple_slurm import Slurm # type: ignore
|
|
18
|
+
from ataraxis_base_utilities import LogLevel, console
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class _JupyterConnectionInfo:
|
|
23
|
+
"""Stores the data used to establish the connection with a Jupyter notebook server running under SLURM control on a
|
|
24
|
+
remote Sun lab server.
|
|
25
|
+
|
|
26
|
+
More specifically, this class is used to transfer the connection metadata collected on the remote server back to
|
|
27
|
+
the local machine that requested the server to be established.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
compute_node: str
|
|
31
|
+
"""The hostname of the compute node where Jupyter is running."""
|
|
32
|
+
|
|
33
|
+
port: int
|
|
34
|
+
"""The port number on which Jupyter is listening for communication. Usually, this is the default port 8888 or 9999.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
token: str
|
|
38
|
+
"""The authentication token for the Jupyter server. This token is used to authenticate the user when establishing
|
|
39
|
+
communication with the Jupyter server."""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def localhost_url(self) -> str:
|
|
43
|
+
"""Returns the localhost URL for connecting to the server.
|
|
44
|
+
|
|
45
|
+
To use this URL, first set up an SSH tunnel to the server via the specific Jupyter communication port and the
|
|
46
|
+
remote server access credentials.
|
|
47
|
+
"""
|
|
48
|
+
return f"http://localhost:{self.port}/?token={self.token}"
|
|
11
49
|
|
|
12
50
|
|
|
13
51
|
class Job:
|
|
@@ -138,3 +176,193 @@ class Job:
|
|
|
138
176
|
|
|
139
177
|
# Returns the script content to caller as a string
|
|
140
178
|
return fixed_script_content
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class JupyterJob(Job):
|
|
182
|
+
"""Specialized Job instance designed to launch a Jupyter notebook server on SLURM.
|
|
183
|
+
|
|
184
|
+
This class extends the base Job class to include Jupyter-specific configuration and commands for starting a
|
|
185
|
+
notebook server in a SLURM environment. Using this specialized job allows users to set up remote Jupyter servers
|
|
186
|
+
while still benefitting from SLURM's job management and fair airtime policies.
|
|
187
|
+
|
|
188
|
+
Notes:
|
|
189
|
+
Jupyter servers directly compete for resources with headless data processing jobs. Therefore, it is important
|
|
190
|
+
to minimize the resource footprint and the runtime of each Jupyter server, if possible.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
job_name: The descriptive name of the Jupyter SLURM job to be created. Primarily, this name is used in terminal
|
|
194
|
+
printouts to identify the job to human operators.
|
|
195
|
+
output_log: The absolute path to the .txt file on the processing server, where to store the standard output
|
|
196
|
+
data of the job.
|
|
197
|
+
error_log: The absolute path to the .txt file on the processing server, where to store the standard error
|
|
198
|
+
data of the job.
|
|
199
|
+
working_directory: The absolute path to the directory where temporary job files will be stored. During runtime,
|
|
200
|
+
classes from this library use that directory to store files such as the job's shell script. All such files
|
|
201
|
+
are automatically removed from the directory at the end of a non-errors runtime.
|
|
202
|
+
conda_environment: The name of the conda environment to activate on the server before running the job logic. The
|
|
203
|
+
environment should contain the necessary Python packages and CLIs to support running the job's logic. For
|
|
204
|
+
Jupyter jobs, this necessarily includes the Jupyter notebook and jupyterlab packages.
|
|
205
|
+
port: The connection port number for Jupyter server. Do not change the default value unless you know what you
|
|
206
|
+
are doing, as the server has most common communication ports closed for security reasons.
|
|
207
|
+
notebook_directory: The directory to use as Jupyter's root. During runtime, Jupyter will only have access to
|
|
208
|
+
items stored in or under this directory. For most runtimes, this should be set to the user's root data or
|
|
209
|
+
working directory.
|
|
210
|
+
cpus_to_use: The number of CPUs to allocate to the Jupyter server. Keep this value as small as possible to avoid
|
|
211
|
+
interfering with headless data processing jobs.
|
|
212
|
+
ram_gb: The amount of RAM, in GB, to allocate to the Jupyter server. Keep this value as small as possible to
|
|
213
|
+
avoid interfering with headless data processing jobs.
|
|
214
|
+
time_limit: The maximum Jupyter server uptime, in minutes. Set this to the expected duration of your jupyter
|
|
215
|
+
session.
|
|
216
|
+
jupyter_args: Stores additional arguments to pass to jupyter notebook initialization command.
|
|
217
|
+
|
|
218
|
+
Attributes:
|
|
219
|
+
port: Stores the connection port of the managed Jupyter server.
|
|
220
|
+
notebook_dir: Stores the absolute path to the directory used as Jupyter's root, relative to the remote server
|
|
221
|
+
root.
|
|
222
|
+
connection_info: Stores the JupyterConnectionInfo instance after the Jupyter server is instantiated.
|
|
223
|
+
host: Stores the hostname of the remote server.
|
|
224
|
+
user: Stores the username used to connect with the remote server.
|
|
225
|
+
connection_info_file: The absolute path to the file that stores connection information, relative to the remote
|
|
226
|
+
server root.
|
|
227
|
+
_command: Stores the shell command for launching the Jupyter server.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def __init__(
|
|
231
|
+
self,
|
|
232
|
+
job_name: str,
|
|
233
|
+
output_log: Path,
|
|
234
|
+
error_log: Path,
|
|
235
|
+
working_directory: Path,
|
|
236
|
+
conda_environment: str,
|
|
237
|
+
notebook_directory: Path,
|
|
238
|
+
port: int = 9999, # Defaults to using port 9999
|
|
239
|
+
cpus_to_use: int = 2, # Defaults to 2 CPU cores
|
|
240
|
+
ram_gb: int = 32, # Defaults to 32 GB of RAM
|
|
241
|
+
time_limit: int = 120, # Defaults to 2 hours of runtime (120 minutes)
|
|
242
|
+
jupyter_args: str = "",
|
|
243
|
+
) -> None:
|
|
244
|
+
# Initializes parent Job class
|
|
245
|
+
super().__init__(
|
|
246
|
+
job_name=job_name,
|
|
247
|
+
output_log=output_log,
|
|
248
|
+
error_log=error_log,
|
|
249
|
+
working_directory=working_directory,
|
|
250
|
+
conda_environment=conda_environment,
|
|
251
|
+
cpus_to_use=cpus_to_use,
|
|
252
|
+
ram_gb=ram_gb,
|
|
253
|
+
time_limit=time_limit,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Saves important jupyter configuration parameters to class attributes
|
|
257
|
+
self.port = port
|
|
258
|
+
self.notebook_dir = notebook_directory
|
|
259
|
+
|
|
260
|
+
# Similar to job ID, these attributes initialize to None and are reconfigured as part of the job submission
|
|
261
|
+
# process.
|
|
262
|
+
self.connection_info: _JupyterConnectionInfo | None = None
|
|
263
|
+
self.host: str | None = None
|
|
264
|
+
self.user: str | None = None
|
|
265
|
+
|
|
266
|
+
# Resolves the server-side path to the jupyter server connection info file.
|
|
267
|
+
self.connection_info_file = working_directory.joinpath(f"{job_name}_connection.txt")
|
|
268
|
+
|
|
269
|
+
# Builds Jupyter launch command.
|
|
270
|
+
self._build_jupyter_command(jupyter_args)
|
|
271
|
+
|
|
272
|
+
def _build_jupyter_command(self, jupyter_args: str) -> None:
|
|
273
|
+
"""Builds the command to launch Jupyter notebook server on the remote Sun lab server."""
|
|
274
|
+
|
|
275
|
+
# Gets the hostname of the compute node and caches it in the connection data file. Also caches the port name.
|
|
276
|
+
self.add_command('echo "COMPUTE_NODE: $(hostname)" > {}'.format(self.connection_info_file))
|
|
277
|
+
self.add_command('echo "PORT: {}" >> {}'.format(self.port, self.connection_info_file))
|
|
278
|
+
|
|
279
|
+
# Generates a random access token for security and caches it in the connection data file.
|
|
280
|
+
self.add_command("TOKEN=$(openssl rand -hex 24)")
|
|
281
|
+
self.add_command('echo "TOKEN: $TOKEN" >> {}'.format(self.connection_info_file))
|
|
282
|
+
|
|
283
|
+
# Builds Jupyter startup command.
|
|
284
|
+
jupyter_cmd = [
|
|
285
|
+
"jupyter lab",
|
|
286
|
+
"--no-browser",
|
|
287
|
+
f"--port={self.port}",
|
|
288
|
+
"--ip=0.0.0.0", # Listen on all interfaces
|
|
289
|
+
"--ServerApp.allow_origin='*'", # Allow connections from SSH tunnel
|
|
290
|
+
"--ServerApp.allow_remote_access=True", # Enable remote access
|
|
291
|
+
"--ServerApp.disable_check_xsrf=True", # Helps with proxy connections
|
|
292
|
+
f"--ServerApp.root_dir={self.notebook_dir}", # Root directory (not notebook-dir)
|
|
293
|
+
"--IdentityProvider.token=$TOKEN", # Token authentication
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
# Adds any additional arguments.
|
|
297
|
+
if jupyter_args:
|
|
298
|
+
jupyter_cmd.append(jupyter_args)
|
|
299
|
+
|
|
300
|
+
# Adds resolved jupyter command to the list of job commands.
|
|
301
|
+
jupyter_cmd_str = " ".join(jupyter_cmd)
|
|
302
|
+
self.add_command(jupyter_cmd_str)
|
|
303
|
+
|
|
304
|
+
def parse_connection_info(self, info_file: Path) -> None:
|
|
305
|
+
"""Parses the connection information file created by the Jupyter job on the server.
|
|
306
|
+
|
|
307
|
+
Use this method to parse the connection file fetched from the server to finalize setting up the Jupyter
|
|
308
|
+
server job.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
info_file: The path to the .txt file generated by the remote server that stores the Jupyter connection
|
|
312
|
+
information to be parsed.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
with open(info_file, "r") as f:
|
|
316
|
+
content = f.read()
|
|
317
|
+
|
|
318
|
+
# Extracts information using regex
|
|
319
|
+
compute_node_match = re.search(r"COMPUTE_NODE: (.+)", content)
|
|
320
|
+
port_match = re.search(r"PORT: (\d+)", content)
|
|
321
|
+
token_match = re.search(r"TOKEN: (.+)", content)
|
|
322
|
+
|
|
323
|
+
if not all([compute_node_match, port_match, token_match]):
|
|
324
|
+
message = f"Could not parse connection information file for the Jupyter server job with id {self.job_id}."
|
|
325
|
+
console.error(message, ValueError)
|
|
326
|
+
|
|
327
|
+
# Stores extracted data inside connection_info attribute as a JupyterConnectionInfo instance.
|
|
328
|
+
self.connection_info = _JupyterConnectionInfo(
|
|
329
|
+
compute_node=compute_node_match.group(1).strip(), # type: ignore
|
|
330
|
+
port=int(port_match.group(1)), # type: ignore
|
|
331
|
+
token=token_match.group(1).strip(), # type: ignore
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def print_connection_info(self) -> None:
|
|
335
|
+
"""Constructs and displays the command to set up the SSH tunnel to the server and the link to the localhost
|
|
336
|
+
server view in the terminal.
|
|
337
|
+
|
|
338
|
+
The SSH command should be used via a separate terminal or subprocess call to establish the secure SSH tunnel to
|
|
339
|
+
the Jupyter server. Once the SSH tunnel is established, the printed localhost url can be used to view the
|
|
340
|
+
server from the local machine.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
# If connection information is not available, there is nothing to print
|
|
344
|
+
if self.connection_info is None:
|
|
345
|
+
console.echo(
|
|
346
|
+
message=(
|
|
347
|
+
f"No connection information is available for the job {self.job_name}, which indicates that the job "
|
|
348
|
+
f"has not been submitted to the server. Submit the job for execution to the remote Sun lab server "
|
|
349
|
+
f"to generate the connection information"
|
|
350
|
+
),
|
|
351
|
+
level=LogLevel.WARNING,
|
|
352
|
+
)
|
|
353
|
+
return # No connection information available, so does not proceed with printing.
|
|
354
|
+
|
|
355
|
+
# Prints generic connection details to terminal
|
|
356
|
+
console.echo(f"Jupyter is running on: {self.connection_info.compute_node}")
|
|
357
|
+
console.echo(f"Port: {self.connection_info.port}")
|
|
358
|
+
console.echo(f"Token: {self.connection_info.token}")
|
|
359
|
+
|
|
360
|
+
# Constructs and displays the SSH tunnel command and the localhost url for connecting to the server
|
|
361
|
+
tunnel_cmd = (
|
|
362
|
+
f"ssh -N -L {self.connection_info.port}:{self.connection_info.compute_node}:{self.connection_info.port} "
|
|
363
|
+
f"{self.user}@{self.host}"
|
|
364
|
+
)
|
|
365
|
+
localhost_url = f"http://localhost:{self.connection_info.port}/?token={self.connection_info.token}"
|
|
366
|
+
print(f"\nTo access locally, run this in a terminal:")
|
|
367
|
+
print(tunnel_cmd)
|
|
368
|
+
print(f"\nThen open: {localhost_url}")
|
sl_shared_assets/server/job.pyi
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
|
|
3
4
|
from _typeshed import Incomplete
|
|
4
5
|
from simple_slurm import Slurm
|
|
5
6
|
|
|
7
|
+
@dataclass
|
|
8
|
+
class _JupyterConnectionInfo:
|
|
9
|
+
"""Stores the data used to establish the connection with a Jupyter notebook server running under SLURM control on a
|
|
10
|
+
remote Sun lab server.
|
|
11
|
+
|
|
12
|
+
More specifically, this class is used to transfer the connection metadata collected on the remote server back to
|
|
13
|
+
the local machine that requested the server to be established.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
compute_node: str
|
|
17
|
+
port: int
|
|
18
|
+
token: str
|
|
19
|
+
@property
|
|
20
|
+
def localhost_url(self) -> str:
|
|
21
|
+
"""Returns the localhost URL for connecting to the server.
|
|
22
|
+
|
|
23
|
+
To use this URL, first set up an SSH tunnel to the server via the specific Jupyter communication port and the
|
|
24
|
+
remote server access credentials.
|
|
25
|
+
"""
|
|
26
|
+
|
|
6
27
|
class Job:
|
|
7
28
|
"""Aggregates the data of a single SLURM-managed job to be executed on the Sun lab BioHPC cluster.
|
|
8
29
|
|
|
@@ -92,3 +113,93 @@ class Job:
|
|
|
92
113
|
executed on the remote compute server. Do not call this method manually unless you know what you are doing.
|
|
93
114
|
The returned string is safe to dump into a .sh (shell script) file and move to the BioHPC server for execution.
|
|
94
115
|
"""
|
|
116
|
+
|
|
117
|
+
class JupyterJob(Job):
|
|
118
|
+
"""Specialized Job instance designed to launch a Jupyter notebook server on SLURM.
|
|
119
|
+
|
|
120
|
+
This class extends the base Job class to include Jupyter-specific configuration and commands for starting a
|
|
121
|
+
notebook server in a SLURM environment. Using this specialized job allows users to set up remote Jupyter servers
|
|
122
|
+
while still benefitting from SLURM's job management and fair airtime policies.
|
|
123
|
+
|
|
124
|
+
Notes:
|
|
125
|
+
Jupyter servers directly compete for resources with headless data processing jobs. Therefore, it is important
|
|
126
|
+
to minimize the resource footprint and the runtime of each Jupyter server, if possible.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
job_name: The descriptive name of the Jupyter SLURM job to be created. Primarily, this name is used in terminal
|
|
130
|
+
printouts to identify the job to human operators.
|
|
131
|
+
output_log: The absolute path to the .txt file on the processing server, where to store the standard output
|
|
132
|
+
data of the job.
|
|
133
|
+
error_log: The absolute path to the .txt file on the processing server, where to store the standard error
|
|
134
|
+
data of the job.
|
|
135
|
+
working_directory: The absolute path to the directory where temporary job files will be stored. During runtime,
|
|
136
|
+
classes from this library use that directory to store files such as the job's shell script. All such files
|
|
137
|
+
are automatically removed from the directory at the end of a non-errors runtime.
|
|
138
|
+
conda_environment: The name of the conda environment to activate on the server before running the job logic. The
|
|
139
|
+
environment should contain the necessary Python packages and CLIs to support running the job's logic. For
|
|
140
|
+
Jupyter jobs, this necessarily includes the Jupyter notebook and jupyterlab packages.
|
|
141
|
+
port: The connection port number for Jupyter server. Do not change the default value unless you know what you
|
|
142
|
+
are doing, as the server has most common communication ports closed for security reasons.
|
|
143
|
+
notebook_directory: The directory to use as Jupyter's root. During runtime, Jupyter will only have access to
|
|
144
|
+
items stored in or under this directory. For most runtimes, this should be set to the user's root data or
|
|
145
|
+
working directory.
|
|
146
|
+
cpus_to_use: The number of CPUs to allocate to the Jupyter server. Keep this value as small as possible to avoid
|
|
147
|
+
interfering with headless data processing jobs.
|
|
148
|
+
ram_gb: The amount of RAM, in GB, to allocate to the Jupyter server. Keep this value as small as possible to
|
|
149
|
+
avoid interfering with headless data processing jobs.
|
|
150
|
+
time_limit: The maximum Jupyter server uptime, in minutes. Set this to the expected duration of your jupyter
|
|
151
|
+
session.
|
|
152
|
+
jupyter_args: Stores additional arguments to pass to jupyter notebook initialization command.
|
|
153
|
+
|
|
154
|
+
Attributes:
|
|
155
|
+
port: Stores the connection port of the managed Jupyter server.
|
|
156
|
+
notebook_dir: Stores the absolute path to the directory used as Jupyter's root, relative to the remote server
|
|
157
|
+
root.
|
|
158
|
+
connection_info: Stores the JupyterConnectionInfo instance after the Jupyter server is instantiated.
|
|
159
|
+
host: Stores the hostname of the remote server.
|
|
160
|
+
user: Stores the username used to connect with the remote server.
|
|
161
|
+
connection_info_file: The absolute path to the file that stores connection information, relative to the remote
|
|
162
|
+
server root.
|
|
163
|
+
_command: Stores the shell command for launching the Jupyter server.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
port: Incomplete
|
|
167
|
+
notebook_dir: Incomplete
|
|
168
|
+
connection_info: _JupyterConnectionInfo | None
|
|
169
|
+
host: str | None
|
|
170
|
+
user: str | None
|
|
171
|
+
connection_info_file: Incomplete
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
job_name: str,
|
|
175
|
+
output_log: Path,
|
|
176
|
+
error_log: Path,
|
|
177
|
+
working_directory: Path,
|
|
178
|
+
conda_environment: str,
|
|
179
|
+
notebook_directory: Path,
|
|
180
|
+
port: int = 9999,
|
|
181
|
+
cpus_to_use: int = 2,
|
|
182
|
+
ram_gb: int = 32,
|
|
183
|
+
time_limit: int = 120,
|
|
184
|
+
jupyter_args: str = "",
|
|
185
|
+
) -> None: ...
|
|
186
|
+
def _build_jupyter_command(self, jupyter_args: str) -> None:
|
|
187
|
+
"""Builds the command to launch Jupyter notebook server on the remote Sun lab server."""
|
|
188
|
+
def parse_connection_info(self, info_file: Path) -> None:
|
|
189
|
+
"""Parses the connection information file created by the Jupyter job on the server.
|
|
190
|
+
|
|
191
|
+
Use this method to parse the connection file fetched from the server to finalize setting up the Jupyter
|
|
192
|
+
server job.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
info_file: The path to the .txt file generated by the remote server that stores the Jupyter connection
|
|
196
|
+
information to be parsed.
|
|
197
|
+
"""
|
|
198
|
+
def print_connection_info(self) -> None:
|
|
199
|
+
"""Constructs and displays the command to set up the SSH tunnel to the server and the link to the localhost
|
|
200
|
+
server view in the terminal.
|
|
201
|
+
|
|
202
|
+
The SSH command should be used via a separate terminal or subprocess call to establish the secure SSH tunnel to
|
|
203
|
+
the Jupyter server. Once the SSH tunnel is established, the printed localhost url can be used to view the
|
|
204
|
+
server from the local machine.
|
|
205
|
+
"""
|