scriptplan 0.9.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.
- scriptplan/__init__.py +22 -0
- scriptplan/cli/__init__.py +7 -0
- scriptplan/cli/main.py +546 -0
- scriptplan/core/__init__.py +0 -0
- scriptplan/core/account.py +125 -0
- scriptplan/core/allocation.py +69 -0
- scriptplan/core/booking.py +39 -0
- scriptplan/core/journal.py +377 -0
- scriptplan/core/leave.py +14 -0
- scriptplan/core/limits.py +354 -0
- scriptplan/core/project.py +924 -0
- scriptplan/core/property.py +1290 -0
- scriptplan/core/resource.py +198 -0
- scriptplan/core/resource_scenario.py +711 -0
- scriptplan/core/scenario.py +5 -0
- scriptplan/core/scenario_data.py +39 -0
- scriptplan/core/shift.py +71 -0
- scriptplan/core/task.py +77 -0
- scriptplan/core/task_scenario.py +1515 -0
- scriptplan/core/timesheet.py +457 -0
- scriptplan/core/working_hours.py +231 -0
- scriptplan/parser/__init__.py +0 -0
- scriptplan/parser/macro_processor.py +264 -0
- scriptplan/parser/tjp.lark +412 -0
- scriptplan/parser/tjp_parser.py +1904 -0
- scriptplan/py.typed +0 -0
- scriptplan/report/__init__.py +75 -0
- scriptplan/report/html_generator.py +477 -0
- scriptplan/report/report.py +466 -0
- scriptplan/report/report_base.py +397 -0
- scriptplan/report/report_context.py +248 -0
- scriptplan/report/resource_report.py +341 -0
- scriptplan/report/table_report.py +693 -0
- scriptplan/report/task_report.py +362 -0
- scriptplan/report/text_report.py +172 -0
- scriptplan/scheduler/__init__.py +0 -0
- scriptplan/scheduler/batch_processor.py +238 -0
- scriptplan/scheduler/scoreboard.py +120 -0
- scriptplan/utils/__init__.py +0 -0
- scriptplan/utils/data_cache.py +46 -0
- scriptplan/utils/logger.py +243 -0
- scriptplan/utils/message_handler.py +515 -0
- scriptplan/utils/time.py +195 -0
- scriptplan-0.9.0.dist-info/METADATA +161 -0
- scriptplan-0.9.0.dist-info/RECORD +49 -0
- scriptplan-0.9.0.dist-info/WHEEL +5 -0
- scriptplan-0.9.0.dist-info/entry_points.txt +2 -0
- scriptplan-0.9.0.dist-info/licenses/LICENSE +201 -0
- scriptplan-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1290 @@
|
|
|
1
|
+
from scriptplan.utils.message_handler import MessageHandler
|
|
2
|
+
from scriptplan.utils.time import TjTime
|
|
3
|
+
|
|
4
|
+
class PropertySet:
|
|
5
|
+
def __init__(self, project, flat_namespace=False):
|
|
6
|
+
self.project = project
|
|
7
|
+
self.flat_namespace = flat_namespace
|
|
8
|
+
self.attributes = []
|
|
9
|
+
self.attributeDefinitions = {}
|
|
10
|
+
self._properties = [] # List for order
|
|
11
|
+
self._propertyMap = {} # Dict fullId -> PropertyTreeNode
|
|
12
|
+
|
|
13
|
+
# Add standard attributes
|
|
14
|
+
# In Ruby: id, name, seqno
|
|
15
|
+
# I will move this logic here from Project.py if I update Project.py later,
|
|
16
|
+
# but for now I can duplicate or rely on Project.py calling _add_standard_attributes.
|
|
17
|
+
# However, cleaner to have it here.
|
|
18
|
+
self.addAttributeType(AttributeDefinition('id', 'ID', StringAttribute, False, False, False, ''))
|
|
19
|
+
self.addAttributeType(AttributeDefinition('name', 'Name', StringAttribute, False, False, False, ''))
|
|
20
|
+
self.addAttributeType(AttributeDefinition('seqno', 'Seq. No', IntegerAttribute, False, False, False, 0))
|
|
21
|
+
|
|
22
|
+
def addAttributeType(self, attribute_definition):
|
|
23
|
+
if self._properties:
|
|
24
|
+
raise RuntimeError("Fatal Error: Attribute types must be defined before properties are added.")
|
|
25
|
+
|
|
26
|
+
self.attributes.append(attribute_definition)
|
|
27
|
+
self.attributeDefinitions[attribute_definition.id] = attribute_definition
|
|
28
|
+
|
|
29
|
+
def eachAttributeDefinition(self):
|
|
30
|
+
return iter(self.attributes)
|
|
31
|
+
|
|
32
|
+
def items(self):
|
|
33
|
+
return len(self._properties)
|
|
34
|
+
|
|
35
|
+
def length(self):
|
|
36
|
+
return len(self._properties)
|
|
37
|
+
|
|
38
|
+
def __len__(self):
|
|
39
|
+
return len(self._properties)
|
|
40
|
+
|
|
41
|
+
def __getitem__(self, key):
|
|
42
|
+
return self._propertyMap.get(key)
|
|
43
|
+
|
|
44
|
+
def __setitem__(self, key, value):
|
|
45
|
+
# Should typically use addProperty
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def __iter__(self):
|
|
49
|
+
return iter(self._properties)
|
|
50
|
+
|
|
51
|
+
def __contains__(self, item):
|
|
52
|
+
if isinstance(item, str):
|
|
53
|
+
return item in self._propertyMap
|
|
54
|
+
return item in self._properties
|
|
55
|
+
|
|
56
|
+
def empty(self):
|
|
57
|
+
return len(self._properties) == 0
|
|
58
|
+
|
|
59
|
+
def addProperty(self, property):
|
|
60
|
+
self._propertyMap[property.fullId] = property
|
|
61
|
+
self._properties.append(property)
|
|
62
|
+
|
|
63
|
+
def removeProperty(self, prop):
|
|
64
|
+
if isinstance(prop, str):
|
|
65
|
+
property_node = self._propertyMap.get(prop)
|
|
66
|
+
else:
|
|
67
|
+
property_node = prop
|
|
68
|
+
|
|
69
|
+
if not property_node:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
# Eliminate references
|
|
73
|
+
for p in self._properties:
|
|
74
|
+
p.removeReferences(property_node)
|
|
75
|
+
|
|
76
|
+
# Recursively remove children
|
|
77
|
+
# Copy children list to avoid modification during iteration issue
|
|
78
|
+
children = list(property_node.children)
|
|
79
|
+
for child in children:
|
|
80
|
+
self.removeProperty(child)
|
|
81
|
+
|
|
82
|
+
if property_node in self._properties:
|
|
83
|
+
self._properties.remove(property_node)
|
|
84
|
+
if property_node.fullId in self._propertyMap:
|
|
85
|
+
del self._propertyMap[property_node.fullId]
|
|
86
|
+
|
|
87
|
+
if property_node.parent:
|
|
88
|
+
if property_node in property_node.parent.children:
|
|
89
|
+
property_node.parent.children.remove(property_node)
|
|
90
|
+
|
|
91
|
+
return property_node
|
|
92
|
+
|
|
93
|
+
def clearProperties(self):
|
|
94
|
+
self._properties.clear()
|
|
95
|
+
self._propertyMap.clear()
|
|
96
|
+
|
|
97
|
+
def index(self):
|
|
98
|
+
for p in self._properties:
|
|
99
|
+
bsIdcs = p.getBSIndicies()
|
|
100
|
+
bsi = ".".join(map(str, bsIdcs))
|
|
101
|
+
p.force('bsi', bsi)
|
|
102
|
+
|
|
103
|
+
def levelSeqNo(self, property_node):
|
|
104
|
+
seqNo = 1
|
|
105
|
+
for p in self._properties:
|
|
106
|
+
if not p.parent:
|
|
107
|
+
if p == property_node:
|
|
108
|
+
return seqNo
|
|
109
|
+
seqNo += 1
|
|
110
|
+
raise ValueError(f"Unknown property {property_node.fullId}")
|
|
111
|
+
|
|
112
|
+
def maxDepth(self):
|
|
113
|
+
md = 0
|
|
114
|
+
for p in self._properties:
|
|
115
|
+
if p.level() > md:
|
|
116
|
+
md = p.level()
|
|
117
|
+
return md + 1
|
|
118
|
+
|
|
119
|
+
def topLevelItems(self):
|
|
120
|
+
items = 0
|
|
121
|
+
for p in self._properties:
|
|
122
|
+
if not p.parent:
|
|
123
|
+
items += 1
|
|
124
|
+
return items
|
|
125
|
+
|
|
126
|
+
def to_ary(self):
|
|
127
|
+
return list(self._properties)
|
|
128
|
+
|
|
129
|
+
def to_s(self):
|
|
130
|
+
# PropertyList.new(self).to_s
|
|
131
|
+
return str(self._properties)
|
|
132
|
+
|
|
133
|
+
def knownAttribute(self, attrId):
|
|
134
|
+
return attrId in self.attributeDefinitions
|
|
135
|
+
|
|
136
|
+
def hasQuery(self, attrId, scenarioIdx=None):
|
|
137
|
+
if not self._properties:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
property_node = self._properties[0]
|
|
141
|
+
method_name = f"query_{attrId}"
|
|
142
|
+
|
|
143
|
+
if hasattr(property_node, method_name):
|
|
144
|
+
return True
|
|
145
|
+
elif scenarioIdx is not None:
|
|
146
|
+
# Check scenario object
|
|
147
|
+
if property_node.data and property_node.data[scenarioIdx]:
|
|
148
|
+
return hasattr(property_node.data[scenarioIdx], method_name)
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
def scenarioSpecific(self, attrId):
|
|
152
|
+
defn = self.attributeDefinitions.get(attrId)
|
|
153
|
+
if defn:
|
|
154
|
+
return defn.scenarioSpecific
|
|
155
|
+
|
|
156
|
+
# Check for query method
|
|
157
|
+
if self._properties:
|
|
158
|
+
prop = self._properties[0]
|
|
159
|
+
if prop.data and prop.data[0] and hasattr(prop.data[0], f"query_{attrId}"):
|
|
160
|
+
return True
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def inheritedFromProject(self, attrId):
|
|
164
|
+
defn = self.attributeDefinitions.get(attrId)
|
|
165
|
+
return defn.inheritedFromProject if defn else False
|
|
166
|
+
|
|
167
|
+
def inheritedFromParent(self, attrId):
|
|
168
|
+
defn = self.attributeDefinitions.get(attrId)
|
|
169
|
+
return defn.inheritedFromParent if defn else False
|
|
170
|
+
|
|
171
|
+
def userDefined(self, attrId):
|
|
172
|
+
defn = self.attributeDefinitions.get(attrId)
|
|
173
|
+
# userDefined attribute on AttributeDefinition not implemented yet, defaulting to False
|
|
174
|
+
return getattr(defn, 'userDefined', False) if defn else False
|
|
175
|
+
|
|
176
|
+
def listAttribute(self, attrId):
|
|
177
|
+
defn = self.attributeDefinitions.get(attrId)
|
|
178
|
+
return defn.isList() if defn else False
|
|
179
|
+
|
|
180
|
+
def defaultValue(self, attrId):
|
|
181
|
+
defn = self.attributeDefinitions.get(attrId)
|
|
182
|
+
return defn.default if defn else None
|
|
183
|
+
|
|
184
|
+
def attributeName(self, attrId):
|
|
185
|
+
defn = self.attributeDefinitions.get(attrId)
|
|
186
|
+
return defn.name if defn else None
|
|
187
|
+
|
|
188
|
+
def attributeType(self, attrId):
|
|
189
|
+
defn = self.attributeDefinitions.get(attrId)
|
|
190
|
+
return defn.objClass if defn else None
|
|
191
|
+
|
|
192
|
+
class PTNProxy:
|
|
193
|
+
"""Proxy for PropertyTreeNode that represents adopted nodes in their new parental context."""
|
|
194
|
+
|
|
195
|
+
def __init__(self, ptn, parent):
|
|
196
|
+
self._ptn = ptn
|
|
197
|
+
if not parent:
|
|
198
|
+
raise ValueError("Adopted properties must have a parent")
|
|
199
|
+
self._parent = parent
|
|
200
|
+
self._index = None
|
|
201
|
+
self._tree = None
|
|
202
|
+
self._level = -1
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def parent(self):
|
|
206
|
+
return self._parent
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def ptn(self):
|
|
210
|
+
return self._ptn
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def logicalId(self):
|
|
214
|
+
if self._ptn.propertySet.flat_namespace:
|
|
215
|
+
return self._ptn.id
|
|
216
|
+
else:
|
|
217
|
+
dot_pos = self._ptn.id.rfind('.')
|
|
218
|
+
if dot_pos >= 0:
|
|
219
|
+
id = self._ptn.id[dot_pos + 1:]
|
|
220
|
+
else:
|
|
221
|
+
id = self._ptn.id
|
|
222
|
+
return f"{self._parent.logicalId}.{id}"
|
|
223
|
+
|
|
224
|
+
def set(self, attribute, val):
|
|
225
|
+
if attribute == 'index':
|
|
226
|
+
self._index = val
|
|
227
|
+
elif attribute == 'tree':
|
|
228
|
+
self._tree = val
|
|
229
|
+
else:
|
|
230
|
+
self._ptn.set(attribute, val)
|
|
231
|
+
|
|
232
|
+
def get(self, attribute):
|
|
233
|
+
if attribute == 'index':
|
|
234
|
+
return self._index
|
|
235
|
+
elif attribute == 'tree':
|
|
236
|
+
return self._tree
|
|
237
|
+
else:
|
|
238
|
+
return self._ptn.get(attribute)
|
|
239
|
+
|
|
240
|
+
def __getitem__(self, key):
|
|
241
|
+
if isinstance(key, tuple):
|
|
242
|
+
attribute, scenarioIdx = key
|
|
243
|
+
else:
|
|
244
|
+
attribute = key
|
|
245
|
+
scenarioIdx = None
|
|
246
|
+
|
|
247
|
+
if attribute == 'index':
|
|
248
|
+
return self._index
|
|
249
|
+
elif attribute == 'tree':
|
|
250
|
+
return self._tree
|
|
251
|
+
else:
|
|
252
|
+
if scenarioIdx is not None:
|
|
253
|
+
return self._ptn[(attribute, scenarioIdx)]
|
|
254
|
+
return self._ptn[attribute]
|
|
255
|
+
|
|
256
|
+
def level(self):
|
|
257
|
+
if self._level >= 0:
|
|
258
|
+
return self._level
|
|
259
|
+
|
|
260
|
+
t = self
|
|
261
|
+
self._level = 0
|
|
262
|
+
while t.parent is not None:
|
|
263
|
+
t = t.parent
|
|
264
|
+
self._level += 1
|
|
265
|
+
return self._level
|
|
266
|
+
|
|
267
|
+
def isChildOf(self, ancestor):
|
|
268
|
+
parent = self
|
|
269
|
+
while parent.parent is not None:
|
|
270
|
+
parent = parent.parent
|
|
271
|
+
if parent == ancestor:
|
|
272
|
+
return True
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
def getIndicies(self):
|
|
276
|
+
idcs = []
|
|
277
|
+
p = self
|
|
278
|
+
while p is not None:
|
|
279
|
+
parent = p.parent
|
|
280
|
+
idcs.insert(0, p.get('index'))
|
|
281
|
+
p = parent
|
|
282
|
+
return idcs
|
|
283
|
+
|
|
284
|
+
def force(self, attribute, val):
|
|
285
|
+
if attribute == 'index':
|
|
286
|
+
self._index = val
|
|
287
|
+
elif attribute == 'tree':
|
|
288
|
+
self._tree = val
|
|
289
|
+
else:
|
|
290
|
+
self._ptn.force(attribute, val)
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def fullId(self):
|
|
294
|
+
return self._ptn.fullId
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def kids(self):
|
|
298
|
+
return self._ptn.kids()
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def adoptees(self):
|
|
302
|
+
return self._ptn.adoptees
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def propertySet(self):
|
|
306
|
+
return self._ptn.propertySet
|
|
307
|
+
|
|
308
|
+
def __getattr__(self, name):
|
|
309
|
+
return getattr(self._ptn, name)
|
|
310
|
+
|
|
311
|
+
def __eq__(self, other):
|
|
312
|
+
if isinstance(other, PTNProxy):
|
|
313
|
+
return self._ptn == other._ptn
|
|
314
|
+
return self._ptn == other
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class PropertyList:
|
|
318
|
+
"""List of PropertyTreeNodes with multi-level sorting support.
|
|
319
|
+
|
|
320
|
+
All nodes in the list must belong to the same PropertySet.
|
|
321
|
+
Sorting can use multiple criteria with ascending/descending direction.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def __init__(self, arg, copyItems=True):
|
|
325
|
+
if isinstance(arg, PropertySet):
|
|
326
|
+
self._items = list(arg._properties) if copyItems else []
|
|
327
|
+
self._propertySet = arg
|
|
328
|
+
self._query = None
|
|
329
|
+
self.resetSorting()
|
|
330
|
+
self.addSortingCriteria('seqno', True, -1)
|
|
331
|
+
self.sort()
|
|
332
|
+
elif isinstance(arg, PropertyList):
|
|
333
|
+
self._items = list(arg._items) if copyItems else []
|
|
334
|
+
self._propertySet = arg._propertySet
|
|
335
|
+
self._query = arg._query.copy() if arg._query else None
|
|
336
|
+
self._sortingLevels = arg._sortingLevels
|
|
337
|
+
self._sortingCriteria = list(arg._sortingCriteria)
|
|
338
|
+
self._sortingUp = list(arg._sortingUp)
|
|
339
|
+
self._scenarioIdx = list(arg._scenarioIdx)
|
|
340
|
+
else:
|
|
341
|
+
# Assume it's a list/iterable
|
|
342
|
+
self._items = list(arg) if copyItems else []
|
|
343
|
+
self._propertySet = arg[0].propertySet if arg else None
|
|
344
|
+
self._query = None
|
|
345
|
+
self.resetSorting()
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def propertySet(self):
|
|
349
|
+
return self._propertySet
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def query(self):
|
|
353
|
+
return self._query
|
|
354
|
+
|
|
355
|
+
@query.setter
|
|
356
|
+
def query(self, value):
|
|
357
|
+
self._query = value
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def sortingLevels(self):
|
|
361
|
+
return self._sortingLevels
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def sortingCriteria(self):
|
|
365
|
+
return self._sortingCriteria
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def sortingUp(self):
|
|
369
|
+
return self._sortingUp
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def scenarioIdx(self):
|
|
373
|
+
return self._scenarioIdx
|
|
374
|
+
|
|
375
|
+
def includeAdopted(self):
|
|
376
|
+
adopted = []
|
|
377
|
+
for p in self._items:
|
|
378
|
+
for ap in p.adoptees:
|
|
379
|
+
adopted.extend(self._includeAdoptedR(ap, p))
|
|
380
|
+
self.append(adopted)
|
|
381
|
+
|
|
382
|
+
def _includeAdoptedR(self, property, parent):
|
|
383
|
+
parentProxy = PTNProxy(property, parent)
|
|
384
|
+
adopted = [parentProxy]
|
|
385
|
+
|
|
386
|
+
for p in property.kids():
|
|
387
|
+
adopted.extend(self._includeAdoptedR(p, parentProxy))
|
|
388
|
+
|
|
389
|
+
return adopted
|
|
390
|
+
|
|
391
|
+
def checkForDuplicates(self, sourceFileInfo=None):
|
|
392
|
+
ptns = {}
|
|
393
|
+
for i in self._items:
|
|
394
|
+
ptn = i.ptn if isinstance(i, PTNProxy) else i
|
|
395
|
+
if ptn in ptns:
|
|
396
|
+
other = ptns[ptn]
|
|
397
|
+
raise ValueError(
|
|
398
|
+
f"An adopted property is included as {i.logicalId if hasattr(i, 'logicalId') else i.fullId} and "
|
|
399
|
+
f"as {other.logicalId if hasattr(other, 'logicalId') else other.fullId}. "
|
|
400
|
+
"Please use stronger filtering to avoid including the property more than once!"
|
|
401
|
+
)
|
|
402
|
+
ptns[ptn] = i
|
|
403
|
+
|
|
404
|
+
def __contains__(self, node):
|
|
405
|
+
target_ptn = node.ptn if isinstance(node, PTNProxy) else node
|
|
406
|
+
for p in self._items:
|
|
407
|
+
p_ptn = p.ptn if isinstance(p, PTNProxy) else p
|
|
408
|
+
if p_ptn == target_ptn:
|
|
409
|
+
return True
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
def __getitem__(self, key):
|
|
413
|
+
if isinstance(key, int):
|
|
414
|
+
return self._items[key]
|
|
415
|
+
# Node lookup
|
|
416
|
+
target_ptn = key.ptn if isinstance(key, PTNProxy) else key
|
|
417
|
+
for n in self._items:
|
|
418
|
+
n_ptn = n.ptn if isinstance(n, PTNProxy) else n
|
|
419
|
+
if n_ptn == target_ptn:
|
|
420
|
+
return n
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
def __len__(self):
|
|
424
|
+
return len(self._items)
|
|
425
|
+
|
|
426
|
+
def __iter__(self):
|
|
427
|
+
return iter(self._items)
|
|
428
|
+
|
|
429
|
+
def to_ary(self):
|
|
430
|
+
return list(self._items)
|
|
431
|
+
|
|
432
|
+
def setSorting(self, modes):
|
|
433
|
+
self.resetSorting()
|
|
434
|
+
for mode in modes:
|
|
435
|
+
self.addSortingCriteria(*mode)
|
|
436
|
+
|
|
437
|
+
def resetSorting(self):
|
|
438
|
+
self._sortingLevels = 0
|
|
439
|
+
self._sortingCriteria = []
|
|
440
|
+
self._sortingUp = []
|
|
441
|
+
self._scenarioIdx = []
|
|
442
|
+
|
|
443
|
+
def append(self, items):
|
|
444
|
+
if isinstance(items, (list, PropertyList)):
|
|
445
|
+
for node in items:
|
|
446
|
+
if node.propertySet != self._propertySet:
|
|
447
|
+
raise ValueError("All nodes must belong to the same PropertySet.")
|
|
448
|
+
self._items.extend(items)
|
|
449
|
+
if len(self._items) != len(set(id(x) for x in self._items)):
|
|
450
|
+
raise ValueError("Duplicate items")
|
|
451
|
+
else:
|
|
452
|
+
self._items.append(items)
|
|
453
|
+
self.sort()
|
|
454
|
+
|
|
455
|
+
def treeMode(self):
|
|
456
|
+
return self._sortingLevels > 0 and self._sortingCriteria[0] == 'tree'
|
|
457
|
+
|
|
458
|
+
def sort(self):
|
|
459
|
+
if self.treeMode():
|
|
460
|
+
sc = self._sortingCriteria.pop(0)
|
|
461
|
+
su = self._sortingUp.pop(0)
|
|
462
|
+
si = self._scenarioIdx.pop(0)
|
|
463
|
+
self._sortingLevels -= 1
|
|
464
|
+
|
|
465
|
+
self._sortInternal()
|
|
466
|
+
self.index()
|
|
467
|
+
self._indexTree()
|
|
468
|
+
|
|
469
|
+
self._sortingCriteria.insert(0, sc)
|
|
470
|
+
self._sortingUp.insert(0, su)
|
|
471
|
+
self._scenarioIdx.insert(0, si)
|
|
472
|
+
self._sortingLevels += 1
|
|
473
|
+
|
|
474
|
+
self._sortInternal()
|
|
475
|
+
else:
|
|
476
|
+
self._sortInternal()
|
|
477
|
+
self.index()
|
|
478
|
+
|
|
479
|
+
def itemIndex(self, item):
|
|
480
|
+
try:
|
|
481
|
+
return self._items.index(item)
|
|
482
|
+
except ValueError:
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
def index(self):
|
|
486
|
+
i = 0
|
|
487
|
+
for p in self._items:
|
|
488
|
+
i += 1
|
|
489
|
+
p.force('index', i)
|
|
490
|
+
|
|
491
|
+
def __str__(self):
|
|
492
|
+
res = "Sorting: "
|
|
493
|
+
for i in range(self._sortingLevels):
|
|
494
|
+
direction = 'up' if self._sortingUp[i] else 'down'
|
|
495
|
+
res += f"{self._sortingCriteria[i]}/{direction}/{self._scenarioIdx[i]}, "
|
|
496
|
+
res += f"\n{len(self._items)} properties:"
|
|
497
|
+
for item in self._items:
|
|
498
|
+
res += f"{item.get('id')}: {item.get('name')}\n"
|
|
499
|
+
return res
|
|
500
|
+
|
|
501
|
+
def addSortingCriteria(self, criteria, up, scIdx):
|
|
502
|
+
if not self._propertySet.knownAttribute(criteria) and \
|
|
503
|
+
not self._propertySet.hasQuery(criteria, scIdx):
|
|
504
|
+
raise ValueError(f"Unknown attribute '{criteria}' used for sorting criterium")
|
|
505
|
+
|
|
506
|
+
if self._propertySet.scenarioSpecific(criteria):
|
|
507
|
+
if scIdx < 0 or (hasattr(self._propertySet.project, 'scenario') and
|
|
508
|
+
self._propertySet.project.scenario(scIdx) is None):
|
|
509
|
+
# Allow if scIdx is valid or we can't verify
|
|
510
|
+
pass
|
|
511
|
+
else:
|
|
512
|
+
scIdx = -1
|
|
513
|
+
|
|
514
|
+
self._sortingCriteria.append(criteria)
|
|
515
|
+
self._sortingUp.append(up)
|
|
516
|
+
self._scenarioIdx.append(scIdx)
|
|
517
|
+
self._sortingLevels += 1
|
|
518
|
+
|
|
519
|
+
def _indexTree(self):
|
|
520
|
+
for property in self._items:
|
|
521
|
+
if isinstance(property, PTNProxy):
|
|
522
|
+
treeIdcs = property.getIndicies()
|
|
523
|
+
else:
|
|
524
|
+
treeIdcs = self._getIndicies(property)
|
|
525
|
+
|
|
526
|
+
tree = ''
|
|
527
|
+
for idx in treeIdcs:
|
|
528
|
+
tree += str(idx).rjust(6, '0')
|
|
529
|
+
property.force('tree', tree)
|
|
530
|
+
|
|
531
|
+
def _getIndicies(self, property):
|
|
532
|
+
idcs = []
|
|
533
|
+
p = property
|
|
534
|
+
while p is not None:
|
|
535
|
+
parent = p.parent
|
|
536
|
+
idx = p.get('index')
|
|
537
|
+
idcs.insert(0, idx)
|
|
538
|
+
p = parent
|
|
539
|
+
return idcs
|
|
540
|
+
|
|
541
|
+
def _sortInternal(self):
|
|
542
|
+
def compare_key(item):
|
|
543
|
+
key_parts = []
|
|
544
|
+
for i in range(self._sortingLevels):
|
|
545
|
+
criteria = self._sortingCriteria[i]
|
|
546
|
+
scIdx = self._scenarioIdx[i]
|
|
547
|
+
up = self._sortingUp[i]
|
|
548
|
+
|
|
549
|
+
if self._query and criteria != 'tree':
|
|
550
|
+
# Query-based sorting
|
|
551
|
+
self._query.scenarioIdx = None if scIdx < 0 else scIdx
|
|
552
|
+
self._query.attributeId = criteria
|
|
553
|
+
self._query.property = item
|
|
554
|
+
self._query.process()
|
|
555
|
+
val = self._query.to_sort()
|
|
556
|
+
else:
|
|
557
|
+
# Static attribute sorting
|
|
558
|
+
if scIdx < 0:
|
|
559
|
+
if criteria == 'id':
|
|
560
|
+
val = item.fullId
|
|
561
|
+
else:
|
|
562
|
+
val = item.get(criteria)
|
|
563
|
+
else:
|
|
564
|
+
val = item[(criteria, scIdx)]
|
|
565
|
+
|
|
566
|
+
# Handle None values
|
|
567
|
+
if val is None:
|
|
568
|
+
val = ''
|
|
569
|
+
|
|
570
|
+
# Invert for descending order
|
|
571
|
+
if not up:
|
|
572
|
+
if isinstance(val, (int, float)):
|
|
573
|
+
val = -val
|
|
574
|
+
elif isinstance(val, str):
|
|
575
|
+
# For strings, we need a different approach
|
|
576
|
+
# Use a tuple with a flag
|
|
577
|
+
val = (1, val) # descending strings will be sorted differently
|
|
578
|
+
else:
|
|
579
|
+
val = (1, val)
|
|
580
|
+
else:
|
|
581
|
+
if isinstance(val, str):
|
|
582
|
+
val = (0, val)
|
|
583
|
+
elif not isinstance(val, (int, float)):
|
|
584
|
+
val = (0, val)
|
|
585
|
+
|
|
586
|
+
key_parts.append(val)
|
|
587
|
+
return tuple(key_parts)
|
|
588
|
+
|
|
589
|
+
self._items.sort(key=compare_key)
|
|
590
|
+
|
|
591
|
+
def delete_if(self, func):
|
|
592
|
+
to_remove = [i for i in self._items if func(i)]
|
|
593
|
+
for i in to_remove:
|
|
594
|
+
self._items.remove(i)
|
|
595
|
+
|
|
596
|
+
def each(self, func):
|
|
597
|
+
for item in self._items:
|
|
598
|
+
func(item)
|
|
599
|
+
|
|
600
|
+
class AttributeDefinition:
|
|
601
|
+
"""Definition of an attribute type that can be added to a PropertySet.
|
|
602
|
+
|
|
603
|
+
The AttributeDefinition describes the meta information of a PropertyTreeNode
|
|
604
|
+
attribute. Based on this information, PropertySet objects generate the
|
|
605
|
+
attribute lists for each PropertyTreeNode upon creation of the node.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
id: The ID of the attribute. Must be unique within the PropertySet.
|
|
609
|
+
name: A descriptive text used in report columns and the like.
|
|
610
|
+
objClass: Reference to the class of the attribute (e.g., StringAttribute).
|
|
611
|
+
inheritedFromParent: True if the node can inherit from parent node.
|
|
612
|
+
inheritedFromProject: True if the node can inherit from global scope.
|
|
613
|
+
scenarioSpecific: True if the attribute can have different values per scenario.
|
|
614
|
+
default: The default value set upon creation of the attribute.
|
|
615
|
+
userDefined: True if this is a user-defined (custom) attribute.
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
__slots__ = ['id', 'name', 'objClass', 'inheritedFromParent',
|
|
619
|
+
'inheritedFromProject', 'scenarioSpecific', 'default', 'userDefined']
|
|
620
|
+
|
|
621
|
+
def __init__(self, id, name, objClass, inheritedFromParent, inheritedFromProject,
|
|
622
|
+
scenarioSpecific, default, userDefined=False):
|
|
623
|
+
self.id = id
|
|
624
|
+
self.name = name
|
|
625
|
+
self.objClass = objClass
|
|
626
|
+
self.inheritedFromParent = inheritedFromParent
|
|
627
|
+
self.inheritedFromProject = inheritedFromProject
|
|
628
|
+
self.scenarioSpecific = scenarioSpecific
|
|
629
|
+
self.default = default
|
|
630
|
+
self.userDefined = userDefined
|
|
631
|
+
|
|
632
|
+
def isList(self):
|
|
633
|
+
"""Return True if this attribute holds a list of values."""
|
|
634
|
+
return issubclass(self.objClass, ListAttributeBase)
|
|
635
|
+
|
|
636
|
+
def __repr__(self):
|
|
637
|
+
return (f"AttributeDefinition(id={self.id!r}, name={self.name!r}, "
|
|
638
|
+
f"objClass={self.objClass.__name__}, scenarioSpecific={self.scenarioSpecific})")
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class AttributeOverwrite(ValueError):
|
|
642
|
+
"""Exception raised when attempting to overwrite an existing attribute value."""
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def deep_clone(value):
|
|
647
|
+
"""Create a copy of a value for inheritance.
|
|
648
|
+
|
|
649
|
+
For most values, we do a deep copy. However, for lists containing
|
|
650
|
+
PropertyTreeNode objects (like Task references in depends), we do
|
|
651
|
+
a shallow copy to preserve object identity.
|
|
652
|
+
"""
|
|
653
|
+
import copy
|
|
654
|
+
|
|
655
|
+
# For lists, check if they contain PropertyTreeNode objects
|
|
656
|
+
if isinstance(value, list):
|
|
657
|
+
if value and hasattr(value[0], 'propertySet'):
|
|
658
|
+
# This is a list of PropertyTreeNode objects (like tasks in depends)
|
|
659
|
+
# Do a shallow copy to preserve object identity
|
|
660
|
+
return list(value)
|
|
661
|
+
else:
|
|
662
|
+
# Regular list, deep copy
|
|
663
|
+
return copy.deepcopy(value)
|
|
664
|
+
|
|
665
|
+
return copy.deepcopy(value)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class AttributeBase:
|
|
669
|
+
"""Base class for all property attribute types.
|
|
670
|
+
|
|
671
|
+
Each property can have multiple attributes of different types. Each type
|
|
672
|
+
must be derived from this class. The class tracks whether the attribute
|
|
673
|
+
value was provided by the project file, inherited from another property,
|
|
674
|
+
or computed during scheduling.
|
|
675
|
+
"""
|
|
676
|
+
|
|
677
|
+
_mode = 0 # 0=provided, 1=inherited, other=calculated
|
|
678
|
+
|
|
679
|
+
def __init__(self, property_node, type_def, container):
|
|
680
|
+
self._type = type_def
|
|
681
|
+
self._property = property_node
|
|
682
|
+
self._container = container
|
|
683
|
+
self.reset()
|
|
684
|
+
|
|
685
|
+
def reset(self):
|
|
686
|
+
"""Reset the attribute value to the default value."""
|
|
687
|
+
self._inherited = False
|
|
688
|
+
self._provided = False
|
|
689
|
+
|
|
690
|
+
if isinstance(self._type, AttributeDefinition):
|
|
691
|
+
self._value = deep_clone(self._type.default)
|
|
692
|
+
else:
|
|
693
|
+
self._value = self._type
|
|
694
|
+
|
|
695
|
+
def getProperty(self):
|
|
696
|
+
"""Return the property node this attribute belongs to."""
|
|
697
|
+
return self._property
|
|
698
|
+
|
|
699
|
+
def getType(self):
|
|
700
|
+
"""Return the attribute type definition."""
|
|
701
|
+
return self._type
|
|
702
|
+
|
|
703
|
+
# Alias for Ruby compatibility
|
|
704
|
+
type = property(lambda self: self._type)
|
|
705
|
+
|
|
706
|
+
def getProvided(self):
|
|
707
|
+
"""Return whether the value was provided."""
|
|
708
|
+
return self._provided
|
|
709
|
+
|
|
710
|
+
# Alias for Ruby compatibility
|
|
711
|
+
provided = property(lambda self: self._provided)
|
|
712
|
+
|
|
713
|
+
def getInherited(self):
|
|
714
|
+
"""Return whether the value was inherited."""
|
|
715
|
+
return self._inherited
|
|
716
|
+
|
|
717
|
+
# Alias for Ruby compatibility
|
|
718
|
+
inherited = property(lambda self: self._inherited)
|
|
719
|
+
|
|
720
|
+
def inherit(self, value):
|
|
721
|
+
"""Inherit value from parent property. Values are deep copied."""
|
|
722
|
+
self._inherited = True
|
|
723
|
+
self._value = deep_clone(value)
|
|
724
|
+
|
|
725
|
+
@classmethod
|
|
726
|
+
def mode(cls):
|
|
727
|
+
"""Return the current attribute setting mode."""
|
|
728
|
+
return cls._mode
|
|
729
|
+
|
|
730
|
+
@classmethod
|
|
731
|
+
def setMode(cls, mode):
|
|
732
|
+
"""Change the mode. 0=provided, 1=inherited, other=calculated."""
|
|
733
|
+
cls._mode = mode
|
|
734
|
+
|
|
735
|
+
def getId(self):
|
|
736
|
+
"""Return the ID of the attribute."""
|
|
737
|
+
return self._type.id
|
|
738
|
+
|
|
739
|
+
# Alias for Ruby compatibility
|
|
740
|
+
id = property(lambda self: self._type.id)
|
|
741
|
+
|
|
742
|
+
def getName(self):
|
|
743
|
+
"""Return the name of the attribute."""
|
|
744
|
+
return self._type.name
|
|
745
|
+
|
|
746
|
+
# Alias for Ruby compatibility
|
|
747
|
+
name = property(lambda self: self._type.name)
|
|
748
|
+
|
|
749
|
+
def set(self, value):
|
|
750
|
+
"""Set the value of the attribute. Flags updated based on mode."""
|
|
751
|
+
if AttributeBase._mode == 0:
|
|
752
|
+
self._provided = True
|
|
753
|
+
elif AttributeBase._mode == 1:
|
|
754
|
+
self._inherited = True
|
|
755
|
+
self._value = value
|
|
756
|
+
|
|
757
|
+
def get(self):
|
|
758
|
+
"""Return the attribute value."""
|
|
759
|
+
return self._value
|
|
760
|
+
|
|
761
|
+
# Alias for legacy purposes
|
|
762
|
+
value = property(lambda self: self.get())
|
|
763
|
+
|
|
764
|
+
def isNil(self):
|
|
765
|
+
"""Check whether the value is uninitialized or nil."""
|
|
766
|
+
v = self.get()
|
|
767
|
+
if isinstance(v, list):
|
|
768
|
+
return len(v) == 0
|
|
769
|
+
return v is None
|
|
770
|
+
|
|
771
|
+
def isList(self):
|
|
772
|
+
return False
|
|
773
|
+
|
|
774
|
+
@classmethod
|
|
775
|
+
def isListClass(cls):
|
|
776
|
+
return False
|
|
777
|
+
|
|
778
|
+
def to_s(self, query=None):
|
|
779
|
+
"""Return the value as String."""
|
|
780
|
+
return str(self.get())
|
|
781
|
+
|
|
782
|
+
def __str__(self):
|
|
783
|
+
return self.to_s()
|
|
784
|
+
|
|
785
|
+
def to_num(self):
|
|
786
|
+
"""Return value as number or None."""
|
|
787
|
+
v = self.get()
|
|
788
|
+
if isinstance(v, (int, float)):
|
|
789
|
+
return v
|
|
790
|
+
return None
|
|
791
|
+
|
|
792
|
+
def to_sort(self):
|
|
793
|
+
"""Return value suitable for sorting."""
|
|
794
|
+
v = self.get()
|
|
795
|
+
if isinstance(v, (int, float)):
|
|
796
|
+
return v
|
|
797
|
+
elif isinstance(v, list):
|
|
798
|
+
# If the attribute is a list, convert to comma separated string
|
|
799
|
+
return ', '.join(str(x) for x in v)
|
|
800
|
+
elif v is not None:
|
|
801
|
+
return str(v)
|
|
802
|
+
return None
|
|
803
|
+
|
|
804
|
+
def to_rti(self, query):
|
|
805
|
+
"""Return RichTextIntermediate value or None."""
|
|
806
|
+
# Placeholder - RichTextIntermediate not implemented
|
|
807
|
+
return None
|
|
808
|
+
|
|
809
|
+
def to_tjp(self):
|
|
810
|
+
"""Return the value in TJP file syntax."""
|
|
811
|
+
return f"{self._type.id} {self.get()}"
|
|
812
|
+
|
|
813
|
+
def _quotedString(self, s):
|
|
814
|
+
"""Format string for TJP output."""
|
|
815
|
+
if '\n' in s:
|
|
816
|
+
return f"-8<-\n{s}\n->8-"
|
|
817
|
+
return f'"{s.replace(chr(34), chr(92) + chr(34))}"'
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
class ListAttributeBase(AttributeBase):
|
|
821
|
+
"""Specialized AttributeBase for list values."""
|
|
822
|
+
|
|
823
|
+
def __init__(self, property_node, type_def, container):
|
|
824
|
+
super().__init__(property_node, type_def, container)
|
|
825
|
+
if self._value is None:
|
|
826
|
+
self._value = []
|
|
827
|
+
elif not isinstance(self._value, list):
|
|
828
|
+
self._value = [self._value]
|
|
829
|
+
|
|
830
|
+
def to_s(self, query=None):
|
|
831
|
+
"""Return the value as comma-separated String."""
|
|
832
|
+
return ', '.join(str(x) for x in self.get())
|
|
833
|
+
|
|
834
|
+
def isList(self):
|
|
835
|
+
return True
|
|
836
|
+
|
|
837
|
+
@classmethod
|
|
838
|
+
def isListClass(cls):
|
|
839
|
+
return True
|
|
840
|
+
|
|
841
|
+
def set(self, value):
|
|
842
|
+
"""Set value - for lists, extends the existing list."""
|
|
843
|
+
if AttributeBase._mode == 0:
|
|
844
|
+
self._provided = True
|
|
845
|
+
elif AttributeBase._mode == 1:
|
|
846
|
+
self._inherited = True
|
|
847
|
+
|
|
848
|
+
if not isinstance(self._value, list):
|
|
849
|
+
self._value = []
|
|
850
|
+
if isinstance(value, list):
|
|
851
|
+
self._value.extend(value)
|
|
852
|
+
else:
|
|
853
|
+
self._value.append(value)
|
|
854
|
+
|
|
855
|
+
def __iter__(self):
|
|
856
|
+
return iter(self._value)
|
|
857
|
+
|
|
858
|
+
def __len__(self):
|
|
859
|
+
return len(self._value)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
# Backwards compatibility aliases
|
|
863
|
+
ListAttribute = ListAttributeBase
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
# Attribute Types
|
|
867
|
+
class StringAttribute(AttributeBase):
|
|
868
|
+
pass
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
class IntegerAttribute(AttributeBase):
|
|
872
|
+
pass
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
class FloatAttribute(AttributeBase):
|
|
876
|
+
pass
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
class DateAttribute(AttributeBase):
|
|
880
|
+
pass
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
class BooleanAttribute(AttributeBase):
|
|
884
|
+
pass
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
class ReferenceAttribute(AttributeBase):
|
|
888
|
+
pass
|
|
889
|
+
|
|
890
|
+
# Data Classes (Value Objects)
|
|
891
|
+
class AlertLevelDefinitions:
|
|
892
|
+
def __init__(self):
|
|
893
|
+
pass
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
class Journal:
|
|
897
|
+
def __init__(self):
|
|
898
|
+
pass
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
class LeaveList(list):
|
|
902
|
+
pass
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
class RealFormat:
|
|
906
|
+
def __init__(self, args=None):
|
|
907
|
+
pass
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
class KeywordArray:
|
|
911
|
+
def __init__(self, args=None):
|
|
912
|
+
pass
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
# Specific Attributes
|
|
916
|
+
class ResourceListAttribute(ListAttributeBase):
|
|
917
|
+
pass
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
class ShiftAssignmentsAttribute(AttributeBase):
|
|
921
|
+
pass
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
class TaskDepListAttribute(ListAttributeBase):
|
|
925
|
+
pass
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
class LogicalExpressionListAttribute(ListAttributeBase):
|
|
929
|
+
pass
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
class PropertyAttribute(AttributeBase):
|
|
933
|
+
pass
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
class RichTextAttribute(AttributeBase):
|
|
937
|
+
pass
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
class ColumnListAttribute(ListAttributeBase):
|
|
941
|
+
pass
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
class AccountAttribute(AttributeBase):
|
|
945
|
+
pass
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
class DefinitionListAttribute(ListAttributeBase):
|
|
949
|
+
pass
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
class FlagListAttribute(ListAttributeBase):
|
|
953
|
+
pass
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
class FormatListAttribute(ListAttributeBase):
|
|
957
|
+
pass
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
class LogicalExpressionAttribute(AttributeBase):
|
|
961
|
+
pass
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
class SymbolListAttribute(ListAttributeBase):
|
|
965
|
+
pass
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
class SymbolAttribute(AttributeBase):
|
|
969
|
+
pass
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
class NodeListAttribute(ListAttributeBase):
|
|
973
|
+
pass
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
class ScenarioListAttribute(ListAttributeBase):
|
|
977
|
+
pass
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
class SortListAttribute(ListAttributeBase):
|
|
981
|
+
pass
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
class JournalSortListAttribute(ListAttributeBase):
|
|
985
|
+
pass
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
class RealFormatAttribute(AttributeBase):
|
|
989
|
+
pass
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
class LeaveListAttribute(ListAttributeBase):
|
|
993
|
+
pass
|
|
994
|
+
|
|
995
|
+
class PropertyTreeNode(MessageHandler):
|
|
996
|
+
def __init__(self, property_set, id, name, parent):
|
|
997
|
+
self.propertySet = property_set
|
|
998
|
+
self.project = property_set.project
|
|
999
|
+
self.parent = parent
|
|
1000
|
+
self.data = None
|
|
1001
|
+
|
|
1002
|
+
self._attributes = {}
|
|
1003
|
+
self._scenarioAttributes = []
|
|
1004
|
+
|
|
1005
|
+
scenario_count = self.project.scenarioCount() if hasattr(self.project, 'scenarioCount') else 1
|
|
1006
|
+
self._scenarioAttributes = [{} for _ in range(scenario_count)]
|
|
1007
|
+
|
|
1008
|
+
if id is None:
|
|
1009
|
+
tag = self.__class__.__name__
|
|
1010
|
+
id = f"_{tag}_{self.propertySet.items() + 1}"
|
|
1011
|
+
if not self.propertySet.flat_namespace and parent:
|
|
1012
|
+
id = f"{parent.fullId}.{id}"
|
|
1013
|
+
|
|
1014
|
+
if not self.propertySet.flat_namespace and id and '.' in id:
|
|
1015
|
+
parent_id = id.rsplit('.', 1)[0]
|
|
1016
|
+
if not self.parent:
|
|
1017
|
+
self.parent = self.propertySet[parent_id]
|
|
1018
|
+
self.subId = id.rsplit('.', 1)[1]
|
|
1019
|
+
else:
|
|
1020
|
+
self.subId = id
|
|
1021
|
+
|
|
1022
|
+
self.id = id
|
|
1023
|
+
self.name = name
|
|
1024
|
+
self.sourceFileInfo = None
|
|
1025
|
+
self.sequenceNo = self.propertySet.items() + 1
|
|
1026
|
+
self.children = []
|
|
1027
|
+
self.adoptees = []
|
|
1028
|
+
self.stepParents = []
|
|
1029
|
+
|
|
1030
|
+
self.set('id', self.fullId)
|
|
1031
|
+
self.set('name', name)
|
|
1032
|
+
self.set('seqno', self.sequenceNo)
|
|
1033
|
+
|
|
1034
|
+
if self.parent:
|
|
1035
|
+
self.parent.addChild(self)
|
|
1036
|
+
|
|
1037
|
+
self.propertySet.addProperty(self)
|
|
1038
|
+
|
|
1039
|
+
@property
|
|
1040
|
+
def fullId(self):
|
|
1041
|
+
res = self.subId
|
|
1042
|
+
if not self.propertySet.flat_namespace:
|
|
1043
|
+
t = self
|
|
1044
|
+
while t.parent:
|
|
1045
|
+
t = t.parent
|
|
1046
|
+
res = f"{t.subId}.{res}"
|
|
1047
|
+
return res
|
|
1048
|
+
|
|
1049
|
+
def addChild(self, child):
|
|
1050
|
+
self.children.append(child)
|
|
1051
|
+
|
|
1052
|
+
def ptn(self):
|
|
1053
|
+
return self
|
|
1054
|
+
|
|
1055
|
+
def adopt(self, property_node):
|
|
1056
|
+
if self == property_node:
|
|
1057
|
+
self.error('adopt_self', 'A property cannot adopt itself')
|
|
1058
|
+
|
|
1059
|
+
# Check for duplicates logic... simplified
|
|
1060
|
+
|
|
1061
|
+
self.adoptees.append(property_node)
|
|
1062
|
+
property_node.getAdopted(self)
|
|
1063
|
+
|
|
1064
|
+
def getAdopted(self, property_node):
|
|
1065
|
+
if property_node not in self.stepParents:
|
|
1066
|
+
self.stepParents.append(property_node)
|
|
1067
|
+
|
|
1068
|
+
def parents(self):
|
|
1069
|
+
p = [self.parent] if self.parent else []
|
|
1070
|
+
return p + self.stepParents
|
|
1071
|
+
|
|
1072
|
+
def backupAttributes(self):
|
|
1073
|
+
# Shallow copy of attributes dictionaries
|
|
1074
|
+
return [self._attributes.copy(), [sa.copy() for sa in self._scenarioAttributes]]
|
|
1075
|
+
|
|
1076
|
+
def restoreAttributes(self, backup):
|
|
1077
|
+
self._attributes, self._scenarioAttributes = backup
|
|
1078
|
+
|
|
1079
|
+
def removeReferences(self, property_node):
|
|
1080
|
+
if property_node in self.children: self.children.remove(property_node)
|
|
1081
|
+
if property_node in self.adoptees: self.adoptees.remove(property_node)
|
|
1082
|
+
if property_node in self.stepParents: self.stepParents.remove(property_node)
|
|
1083
|
+
|
|
1084
|
+
def level(self):
|
|
1085
|
+
lvl = 0
|
|
1086
|
+
t = self
|
|
1087
|
+
while t.parent:
|
|
1088
|
+
lvl += 1
|
|
1089
|
+
t = t.parent
|
|
1090
|
+
return lvl
|
|
1091
|
+
|
|
1092
|
+
def getBSIndicies(self):
|
|
1093
|
+
idcs = []
|
|
1094
|
+
p = self
|
|
1095
|
+
while p:
|
|
1096
|
+
parent = p.parent
|
|
1097
|
+
idx = parent.levelSeqNo(p) if parent else self.propertySet.levelSeqNo(p)
|
|
1098
|
+
idcs.insert(0, idx)
|
|
1099
|
+
p = parent
|
|
1100
|
+
return idcs
|
|
1101
|
+
|
|
1102
|
+
def levelSeqNo(self, node):
|
|
1103
|
+
try:
|
|
1104
|
+
return self.children.index(node) + 1
|
|
1105
|
+
except ValueError:
|
|
1106
|
+
raise ValueError(f"Node {node.fullId} is not a child of {self.fullId}")
|
|
1107
|
+
|
|
1108
|
+
def inheritAttributes(self):
|
|
1109
|
+
# Inherit non-scenario-specific values
|
|
1110
|
+
for attrDef in self.propertySet.attributes:
|
|
1111
|
+
if attrDef.scenarioSpecific or not attrDef.inheritedFromParent:
|
|
1112
|
+
continue
|
|
1113
|
+
|
|
1114
|
+
aId = attrDef.id
|
|
1115
|
+
if self.parent:
|
|
1116
|
+
# If parent provided or inherited
|
|
1117
|
+
parent_attr = self.parent._get_attribute(aId)
|
|
1118
|
+
if parent_attr.provided or parent_attr.inherited:
|
|
1119
|
+
my_attr = self._get_attribute(aId)
|
|
1120
|
+
# Only inherit if not already provided explicitly
|
|
1121
|
+
if not my_attr.provided:
|
|
1122
|
+
my_attr.inherit(parent_attr.get())
|
|
1123
|
+
else:
|
|
1124
|
+
if attrDef.inheritedFromProject:
|
|
1125
|
+
# Check project
|
|
1126
|
+
if aId in self.project.attributes:
|
|
1127
|
+
val = self.project[aId]
|
|
1128
|
+
if val is not None:
|
|
1129
|
+
my_attr = self._get_attribute(aId)
|
|
1130
|
+
# Only inherit if not already provided explicitly
|
|
1131
|
+
if not my_attr.provided:
|
|
1132
|
+
my_attr.inherit(val)
|
|
1133
|
+
|
|
1134
|
+
# Inherit scenario-specific values
|
|
1135
|
+
for attrDef in self.propertySet.attributes:
|
|
1136
|
+
if not attrDef.scenarioSpecific or not attrDef.inheritedFromParent:
|
|
1137
|
+
continue
|
|
1138
|
+
|
|
1139
|
+
scenario_count = self.project.scenarioCount() if hasattr(self.project, 'scenarioCount') else 1
|
|
1140
|
+
for scenarioIdx in range(scenario_count):
|
|
1141
|
+
if self.parent:
|
|
1142
|
+
parent_attr = self.parent._get_scenario_attribute(attrDef.id, scenarioIdx)
|
|
1143
|
+
if parent_attr.provided or parent_attr.inherited:
|
|
1144
|
+
my_attr = self._get_scenario_attribute(attrDef.id, scenarioIdx)
|
|
1145
|
+
# Only inherit if not already provided explicitly
|
|
1146
|
+
if not my_attr.provided:
|
|
1147
|
+
my_attr.inherit(parent_attr.get())
|
|
1148
|
+
else:
|
|
1149
|
+
if attrDef.inheritedFromProject:
|
|
1150
|
+
val = self.project[attrDef.id]
|
|
1151
|
+
# Project attributes usually not scenario specific or stored differently?
|
|
1152
|
+
# Ruby: if @project[attrDef.id] && ...
|
|
1153
|
+
# If project has it, inherit.
|
|
1154
|
+
if val is not None:
|
|
1155
|
+
my_attr = self._get_scenario_attribute(attrDef.id, scenarioIdx)
|
|
1156
|
+
# Only inherit if not already provided explicitly
|
|
1157
|
+
if not my_attr.provided:
|
|
1158
|
+
my_attr.inherit(val)
|
|
1159
|
+
|
|
1160
|
+
def ancestors(self, includeStepParents=False):
|
|
1161
|
+
nodes = []
|
|
1162
|
+
if includeStepParents:
|
|
1163
|
+
for p in self.parents():
|
|
1164
|
+
nodes.append(p)
|
|
1165
|
+
nodes.extend(p.ancestors(True))
|
|
1166
|
+
else:
|
|
1167
|
+
n = self
|
|
1168
|
+
while n.parent:
|
|
1169
|
+
n = n.parent
|
|
1170
|
+
nodes.append(n)
|
|
1171
|
+
return nodes
|
|
1172
|
+
|
|
1173
|
+
def root(self):
|
|
1174
|
+
n = self
|
|
1175
|
+
while n.parent:
|
|
1176
|
+
n = n.parent
|
|
1177
|
+
return n
|
|
1178
|
+
|
|
1179
|
+
def provided(self, attributeId, scenarioIdx=None):
|
|
1180
|
+
if scenarioIdx is not None:
|
|
1181
|
+
if attributeId not in self._scenarioAttributes[scenarioIdx]:
|
|
1182
|
+
return False
|
|
1183
|
+
return self._scenarioAttributes[scenarioIdx][attributeId].provided
|
|
1184
|
+
else:
|
|
1185
|
+
if attributeId not in self._attributes:
|
|
1186
|
+
return False
|
|
1187
|
+
return self._attributes[attributeId].provided
|
|
1188
|
+
|
|
1189
|
+
def inherited(self, attributeId, scenarioIdx=None):
|
|
1190
|
+
if scenarioIdx is not None:
|
|
1191
|
+
if attributeId not in self._scenarioAttributes[scenarioIdx]:
|
|
1192
|
+
return False
|
|
1193
|
+
return self._scenarioAttributes[scenarioIdx][attributeId].inherited
|
|
1194
|
+
else:
|
|
1195
|
+
if attributeId not in self._attributes:
|
|
1196
|
+
return False
|
|
1197
|
+
return self._attributes[attributeId].inherited
|
|
1198
|
+
|
|
1199
|
+
def checkFailsAndWarnings(self):
|
|
1200
|
+
# Placeholder for logic
|
|
1201
|
+
pass
|
|
1202
|
+
|
|
1203
|
+
def force(self, attribute_id, value):
|
|
1204
|
+
attr = self._get_attribute(attribute_id)
|
|
1205
|
+
attr.set(value)
|
|
1206
|
+
|
|
1207
|
+
def set(self, attribute_id, value):
|
|
1208
|
+
attr = self._get_attribute(attribute_id)
|
|
1209
|
+
if attr.type.scenarioSpecific:
|
|
1210
|
+
raise ValueError(f"Attribute {attribute_id} is scenario specific, use []=")
|
|
1211
|
+
attr.set(value)
|
|
1212
|
+
|
|
1213
|
+
def _get_attribute(self, attribute_id):
|
|
1214
|
+
if attribute_id in self._attributes:
|
|
1215
|
+
return self._attributes[attribute_id]
|
|
1216
|
+
|
|
1217
|
+
defn = self.attributeDefinition(attribute_id)
|
|
1218
|
+
if not defn:
|
|
1219
|
+
raise ValueError(f"Unknown attribute {attribute_id}")
|
|
1220
|
+
|
|
1221
|
+
if defn.scenarioSpecific:
|
|
1222
|
+
raise ValueError(f"Attribute {attribute_id} is scenario specific")
|
|
1223
|
+
|
|
1224
|
+
attr = defn.objClass(self, defn, self)
|
|
1225
|
+
self._attributes[attribute_id] = attr
|
|
1226
|
+
return attr
|
|
1227
|
+
|
|
1228
|
+
def _get_scenario_attribute(self, attribute_id, scenario_idx):
|
|
1229
|
+
if attribute_id in self._scenarioAttributes[scenario_idx]:
|
|
1230
|
+
return self._scenarioAttributes[scenario_idx][attribute_id]
|
|
1231
|
+
|
|
1232
|
+
defn = self.attributeDefinition(attribute_id)
|
|
1233
|
+
if not defn:
|
|
1234
|
+
raise ValueError(f"Unknown attribute {attribute_id}")
|
|
1235
|
+
|
|
1236
|
+
if not defn.scenarioSpecific:
|
|
1237
|
+
raise ValueError(f"Attribute {attribute_id} is not scenario specific")
|
|
1238
|
+
|
|
1239
|
+
scenario_obj = self.data[scenario_idx] if self.data else None
|
|
1240
|
+
attr = defn.objClass(self, defn, scenario_obj if scenario_obj else self)
|
|
1241
|
+
self._scenarioAttributes[scenario_idx][attribute_id] = attr
|
|
1242
|
+
return attr
|
|
1243
|
+
|
|
1244
|
+
def attributeDefinition(self, attribute_id):
|
|
1245
|
+
return self.propertySet.attributeDefinitions.get(attribute_id)
|
|
1246
|
+
|
|
1247
|
+
def get(self, attribute_id, scenarioIdx=None):
|
|
1248
|
+
if scenarioIdx is not None:
|
|
1249
|
+
attr = self._get_scenario_attribute(attribute_id, scenarioIdx)
|
|
1250
|
+
else:
|
|
1251
|
+
attr = self._get_attribute(attribute_id)
|
|
1252
|
+
return attr.get()
|
|
1253
|
+
|
|
1254
|
+
def __getitem__(self, key):
|
|
1255
|
+
if isinstance(key, tuple):
|
|
1256
|
+
attribute_id, scenario = key
|
|
1257
|
+
attr = self._get_scenario_attribute(attribute_id, scenario)
|
|
1258
|
+
return attr.get()
|
|
1259
|
+
else:
|
|
1260
|
+
return self.get(key)
|
|
1261
|
+
|
|
1262
|
+
def __setitem__(self, key, value):
|
|
1263
|
+
if isinstance(key, tuple):
|
|
1264
|
+
attribute_id, scenario = key
|
|
1265
|
+
attr = self._get_scenario_attribute(attribute_id, scenario)
|
|
1266
|
+
attr.set(value)
|
|
1267
|
+
else:
|
|
1268
|
+
self.set(key, value)
|
|
1269
|
+
|
|
1270
|
+
def all(self):
|
|
1271
|
+
res = [self]
|
|
1272
|
+
for child in self.kids():
|
|
1273
|
+
res.extend(child.all())
|
|
1274
|
+
return res
|
|
1275
|
+
|
|
1276
|
+
def kids(self):
|
|
1277
|
+
return self.children + self.adoptees
|
|
1278
|
+
|
|
1279
|
+
def allLeaves(self, without_self=False):
|
|
1280
|
+
res = []
|
|
1281
|
+
if self.leaf():
|
|
1282
|
+
if not without_self:
|
|
1283
|
+
res.append(self)
|
|
1284
|
+
else:
|
|
1285
|
+
for c in self.kids():
|
|
1286
|
+
res.extend(c.allLeaves())
|
|
1287
|
+
return res
|
|
1288
|
+
|
|
1289
|
+
def leaf(self):
|
|
1290
|
+
return not self.children and not self.adoptees
|