Coverage for pyEDAA/Reports/Requirements/__init__.py: 98%
838 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 23:49 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 23:49 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²) #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32Abstract data model of 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
53from typing import 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 TypeError: If parameter ``keyValuePairs`` isn't a mapping.
333 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`.
334 :raises ValueError: If parameter ``identifier`` is ``None``.
335 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`.
336 :raises ValueError: If parameter ``identifier`` is empty.
337 :raises ValueError: If parameter ``description`` is ``None``.
338 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
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 TypeError: If parameter ``keyValuePairs`` isn't a mapping.
417 :raises ValueError: If parameter ``version`` is ``None``.
418 :raises TypeError: If parameter ``version`` isn't of type :class:`~pyTooling.Versioning.SemanticVersion`.
419 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
420 :raises ValueError: If parameter ``name`` is empty.
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 TypeError: If parameter ``keyValuePairs`` isn't a mapping.
776 :raises TypeError: If parameter ``parent`` isn't of type :class:`RequirementSpecification`.
777 :raises ValueError: If parameter ``identifier`` is ``None``.
778 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`.
779 :raises ValueError: If parameter ``identifier`` is empty.
780 :raises ValueError: If parameter ``description`` is ``None``.
781 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
782 :raises ValueError: If parameter ``name`` is ``None``.
783 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
784 :raises ValueError: If parameter ``name`` is empty.
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 TypeError: If parameter ``keyValuePairs`` isn't a mapping.
989 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in
990 :attr:`_PARENT_TYPE`.
991 :raises ValueError: If parameter ``identifier`` is ``None``.
992 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`.
993 :raises ValueError: If parameter ``identifier`` is empty.
994 :raises ValueError: If parameter ``description`` is ``None``.
995 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
996 :raises ValueError: If parameter ``title`` is ``None``.
997 :raises TypeError: If parameter ``title`` isn't of type :class:`str`.
998 :raises ValueError: If parameter ``title`` is empty.
999 :raises ValueError: If parameter ``rationale`` is ``None``.
1000 :raises TypeError: If parameter ``rationale`` isn't of type :class:`str`.
1001 :raises TypeError: If parameter ``created`` isn't of type :class:`~datetime.datetime`.
1002 :raises ValueError: If parameter ``state`` is ``None``.
1003 :raises TypeError: If parameter ``state`` isn't of type :class:`ItemState`.
1004 :raises TypeError: If parameter ``layer`` isn't of type :class:`Layer`.
1005 :raises NotInSameSpecificationError: If parameter ``layer`` isn't part of the item's specification.
1006 :raises TypeError: If parameter ``authors``, ``owners``, ``personas``, ``labels`` or
1007 ``attachments``
1008 isn't iterable, or is a single element.
1009 :raises TypeError: If parameter ``authors`` or ``owners`` contains an element not of type
1010 :class:`~pyEDAA.Reports.Person`.
1011 :raises TypeError: If parameter ``personas``, ``labels`` or ``attachments`` contains an element
1012 not of
1013 type :class:`Persona`, :class:`Label` or :class:`Document`.
1014 :raises NotInSameSpecificationError: If parameter ``authors``, ``owners``, ``personas``, ``labels`` or
1015 ``attachments``
1016 contains an element, which isn't part of the item's specification.
1017 :raises DuplicatePersonError: If parameter ``authors`` or ``owners`` contains a person twice.
1018 :raises DuplicatePersonaError: If parameter ``personas`` contains a persona twice.
1019 :raises DuplicateLabelError: If parameter ``labels`` contains a label twice.
1020 :raises DuplicateDocumentError: If parameter ``attachments`` contains a document twice.
1021 """
1022 super().__init__(identifier, description=description, keyValuePairs=keyValuePairs, parent=parent)
1024 if title is None:
1025 raise ValueError("Parameter 'title' is None.")
1026 elif not isinstance(title, str):
1027 ex = TypeError("Parameter 'title' is not of type 'str'.")
1028 ex.add_note(f"Got type '{getFullyQualifiedName(title)}'.")
1029 raise ex
1030 elif title == "":
1031 raise ValueError("Parameter 'title' is empty.")
1033 if rationale is None:
1034 raise ValueError("Parameter 'rationale' is None.")
1035 elif not isinstance(rationale, str):
1036 ex = TypeError("Parameter 'rationale' is not of type 'str'.")
1037 ex.add_note(f"Got type '{getFullyQualifiedName(rationale)}'.")
1038 raise ex
1040 if created is None:
1041 created = datetime.now()
1042 elif not isinstance(created, datetime):
1043 ex = TypeError("Parameter 'created' is not of type 'datetime'.")
1044 ex.add_note(f"Got type '{getFullyQualifiedName(created)}'.")
1045 raise ex
1047 if state is None:
1048 raise ValueError("Parameter 'state' is None.")
1049 elif not isinstance(state, ItemState):
1050 ex = TypeError("Parameter 'state' is not of type 'ItemState'.")
1051 ex.add_note(f"Got type '{getFullyQualifiedName(state)}'.")
1052 raise ex
1054 if layer is not None:
1055 if not isinstance(layer, Layer):
1056 ex = TypeError("Parameter 'layer' is not of type 'Layer'.")
1057 ex.add_note(f"Got type '{getFullyQualifiedName(layer)}'.")
1058 raise ex
1059 elif self._root is None or layer._root is not self._root:
1060 ex = NotInSameSpecificationError("Parameter 'layer' isn't part of the item's specification.")
1061 ex.add_note(f"Got layer '{layer._id}'.")
1062 raise ex
1064 self._title = title
1065 self._rationale = rationale
1066 self._created = created
1067 self._state = state
1068 self._layer = layer
1069 self._authors = {}
1070 self._owners = {}
1071 self._personas = {}
1072 self._labels = {}
1073 self._attachments = {}
1074 self._children = {}
1075 self._outboundLinks = []
1076 self._inboundLinks = []
1077 self._history = []
1079 for parameterName, references, referenceType, container, duplicateError in (
1080 ("authors", authors, Person, self._authors, DuplicatePersonError),
1081 ("owners", owners, Person, self._owners, DuplicatePersonError),
1082 ("personas", personas, Persona, self._personas, DuplicatePersonaError),
1083 ("labels", labels, Label, self._labels, DuplicateLabelError),
1084 ("attachments", attachments, Document, self._attachments, DuplicateDocumentError),
1085 ):
1086 if references is None:
1087 continue
1088 elif not isinstance(references, Iterable):
1089 ex = TypeError(f"Parameter '{parameterName}' is not iterable.")
1090 ex.add_note(f"Got type '{getFullyQualifiedName(references)}'.")
1091 raise ex
1092 elif isinstance(references, KeyValuePairsMixin):
1093 ex = TypeError(f"Parameter '{parameterName}' is a single element, not an iterable of elements.")
1094 ex.add_note(f"Got type '{getFullyQualifiedName(references)}'.")
1095 raise ex
1097 for reference in references:
1098 if not isinstance(reference, referenceType):
1099 ex = TypeError(f"Parameter '{parameterName}' contains an element not of type '{referenceType.__name__}'.")
1100 ex.add_note(f"Got type '{getFullyQualifiedName(reference)}'.")
1101 raise ex
1103 if referenceType is Person:
1104 key = reference._userID
1105 registered = self._root is not None and self._root._persons.get(key) is reference
1106 else:
1107 key = reference._id
1108 registered = self._root is not None and reference._root is self._root
1110 if not registered:
1111 ex = NotInSameSpecificationError(
1112 f"Parameter '{parameterName}' contains an element not part of the item's specification."
1113 )
1114 ex.add_note(f"Got {referenceType.__name__.lower()} '{key}'.")
1115 raise ex
1116 elif key in container:
1117 ex = duplicateError(f"Parameter '{parameterName}' contains an element twice.")
1118 ex.add_note(f"Got {referenceType.__name__.lower()} '{key}'.")
1119 raise ex
1121 container[key] = reference
1123 def _AddElement(self, element: Self | HistoryEvent) -> None:
1124 """
1125 Add a child item or a history event, which names this item as its parent.
1127 A child item is registered with every item below it in the specification.
1129 :param element: The child item or history event.
1130 :raises DuplicateRequirementError: If the child is a requirement, and the specification already contains a
1131 requirement of an ID of the child or a requirement below it.
1132 :raises DuplicateSpecificationItemError: If the child is a specification item, and the specification already
1133 contains a specification item of an ID of the child or an item below it.
1134 :raises DuplicateRequirementError: If the child is a requirement, this item isn't part of a specification, and
1135 it has a child of this ID already.
1136 :raises DuplicateSpecificationItemError: If the child is a specification item, this item isn't part of a
1137 specification, and it has a child of this ID already.
1138 :raises RequirementsError: If the history event is earlier than the item's creation or its last event.
1139 """
1140 if isinstance(element, HistoryEvent):
1141 previous = self._history[-1]._timestamp if len(self._history) > 0 else self._created
1142 if element._timestamp.astimezone() < previous.astimezone():
1143 ex = RequirementsError("History event is earlier than the item's creation or its last event.")
1144 ex.add_note(f"Got '{element._timestamp.isoformat()}' after '{previous.isoformat()}'.")
1145 raise ex from ValueError(f"Parameter 'timestamp' is earlier than '{previous.isoformat()}'.")
1147 self._history.append(element)
1148 else:
1149 if self._root is not None:
1150 self._root._AddItems(element)
1151 elif element._id in self._children:
1152 raise element._DUPLICATE_ERROR(f"{getTypeName(self)} '{self._id}' already has a child '{element._id}'.")
1154 self._children[element._id] = element
1156 @readonly
1157 def Title(self) -> str:
1158 """
1159 Read-only property to access the item's title (:attr:`_title`).
1161 :returns: The title.
1162 """
1163 return self._title
1165 @readonly
1166 def Rationale(self) -> str:
1167 """
1168 Read-only property to access why the item exists or is as it is (:attr:`_rationale`).
1170 :returns: The rationale, or ``""``.
1171 """
1172 return self._rationale
1174 @readonly
1175 def Created(self) -> datetime:
1176 """
1177 Read-only property to access the time the item was created (:attr:`_created`).
1179 :returns: The time; without a time zone, it is a local time.
1180 """
1181 return self._created
1183 @property
1184 def State(self) -> ItemState:
1185 """
1186 Property to access the item's state in its life cycle (:attr:`_state`).
1188 :returns: The state.
1189 :raises ValueError: If ``None`` is assigned.
1190 :raises TypeError: If the assigned state isn't of type :class:`ItemState`.
1191 """
1192 return self._state
1194 @State.setter
1195 def State(self, state: ItemState) -> None:
1196 if state is None:
1197 raise ValueError("Parameter 'state' is None.")
1198 elif not isinstance(state, ItemState):
1199 ex = TypeError("Parameter 'state' is not of type 'ItemState'.")
1200 ex.add_note(f"Got type '{getFullyQualifiedName(state)}'.")
1201 raise ex
1203 self._state = state
1205 @readonly
1206 def Layer(self) -> Nullable[Layer]:
1207 """
1208 Read-only property to access the layer the item belongs to (:attr:`_layer`).
1210 :returns: The layer, or ``None``.
1211 """
1212 return self._layer
1214 @readonly
1215 def Authors(self) -> dict[str, Person]:
1216 """
1217 Read-only property to access the authors of the item (:attr:`_authors`).
1219 :returns: The authors, by user ID.
1220 """
1221 return self._authors
1223 @readonly
1224 def Owners(self) -> dict[str, Person]:
1225 """
1226 Read-only property to access the owners of the item (:attr:`_owners`).
1228 :returns: The owners, by user ID.
1229 """
1230 return self._owners
1232 @readonly
1233 def Personas(self) -> dict[str, Persona]:
1234 """
1235 Read-only property to access the personas the item is about (:attr:`_personas`).
1237 E.g. the role of a user story: "As a reviewer, I want ...".
1239 :returns: The personas, by ID.
1240 """
1241 return self._personas
1243 @readonly
1244 def Labels(self) -> dict[str, Label]:
1245 """
1246 Read-only property to access the labels of the item (:attr:`_labels`).
1248 :returns: The labels, by ID.
1249 """
1250 return self._labels
1252 @readonly
1253 def Attachments(self) -> dict[str, Document]:
1254 """
1255 Read-only property to access the documents attached to the item (:attr:`_attachments`).
1257 :returns: The documents, by ID.
1258 """
1259 return self._attachments
1261 @readonly
1262 def Children(self) -> dict[str, Self]:
1263 """
1264 Read-only property to access the child items (:attr:`_children`).
1266 :returns: The child items, by ID.
1267 """
1268 return self._children
1270 @readonly
1271 def OutboundLinks(self) -> list[Link]:
1272 """
1273 Read-only property to access the links from this item (:attr:`_outboundLinks`).
1275 :returns: The links, in the order they were added.
1276 """
1277 return self._outboundLinks
1279 @readonly
1280 def InboundLinks(self) -> list[Link]:
1281 """
1282 Read-only property to access the links to this item (:attr:`_inboundLinks`).
1284 :returns: The links, in the order they were added.
1285 """
1286 return self._inboundLinks
1288 @readonly
1289 def History(self) -> list[HistoryEvent]:
1290 """
1291 Read-only property to access the events in the item's life cycle (:attr:`_history`).
1293 :returns: The events, in chronological order.
1294 """
1295 return self._history
1297 def IterateChildren(self, predicate: Nullable[Callable[[Self], bool]] = None) -> Generator[Self, None, None]:
1298 """
1299 Iterate all or selected child items.
1301 If parameter ``predicate`` is not None, the given filter function is used to skip items in the generator.
1303 :param predicate: Optional, filter function accepting an item and returning a boolean. Default: ``None``.
1304 :returns: A generator of the child items.
1305 """
1306 if predicate is None:
1307 yield from self._children.values()
1308 else:
1309 for child in self._children.values():
1310 if predicate(child):
1311 yield child
1313 def IterateDescendants(self, predicate: Nullable[Callable[[Self], bool]] = None) -> Generator[Self, None, None]:
1314 """
1315 Iterate all or selected items below this one - its children, grandchildren, ... -, depth-first: each child
1316 followed by the items below it.
1318 If parameter ``predicate`` is not None, the given filter function is used to skip items in the generator. A skipped
1319 item's children are still iterated.
1321 :param predicate: Optional, filter function accepting an item and returning a boolean. Default: ``None``.
1322 :returns: A generator of the items below this one.
1323 """
1324 if predicate is None:
1325 for child in self._children.values():
1326 yield child
1327 yield from child.IterateDescendants()
1328 else:
1329 for child in self._children.values():
1330 if predicate(child):
1331 yield child
1333 yield from child.IterateDescendants(predicate)
1335 def LinkTo(
1336 self,
1337 destination: BaseItem,
1338 kind: LinkKind,
1339 *,
1340 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None
1341 ) -> Link:
1342 """
1343 Create an outbound link from this item to the given item.
1345 :param destination: The item the link points to.
1346 :param kind: The relationship the link states.
1347 :param keyValuePairs: Optional, the key-value pairs of the link. Default: ``None``.
1348 :returns: The link from this item to the destination.
1349 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
1350 :raises NotInSameSpecificationError: If this item isn't part of a specification.
1351 :raises ValueError: If parameter ``destination`` is ``None``.
1352 :raises TypeError: If parameter ``destination`` isn't of type :class:`BaseItem`.
1353 :raises ValueError: If parameter ``destination`` is this item.
1354 :raises NotInSameSpecificationError: If parameter ``destination`` isn't part of this item's specification.
1355 :raises ValueError: If parameter ``kind`` is ``None``.
1356 :raises TypeError: If parameter ``kind`` isn't of type :class:`LinkKind`.
1357 :raises DuplicateLinkError: If this item has a link of this kind to the destination already.
1359 .. seealso::
1361 :meth:`LinkFrom`
1362 |rarr| Create an inbound link from the given item to this item.
1363 """
1364 return Link(self, destination, kind, keyValuePairs=keyValuePairs)
1366 def LinkFrom(
1367 self,
1368 source: BaseItem,
1369 kind: LinkKind,
1370 *,
1371 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None
1372 ) -> Link:
1373 """
1374 Create an inbound link from the given item to this item.
1376 :param source: The item the link starts at.
1377 :param kind: The relationship the link states.
1378 :param keyValuePairs: Optional, the key-value pairs of the link. Default: ``None``.
1379 :returns: The link from the source to this item.
1380 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
1381 :raises ValueError: If parameter ``source`` is ``None``.
1382 :raises TypeError: If parameter ``source`` isn't of type :class:`BaseItem`.
1383 :raises NotInSameSpecificationError: If parameter ``source`` isn't part of a specification.
1384 :raises ValueError: If parameter ``source`` is this item.
1385 :raises NotInSameSpecificationError: If this item isn't part of the source's specification.
1386 :raises ValueError: If parameter ``kind`` is ``None``.
1387 :raises TypeError: If parameter ``kind`` isn't of type :class:`LinkKind`.
1388 :raises DuplicateLinkError: If the source has a link of this kind to this item already.
1390 .. seealso::
1392 :meth:`LinkTo`
1393 |rarr| Create an outbound link from this item to the given item.
1394 """
1395 return Link(source, self, kind, keyValuePairs=keyValuePairs)
1397 def IterateOutboundLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[Link, None, None]:
1398 """
1399 Iterate all or selected links from this item.
1401 If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator.
1403 :param predicate: Optional, filter function accepting a link and returning a boolean. Default: ``None``.
1404 :returns: A generator of the links from this item.
1405 """
1406 if predicate is None:
1407 yield from self._outboundLinks
1408 else:
1409 for link in self._outboundLinks:
1410 if predicate(link):
1411 yield link
1413 def IterateInboundLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[Link, None, None]:
1414 """
1415 Iterate all or selected links to this item.
1417 If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator.
1419 :param predicate: Optional, filter function accepting a link and returning a boolean. Default: ``None``.
1420 :returns: A generator of the links to this item.
1421 """
1422 if predicate is None:
1423 yield from self._inboundLinks
1424 else:
1425 for link in self._inboundLinks:
1426 if predicate(link):
1427 yield link
1429 def _IterateTrace(
1430 self,
1431 predicate: Nullable[Callable[[Link], bool]],
1432 inbound: bool
1433 ) -> Generator[BaseItem, None, None]:
1434 """
1435 Iterate the items reached by following links transitively, depth-first, each item once.
1437 :param predicate: Filter function accepting a link and returning a boolean, or ``None`` to follow every link.
1438 :param inbound: ``True`` to follow links to their sources, ``False`` to their destinations.
1439 :returns: A generator of the items reached, without this item.
1440 """
1441 visited = {self}
1443 if predicate is None:
1444 def follow(item: BaseItem) -> Generator[BaseItem, None, None]:
1445 """
1446 Nested function for recursion, following every link.
1448 :param item: The item, whose links are followed.
1449 :returns: A generator of the items reached from the item, not visited before.
1450 """
1451 for link in (item._inboundLinks if inbound else item._outboundLinks):
1452 reached = link._source if inbound else link._destination
1453 if reached not in visited:
1454 visited.add(reached)
1455 yield reached
1456 yield from follow(reached)
1457 else:
1458 def follow(item: BaseItem) -> Generator[BaseItem, None, None]:
1459 """
1460 Nested function for recursion, following the links the predicate accepts.
1462 :param item: The item, whose links are followed.
1463 :returns: A generator of the items reached from the item, not visited before.
1464 """
1465 for link in (item._inboundLinks if inbound else item._outboundLinks):
1466 reached = link._source if inbound else link._destination
1467 if reached not in visited and predicate(link):
1468 visited.add(reached)
1469 yield reached
1470 yield from follow(reached)
1472 yield from follow(self)
1474 def IterateTrace(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[BaseItem, None, None]:
1475 """
1476 Iterate the items this item traces to: the destinations of its links, and of their links, ..., depth-first, each
1477 item once.
1479 If parameter ``predicate`` is not None, only links accepted by the given filter function are followed.
1481 :param predicate: Optional, filter function accepting a link and returning a boolean. Default: ``None``.
1482 :returns: A generator of the items reached, without this item.
1483 """
1484 yield from self._IterateTrace(predicate, False)
1486 def IterateInboundTrace(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[BaseItem, None, None]:
1487 """
1488 Iterate the items tracing to this item: the sources of its inbound links, and of their inbound links, ...,
1489 depth-first, each 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, True)
1499@export
1500class Requirement(BaseItem):
1501 """
1502 A requirement: a condition or capability the system must, should or may meet, its kind and its obligation.
1504 Its parent is the :class:`RequirementSpecification` for a top-level requirement, or the :class:`Requirement` it
1505 refines - a child requirement. The specification registers either by its ID.
1506 """
1508 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: In a requirement or the specification; assigned below.
1510 _kind: RequirementKind #: What the requirement constrains.
1511 _obligation: _Obligation #: Whether the requirement must, should or may be met.
1512 _verificationMethods: VerificationMethod #: The methods verifying the requirement.
1513 _acceptanceCriteria: list[str] #: The criteria the verification accepts the requirement by.
1514 _specificationItems: dict[str, SpecificationItem] #: The specification items specifying this requirement, by ID.
1516 def __init__(
1517 self,
1518 identifier: str,
1519 title: str,
1520 kind: RequirementKind,
1521 description: str = "",
1522 created: Nullable[datetime] = None,
1523 *,
1524 obligation: Obligation = Obligation.Mandatory,
1525 verificationMethods: VerificationMethod = VerificationMethod.Unknown,
1526 acceptanceCriteria: Nullable[Iterable[str]] = None,
1527 rationale: str = "",
1528 state: ItemState = ItemState.Draft,
1529 layer: Nullable[Layer] = None,
1530 authors: Nullable[Iterable[Person]] = None,
1531 owners: Nullable[Iterable[Person]] = None,
1532 personas: Nullable[Iterable[Persona]] = None,
1533 labels: Nullable[Iterable[Label]] = None,
1534 attachments: Nullable[Iterable[Document]] = None,
1535 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None,
1536 parent: Nullable[Requirement | RequirementSpecification] = None
1537 ) -> None:
1538 """
1539 Initializes a requirement, adds it to its parent, and registers it in its specification.
1541 :param identifier: The requirement's ID.
1542 :param title: The requirement's title.
1543 :param kind: What the requirement constrains.
1544 :param description: Optional, the requirement's description. Default: ``""``.
1545 :param created: Optional, the time the requirement was created; without a time zone, it is a
1546 local time. Default: ``None``, the current time.
1547 :param obligation: Optional, whether the requirement must, should or may be met. Default:
1548 :attr:`Obligation.Mandatory`.
1549 :param verificationMethods: Optional, the methods verifying the requirement. Default:
1550 :attr:`VerificationMethod.Unknown`.
1551 :param acceptanceCriteria: Optional, the criteria the verification accepts the requirement by. Default:
1552 ``None``.
1553 :param rationale: Optional, why the requirement exists or is as it is. Default: ``""``.
1554 :param state: Optional, the requirement's state. Default: :attr:`ItemState.Draft`.
1555 :param layer: Optional, the layer the requirement belongs to. Default: ``None``.
1556 :param authors: Optional, the authors of the requirement. Default: ``None``.
1557 :param owners: Optional, the owners of the requirement. Default: ``None``.
1558 :param personas: Optional, the personas the requirement is about. Default: ``None``.
1559 :param labels: Optional, the labels of the requirement. Default: ``None``.
1560 :param attachments: Optional, the documents attached to the requirement. Default: ``None``.
1561 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
1562 :param parent: Optional, the requirement this one refines, or the specification. Default:
1563 ``None``.
1564 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
1565 :raises TypeError: If parameter ``parent`` isn't of type :class:`Requirement` or
1566 :class:`RequirementSpecification`.
1567 :raises ValueError: If parameter ``identifier`` is ``None``.
1568 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`.
1569 :raises ValueError: If parameter ``identifier`` is empty.
1570 :raises ValueError: If parameter ``description`` is ``None``.
1571 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
1572 :raises ValueError: If parameter ``title`` is ``None``.
1573 :raises TypeError: If parameter ``title`` isn't of type :class:`str`.
1574 :raises ValueError: If parameter ``title`` is empty.
1575 :raises ValueError: If parameter ``rationale`` is ``None``.
1576 :raises TypeError: If parameter ``rationale`` isn't of type :class:`str`.
1577 :raises TypeError: If parameter ``created`` isn't of type :class:`~datetime.datetime`.
1578 :raises ValueError: If parameter ``state`` is ``None``.
1579 :raises TypeError: If parameter ``state`` isn't of type :class:`ItemState`.
1580 :raises TypeError: If parameter ``layer`` isn't of type :class:`Layer`.
1581 :raises NotInSameSpecificationError: If parameter ``layer`` isn't part of the requirement's specification.
1582 :raises TypeError: If parameter ``authors``, ``owners``, ``personas``, ``labels`` or
1583 ``attachments``
1584 isn't iterable, or is a single element.
1585 :raises TypeError: If parameter ``authors`` or ``owners`` contains an element not of type
1586 :class:`~pyEDAA.Reports.Person`.
1587 :raises TypeError: If parameter ``personas``, ``labels`` or ``attachments`` contains an element
1588 not
1589 of type :class:`Persona`, :class:`Label` or :class:`Document`.
1590 :raises NotInSameSpecificationError: If parameter ``authors``, ``owners``, ``personas``, ``labels`` or
1591 ``attachments``
1592 contains an element, which isn't part of the requirement's specification.
1593 :raises DuplicatePersonError: If parameter ``authors`` or ``owners`` contains a person twice.
1594 :raises DuplicatePersonaError: If parameter ``personas`` contains a persona twice.
1595 :raises DuplicateLabelError: If parameter ``labels`` contains a label twice.
1596 :raises DuplicateDocumentError: If parameter ``attachments`` contains a document twice.
1597 :raises ValueError: If parameter ``kind`` is ``None``.
1598 :raises TypeError: If parameter ``kind`` isn't of type :class:`RequirementKind`.
1599 :raises ValueError: If parameter ``obligation`` is ``None``.
1600 :raises TypeError: If parameter ``obligation`` isn't of type :class:`Obligation`.
1601 :raises ValueError: If parameter ``verificationMethods`` is ``None``.
1602 :raises TypeError: If parameter ``verificationMethods`` isn't of type :class:`VerificationMethod`.
1603 :raises TypeError: If parameter ``acceptanceCriteria`` isn't iterable, or is a single string.
1604 :raises TypeError: If parameter ``acceptanceCriteria`` contains an element not of type
1605 :class:`str`.
1606 :raises ValueError: If parameter ``acceptanceCriteria`` contains an empty criterion.
1607 :raises DuplicateRequirementError: If the specification already contains a requirement of this ID.
1608 """
1609 super().__init__(
1610 identifier,
1611 title,
1612 description,
1613 created,
1614 rationale=rationale,
1615 state=state,
1616 layer=layer,
1617 authors=authors,
1618 owners=owners,
1619 personas=personas,
1620 labels=labels,
1621 attachments=attachments,
1622 keyValuePairs=keyValuePairs,
1623 parent=parent
1624 )
1626 if kind is None:
1627 raise ValueError("Parameter 'kind' is None.")
1628 elif not isinstance(kind, RequirementKind):
1629 ex = TypeError("Parameter 'kind' is not of type 'RequirementKind'.")
1630 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.")
1631 raise ex
1633 if obligation is None:
1634 raise ValueError("Parameter 'obligation' is None.")
1635 elif not isinstance(obligation, Obligation):
1636 ex = TypeError("Parameter 'obligation' is not of type 'Obligation'.")
1637 ex.add_note(f"Got type '{getFullyQualifiedName(obligation)}'.")
1638 raise ex
1640 if verificationMethods is None:
1641 raise ValueError("Parameter 'verificationMethods' is None.")
1642 elif not isinstance(verificationMethods, VerificationMethod):
1643 ex = TypeError("Parameter 'verificationMethods' is not of type 'VerificationMethod'.")
1644 ex.add_note(f"Got type '{getFullyQualifiedName(verificationMethods)}'.")
1645 raise ex
1647 self._acceptanceCriteria = []
1648 if acceptanceCriteria is not None:
1649 if not isinstance(acceptanceCriteria, Iterable):
1650 ex = TypeError("Parameter 'acceptanceCriteria' is not iterable.")
1651 ex.add_note(f"Got type '{getFullyQualifiedName(acceptanceCriteria)}'.")
1652 raise ex
1653 elif isinstance(acceptanceCriteria, str):
1654 ex = TypeError("Parameter 'acceptanceCriteria' is a single string, not an iterable of strings.")
1655 ex.add_note(f"Got value '{acceptanceCriteria}'.")
1656 raise ex
1658 for criterion in acceptanceCriteria:
1659 if not isinstance(criterion, str):
1660 ex = TypeError("Parameter 'acceptanceCriteria' contains an element not of type 'str'.")
1661 ex.add_note(f"Got type '{getFullyQualifiedName(criterion)}'.")
1662 raise ex
1663 elif criterion == "":
1664 raise ValueError("Parameter 'acceptanceCriteria' contains an empty criterion.")
1666 self._acceptanceCriteria.append(criterion)
1668 self._kind = kind
1669 self._obligation = obligation
1670 self._verificationMethods = verificationMethods
1671 self._specificationItems = {}
1673 if parent is not None:
1674 parent._AddElement(self)
1676 @readonly
1677 def Kind(self) -> RequirementKind:
1678 """
1679 Read-only property to access what the requirement constrains (:attr:`_kind`).
1681 :returns: The requirement's kind.
1682 """
1683 return self._kind
1685 @readonly
1686 def Obligation(self) -> Obligation:
1687 """
1688 Read-only property to access whether the requirement must, should or may be met (:attr:`_obligation`).
1690 :returns: The requirement's obligation.
1691 """
1692 return self._obligation
1694 @readonly
1695 def VerificationMethods(self) -> VerificationMethod:
1696 """
1697 Read-only property to access the methods verifying the requirement (:attr:`_verificationMethods`).
1699 :returns: The verification methods.
1700 """
1701 return self._verificationMethods
1703 @readonly
1704 def AcceptanceCriteria(self) -> list[str]:
1705 """
1706 Read-only property to access the criteria the verification accepts the requirement by (:attr:`_acceptanceCriteria`).
1708 :returns: The acceptance criteria, in the order given.
1709 """
1710 return self._acceptanceCriteria
1712 @readonly
1713 def SpecificationItems(self) -> dict[str, SpecificationItem]:
1714 """
1715 Read-only property to access the specification items specifying this requirement (:attr:`_specificationItems`).
1717 The relation is maintained by :meth:`SpecificationItem.AddRequirement`.
1719 :returns: The specification items, by ID.
1720 """
1721 return self._specificationItems
1724Requirement._PARENT_TYPE = (Requirement, RequirementSpecification)
1725Requirement._DUPLICATE_ERROR = DuplicateRequirementError
1728@export
1729class SpecificationItem(BaseItem):
1730 """
1731 A specification item: how the system meets requirements.
1733 E.g. a design decision, an architecture or an interface description. It mirrors a requirement - with an ID, a title, a
1734 state, a layer, persons, personas, labels, documents, child items, links and a history -, and specifies any number of
1735 requirements; a requirement is specified by any number of specification items.
1737 Its parent is the :class:`RequirementSpecification` for a top-level item, or the :class:`SpecificationItem` it
1738 details. The specification registers either by its ID.
1739 """
1741 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: An item is in an item or the specification; assigned below the class.
1743 _requirements: dict[str, Requirement] #: The requirements this item specifies, by ID.
1745 def __init__(
1746 self,
1747 identifier: str,
1748 title: str,
1749 description: str = "",
1750 created: Nullable[datetime] = None,
1751 *,
1752 requirements: Nullable[Iterable[Requirement]] = None,
1753 rationale: str = "",
1754 state: ItemState = ItemState.Draft,
1755 layer: Nullable[Layer] = None,
1756 authors: Nullable[Iterable[Person]] = None,
1757 owners: Nullable[Iterable[Person]] = None,
1758 personas: Nullable[Iterable[Persona]] = None,
1759 labels: Nullable[Iterable[Label]] = None,
1760 attachments: Nullable[Iterable[Document]] = None,
1761 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None,
1762 parent: Nullable[SpecificationItem | RequirementSpecification] = None
1763 ) -> None:
1764 """
1765 Initializes a specification item, adds it to its parent, registers it in its specification, and relates it to the
1766 requirements it specifies.
1768 :param identifier: The item's ID.
1769 :param title: The item's title.
1770 :param description: Optional, the item's description. Default: ``""``.
1771 :param created: Optional, the time the item was created; without a time zone, it is a local
1772 time. Default: ``None``, the current time.
1773 :param requirements: Optional, the requirements the item specifies. Default: ``None``.
1774 :param rationale: Optional, why the item exists or is as it is. Default: ``""``.
1775 :param state: Optional, the item's state. Default: :attr:`ItemState.Draft`.
1776 :param layer: Optional, the layer the item belongs to. Default: ``None``.
1777 :param authors: Optional, the authors of the item. Default: ``None``.
1778 :param owners: Optional, the owners of the item. Default: ``None``.
1779 :param personas: Optional, the personas the item is about. Default: ``None``.
1780 :param labels: Optional, the labels of the item. Default: ``None``.
1781 :param attachments: Optional, the documents attached to the item. Default: ``None``.
1782 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
1783 :param parent: Optional, the specification item this one details, or the specification.
1784 Default: ``None``.
1785 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
1786 :raises TypeError: If parameter ``parent`` isn't of type :class:`SpecificationItem` or
1787 :class:`RequirementSpecification`.
1788 :raises ValueError: If parameter ``identifier`` is ``None``.
1789 :raises TypeError: If parameter ``identifier`` isn't of type :class:`str`.
1790 :raises ValueError: If parameter ``identifier`` is empty.
1791 :raises ValueError: If parameter ``description`` is ``None``.
1792 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
1793 :raises ValueError: If parameter ``title`` is ``None``.
1794 :raises TypeError: If parameter ``title`` isn't of type :class:`str`.
1795 :raises ValueError: If parameter ``title`` is empty.
1796 :raises ValueError: If parameter ``rationale`` is ``None``.
1797 :raises TypeError: If parameter ``rationale`` isn't of type :class:`str`.
1798 :raises TypeError: If parameter ``created`` isn't of type :class:`~datetime.datetime`.
1799 :raises ValueError: If parameter ``state`` is ``None``.
1800 :raises TypeError: If parameter ``state`` isn't of type :class:`ItemState`.
1801 :raises TypeError: If parameter ``layer`` isn't of type :class:`Layer`.
1802 :raises NotInSameSpecificationError: If parameter ``layer`` isn't part of the item's specification.
1803 :raises TypeError: If parameter ``authors``, ``owners``, ``personas``, ``labels`` or
1804 ``attachments`` isn't iterable, or is a single element.
1805 :raises TypeError: If parameter ``authors`` or ``owners`` contains an element not of type
1806 :class:`~pyEDAA.Reports.Person`.
1807 :raises TypeError: If parameter ``personas``, ``labels`` or ``attachments`` contains an
1808 element not of type :class:`Persona`, :class:`Label` or :class:`Document`.
1809 :raises NotInSameSpecificationError: If parameter ``authors``, ``owners``, ``personas``, ``labels`` or
1810 ``attachments`` contains an element, which isn't part of the item's
1811 specification.
1812 :raises DuplicatePersonError: If parameter ``authors`` or ``owners`` contains a person twice.
1813 :raises DuplicatePersonaError: If parameter ``personas`` contains a persona twice.
1814 :raises DuplicateLabelError: If parameter ``labels`` contains a label twice.
1815 :raises DuplicateDocumentError: If parameter ``attachments`` contains a document twice.
1816 :raises TypeError: If parameter ``requirements`` isn't iterable, or is a single element.
1817 :raises TypeError: If parameter ``requirements`` contains an element not of type
1818 :class:`Requirement`.
1819 :raises NotInSameSpecificationError: If parameter ``requirements`` contains a requirement, which isn't part of
1820 the item's specification.
1821 :raises DuplicateRequirementError: If parameter ``requirements`` contains a requirement twice.
1822 :raises DuplicateSpecificationItemError: If the specification already contains a specification item of this ID.
1823 """
1824 super().__init__(
1825 identifier,
1826 title,
1827 description,
1828 created,
1829 rationale=rationale,
1830 state=state,
1831 layer=layer,
1832 authors=authors,
1833 owners=owners,
1834 personas=personas,
1835 labels=labels,
1836 attachments=attachments,
1837 keyValuePairs=keyValuePairs,
1838 parent=parent
1839 )
1841 self._requirements = {}
1843 if requirements is not None:
1844 if not isinstance(requirements, Iterable):
1845 ex = TypeError("Parameter 'requirements' is not iterable.")
1846 ex.add_note(f"Got type '{getFullyQualifiedName(requirements)}'.")
1847 raise ex
1848 elif isinstance(requirements, KeyValuePairsMixin):
1849 ex = TypeError("Parameter 'requirements' is a single element, not an iterable of elements.")
1850 ex.add_note(f"Got type '{getFullyQualifiedName(requirements)}'.")
1851 raise ex
1853 for requirement in requirements:
1854 if not isinstance(requirement, Requirement):
1855 ex = TypeError("Parameter 'requirements' contains an element not of type 'Requirement'.")
1856 ex.add_note(f"Got type '{getFullyQualifiedName(requirement)}'.")
1857 raise ex
1858 elif self._root is None or requirement._root is not self._root:
1859 ex = NotInSameSpecificationError(
1860 "Parameter 'requirements' contains a requirement not part of the item's specification."
1861 )
1862 ex.add_note(f"Got requirement '{requirement._id}'.")
1863 raise ex
1864 elif requirement._id in self._requirements:
1865 ex = DuplicateRequirementError("Parameter 'requirements' contains a requirement twice.")
1866 ex.add_note(f"Got requirement '{requirement._id}'.")
1867 raise ex
1869 self._requirements[requirement._id] = requirement
1871 if parent is not None:
1872 parent._AddElement(self)
1874 for requirement in self._requirements.values():
1875 requirement._specificationItems[identifier] = self
1877 @readonly
1878 def Requirements(self) -> dict[str, Requirement]:
1879 """
1880 Read-only property to access the requirements this item specifies (:attr:`_requirements`).
1882 :returns: The requirements, by ID.
1883 """
1884 return self._requirements
1886 def AddRequirement(self, requirement: Requirement) -> None:
1887 """
1888 Relate this item to a requirement it specifies: the item lists the requirement, and the requirement lists the item.
1890 :param requirement: The requirement.
1891 :raises ValueError: If parameter ``requirement`` is ``None``.
1892 :raises TypeError: If parameter ``requirement`` isn't of type :class:`Requirement`.
1893 :raises NotInSameSpecificationError: If parameter ``requirement`` isn't part of the item's specification.
1894 :raises DuplicateRequirementError: If the item specifies the requirement already.
1895 """
1896 if requirement is None:
1897 raise ValueError("Parameter 'requirement' is None.")
1898 elif not isinstance(requirement, Requirement):
1899 ex = TypeError("Parameter 'requirement' is not of type 'Requirement'.")
1900 ex.add_note(f"Got type '{getFullyQualifiedName(requirement)}'.")
1901 raise ex
1902 elif self._root is None or requirement._root is not self._root:
1903 ex = NotInSameSpecificationError("Parameter 'requirement' isn't part of the item's specification.")
1904 ex.add_note(f"Got requirement '{requirement._id}'.")
1905 raise ex
1906 elif requirement._id in self._requirements:
1907 raise DuplicateRequirementError(
1908 f"Specification item '{self._id}' specifies requirement '{requirement._id}' already."
1909 )
1911 self._requirements[requirement._id] = requirement
1912 requirement._specificationItems[self._id] = self
1915SpecificationItem._PARENT_TYPE = (SpecificationItem, RequirementSpecification)
1916SpecificationItem._DUPLICATE_ERROR = DuplicateSpecificationItemError
1919@export
1920class LinkKind(Enum):
1921 """
1922 The relationship a link states: the source item *<kind>* the destination item.
1924 A link connects items of the same or of different layers, e.g. an FPGA firmware requirement derived from an electrical
1925 one. Which specification item specifies which requirement, isn't a link, but the relation of
1926 :attr:`SpecificationItem.Requirements` and :attr:`Requirement.SpecificationItems`.
1927 """
1929 Refines = 1 #: The source states the destination in more detail, outside the item hierarchy.
1930 DerivesFrom = 2 #: The source is derived from the destination, e.g. from a higher layer.
1931 CounterpartOf = 3 #: The source is the destination's counterpart in another layer.
1932 DependsOn = 4 #: The source can only be met, if the destination is met.
1933 ConflictsWith = 5 #: The source and the destination can't both be met.
1934 Duplicates = 6 #: The source states the same as the destination.
1935 Replaces = 7 #: The source replaces the destination, which is obsolete.
1936 RelatesTo = 8 #: Any other relationship.
1939@export
1940class Link(KeyValuePairsMixin, metaclass=ExtendedType, slots=True):
1941 """
1942 A link from an item - its source - to another item of the same specification - its destination -, and the
1943 relationship it states.
1945 A link connects two items of the item hierarchy; it is no element of the hierarchy and has no parent. The source
1946 lists it in :attr:`~BaseItem.OutboundLinks`, the destination in :attr:`~BaseItem.InboundLinks`.
1947 """
1949 _source: BaseItem #: The item the link starts at.
1950 _destination: BaseItem #: The item the link points to.
1951 _kind: LinkKind #: The relationship the link states.
1953 def __init__(
1954 self,
1955 source: BaseItem,
1956 destination: BaseItem,
1957 kind: LinkKind,
1958 *,
1959 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None
1960 ) -> None:
1961 """
1962 Initializes a link, and adds it to its source and its destination.
1964 :param source: The item the link starts at.
1965 :param destination: The item the link points to.
1966 :param kind: The relationship the link states.
1967 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
1968 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
1969 :raises ValueError: If parameter ``source`` is ``None``.
1970 :raises TypeError: If parameter ``source`` isn't of type :class:`BaseItem`.
1971 :raises NotInSameSpecificationError: If parameter ``source`` isn't part of a specification.
1972 :raises ValueError: If parameter ``destination`` is ``None``.
1973 :raises TypeError: If parameter ``destination`` isn't of type :class:`BaseItem`.
1974 :raises ValueError: If parameter ``destination`` is the source.
1975 :raises NotInSameSpecificationError: If parameter ``destination`` isn't part of the source's specification.
1976 :raises ValueError: If parameter ``kind`` is ``None``.
1977 :raises TypeError: If parameter ``kind`` isn't of type :class:`LinkKind`.
1978 :raises DuplicateLinkError: If the source has a link of this kind to this destination already.
1979 """
1980 super().__init__(keyValuePairs)
1982 if source is None:
1983 raise ValueError("Parameter 'source' is None.")
1984 elif not isinstance(source, BaseItem):
1985 ex = TypeError("Parameter 'source' is not of type 'BaseItem'.")
1986 ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.")
1987 raise ex
1988 elif source._root is None:
1989 ex = NotInSameSpecificationError("Parameter 'source' isn't part of a specification.")
1990 ex.add_note(f"Got item '{source._id}'.")
1991 raise ex
1993 if destination is None:
1994 raise ValueError("Parameter 'destination' is None.")
1995 elif not isinstance(destination, BaseItem):
1996 ex = TypeError("Parameter 'destination' is not of type 'BaseItem'.")
1997 ex.add_note(f"Got type '{getFullyQualifiedName(destination)}'.")
1998 raise ex
1999 elif destination is source:
2000 ex = ValueError("Parameter 'destination' is the source.")
2001 ex.add_note(f"Got item '{destination._id}'.")
2002 raise ex
2003 elif destination._root is not source._root:
2004 ex = NotInSameSpecificationError("Parameter 'destination' isn't part of the source's specification.")
2005 ex.add_note(f"Got item '{destination._id}'.")
2006 raise ex
2008 if kind is None:
2009 raise ValueError("Parameter 'kind' is None.")
2010 elif not isinstance(kind, LinkKind):
2011 ex = TypeError("Parameter 'kind' is not of type 'LinkKind'.")
2012 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.")
2013 raise ex
2015 for link in source._outboundLinks:
2016 if link._destination is destination and link._kind is kind:
2017 ex = DuplicateLinkError(f"{getTypeName(source)} '{source._id}' has a '{kind.name}' link already.")
2018 ex.add_note(f"Got destination '{destination._id}'.")
2019 raise ex
2021 self._source = source
2022 self._destination = destination
2023 self._kind = kind
2025 source._outboundLinks.append(self)
2026 destination._inboundLinks.append(self)
2028 @readonly
2029 def Source(self) -> BaseItem:
2030 """
2031 Read-only property to access the item the link starts at (:attr:`_source`).
2033 :returns: The source item.
2034 """
2035 return self._source
2037 @readonly
2038 def Destination(self) -> BaseItem:
2039 """
2040 Read-only property to access the item the link points to (:attr:`_destination`).
2042 :returns: The destination item.
2043 """
2044 return self._destination
2046 @readonly
2047 def Kind(self) -> LinkKind:
2048 """
2049 Read-only property to access the relationship the link states (:attr:`_kind`).
2051 :returns: The link's kind.
2052 """
2053 return self._kind
2056@export
2057class EventKind(Enum):
2058 """
2059 The kind of an event in the life cycle of an item.
2060 """
2062 Opened = 1 #: The item was opened, e.g. written as a draft.
2063 Modified = 2 #: The item's text or fields were changed.
2064 Reviewed = 3 #: The item was reviewed.
2065 Approved = 4 #: The item was approved: it became final.
2066 Rejected = 5 #: The item was rejected.
2067 Split = 6 #: The item was split; the event names the other items involved.
2068 Merged = 7 #: The item was merged; the event names the other items involved.
2069 Closed = 8 #: The item was closed after its verification, e.g. released.
2070 Reopened = 9 #: The closed item was opened again.
2071 Obsoleted = 10 #: The item became obsolete, e.g. replaced by the items the event names.
2074# A class with a property named like a class - ``Person`` - can't name that class in the annotation of a field: the
2075# class body's namespace, where annotations are evaluated, binds the name to the property.
2076_Person = Person
2079@export
2080class HistoryEvent(Base):
2081 """
2082 An event in the life cycle of an item: its kind, its time, the person causing it, and the other items involved.
2084 Its parent is the item, which lists it in :attr:`~BaseItem.History`, in chronological order. A split or a merge names
2085 the other items involved, e.g. the requirements a requirement was split into, or merged with.
2086 """
2088 _PARENT_TYPE: ClassVar[tuple[type, ...]] = (BaseItem, ) #: An event is in the history of an item.
2090 _kind: EventKind #: The kind of the event.
2091 _timestamp: datetime #: The time of the event.
2092 _person: Nullable[_Person] #: The person causing the event, or ``None``.
2093 _involved: list[BaseItem] #: The other items involved, in the order given.
2094 _description: str #: The event's description, or ``""``.
2096 def __init__(
2097 self,
2098 kind: EventKind,
2099 timestamp: datetime,
2100 *,
2101 person: Nullable[Person] = None,
2102 involved: Nullable[Iterable[BaseItem]] = None,
2103 description: str = "",
2104 keyValuePairs: Nullable[Mapping[Hashable, Any]] = None,
2105 parent: Nullable[BaseItem] = None
2106 ) -> None:
2107 """
2108 Initializes a history event, and adds it to the history of its item.
2110 :param kind: The kind of the event.
2111 :param timestamp: The time of the event; without a time zone, it is a local time.
2112 :param person: Optional, the person causing the event. Default: ``None``.
2113 :param involved: Optional, the other items involved. Default: ``None``.
2114 :param description: Optional, the event's description. Default: ``""``.
2115 :param keyValuePairs: Optional, the key-value pairs. Default: ``None``.
2116 :param parent: Optional, the item the event happened to. Default: ``None``.
2117 :raises TypeError: If parameter ``keyValuePairs`` isn't a mapping.
2118 :raises TypeError: If parameter ``parent`` isn't of type :class:`BaseItem`.
2119 :raises ValueError: If parameter ``kind`` is ``None``.
2120 :raises TypeError: If parameter ``kind`` isn't of type :class:`EventKind`.
2121 :raises ValueError: If parameter ``timestamp`` is ``None``.
2122 :raises TypeError: If parameter ``timestamp`` isn't of type :class:`~datetime.datetime`.
2123 :raises TypeError: If parameter ``person`` isn't of type :class:`~pyEDAA.Reports.Person`.
2124 :raises NotInSameSpecificationError: If parameter ``person`` isn't part of the item's specification.
2125 :raises TypeError: If parameter ``involved`` isn't iterable, or is a single element.
2126 :raises TypeError: If parameter ``involved`` contains an element not of type
2127 :class:`BaseItem`.
2128 :raises NotInSameSpecificationError: If parameter ``involved`` contains an item, which isn't part of the item's
2129 specification.
2130 :raises DuplicateRequirementError: If parameter ``involved`` contains a requirement twice.
2131 :raises DuplicateSpecificationItemError: If parameter ``involved`` contains a specification item twice.
2132 :raises ValueError: If parameter ``involved`` names no item for a split or a merge.
2133 :raises ValueError: If parameter ``description`` is ``None``.
2134 :raises TypeError: If parameter ``description`` isn't of type :class:`str`.
2135 :raises RequirementsError: If parameter ``timestamp`` is earlier than the item's creation or its last
2136 event.
2137 """
2138 super().__init__(keyValuePairs=keyValuePairs, parent=parent)
2140 if kind is None:
2141 raise ValueError("Parameter 'kind' is None.")
2142 elif not isinstance(kind, EventKind):
2143 ex = TypeError("Parameter 'kind' is not of type 'EventKind'.")
2144 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.")
2145 raise ex
2147 if timestamp is None:
2148 raise ValueError("Parameter 'timestamp' is None.")
2149 elif not isinstance(timestamp, datetime):
2150 ex = TypeError("Parameter 'timestamp' is not of type 'datetime'.")
2151 ex.add_note(f"Got type '{getFullyQualifiedName(timestamp)}'.")
2152 raise ex
2154 if person is not None:
2155 if not isinstance(person, Person):
2156 ex = TypeError("Parameter 'person' is not of type 'Person'.")
2157 ex.add_note(f"Got type '{getFullyQualifiedName(person)}'.")
2158 raise ex
2159 elif self._root is None or self._root._persons.get(person._userID) is not person:
2160 ex = NotInSameSpecificationError("Parameter 'person' isn't part of the item's specification.")
2161 ex.add_note(f"Got person '{person._userID}'.")
2162 raise ex
2164 self._involved = []
2165 if involved is not None:
2166 if not isinstance(involved, Iterable):
2167 ex = TypeError("Parameter 'involved' is not iterable.")
2168 ex.add_note(f"Got type '{getFullyQualifiedName(involved)}'.")
2169 raise ex
2170 elif isinstance(involved, KeyValuePairsMixin):
2171 ex = TypeError("Parameter 'involved' is a single element, not an iterable of elements.")
2172 ex.add_note(f"Got type '{getFullyQualifiedName(involved)}'.")
2173 raise ex
2175 for item in involved:
2176 if not isinstance(item, BaseItem):
2177 ex = TypeError("Parameter 'involved' contains an element not of type 'BaseItem'.")
2178 ex.add_note(f"Got type '{getFullyQualifiedName(item)}'.")
2179 raise ex
2180 elif self._root is None or item._root is not self._root:
2181 ex = NotInSameSpecificationError(
2182 "Parameter 'involved' contains an item not part of the item's specification."
2183 )
2184 ex.add_note(f"Got item '{item._id}'.")
2185 raise ex
2186 elif item in self._involved:
2187 ex = item._DUPLICATE_ERROR("Parameter 'involved' contains an item twice.")
2188 ex.add_note(f"Got item '{item._id}'.")
2189 raise ex
2191 self._involved.append(item)
2193 if len(self._involved) == 0 and kind in (EventKind.Split, EventKind.Merged):
2194 raise ValueError(f"Parameter 'involved' names no item for a '{kind.name}' event.")
2196 if description is None:
2197 raise ValueError("Parameter 'description' is None.")
2198 elif not isinstance(description, str):
2199 ex = TypeError("Parameter 'description' is not of type 'str'.")
2200 ex.add_note(f"Got type '{getFullyQualifiedName(description)}'.")
2201 raise ex
2203 self._kind = kind
2204 self._timestamp = timestamp
2205 self._person = person
2206 self._description = description
2208 if parent is not None:
2209 parent._AddElement(self)
2211 @readonly
2212 def Kind(self) -> EventKind:
2213 """
2214 Read-only property to access the kind of the event (:attr:`_kind`).
2216 :returns: The event's kind.
2217 """
2218 return self._kind
2220 @readonly
2221 def Timestamp(self) -> datetime:
2222 """
2223 Read-only property to access the time of the event (:attr:`_timestamp`).
2225 :returns: The time; without a time zone, it is a local time.
2226 """
2227 return self._timestamp
2229 @readonly
2230 def Person(self) -> Nullable[Person]:
2231 """
2232 Read-only property to access the person causing the event (:attr:`_person`).
2234 :returns: The person, or ``None``.
2235 """
2236 return self._person
2238 @readonly
2239 def Involved(self) -> list[BaseItem]:
2240 """
2241 Read-only property to access the other items involved in the event (:attr:`_involved`).
2243 :returns: The items, in the order given.
2244 """
2245 return self._involved
2247 @readonly
2248 def Description(self) -> str:
2249 """
2250 Read-only property to access the event's description (:attr:`_description`).
2252 :returns: The description, or ``""``.
2253 """
2254 return self._description