fushinryu-model 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fushinryu_model/__init__.py +25 -0
- fushinryu_model/_merge_utils.py +31 -0
- fushinryu_model/acceptance_criterion.py +69 -0
- fushinryu_model/acceptance_criterion_validation.py +61 -0
- fushinryu_model/py.typed +0 -0
- fushinryu_model/scope.py +114 -0
- fushinryu_model/user_story.py +73 -0
- fushinryu_model-0.1.0.dist-info/METADATA +61 -0
- fushinryu_model-0.1.0.dist-info/RECORD +11 -0
- fushinryu_model-0.1.0.dist-info/WHEEL +4 -0
- fushinryu_model-0.1.0.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Domain model for the Fūshinryū (風心流) development projects management methodology."""
|
|
2
|
+
|
|
3
|
+
from fushinryu_model.acceptance_criterion import AcceptanceCriterion
|
|
4
|
+
from fushinryu_model.acceptance_criterion_validation import (
|
|
5
|
+
AutomatedValidation,
|
|
6
|
+
ManualValidation,
|
|
7
|
+
ValidationEntry,
|
|
8
|
+
)
|
|
9
|
+
from fushinryu_model.scope import Scope
|
|
10
|
+
from fushinryu_model.user_story import UserStory, UserStoryType
|
|
11
|
+
|
|
12
|
+
NAME = "fushinryu-model"
|
|
13
|
+
VERSION = "0.1.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AcceptanceCriterion",
|
|
17
|
+
"AutomatedValidation",
|
|
18
|
+
"ManualValidation",
|
|
19
|
+
"NAME",
|
|
20
|
+
"Scope",
|
|
21
|
+
"UserStory",
|
|
22
|
+
"UserStoryType",
|
|
23
|
+
"ValidationEntry",
|
|
24
|
+
"VERSION",
|
|
25
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Private utilities for merging collections of domain entities by integer id."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
from pleroma.contrib.pydantic import MergeableModel
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound=MergeableModel)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def merge_by_id(parent: frozenset[T], child: frozenset[T]) -> frozenset[T]:
|
|
13
|
+
"""Merges two frozensets of entities keyed by their integer ``id`` field.
|
|
14
|
+
|
|
15
|
+
For entities present in both sets (same id), the child entity is merged into
|
|
16
|
+
the parent using the entity's own :meth:`~pleroma.MergeableModel.merge` method,
|
|
17
|
+
so child field values override parent field values. Entities present in only one
|
|
18
|
+
set are included as-is.
|
|
19
|
+
|
|
20
|
+
:param parent: The base collection.
|
|
21
|
+
:param child: The overriding collection.
|
|
22
|
+
:return: A new frozenset containing all entities from both collections.
|
|
23
|
+
"""
|
|
24
|
+
result: dict[int, Any] = {item.id: item for item in parent} # type: ignore[attr-defined]
|
|
25
|
+
for item in child:
|
|
26
|
+
item_id: int = item.id # type: ignore[attr-defined]
|
|
27
|
+
if item_id in result:
|
|
28
|
+
result[item_id] = type(item).merge([result[item_id], item])
|
|
29
|
+
else:
|
|
30
|
+
result[item_id] = item
|
|
31
|
+
return frozenset(result.values())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""AcceptanceCriterion domain entity for the fushinryu methodology model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Self
|
|
6
|
+
|
|
7
|
+
from pleroma.contrib.pydantic import MergeableModel
|
|
8
|
+
|
|
9
|
+
from fushinryu_model.acceptance_criterion_validation import (
|
|
10
|
+
AutomatedValidation,
|
|
11
|
+
ManualValidation,
|
|
12
|
+
ValidationEntry,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _merge_validations(
|
|
17
|
+
parent: frozenset[ValidationEntry],
|
|
18
|
+
child: frozenset[ValidationEntry],
|
|
19
|
+
) -> frozenset[ValidationEntry]:
|
|
20
|
+
manual: set[ManualValidation] = set()
|
|
21
|
+
auto_by_key: dict[tuple[str, str], AutomatedValidation] = {}
|
|
22
|
+
|
|
23
|
+
for v in parent | child:
|
|
24
|
+
if isinstance(v, ManualValidation):
|
|
25
|
+
manual.add(v)
|
|
26
|
+
else:
|
|
27
|
+
key = (v.source, v.name)
|
|
28
|
+
if key not in auto_by_key or v.timestamp > auto_by_key[key].timestamp:
|
|
29
|
+
auto_by_key[key] = v
|
|
30
|
+
|
|
31
|
+
return frozenset(manual | set(auto_by_key.values()))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AcceptanceCriterion(MergeableModel):
|
|
35
|
+
"""A testable condition attached to a UserStory.
|
|
36
|
+
|
|
37
|
+
:param id: Integer identifier, unique within the enclosing UserStory.
|
|
38
|
+
:param given: Optional description of the initial context or state.
|
|
39
|
+
:param when: Optional description of the trigger condition.
|
|
40
|
+
:param then: Mandatory description of the expected outcome.
|
|
41
|
+
:param active: Whether this criterion is an active requirement.
|
|
42
|
+
Set to ``False`` when a criterion is discarded without removing it.
|
|
43
|
+
:param validations: Validation records attached to this criterion.
|
|
44
|
+
An AC is considered validated when this set is non-empty and every
|
|
45
|
+
record has ``passed=True``.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
id: int
|
|
49
|
+
given: str | None = None
|
|
50
|
+
when: str | None = None
|
|
51
|
+
then: str
|
|
52
|
+
active: bool = True
|
|
53
|
+
validations: frozenset[ValidationEntry] = frozenset()
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_validated(self) -> bool:
|
|
57
|
+
"""Return True when all validations are present and passed."""
|
|
58
|
+
return bool(self.validations) and all(v.passed for v in self.validations)
|
|
59
|
+
|
|
60
|
+
def _merge_two(self, other: Self, *, overwrite_none: bool = False) -> Self:
|
|
61
|
+
kwargs: dict[str, Any] = {}
|
|
62
|
+
for name in type(self).model_fields:
|
|
63
|
+
if name == "validations":
|
|
64
|
+
continue
|
|
65
|
+
self_val: Any = getattr(self, name)
|
|
66
|
+
other_val: Any = getattr(other, name)
|
|
67
|
+
kwargs[name] = other_val if (overwrite_none or other_val is not None) else self_val
|
|
68
|
+
kwargs["validations"] = _merge_validations(self.validations, other.validations)
|
|
69
|
+
return type(self)(**kwargs)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Validation record types for AcceptanceCriterion fulfillment tracking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Annotated, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AcceptanceCriterionValidation(BaseModel):
|
|
12
|
+
"""Base class for a validation record attached to an AcceptanceCriterion.
|
|
13
|
+
|
|
14
|
+
:param passed: Whether the criterion passed this validation.
|
|
15
|
+
:param timestamp: When the validation was recorded (must be timezone-aware).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(frozen=True)
|
|
19
|
+
|
|
20
|
+
passed: bool = False
|
|
21
|
+
timestamp: datetime
|
|
22
|
+
|
|
23
|
+
@field_validator("timestamp")
|
|
24
|
+
@classmethod
|
|
25
|
+
def _require_timezone(cls, v: datetime) -> datetime:
|
|
26
|
+
if v.tzinfo is None:
|
|
27
|
+
raise ValueError("timestamp must be timezone-aware")
|
|
28
|
+
return v
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ManualValidation(AcceptanceCriterionValidation):
|
|
32
|
+
"""A human-authored validation record.
|
|
33
|
+
|
|
34
|
+
:param verdict: Description of why the criterion passed or failed.
|
|
35
|
+
:param kind: Discriminator field, always ``"manual"``.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
kind: Literal["manual"] = "manual"
|
|
39
|
+
verdict: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AutomatedValidation(AcceptanceCriterionValidation):
|
|
43
|
+
"""A test-linked validation record.
|
|
44
|
+
|
|
45
|
+
When two automated validations share the same ``source`` and ``name`` for a
|
|
46
|
+
given criterion, the one with the most recent ``timestamp`` prevails on merge.
|
|
47
|
+
|
|
48
|
+
:param source: The file, class, or module where the validation procedure lives.
|
|
49
|
+
:param name: The test or check name within that source.
|
|
50
|
+
:param kind: Discriminator field, always ``"automated"``.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
kind: Literal["automated"] = "automated"
|
|
54
|
+
source: str
|
|
55
|
+
name: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
ValidationEntry = Annotated[
|
|
59
|
+
ManualValidation | AutomatedValidation,
|
|
60
|
+
Field(discriminator="kind"),
|
|
61
|
+
]
|
fushinryu_model/py.typed
ADDED
|
File without changes
|
fushinryu_model/scope.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Scope domain entity for the fushinryu methodology model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Self
|
|
7
|
+
|
|
8
|
+
from pydantic import field_validator
|
|
9
|
+
from pleroma.contrib.pydantic import MergeableModel
|
|
10
|
+
|
|
11
|
+
from fushinryu_model._merge_utils import merge_by_id
|
|
12
|
+
from fushinryu_model.user_story import UserStory
|
|
13
|
+
|
|
14
|
+
_ID_PATTERN = re.compile(r"(_[a-zA-Z]|[a-zA-Z])[a-zA-Z0-9_]*")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _dfs_linearize(root: Scope) -> list[Scope]:
|
|
18
|
+
"""Returns scopes in decreasing precedence order (root first, base last).
|
|
19
|
+
|
|
20
|
+
Uses DFS pre-order traversal with last-occurrence deduplication: when a
|
|
21
|
+
scope is visited more than once (shared ancestor), the later visit wins,
|
|
22
|
+
pushing the ancestor toward the end (lowest precedence).
|
|
23
|
+
|
|
24
|
+
:param root: The scope to linearise.
|
|
25
|
+
:return: List of scopes from highest to lowest merge precedence.
|
|
26
|
+
:raises ValueError: If a cycle is detected in the parent graph.
|
|
27
|
+
"""
|
|
28
|
+
last_visit: dict[str, int] = {}
|
|
29
|
+
scope_map: dict[str, Scope] = {}
|
|
30
|
+
visiting: set[str] = set()
|
|
31
|
+
counter: list[int] = [0]
|
|
32
|
+
|
|
33
|
+
def _visit(scope: Scope) -> None:
|
|
34
|
+
if scope.id in visiting:
|
|
35
|
+
raise ValueError(f"Cycle detected involving scope '{scope.id}'")
|
|
36
|
+
visiting.add(scope.id)
|
|
37
|
+
last_visit[scope.id] = counter[0]
|
|
38
|
+
scope_map[scope.id] = scope
|
|
39
|
+
counter[0] += 1
|
|
40
|
+
for parent in scope.parents:
|
|
41
|
+
_visit(parent)
|
|
42
|
+
visiting.discard(scope.id)
|
|
43
|
+
|
|
44
|
+
_visit(root)
|
|
45
|
+
return sorted(scope_map.values(), key=lambda s: last_visit[s.id])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Scope(MergeableModel):
|
|
49
|
+
"""An organizational unit grouping related project artifacts.
|
|
50
|
+
|
|
51
|
+
:param name: Human-readable display name for the scope.
|
|
52
|
+
:param id: Identifier following programming variable naming conventions:
|
|
53
|
+
starts with a letter or underscore-then-letter, followed by letters,
|
|
54
|
+
digits, or underscores.
|
|
55
|
+
:param description: A description of the scope's purpose.
|
|
56
|
+
:param user_stories: The high-level requirements belonging to this scope.
|
|
57
|
+
Each story's id must be unique within this scope.
|
|
58
|
+
:param parents: Ordered parent scopes forming a directed acyclic graph.
|
|
59
|
+
The first parent takes precedence over subsequent parents during
|
|
60
|
+
collapse. Defaults to an empty sequence (no parents).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
id: str
|
|
65
|
+
description: str
|
|
66
|
+
user_stories: frozenset[UserStory] = frozenset()
|
|
67
|
+
parents: tuple[Scope, ...] = ()
|
|
68
|
+
|
|
69
|
+
@field_validator("id")
|
|
70
|
+
@classmethod
|
|
71
|
+
def _validate_id(cls, v: str) -> str:
|
|
72
|
+
if not _ID_PATTERN.fullmatch(v):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"id must follow identifier naming: starts with a letter or _+letter, "
|
|
75
|
+
"followed by letters, digits, or underscores"
|
|
76
|
+
)
|
|
77
|
+
return v
|
|
78
|
+
|
|
79
|
+
@field_validator("user_stories")
|
|
80
|
+
@classmethod
|
|
81
|
+
def _validate_unique_us_ids(cls, v: frozenset[UserStory]) -> frozenset[UserStory]:
|
|
82
|
+
ids = [us.id for us in v]
|
|
83
|
+
if len(ids) != len(set(ids)):
|
|
84
|
+
raise ValueError("user_stories ids must be unique within a Scope")
|
|
85
|
+
return v
|
|
86
|
+
|
|
87
|
+
def _merge_two(self, other: Self, *, overwrite_none: bool = False) -> Self:
|
|
88
|
+
kwargs: dict[str, Any] = {}
|
|
89
|
+
for name in type(self).model_fields:
|
|
90
|
+
self_val: Any = getattr(self, name)
|
|
91
|
+
other_val: Any = getattr(other, name)
|
|
92
|
+
kwargs[name] = other_val if (overwrite_none or other_val is not None) else self_val
|
|
93
|
+
kwargs["user_stories"] = merge_by_id(self.user_stories, other.user_stories)
|
|
94
|
+
return type(self)(**kwargs)
|
|
95
|
+
|
|
96
|
+
def collapse(self) -> Scope:
|
|
97
|
+
"""Merges the full scope hierarchy into a single flat Scope.
|
|
98
|
+
|
|
99
|
+
Linearises the ancestor DAG using DFS pre-order with last-occurrence
|
|
100
|
+
deduplication, then merges all scopes from lowest to highest precedence.
|
|
101
|
+
The returned scope has no parents.
|
|
102
|
+
|
|
103
|
+
:return: A new Scope representing the effective merged scope.
|
|
104
|
+
:raises ValueError: If a cycle is detected in the parent graph.
|
|
105
|
+
"""
|
|
106
|
+
order = _dfs_linearize(self)
|
|
107
|
+
flat = [s.model_copy(update={"parents": ()}) for s in order]
|
|
108
|
+
result = flat[-1]
|
|
109
|
+
for scope in flat[-2::-1]:
|
|
110
|
+
result = type(self).merge([result, scope])
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
Scope.model_rebuild()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""UserStory domain entity for the fushinryu methodology model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Self
|
|
7
|
+
|
|
8
|
+
from pydantic import field_validator
|
|
9
|
+
from pleroma.contrib.pydantic import MergeableModel
|
|
10
|
+
|
|
11
|
+
from fushinryu_model._merge_utils import merge_by_id
|
|
12
|
+
from fushinryu_model.acceptance_criterion import AcceptanceCriterion
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UserStoryType(str, Enum):
|
|
16
|
+
"""Classifies a UserStory as functional or technical."""
|
|
17
|
+
|
|
18
|
+
FUNCTIONAL = "functional"
|
|
19
|
+
TECHNICAL = "technical"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UserStory(MergeableModel):
|
|
23
|
+
"""A high-level requirement scoped to an organizational unit.
|
|
24
|
+
|
|
25
|
+
:param id: Integer identifier, unique within the enclosing Scope.
|
|
26
|
+
:param who: The role or persona that wants the feature.
|
|
27
|
+
:param what: The capability or behaviour desired.
|
|
28
|
+
:param why: The business or technical justification.
|
|
29
|
+
:param type: Whether the story is functional or technical.
|
|
30
|
+
:param active: Whether this story is an active requirement.
|
|
31
|
+
Set to ``False`` when a story is retired without removing it.
|
|
32
|
+
An inactive story implicitly deactivates all its acceptance criteria
|
|
33
|
+
for validation purposes.
|
|
34
|
+
:param acceptance_criteria: The testable conditions defining completion.
|
|
35
|
+
Each criterion's id must be unique within this story.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
id: int
|
|
39
|
+
who: str
|
|
40
|
+
what: str
|
|
41
|
+
why: str
|
|
42
|
+
type: UserStoryType
|
|
43
|
+
active: bool = True
|
|
44
|
+
acceptance_criteria: frozenset[AcceptanceCriterion] = frozenset()
|
|
45
|
+
|
|
46
|
+
@field_validator("acceptance_criteria")
|
|
47
|
+
@classmethod
|
|
48
|
+
def _validate_unique_ac_ids(
|
|
49
|
+
cls, v: frozenset[AcceptanceCriterion]
|
|
50
|
+
) -> frozenset[AcceptanceCriterion]:
|
|
51
|
+
ids = [ac.id for ac in v]
|
|
52
|
+
if len(ids) != len(set(ids)):
|
|
53
|
+
raise ValueError("acceptance_criteria ids must be unique within a UserStory")
|
|
54
|
+
return v
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_validated(self) -> bool:
|
|
58
|
+
"""Return True when the story is active, has active criteria, and all are validated."""
|
|
59
|
+
if not self.active:
|
|
60
|
+
return False
|
|
61
|
+
active = [ac for ac in self.acceptance_criteria if ac.active]
|
|
62
|
+
return bool(active) and all(ac.is_validated for ac in active)
|
|
63
|
+
|
|
64
|
+
def _merge_two(self, other: Self, *, overwrite_none: bool = False) -> Self:
|
|
65
|
+
kwargs: dict[str, Any] = {}
|
|
66
|
+
for name in type(self).model_fields:
|
|
67
|
+
self_val: Any = getattr(self, name)
|
|
68
|
+
other_val: Any = getattr(other, name)
|
|
69
|
+
kwargs[name] = other_val if (overwrite_none or other_val is not None) else self_val
|
|
70
|
+
kwargs["acceptance_criteria"] = merge_by_id(
|
|
71
|
+
self.acceptance_criteria, other.acceptance_criteria
|
|
72
|
+
)
|
|
73
|
+
return type(self)(**kwargs)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fushinryu-model
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Model of the Fushinryu development projects management methodology.
|
|
5
|
+
Project-URL: Repository, https://gitlab.com/Kencho1/fushinryu-model
|
|
6
|
+
Project-URL: Documentation, https://fushinryu-model.readthedocs.io
|
|
7
|
+
Author: Jesús Alonso Abad
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: pleroma>=0.2.0
|
|
21
|
+
Requires-Dist: pydantic>=2.0
|
|
22
|
+
Provides-Extra: docs
|
|
23
|
+
Requires-Dist: mkdocs-material>=9.0; extra == 'docs'
|
|
24
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: mypy~=1.8; extra == 'test'
|
|
27
|
+
Requires-Dist: pytest-cov~=6.0; extra == 'test'
|
|
28
|
+
Requires-Dist: pytest-mark-ac~=1.1; extra == 'test'
|
|
29
|
+
Requires-Dist: pytest~=8.3; extra == 'test'
|
|
30
|
+
Requires-Dist: pyyaml~=6.0; extra == 'test'
|
|
31
|
+
Requires-Dist: ruff~=0.8; extra == 'test'
|
|
32
|
+
Requires-Dist: tox~=4.0; extra == 'test'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# fushinryu-model
|
|
36
|
+
|
|
37
|
+
Model of the Fūshinryū (風心流) development projects management methodology.
|
|
38
|
+
|
|
39
|
+
[](https://www.python.org)
|
|
40
|
+
[](LICENSE.md)
|
|
41
|
+
[](https://fushinryu-model.readthedocs.io)
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
pip install fushinryu-model
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick example
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from datetime import datetime, timezone
|
|
53
|
+
from fushinryu_model import AcceptanceCriterion, ManualValidation
|
|
54
|
+
|
|
55
|
+
ac = AcceptanceCriterion(id=1, then="the system returns HTTP 200")
|
|
56
|
+
validation = ManualValidation(verdict="verified manually", passed=True, timestamp=datetime.now(timezone.utc))
|
|
57
|
+
ac = ac.model_copy(update={"validations": frozenset([validation])})
|
|
58
|
+
assert ac.is_validated
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Full documentation: <https://fushinryu-model.readthedocs.io>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
fushinryu_model/__init__.py,sha256=sj7dqQcvXPhcBZkZ5AFxk6HP2glhShWNLwzwGQXsFnM,640
|
|
2
|
+
fushinryu_model/_merge_utils.py,sha256=y8SJCZSDfJQHFRpLx4xq-Y11ZFPdd3t6eNOmxna2guQ,1216
|
|
3
|
+
fushinryu_model/acceptance_criterion.py,sha256=L2gtyjIHfRquz8OqgDI4y4m2yBbDey5hnONuZNbmG1I,2523
|
|
4
|
+
fushinryu_model/acceptance_criterion_validation.py,sha256=oEN9-hNWvHznz4Ng6Xl3ouhBiIMgbf7OhUv0CFMRLFs,1807
|
|
5
|
+
fushinryu_model/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
fushinryu_model/scope.py,sha256=-X5gLw0OOy3npOfFKAu47Q1o9w3VRjB_EJqCA_KYcrQ,4278
|
|
7
|
+
fushinryu_model/user_story.py,sha256=0pER3SgDNt9EgZ7lE6G2S-5bUH6l3ms8mwhvEcZJyCo,2711
|
|
8
|
+
fushinryu_model-0.1.0.dist-info/METADATA,sha256=IApfJ4zkYIF-qmqtnbmew78H2yt_R6faNykZAGuNP7g,2301
|
|
9
|
+
fushinryu_model-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
10
|
+
fushinryu_model-0.1.0.dist-info/licenses/LICENSE.md,sha256=uRNHhD7Ir7jDL77vGaOfzh-omRzVWtlIf7FZED1oXzA,1075
|
|
11
|
+
fushinryu_model-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jesús Alonso Abad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|