sl-shared-assets 4.0.1__py3-none-any.whl → 5.0.1__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.

Files changed (39) hide show
  1. sl_shared_assets/__init__.py +48 -41
  2. sl_shared_assets/command_line_interfaces/__init__.py +3 -0
  3. sl_shared_assets/command_line_interfaces/configure.py +173 -0
  4. sl_shared_assets/command_line_interfaces/manage.py +226 -0
  5. sl_shared_assets/data_classes/__init__.py +33 -32
  6. sl_shared_assets/data_classes/configuration_data.py +267 -79
  7. sl_shared_assets/data_classes/session_data.py +226 -289
  8. sl_shared_assets/server/__init__.py +24 -4
  9. sl_shared_assets/server/job.py +6 -7
  10. sl_shared_assets/server/pipeline.py +585 -0
  11. sl_shared_assets/server/server.py +57 -25
  12. sl_shared_assets/tools/__init__.py +9 -8
  13. sl_shared_assets/tools/packaging_tools.py +14 -25
  14. sl_shared_assets/tools/project_management_tools.py +602 -523
  15. sl_shared_assets/tools/transfer_tools.py +88 -23
  16. {sl_shared_assets-4.0.1.dist-info → sl_shared_assets-5.0.1.dist-info}/METADATA +46 -203
  17. sl_shared_assets-5.0.1.dist-info/RECORD +23 -0
  18. sl_shared_assets-5.0.1.dist-info/entry_points.txt +3 -0
  19. sl_shared_assets/__init__.pyi +0 -91
  20. sl_shared_assets/cli.py +0 -501
  21. sl_shared_assets/cli.pyi +0 -106
  22. sl_shared_assets/data_classes/__init__.pyi +0 -75
  23. sl_shared_assets/data_classes/configuration_data.pyi +0 -235
  24. sl_shared_assets/data_classes/runtime_data.pyi +0 -157
  25. sl_shared_assets/data_classes/session_data.pyi +0 -379
  26. sl_shared_assets/data_classes/surgery_data.pyi +0 -89
  27. sl_shared_assets/server/__init__.pyi +0 -11
  28. sl_shared_assets/server/job.pyi +0 -205
  29. sl_shared_assets/server/server.pyi +0 -298
  30. sl_shared_assets/tools/__init__.pyi +0 -19
  31. sl_shared_assets/tools/ascension_tools.py +0 -265
  32. sl_shared_assets/tools/ascension_tools.pyi +0 -68
  33. sl_shared_assets/tools/packaging_tools.pyi +0 -58
  34. sl_shared_assets/tools/project_management_tools.pyi +0 -239
  35. sl_shared_assets/tools/transfer_tools.pyi +0 -53
  36. sl_shared_assets-4.0.1.dist-info/RECORD +0 -36
  37. sl_shared_assets-4.0.1.dist-info/entry_points.txt +0 -7
  38. {sl_shared_assets-4.0.1.dist-info → sl_shared_assets-5.0.1.dist-info}/WHEEL +0 -0
  39. {sl_shared_assets-4.0.1.dist-info → sl_shared_assets-5.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,585 @@
1
+ """This module provides tools and classes for running complex data processing pipelines on remote compute servers.
2
+ A Pipeline represents a higher unit of abstraction relative to the Job class, often leveraging multiple sequential or
3
+ parallel processing jobs to conduct the required processing."""
4
+
5
+ import copy
6
+ from enum import IntEnum, StrEnum
7
+ from random import randint
8
+ import shutil as sh
9
+ from pathlib import Path
10
+ from dataclasses import field, dataclass
11
+
12
+ from xxhash import xxh3_64
13
+ from filelock import FileLock
14
+ from ataraxis_base_utilities import console, ensure_directory_exists
15
+ from ataraxis_data_structures import YamlConfig
16
+ from ataraxis_time.time_helpers import get_timestamp
17
+
18
+ from .job import Job
19
+ from .server import Server
20
+
21
+
22
+ class TrackerFileNames(StrEnum):
23
+ """Defines a set of processing tacker .yaml files used by the Sun lab data preprocessing, processing, and dataset
24
+ formation pipelines to track the progress of the remotely executed pipelines.
25
+
26
+ This enumeration standardizes the names for all processing tracker files used in the lab. It is designed to be used
27
+ via the get_processing_tracker() function to generate ProcessingTracker instances.
28
+
29
+ Notes:
30
+ The elements in this enumeration match the elements in the ProcessingPipelines enumeration, since each valid
31
+ ProcessingPipeline instance has an associated ProcessingTracker file instance.
32
+ """
33
+
34
+ MANIFEST = "manifest_generation_tracker.yaml"
35
+ """This file is used to track the state of the project manifest generation pipeline."""
36
+ CHECKSUM = "checksum_resolution_tracker.yaml"
37
+ """This file is used to track the state of the checksum resolution pipeline."""
38
+ PREPARATION = "processing_preparation_tracker.yaml"
39
+ """This file is used to track the state of the data processing preparation pipeline."""
40
+ BEHAVIOR = "behavior_processing_tracker.yaml"
41
+ """This file is used to track the state of the behavior log processing pipeline."""
42
+ SUITE2P = "suite2p_processing_tracker.yaml"
43
+ """This file is used to track the state of the single-day suite2p processing pipeline."""
44
+ VIDEO = "video_processing_tracker.yaml"
45
+ """This file is used to track the state of the video (DeepLabCut) processing pipeline."""
46
+ FORGING = "dataset_forging_tracker.yaml"
47
+ """This file is used to track the state of the dataset creation (forging) pipeline."""
48
+ MULTIDAY = "multiday_processing_tracker.yaml"
49
+ """This file is used to track the state of the multiday suite2p processing pipeline."""
50
+ ARCHIVING = "data_archiving_tracker.yaml"
51
+ """This file is used to track the state of the data archiving pipeline."""
52
+
53
+
54
+ class ProcessingPipelines(StrEnum):
55
+ """Defines the set of processing pipelines currently supported in the Sun lab.
56
+
57
+ All processing pipelines currently supported by the lab codebase are defined in this enumeration. Primarily,
58
+ the elements from this enumeration are used in terminal messages and data logging entries to identify the pipelines
59
+ to the user.
60
+
61
+ Notes:
62
+ The elements in this enumeration match the elements in the ProcessingTracker enumeration, since each valid
63
+ ProcessingPipeline instance has an associated ProcessingTracker file instance.
64
+
65
+ The order of pipelines in this enumeration loosely follows the sequence in which they are executed during the
66
+ lifetime of the Sun lab data on the remote compute server.
67
+ """
68
+
69
+ MANIFEST = "manifest generation"
70
+ """Project manifest generation pipeline. This pipeline is generally not used in most runtime contexts. It allows
71
+ manually regenerating the project manifest .feather file, which is typically only used during testing. All other
72
+ pipeline automatically conduct the manifest (re)generation at the end of their runtime."""
73
+ CHECKSUM = "checksum resolution"
74
+ """Checksum resolution pipeline. Primarily, it is used to verify that the raw data has been transferred to the
75
+ remote storage server from the main acquisition system PC intact. This pipeline is sometimes also used to
76
+ regenerate (re-checksum) the data stored on the remote compute server."""
77
+ PREPARATION = "processing preparation"
78
+ """Data processing preparation pipeline. Since the compute server uses a two-volume design with a slow (HDD) storage
79
+ volume and a fast (NVME) working volume, to optimize data processing performance, the data needs to be transferred
80
+ to the working volume before processing. This pipeline copies the raw data for the target session from the storage
81
+ volume to the working volume."""
82
+ BEHAVIOR = "behavior processing"
83
+ """Behavior processing pipeline. This pipeline is used to process .npz log files to extract animal behavior data
84
+ acquired during a single session (day). The processed logs also contain the timestamps use to synchronize behavior
85
+ to video and mesoscope frame data, and experiment configuration and task information."""
86
+ SUITE2P = "single-day suite2p processing"
87
+ """Single-day suite2p pipeline. This pipeline is used to extract the cell activity data from 2-photon imaging data
88
+ acquired during a single session (day)."""
89
+ VIDEO = "video processing"
90
+ """DeepLabCut (Video) processing pipeline. This pipeline is used to extract animal pose estimation data from the
91
+ behavior video frames acquired during a single session (day)."""
92
+ MULTIDAY = "multi-day suite2p processing"
93
+ """Multi-day suite2p processing (cell tracking) pipeline. This pipeline is used to track cells processed with the
94
+ single-day suite2p pipelines across multiple days. It is executed for all sessions marked for integration into the
95
+ same dataset as the first step of dataset creation."""
96
+ FORGING = "dataset forging"
97
+ """Dataset creation (forging) pipeline. This pipeline typically runs after the multi-day pipeline. It extracts and
98
+ integrates the processed data from various sources such as brain activity, behavior, videos, etc., into a unified
99
+ dataset."""
100
+ ARCHIVING = "data archiving"
101
+ """Data archiving pipeline. To conserve the (limited) space on the fast working volume, once the data has been
102
+ processed and integrated into a stable dataset, the processed data folder is moved to the storage volume and all
103
+ folders under the root session folder on the processed data volume are deleted."""
104
+
105
+
106
+ class ProcessingStatus(IntEnum):
107
+ """Maps integer-based processing pipeline status (state) codes to human-readable names.
108
+
109
+ This enumeration is used to track and communicate the progress of Sun lab processing pipelines as they are executed
110
+ by the remote compute server. Specifically, the codes from this enumeration are used by the ProcessingPipeline
111
+ class to communicate the status of the managed pipelines to external processes.
112
+
113
+ Notes:
114
+ The status codes from this enumeration track the state of the pipeline as a whole, instead of tracking the
115
+ state of each job that comprises the pipeline.
116
+ """
117
+
118
+ RUNNING = 0
119
+ """The pipeline is currently running on the remote server. It may be executed (in progress) or waiting for
120
+ the required resources to become available (queued)."""
121
+ SUCCEEDED = 1
122
+ """The server has successfully completed the processing pipeline."""
123
+ FAILED = 2
124
+ """The server has failed to complete the pipeline due to a runtime error."""
125
+ ABORTED = 3
126
+ """The pipeline execution has been aborted prematurely, either by the manager process or due to an overriding
127
+ request from another user."""
128
+
129
+
130
+ @dataclass()
131
+ class ProcessingTracker(YamlConfig):
132
+ """Wraps the .yaml file that tracks the state of a data processing pipeline and provides tools for communicating the
133
+ state between multiple processes in a thread-safe manner.
134
+
135
+ This class is used by all data processing pipelines running on the remote compute server(s) to prevent race
136
+ conditions and ensure that pipelines have exclusive access to the processed data. It is also used to evaluate the
137
+ status (success / failure) of each pipeline as they are executed by the remote server.
138
+
139
+ Note:
140
+ In library version 4.0.0 the processing trackers have been refactored to work similar to 'lock' files. That is,
141
+ when a pipeline starts running on the remote server, its tracker is switched into the 'running' (locked) state
142
+ until the pipeline completes, aborts, or encounters an error. When the tracker is locked, all modifications to
143
+ the tracker or processed data have to originate from the same process that started the pipeline that locked the
144
+ tracker file. This feature supports running complex processing pipelines that use multiple concurrent and / or
145
+ sequential processing jobs on the remote server.
146
+
147
+ This instance frequently refers to a 'manager process' in method documentation. A 'manager process' is the
148
+ highest-level process that manages the tracked pipeline. When a pipeline runs on remote compute servers, the
149
+ manager process is typically the process running on the non-server machine (user PC) that submits the remote
150
+ processing jobs to the compute server (via SSH or similar protocol). The worker process(es) that run the
151
+ processing job(s) on the remote compute servers are NOT considered manager processes.
152
+ """
153
+
154
+ file_path: Path
155
+ """Stores the path to the .yaml file used to cache the tracker data on disk. The class instance functions as a
156
+ wrapper around the data stored inside the specified .yaml file."""
157
+ _complete: bool = False
158
+ """Tracks whether the processing runtime managed by this tracker has finished successfully."""
159
+ _encountered_error: bool = False
160
+ """Tracks whether the processing runtime managed by this tracker has encountered an error and has finished
161
+ unsuccessfully."""
162
+ _running: bool = False
163
+ """Tracks whether the processing runtime managed by this tracker is currently running."""
164
+ _manager_id: int = -1
165
+ """Stores the xxHash3-64 hash value that represents the unique identifier of the manager process that started the
166
+ runtime. The manager process is typically running on a remote control machine (computer) and is used to
167
+ support processing runtimes that are distributed over multiple separate batch jobs on the compute server. This
168
+ ID should be generated using the 'generate_manager_id()' function exposed by this library."""
169
+ _lock_path: str = field(init=False)
170
+ """Stores the path to the .lock file used to ensure that only a single process can simultaneously access the data
171
+ stored inside the tracker file."""
172
+ _job_count: int = 1
173
+ """Stores the total number of jobs to be executed as part of the tracked pipeline. This is used to
174
+ determine when the tracked pipeline is fully complete when tracking intermediate job outcomes."""
175
+ _completed_jobs: int = 0
176
+ """Stores the total number of jobs completed by the tracked pipeline. This is used together with the '_job_count'
177
+ field to determine when the tracked pipeline is fully complete."""
178
+
179
+ def __post_init__(self) -> None:
180
+ # Generates the .lock file path for the target tracker .yaml file.
181
+ if self.file_path is not None:
182
+ self._lock_path = str(self.file_path.with_suffix(self.file_path.suffix + ".lock"))
183
+
184
+ # Ensures that the input processing tracker file name is supported.
185
+ if self.file_path.name not in tuple(TrackerFileNames):
186
+ message = (
187
+ f"Unsupported processing tracker file encountered when instantiating a ProcessingTracker "
188
+ f"instance: {self.file_path}. Currently, only the following tracker file names are "
189
+ f"supported: {', '.join(tuple(TrackerFileNames))}."
190
+ )
191
+ console.error(message=message, error=ValueError)
192
+
193
+ else:
194
+ self._lock_path = ""
195
+
196
+ def _load_state(self) -> None:
197
+ """Reads the current processing state from the wrapped .YAML file."""
198
+ if self.file_path.exists():
199
+ # Loads the data for the state values but does not replace the file path or lock attributes.
200
+ instance: ProcessingTracker = self.from_yaml(self.file_path) # type: ignore
201
+ self._complete = copy.copy(instance._complete)
202
+ self._encountered_error = copy.copy(instance._encountered_error)
203
+ self._running = copy.copy(instance._running)
204
+ self._manager_id = copy.copy(instance._manager_id)
205
+ else:
206
+ # Otherwise, if the tracker file does not exist, generates a new .yaml file using default instance values
207
+ # and saves it to disk using the specified tracker file path.
208
+ self._save_state()
209
+
210
+ def _save_state(self) -> None:
211
+ """Saves the current processing state stored inside instance attributes to the specified .YAML file."""
212
+ # Resets the _lock_path and file_path to None before dumping the data to .YAML to avoid issues with loading it
213
+ # back.
214
+ original = copy.deepcopy(self)
215
+ original.file_path = None # type: ignore
216
+ original._lock_path = None # type: ignore
217
+ original.to_yaml(file_path=self.file_path)
218
+
219
+ def start(self, manager_id: int, job_count: int = 1) -> None:
220
+ """Configures the tracker file to indicate that a manager process is currently executing the tracked processing
221
+ runtime.
222
+
223
+ Calling this method effectively 'locks' the tracked session and processing runtime combination to only be
224
+ accessible from the manager process that calls this method. Calling this method for an already running runtime
225
+ managed by the same process does not have any effect, so it is safe to call this method at the beginning of
226
+ each processing job that makes up the runtime.
227
+
228
+ Args:
229
+ manager_id: The unique xxHash-64 hash identifier of the manager process which attempts to start the runtime
230
+ tracked by this tracker file.
231
+ job_count: The total number of jobs to be executed as part of the tracked pipeline. This is used to make
232
+ the stop() method properly track the end of the pipeline as a whole, rather than the end of intermediate
233
+ jobs. Primarily, this is used by multi-job pipelines where all jobs are submitted as part of a single
234
+ phase and the job completion order cannot be known in-advance.
235
+
236
+ Raises:
237
+ TimeoutError: If the .lock file for the target .YAML file cannot be acquired within the timeout period.
238
+ """
239
+ # Acquires the lock
240
+ lock = FileLock(self._lock_path)
241
+ with lock.acquire(timeout=10.0):
242
+ # Loads tracker state from the .yaml file
243
+ self._load_state()
244
+
245
+ # If the runtime is already running from a different process, aborts with an error.
246
+ if self._running and manager_id != self._manager_id:
247
+ message = (
248
+ f"Unable to start the processing runtime from the manager process with id {manager_id}. The "
249
+ f"{self.file_path.name} tracker file indicates that the manager process with id {self._manager_id} "
250
+ f"is currently executing the tracked runtime. Only a single manager process is allowed to execute "
251
+ f"the runtime at the same time."
252
+ )
253
+ console.error(message=message, error=RuntimeError)
254
+ raise RuntimeError(message) # Fallback to appease mypy, should not be reachable
255
+
256
+ # Otherwise, if the runtime is already running for the current manager process, returns without modifying
257
+ # the tracker data.
258
+ elif self._running and manager_id == self._manager_id:
259
+ return
260
+
261
+ # Otherwise, locks the runtime for the current manager process and updates the cached tracker data
262
+ self._running = True
263
+ self._manager_id = manager_id
264
+ self._complete = False
265
+ self._encountered_error = False
266
+ self._job_count = job_count
267
+ self._save_state()
268
+
269
+ def error(self, manager_id: int) -> None:
270
+ """Configures the tracker file to indicate that the tracked processing runtime encountered an error and failed
271
+ to complete.
272
+
273
+ This method fulfills two main purposes. First, it 'unlocks' the runtime, allowing other manager processes to
274
+ interface with the tracked runtime. Second, it updates the tracker file to reflect that the runtime was
275
+ interrupted due to an error, which is used by the manager processes to detect and handle processing failures.
276
+
277
+ Args:
278
+ manager_id: The unique xxHash-64 hash identifier of the manager process which attempts to report that the
279
+ runtime tracked by this tracker file has encountered an error.
280
+
281
+ Raises:
282
+ TimeoutError: If the .lock file for the target .YAML file cannot be acquired within the timeout period.
283
+ """
284
+ lock = FileLock(self._lock_path)
285
+ with lock.acquire(timeout=10.0):
286
+ # Loads tracker state from the .yaml file
287
+ self._load_state()
288
+
289
+ # If the runtime is not running, returns without doing anything
290
+ if not self._running:
291
+ return
292
+
293
+ # Ensures that only the active manager process can report runtime errors using the tracker file
294
+ if manager_id != self._manager_id:
295
+ message = (
296
+ f"Unable to report that the processing runtime has encountered an error from the manager process "
297
+ f"with id {manager_id}. The {self.file_path.name} tracker file indicates that the runtime is "
298
+ f"managed by the process with id {self._manager_id}, preventing other processes from interfacing "
299
+ f"with the runtime."
300
+ )
301
+ console.error(message=message, error=RuntimeError)
302
+ raise RuntimeError(message) # Fallback to appease mypy, should not be reachable
303
+
304
+ # Indicates that the runtime aborted with an error
305
+ self._running = False
306
+ self._manager_id = -1
307
+ self._complete = False
308
+ self._encountered_error = True
309
+ self._save_state()
310
+
311
+ def stop(self, manager_id: int) -> None:
312
+ """Configures the tracker file to indicate that the tracked processing runtime has been completed successfully.
313
+
314
+ This method 'unlocks' the runtime, allowing other manager processes to interface with the tracked runtime. It
315
+ also configures the tracker file to indicate that the runtime has been completed successfully, which is used
316
+ by the manager processes to detect and handle processing completion.
317
+
318
+ Args:
319
+ manager_id: The unique xxHash-64 hash identifier of the manager process which attempts to report that the
320
+ runtime tracked by this tracker file has been completed successfully.
321
+
322
+ Raises:
323
+ TimeoutError: If the .lock file for the target .YAML file cannot be acquired within the timeout period.
324
+ """
325
+ lock = FileLock(self._lock_path)
326
+ with lock.acquire(timeout=10.0):
327
+ # Loads tracker state from the .yaml file
328
+ self._load_state()
329
+
330
+ # If the runtime is not running, does not do anything
331
+ if not self._running:
332
+ return
333
+
334
+ # Ensures that only the active manager process can report runtime completion using the tracker file
335
+ if manager_id != self._manager_id:
336
+ message = (
337
+ f"Unable to report that the processing runtime has completed successfully from the manager process "
338
+ f"with id {manager_id}. The {self.file_path.name} tracker file indicates that the runtime is "
339
+ f"managed by the process with id {self._manager_id}, preventing other processes from interfacing "
340
+ f"with the runtime."
341
+ )
342
+ console.error(message=message, error=RuntimeError)
343
+ raise RuntimeError(message) # Fallback to appease mypy, should not be reachable
344
+
345
+ # Increments completed job tracker
346
+ self._completed_jobs += 1
347
+
348
+ # If the pipeline has completed all required jobs, marks the runtime as complete (stopped)
349
+ if self._completed_jobs >= self._job_count:
350
+ self._running = False
351
+ self._manager_id = -1
352
+ self._complete = True
353
+ self._encountered_error = False
354
+ self._save_state()
355
+
356
+ def abort(self) -> None:
357
+ """Resets the runtime tracker file to the default state.
358
+
359
+ This method can be used to reset the runtime tracker file, regardless of the current runtime state. Unlike other
360
+ instance methods, this method can be called from any manager process, even if the runtime is already locked by
361
+ another process. This method is only intended to be used in the case of emergency to 'unlock' a deadlocked
362
+ runtime.
363
+ """
364
+ lock = FileLock(self._lock_path)
365
+ with lock.acquire(timeout=10.0):
366
+ # Loads tracker state from the .yaml file.
367
+ self._load_state()
368
+
369
+ # Resets the tracker file to the default state. Note, does not indicate that the runtime completed nor
370
+ # that it has encountered an error.
371
+ self._running = False
372
+ self._manager_id = -1
373
+ self._complete = False
374
+ self._encountered_error = False
375
+ self._save_state()
376
+
377
+ @property
378
+ def is_complete(self) -> bool:
379
+ """Returns True if the tracker wrapped by the instance indicates that the processing runtime has been completed
380
+ successfully and that the runtime is not currently ongoing."""
381
+ lock = FileLock(self._lock_path)
382
+ with lock.acquire(timeout=10.0):
383
+ # Loads tracker state from the .yaml file
384
+ self._load_state()
385
+ return self._complete
386
+
387
+ @property
388
+ def encountered_error(self) -> bool:
389
+ """Returns True if the tracker wrapped by the instance indicates that the processing runtime has aborted due
390
+ to encountering an error."""
391
+ lock = FileLock(self._lock_path)
392
+ with lock.acquire(timeout=10.0):
393
+ # Loads tracker state from the .yaml file
394
+ self._load_state()
395
+ return self._encountered_error
396
+
397
+ @property
398
+ def is_running(self) -> bool:
399
+ """Returns True if the tracker wrapped by the instance indicates that the processing runtime is currently
400
+ ongoing."""
401
+ lock = FileLock(self._lock_path)
402
+ with lock.acquire(timeout=10.0):
403
+ # Loads tracker state from the .yaml file
404
+ self._load_state()
405
+ return self._running
406
+
407
+
408
+ @dataclass()
409
+ class ProcessingPipeline:
410
+ """Encapsulates access to a processing pipeline running on the remote compute server.
411
+
412
+ This class functions as an interface for all data processing pipelines running on Sun lab compute servers. It is
413
+ pipeline-type-agnostic and works for all data processing pipelines supported by this library. After instantiation,
414
+ the class automatically handles all interactions with the server necessary to run the remote processing pipeline and
415
+ verify the runtime outcome via the runtime_cycle() method that has to be called cyclically until the pipeline is
416
+ complete.
417
+
418
+ Notes:
419
+ Each pipeline may be executed in one or more stages, each stage using one or more parallel jobs. As such, each
420
+ pipeline can be seen as an execution graph that sequentially submits batches of jobs to the remote server. The
421
+ processing graph for each pipeline is fully resolved at the instantiation of this class instance, so each
422
+ instance contains the necessary data to run the entire processing pipeline.
423
+
424
+ The minimum self-contained unit of the processing pipeline is a single job. Since jobs can depend on the output
425
+ of other jobs, they are organized into stages based on the dependency graph between jobs. Combined with cluster
426
+ management software, such as SLURM, this class can efficiently execute processing pipelines on scalable compute
427
+ clusters.
428
+ """
429
+
430
+ pipeline_type: ProcessingPipelines
431
+ """Stores the name of the processing pipeline managed by this instance. Primarily, this is used to identify the
432
+ pipeline to the user in terminal messages and logs."""
433
+ server: Server
434
+ """Stores the reference to the Server object that maintains bidirectional communication with the remote server
435
+ running the pipeline."""
436
+ manager_id: int
437
+ """The unique identifier for the manager process that constructs and manages the runtime of the tracked pipeline.
438
+ This is used to ensure that only a single pipeline instance can work with each session's data at the same time on
439
+ the remote server."""
440
+ jobs: dict[int, tuple[tuple[Job, Path], ...]]
441
+ """Stores the dictionary that maps the pipeline processing stage integer-codes to two-element tuples. Each tuple
442
+ stores the Job objects and the paths to their remote working directories to be submitted to the server at each
443
+ stage."""
444
+ remote_tracker_path: Path
445
+ """The path to the pipeline's processing tracker .yaml file stored on the remote compute server."""
446
+ local_tracker_path: Path
447
+ """The path to the pipeline's processing tracker .yaml file on the local machine. The remote file is pulled to
448
+ this location when the instance verifies the outcome of each tracked pipeline's processing stage."""
449
+ session: str
450
+ """The ID of the session whose data is being processed by the tracked pipeline."""
451
+ animal: str
452
+ """The ID of the animal whose data is being processed by the tracked pipeline."""
453
+ project: str
454
+ """The name of the project whose data is being processed by the tracked pipeline."""
455
+ keep_job_logs: bool = False
456
+ """Determines whether to keep the logs for the jobs making up the pipeline execution graph or (default) to remove
457
+ them after pipeline successfully ends its runtime. If the pipeline fails to complete its runtime, the logs are kept
458
+ regardless of this setting."""
459
+ pipeline_status: ProcessingStatus | int = ProcessingStatus.RUNNING
460
+ """Stores the current status of the tracked remote pipeline. This field is updated each time runtime_cycle()
461
+ instance method is called."""
462
+ _pipeline_stage: int = 0
463
+ """Stores the current stage of the tracked pipeline. This field is monotonically incremented by the runtime_cycle()
464
+ method to sequentially submit batches of jobs to the server in a processing-stage-driven fashion."""
465
+
466
+ def __post_init__(self) -> None:
467
+ """Carries out the necessary filesystem setup tasks to support pipeline execution."""
468
+
469
+ # Ensures that the input processing tracker file name is supported.
470
+ if self.pipeline_type not in tuple(ProcessingPipelines):
471
+ message = (
472
+ f"Unsupported processing pipeline type encountered when instantiating a ProcessingPipeline "
473
+ f"instance: {self.pipeline_type}. Currently, only the following pipeline types are "
474
+ f"supported: {', '.join(tuple(ProcessingPipelines))}."
475
+ )
476
+ console.error(message=message, error=ValueError)
477
+
478
+ ensure_directory_exists(self.local_tracker_path) # Ensures that the local temporary directory exists
479
+
480
+ def runtime_cycle(self) -> None:
481
+ """Checks the current status of the tracked pipeline and, if necessary, submits additional batches of jobs to
482
+ the remote server to progress the pipeline.
483
+
484
+ This method is the main entry point for all interactions with the processing pipeline managed by this instance.
485
+ It checks the current state of the pipeline, advances the pipeline's processing stage, and submits the necessary
486
+ jobs to the remote server. The runtime manager process should call this method repeatedly (cyclically) to run
487
+ the pipeline until the 'is_running' property of the instance returns True.
488
+
489
+ Notes:
490
+ While the 'is_running' property can be used to determine whether the pipeline is still running, to resolve
491
+ the final status of the pipeline (success or failure), the manager process should access the
492
+ 'status' instance property.
493
+ """
494
+
495
+ # This clause is executed the first time the method is called for the newly initialized pipeline tracker
496
+ # instance. It submits the first batch of processing jobs (first stage) to the remote server. For one-stage
497
+ # pipelines, this is the only time when pipeline jobs are submitted to the server.
498
+ if self._pipeline_stage == 0:
499
+ self._pipeline_stage += 1
500
+ self._submit_jobs()
501
+
502
+ # Waits until all jobs submitted to the server as part of the current processing stage are completed before
503
+ # advancing further.
504
+ for job, _ in self.jobs[self._pipeline_stage]: # Ignores working directories as part of this iteration.
505
+ if not self.server.job_complete(job=job):
506
+ return
507
+
508
+ # If all jobs for the current processing stage have completed, checks the pipeline's processing tracker file to
509
+ # determine if all jobs completed successfully.
510
+ self.server.pull_file(remote_file_path=self.remote_tracker_path, local_file_path=self.local_tracker_path)
511
+ tracker = ProcessingTracker(self.local_tracker_path)
512
+
513
+ # If the stage failed due to encountering an error, removes the local tracker copy and marks the pipeline
514
+ # as 'failed'. It is expected that the pipeline state is then handed by the manager process to notify the
515
+ # user about the runtime failure.
516
+ if tracker.encountered_error:
517
+ sh.rmtree(self.local_tracker_path.parent) # Removes local temporary data
518
+ self.pipeline_status = ProcessingStatus.FAILED # Updates the processing status to 'failed'
519
+
520
+ # If this was the last processing stage, the tracker indicates that the processing has been completed. In this
521
+ # case, initializes the shutdown sequence:
522
+ elif tracker.is_complete:
523
+ sh.rmtree(self.local_tracker_path.parent) # Removes local temporary data
524
+ self.pipeline_status = ProcessingStatus.SUCCEEDED # Updates the job status to 'succeeded'
525
+
526
+ # If the pipeline was configured to remove logs after completing successfully, removes the runtime log for
527
+ # each job submitted as part of this pipeline from the remote server.
528
+ if not self.keep_job_logs:
529
+ for stage_jobs in self.jobs.values():
530
+ for _, directory in stage_jobs: # Ignores job objects as part of this iteration.
531
+ self.server.remove(remote_path=directory, recursive=True, is_dir=True)
532
+
533
+ # If the processing is not complete (according to the tracker), this indicates that the pipeline has more
534
+ # stages to execute. In this case, increments the processing stage tracker and submits the next batch of jobs
535
+ # to the server.
536
+ elif tracker.is_running:
537
+ self._pipeline_stage += 1
538
+ self._submit_jobs()
539
+
540
+ # The final and the rarest state: the pipeline was aborted before it finished the runtime. Generally, this state
541
+ # should not be encountered during most runtimes.
542
+ else:
543
+ self.pipeline_status = ProcessingStatus.ABORTED
544
+
545
+ def _submit_jobs(self) -> None:
546
+ """This worker method submits the processing jobs for the currently active processing stage to the remote
547
+ server.
548
+
549
+ It is used internally by the runtime_cycle() method to iteratively execute all stages of the managed processing
550
+ pipeline on the remote server.
551
+ """
552
+ for job, _ in self.jobs[self._pipeline_stage]:
553
+ self.server.submit_job(job=job, verbose=False) # Silences terminal printouts
554
+
555
+ @property
556
+ def is_running(self) -> bool:
557
+ """Returns True if the pipeline is currently running, False otherwise."""
558
+ if self.pipeline_status == ProcessingStatus.RUNNING:
559
+ return True
560
+ return False
561
+
562
+ @property
563
+ def status(self) -> ProcessingStatus:
564
+ """Returns the current status of the pipeline packaged into a ProcessingStatus instance."""
565
+ return ProcessingStatus(self.pipeline_status)
566
+
567
+
568
+ def generate_manager_id() -> int:
569
+ """Generates and returns a unique integer identifier that can be used to identify the manager process that calls
570
+ this function.
571
+
572
+ The identifier is generated based on the current timestamp, accurate to microseconds, and a random number between 1
573
+ and 9999999999999. This ensures that the identifier is unique for each function call. The generated identifier
574
+ string is converted to a unique integer value using the xxHash-64 algorithm before it is returned to the caller.
575
+
576
+ Notes:
577
+ This function should be used to generate manager process identifiers for working with ProcessingTracker
578
+ instances from sl-shared-assets version 4.0.0 and above.
579
+ """
580
+ timestamp = get_timestamp()
581
+ random_number = randint(1, 9999999999999)
582
+ manager_id = f"{timestamp}_{random_number}"
583
+ id_hash = xxh3_64()
584
+ id_hash.update(manager_id)
585
+ return id_hash.intdigest()