forge-mvc-workflow 1.0.0b6__tar.gz
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.
- forge_mvc_workflow-1.0.0b6/PKG-INFO +101 -0
- forge_mvc_workflow-1.0.0b6/README.md +81 -0
- forge_mvc_workflow-1.0.0b6/forge_mvc_workflow/__init__.py +54 -0
- forge_mvc_workflow-1.0.0b6/forge_mvc_workflow/jinja.py +70 -0
- forge_mvc_workflow-1.0.0b6/forge_mvc_workflow/status.py +114 -0
- forge_mvc_workflow-1.0.0b6/forge_mvc_workflow/transitions.py +87 -0
- forge_mvc_workflow-1.0.0b6/forge_mvc_workflow.egg-info/PKG-INFO +101 -0
- forge_mvc_workflow-1.0.0b6/forge_mvc_workflow.egg-info/SOURCES.txt +11 -0
- forge_mvc_workflow-1.0.0b6/forge_mvc_workflow.egg-info/dependency_links.txt +1 -0
- forge_mvc_workflow-1.0.0b6/forge_mvc_workflow.egg-info/requires.txt +2 -0
- forge_mvc_workflow-1.0.0b6/forge_mvc_workflow.egg-info/top_level.txt +1 -0
- forge_mvc_workflow-1.0.0b6/pyproject.toml +35 -0
- forge_mvc_workflow-1.0.0b6/setup.cfg +4 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forge-mvc-workflow
|
|
3
|
+
Version: 1.0.0b6
|
|
4
|
+
Summary: Forge workflow — statuts et transitions applicatives.
|
|
5
|
+
Author: Roger Cauchon
|
|
6
|
+
License-Expression: LicenseRef-Forge-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/caucrogeGit/Forge
|
|
8
|
+
Project-URL: Repository, https://github.com/caucrogeGit/Forge
|
|
9
|
+
Project-URL: Documentation, https://caucrogegit.github.io/Forge/
|
|
10
|
+
Keywords: python,mvc,forge,workflow,statuts
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: forge-mvc<2,>=1.0.0b5
|
|
19
|
+
Requires-Dist: markupsafe>=2.0
|
|
20
|
+
|
|
21
|
+
# forge-mvc-workflow
|
|
22
|
+
|
|
23
|
+
Module workflow pour Forge — statuts et transitions applicatives.
|
|
24
|
+
|
|
25
|
+
Extrait du core Forge depuis la version 2.6.0 (ADR-004).
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install forge-mvc-workflow
|
|
31
|
+
# ou en mode developpement
|
|
32
|
+
pip install -e packages/forge-mvc-workflow/
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from forge_mvc_workflow import (
|
|
39
|
+
WorkflowStatus,
|
|
40
|
+
WorkflowTransition,
|
|
41
|
+
make_status,
|
|
42
|
+
make_transition,
|
|
43
|
+
validate_statuses,
|
|
44
|
+
validate_transitions,
|
|
45
|
+
can_transition,
|
|
46
|
+
get_available_transitions,
|
|
47
|
+
make_workflow_jinja_helpers,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Definir des statuts
|
|
51
|
+
statuses = validate_statuses([
|
|
52
|
+
make_status("brouillon", label="Brouillon", color="gray"),
|
|
53
|
+
make_status("confirme", label="Confirme", color="green"),
|
|
54
|
+
make_status("annule", label="Annule", color="red"),
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
# Definir des transitions autorisees
|
|
58
|
+
transitions = validate_transitions([
|
|
59
|
+
make_transition("brouillon", "confirme"),
|
|
60
|
+
make_transition("brouillon", "annule"),
|
|
61
|
+
make_transition("confirme", "annule"),
|
|
62
|
+
], statuses=statuses)
|
|
63
|
+
|
|
64
|
+
# Verifier une transition
|
|
65
|
+
if can_transition(transitions, "brouillon", "confirme"):
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
# Obtenir les transitions disponibles depuis un statut
|
|
69
|
+
available = get_available_transitions(transitions, "brouillon")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Helpers Jinja2
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
env.globals.update(make_workflow_jinja_helpers())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Dans un template :
|
|
79
|
+
|
|
80
|
+
```jinja2
|
|
81
|
+
{{ workflow_status_badge(reservation.status) }}
|
|
82
|
+
{{ workflow_status_label(reservation.status) }}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Cas d'usage
|
|
86
|
+
|
|
87
|
+
- Statut d'une reservation (brouillon → confirmee → terminee → annulee)
|
|
88
|
+
- Statut d'un document (redaction → validation → publie)
|
|
89
|
+
- Statut d'une commande (panier → payee → expediee → livree)
|
|
90
|
+
|
|
91
|
+
## API publique
|
|
92
|
+
|
|
93
|
+
- `WorkflowStatus` — dataclass statut (name, label, color)
|
|
94
|
+
- `WorkflowTransition` — dataclass transition (from_status, to_status)
|
|
95
|
+
- `make_status(name, label, color)` — constructeur valide
|
|
96
|
+
- `make_transition(from_status, to_status)` — constructeur valide
|
|
97
|
+
- `validate_statuses(list)` — valide une liste de statuts
|
|
98
|
+
- `validate_transitions(list, statuses)` — valide les transitions par rapport aux statuts
|
|
99
|
+
- `can_transition(transitions, from_name, to_name)` — teste une transition
|
|
100
|
+
- `get_available_transitions(transitions, from_name)` — liste les transitions disponibles
|
|
101
|
+
- `make_workflow_jinja_helpers()` — dict de helpers Jinja2
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# forge-mvc-workflow
|
|
2
|
+
|
|
3
|
+
Module workflow pour Forge — statuts et transitions applicatives.
|
|
4
|
+
|
|
5
|
+
Extrait du core Forge depuis la version 2.6.0 (ADR-004).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install forge-mvc-workflow
|
|
11
|
+
# ou en mode developpement
|
|
12
|
+
pip install -e packages/forge-mvc-workflow/
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from forge_mvc_workflow import (
|
|
19
|
+
WorkflowStatus,
|
|
20
|
+
WorkflowTransition,
|
|
21
|
+
make_status,
|
|
22
|
+
make_transition,
|
|
23
|
+
validate_statuses,
|
|
24
|
+
validate_transitions,
|
|
25
|
+
can_transition,
|
|
26
|
+
get_available_transitions,
|
|
27
|
+
make_workflow_jinja_helpers,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Definir des statuts
|
|
31
|
+
statuses = validate_statuses([
|
|
32
|
+
make_status("brouillon", label="Brouillon", color="gray"),
|
|
33
|
+
make_status("confirme", label="Confirme", color="green"),
|
|
34
|
+
make_status("annule", label="Annule", color="red"),
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
# Definir des transitions autorisees
|
|
38
|
+
transitions = validate_transitions([
|
|
39
|
+
make_transition("brouillon", "confirme"),
|
|
40
|
+
make_transition("brouillon", "annule"),
|
|
41
|
+
make_transition("confirme", "annule"),
|
|
42
|
+
], statuses=statuses)
|
|
43
|
+
|
|
44
|
+
# Verifier une transition
|
|
45
|
+
if can_transition(transitions, "brouillon", "confirme"):
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
# Obtenir les transitions disponibles depuis un statut
|
|
49
|
+
available = get_available_transitions(transitions, "brouillon")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Helpers Jinja2
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
env.globals.update(make_workflow_jinja_helpers())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Dans un template :
|
|
59
|
+
|
|
60
|
+
```jinja2
|
|
61
|
+
{{ workflow_status_badge(reservation.status) }}
|
|
62
|
+
{{ workflow_status_label(reservation.status) }}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Cas d'usage
|
|
66
|
+
|
|
67
|
+
- Statut d'une reservation (brouillon → confirmee → terminee → annulee)
|
|
68
|
+
- Statut d'un document (redaction → validation → publie)
|
|
69
|
+
- Statut d'une commande (panier → payee → expediee → livree)
|
|
70
|
+
|
|
71
|
+
## API publique
|
|
72
|
+
|
|
73
|
+
- `WorkflowStatus` — dataclass statut (name, label, color)
|
|
74
|
+
- `WorkflowTransition` — dataclass transition (from_status, to_status)
|
|
75
|
+
- `make_status(name, label, color)` — constructeur valide
|
|
76
|
+
- `make_transition(from_status, to_status)` — constructeur valide
|
|
77
|
+
- `validate_statuses(list)` — valide une liste de statuts
|
|
78
|
+
- `validate_transitions(list, statuses)` — valide les transitions par rapport aux statuts
|
|
79
|
+
- `can_transition(transitions, from_name, to_name)` — teste une transition
|
|
80
|
+
- `get_available_transitions(transitions, from_name)` — liste les transitions disponibles
|
|
81
|
+
- `make_workflow_jinja_helpers()` — dict de helpers Jinja2
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Forge workflow — statuts et transitions applicatives.
|
|
2
|
+
|
|
3
|
+
Module officiel Forge livre separement depuis Forge 2.6.0 (ADR-004).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from forge_mvc_workflow.jinja import (
|
|
9
|
+
make_workflow_jinja_helpers,
|
|
10
|
+
workflow_status_badge,
|
|
11
|
+
workflow_status_badge_class,
|
|
12
|
+
workflow_status_color,
|
|
13
|
+
workflow_status_label,
|
|
14
|
+
)
|
|
15
|
+
from forge_mvc_workflow.status import (
|
|
16
|
+
WorkflowStatus,
|
|
17
|
+
WorkflowStatusError,
|
|
18
|
+
find_status,
|
|
19
|
+
make_status,
|
|
20
|
+
normalize_status_name,
|
|
21
|
+
validate_status_name,
|
|
22
|
+
validate_statuses,
|
|
23
|
+
)
|
|
24
|
+
from forge_mvc_workflow.transitions import (
|
|
25
|
+
WorkflowTransition,
|
|
26
|
+
WorkflowTransitionError,
|
|
27
|
+
can_transition,
|
|
28
|
+
get_available_transitions,
|
|
29
|
+
make_transition,
|
|
30
|
+
validate_transitions,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__version__ = "1.0.0b6"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"WorkflowStatus",
|
|
37
|
+
"WorkflowStatusError",
|
|
38
|
+
"find_status",
|
|
39
|
+
"make_status",
|
|
40
|
+
"normalize_status_name",
|
|
41
|
+
"validate_status_name",
|
|
42
|
+
"validate_statuses",
|
|
43
|
+
"WorkflowTransition",
|
|
44
|
+
"WorkflowTransitionError",
|
|
45
|
+
"can_transition",
|
|
46
|
+
"get_available_transitions",
|
|
47
|
+
"make_transition",
|
|
48
|
+
"validate_transitions",
|
|
49
|
+
"make_workflow_jinja_helpers",
|
|
50
|
+
"workflow_status_badge",
|
|
51
|
+
"workflow_status_badge_class",
|
|
52
|
+
"workflow_status_color",
|
|
53
|
+
"workflow_status_label",
|
|
54
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Helpers Jinja d'affichage de statuts workflow pour Forge."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from markupsafe import Markup, escape
|
|
6
|
+
|
|
7
|
+
from .status import WorkflowStatus
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_BASE = "inline-flex items-center rounded-full px-2 py-1 text-xs font-medium"
|
|
11
|
+
|
|
12
|
+
_BADGE_CLASSES: dict[str, str] = {
|
|
13
|
+
"gray": f"{_BASE} bg-gray-100 text-gray-700",
|
|
14
|
+
"blue": f"{_BASE} bg-blue-100 text-blue-700",
|
|
15
|
+
"green": f"{_BASE} bg-green-100 text-green-700",
|
|
16
|
+
"yellow": f"{_BASE} bg-yellow-100 text-yellow-700",
|
|
17
|
+
"red": f"{_BASE} bg-red-100 text-red-700",
|
|
18
|
+
"purple": f"{_BASE} bg-purple-100 text-purple-700",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_DEFAULT_COLOR = "gray"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def workflow_status_label(status: WorkflowStatus | str | None) -> str:
|
|
25
|
+
"""Return the display label for a status.
|
|
26
|
+
|
|
27
|
+
Accepts a WorkflowStatus, a plain string, or None.
|
|
28
|
+
Falls back to the name if label is empty, or to an empty string for None.
|
|
29
|
+
"""
|
|
30
|
+
if isinstance(status, WorkflowStatus):
|
|
31
|
+
return status.label or status.name
|
|
32
|
+
if status is None:
|
|
33
|
+
return ""
|
|
34
|
+
return str(status)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def workflow_status_color(status: WorkflowStatus | str | None) -> str:
|
|
38
|
+
"""Return the color string for a status, defaulting to 'gray'."""
|
|
39
|
+
if isinstance(status, WorkflowStatus):
|
|
40
|
+
return status.color or _DEFAULT_COLOR
|
|
41
|
+
return _DEFAULT_COLOR
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def workflow_status_badge_class(status: WorkflowStatus | str | None) -> str:
|
|
45
|
+
"""Return Tailwind CSS classes for a status badge.
|
|
46
|
+
|
|
47
|
+
Unmapped colors fall back to the gray palette.
|
|
48
|
+
"""
|
|
49
|
+
color = workflow_status_color(status)
|
|
50
|
+
return _BADGE_CLASSES.get(color, _BADGE_CLASSES[_DEFAULT_COLOR])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def workflow_status_badge(status: WorkflowStatus | str | None) -> Markup:
|
|
54
|
+
"""Return a safe HTML <span> badge for a status.
|
|
55
|
+
|
|
56
|
+
Marked as Markup so Jinja2 does not double-escape it.
|
|
57
|
+
"""
|
|
58
|
+
label = workflow_status_label(status)
|
|
59
|
+
classes = workflow_status_badge_class(status)
|
|
60
|
+
return Markup(f'<span class="{classes}">{escape(label)}</span>')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def make_workflow_jinja_helpers() -> dict[str, object]:
|
|
64
|
+
"""Return a dict of workflow helpers ready for injection into a Jinja2 env."""
|
|
65
|
+
return {
|
|
66
|
+
"workflow_status_label": workflow_status_label,
|
|
67
|
+
"workflow_status_color": workflow_status_color,
|
|
68
|
+
"workflow_status_badge_class": workflow_status_badge_class,
|
|
69
|
+
"workflow_status_badge": workflow_status_badge,
|
|
70
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Statuts génériques de workflow pour Forge."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_VALID_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
10
|
+
_SAFE_NORMALIZE_RE = re.compile(r"[\s\-]+")
|
|
11
|
+
_UNSAFE_CHARS_RE = re.compile(r"[^a-z0-9_\s\-]")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WorkflowStatusError(ValueError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class WorkflowStatus:
|
|
20
|
+
name: str
|
|
21
|
+
label: str = ""
|
|
22
|
+
color: str = ""
|
|
23
|
+
is_initial: bool = False
|
|
24
|
+
is_final: bool = False
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
self.name = validate_status_name(self.name)
|
|
28
|
+
if not self.label:
|
|
29
|
+
self.label = self.name
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def normalize_status_name(value: str) -> str:
|
|
33
|
+
"""Convert a raw string to a valid snake_case status name.
|
|
34
|
+
|
|
35
|
+
Only spaces and hyphens are converted to underscores. Any other
|
|
36
|
+
non-alphanumeric character causes a WorkflowStatusError.
|
|
37
|
+
"""
|
|
38
|
+
if not isinstance(value, str):
|
|
39
|
+
raise WorkflowStatusError("Le nom de statut doit être une chaîne.")
|
|
40
|
+
lowered = value.strip().lower()
|
|
41
|
+
if _UNSAFE_CHARS_RE.search(lowered):
|
|
42
|
+
raise WorkflowStatusError(
|
|
43
|
+
f"Le nom de statut '{value}' contient des caractères non autorisés. "
|
|
44
|
+
"Utilisez uniquement des lettres, des chiffres, des espaces et des tirets."
|
|
45
|
+
)
|
|
46
|
+
normalized = _SAFE_NORMALIZE_RE.sub("_", lowered)
|
|
47
|
+
normalized = re.sub(r"_+", "_", normalized).strip("_")
|
|
48
|
+
return normalized
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_status_name(value: str) -> str:
|
|
52
|
+
"""Validate and return a normalized status name, or raise WorkflowStatusError."""
|
|
53
|
+
if not isinstance(value, str):
|
|
54
|
+
raise WorkflowStatusError("Le nom de statut doit être une chaîne.")
|
|
55
|
+
if not value or not value.strip():
|
|
56
|
+
raise WorkflowStatusError("Le nom de statut ne peut pas être vide.")
|
|
57
|
+
normalized = normalize_status_name(value)
|
|
58
|
+
if not normalized:
|
|
59
|
+
raise WorkflowStatusError(
|
|
60
|
+
f"Le nom de statut '{value}' est invalide après normalisation."
|
|
61
|
+
)
|
|
62
|
+
if not _VALID_NAME_RE.match(normalized):
|
|
63
|
+
raise WorkflowStatusError(
|
|
64
|
+
f"Le nom de statut '{value}' contient des caractères non autorisés. "
|
|
65
|
+
"Utilisez uniquement des lettres minuscules, des chiffres et des tirets bas."
|
|
66
|
+
)
|
|
67
|
+
return normalized
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def make_status(
|
|
71
|
+
name: str,
|
|
72
|
+
label: str = "",
|
|
73
|
+
color: str = "",
|
|
74
|
+
is_initial: bool = False,
|
|
75
|
+
is_final: bool = False,
|
|
76
|
+
) -> WorkflowStatus:
|
|
77
|
+
"""Create a WorkflowStatus with validated and normalized name."""
|
|
78
|
+
return WorkflowStatus(
|
|
79
|
+
name=name,
|
|
80
|
+
label=label,
|
|
81
|
+
color=color,
|
|
82
|
+
is_initial=is_initial,
|
|
83
|
+
is_final=is_final,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate_statuses(statuses: list[WorkflowStatus]) -> list[WorkflowStatus]:
|
|
88
|
+
"""Validate a list of statuses: no duplicates, at most one initial."""
|
|
89
|
+
seen_names: set[str] = set()
|
|
90
|
+
initial_count = 0
|
|
91
|
+
for status in statuses:
|
|
92
|
+
if status.name in seen_names:
|
|
93
|
+
raise WorkflowStatusError(
|
|
94
|
+
f"Statut en doublon : '{status.name}'."
|
|
95
|
+
)
|
|
96
|
+
seen_names.add(status.name)
|
|
97
|
+
if status.is_initial:
|
|
98
|
+
initial_count += 1
|
|
99
|
+
if initial_count > 1:
|
|
100
|
+
raise WorkflowStatusError(
|
|
101
|
+
f"{initial_count} statuts marqués comme initiaux. "
|
|
102
|
+
"Au plus un statut peut être initial."
|
|
103
|
+
)
|
|
104
|
+
return statuses
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def find_status(
|
|
108
|
+
statuses: list[WorkflowStatus], name: str
|
|
109
|
+
) -> WorkflowStatus | None:
|
|
110
|
+
"""Return the status with matching name, or None if not found."""
|
|
111
|
+
for status in statuses:
|
|
112
|
+
if status.name == name:
|
|
113
|
+
return status
|
|
114
|
+
return None
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Transitions génériques de workflow pour Forge."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .status import WorkflowStatus, WorkflowStatusError, validate_status_name
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WorkflowTransitionError(ValueError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class WorkflowTransition:
|
|
16
|
+
from_status: str
|
|
17
|
+
to_status: str
|
|
18
|
+
|
|
19
|
+
def __post_init__(self) -> None:
|
|
20
|
+
try:
|
|
21
|
+
object.__setattr__(self, "from_status", validate_status_name(self.from_status))
|
|
22
|
+
object.__setattr__(self, "to_status", validate_status_name(self.to_status))
|
|
23
|
+
except WorkflowStatusError as exc:
|
|
24
|
+
raise WorkflowTransitionError(str(exc)) from exc
|
|
25
|
+
if self.from_status == self.to_status:
|
|
26
|
+
raise WorkflowTransitionError(
|
|
27
|
+
f"Une transition ne peut pas pointer vers le même statut : '{self.from_status}'."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def make_transition(from_status: str, to_status: str) -> WorkflowTransition:
|
|
32
|
+
"""Create a validated WorkflowTransition."""
|
|
33
|
+
return WorkflowTransition(from_status=from_status, to_status=to_status)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_transitions(
|
|
37
|
+
transitions: list[WorkflowTransition],
|
|
38
|
+
statuses: list[WorkflowStatus] | None = None,
|
|
39
|
+
) -> list[WorkflowTransition]:
|
|
40
|
+
"""Validate a list of transitions.
|
|
41
|
+
|
|
42
|
+
Checks:
|
|
43
|
+
- No duplicate (from_status, to_status) pairs.
|
|
44
|
+
- If statuses is provided, all referenced names must exist in that list.
|
|
45
|
+
"""
|
|
46
|
+
seen: set[tuple[str, str]] = set()
|
|
47
|
+
for t in transitions:
|
|
48
|
+
key = (t.from_status, t.to_status)
|
|
49
|
+
if key in seen:
|
|
50
|
+
raise WorkflowTransitionError(
|
|
51
|
+
f"Transition en doublon : '{t.from_status}' → '{t.to_status}'."
|
|
52
|
+
)
|
|
53
|
+
seen.add(key)
|
|
54
|
+
|
|
55
|
+
if statuses is not None:
|
|
56
|
+
known = {s.name for s in statuses}
|
|
57
|
+
for t in transitions:
|
|
58
|
+
if t.from_status not in known:
|
|
59
|
+
raise WorkflowTransitionError(
|
|
60
|
+
f"Statut source inconnu : '{t.from_status}'."
|
|
61
|
+
)
|
|
62
|
+
if t.to_status not in known:
|
|
63
|
+
raise WorkflowTransitionError(
|
|
64
|
+
f"Statut cible inconnu : '{t.to_status}'."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return transitions
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def can_transition(
|
|
71
|
+
transitions: list[WorkflowTransition],
|
|
72
|
+
from_name: str,
|
|
73
|
+
to_name: str,
|
|
74
|
+
) -> bool:
|
|
75
|
+
"""Return True if a transition from from_name to to_name is defined."""
|
|
76
|
+
from_norm = validate_status_name(from_name)
|
|
77
|
+
to_norm = validate_status_name(to_name)
|
|
78
|
+
return any(t.from_status == from_norm and t.to_status == to_norm for t in transitions)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_available_transitions(
|
|
82
|
+
transitions: list[WorkflowTransition],
|
|
83
|
+
from_name: str,
|
|
84
|
+
) -> list[WorkflowTransition]:
|
|
85
|
+
"""Return all transitions starting from from_name."""
|
|
86
|
+
from_norm = validate_status_name(from_name)
|
|
87
|
+
return [t for t in transitions if t.from_status == from_norm]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forge-mvc-workflow
|
|
3
|
+
Version: 1.0.0b6
|
|
4
|
+
Summary: Forge workflow — statuts et transitions applicatives.
|
|
5
|
+
Author: Roger Cauchon
|
|
6
|
+
License-Expression: LicenseRef-Forge-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/caucrogeGit/Forge
|
|
8
|
+
Project-URL: Repository, https://github.com/caucrogeGit/Forge
|
|
9
|
+
Project-URL: Documentation, https://caucrogegit.github.io/Forge/
|
|
10
|
+
Keywords: python,mvc,forge,workflow,statuts
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: forge-mvc<2,>=1.0.0b5
|
|
19
|
+
Requires-Dist: markupsafe>=2.0
|
|
20
|
+
|
|
21
|
+
# forge-mvc-workflow
|
|
22
|
+
|
|
23
|
+
Module workflow pour Forge — statuts et transitions applicatives.
|
|
24
|
+
|
|
25
|
+
Extrait du core Forge depuis la version 2.6.0 (ADR-004).
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install forge-mvc-workflow
|
|
31
|
+
# ou en mode developpement
|
|
32
|
+
pip install -e packages/forge-mvc-workflow/
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from forge_mvc_workflow import (
|
|
39
|
+
WorkflowStatus,
|
|
40
|
+
WorkflowTransition,
|
|
41
|
+
make_status,
|
|
42
|
+
make_transition,
|
|
43
|
+
validate_statuses,
|
|
44
|
+
validate_transitions,
|
|
45
|
+
can_transition,
|
|
46
|
+
get_available_transitions,
|
|
47
|
+
make_workflow_jinja_helpers,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Definir des statuts
|
|
51
|
+
statuses = validate_statuses([
|
|
52
|
+
make_status("brouillon", label="Brouillon", color="gray"),
|
|
53
|
+
make_status("confirme", label="Confirme", color="green"),
|
|
54
|
+
make_status("annule", label="Annule", color="red"),
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
# Definir des transitions autorisees
|
|
58
|
+
transitions = validate_transitions([
|
|
59
|
+
make_transition("brouillon", "confirme"),
|
|
60
|
+
make_transition("brouillon", "annule"),
|
|
61
|
+
make_transition("confirme", "annule"),
|
|
62
|
+
], statuses=statuses)
|
|
63
|
+
|
|
64
|
+
# Verifier une transition
|
|
65
|
+
if can_transition(transitions, "brouillon", "confirme"):
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
# Obtenir les transitions disponibles depuis un statut
|
|
69
|
+
available = get_available_transitions(transitions, "brouillon")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Helpers Jinja2
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
env.globals.update(make_workflow_jinja_helpers())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Dans un template :
|
|
79
|
+
|
|
80
|
+
```jinja2
|
|
81
|
+
{{ workflow_status_badge(reservation.status) }}
|
|
82
|
+
{{ workflow_status_label(reservation.status) }}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Cas d'usage
|
|
86
|
+
|
|
87
|
+
- Statut d'une reservation (brouillon → confirmee → terminee → annulee)
|
|
88
|
+
- Statut d'un document (redaction → validation → publie)
|
|
89
|
+
- Statut d'une commande (panier → payee → expediee → livree)
|
|
90
|
+
|
|
91
|
+
## API publique
|
|
92
|
+
|
|
93
|
+
- `WorkflowStatus` — dataclass statut (name, label, color)
|
|
94
|
+
- `WorkflowTransition` — dataclass transition (from_status, to_status)
|
|
95
|
+
- `make_status(name, label, color)` — constructeur valide
|
|
96
|
+
- `make_transition(from_status, to_status)` — constructeur valide
|
|
97
|
+
- `validate_statuses(list)` — valide une liste de statuts
|
|
98
|
+
- `validate_transitions(list, statuses)` — valide les transitions par rapport aux statuts
|
|
99
|
+
- `can_transition(transitions, from_name, to_name)` — teste une transition
|
|
100
|
+
- `get_available_transitions(transitions, from_name)` — liste les transitions disponibles
|
|
101
|
+
- `make_workflow_jinja_helpers()` — dict de helpers Jinja2
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
forge_mvc_workflow/__init__.py
|
|
4
|
+
forge_mvc_workflow/jinja.py
|
|
5
|
+
forge_mvc_workflow/status.py
|
|
6
|
+
forge_mvc_workflow/transitions.py
|
|
7
|
+
forge_mvc_workflow.egg-info/PKG-INFO
|
|
8
|
+
forge_mvc_workflow.egg-info/SOURCES.txt
|
|
9
|
+
forge_mvc_workflow.egg-info/dependency_links.txt
|
|
10
|
+
forge_mvc_workflow.egg-info/requires.txt
|
|
11
|
+
forge_mvc_workflow.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
forge_mvc_workflow
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "forge-mvc-workflow"
|
|
7
|
+
version = "1.0.0b6"
|
|
8
|
+
description = "Forge workflow — statuts et transitions applicatives."
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Roger Cauchon" },
|
|
13
|
+
]
|
|
14
|
+
license = "LicenseRef-Forge-Proprietary"
|
|
15
|
+
keywords = ["python", "mvc", "forge", "workflow", "statuts"]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"forge-mvc>=1.0.0b5,<2",
|
|
18
|
+
"markupsafe>=2.0",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 4 - Beta",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/caucrogeGit/Forge"
|
|
30
|
+
Repository = "https://github.com/caucrogeGit/Forge"
|
|
31
|
+
Documentation = "https://caucrogegit.github.io/Forge/"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["."]
|
|
35
|
+
include = ["forge_mvc_workflow*"]
|