Coverage for pyTooling/Diagram/Gantt.py: 99%
176 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-03 23:02 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-03 23:02 +0000
1# ==================================================================================================================== #
2# _____ _ _ ____ _ ____ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \(_) __ _ __ _ _ __ __ _ _ __ ___ / ___| __ _ _ __ | |_| |_ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | | |/ _` |/ _` | '__/ _` | '_ ` _ \ | | _ / _` | '_ \| __| __| #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | | (_| | (_| | | | (_| | | | | | || |_| | (_| | | | | |_| |_ #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/|_|\__,_|\__, |_| \__,_|_| |_| |_(_)____|\__,_|_| |_|\__|\__| #
7# |_| |___/ |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-2026 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32A Gantt chart: rows of bars on a time scale.
34.. code-block:: python
36 from datetime import datetime, timedelta
37 from pyTooling.Diagram.Gantt import Diagram, Row, Bar
39 begin = datetime(2026, 9, 15, 8, 0)
40 diagram = Diagram("Nightly build", begin)
42 row = Row("Compile", parent=diagram)
43 Bar(begin, begin + timedelta(minutes=4), parent=row)
45 print(f"{row.Name}: {row.DurationInSeconds} s, beginning {row.BeginSinceOrigin} after the diagram's origin")
47Every element knows the :class:`Diagram` it belongs to and the element containing it, so an offset is answered
48without a search: a bar reports its begin as a time, as the distance from the diagram's **origin**, and as the
49distance from the **row** it sits in.
51The classes describe a chart; they don't draw one. A producer of data derives from them and adds what its domain
52knows - :class:`pyTooling.Tracing.Render.GanttLayout` builds a diagram from a software execution trace - and a
53renderer draws what any of them describe.
55.. seealso::
57 :mod:`pyTooling.Tracing.Render`
58 |rarr| A software execution trace as a Gantt chart, and the renderers drawing it.
59"""
60from __future__ import annotations
62from datetime import datetime, timedelta
63from typing import Iterator, Optional as Nullable
65from pyTooling.Decorators import export, readonly
66from pyTooling.MetaClasses import ExtendedType
67from pyTooling.Common import getFullyQualifiedName
68from pyTooling.Diagram import DiagramError
71@export
72class Bar(metaclass=ExtendedType, slots=True):
73 """
74 A bar of a Gantt chart: a time range within one :class:`Row`.
76 A bar reports its position three ways - as the times themselves (:attr:`Begin`, :attr:`End`), as the distance
77 from the diagram's origin (:attr:`BeginSinceOrigin`), and as the distance from the row it sits in
78 (:attr:`BeginSinceParent`) - because a chart is drawn on the second and a report usually wants one of the others.
79 """
80 _parent: Row #: The row this bar sits in.
81 _diagram: Diagram #: The diagram this bar belongs to.
82 _begin: datetime #: Begin of the bar.
83 _end: datetime #: End of the bar.
85 def __init__(self, begin: datetime, end: datetime, *, parent: Row) -> None:
86 """
87 Initializes a bar and appends it to its row.
89 :param begin: Begin of the bar.
90 :param end: End of the bar, which may equal the begin but must not precede it.
91 :param parent: The row the bar sits in.
92 :raises ValueError: If parameter 'begin', 'end' or 'parent' is None.
93 :raises TypeError: If parameter 'begin' or 'end' is not of type :class:`~datetime.datetime`.
94 :raises TypeError: If parameter 'parent' is not of type :class:`Row`.
95 :raises ValueError: If the end precedes the begin.
96 """
97 for parameterName, parameter in (("begin", begin), ("end", end)):
98 if parameter is None:
99 raise ValueError(f"Parameter '{parameterName}' is None.")
100 elif not isinstance(parameter, datetime):
101 ex = TypeError(f"Parameter '{parameterName}' is not of type 'datetime'.")
102 ex.add_note(f"Got type '{getFullyQualifiedName(parameter)}'.")
103 raise ex
105 if parent is None: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 raise ValueError("Parameter 'parent' is None.")
107 elif not isinstance(parent, Row):
108 ex = TypeError("Parameter 'parent' is not of type 'Row'.")
109 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
110 raise ex
112 if end < begin:
113 ex = ValueError("A bar's end precedes its begin.")
114 ex.add_note(f"Got begin '{begin}' and end '{end}'.")
115 raise ex
117 self._parent = parent
118 self._diagram = parent.Diagram
119 self._begin = begin
120 self._end = end
121 parent._bars.append(self)
123 @readonly
124 def Parent(self) -> Row:
125 """
126 Read-only property to access the row this bar sits in (:attr:`_parent`).
128 :returns: The row.
129 """
130 return self._parent
132 @readonly
133 def Diagram(self) -> Diagram:
134 """
135 Read-only property to access the diagram this bar belongs to (:attr:`_diagram`).
137 :returns: The diagram.
138 """
139 return self._diagram
141 @readonly
142 def Begin(self) -> datetime:
143 """
144 Read-only property to access the begin of the bar (:attr:`_begin`).
146 :returns: The time the bar begins.
147 """
148 return self._begin
150 @readonly
151 def End(self) -> datetime:
152 """
153 Read-only property to access the end of the bar (:attr:`_end`).
155 :returns: The time the bar ends.
156 """
157 return self._end
159 @readonly
160 def Duration(self) -> timedelta:
161 """
162 Read-only property to return the length of the bar.
164 :returns: The length from the bar's begin to its end.
165 """
166 return self._end - self._begin
168 @readonly
169 def DurationInSeconds(self) -> float:
170 """
171 Read-only property to return the length of the bar as a number.
173 :returns: The length in seconds.
174 """
175 return (self._end - self._begin).total_seconds()
177 @readonly
178 def BeginSinceOrigin(self) -> timedelta:
179 """
180 Read-only property to return how long after the diagram's origin the bar begins.
182 :returns: The distance from the origin to the bar's begin.
183 """
184 return self._begin - self._diagram.Origin
186 @readonly
187 def EndSinceOrigin(self) -> timedelta:
188 """
189 Read-only property to return how long after the diagram's origin the bar ends.
191 :returns: The distance from the origin to the bar's end.
192 """
193 return self._end - self._diagram.Origin
195 @readonly
196 def BeginSinceOriginInSeconds(self) -> float:
197 """
198 Read-only property to return how long after the diagram's origin the bar begins, as a number.
200 :returns: The distance from the origin to the bar's begin, in seconds.
201 """
202 return (self._begin - self._diagram.Origin).total_seconds()
204 @readonly
205 def EndSinceOriginInSeconds(self) -> float:
206 """
207 Read-only property to return how long after the diagram's origin the bar ends, as a number.
209 :returns: The distance from the origin to the bar's end, in seconds.
210 """
211 return (self._end - self._diagram.Origin).total_seconds()
213 @readonly
214 def BeginSinceParent(self) -> timedelta:
215 """
216 Read-only property to return how long after its row the bar begins.
218 :returns: The distance from the row's begin to the bar's begin, which is zero for the row's earliest bar.
219 """
220 return self._begin - self._parent.Begin
222 @readonly
223 def EndSinceParent(self) -> timedelta:
224 """
225 Read-only property to return how long after its row's begin the bar ends.
227 :returns: The distance from the row's begin to the bar's end.
228 """
229 return self._end - self._parent.Begin
232@export
233class Row(metaclass=ExtendedType, slots=True):
234 """
235 A row of a Gantt chart: the bars drawn on one line, under one name.
237 A row spans its bars: it begins with its earliest bar and ends with its latest, whether or not they touch.
238 """
239 _parent: Diagram #: The diagram this row belongs to.
240 _name: str #: Name of the row, which labels it in a chart.
241 _bars: list[Bar] #: The bars of this row, in the order they were added.
243 def __init__(self, name: str, *, parent: Diagram) -> None:
244 """
245 Initializes a row without bars and appends it to its diagram.
247 :param name: Name of the row.
248 :param parent: The diagram the row belongs to.
249 :raises ValueError: If parameter 'name' or 'parent' is None.
250 :raises TypeError: If parameter 'name' is not of type :class:`str`.
251 :raises TypeError: If parameter 'parent' is not of type :class:`Diagram`.
252 """
253 if name is None:
254 raise ValueError("Parameter 'name' is None.")
255 elif not isinstance(name, str):
256 ex = TypeError("Parameter 'name' is not of type 'str'.")
257 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
258 raise ex
260 if parent is None:
261 raise ValueError("Parameter 'parent' is None.")
262 elif not isinstance(parent, Diagram):
263 ex = TypeError("Parameter 'parent' is not of type 'Diagram'.")
264 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
265 raise ex
267 self._parent = parent
268 self._name = name
269 self._bars = []
270 parent._rows.append(self)
272 @readonly
273 def Parent(self) -> Diagram:
274 """
275 Read-only property to access the diagram this row belongs to (:attr:`_parent`).
277 :returns: The diagram.
278 """
279 return self._parent
281 @readonly
282 def Diagram(self) -> Diagram:
283 """
284 Read-only property to access the diagram this row belongs to (:attr:`_parent`), which is what contains it.
286 :returns: The diagram.
287 """
288 return self._parent
290 @readonly
291 def Name(self) -> str:
292 """
293 Read-only property to access the name of the row (:attr:`_name`).
295 :returns: The name.
296 """
297 return self._name
299 @readonly
300 def Bars(self) -> tuple[Bar, ...]:
301 """
302 Read-only property to return the bars of this row (:attr:`_bars`).
304 :returns: The bars, in the order they were added.
305 """
306 return tuple(self._bars)
308 @readonly
309 def Begin(self) -> datetime:
310 """
311 Read-only property to return the begin of the row's earliest bar.
313 :returns: The time the row begins.
314 :raises DiagramError: If the row has no bars.
315 """
316 if len(self._bars) == 0:
317 raise DiagramError(f"Row '{self._name}' has no bars, so it has no begin.")
319 return min(bar.Begin for bar in self._bars)
321 @readonly
322 def End(self) -> datetime:
323 """
324 Read-only property to return the end of the row's latest bar.
326 :returns: The time the row ends.
327 :raises DiagramError: If the row has no bars.
328 """
329 if len(self._bars) == 0:
330 raise DiagramError(f"Row '{self._name}' has no bars, so it has no end.")
332 return max(bar.End for bar in self._bars)
334 @readonly
335 def Duration(self) -> timedelta:
336 """
337 Read-only property to return the length of the row.
339 :returns: The length from the row's begin to its end, gaps between its bars included.
340 :raises DiagramError: If the row has no bars.
341 """
342 return self.End - self.Begin
344 @readonly
345 def DurationInSeconds(self) -> float:
346 """
347 Read-only property to return the length of the row as a number.
349 :returns: The length in seconds, gaps between its bars included.
350 :raises DiagramError: If the row has no bars.
351 """
352 return (self.End - self.Begin).total_seconds()
354 @readonly
355 def BeginSinceOrigin(self) -> timedelta:
356 """
357 Read-only property to return how long after the diagram's origin the row begins.
359 :returns: The distance from the origin to the row's begin.
360 :raises DiagramError: If the row has no bars.
361 """
362 return self.Begin - self._parent.Origin
364 @readonly
365 def EndSinceOrigin(self) -> timedelta:
366 """
367 Read-only property to return how long after the diagram's origin the row ends.
369 :returns: The distance from the origin to the row's end.
370 :raises DiagramError: If the row has no bars.
371 """
372 return self.End - self._parent.Origin
374 @readonly
375 def BarCount(self) -> int:
376 """
377 Read-only property to return the number of bars.
379 :returns: Number of bars.
380 """
381 return len(self._bars)
383 def IterateBars(self) -> Iterator[Bar]:
384 """
385 Returns an iterator to iterate the bars of this row.
387 :returns: Iterator to iterate all bars, in the order they were added.
388 """
389 return iter(self._bars)
391 def __len__(self) -> int:
392 """
393 Returns the number of bars in this row.
395 :returns: Number of bars.
396 """
397 return len(self._bars)
399 def __iter__(self) -> Iterator[Bar]:
400 """
401 Returns an iterator to iterate the bars of this row.
403 :returns: Iterator to iterate all bars, in the order they were added.
404 """
405 return iter(self._bars)
408@export
409class Diagram(metaclass=ExtendedType, slots=True):
410 """
411 A Gantt chart: rows of bars, and the origin their offsets are counted from.
413 The origin is stated rather than derived, because the scale a chart is drawn on usually begins before its first
414 bar - a pipeline starts before its first job does.
415 """
416 _title: str #: Title of the diagram.
417 _origin: datetime #: The time offsets are counted from.
418 _rows: list[Row] #: The rows of this diagram, in the order they were added.
420 def __init__(self, title: str, origin: datetime) -> None:
421 """
422 Initializes a diagram without rows.
424 :param title: Title of the diagram.
425 :param origin: The time offsets are counted from.
426 :raises ValueError: If parameter 'title' or 'origin' is None.
427 :raises TypeError: If parameter 'title' is not of type :class:`str`.
428 :raises TypeError: If parameter 'origin' is not of type :class:`~datetime.datetime`.
429 """
430 if title is None:
431 raise ValueError("Parameter 'title' is None.")
432 elif not isinstance(title, str):
433 ex = TypeError("Parameter 'title' is not of type 'str'.")
434 ex.add_note(f"Got type '{getFullyQualifiedName(title)}'.")
435 raise ex
437 if origin is None:
438 raise ValueError("Parameter 'origin' is None.")
439 elif not isinstance(origin, datetime):
440 ex = TypeError("Parameter 'origin' is not of type 'datetime'.")
441 ex.add_note(f"Got type '{getFullyQualifiedName(origin)}'.")
442 raise ex
444 self._title = title
445 self._origin = origin
446 self._rows = []
448 @readonly
449 def Title(self) -> str:
450 """
451 Read-only property to access the title of the diagram (:attr:`_title`).
453 :returns: The title.
454 """
455 return self._title
457 @readonly
458 def Origin(self) -> datetime:
459 """
460 Read-only property to access the time offsets are counted from (:attr:`_origin`).
462 :returns: The origin.
463 """
464 return self._origin
466 @readonly
467 def Rows(self) -> tuple[Row, ...]:
468 """
469 Read-only property to return the rows of this diagram (:attr:`_rows`).
471 :returns: The rows, in the order they were added.
472 """
473 return tuple(self._rows)
475 @readonly
476 def RowCount(self) -> int:
477 """
478 Read-only property to return the number of rows.
480 :returns: Number of rows.
481 """
482 return len(self._rows)
484 def IterateRows(self) -> Iterator[Row]:
485 """
486 Returns an iterator to iterate the rows of this diagram.
488 :returns: Iterator to iterate all rows, in the order they were added.
489 """
490 return iter(self._rows)
492 def __len__(self) -> int:
493 """
494 Returns the number of rows in this diagram.
496 :returns: Number of rows.
497 """
498 return len(self._rows)
500 def __iter__(self) -> Iterator[Row]:
501 """
502 Returns an iterator to iterate the rows of this diagram.
504 :returns: Iterator to iterate all rows, in the order they were added.
505 """
506 return iter(self._rows)