Coverage for python/lsst/ctrl/mpexec/reports.py: 67%
70 statements
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-25 09:44 +0000
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-25 09:44 +0000
1# This file is part of ctrl_mpexec.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22from __future__ import annotations
24__all__ = ["ExceptionInfo", "ExecutionStatus", "Report", "QuantumReport"]
26import enum
27import sys
28from typing import Any
30import pydantic
31from lsst.daf.butler import DataCoordinate, DataId, DataIdValue
32from lsst.daf.butler._compat import PYDANTIC_V2, _BaseModelCompat
33from lsst.utils.introspection import get_full_type_name
36def _serializeDataId(dataId: DataId) -> dict[str, DataIdValue]:
37 if isinstance(dataId, DataCoordinate):
38 return dataId.byName()
39 else:
40 return dataId # type: ignore
43class ExecutionStatus(enum.Enum):
44 """Possible values for job execution status.
46 Status `FAILURE` is set if one or more tasks failed. Status `TIMEOUT` is
47 set if there are no failures but one or more tasks timed out. Timeouts can
48 only be detected in multi-process mode, child task is killed on timeout
49 and usually should have non-zero exit code.
50 """
52 SUCCESS = "success"
53 FAILURE = "failure"
54 TIMEOUT = "timeout"
55 SKIPPED = "skipped"
58class ExceptionInfo(_BaseModelCompat):
59 """Information about exception."""
61 className: str
62 """Name of the exception class if exception was raised."""
64 message: str
65 """Exception message for in-process quantum execution, None if
66 quantum was executed in sub-process.
67 """
69 @classmethod
70 def from_exception(cls, exception: Exception) -> ExceptionInfo:
71 """Construct instance from an exception."""
72 return cls(className=get_full_type_name(exception), message=str(exception))
75class QuantumReport(_BaseModelCompat):
76 """Task execution report for a single Quantum."""
78 status: ExecutionStatus = ExecutionStatus.SUCCESS
79 """Execution status, one of the values in `ExecutionStatus` enum."""
81 dataId: dict[str, DataIdValue]
82 """Quantum DataId."""
84 taskLabel: str | None
85 """Label for a task executing this Quantum."""
87 exitCode: int | None = None
88 """Exit code for a sub-process executing Quantum, None for in-process
89 Quantum execution. Negative if process was killed by a signal.
90 """
92 exceptionInfo: ExceptionInfo | None = None
93 """Exception information if exception was raised."""
95 def __init__(
96 self,
97 dataId: DataId,
98 taskLabel: str,
99 status: ExecutionStatus = ExecutionStatus.SUCCESS,
100 exitCode: int | None = None,
101 exceptionInfo: ExceptionInfo | None = None,
102 ):
103 super().__init__(
104 status=status,
105 dataId=_serializeDataId(dataId),
106 taskLabel=taskLabel,
107 exitCode=exitCode,
108 exceptionInfo=exceptionInfo,
109 )
111 @classmethod
112 def from_exception(
113 cls,
114 exception: Exception,
115 dataId: DataId,
116 taskLabel: str,
117 ) -> QuantumReport:
118 """Construct report instance from an exception and other pieces of
119 data.
120 """
121 return cls(
122 status=ExecutionStatus.FAILURE,
123 dataId=dataId,
124 taskLabel=taskLabel,
125 exceptionInfo=ExceptionInfo.from_exception(exception),
126 )
128 @classmethod
129 def from_exit_code(
130 cls,
131 exitCode: int,
132 dataId: DataId,
133 taskLabel: str,
134 ) -> QuantumReport:
135 """Construct report instance from an exit code and other pieces of
136 data.
137 """
138 return cls(
139 status=ExecutionStatus.SUCCESS if exitCode == 0 else ExecutionStatus.FAILURE,
140 dataId=dataId,
141 taskLabel=taskLabel,
142 exitCode=exitCode,
143 )
146class Report(_BaseModelCompat):
147 """Execution report for the whole job with one or few quanta."""
149 status: ExecutionStatus = ExecutionStatus.SUCCESS
150 """Job status."""
152 cmdLine: list[str] | None = None
153 """Command line for the whole job."""
155 exitCode: int | None = None
156 """Job exit code, this obviously cannot be set in pipetask."""
158 exceptionInfo: ExceptionInfo | None = None
159 """Exception information if exception was raised."""
161 quantaReports: list[QuantumReport] = []
162 """List of per-quantum reports, ordering is not specified. Some or all
163 quanta may not produce a report.
164 """
166 if PYDANTIC_V2: 166 ↛ 169line 166 didn't jump to line 169, because the condition on line 166 was never true
167 # Always want to validate the default value for cmdLine so
168 # use a model_validator.
169 @pydantic.model_validator(mode="before") # type: ignore[attr-defined]
170 @classmethod
171 def _set_cmdLine(cls, data: Any) -> Any:
172 if data.get("cmdLine") is None:
173 data["cmdLine"] = sys.argv
174 return data
176 else:
178 @pydantic.validator("cmdLine", always=True)
179 def _set_cmdLine(cls, v: list[str] | None) -> list[str]: # noqa: N805
180 if v is None:
181 v = sys.argv
182 return v
184 def set_exception(self, exception: Exception) -> None:
185 """Update exception information from an exception object."""
186 self.exceptionInfo = ExceptionInfo.from_exception(exception)