From 6bac9d6dd753835c05e8c3e6a7a95e85797643de Mon Sep 17 00:00:00 2001 From: JulianKimmig Date: Mon, 5 Jan 2026 17:12:08 +0100 Subject: [PATCH 1/6] docs: add CONTRIBUTING.md to outline development setup and guidelines - Created a new CONTRIBUTING.md file to provide instructions for setting up the development environment, running tests, and adhering to code style. - Included prerequisites, recommended environment variables, and commands for installing dependencies and running tests. - Outlined expectations for test-driven development and usage of pre-commit hooks. --- CONTRIBUTING.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4a1bbb8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# Contributing to funcnodes-worker + +This repository contains the **worker runtime** used to execute FuncNodes graphs (WebSocket RPC server, loops, worker config). + +## Development setup (Python) + +Prereqs: + +- Python **3.11+** +- `uv` (https://github.com/astral-sh/uv) + +Recommended environment variables (keep caches/config local): + +- `UV_CACHE_DIR=.cache/uv` +- `FUNCNODES_CONFIG_DIR=.funcnodes` + +Install dev dependencies: + +```bash +cd funcnodes_worker +UV_CACHE_DIR=.cache/uv uv sync --group dev +``` + +Run unit tests: + +```bash +cd funcnodes_worker +FUNCNODES_CONFIG_DIR=.funcnodes UV_CACHE_DIR=.cache/uv uv run pytest +``` + +## Full test matrix (tox) + +`funcnodes-worker` has optional extras (`venv`, `http`, `subprocess-monitor`) and a tox matrix to test combinations. + +```bash +cd funcnodes_worker +FUNCNODES_CONFIG_DIR=.funcnodes UV_CACHE_DIR=.cache/uv uv run tox +``` + +## Code style & hooks + +Run pre-commit: + +```bash +cd funcnodes_worker +UV_CACHE_DIR=.cache/uv uv run pre-commit install +UV_CACHE_DIR=.cache/uv uv run pre-commit run -a +``` + +## TDD expectations + +- Write tests first; cover edge cases. +- Avoid mocks unless simulating external resources. From 598f409560cc6b1420723d3a4df62dfcee2c169d Mon Sep 17 00:00:00 2001 From: Julian Kimmig Date: Thu, 15 Jan 2026 18:48:23 +0100 Subject: [PATCH 2/6] docs(readme): add worker overview and usage Document purpose, features, installation, dependencies, and a basic WSWorker instantiation example. --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/README.md b/README.md index e69de29..95ad281 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,45 @@ +# FuncNodes Worker + +**FuncNodes Worker** (`funcnodes_worker`) is the execution engine for the [FuncNodes](https://github.com/Linkdlab/FuncNodes) ecosystem. It provides the runtime environment where nodes are executed, managed, and controlled. + +> [!NOTE] +> For the full application usage, please refer to the main [FuncNodes repository](https://github.com/Linkdlab/FuncNodes). + +## Key Features + +- **Execution Runtime**: Manages the `NodeSpace` and the main event loop, ensuring efficient asynchronous execution of nodes. +- **Remote Control**: + - **WebSocket Worker**: Allows remote management and frontend interaction via `aiohttp` WebSockets. + - **Message Queue Worker**: Supports communication via multiprocessing queues for inter-process coordination. +- **Process Management**: Handles state persistence, heartbeats, and safe shutdown procedures. +- **Extensibility**: Supports "External Workers" to offload tasks to auxiliary processes or environments. + +## Installation + +```bash +pip install funcnodes-worker +``` + +## Dependencies + +- **funcnodes-core**: The core logic definitions. +- **aiohttp**: For WebSocket communication (optional but recommended). +- **pydantic**: For configuration and data validation. + +## Usage + +In most cases, `funcnodes-worker` is used internally by the `funcnodes` main package. However, you can instantiate a worker programmatically if needed: + +```python +from funcnodes_worker import WSWorker + +# Create a worker that listens on a specific host and port +worker = WSWorker(host="localhost", port=9382) + +# Start the worker loop (usually handled by an async runner) +# await worker.run() +``` + +## Documentation + +For comprehensive documentation, visit the [FuncNodes Documentation](https://linkdlab.github.io/FuncNodes). From 1a35485a8d341e437c93e2b402ad860b6515f38b Mon Sep 17 00:00:00 2001 From: JulianKimmig Date: Wed, 6 May 2026 10:59:56 +0200 Subject: [PATCH 3/6] feat(worker): add autostart configuration option with default value - Introduced an `autostart` boolean field in the WorkerJson TypedDict to manage worker startup behavior. - Updated the Worker class to initialize `autostart` to False if not provided in the configuration. - Enhanced tests to verify the default behavior of the `autostart` field when missing from the configuration. --- src/funcnodes_worker/worker.py | 5 +++++ tests/test_worker.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/funcnodes_worker/worker.py b/src/funcnodes_worker/worker.py index 8f38ba9..c3c51cc 100644 --- a/src/funcnodes_worker/worker.py +++ b/src/funcnodes_worker/worker.py @@ -548,6 +548,7 @@ class WorkerJson(TypedDict): data_path: Optional[str] env_path: Optional[str] pid: Optional[int] + autostart: bool # shelves_dependencies: Dict[str, ShelfDict] worker_dependencies: Dict[str, WorkerDict] @@ -770,6 +771,7 @@ def generate_config(self) -> WorkerJson: worker_dependencies=worker_dependencies, package_dependencies=self._package_dependencies.copy(), pid=os.getpid(), + autostart=False, update_on_startup={}, ) ) @@ -785,6 +787,9 @@ def update_config(self, conf: WorkerJson) -> WorkerJson: if "update_on_startup" not in conf: conf["update_on_startup"] = {} # pragma: no cover + if "autostart" not in conf: + conf["autostart"] = False + if "funcnodes" not in conf["update_on_startup"]: conf["update_on_startup"]["funcnodes"] = True # pragma: no cover if "funcnodes-core" not in conf["update_on_startup"]: diff --git a/tests/test_worker.py b/tests/test_worker.py index 848fb1d..41dc702 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -249,6 +249,7 @@ async def test_worker_case_config_generation(worker_case): "pid": os.getpid(), "type": worker_case.__class__.__name__, "env_path": None, + "autostart": False, "update_on_startup": { "funcnodes": True, "funcnodes-core": True, @@ -266,6 +267,7 @@ async def test_worker_case_exportable_config(worker_case): "name": worker_case.name(), "package_dependencies": {}, "type": worker_case.__class__.__name__, + "autostart": False, "update_on_startup": { "funcnodes": True, "funcnodes-core": True, @@ -291,6 +293,16 @@ async def test_worker_case_load_config(worker_case): assert config["uuid"] == worker_case.uuid() +@funcnodes_test +async def test_worker_case_missing_autostart_defaults_false(worker_case): + config = worker_case.config + config.pop("autostart", None) + + updated = worker_case.update_config(config) + + assert updated["autostart"] is False + + @funcnodes_test async def test_worker_case_process_file_handling(worker_case): worker_case._write_process_file() From 02b6a51f81c872b2f9b7314339403f1a06e9068a Mon Sep 17 00:00:00 2001 From: JulianKimmig Date: Wed, 6 May 2026 12:17:52 +0200 Subject: [PATCH 4/6] feat(worker): add methods to get and update worker configuration - Introduced `get_config` method to retrieve the current worker configuration. - Added `update_worker_config` method to allow updating of worker properties such as name, autostart, and update_on_startup. - Enhanced tests to validate the functionality of the new configuration methods, ensuring correct updates and retrieval of worker settings. --- src/funcnodes_worker/worker.py | 32 ++++++++++++++++++++++++++++++++ tests/test_worker.py | 20 ++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/funcnodes_worker/worker.py b/src/funcnodes_worker/worker.py index c3c51cc..f2c0ba1 100644 --- a/src/funcnodes_worker/worker.py +++ b/src/funcnodes_worker/worker.py @@ -980,6 +980,38 @@ def export_worker(self, with_files: bool = True) -> bytes: return zip_bytes + @exposed_method() + def get_config(self) -> WorkerJson: + """returns the current worker configuration""" + return self.update_config(self.config) + + @exposed_method() + def update_worker_config( + self, + name: Optional[str] = None, + autostart: Optional[bool] = None, + update_on_startup: Optional[PossibleUpdates] = None, + ) -> WorkerJson: + """updates editable worker configuration values""" + config = self.update_config(self.config) + + if name is not None: + stripped_name = name.strip() + self._name = stripped_name or None + config["name"] = self._name + + if autostart is not None: + config["autostart"] = bool(autostart) + + if update_on_startup is not None: + current_update_on_startup = config.get("update_on_startup", {}) + current_update_on_startup.update( + {key: bool(value) for key, value in update_on_startup.items()} + ) + config["update_on_startup"] = current_update_on_startup + + return self.write_config(config) + @exposed_method() async def update( self, diff --git a/tests/test_worker.py b/tests/test_worker.py index 41dc702..8966bc5 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -303,6 +303,26 @@ async def test_worker_case_missing_autostart_defaults_false(worker_case): assert updated["autostart"] is False +@funcnodes_test +async def test_worker_case_update_worker_config(worker_case): + updated = worker_case.update_worker_config( + name="renamed worker", + autostart=True, + update_on_startup={"funcnodes": False}, + ) + + assert updated["name"] == "renamed worker" + assert updated["autostart"] is True + assert updated["update_on_startup"]["funcnodes"] is False + assert updated["update_on_startup"]["funcnodes-core"] is True + assert worker_case.name() == "renamed worker" + + loaded = worker_case.load_config() + assert loaded is not None + assert loaded["name"] == "renamed worker" + assert loaded["autostart"] is True + + @funcnodes_test async def test_worker_case_process_file_handling(worker_case): worker_case._write_process_file() From b0eba8748d7c7527384ddbc6b8d3837028901c98 Mon Sep 17 00:00:00 2001 From: JulianKimmig Date: Wed, 6 May 2026 13:46:47 +0200 Subject: [PATCH 5/6] feat(worker): enhance autostart configuration with new policy options - Introduced `AutostartPolicy` type and `normalize_autostart_policy` function to manage autostart behavior. - Updated `WorkerJson` to use `AutostartPolicy` instead of a boolean for the `autostart` field. - Modified worker initialization and configuration methods to support new autostart policies. - Updated tests to reflect changes in autostart handling, ensuring correct defaults and migration from boolean values. --- src/funcnodes_worker/worker.py | 34 ++++++++++++++++++++++++++++------ tests/test_worker.py | 23 ++++++++++++++++------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/funcnodes_worker/worker.py b/src/funcnodes_worker/worker.py index f2c0ba1..126516c 100644 --- a/src/funcnodes_worker/worker.py +++ b/src/funcnodes_worker/worker.py @@ -541,6 +541,29 @@ class PossibleUpdates(TypedDict, total=False): funcnodes: bool +AutostartPolicy = Literal["never", "always", "unless-stopped"] +AUTOSTART_POLICIES: tuple[AutostartPolicy, ...] = ( + "never", + "always", + "unless-stopped", +) + + +def normalize_autostart_policy(value: object = None) -> AutostartPolicy: + if value is True: + return "unless-stopped" + if value in (False, None): + return "never" + if isinstance(value, str): + if value in AUTOSTART_POLICIES: + return cast(AutostartPolicy, value) + raise ValueError( + "Invalid autostart policy " + f"{value!r}. Expected one of {', '.join(AUTOSTART_POLICIES)}." + ) + raise ValueError(f"Invalid autostart policy {value!r}.") + + class WorkerJson(TypedDict): type: str uuid: str @@ -548,7 +571,7 @@ class WorkerJson(TypedDict): data_path: Optional[str] env_path: Optional[str] pid: Optional[int] - autostart: bool + autostart: AutostartPolicy # shelves_dependencies: Dict[str, ShelfDict] worker_dependencies: Dict[str, WorkerDict] @@ -771,7 +794,7 @@ def generate_config(self) -> WorkerJson: worker_dependencies=worker_dependencies, package_dependencies=self._package_dependencies.copy(), pid=os.getpid(), - autostart=False, + autostart="never", update_on_startup={}, ) ) @@ -787,8 +810,7 @@ def update_config(self, conf: WorkerJson) -> WorkerJson: if "update_on_startup" not in conf: conf["update_on_startup"] = {} # pragma: no cover - if "autostart" not in conf: - conf["autostart"] = False + conf["autostart"] = normalize_autostart_policy(conf.get("autostart")) if "funcnodes" not in conf["update_on_startup"]: conf["update_on_startup"]["funcnodes"] = True # pragma: no cover @@ -989,7 +1011,7 @@ def get_config(self) -> WorkerJson: def update_worker_config( self, name: Optional[str] = None, - autostart: Optional[bool] = None, + autostart: Optional[AutostartPolicy | bool] = None, update_on_startup: Optional[PossibleUpdates] = None, ) -> WorkerJson: """updates editable worker configuration values""" @@ -1001,7 +1023,7 @@ def update_worker_config( config["name"] = self._name if autostart is not None: - config["autostart"] = bool(autostart) + config["autostart"] = normalize_autostart_policy(autostart) if update_on_startup is not None: current_update_on_startup = config.get("update_on_startup", {}) diff --git a/tests/test_worker.py b/tests/test_worker.py index 8966bc5..b33b6ec 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -249,7 +249,7 @@ async def test_worker_case_config_generation(worker_case): "pid": os.getpid(), "type": worker_case.__class__.__name__, "env_path": None, - "autostart": False, + "autostart": "never", "update_on_startup": { "funcnodes": True, "funcnodes-core": True, @@ -267,7 +267,7 @@ async def test_worker_case_exportable_config(worker_case): "name": worker_case.name(), "package_dependencies": {}, "type": worker_case.__class__.__name__, - "autostart": False, + "autostart": "never", "update_on_startup": { "funcnodes": True, "funcnodes-core": True, @@ -294,25 +294,34 @@ async def test_worker_case_load_config(worker_case): @funcnodes_test -async def test_worker_case_missing_autostart_defaults_false(worker_case): +async def test_worker_case_missing_autostart_defaults_never(worker_case): config = worker_case.config config.pop("autostart", None) updated = worker_case.update_config(config) - assert updated["autostart"] is False + assert updated["autostart"] == "never" + + +@funcnodes_test +async def test_worker_case_legacy_autostart_booleans_migrate(worker_case): + true_config = worker_case.update_config({**worker_case.config, "autostart": True}) + false_config = worker_case.update_config({**worker_case.config, "autostart": False}) + + assert true_config["autostart"] == "unless-stopped" + assert false_config["autostart"] == "never" @funcnodes_test async def test_worker_case_update_worker_config(worker_case): updated = worker_case.update_worker_config( name="renamed worker", - autostart=True, + autostart="unless-stopped", update_on_startup={"funcnodes": False}, ) assert updated["name"] == "renamed worker" - assert updated["autostart"] is True + assert updated["autostart"] == "unless-stopped" assert updated["update_on_startup"]["funcnodes"] is False assert updated["update_on_startup"]["funcnodes-core"] is True assert worker_case.name() == "renamed worker" @@ -320,7 +329,7 @@ async def test_worker_case_update_worker_config(worker_case): loaded = worker_case.load_config() assert loaded is not None assert loaded["name"] == "renamed worker" - assert loaded["autostart"] is True + assert loaded["autostart"] == "unless-stopped" @funcnodes_test From 15612a20b27142019dc93aea2ff1b80d3f4cdb6b Mon Sep 17 00:00:00 2001 From: Julian Kimmig Date: Wed, 6 May 2026 14:25:16 +0200 Subject: [PATCH 6/6] =?UTF-8?q?bump:=20version=201.5.1=20=E2=86=92=201.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aea375..cbc8d5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.6.0 (2026-05-06) + +### Feat + +- **worker**: enhance autostart configuration with new policy options +- **worker**: add methods to get and update worker configuration +- **worker**: add autostart configuration option with default value + ## 1.5.1 (2025-12-18) ### Refactor diff --git a/pyproject.toml b/pyproject.toml index ae49979..af0f2db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "funcnodes-worker" -version = "1.5.1" +version = "1.6.0" description = "Worker package for FuncNodes" readme = "README.md" authors = [{name = "Julian Kimmig", email = "julian.kimmig@linkdlab.de"}] diff --git a/uv.lock b/uv.lock index e922524..6a6cdba 100644 --- a/uv.lock +++ b/uv.lock @@ -473,7 +473,7 @@ wheels = [ [[package]] name = "funcnodes-worker" -version = "1.5.1" +version = "1.6.0" source = { editable = "." } dependencies = [ { name = "asynctoolkit" },