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.
Files changed (44) hide show
  1. urban_workflow/__init__.py +118 -0
  2. urban_workflow/clock.py +45 -0
  3. urban_workflow/config/__init__.py +32 -0
  4. urban_workflow/config/rules.py +85 -0
  5. urban_workflow/config/states.py +118 -0
  6. urban_workflow/config/workflow.py +189 -0
  7. urban_workflow/constants.py +22 -0
  8. urban_workflow/dsl/__init__.py +54 -0
  9. urban_workflow/dsl/errors.py +23 -0
  10. urban_workflow/dsl/generated/WorkflowLexer.py +281 -0
  11. urban_workflow/dsl/generated/WorkflowParser.py +2929 -0
  12. urban_workflow/dsl/generated/WorkflowParserVisitor.py +193 -0
  13. urban_workflow/dsl/generated/__init__.py +4 -0
  14. urban_workflow/dsl/loader.py +68 -0
  15. urban_workflow/dsl/parser.py +128 -0
  16. urban_workflow/dsl/visitor.py +942 -0
  17. urban_workflow/engine/__init__.py +31 -0
  18. urban_workflow/engine/append_events.py +82 -0
  19. urban_workflow/engine/engine.py +86 -0
  20. urban_workflow/engine/factory.py +145 -0
  21. urban_workflow/engine/processor.py +308 -0
  22. urban_workflow/engine/serializer.py +72 -0
  23. urban_workflow/engine/translator.py +76 -0
  24. urban_workflow/exceptions.py +61 -0
  25. urban_workflow/models.py +188 -0
  26. urban_workflow/outputs/__init__.py +38 -0
  27. urban_workflow/outputs/base.py +163 -0
  28. urban_workflow/outputs/memory.py +155 -0
  29. urban_workflow/registry.py +130 -0
  30. urban_workflow/storage_sql/__init__.py +45 -0
  31. urban_workflow/storage_sql/outputs.py +114 -0
  32. urban_workflow/storage_sql/schema.py +71 -0
  33. urban_workflow/storage_sql/storage.py +251 -0
  34. urban_workflow/templating/__init__.py +40 -0
  35. urban_workflow/templating/context.py +102 -0
  36. urban_workflow/templating/metadata.py +41 -0
  37. urban_workflow/templating/registry.py +121 -0
  38. urban_workflow/templating/translator.py +62 -0
  39. urban_workflow/values.py +106 -0
  40. urban_workflow-0.1.0.dist-info/METADATA +161 -0
  41. urban_workflow-0.1.0.dist-info/RECORD +44 -0
  42. urban_workflow-0.1.0.dist-info/WHEEL +5 -0
  43. urban_workflow-0.1.0.dist-info/licenses/LICENSE +21 -0
  44. 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
+ ]
@@ -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}"