Skip to content

Commit 7c84335

Browse files
authored
Merge pull request #8290 from chrisburr/feature/move-more-to-dirac-common-2
[9.0] Move more utilities to DIRACCommon
2 parents cf0ecb7 + c815663 commit 7c84335

File tree

31 files changed

+2359
-1508
lines changed

31 files changed

+2359
-1508
lines changed

dirac-common/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ classifiers = [
2121
]
2222
dependencies = [
2323
"typing-extensions>=4.0.0",
24+
"diraccfg",
25+
"pydantic>=2.0.0",
2426
]
2527
dynamic = ["version"]
2628

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)