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.
- DIRACCommon/ConfigurationSystem/Client/Helpers/Resources.py +52 -0
- DIRACCommon/ConfigurationSystem/Client/Helpers/__init__.py +3 -0
- DIRACCommon/ConfigurationSystem/Client/__init__.py +3 -0
- DIRACCommon/ConfigurationSystem/__init__.py +3 -0
- DIRACCommon/Core/Utilities/ClassAd/ClassAdLight.py +295 -0
- DIRACCommon/Core/Utilities/ClassAd/__init__.py +1 -0
- DIRACCommon/Core/Utilities/DErrno.py +327 -0
- DIRACCommon/Core/Utilities/JDL.py +199 -0
- DIRACCommon/Core/Utilities/List.py +127 -0
- DIRACCommon/Core/Utilities/ReturnValues.py +255 -0
- DIRACCommon/Core/Utilities/StateMachine.py +185 -0
- DIRACCommon/Core/Utilities/TimeUtilities.py +259 -0
- DIRACCommon/Core/Utilities/__init__.py +3 -0
- DIRACCommon/Core/__init__.py +1 -0
- DIRACCommon/WorkloadManagementSystem/Client/JobState/JobManifest.py +235 -0
- DIRACCommon/WorkloadManagementSystem/Client/JobState/__init__.py +0 -0
- DIRACCommon/WorkloadManagementSystem/Client/JobStatus.py +95 -0
- DIRACCommon/WorkloadManagementSystem/Client/__init__.py +1 -0
- DIRACCommon/WorkloadManagementSystem/DB/JobDBUtils.py +170 -0
- DIRACCommon/WorkloadManagementSystem/DB/__init__.py +1 -0
- DIRACCommon/WorkloadManagementSystem/Utilities/JobModel.py +236 -0
- DIRACCommon/WorkloadManagementSystem/Utilities/JobStatusUtility.py +93 -0
- DIRACCommon/WorkloadManagementSystem/Utilities/ParametricJob.py +179 -0
- DIRACCommon/WorkloadManagementSystem/Utilities/__init__.py +1 -0
- DIRACCommon/WorkloadManagementSystem/__init__.py +1 -0
- DIRACCommon/__init__.py +21 -0
- diraccommon-9.0.0.dist-info/METADATA +281 -0
- diraccommon-9.0.0.dist-info/RECORD +29 -0
- 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
|