()
| 650 | |
| 651 | # Plugin loader & runner |
| 652 | def load_plugins(): |
| 653 | plugins = [] |
| 654 | if not os.path.isdir(PLUGINS_DIR): |
| 655 | return plugins |
| 656 | for fname in sorted(os.listdir(PLUGINS_DIR)): |
| 657 | if not fname.endswith(".py"): |
| 658 | continue |
| 659 | full = os.path.join(PLUGINS_DIR, fname) |
| 660 | try: |
| 661 | ns = {} |
| 662 | with open(full, "r", encoding="utf-8") as f: |
| 663 | code = f.read() |
| 664 | exec(compile(code, full, "exec"), ns) |
| 665 | if "run" in ns and callable(ns["run"]): |
| 666 | plugins.append({"name": fname, "run": ns["run"]}) |
| 667 | log(f"Loaded plugin: {fname}") |
| 668 | else: |
| 669 | log(f"Plugin {fname} has no run(data) function; skipped", "WARN") |
| 670 | except Exception as e: |
| 671 | log(f"Failed loading plugin {fname}: {e}", "ERROR") |
| 672 | return plugins |
| 673 | |
| 674 | |
| 675 | def run_plugins(plugins, data): |
no test coverage detected