Files
rpa_vision_v3/visual_workflow_builder/backend/services/serialization.py
Dom a27b74cf22 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>
2026-01-29 11:23:51 +01:00

188 lines
6.4 KiB
Python

"""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