From 4ac4c38b379609a8939ff729b750aaef6c100309 Mon Sep 17 00:00:00 2001 From: JulianKimmig Date: Mon, 12 Jan 2026 13:34:41 +0100 Subject: [PATCH 1/4] feat(input): add json input node Add JSON input node with tests covering valid and invalid payloads. --- src/funcnodes_basic/input.py | 14 ++++++++++++++ tests/test_inputs.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/funcnodes_basic/input.py b/src/funcnodes_basic/input.py index d713bf4..c7fbd13 100644 --- a/src/funcnodes_basic/input.py +++ b/src/funcnodes_basic/input.py @@ -1,5 +1,6 @@ from typing import Union import funcnodes_core as fn +import json @fn.NodeDecorator( @@ -14,6 +15,18 @@ def any_input(input: Union[str, float, int, bool]) -> str: return input +@fn.NodeDecorator( + node_id="input.json", + node_name="JSON Input", + description="Input a JSON object", + outputs=[ + {"name": "json"}, + ], +) +def json_input(input: str) -> Union[str, int, float, bool, dict]: + return json.loads(input) + + @fn.NodeDecorator( node_id="input.str", node_name="String Input", @@ -73,6 +86,7 @@ def bool_input(input: bool) -> bool: NODE_SHELF = fn.Shelf( nodes=[ + json_input, any_input, str_input, int_input, diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 98e7d58..aea8414 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -121,3 +121,33 @@ async def test_bool_input(): node.inputs["input"].value = "False" await node assert node.outputs["boolean"].value is False + + +@pytest_funcnodes.nodetest(input.json_input) +async def test_json_input_basic(): + node = input.json_input() + + node.inputs["input"].value = '{"a": 1}' + await node + assert node.outputs["json"].value == {"a": 1} + + node.inputs["input"].value = '["a", 1]' + await node + assert node.outputs["json"].value == ["a", 1] + + node.inputs["input"].value = "true" + await node + assert node.outputs["json"].value is True + + node.inputs["input"].value = "null" + await node + assert node.outputs["json"].value is None + + +@pytest_funcnodes.nodetest(input.json_input) +async def test_json_input_invalid(): + node = input.json_input() + + node.inputs["input"].value = "{" + with pytest.raises(fn.NodeTriggerError): + await node From 79b811a72a82d13e3bfb45c02da3d8e1f73ebe34 Mon Sep 17 00:00:00 2001 From: JulianKimmig Date: Mon, 12 Jan 2026 13:35:32 +0100 Subject: [PATCH 2/4] feat(dicts): add nodes to set missing or existing keys Add dict set-default and set-key nodes with tests. --- commit_message.md | 3 ++ src/funcnodes_basic/dicts.py | 23 +++++++++++++++ tests/test_dict.py | 56 ++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 commit_message.md diff --git a/commit_message.md b/commit_message.md new file mode 100644 index 0000000..9fc9b72 --- /dev/null +++ b/commit_message.md @@ -0,0 +1,3 @@ +feat(input): add json input node + +Add JSON input node with tests covering valid and invalid payloads. diff --git a/src/funcnodes_basic/dicts.py b/src/funcnodes_basic/dicts.py index 604aeb8..65b43c9 100644 --- a/src/funcnodes_basic/dicts.py +++ b/src/funcnodes_basic/dicts.py @@ -96,6 +96,27 @@ def dict_to_list(dictionary: dict) -> Tuple[List[Any], List[Any]]: return list(keys), list(values) +@fn.NodeDecorator( + id="dict_set_default", + name="Dict Set Default", +) +def dict_set_default(dictionary: dict, key: Any, value: Any) -> dict: + result = dict(dictionary) + if key not in result: + result[key] = value + return result + + +@fn.NodeDecorator( + id="dict_set_key", + name="Dict Set Key", +) +def dict_set_key(dictionary: dict, key: Any, value: Any) -> dict: + result = dict(dictionary) + result[key] = value + return result + + NODE_SHELF = fn.Shelf( nodes=[ DictGetNode, @@ -105,6 +126,8 @@ def dict_to_list(dictionary: dict) -> Tuple[List[Any], List[Any]]: dict_from_items, dict_from_keys_values, dict_to_list, + dict_set_default, + dict_set_key, ], name="Dicts", description="Work with dictionaries", diff --git a/tests/test_dict.py b/tests/test_dict.py index 68a61d4..1f5b9a0 100644 --- a/tests/test_dict.py +++ b/tests/test_dict.py @@ -99,3 +99,59 @@ async def test_dict_to_list(): assert node.outputs["keys"].value == ["a", "b", "c"] assert node.outputs["values"].value == [1, 2, 3] + + +@pytest_funcnodes.nodetest(dicts.dict_set_default) +async def test_dict_set_default_missing_key(): + testdict = {"a": 1} + + node = dicts.dict_set_default() + + node.inputs["dictionary"].value = testdict + node.inputs["key"].value = "b" + node.inputs["value"].value = 2 + await node + + assert node.outputs["out"].value == {"a": 1, "b": 2} + + +@pytest_funcnodes.nodetest(dicts.dict_set_default) +async def test_dict_set_default_existing_key(): + testdict = {"a": 1} + + node = dicts.dict_set_default() + + node.inputs["dictionary"].value = testdict + node.inputs["key"].value = "a" + node.inputs["value"].value = 2 + await node + + assert node.outputs["out"].value == {"a": 1} + + +@pytest_funcnodes.nodetest(dicts.dict_set_key) +async def test_dict_set_key_overwrite(): + testdict = {"a": 1} + + node = dicts.dict_set_key() + + node.inputs["dictionary"].value = testdict + node.inputs["key"].value = "a" + node.inputs["value"].value = 2 + await node + + assert node.outputs["out"].value == {"a": 2} + + +@pytest_funcnodes.nodetest(dicts.dict_set_key) +async def test_dict_set_key_new_key(): + testdict = {"a": 1} + + node = dicts.dict_set_key() + + node.inputs["dictionary"].value = testdict + node.inputs["key"].value = "b" + node.inputs["value"].value = 2 + await node + + assert node.outputs["out"].value == {"a": 1, "b": 2} From 3f197c011e3b31ada10925f81329544c91d3dcdc Mon Sep 17 00:00:00 2001 From: JulianKimmig Date: Mon, 12 Jan 2026 14:11:14 +0100 Subject: [PATCH 3/4] feat(basic): add dict utilities and list/json helpers Add dict helpers for deep get/set, merge, key selection/rename, and pop/delete with dynamic key value options. Add json_dump plus list flatten/unique nodes with tests. --- src/funcnodes_basic/dicts.py | 206 +++++++++++++++++++++++++++++++++- src/funcnodes_basic/input.py | 18 ++- src/funcnodes_basic/lists.py | 40 +++++++ tests/test_dict.py | 209 +++++++++++++++++++++++++++++++++++ tests/test_inputs.py | 25 +++++ tests/test_lists.py | 24 ++++ 6 files changed, 519 insertions(+), 3 deletions(-) diff --git a/src/funcnodes_basic/dicts.py b/src/funcnodes_basic/dicts.py index 65b43c9..8bf55ca 100644 --- a/src/funcnodes_basic/dicts.py +++ b/src/funcnodes_basic/dicts.py @@ -3,7 +3,7 @@ """ import funcnodes_core as fn -from typing import Any, List, Tuple +from typing import Any, List, Tuple, Annotated, Literal class DictGetNode(fn.Node): @@ -117,6 +117,203 @@ def dict_set_key(dictionary: dict, key: Any, value: Any) -> dict: return result +@fn.NodeDecorator( + id="dict_merge", + name="Dict Merge", +) +def dict_merge( + a: dict, + b: dict, + prefer: Annotated[Literal["a", "b"], fn.InputMeta(hidden=True)] = "b", +) -> dict: + if prefer == "a": + return {**b, **a} + return {**a, **b} + + +@fn.NodeDecorator( + id="dict_select_keys", + name="Dict Select Keys", +) +def dict_select_keys( + dictionary: Annotated[ + dict, + fn.InputMeta( + on={ + "after_set_value": fn.decorator.update_other_io_options( + "keys", lambda x: list(x.keys()) + ) + } + ), + ], + keys: List[Any], + ignore_missing: bool = True, +) -> dict: + selected: dict = {} + for key in keys: + if key in dictionary: + selected[key] = dictionary[key] + elif not ignore_missing: + raise KeyError(key) + return selected + + +@fn.NodeDecorator( + id="dict_rename_key", + name="Dict Rename Key", +) +def dict_rename_key( + dictionary: Annotated[ + dict, + fn.InputMeta( + on={ + "after_set_value": fn.decorator.update_other_io_options( + "old_key", lambda x: list(x.keys()) + ) + } + ), + ], + old_key: Any, + new_key: Any, + overwrite: bool = False, +) -> dict: + if old_key == new_key: + return dict(dictionary) + + result = dict(dictionary) + if old_key not in result: + raise KeyError(old_key) + if new_key in result and not overwrite: + raise KeyError(new_key) + + result[new_key] = result.pop(old_key) + return result + + +@fn.NodeDecorator( + id="dict_delete_key", + name="Dict Delete Key", +) +def dict_delete_key( + dictionary: Annotated[ + dict, + fn.InputMeta( + on={ + "after_set_value": fn.decorator.update_other_io_options( + "key", lambda x: list(x.keys()) + ) + } + ), + ], + key: Any, + ignore_missing: bool = True, +) -> dict: + result = dict(dictionary) + if key in result: + del result[key] + elif not ignore_missing: + raise KeyError(key) + return result + + +@fn.NodeDecorator( + id="dict_pop", + name="Dict Pop", + outputs=[ + {"name": "new_dict"}, + {"name": "value"}, + ], +) +def dict_pop( + dictionary: Annotated[ + dict, + fn.InputMeta( + on={ + "after_set_value": fn.decorator.update_other_io_options( + "key", lambda x: list(x.keys()) + ) + } + ), + ], + key: Any, + default: Any = fn.NoValue, +) -> Tuple[dict, Any]: + result = dict(dictionary) + if key in result: + value = result.pop(key) + return result, value + if default is fn.NoValue or default == str(fn.NoValue): + raise KeyError(key) + return result, default + + +@fn.NodeDecorator( + id="dict_deep_get", + name="Dict Deep Get", +) +def dict_deep_get(obj: Any, path: List[Any]) -> Any: + current = obj + for key in path: + if isinstance(current, dict): + if key in current: + current = current[key] + else: + return fn.NoValue + elif isinstance(current, (list, tuple)): + if isinstance(key, int): + index = key + elif isinstance(key, str) and key.lstrip("-").isdigit(): + index = int(key) + else: + return fn.NoValue + if -len(current) <= index < len(current): + current = current[index] + else: + return fn.NoValue + else: + return fn.NoValue + return current + + +@fn.NodeDecorator( + id="dict_deep_set", + name="Dict Deep Set", +) +def dict_deep_set( + dictionary: dict, + path: List[Any], + value: Any, + create_missing: bool = True, +) -> dict: + if not path: + raise ValueError("path must not be empty") + + if not isinstance(dictionary, dict): + raise TypeError("dictionary must be a dict") + + result = dict(dictionary) + current = result + + for key in path[:-1]: + if key in current: + next_value = current[key] + if not isinstance(next_value, dict): + raise TypeError( + f"Expected dict at path segment '{key}', got {type(next_value)}" + ) + next_dict = dict(next_value) + else: + if not create_missing: + raise KeyError(key) + next_dict = {} + + current[key] = next_dict + current = next_dict + + current[path[-1]] = value + return result + + NODE_SHELF = fn.Shelf( nodes=[ DictGetNode, @@ -128,6 +325,13 @@ def dict_set_key(dictionary: dict, key: Any, value: Any) -> dict: dict_to_list, dict_set_default, dict_set_key, + dict_merge, + dict_select_keys, + dict_rename_key, + dict_delete_key, + dict_pop, + dict_deep_get, + dict_deep_set, ], name="Dicts", description="Work with dictionaries", diff --git a/src/funcnodes_basic/input.py b/src/funcnodes_basic/input.py index c7fbd13..7342c50 100644 --- a/src/funcnodes_basic/input.py +++ b/src/funcnodes_basic/input.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, Any import funcnodes_core as fn import json @@ -24,7 +24,20 @@ def any_input(input: Union[str, float, int, bool]) -> str: ], ) def json_input(input: str) -> Union[str, int, float, bool, dict]: - return json.loads(input) + return json.loads(input, cls=fn.JSONDecoder) + + +@fn.NodeDecorator( + node_id="input.json_dump", + node_name="JSON Dump", + description="Dump a Python object as a JSON string", + outputs=[ + {"name": "json"}, + ], +) +def json_dump(obj: Any, indent: int = 0, sort_keys: bool = False) -> str: + indent = int(indent) + return json.dumps(obj, indent=indent if indent > 0 else None, sort_keys=sort_keys) @fn.NodeDecorator( @@ -87,6 +100,7 @@ def bool_input(input: bool) -> bool: NODE_SHELF = fn.Shelf( nodes=[ json_input, + json_dump, any_input, str_input, int_input, diff --git a/src/funcnodes_basic/lists.py b/src/funcnodes_basic/lists.py index d1080de..f1554b8 100644 --- a/src/funcnodes_basic/lists.py +++ b/src/funcnodes_basic/lists.py @@ -234,6 +234,44 @@ def list_slice_step( return lst[start:end:step] +@fn.NodeDecorator( + id="list_flatten", + name="List Flatten", + description="Flatten a list by one level.", +) +def list_flatten(lst: List[Any]) -> List[Any]: + flattened: List[Any] = [] + for item in lst: + if isinstance(item, (list, tuple)): + flattened.extend(item) + else: + flattened.append(item) + return flattened + + +@fn.NodeDecorator( + id="list_unique", + name="List Unique", + description="Remove duplicates while preserving order.", +) +def list_unique(lst: List[Any]) -> List[Any]: + unique: List[Any] = [] + seen_hashable = set() + + for item in lst: + try: + if item in seen_hashable: + continue + seen_hashable.add(item) + unique.append(item) + except TypeError: + if any(item == existing for existing in unique): + continue + unique.append(item) + + return unique + + NODE_SHELF = fn.Shelf( nodes=[ contains, @@ -252,6 +290,8 @@ def list_slice_step( list_set, list_slice, list_slice_step, + list_flatten, + list_unique, ], subshelves=[], name="Lists", diff --git a/tests/test_dict.py b/tests/test_dict.py index 1f5b9a0..a5deaf9 100644 --- a/tests/test_dict.py +++ b/tests/test_dict.py @@ -1,5 +1,7 @@ from funcnodes_basic import dicts import pytest_funcnodes +import pytest +import funcnodes_core as fn # DictGetNode, # dict_keys, # dict_values, @@ -155,3 +157,210 @@ async def test_dict_set_key_new_key(): await node assert node.outputs["out"].value == {"a": 1, "b": 2} + + +@pytest_funcnodes.nodetest(dicts.dict_merge) +async def test_dict_merge_prefer_b_default(): + a = {"a": 1, "x": 1} + b = {"x": 2, "b": 2} + + node = dicts.dict_merge() + node.inputs["a"].value = a + node.inputs["b"].value = b + await node + + assert node.outputs["out"].value == {"a": 1, "x": 2, "b": 2} + assert a == {"a": 1, "x": 1} + assert b == {"x": 2, "b": 2} + + +@pytest_funcnodes.nodetest(dicts.dict_merge) +async def test_dict_merge_prefer_a(): + a = {"a": 1, "x": 1} + b = {"x": 2, "b": 2} + + node = dicts.dict_merge() + node.inputs["a"].value = a + node.inputs["b"].value = b + node.inputs["prefer"].value = "a" + await node + + assert node.outputs["out"].value == {"x": 1, "b": 2, "a": 1} + + +@pytest_funcnodes.nodetest(dicts.dict_select_keys) +async def test_dict_select_keys_basic_and_ignore_missing(): + testdict = {"a": 1, "b": 2} + + node = dicts.dict_select_keys() + node.inputs["dictionary"].value = testdict + assert node.inputs["keys"].value_options["options"] == ["a", "b"] + node.inputs["keys"].value = ["b", "c", "a"] + await node + + assert node.outputs["out"].value == {"b": 2, "a": 1} + assert testdict == {"a": 1, "b": 2} + + +@pytest_funcnodes.nodetest(dicts.dict_select_keys) +async def test_dict_select_keys_missing_raises_when_not_ignored(): + node = dicts.dict_select_keys() + node.inputs["dictionary"].value = {"a": 1} + node.inputs["keys"].value = ["b"] + node.inputs["ignore_missing"].value = False + with pytest.raises(fn.NodeTriggerError): + await node + + +@pytest_funcnodes.nodetest(dicts.dict_rename_key) +async def test_dict_rename_key_basic(): + node = dicts.dict_rename_key() + node.inputs["dictionary"].value = {"a": 1} + assert node.inputs["old_key"].value_options["options"] == ["a"] + node.inputs["old_key"].value = "a" + node.inputs["new_key"].value = "b" + await node + + assert node.outputs["out"].value == {"b": 1} + + +@pytest_funcnodes.nodetest(dicts.dict_rename_key) +async def test_dict_rename_key_missing_old_raises(): + node = dicts.dict_rename_key() + node.inputs["dictionary"].value = {"a": 1} + node.inputs["old_key"].value = "missing" + node.inputs["new_key"].value = "b" + with pytest.raises(fn.NodeTriggerError): + await node + + +@pytest_funcnodes.nodetest(dicts.dict_rename_key) +async def test_dict_rename_key_existing_new_key_requires_overwrite(): + node = dicts.dict_rename_key() + node.inputs["dictionary"].value = {"a": 1, "b": 2} + node.inputs["old_key"].value = "a" + node.inputs["new_key"].value = "b" + with pytest.raises(fn.NodeTriggerError): + await node + + node.inputs["overwrite"].value = True + await node + assert node.outputs["out"].value == {"b": 1} + + +@pytest_funcnodes.nodetest(dicts.dict_delete_key) +async def test_dict_delete_key_basic_and_ignore_missing(): + node = dicts.dict_delete_key() + node.inputs["dictionary"].value = {"a": 1} + assert node.inputs["key"].value_options["options"] == ["a"] + node.inputs["key"].value = "a" + await node + assert node.outputs["out"].value == {} + + node.inputs["dictionary"].value = {"a": 1} + node.inputs["key"].value = "missing" + await node + assert node.outputs["out"].value == {"a": 1} + + +@pytest_funcnodes.nodetest(dicts.dict_delete_key) +async def test_dict_delete_key_missing_raises_when_not_ignored(): + node = dicts.dict_delete_key() + node.inputs["dictionary"].value = {"a": 1} + node.inputs["key"].value = "missing" + node.inputs["ignore_missing"].value = False + with pytest.raises(fn.NodeTriggerError): + await node + + +@pytest_funcnodes.nodetest(dicts.dict_pop) +async def test_dict_pop_basic(): + testdict = {"a": 1, "b": 2} + + node = dicts.dict_pop() + node.inputs["dictionary"].value = testdict + assert node.inputs["key"].value_options["options"] == ["a", "b"] + node.inputs["key"].value = "a" + await node + + assert node.outputs["new_dict"].value == {"b": 2} + assert node.outputs["value"].value == 1 + assert testdict == {"a": 1, "b": 2} + + +@pytest_funcnodes.nodetest(dicts.dict_pop) +async def test_dict_pop_missing_uses_default(): + node = dicts.dict_pop() + node.inputs["dictionary"].value = {"a": 1} + node.inputs["key"].value = "missing" + node.inputs["default"].value = 123 + await node + + assert node.outputs["new_dict"].value == {"a": 1} + assert node.outputs["value"].value == 123 + + +@pytest_funcnodes.nodetest(dicts.dict_pop) +async def test_dict_pop_missing_without_default_raises(): + node = dicts.dict_pop() + node.inputs["dictionary"].value = {"a": 1} + node.inputs["key"].value = "missing" + with pytest.raises(fn.NodeTriggerError): + await node + + +@pytest_funcnodes.nodetest(dicts.dict_deep_get) +async def test_dict_deep_get_basic_and_missing(): + node = dicts.dict_deep_get() + node.inputs["obj"].value = {"a": {"b": 2}} + node.inputs["path"].value = ["a", "b"] + await node + assert node.outputs["out"].value == 2 + + node.inputs["path"].value = ["a", "missing"] + await node + assert node.outputs["out"].value is fn.NoValue + + node.inputs["path"].value = [] + await node + assert node.outputs["out"].value == {"a": {"b": 2}} + + +@pytest_funcnodes.nodetest(dicts.dict_deep_set) +async def test_dict_deep_set_basic_and_create_missing(): + testdict = {"a": {"b": 1}} + + node = dicts.dict_deep_set() + node.inputs["dictionary"].value = testdict + node.inputs["path"].value = ["a", "b"] + node.inputs["value"].value = 2 + await node + assert node.outputs["out"].value == {"a": {"b": 2}} + assert testdict == {"a": {"b": 1}} + + node.inputs["dictionary"].value = {} + node.inputs["path"].value = ["a", "b"] + node.inputs["value"].value = 1 + await node + assert node.outputs["out"].value == {"a": {"b": 1}} + + +@pytest_funcnodes.nodetest(dicts.dict_deep_set) +async def test_dict_deep_set_missing_raises_when_not_creating(): + node = dicts.dict_deep_set() + node.inputs["dictionary"].value = {} + node.inputs["path"].value = ["a", "b"] + node.inputs["value"].value = 1 + node.inputs["create_missing"].value = False + with pytest.raises(fn.NodeTriggerError): + await node + + +@pytest_funcnodes.nodetest(dicts.dict_deep_set) +async def test_dict_deep_set_empty_path_raises(): + node = dicts.dict_deep_set() + node.inputs["dictionary"].value = {"a": 1} + node.inputs["path"].value = [] + node.inputs["value"].value = 2 + with pytest.raises(fn.NodeTriggerError): + await node diff --git a/tests/test_inputs.py b/tests/test_inputs.py index aea8414..fec828b 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -2,6 +2,7 @@ import pytest_funcnodes import pytest import funcnodes_core as fn +import json @pytest_funcnodes.nodetest(input.any_input) @@ -151,3 +152,27 @@ async def test_json_input_invalid(): node.inputs["input"].value = "{" with pytest.raises(fn.NodeTriggerError): await node + + +@pytest_funcnodes.nodetest(input.json_dump) +async def test_json_dump_roundtrip(): + node = input.json_dump() + + node.inputs["obj"].value = {"b": 2, "a": 1} + await node + + dumped = node.outputs["json"].value + assert json.loads(dumped) == {"b": 2, "a": 1} + + node.inputs["sort_keys"].value = True + await node + assert node.outputs["json"].value == '{"a": 1, "b": 2}' + + +@pytest_funcnodes.nodetest(input.json_dump) +async def test_json_dump_non_serializable(): + node = input.json_dump() + + node.inputs["obj"].value = {1, 2, 3} + with pytest.raises(fn.NodeTriggerError): + await node diff --git a/tests/test_lists.py b/tests/test_lists.py index e9c4929..a2fe7c7 100644 --- a/tests/test_lists.py +++ b/tests/test_lists.py @@ -237,3 +237,27 @@ async def test_list_slice_step(): assert node.outputs["out"].value == [2, 4] assert testlist == [1, 2, 3, 4, 5] + + +@pytest_funcnodes.nodetest(lists.list_flatten) +async def test_list_flatten(): + testlist = [1, [2, 3], (4, 5), "ab", {"x": 1}] + + node = lists.list_flatten() + node.inputs["lst"].value = testlist + await node + + assert node.outputs["out"].value == [1, 2, 3, 4, 5, "ab", {"x": 1}] + assert testlist == [1, [2, 3], (4, 5), "ab", {"x": 1}] + + +@pytest_funcnodes.nodetest(lists.list_unique) +async def test_list_unique(): + testlist = [1, 2, 1, {"a": 1}, {"a": 1}, 2] + + node = lists.list_unique() + node.inputs["lst"].value = testlist + await node + + assert node.outputs["out"].value == [1, 2, {"a": 1}] + assert testlist == [1, 2, 1, {"a": 1}, {"a": 1}, 2] From 2fbb68465ad47d723bfb5740db42383415daa196 Mon Sep 17 00:00:00 2001 From: Julian Kimmig Date: Mon, 12 Jan 2026 14:12:13 +0100 Subject: [PATCH 4/4] =?UTF-8?q?bump:=20version=201.1.0=20=E2=86=92=201.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ commit_message.md | 7 +++++-- pyproject.toml | 2 +- uv.lock | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c926c4d..6266a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## v1.2.0 (2026-01-12) + +### Feat + +- **basic**: add dict utilities and list/json helpers +- **dicts**: add nodes to set missing or existing keys +- **input**: add json input node + ## v1.1.0 (2025-11-21) ### Feat diff --git a/commit_message.md b/commit_message.md index 9fc9b72..3820244 100644 --- a/commit_message.md +++ b/commit_message.md @@ -1,3 +1,6 @@ -feat(input): add json input node +feat(basic): add dict utilities and list/json helpers -Add JSON input node with tests covering valid and invalid payloads. +Add dict helpers for deep get/set, merge, key selection/rename, and +pop/delete with dynamic key value options. + +Add json_dump plus list flatten/unique nodes with tests. diff --git a/pyproject.toml b/pyproject.toml index b395acb..5d96ecc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "funcnodes-basic" -version = "1.1.0" +version = "1.2.0" description = "Basic functionalities for funcnodes" readme = "README.md" classifiers = [ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",] diff --git a/uv.lock b/uv.lock index 50d9485..b50f588 100644 --- a/uv.lock +++ b/uv.lock @@ -380,7 +380,7 @@ wheels = [ [[package]] name = "funcnodes-basic" -version = "1.1.0" +version = "1.2.0" source = { editable = "." } dependencies = [ { name = "funcnodes" },