commit e27e1d92ee131576d0a8541dfc7fd6c4eb16f2b8 Author: Magnus Küderli Date: Tue Sep 1 12:26:36 2026 +0200 initial commit. Shared module interface which can be implemented by modules. Those modules are placed in an folder structure which allows the main app to automatically detect and load them diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90d8394 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Byte-compiled / cache +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environments +venv/ +.venv/ +env/ +ENV/ + +# Distribution / packaging +build/ +dist/ +*.egg-info/ +*.egg + +# Environment / secrets +.env +.env.* + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Flask +instance/ +.webassets-cache + +# Logs +*.log + +# Local storage (your module data — probably don't want to commit this) +storage/ diff --git a/base/__init__.py b/base/__init__.py new file mode 100644 index 0000000..1dca809 --- /dev/null +++ b/base/__init__.py @@ -0,0 +1 @@ +from .basemodule import BaseModule diff --git a/base/basemodule.py b/base/basemodule.py new file mode 100644 index 0000000..6cf5aa7 --- /dev/null +++ b/base/basemodule.py @@ -0,0 +1,20 @@ +from flask import jsonify, render_template, Blueprint, app +from abc import abstractmethod, ABC + +class BaseModule(ABC): + def __init__(self, app: App, name: str, faIcon: str, blueprint: Blueprint): + self.name: str = name + self.faIcon = faIcon + self.blueprint: Blueprint = blueprint + self.blueprint.add_url_rule("/page", view_func=self.get_page) + app.register_blueprint(self.blueprint, url_prefix=f"/modules/{name}") + + @abstractmethod + def get_page(self): + # return render template with modules html and css + pass + + @abstractmethod + def sync(self): + # return list of errors (parsing errors etc) + pass diff --git a/base/registry.py b/base/registry.py new file mode 100644 index 0000000..962750f --- /dev/null +++ b/base/registry.py @@ -0,0 +1,36 @@ +from .basemodule import BaseModule +import importlib +import pkgutil +from pathlib import Path + +# written by ai: +def discover_modules(app: Flask, modules_package: str = "modules") -> list[BaseModule]: + """ + Scans the modules/ directory, imports each submodule's module.py, + instantiates its MODULE_CLASS, and returns the list of live instances. + """ + registry: list[BaseModule] = [] + + package = importlib.import_module(modules_package) + package_path = Path(package.__file__).parent + + for _, name, is_pkg in pkgutil.iter_modules([str(package_path)]): + print(name, is_pkg) + if not is_pkg: + continue # skip stray files, only real module folders count + + mod_path = f"{modules_package}.{name}.module" + try: + mod = importlib.import_module(mod_path) + except ModuleNotFoundError: + continue # folder without a module.py -> skip silently, or log a warning + + cls = getattr(mod, "MODULE_CLASS", None) + if cls is None: + continue # convention not followed — skip, or raise if you want it strict + + instance = cls(app=app) + registry.append(instance) + + return registry + diff --git a/main.py b/main.py new file mode 100644 index 0000000..77c2f47 --- /dev/null +++ b/main.py @@ -0,0 +1,13 @@ +from flask import Flask, render_template +from base.registry import discover_modules + +app = Flask(__name__) +modules = discover_modules(app) +print(modules) + +@app.route("/") +def main_page(): + return render_template() + +if __name__ == "__main__": + app.run(debug=True) diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/testmodule/__init__.py b/modules/testmodule/__init__.py new file mode 100644 index 0000000..1b0b101 --- /dev/null +++ b/modules/testmodule/__init__.py @@ -0,0 +1 @@ +from .module import TestModule diff --git a/modules/testmodule/module.py b/modules/testmodule/module.py new file mode 100644 index 0000000..ef7c5cb --- /dev/null +++ b/modules/testmodule/module.py @@ -0,0 +1,29 @@ +from typing import override +from flask import Blueprint, render_template, jsonify, app +from base.basemodule import BaseModule + +class TestModule(BaseModule): + def __init__(self, app: app): + name = "testmodule" + faIcon = "fa-vial" + blueprint = Blueprint( + "testmodule", __name__, + template_folder="templates", + static_folder="static" + ) + blueprint.add_url_rule("/api/data", view_func=self.status_api) + + super().__init__(app=app, name=name, faIcon=faIcon, blueprint=blueprint) + + def status_api(self): + return jsonify({"status": "ok", "value": 42}) + + @override + def get_page(self): + return render_template("_page.html") + + @override + def sync(self): + print("syncing") + +MODULE_CLASS = TestModule diff --git a/modules/testmodule/templates/_fragment.js b/modules/testmodule/templates/_fragment.js new file mode 100644 index 0000000..a94e752 --- /dev/null +++ b/modules/testmodule/templates/_fragment.js @@ -0,0 +1,7 @@ +document.getElementById("testmodule-refresh").addEventListener("click", async () => { + const res = await fetch("/api/testmodule/data"); + const data = await res.json(); + + document.getElementById("testmodule-output").textContent = JSON.stringify(data, null, 2); + document.getElementById("testmodule-synced").textContent = new Date().toLocaleTimeString(); +}); diff --git a/modules/testmodule/templates/_page.html b/modules/testmodule/templates/_page.html new file mode 100644 index 0000000..7ab8dc3 --- /dev/null +++ b/modules/testmodule/templates/_page.html @@ -0,0 +1,8 @@ +
+

Test Module

+

Last synced: never

+ +
+
+ +