v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
188
visual_workflow_builder/backend/services/serialization.py
Normal file
188
visual_workflow_builder/backend/services/serialization.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""backend/services/serialization.py
|
||||
|
||||
Persistance simple (JSON/YAML) pour les workflows.
|
||||
|
||||
Auteur : Dom, Alice, Kiro - 08 janvier 2026
|
||||
|
||||
Patch #1:
|
||||
- Fournit WorkflowDatabase + WorkflowSerializer utilisés par api/workflows.py
|
||||
- Stockage fichier: un workflow = un fichier <id>.json dans un répertoire
|
||||
|
||||
Design:
|
||||
- Permissif: on conserve les champs inconnus via models.py
|
||||
- Robuste: erreurs encapsulées, pas de crash au boot
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, List
|
||||
|
||||
import yaml # PyYAML
|
||||
|
||||
from models import VisualWorkflow, generate_id, WorkflowSettings
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class SerializationError(Exception):
|
||||
"""Erreur de sérialisation."""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""Erreur de validation pour les workflows sérialisés."""
|
||||
|
||||
def __init__(self, errors: List[str]):
|
||||
super().__init__(", ".join(errors))
|
||||
self.errors = errors
|
||||
|
||||
|
||||
def create_empty_workflow(name: str, description: str, created_by: str) -> VisualWorkflow:
|
||||
"""Crée un workflow vide avec les paramètres de base."""
|
||||
now = datetime.now()
|
||||
return VisualWorkflow(
|
||||
id=WorkflowSerializer.generate_workflow_id(),
|
||||
name=name,
|
||||
description=description,
|
||||
version="1.0.0",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
created_by=created_by,
|
||||
nodes=[],
|
||||
edges=[],
|
||||
variables=[],
|
||||
settings=WorkflowSettings(),
|
||||
)
|
||||
|
||||
|
||||
class WorkflowSerializer:
|
||||
"""Sérialise/désérialise les workflows vers/depuis JSON ou YAML."""
|
||||
|
||||
@staticmethod
|
||||
def generate_workflow_id() -> str:
|
||||
"""Génère un identifiant unique pour un workflow."""
|
||||
return generate_id("wf")
|
||||
|
||||
@staticmethod
|
||||
def serialize(workflow: VisualWorkflow, format: str = "json") -> str:
|
||||
"""Sérialise un workflow vers une chaîne JSON ou YAML."""
|
||||
try:
|
||||
data = workflow.to_dict()
|
||||
fmt = (format or "json").lower()
|
||||
if fmt == "json":
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
if fmt in ("yml", "yaml"):
|
||||
return yaml.safe_dump(data, allow_unicode=True, sort_keys=False)
|
||||
raise SerializationError(f"Format non supporté: {format}")
|
||||
except Exception as e:
|
||||
raise SerializationError(str(e)) from e
|
||||
|
||||
@staticmethod
|
||||
def deserialize(raw: Any, format: str = "json") -> VisualWorkflow:
|
||||
"""Désérialise un workflow depuis une chaîne JSON ou YAML."""
|
||||
try:
|
||||
fmt = (format or "json").lower()
|
||||
|
||||
if isinstance(raw, (bytes, bytearray)):
|
||||
raw = raw.decode("utf-8", errors="replace")
|
||||
|
||||
if isinstance(raw, str):
|
||||
if fmt == "json":
|
||||
data = json.loads(raw)
|
||||
elif fmt in ("yml", "yaml"):
|
||||
data = yaml.safe_load(raw)
|
||||
else:
|
||||
raise SerializationError(f"Format non supporté: {format}")
|
||||
elif isinstance(raw, dict):
|
||||
data = raw
|
||||
else:
|
||||
raise SerializationError("Type d'entrée invalide pour la désérialisation")
|
||||
|
||||
wf = VisualWorkflow.from_dict(data)
|
||||
errors = wf.validate()
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
return wf
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise SerializationError(str(e)) from e
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowDatabase:
|
||||
"""Stockage de workflows basé sur des fichiers."""
|
||||
|
||||
root_dir: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Crée le répertoire de stockage s'il n'existe pas."""
|
||||
os.makedirs(self.root_dir, exist_ok=True)
|
||||
|
||||
def _path(self, workflow_id: str) -> str:
|
||||
"""Retourne le chemin du fichier pour un workflow donné."""
|
||||
safe_id = "".join(c for c in workflow_id if c.isalnum() or c in ("_", "-")) or workflow_id
|
||||
return os.path.join(self.root_dir, f"{safe_id}.json")
|
||||
|
||||
def exists(self, workflow_id: str) -> bool:
|
||||
"""Vérifie si un workflow existe."""
|
||||
return os.path.exists(self._path(workflow_id))
|
||||
|
||||
def save(self, workflow: VisualWorkflow) -> None:
|
||||
"""Sauvegarde un workflow sur disque."""
|
||||
try:
|
||||
payload = WorkflowSerializer.serialize(workflow, format="json")
|
||||
with open(self._path(workflow.id), "w", encoding="utf-8") as f:
|
||||
f.write(payload)
|
||||
except Exception as e:
|
||||
raise SerializationError(f"Échec de la sauvegarde du workflow: {e}") from e
|
||||
|
||||
def load(self, workflow_id: str) -> Optional[VisualWorkflow]:
|
||||
"""Charge un workflow depuis le disque."""
|
||||
path = self._path(workflow_id)
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
return WorkflowSerializer.deserialize(raw, format="json")
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise SerializationError(f"Échec du chargement du workflow '{workflow_id}': {e}") from e
|
||||
|
||||
def delete(self, workflow_id: str) -> None:
|
||||
"""Supprime un workflow du disque."""
|
||||
path = self._path(workflow_id)
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
except Exception as e:
|
||||
raise SerializationError(f"Échec de la suppression du workflow '{workflow_id}': {e}") from e
|
||||
|
||||
def list(self) -> List[VisualWorkflow]:
|
||||
"""Liste tous les workflows disponibles.
|
||||
|
||||
Les fichiers invalides sont ignorés silencieusement pour éviter
|
||||
de bloquer le chargement de tous les workflows.
|
||||
"""
|
||||
workflows: List[VisualWorkflow] = []
|
||||
if not os.path.isdir(self.root_dir):
|
||||
return []
|
||||
for fname in os.listdir(self.root_dir):
|
||||
if not fname.endswith(".json"):
|
||||
continue
|
||||
wf_id = fname[:-5]
|
||||
try:
|
||||
wf = self.load(wf_id)
|
||||
if wf is not None:
|
||||
workflows.append(wf)
|
||||
except (SerializationError, ValidationError) as e:
|
||||
# Ignorer les fichiers invalides et continuer
|
||||
print(f"⚠️ Workflow ignoré '{wf_id}': {e}")
|
||||
except Exception as e:
|
||||
# Autres erreurs - les ignorer aussi pour ne pas bloquer
|
||||
print(f"⚠️ Erreur inattendue pour '{wf_id}': {e}")
|
||||
return workflows
|
||||
Reference in New Issue
Block a user