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