urban-workflow 0.1.0__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.
Files changed (62) hide show
  1. urban_workflow-0.1.0/LICENSE +21 -0
  2. urban_workflow-0.1.0/PKG-INFO +161 -0
  3. urban_workflow-0.1.0/README.md +109 -0
  4. urban_workflow-0.1.0/pyproject.toml +63 -0
  5. urban_workflow-0.1.0/setup.cfg +4 -0
  6. urban_workflow-0.1.0/tests/test_config.py +152 -0
  7. urban_workflow-0.1.0/tests/test_docs_examples.py +226 -0
  8. urban_workflow-0.1.0/tests/test_dsl.py +527 -0
  9. urban_workflow-0.1.0/tests/test_dsl_includes.py +95 -0
  10. urban_workflow-0.1.0/tests/test_dsl_validation.py +275 -0
  11. urban_workflow-0.1.0/tests/test_engine.py +586 -0
  12. urban_workflow-0.1.0/tests/test_engine_dsl.py +161 -0
  13. urban_workflow-0.1.0/tests/test_models.py +58 -0
  14. urban_workflow-0.1.0/tests/test_playground.py +155 -0
  15. urban_workflow-0.1.0/tests/test_registry.py +75 -0
  16. urban_workflow-0.1.0/tests/test_storage_sql.py +293 -0
  17. urban_workflow-0.1.0/tests/test_templating.py +329 -0
  18. urban_workflow-0.1.0/tests/test_values.py +60 -0
  19. urban_workflow-0.1.0/urban_workflow/__init__.py +118 -0
  20. urban_workflow-0.1.0/urban_workflow/clock.py +45 -0
  21. urban_workflow-0.1.0/urban_workflow/config/__init__.py +32 -0
  22. urban_workflow-0.1.0/urban_workflow/config/rules.py +85 -0
  23. urban_workflow-0.1.0/urban_workflow/config/states.py +118 -0
  24. urban_workflow-0.1.0/urban_workflow/config/workflow.py +189 -0
  25. urban_workflow-0.1.0/urban_workflow/constants.py +22 -0
  26. urban_workflow-0.1.0/urban_workflow/dsl/__init__.py +54 -0
  27. urban_workflow-0.1.0/urban_workflow/dsl/errors.py +23 -0
  28. urban_workflow-0.1.0/urban_workflow/dsl/generated/WorkflowLexer.py +281 -0
  29. urban_workflow-0.1.0/urban_workflow/dsl/generated/WorkflowParser.py +2929 -0
  30. urban_workflow-0.1.0/urban_workflow/dsl/generated/WorkflowParserVisitor.py +193 -0
  31. urban_workflow-0.1.0/urban_workflow/dsl/generated/__init__.py +4 -0
  32. urban_workflow-0.1.0/urban_workflow/dsl/loader.py +68 -0
  33. urban_workflow-0.1.0/urban_workflow/dsl/parser.py +128 -0
  34. urban_workflow-0.1.0/urban_workflow/dsl/visitor.py +942 -0
  35. urban_workflow-0.1.0/urban_workflow/engine/__init__.py +31 -0
  36. urban_workflow-0.1.0/urban_workflow/engine/append_events.py +82 -0
  37. urban_workflow-0.1.0/urban_workflow/engine/engine.py +86 -0
  38. urban_workflow-0.1.0/urban_workflow/engine/factory.py +145 -0
  39. urban_workflow-0.1.0/urban_workflow/engine/processor.py +308 -0
  40. urban_workflow-0.1.0/urban_workflow/engine/serializer.py +72 -0
  41. urban_workflow-0.1.0/urban_workflow/engine/translator.py +76 -0
  42. urban_workflow-0.1.0/urban_workflow/exceptions.py +61 -0
  43. urban_workflow-0.1.0/urban_workflow/models.py +188 -0
  44. urban_workflow-0.1.0/urban_workflow/outputs/__init__.py +38 -0
  45. urban_workflow-0.1.0/urban_workflow/outputs/base.py +163 -0
  46. urban_workflow-0.1.0/urban_workflow/outputs/memory.py +155 -0
  47. urban_workflow-0.1.0/urban_workflow/registry.py +130 -0
  48. urban_workflow-0.1.0/urban_workflow/storage_sql/__init__.py +45 -0
  49. urban_workflow-0.1.0/urban_workflow/storage_sql/outputs.py +114 -0
  50. urban_workflow-0.1.0/urban_workflow/storage_sql/schema.py +71 -0
  51. urban_workflow-0.1.0/urban_workflow/storage_sql/storage.py +251 -0
  52. urban_workflow-0.1.0/urban_workflow/templating/__init__.py +40 -0
  53. urban_workflow-0.1.0/urban_workflow/templating/context.py +102 -0
  54. urban_workflow-0.1.0/urban_workflow/templating/metadata.py +41 -0
  55. urban_workflow-0.1.0/urban_workflow/templating/registry.py +121 -0
  56. urban_workflow-0.1.0/urban_workflow/templating/translator.py +62 -0
  57. urban_workflow-0.1.0/urban_workflow/values.py +106 -0
  58. urban_workflow-0.1.0/urban_workflow.egg-info/PKG-INFO +161 -0
  59. urban_workflow-0.1.0/urban_workflow.egg-info/SOURCES.txt +60 -0
  60. urban_workflow-0.1.0/urban_workflow.egg-info/dependency_links.txt +1 -0
  61. urban_workflow-0.1.0/urban_workflow.egg-info/requires.txt +12 -0
  62. urban_workflow-0.1.0/urban_workflow.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 black beard
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.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: urban-workflow
3
+ Version: 0.1.0
4
+ Summary: Moteur de workflow piloté par un DSL métier — portage Python de Black-Beard-Sdk/Workflow, sorties JSON normalisées via urban-json (JSLT).
5
+ Author-email: Gael Beard <gaelgael5@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 black beard
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/gaelgael5/urban-workflow
29
+ Project-URL: Issues, https://github.com/gaelgael5/urban-workflow/issues
30
+ Classifier: Development Status :: 3 - Alpha
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.10
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Classifier: Programming Language :: Python :: 3.13
37
+ Classifier: Topic :: Software Development :: Libraries
38
+ Requires-Python: >=3.10
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: urban-json>=0.2.1
42
+ Provides-Extra: sql
43
+ Requires-Dist: sqlalchemy>=2.0; extra == "sql"
44
+ Provides-Extra: dev
45
+ Requires-Dist: pytest>=7; extra == "dev"
46
+ Requires-Dist: ruff>=0.4; extra == "dev"
47
+ Requires-Dist: sqlalchemy>=2.0; extra == "dev"
48
+ Requires-Dist: fastapi>=0.110; extra == "dev"
49
+ Requires-Dist: uvicorn>=0.27; extra == "dev"
50
+ Requires-Dist: httpx>=0.27; extra == "dev"
51
+ Dynamic: license-file
52
+
53
+ # urban-workflow
54
+
55
+ Moteur de workflow piloté par un **langage dédié** (DSL) — portage Python de
56
+ [Black-Beard-Sdk/Workflow](https://github.com/Black-Beard-Sdk/Workflow)
57
+ (.NET), dont le système de normalisation des sorties JSON est remplacé par
58
+ [urban-json](https://pypi.org/project/urban-json/) (moteur JSLT) : les
59
+ messages produits par le moteur sont décrits par des templates `.jslt`
60
+ déclaratifs, modifiables sans redéploiement.
61
+
62
+ ## Un workflow en quelques lignes
63
+
64
+ ```sql
65
+ NAME ValidationAchat VERSION 1
66
+ MATCHING (Type = 'achat')
67
+
68
+ DEFINE EVENT DemandeSoumise 'une demande est déposée';
69
+ DEFINE RULE MontantInferieur (DECIMAL seuil) 'montant sous le seuil';
70
+ DEFINE ACTION NotifierValideur (TEXT dossier) 'demande d''approbation';
71
+ DEFINE CONST seuil_auto 500 'approbation automatique sous ce montant';
72
+
73
+ INITIALIZE WORKFLOW
74
+ ON EVENT DemandeSoumise RECURSIVE SWITCH Tri
75
+
76
+ DEFINE STATE Tri 'aiguillage'
77
+ ON EVENT DemandeSoumise
78
+ WHEN MontantInferieur(seuil = seuil_auto)
79
+ SWITCH Approuvee
80
+ EXECUTE NotifierValideur(dossier = @Workflow.ExternalId)
81
+ SWITCH EnValidation
82
+ ;
83
+ DEFINE STATE Approuvee 'approbation automatique' ;
84
+ DEFINE STATE EnValidation 'en attente d''un valideur'
85
+ EXPIRE AFTER 3 DAY SWITCH Expiree
86
+ ;
87
+ DEFINE STATE Expiree 'sans réponse' ;
88
+ ```
89
+
90
+ ```python
91
+ from urban_workflow import (
92
+ FunctionRegistry, MemoryStorage, Workflow,
93
+ WorkflowEngine, WorkflowFactory, WorkflowProcessor, WorkflowsConfig,
94
+ )
95
+ from urban_workflow.dsl import parse_config
96
+ from urban_workflow.outputs import default_memory_output
97
+
98
+ rules = FunctionRegistry("rule")
99
+ rules.add("MontantInferieur", lambda ctx, seuil: float(
100
+ ctx.incoming_event.extended_datas.get("Montant", 0)) < seuil)
101
+
102
+ storage = MemoryStorage()
103
+ configs = WorkflowsConfig().add_document(parse_config(SCRIPT, rules=rules))
104
+ processor = WorkflowProcessor(configs, WorkflowFactory())
105
+ processor.load_existing_workflows_by_external_id = (
106
+ lambda key: storage.get_by(Workflow, key, "external_id"))
107
+ processor.output_actions = default_memory_output(storage)
108
+ engine = WorkflowEngine(processor)
109
+
110
+ engine.evaluate_event({"Name": "DemandeSoumise", "ExternalId": "DA-42",
111
+ "Type": "achat", "Montant": 120})
112
+ ```
113
+
114
+ ## Documentation
115
+
116
+ | Guide | Contenu |
117
+ |---|---|
118
+ | [docs/guide-dsl.md](docs/guide-dsl.md) | **Créer un workflow** : référence complète du langage — création d'instances, transitions `SWITCH`, conditions `WHEN`, actions, expiration, fragments |
119
+ | [docs/guide-integration.md](docs/guide-integration.md) | **L'utiliser dans du code** : règles métier, assemblage du moteur, templates JSLT, chaîne de sorties, événements techniques |
120
+ | [docs/guide-tests.md](docs/guide-tests.md) | **Le tester** : règles pures, configuration, templates, scénarios de bout en bout |
121
+ | [docs/analyse-portage.md](docs/analyse-portage.md) | Analyse du projet source et choix structurants du portage |
122
+
123
+ English versions: [docs/en/dsl-guide.md](docs/en/dsl-guide.md) ·
124
+ [docs/en/integration-guide.md](docs/en/integration-guide.md) ·
125
+ [docs/en/testing-guide.md](docs/en/testing-guide.md)
126
+
127
+ ## Playground
128
+
129
+ Un site web local pour écrire les scripts avec **intellisense**,
130
+ diagnostics positionnés, paramétrage des règles métier et simulation
131
+ d'événements sur le vrai moteur — distribution séparée
132
+ [`urban-workflow-playground`](playground/README.md) :
133
+
134
+ ```bash
135
+ pip install urban-workflow-playground
136
+ urban-workflow-playground # http://127.0.0.1:8777
137
+ ```
138
+
139
+ ## Installation et développement
140
+
141
+ ```bash
142
+ pip install -e .[dev]
143
+ python3 -m pytest -q
144
+ ```
145
+
146
+ Dépendance unique du cœur : `urban-json` (qui apporte le runtime ANTLR du
147
+ parseur DSL). Python ≥ 3.10.
148
+
149
+ ## État du portage
150
+
151
+ Socle, DSL (grammaires ANTLR reprises du source), moteur, templating
152
+ urban-json et persistance SQL (extra `[sql]`, SQLAlchemy Core) sont
153
+ fonctionnels et testés (scénarios de bout en bout *script DSL →
154
+ événements → états → messages*, cycle de vie persisté en SQL), avec CI
155
+ GitHub Actions (tests Python 3.10 → 3.13, lint ruff). Les bugs connus du
156
+ moteur source ne sont pas reproduits — chacun est couvert par un test
157
+ dédié.
158
+
159
+ ## Licence
160
+
161
+ Voir [LICENSE](LICENSE).
@@ -0,0 +1,109 @@
1
+ # urban-workflow
2
+
3
+ Moteur de workflow piloté par un **langage dédié** (DSL) — portage Python de
4
+ [Black-Beard-Sdk/Workflow](https://github.com/Black-Beard-Sdk/Workflow)
5
+ (.NET), dont le système de normalisation des sorties JSON est remplacé par
6
+ [urban-json](https://pypi.org/project/urban-json/) (moteur JSLT) : les
7
+ messages produits par le moteur sont décrits par des templates `.jslt`
8
+ déclaratifs, modifiables sans redéploiement.
9
+
10
+ ## Un workflow en quelques lignes
11
+
12
+ ```sql
13
+ NAME ValidationAchat VERSION 1
14
+ MATCHING (Type = 'achat')
15
+
16
+ DEFINE EVENT DemandeSoumise 'une demande est déposée';
17
+ DEFINE RULE MontantInferieur (DECIMAL seuil) 'montant sous le seuil';
18
+ DEFINE ACTION NotifierValideur (TEXT dossier) 'demande d''approbation';
19
+ DEFINE CONST seuil_auto 500 'approbation automatique sous ce montant';
20
+
21
+ INITIALIZE WORKFLOW
22
+ ON EVENT DemandeSoumise RECURSIVE SWITCH Tri
23
+
24
+ DEFINE STATE Tri 'aiguillage'
25
+ ON EVENT DemandeSoumise
26
+ WHEN MontantInferieur(seuil = seuil_auto)
27
+ SWITCH Approuvee
28
+ EXECUTE NotifierValideur(dossier = @Workflow.ExternalId)
29
+ SWITCH EnValidation
30
+ ;
31
+ DEFINE STATE Approuvee 'approbation automatique' ;
32
+ DEFINE STATE EnValidation 'en attente d''un valideur'
33
+ EXPIRE AFTER 3 DAY SWITCH Expiree
34
+ ;
35
+ DEFINE STATE Expiree 'sans réponse' ;
36
+ ```
37
+
38
+ ```python
39
+ from urban_workflow import (
40
+ FunctionRegistry, MemoryStorage, Workflow,
41
+ WorkflowEngine, WorkflowFactory, WorkflowProcessor, WorkflowsConfig,
42
+ )
43
+ from urban_workflow.dsl import parse_config
44
+ from urban_workflow.outputs import default_memory_output
45
+
46
+ rules = FunctionRegistry("rule")
47
+ rules.add("MontantInferieur", lambda ctx, seuil: float(
48
+ ctx.incoming_event.extended_datas.get("Montant", 0)) < seuil)
49
+
50
+ storage = MemoryStorage()
51
+ configs = WorkflowsConfig().add_document(parse_config(SCRIPT, rules=rules))
52
+ processor = WorkflowProcessor(configs, WorkflowFactory())
53
+ processor.load_existing_workflows_by_external_id = (
54
+ lambda key: storage.get_by(Workflow, key, "external_id"))
55
+ processor.output_actions = default_memory_output(storage)
56
+ engine = WorkflowEngine(processor)
57
+
58
+ engine.evaluate_event({"Name": "DemandeSoumise", "ExternalId": "DA-42",
59
+ "Type": "achat", "Montant": 120})
60
+ ```
61
+
62
+ ## Documentation
63
+
64
+ | Guide | Contenu |
65
+ |---|---|
66
+ | [docs/guide-dsl.md](docs/guide-dsl.md) | **Créer un workflow** : référence complète du langage — création d'instances, transitions `SWITCH`, conditions `WHEN`, actions, expiration, fragments |
67
+ | [docs/guide-integration.md](docs/guide-integration.md) | **L'utiliser dans du code** : règles métier, assemblage du moteur, templates JSLT, chaîne de sorties, événements techniques |
68
+ | [docs/guide-tests.md](docs/guide-tests.md) | **Le tester** : règles pures, configuration, templates, scénarios de bout en bout |
69
+ | [docs/analyse-portage.md](docs/analyse-portage.md) | Analyse du projet source et choix structurants du portage |
70
+
71
+ English versions: [docs/en/dsl-guide.md](docs/en/dsl-guide.md) ·
72
+ [docs/en/integration-guide.md](docs/en/integration-guide.md) ·
73
+ [docs/en/testing-guide.md](docs/en/testing-guide.md)
74
+
75
+ ## Playground
76
+
77
+ Un site web local pour écrire les scripts avec **intellisense**,
78
+ diagnostics positionnés, paramétrage des règles métier et simulation
79
+ d'événements sur le vrai moteur — distribution séparée
80
+ [`urban-workflow-playground`](playground/README.md) :
81
+
82
+ ```bash
83
+ pip install urban-workflow-playground
84
+ urban-workflow-playground # http://127.0.0.1:8777
85
+ ```
86
+
87
+ ## Installation et développement
88
+
89
+ ```bash
90
+ pip install -e .[dev]
91
+ python3 -m pytest -q
92
+ ```
93
+
94
+ Dépendance unique du cœur : `urban-json` (qui apporte le runtime ANTLR du
95
+ parseur DSL). Python ≥ 3.10.
96
+
97
+ ## État du portage
98
+
99
+ Socle, DSL (grammaires ANTLR reprises du source), moteur, templating
100
+ urban-json et persistance SQL (extra `[sql]`, SQLAlchemy Core) sont
101
+ fonctionnels et testés (scénarios de bout en bout *script DSL →
102
+ événements → états → messages*, cycle de vie persisté en SQL), avec CI
103
+ GitHub Actions (tests Python 3.10 → 3.13, lint ruff). Les bugs connus du
104
+ moteur source ne sont pas reproduits — chacun est couvert par un test
105
+ dédié.
106
+
107
+ ## Licence
108
+
109
+ Voir [LICENSE](LICENSE).
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "urban-workflow"
7
+ version = "0.1.0"
8
+ description = "Moteur de workflow piloté par un DSL métier — portage Python de Black-Beard-Sdk/Workflow, sorties JSON normalisées via urban-json (JSLT)."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ authors = [{ name = "Gael Beard", email = "gaelgael5@gmail.com" }]
12
+ requires-python = ">=3.10"
13
+ dependencies = [
14
+ "urban-json>=0.2.1",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Libraries",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/gaelgael5/urban-workflow"
29
+ Issues = "https://github.com/gaelgael5/urban-workflow/issues"
30
+
31
+ [project.optional-dependencies]
32
+ sql = [
33
+ "sqlalchemy>=2.0",
34
+ ]
35
+ # Le playground est une distribution séparée : pip install urban-workflow-playground
36
+ # (sous-projet playground/ de ce dépôt).
37
+ dev = [
38
+ "pytest>=7",
39
+ "ruff>=0.4",
40
+ "sqlalchemy>=2.0",
41
+ "fastapi>=0.110",
42
+ "uvicorn>=0.27",
43
+ "httpx>=0.27",
44
+ ]
45
+
46
+ [tool.setuptools.packages.find]
47
+ include = ["urban_workflow*"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
51
+
52
+ [tool.ruff]
53
+ line-length = 100
54
+ target-version = "py310"
55
+ extend-exclude = ["urban_workflow/dsl/generated"]
56
+
57
+ [tool.ruff.lint]
58
+ # Jeu de règles explicite : la CI ne dépend pas des défauts de ruff.
59
+ select = ["E4", "E7", "E9", "F", "I", "UP", "RUF100"]
60
+
61
+ [tool.ruff.lint.per-file-ignores]
62
+ # Imports après pytest.importorskip("sqlalchemy") : voulu.
63
+ "tests/test_storage_sql.py" = ["E402"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,152 @@
1
+ """Tests du modèle de configuration et des builders fluides.
2
+
3
+ La configuration construite ici reprend celle de ``WorkflowsUnitTest``
4
+ du projet source (``InitializeWorkflow``) : construction à la main, sans
5
+ DSL — le moteur viendra l'exécuter à l'étape suivante.
6
+ """
7
+
8
+ import pytest
9
+
10
+ from urban_workflow import (
11
+ IncomingEvent,
12
+ IncomingEventConfig,
13
+ InitializationOnEventConfig,
14
+ MissingConfigurationError,
15
+ MissingStateError,
16
+ ResultActionConfig,
17
+ RunContext,
18
+ StateConfig,
19
+ TransitionConfig,
20
+ Workflow,
21
+ WorkflowConfig,
22
+ WorkflowsConfig,
23
+ constants,
24
+ )
25
+ from urban_workflow.values import ContextRef
26
+
27
+
28
+ def build_wrk1(version: int = 1) -> WorkflowConfig:
29
+ """La config du test source InitializeWorkflow."""
30
+ return (
31
+ WorkflowConfig(name="wrk1", label="wrk1 config", version=version)
32
+ .add_initializer(
33
+ InitializationOnEventConfig(event_name="evnt1", recursive=True).add_switch("State1")
34
+ )
35
+ .add_state(
36
+ StateConfig(name="State1", label="State1").add_event(
37
+ IncomingEventConfig(name="evnt1").add_transition(
38
+ TransitionConfig(target_state_name="State2")
39
+ )
40
+ )
41
+ )
42
+ .add_state(StateConfig(name="State2", label="State2"))
43
+ )
44
+
45
+
46
+ def test_build_resolves_target_states():
47
+ config = build_wrk1().build()
48
+
49
+ switch = config.initializers["evnt1"].switchs[0]
50
+ assert switch.target_state is config.states["State1"]
51
+
52
+ transition = config.states["State1"].events["evnt1"].transitions[0]
53
+ assert transition.target_state is config.states["State2"]
54
+ assert transition.label == "State1 -> State2"
55
+
56
+
57
+ def test_build_declares_events_automatically():
58
+ config = build_wrk1().build()
59
+ # $_result (technique), evnt1 (initialiseur + état)
60
+ assert constants.RESULT_ACTION_NAME in config.declared_events
61
+ assert "evnt1" in config.declared_events
62
+ assert config.declared_events[constants.RESULT_ACTION_NAME].label != ""
63
+
64
+
65
+ def test_build_missing_state_raises():
66
+ config = WorkflowConfig(name="bad", version=1).add_initializer(
67
+ InitializationOnEventConfig(event_name="e").add_switch("Nowhere")
68
+ )
69
+ with pytest.raises(MissingStateError) as exc:
70
+ config.build()
71
+ assert "Nowhere" in str(exc.value)
72
+
73
+
74
+ def test_build_is_idempotent():
75
+ config = build_wrk1()
76
+ config.build()
77
+ config.build() # ne doit pas re-déclarer ni échouer
78
+ assert len(config.declared_events) == 2
79
+
80
+
81
+ def test_evaluate_filter_matching():
82
+ config = build_wrk1()
83
+ config.add_filter("Country", "France")
84
+
85
+ event_fr = IncomingEvent(name="evnt1")
86
+ event_fr.extended_datas["Country"] = "France"
87
+ event_de = IncomingEvent(name="evnt1")
88
+ event_de.extended_datas["Country"] = "Germany"
89
+ event_none = IncomingEvent(name="evnt1")
90
+
91
+ assert config.evaluate_filter(event_fr) is True
92
+ assert config.evaluate_filter(event_de) is False
93
+ assert config.evaluate_filter(event_none) is False
94
+
95
+
96
+ def test_workflows_config_get_by_name_and_version():
97
+ configs = WorkflowsConfig()
98
+ configs.add_document(build_wrk1(version=1))
99
+ configs.add_document(build_wrk1(version=2))
100
+
101
+ assert configs.get("wrk1", 2).version == 2
102
+ assert len(configs.get_all("wrk1")) == 2
103
+ with pytest.raises(MissingConfigurationError):
104
+ configs.get("wrk1", 99)
105
+ with pytest.raises(MissingConfigurationError):
106
+ configs.get("unknown", 1)
107
+
108
+
109
+ def test_workflows_config_matching_honors_filters_and_version_selector():
110
+ configs = WorkflowsConfig()
111
+ configs.add_document(build_wrk1(version=1))
112
+ configs.add_document(build_wrk1(version=2))
113
+
114
+ event = IncomingEvent(name="evnt1")
115
+ assert {c.version for c in configs.matching(event)} == {1, 2}
116
+
117
+ # Restreint la création aux workflows version 2.
118
+ configs.set_version_selector("wrk1", 2)
119
+ assert {c.version for c in configs.matching(event)} == {2}
120
+
121
+
122
+ def test_result_action_config_map_and_arguments():
123
+ action_config = (
124
+ ResultActionConfig(name="Cut", kind=constants.PUSH_ACTION_NAME)
125
+ .add_argument("key", "@IncomingEvent.ExternalId")
126
+ .add_argument("mode", "hard")
127
+ )
128
+ assert isinstance(action_config.arguments["key"], ContextRef)
129
+
130
+ incoming = IncomingEvent(name="evnt1", external_id="ext-7")
131
+ ctx = RunContext(workflow=Workflow(), incoming_event=incoming)
132
+
133
+ action = action_config.map(ctx)
134
+ assert action.name == "Cut"
135
+ assert action.kind == constants.PUSH_ACTION_NAME
136
+ assert action.event_date is not None
137
+ # Les arguments restent paresseux jusqu'à la résolution explicite.
138
+ assert action.resolve_arguments(ctx) == {"key": "ext-7", "mode": "hard"}
139
+
140
+
141
+ def test_rule_actions_grouped_by_shared_rule():
142
+ def always(ctx):
143
+ return True
144
+
145
+ state = StateConfig(name="S")
146
+ state.add_incoming_actions(always, ResultActionConfig(name="A"))
147
+ state.add_incoming_actions(always, ResultActionConfig(name="B"))
148
+ state.add_incoming_actions(None, ResultActionConfig(name="C"))
149
+
150
+ assert len(state.incoming_rules) == 2 # 'always' regroupé, None séparé
151
+ grouped = next(r for r in state.incoming_rules if r.when_rule is always)
152
+ assert [a.name for a in grouped.actions] == ["A", "B"]