backlogops 0.1__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.
- backlogops/__init__.py +87 -0
- backlogops/apply_format_rules.py +95 -0
- backlogops/available_teams.py +205 -0
- backlogops/available_teams_config.py +538 -0
- backlogops/available_teams_wizard.py +448 -0
- backlogops/backlog.py +465 -0
- backlogops/backlog_helpers.py +658 -0
- backlogops/backlog_releases.py +299 -0
- backlogops/backlog_releases_io.py +200 -0
- backlogops/console_yes_no_bridge.py +45 -0
- backlogops/date_ranges.py +59 -0
- backlogops/demo_backlog.py +129 -0
- backlogops/estimate_ready_date.py +379 -0
- backlogops/format_rules.py +73 -0
- backlogops/io_config.py +277 -0
- backlogops/key_list_io.py +227 -0
- backlogops/levels.py +165 -0
- backlogops/move_keys_first.py +166 -0
- backlogops/no_text_io.py +100 -0
- backlogops/order_by_dependencies.py +381 -0
- backlogops/person.py +25 -0
- backlogops/py.typed +0 -0
- backlogops/release_backlog_updates.py +239 -0
- backlogops/release_change_io.py +134 -0
- backlogops/releases.py +170 -0
- backlogops/table_create.py +47 -0
- backlogops/table_rows.py +125 -0
- backlogops/team.py +190 -0
- backlogops/work_hours.py +155 -0
- backlogops-0.1.dist-info/METADATA +238 -0
- backlogops-0.1.dist-info/RECORD +34 -0
- backlogops-0.1.dist-info/WHEEL +5 -0
- backlogops-0.1.dist-info/licenses/LICENSE.txt +22 -0
- backlogops-0.1.dist-info/top_level.txt +1 -0
backlogops/backlog.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Internal representation of a backlog."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import date
|
|
9
|
+
from dataclasses import dataclass, field, fields
|
|
10
|
+
from enum import IntEnum, auto
|
|
11
|
+
from typing import Optional, TextIO
|
|
12
|
+
from backlogops.levels import Levels, DEFAULT_LEVELS, level_number_from_name
|
|
13
|
+
from backlogops.backlog_helpers import build_item_kwargs, check_key_syntax
|
|
14
|
+
from backlogops.backlog_helpers import construct, field_type_hints, find_cycle
|
|
15
|
+
from backlogops.backlog_helpers import report_bad_value, check_field_types
|
|
16
|
+
from backlogops.backlog_helpers import report_unknown_reference
|
|
17
|
+
|
|
18
|
+
DEPENDENCY_FIELDS = ('depends_on_f2s', 'depends_on_f2f', 'depends_on_s2s')
|
|
19
|
+
"""Names of the fields that hold dependency keys of a backlog item."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Status(IntEnum):
|
|
23
|
+
"""Status of a backlog item.
|
|
24
|
+
|
|
25
|
+
The meaning of each status is:
|
|
26
|
+
- TODO: The backlog item is not started yet. It will be done
|
|
27
|
+
sometimes in the future. The complete story points on
|
|
28
|
+
the item consumes FTE time.
|
|
29
|
+
- IN_PROGRESS: The backlog item is in progress. We do not know
|
|
30
|
+
how much of it is done, so the complete story points on
|
|
31
|
+
the item consumes FTE time.
|
|
32
|
+
- DONE: The backlog item is finished. No work left to do.
|
|
33
|
+
The story points on the item will not consume any more
|
|
34
|
+
FTE time.
|
|
35
|
+
- REJECTED: The backlog item is rejected. The work will not be done.
|
|
36
|
+
This is only present in the backlog to record the explicit
|
|
37
|
+
decision not to do the work.
|
|
38
|
+
The story points on the item will not consume any more
|
|
39
|
+
FTE time.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
TODO = auto()
|
|
43
|
+
IN_PROGRESS = auto()
|
|
44
|
+
DONE = auto()
|
|
45
|
+
REJECTED = auto()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class BacklogItem: # pylint: disable=too-many-instance-attributes
|
|
50
|
+
"""Internal representation of a backlog item.
|
|
51
|
+
|
|
52
|
+
The backlog item has a number of defined fields that are used
|
|
53
|
+
by the backlog operations. In addition, it has a number of extra
|
|
54
|
+
fields that store useful information (like descriptions) that are
|
|
55
|
+
not used by the backlog operations.
|
|
56
|
+
|
|
57
|
+
Fields:
|
|
58
|
+
key: The key of the backlog item. Required. Must be unique.
|
|
59
|
+
Must not be empty, must not contain whitespace and must
|
|
60
|
+
not contain any of the characters , . ; : ( ) [ ] { }.
|
|
61
|
+
level: The level of the backlog item. Required. Must be an integer.
|
|
62
|
+
title: The title of the backlog item. Required.
|
|
63
|
+
story_points: The story points of the backlog item.
|
|
64
|
+
status: The status of the backlog item.
|
|
65
|
+
parent_key: The key of the parent backlog item. Optional.
|
|
66
|
+
Must exist as a key in the backlog.
|
|
67
|
+
Parent keys are used to build the hierarchy of the backlog.
|
|
68
|
+
The parent key must be at a higher level than the current
|
|
69
|
+
item. Parent keys introduce implicit dependencies between
|
|
70
|
+
items: the current item cannot start before the parent
|
|
71
|
+
item starts, and the parent item cannot finish before
|
|
72
|
+
all its children have finished.
|
|
73
|
+
release: The release of the backlog item. Optional.
|
|
74
|
+
Follows the same character rules as the key.
|
|
75
|
+
Must not be empty string.
|
|
76
|
+
team: The team responsible for the backlog item. Optional.
|
|
77
|
+
Must not be empty string. Must be a valid team name.
|
|
78
|
+
If None the item can be done by any team. If not None.
|
|
79
|
+
the item can only be done by the specified team.
|
|
80
|
+
depends_on_f2s: The list of keys of the backlog items that must
|
|
81
|
+
have been finished before the current item can
|
|
82
|
+
start. May be empty.
|
|
83
|
+
depends_on_f2f: The list of keys of the backlog items that must
|
|
84
|
+
have been finished before the current item can
|
|
85
|
+
finish. May be empty.
|
|
86
|
+
depends_on_s2s: The list of keys of the backlog items that must
|
|
87
|
+
have been started before the current item can
|
|
88
|
+
start. May be empty.
|
|
89
|
+
planned_ready_date: The planned ready date of the backlog item.
|
|
90
|
+
The date that is communicated to the
|
|
91
|
+
customer. Optional.
|
|
92
|
+
estimated_ready_date: The estimated ready date of the backlog
|
|
93
|
+
item. Optional.
|
|
94
|
+
extra_fields: Additional input fields not used by the backlog
|
|
95
|
+
operations, stored by name.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
key: str
|
|
99
|
+
level: int
|
|
100
|
+
title: str
|
|
101
|
+
story_points: int
|
|
102
|
+
status: Status
|
|
103
|
+
parent_key: Optional[str] = None
|
|
104
|
+
release: Optional[str] = None
|
|
105
|
+
team: Optional[str] = None
|
|
106
|
+
depends_on_f2s: list[str] = field(default_factory=list)
|
|
107
|
+
depends_on_f2f: list[str] = field(default_factory=list)
|
|
108
|
+
depends_on_s2s: list[str] = field(default_factory=list)
|
|
109
|
+
planned_ready_date: Optional[date] = None
|
|
110
|
+
estimated_ready_date: Optional[date] = None
|
|
111
|
+
extra_fields: dict[str, object] = field(default_factory=dict)
|
|
112
|
+
|
|
113
|
+
def __getitem__(self, field_name: str) -> object:
|
|
114
|
+
"""Return a mandatory or extra field by name."""
|
|
115
|
+
# pylint: disable-next=no-member
|
|
116
|
+
if field_name in self.__dataclass_fields__:
|
|
117
|
+
return getattr(self, field_name)
|
|
118
|
+
return self.extra_fields[field_name]
|
|
119
|
+
|
|
120
|
+
def __setitem__(self, field_name: str, value: object) -> None:
|
|
121
|
+
"""Set a mandatory or extra field by name."""
|
|
122
|
+
# pylint: disable-next=no-member
|
|
123
|
+
if field_name in self.__dataclass_fields__:
|
|
124
|
+
setattr(self, field_name, value)
|
|
125
|
+
else:
|
|
126
|
+
self.extra_fields[field_name] = value
|
|
127
|
+
|
|
128
|
+
def __contains__(self, field_name: str) -> bool:
|
|
129
|
+
"""Check if a mandatory or extra field exists by name."""
|
|
130
|
+
# pylint: disable-next=no-member
|
|
131
|
+
return field_name in self.__dataclass_fields__ or \
|
|
132
|
+
field_name in self.extra_fields
|
|
133
|
+
|
|
134
|
+
def to_dict(self) -> dict[str, object]:
|
|
135
|
+
"""Return a dictionary representation of the backlog item."""
|
|
136
|
+
result = {}
|
|
137
|
+
# pylint: disable-next=no-member
|
|
138
|
+
for field_name in self.__dataclass_fields__:
|
|
139
|
+
result[field_name] = getattr(self, field_name)
|
|
140
|
+
for field_name, value in self.extra_fields.items():
|
|
141
|
+
result[field_name] = value
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
def _check_field_types(self, stderr_file: TextIO) -> None:
|
|
145
|
+
"""Check that every field holds a value of its declared type."""
|
|
146
|
+
check_field_types(self, stderr_file)
|
|
147
|
+
|
|
148
|
+
def _check_key_constraints(self, stderr_file: TextIO) -> None:
|
|
149
|
+
"""Check the key, release and dependency keys for valid syntax."""
|
|
150
|
+
check_key_syntax('key', self.key, stderr_file)
|
|
151
|
+
if self.release is not None:
|
|
152
|
+
check_key_syntax('release', self.release, stderr_file)
|
|
153
|
+
for dep_field in DEPENDENCY_FIELDS:
|
|
154
|
+
for index, dep_key in enumerate(getattr(self, dep_field)):
|
|
155
|
+
check_key_syntax(f'{dep_field}[{index}]', dep_key, stderr_file)
|
|
156
|
+
|
|
157
|
+
def _check_no_field_shadow(self, stderr_file: TextIO) -> None:
|
|
158
|
+
"""Check that no extra field shadows a named field."""
|
|
159
|
+
# pylint: disable-next=no-member
|
|
160
|
+
named = set(self.__dataclass_fields__)
|
|
161
|
+
for extra_key in self.extra_fields:
|
|
162
|
+
if extra_key in named:
|
|
163
|
+
report_bad_value('extra_fields', extra_key,
|
|
164
|
+
'shadows a named backlog item field',
|
|
165
|
+
stderr_file)
|
|
166
|
+
|
|
167
|
+
def check_consistency(self, stderr_file: TextIO = sys.stderr) -> None:
|
|
168
|
+
"""Check the internal consistency of the backlog item.
|
|
169
|
+
|
|
170
|
+
The documented constraints are checked on all member variables.
|
|
171
|
+
Field types are verified, the key, release and dependency keys
|
|
172
|
+
are checked for valid syntax, and the extra fields are checked
|
|
173
|
+
not to shadow a named field. References between items are not
|
|
174
|
+
checked here; that is done by :func:`check_backlog_consistency`.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
stderr_file: The file to report errors to.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
TypeError: If a field has the wrong type.
|
|
181
|
+
ValueError: If a field value violates a constraint, or if an
|
|
182
|
+
extra field shadows a named field.
|
|
183
|
+
"""
|
|
184
|
+
self._check_field_types(stderr_file)
|
|
185
|
+
self._check_key_constraints(stderr_file)
|
|
186
|
+
self._check_no_field_shadow(stderr_file)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
type Backlog = list[BacklogItem]
|
|
190
|
+
"""Internal representation of a backlog."""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def prepare_item_data(data: dict[str, object], levels: Levels,
|
|
194
|
+
stderr_file: TextIO = sys.stderr) -> dict[str, object]:
|
|
195
|
+
"""Return item data with a string level resolved to its number.
|
|
196
|
+
|
|
197
|
+
A ``level`` given as a string is matched against the level names and
|
|
198
|
+
aliases in ``levels`` and replaced by the level number. Integer
|
|
199
|
+
levels and absent levels are returned unchanged, so that type and
|
|
200
|
+
missing-field checks happen as usual when the data is converted.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
data: The raw input data for one backlog item.
|
|
204
|
+
levels: The levels used to resolve a string level.
|
|
205
|
+
stderr_file: The file to report errors to.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The input data, with a string level replaced by its number.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
ValueError: If a string level matches no level name or alias.
|
|
212
|
+
"""
|
|
213
|
+
level_value = data.get('level')
|
|
214
|
+
if not isinstance(level_value, str):
|
|
215
|
+
return data
|
|
216
|
+
number = level_number_from_name(level_value, levels, stderr_file)
|
|
217
|
+
return {**data, 'level': number}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_backlog_item(data: dict[str, object], levels: Optional[Levels] = None,
|
|
221
|
+
stderr_file: TextIO = sys.stderr) -> BacklogItem:
|
|
222
|
+
"""Get a backlog item from a dictionary.
|
|
223
|
+
|
|
224
|
+
The dictionary is expected to hold the mandatory fields of the
|
|
225
|
+
BacklogItem dataclass and any number of extra fields. Field values
|
|
226
|
+
are converted to their declared types (for example ISO date strings
|
|
227
|
+
to ``date`` and status names to ``Status``) and checked. A ``level``
|
|
228
|
+
given as a string is resolved to its level number using ``levels``.
|
|
229
|
+
When ``levels`` is None the default levels are used. Errors are
|
|
230
|
+
reported to the given file object.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
data: The dictionary to get the backlog item from.
|
|
234
|
+
levels: The levels used to resolve a string level, or None to
|
|
235
|
+
use :data:`DEFAULT_LEVELS`.
|
|
236
|
+
stderr_file: The file to report errors to.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
KeyError: If a mandatory field is missing.
|
|
240
|
+
TypeError: If a field has a type that cannot be converted.
|
|
241
|
+
ValueError: If a string level matches no level name or alias.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The backlog item.
|
|
245
|
+
"""
|
|
246
|
+
chosen_levels = DEFAULT_LEVELS if levels is None else levels
|
|
247
|
+
field_types = field_type_hints(BacklogItem)
|
|
248
|
+
prepared = prepare_item_data(data, chosen_levels, stderr_file)
|
|
249
|
+
item_kwargs = build_item_kwargs(fields(BacklogItem), field_types, prepared,
|
|
250
|
+
stderr_file)
|
|
251
|
+
return construct(BacklogItem, item_kwargs)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_backlog(datalist: list[dict[str, object]],
|
|
255
|
+
levels: Optional[Levels] = None,
|
|
256
|
+
stderr_file: TextIO = sys.stderr) -> Backlog:
|
|
257
|
+
"""Get a backlog from a list of dictionaries.
|
|
258
|
+
|
|
259
|
+
Each dictionary is converted to a backlog item as documented for
|
|
260
|
+
:func:`get_backlog_item`.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
datalist: The list of dictionaries to get the backlog from.
|
|
264
|
+
levels: The levels used to convert level names to level numbers,
|
|
265
|
+
or None to use :data:`DEFAULT_LEVELS`.
|
|
266
|
+
stderr_file: The file to report errors to.
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
KeyError: If a mandatory field is missing.
|
|
270
|
+
TypeError: If a field has a type that cannot be converted.
|
|
271
|
+
ValueError: If a string level matches no level name or alias.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
The backlog.
|
|
275
|
+
"""
|
|
276
|
+
return [get_backlog_item(data, levels, stderr_file) for data in datalist]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def check_unique_keys(backlog: Backlog,
|
|
280
|
+
stderr_file: TextIO = sys.stderr) -> set[str]:
|
|
281
|
+
"""Check that all backlog item keys are unique.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
backlog: The backlog to check.
|
|
285
|
+
stderr_file: The file to report errors to.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
The set of all keys, for reuse by later checks.
|
|
289
|
+
|
|
290
|
+
Raises:
|
|
291
|
+
ValueError: If two items share the same key.
|
|
292
|
+
"""
|
|
293
|
+
keys: set[str] = set()
|
|
294
|
+
for item in backlog:
|
|
295
|
+
if item.key in keys:
|
|
296
|
+
report_bad_value('key', item.key, 'duplicate backlog item key',
|
|
297
|
+
stderr_file)
|
|
298
|
+
keys.add(item.key)
|
|
299
|
+
return keys
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def check_key_references(backlog: Backlog, known_keys: set[str],
|
|
303
|
+
stderr_file: TextIO = sys.stderr) -> None:
|
|
304
|
+
"""Check that parent and dependency keys reference existing items.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
backlog: The backlog to check.
|
|
308
|
+
known_keys: The set of keys that exist in the backlog.
|
|
309
|
+
stderr_file: The file to report errors to.
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
KeyError: If a parent_key or dependency key is unknown.
|
|
313
|
+
"""
|
|
314
|
+
for item in backlog:
|
|
315
|
+
if item.parent_key is not None and \
|
|
316
|
+
item.parent_key not in known_keys:
|
|
317
|
+
report_unknown_reference('parent_key', item.key, item.parent_key,
|
|
318
|
+
stderr_file)
|
|
319
|
+
for dep_field in DEPENDENCY_FIELDS:
|
|
320
|
+
for dep_key in getattr(item, dep_field):
|
|
321
|
+
if dep_key not in known_keys:
|
|
322
|
+
report_unknown_reference(dep_field, item.key, dep_key,
|
|
323
|
+
stderr_file)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def event_start(key: str) -> str:
|
|
327
|
+
"""Return the start-event node name for a backlog item key."""
|
|
328
|
+
return f'{key}:start'
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def event_finish(key: str) -> str:
|
|
332
|
+
"""Return the finish-event node name for a backlog item key."""
|
|
333
|
+
return f'{key}:finish'
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def item_dependency_edges(item: BacklogItem) -> list[tuple[str, str]]:
|
|
337
|
+
"""Return the directed scheduling edges implied by one item.
|
|
338
|
+
|
|
339
|
+
Each item has a start event and a finish event. An edge ``a -> b``
|
|
340
|
+
means that event ``a`` cannot happen before event ``b`` (``a``
|
|
341
|
+
depends on ``b``). An item finish depends on its own start. The
|
|
342
|
+
dependency lists add finish-to-start, finish-to-finish and
|
|
343
|
+
start-to-start edges. A parent relation adds two implicit edges: the
|
|
344
|
+
child cannot start before the parent starts, and the parent cannot
|
|
345
|
+
finish before the child finishes.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
item: The backlog item to take the edges from.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
The directed (source, target) event edges of the item.
|
|
352
|
+
"""
|
|
353
|
+
start = event_start(item.key)
|
|
354
|
+
finish = event_finish(item.key)
|
|
355
|
+
edges = [(finish, start)]
|
|
356
|
+
edges += [(start, event_finish(dep)) for dep in item.depends_on_f2s]
|
|
357
|
+
edges += [(finish, event_finish(dep)) for dep in item.depends_on_f2f]
|
|
358
|
+
edges += [(start, event_start(dep)) for dep in item.depends_on_s2s]
|
|
359
|
+
if item.parent_key is not None:
|
|
360
|
+
edges.append((start, event_start(item.parent_key)))
|
|
361
|
+
edges.append((event_finish(item.parent_key), finish))
|
|
362
|
+
return edges
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def build_dependency_graph(backlog: Backlog) -> dict[str, list[str]]:
|
|
366
|
+
"""Return the scheduling-event dependency graph of a backlog.
|
|
367
|
+
|
|
368
|
+
The nodes are the start and finish events of the items, named
|
|
369
|
+
``'<key>:start'`` and ``'<key>:finish'`` (``:`` cannot appear in a
|
|
370
|
+
key). The edges combine the explicit dependency lists with the
|
|
371
|
+
implicit parent relations, as described in
|
|
372
|
+
:func:`item_dependency_edges`. A cycle in this graph is an
|
|
373
|
+
unsatisfiable set of scheduling constraints.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
backlog: The backlog to build the graph from.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
A mapping from each event node to the events it depends on.
|
|
380
|
+
"""
|
|
381
|
+
graph: dict[str, list[str]] = {}
|
|
382
|
+
for item in backlog:
|
|
383
|
+
for source, target in item_dependency_edges(item):
|
|
384
|
+
graph.setdefault(source, []).append(target)
|
|
385
|
+
return graph
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def check_no_cycles(backlog: Backlog,
|
|
389
|
+
stderr_file: TextIO = sys.stderr) -> None:
|
|
390
|
+
"""Check that the scheduling-event graph of a backlog has no cycles.
|
|
391
|
+
|
|
392
|
+
The graph combines the explicit dependencies with the implicit
|
|
393
|
+
parent relations. A self dependency is treated as a cycle of length
|
|
394
|
+
one. A valid parent and child nesting is not a cycle, because the
|
|
395
|
+
parent and child start and finish events stay distinct.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
backlog: The backlog to check.
|
|
399
|
+
stderr_file: The file to report errors to.
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
ValueError: If a dependency cycle is found.
|
|
403
|
+
"""
|
|
404
|
+
cycle = find_cycle(build_dependency_graph(backlog))
|
|
405
|
+
if cycle is not None:
|
|
406
|
+
message = 'Dependency cycle in backlog: ' + ' -> '.join(cycle)
|
|
407
|
+
print(message, file=stderr_file)
|
|
408
|
+
raise ValueError(message)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def check_parent_levels(backlog: Backlog, items_by_key: dict[str, BacklogItem],
|
|
412
|
+
stderr_file: TextIO = sys.stderr) -> None:
|
|
413
|
+
"""Check that each parent is at a higher level than its child.
|
|
414
|
+
|
|
415
|
+
A parent is a bigger backlog item than its children, so its level
|
|
416
|
+
number must be strictly higher than the item that references it. The
|
|
417
|
+
parent references are assumed to exist, as already checked by
|
|
418
|
+
:func:`check_key_references`.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
backlog: The backlog to check.
|
|
422
|
+
items_by_key: A mapping from each key to its backlog item.
|
|
423
|
+
stderr_file: The file to report errors to.
|
|
424
|
+
|
|
425
|
+
Raises:
|
|
426
|
+
ValueError: If a parent is not at a higher level than its child.
|
|
427
|
+
"""
|
|
428
|
+
for item in backlog:
|
|
429
|
+
if item.parent_key is None:
|
|
430
|
+
continue
|
|
431
|
+
parent = items_by_key[item.parent_key]
|
|
432
|
+
if parent.level <= item.level:
|
|
433
|
+
report_bad_value('parent_key', item.parent_key,
|
|
434
|
+
f'parent level {parent.level} is not higher '
|
|
435
|
+
f'than item level {item.level}', stderr_file)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def check_backlog_consistency(backlog: Backlog,
|
|
439
|
+
stderr_file: TextIO = sys.stderr) -> None:
|
|
440
|
+
"""Check the consistency of a backlog.
|
|
441
|
+
|
|
442
|
+
Every item is checked for internal consistency, all keys are checked
|
|
443
|
+
for uniqueness, parent and dependency keys are checked to reference
|
|
444
|
+
existing items, each parent is checked to be at a higher level than
|
|
445
|
+
its child, and the scheduling-event graph is checked to be free of
|
|
446
|
+
cycles.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
backlog: The backlog to check.
|
|
450
|
+
stderr_file: The file to report errors to.
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
KeyError: If a key reference is invalid.
|
|
454
|
+
TypeError: If a field has the wrong type.
|
|
455
|
+
ValueError: If a field value violates a constraint, if keys are
|
|
456
|
+
not unique, if a parent is not at a higher level than its
|
|
457
|
+
child, or if there is a dependency cycle.
|
|
458
|
+
"""
|
|
459
|
+
for item in backlog:
|
|
460
|
+
item.check_consistency(stderr_file)
|
|
461
|
+
known_keys = check_unique_keys(backlog, stderr_file)
|
|
462
|
+
items_by_key = {item.key: item for item in backlog}
|
|
463
|
+
check_key_references(backlog, known_keys, stderr_file)
|
|
464
|
+
check_parent_levels(backlog, items_by_key, stderr_file)
|
|
465
|
+
check_no_cycles(backlog, stderr_file)
|