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,185 @@
1
+ """ StateMachine
2
+
3
+ This module contains the basic blocks to build a state machine (State and StateMachine)
4
+ """
5
+ from DIRACCommon.Core.Utilities.ReturnValues import S_OK, S_ERROR
6
+
7
+
8
+ class State:
9
+ """
10
+ State class that represents a single step on a StateMachine, with all the
11
+ possible transitions, the default transition and an ordering level.
12
+
13
+
14
+ examples:
15
+ >>> s0 = State(100)
16
+ >>> s1 = State(0, ['StateName1', 'StateName2'], defState='StateName1')
17
+ >>> s2 = State(0, ['StateName1', 'StateName2'])
18
+ # this example is tricky. The transition rule says that will go to
19
+ # nextState, e.g. 'StateNext'. But, it is not on the stateMap, and there
20
+ # is no default defined, so it will end up going to StateNext anyway. You
21
+ # must be careful while defining states and their stateMaps and defaults.
22
+ """
23
+
24
+ def __init__(self, level, stateMap=None, defState=None):
25
+ """
26
+ :param int level: each state is mapped to an integer, which is used to sort the states according to that integer.
27
+ :param list stateMap: it is a list (of strings) with the reachable states from this particular status.
28
+ If not defined, we assume there are no restrictions.
29
+ :param str defState: default state used in case the next state is not in stateMap (not defined or simply not there).
30
+ """
31
+
32
+ self.level = level
33
+ self.stateMap = stateMap if stateMap else []
34
+ self.default = defState
35
+
36
+ def transitionRule(self, nextState):
37
+ """
38
+ Method that selects next state, knowing the default and the transitions
39
+ map, and the proposed next state. If <nextState> is in stateMap, goes there.
40
+ If not, then goes to <self.default> if any. Otherwise, goes to <nextState>
41
+ anyway.
42
+
43
+ examples:
44
+ >>> s0.transitionRule('nextState')
45
+ 'nextState'
46
+ >>> s1.transitionRule('StateName2')
47
+ 'StateName2'
48
+ >>> s1.transitionRule('StateNameNotInMap')
49
+ 'StateName1'
50
+ >>> s2.transitionRule('StateNameNotInMap')
51
+ 'StateNameNotInMap'
52
+
53
+ :param str nextState: name of the state in the stateMap
54
+ :return: state name
55
+ :rtype: str
56
+ """
57
+
58
+ # If next state is on the list of next states, go ahead.
59
+ if nextState in self.stateMap:
60
+ return nextState
61
+
62
+ # If not, calculate defaultState:
63
+ # if there is a default, that one
64
+ # otherwise is nextState (states with empty list have no movement restrictions)
65
+ defaultNext = self.default if self.default else nextState
66
+ return defaultNext
67
+
68
+
69
+ class StateMachine:
70
+ """
71
+ StateMachine class that represents the whole state machine with all transitions.
72
+
73
+ examples:
74
+ >>> sm0 = StateMachine()
75
+ >>> sm1 = StateMachine(state = 'Active')
76
+
77
+ :param state: current state of the StateMachine, could be None if we do not use the
78
+ StateMachine to calculate transitions. Beware, it is not checked if the
79
+ state is on the states map !
80
+ :type state: None or str
81
+
82
+ """
83
+
84
+ def __init__(self, state=None):
85
+ """
86
+ Constructor.
87
+ """
88
+
89
+ self.state = state
90
+ # To be overwritten by child classes, unless you like Nirvana state that much.
91
+ self.states = {"Nirvana": State(100)}
92
+
93
+ def getLevelOfState(self, state):
94
+ """
95
+ Given a state name, it returns its level (integer), which defines the hierarchy.
96
+
97
+ >>> sm0.getLevelOfState('Nirvana')
98
+ 100
99
+ >>> sm0.getLevelOfState('AnotherState')
100
+ -1
101
+
102
+ :param str state: name of the state, it should be on <self.states> key set
103
+ :return: `int` || -1 (if not in <self.states>)
104
+ """
105
+
106
+ if state not in self.states:
107
+ return -1
108
+ return self.states[state].level
109
+
110
+ def setState(self, candidateState, noWarn=False, *, logger_warn=None):
111
+ """Makes sure the state is either None or known to the machine, and that it is a valid state to move into.
112
+ Final states are also checked.
113
+
114
+ examples:
115
+ >>> sm0.setState(None)['OK']
116
+ True
117
+ >>> sm0.setState('Nirvana')['OK']
118
+ True
119
+ >>> sm0.setState('AnotherState')['OK']
120
+ False
121
+
122
+ :param state: state which will be set as current state of the StateMachine
123
+ :type state: None or str
124
+ :return: S_OK || S_ERROR
125
+ """
126
+ if candidateState == self.state:
127
+ return S_OK(candidateState)
128
+
129
+ if not candidateState:
130
+ self.state = candidateState
131
+ elif candidateState in self.states:
132
+ if not self.states[self.state].stateMap:
133
+ if not noWarn and logger_warn:
134
+ logger_warn("Final state, won't move", f"({self.state}, asked to move to {candidateState})")
135
+ return S_OK(self.state)
136
+ if candidateState not in self.states[self.state].stateMap and logger_warn:
137
+ logger_warn(f"Can't move from {self.state} to {candidateState}, choosing a good one")
138
+ result = self.getNextState(candidateState)
139
+ if not result["OK"]:
140
+ return result
141
+ self.state = result["Value"]
142
+ # If the StateMachine does not accept the candidate, return error message
143
+ else:
144
+ return S_ERROR(f"setState: {candidateState!r} is not a valid state")
145
+
146
+ return S_OK(self.state)
147
+
148
+ def getStates(self):
149
+ """
150
+ Returns all possible states in the state map
151
+
152
+ examples:
153
+ >>> sm0.getStates()
154
+ [ 'Nirvana' ]
155
+
156
+ :return: list(stateNames)
157
+ """
158
+
159
+ return list(self.states)
160
+
161
+ def getNextState(self, candidateState):
162
+ """
163
+ Method that gets the next state, given the proposed transition to candidateState.
164
+ If candidateState is not on the state map <self.states>, it is rejected. If it is
165
+ not the case, we have two options: if <self.state> is None, then the next state
166
+ will be <candidateState>. Otherwise, the current state is using its own
167
+ transition rule to decide.
168
+
169
+ examples:
170
+ >>> sm0.getNextState(None)
171
+ S_OK(None)
172
+ >>> sm0.getNextState('NextState')
173
+ S_OK('NextState')
174
+
175
+ :param str candidateState: name of the next state
176
+ :return: S_OK(nextState) || S_ERROR
177
+ """
178
+ if candidateState not in self.states:
179
+ return S_ERROR(f"getNextState: {candidateState!r} is not a valid state")
180
+
181
+ # FIXME: do we need this anymore ?
182
+ if self.state is None:
183
+ return S_OK(candidateState)
184
+
185
+ return S_OK(self.states[self.state].transitionRule(candidateState))
@@ -0,0 +1,259 @@
1
+ """
2
+ DIRAC TimeUtilities module
3
+ Support for basic Date and Time operations
4
+ based on system datetime module.
5
+
6
+ It provides common interface to UTC timestamps,
7
+ converter to string types and back.
8
+
9
+ Useful timedelta constant are also provided to
10
+ define time intervals.
11
+
12
+ Notice: datetime.timedelta objects allow multiplication and division by interger
13
+ but not by float. Thus:
14
+
15
+ - DIRAC.TimeUtilities.second * 1.5 is not allowed
16
+ - DIRAC.TimeUtilities.second * 3 / 2 is allowed
17
+
18
+ An timeInterval class provides a method to check
19
+ if a give datetime is in the defined interval.
20
+
21
+ """
22
+ import datetime
23
+ import sys
24
+ import time
25
+
26
+ # Some useful constants for time operations
27
+ microsecond = datetime.timedelta(microseconds=1)
28
+ second = datetime.timedelta(seconds=1)
29
+ minute = datetime.timedelta(minutes=1)
30
+ hour = datetime.timedelta(hours=1)
31
+ day = datetime.timedelta(days=1)
32
+ week = datetime.timedelta(days=7)
33
+
34
+
35
+ def timeThis(method, *, logger_info=None):
36
+ """Function to be used as a decorator for timing other functions/methods"""
37
+
38
+ def timed(*args, **kw):
39
+ """What actually times"""
40
+ ts = time.time()
41
+ result = method(*args, **kw)
42
+ if sys.stdout.isatty():
43
+ return result
44
+ te = time.time()
45
+
46
+ pre = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d %H:%M:%S UTC ")
47
+
48
+ try:
49
+ pre += args[0].log.getName() + "/" + args[0].log.getSubName() + " TIME: " + args[0].transString
50
+ except AttributeError:
51
+ try:
52
+ pre += args[0].log.getName() + " TIME: " + args[0].transString
53
+ except AttributeError:
54
+ try:
55
+ pre += args[0].log.getName() + "/" + args[0].log.getSubName() + " TIME: "
56
+ except AttributeError:
57
+ pre += "TIME: "
58
+ except IndexError:
59
+ pre += "TIME: "
60
+
61
+ argsLen = ""
62
+ if args:
63
+ try:
64
+ if isinstance(args[1], (list, dict)):
65
+ argsLen = f"arguments len: {len(args[1])}"
66
+ except IndexError:
67
+ if kw:
68
+ try:
69
+ if isinstance(list(list(kw.items())[0])[1], (list, dict)):
70
+ argsLen = f"arguments len: {len(list(list(kw.items())[0])[1])}"
71
+ except IndexError:
72
+ argsLen = ""
73
+
74
+ if logger_info is not None:
75
+ logger_info(f"{pre} Exec time ===> function {method.__name__!r} {argsLen} -> {te - ts:2.2f} sec")
76
+ return result
77
+
78
+ return timed
79
+
80
+
81
+ def toEpoch(dateTimeObject=None):
82
+ """
83
+ Get seconds since epoch. Accepts datetime or date objects
84
+ """
85
+ return toEpochMilliSeconds(dateTimeObject) // 1000
86
+
87
+
88
+ def toEpochMilliSeconds(dateTimeObject=None):
89
+ """
90
+ Get milliseconds since epoch
91
+ """
92
+ if dateTimeObject is None:
93
+ dateTimeObject = datetime.datetime.utcnow()
94
+ if dateTimeObject.resolution == datetime.timedelta(days=1):
95
+ # Add time information corresponding to midnight UTC if it's a datetime.date
96
+ dateTimeObject = datetime.datetime.combine(
97
+ dateTimeObject, datetime.time.min.replace(tzinfo=datetime.timezone.utc)
98
+ )
99
+ posixTime = dateTimeObject.replace(tzinfo=datetime.timezone.utc).timestamp()
100
+ return int(posixTime * 1000)
101
+
102
+
103
+ def fromEpoch(epoch):
104
+ """
105
+ Get datetime object from epoch
106
+ """
107
+ # Check if the timestamp is in milliseconds
108
+ if epoch > 10**17: # nanoseconds
109
+ epoch /= 1000**3
110
+ elif epoch > 10**14: # microseconds
111
+ epoch /= 1000**2
112
+ elif epoch > 10**11: # milliseconds
113
+ epoch /= 1000
114
+ return datetime.datetime.utcfromtimestamp(epoch)
115
+
116
+
117
+ def toString(myDate=None):
118
+ """
119
+ Convert to String
120
+ if argument type is neither _dateTimeType, _dateType, nor _timeType
121
+ the current dateTime converted to String is returned instead
122
+
123
+ Notice: datetime.timedelta are converted to strings using the format:
124
+ [day] days [hour]:[min]:[sec]:[microsec]
125
+ where hour, min, sec, microsec are always positive integers,
126
+ and day carries the sign.
127
+ To keep internal consistency we are using:
128
+ [hour]:[min]:[sec]:[microsec]
129
+ where min, sec, microsec are always positive integers and hour carries the sign.
130
+ """
131
+ if isinstance(myDate, datetime.date):
132
+ return str(myDate)
133
+
134
+ elif isinstance(myDate, datetime.time):
135
+ return "%02d:%02d:%02d.%06d" % (
136
+ myDate.days * 24 + myDate.seconds / 3600,
137
+ myDate.seconds % 3600 / 60,
138
+ myDate.seconds % 60,
139
+ myDate.microseconds,
140
+ )
141
+ else:
142
+ return toString(datetime.datetime.utcnow())
143
+
144
+
145
+ def fromString(myDate=None):
146
+ """
147
+ Convert date/time/datetime String back to appropriated objects
148
+
149
+ The format of the string it is assume to be that returned by toString method.
150
+ See notice on toString method
151
+ On Error, return None
152
+
153
+ :param myDate: the date string to be converted
154
+ :type myDate: str or datetime.datetime
155
+ """
156
+ if isinstance(myDate, datetime.datetime):
157
+ return myDate
158
+ if isinstance(myDate, str):
159
+ if myDate.find(" ") > 0:
160
+ dateTimeTuple = myDate.split(" ")
161
+ dateTuple = dateTimeTuple[0].split("-")
162
+ try:
163
+ return datetime.datetime(year=dateTuple[0], month=dateTuple[1], day=dateTuple[2]) + fromString(
164
+ dateTimeTuple[1]
165
+ )
166
+ # return datetime.datetime.utcnow().combine( fromString( dateTimeTuple[0] ),
167
+ # fromString( dateTimeTuple[1] ) )
168
+ except Exception:
169
+ try:
170
+ return datetime.datetime(
171
+ year=int(dateTuple[0]), month=int(dateTuple[1]), day=int(dateTuple[2])
172
+ ) + fromString(dateTimeTuple[1])
173
+ except ValueError:
174
+ return None
175
+ # return datetime.datetime.utcnow().combine( fromString( dateTimeTuple[0] ),
176
+ # fromString( dateTimeTuple[1] ) )
177
+ elif myDate.find(":") > 0:
178
+ timeTuple = myDate.replace(".", ":").split(":")
179
+ try:
180
+ if len(timeTuple) == 4:
181
+ return datetime.timedelta(
182
+ hours=int(timeTuple[0]),
183
+ minutes=int(timeTuple[1]),
184
+ seconds=int(timeTuple[2]),
185
+ microseconds=int(timeTuple[3]),
186
+ )
187
+ elif len(timeTuple) == 3:
188
+ try:
189
+ return datetime.timedelta(
190
+ hours=int(timeTuple[0]),
191
+ minutes=int(timeTuple[1]),
192
+ seconds=int(timeTuple[2]),
193
+ microseconds=0,
194
+ )
195
+ except ValueError:
196
+ return None
197
+ else:
198
+ return None
199
+ except Exception:
200
+ return None
201
+ elif myDate.find("-") > 0:
202
+ dateTuple = myDate.split("-")
203
+ try:
204
+ return datetime.date(int(dateTuple[0]), int(dateTuple[1]), int(dateTuple[2]))
205
+ except Exception:
206
+ return None
207
+
208
+ return None
209
+
210
+
211
+ class timeInterval:
212
+ """
213
+ Simple class to define a timeInterval object able to check if a given
214
+ dateTime is inside
215
+ """
216
+
217
+ def __init__(self, initialDateTime, intervalTimeDelta):
218
+ """
219
+ Initialization method, it requires the initial dateTime and the
220
+ timedelta that define the limits.
221
+ The upper limit is not included thus it is [begin,end)
222
+ If not properly initialized an error flag is set, and subsequent calls
223
+ to any method will return None
224
+ """
225
+ if not isinstance(initialDateTime, datetime.datetime) or not isinstance(intervalTimeDelta, datetime.timedelta):
226
+ self.__error = True
227
+ return None
228
+ self.__error = False
229
+ if intervalTimeDelta.days < 0:
230
+ self.__startDateTime = initialDateTime + intervalTimeDelta
231
+ self.__endDateTime = initialDateTime
232
+ else:
233
+ self.__startDateTime = initialDateTime
234
+ self.__endDateTime = initialDateTime + intervalTimeDelta
235
+
236
+ def includes(self, myDateTime):
237
+ """ """
238
+ if self.__error:
239
+ return None
240
+ if not isinstance(myDateTime, datetime.datetime):
241
+ return None
242
+ if myDateTime < self.__startDateTime:
243
+ return False
244
+ if myDateTime >= self.__endDateTime:
245
+ return False
246
+ return True
247
+
248
+
249
+ def queryTime(f):
250
+ """Decorator to measure the function call time"""
251
+
252
+ def measureQueryTime(*args, **kwargs):
253
+ start = time.time()
254
+ result = f(*args, **kwargs)
255
+ if result["OK"] and "QueryTime" not in result:
256
+ result["QueryTime"] = time.time() - start
257
+ return result
258
+
259
+ return measureQueryTime
@@ -0,0 +1,3 @@
1
+ """
2
+ DIRACCommon.Utils - Stateless utility functions
3
+ """
@@ -0,0 +1 @@
1
+ """DIRACCommon Core utilities"""
@@ -0,0 +1,235 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, TypedDict
4
+
5
+ from diraccfg import CFG
6
+
7
+ from DIRACCommon.Core.Utilities import List
8
+ from DIRACCommon.Core.Utilities.JDL import dumpCFGAsJDL, loadJDLAsCFG
9
+ from DIRACCommon.Core.Utilities.ReturnValues import S_ERROR, S_OK
10
+
11
+
12
+ class JobManifestNumericalVar(TypedDict):
13
+ CPUTime: int
14
+ Priority: int
15
+
16
+
17
+ class JobManifestConfig(TypedDict):
18
+ """Dictionary type for defining the information JobManifest needs from the CS"""
19
+
20
+ defaultForGroup: JobManifestNumericalVar
21
+ minForGroup: JobManifestNumericalVar
22
+ maxForGroup: JobManifestNumericalVar
23
+ allowedJobTypesForGroup: list[str]
24
+
25
+ maxInputData: int
26
+
27
+
28
+ class JobManifest:
29
+ def __init__(self, manifest=""):
30
+ self.__manifest = CFG()
31
+ self.__dirty = False
32
+ if manifest:
33
+ result = self.load(manifest)
34
+ if not result["OK"]:
35
+ raise Exception(result["Message"])
36
+
37
+ def isDirty(self):
38
+ return self.__dirty
39
+
40
+ def setDirty(self):
41
+ self.__dirty = True
42
+
43
+ def clearDirty(self):
44
+ self.__dirty = False
45
+
46
+ def load(self, dataString):
47
+ """
48
+ Auto discover format type based on [ .. ] of JDL
49
+ """
50
+ dataString = dataString.strip()
51
+ if dataString[0] == "[" and dataString[-1] == "]":
52
+ return self.loadJDL(dataString)
53
+ else:
54
+ return self.loadCFG(dataString)
55
+
56
+ def loadJDL(self, jdlString):
57
+ """
58
+ Load job manifest from JDL format
59
+ """
60
+ result = loadJDLAsCFG(jdlString.strip())
61
+ if not result["OK"]:
62
+ self.__manifest = CFG()
63
+ return result
64
+ self.__manifest = result["Value"][0]
65
+ return S_OK()
66
+
67
+ def loadCFG(self, cfgString):
68
+ """
69
+ Load job manifest from CFG format
70
+ """
71
+ try:
72
+ self.__manifest.loadFromBuffer(cfgString)
73
+ except Exception as e:
74
+ return S_ERROR(f"Can't load manifest from cfg: {str(e)}")
75
+ return S_OK()
76
+
77
+ def dumpAsCFG(self):
78
+ return str(self.__manifest)
79
+
80
+ def getAsCFG(self):
81
+ return self.__manifest.clone()
82
+
83
+ def dumpAsJDL(self):
84
+ return dumpCFGAsJDL(self.__manifest)
85
+
86
+ def _checkNumericalVar(
87
+ self,
88
+ varName: Literal["CPUTime", "Priority"],
89
+ defaultVal: int,
90
+ minVal: int,
91
+ maxVal: int,
92
+ config: JobManifestConfig,
93
+ ):
94
+ """
95
+ Check a numerical var
96
+ """
97
+ if varName in self.__manifest:
98
+ varValue = self.__manifest[varName]
99
+ else:
100
+ varValue = config["defaultForGroup"].get(varName, defaultVal)
101
+ try:
102
+ varValue = int(varValue)
103
+ except ValueError:
104
+ return S_ERROR(f"{varName} must be a number")
105
+ minVal = config["minForGroup"][varName]
106
+ maxVal = config["maxForGroup"][varName]
107
+ varValue = max(minVal, min(varValue, maxVal))
108
+ if varName not in self.__manifest:
109
+ self.__manifest.setOption(varName, varValue)
110
+ return S_OK(varValue)
111
+
112
+ def __contains__(self, key):
113
+ """Check if the manifest has the required key"""
114
+ return key in self.__manifest
115
+
116
+ def setOptionsFromDict(self, varDict):
117
+ for k in sorted(varDict):
118
+ self.setOption(k, varDict[k])
119
+
120
+ def check(self, *, config: JobManifestConfig):
121
+ """
122
+ Check that the manifest is OK
123
+ """
124
+ for k in ["Owner", "OwnerGroup"]:
125
+ if k not in self.__manifest:
126
+ return S_ERROR(f"Missing var {k} in manifest")
127
+
128
+ # Check CPUTime
129
+ result = self._checkNumericalVar("CPUTime", 86400, 100, 500000, config=config)
130
+ if not result["OK"]:
131
+ return result
132
+
133
+ result = self._checkNumericalVar("Priority", 1, 0, 10, config=config)
134
+ if not result["OK"]:
135
+ return result
136
+
137
+ if "InputData" in self.__manifest:
138
+ nInput = len(List.fromChar(self.__manifest["InputData"]))
139
+ if nInput > config["maxInputData"]:
140
+ return S_ERROR(
141
+ f"Number of Input Data Files ({nInput}) greater than current limit: {config['maxInputData']}"
142
+ )
143
+
144
+ if "JobType" in self.__manifest:
145
+ varValue = self.__manifest["JobType"]
146
+ for v in List.fromChar(varValue):
147
+ if v not in config["allowedJobTypesForGroup"]:
148
+ return S_ERROR(f"{v} is not a valid value for JobType")
149
+
150
+ return S_OK()
151
+
152
+ def createSection(self, secName, contents=False):
153
+ if secName not in self.__manifest:
154
+ if contents and not isinstance(contents, CFG):
155
+ return S_ERROR(f"Contents for section {secName} is not a cfg object")
156
+ self.__dirty = True
157
+ return S_OK(self.__manifest.createNewSection(secName, contents=contents))
158
+ return S_ERROR(f"Section {secName} already exists")
159
+
160
+ def getSection(self, secName):
161
+ self.__dirty = True
162
+ if secName not in self.__manifest:
163
+ return S_ERROR(f"{secName} does not exist")
164
+ sec = self.__manifest[secName]
165
+ if not sec:
166
+ return S_ERROR(f"{secName} section empty")
167
+ return S_OK(sec)
168
+
169
+ def setSectionContents(self, secName, contents):
170
+ if contents and not isinstance(contents, CFG):
171
+ return S_ERROR(f"Contents for section {secName} is not a cfg object")
172
+ self.__dirty = True
173
+ if secName in self.__manifest:
174
+ self.__manifest[secName].reset()
175
+ self.__manifest[secName].mergeWith(contents)
176
+ else:
177
+ self.__manifest.createNewSection(secName, contents=contents)
178
+
179
+ def setOption(self, varName, varValue):
180
+ """
181
+ Set a var in job manifest
182
+ """
183
+ self.__dirty = True
184
+ levels = List.fromChar(varName, "/")
185
+ cfg = self.__manifest
186
+ for l in levels[:-1]:
187
+ if l not in cfg:
188
+ cfg.createNewSection(l)
189
+ cfg = cfg[l]
190
+ cfg.setOption(levels[-1], varValue)
191
+
192
+ def remove(self, opName):
193
+ levels = List.fromChar(opName, "/")
194
+ cfg = self.__manifest
195
+ for l in levels[:-1]:
196
+ if l not in cfg:
197
+ return S_ERROR(f"{opName} does not exist")
198
+ cfg = cfg[l]
199
+ if cfg.deleteKey(levels[-1]):
200
+ self.__dirty = True
201
+ return S_OK()
202
+ return S_ERROR(f"{opName} does not exist")
203
+
204
+ def getOption(self, varName, defaultValue=None):
205
+ """
206
+ Get a variable from the job manifest
207
+ """
208
+ cfg = self.__manifest
209
+ return cfg.getOption(varName, defaultValue)
210
+
211
+ def getOptionList(self, section=""):
212
+ """
213
+ Get a list of variables in a section of the job manifest
214
+ """
215
+ cfg = self.__manifest.getRecursive(section)
216
+ if not cfg or "value" not in cfg:
217
+ return []
218
+ cfg = cfg["value"]
219
+ return cfg.listOptions()
220
+
221
+ def isOption(self, opName):
222
+ """
223
+ Check if it is a valid option
224
+ """
225
+ return self.__manifest.isOption(opName)
226
+
227
+ def getSectionList(self, section=""):
228
+ """
229
+ Get a list of sections in the job manifest
230
+ """
231
+ cfg = self.__manifest.getRecursive(section)
232
+ if not cfg or "value" not in cfg:
233
+ return []
234
+ cfg = cfg["value"]
235
+ return cfg.listSections()