Coverage for pyEDAA/Reports/Requirements/__init__.py: 98%
837 statements
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-14 02:26 +0000
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-14 02:26 +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 requirements management.
34A :class:`RequirementSpecification` is the root: it registers the persons, personas, labels, documents and layers its
35items refer to, and every requirement and specification item by its ID.
37A :class:`Requirement` has a title, a description, a creation time, a state, a kind and an obligation. It names its
38authors and owners (:class:`~pyEDAA.Reports.Person`), the personas it is about (:class:`Persona`), its labels
39(:class:`Label`), the documents attached to it (:class:`Document`), and the layer it belongs to (:class:`Layer`) - all
40registered in the same specification. A requirement contains child requirements, which refine it.
42A :class:`SpecificationItem` mirrors a requirement: it states how the system meets requirements. Specification items and
43requirements are in an N:M relation.
45An item's :class:`Link` states a relationship to another item, e.g. to its counterpart in another layer. An item's
46history lists the events of its life cycle (:class:`HistoryEvent`), e.g. a split into other requirements.
47"""
48from __future__ import annotations
50from datetime import datetime
51from enum import Enum, Flag
52from typing import Any, Callable, ClassVar, Generator, Hashable, Iterable, Mapping, \
53 Optional as Nullable, Self
55from pyTooling.Common import getFullyQualifiedName, getTypeName
56from pyTooling.DataModel import KeyValuePairsMixin
57from pyTooling.Decorators import export, readonly
58from pyTooling.Exceptions import AlreadyInHierarchyError, DuplicateElementError, NotInSameHierarchyError
59from pyTooling.MetaClasses import ExtendedType
60from pyTooling.Versioning import SemanticVersion
62from pyEDAA.Reports import Person, ReportException
65@export
66class RequirementsError(ReportException):
67 """Base-exception of the requirements model."""
70@export
71class NotInSameSpecificationError(RequirementsError, NotInSameHierarchyError):
72 """
73 The exception is raised when an element is used together with an element or a person of another specification.
75 The element or person itself is valid, only its ownership is wrong: add it to the specification first. E.g. a link
76 from a requirement to a requirement of another specification.
77 """
80@export
81class AlreadyInSpecificationError(RequirementsError, AlreadyInHierarchyError):
82 """The exception is raised when an element is added to a specification or an item, but it already has a parent."""
85@export
86class DuplicatePersonaError(RequirementsError, DuplicateElementError):
87 """The exception is raised when a specification or an item already contains a persona of the same ID."""
90@export
91class DuplicateLabelError(RequirementsError, DuplicateElementError):
92 """The exception is raised when a specification or an item already contains a label of the same ID."""
95@export
96class DuplicateDocumentError(RequirementsError, DuplicateElementError):
97 """The exception is raised when a specification or an item already contains a document of the same ID."""
100@export
101class DuplicateLayerError(RequirementsError, DuplicateElementError):
102 """The exception is raised when a specification already contains a layer of the same ID."""
105@export
106class DuplicatePersonError(RequirementsError, DuplicateElementError):
107 """The exception is raised when a specification or an item already contains a person of the same user ID."""
110@export
111class DuplicateRequirementError(RequirementsError, DuplicateElementError):
112 """The exception is raised when a specification or an item already contains a requirement of the same ID."""
115@export
116class DuplicateSpecificationItemError(RequirementsError, DuplicateElementError):
117 """The exception is raised when a specification or an item already contains a specification item of the same ID."""
120@export
121class DuplicateLinkError(RequirementsError, DuplicateElementError):
122 """The exception is raised when an item already has a link of the same kind to the same destination."""
125@export
126class ItemState(Enum):
127 """
128 The state of an item in its life cycle.
130 An item is open from :attr:`Placeholder` over :attr:`Draft`, :attr:`InReview`, :attr:`Final` and :attr:`Implemented`
131 to :attr:`Verified`. It ends :attr:`Closed` after its verification, or :attr:`Rejected` or :attr:`Obsolete` at any
132 time.
133 """
135 Placeholder = 1 #: Reserved, e.g. an ID and a title, but not yet written.
136 Draft = 2 #: Written, but not yet reviewed.
137 InReview = 3 #: Under review by its stakeholders.
138 Final = 4 #: Reviewed and agreed by its stakeholders; it binds the design.
139 Implemented = 5 #: Implemented, but not yet verified.
140 Verified = 6 #: Implemented and verified; open for a regression.
141 Closed = 7 #: Verified and released, e.g. in a baseline; it changes no more.
142 Rejected = 8 #: Not accepted by its stakeholders.
143 Obsolete = 9 #: No longer valid, e.g. replaced by another one, split or merged.
146@export
147class RequirementKind(Enum):
148 """
149 The kind of a requirement: what it constrains.
151 The kinds follow the requirement types of ISO/IEC/IEEE 29148, with safety and security as kinds of their own, as
152 functional safety and security standards treat them separately.
153 """
155 Functional = 1 #: A function the system performs, e.g. a register map or a protocol's behavior.
156 Performance = 2 #: How well a function is performed, e.g. a clock frequency, latency or throughput.
157 Quality = 3 #: A quality attribute, e.g. reliability, maintainability or usability.
158 Safety = 4 #: A measure against hazards, e.g. a safety mechanism of ISO 26262 or IEC 61508.
159 Security = 5 #: A measure against threats, e.g. encrypted bitstreams or secure boot.
160 Interface = 6 #: An interface to another system, e.g. a pin-out, a bus or a signal level.
161 Environmental = 7 #: An operating condition, e.g. a temperature range, vibration or EMC.
162 Constraint = 8 #: A design or implementation constraint, e.g. an FPGA family or a language.
163 Regulatory = 9 #: A standard, law or certification the system complies with.
164 Other = 10 #: Any other requirement.
167@export
168class Obligation(Enum):
169 """
170 The obligation of a requirement: whether it must, should or may be met.
172 The levels are those of the keywords of ISO/IEC/IEEE 29148 (*shall*, *should*, *may*) and of RFC 2119 (*MUST*,
173 *SHOULD*, *MAY*). A negated requirement (*shall not*) has the obligation of its positive keyword; the negation is part
174 of its text.
175 """
177 Mandatory = 1 #: Binding: the requirement must be met (*shall*, *must*).
178 Recommended = 2 #: A goal: the requirement should be met, deviations are justified (*should*).
179 Optional = 3 #: A permission: the requirement may be met (*may*).
182# A class with a property named like a class - ``Obligation`` - can't name that class in the annotation of a field: the
183# class body's namespace, where annotations are evaluated, binds the name to the property.
184_Obligation = Obligation
187@export
188class VerificationMethod(Flag):
189 """
190 The methods verifying a requirement, as flags.
192 The methods follow the verification methods of ISO/IEC/IEEE 29148 - inspection, analysis, demonstration and test -,
193 with simulation and formal verification as methods of their own, as hardware designs are verified by them.
194 """
196 Unknown = 0 #: Not decided yet.
197 Inspection = 1 #: Examining the item, e.g. reviewing a schematic or source code.
198 Analysis = 2 #: Calculating or modeling, e.g. a timing analysis or a power budget.
199 Demonstration = 4 #: Operating the system without measurements, e.g. showing a feature works.
200 Test = 8 #: Operating the system with measurements under defined conditions, e.g. on a test bench.
201 Simulation = 16 #: Simulating a model of the design, e.g. a VHDL testbench.
202 Formal = 32 #: Proving properties of the design, e.g. by formal property checking.
205@export
206class Base(KeyValuePairsMixin, metaclass=ExtendedType, slots=True):
207 """
208 Base-class of every element of the requirements model: key-value pairs, a reference to the element containing it, and
209 a reference to the specification it is part of.
210 """
212 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: The types a parent may have; set by each class with a parent.
214 _parent: Nullable[Base] #: The element containing this one, or ``None``.
215 _root: Nullable[RequirementSpecification] #: The specification, or ``None`` while the element isn't in one.
217 def __init__(self, *, keyValuePairs: Nullable[Mapping[Hashable, Any]] = None, parent: Nullable[Base] = None) -> None:
218 """
219 Initializes an element with its key-value pairs, its parent, and the parent's specification.
221 The parent doesn't list the element yet: the class setting the field the parent stores it by - an ID - adds it with
222 ``parent._AddElement(self)``.
224 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
225 :param parent: Optional, the element containing this one. Default: ``None``.
226 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
227 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`.
228 """
229 super().__init__(keyValuePairs)
231 if parent is None:
232 self._parent = None
233 self._root = None
234 elif not isinstance(parent, self._PARENT_TYPE):
235 typeNames = " or ".join(f"'{parentType.__name__}'" for parentType in self._PARENT_TYPE)
236 ex = TypeError(f"Parameter 'parent' is not of type {typeNames}.")
237 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
238 raise ex
239 else:
240 self._parent = parent
241 self._root = parent._root
243 @property
244 def Parent(self) -> Nullable[Base]:
245 """
246 Property to access the element containing this one (:attr:`_parent`).
248 Assigning a parent to an element without one adds the element to it, and sets the parent's specification as the
249 specification of this element and of every element below it.
251 :returns: The parent, or ``None``.
252 :raises ValueError: If ``None`` is assigned.
253 :raises TypeError: If the assigned parent isn't of a type this class declares in
254 :attr:`_PARENT_TYPE`.
255 :raises TypeError: If a parent is assigned to a :class:`RequirementSpecification`, which has
256 no
257 parent.
258 :raises AlreadyInSpecificationError: If the element already has a parent.
259 :raises DuplicatePersonaError: If the element is a persona, and the specification already contains a
260 persona of its ID.
261 :raises DuplicateLabelError: If the element is a label, and the specification already contains a label
262 of its ID.
263 :raises DuplicateDocumentError: If the element is a document, and the specification already contains a
264 document of its ID.
265 :raises DuplicateLayerError: If the element is a layer, and the specification already contains a layer
266 of its ID.
267 :raises DuplicateRequirementError: If the specification already contains a requirement of an ID of this
268 requirement or of a requirement below it.
269 :raises DuplicateSpecificationItemError: If the specification already contains a specification item of an ID of this
270 item or of an item below it.
271 """
272 return self._parent
274 @Parent.setter
275 def Parent(self, parent: Base) -> None:
276 if parent is None:
277 raise ValueError("Parameter 'parent' is None.")
278 elif not isinstance(parent, self._PARENT_TYPE):
279 typeNames = " or ".join(f"'{parentType.__name__}'" for parentType in self._PARENT_TYPE)
280 ex = TypeError(f"Parameter 'parent' is not of type {typeNames}.")
281 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
282 raise ex
284 if self._parent is not None:
285 raise AlreadyInSpecificationError(f"{getTypeName(self)} already has a parent.")
287 parent._AddElement(self)
288 self._parent = parent
289 self._root = parent._root
291 @readonly
292 def Root(self) -> Nullable[RequirementSpecification]:
293 """
294 Read-only property to access the specification this element is part of (:attr:`_root`).
296 The specification is maintained by :attr:`Parent`: assigning a parent sets it for this element and every element
297 below it.
299 :returns: The specification, or ``None`` while the element isn't part of one.
300 """
301 return self._root
304@export
305class BaseWithID(Base):
306 """
307 Base-class of the elements of the requirements model, which have an ID and a description.
309 The ID is unique among the elements of the same class in a specification.
310 """
312 _DUPLICATE_ERROR: ClassVar[type[RequirementsError]] #: The error a second element of an ID raises; set by each class.
314 _id: str #: The element's ID.
315 _description: str #: The element's description, or ``""``.
317 def __init__(
318 self,
319 identifier: str,
320 *,
321 description: str = "",
322 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None,
323 parent: Nullable[Base] = None
324 ) -> None:
325 """
326 Initializes an element with its ID, its description, its key-value pairs and its parent.
328 :param identifier: The element's ID.
329 :param description: Optional, the element's description. Default: ``""``.
330 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
331 :param parent: Optional, the element containing this one. Default: ``None``.
332 :raises ValueError: If parameter ``identifier`` is ``None``.
333 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`.
334 :raises ValueError: If parameter ``identifier`` is empty.
335 :raises ValueError: If parameter ``description`` is ``None``.
336 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
337 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
338 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`.
339 """
340 super().__init__(keyValuePairs=keyValuePairs, parent=parent)
342 if identifier is None:
343 raise ValueError("Parameter 'identifier' is None.")
344 elif not isinstance(identifier, str):
345 ex = TypeError("Parameter 'identifier' is not of type 'str'.")
346 ex.add_note(f"Got type '{getFullyQualifiedName(identifier)}'.")
347 raise ex
348 elif identifier == "":
349 raise ValueError("Parameter 'identifier' is empty.")
351 if description is None:
352 raise ValueError("Parameter 'description' is None.")
353 elif not isinstance(description, str):
354 ex = TypeError("Parameter 'description' is not of type 'str'.")
355 ex.add_note(f"Got type '{getFullyQualifiedName(description)}'.")
356 raise ex
358 self._id = identifier
359 self._description = description
361 @readonly
362 def ID(self) -> str:
363 """
364 Read-only property to access the element's ID (:attr:`_id`).
366 :returns: The ID.
367 """
368 return self._id
370 @readonly
371 def Description(self) -> str:
372 """
373 Read-only property to access the element's description (:attr:`_description`).
375 :returns: The description, or ``""``.
376 """
377 return self._description
380@export
381class RequirementSpecification(Base):
382 """
383 The root of the requirements model: the requirements and specification items of a system, and the personas, labels,
384 documents and layers they refer to.
386 It registers every requirement and every specification item by its ID - also a child item -, and is its own
387 specification.
389 Persons have no parent: the specification registers the persons its items refer to by their user IDs, and a person may
390 be registered in several specifications.
391 """
393 _name: Nullable[str] #: The specification's name, or ``None``.
394 _version: SemanticVersion #: The version of the specification.
395 _persons: dict[str, Person] #: The persons, by user ID.
396 _personas: dict[str, Persona] #: The personas, by ID.
397 _labels: dict[str, Label] #: The labels, by ID.
398 _documents: dict[str, Document] #: The documents, by ID.
399 _layers: dict[str, Layer] #: The layers, by ID.
400 _requirements: dict[str, Requirement] #: Every requirement, by ID, in the order they were added.
401 _specificationItems: dict[str, SpecificationItem] #: Every specification item, by ID, in the order they were added.
403 def __init__(
404 self,
405 version: SemanticVersion,
406 name: Nullable[str] = None,
407 *,
408 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None
409 ) -> None:
410 """
411 Initializes an empty requirement specification.
413 :param version: The version of the specification.
414 :param name: Optional, the specification's name. Default: ``None``.
415 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
416 :raises ValueError: If parameter ``version`` is ``None``.
417 :raises TypeError: If parameter ``version`` isn't of type :class:`~pyTooling.Versioning.SemanticVersion`.
418 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
419 :raises ValueError: If parameter ``name`` is empty.
420 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
421 """
422 super().__init__(keyValuePairs=keyValuePairs)
424 if version is None:
425 raise ValueError("Parameter 'version' is None.")
426 elif not isinstance(version, SemanticVersion):
427 ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.")
428 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
429 raise ex
431 if name is not None:
432 if not isinstance(name, str):
433 ex = TypeError("Parameter 'name' is not of type 'str'.")
434 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
435 raise ex
436 elif name == "":
437 raise ValueError("Parameter 'name' is empty.")
439 self._root = self
440 self._name = name
441 self._version = version
442 self._persons = {}
443 self._personas = {}
444 self._labels = {}
445 self._documents = {}
446 self._layers = {}
447 self._requirements = {}
448 self._specificationItems = {}
450 @Base.Parent.setter
451 def Parent(self, parent: None) -> None:
452 ex = TypeError(f"A '{getFullyQualifiedName(self)}' has no parent.")
453 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
454 raise ex
456 def _AddElement(self, element: BaseWithID) -> None:
457 """
458 Register a persona, label, document, layer, top-level requirement or top-level specification item, which names the
459 specification as its parent.
461 :param element: The element.
462 :raises DuplicatePersonaError: If the specification already contains a persona of the element's ID.
463 :raises DuplicateLabelError: If the specification already contains a label of the element's ID.
464 :raises DuplicateDocumentError: If the specification already contains a document of the element's ID.
465 :raises DuplicateLayerError: If the specification already contains a layer of the element's ID.
466 :raises DuplicateRequirementError: If the specification already contains a requirement of the ID of the
467 requirement or of a requirement below it.
468 :raises DuplicateSpecificationItemError: If the specification already contains a specification item of the ID of the
469 item or of an item below it.
470 """
471 if isinstance(element, BaseItem):
472 self._AddItems(element)
473 return
475 if isinstance(element, Persona):
476 elements = self._personas
477 elif isinstance(element, Label):
478 elements = self._labels
479 elif isinstance(element, Document):
480 elements = self._documents
481 else:
482 elements = self._layers
484 if element._id in elements:
485 raise element._DUPLICATE_ERROR(
486 f"{getTypeName(element)} '{element._id}' already exists in the specification."
487 )
489 elements[element._id] = element
491 def _AddItems(self, item: BaseItem) -> None:
492 """
493 Register an item and every item below it by their IDs, and set the specification as their specification and the
494 specification of their history events.
496 Either all of them are registered, or none.
498 :param item: The item joining the specification.
499 :raises DuplicateRequirementError: If the item is a requirement, and an ID of these requirements is registered
500 already, or used twice by them.
501 :raises DuplicateSpecificationItemError: If the item is a specification item, and an ID of these items is registered
502 already, or used twice by them.
503 """
504 items = self._requirements if isinstance(item, Requirement) else self._specificationItems
505 ids = set()
506 subtree = (item, *item.IterateDescendants())
507 for element in subtree:
508 if element._id in items or element._id in ids:
509 raise element._DUPLICATE_ERROR(
510 f"{getTypeName(element)} '{element._id}' already exists in the specification."
511 )
513 ids.add(element._id)
515 for element in subtree:
516 element._root = self
517 items[element._id] = element
518 for event in element._history:
519 event._root = self
521 @readonly
522 def Name(self) -> Nullable[str]:
523 """
524 Read-only property to access the specification's name (:attr:`_name`).
526 :returns: The name, or ``None``.
527 """
528 return self._name
530 @readonly
531 def Version(self) -> SemanticVersion:
532 """
533 Read-only property to access the version of the specification (:attr:`_version`).
535 :returns: The version.
536 """
537 return self._version
539 @readonly
540 def Persons(self) -> dict[str, Person]:
541 """
542 Read-only property to access the persons (:attr:`_persons`).
544 :returns: The persons, by user ID.
545 """
546 return self._persons
548 def IteratePersons(self, predicate: Nullable[Callable[[Person], bool]] = None) -> Generator[Person, None, None]:
549 """
550 Iterate all or selected persons.
552 If parameter ``predicate`` is not None, the given filter function is used to skip persons in the generator.
554 :param predicate: Optional, filter function accepting a person and returning a boolean. Default: ``None``.
555 :returns: A generator of the persons, in the order they were added.
556 """
557 if predicate is None:
558 yield from self._persons.values()
559 else:
560 for person in self._persons.values():
561 if predicate(person): 561 ↛ 562line 561 didn't jump to line 562 because the condition on line 561 was never true
562 yield person
564 def AddPerson(self, person: Person) -> None:
565 """
566 Register a person, whom the specification's items can refer to.
568 E.g. an author or an owner of requirements. A person has no parent: the same person may be registered in several
569 specifications.
571 :param person: The person.
572 :raises ValueError: If parameter ``person`` is ``None``.
573 :raises TypeError: If parameter ``person`` isn't of type :class:`~pyEDAA.Reports.Person`.
574 :raises DuplicatePersonError: If the specification already contains a person of this user ID.
575 """
576 if person is None:
577 raise ValueError("Parameter 'person' is None.")
578 elif not isinstance(person, Person):
579 ex = TypeError("Parameter 'person' is not of type 'Person'.")
580 ex.add_note(f"Got type '{getFullyQualifiedName(person)}'.")
581 raise ex
582 elif person._userID in self._persons:
583 raise DuplicatePersonError(f"Person '{person._userID}' already exists in the specification.")
585 self._persons[person._userID] = person
587 @readonly
588 def Personas(self) -> dict[str, Persona]:
589 """
590 Read-only property to access the personas (:attr:`_personas`).
592 :returns: The personas, by ID.
593 """
594 return self._personas
596 def IteratePersonas(self, predicate: Nullable[Callable[[Persona], bool]] = None) -> Generator[Persona, None, None]:
597 """
598 Iterate all or selected personas.
600 If parameter ``predicate`` is not None, the given filter function is used to skip personas in the generator.
602 :param predicate: Optional, filter function accepting a persona and returning a boolean. Default: ``None``.
603 :returns: A generator of the personas, in the order they were added.
604 """
605 if predicate is None:
606 yield from self._personas.values()
607 else:
608 for persona in self._personas.values():
609 if predicate(persona):
610 yield persona
612 @readonly
613 def Labels(self) -> dict[str, Label]:
614 """
615 Read-only property to access the labels (:attr:`_labels`).
617 :returns: The labels, by ID.
618 """
619 return self._labels
621 def IterateLabels(self, predicate: Nullable[Callable[[Label], bool]] = None) -> Generator[Label, None, None]:
622 """
623 Iterate all or selected labels.
625 If parameter ``predicate`` is not None, the given filter function is used to skip labels in the generator.
627 :param predicate: Optional, filter function accepting a label and returning a boolean. Default: ``None``.
628 :returns: A generator of the labels, in the order they were added.
629 """
630 if predicate is None:
631 yield from self._labels.values()
632 else:
633 for label in self._labels.values():
634 if predicate(label):
635 yield label
637 @readonly
638 def Documents(self) -> dict[str, Document]:
639 """
640 Read-only property to access the documents (:attr:`_documents`).
642 :returns: The documents, by ID.
643 """
644 return self._documents
646 def IterateDocuments(self, predicate: Nullable[Callable[[Document], bool]] = None) -> Generator[Document, None, None]:
647 """
648 Iterate all or selected documents.
650 If parameter ``predicate`` is not None, the given filter function is used to skip documents in the generator.
652 :param predicate: Optional, filter function accepting a document and returning a boolean. Default: ``None``.
653 :returns: A generator of the documents, in the order they were added.
654 """
655 if predicate is None:
656 yield from self._documents.values()
657 else:
658 for document in self._documents.values():
659 if predicate(document):
660 yield document
662 @readonly
663 def Layers(self) -> dict[str, Layer]:
664 """
665 Read-only property to access the layers (:attr:`_layers`).
667 :returns: The layers, by ID.
668 """
669 return self._layers
671 def IterateLayers(self, predicate: Nullable[Callable[[Layer], bool]] = None) -> Generator[Layer, None, None]:
672 """
673 Iterate all or selected layers.
675 If parameter ``predicate`` is not None, the given filter function is used to skip layers in the generator.
677 :param predicate: Optional, filter function accepting a layer and returning a boolean. Default: ``None``.
678 :returns: A generator of the layers, in the order they were added.
679 """
680 if predicate is None:
681 yield from self._layers.values()
682 else:
683 for layer in self._layers.values():
684 if predicate(layer):
685 yield layer
687 @readonly
688 def Requirements(self) -> dict[str, Requirement]:
689 """
690 Read-only property to access every requirement (:attr:`_requirements`), also the child requirements.
692 :returns: The requirements, by ID, in the order they were added.
693 """
694 return self._requirements
696 @readonly
697 def SpecificationItems(self) -> dict[str, SpecificationItem]:
698 """
699 Read-only property to access every specification item (:attr:`_specificationItems`), also the child items.
701 :returns: The specification items, by ID, in the order they were added.
702 """
703 return self._specificationItems
705 def IterateRequirements(
706 self,
707 predicate: Nullable[Callable[[Requirement], bool]] = None
708 ) -> Generator[Requirement, None, None]:
709 """
710 Iterate all or selected requirements, also the child requirements, in the order they were added.
712 If parameter ``predicate`` is not None, the given filter function is used to skip requirements in the generator.
714 :param predicate: Optional, filter function accepting a requirement and returning a boolean. Default: ``None``.
715 :returns: A generator of the requirements.
716 """
717 if predicate is None:
718 yield from self._requirements.values()
719 else:
720 for requirement in self._requirements.values():
721 if predicate(requirement):
722 yield requirement
724 def IterateSpecificationItems(
725 self,
726 predicate: Nullable[Callable[[SpecificationItem], bool]] = None
727 ) -> Generator[SpecificationItem, None, None]:
728 """
729 Iterate all or selected specification items, also the child items, in the order they were added.
731 If parameter ``predicate`` is not None, the given filter function is used to skip items in the generator.
733 :param predicate: Optional, filter function accepting a specification item and returning a boolean. Default:
734 ``None``.
735 :returns: A generator of the specification items.
736 """
737 if predicate is None:
738 yield from self._specificationItems.values()
739 else:
740 for specification in self._specificationItems.values():
741 if predicate(specification):
742 yield specification
745@export
746class BaseWithName(BaseWithID):
747 """
748 Base-class of the elements a specification registers for its requirements to refer to: an ID, a name and a
749 description.
751 Its parent is the :class:`RequirementSpecification`.
752 """
754 _PARENT_TYPE: ClassVar[tuple[type, ...]] = (RequirementSpecification, ) #: The element is in a specification.
756 _name: str #: The element's name.
758 def __init__(
759 self,
760 identifier: str,
761 name: str,
762 *,
763 description: str = "",
764 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None,
765 parent: Nullable[RequirementSpecification] = None
766 ) -> None:
767 """
768 Initializes an element with its ID, name and description, and registers it in its specification.
770 :param identifier: The element's ID.
771 :param name: The element's name.
772 :param description: Optional, the element's description. Default: ``""``.
773 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
774 :param parent: Optional, the specification. Default: ``None``.
775 :raises ValueError: If parameter ``identifier`` is ``None``.
776 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`.
777 :raises ValueError: If parameter ``identifier`` is empty.
778 :raises ValueError: If parameter ``name`` is ``None``.
779 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
780 :raises ValueError: If parameter ``name`` is empty.
781 :raises ValueError: If parameter ``description`` is ``None``.
782 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
783 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
784 :raises TypeError: If parameter ``parent`` isn't of type :class:`RequirementSpecification`.
785 :raises DuplicatePersonaError: If the element is a persona, and the specification already contains a persona of
786 this ID.
787 :raises DuplicateLabelError: If the element is a label, and the specification already contains a label of this
788 ID.
789 :raises DuplicateDocumentError: If the element is a document, and the specification already contains a document of
790 this ID.
791 :raises DuplicateLayerError: If the element is a layer, and the specification already contains a layer of this
792 ID.
793 """
794 super().__init__(identifier, description=description, keyValuePairs=keyValuePairs, parent=parent)
796 if name is None:
797 raise ValueError("Parameter 'name' is None.")
798 elif not isinstance(name, str):
799 ex = TypeError("Parameter 'name' is not of type 'str'.")
800 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
801 raise ex
802 elif name == "":
803 raise ValueError("Parameter 'name' is empty.")
805 self._name = name
807 if parent is not None:
808 parent._AddElement(self)
810 @readonly
811 def Name(self) -> str:
812 """
813 Read-only property to access the element's name (:attr:`_name`).
815 :returns: The name.
816 """
817 return self._name
820@export
821class Persona(BaseWithName):
822 """
823 A persona: a role a user of the system acts in, like the role of a use case or a user story.
825 E.g. an administrator, a reviewer or a quality inspector. A real person, e.g. an author, is a
826 :class:`~pyEDAA.Reports.Person`.
827 """
829 _DUPLICATE_ERROR = DuplicatePersonaError #: The error a second persona of an ID raises.
832@export
833class Label(BaseWithName):
834 """
835 A label to classify items across layers and kinds.
837 E.g. a product variant, a release or a customer.
838 """
840 _DUPLICATE_ERROR = DuplicateLabelError #: The error a second label of an ID raises.
843@export
844class Document(BaseWithName):
845 """
846 A document attached to items.
848 E.g. a datasheet, a schematic or a standard.
849 """
851 _DUPLICATE_ERROR = DuplicateDocumentError #: The error a second document of an ID raises.
854@export
855class Layer(BaseWithName):
856 """
857 A layer of the system: the items of one engineering discipline.
859 E.g. electrical, mechanical, FPGA firmware and software requirements are four layers. Each item belongs to at most one
860 layer. A requirement of one layer has its counterparts in other layers, e.g. an electrical requirement in the FPGA
861 firmware, and this one in the software; links of kind :attr:`LinkKind.CounterpartOf` or :attr:`LinkKind.DerivesFrom`
862 state it.
863 """
865 _DUPLICATE_ERROR = DuplicateLayerError #: The error a second layer of an ID raises.
867 def IterateRequirements(
868 self,
869 predicate: Nullable[Callable[[Requirement], bool]] = None
870 ) -> Generator[Requirement, None, None]:
871 """
872 Iterate all or selected requirements of this layer, in the order the specification registered them.
874 If parameter ``predicate`` is not None, the given filter function is used to skip requirements in the generator.
876 :param predicate: Optional, filter function accepting a requirement and returning a boolean. Default: ``None``.
877 :returns: A generator of the layer's requirements; an empty one, while the layer isn't part of a
878 specification.
879 """
880 if self._root is None:
881 return
883 if predicate is None: 883 ↛ 888line 883 didn't jump to line 888 because the condition on line 883 was always true
884 for requirement in self._root._requirements.values():
885 if requirement._layer is self:
886 yield requirement
887 else:
888 for requirement in self._root._requirements.values():
889 if requirement._layer is self and predicate(requirement):
890 yield requirement
892 def IterateSpecificationItems(
893 self,
894 predicate: Nullable[Callable[[SpecificationItem], bool]] = None
895 ) -> Generator[SpecificationItem, None, None]:
896 """
897 Iterate all or selected specification items of this layer, in the order the specification registered them.
899 If parameter ``predicate`` is not None, the given filter function is used to skip items in the generator.
901 :param predicate: Optional, filter function accepting a specification item and returning a boolean. Default:
902 ``None``.
903 :returns: A generator of the layer's specification items; an empty one, while the layer isn't part of a
904 specification.
905 """
906 if self._root is None:
907 return
909 if predicate is None: 909 ↛ 914line 909 didn't jump to line 914 because the condition on line 909 was always true
910 for specification in self._root._specificationItems.values():
911 if specification._layer is self:
912 yield specification
913 else:
914 for specification in self._root._specificationItems.values():
915 if specification._layer is self and predicate(specification):
916 yield specification
919# A class with a property named like a class - ``Layer`` - can't name that class in the annotation of a field: the class
920# body's namespace, where annotations are evaluated, binds the name to the property.
921_Layer = Layer
924@export
925class BaseItem(BaseWithID):
926 """
927 Base-class of the items of a specification - requirements and specification items -: a title, a creation time, a
928 state, the layer, the persons, personas, labels and documents it refers to, its child items, its links and its
929 history.
931 An item refers only to persons, personas, labels, documents and layers of its own specification, so it must be part of
932 one to refer to any. A child item has the class of its parent; its ID is unique in the specification, too.
933 """
935 _title: str #: The item's title.
936 _rationale: str #: Why the item exists or is as it is, or ``""``.
937 _created: datetime #: The time the item was created.
938 _state: ItemState #: The item's state in its life cycle.
939 _layer: Nullable[_Layer] #: The layer the item belongs to, or ``None``.
940 _authors: dict[str, Person] #: The authors of the item, by user ID.
941 _owners: dict[str, Person] #: The owners of the item, by user ID.
942 _personas: dict[str, Persona] #: The personas the item is about, by ID.
943 _labels: dict[str, Label] #: The labels of the item, by ID.
944 _attachments: dict[str, Document] #: The documents attached to the item, by ID.
945 _children: dict[str, Self] #: The child items, by ID.
946 _outboundLinks: list[Link] #: The links from this item, in the order they were added.
947 _inboundLinks: list[Link] #: The links to this item, in the order they were added.
948 _history: list[HistoryEvent] #: The events in the item's life cycle, in chronological order.
950 def __init__(
951 self,
952 identifier: str,
953 title: str,
954 description: str = "",
955 created: Nullable[datetime] = None,
956 *,
957 rationale: str = "",
958 state: ItemState = ItemState.Draft,
959 layer: Nullable[Layer] = None,
960 authors: Nullable[Iterable[Person]] = None,
961 owners: Nullable[Iterable[Person]] = None,
962 personas: Nullable[Iterable[Persona]] = None,
963 labels: Nullable[Iterable[Label]] = None,
964 attachments: Nullable[Iterable[Document]] = None,
965 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None,
966 parent: Nullable[BaseItem | RequirementSpecification] = None
967 ) -> None:
968 """
969 Initializes an item with its title, creation time, state, layer, and the persons, personas, labels and documents it
970 refers to.
972 :param identifier: The item's ID.
973 :param title: The item's title.
974 :param description: Optional, the item's description. Default: ``""``.
975 :param created: Optional, the time the item was created; without a time zone, it is a local
976 time.
977 Default: ``None``, the current time.
978 :param rationale: Optional, why the item exists or is as it is. Default: ``""``.
979 :param state: Optional, the item's state. Default: :attr:`ItemState.Draft`.
980 :param layer: Optional, the layer the item belongs to. Default: ``None``.
981 :param authors: Optional, the authors of the item. Default: ``None``.
982 :param owners: Optional, the owners of the item. Default: ``None``.
983 :param personas: Optional, the personas the item is about. Default: ``None``.
984 :param labels: Optional, the labels of the item. Default: ``None``.
985 :param attachments: Optional, the documents attached to the item. Default: ``None``.
986 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
987 :param parent: Optional, the parent item or the specification. Default: ``None``.
988 :raises ValueError: If parameter ``identifier`` is ``None``.
989 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`.
990 :raises ValueError: If parameter ``identifier`` is empty.
991 :raises ValueError: If parameter ``title`` is ``None``.
992 :raises TypeError: If parameter ``title`` isn't of type :class:`str`.
993 :raises ValueError: If parameter ``title`` is empty.
994 :raises ValueError: If parameter ``description`` is ``None``.
995 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
996 :raises TypeError: If parameter ``created`` isn't of type :class:`~datetime.datetime`.
997 :raises ValueError: If parameter ``rationale`` is ``None``.
998 :raises TypeError: If parameter ``rationale`` isn't of type :class:`str`.
999 :raises ValueError: If parameter ``state`` is ``None``.
1000 :raises TypeError: If parameter ``state`` isn't of type :class:`ItemState`.
1001 :raises TypeError: If parameter ``layer`` isn't of type :class:`Layer`.
1002 :raises NotInSameSpecificationError: If parameter ``layer`` isn't part of the item's specification.
1003 :raises TypeError: If parameter ``authors`` isn't iterable, or is a single element.
1004 :raises TypeError: If parameter ``authors`` contains an element not of type
1005 :class:`~pyEDAA.Reports.Person`.
1006 :raises NotInSameSpecificationError: If parameter ``authors`` contains an element, which isn't part of the item's
1007 specification.
1008 :raises DuplicatePersonError: If parameter ``authors`` contains a person twice.
1009 :raises TypeError: If parameter ``owners`` isn't iterable, or is a single element.
1010 :raises TypeError: If parameter ``owners`` contains an element not of type
1011 :class:`~pyEDAA.Reports.Person`.
1012 :raises NotInSameSpecificationError: If parameter ``owners`` contains an element, which isn't part of the item's
1013 specification.
1014 :raises DuplicatePersonError: If parameter ``owners`` contains a person twice.
1015 :raises TypeError: If parameter ``personas`` isn't iterable, or is a single element.
1016 :raises TypeError: If parameter ``personas`` contains an element not of type :class:`Persona`.
1017 :raises NotInSameSpecificationError: If parameter ``personas`` contains an element, which isn't part of the item's
1018 specification.
1019 :raises DuplicatePersonaError: If parameter ``personas`` contains a persona twice.
1020 :raises TypeError: If parameter ``labels`` isn't iterable, or is a single element.
1021 :raises TypeError: If parameter ``labels`` contains an element not of type :class:`Label`.
1022 :raises NotInSameSpecificationError: If parameter ``labels`` contains an element, which isn't part of the item's
1023 specification.
1024 :raises DuplicateLabelError: If parameter ``labels`` contains a label twice.
1025 :raises TypeError: If parameter ``attachments`` isn't iterable, or is a single element.
1026 :raises TypeError: If parameter ``attachments`` contains an element not of type :class:`Document`.
1027 :raises NotInSameSpecificationError: If parameter ``attachments`` contains an element, which isn't part of the
1028 item's specification.
1029 :raises DuplicateDocumentError: If parameter ``attachments`` contains a document twice.
1030 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
1031 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in
1032 :attr:`_PARENT_TYPE`.
1033 """
1034 super().__init__(identifier, description=description, keyValuePairs=keyValuePairs, parent=parent)
1036 if title is None:
1037 raise ValueError("Parameter 'title' is None.")
1038 elif not isinstance(title, str):
1039 ex = TypeError("Parameter 'title' is not of type 'str'.")
1040 ex.add_note(f"Got type '{getFullyQualifiedName(title)}'.")
1041 raise ex
1042 elif title == "":
1043 raise ValueError("Parameter 'title' is empty.")
1045 if rationale is None:
1046 raise ValueError("Parameter 'rationale' is None.")
1047 elif not isinstance(rationale, str):
1048 ex = TypeError("Parameter 'rationale' is not of type 'str'.")
1049 ex.add_note(f"Got type '{getFullyQualifiedName(rationale)}'.")
1050 raise ex
1052 if created is None:
1053 created = datetime.now()
1054 elif not isinstance(created, datetime):
1055 ex = TypeError("Parameter 'created' is not of type 'datetime'.")
1056 ex.add_note(f"Got type '{getFullyQualifiedName(created)}'.")
1057 raise ex
1059 if state is None:
1060 raise ValueError("Parameter 'state' is None.")
1061 elif not isinstance(state, ItemState):
1062 ex = TypeError("Parameter 'state' is not of type 'ItemState'.")
1063 ex.add_note(f"Got type '{getFullyQualifiedName(state)}'.")
1064 raise ex
1066 if layer is not None:
1067 if not isinstance(layer, Layer):
1068 ex = TypeError("Parameter 'layer' is not of type 'Layer'.")
1069 ex.add_note(f"Got type '{getFullyQualifiedName(layer)}'.")
1070 raise ex
1071 elif self._root is None or layer._root is not self._root:
1072 ex = NotInSameSpecificationError("Parameter 'layer' isn't part of the item's specification.")
1073 ex.add_note(f"Got layer '{layer._id}'.")
1074 raise ex
1076 self._title = title
1077 self._rationale = rationale
1078 self._created = created
1079 self._state = state
1080 self._layer = layer
1081 self._authors = {}
1082 self._owners = {}
1083 self._personas = {}
1084 self._labels = {}
1085 self._attachments = {}
1086 self._children = {}
1087 self._outboundLinks = []
1088 self._inboundLinks = []
1089 self._history = []
1091 for parameterName, references, referenceType, container, duplicateError in (
1092 ("authors", authors, Person, self._authors, DuplicatePersonError),
1093 ("owners", owners, Person, self._owners, DuplicatePersonError),
1094 ("personas", personas, Persona, self._personas, DuplicatePersonaError),
1095 ("labels", labels, Label, self._labels, DuplicateLabelError),
1096 ("attachments", attachments, Document, self._attachments, DuplicateDocumentError),
1097 ):
1098 if references is None:
1099 continue
1100 elif not isinstance(references, Iterable):
1101 ex = TypeError(f"Parameter '{parameterName}' is not iterable.")
1102 ex.add_note(f"Got type '{getFullyQualifiedName(references)}'.")
1103 raise ex
1104 elif isinstance(references, KeyValuePairsMixin):
1105 ex = TypeError(f"Parameter '{parameterName}' is a single element, not an iterable of elements.")
1106 ex.add_note(f"Got type '{getFullyQualifiedName(references)}'.")
1107 raise ex
1109 for reference in references:
1110 if not isinstance(reference, referenceType):
1111 ex = TypeError(f"Parameter '{parameterName}' contains an element not of type '{referenceType.__name__}'.")
1112 ex.add_note(f"Got type '{getFullyQualifiedName(reference)}'.")
1113 raise ex
1115 if referenceType is Person:
1116 key = reference._userID
1117 registered = self._root is not None and self._root._persons.get(key) is reference
1118 else:
1119 key = reference._id
1120 registered = self._root is not None and reference._root is self._root
1122 if not registered:
1123 ex = NotInSameSpecificationError(
1124 f"Parameter '{parameterName}' contains an element not part of the item's specification."
1125 )
1126 ex.add_note(f"Got {referenceType.__name__.lower()} '{key}'.")
1127 raise ex
1128 elif key in container:
1129 ex = duplicateError(f"Parameter '{parameterName}' contains an element twice.")
1130 ex.add_note(f"Got {referenceType.__name__.lower()} '{key}'.")
1131 raise ex
1133 container[key] = reference
1135 def _AddElement(self, element: Self | HistoryEvent) -> None:
1136 """
1137 Add a child item or a history event, which names this item as its parent.
1139 A child item is registered with every item below it in the specification.
1141 :param element: The child item or history event.
1142 :raises DuplicateRequirementError: If the child is a requirement, and the specification already contains a
1143 requirement of an ID of the child or a requirement below it.
1144 :raises DuplicateSpecificationItemError: If the child is a specification item, and the specification already
1145 contains a specification item of an ID of the child or an item below it.
1146 :raises DuplicateRequirementError: If the child is a requirement, this item isn't part of a specification, and
1147 it has a child of this ID already.
1148 :raises DuplicateSpecificationItemError: If the child is a specification item, this item isn't part of a
1149 specification, and it has a child of this ID already.
1150 :raises RequirementsError: If the history event is earlier than the item's creation or its last event.
1151 """
1152 if isinstance(element, HistoryEvent):
1153 previous = self._history[-1]._timestamp if len(self._history) > 0 else self._created
1154 if element._timestamp.astimezone() < previous.astimezone():
1155 ex = RequirementsError("History event is earlier than the item's creation or its last event.")
1156 ex.add_note(f"Got '{element._timestamp.isoformat()}' after '{previous.isoformat()}'.")
1157 raise ex from ValueError(f"Parameter 'timestamp' is earlier than '{previous.isoformat()}'.")
1159 self._history.append(element)
1160 else:
1161 if self._root is not None:
1162 self._root._AddItems(element)
1163 elif element._id in self._children:
1164 raise element._DUPLICATE_ERROR(f"{getTypeName(self)} '{self._id}' already has a child '{element._id}'.")
1166 self._children[element._id] = element
1168 @readonly
1169 def Title(self) -> str:
1170 """
1171 Read-only property to access the item's title (:attr:`_title`).
1173 :returns: The title.
1174 """
1175 return self._title
1177 @readonly
1178 def Rationale(self) -> str:
1179 """
1180 Read-only property to access why the item exists or is as it is (:attr:`_rationale`).
1182 :returns: The rationale, or ``""``.
1183 """
1184 return self._rationale
1186 @readonly
1187 def Created(self) -> datetime:
1188 """
1189 Read-only property to access the time the item was created (:attr:`_created`).
1191 :returns: The time; without a time zone, it is a local time.
1192 """
1193 return self._created
1195 @property
1196 def State(self) -> ItemState:
1197 """
1198 Property to access the item's state in its life cycle (:attr:`_state`).
1200 :returns: The state.
1201 :raises ValueError: If ``None`` is assigned.
1202 :raises TypeError: If the assigned state isn't of type :class:`ItemState`.
1203 """
1204 return self._state
1206 @State.setter
1207 def State(self, state: ItemState) -> None:
1208 if state is None:
1209 raise ValueError("Parameter 'state' is None.")
1210 elif not isinstance(state, ItemState):
1211 ex = TypeError("Parameter 'state' is not of type 'ItemState'.")
1212 ex.add_note(f"Got type '{getFullyQualifiedName(state)}'.")
1213 raise ex
1215 self._state = state
1217 @readonly
1218 def Layer(self) -> Nullable[Layer]:
1219 """
1220 Read-only property to access the layer the item belongs to (:attr:`_layer`).
1222 :returns: The layer, or ``None``.
1223 """
1224 return self._layer
1226 @readonly
1227 def Authors(self) -> dict[str, Person]:
1228 """
1229 Read-only property to access the authors of the item (:attr:`_authors`).
1231 :returns: The authors, by user ID.
1232 """
1233 return self._authors
1235 @readonly
1236 def Owners(self) -> dict[str, Person]:
1237 """
1238 Read-only property to access the owners of the item (:attr:`_owners`).
1240 :returns: The owners, by user ID.
1241 """
1242 return self._owners
1244 @readonly
1245 def Personas(self) -> dict[str, Persona]:
1246 """
1247 Read-only property to access the personas the item is about (:attr:`_personas`).
1249 E.g. the role of a user story: "As a reviewer, I want ...".
1251 :returns: The personas, by ID.
1252 """
1253 return self._personas
1255 @readonly
1256 def Labels(self) -> dict[str, Label]:
1257 """
1258 Read-only property to access the labels of the item (:attr:`_labels`).
1260 :returns: The labels, by ID.
1261 """
1262 return self._labels
1264 @readonly
1265 def Attachments(self) -> dict[str, Document]:
1266 """
1267 Read-only property to access the documents attached to the item (:attr:`_attachments`).
1269 :returns: The documents, by ID.
1270 """
1271 return self._attachments
1273 @readonly
1274 def Children(self) -> dict[str, Self]:
1275 """
1276 Read-only property to access the child items (:attr:`_children`).
1278 :returns: The child items, by ID.
1279 """
1280 return self._children
1282 @readonly
1283 def OutboundLinks(self) -> list[Link]:
1284 """
1285 Read-only property to access the links from this item (:attr:`_outboundLinks`).
1287 :returns: The links, in the order they were added.
1288 """
1289 return self._outboundLinks
1291 @readonly
1292 def InboundLinks(self) -> list[Link]:
1293 """
1294 Read-only property to access the links to this item (:attr:`_inboundLinks`).
1296 :returns: The links, in the order they were added.
1297 """
1298 return self._inboundLinks
1300 @readonly
1301 def History(self) -> list[HistoryEvent]:
1302 """
1303 Read-only property to access the events in the item's life cycle (:attr:`_history`).
1305 :returns: The events, in chronological order.
1306 """
1307 return self._history
1309 def IterateChildren(self, predicate: Nullable[Callable[[Self], bool]] = None) -> Generator[Self, None, None]:
1310 """
1311 Iterate all or selected child items.
1313 If parameter ``predicate`` is not None, the given filter function is used to skip items in the generator.
1315 :param predicate: Optional, filter function accepting an item and returning a boolean. Default: ``None``.
1316 :returns: A generator of the child items.
1317 """
1318 if predicate is None:
1319 yield from self._children.values()
1320 else:
1321 for child in self._children.values():
1322 if predicate(child):
1323 yield child
1325 def IterateDescendants(self, predicate: Nullable[Callable[[Self], bool]] = None) -> Generator[Self, None, None]:
1326 """
1327 Iterate all or selected items below this one - its children, grandchildren, ... -, depth-first: each child
1328 followed by the items below it.
1330 If parameter ``predicate`` is not None, the given filter function is used to skip items in the generator. A skipped
1331 item's children are still iterated.
1333 :param predicate: Optional, filter function accepting an item and returning a boolean. Default: ``None``.
1334 :returns: A generator of the items below this one.
1335 """
1336 if predicate is None:
1337 for child in self._children.values():
1338 yield child
1339 yield from child.IterateDescendants()
1340 else:
1341 for child in self._children.values():
1342 if predicate(child):
1343 yield child
1345 yield from child.IterateDescendants(predicate)
1347 def LinkTo(
1348 self,
1349 destination: BaseItem,
1350 kind: LinkKind,
1351 *,
1352 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None
1353 ) -> Link:
1354 """
1355 Create an outbound link from this item to the given item.
1357 :param destination: The item the link points to.
1358 :param kind: The relationship the link states.
1359 :param keyValuePairs: Optional, the key-value pairs of the link. Default: ``None``.
1360 :returns: The link from this item to the destination.
1361 :raises ValueError: If parameter ``destination`` is ``None``.
1362 :raises TypeError: If parameter ``destination`` isn't of type :class:`BaseItem`.
1363 :raises ValueError: If parameter ``destination`` is this item.
1364 :raises NotInSameSpecificationError: If parameter ``destination`` isn't part of this item's specification.
1365 :raises ValueError: If parameter ``kind`` is ``None``.
1366 :raises TypeError: If parameter ``kind`` isn't of type :class:`LinkKind`.
1367 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
1368 :raises NotInSameSpecificationError: If this item isn't part of a specification.
1369 :raises DuplicateLinkError: If this item has a link of this kind to the destination already.
1371 .. seealso::
1373 :meth:`LinkFrom`
1374 |rarr| Create an inbound link from the given item to this item.
1375 """
1376 return Link(self, destination, kind, keyValuePairs=keyValuePairs)
1378 def LinkFrom(
1379 self,
1380 source: BaseItem,
1381 kind: LinkKind,
1382 *,
1383 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None
1384 ) -> Link:
1385 """
1386 Create an inbound link from the given item to this item.
1388 :param source: The item the link starts at.
1389 :param kind: The relationship the link states.
1390 :param keyValuePairs: Optional, the key-value pairs of the link. Default: ``None``.
1391 :returns: The link from the source to this item.
1392 :raises ValueError: If parameter ``source`` is ``None``.
1393 :raises TypeError: If parameter ``source`` isn't of type :class:`BaseItem`.
1394 :raises NotInSameSpecificationError: If parameter ``source`` isn't part of a specification.
1395 :raises ValueError: If parameter ``source`` is this item.
1396 :raises ValueError: If parameter ``kind`` is ``None``.
1397 :raises TypeError: If parameter ``kind`` isn't of type :class:`LinkKind`.
1398 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
1399 :raises NotInSameSpecificationError: If this item isn't part of the source's specification.
1400 :raises DuplicateLinkError: If the source has a link of this kind to this item already.
1402 .. seealso::
1404 :meth:`LinkTo`
1405 |rarr| Create an outbound link from this item to the given item.
1406 """
1407 return Link(source, self, kind, keyValuePairs=keyValuePairs)
1409 def IterateOutboundLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[Link, None, None]:
1410 """
1411 Iterate all or selected links from this item.
1413 If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator.
1415 :param predicate: Optional, filter function accepting a link and returning a boolean. Default: ``None``.
1416 :returns: A generator of the links from this item.
1417 """
1418 if predicate is None:
1419 yield from self._outboundLinks
1420 else:
1421 for link in self._outboundLinks:
1422 if predicate(link):
1423 yield link
1425 def IterateInboundLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[Link, None, None]:
1426 """
1427 Iterate all or selected links to this item.
1429 If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator.
1431 :param predicate: Optional, filter function accepting a link and returning a boolean. Default: ``None``.
1432 :returns: A generator of the links to this item.
1433 """
1434 if predicate is None:
1435 yield from self._inboundLinks
1436 else:
1437 for link in self._inboundLinks:
1438 if predicate(link):
1439 yield link
1441 def _IterateTrace(
1442 self,
1443 predicate: Nullable[Callable[[Link], bool]],
1444 inbound: bool
1445 ) -> Generator[BaseItem, None, None]:
1446 """
1447 Iterate the items reached by following links transitively, depth-first, each item once.
1449 :param predicate: Filter function accepting a link and returning a boolean, or ``None`` to follow every link.
1450 :param inbound: ``True`` to follow links to their sources, ``False`` to their destinations.
1451 :returns: A generator of the items reached, without this item.
1452 """
1453 visited = {self}
1455 if predicate is None:
1456 def follow(item: BaseItem) -> Generator[BaseItem, None, None]:
1457 """
1458 Nested function for recursion, following every link.
1460 :param item: The item, whose links are followed.
1461 :returns: A generator of the items reached from the item, not visited before.
1462 """
1463 for link in (item._inboundLinks if inbound else item._outboundLinks):
1464 reached = link._source if inbound else link._destination
1465 if reached not in visited:
1466 visited.add(reached)
1467 yield reached
1468 yield from follow(reached)
1469 else:
1470 def follow(item: BaseItem) -> Generator[BaseItem, None, None]:
1471 """
1472 Nested function for recursion, following the links the predicate accepts.
1474 :param item: The item, whose links are followed.
1475 :returns: A generator of the items reached from the item, not visited before.
1476 """
1477 for link in (item._inboundLinks if inbound else item._outboundLinks):
1478 reached = link._source if inbound else link._destination
1479 if reached not in visited and predicate(link):
1480 visited.add(reached)
1481 yield reached
1482 yield from follow(reached)
1484 yield from follow(self)
1486 def IterateTrace(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[BaseItem, None, None]:
1487 """
1488 Iterate the items this item traces to: the destinations of its links, and of their links, ..., depth-first, each
1489 item once.
1491 If parameter ``predicate`` is not None, only links accepted by the given filter function are followed.
1493 :param predicate: Optional, filter function accepting a link and returning a boolean. Default: ``None``.
1494 :returns: A generator of the items reached, without this item.
1495 """
1496 yield from self._IterateTrace(predicate, False)
1498 def IterateInboundTrace(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[BaseItem, None, None]:
1499 """
1500 Iterate the items tracing to this item: the sources of its inbound links, and of their inbound links, ...,
1501 depth-first, each item once.
1503 If parameter ``predicate`` is not None, only links accepted by the given filter function are followed.
1505 :param predicate: Optional, filter function accepting a link and returning a boolean. Default: ``None``.
1506 :returns: A generator of the items reached, without this item.
1507 """
1508 yield from self._IterateTrace(predicate, True)
1511@export
1512class Requirement(BaseItem):
1513 """
1514 A requirement: a condition or capability the system must, should or may meet, its kind and its obligation.
1516 Its parent is the :class:`RequirementSpecification` for a top-level requirement, or the :class:`Requirement` it
1517 refines - a child requirement. The specification registers either by its ID.
1518 """
1520 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: In a requirement or the specification; assigned below.
1522 _kind: RequirementKind #: What the requirement constrains.
1523 _obligation: _Obligation #: Whether the requirement must, should or may be met.
1524 _verificationMethods: VerificationMethod #: The methods verifying the requirement.
1525 _acceptanceCriteria: list[str] #: The criteria the verification accepts the requirement by.
1526 _specificationItems: dict[str, SpecificationItem] #: The specification items specifying this requirement, by ID.
1528 def __init__(
1529 self,
1530 identifier: str,
1531 title: str,
1532 kind: RequirementKind,
1533 description: str = "",
1534 created: Nullable[datetime] = None,
1535 *,
1536 obligation: Obligation = Obligation.Mandatory,
1537 verificationMethods: VerificationMethod = VerificationMethod.Unknown,
1538 acceptanceCriteria: Nullable[Iterable[str]] = None,
1539 rationale: str = "",
1540 state: ItemState = ItemState.Draft,
1541 layer: Nullable[Layer] = None,
1542 authors: Nullable[Iterable[Person]] = None,
1543 owners: Nullable[Iterable[Person]] = None,
1544 personas: Nullable[Iterable[Persona]] = None,
1545 labels: Nullable[Iterable[Label]] = None,
1546 attachments: Nullable[Iterable[Document]] = None,
1547 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None,
1548 parent: Nullable[Requirement | RequirementSpecification] = None
1549 ) -> None:
1550 """
1551 Initializes a requirement, adds it to its parent, and registers it in its specification.
1553 :param identifier: The requirement's ID.
1554 :param title: The requirement's title.
1555 :param kind: What the requirement constrains.
1556 :param description: Optional, the requirement's description. Default: ``""``.
1557 :param created: Optional, the time the requirement was created; without a time zone, it is a
1558 local time. Default: ``None``, the current time.
1559 :param obligation: Optional, whether the requirement must, should or may be met. Default:
1560 :attr:`Obligation.Mandatory`.
1561 :param verificationMethods: Optional, the methods verifying the requirement. Default:
1562 :attr:`VerificationMethod.Unknown`.
1563 :param acceptanceCriteria: Optional, the criteria the verification accepts the requirement by. Default:
1564 ``None``.
1565 :param rationale: Optional, why the requirement exists or is as it is. Default: ``""``.
1566 :param state: Optional, the requirement's state. Default: :attr:`ItemState.Draft`.
1567 :param layer: Optional, the layer the requirement belongs to. Default: ``None``.
1568 :param authors: Optional, the authors of the requirement. Default: ``None``.
1569 :param owners: Optional, the owners of the requirement. Default: ``None``.
1570 :param personas: Optional, the personas the requirement is about. Default: ``None``.
1571 :param labels: Optional, the labels of the requirement. Default: ``None``.
1572 :param attachments: Optional, the documents attached to the requirement. Default: ``None``.
1573 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
1574 :param parent: Optional, the requirement this one refines, or the specification. Default:
1575 ``None``.
1576 :raises ValueError: If parameter ``identifier`` is ``None``.
1577 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`.
1578 :raises ValueError: If parameter ``identifier`` is empty.
1579 :raises ValueError: If parameter ``title`` is ``None``.
1580 :raises TypeError: If parameter ``title`` isn't of type :class:`str`.
1581 :raises ValueError: If parameter ``title`` is empty.
1582 :raises ValueError: If parameter ``kind`` is ``None``.
1583 :raises TypeError: If parameter ``kind`` isn't of type :class:`RequirementKind`.
1584 :raises ValueError: If parameter ``description`` is ``None``.
1585 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
1586 :raises TypeError: If parameter ``created`` isn't of type :class:`~datetime.datetime`.
1587 :raises ValueError: If parameter ``obligation`` is ``None``.
1588 :raises TypeError: If parameter ``obligation`` isn't of type :class:`Obligation`.
1589 :raises ValueError: If parameter ``verificationMethods`` is ``None``.
1590 :raises TypeError: If parameter ``verificationMethods`` isn't of type :class:`VerificationMethod`.
1591 :raises TypeError: If parameter ``acceptanceCriteria`` isn't iterable, or is a single string.
1592 :raises TypeError: If parameter ``acceptanceCriteria`` contains an element not of type
1593 :class:`str`.
1594 :raises ValueError: If parameter ``acceptanceCriteria`` contains an empty criterion.
1595 :raises ValueError: If parameter ``rationale`` is ``None``.
1596 :raises TypeError: If parameter ``rationale`` isn't of type :class:`str`.
1597 :raises ValueError: If parameter ``state`` is ``None``.
1598 :raises TypeError: If parameter ``state`` isn't of type :class:`ItemState`.
1599 :raises TypeError: If parameter ``layer`` isn't of type :class:`Layer`.
1600 :raises NotInSameSpecificationError: If parameter ``layer`` isn't part of the requirement's specification.
1601 :raises TypeError: If parameter ``authors`` isn't iterable, or is a single element.
1602 :raises TypeError: If parameter ``authors`` contains an element not of type
1603 :class:`~pyEDAA.Reports.Person`.
1604 :raises NotInSameSpecificationError: If parameter ``authors`` contains an element, which isn't part of the
1605 requirement's specification.
1606 :raises DuplicatePersonError: If parameter ``authors`` contains a person twice.
1607 :raises TypeError: If parameter ``owners`` isn't iterable, or is a single element.
1608 :raises TypeError: If parameter ``owners`` contains an element not of type
1609 :class:`~pyEDAA.Reports.Person`.
1610 :raises NotInSameSpecificationError: If parameter ``owners`` contains an element, which isn't part of the
1611 requirement's specification.
1612 :raises DuplicatePersonError: If parameter ``owners`` contains a person twice.
1613 :raises TypeError: If parameter ``personas`` isn't iterable, or is a single element.
1614 :raises TypeError: If parameter ``personas`` contains an element not of type :class:`Persona`.
1615 :raises NotInSameSpecificationError: If parameter ``personas`` contains an element, which isn't part of the
1616 requirement's specification.
1617 :raises DuplicatePersonaError: If parameter ``personas`` contains a persona twice.
1618 :raises TypeError: If parameter ``labels`` isn't iterable, or is a single element.
1619 :raises TypeError: If parameter ``labels`` contains an element not of type :class:`Label`.
1620 :raises NotInSameSpecificationError: If parameter ``labels`` contains an element, which isn't part of the
1621 requirement's specification.
1622 :raises DuplicateLabelError: If parameter ``labels`` contains a label twice.
1623 :raises TypeError: If parameter ``attachments`` isn't iterable, or is a single element.
1624 :raises TypeError: If parameter ``attachments`` contains an element not of type :class:`Document`.
1625 :raises NotInSameSpecificationError: If parameter ``attachments`` contains an element, which isn't part of the
1626 requirement's specification.
1627 :raises DuplicateDocumentError: If parameter ``attachments`` contains a document twice.
1628 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
1629 :raises TypeError: If parameter ``parent`` isn't of type :class:`Requirement` or
1630 :class:`RequirementSpecification`.
1631 :raises DuplicateRequirementError: If the specification already contains a requirement of this ID.
1632 """
1633 super().__init__(
1634 identifier,
1635 title,
1636 description,
1637 created,
1638 rationale=rationale,
1639 state=state,
1640 layer=layer,
1641 authors=authors,
1642 owners=owners,
1643 personas=personas,
1644 labels=labels,
1645 attachments=attachments,
1646 keyValuePairs=keyValuePairs,
1647 parent=parent
1648 )
1650 if kind is None:
1651 raise ValueError("Parameter 'kind' is None.")
1652 elif not isinstance(kind, RequirementKind):
1653 ex = TypeError("Parameter 'kind' is not of type 'RequirementKind'.")
1654 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.")
1655 raise ex
1657 if obligation is None:
1658 raise ValueError("Parameter 'obligation' is None.")
1659 elif not isinstance(obligation, Obligation):
1660 ex = TypeError("Parameter 'obligation' is not of type 'Obligation'.")
1661 ex.add_note(f"Got type '{getFullyQualifiedName(obligation)}'.")
1662 raise ex
1664 if verificationMethods is None:
1665 raise ValueError("Parameter 'verificationMethods' is None.")
1666 elif not isinstance(verificationMethods, VerificationMethod):
1667 ex = TypeError("Parameter 'verificationMethods' is not of type 'VerificationMethod'.")
1668 ex.add_note(f"Got type '{getFullyQualifiedName(verificationMethods)}'.")
1669 raise ex
1671 self._acceptanceCriteria = acceptanceCriterionList = []
1672 if acceptanceCriteria is not None:
1673 if not isinstance(acceptanceCriteria, Iterable):
1674 ex = TypeError("Parameter 'acceptanceCriteria' is not iterable.")
1675 ex.add_note(f"Got type '{getFullyQualifiedName(acceptanceCriteria)}'.")
1676 raise ex
1677 elif isinstance(acceptanceCriteria, str):
1678 ex = TypeError("Parameter 'acceptanceCriteria' is a single string, not an iterable of strings.")
1679 ex.add_note(f"Got value '{acceptanceCriteria}'.")
1680 raise ex
1682 for criterion in acceptanceCriteria:
1683 if not isinstance(criterion, str):
1684 ex = TypeError("Parameter 'acceptanceCriteria' contains an element not of type 'str'.")
1685 ex.add_note(f"Got type '{getFullyQualifiedName(criterion)}'.")
1686 raise ex
1687 elif criterion == "":
1688 raise ValueError("Parameter 'acceptanceCriteria' contains an empty criterion.")
1690 acceptanceCriterionList.append(criterion)
1692 self._kind = kind
1693 self._obligation = obligation
1694 self._verificationMethods = verificationMethods
1695 self._specificationItems = {}
1697 if parent is not None:
1698 parent._AddElement(self)
1700 @readonly
1701 def Kind(self) -> RequirementKind:
1702 """
1703 Read-only property to access what the requirement constrains (:attr:`_kind`).
1705 :returns: The requirement's kind.
1706 """
1707 return self._kind
1709 @readonly
1710 def Obligation(self) -> Obligation:
1711 """
1712 Read-only property to access whether the requirement must, should or may be met (:attr:`_obligation`).
1714 :returns: The requirement's obligation.
1715 """
1716 return self._obligation
1718 @readonly
1719 def VerificationMethods(self) -> VerificationMethod:
1720 """
1721 Read-only property to access the methods verifying the requirement (:attr:`_verificationMethods`).
1723 :returns: The verification methods.
1724 """
1725 return self._verificationMethods
1727 @readonly
1728 def AcceptanceCriteria(self) -> list[str]:
1729 """
1730 Read-only property to access the criteria the verification accepts the requirement by (:attr:`_acceptanceCriteria`).
1732 :returns: The acceptance criteria, in the order given.
1733 """
1734 return self._acceptanceCriteria
1736 @readonly
1737 def SpecificationItems(self) -> dict[str, SpecificationItem]:
1738 """
1739 Read-only property to access the specification items specifying this requirement (:attr:`_specificationItems`).
1741 The relation is maintained by :meth:`SpecificationItem.AddRequirement`.
1743 :returns: The specification items, by ID.
1744 """
1745 return self._specificationItems
1748Requirement._PARENT_TYPE = (Requirement, RequirementSpecification)
1749Requirement._DUPLICATE_ERROR = DuplicateRequirementError
1752@export
1753class SpecificationItem(BaseItem):
1754 """
1755 A specification item: how the system meets requirements.
1757 E.g. a design decision, an architecture or an interface description. It mirrors a requirement - with an ID, a title, a
1758 state, a layer, persons, personas, labels, documents, child items, links and a history -, and specifies any number of
1759 requirements; a requirement is specified by any number of specification items.
1761 Its parent is the :class:`RequirementSpecification` for a top-level item, or the :class:`SpecificationItem` it
1762 details. The specification registers either by its ID.
1763 """
1765 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: An item is in an item or the specification; assigned below the class.
1767 _requirements: dict[str, Requirement] #: The requirements this item specifies, by ID.
1769 def __init__(
1770 self,
1771 identifier: str,
1772 title: str,
1773 description: str = "",
1774 created: Nullable[datetime] = None,
1775 *,
1776 requirements: Nullable[Iterable[Requirement]] = None,
1777 rationale: str = "",
1778 state: ItemState = ItemState.Draft,
1779 layer: Nullable[Layer] = None,
1780 authors: Nullable[Iterable[Person]] = None,
1781 owners: Nullable[Iterable[Person]] = None,
1782 personas: Nullable[Iterable[Persona]] = None,
1783 labels: Nullable[Iterable[Label]] = None,
1784 attachments: Nullable[Iterable[Document]] = None,
1785 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None,
1786 parent: Nullable[SpecificationItem | RequirementSpecification] = None
1787 ) -> None:
1788 """
1789 Initializes a specification item, adds it to its parent, registers it in its specification, and relates it to the
1790 requirements it specifies.
1792 :param identifier: The item's ID.
1793 :param title: The item's title.
1794 :param description: Optional, the item's description. Default: ``""``.
1795 :param created: Optional, the time the item was created; without a time zone, it is a local
1796 time. Default: ``None``, the current time.
1797 :param requirements: Optional, the requirements the item specifies. Default: ``None``.
1798 :param rationale: Optional, why the item exists or is as it is. Default: ``""``.
1799 :param state: Optional, the item's state. Default: :attr:`ItemState.Draft`.
1800 :param layer: Optional, the layer the item belongs to. Default: ``None``.
1801 :param authors: Optional, the authors of the item. Default: ``None``.
1802 :param owners: Optional, the owners of the item. Default: ``None``.
1803 :param personas: Optional, the personas the item is about. Default: ``None``.
1804 :param labels: Optional, the labels of the item. Default: ``None``.
1805 :param attachments: Optional, the documents attached to the item. Default: ``None``.
1806 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
1807 :param parent: Optional, the specification item this one details, or the specification.
1808 Default: ``None``.
1809 :raises ValueError: If parameter ``identifier`` is ``None``.
1810 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`.
1811 :raises ValueError: If parameter ``identifier`` is empty.
1812 :raises ValueError: If parameter ``title`` is ``None``.
1813 :raises TypeError: If parameter ``title`` isn't of type :class:`str`.
1814 :raises ValueError: If parameter ``title`` is empty.
1815 :raises ValueError: If parameter ``description`` is ``None``.
1816 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
1817 :raises TypeError: If parameter ``created`` isn't of type :class:`~datetime.datetime`.
1818 :raises TypeError: If parameter ``requirements`` isn't iterable, or is a single element.
1819 :raises TypeError: If parameter ``requirements`` contains an element not of type
1820 :class:`Requirement`.
1821 :raises NotInSameSpecificationError: If parameter ``requirements`` contains a requirement, which isn't part of
1822 the item's specification.
1823 :raises DuplicateRequirementError: If parameter ``requirements`` contains a requirement twice.
1824 :raises ValueError: If parameter ``rationale`` is ``None``.
1825 :raises TypeError: If parameter ``rationale`` isn't of type :class:`str`.
1826 :raises ValueError: If parameter ``state`` is ``None``.
1827 :raises TypeError: If parameter ``state`` isn't of type :class:`ItemState`.
1828 :raises TypeError: If parameter ``layer`` isn't of type :class:`Layer`.
1829 :raises NotInSameSpecificationError: If parameter ``layer`` isn't part of the item's specification.
1830 :raises TypeError: If parameter ``authors`` isn't iterable, or is a single element.
1831 :raises TypeError: If parameter ``authors`` contains an element not of type
1832 :class:`~pyEDAA.Reports.Person`.
1833 :raises NotInSameSpecificationError: If parameter ``authors`` contains an element, which isn't part of the
1834 item's specification.
1835 :raises DuplicatePersonError: If parameter ``authors`` contains a person twice.
1836 :raises TypeError: If parameter ``owners`` isn't iterable, or is a single element.
1837 :raises TypeError: If parameter ``owners`` contains an element not of type
1838 :class:`~pyEDAA.Reports.Person`.
1839 :raises NotInSameSpecificationError: If parameter ``owners`` contains an element, which isn't part of the item's
1840 specification.
1841 :raises DuplicatePersonError: If parameter ``owners`` contains a person twice.
1842 :raises TypeError: If parameter ``personas`` isn't iterable, or is a single element.
1843 :raises TypeError: If parameter ``personas`` contains an element not of type :class:`Persona`.
1844 :raises NotInSameSpecificationError: If parameter ``personas`` contains an element, which isn't part of the
1845 item's specification.
1846 :raises DuplicatePersonaError: If parameter ``personas`` contains a persona twice.
1847 :raises TypeError: If parameter ``labels`` isn't iterable, or is a single element.
1848 :raises TypeError: If parameter ``labels`` contains an element not of type :class:`Label`.
1849 :raises NotInSameSpecificationError: If parameter ``labels`` contains an element, which isn't part of the item's
1850 specification.
1851 :raises DuplicateLabelError: If parameter ``labels`` contains a label twice.
1852 :raises TypeError: If parameter ``attachments`` isn't iterable, or is a single element.
1853 :raises TypeError: If parameter ``attachments`` contains an element not of type
1854 :class:`Document`.
1855 :raises NotInSameSpecificationError: If parameter ``attachments`` contains an element, which isn't part of the
1856 item's specification.
1857 :raises DuplicateDocumentError: If parameter ``attachments`` contains a document twice.
1858 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
1859 :raises TypeError: If parameter ``parent`` isn't of type :class:`SpecificationItem` or
1860 :class:`RequirementSpecification`.
1861 :raises DuplicateSpecificationItemError: If the specification already contains a specification item of this ID.
1862 """
1863 super().__init__(
1864 identifier,
1865 title,
1866 description,
1867 created,
1868 rationale=rationale,
1869 state=state,
1870 layer=layer,
1871 authors=authors,
1872 owners=owners,
1873 personas=personas,
1874 labels=labels,
1875 attachments=attachments,
1876 keyValuePairs=keyValuePairs,
1877 parent=parent
1878 )
1880 self._requirements = {}
1882 if requirements is not None:
1883 if not isinstance(requirements, Iterable):
1884 ex = TypeError("Parameter 'requirements' is not iterable.")
1885 ex.add_note(f"Got type '{getFullyQualifiedName(requirements)}'.")
1886 raise ex
1887 elif isinstance(requirements, KeyValuePairsMixin):
1888 ex = TypeError("Parameter 'requirements' is a single element, not an iterable of elements.")
1889 ex.add_note(f"Got type '{getFullyQualifiedName(requirements)}'.")
1890 raise ex
1892 for requirement in requirements:
1893 if not isinstance(requirement, Requirement):
1894 ex = TypeError("Parameter 'requirements' contains an element not of type 'Requirement'.")
1895 ex.add_note(f"Got type '{getFullyQualifiedName(requirement)}'.")
1896 raise ex
1897 elif self._root is None or requirement._root is not self._root:
1898 ex = NotInSameSpecificationError(
1899 "Parameter 'requirements' contains a requirement not part of the item's specification."
1900 )
1901 ex.add_note(f"Got requirement '{requirement._id}'.")
1902 raise ex
1903 elif requirement._id in self._requirements:
1904 ex = DuplicateRequirementError("Parameter 'requirements' contains a requirement twice.")
1905 ex.add_note(f"Got requirement '{requirement._id}'.")
1906 raise ex
1908 self._requirements[requirement._id] = requirement
1910 if parent is not None:
1911 parent._AddElement(self)
1913 for requirement in self._requirements.values():
1914 requirement._specificationItems[identifier] = self
1916 @readonly
1917 def Requirements(self) -> dict[str, Requirement]:
1918 """
1919 Read-only property to access the requirements this item specifies (:attr:`_requirements`).
1921 :returns: The requirements, by ID.
1922 """
1923 return self._requirements
1925 def AddRequirement(self, requirement: Requirement) -> None:
1926 """
1927 Relate this item to a requirement it specifies: the item lists the requirement, and the requirement lists the item.
1929 :param requirement: The requirement.
1930 :raises ValueError: If parameter ``requirement`` is ``None``.
1931 :raises TypeError: If parameter ``requirement`` isn't of type :class:`Requirement`.
1932 :raises NotInSameSpecificationError: If parameter ``requirement`` isn't part of the item's specification.
1933 :raises DuplicateRequirementError: If the item specifies the requirement already.
1934 """
1935 if requirement is None:
1936 raise ValueError("Parameter 'requirement' is None.")
1937 elif not isinstance(requirement, Requirement):
1938 ex = TypeError("Parameter 'requirement' is not of type 'Requirement'.")
1939 ex.add_note(f"Got type '{getFullyQualifiedName(requirement)}'.")
1940 raise ex
1941 elif self._root is None or requirement._root is not self._root:
1942 ex = NotInSameSpecificationError("Parameter 'requirement' isn't part of the item's specification.")
1943 ex.add_note(f"Got requirement '{requirement._id}'.")
1944 raise ex
1945 elif requirement._id in self._requirements:
1946 raise DuplicateRequirementError(
1947 f"Specification item '{self._id}' specifies requirement '{requirement._id}' already."
1948 )
1950 self._requirements[requirement._id] = requirement
1951 requirement._specificationItems[self._id] = self
1954SpecificationItem._PARENT_TYPE = (SpecificationItem, RequirementSpecification)
1955SpecificationItem._DUPLICATE_ERROR = DuplicateSpecificationItemError
1958@export
1959class LinkKind(Enum):
1960 """
1961 The relationship a link states: the source item *<kind>* the destination item.
1963 A link connects items of the same or of different layers, e.g. an FPGA firmware requirement derived from an electrical
1964 one. Which specification item specifies which requirement, isn't a link, but the relation of
1965 :attr:`SpecificationItem.Requirements` and :attr:`Requirement.SpecificationItems`.
1966 """
1968 Refines = 1 #: The source states the destination in more detail, outside the item hierarchy.
1969 DerivesFrom = 2 #: The source is derived from the destination, e.g. from a higher layer.
1970 CounterpartOf = 3 #: The source is the destination's counterpart in another layer.
1971 DependsOn = 4 #: The source can only be met, if the destination is met.
1972 ConflictsWith = 5 #: The source and the destination can't both be met.
1973 Duplicates = 6 #: The source states the same as the destination.
1974 Replaces = 7 #: The source replaces the destination, which is obsolete.
1975 RelatesTo = 8 #: Any other relationship.
1978@export
1979class Link(KeyValuePairsMixin, metaclass=ExtendedType, slots=True):
1980 """
1981 A link from an item - its source - to another item of the same specification - its destination -, and the
1982 relationship it states.
1984 A link connects two items of the item hierarchy; it is no element of the hierarchy and has no parent. The source
1985 lists it in :attr:`~BaseItem.OutboundLinks`, the destination in :attr:`~BaseItem.InboundLinks`.
1986 """
1988 _source: BaseItem #: The item the link starts at.
1989 _destination: BaseItem #: The item the link points to.
1990 _kind: LinkKind #: The relationship the link states.
1992 def __init__(
1993 self,
1994 source: BaseItem,
1995 destination: BaseItem,
1996 kind: LinkKind,
1997 *,
1998 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None
1999 ) -> None:
2000 """
2001 Initializes a link, and adds it to its source and its destination.
2003 :param source: The item the link starts at.
2004 :param destination: The item the link points to.
2005 :param kind: The relationship the link states.
2006 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
2007 :raises ValueError: If parameter ``source`` is ``None``.
2008 :raises TypeError: If parameter ``source`` isn't of type :class:`BaseItem`.
2009 :raises NotInSameSpecificationError: If parameter ``source`` isn't part of a specification.
2010 :raises ValueError: If parameter ``destination`` is ``None``.
2011 :raises TypeError: If parameter ``destination`` isn't of type :class:`BaseItem`.
2012 :raises ValueError: If parameter ``destination`` is the source.
2013 :raises NotInSameSpecificationError: If parameter ``destination`` isn't part of the source's specification.
2014 :raises ValueError: If parameter ``kind`` is ``None``.
2015 :raises TypeError: If parameter ``kind`` isn't of type :class:`LinkKind`.
2016 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
2017 :raises DuplicateLinkError: If the source has a link of this kind to this destination already.
2018 """
2019 super().__init__(keyValuePairs)
2021 if source is None:
2022 raise ValueError("Parameter 'source' is None.")
2023 elif not isinstance(source, BaseItem):
2024 ex = TypeError("Parameter 'source' is not of type 'BaseItem'.")
2025 ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.")
2026 raise ex
2027 elif source._root is None:
2028 ex = NotInSameSpecificationError("Parameter 'source' isn't part of a specification.")
2029 ex.add_note(f"Got item '{source._id}'.")
2030 raise ex
2032 if destination is None:
2033 raise ValueError("Parameter 'destination' is None.")
2034 elif not isinstance(destination, BaseItem):
2035 ex = TypeError("Parameter 'destination' is not of type 'BaseItem'.")
2036 ex.add_note(f"Got type '{getFullyQualifiedName(destination)}'.")
2037 raise ex
2038 elif destination is source:
2039 ex = ValueError("Parameter 'destination' is the source.")
2040 ex.add_note(f"Got item '{destination._id}'.")
2041 raise ex
2042 elif destination._root is not source._root:
2043 ex = NotInSameSpecificationError("Parameter 'destination' isn't part of the source's specification.")
2044 ex.add_note(f"Got item '{destination._id}'.")
2045 raise ex
2047 if kind is None:
2048 raise ValueError("Parameter 'kind' is None.")
2049 elif not isinstance(kind, LinkKind):
2050 ex = TypeError("Parameter 'kind' is not of type 'LinkKind'.")
2051 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.")
2052 raise ex
2054 for link in source._outboundLinks:
2055 if link._destination is destination and link._kind is kind:
2056 ex = DuplicateLinkError(f"{getTypeName(source)} '{source._id}' has a '{kind.name}' link already.")
2057 ex.add_note(f"Got destination '{destination._id}'.")
2058 raise ex
2060 self._source = source
2061 self._destination = destination
2062 self._kind = kind
2064 source._outboundLinks.append(self)
2065 destination._inboundLinks.append(self)
2067 @readonly
2068 def Source(self) -> BaseItem:
2069 """
2070 Read-only property to access the item the link starts at (:attr:`_source`).
2072 :returns: The source item.
2073 """
2074 return self._source
2076 @readonly
2077 def Destination(self) -> BaseItem:
2078 """
2079 Read-only property to access the item the link points to (:attr:`_destination`).
2081 :returns: The destination item.
2082 """
2083 return self._destination
2085 @readonly
2086 def Kind(self) -> LinkKind:
2087 """
2088 Read-only property to access the relationship the link states (:attr:`_kind`).
2090 :returns: The link's kind.
2091 """
2092 return self._kind
2095@export
2096class EventKind(Enum):
2097 """
2098 The kind of an event in the life cycle of an item.
2099 """
2101 Opened = 1 #: The item was opened, e.g. written as a draft.
2102 Modified = 2 #: The item's text or fields were changed.
2103 Reviewed = 3 #: The item was reviewed.
2104 Approved = 4 #: The item was approved: it became final.
2105 Rejected = 5 #: The item was rejected.
2106 Split = 6 #: The item was split; the event names the other items involved.
2107 Merged = 7 #: The item was merged; the event names the other items involved.
2108 Closed = 8 #: The item was closed after its verification, e.g. released.
2109 Reopened = 9 #: The closed item was opened again.
2110 Obsoleted = 10 #: The item became obsolete, e.g. replaced by the items the event names.
2113# A class with a property named like a class - ``Person`` - can't name that class in the annotation of a field: the
2114# class body's namespace, where annotations are evaluated, binds the name to the property.
2115_Person = Person
2118@export
2119class HistoryEvent(Base):
2120 """
2121 An event in the life cycle of an item: its kind, its time, the person causing it, and the other items involved.
2123 Its parent is the item, which lists it in :attr:`~BaseItem.History`, in chronological order. A split or a merge names
2124 the other items involved, e.g. the requirements a requirement was split into, or merged with.
2125 """
2127 _PARENT_TYPE: ClassVar[tuple[type, ...]] = (BaseItem, ) #: An event is in the history of an item.
2129 _kind: EventKind #: The kind of the event.
2130 _timestamp: datetime #: The time of the event.
2131 _person: Nullable[_Person] #: The person causing the event, or ``None``.
2132 _involved: list[BaseItem] #: The other items involved, in the order given.
2133 _description: str #: The event's description, or ``""``.
2135 def __init__(
2136 self,
2137 kind: EventKind,
2138 timestamp: datetime,
2139 *,
2140 person: Nullable[Person] = None,
2141 involved: Nullable[Iterable[BaseItem]] = None,
2142 description: str = "",
2143 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None,
2144 parent: Nullable[BaseItem] = None
2145 ) -> None:
2146 """
2147 Initializes a history event, and adds it to the history of its item.
2149 :param kind: The kind of the event.
2150 :param timestamp: The time of the event; without a time zone, it is a local time.
2151 :param person: Optional, the person causing the event. Default: ``None``.
2152 :param involved: Optional, the other items involved. Default: ``None``.
2153 :param description: Optional, the event's description. Default: ``""``.
2154 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
2155 :param parent: Optional, the item the event happened to. Default: ``None``.
2156 :raises ValueError: If parameter ``kind`` is ``None``.
2157 :raises TypeError: If parameter ``kind`` isn't of type :class:`EventKind`.
2158 :raises ValueError: If parameter ``timestamp`` is ``None``.
2159 :raises TypeError: If parameter ``timestamp`` isn't of type :class:`~datetime.datetime`.
2160 :raises RequirementsError: If parameter ``timestamp`` is earlier than the item's creation or its last
2161 event.
2162 :raises TypeError: If parameter ``person`` isn't of type :class:`~pyEDAA.Reports.Person`.
2163 :raises NotInSameSpecificationError: If parameter ``person`` isn't part of the item's specification.
2164 :raises TypeError: If parameter ``involved`` isn't iterable, or is a single element.
2165 :raises TypeError: If parameter ``involved`` contains an element not of type
2166 :class:`BaseItem`.
2167 :raises NotInSameSpecificationError: If parameter ``involved`` contains an item, which isn't part of the item's
2168 specification.
2169 :raises DuplicateRequirementError: If parameter ``involved`` contains a requirement twice.
2170 :raises DuplicateSpecificationItemError: If parameter ``involved`` contains a specification item twice.
2171 :raises ValueError: If parameter ``involved`` names no item for a split or a merge.
2172 :raises ValueError: If parameter ``description`` is ``None``.
2173 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
2174 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
2175 :raises TypeError: If parameter ``parent`` isn't of type :class:`BaseItem`.
2176 """
2177 super().__init__(keyValuePairs=keyValuePairs, parent=parent)
2179 if kind is None:
2180 raise ValueError("Parameter 'kind' is None.")
2181 elif not isinstance(kind, EventKind):
2182 ex = TypeError("Parameter 'kind' is not of type 'EventKind'.")
2183 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.")
2184 raise ex
2186 if timestamp is None:
2187 raise ValueError("Parameter 'timestamp' is None.")
2188 elif not isinstance(timestamp, datetime):
2189 ex = TypeError("Parameter 'timestamp' is not of type 'datetime'.")
2190 ex.add_note(f"Got type '{getFullyQualifiedName(timestamp)}'.")
2191 raise ex
2193 if person is not None:
2194 if not isinstance(person, Person):
2195 ex = TypeError("Parameter 'person' is not of type 'Person'.")
2196 ex.add_note(f"Got type '{getFullyQualifiedName(person)}'.")
2197 raise ex
2198 elif self._root is None or self._root._persons.get(person._userID) is not person:
2199 ex = NotInSameSpecificationError("Parameter 'person' isn't part of the item's specification.")
2200 ex.add_note(f"Got person '{person._userID}'.")
2201 raise ex
2203 self._involved = involvedList = []
2204 if involved is not None:
2205 if not isinstance(involved, Iterable):
2206 ex = TypeError("Parameter 'involved' is not iterable.")
2207 ex.add_note(f"Got type '{getFullyQualifiedName(involved)}'.")
2208 raise ex
2209 elif isinstance(involved, KeyValuePairsMixin):
2210 ex = TypeError("Parameter 'involved' is a single element, not an iterable of elements.")
2211 ex.add_note(f"Got type '{getFullyQualifiedName(involved)}'.")
2212 raise ex
2214 for item in involved:
2215 if not isinstance(item, BaseItem):
2216 ex = TypeError("Parameter 'involved' contains an element not of type 'BaseItem'.")
2217 ex.add_note(f"Got type '{getFullyQualifiedName(item)}'.")
2218 raise ex
2219 elif self._root is None or item._root is not self._root:
2220 ex = NotInSameSpecificationError(
2221 "Parameter 'involved' contains an item not part of the item's specification."
2222 )
2223 ex.add_note(f"Got item '{item._id}'.")
2224 raise ex
2225 elif item in self._involved:
2226 ex = item._DUPLICATE_ERROR("Parameter 'involved' contains an item twice.")
2227 ex.add_note(f"Got item '{item._id}'.")
2228 raise ex
2230 involvedList.append(item)
2232 if len(self._involved) == 0 and kind in (EventKind.Split, EventKind.Merged):
2233 raise ValueError(f"Parameter 'involved' names no item for a '{kind.name}' event.")
2235 if description is None:
2236 raise ValueError("Parameter 'description' is None.")
2237 elif not isinstance(description, str):
2238 ex = TypeError("Parameter 'description' is not of type 'str'.")
2239 ex.add_note(f"Got type '{getFullyQualifiedName(description)}'.")
2240 raise ex
2242 self._kind = kind
2243 self._timestamp = timestamp
2244 self._person = person
2245 self._description = description
2247 if parent is not None:
2248 parent._AddElement(self)
2250 @readonly
2251 def Kind(self) -> EventKind:
2252 """
2253 Read-only property to access the kind of the event (:attr:`_kind`).
2255 :returns: The event's kind.
2256 """
2257 return self._kind
2259 @readonly
2260 def Timestamp(self) -> datetime:
2261 """
2262 Read-only property to access the time of the event (:attr:`_timestamp`).
2264 :returns: The time; without a time zone, it is a local time.
2265 """
2266 return self._timestamp
2268 @readonly
2269 def Person(self) -> Nullable[Person]:
2270 """
2271 Read-only property to access the person causing the event (:attr:`_person`).
2273 :returns: The person, or ``None``.
2274 """
2275 return self._person
2277 @readonly
2278 def Involved(self) -> list[BaseItem]:
2279 """
2280 Read-only property to access the other items involved in the event (:attr:`_involved`).
2282 :returns: The items, in the order given.
2283 """
2284 return self._involved
2286 @readonly
2287 def Description(self) -> str:
2288 """
2289 Read-only property to access the event's description (:attr:`_description`).
2291 :returns: The description, or ``""``.
2292 """
2293 return self._description