qq_lib.properties.info
Structured storage and serialization of qq job metadata.
This module defines the Info dataclass, which provides a representation
of qq job information: submission parameters, resource requests, job state,
timing data, dependencies, and execution context. It handles
loading and exporting YAML info files both locally and from remote hosts, and
offers minimal helpers such as command-line reconstruction for resubmission.
Info focuses strictly on data representation and safe serialization; higher-level
logic (state interpretation, batch-system interaction, consistency checks) is
implemented in Informer and related components.
1# Released under MIT License. 2# Copyright (c) 2025-2026 Ladislav Bartos and Robert Vacha Lab 3 4""" 5Structured storage and serialization of qq job metadata. 6 7This module defines the `Info` dataclass, which provides a representation 8of qq job information: submission parameters, resource requests, job state, 9timing data, dependencies, and execution context. It handles 10loading and exporting YAML info files both locally and from remote hosts, and 11offers minimal helpers such as command-line reconstruction for resubmission. 12 13`Info` focuses strictly on data representation and safe serialization; higher-level 14logic (state interpretation, batch-system interaction, consistency checks) is 15implemented in `Informer` and related components. 16""" 17 18from dataclasses import dataclass, field, fields 19from datetime import datetime 20from pathlib import Path 21from typing import Self 22 23import yaml 24 25from qq_lib.batch.interface import AnyBatchClass, BatchInterface 26from qq_lib.core.common import load_yaml_dumper, load_yaml_loader 27from qq_lib.core.config import CFG 28from qq_lib.core.error import QQError 29from qq_lib.core.logger import get_logger 30from qq_lib.properties.depend import Depend 31from qq_lib.properties.interpreter import Interpreter 32from qq_lib.properties.resubmit_host import ResubmitHost 33from qq_lib.properties.transfer_mode import Success, TransferMode 34 35from .job_type import JobType 36from .loop import LoopInfo 37from .resources import Resources 38from .states import NaiveState 39 40logger = get_logger(__name__) 41 42SafeLoader: type[yaml.SafeLoader] = load_yaml_loader() 43Dumper: type[yaml.Dumper] = load_yaml_dumper() 44 45 46@dataclass 47class Info: 48 """ 49 Dataclass storing information about a qq job. 50 51 Exposes only minimal functionality for loading, exporting, and basic access. 52 More complex operations, such as transforming or combining the data 53 should be implemented in Informer. 54 """ 55 56 # The batch system class used 57 batch_system: AnyBatchClass 58 59 # Version of qq that submitted the job 60 qq_version: str 61 62 # Name of the user who submitted the job 63 username: str 64 65 # Job identifier inside the batch system 66 job_id: str 67 68 # Job name 69 job_name: str 70 71 # Name of the script executed 72 script_name: str 73 74 # Queue the job was submitted to 75 queue: str 76 77 # Type of the qq job 78 job_type: JobType 79 80 # Host from which the job was submitted 81 input_machine: str 82 83 # Directory from which the job was submitted 84 input_dir: Path 85 86 # Job state according to qq 87 job_state: NaiveState 88 89 # Job submission timestamp 90 submission_time: datetime 91 92 # Name of the file for storing standard output of the executed script 93 stdout_file: str 94 95 # Name of the file for storing error output of the executed script 96 stderr_file: str 97 98 # Resources allocated to the job 99 resources: Resources 100 101 # List of files and directories to not copy to the working directory. 102 excluded_files: list[Path] = field(default_factory=list) 103 104 # List of files and directories to explicitly copy to the working directory. 105 included_files: list[Path] = field(default_factory=list) 106 107 # Mode of transferring files from the working directory to the input directory after job completion. 108 transfer_mode: list[TransferMode] = field(default_factory=lambda: [Success()]) 109 110 # List of dependencies. 111 depend: list[Depend] = field(default_factory=list) 112 113 # Loop job-associated information. 114 loop_info: LoopInfo | None = None 115 116 # Account associated with the job 117 account: str | None = None 118 119 # Batch server the job was submitted to 120 # Can be `None` which indicates the job was submitted 121 # to the default (main) batch server the input machine is connected to 122 server: str | None = None 123 124 # Hosts from which a loop job or a continuous job should be resubmitted 125 resubmit_from: list[ResubmitHost] = field(default_factory=list) 126 127 # Interpreter to use for running the submitted script 128 interpreter: Interpreter | None = None 129 130 # Job start time 131 start_time: datetime | None = None 132 133 # Main node assigned to the job 134 main_node: str | None = None 135 136 # All nodes assigned to the job 137 all_nodes: list[str] | None = None 138 139 # Working directory 140 work_dir: Path | None = None 141 142 # Job completion time 143 completion_time: datetime | None = None 144 145 # Exit code of qq run 146 job_exit_code: int | None = None 147 148 @classmethod 149 def from_file(cls, file: Path, host: str | None = None) -> Self: 150 """ 151 Load an Info instance from a YAML file, either locally or on a remote host. 152 153 If `host` is provided, the file will be read from the remote host using 154 the batch system's `read_remote_file` method. Otherwise, the file is read locally. 155 156 Args: 157 file (Path): Path to the YAML qq info file. 158 host (str | None): Optional hostname of the remote machine where the file resides. 159 If None, the file is assumed to be local. 160 161 Returns: 162 Info: Instance constructed from the file. 163 164 Raises: 165 QQError: If the file does not exist, cannot be reached, cannot be parsed, 166 or does not contain all mandatory information. 167 """ 168 try: 169 if host: 170 # remote file 171 logger.debug(f"Loading qq info from '{file}' on '{host}'.") 172 173 BatchSystem = BatchInterface.from_env_var_or_guess() 174 data: dict[str, object] = yaml.load( 175 BatchSystem.read_remote_file(host, file), 176 Loader=SafeLoader, 177 ) 178 else: 179 # local file 180 logger.debug(f"Loading qq info from '{file}'.") 181 182 try: 183 with file.open("r") as input: 184 data: dict[str, object] = yaml.load(input, Loader=SafeLoader) 185 except FileNotFoundError: 186 raise QQError(f"qq info file '{file}' does not exist.") 187 except PermissionError: 188 raise QQError( 189 f"No permission to read file '{file}' or access its parent directory." 190 ) 191 except IsADirectoryError: 192 raise QQError(f"Expected a file but path is a directory: {file}.") 193 except UnicodeDecodeError as e: 194 raise QQError(f"File is not valid UTF-8 text: {file}.") from e 195 except yaml.YAMLError as e: 196 raise QQError(f"Failed to parse YAML in {file}: {e}.") from e 197 198 return cls._from_dict(data) 199 except yaml.YAMLError as e: 200 raise QQError(f"Could not parse the qq info file '{file}': {e}.") from e 201 except TypeError as e: 202 raise QQError(f"Invalid qq info file '{file}': {e}.") from e 203 204 def to_file(self, file: Path, host: str | None = None) -> None: 205 """ 206 Export this Info instance to a YAML file, either locally or on a remote host. 207 208 If `host` is provided, the file will be written to the remote host using 209 the batch system's `write_remote_file` method. Otherwise, the file is written locally. 210 211 Args: 212 file (Path): Path to write the YAML file. 213 host (str | None): Optional hostname of the remote machine where the file should be written. 214 If None, the file is written locally. 215 216 Raises: 217 QQError: If the file cannot be created, reached, or written to. 218 """ 219 try: 220 content = "# qq job info file\n" + self._to_yaml() + "\n" 221 222 if host: 223 # remote file 224 logger.debug(f"Exporting qq info into '{file}' on '{host}'.") 225 self.batch_system.write_remote_file(host, file, content) 226 else: 227 # local file 228 logger.debug(f"Exporting qq info into '{file}'.") 229 with file.open("w") as output: 230 output.write(content) 231 except Exception as e: 232 raise QQError(f"Cannot create or write to file '{file}': {e}") from e 233 234 def _to_yaml(self) -> str: 235 """ 236 Serialize the Info instance to a YAML string. 237 238 Returns: 239 str: YAML representation of the Info object. 240 """ 241 return yaml.dump( 242 self._to_dict(), default_flow_style=False, sort_keys=False, Dumper=Dumper 243 ) 244 245 def _to_dict(self) -> dict[str, object]: 246 """ 247 Convert the Info instance into a dictionary of string-object pairs. 248 Fields that are None are ignored. 249 250 Returns: 251 dict[str, object]: Dictionary containing all fields with non-None values, 252 converting enums and nested objects appropriately. 253 """ 254 result: dict[str, object] = {} 255 256 for f in fields(self): 257 value = getattr(self, f.name) 258 # ignore None fields 259 if value is None: 260 continue 261 262 # empty lists are ignored 263 if isinstance(value, list) and not value: 264 continue 265 266 # convert job type 267 if f.type == JobType: 268 result[f.name] = str(value) 269 # convert resources 270 elif f.type == Resources or f.type == LoopInfo | None: 271 result[f.name] = value.to_dict() 272 # convert the state and the batch system 273 elif ( 274 f.type == NaiveState 275 or f.type == AnyBatchClass 276 or f.type == Path 277 or f.type == Path | None 278 ): 279 result[f.name] = str(value) 280 # convert list of excluded/included files 281 elif f.type == list[Path]: 282 result[f.name] = [str(x) for x in value] 283 # convert transfer modes, resubmit hosts, and dependencies 284 elif ( 285 f.type == list[TransferMode] 286 or f.type == list[ResubmitHost] 287 or f.type == list[Depend] 288 ): 289 result[f.name] = [x.to_str() for x in value] 290 # convert interpreter 291 elif f.type == Interpreter or f.type == Interpreter | None: 292 result[f.name] = value.to_dict() 293 # convert timestamp 294 elif f.type == datetime or f.type == datetime | None: 295 result[f.name] = value.strftime(CFG.date_formats.standard) 296 else: 297 result[f.name] = value 298 299 return result 300 301 @classmethod 302 def _from_dict(cls, data: dict[str, object]) -> Self: 303 """ 304 Construct an Info instance from a dictionary. 305 306 Args: 307 data (dict[str, object]): Dictionary containing field names and values. 308 309 Returns: 310 Info: An Info instance. 311 312 Raises: 313 TypeError: If required fields are missing. 314 """ 315 init_kwargs = {} 316 for f in fields(cls): 317 name = f.name 318 # skip undefined fields 319 if name not in data: 320 continue 321 322 value = data[name] 323 324 # convert job type 325 if f.type == JobType and isinstance(value, str): 326 init_kwargs[name] = JobType.from_str(value) 327 # convert optional loop job info 328 elif f.type == LoopInfo | None and isinstance(value, dict): 329 init_kwargs[name] = LoopInfo.from_dict(value) # ty: ignore[invalid-argument-type] 330 # convert resources 331 elif f.type == Resources: 332 init_kwargs[name] = Resources(**value) # ty: ignore[invalid-argument-type] 333 # convert the batch system 334 elif f.type == AnyBatchClass and isinstance(value, str): 335 init_kwargs[name] = BatchInterface.from_str(value) 336 # convert the job state 337 elif f.type == NaiveState and isinstance(value, str): 338 init_kwargs[name] = ( 339 NaiveState.from_str(value) if value else NaiveState.UNKNOWN 340 ) 341 # convert paths (incl. optional paths) 342 elif f.type == Path or f.type == Path | None: 343 init_kwargs[name] = Path(value) # ty: ignore[invalid-argument-type] 344 # convert the list of excluded paths 345 elif f.type == list[Path] and isinstance(value, list): 346 init_kwargs[name] = [ 347 Path(v) if isinstance(v, str) else v for v in value 348 ] 349 # convert transfer modes 350 elif f.type == list[TransferMode] and isinstance(value, list): 351 init_kwargs[name] = [TransferMode.from_str(x) for x in value] # ty: ignore[invalid-argument-type] 352 # convert dependencies 353 elif f.type == list[Depend] and isinstance(value, list): 354 init_kwargs[name] = [Depend.from_str(x) for x in value] # ty: ignore[invalid-argument-type] 355 # convert resubmit hosts 356 elif f.type == list[ResubmitHost] and isinstance(value, list): 357 init_kwargs[name] = [ResubmitHost.from_str(x) for x in value] # ty: ignore[invalid-argument-type] 358 # convert interpreter from string (legacy) 359 elif (f.type == Interpreter or f.type == Interpreter | None) and isinstance( 360 value, str 361 ): 362 init_kwargs[name] = Interpreter.from_str(value) 363 # convert interpreter from dict 364 elif (f.type == Interpreter or f.type == Interpreter | None) and isinstance( 365 value, dict 366 ): 367 init_kwargs[name] = Interpreter.from_dict(value) # ty: ignore[invalid-argument-type] 368 # convert timestamp 369 elif (f.type == datetime or f.type == datetime | None) and isinstance( 370 value, str 371 ): 372 init_kwargs[name] = datetime.strptime(value, CFG.date_formats.standard) 373 else: 374 init_kwargs[name] = value 375 376 return cls(**init_kwargs)
47@dataclass 48class Info: 49 """ 50 Dataclass storing information about a qq job. 51 52 Exposes only minimal functionality for loading, exporting, and basic access. 53 More complex operations, such as transforming or combining the data 54 should be implemented in Informer. 55 """ 56 57 # The batch system class used 58 batch_system: AnyBatchClass 59 60 # Version of qq that submitted the job 61 qq_version: str 62 63 # Name of the user who submitted the job 64 username: str 65 66 # Job identifier inside the batch system 67 job_id: str 68 69 # Job name 70 job_name: str 71 72 # Name of the script executed 73 script_name: str 74 75 # Queue the job was submitted to 76 queue: str 77 78 # Type of the qq job 79 job_type: JobType 80 81 # Host from which the job was submitted 82 input_machine: str 83 84 # Directory from which the job was submitted 85 input_dir: Path 86 87 # Job state according to qq 88 job_state: NaiveState 89 90 # Job submission timestamp 91 submission_time: datetime 92 93 # Name of the file for storing standard output of the executed script 94 stdout_file: str 95 96 # Name of the file for storing error output of the executed script 97 stderr_file: str 98 99 # Resources allocated to the job 100 resources: Resources 101 102 # List of files and directories to not copy to the working directory. 103 excluded_files: list[Path] = field(default_factory=list) 104 105 # List of files and directories to explicitly copy to the working directory. 106 included_files: list[Path] = field(default_factory=list) 107 108 # Mode of transferring files from the working directory to the input directory after job completion. 109 transfer_mode: list[TransferMode] = field(default_factory=lambda: [Success()]) 110 111 # List of dependencies. 112 depend: list[Depend] = field(default_factory=list) 113 114 # Loop job-associated information. 115 loop_info: LoopInfo | None = None 116 117 # Account associated with the job 118 account: str | None = None 119 120 # Batch server the job was submitted to 121 # Can be `None` which indicates the job was submitted 122 # to the default (main) batch server the input machine is connected to 123 server: str | None = None 124 125 # Hosts from which a loop job or a continuous job should be resubmitted 126 resubmit_from: list[ResubmitHost] = field(default_factory=list) 127 128 # Interpreter to use for running the submitted script 129 interpreter: Interpreter | None = None 130 131 # Job start time 132 start_time: datetime | None = None 133 134 # Main node assigned to the job 135 main_node: str | None = None 136 137 # All nodes assigned to the job 138 all_nodes: list[str] | None = None 139 140 # Working directory 141 work_dir: Path | None = None 142 143 # Job completion time 144 completion_time: datetime | None = None 145 146 # Exit code of qq run 147 job_exit_code: int | None = None 148 149 @classmethod 150 def from_file(cls, file: Path, host: str | None = None) -> Self: 151 """ 152 Load an Info instance from a YAML file, either locally or on a remote host. 153 154 If `host` is provided, the file will be read from the remote host using 155 the batch system's `read_remote_file` method. Otherwise, the file is read locally. 156 157 Args: 158 file (Path): Path to the YAML qq info file. 159 host (str | None): Optional hostname of the remote machine where the file resides. 160 If None, the file is assumed to be local. 161 162 Returns: 163 Info: Instance constructed from the file. 164 165 Raises: 166 QQError: If the file does not exist, cannot be reached, cannot be parsed, 167 or does not contain all mandatory information. 168 """ 169 try: 170 if host: 171 # remote file 172 logger.debug(f"Loading qq info from '{file}' on '{host}'.") 173 174 BatchSystem = BatchInterface.from_env_var_or_guess() 175 data: dict[str, object] = yaml.load( 176 BatchSystem.read_remote_file(host, file), 177 Loader=SafeLoader, 178 ) 179 else: 180 # local file 181 logger.debug(f"Loading qq info from '{file}'.") 182 183 try: 184 with file.open("r") as input: 185 data: dict[str, object] = yaml.load(input, Loader=SafeLoader) 186 except FileNotFoundError: 187 raise QQError(f"qq info file '{file}' does not exist.") 188 except PermissionError: 189 raise QQError( 190 f"No permission to read file '{file}' or access its parent directory." 191 ) 192 except IsADirectoryError: 193 raise QQError(f"Expected a file but path is a directory: {file}.") 194 except UnicodeDecodeError as e: 195 raise QQError(f"File is not valid UTF-8 text: {file}.") from e 196 except yaml.YAMLError as e: 197 raise QQError(f"Failed to parse YAML in {file}: {e}.") from e 198 199 return cls._from_dict(data) 200 except yaml.YAMLError as e: 201 raise QQError(f"Could not parse the qq info file '{file}': {e}.") from e 202 except TypeError as e: 203 raise QQError(f"Invalid qq info file '{file}': {e}.") from e 204 205 def to_file(self, file: Path, host: str | None = None) -> None: 206 """ 207 Export this Info instance to a YAML file, either locally or on a remote host. 208 209 If `host` is provided, the file will be written to the remote host using 210 the batch system's `write_remote_file` method. Otherwise, the file is written locally. 211 212 Args: 213 file (Path): Path to write the YAML file. 214 host (str | None): Optional hostname of the remote machine where the file should be written. 215 If None, the file is written locally. 216 217 Raises: 218 QQError: If the file cannot be created, reached, or written to. 219 """ 220 try: 221 content = "# qq job info file\n" + self._to_yaml() + "\n" 222 223 if host: 224 # remote file 225 logger.debug(f"Exporting qq info into '{file}' on '{host}'.") 226 self.batch_system.write_remote_file(host, file, content) 227 else: 228 # local file 229 logger.debug(f"Exporting qq info into '{file}'.") 230 with file.open("w") as output: 231 output.write(content) 232 except Exception as e: 233 raise QQError(f"Cannot create or write to file '{file}': {e}") from e 234 235 def _to_yaml(self) -> str: 236 """ 237 Serialize the Info instance to a YAML string. 238 239 Returns: 240 str: YAML representation of the Info object. 241 """ 242 return yaml.dump( 243 self._to_dict(), default_flow_style=False, sort_keys=False, Dumper=Dumper 244 ) 245 246 def _to_dict(self) -> dict[str, object]: 247 """ 248 Convert the Info instance into a dictionary of string-object pairs. 249 Fields that are None are ignored. 250 251 Returns: 252 dict[str, object]: Dictionary containing all fields with non-None values, 253 converting enums and nested objects appropriately. 254 """ 255 result: dict[str, object] = {} 256 257 for f in fields(self): 258 value = getattr(self, f.name) 259 # ignore None fields 260 if value is None: 261 continue 262 263 # empty lists are ignored 264 if isinstance(value, list) and not value: 265 continue 266 267 # convert job type 268 if f.type == JobType: 269 result[f.name] = str(value) 270 # convert resources 271 elif f.type == Resources or f.type == LoopInfo | None: 272 result[f.name] = value.to_dict() 273 # convert the state and the batch system 274 elif ( 275 f.type == NaiveState 276 or f.type == AnyBatchClass 277 or f.type == Path 278 or f.type == Path | None 279 ): 280 result[f.name] = str(value) 281 # convert list of excluded/included files 282 elif f.type == list[Path]: 283 result[f.name] = [str(x) for x in value] 284 # convert transfer modes, resubmit hosts, and dependencies 285 elif ( 286 f.type == list[TransferMode] 287 or f.type == list[ResubmitHost] 288 or f.type == list[Depend] 289 ): 290 result[f.name] = [x.to_str() for x in value] 291 # convert interpreter 292 elif f.type == Interpreter or f.type == Interpreter | None: 293 result[f.name] = value.to_dict() 294 # convert timestamp 295 elif f.type == datetime or f.type == datetime | None: 296 result[f.name] = value.strftime(CFG.date_formats.standard) 297 else: 298 result[f.name] = value 299 300 return result 301 302 @classmethod 303 def _from_dict(cls, data: dict[str, object]) -> Self: 304 """ 305 Construct an Info instance from a dictionary. 306 307 Args: 308 data (dict[str, object]): Dictionary containing field names and values. 309 310 Returns: 311 Info: An Info instance. 312 313 Raises: 314 TypeError: If required fields are missing. 315 """ 316 init_kwargs = {} 317 for f in fields(cls): 318 name = f.name 319 # skip undefined fields 320 if name not in data: 321 continue 322 323 value = data[name] 324 325 # convert job type 326 if f.type == JobType and isinstance(value, str): 327 init_kwargs[name] = JobType.from_str(value) 328 # convert optional loop job info 329 elif f.type == LoopInfo | None and isinstance(value, dict): 330 init_kwargs[name] = LoopInfo.from_dict(value) # ty: ignore[invalid-argument-type] 331 # convert resources 332 elif f.type == Resources: 333 init_kwargs[name] = Resources(**value) # ty: ignore[invalid-argument-type] 334 # convert the batch system 335 elif f.type == AnyBatchClass and isinstance(value, str): 336 init_kwargs[name] = BatchInterface.from_str(value) 337 # convert the job state 338 elif f.type == NaiveState and isinstance(value, str): 339 init_kwargs[name] = ( 340 NaiveState.from_str(value) if value else NaiveState.UNKNOWN 341 ) 342 # convert paths (incl. optional paths) 343 elif f.type == Path or f.type == Path | None: 344 init_kwargs[name] = Path(value) # ty: ignore[invalid-argument-type] 345 # convert the list of excluded paths 346 elif f.type == list[Path] and isinstance(value, list): 347 init_kwargs[name] = [ 348 Path(v) if isinstance(v, str) else v for v in value 349 ] 350 # convert transfer modes 351 elif f.type == list[TransferMode] and isinstance(value, list): 352 init_kwargs[name] = [TransferMode.from_str(x) for x in value] # ty: ignore[invalid-argument-type] 353 # convert dependencies 354 elif f.type == list[Depend] and isinstance(value, list): 355 init_kwargs[name] = [Depend.from_str(x) for x in value] # ty: ignore[invalid-argument-type] 356 # convert resubmit hosts 357 elif f.type == list[ResubmitHost] and isinstance(value, list): 358 init_kwargs[name] = [ResubmitHost.from_str(x) for x in value] # ty: ignore[invalid-argument-type] 359 # convert interpreter from string (legacy) 360 elif (f.type == Interpreter or f.type == Interpreter | None) and isinstance( 361 value, str 362 ): 363 init_kwargs[name] = Interpreter.from_str(value) 364 # convert interpreter from dict 365 elif (f.type == Interpreter or f.type == Interpreter | None) and isinstance( 366 value, dict 367 ): 368 init_kwargs[name] = Interpreter.from_dict(value) # ty: ignore[invalid-argument-type] 369 # convert timestamp 370 elif (f.type == datetime or f.type == datetime | None) and isinstance( 371 value, str 372 ): 373 init_kwargs[name] = datetime.strptime(value, CFG.date_formats.standard) 374 else: 375 init_kwargs[name] = value 376 377 return cls(**init_kwargs)
Dataclass storing information about a qq job.
Exposes only minimal functionality for loading, exporting, and basic access. More complex operations, such as transforming or combining the data should be implemented in Informer.
149 @classmethod 150 def from_file(cls, file: Path, host: str | None = None) -> Self: 151 """ 152 Load an Info instance from a YAML file, either locally or on a remote host. 153 154 If `host` is provided, the file will be read from the remote host using 155 the batch system's `read_remote_file` method. Otherwise, the file is read locally. 156 157 Args: 158 file (Path): Path to the YAML qq info file. 159 host (str | None): Optional hostname of the remote machine where the file resides. 160 If None, the file is assumed to be local. 161 162 Returns: 163 Info: Instance constructed from the file. 164 165 Raises: 166 QQError: If the file does not exist, cannot be reached, cannot be parsed, 167 or does not contain all mandatory information. 168 """ 169 try: 170 if host: 171 # remote file 172 logger.debug(f"Loading qq info from '{file}' on '{host}'.") 173 174 BatchSystem = BatchInterface.from_env_var_or_guess() 175 data: dict[str, object] = yaml.load( 176 BatchSystem.read_remote_file(host, file), 177 Loader=SafeLoader, 178 ) 179 else: 180 # local file 181 logger.debug(f"Loading qq info from '{file}'.") 182 183 try: 184 with file.open("r") as input: 185 data: dict[str, object] = yaml.load(input, Loader=SafeLoader) 186 except FileNotFoundError: 187 raise QQError(f"qq info file '{file}' does not exist.") 188 except PermissionError: 189 raise QQError( 190 f"No permission to read file '{file}' or access its parent directory." 191 ) 192 except IsADirectoryError: 193 raise QQError(f"Expected a file but path is a directory: {file}.") 194 except UnicodeDecodeError as e: 195 raise QQError(f"File is not valid UTF-8 text: {file}.") from e 196 except yaml.YAMLError as e: 197 raise QQError(f"Failed to parse YAML in {file}: {e}.") from e 198 199 return cls._from_dict(data) 200 except yaml.YAMLError as e: 201 raise QQError(f"Could not parse the qq info file '{file}': {e}.") from e 202 except TypeError as e: 203 raise QQError(f"Invalid qq info file '{file}': {e}.") from e
Load an Info instance from a YAML file, either locally or on a remote host.
If host is provided, the file will be read from the remote host using
the batch system's read_remote_file method. Otherwise, the file is read locally.
Arguments:
- file (Path): Path to the YAML qq info file.
- host (str | None): Optional hostname of the remote machine where the file resides. If None, the file is assumed to be local.
Returns:
Info: Instance constructed from the file.
Raises:
- QQError: If the file does not exist, cannot be reached, cannot be parsed, or does not contain all mandatory information.
205 def to_file(self, file: Path, host: str | None = None) -> None: 206 """ 207 Export this Info instance to a YAML file, either locally or on a remote host. 208 209 If `host` is provided, the file will be written to the remote host using 210 the batch system's `write_remote_file` method. Otherwise, the file is written locally. 211 212 Args: 213 file (Path): Path to write the YAML file. 214 host (str | None): Optional hostname of the remote machine where the file should be written. 215 If None, the file is written locally. 216 217 Raises: 218 QQError: If the file cannot be created, reached, or written to. 219 """ 220 try: 221 content = "# qq job info file\n" + self._to_yaml() + "\n" 222 223 if host: 224 # remote file 225 logger.debug(f"Exporting qq info into '{file}' on '{host}'.") 226 self.batch_system.write_remote_file(host, file, content) 227 else: 228 # local file 229 logger.debug(f"Exporting qq info into '{file}'.") 230 with file.open("w") as output: 231 output.write(content) 232 except Exception as e: 233 raise QQError(f"Cannot create or write to file '{file}': {e}") from e
Export this Info instance to a YAML file, either locally or on a remote host.
If host is provided, the file will be written to the remote host using
the batch system's write_remote_file method. Otherwise, the file is written locally.
Arguments:
- file (Path): Path to write the YAML file.
- host (str | None): Optional hostname of the remote machine where the file should be written. If None, the file is written locally.
Raises:
- QQError: If the file cannot be created, reached, or written to.