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.
- urban_workflow-0.1.0/LICENSE +21 -0
- urban_workflow-0.1.0/PKG-INFO +161 -0
- urban_workflow-0.1.0/README.md +109 -0
- urban_workflow-0.1.0/pyproject.toml +63 -0
- urban_workflow-0.1.0/setup.cfg +4 -0
- urban_workflow-0.1.0/tests/test_config.py +152 -0
- urban_workflow-0.1.0/tests/test_docs_examples.py +226 -0
- urban_workflow-0.1.0/tests/test_dsl.py +527 -0
- urban_workflow-0.1.0/tests/test_dsl_includes.py +95 -0
- urban_workflow-0.1.0/tests/test_dsl_validation.py +275 -0
- urban_workflow-0.1.0/tests/test_engine.py +586 -0
- urban_workflow-0.1.0/tests/test_engine_dsl.py +161 -0
- urban_workflow-0.1.0/tests/test_models.py +58 -0
- urban_workflow-0.1.0/tests/test_playground.py +155 -0
- urban_workflow-0.1.0/tests/test_registry.py +75 -0
- urban_workflow-0.1.0/tests/test_storage_sql.py +293 -0
- urban_workflow-0.1.0/tests/test_templating.py +329 -0
- urban_workflow-0.1.0/tests/test_values.py +60 -0
- urban_workflow-0.1.0/urban_workflow/__init__.py +118 -0
- urban_workflow-0.1.0/urban_workflow/clock.py +45 -0
- urban_workflow-0.1.0/urban_workflow/config/__init__.py +32 -0
- urban_workflow-0.1.0/urban_workflow/config/rules.py +85 -0
- urban_workflow-0.1.0/urban_workflow/config/states.py +118 -0
- urban_workflow-0.1.0/urban_workflow/config/workflow.py +189 -0
- urban_workflow-0.1.0/urban_workflow/constants.py +22 -0
- urban_workflow-0.1.0/urban_workflow/dsl/__init__.py +54 -0
- urban_workflow-0.1.0/urban_workflow/dsl/errors.py +23 -0
- urban_workflow-0.1.0/urban_workflow/dsl/generated/WorkflowLexer.py +281 -0
- urban_workflow-0.1.0/urban_workflow/dsl/generated/WorkflowParser.py +2929 -0
- urban_workflow-0.1.0/urban_workflow/dsl/generated/WorkflowParserVisitor.py +193 -0
- urban_workflow-0.1.0/urban_workflow/dsl/generated/__init__.py +4 -0
- urban_workflow-0.1.0/urban_workflow/dsl/loader.py +68 -0
- urban_workflow-0.1.0/urban_workflow/dsl/parser.py +128 -0
- urban_workflow-0.1.0/urban_workflow/dsl/visitor.py +942 -0
- urban_workflow-0.1.0/urban_workflow/engine/__init__.py +31 -0
- urban_workflow-0.1.0/urban_workflow/engine/append_events.py +82 -0
- urban_workflow-0.1.0/urban_workflow/engine/engine.py +86 -0
- urban_workflow-0.1.0/urban_workflow/engine/factory.py +145 -0
- urban_workflow-0.1.0/urban_workflow/engine/processor.py +308 -0
- urban_workflow-0.1.0/urban_workflow/engine/serializer.py +72 -0
- urban_workflow-0.1.0/urban_workflow/engine/translator.py +76 -0
- urban_workflow-0.1.0/urban_workflow/exceptions.py +61 -0
- urban_workflow-0.1.0/urban_workflow/models.py +188 -0
- urban_workflow-0.1.0/urban_workflow/outputs/__init__.py +38 -0
- urban_workflow-0.1.0/urban_workflow/outputs/base.py +163 -0
- urban_workflow-0.1.0/urban_workflow/outputs/memory.py +155 -0
- urban_workflow-0.1.0/urban_workflow/registry.py +130 -0
- urban_workflow-0.1.0/urban_workflow/storage_sql/__init__.py +45 -0
- urban_workflow-0.1.0/urban_workflow/storage_sql/outputs.py +114 -0
- urban_workflow-0.1.0/urban_workflow/storage_sql/schema.py +71 -0
- urban_workflow-0.1.0/urban_workflow/storage_sql/storage.py +251 -0
- urban_workflow-0.1.0/urban_workflow/templating/__init__.py +40 -0
- urban_workflow-0.1.0/urban_workflow/templating/context.py +102 -0
- urban_workflow-0.1.0/urban_workflow/templating/metadata.py +41 -0
- urban_workflow-0.1.0/urban_workflow/templating/registry.py +121 -0
- urban_workflow-0.1.0/urban_workflow/templating/translator.py +62 -0
- urban_workflow-0.1.0/urban_workflow/values.py +106 -0
- urban_workflow-0.1.0/urban_workflow.egg-info/PKG-INFO +161 -0
- urban_workflow-0.1.0/urban_workflow.egg-info/SOURCES.txt +60 -0
- urban_workflow-0.1.0/urban_workflow.egg-info/dependency_links.txt +1 -0
- urban_workflow-0.1.0/urban_workflow.egg-info/requires.txt +12 -0
- 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,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"]
|