urban-workflow-playground 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_playground-0.1.0/PKG-INFO +63 -0
- urban_workflow_playground-0.1.0/README.md +44 -0
- urban_workflow_playground-0.1.0/pyproject.toml +37 -0
- urban_workflow_playground-0.1.0/setup.cfg +4 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/__init__.py +13 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/__main__.py +18 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/app.py +344 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/app.js +426 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/examples.js +119 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/index.html +87 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/style.css +211 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/codemirror.css +344 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/codemirror.js +9884 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/lint.css +79 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/lint.js +288 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/show-hint.css +37 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/show-hint.js +523 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/simple.js +216 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/PKG-INFO +63 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/SOURCES.txt +22 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/dependency_links.txt +1 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/entry_points.txt +2 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/requires.txt +3 -0
- urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: urban-workflow-playground
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Playground web du DSL urban-workflow : éditeur avec intellisense, paramétrage des règles métier, diagnostics positionnés et simulation d'événements.
|
|
5
|
+
Author-email: Gael Beard <gaelgael5@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gaelgael5/urban-workflow
|
|
8
|
+
Project-URL: Issues, https://github.com/gaelgael5/urban-workflow/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: urban-workflow>=0.1.0
|
|
17
|
+
Requires-Dist: fastapi>=0.110
|
|
18
|
+
Requires-Dist: uvicorn>=0.27
|
|
19
|
+
|
|
20
|
+
# urban-workflow-playground
|
|
21
|
+
|
|
22
|
+
Site web local pour écrire et tester des scripts de workflow
|
|
23
|
+
[urban-workflow](https://pypi.org/project/urban-workflow/) :
|
|
24
|
+
|
|
25
|
+
- **éditeur** avec coloration du DSL et **intellisense contextuelle**
|
|
26
|
+
(`Ctrl+Espace` ou automatique) : événements après `ON EVENT`, états
|
|
27
|
+
après `SWITCH`, règles après `WHEN`/`AND`/`OR`/`NOT`, actions après
|
|
28
|
+
`EXECUTE`, chemins après `@`, constantes après `=` — alimentée en
|
|
29
|
+
continu par l'analyse **réelle** du script (parseur ANTLR + visiteur) ;
|
|
30
|
+
- **diagnostics positionnés** (syntaxe et validation sémantique),
|
|
31
|
+
soulignés dans l'éditeur et cliquables ;
|
|
32
|
+
- **paramétrage** : déclaration des règles métier disponibles (nom,
|
|
33
|
+
paramètres typés, résultat simulé), persistée dans le navigateur —
|
|
34
|
+
c'est contre ces signatures que les appels `WHEN` sont validés ;
|
|
35
|
+
- **structure** : vue arborescente de la configuration compilée
|
|
36
|
+
(initialiseurs, états, transitions) ;
|
|
37
|
+
- **simulation** : une liste JSON d'événements rejoués dans l'ordre sur
|
|
38
|
+
le vrai moteur en mémoire — états atteints, actions poussées
|
|
39
|
+
(messages inclus), journal fonctionnel des règles.
|
|
40
|
+
|
|
41
|
+
## Lancement
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install urban-workflow-playground
|
|
45
|
+
urban-workflow-playground # http://127.0.0.1:8777
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
ou `python -m urban_workflow_playground`. Variables : `PLAYGROUND_HOST`
|
|
49
|
+
(défaut `127.0.0.1`), `PLAYGROUND_PORT` (défaut `8777`).
|
|
50
|
+
|
|
51
|
+
Depuis un clone du dépôt : `pip install -e ./playground`.
|
|
52
|
+
|
|
53
|
+
## Notes
|
|
54
|
+
|
|
55
|
+
- Outil de **développement local** : aucune authentification, ne pas
|
|
56
|
+
exposer publiquement.
|
|
57
|
+
- Les règles simulées retournent le booléen configuré (le moteur et la
|
|
58
|
+
validation sont réels ; seule la logique interne des règles est
|
|
59
|
+
remplacée).
|
|
60
|
+
- Le visiteur s'arrête à la première erreur sémantique : les erreurs
|
|
61
|
+
se corrigent donc une par une (la syntaxe, elle, est remontée en bloc).
|
|
62
|
+
- `urban_workflow_playground/static/vendor/` : CodeMirror 5.65.18
|
|
63
|
+
(licence MIT), embarqué pour fonctionner hors ligne.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# urban-workflow-playground
|
|
2
|
+
|
|
3
|
+
Site web local pour écrire et tester des scripts de workflow
|
|
4
|
+
[urban-workflow](https://pypi.org/project/urban-workflow/) :
|
|
5
|
+
|
|
6
|
+
- **éditeur** avec coloration du DSL et **intellisense contextuelle**
|
|
7
|
+
(`Ctrl+Espace` ou automatique) : événements après `ON EVENT`, états
|
|
8
|
+
après `SWITCH`, règles après `WHEN`/`AND`/`OR`/`NOT`, actions après
|
|
9
|
+
`EXECUTE`, chemins après `@`, constantes après `=` — alimentée en
|
|
10
|
+
continu par l'analyse **réelle** du script (parseur ANTLR + visiteur) ;
|
|
11
|
+
- **diagnostics positionnés** (syntaxe et validation sémantique),
|
|
12
|
+
soulignés dans l'éditeur et cliquables ;
|
|
13
|
+
- **paramétrage** : déclaration des règles métier disponibles (nom,
|
|
14
|
+
paramètres typés, résultat simulé), persistée dans le navigateur —
|
|
15
|
+
c'est contre ces signatures que les appels `WHEN` sont validés ;
|
|
16
|
+
- **structure** : vue arborescente de la configuration compilée
|
|
17
|
+
(initialiseurs, états, transitions) ;
|
|
18
|
+
- **simulation** : une liste JSON d'événements rejoués dans l'ordre sur
|
|
19
|
+
le vrai moteur en mémoire — états atteints, actions poussées
|
|
20
|
+
(messages inclus), journal fonctionnel des règles.
|
|
21
|
+
|
|
22
|
+
## Lancement
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install urban-workflow-playground
|
|
26
|
+
urban-workflow-playground # http://127.0.0.1:8777
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
ou `python -m urban_workflow_playground`. Variables : `PLAYGROUND_HOST`
|
|
30
|
+
(défaut `127.0.0.1`), `PLAYGROUND_PORT` (défaut `8777`).
|
|
31
|
+
|
|
32
|
+
Depuis un clone du dépôt : `pip install -e ./playground`.
|
|
33
|
+
|
|
34
|
+
## Notes
|
|
35
|
+
|
|
36
|
+
- Outil de **développement local** : aucune authentification, ne pas
|
|
37
|
+
exposer publiquement.
|
|
38
|
+
- Les règles simulées retournent le booléen configuré (le moteur et la
|
|
39
|
+
validation sont réels ; seule la logique interne des règles est
|
|
40
|
+
remplacée).
|
|
41
|
+
- Le visiteur s'arrête à la première erreur sémantique : les erreurs
|
|
42
|
+
se corrigent donc une par une (la syntaxe, elle, est remontée en bloc).
|
|
43
|
+
- `urban_workflow_playground/static/vendor/` : CodeMirror 5.65.18
|
|
44
|
+
(licence MIT), embarqué pour fonctionner hors ligne.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "urban-workflow-playground"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Playground web du DSL urban-workflow : éditeur avec intellisense, paramétrage des règles métier, diagnostics positionnés et simulation d'événements."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Gael Beard", email = "gaelgael5@gmail.com" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"urban-workflow>=0.1.0",
|
|
15
|
+
"fastapi>=0.110",
|
|
16
|
+
"uvicorn>=0.27",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
"Environment :: Web Environment",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/gaelgael5/urban-workflow"
|
|
28
|
+
Issues = "https://github.com/gaelgael5/urban-workflow/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
urban-workflow-playground = "urban_workflow_playground.__main__:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
include = ["urban_workflow_playground*"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
urban_workflow_playground = ["static/*", "static/vendor/*"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Playground web du DSL urban-workflow.
|
|
2
|
+
|
|
3
|
+
Éditeur avec intellisense, diagnostics positionnés, paramétrage des
|
|
4
|
+
règles métier et simulation d'événements sur le vrai moteur.
|
|
5
|
+
|
|
6
|
+
Lancement ::
|
|
7
|
+
|
|
8
|
+
pip install urban-workflow-playground
|
|
9
|
+
urban-workflow-playground # http://127.0.0.1:8777
|
|
10
|
+
# ou : python -m urban_workflow_playground
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""``python -m urban_workflow_playground`` — démarre le serveur du playground."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import uvicorn
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
uvicorn.run(
|
|
10
|
+
"urban_workflow_playground.app:app",
|
|
11
|
+
host=os.environ.get("PLAYGROUND_HOST", "127.0.0.1"),
|
|
12
|
+
port=int(os.environ.get("PLAYGROUND_PORT", "8777")),
|
|
13
|
+
reload=False,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
main()
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""API du playground : analyse (diagnostics + symboles) et simulation.
|
|
2
|
+
|
|
3
|
+
Deux endpoints :
|
|
4
|
+
|
|
5
|
+
- ``POST /api/analyze`` — analyse un script avec les règles paramétrées ;
|
|
6
|
+
retourne les erreurs **positionnées** (syntaxe et sémantique), les
|
|
7
|
+
symboles pour l'intellisense (événements, règles, actions, constantes,
|
|
8
|
+
états) et un résumé structurel de la configuration ;
|
|
9
|
+
- ``POST /api/simulate`` — rejoue une suite d'événements sur le script
|
|
10
|
+
(moteur complet en mémoire) et retourne états, actions poussées et
|
|
11
|
+
journal fonctionnel.
|
|
12
|
+
|
|
13
|
+
Les règles métier n'existant pas côté navigateur, le panneau de
|
|
14
|
+
paramétrage les déclare (nom, paramètres typés, résultat simulé) ; le
|
|
15
|
+
serveur fabrique des callables à la signature exacte pour que la
|
|
16
|
+
validation des appels ``WHEN`` soit la vraie.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from fastapi import FastAPI
|
|
26
|
+
from fastapi.responses import FileResponse
|
|
27
|
+
from fastapi.staticfiles import StaticFiles
|
|
28
|
+
from pydantic import BaseModel, Field
|
|
29
|
+
|
|
30
|
+
from urban_workflow import (
|
|
31
|
+
FunctionRegistry,
|
|
32
|
+
MemoryStorage,
|
|
33
|
+
Workflow,
|
|
34
|
+
WorkflowEngine,
|
|
35
|
+
WorkflowFactory,
|
|
36
|
+
WorkflowProcessor,
|
|
37
|
+
WorkflowsConfig,
|
|
38
|
+
)
|
|
39
|
+
from urban_workflow.dsl import WorkflowConfigVisitor, WorkflowScriptParser
|
|
40
|
+
from urban_workflow.exceptions import DslSyntaxError, WorkflowError
|
|
41
|
+
from urban_workflow.outputs import default_memory_output
|
|
42
|
+
from urban_workflow.templating.context import json_safe
|
|
43
|
+
|
|
44
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
45
|
+
|
|
46
|
+
_IDENTIFIER = re.compile(r"^[A-Za-z][A-Za-z0-9_$#]*$")
|
|
47
|
+
_POSITIONED = re.compile(r"^(?:[^:\n]*:)?(\d+):(\d+):\s*(.*)$", re.DOTALL)
|
|
48
|
+
|
|
49
|
+
app = FastAPI(title="urban-workflow playground")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ----------------------------------------------------------------------
|
|
53
|
+
# Modèles d'échange
|
|
54
|
+
# ----------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RuleParam(BaseModel):
|
|
58
|
+
name: str
|
|
59
|
+
type: str = "TEXT" # TEXT | INTEGER | DECIMAL
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RuleSetting(BaseModel):
|
|
63
|
+
"""Une règle métier paramétrée dans le panneau."""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
params: list[RuleParam] = Field(default_factory=list)
|
|
67
|
+
result: bool = True # résultat simulé
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AnalyzeRequest(BaseModel):
|
|
71
|
+
script: str
|
|
72
|
+
rules: list[RuleSetting] = Field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SimulateRequest(BaseModel):
|
|
76
|
+
script: str
|
|
77
|
+
rules: list[RuleSetting] = Field(default_factory=list)
|
|
78
|
+
events: list[dict[str, Any]] = Field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ----------------------------------------------------------------------
|
|
82
|
+
# Règles simulées
|
|
83
|
+
# ----------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _build_registry(settings: list[RuleSetting]) -> FunctionRegistry:
|
|
87
|
+
"""Fabrique des callables à la signature exacte des règles paramétrées."""
|
|
88
|
+
registry = FunctionRegistry("rule")
|
|
89
|
+
for setting in settings:
|
|
90
|
+
if not _IDENTIFIER.match(setting.name):
|
|
91
|
+
continue
|
|
92
|
+
params = [p.name for p in setting.params if _IDENTIFIER.match(p.name)]
|
|
93
|
+
signature = ", ".join(["ctx", *params])
|
|
94
|
+
namespace: dict[str, Any] = {}
|
|
95
|
+
exec(
|
|
96
|
+
f"def _rule({signature}):\n return {bool(setting.result)}",
|
|
97
|
+
namespace,
|
|
98
|
+
)
|
|
99
|
+
if setting.name not in registry:
|
|
100
|
+
registry.add(setting.name, namespace["_rule"])
|
|
101
|
+
return registry
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ----------------------------------------------------------------------
|
|
105
|
+
# Extraction de symboles (repli regex quand le script ne compile pas)
|
|
106
|
+
# ----------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
_RE_EVENT = re.compile(r"\bDEFINE\s+EVENT\s+(\w+)", re.IGNORECASE)
|
|
109
|
+
_RE_RULE = re.compile(r"\bDEFINE\s+RULE\s+(\w+)\s*\(([^)]*)\)", re.IGNORECASE)
|
|
110
|
+
_RE_ACTION = re.compile(r"\bDEFINE\s+ACTION\s+(\w+)\s*\(([^)]*)\)", re.IGNORECASE)
|
|
111
|
+
_RE_CONST = re.compile(r"\bDEFINE\s+CONST\s+(\w+)", re.IGNORECASE)
|
|
112
|
+
_RE_STATE = re.compile(r"\bDEFINE\s+STATE\s+(\w+)", re.IGNORECASE)
|
|
113
|
+
_RE_PARAM = re.compile(r"(TEXT|INTEGER|DECIMAL)\s+(\w+)", re.IGNORECASE)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _params_from_text(text: str) -> list[dict[str, str]]:
|
|
117
|
+
return [
|
|
118
|
+
{"name": name, "type": kind.upper()}
|
|
119
|
+
for kind, name in _RE_PARAM.findall(text or "")
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _symbols_from_text(script: str) -> dict[str, Any]:
|
|
124
|
+
return {
|
|
125
|
+
"events": sorted(set(_RE_EVENT.findall(script))),
|
|
126
|
+
"rules": [
|
|
127
|
+
{"name": name, "params": _params_from_text(params)}
|
|
128
|
+
for name, params in _RE_RULE.findall(script)
|
|
129
|
+
],
|
|
130
|
+
"actions": [
|
|
131
|
+
{"name": name, "params": _params_from_text(params)}
|
|
132
|
+
for name, params in _RE_ACTION.findall(script)
|
|
133
|
+
],
|
|
134
|
+
"constants": sorted(set(_RE_CONST.findall(script))),
|
|
135
|
+
"states": sorted(set(_RE_STATE.findall(script))),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_DSL_TYPE_NAMES = {str: "TEXT", int: "INTEGER", float: "DECIMAL"}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _symbols_from_visitor(visitor: WorkflowConfigVisitor, config) -> dict[str, Any]:
|
|
143
|
+
def declared(kind: str) -> list[dict[str, Any]]:
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
"name": d.name,
|
|
147
|
+
"params": [
|
|
148
|
+
{"name": n, "type": _DSL_TYPE_NAMES.get(t, t.__name__.upper())}
|
|
149
|
+
for n, t in d.parameters.items()
|
|
150
|
+
],
|
|
151
|
+
"comment": d.comment,
|
|
152
|
+
}
|
|
153
|
+
for d in visitor.declarations.values()
|
|
154
|
+
if d.kind == kind
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"events": sorted(config.declared_events),
|
|
159
|
+
"rules": declared("rule"),
|
|
160
|
+
"actions": declared("action"),
|
|
161
|
+
"constants": [
|
|
162
|
+
{"name": c.key, "value": c.value, "comment": c.comment}
|
|
163
|
+
for c in visitor.constants.values()
|
|
164
|
+
],
|
|
165
|
+
"states": sorted(config.states),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ----------------------------------------------------------------------
|
|
170
|
+
# Erreurs positionnées
|
|
171
|
+
# ----------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _error_entry(message: str, line: int = 1, column: int = 0) -> dict[str, Any]:
|
|
175
|
+
return {"line": line, "column": column, "message": message, "severity": "error"}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _positioned(exc: WorkflowError) -> dict[str, Any]:
|
|
179
|
+
"""Retrouve ``ligne:colonne`` dans le message des erreurs du visiteur."""
|
|
180
|
+
match = _POSITIONED.match(str(exc))
|
|
181
|
+
if match:
|
|
182
|
+
return _error_entry(match.group(3), int(match.group(1)), int(match.group(2)))
|
|
183
|
+
return _error_entry(str(exc))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _summary(config) -> dict[str, Any]:
|
|
187
|
+
return {
|
|
188
|
+
"name": config.name,
|
|
189
|
+
"version": config.version,
|
|
190
|
+
"concurrency": config.concurrency,
|
|
191
|
+
"label": config.label,
|
|
192
|
+
"filters": dict(config.filters),
|
|
193
|
+
"initializers": [
|
|
194
|
+
{
|
|
195
|
+
"event": init.event_name,
|
|
196
|
+
"recursive": init.recursive,
|
|
197
|
+
"switchs": [
|
|
198
|
+
{"target": s.target_state_name, "when": s.when_rule_text}
|
|
199
|
+
for s in init.switchs
|
|
200
|
+
],
|
|
201
|
+
}
|
|
202
|
+
for init in config.initializers.values()
|
|
203
|
+
],
|
|
204
|
+
"states": [
|
|
205
|
+
{
|
|
206
|
+
"name": state.name,
|
|
207
|
+
"label": state.label,
|
|
208
|
+
"events": [
|
|
209
|
+
{
|
|
210
|
+
"name": event.name,
|
|
211
|
+
"transitions": [
|
|
212
|
+
{"target": t.target_state_name, "when": t.when_rule_text}
|
|
213
|
+
for t in event.transitions
|
|
214
|
+
],
|
|
215
|
+
}
|
|
216
|
+
for event in state.events.values()
|
|
217
|
+
],
|
|
218
|
+
}
|
|
219
|
+
for state in config.states.values()
|
|
220
|
+
],
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ----------------------------------------------------------------------
|
|
225
|
+
# Endpoints
|
|
226
|
+
# ----------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@app.post("/api/analyze")
|
|
230
|
+
def analyze(request: AnalyzeRequest) -> dict[str, Any]:
|
|
231
|
+
errors: list[dict[str, Any]] = []
|
|
232
|
+
summary: dict[str, Any] | None = None
|
|
233
|
+
symbols = _symbols_from_text(request.script)
|
|
234
|
+
|
|
235
|
+
parser = WorkflowScriptParser.parse_string(request.script, filename="playground")
|
|
236
|
+
for error in parser.errors:
|
|
237
|
+
errors.append(_error_entry(error.message, error.line, error.column))
|
|
238
|
+
|
|
239
|
+
if not errors:
|
|
240
|
+
visitor = WorkflowConfigVisitor(
|
|
241
|
+
rules=_build_registry(request.rules), filename="playground"
|
|
242
|
+
)
|
|
243
|
+
try:
|
|
244
|
+
config = parser.visit(visitor)
|
|
245
|
+
if not parser.is_fragment:
|
|
246
|
+
summary = _summary(config)
|
|
247
|
+
symbols = _symbols_from_visitor(visitor, config)
|
|
248
|
+
except DslSyntaxError as exc:
|
|
249
|
+
errors.extend(
|
|
250
|
+
_error_entry(e.message, e.line, e.column) for e in exc.errors
|
|
251
|
+
)
|
|
252
|
+
except WorkflowError as exc:
|
|
253
|
+
errors.append(_positioned(exc))
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"ok": not errors,
|
|
257
|
+
"is_fragment": parser.is_fragment,
|
|
258
|
+
"errors": errors,
|
|
259
|
+
"symbols": symbols,
|
|
260
|
+
"summary": summary,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@app.post("/api/simulate")
|
|
265
|
+
def simulate(request: SimulateRequest) -> dict[str, Any]:
|
|
266
|
+
registry = _build_registry(request.rules)
|
|
267
|
+
|
|
268
|
+
parser = WorkflowScriptParser.parse_string(request.script, filename="playground")
|
|
269
|
+
try:
|
|
270
|
+
parser.check()
|
|
271
|
+
config = parser.visit(WorkflowConfigVisitor(rules=registry, filename="playground"))
|
|
272
|
+
except WorkflowError as exc:
|
|
273
|
+
return {"ok": False, "error": str(exc), "steps": []}
|
|
274
|
+
|
|
275
|
+
storage = MemoryStorage()
|
|
276
|
+
processor = WorkflowProcessor(
|
|
277
|
+
WorkflowsConfig().add_document(config), WorkflowFactory()
|
|
278
|
+
)
|
|
279
|
+
processor.load_existing_workflows_by_external_id = lambda key: storage.get_by(
|
|
280
|
+
Workflow, key, "external_id"
|
|
281
|
+
)
|
|
282
|
+
processor.output_actions = default_memory_output(storage)
|
|
283
|
+
engine = WorkflowEngine(processor)
|
|
284
|
+
|
|
285
|
+
steps: list[dict[str, Any]] = []
|
|
286
|
+
for payload in request.events:
|
|
287
|
+
step: dict[str, Any] = {"event": payload.get("Name", "?")}
|
|
288
|
+
try:
|
|
289
|
+
contexts = engine.evaluate_event(payload)
|
|
290
|
+
except Exception as exc: # erreur de payload ou de règle : on la montre
|
|
291
|
+
step["error"] = f"{type(exc).__name__}: {exc}"
|
|
292
|
+
steps.append(step)
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
step["impacted"] = [
|
|
296
|
+
{
|
|
297
|
+
"workflow": ctx.workflow.workflow_name,
|
|
298
|
+
"external_id": ctx.workflow.external_id,
|
|
299
|
+
"from_state": ctx.event.from_state,
|
|
300
|
+
"to_state": ctx.event.to_state,
|
|
301
|
+
"actions": [
|
|
302
|
+
{
|
|
303
|
+
"name": pushed.name,
|
|
304
|
+
"kind": pushed.kind,
|
|
305
|
+
"message": json_safe(pushed.execute_message),
|
|
306
|
+
}
|
|
307
|
+
for pushed in ctx.event.actions
|
|
308
|
+
],
|
|
309
|
+
"log": list(ctx.functional_log),
|
|
310
|
+
}
|
|
311
|
+
for ctx in contexts
|
|
312
|
+
]
|
|
313
|
+
steps.append(step)
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"ok": True,
|
|
317
|
+
"steps": steps,
|
|
318
|
+
"workflows": [
|
|
319
|
+
{
|
|
320
|
+
"workflow": w.workflow_name,
|
|
321
|
+
"external_id": w.external_id,
|
|
322
|
+
"state": w.current_state,
|
|
323
|
+
"extended_datas": json_safe(w.extended_datas),
|
|
324
|
+
"events": [
|
|
325
|
+
{"name": e.name, "from": e.from_state, "to": e.to_state}
|
|
326
|
+
for e in w.events
|
|
327
|
+
],
|
|
328
|
+
}
|
|
329
|
+
for w in storage.get_all(Workflow)
|
|
330
|
+
],
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# ----------------------------------------------------------------------
|
|
335
|
+
# Fichiers statiques
|
|
336
|
+
# ----------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@app.get("/")
|
|
340
|
+
def index() -> FileResponse:
|
|
341
|
+
return FileResponse(STATIC_DIR / "index.html")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|