Source code for jobflow_remote.utils.remote

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, ClassVar

from jobflow_remote import ConfigManager

if TYPE_CHECKING:
    from pathlib import Path

    from jobflow_remote.config.base import Project
    from jobflow_remote.remote.host.base import BaseHost

logger = logging.getLogger(__name__)


[docs] class SharedHosts: """ A singleton context manager to allow sharing the same host objects. Hosts are stored internally, associated to the worker name Being a singleton, opening the context manager multiple times allows to share the hosts across different sections of the code, if needed. Hosts connections are all closed only when leaving the last context manager. A single project can be active at a time: the project is bound when the outermost context is opened and released when the last one is closed. Opening a nested context for a different project raises an error, since sharing host connections across projects is not supported. Examples -------- >>> with SharedHosts(project) as shared_hosts: ... host = shared_hosts.get_host("worker_name") ... # Use host as required """ _instance: SharedHosts = None _ref_count: int = 0 _hosts: ClassVar[dict[str, BaseHost]] = {} _project: Project | None = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self, project: Project | None = None): """ Parameters ---------- project The project configuration. """ cls = self.__class__ if cls._ref_count == 0: # No context is currently active: this is (or precedes) the # outermost context, so (re)bind to the requested project, # discarding any project left over from a previous operation. if project is None: config_manager: ConfigManager = ConfigManager() project = config_manager.get_project(None) cls._project = project elif project is not None and project.name != cls._project.name: # A context is already open and bound to another project. Sharing # host connections across projects is not supported, so refuse # instead of silently using the wrong project's workers. raise ValueError( f"SharedHosts is already active for project '{cls._project.name}' " f"and cannot be used for a different project ('{project.name}') " "at the same time." )
[docs] def get_host(self, worker: str) -> BaseHost: """ Return the shared host, if already defined, otherwise retrieve the host from the project and connect it. Parameters ---------- worker The name of a worker defined in the project Returns ------- BaseHost The shared host. """ if worker not in self._project.workers: raise ValueError(f"Worker {worker} not defined in {self._project.name}") if worker in self._hosts: return self._hosts[worker] host = self._project.workers[worker].get_host() host.connect() self._hosts[worker] = host return host
[docs] def close_hosts(self) -> None: """Close the connection to all the connected hosts""" for worker in list(self._hosts): try: self._hosts[worker].close() except Exception: logger.exception( f"Error while closing the connection to the {worker} worker" ) finally: self._hosts.pop(worker)
def __enter__(self): # Increment reference count self.__class__._ref_count += 1 return self def __exit__(self, exc_type, exc_val, exc_tb): # Decrement reference count self.__class__._ref_count -= 1 # Cleanup only when the last context exits if self.__class__._ref_count == 0: self.close_hosts() # Release the project so the next operation rebinds a fresh one self.__class__._project = None
[docs] class UnsafeDeletionError(Exception): """ Error to signal that Job files could not be deleted as the safety check did not pass. """
[docs] def safe_remove_job_files( host: BaseHost, run_dir: str | Path | None, raise_on_error: bool = False ) -> bool: if not run_dir: return False remote_files = host.listdir(run_dir) # safety measure to avoid mistakenly deleting other folders if not remote_files: return False if any(fn in remote_files for fn in ("jfremote_in.json", "jfremote_in.json.gz")): return host.rmtree(path=run_dir, raise_on_error=raise_on_error) if raise_on_error: raise UnsafeDeletionError( f"Could not delete folder {run_dir} " "since it may not contain a jobflow-remote execution. Some files are present, " "but jfremote_in.json is missing", ) logger.warning( f"Did not delete folder {run_dir} " "since it may not contain a jobflow-remote executionSome files are present, " "but jfremote_in.json is missing", ) return False