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
logger = <Logger qq_lib.properties.depend (INFO)>
class DependType(enum.Enum):
 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.

AFTER_START = <DependType.AFTER_START: 0>
AFTER_SUCCESS = <DependType.AFTER_SUCCESS: 1>
AFTER_FAILURE = <DependType.AFTER_FAILURE: 2>
AFTER_COMPLETION = <DependType.AFTER_COMPLETION: 3>
@classmethod
def from_str(cls, string: str) -> DependType:
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.
def to_str(self) -> str:
 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).
@dataclass(frozen=True)
class Depend:
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.
Depend(type: DependType, jobs: list[str])
type: DependType
jobs: list[str]
@classmethod
def from_str(cls, raw_depend: str):
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.
@classmethod
def multi_from_str(cls, raw: str) -> list[typing.Self]:
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.
def to_str(self) -> str:
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.

def filter_dependencies( batch_system: AnyBatchClass, dependencies: list[Depend]) -> list[Depend]:
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.