urban-workflow 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.
- urban_workflow/__init__.py +118 -0
- urban_workflow/clock.py +45 -0
- urban_workflow/config/__init__.py +32 -0
- urban_workflow/config/rules.py +85 -0
- urban_workflow/config/states.py +118 -0
- urban_workflow/config/workflow.py +189 -0
- urban_workflow/constants.py +22 -0
- urban_workflow/dsl/__init__.py +54 -0
- urban_workflow/dsl/errors.py +23 -0
- urban_workflow/dsl/generated/WorkflowLexer.py +281 -0
- urban_workflow/dsl/generated/WorkflowParser.py +2929 -0
- urban_workflow/dsl/generated/WorkflowParserVisitor.py +193 -0
- urban_workflow/dsl/generated/__init__.py +4 -0
- urban_workflow/dsl/loader.py +68 -0
- urban_workflow/dsl/parser.py +128 -0
- urban_workflow/dsl/visitor.py +942 -0
- urban_workflow/engine/__init__.py +31 -0
- urban_workflow/engine/append_events.py +82 -0
- urban_workflow/engine/engine.py +86 -0
- urban_workflow/engine/factory.py +145 -0
- urban_workflow/engine/processor.py +308 -0
- urban_workflow/engine/serializer.py +72 -0
- urban_workflow/engine/translator.py +76 -0
- urban_workflow/exceptions.py +61 -0
- urban_workflow/models.py +188 -0
- urban_workflow/outputs/__init__.py +38 -0
- urban_workflow/outputs/base.py +163 -0
- urban_workflow/outputs/memory.py +155 -0
- urban_workflow/registry.py +130 -0
- urban_workflow/storage_sql/__init__.py +45 -0
- urban_workflow/storage_sql/outputs.py +114 -0
- urban_workflow/storage_sql/schema.py +71 -0
- urban_workflow/storage_sql/storage.py +251 -0
- urban_workflow/templating/__init__.py +40 -0
- urban_workflow/templating/context.py +102 -0
- urban_workflow/templating/metadata.py +41 -0
- urban_workflow/templating/registry.py +121 -0
- urban_workflow/templating/translator.py +62 -0
- urban_workflow/values.py +106 -0
- urban_workflow-0.1.0.dist-info/METADATA +161 -0
- urban_workflow-0.1.0.dist-info/RECORD +44 -0
- urban_workflow-0.1.0.dist-info/WHEEL +5 -0
- urban_workflow-0.1.0.dist-info/licenses/LICENSE +21 -0
- urban_workflow-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""urban-workflow — moteur de workflow piloté par un DSL métier.
|
|
2
|
+
|
|
3
|
+
Portage Python de `Black-Beard-Sdk/Workflow
|
|
4
|
+
<https://github.com/Black-Beard-Sdk/Workflow>`_, avec normalisation des
|
|
5
|
+
sorties JSON par `urban-json <https://pypi.org/project/urban-json/>`_.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .clock import WorkflowClock
|
|
9
|
+
from .config import (
|
|
10
|
+
DeclaredEventConfig,
|
|
11
|
+
IncomingEventConfig,
|
|
12
|
+
InitializationConfig,
|
|
13
|
+
InitializationOnEventConfig,
|
|
14
|
+
ResultActionConfig,
|
|
15
|
+
ResultRuleConfig,
|
|
16
|
+
StateConfig,
|
|
17
|
+
TransitionConfig,
|
|
18
|
+
WorkflowConfig,
|
|
19
|
+
WorkflowsConfig,
|
|
20
|
+
)
|
|
21
|
+
from .engine import (
|
|
22
|
+
ActionTranslator,
|
|
23
|
+
DefaultActionTranslator,
|
|
24
|
+
WorkflowEngine,
|
|
25
|
+
WorkflowFactory,
|
|
26
|
+
WorkflowProcessor,
|
|
27
|
+
parse_incoming_event,
|
|
28
|
+
)
|
|
29
|
+
from .exceptions import (
|
|
30
|
+
DuplicatedArgumentNameError,
|
|
31
|
+
InvalidArgumentNameError,
|
|
32
|
+
InvalidIncomingEventFormatError,
|
|
33
|
+
InvalidMethodReferenceError,
|
|
34
|
+
LockFailedError,
|
|
35
|
+
MissingArgumentNameError,
|
|
36
|
+
MissingConfigurationError,
|
|
37
|
+
MissingStateError,
|
|
38
|
+
ResolutionArgumentError,
|
|
39
|
+
WorkflowError,
|
|
40
|
+
)
|
|
41
|
+
from .models import (
|
|
42
|
+
ChangeEnum,
|
|
43
|
+
CurrentEvaluation,
|
|
44
|
+
Event,
|
|
45
|
+
IncomingEvent,
|
|
46
|
+
PushedAction,
|
|
47
|
+
ResultAction,
|
|
48
|
+
RuleSpan,
|
|
49
|
+
RunContext,
|
|
50
|
+
Workflow,
|
|
51
|
+
)
|
|
52
|
+
from .outputs import (
|
|
53
|
+
DoNothingTransaction,
|
|
54
|
+
LockObject,
|
|
55
|
+
MemoryLocker,
|
|
56
|
+
MemoryStorage,
|
|
57
|
+
OutputAction,
|
|
58
|
+
PushBusOutputAction,
|
|
59
|
+
PushModelOutputAction,
|
|
60
|
+
SetPropertiesOutputAction,
|
|
61
|
+
Transaction,
|
|
62
|
+
)
|
|
63
|
+
from .registry import FunctionRegistry
|
|
64
|
+
from .values import ContextRef, make_value, resolve_value
|
|
65
|
+
|
|
66
|
+
__version__ = "0.1.0"
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"ActionTranslator",
|
|
70
|
+
"ChangeEnum",
|
|
71
|
+
"ContextRef",
|
|
72
|
+
"CurrentEvaluation",
|
|
73
|
+
"DefaultActionTranslator",
|
|
74
|
+
"DoNothingTransaction",
|
|
75
|
+
"LockObject",
|
|
76
|
+
"MemoryLocker",
|
|
77
|
+
"MemoryStorage",
|
|
78
|
+
"OutputAction",
|
|
79
|
+
"PushBusOutputAction",
|
|
80
|
+
"PushModelOutputAction",
|
|
81
|
+
"SetPropertiesOutputAction",
|
|
82
|
+
"Transaction",
|
|
83
|
+
"WorkflowEngine",
|
|
84
|
+
"WorkflowFactory",
|
|
85
|
+
"WorkflowProcessor",
|
|
86
|
+
"parse_incoming_event",
|
|
87
|
+
"DeclaredEventConfig",
|
|
88
|
+
"DuplicatedArgumentNameError",
|
|
89
|
+
"Event",
|
|
90
|
+
"FunctionRegistry",
|
|
91
|
+
"IncomingEvent",
|
|
92
|
+
"IncomingEventConfig",
|
|
93
|
+
"InitializationConfig",
|
|
94
|
+
"InitializationOnEventConfig",
|
|
95
|
+
"InvalidArgumentNameError",
|
|
96
|
+
"InvalidIncomingEventFormatError",
|
|
97
|
+
"InvalidMethodReferenceError",
|
|
98
|
+
"LockFailedError",
|
|
99
|
+
"MissingArgumentNameError",
|
|
100
|
+
"MissingConfigurationError",
|
|
101
|
+
"MissingStateError",
|
|
102
|
+
"PushedAction",
|
|
103
|
+
"ResolutionArgumentError",
|
|
104
|
+
"ResultAction",
|
|
105
|
+
"ResultActionConfig",
|
|
106
|
+
"ResultRuleConfig",
|
|
107
|
+
"RuleSpan",
|
|
108
|
+
"RunContext",
|
|
109
|
+
"StateConfig",
|
|
110
|
+
"TransitionConfig",
|
|
111
|
+
"Workflow",
|
|
112
|
+
"WorkflowClock",
|
|
113
|
+
"WorkflowConfig",
|
|
114
|
+
"WorkflowError",
|
|
115
|
+
"WorkflowsConfig",
|
|
116
|
+
"make_value",
|
|
117
|
+
"resolve_value",
|
|
118
|
+
]
|
urban_workflow/clock.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Horloge du moteur, injectable pour les tests.
|
|
2
|
+
|
|
3
|
+
Portage de ``WorkflowClock`` : tout le moteur date ses objets via
|
|
4
|
+
``WorkflowClock.now()`` et jamais via ``datetime.now`` directement, ce qui
|
|
5
|
+
permet de figer le temps dans les tests (expirations, rappels différés).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable, Iterator
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _default_provider() -> datetime:
|
|
16
|
+
return datetime.now(timezone.utc)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorkflowClock:
|
|
20
|
+
"""Source de temps unique du moteur (UTC, avec fuseau)."""
|
|
21
|
+
|
|
22
|
+
_provider: Callable[[], datetime] = staticmethod(_default_provider)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def now(cls) -> datetime:
|
|
26
|
+
return cls._provider()
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def set_provider(cls, provider: Callable[[], datetime]) -> None:
|
|
30
|
+
cls._provider = staticmethod(provider)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def reset(cls) -> None:
|
|
34
|
+
cls._provider = staticmethod(_default_provider)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
@contextmanager
|
|
38
|
+
def frozen(cls, instant: datetime) -> Iterator[datetime]:
|
|
39
|
+
"""Fige l'horloge sur ``instant`` le temps du bloc ``with``."""
|
|
40
|
+
previous = cls._provider
|
|
41
|
+
cls._provider = staticmethod(lambda: instant)
|
|
42
|
+
try:
|
|
43
|
+
yield instant
|
|
44
|
+
finally:
|
|
45
|
+
cls._provider = previous
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Modèle de configuration des workflows (cible du visiteur DSL)."""
|
|
2
|
+
|
|
3
|
+
from .rules import (
|
|
4
|
+
DeclaredEventConfig,
|
|
5
|
+
ResultActionConfig,
|
|
6
|
+
ResultRuleConfig,
|
|
7
|
+
Rule,
|
|
8
|
+
append_rule_actions,
|
|
9
|
+
)
|
|
10
|
+
from .states import (
|
|
11
|
+
IncomingEventConfig,
|
|
12
|
+
InitializationConfig,
|
|
13
|
+
InitializationOnEventConfig,
|
|
14
|
+
StateConfig,
|
|
15
|
+
TransitionConfig,
|
|
16
|
+
)
|
|
17
|
+
from .workflow import WorkflowConfig, WorkflowsConfig
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DeclaredEventConfig",
|
|
21
|
+
"IncomingEventConfig",
|
|
22
|
+
"InitializationConfig",
|
|
23
|
+
"InitializationOnEventConfig",
|
|
24
|
+
"ResultActionConfig",
|
|
25
|
+
"ResultRuleConfig",
|
|
26
|
+
"Rule",
|
|
27
|
+
"StateConfig",
|
|
28
|
+
"TransitionConfig",
|
|
29
|
+
"WorkflowConfig",
|
|
30
|
+
"WorkflowsConfig",
|
|
31
|
+
"append_rule_actions",
|
|
32
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Configuration des règles conditionnées et des actions à produire."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..clock import WorkflowClock
|
|
10
|
+
from ..models import ResultAction, RuleSpan, RunContext
|
|
11
|
+
from ..values import ContextValue, make_value
|
|
12
|
+
|
|
13
|
+
#: Une règle ``WHEN`` compilée : prédicat sur le contexte d'exécution.
|
|
14
|
+
Rule = Callable[[RunContext], bool]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DeclaredEventConfig:
|
|
19
|
+
"""Événement déclaré (``DEFINE EVENT``) ou technique."""
|
|
20
|
+
|
|
21
|
+
name: str = ""
|
|
22
|
+
label: str = ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ResultActionConfig:
|
|
27
|
+
"""Action à produire (``EXECUTE action(args)`` / ``STORE (…)``)."""
|
|
28
|
+
|
|
29
|
+
name: str = ""
|
|
30
|
+
label: str = ""
|
|
31
|
+
delay: int = 0
|
|
32
|
+
kind: str = ""
|
|
33
|
+
arguments: dict[str, ContextValue] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
def add_argument(self, key: str, value: Any) -> ResultActionConfig:
|
|
36
|
+
"""Ajoute un argument ; les chaînes ``@a.b.c`` deviennent des références."""
|
|
37
|
+
self.arguments[key] = make_value(value)
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def map(self, ctx: RunContext) -> ResultAction:
|
|
41
|
+
"""Instancie l'action pour un contexte donné (arguments non résolus)."""
|
|
42
|
+
return ResultAction(
|
|
43
|
+
name=self.name,
|
|
44
|
+
label=self.name,
|
|
45
|
+
delay=self.delay,
|
|
46
|
+
kind=self.kind,
|
|
47
|
+
event=ctx.event,
|
|
48
|
+
event_date=WorkflowClock.now(),
|
|
49
|
+
arguments=dict(self.arguments),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ResultRuleConfig:
|
|
55
|
+
"""Un bloc ``WHEN règle → actions`` (règle absente = toujours vrai)."""
|
|
56
|
+
|
|
57
|
+
when_rule: Rule | None = None
|
|
58
|
+
when_rule_text: str = ""
|
|
59
|
+
when_rule_code: str = ""
|
|
60
|
+
when_rule_position: RuleSpan = field(default_factory=RuleSpan.none)
|
|
61
|
+
actions: list[ResultActionConfig] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
def add_actions(self, *actions: ResultActionConfig) -> ResultRuleConfig:
|
|
64
|
+
self.actions.extend(actions)
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def append_rule_actions(
|
|
69
|
+
rules: list[ResultRuleConfig],
|
|
70
|
+
when_rule: Rule | None,
|
|
71
|
+
*actions: ResultActionConfig,
|
|
72
|
+
) -> ResultRuleConfig:
|
|
73
|
+
"""Accroche ``actions`` au bloc portant ``when_rule``, créé au besoin.
|
|
74
|
+
|
|
75
|
+
Reprend le comportement des extensions ``AddAction``/``AddIncomingActions``
|
|
76
|
+
du source : les actions partageant la même règle sont regroupées.
|
|
77
|
+
"""
|
|
78
|
+
for rule in rules:
|
|
79
|
+
if rule.when_rule is when_rule:
|
|
80
|
+
rule.add_actions(*actions)
|
|
81
|
+
return rule
|
|
82
|
+
rule = ResultRuleConfig(when_rule=when_rule)
|
|
83
|
+
rule.add_actions(*actions)
|
|
84
|
+
rules.append(rule)
|
|
85
|
+
return rule
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Configuration des états, transitions et initialisations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from ..models import RuleSpan
|
|
8
|
+
from .rules import ResultActionConfig, ResultRuleConfig, Rule, append_rule_actions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class TransitionConfig:
|
|
13
|
+
"""``(WHEN règle)? … SWITCH état`` au sein d'un ``ON EVENT``."""
|
|
14
|
+
|
|
15
|
+
target_state_name: str = ""
|
|
16
|
+
when_rule: Rule | None = None
|
|
17
|
+
when_rule_text: str = ""
|
|
18
|
+
when_rule_code: str = ""
|
|
19
|
+
when_rule_position: RuleSpan = field(default_factory=RuleSpan.none)
|
|
20
|
+
rule_actions: list[ResultRuleConfig] = field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
#: Résolu par :meth:`WorkflowConfig.build`.
|
|
23
|
+
target_state: StateConfig | None = None
|
|
24
|
+
label: str = ""
|
|
25
|
+
|
|
26
|
+
def add_actions(
|
|
27
|
+
self, when_rule: Rule | None, *actions: ResultActionConfig
|
|
28
|
+
) -> TransitionConfig:
|
|
29
|
+
append_rule_actions(self.rule_actions, when_rule, *actions)
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class IncomingEventConfig:
|
|
35
|
+
"""``ON EVENT nom`` d'un état : règles exécutées et transitions."""
|
|
36
|
+
|
|
37
|
+
name: str = ""
|
|
38
|
+
transitions: list[TransitionConfig] = field(default_factory=list)
|
|
39
|
+
rules: list[ResultRuleConfig] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
def add_transition(self, transition: TransitionConfig) -> IncomingEventConfig:
|
|
42
|
+
self.transitions.append(transition)
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def add_action(
|
|
46
|
+
self, when_rule: Rule | None, action: ResultActionConfig
|
|
47
|
+
) -> IncomingEventConfig:
|
|
48
|
+
append_rule_actions(self.rules, when_rule, action)
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class StateConfig:
|
|
54
|
+
"""``DEFINE STATE`` : événements écoutés et règles d'entrée/sortie."""
|
|
55
|
+
|
|
56
|
+
name: str = ""
|
|
57
|
+
label: str = ""
|
|
58
|
+
events: dict[str, IncomingEventConfig] = field(default_factory=dict)
|
|
59
|
+
incoming_rules: list[ResultRuleConfig] = field(default_factory=list)
|
|
60
|
+
outcoming_rules: list[ResultRuleConfig] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
def add_event(self, event: IncomingEventConfig) -> StateConfig:
|
|
63
|
+
self.events[event.name] = event
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def add_incoming_actions(
|
|
67
|
+
self, when_rule: Rule | None, *actions: ResultActionConfig
|
|
68
|
+
) -> StateConfig:
|
|
69
|
+
append_rule_actions(self.incoming_rules, when_rule, *actions)
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def add_outcoming_actions(
|
|
73
|
+
self, when_rule: Rule | None, *actions: ResultActionConfig
|
|
74
|
+
) -> StateConfig:
|
|
75
|
+
append_rule_actions(self.outcoming_rules, when_rule, *actions)
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class InitializationConfig:
|
|
81
|
+
"""Un ``SWITCH`` d'initialisation : ``(WHEN règle)? SWITCH état``."""
|
|
82
|
+
|
|
83
|
+
target_state_name: str = ""
|
|
84
|
+
when_rule: Rule | None = None
|
|
85
|
+
when_rule_text: str = ""
|
|
86
|
+
when_rule_code: str = ""
|
|
87
|
+
when_rule_position: RuleSpan = field(default_factory=RuleSpan.none)
|
|
88
|
+
|
|
89
|
+
#: Résolu par :meth:`WorkflowConfig.build`.
|
|
90
|
+
target_state: StateConfig | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class InitializationOnEventConfig:
|
|
95
|
+
"""``INITIALIZE WORKFLOW ON EVENT nom`` : création de workflow."""
|
|
96
|
+
|
|
97
|
+
event_name: str = ""
|
|
98
|
+
recursive: bool = False
|
|
99
|
+
switchs: list[InitializationConfig] = field(default_factory=list)
|
|
100
|
+
|
|
101
|
+
def add_switch(
|
|
102
|
+
self,
|
|
103
|
+
target_state_name: str,
|
|
104
|
+
when_rule: Rule | None = None,
|
|
105
|
+
when_rule_text: str = "",
|
|
106
|
+
when_rule_code: str = "",
|
|
107
|
+
when_rule_position: RuleSpan | None = None,
|
|
108
|
+
) -> InitializationOnEventConfig:
|
|
109
|
+
self.switchs.append(
|
|
110
|
+
InitializationConfig(
|
|
111
|
+
target_state_name=target_state_name,
|
|
112
|
+
when_rule=when_rule,
|
|
113
|
+
when_rule_text=when_rule_text,
|
|
114
|
+
when_rule_code=when_rule_code,
|
|
115
|
+
when_rule_position=when_rule_position or RuleSpan.none(),
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
return self
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Configuration d'un workflow et collection multi-configurations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from .. import constants
|
|
10
|
+
from ..exceptions import MissingConfigurationError, MissingStateError
|
|
11
|
+
from ..models import IncomingEvent
|
|
12
|
+
from .rules import DeclaredEventConfig
|
|
13
|
+
from .states import InitializationOnEventConfig, StateConfig
|
|
14
|
+
|
|
15
|
+
_TECHNICAL_EVENT_LABELS = {
|
|
16
|
+
constants.RESULT_ACTION_NAME: (
|
|
17
|
+
"technical result of execution of an order pushed by the current workflow"
|
|
18
|
+
),
|
|
19
|
+
constants.EXPIRED_EVENT_NAME: "Expiration of the state",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class WorkflowConfig:
|
|
25
|
+
"""Machine à états déclarée (une version d'un workflow)."""
|
|
26
|
+
|
|
27
|
+
name: str = ""
|
|
28
|
+
label: str = ""
|
|
29
|
+
version: int = 0
|
|
30
|
+
concurrency: int = 1
|
|
31
|
+
crc: int = 0
|
|
32
|
+
filters: dict[str, str] = field(default_factory=dict)
|
|
33
|
+
initializers: dict[str, InitializationOnEventConfig] = field(default_factory=dict)
|
|
34
|
+
declared_events: dict[str, DeclaredEventConfig] = field(default_factory=dict)
|
|
35
|
+
states: dict[str, StateConfig] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
def __post_init__(self) -> None:
|
|
38
|
+
self._built = False
|
|
39
|
+
self._build_lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
# Construction fluide
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def add_state(self, state: StateConfig) -> WorkflowConfig:
|
|
46
|
+
self.states[state.name] = state
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def add_initializer(self, initializer: InitializationOnEventConfig) -> WorkflowConfig:
|
|
50
|
+
self.initializers[initializer.event_name] = initializer
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def add_filter(self, key: str, value: str) -> WorkflowConfig:
|
|
54
|
+
"""``MATCHING (key = 'value')`` : filtre de routage des événements."""
|
|
55
|
+
self.filters[key] = value
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
# Construction / résolution
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def build(self) -> WorkflowConfig:
|
|
63
|
+
"""Résout les références et prépare la configuration (idempotent)."""
|
|
64
|
+
if self._built:
|
|
65
|
+
return self
|
|
66
|
+
with self._build_lock:
|
|
67
|
+
if self._built:
|
|
68
|
+
return self
|
|
69
|
+
self._build_declared_events()
|
|
70
|
+
self._resolve_target_states()
|
|
71
|
+
self._build_transition_labels()
|
|
72
|
+
self._built = True
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def _build_declared_events(self) -> None:
|
|
76
|
+
def declare(name: str) -> None:
|
|
77
|
+
if name not in self.declared_events:
|
|
78
|
+
self.declared_events[name] = DeclaredEventConfig(
|
|
79
|
+
name=name,
|
|
80
|
+
label=_TECHNICAL_EVENT_LABELS.get(name, ""),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
declare(constants.RESULT_ACTION_NAME)
|
|
84
|
+
|
|
85
|
+
for event_name in self.initializers:
|
|
86
|
+
declare(event_name)
|
|
87
|
+
|
|
88
|
+
for state in self.states.values():
|
|
89
|
+
for event_name in state.events:
|
|
90
|
+
declare(event_name)
|
|
91
|
+
|
|
92
|
+
def _resolve_target_states(self) -> None:
|
|
93
|
+
def resolve(target_state_name: str) -> StateConfig:
|
|
94
|
+
state = self.states.get(target_state_name)
|
|
95
|
+
if state is None:
|
|
96
|
+
raise MissingStateError(f"missing state name {target_state_name}")
|
|
97
|
+
return state
|
|
98
|
+
|
|
99
|
+
for initializer in self.initializers.values():
|
|
100
|
+
for switch in initializer.switchs:
|
|
101
|
+
switch.target_state = resolve(switch.target_state_name)
|
|
102
|
+
|
|
103
|
+
for state in self.states.values():
|
|
104
|
+
for event in state.events.values():
|
|
105
|
+
for transition in event.transitions:
|
|
106
|
+
transition.target_state = resolve(transition.target_state_name)
|
|
107
|
+
|
|
108
|
+
def _build_transition_labels(self) -> None:
|
|
109
|
+
for state_name, state in self.states.items():
|
|
110
|
+
for event in state.events.values():
|
|
111
|
+
for transition in event.transitions:
|
|
112
|
+
transition.label = f"{state_name} -> {transition.target_state_name}"
|
|
113
|
+
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
# Routage
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def evaluate_filter(self, event: IncomingEvent) -> bool:
|
|
119
|
+
"""Vrai si les données étendues de l'événement satisfont ``MATCHING``."""
|
|
120
|
+
self.build()
|
|
121
|
+
for key, expected in self.filters.items():
|
|
122
|
+
actual = event.extended_datas.get(key)
|
|
123
|
+
if actual is None or str(actual) != expected:
|
|
124
|
+
return False
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class WorkflowsConfig:
|
|
129
|
+
"""Ensemble des configurations chargées dans le processeur.
|
|
130
|
+
|
|
131
|
+
Gère le versionnage (plusieurs versions d'un même workflow) et le
|
|
132
|
+
routage d'un événement vers les configurations dont les filtres
|
|
133
|
+
``MATCHING`` correspondent.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self) -> None:
|
|
137
|
+
self._configs: list[WorkflowConfig] = []
|
|
138
|
+
self._by_name_version: dict[tuple[str, int], WorkflowConfig] = {}
|
|
139
|
+
self._selectable: list[WorkflowConfig] = []
|
|
140
|
+
self._version_selectors: dict[str, set[int]] = {}
|
|
141
|
+
self._lock = threading.Lock()
|
|
142
|
+
|
|
143
|
+
def set_version_selector(self, workflow_name: str, version: int) -> WorkflowsConfig:
|
|
144
|
+
"""Restreint la création de nouveaux workflows ``workflow_name`` à ``version``.
|
|
145
|
+
|
|
146
|
+
Sans sélecteur, toutes les versions chargées sont candidates ; les
|
|
147
|
+
workflows déjà créés continuent, eux, d'utiliser leur version d'origine.
|
|
148
|
+
"""
|
|
149
|
+
with self._lock:
|
|
150
|
+
self._version_selectors.setdefault(workflow_name, set()).add(version)
|
|
151
|
+
self._rebuild_selectable()
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def add_document(self, config: WorkflowConfig) -> WorkflowsConfig:
|
|
155
|
+
config.build()
|
|
156
|
+
with self._lock:
|
|
157
|
+
self._configs.append(config)
|
|
158
|
+
self._by_name_version[(config.name, config.version)] = config
|
|
159
|
+
self._rebuild_selectable()
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def _rebuild_selectable(self) -> None:
|
|
163
|
+
self._selectable = [
|
|
164
|
+
config
|
|
165
|
+
for config in self._configs
|
|
166
|
+
if config.name not in self._version_selectors
|
|
167
|
+
or config.version in self._version_selectors[config.name]
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
def matching(self, event: IncomingEvent) -> Iterator[WorkflowConfig]:
|
|
171
|
+
"""Configurations candidates pour un événement entrant."""
|
|
172
|
+
for config in self._selectable:
|
|
173
|
+
if config.evaluate_filter(event):
|
|
174
|
+
yield config
|
|
175
|
+
|
|
176
|
+
def get(self, workflow_name: str, version: int) -> WorkflowConfig:
|
|
177
|
+
config = self._by_name_version.get((workflow_name, version))
|
|
178
|
+
if config is None:
|
|
179
|
+
raise MissingConfigurationError(f"{workflow_name} V {version}")
|
|
180
|
+
return config
|
|
181
|
+
|
|
182
|
+
def try_get(self, workflow_name: str, version: int) -> WorkflowConfig | None:
|
|
183
|
+
return self._by_name_version.get((workflow_name, version))
|
|
184
|
+
|
|
185
|
+
def get_all(self, workflow_name: str) -> list[WorkflowConfig]:
|
|
186
|
+
return [config for config in self._configs if config.name == workflow_name]
|
|
187
|
+
|
|
188
|
+
def __len__(self) -> int:
|
|
189
|
+
return len(self._configs)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Noms techniques du moteur (événements et actions réservés).
|
|
2
|
+
|
|
3
|
+
Portage de ``Bb.Workflows.Constants`` — les valeurs sont conservées à
|
|
4
|
+
l'identique pour rester compatible avec les configurations existantes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
CURRENT_WORKFLOW = "_workflow"
|
|
8
|
+
|
|
9
|
+
SET_VALUE_ACTION_NAME = "$_update"
|
|
10
|
+
PUSH_ACTION_NAME = "$_push"
|
|
11
|
+
PUSH_REMINDER = "$_reminder"
|
|
12
|
+
|
|
13
|
+
EVENT_ID = "EventId"
|
|
14
|
+
TASK_UUID = "TaskId"
|
|
15
|
+
|
|
16
|
+
# Événements techniques
|
|
17
|
+
RESULT_ACTION_NAME = "$_result"
|
|
18
|
+
EXPIRED_EVENT_NAME = "__expired"
|
|
19
|
+
CANCEL_REMINDER_ACTION = "__cancelReminderAction"
|
|
20
|
+
|
|
21
|
+
# Propriétés réservées des données étendues
|
|
22
|
+
WORKFLOW_ID_PROPERTY = "WorkflowId"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""DSL de déclaration des workflows.
|
|
2
|
+
|
|
3
|
+
Grammaires ANTLR reprises telles quelles du projet source
|
|
4
|
+
(``urban_workflow/dsl/grammar/``), parseur Python généré
|
|
5
|
+
(``urban_workflow/dsl/generated/``, ANTLR 4.13.2), visiteur de construction
|
|
6
|
+
:class:`WorkflowConfigVisitor` et chargeur de répertoires avec
|
|
7
|
+
``INCLUDE``/``FRAGMENT``.
|
|
8
|
+
|
|
9
|
+
Point d'entrée simple ::
|
|
10
|
+
|
|
11
|
+
from urban_workflow import FunctionRegistry
|
|
12
|
+
from urban_workflow.dsl import parse_config
|
|
13
|
+
|
|
14
|
+
rules = FunctionRegistry("rule")
|
|
15
|
+
rules.add("IsMajor", is_major)
|
|
16
|
+
config = parse_config(script_text, rules=rules, filename="demo.wf")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from ..config import WorkflowConfig
|
|
22
|
+
from ..registry import FunctionRegistry
|
|
23
|
+
from .errors import DslErrorModel
|
|
24
|
+
from .loader import load_directory, parse_directory
|
|
25
|
+
from .parser import WorkflowScriptParser
|
|
26
|
+
from .visitor import ConstantModel, DeclaredFunction, WorkflowConfigVisitor
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ConstantModel",
|
|
30
|
+
"DeclaredFunction",
|
|
31
|
+
"DslErrorModel",
|
|
32
|
+
"WorkflowConfigVisitor",
|
|
33
|
+
"WorkflowScriptParser",
|
|
34
|
+
"load_directory",
|
|
35
|
+
"parse_config",
|
|
36
|
+
"parse_directory",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_config(
|
|
41
|
+
source: str,
|
|
42
|
+
rules: FunctionRegistry | None = None,
|
|
43
|
+
filename: str = "",
|
|
44
|
+
) -> WorkflowConfig:
|
|
45
|
+
"""Analyse un script DSL complet et construit sa configuration.
|
|
46
|
+
|
|
47
|
+
Lève :class:`~urban_workflow.exceptions.DslSyntaxError` sur erreur de
|
|
48
|
+
syntaxe, et les exceptions de validation positionnées du visiteur sur
|
|
49
|
+
erreur sémantique (règle non enregistrée, action non déclarée,
|
|
50
|
+
argument inconnu ou manquant, état ``SWITCH`` absent…).
|
|
51
|
+
"""
|
|
52
|
+
parser = WorkflowScriptParser.parse_string(source, filename=filename).check()
|
|
53
|
+
visitor = WorkflowConfigVisitor(rules=rules, filename=filename)
|
|
54
|
+
return parser.visit(visitor)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Erreurs positionnées du parseur DSL (portage de ``ErrorModel``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class DslErrorModel:
|
|
10
|
+
"""Une erreur de syntaxe positionnée dans un script DSL."""
|
|
11
|
+
|
|
12
|
+
message: str = ""
|
|
13
|
+
filename: str = ""
|
|
14
|
+
line: int = 0
|
|
15
|
+
column: int = 0
|
|
16
|
+
start_index: int = 0
|
|
17
|
+
text: str = ""
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
location = f"{self.filename}:{self.line}:{self.column}" if self.filename else (
|
|
21
|
+
f"{self.line}:{self.column}"
|
|
22
|
+
)
|
|
23
|
+
return f"{location}: {self.message}"
|