foxygit / RPA-Remisser Log in
commits tags

/rpa/blocks.py · 3.61 KB

raw
"""Block-bibliotek: återanvändbara flödesdelar i grafform
(`{name, description, nodes, edges, settings?}`). Medföljande block ligger i
repo-mappen `blocks/` (läs-bara), användarens egna i `data/blocks/`.
Spegel av rpa/flows.py. Gamla `{steps}`-block migreras vid inläsning.
"""

from __future__ import annotations

import json
import re
from pathlib import Path

from . import config
from .migrate import steps_to_graph
from .schema import validate_graph

NAME_RE = re.compile(r"^[a-zA-Z0-9_\-åäöÅÄÖ ]{1,80}$")


class InvalidBlockName(ValueError):
    pass


class BlockNotFound(FileNotFoundError):
    pass


class InvalidBlock(ValueError):
    def __init__(self, errors: list[str]):
        self.errors = errors
        super().__init__("Ogiltigt block:\n- " + "\n- ".join(errors))


def _check_name(name: str) -> str:
    if not NAME_RE.match(name or ""):
        raise InvalidBlockName(f"Ogiltigt blocknamn: {name!r}")
    return name


def _read(path: Path, builtin: bool) -> dict:
    data = json.loads(path.read_text(encoding="utf-8"))
    if data.get("nodes") is None and "steps" in data:
        g = steps_to_graph(data.get("steps", []))
        data = {**data, "nodes": g["nodes"], "edges": g["edges"], "start": g["start"]}
    return {
        "name": data.get("name") or path.stem,
        "description": data.get("description", ""),
        "nodes": data.get("nodes", []),
        "edges": data.get("edges", []),
        "start": data.get("start"),
        "settings": data.get("settings", {}),
        "builtin": builtin,
    }


def list_blocks() -> list[dict]:
    """Metadata för alla block (medföljande + användarens; användarens vinner vid
    namnkrock)."""
    out: dict[str, dict] = {}
    for d, builtin in ((config.BUILTIN_BLOCKS_DIR, True), (config.BLOCKS_DIR, False)):
        if not d.exists():
            continue
        for p in sorted(d.glob("*.json")):
            try:
                b = _read(p, builtin)
            except (json.JSONDecodeError, OSError):
                continue
            out[b["name"]] = {
                "name": b["name"],
                "description": b["description"],
                "step_count": len(b["nodes"]),
                "builtin": builtin,
            }
    return sorted(out.values(), key=lambda b: b["name"].lower())


def load_block(name: str) -> dict:
    _check_name(name)
    for d, builtin in ((config.BLOCKS_DIR, False), (config.BUILTIN_BLOCKS_DIR, True)):
        p = d / f"{name}.json"
        if p.exists():
            return _read(p, builtin)
    raise BlockNotFound(f"Blocket finns inte: {name}")


def save_block(name: str, block: dict) -> None:
    _check_name(name)
    if block.get("nodes") is None and "steps" in block:
        g = steps_to_graph(block.get("steps", []))
        block = {**block, "nodes": g["nodes"], "edges": g["edges"]}
    normalized = {
        "name": name,
        "description": str(block.get("description", "")),
        "nodes": block.get("nodes", []),
        "edges": block.get("edges", []),
        "settings": block.get("settings", {}),
    }
    # ett block är ett fragment - ingen krav på exakt en start
    errors = [
        e for e in validate_graph(normalized, strict=False)
        if "startnod" not in e.lower()
    ]
    if errors:
        raise InvalidBlock(errors)
    config.BLOCKS_DIR.mkdir(parents=True, exist_ok=True)
    (config.BLOCKS_DIR / f"{name}.json").write_text(
        json.dumps(normalized, ensure_ascii=False, indent=2), encoding="utf-8"
    )


def delete_block(name: str) -> bool:
    _check_name(name)
    p = config.BLOCKS_DIR / f"{name}.json"
    if p.exists():
        p.unlink()
        return True
    return False