qq_lib.properties.depend
Representation and handling of qq job dependencies.
This module defines DependType, an enumeration of supported dependency
conditions and the Depend dataclass, which stores both the dependency type
and referenced job IDs.
1# Released under MIT License. 2# Copyright (c) 2025-2026 Ladislav Bartos and Robert Vacha Lab 3 4""" 5Representation and handling of qq job dependencies. 6 7This module defines `DependType`, an enumeration of supported dependency 8conditions and the `Depend` dataclass, which stores both the dependency type 9and referenced job IDs. 10""" 11 12import re 13from dataclasses import dataclass 14from enum import Enum 15from typing import Self 16 17from qq_lib.batch.interface import AnyBatchClass 18from qq_lib.core.error import QQError 19from qq_lib.core.logger import get_logger 20 21logger = get_logger(__name__) 22 23 24class DependType(Enum): 25 """ 26 Enumeration of supported job dependency types. 27 """ 28 29 # Job should start after other job has started. 30 AFTER_START = 0 31 32 # Job should start after other job has finished successfully. 33 AFTER_SUCCESS = 1 34 35 # Job should start after other job has failed or been killed. 36 AFTER_FAILURE = 2 37 38 # Job should start after other job has finished, failed or been killed. 39 AFTER_COMPLETION = 3 40 41 @classmethod 42 def from_str(cls, string: str) -> "DependType": 43 """ 44 Convert a dependency string keyword to a `DependType`. 45 46 Supported dependency keywords: 47 - "after" - AFTER_START 48 - "afterok" - AFTER_SUCCESS 49 - "afternotok" - AFTER_FAILURE 50 - "afterany" - AFTER_COMPLETION 51 52 Args: 53 string (str): Dependency type keyword. 54 55 Returns: 56 DependType: Corresponding dependency type enum. 57 58 Raises: 59 QQError: If the given string does not correspond to any known dependency type. 60 """ 61 match string: 62 case "after": 63 return cls.AFTER_START 64 case "afterok": 65 return cls.AFTER_SUCCESS 66 case "afternotok": 67 return cls.AFTER_FAILURE 68 case "afterany": 69 return cls.AFTER_COMPLETION 70 71 raise QQError(f"Unknown dependency type '{string}'") 72 73 def to_str(self) -> str: 74 """ 75 Convert the dependency type to its corresponding string keyword. 76 77 Returns: 78 str: One of the recognized dependency keywords: 79 - "after" for AFTER_START 80 - "afterok" for AFTER_SUCCESS 81 - "afternotok" for AFTER_FAILURE 82 - "afterany" for AFTER_COMPLETION 83 84 Raises: 85 QQError: If the dependency type is unrecognized (should not occur). 86 """ 87 match self: 88 case DependType.AFTER_START: 89 return "after" 90 case DependType.AFTER_SUCCESS: 91 return "afterok" 92 case DependType.AFTER_FAILURE: 93 return "afternotok" 94 case DependType.AFTER_COMPLETION: 95 return "afterany" 96 97 raise QQError( 98 f"Unknown dependency type '{self}'. This is a bug; please report it." 99 ) 100 101 102@dataclass(frozen=True) 103class Depend: 104 """ 105 Representation of a parsed job dependency. 106 107 A dependency defines both the type of dependency (e.g., after start, after success) 108 and a list of job IDs that the current job depends on. 109 110 Attributes: 111 type (DependType): The type of dependency, determined by the dependency keyword. 112 jobs (list[str]): List of job IDs this dependency refers to. 113 """ 114 115 type: DependType 116 jobs: list[str] 117 118 @classmethod 119 def from_str(cls, raw_depend: str): 120 """ 121 Initialize a `Depend` object by parsing a raw dependency specification. 122 123 The expected format is `<type>=<job_id>[:<job_id>...]`, where: 124 - `<type>` is one of `"after"`, `"afterok"`, `"afternotok"`, or `"afterany"`. 125 - `<job_id>` values are colon-separated job identifiers. 126 127 Args: 128 raw_depend (str): Raw dependency string to parse. 129 130 Raises: 131 QQError: If the dependency string is malformed or cannot be parsed. 132 """ 133 logger.debug(f"Depend string to parse: '{raw_depend}'.") 134 try: 135 raw_type, raw_jobs = raw_depend.split("=") 136 jobs = raw_jobs.split(":") 137 if any(x.strip() == "" for x in jobs): 138 raise QQError("Missing job id.") 139 type = DependType.from_str(raw_type) 140 return cls(type, jobs) 141 except Exception as e: 142 raise QQError( 143 f"Could not parse dependency specification '{raw_depend}': {e}." 144 ) from e 145 146 @classmethod 147 def multi_from_str(cls, raw: str) -> list[Self]: 148 """ 149 Parse a combined dependency string into a list of `Depend` objects. 150 151 The input may contain multiple dependency expressions separated by commas, 152 spaces, or both. Each expression will be parsed into a separate `Depend` instance. 153 154 Args: 155 raw (str): Raw dependency string possibly containing multiple expressions. 156 157 Returns: 158 list[Depend]: A list of parsed dependency objects. 159 160 Raises: 161 QQError: If any dependency expression within the string is malformed. 162 """ 163 logger.debug(f"Full depend string to parse: '{raw}'.") 164 return [Depend.from_str(dep) for dep in re.split(r"[,\s]+", raw.strip()) if dep] 165 166 def to_str(self) -> str: 167 """ 168 Convert the full Depend object to a string representation. 169 170 Format: 171 <type>=<job_id1>:<job_id2>:... 172 173 Returns: 174 str: String representation combining the dependency type keyword and 175 the colon-separated list of job IDs. 176 """ 177 return f"{self.type.to_str()}={':'.join(self.jobs)}" 178 179 180def filter_dependencies( 181 batch_system: AnyBatchClass, dependencies: list[Depend] 182) -> list[Depend]: 183 """ 184 Filter dependencies to only include those that are still present in the batch system. 185 186 Args: 187 batch_system (AnyBatchClass): The batch system to check job presence against. 188 dependencies (list[Depend]): The list of dependencies to filter. 189 190 Returns: 191 list[Depend]: The filtered list of dependencies. 192 """ 193 filtered: list[Depend] = [] 194 for depend in dependencies: 195 # get jobs that are still present in the batch system 196 valid_jobs = [ 197 job_id 198 for job_id in depend.jobs 199 if not (batch_system.get_batch_job(job_id)).is_empty() 200 ] 201 202 if valid_jobs: 203 filtered.append(Depend(type=depend.type, jobs=valid_jobs)) 204 205 logger.debug(f"Filtered dependencies: {filtered}.") 206 return filtered
25class DependType(Enum): 26 """ 27 Enumeration of supported job dependency types. 28 """ 29 30 # Job should start after other job has started. 31 AFTER_START = 0 32 33 # Job should start after other job has finished successfully. 34 AFTER_SUCCESS = 1 35 36 # Job should start after other job has failed or been killed. 37 AFTER_FAILURE = 2 38 39 # Job should start after other job has finished, failed or been killed. 40 AFTER_COMPLETION = 3 41 42 @classmethod 43 def from_str(cls, string: str) -> "DependType": 44 """ 45 Convert a dependency string keyword to a `DependType`. 46 47 Supported dependency keywords: 48 - "after" - AFTER_START 49 - "afterok" - AFTER_SUCCESS 50 - "afternotok" - AFTER_FAILURE 51 - "afterany" - AFTER_COMPLETION 52 53 Args: 54 string (str): Dependency type keyword. 55 56 Returns: 57 DependType: Corresponding dependency type enum. 58 59 Raises: 60 QQError: If the given string does not correspond to any known dependency type. 61 """ 62 match string: 63 case "after": 64 return cls.AFTER_START 65 case "afterok": 66 return cls.AFTER_SUCCESS 67 case "afternotok": 68 return cls.AFTER_FAILURE 69 case "afterany": 70 return cls.AFTER_COMPLETION 71 72 raise QQError(f"Unknown dependency type '{string}'") 73 74 def to_str(self) -> str: 75 """ 76 Convert the dependency type to its corresponding string keyword. 77 78 Returns: 79 str: One of the recognized dependency keywords: 80 - "after" for AFTER_START 81 - "afterok" for AFTER_SUCCESS 82 - "afternotok" for AFTER_FAILURE 83 - "afterany" for AFTER_COMPLETION 84 85 Raises: 86 QQError: If the dependency type is unrecognized (should not occur). 87 """ 88 match self: 89 case DependType.AFTER_START: 90 return "after" 91 case DependType.AFTER_SUCCESS: 92 return "afterok" 93 case DependType.AFTER_FAILURE: 94 return "afternotok" 95 case DependType.AFTER_COMPLETION: 96 return "afterany" 97 98 raise QQError( 99 f"Unknown dependency type '{self}'. This is a bug; please report it." 100 )
Enumeration of supported job dependency types.
42 @classmethod 43 def from_str(cls, string: str) -> "DependType": 44 """ 45 Convert a dependency string keyword to a `DependType`. 46 47 Supported dependency keywords: 48 - "after" - AFTER_START 49 - "afterok" - AFTER_SUCCESS 50 - "afternotok" - AFTER_FAILURE 51 - "afterany" - AFTER_COMPLETION 52 53 Args: 54 string (str): Dependency type keyword. 55 56 Returns: 57 DependType: Corresponding dependency type enum. 58 59 Raises: 60 QQError: If the given string does not correspond to any known dependency type. 61 """ 62 match string: 63 case "after": 64 return cls.AFTER_START 65 case "afterok": 66 return cls.AFTER_SUCCESS 67 case "afternotok": 68 return cls.AFTER_FAILURE 69 case "afterany": 70 return cls.AFTER_COMPLETION 71 72 raise QQError(f"Unknown dependency type '{string}'")
Convert a dependency string keyword to a DependType.
Supported dependency keywords:
- "after" - AFTER_START
- "afterok" - AFTER_SUCCESS
- "afternotok" - AFTER_FAILURE
- "afterany" - AFTER_COMPLETION
Arguments:
- string (str): Dependency type keyword.
Returns:
DependType: Corresponding dependency type enum.
Raises:
- QQError: If the given string does not correspond to any known dependency type.
74 def to_str(self) -> str: 75 """ 76 Convert the dependency type to its corresponding string keyword. 77 78 Returns: 79 str: One of the recognized dependency keywords: 80 - "after" for AFTER_START 81 - "afterok" for AFTER_SUCCESS 82 - "afternotok" for AFTER_FAILURE 83 - "afterany" for AFTER_COMPLETION 84 85 Raises: 86 QQError: If the dependency type is unrecognized (should not occur). 87 """ 88 match self: 89 case DependType.AFTER_START: 90 return "after" 91 case DependType.AFTER_SUCCESS: 92 return "afterok" 93 case DependType.AFTER_FAILURE: 94 return "afternotok" 95 case DependType.AFTER_COMPLETION: 96 return "afterany" 97 98 raise QQError( 99 f"Unknown dependency type '{self}'. This is a bug; please report it." 100 )
Convert the dependency type to its corresponding string keyword.
Returns:
str: One of the recognized dependency keywords: - "after" for AFTER_START - "afterok" for AFTER_SUCCESS - "afternotok" for AFTER_FAILURE - "afterany" for AFTER_COMPLETION
Raises:
- QQError: If the dependency type is unrecognized (should not occur).
103@dataclass(frozen=True) 104class Depend: 105 """ 106 Representation of a parsed job dependency. 107 108 A dependency defines both the type of dependency (e.g., after start, after success) 109 and a list of job IDs that the current job depends on. 110 111 Attributes: 112 type (DependType): The type of dependency, determined by the dependency keyword. 113 jobs (list[str]): List of job IDs this dependency refers to. 114 """ 115 116 type: DependType 117 jobs: list[str] 118 119 @classmethod 120 def from_str(cls, raw_depend: str): 121 """ 122 Initialize a `Depend` object by parsing a raw dependency specification. 123 124 The expected format is `<type>=<job_id>[:<job_id>...]`, where: 125 - `<type>` is one of `"after"`, `"afterok"`, `"afternotok"`, or `"afterany"`. 126 - `<job_id>` values are colon-separated job identifiers. 127 128 Args: 129 raw_depend (str): Raw dependency string to parse. 130 131 Raises: 132 QQError: If the dependency string is malformed or cannot be parsed. 133 """ 134 logger.debug(f"Depend string to parse: '{raw_depend}'.") 135 try: 136 raw_type, raw_jobs = raw_depend.split("=") 137 jobs = raw_jobs.split(":") 138 if any(x.strip() == "" for x in jobs): 139 raise QQError("Missing job id.") 140 type = DependType.from_str(raw_type) 141 return cls(type, jobs) 142 except Exception as e: 143 raise QQError( 144 f"Could not parse dependency specification '{raw_depend}': {e}." 145 ) from e 146 147 @classmethod 148 def multi_from_str(cls, raw: str) -> list[Self]: 149 """ 150 Parse a combined dependency string into a list of `Depend` objects. 151 152 The input may contain multiple dependency expressions separated by commas, 153 spaces, or both. Each expression will be parsed into a separate `Depend` instance. 154 155 Args: 156 raw (str): Raw dependency string possibly containing multiple expressions. 157 158 Returns: 159 list[Depend]: A list of parsed dependency objects. 160 161 Raises: 162 QQError: If any dependency expression within the string is malformed. 163 """ 164 logger.debug(f"Full depend string to parse: '{raw}'.") 165 return [Depend.from_str(dep) for dep in re.split(r"[,\s]+", raw.strip()) if dep] 166 167 def to_str(self) -> str: 168 """ 169 Convert the full Depend object to a string representation. 170 171 Format: 172 <type>=<job_id1>:<job_id2>:... 173 174 Returns: 175 str: String representation combining the dependency type keyword and 176 the colon-separated list of job IDs. 177 """ 178 return f"{self.type.to_str()}={':'.join(self.jobs)}"
Representation of a parsed job dependency.
A dependency defines both the type of dependency (e.g., after start, after success) and a list of job IDs that the current job depends on.
Attributes:
- type (DependType): The type of dependency, determined by the dependency keyword.
- jobs (list[str]): List of job IDs this dependency refers to.
119 @classmethod 120 def from_str(cls, raw_depend: str): 121 """ 122 Initialize a `Depend` object by parsing a raw dependency specification. 123 124 The expected format is `<type>=<job_id>[:<job_id>...]`, where: 125 - `<type>` is one of `"after"`, `"afterok"`, `"afternotok"`, or `"afterany"`. 126 - `<job_id>` values are colon-separated job identifiers. 127 128 Args: 129 raw_depend (str): Raw dependency string to parse. 130 131 Raises: 132 QQError: If the dependency string is malformed or cannot be parsed. 133 """ 134 logger.debug(f"Depend string to parse: '{raw_depend}'.") 135 try: 136 raw_type, raw_jobs = raw_depend.split("=") 137 jobs = raw_jobs.split(":") 138 if any(x.strip() == "" for x in jobs): 139 raise QQError("Missing job id.") 140 type = DependType.from_str(raw_type) 141 return cls(type, jobs) 142 except Exception as e: 143 raise QQError( 144 f"Could not parse dependency specification '{raw_depend}': {e}." 145 ) from e
Initialize a Depend object by parsing a raw dependency specification.
The expected format is <type>=<job_id>[:<job_id>...], where:
<type>is one of"after","afterok","afternotok", or"afterany".<job_id>values are colon-separated job identifiers.
Arguments:
- raw_depend (str): Raw dependency string to parse.
Raises:
- QQError: If the dependency string is malformed or cannot be parsed.
147 @classmethod 148 def multi_from_str(cls, raw: str) -> list[Self]: 149 """ 150 Parse a combined dependency string into a list of `Depend` objects. 151 152 The input may contain multiple dependency expressions separated by commas, 153 spaces, or both. Each expression will be parsed into a separate `Depend` instance. 154 155 Args: 156 raw (str): Raw dependency string possibly containing multiple expressions. 157 158 Returns: 159 list[Depend]: A list of parsed dependency objects. 160 161 Raises: 162 QQError: If any dependency expression within the string is malformed. 163 """ 164 logger.debug(f"Full depend string to parse: '{raw}'.") 165 return [Depend.from_str(dep) for dep in re.split(r"[,\s]+", raw.strip()) if dep]
Parse a combined dependency string into a list of Depend objects.
The input may contain multiple dependency expressions separated by commas,
spaces, or both. Each expression will be parsed into a separate Depend instance.
Arguments:
- raw (str): Raw dependency string possibly containing multiple expressions.
Returns:
list[Depend]: A list of parsed dependency objects.
Raises:
- QQError: If any dependency expression within the string is malformed.
167 def to_str(self) -> str: 168 """ 169 Convert the full Depend object to a string representation. 170 171 Format: 172 <type>=<job_id1>:<job_id2>:... 173 174 Returns: 175 str: String representation combining the dependency type keyword and 176 the colon-separated list of job IDs. 177 """ 178 return f"{self.type.to_str()}={':'.join(self.jobs)}"
Convert the full Depend object to a string representation.
Format:
= : :...
Returns:
str: String representation combining the dependency type keyword and the colon-separated list of job IDs.
181def filter_dependencies( 182 batch_system: AnyBatchClass, dependencies: list[Depend] 183) -> list[Depend]: 184 """ 185 Filter dependencies to only include those that are still present in the batch system. 186 187 Args: 188 batch_system (AnyBatchClass): The batch system to check job presence against. 189 dependencies (list[Depend]): The list of dependencies to filter. 190 191 Returns: 192 list[Depend]: The filtered list of dependencies. 193 """ 194 filtered: list[Depend] = [] 195 for depend in dependencies: 196 # get jobs that are still present in the batch system 197 valid_jobs = [ 198 job_id 199 for job_id in depend.jobs 200 if not (batch_system.get_batch_job(job_id)).is_empty() 201 ] 202 203 if valid_jobs: 204 filtered.append(Depend(type=depend.type, jobs=valid_jobs)) 205 206 logger.debug(f"Filtered dependencies: {filtered}.") 207 return filtered
Filter dependencies to only include those that are still present in the batch system.
Arguments:
- batch_system (AnyBatchClass): The batch system to check job presence against.
- dependencies (list[Depend]): The list of dependencies to filter.
Returns:
list[Depend]: The filtered list of dependencies.