DIRACCommon 9.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. DIRACCommon/ConfigurationSystem/Client/Helpers/Resources.py +52 -0
  2. DIRACCommon/ConfigurationSystem/Client/Helpers/__init__.py +3 -0
  3. DIRACCommon/ConfigurationSystem/Client/__init__.py +3 -0
  4. DIRACCommon/ConfigurationSystem/__init__.py +3 -0
  5. DIRACCommon/Core/Utilities/ClassAd/ClassAdLight.py +295 -0
  6. DIRACCommon/Core/Utilities/ClassAd/__init__.py +1 -0
  7. DIRACCommon/Core/Utilities/DErrno.py +327 -0
  8. DIRACCommon/Core/Utilities/JDL.py +199 -0
  9. DIRACCommon/Core/Utilities/List.py +127 -0
  10. DIRACCommon/Core/Utilities/ReturnValues.py +255 -0
  11. DIRACCommon/Core/Utilities/StateMachine.py +185 -0
  12. DIRACCommon/Core/Utilities/TimeUtilities.py +259 -0
  13. DIRACCommon/Core/Utilities/__init__.py +3 -0
  14. DIRACCommon/Core/__init__.py +1 -0
  15. DIRACCommon/WorkloadManagementSystem/Client/JobState/JobManifest.py +235 -0
  16. DIRACCommon/WorkloadManagementSystem/Client/JobState/__init__.py +0 -0
  17. DIRACCommon/WorkloadManagementSystem/Client/JobStatus.py +95 -0
  18. DIRACCommon/WorkloadManagementSystem/Client/__init__.py +1 -0
  19. DIRACCommon/WorkloadManagementSystem/DB/JobDBUtils.py +170 -0
  20. DIRACCommon/WorkloadManagementSystem/DB/__init__.py +1 -0
  21. DIRACCommon/WorkloadManagementSystem/Utilities/JobModel.py +236 -0
  22. DIRACCommon/WorkloadManagementSystem/Utilities/JobStatusUtility.py +93 -0
  23. DIRACCommon/WorkloadManagementSystem/Utilities/ParametricJob.py +179 -0
  24. DIRACCommon/WorkloadManagementSystem/Utilities/__init__.py +1 -0
  25. DIRACCommon/WorkloadManagementSystem/__init__.py +1 -0
  26. DIRACCommon/__init__.py +21 -0
  27. diraccommon-9.0.0.dist-info/METADATA +281 -0
  28. diraccommon-9.0.0.dist-info/RECORD +29 -0
  29. diraccommon-9.0.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,199 @@
1
+ """Transformation classes around the JDL format."""
2
+
3
+ from diraccfg import CFG
4
+ from pydantic import ValidationError
5
+
6
+ from DIRACCommon.Core.Utilities.ReturnValues import S_OK, S_ERROR
7
+ from DIRACCommon.Core.Utilities import List
8
+ from DIRACCommon.Core.Utilities.ClassAd.ClassAdLight import ClassAd
9
+ from DIRACCommon.WorkloadManagementSystem.Utilities.JobModel import BaseJobDescriptionModel
10
+
11
+ ARGUMENTS = "Arguments"
12
+ BANNED_SITES = "BannedSites"
13
+ CPU_TIME = "CPUTime"
14
+ EXECUTABLE = "Executable"
15
+ EXECUTION_ENVIRONMENT = "ExecutionEnvironment"
16
+ GRID_CE = "GridCE"
17
+ INPUT_DATA = "InputData"
18
+ INPUT_DATA_POLICY = "InputDataPolicy"
19
+ INPUT_SANDBOX = "InputSandbox"
20
+ JOB_CONFIG_ARGS = "JobConfigArgs"
21
+ JOB_TYPE = "JobType"
22
+ JOB_GROUP = "JobGroup"
23
+ LOG_LEVEL = "LogLevel"
24
+ NUMBER_OF_PROCESSORS = "NumberOfProcessors"
25
+ MAX_NUMBER_OF_PROCESSORS = "MaxNumberOfProcessors"
26
+ MIN_NUMBER_OF_PROCESSORS = "MinNumberOfProcessors"
27
+ OUTPUT_DATA = "OutputData"
28
+ OUTPUT_PATH = "OutputPath"
29
+ OUTPUT_SE = "OutputSE"
30
+ PLATFORM = "Platform"
31
+ PRIORITY = "Priority"
32
+ STD_ERROR = "StdError"
33
+ STD_OUTPUT = "StdOutput"
34
+ OUTPUT_SANDBOX = "OutputSandbox"
35
+ JOB_NAME = "JobName"
36
+ SITE = "Site"
37
+ TAGS = "Tags"
38
+
39
+ OWNER = "Owner"
40
+ OWNER_GROUP = "OwnerGroup"
41
+ VO = "VirtualOrganization"
42
+
43
+ CREDENTIALS_FIELDS = {OWNER, OWNER_GROUP, VO}
44
+
45
+
46
+ def loadJDLAsCFG(jdl):
47
+ """
48
+ Load a JDL as CFG
49
+ """
50
+
51
+ def cleanValue(value):
52
+ value = value.strip()
53
+ if value[0] == '"':
54
+ entries = []
55
+ iPos = 1
56
+ current = ""
57
+ state = "in"
58
+ while iPos < len(value):
59
+ if value[iPos] == '"':
60
+ if state == "in":
61
+ entries.append(current)
62
+ current = ""
63
+ state = "out"
64
+ elif state == "out":
65
+ current = current.strip()
66
+ if current not in (",",):
67
+ return S_ERROR("value seems a list but is not separated in commas")
68
+ current = ""
69
+ state = "in"
70
+ else:
71
+ current += value[iPos]
72
+ iPos += 1
73
+ if state == "in":
74
+ return S_ERROR('value is opened with " but is not closed')
75
+ return S_OK(", ".join(entries))
76
+ else:
77
+ return S_OK(value.replace('"', ""))
78
+
79
+ def assignValue(key, value, cfg):
80
+ key = key.strip()
81
+ if len(key) == 0:
82
+ return S_ERROR("Invalid key name")
83
+ value = value.strip()
84
+ if not value:
85
+ return S_ERROR(f"No value for key {key}")
86
+ if value[0] == "{":
87
+ if value[-1] != "}":
88
+ return S_ERROR("Value '%s' seems a list but does not end in '}'" % (value))
89
+ valList = List.fromChar(value[1:-1])
90
+ for i in range(len(valList)):
91
+ result = cleanValue(valList[i])
92
+ if not result["OK"]:
93
+ return S_ERROR(f"Var {key} : {result['Message']}")
94
+ valList[i] = result["Value"]
95
+ if valList[i] is None:
96
+ return S_ERROR(f"List value '{value}' seems invalid for item {i}")
97
+ value = ", ".join(valList)
98
+ else:
99
+ result = cleanValue(value)
100
+ if not result["OK"]:
101
+ return S_ERROR(f"Var {key} : {result['Message']}")
102
+ nV = result["Value"]
103
+ if nV is None:
104
+ return S_ERROR(f"Value '{value} seems invalid")
105
+ value = nV
106
+ cfg.setOption(key, value)
107
+ return S_OK()
108
+
109
+ if jdl[0] == "[":
110
+ iPos = 1
111
+ else:
112
+ iPos = 0
113
+ key = ""
114
+ value = ""
115
+ action = "key"
116
+ insideLiteral = False
117
+ cfg = CFG()
118
+ while iPos < len(jdl):
119
+ char = jdl[iPos]
120
+ if char == ";" and not insideLiteral:
121
+ if key.strip():
122
+ result = assignValue(key, value, cfg)
123
+ if not result["OK"]:
124
+ return result
125
+ key = ""
126
+ value = ""
127
+ action = "key"
128
+ elif char == "[" and not insideLiteral:
129
+ key = key.strip()
130
+ if not key:
131
+ return S_ERROR("Invalid key in JDL")
132
+ if value.strip():
133
+ return S_ERROR(f"Key {key} seems to have a value and open a sub JDL at the same time")
134
+ result = loadJDLAsCFG(jdl[iPos:])
135
+ if not result["OK"]:
136
+ return result
137
+ subCfg, subPos = result["Value"]
138
+ cfg.createNewSection(key, contents=subCfg)
139
+ key = ""
140
+ value = ""
141
+ action = "key"
142
+ insideLiteral = False
143
+ iPos += subPos
144
+ elif char == "=" and not insideLiteral:
145
+ if action == "key":
146
+ action = "value"
147
+ insideLiteral = False
148
+ else:
149
+ value += char
150
+ elif char == "]" and not insideLiteral:
151
+ key = key.strip()
152
+ if len(key) > 0:
153
+ result = assignValue(key, value, cfg)
154
+ if not result["OK"]:
155
+ return result
156
+ return S_OK((cfg, iPos))
157
+ else:
158
+ if action == "key":
159
+ key += char
160
+ else:
161
+ value += char
162
+ if char == '"':
163
+ insideLiteral = not insideLiteral
164
+ iPos += 1
165
+
166
+ return S_OK((cfg, iPos))
167
+
168
+
169
+ def dumpCFGAsJDL(cfg, level=1, tab=" "):
170
+ indent = tab * level
171
+ contents = [f"{tab * (level - 1)}["]
172
+ sections = cfg.listSections()
173
+
174
+ for key in cfg:
175
+ if key in sections:
176
+ contents.append(f"{indent}{key} =")
177
+ contents.append(f"{dumpCFGAsJDL(cfg[key], level + 1, tab)};")
178
+ else:
179
+ val = List.fromChar(cfg[key])
180
+ # Some attributes are never lists
181
+ if len(val) < 2 or key in [ARGUMENTS, EXECUTABLE, STD_OUTPUT, STD_ERROR]:
182
+ value = cfg[key]
183
+ try:
184
+ try_value = float(value)
185
+ contents.append(f"{tab * level}{key} = {value};")
186
+ except Exception:
187
+ contents.append(f'{tab * level}{key} = "{value}";')
188
+ else:
189
+ contents.append(f"{indent}{key} =")
190
+ contents.append("%s{" % indent)
191
+ for iPos in range(len(val)):
192
+ try:
193
+ value = float(val[iPos])
194
+ except Exception:
195
+ val[iPos] = f'"{val[iPos]}"'
196
+ contents.append(",\n".join([f"{tab * (level + 1)}{value}" for value in val]))
197
+ contents.append("%s};" % indent)
198
+ contents.append(f"{tab * (level - 1)}]")
199
+ return "\n".join(contents)
@@ -0,0 +1,127 @@
1
+ """Collection of DIRAC useful list related modules.
2
+ By default on Error they return None.
3
+ """
4
+ import random
5
+ import sys
6
+ from typing import Any, TypeVar
7
+ from collections.abc import Iterable
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ def uniqueElements(aList: list) -> list:
13
+ """Utility to retrieve list of unique elements in a list (order is kept)."""
14
+
15
+ # Use dict.fromkeys instead of set ensure the order is preserved
16
+ return list(dict.fromkeys(aList))
17
+
18
+
19
+ def appendUnique(aList: list, anObject: Any):
20
+ """Append to list if object does not exist.
21
+
22
+ :param aList: list of elements
23
+ :param anObject: object you want to append
24
+ """
25
+ if anObject not in aList:
26
+ aList.append(anObject)
27
+
28
+
29
+ def fromChar(inputString: str, sepChar: str = ","):
30
+ """Generates a list splitting a string by the required character(s)
31
+ resulting string items are stripped and empty items are removed.
32
+
33
+ :param inputString: list serialised to string
34
+ :param sepChar: separator
35
+ :return: list of strings or None if sepChar has a wrong type
36
+ """
37
+ # to prevent getting an empty String as argument
38
+ if not (isinstance(inputString, str) and isinstance(sepChar, str) and sepChar):
39
+ return None
40
+ return [fieldString.strip() for fieldString in inputString.split(sepChar) if len(fieldString.strip()) > 0]
41
+
42
+
43
+ def randomize(aList: Iterable[T]) -> list[T]:
44
+ """Return a randomly sorted list.
45
+
46
+ :param aList: list to permute
47
+ """
48
+ tmpList = list(aList)
49
+ random.shuffle(tmpList)
50
+ return tmpList
51
+
52
+
53
+ def pop(aList, popElement):
54
+ """Pop the first element equal to popElement from the list.
55
+
56
+ :param aList: list
57
+ :type aList: python:list
58
+ :param popElement: element to pop
59
+ """
60
+ if popElement in aList:
61
+ return aList.pop(aList.index(popElement))
62
+
63
+
64
+ def stringListToString(aList: list) -> str:
65
+ """This function is used for making MySQL queries with a list of string elements.
66
+
67
+ :param aList: list to be serialized to string for making queries
68
+ """
69
+ return ",".join(f"'{x}'" for x in aList)
70
+
71
+
72
+ def intListToString(aList: list) -> str:
73
+ """This function is used for making MySQL queries with a list of int elements.
74
+
75
+ :param aList: list to be serialized to string for making queries
76
+ """
77
+ return ",".join(str(x) for x in aList)
78
+
79
+
80
+ def getChunk(aList: list, chunkSize: int):
81
+ """Generator yielding chunk from a list of a size chunkSize.
82
+
83
+ :param aList: list to be splitted
84
+ :param chunkSize: lenght of one chunk
85
+ :raise: StopIteration
86
+
87
+ Usage:
88
+
89
+ >>> for chunk in getChunk( aList, chunkSize=10):
90
+ process( chunk )
91
+
92
+ """
93
+ chunkSize = int(chunkSize)
94
+ for i in range(0, len(aList), chunkSize):
95
+ yield aList[i : i + chunkSize]
96
+
97
+
98
+ def breakListIntoChunks(aList: list, chunkSize: int):
99
+ """This function takes a list as input and breaks it into list of size 'chunkSize'.
100
+ It returns a list of lists.
101
+
102
+ :param aList: list of elements
103
+ :param chunkSize: len of a single chunk
104
+ :return: list of lists of length of chunkSize
105
+ :raise: RuntimeError if numberOfFilesInChunk is less than 1
106
+ """
107
+ if chunkSize < 1:
108
+ raise RuntimeError("chunkSize cannot be less than 1")
109
+ if isinstance(aList, (set, dict, tuple, {}.keys().__class__, {}.items().__class__, {}.values().__class__)):
110
+ aList = list(aList)
111
+ return [chunk for chunk in getChunk(aList, chunkSize)]
112
+
113
+
114
+ def getIndexInList(anItem: Any, aList: list) -> int:
115
+ """Return the index of the element x in the list l
116
+ or sys.maxint if it does not exist
117
+
118
+ :param anItem: element to look for
119
+ :param aList: list to look into
120
+
121
+ :return: the index or sys.maxint
122
+ """
123
+ # try:
124
+ if anItem in aList:
125
+ return aList.index(anItem)
126
+ else:
127
+ return sys.maxsize
@@ -0,0 +1,255 @@
1
+ """
2
+ DIRAC return dictionary
3
+
4
+ Message values are converted to string
5
+
6
+ keys are converted to string
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import functools
11
+ import sys
12
+ import traceback
13
+ from types import TracebackType
14
+ from typing import Any, Callable, cast, Generic, Literal, overload, Type, TypeVar, Union
15
+ from typing_extensions import TypedDict, ParamSpec, NotRequired
16
+
17
+ from DIRACCommon.Core.Utilities.DErrno import strerror
18
+
19
+
20
+ T = TypeVar("T")
21
+ P = ParamSpec("P")
22
+
23
+
24
+ class DOKReturnType(TypedDict, Generic[T]):
25
+ """used for typing the DIRAC return structure"""
26
+
27
+ OK: Literal[True]
28
+ Value: T
29
+
30
+
31
+ class DErrorReturnType(TypedDict):
32
+ """used for typing the DIRAC return structure"""
33
+
34
+ OK: Literal[False]
35
+ Message: str
36
+ Errno: int
37
+ ExecInfo: NotRequired[tuple[type[BaseException], BaseException, TracebackType]]
38
+ CallStack: NotRequired[list[str]]
39
+
40
+
41
+ DReturnType = Union[DOKReturnType[T], DErrorReturnType]
42
+
43
+
44
+ def S_ERROR(*args: Any, **kwargs: Any) -> DErrorReturnType:
45
+ """return value on error condition
46
+
47
+ Arguments are either Errno and ErrorMessage or just ErrorMessage fro backward compatibility
48
+
49
+ :param int errno: Error number
50
+ :param string message: Error message
51
+ :param list callStack: Manually override the CallStack attribute better performance
52
+ """
53
+ callStack = kwargs.pop("callStack", None)
54
+
55
+ result: DErrorReturnType = {"OK": False, "Errno": 0, "Message": ""}
56
+
57
+ message = ""
58
+ if args:
59
+ if isinstance(args[0], int):
60
+ result["Errno"] = args[0]
61
+ if len(args) > 1:
62
+ message = args[1]
63
+ else:
64
+ message = args[0]
65
+
66
+ if result["Errno"]:
67
+ message = f"{strerror(result['Errno'])} ( {result['Errno']} : {message})"
68
+ result["Message"] = message
69
+
70
+ if callStack is None:
71
+ try:
72
+ callStack = traceback.format_stack()
73
+ callStack.pop()
74
+ except Exception:
75
+ callStack = []
76
+
77
+ result["CallStack"] = callStack
78
+
79
+ return result
80
+
81
+
82
+ # mypy doesn't understand default parameter values with generics so use overloads (python/mypy#3737)
83
+ @overload
84
+ def S_OK() -> DOKReturnType[None]:
85
+ ...
86
+
87
+
88
+ @overload
89
+ def S_OK(value: T) -> DOKReturnType[T]:
90
+ ...
91
+
92
+
93
+ def S_OK(value=None): # type: ignore
94
+ """return value on success
95
+
96
+ :param value: value of the 'Value'
97
+ :return: dictionary { 'OK' : True, 'Value' : value }
98
+ """
99
+ return {"OK": True, "Value": value}
100
+
101
+
102
+ def isReturnStructure(unk: Any) -> bool:
103
+ """Check if value is an `S_OK`/`S_ERROR` object"""
104
+ if not isinstance(unk, dict):
105
+ return False
106
+ if "OK" not in unk:
107
+ return False
108
+ if unk["OK"]:
109
+ return "Value" in unk
110
+ else:
111
+ return "Message" in unk
112
+
113
+
114
+ def isSError(value: Any) -> bool:
115
+ """Check if value is an `S_ERROR` object"""
116
+ if not isinstance(value, dict):
117
+ return False
118
+ if "OK" not in value:
119
+ return False
120
+ return "Message" in value
121
+
122
+
123
+ def reprReturnErrorStructure(struct: DErrorReturnType, full: bool = False) -> str:
124
+ errorNumber = struct.get("Errno", 0)
125
+ message = struct.get("Message", "")
126
+ if errorNumber:
127
+ reprStr = f"{strerror(errorNumber)} ( {errorNumber} : {message})"
128
+ else:
129
+ reprStr = message
130
+
131
+ if full:
132
+ callStack = struct.get("CallStack")
133
+ if callStack:
134
+ reprStr += "\n" + "".join(callStack)
135
+
136
+ return reprStr
137
+
138
+
139
+ def returnSingleResult(dictRes: DReturnType[Any]) -> DReturnType[Any]:
140
+ """Transform the S_OK{Successful/Failed} dictionary convention into
141
+ an S_OK/S_ERROR return. To be used when a single returned entity
142
+ is expected from a generally bulk call.
143
+
144
+ :param dictRes: S_ERROR or S_OK( "Failed" : {}, "Successful" : {})
145
+ :returns: S_ERROR or S_OK(value)
146
+
147
+ The following rules are applied:
148
+
149
+ - if dictRes is an S_ERROR: returns it as is
150
+ - we start by looking at the Failed directory
151
+ - if there are several items in a dictionary, we return the first one
152
+ - if both dictionaries are empty, we return S_ERROR
153
+ - For an item in Failed, we return S_ERROR
154
+ - Far an item in Successful we return S_OK
155
+
156
+ Behavior examples (would be perfect unit test :-) )::
157
+
158
+ {'Message': 'Kaput', 'OK': False} -> {'Message': 'Kaput', 'OK': False}
159
+ {'OK': True, 'Value': {'Successful': {}, 'Failed': {'a': 1}}} -> {'Message': '1', 'OK': False}
160
+ {'OK': True, 'Value': {'Successful': {'b': 2}, 'Failed': {}}} -> {'OK': True, 'Value': 2}
161
+ {'OK': True, 'Value': {'Successful': {'b': 2}, 'Failed': {'a': 1}}} -> {'Message': '1', 'OK': False}
162
+ {'OK': True, 'Value': {'Successful': {'b': 2}, 'Failed': {'a': 1, 'c': 3}}} -> {'Message': '1', 'OK': False}
163
+ {'OK': True, 'Value': {'Successful': {'b': 2, 'd': 4}, 'Failed': {}}} -> {'OK': True, 'Value': 2}
164
+ {'OK': True, 'Value': {'Successful': {}, 'Failed': {}}} ->
165
+ {'Message': 'returnSingleResult: Failed and Successful dictionaries are empty', 'OK': False}
166
+ """
167
+ # if S_ERROR was returned, we return it as well
168
+ if not dictRes["OK"]:
169
+ return dictRes
170
+ # if there is a Failed, we return the first one in an S_ERROR
171
+ if "Failed" in dictRes["Value"] and len(dictRes["Value"]["Failed"]):
172
+ errorMessage = list(dictRes["Value"]["Failed"].values())[0]
173
+ if isinstance(errorMessage, dict):
174
+ if isReturnStructure(errorMessage):
175
+ return cast(DErrorReturnType, errorMessage)
176
+ else:
177
+ return S_ERROR(str(errorMessage))
178
+ return S_ERROR(errorMessage)
179
+ # if there is a Successful, we return the first one in an S_OK
180
+ elif "Successful" in dictRes["Value"] and len(dictRes["Value"]["Successful"]):
181
+ return S_OK(list(dictRes["Value"]["Successful"].values())[0])
182
+ else:
183
+ return S_ERROR("returnSingleResult: Failed and Successful dictionaries are empty")
184
+
185
+
186
+ class SErrorException(Exception):
187
+ """Exception class for use with `convertToReturnValue`"""
188
+
189
+ def __init__(self, result: DErrorReturnType | str, errCode: int = 0):
190
+ """Create a new exception return value
191
+
192
+ If `result` is a `S_ERROR` return it directly else convert it to an
193
+ appropriate value using `S_ERROR(errCode, result)`.
194
+
195
+ :param result: The error to propagate
196
+ :param errCode: the error code to propagate
197
+ """
198
+ if not isSError(result):
199
+ result = S_ERROR(errCode, result)
200
+ self.result = cast(DErrorReturnType, result)
201
+
202
+
203
+ def returnValueOrRaise(result: DReturnType[T], *, errorCode: int = 0) -> T:
204
+ """Unwrap an S_OK/S_ERROR response into a value or Exception
205
+
206
+ This method assists with using exceptions in DIRAC code by raising
207
+ :exc:`SErrorException` if `result` is an error. This can then by propagated
208
+ automatically as an `S_ERROR` by wrapping public facing functions with
209
+ `@convertToReturnValue`.
210
+
211
+ :param result: Result of a DIRAC function which returns `S_OK`/`S_ERROR`
212
+ :returns: The value associated with the `S_OK` object
213
+ :raises: If `result["OK"]` is falsey the original exception is re-raised.
214
+ If no exception is known an :exc:`SErrorException` is raised.
215
+ """
216
+ if not result["OK"]:
217
+ if "ExecInfo" in result:
218
+ raise result["ExecInfo"][0]
219
+ else:
220
+ raise SErrorException(result, errorCode)
221
+ return result["Value"]
222
+
223
+
224
+ def convertToReturnValue(func: Callable[P, T]) -> Callable[P, DReturnType[T]]:
225
+ """Decorate a function to convert return values to `S_OK`/`S_ERROR`
226
+
227
+ If `func` returns, wrap the return value in `S_OK`.
228
+ If `func` raises :exc:`SErrorException`, return the associated `S_ERROR`
229
+ If `func` raises any other exception type, convert it to an `S_ERROR` object
230
+
231
+ :param result: The bare result of a function call
232
+ :returns: `S_OK`/`S_ERROR`
233
+ """
234
+
235
+ @functools.wraps(func)
236
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> DReturnType[T]:
237
+ try:
238
+ value = func(*args, **kwargs)
239
+ except SErrorException as e:
240
+ return e.result
241
+ except Exception as e:
242
+ retval = S_ERROR(f"{repr(e)}: {e}")
243
+ # Replace CallStack with the one from the exception
244
+ # Use cast as mypy doesn't understand that sys.exc_info can't return None in an exception block
245
+ retval["ExecInfo"] = cast(tuple[type[BaseException], BaseException, TracebackType], sys.exc_info())
246
+ exc_type, exc_value, exc_tb = retval["ExecInfo"]
247
+ retval["CallStack"] = traceback.format_tb(exc_tb)
248
+ return retval
249
+ else:
250
+ return S_OK(value)
251
+
252
+ # functools will copy the annotations. Since we change the return type
253
+ # we have to update it
254
+ wrapped.__annotations__["return"] = DReturnType
255
+ return wrapped