Coverage for pyTooling/Diagram/Gantt.py: 99%
176 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 12:33 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 12:33 +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
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)