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.
Files changed (49) hide show
  1. scriptplan/__init__.py +22 -0
  2. scriptplan/cli/__init__.py +7 -0
  3. scriptplan/cli/main.py +546 -0
  4. scriptplan/core/__init__.py +0 -0
  5. scriptplan/core/account.py +125 -0
  6. scriptplan/core/allocation.py +69 -0
  7. scriptplan/core/booking.py +39 -0
  8. scriptplan/core/journal.py +377 -0
  9. scriptplan/core/leave.py +14 -0
  10. scriptplan/core/limits.py +354 -0
  11. scriptplan/core/project.py +924 -0
  12. scriptplan/core/property.py +1290 -0
  13. scriptplan/core/resource.py +198 -0
  14. scriptplan/core/resource_scenario.py +711 -0
  15. scriptplan/core/scenario.py +5 -0
  16. scriptplan/core/scenario_data.py +39 -0
  17. scriptplan/core/shift.py +71 -0
  18. scriptplan/core/task.py +77 -0
  19. scriptplan/core/task_scenario.py +1515 -0
  20. scriptplan/core/timesheet.py +457 -0
  21. scriptplan/core/working_hours.py +231 -0
  22. scriptplan/parser/__init__.py +0 -0
  23. scriptplan/parser/macro_processor.py +264 -0
  24. scriptplan/parser/tjp.lark +412 -0
  25. scriptplan/parser/tjp_parser.py +1904 -0
  26. scriptplan/py.typed +0 -0
  27. scriptplan/report/__init__.py +75 -0
  28. scriptplan/report/html_generator.py +477 -0
  29. scriptplan/report/report.py +466 -0
  30. scriptplan/report/report_base.py +397 -0
  31. scriptplan/report/report_context.py +248 -0
  32. scriptplan/report/resource_report.py +341 -0
  33. scriptplan/report/table_report.py +693 -0
  34. scriptplan/report/task_report.py +362 -0
  35. scriptplan/report/text_report.py +172 -0
  36. scriptplan/scheduler/__init__.py +0 -0
  37. scriptplan/scheduler/batch_processor.py +238 -0
  38. scriptplan/scheduler/scoreboard.py +120 -0
  39. scriptplan/utils/__init__.py +0 -0
  40. scriptplan/utils/data_cache.py +46 -0
  41. scriptplan/utils/logger.py +243 -0
  42. scriptplan/utils/message_handler.py +515 -0
  43. scriptplan/utils/time.py +195 -0
  44. scriptplan-0.9.0.dist-info/METADATA +161 -0
  45. scriptplan-0.9.0.dist-info/RECORD +49 -0
  46. scriptplan-0.9.0.dist-info/WHEEL +5 -0
  47. scriptplan-0.9.0.dist-info/entry_points.txt +2 -0
  48. scriptplan-0.9.0.dist-info/licenses/LICENSE +201 -0
  49. 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