Coverage for pyEDAA/Reports/RiskManagement/__init__.py: 100%

642 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-21 23:49 +0000

1# ==================================================================================================================== # 

2# _____ ____ _ _ ____ _ # 

3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # 

4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # 

5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # 

6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # 

7# |_| |___/ |_| # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²) # 

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""" 

32Abstract data model of risk management: a risk register of risks, their implications and the mitigations against 

33them. 

34 

35The root, :class:`RiskManagement`, holds every :class:`Risk`, :class:`Implication` and :class:`Mitigation` by ID. A risk 

36references its implications - what happens, if it occurs - and its mitigations - what reduces it. Implications and 

37mitigations are elements of their own, so several risks can share one, e.g. a mitigation reducing two risks. 

38 

39The implications form a chain: an implication can have implications of its own, and a mitigation can have 

40implications too: its side effects. Both are the second-order implications of a risk. An implication can be mitigated 

41like a risk. 

42 

43Risks of a register relate to each other by a :class:`Link`: from a source risk to a destination risk, of a 

44:class:`LinkKind`, e.g. one risk causing another. A link has no parent: its risks and the register list it. 

45 

46A risk's :attr:`~Risk.Score` - the rank of its probability times the rank of its impact - classifies it by a 

47:class:`RiskLevel`. A risk names its owner - a :class:`~pyEDAA.Reports.Person` the register refers to - and the date of 

48its next review; a mitigation states its :class:`MitigationKind` and its :class:`MitigationState`. 

49 

50Every element has key-value pairs (:class:`~pyTooling.DataModel.KeyValuePairsMixin`) for data the model doesn't name. 

51""" 

52from __future__ import annotations 

53 

54from datetime import datetime 

55from enum import Enum 

56from typing import Any, Callable, ClassVar, Generator, Hashable, Iterable, Mapping, Optional as Nullable 

57 

58from pyTooling.Common import getFullyQualifiedName 

59from pyTooling.DataModel import KeyValuePairsMixin 

60from pyTooling.Decorators import export, readonly 

61from pyTooling.Exceptions import AlreadyInHierarchyError, DuplicateElementError, NotInSameHierarchyError 

62from pyTooling.MetaClasses import ExtendedType 

63from pyTooling.Versioning import SemanticVersion 

64 

65from pyEDAA.Reports import Person, ReportException 

66 

67 

68@export 

69class RiskManagementError(ReportException): 

70 """Base-exception of the risk management model.""" 

71 

72 

73@export 

74class NotInSameRiskRegisterError(RiskManagementError, NotInSameHierarchyError): 

75 """ 

76 The exception is raised when an element is used together with an element or a person of another risk register. 

77 

78 The element or person itself is valid, only its ownership is wrong: add it to the risk register first. E.g. a risk 

79 referencing a mitigation, which isn't part of the risk's register. 

80 """ 

81 

82 

83@export 

84class AlreadyInRiskRegisterError(RiskManagementError, AlreadyInHierarchyError): 

85 """The exception is raised when an element is added to a risk register, but it's already part of a risk register.""" 

86 

87 

88@export 

89class DuplicateRiskError(RiskManagementError, DuplicateElementError): 

90 """The exception is raised when a risk register already contains a risk of the same ID.""" 

91 

92 

93@export 

94class DuplicateImplicationError(RiskManagementError, DuplicateElementError): 

95 """The exception is raised when a risk register or an element already contains an implication of the same ID.""" 

96 

97 

98@export 

99class DuplicateMitigationError(RiskManagementError, DuplicateElementError): 

100 """The exception is raised when a risk register or an element already contains a mitigation of the same ID.""" 

101 

102 

103@export 

104class DuplicatePersonError(RiskManagementError, DuplicateElementError): 

105 """The exception is raised when a risk register already contains a person of the same user ID.""" 

106 

107 

108@export 

109class DuplicateLinkError(RiskManagementError, DuplicateElementError): 

110 """The exception is raised when a risk already has a link of the same kind to the same destination.""" 

111 

112 

113@export 

114class Probability(Enum): 

115 """ 

116 The probability of a risk to occur. 

117 """ 

118 

119 Low = 1 #: Identified, but sleeping. 

120 Medium = 2 #: Likely to occur soon. 

121 High = 3 #: Very likely to occur. 

122 Occurred = 4 #: Occurred: the risk is active and causes heavy effects. 

123 

124 __RANKS: ClassVar[dict[int, int]] = { 

125 Low: 1, 

126 Medium: 2, 

127 High: 3, 

128 Occurred: 4, 

129 } #: The rank of each probability, by the probability's value. 

130 

131 @readonly 

132 def Rank(self) -> int: 

133 """ 

134 Read-only property to return the probability's rank on its ordinal scale. 

135 

136 :returns: The rank in range 1..4; ``1`` is the lowest probability. 

137 """ 

138 return self.__RANKS[self.value] 

139 

140 

141@export 

142class Impact(Enum): 

143 """ 

144 The impact of a risk, if it occurs, as the delay it causes. 

145 """ 

146 

147 Low = 1 #: A delay of a few days. 

148 Medium = 2 #: A delay of multiple days. 

149 High = 3 #: A delay of a few weeks. 

150 Critical = 4 #: A delay of multiple weeks. 

151 

152 __RANKS: ClassVar[dict[int, int]] = { 

153 Low: 1, 

154 Medium: 2, 

155 High: 3, 

156 Critical: 4, 

157 } #: The rank of each impact, by the impact's value. 

158 

159 @readonly 

160 def Rank(self) -> int: 

161 """ 

162 Read-only property to return the impact's rank on its ordinal scale. 

163 

164 :returns: The rank in range 1..4; ``1`` is the lowest impact. 

165 """ 

166 return self.__RANKS[self.value] 

167 

168 

169@export 

170class RiskLevel(Enum): 

171 """ 

172 The level of a risk, classified by its score: the rank of its probability times the rank of its impact. 

173 

174 .. code-block:: text 

175 

176 Impact: low medium high critical 

177 Probability: low 1 2 3 4 

178 medium 2 4 6 8 

179 high 3 6 9 12 

180 occurred 4 8 12 16 

181 """ 

182 

183 Low = 1 #: A score of 1 to 3. 

184 Medium = 2 #: A score of 4 to 6. 

185 High = 3 #: A score of 8 or 9. 

186 Critical = 4 #: A score of 12 to 16. 

187 

188 

189@export 

190class RiskState(Enum): 

191 """ 

192 The state of a risk in its life cycle. 

193 

194 A risk ends either *solved* - its mitigations removed it - or *closed* - it isn't tracked anymore, without being 

195 solved. 

196 """ 

197 

198 Open = 1 #: Identified and tracked. 

199 OnHold = 2 #: Tracking is paused, e.g. until a decision is made. 

200 Closed = 3 #: Not tracked anymore, without being solved, e.g. accepted, obsolete or its time has passed. 

201 Solved = 4 #: Removed by its mitigations: it can't occur anymore. 

202 

203 DEFAULT = Open #: The state of a newly identified risk. 

204 

205 

206@export 

207class LinkKind(Enum): 

208 """ 

209 The relationship a :class:`Link` states from its source risk to its destination risk. 

210 """ 

211 

212 Causes = 1 #: The source, if it occurs, causes the destination to occur. 

213 Amplifies = 2 #: The source, if it occurs, raises the probability or the impact of the destination. 

214 DependsOn = 3 #: The source can only occur, if the destination occurred. 

215 Duplicates = 4 #: The source describes the same risk as the destination. 

216 Related = 5 #: The source relates to the destination in another way. 

217 

218 DEFAULT = Related #: The relationship of a link, which states no other. 

219 

220 

221@export 

222class MitigationKind(Enum): 

223 """ 

224 The way a mitigation reduces a risk or an implication. 

225 """ 

226 

227 Avoid = 1 #: Removes the cause, so the risk can't occur. 

228 Prevent = 2 #: Reduces the probability the risk occurs. 

229 Contingency = 3 #: Reduces the impact: a plan applied, when the risk occurs. 

230 Transfer = 4 #: Moves the impact to a third party, e.g. by a contract or an insurance. 

231 

232 DEFAULT = Prevent #: The way of a mitigation, which states no other. 

233 

234 

235@export 

236class MitigationState(Enum): 

237 """ 

238 The state of a mitigation: whether it is applied. 

239 

240 A mitigation *on hold* waits to be planned; a new mitigation is *planned*. 

241 """ 

242 

243 OnHold = 1 #: Proposed, but waiting to be planned, e.g. for a decision or a budget. 

244 Planned = 2 #: Decided, but not started. 

245 Active = 3 #: Being applied. 

246 Done = 4 #: Applied completely. 

247 Abandoned = 5 #: Not applied anymore, e.g. too expensive or without effect. 

248 

249 DEFAULT = Planned #: The state of a newly decided mitigation. 

250 

251 

252@export 

253class Base(KeyValuePairsMixin, metaclass=ExtendedType, slots=True): 

254 """ 

255 Base-class of every element of the risk management model: key-value pairs, a reference to the risk register 

256 containing the element, and to the register's root. 

257 """ 

258 

259 _parent: Nullable[RiskManagement] #: The risk register containing this element, or ``None``. 

260 _root: Nullable[RiskManagement] #: The risk register's root, or ``None`` while the element isn't part of one. 

261 

262 def __init__( 

263 self, 

264 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None, 

265 *, 

266 parent: Nullable[RiskManagement] = None 

267 ) -> None: 

268 """ 

269 Initializes an element with its key-value pairs, its parent and the parent's root. 

270 

271 The parent doesn't list the element yet: the class setting the fields the parent checks - an ID and the referenced 

272 elements - adds it with ``parent._AddElement(self)``. 

273 

274 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``. 

275 :param parent: Optional, the risk register containing this element. Default: ``None``. 

276 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping. 

277 :raises TypeError: If parameter ``parent`` isn't of type :class:`RiskManagement`. 

278 """ 

279 super().__init__(keyValuePairs) 

280 

281 if parent is None: 

282 self._parent = None 

283 self._root = None 

284 elif not isinstance(parent, RiskManagement): 

285 ex = TypeError("Parameter 'parent' is not of type 'RiskManagement'.") 

286 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

287 raise ex 

288 else: 

289 self._parent = parent 

290 self._root = parent._root 

291 

292 @property 

293 def Parent(self) -> Nullable[RiskManagement]: 

294 """ 

295 Property to access the risk register containing this element (:attr:`_parent`). 

296 

297 Assigning a risk register adds this element to it, and sets the register's root as the root of this element. 

298 

299 :returns: The parent, or ``None``. 

300 :raises ValueError: If ``None`` is assigned. 

301 :raises TypeError: If the assigned parent isn't of type :class:`RiskManagement`. 

302 :raises TypeError: If a parent is assigned to a :class:`RiskManagement`, which has no parent. 

303 :raises AlreadyInRiskRegisterError: If the element is already part of a risk register. 

304 :raises DuplicateRiskError: If the element is a risk, and the risk register already contains a risk of its 

305 ID. 

306 :raises DuplicateImplicationError: If the element is an implication, and the risk register already contains an 

307 implication of its ID. 

308 :raises DuplicateMitigationError: If the element is a mitigation, and the risk register already contains a 

309 mitigation of its ID. 

310 :raises NotInSameRiskRegisterError: If the element references an element, which isn't part of the risk register. 

311 |br| 

312 Add the referenced elements to the risk register first. 

313 """ 

314 return self._parent 

315 

316 @Parent.setter 

317 def Parent(self, parent: RiskManagement) -> None: 

318 if parent is None: 

319 raise ValueError("Parameter 'parent' is None.") 

320 elif not isinstance(parent, RiskManagement): 

321 ex = TypeError("Parameter 'parent' is not of type 'RiskManagement'.") 

322 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

323 raise ex 

324 elif self._parent is not None: 

325 ex = AlreadyInRiskRegisterError("Element is already part of a risk register.") 

326 ex.add_note(f"Got element '{self!r}'.") 

327 raise ex 

328 

329 parent._AddElement(self) 

330 self._parent = parent 

331 self._root = parent._root 

332 

333 @readonly 

334 def Root(self) -> Nullable[RiskManagement]: 

335 """ 

336 Read-only property to access the risk register's root (:attr:`_root`). 

337 

338 The root is maintained by :attr:`Parent`: assigning a parent sets it. 

339 

340 :returns: The root, or ``None`` while the element isn't part of a risk register. 

341 """ 

342 return self._root 

343 

344 

345@export 

346class RiskManagement(Base): 

347 """ 

348 The risk register: every risk, implication and mitigation, each by its ID, the links between the risks, the persons 

349 it refers to, the register's version and its optional name. 

350 

351 It is its own root, and has no parent. An element joins it by naming it as its parent, in the element's constructor 

352 or by :attr:`~Base.Parent`. 

353 """ 

354 

355 _version: SemanticVersion #: The version of the risk register. 

356 _name: Nullable[str] #: The name of the risk register, if given. 

357 _risks: dict[str, Risk] #: The risks, by ID. 

358 _implications: dict[str, Implication] #: The implications, by ID. 

359 _mitigations: dict[str, Mitigation] #: The mitigations, by ID. 

360 _links: list[Link] #: The links between the risks, in the order they were added. 

361 _persons: dict[str, Person] #: The persons the register refers to, e.g. as risk owners, by user ID. 

362 

363 def __init__( 

364 self, 

365 version: SemanticVersion, 

366 name: Nullable[str] = None, 

367 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None 

368 ) -> None: 

369 """ 

370 Initializes an empty risk register of a version. 

371 

372 :param version: The version of the risk register. 

373 :param name: Optional, the name of the risk register, e.g. the project's name. Default: ``None``. 

374 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``. 

375 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping. 

376 :raises ValueError: If parameter ``version`` is ``None``. 

377 :raises TypeError: If parameter ``version`` isn't of type :class:`~pyTooling.Versioning.SemanticVersion`. 

378 :raises TypeError: If parameter ``name`` isn't of type :class:`str`. 

379 :raises ValueError: If parameter ``name`` is empty. 

380 """ 

381 super().__init__(keyValuePairs) 

382 

383 if version is None: 

384 raise ValueError("Parameter 'version' is None.") 

385 elif not isinstance(version, SemanticVersion): 

386 ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.") 

387 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

388 raise ex 

389 

390 if name is not None: 

391 if not isinstance(name, str): 

392 ex = TypeError("Parameter 'name' is not of type 'str'.") 

393 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") 

394 raise ex 

395 elif name == "": 

396 raise ValueError("Parameter 'name' is empty.") 

397 

398 self._root = self 

399 self._version = version 

400 self._name = name 

401 self._risks = {} 

402 self._implications = {} 

403 self._mitigations = {} 

404 self._links = [] 

405 self._persons = {} 

406 

407 @Base.Parent.setter 

408 def Parent(self, parent: None) -> None: 

409 ex = TypeError(f"A '{getFullyQualifiedName(self)}' has no parent.") 

410 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

411 raise ex 

412 

413 def _AddElement(self, element: BaseWithID) -> None: 

414 """ 

415 Add a risk, an implication or a mitigation, which names the risk register as its parent. 

416 

417 :param element: The risk, implication or mitigation. 

418 :raises DuplicateRiskError: If the risk register already contains a risk of the element's ID. 

419 :raises DuplicateImplicationError: If the risk register already contains an implication of the element's ID. 

420 :raises DuplicateMitigationError: If the risk register already contains a mitigation of the element's ID. 

421 :raises NotInSameRiskRegisterError: If the element references an implication or a mitigation, which isn't part of 

422 the 

423 risk register. |br| 

424 Add the referenced elements to the risk register first. 

425 :raises NotInSameRiskRegisterError: If the element is a risk, whose owner isn't a person of the risk register. |br| 

426 Add the person to the risk register first. 

427 """ 

428 if isinstance(element, Risk): 

429 kind = "Risk" 

430 elements = self._risks 

431 exception = DuplicateRiskError 

432 elif isinstance(element, Implication): 

433 kind = "Implication" 

434 elements = self._implications 

435 exception = DuplicateImplicationError 

436 else: 

437 kind = "Mitigation" 

438 elements = self._mitigations 

439 exception = DuplicateMitigationError 

440 

441 if element._id in elements: 

442 raise exception(f"{kind} '{element._id}' already exists in the risk register.") 

443 

444 for implication in element._implications.values(): 

445 if implication._root is not self: 

446 ex = NotInSameRiskRegisterError(f"{kind} '{element._id}' references an implication outside the risk register.") 

447 ex.add_note(f"Got implication '{implication._id}'.") 

448 ex.add_note("Add the implication to the risk register first.") 

449 raise ex 

450 

451 if isinstance(element, MitigationsMixin): 

452 for mitigation in element._mitigations.values(): 

453 if mitigation._root is not self: 

454 ex = NotInSameRiskRegisterError(f"{kind} '{element._id}' references a mitigation outside the risk register.") 

455 ex.add_note(f"Got mitigation '{mitigation._id}'.") 

456 ex.add_note("Add the mitigation to the risk register first.") 

457 raise ex 

458 

459 if isinstance(element, Risk) and element._owner is not None: 

460 if self._persons.get(element._owner._userID) is not element._owner: 

461 ex = NotInSameRiskRegisterError(f"Risk '{element._id}' has an owner outside the risk register.") 

462 ex.add_note(f"Got person '{element._owner._userID}'.") 

463 ex.add_note("Add the person to the risk register first.") 

464 raise ex 

465 

466 elements[element._id] = element 

467 

468 @readonly 

469 def Version(self) -> SemanticVersion: 

470 """ 

471 Read-only property to access the version of the risk register (:attr:`_version`). 

472 

473 :returns: The version. 

474 """ 

475 return self._version 

476 

477 @readonly 

478 def Name(self) -> Nullable[str]: 

479 """ 

480 Read-only property to access the name of the risk register (:attr:`_name`). 

481 

482 :returns: The name, or ``None`` if none is given. 

483 """ 

484 return self._name 

485 

486 @readonly 

487 def Risks(self) -> dict[str, Risk]: 

488 """ 

489 Read-only property to access the risks (:attr:`_risks`). 

490 

491 :returns: The risks, by ID. 

492 """ 

493 return self._risks 

494 

495 @readonly 

496 def Implications(self) -> dict[str, Implication]: 

497 """ 

498 Read-only property to access the implications (:attr:`_implications`). 

499 

500 :returns: The implications, by ID. 

501 """ 

502 return self._implications 

503 

504 @readonly 

505 def Mitigations(self) -> dict[str, Mitigation]: 

506 """ 

507 Read-only property to access the mitigations (:attr:`_mitigations`). 

508 

509 :returns: The mitigations, by ID. 

510 """ 

511 return self._mitigations 

512 

513 @readonly 

514 def Links(self) -> list[Link]: 

515 """ 

516 Read-only property to access the links between the risks (:attr:`_links`). 

517 

518 :returns: The links, in the order they were added. 

519 """ 

520 return self._links 

521 

522 @readonly 

523 def Persons(self) -> dict[str, Person]: 

524 """ 

525 Read-only property to access the persons the risk register refers to (:attr:`_persons`). 

526 

527 A risk's owner is one of them. 

528 

529 :returns: The persons, by user ID. 

530 """ 

531 return self._persons 

532 

533 def AddPerson(self, person: Person) -> None: 

534 """ 

535 Add a person the risk register refers to. 

536 

537 A risk's owner must be added first. The same person may be added to several risk registers. 

538 

539 :param person: The person. 

540 :raises ValueError: If parameter ``person`` is ``None``. 

541 :raises TypeError: If parameter ``person`` isn't of type :class:`~pyEDAA.Reports.Person`. 

542 :raises DuplicatePersonError: If the risk register already contains a person of the same user ID. 

543 """ 

544 if person is None: 

545 raise ValueError("Parameter 'person' is None.") 

546 elif not isinstance(person, Person): 

547 ex = TypeError("Parameter 'person' is not of type 'Person'.") 

548 ex.add_note(f"Got type '{getFullyQualifiedName(person)}'.") 

549 raise ex 

550 elif person._userID in self._persons: 

551 raise DuplicatePersonError(f"Person '{person._userID}' already exists in the risk register.") 

552 

553 self._persons[person._userID] = person 

554 

555 def IterateRisks(self, predicate: Nullable[Callable[[Risk], bool]] = None) -> Generator[Risk, None, None]: 

556 """ 

557 Iterate all or selected risks. 

558 

559 If parameter ``predicate`` is not None, the given filter function is used to skip risks in the generator. 

560 

561 :param predicate: Optional, filter function accepting any risk and returning a boolean. 

562 :returns: A generator of the risks, in the order they were added. 

563 """ 

564 if predicate is None: 

565 yield from self._risks.values() 

566 else: 

567 for risk in self._risks.values(): 

568 if predicate(risk): 

569 yield risk 

570 

571 def IterateImplications( 

572 self, 

573 predicate: Nullable[Callable[[Implication], bool]] = None 

574 ) -> Generator[Implication, None, None]: 

575 """ 

576 Iterate all or selected implications. 

577 

578 If parameter ``predicate`` is not None, the given filter function is used to skip implications in the generator. 

579 

580 :param predicate: Optional, filter function accepting any implication and returning a boolean. 

581 :returns: A generator of the implications, in the order they were added. 

582 """ 

583 if predicate is None: 

584 yield from self._implications.values() 

585 else: 

586 for implication in self._implications.values(): 

587 if predicate(implication): 

588 yield implication 

589 

590 def IterateMitigations( 

591 self, 

592 predicate: Nullable[Callable[[Mitigation], bool]] = None 

593 ) -> Generator[Mitigation, None, None]: 

594 """ 

595 Iterate all or selected mitigations. 

596 

597 If parameter ``predicate`` is not None, the given filter function is used to skip mitigations in the generator. 

598 

599 :param predicate: Optional, filter function accepting any mitigation and returning a boolean. 

600 :returns: A generator of the mitigations, in the order they were added. 

601 """ 

602 if predicate is None: 

603 yield from self._mitigations.values() 

604 else: 

605 for mitigation in self._mitigations.values(): 

606 if predicate(mitigation): 

607 yield mitigation 

608 

609 def IterateLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[Link, None, None]: 

610 """ 

611 Iterate all or selected links between the risks. 

612 

613 If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator. 

614 

615 :param predicate: Optional, filter function accepting any link and returning a boolean. 

616 :returns: A generator of the links, in the order they were added. 

617 """ 

618 if predicate is None: 

619 yield from self._links 

620 else: 

621 for link in self._links: 

622 if predicate(link): 

623 yield link 

624 

625 def __repr__(self) -> str: 

626 """ 

627 Return a representation of the risk register for debugging. 

628 

629 A register of version 1.0.0 with 3 risks, 4 implications and 2 mitigations reads 

630 ``<RiskManagement 1.0.0: 3 risks, 4 implications, 2 mitigations>``. 

631 

632 :returns: The register's version and its numbers of risks, implications and mitigations. 

633 """ 

634 return ( 

635 f"<RiskManagement {self._version}: {len(self._risks)} risks, {len(self._implications)} implications, " 

636 f"{len(self._mitigations)} mitigations>" 

637 ) 

638 

639 

640@export 

641class BaseWithID(Base): 

642 """ 

643 Base-class of the elements a risk register holds by ID - risks, implications and mitigations -: an ID and a 

644 description. 

645 """ 

646 

647 _id: str #: The element's ID, unique among the elements of its kind in a risk register. 

648 _description: str #: The element's description. 

649 

650 def __init__( 

651 self, 

652 identifier: str, 

653 description: str, 

654 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None, 

655 *, 

656 parent: Nullable[RiskManagement] = None 

657 ) -> None: 

658 """ 

659 Initializes an element with its ID, its description, its key-value pairs and its parent. 

660 

661 :param identifier: The element's ID, unique among the elements of its kind in a risk register. 

662 :param description: The element's description. 

663 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``. 

664 :param parent: Optional, the risk register containing this element. Default: ``None``. 

665 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping. 

666 :raises TypeError: If parameter ``parent`` isn't of type :class:`RiskManagement`. 

667 :raises ValueError: If parameter ``identifier`` is ``None``. 

668 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`. 

669 :raises ValueError: If parameter ``identifier`` is empty. 

670 :raises ValueError: If parameter ``description`` is ``None``. 

671 :raises TypeError: If parameter ``description`` isn't of type :class:`str`. 

672 :raises ValueError: If parameter ``description`` is empty. 

673 """ 

674 super().__init__(keyValuePairs, parent=parent) 

675 

676 if identifier is None: 

677 raise ValueError("Parameter 'identifier' is None.") 

678 elif not isinstance(identifier, str): 

679 ex = TypeError("Parameter 'identifier' is not of type 'str'.") 

680 ex.add_note(f"Got type '{getFullyQualifiedName(identifier)}'.") 

681 raise ex 

682 elif identifier == "": 

683 raise ValueError("Parameter 'identifier' is empty.") 

684 

685 if description is None: 

686 raise ValueError("Parameter 'description' is None.") 

687 elif not isinstance(description, str): 

688 ex = TypeError("Parameter 'description' is not of type 'str'.") 

689 ex.add_note(f"Got type '{getFullyQualifiedName(description)}'.") 

690 raise ex 

691 elif description == "": 

692 raise ValueError("Parameter 'description' is empty.") 

693 

694 self._id = identifier 

695 self._description = description 

696 

697 @readonly 

698 def ID(self) -> str: 

699 """ 

700 Read-only property to access the element's ID (:attr:`_id`). 

701 

702 :returns: The ID. 

703 """ 

704 return self._id 

705 

706 @readonly 

707 def Description(self) -> str: 

708 """ 

709 Read-only property to access the element's description (:attr:`_description`). 

710 

711 :returns: The description. 

712 """ 

713 return self._description 

714 

715 

716@export 

717class ImplicationsMixin(metaclass=ExtendedType, mixin=True): 

718 """ 

719 A mixin-class adding implications to an element: the consequences the element has. 

720 

721 The implications of a risk are what happens, if it occurs; those of a mitigation are its side effects. An element of 

722 a risk register references only implications of the same register. 

723 """ 

724 

725 _implications: dict[str, Implication] #: The implications, by ID. 

726 

727 def __init__(self, implications: Nullable[Iterable[Implication]] = None) -> None: 

728 """ 

729 Initializes the implications. 

730 

731 :param implications: Optional, the implications. Default: ``None``. 

732 :raises TypeError: If parameter ``implications`` isn't iterable. 

733 :raises TypeError: If parameter ``implications`` is a single element, not an iterable. 

734 :raises TypeError: If parameter ``implications`` contains an element not of type 

735 :class:`Implication`. 

736 :raises DuplicateImplicationError: If parameter ``implications`` contains two implications of the same ID. 

737 :raises NotInSameRiskRegisterError: If parameter ``implications`` contains an implication, which isn't part of the 

738 element's risk register. |br| 

739 Add the implication to the risk register first. 

740 """ 

741 self._implications = {} 

742 

743 if implications is None: 

744 return 

745 elif not isinstance(implications, Iterable): 

746 ex = TypeError("Parameter 'implications' is not iterable.") 

747 ex.add_note(f"Got type '{getFullyQualifiedName(implications)}'.") 

748 raise ex 

749 elif isinstance(implications, KeyValuePairsMixin): 

750 ex = TypeError("Parameter 'implications' is a single element, not an iterable of elements.") 

751 ex.add_note(f"Got type '{getFullyQualifiedName(implications)}'.") 

752 raise ex 

753 

754 for implication in implications: 

755 if not isinstance(implication, Implication): 

756 ex = TypeError("Parameter 'implications' contains an element not of type 'Implication'.") 

757 ex.add_note(f"Got type '{getFullyQualifiedName(implication)}'.") 

758 raise ex 

759 elif implication._id in self._implications: 

760 raise DuplicateImplicationError(f"Parameter 'implications' contains implication '{implication._id}' twice.") 

761 elif self._root is not None and implication._root is not self._root: 

762 ex = NotInSameRiskRegisterError( 

763 "Parameter 'implications' contains an implication outside the element's risk register." 

764 ) 

765 ex.add_note(f"Got implication '{implication._id}'.") 

766 ex.add_note("Add the implication to the risk register first.") 

767 raise ex 

768 

769 self._implications[implication._id] = implication 

770 

771 @readonly 

772 def Implications(self) -> dict[str, Implication]: 

773 """ 

774 Read-only property to access the implications (:attr:`_implications`). 

775 

776 :returns: The implications, by ID. 

777 """ 

778 return self._implications 

779 

780 def AddImplication(self, implication: Implication) -> None: 

781 """ 

782 Add an implication. 

783 

784 :param implication: The implication. 

785 :raises ValueError: If parameter ``implication`` is ``None``. 

786 :raises TypeError: If parameter ``implication`` isn't of type :class:`Implication`. 

787 :raises DuplicateImplicationError: If the element already has an implication of the same ID. 

788 :raises NotInSameRiskRegisterError: If the implication isn't part of the element's risk register. |br| 

789 Add the implication to the risk register first. 

790 """ 

791 if implication is None: 

792 raise ValueError("Parameter 'implication' is None.") 

793 elif not isinstance(implication, Implication): 

794 ex = TypeError("Parameter 'implication' is not of type 'Implication'.") 

795 ex.add_note(f"Got type '{getFullyQualifiedName(implication)}'.") 

796 raise ex 

797 elif implication._id in self._implications: 

798 raise DuplicateImplicationError(f"Element already has implication '{implication._id}'.") 

799 elif self._root is not None and implication._root is not self._root: 

800 ex = NotInSameRiskRegisterError("Parameter 'implication' is outside the element's risk register.") 

801 ex.add_note(f"Got implication '{implication._id}'.") 

802 ex.add_note("Add the implication to the risk register first.") 

803 raise ex 

804 

805 self._implications[implication._id] = implication 

806 

807 

808@export 

809class MitigationsMixin(metaclass=ExtendedType, mixin=True): 

810 """ 

811 A mixin-class adding mitigations to an element: the measures reducing the element's probability or impact. 

812 

813 An element of a risk register references only mitigations of the same register. 

814 """ 

815 

816 _mitigations: dict[str, Mitigation] #: The mitigations, by ID. 

817 

818 def __init__(self, mitigations: Nullable[Iterable[Mitigation]] = None) -> None: 

819 """ 

820 Initializes the mitigations. 

821 

822 :param mitigations: Optional, the mitigations. Default: ``None``. 

823 :raises TypeError: If parameter ``mitigations`` isn't iterable. 

824 :raises TypeError: If parameter ``mitigations`` is a single element, not an iterable. 

825 :raises TypeError: If parameter ``mitigations`` contains an element not of type 

826 :class:`Mitigation`. 

827 :raises DuplicateMitigationError: If parameter ``mitigations`` contains two mitigations of the same ID. 

828 :raises NotInSameRiskRegisterError: If parameter ``mitigations`` contains a mitigation, which isn't part of the 

829 element's risk register. |br| 

830 Add the mitigation to the risk register first. 

831 """ 

832 self._mitigations = {} 

833 

834 if mitigations is None: 

835 return 

836 elif not isinstance(mitigations, Iterable): 

837 ex = TypeError("Parameter 'mitigations' is not iterable.") 

838 ex.add_note(f"Got type '{getFullyQualifiedName(mitigations)}'.") 

839 raise ex 

840 elif isinstance(mitigations, KeyValuePairsMixin): 

841 ex = TypeError("Parameter 'mitigations' is a single element, not an iterable of elements.") 

842 ex.add_note(f"Got type '{getFullyQualifiedName(mitigations)}'.") 

843 raise ex 

844 

845 for mitigation in mitigations: 

846 if not isinstance(mitigation, Mitigation): 

847 ex = TypeError("Parameter 'mitigations' contains an element not of type 'Mitigation'.") 

848 ex.add_note(f"Got type '{getFullyQualifiedName(mitigation)}'.") 

849 raise ex 

850 elif mitigation._id in self._mitigations: 

851 raise DuplicateMitigationError(f"Parameter 'mitigations' contains mitigation '{mitigation._id}' twice.") 

852 elif self._root is not None and mitigation._root is not self._root: 

853 ex = NotInSameRiskRegisterError( 

854 "Parameter 'mitigations' contains a mitigation outside the element's risk register." 

855 ) 

856 ex.add_note(f"Got mitigation '{mitigation._id}'.") 

857 ex.add_note("Add the mitigation to the risk register first.") 

858 raise ex 

859 

860 self._mitigations[mitigation._id] = mitigation 

861 

862 @readonly 

863 def Mitigations(self) -> dict[str, Mitigation]: 

864 """ 

865 Read-only property to access the mitigations (:attr:`_mitigations`). 

866 

867 :returns: The mitigations, by ID. 

868 """ 

869 return self._mitigations 

870 

871 def AddMitigation(self, mitigation: Mitigation) -> None: 

872 """ 

873 Add a mitigation. 

874 

875 :param mitigation: The mitigation. 

876 :raises ValueError: If parameter ``mitigation`` is ``None``. 

877 :raises TypeError: If parameter ``mitigation`` isn't of type :class:`Mitigation`. 

878 :raises DuplicateMitigationError: If the element already has a mitigation of the same ID. 

879 :raises NotInSameRiskRegisterError: If the mitigation isn't part of the element's risk register. |br| 

880 Add the mitigation to the risk register first. 

881 """ 

882 if mitigation is None: 

883 raise ValueError("Parameter 'mitigation' is None.") 

884 elif not isinstance(mitigation, Mitigation): 

885 ex = TypeError("Parameter 'mitigation' is not of type 'Mitigation'.") 

886 ex.add_note(f"Got type '{getFullyQualifiedName(mitigation)}'.") 

887 raise ex 

888 elif mitigation._id in self._mitigations: 

889 raise DuplicateMitigationError(f"Element already has mitigation '{mitigation._id}'.") 

890 elif self._root is not None and mitigation._root is not self._root: 

891 ex = NotInSameRiskRegisterError("Parameter 'mitigation' is outside the element's risk register.") 

892 ex.add_note(f"Got mitigation '{mitigation._id}'.") 

893 ex.add_note("Add the mitigation to the risk register first.") 

894 raise ex 

895 

896 self._mitigations[mitigation._id] = mitigation 

897 

898 

899@export 

900class Implication(BaseWithID, ImplicationsMixin, MitigationsMixin): 

901 """ 

902 An implication: a consequence of a risk, if it occurs, of another implication, or of a mitigation, if applied. 

903 

904 An implication is shared by every element naming it, e.g. two risks delaying the same milestone. It has implications 

905 of its own - the next order of consequences -, and it can be mitigated like a risk. 

906 """ 

907 

908 def __init__( 

909 self, 

910 identifier: str, 

911 description: str, 

912 implications: Nullable[Iterable[Implication]] = None, 

913 mitigations: Nullable[Iterable[Mitigation]] = None, 

914 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None, 

915 *, 

916 parent: Nullable[RiskManagement] = None 

917 ) -> None: 

918 """ 

919 Initializes an implication, and adds it to its risk register. 

920 

921 :param identifier: The implication's ID, unique among the implications of a risk register. 

922 :param description: The implication's description. 

923 :param implications: Optional, the implications of this implication. Default: ``None``. 

924 :param mitigations: Optional, the mitigations of this implication. Default: ``None``. 

925 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``. 

926 :param parent: Optional, the risk register containing the implication. Default: ``None``. 

927 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping. 

928 :raises TypeError: If parameter ``parent`` isn't of type :class:`RiskManagement`. 

929 :raises ValueError: If parameter ``identifier`` is ``None``. 

930 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`. 

931 :raises ValueError: If parameter ``identifier`` is empty. 

932 :raises ValueError: If parameter ``description`` is ``None``. 

933 :raises TypeError: If parameter ``description`` isn't of type :class:`str`. 

934 :raises ValueError: If parameter ``description`` is empty. 

935 :raises TypeError: If parameter ``implications`` isn't iterable. 

936 :raises TypeError: If parameter ``implications`` is a single element, not an iterable. 

937 :raises TypeError: If parameter ``implications`` contains an element not of type 

938 :class:`Implication`. 

939 :raises DuplicateImplicationError: If parameter ``implications`` contains two implications of the same ID. 

940 :raises NotInSameRiskRegisterError: If parameter ``implications`` contains an implication, which isn't part of the 

941 risk register. |br| 

942 Add the implication to the risk register first. 

943 :raises TypeError: If parameter ``mitigations`` isn't iterable. 

944 :raises TypeError: If parameter ``mitigations`` is a single element, not an iterable. 

945 :raises TypeError: If parameter ``mitigations`` contains an element not of type 

946 :class:`Mitigation`. 

947 :raises DuplicateMitigationError: If parameter ``mitigations`` contains two mitigations of the same ID. 

948 :raises NotInSameRiskRegisterError: If parameter ``mitigations`` contains a mitigation, which isn't part of the risk 

949 register. |br| 

950 Add the mitigation to the risk register first. 

951 :raises DuplicateImplicationError: If the risk register already contains an implication of this ID. 

952 """ 

953 super().__init__(identifier, description, keyValuePairs, parent=parent) 

954 ImplicationsMixin.__init__(self, implications) 

955 MitigationsMixin.__init__(self, mitigations) 

956 

957 if parent is not None: 

958 parent._AddElement(self) 

959 

960 def __repr__(self) -> str: 

961 """ 

962 Return a representation of the implication for debugging. 

963 

964 An implication ``I1`` with 2 implications and 1 mitigation reads ``<Implication I1: 2 implications, 

965 1 mitigations>``. 

966 

967 :returns: The implication's ID and its numbers of implications and mitigations. 

968 """ 

969 return f"<Implication {self._id}: {len(self._implications)} implications, {len(self._mitigations)} mitigations>" 

970 

971 

972@export 

973class Mitigation(BaseWithID, ImplicationsMixin): 

974 """ 

975 A mitigation: a measure reducing the probability or the impact of a risk or of an implication. 

976 

977 A mitigation is shared by every element naming it, e.g. a second supplier reducing two risks. Its implications are 

978 its side effects: the consequences of applying it. 

979 """ 

980 

981 _kind: MitigationKind #: The way the mitigation reduces a risk or an implication. 

982 _state: MitigationState #: Whether the mitigation is applied. 

983 

984 def __init__( 

985 self, 

986 identifier: str, 

987 description: str, 

988 implications: Nullable[Iterable[Implication]] = None, 

989 kind: MitigationKind = MitigationKind.Prevent, 

990 state: MitigationState = MitigationState.Planned, 

991 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None, 

992 *, 

993 parent: Nullable[RiskManagement] = None 

994 ) -> None: 

995 """ 

996 Initializes a mitigation, and adds it to its risk register. 

997 

998 :param identifier: The mitigation's ID, unique among the mitigations of a risk register. 

999 :param description: The mitigation's description. 

1000 :param implications: Optional, the side effects of applying the mitigation. Default: ``None``. 

1001 :param kind: Optional, the way the mitigation reduces a risk or an implication. Default: 

1002 ``MitigationKind.Prevent``. 

1003 :param state: Optional, whether the mitigation is applied. Default: 

1004 ``MitigationState.Planned``. 

1005 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``. 

1006 :param parent: Optional, the risk register containing the mitigation. Default: ``None``. 

1007 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping. 

1008 :raises TypeError: If parameter ``parent`` isn't of type :class:`RiskManagement`. 

1009 :raises ValueError: If parameter ``identifier`` is ``None``. 

1010 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`. 

1011 :raises ValueError: If parameter ``identifier`` is empty. 

1012 :raises ValueError: If parameter ``description`` is ``None``. 

1013 :raises TypeError: If parameter ``description`` isn't of type :class:`str`. 

1014 :raises ValueError: If parameter ``description`` is empty. 

1015 :raises TypeError: If parameter ``implications`` isn't iterable. 

1016 :raises TypeError: If parameter ``implications`` is a single element, not an iterable. 

1017 :raises TypeError: If parameter ``implications`` contains an element not of type 

1018 :class:`Implication`. 

1019 :raises DuplicateImplicationError: If parameter ``implications`` contains two implications of the same ID. 

1020 :raises NotInSameRiskRegisterError: If parameter ``implications`` contains an implication, which isn't part of the 

1021 risk register. |br| 

1022 Add the implication to the risk register first. 

1023 :raises ValueError: If parameter ``kind`` is ``None``. 

1024 :raises TypeError: If parameter ``kind`` isn't of type :class:`MitigationKind`. 

1025 :raises ValueError: If parameter ``state`` is ``None``. 

1026 :raises TypeError: If parameter ``state`` isn't of type :class:`MitigationState`. 

1027 :raises DuplicateMitigationError: If the risk register already contains a mitigation of this ID. 

1028 """ 

1029 super().__init__(identifier, description, keyValuePairs, parent=parent) 

1030 ImplicationsMixin.__init__(self, implications) 

1031 

1032 if kind is None: 

1033 raise ValueError("Parameter 'kind' is None.") 

1034 elif not isinstance(kind, MitigationKind): 

1035 ex = TypeError("Parameter 'kind' is not of type 'MitigationKind'.") 

1036 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.") 

1037 raise ex 

1038 

1039 if state is None: 

1040 raise ValueError("Parameter 'state' is None.") 

1041 elif not isinstance(state, MitigationState): 

1042 ex = TypeError("Parameter 'state' is not of type 'MitigationState'.") 

1043 ex.add_note(f"Got type '{getFullyQualifiedName(state)}'.") 

1044 raise ex 

1045 

1046 self._kind = kind 

1047 self._state = state 

1048 

1049 if parent is not None: 

1050 parent._AddElement(self) 

1051 

1052 @readonly 

1053 def Kind(self) -> MitigationKind: 

1054 """ 

1055 Read-only property to access the way the mitigation reduces a risk or an implication (:attr:`_kind`). 

1056 

1057 :returns: The mitigation's kind. 

1058 """ 

1059 return self._kind 

1060 

1061 @property 

1062 def State(self) -> MitigationState: 

1063 """ 

1064 Property to access whether the mitigation is applied (:attr:`_state`). 

1065 

1066 :returns: The mitigation's state. 

1067 :raises ValueError: If ``None`` is assigned. 

1068 :raises TypeError: If the assigned value isn't of type :class:`MitigationState`. 

1069 """ 

1070 return self._state 

1071 

1072 @State.setter 

1073 def State(self, value: MitigationState) -> None: 

1074 if value is None: 

1075 raise ValueError("Parameter 'value' is None.") 

1076 elif not isinstance(value, MitigationState): 

1077 ex = TypeError("Parameter 'value' is not of type 'MitigationState'.") 

1078 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

1079 raise ex 

1080 

1081 self._state = value 

1082 

1083 def __repr__(self) -> str: 

1084 """ 

1085 Return a representation of the mitigation for debugging. 

1086 

1087 A planned, preventing mitigation ``M1`` with 1 side effect reads ``<Mitigation M1: Prevent, Planned, 

1088 1 implications>``. 

1089 

1090 :returns: The mitigation's ID, kind, state and number of implications. 

1091 """ 

1092 return f"<Mitigation {self._id}: {self._kind.name}, {self._state.name}, {len(self._implications)} implications>" 

1093 

1094 

1095# A class with a property named like a class - ``Probability``, ``Impact`` - can't name that class in the annotation of 

1096# a field: the class body's namespace, where annotations are evaluated, binds the name to the property. 

1097_Probability = Probability 

1098_Impact = Impact 

1099 

1100 

1101@export 

1102class Risk(BaseWithID, ImplicationsMixin, MitigationsMixin): 

1103 """ 

1104 A risk: its title and description, the probability it occurs, its impact, its state, its owner, the date of its next 

1105 review, the implications and mitigations it references, and its links to other risks. 

1106 

1107 The implications of a risk are its first-order implications. Their implications and the side effects of the risk's 

1108 mitigations are its second-order implications (:attr:`SecondOrderImplications`). 

1109 """ 

1110 

1111 _title: str #: The risk's title. 

1112 _probability: _Probability #: The probability the risk occurs. 

1113 _impact: _Impact #: The impact of the risk, if it occurs. 

1114 _state: RiskState #: The state of the risk in its life cycle. 

1115 _owner: Nullable[Person] #: The person responsible for the risk, if named. 

1116 _nextReview: Nullable[datetime] #: The date of the risk's next review, if scheduled. 

1117 _outboundLinks: list[Link] #: The links from this risk to other risks, in the order they were added. 

1118 _inboundLinks: list[Link] #: The links from other risks to this risk, in the order they were added. 

1119 

1120 def __init__( 

1121 self, 

1122 identifier: str, 

1123 title: str, 

1124 description: str, 

1125 probability: Probability, 

1126 impact: Impact, 

1127 state: RiskState = RiskState.Open, 

1128 implications: Nullable[Iterable[Implication]] = None, 

1129 mitigations: Nullable[Iterable[Mitigation]] = None, 

1130 owner: Nullable[Person] = None, 

1131 nextReview: Nullable[datetime] = None, 

1132 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None, 

1133 *, 

1134 parent: Nullable[RiskManagement] = None 

1135 ) -> None: 

1136 """ 

1137 Initializes a risk, and adds it to its risk register. 

1138 

1139 :param identifier: The risk's ID, unique among the risks of a risk register. 

1140 :param title: The risk's title. 

1141 :param description: The risk's description. 

1142 :param probability: The probability the risk occurs. 

1143 :param impact: The impact of the risk, if it occurs. 

1144 :param state: Optional, the state of the risk. Default: ``RiskState.Open``. 

1145 :param implications: Optional, the first-order implications of the risk. Default: ``None``. 

1146 :param mitigations: Optional, the mitigations of the risk. Default: ``None``. 

1147 :param owner: Optional, the person responsible for the risk; a person of the risk register. 

1148 Default: ``None``. 

1149 :param nextReview: Optional, the date of the risk's next review; without a time zone, it is a local 

1150 time. Default: ``None``. 

1151 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``. 

1152 :param parent: Optional, the risk register containing the risk. Default: ``None``. 

1153 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping. 

1154 :raises TypeError: If parameter ``parent`` isn't of type :class:`RiskManagement`. 

1155 :raises ValueError: If parameter ``identifier`` is ``None``. 

1156 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`. 

1157 :raises ValueError: If parameter ``identifier`` is empty. 

1158 :raises ValueError: If parameter ``description`` is ``None``. 

1159 :raises TypeError: If parameter ``description`` isn't of type :class:`str`. 

1160 :raises ValueError: If parameter ``description`` is empty. 

1161 :raises TypeError: If parameter ``implications`` isn't iterable. 

1162 :raises TypeError: If parameter ``implications`` is a single element, not an iterable. 

1163 :raises TypeError: If parameter ``implications`` contains an element not of type 

1164 :class:`Implication`. 

1165 :raises DuplicateImplicationError: If parameter ``implications`` contains two implications of the same ID. 

1166 :raises NotInSameRiskRegisterError: If parameter ``implications`` contains an implication, which isn't part of the 

1167 risk register. |br| 

1168 Add the implication to the risk register first. 

1169 :raises TypeError: If parameter ``mitigations`` isn't iterable. 

1170 :raises TypeError: If parameter ``mitigations`` is a single element, not an iterable. 

1171 :raises TypeError: If parameter ``mitigations`` contains an element not of type 

1172 :class:`Mitigation`. 

1173 :raises DuplicateMitigationError: If parameter ``mitigations`` contains two mitigations of the same ID. 

1174 :raises NotInSameRiskRegisterError: If parameter ``mitigations`` contains a mitigation, which isn't part of the risk 

1175 register. |br| 

1176 Add the mitigation to the risk register first. 

1177 :raises ValueError: If parameter ``title`` is ``None``. 

1178 :raises TypeError: If parameter ``title`` isn't of type :class:`str`. 

1179 :raises ValueError: If parameter ``title`` is empty. 

1180 :raises ValueError: If parameter ``probability`` is ``None``. 

1181 :raises TypeError: If parameter ``probability`` isn't of type :class:`Probability`. 

1182 :raises ValueError: If parameter ``impact`` is ``None``. 

1183 :raises TypeError: If parameter ``impact`` isn't of type :class:`Impact`. 

1184 :raises ValueError: If parameter ``state`` is ``None``. 

1185 :raises TypeError: If parameter ``state`` isn't of type :class:`RiskState`. 

1186 :raises TypeError: If parameter ``owner`` isn't of type :class:`~pyEDAA.Reports.Person`. 

1187 :raises NotInSameRiskRegisterError: If parameter ``owner`` isn't a person of the risk register. |br| 

1188 Add the person to the risk register first. 

1189 :raises TypeError: If parameter ``nextReview`` isn't of type :class:`~datetime.datetime`. 

1190 :raises DuplicateRiskError: If the risk register already contains a risk of this ID. 

1191 """ 

1192 super().__init__(identifier, description, keyValuePairs, parent=parent) 

1193 ImplicationsMixin.__init__(self, implications) 

1194 MitigationsMixin.__init__(self, mitigations) 

1195 

1196 if title is None: 

1197 raise ValueError("Parameter 'title' is None.") 

1198 elif not isinstance(title, str): 

1199 ex = TypeError("Parameter 'title' is not of type 'str'.") 

1200 ex.add_note(f"Got type '{getFullyQualifiedName(title)}'.") 

1201 raise ex 

1202 elif title == "": 

1203 raise ValueError("Parameter 'title' is empty.") 

1204 

1205 if probability is None: 

1206 raise ValueError("Parameter 'probability' is None.") 

1207 elif not isinstance(probability, Probability): 

1208 ex = TypeError("Parameter 'probability' is not of type 'Probability'.") 

1209 ex.add_note(f"Got type '{getFullyQualifiedName(probability)}'.") 

1210 raise ex 

1211 

1212 if impact is None: 

1213 raise ValueError("Parameter 'impact' is None.") 

1214 elif not isinstance(impact, Impact): 

1215 ex = TypeError("Parameter 'impact' is not of type 'Impact'.") 

1216 ex.add_note(f"Got type '{getFullyQualifiedName(impact)}'.") 

1217 raise ex 

1218 

1219 if state is None: 

1220 raise ValueError("Parameter 'state' is None.") 

1221 elif not isinstance(state, RiskState): 

1222 ex = TypeError("Parameter 'state' is not of type 'RiskState'.") 

1223 ex.add_note(f"Got type '{getFullyQualifiedName(state)}'.") 

1224 raise ex 

1225 

1226 if owner is not None: 

1227 if not isinstance(owner, Person): 

1228 ex = TypeError("Parameter 'owner' is not of type 'Person'.") 

1229 ex.add_note(f"Got type '{getFullyQualifiedName(owner)}'.") 

1230 raise ex 

1231 elif self._root is not None and self._root._persons.get(owner._userID) is not owner: 

1232 ex = NotInSameRiskRegisterError("Parameter 'owner' isn't a person of the risk register.") 

1233 ex.add_note(f"Got person '{owner._userID}'.") 

1234 ex.add_note("Add the person to the risk register first.") 

1235 raise ex 

1236 

1237 if nextReview is not None and not isinstance(nextReview, datetime): 

1238 ex = TypeError("Parameter 'nextReview' is not of type 'datetime'.") 

1239 ex.add_note(f"Got type '{getFullyQualifiedName(nextReview)}'.") 

1240 raise ex 

1241 

1242 self._title = title 

1243 self._probability = probability 

1244 self._impact = impact 

1245 self._state = state 

1246 self._owner = owner 

1247 self._nextReview = nextReview 

1248 self._outboundLinks = [] 

1249 self._inboundLinks = [] 

1250 

1251 if parent is not None: 

1252 parent._AddElement(self) 

1253 

1254 @readonly 

1255 def Title(self) -> str: 

1256 """ 

1257 Read-only property to access the risk's title (:attr:`_title`). 

1258 

1259 :returns: The title. 

1260 """ 

1261 return self._title 

1262 

1263 @property 

1264 def Probability(self) -> Probability: 

1265 """ 

1266 Property to access the probability the risk occurs (:attr:`_probability`). 

1267 

1268 :returns: The probability. 

1269 :raises ValueError: If ``None`` is assigned. 

1270 :raises TypeError: If the assigned value isn't of type :class:`Probability`. 

1271 """ 

1272 return self._probability 

1273 

1274 @Probability.setter 

1275 def Probability(self, value: Probability) -> None: 

1276 if value is None: 

1277 raise ValueError("Parameter 'value' is None.") 

1278 elif not isinstance(value, Probability): 

1279 ex = TypeError("Parameter 'value' is not of type 'Probability'.") 

1280 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

1281 raise ex 

1282 

1283 self._probability = value 

1284 

1285 @property 

1286 def Impact(self) -> Impact: 

1287 """ 

1288 Property to access the impact of the risk, if it occurs (:attr:`_impact`). 

1289 

1290 :returns: The impact. 

1291 :raises ValueError: If ``None`` is assigned. 

1292 :raises TypeError: If the assigned value isn't of type :class:`Impact`. 

1293 """ 

1294 return self._impact 

1295 

1296 @Impact.setter 

1297 def Impact(self, value: Impact) -> None: 

1298 if value is None: 

1299 raise ValueError("Parameter 'value' is None.") 

1300 elif not isinstance(value, Impact): 

1301 ex = TypeError("Parameter 'value' is not of type 'Impact'.") 

1302 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

1303 raise ex 

1304 

1305 self._impact = value 

1306 

1307 @property 

1308 def State(self) -> RiskState: 

1309 """ 

1310 Property to access the state of the risk in its life cycle (:attr:`_state`). 

1311 

1312 :returns: The state. 

1313 :raises ValueError: If ``None`` is assigned. 

1314 :raises TypeError: If the assigned value isn't of type :class:`RiskState`. 

1315 """ 

1316 return self._state 

1317 

1318 @State.setter 

1319 def State(self, value: RiskState) -> None: 

1320 if value is None: 

1321 raise ValueError("Parameter 'value' is None.") 

1322 elif not isinstance(value, RiskState): 

1323 ex = TypeError("Parameter 'value' is not of type 'RiskState'.") 

1324 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

1325 raise ex 

1326 

1327 self._state = value 

1328 

1329 @property 

1330 def Owner(self) -> Nullable[Person]: 

1331 """ 

1332 Property to access the person responsible for the risk (:attr:`_owner`). 

1333 

1334 The owner of a risk in a risk register is a person of that register (:attr:`RiskManagement.Persons`). 

1335 

1336 :returns: The owner, or ``None`` if none is named. 

1337 :raises TypeError: If the assigned value isn't of type :class:`~pyEDAA.Reports.Person`. 

1338 :raises NotInSameRiskRegisterError: If the assigned value isn't a person of the risk register. |br| 

1339 Add the person to the risk register first. 

1340 """ 

1341 return self._owner 

1342 

1343 @Owner.setter 

1344 def Owner(self, value: Nullable[Person]) -> None: 

1345 if value is not None: 

1346 if not isinstance(value, Person): 

1347 ex = TypeError("Parameter 'value' is not of type 'Person'.") 

1348 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

1349 raise ex 

1350 elif self._root is not None and self._root._persons.get(value._userID) is not value: 

1351 ex = NotInSameRiskRegisterError("Parameter 'value' isn't a person of the risk register.") 

1352 ex.add_note(f"Got person '{value._userID}'.") 

1353 ex.add_note("Add the person to the risk register first.") 

1354 raise ex 

1355 

1356 self._owner = value 

1357 

1358 @property 

1359 def NextReview(self) -> Nullable[datetime]: 

1360 """ 

1361 Property to access the date of the risk's next review (:attr:`_nextReview`). 

1362 

1363 :returns: The date, or ``None`` if no review is scheduled; without a time zone, it is a local time. 

1364 :raises TypeError: If the assigned value isn't of type :class:`~datetime.datetime`. 

1365 """ 

1366 return self._nextReview 

1367 

1368 @NextReview.setter 

1369 def NextReview(self, value: Nullable[datetime]) -> None: 

1370 if value is not None and not isinstance(value, datetime): 

1371 ex = TypeError("Parameter 'value' is not of type 'datetime'.") 

1372 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

1373 raise ex 

1374 

1375 self._nextReview = value 

1376 

1377 @readonly 

1378 def Score(self) -> int: 

1379 """ 

1380 Read-only property to return the risk's score: the rank of its probability times the rank of its impact. 

1381 

1382 :returns: The score in range 1..16. 

1383 """ 

1384 return self._probability.Rank * self._impact.Rank 

1385 

1386 @readonly 

1387 def Level(self) -> RiskLevel: 

1388 """ 

1389 Read-only property to return the risk's level, classified by its :attr:`Score` (see :class:`RiskLevel`). 

1390 

1391 :returns: The risk's level. 

1392 """ 

1393 score = self.Score 

1394 if score >= 12: 

1395 return RiskLevel.Critical 

1396 elif score >= 8: 

1397 return RiskLevel.High 

1398 elif score >= 4: 

1399 return RiskLevel.Medium 

1400 

1401 return RiskLevel.Low 

1402 

1403 @readonly 

1404 def SecondOrderImplications(self) -> dict[str, Implication]: 

1405 """ 

1406 Read-only property to return the second-order implications: the implications of the risk's implications and of its 

1407 mitigations. 

1408 

1409 The implications of a mitigation are its side effects. An implication reached several times is returned once. 

1410 

1411 :returns: The second-order implications, by ID. 

1412 """ 

1413 implications = {} 

1414 for element in (*self._implications.values(), *self._mitigations.values()): 

1415 implications.update(element._implications) 

1416 

1417 return implications 

1418 

1419 @readonly 

1420 def OutboundLinks(self) -> list[Link]: 

1421 """ 

1422 Read-only property to access the links from this risk to other risks (:attr:`_outboundLinks`). 

1423 

1424 :returns: The links, in the order they were added. 

1425 """ 

1426 return self._outboundLinks 

1427 

1428 @readonly 

1429 def InboundLinks(self) -> list[Link]: 

1430 """ 

1431 Read-only property to access the links from other risks to this risk (:attr:`_inboundLinks`). 

1432 

1433 :returns: The links, in the order they were added. 

1434 """ 

1435 return self._inboundLinks 

1436 

1437 def LinkTo(self, destination: Risk, kind: LinkKind, keyValuePairs: Nullable[Mapping[Hashable, Any]] = None) -> Link: 

1438 """ 

1439 Create an outbound link from this risk to the given risk. 

1440 

1441 :param destination: The risk the link ends at. 

1442 :param kind: The relationship the link states. 

1443 :param keyValuePairs: Optional, the key-value pairs of the link. Default: ``None``. 

1444 :returns: The link from this risk to the destination. 

1445 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping. 

1446 :raises NotInSameRiskRegisterError: If this risk isn't part of a risk register. 

1447 :raises ValueError: If parameter ``destination`` is ``None``. 

1448 :raises TypeError: If parameter ``destination`` isn't of type :class:`Risk`. 

1449 :raises ValueError: If parameter ``destination`` is this risk. 

1450 :raises NotInSameRiskRegisterError: If parameter ``destination`` isn't part of this risk's risk register. 

1451 :raises ValueError: If parameter ``kind`` is ``None``. 

1452 :raises TypeError: If parameter ``kind`` isn't of type :class:`LinkKind`. 

1453 :raises DuplicateLinkError: If this risk already has a link of this kind to the destination. 

1454 

1455 .. seealso:: 

1456 

1457 :meth:`LinkFrom` 

1458 |rarr| Create an inbound link from the given risk to this risk. 

1459 """ 

1460 return Link(self, destination, kind, keyValuePairs) 

1461 

1462 def LinkFrom(self, source: Risk, kind: LinkKind, keyValuePairs: Nullable[Mapping[Hashable, Any]] = None) -> Link: 

1463 """ 

1464 Create an inbound link from the given risk to this risk. 

1465 

1466 :param source: The risk the link starts at. 

1467 :param kind: The relationship the link states. 

1468 :param keyValuePairs: Optional, the key-value pairs of the link. Default: ``None``. 

1469 :returns: The link from the source to this risk. 

1470 :raises ValueError: If parameter ``source`` is this risk. 

1471 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping. 

1472 :raises ValueError: If parameter ``source`` is ``None``. 

1473 :raises TypeError: If parameter ``source`` isn't of type :class:`Risk`. 

1474 :raises NotInSameRiskRegisterError: If parameter ``source`` isn't part of a risk register. 

1475 :raises NotInSameRiskRegisterError: If this risk isn't part of the source's risk register. 

1476 :raises ValueError: If parameter ``kind`` is ``None``. 

1477 :raises TypeError: If parameter ``kind`` isn't of type :class:`LinkKind`. 

1478 :raises DuplicateLinkError: If the source already has a link of this kind to this risk. 

1479 

1480 .. seealso:: 

1481 

1482 :meth:`LinkTo` 

1483 |rarr| Create an outbound link from this risk to the given risk. 

1484 """ 

1485 if source is self: 

1486 ex = ValueError("Parameter 'source' is this risk.") 

1487 ex.add_note(f"Got risk '{source._id}'.") 

1488 raise ex 

1489 

1490 return Link(source, self, kind, keyValuePairs) 

1491 

1492 def __repr__(self) -> str: 

1493 """ 

1494 Return a representation of the risk for debugging. 

1495 

1496 An open risk ``R1`` of medium probability and high impact reads ``<Risk R1: Open, probability Medium, 

1497 impact High, level Medium>``. 

1498 

1499 :returns: The risk's ID, state, probability, impact and level. 

1500 """ 

1501 return ( 

1502 f"<Risk {self._id}: {self._state.name}, probability {self._probability.name}, impact {self._impact.name}, " 

1503 f"level {self.Level.name}>" 

1504 ) 

1505 

1506 

1507@export 

1508class Link(KeyValuePairsMixin, metaclass=ExtendedType, slots=True): 

1509 """ 

1510 A link from a source risk to a destination risk of the same risk register, and the relationship it states. 

1511 

1512 A link connects two risks; it is no element of the risk register and has no parent. The source lists it in 

1513 :attr:`~Risk.OutboundLinks`, the destination in :attr:`~Risk.InboundLinks`, the risk register in 

1514 :attr:`~RiskManagement.Links`. 

1515 """ 

1516 

1517 _source: Risk #: The risk the link starts at. 

1518 _destination: Risk #: The risk the link ends at. 

1519 _kind: LinkKind #: The relationship the link states. 

1520 

1521 def __init__( 

1522 self, 

1523 source: Risk, 

1524 destination: Risk, 

1525 kind: LinkKind, 

1526 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None 

1527 ) -> None: 

1528 """ 

1529 Initializes a link, and adds it to its source, its destination and their risk register. 

1530 

1531 :param source: The risk the link starts at. 

1532 :param destination: The risk the link ends at. 

1533 :param kind: The relationship the link states. 

1534 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``. 

1535 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping. 

1536 :raises ValueError: If parameter ``source`` is ``None``. 

1537 :raises TypeError: If parameter ``source`` isn't of type :class:`Risk`. 

1538 :raises NotInSameRiskRegisterError: If parameter ``source`` isn't part of a risk register. 

1539 :raises ValueError: If parameter ``destination`` is ``None``. 

1540 :raises TypeError: If parameter ``destination`` isn't of type :class:`Risk`. 

1541 :raises ValueError: If parameter ``destination`` is the source. 

1542 :raises NotInSameRiskRegisterError: If parameter ``destination`` isn't part of the source's risk register. 

1543 :raises ValueError: If parameter ``kind`` is ``None``. 

1544 :raises TypeError: If parameter ``kind`` isn't of type :class:`LinkKind`. 

1545 :raises DuplicateLinkError: If the source already has a link of this kind to the destination. 

1546 """ 

1547 super().__init__(keyValuePairs) 

1548 

1549 if source is None: 

1550 raise ValueError("Parameter 'source' is None.") 

1551 elif not isinstance(source, Risk): 

1552 ex = TypeError("Parameter 'source' is not of type 'Risk'.") 

1553 ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.") 

1554 raise ex 

1555 elif source._root is None: 

1556 ex = NotInSameRiskRegisterError("Parameter 'source' isn't part of a risk register.") 

1557 ex.add_note(f"Got risk '{source._id}'.") 

1558 raise ex 

1559 

1560 if destination is None: 

1561 raise ValueError("Parameter 'destination' is None.") 

1562 elif not isinstance(destination, Risk): 

1563 ex = TypeError("Parameter 'destination' is not of type 'Risk'.") 

1564 ex.add_note(f"Got type '{getFullyQualifiedName(destination)}'.") 

1565 raise ex 

1566 elif destination is source: 

1567 ex = ValueError("Parameter 'destination' is the source.") 

1568 ex.add_note(f"Got risk '{destination._id}'.") 

1569 raise ex 

1570 elif destination._root is not source._root: 

1571 ex = NotInSameRiskRegisterError("Parameter 'destination' isn't part of the source's risk register.") 

1572 ex.add_note(f"Got risk '{destination._id}'.") 

1573 raise ex 

1574 

1575 if kind is None: 

1576 raise ValueError("Parameter 'kind' is None.") 

1577 elif not isinstance(kind, LinkKind): 

1578 ex = TypeError("Parameter 'kind' is not of type 'LinkKind'.") 

1579 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.") 

1580 raise ex 

1581 

1582 for link in source._outboundLinks: 

1583 if link._destination is destination and link._kind is kind: 

1584 raise DuplicateLinkError(f"Risk '{source._id}' already has a '{kind.name}' link to risk '{destination._id}'.") 

1585 

1586 self._source = source 

1587 self._destination = destination 

1588 self._kind = kind 

1589 

1590 source._outboundLinks.append(self) 

1591 destination._inboundLinks.append(self) 

1592 source._root._links.append(self) 

1593 

1594 

1595 @readonly 

1596 def Source(self) -> Risk: 

1597 """ 

1598 Read-only property to access the risk the link starts at (:attr:`_source`). 

1599 

1600 :returns: The source risk. 

1601 """ 

1602 return self._source 

1603 

1604 @readonly 

1605 def Destination(self) -> Risk: 

1606 """ 

1607 Read-only property to access the risk the link ends at (:attr:`_destination`). 

1608 

1609 :returns: The destination risk. 

1610 """ 

1611 return self._destination 

1612 

1613 @readonly 

1614 def Kind(self) -> LinkKind: 

1615 """ 

1616 Read-only property to access the relationship the link states (:attr:`_kind`). 

1617 

1618 :returns: The link's kind. 

1619 """ 

1620 return self._kind 

1621 

1622 def __repr__(self) -> str: 

1623 """ 

1624 Return a representation of the link for debugging. 

1625 

1626 A link stating risk ``R1`` causes risk ``R2`` reads ``<Link R1 Causes R2>``. 

1627 

1628 :returns: The IDs of the source and the destination, and the link's kind. 

1629 """ 

1630 return f"<Link {self._source._id} {self._kind.name} {self._destination._id}>"