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