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.
Files changed (24) hide show
  1. urban_workflow_playground-0.1.0/PKG-INFO +63 -0
  2. urban_workflow_playground-0.1.0/README.md +44 -0
  3. urban_workflow_playground-0.1.0/pyproject.toml +37 -0
  4. urban_workflow_playground-0.1.0/setup.cfg +4 -0
  5. urban_workflow_playground-0.1.0/urban_workflow_playground/__init__.py +13 -0
  6. urban_workflow_playground-0.1.0/urban_workflow_playground/__main__.py +18 -0
  7. urban_workflow_playground-0.1.0/urban_workflow_playground/app.py +344 -0
  8. urban_workflow_playground-0.1.0/urban_workflow_playground/static/app.js +426 -0
  9. urban_workflow_playground-0.1.0/urban_workflow_playground/static/examples.js +119 -0
  10. urban_workflow_playground-0.1.0/urban_workflow_playground/static/index.html +87 -0
  11. urban_workflow_playground-0.1.0/urban_workflow_playground/static/style.css +211 -0
  12. urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/codemirror.css +344 -0
  13. urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/codemirror.js +9884 -0
  14. urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/lint.css +79 -0
  15. urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/lint.js +288 -0
  16. urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/show-hint.css +37 -0
  17. urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/show-hint.js +523 -0
  18. urban_workflow_playground-0.1.0/urban_workflow_playground/static/vendor/simple.js +216 -0
  19. urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/PKG-INFO +63 -0
  20. urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/SOURCES.txt +22 -0
  21. urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/dependency_links.txt +1 -0
  22. urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/entry_points.txt +2 -0
  23. urban_workflow_playground-0.1.0/urban_workflow_playground.egg-info/requires.txt +3 -0
  24. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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")