161 lines
5.1 KiB
Python
161 lines
5.1 KiB
Python
"""Module system — base class + discovery."""
|
|
from __future__ import annotations
|
|
import importlib.util, inspect, json, sys
|
|
from pathlib import Path
|
|
from flask import Blueprint
|
|
|
|
CORE = frozenset({
|
|
# Core feature IDs
|
|
"stacks", "app_store", "docker_images", "audit", "settings", "wizard",
|
|
"language", "i18n", "projects", "system_info", "updater", "git_browser",
|
|
"store", "dashboard", "docker", "core", "modules",
|
|
# Repo module IDs that are core features (not optional)
|
|
"audit_log", "audit-log",
|
|
"system-info", "system_info",
|
|
"git-browser", "git_browser",
|
|
})
|
|
|
|
|
|
class Module:
|
|
ID = ""
|
|
NAME = ""
|
|
ICON = "🧩"
|
|
DESC = ""
|
|
VER = "1.0.0"
|
|
|
|
# Old-style compat attributes
|
|
MODULE_ID = ""
|
|
MODULE_NAME = ""
|
|
MODULE_ICON = ""
|
|
MODULE_DESC = ""
|
|
|
|
def __init_subclass__(cls, **kw):
|
|
"""Sync old MODULE_* attrs to new ID/NAME/ICON/DESC."""
|
|
super().__init_subclass__(**kw)
|
|
# Old-style MODULE_ID → new ID (old takes priority if set)
|
|
if cls.__dict__.get("MODULE_ID"):
|
|
cls.ID = cls.MODULE_ID
|
|
if cls.__dict__.get("MODULE_NAME"):
|
|
cls.NAME = cls.MODULE_NAME
|
|
if cls.__dict__.get("MODULE_ICON"):
|
|
cls.ICON = cls.MODULE_ICON
|
|
if cls.__dict__.get("MODULE_DESC"):
|
|
cls.DESC = cls.MODULE_DESC
|
|
|
|
def blueprint(self) -> Blueprint | None:
|
|
return None
|
|
|
|
def pages(self) -> list[dict]:
|
|
return []
|
|
|
|
def on_load(self, app) -> None:
|
|
pass
|
|
|
|
def settings_html(self) -> str | None:
|
|
return None
|
|
|
|
def get_config(self, key=None, default=None):
|
|
"""Haal module-specifieke config op."""
|
|
import core as cfg
|
|
mc = cfg.load().get("MODULE_SETTINGS", {}).get(self.ID, {})
|
|
if key is None:
|
|
return mc
|
|
return mc.get(key, default)
|
|
|
|
def save_config(self, updates: dict):
|
|
"""Sla module-specifieke config op."""
|
|
import core as cfg
|
|
c = cfg.load()
|
|
ms = dict(c.get("MODULE_SETTINGS", {}))
|
|
mc = dict(ms.get(self.ID, {}))
|
|
mc.update(updates)
|
|
ms[self.ID] = mc
|
|
cfg.patch({"MODULE_SETTINGS": ms})
|
|
|
|
def info(self) -> dict:
|
|
return {"id": self.ID, "name": self.NAME, "icon": self.ICON,
|
|
"desc": self.DESC, "version": self.VER,
|
|
"pages": self.pages(), "core": self.ID in CORE}
|
|
|
|
|
|
def discover(dirs: list[Path]) -> list[tuple[str, type, Path]]:
|
|
found = []
|
|
seen = set()
|
|
for base in dirs:
|
|
if not base.exists():
|
|
continue
|
|
for d in sorted(base.iterdir()):
|
|
if not d.is_dir() or d.name.startswith("_"):
|
|
continue
|
|
if not (d / "__init__.py").exists():
|
|
continue
|
|
# Vereist module.json
|
|
if not (d / "module.json").exists():
|
|
continue
|
|
mid = d.name
|
|
if mid in seen:
|
|
continue
|
|
# Skip CORE modules (dir name check)
|
|
if mid in CORE:
|
|
continue
|
|
seen.add(mid)
|
|
meta = _meta(d)
|
|
# Skip CORE modules (meta ID check)
|
|
meta_id = meta.get("id", mid)
|
|
if meta_id in CORE:
|
|
continue
|
|
if meta.get("enabled") is False:
|
|
continue
|
|
cls = _load(mid, d)
|
|
if cls:
|
|
# Final check: skip if the class ID is CORE
|
|
try:
|
|
inst = cls()
|
|
if inst.ID in CORE:
|
|
continue
|
|
except Exception:
|
|
pass
|
|
found.append((meta.get("order", 50), mid, cls, d.resolve()))
|
|
found.sort(key=lambda x: x[0])
|
|
return [(m, c, p) for _, m, c, p in found]
|
|
|
|
|
|
def _meta(d: Path) -> dict:
|
|
f = d / "module.json"
|
|
if f.exists():
|
|
try:
|
|
return json.loads(f.read_text())
|
|
except Exception:
|
|
pass
|
|
return {}
|
|
|
|
|
|
def _load(mid: str, d: Path) -> type | None:
|
|
cls, _ = _load_detail(mid, d)
|
|
return cls
|
|
|
|
|
|
def _load_detail(mid: str, d: Path) -> tuple[type | None, str]:
|
|
"""Load module class, return (cls, error_msg)."""
|
|
try:
|
|
name = f"_mod_{mid}"
|
|
init_file = d / "__init__.py"
|
|
if not init_file.exists():
|
|
return None, f"__init__.py niet gevonden in {d}"
|
|
spec = importlib.util.spec_from_file_location(
|
|
name, str(init_file),
|
|
submodule_search_locations=[str(d)])
|
|
if not spec or not spec.loader:
|
|
return None, f"kon spec niet laden voor {init_file}"
|
|
pkg = importlib.util.module_from_spec(spec)
|
|
sys.modules[name] = pkg
|
|
spec.loader.exec_module(pkg)
|
|
for _, obj in inspect.getmembers(pkg, inspect.isclass):
|
|
if obj is not Module and issubclass(obj, Module) and obj.__module__ == name:
|
|
return obj, ""
|
|
# Toon welke classes er WEL zijn
|
|
classes = [n for n, o in inspect.getmembers(pkg, inspect.isclass) if o.__module__ == name]
|
|
return None, f"geen Module subclass gevonden. Classes: {classes or 'geen'}"
|
|
except Exception as e:
|
|
print(f" ✖ {mid}: {e}")
|
|
return None, f"{type(e).__name__}: {e}"
|