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
« 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.
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.
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.
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.
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`.
50Every element has key-value pairs (:class:`~pyTooling.DataModel.KeyValuePairsMixin`) for data the model doesn't name.
51"""
52from __future__ import annotations
54from datetime import datetime
55from enum import Enum
56from typing import Any, Callable, ClassVar, Generator, Hashable, Iterable, Mapping, Optional as Nullable
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
65from pyEDAA.Reports import Person, ReportException
68@export
69class RiskManagementError(ReportException):
70 """Base-exception of the risk management model."""
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.
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 """
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."""
88@export
89class DuplicateRiskError(RiskManagementError, DuplicateElementError):
90 """The exception is raised when a risk register already contains a risk of the same ID."""
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."""
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."""
103@export
104class DuplicatePersonError(RiskManagementError, DuplicateElementError):
105 """The exception is raised when a risk register already contains a person of the same user ID."""
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."""
113@export
114class Probability(Enum):
115 """
116 The probability of a risk to occur.
117 """
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.
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.
131 @readonly
132 def Rank(self) -> int:
133 """
134 Read-only property to return the probability's rank on its ordinal scale.
136 :returns: The rank in range 1..4; ``1`` is the lowest probability.
137 """
138 return self.__RANKS[self.value]
141@export
142class Impact(Enum):
143 """
144 The impact of a risk, if it occurs, as the delay it causes.
145 """
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.
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.
159 @readonly
160 def Rank(self) -> int:
161 """
162 Read-only property to return the impact's rank on its ordinal scale.
164 :returns: The rank in range 1..4; ``1`` is the lowest impact.
165 """
166 return self.__RANKS[self.value]
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.
174 .. code-block:: text
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 """
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.
189@export
190class RiskState(Enum):
191 """
192 The state of a risk in its life cycle.
194 A risk ends either *solved* - its mitigations removed it - or *closed* - it isn't tracked anymore, without being
195 solved.
196 """
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.
203 DEFAULT = Open #: The state of a newly identified risk.
206@export
207class LinkKind(Enum):
208 """
209 The relationship a :class:`Link` states from its source risk to its destination risk.
210 """
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.
218 DEFAULT = Related #: The relationship of a link, which states no other.
221@export
222class MitigationKind(Enum):
223 """
224 The way a mitigation reduces a risk or an implication.
225 """
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.
232 DEFAULT = Prevent #: The way of a mitigation, which states no other.
235@export
236class MitigationState(Enum):
237 """
238 The state of a mitigation: whether it is applied.
240 A mitigation *on hold* waits to be planned; a new mitigation is *planned*.
241 """
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.
249 DEFAULT = Planned #: The state of a newly decided mitigation.
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 """
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.
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.
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)``.
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)
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
292 @property
293 def Parent(self) -> Nullable[RiskManagement]:
294 """
295 Property to access the risk register containing this element (:attr:`_parent`).
297 Assigning a risk register adds this element to it, and sets the register's root as the root of this element.
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
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
329 parent._AddElement(self)
330 self._parent = parent
331 self._root = parent._root
333 @readonly
334 def Root(self) -> Nullable[RiskManagement]:
335 """
336 Read-only property to access the risk register's root (:attr:`_root`).
338 The root is maintained by :attr:`Parent`: assigning a parent sets it.
340 :returns: The root, or ``None`` while the element isn't part of a risk register.
341 """
342 return self._root
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.
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 """
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.
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.
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)
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
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.")
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 = {}
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
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.
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
441 if element._id in elements:
442 raise exception(f"{kind} '{element._id}' already exists in the risk register.")
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
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
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
466 elements[element._id] = element
468 @readonly
469 def Version(self) -> SemanticVersion:
470 """
471 Read-only property to access the version of the risk register (:attr:`_version`).
473 :returns: The version.
474 """
475 return self._version
477 @readonly
478 def Name(self) -> Nullable[str]:
479 """
480 Read-only property to access the name of the risk register (:attr:`_name`).
482 :returns: The name, or ``None`` if none is given.
483 """
484 return self._name
486 @readonly
487 def Risks(self) -> dict[str, Risk]:
488 """
489 Read-only property to access the risks (:attr:`_risks`).
491 :returns: The risks, by ID.
492 """
493 return self._risks
495 @readonly
496 def Implications(self) -> dict[str, Implication]:
497 """
498 Read-only property to access the implications (:attr:`_implications`).
500 :returns: The implications, by ID.
501 """
502 return self._implications
504 @readonly
505 def Mitigations(self) -> dict[str, Mitigation]:
506 """
507 Read-only property to access the mitigations (:attr:`_mitigations`).
509 :returns: The mitigations, by ID.
510 """
511 return self._mitigations
513 @readonly
514 def Links(self) -> list[Link]:
515 """
516 Read-only property to access the links between the risks (:attr:`_links`).
518 :returns: The links, in the order they were added.
519 """
520 return self._links
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`).
527 A risk's owner is one of them.
529 :returns: The persons, by user ID.
530 """
531 return self._persons
533 def AddPerson(self, person: Person) -> None:
534 """
535 Add a person the risk register refers to.
537 A risk's owner must be added first. The same person may be added to several risk registers.
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.")
553 self._persons[person._userID] = person
555 def IterateRisks(self, predicate: Nullable[Callable[[Risk], bool]] = None) -> Generator[Risk, None, None]:
556 """
557 Iterate all or selected risks.
559 If parameter ``predicate`` is not None, the given filter function is used to skip risks in the generator.
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
571 def IterateImplications(
572 self,
573 predicate: Nullable[Callable[[Implication], bool]] = None
574 ) -> Generator[Implication, None, None]:
575 """
576 Iterate all or selected implications.
578 If parameter ``predicate`` is not None, the given filter function is used to skip implications in the generator.
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
590 def IterateMitigations(
591 self,
592 predicate: Nullable[Callable[[Mitigation], bool]] = None
593 ) -> Generator[Mitigation, None, None]:
594 """
595 Iterate all or selected mitigations.
597 If parameter ``predicate`` is not None, the given filter function is used to skip mitigations in the generator.
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
609 def IterateLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[Link, None, None]:
610 """
611 Iterate all or selected links between the risks.
613 If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator.
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
625 def __repr__(self) -> str:
626 """
627 Return a representation of the risk register for debugging.
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>``.
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 )
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 """
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.
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.
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)
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.")
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.")
694 self._id = identifier
695 self._description = description
697 @readonly
698 def ID(self) -> str:
699 """
700 Read-only property to access the element's ID (:attr:`_id`).
702 :returns: The ID.
703 """
704 return self._id
706 @readonly
707 def Description(self) -> str:
708 """
709 Read-only property to access the element's description (:attr:`_description`).
711 :returns: The description.
712 """
713 return self._description
716@export
717class ImplicationsMixin(metaclass=ExtendedType, mixin=True):
718 """
719 A mixin-class adding implications to an element: the consequences the element has.
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 """
725 _implications: dict[str, Implication] #: The implications, by ID.
727 def __init__(self, implications: Nullable[Iterable[Implication]] = None) -> None:
728 """
729 Initializes the implications.
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 = {}
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
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
769 self._implications[implication._id] = implication
771 @readonly
772 def Implications(self) -> dict[str, Implication]:
773 """
774 Read-only property to access the implications (:attr:`_implications`).
776 :returns: The implications, by ID.
777 """
778 return self._implications
780 def AddImplication(self, implication: Implication) -> None:
781 """
782 Add an implication.
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
805 self._implications[implication._id] = implication
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.
813 An element of a risk register references only mitigations of the same register.
814 """
816 _mitigations: dict[str, Mitigation] #: The mitigations, by ID.
818 def __init__(self, mitigations: Nullable[Iterable[Mitigation]] = None) -> None:
819 """
820 Initializes the mitigations.
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 = {}
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
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
860 self._mitigations[mitigation._id] = mitigation
862 @readonly
863 def Mitigations(self) -> dict[str, Mitigation]:
864 """
865 Read-only property to access the mitigations (:attr:`_mitigations`).
867 :returns: The mitigations, by ID.
868 """
869 return self._mitigations
871 def AddMitigation(self, mitigation: Mitigation) -> None:
872 """
873 Add a mitigation.
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
896 self._mitigations[mitigation._id] = mitigation
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.
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 """
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.
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)
957 if parent is not None:
958 parent._AddElement(self)
960 def __repr__(self) -> str:
961 """
962 Return a representation of the implication for debugging.
964 An implication ``I1`` with 2 implications and 1 mitigation reads ``<Implication I1: 2 implications,
965 1 mitigations>``.
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>"
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.
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 """
981 _kind: MitigationKind #: The way the mitigation reduces a risk or an implication.
982 _state: MitigationState #: Whether the mitigation is applied.
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.
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)
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
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
1046 self._kind = kind
1047 self._state = state
1049 if parent is not None:
1050 parent._AddElement(self)
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`).
1057 :returns: The mitigation's kind.
1058 """
1059 return self._kind
1061 @property
1062 def State(self) -> MitigationState:
1063 """
1064 Property to access whether the mitigation is applied (:attr:`_state`).
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
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
1081 self._state = value
1083 def __repr__(self) -> str:
1084 """
1085 Return a representation of the mitigation for debugging.
1087 A planned, preventing mitigation ``M1`` with 1 side effect reads ``<Mitigation M1: Prevent, Planned,
1088 1 implications>``.
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>"
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
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.
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 """
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.
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.
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)
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.")
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
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
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
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
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
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 = []
1251 if parent is not None:
1252 parent._AddElement(self)
1254 @readonly
1255 def Title(self) -> str:
1256 """
1257 Read-only property to access the risk's title (:attr:`_title`).
1259 :returns: The title.
1260 """
1261 return self._title
1263 @property
1264 def Probability(self) -> Probability:
1265 """
1266 Property to access the probability the risk occurs (:attr:`_probability`).
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
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
1283 self._probability = value
1285 @property
1286 def Impact(self) -> Impact:
1287 """
1288 Property to access the impact of the risk, if it occurs (:attr:`_impact`).
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
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
1305 self._impact = value
1307 @property
1308 def State(self) -> RiskState:
1309 """
1310 Property to access the state of the risk in its life cycle (:attr:`_state`).
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
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
1327 self._state = value
1329 @property
1330 def Owner(self) -> Nullable[Person]:
1331 """
1332 Property to access the person responsible for the risk (:attr:`_owner`).
1334 The owner of a risk in a risk register is a person of that register (:attr:`RiskManagement.Persons`).
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
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
1356 self._owner = value
1358 @property
1359 def NextReview(self) -> Nullable[datetime]:
1360 """
1361 Property to access the date of the risk's next review (:attr:`_nextReview`).
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
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
1375 self._nextReview = value
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.
1382 :returns: The score in range 1..16.
1383 """
1384 return self._probability.Rank * self._impact.Rank
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`).
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
1401 return RiskLevel.Low
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.
1409 The implications of a mitigation are its side effects. An implication reached several times is returned once.
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)
1417 return implications
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`).
1424 :returns: The links, in the order they were added.
1425 """
1426 return self._outboundLinks
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`).
1433 :returns: The links, in the order they were added.
1434 """
1435 return self._inboundLinks
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.
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.
1455 .. seealso::
1457 :meth:`LinkFrom`
1458 |rarr| Create an inbound link from the given risk to this risk.
1459 """
1460 return Link(self, destination, kind, keyValuePairs)
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.
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.
1480 .. seealso::
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
1490 return Link(source, self, kind, keyValuePairs)
1492 def __repr__(self) -> str:
1493 """
1494 Return a representation of the risk for debugging.
1496 An open risk ``R1`` of medium probability and high impact reads ``<Risk R1: Open, probability Medium,
1497 impact High, level Medium>``.
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 )
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.
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 """
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.
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.
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)
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
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
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
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}'.")
1586 self._source = source
1587 self._destination = destination
1588 self._kind = kind
1590 source._outboundLinks.append(self)
1591 destination._inboundLinks.append(self)
1592 source._root._links.append(self)
1595 @readonly
1596 def Source(self) -> Risk:
1597 """
1598 Read-only property to access the risk the link starts at (:attr:`_source`).
1600 :returns: The source risk.
1601 """
1602 return self._source
1604 @readonly
1605 def Destination(self) -> Risk:
1606 """
1607 Read-only property to access the risk the link ends at (:attr:`_destination`).
1609 :returns: The destination risk.
1610 """
1611 return self._destination
1613 @readonly
1614 def Kind(self) -> LinkKind:
1615 """
1616 Read-only property to access the relationship the link states (:attr:`_kind`).
1618 :returns: The link's kind.
1619 """
1620 return self._kind
1622 def __repr__(self) -> str:
1623 """
1624 Return a representation of the link for debugging.
1626 A link stating risk ``R1`` causes risk ``R2`` reads ``<Link R1 Causes R2>``.
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}>"