feat: add ProjectKey single-output relay node

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 11:34:22 +01:00
parent 628b256981
commit bd7d314ae8
2 changed files with 149 additions and 2 deletions
+48
View File
@@ -231,13 +231,61 @@ class ProjectSource:
return () return ()
class ProjectKey:
"""Single-output relay — fetches one key from a ProjectSource."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"source_label": ("STRING", {"default": "", "multiline": False}),
"key_name": ("STRING", {"default": "", "multiline": False}),
"key_type": ("STRING", {"default": "STRING", "multiline": False}),
},
"optional": {
"manager_url": ("STRING", {"default": "http://localhost:8080", "multiline": False}),
"project_name": ("STRING", {"default": "", "multiline": False}),
"file_name": ("STRING", {"default": "", "multiline": False}),
"sequence_number": ("INT", {"default": 1, "min": 1, "max": 9999}),
},
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("value",)
FUNCTION = "fetch_key"
CATEGORY = "utils/json/project"
OUTPUT_NODE = False
def fetch_key(self, source_label, key_name, key_type,
manager_url="http://localhost:8080", project_name="",
file_name="", sequence_number=1):
data = _fetch_data(manager_url, project_name, file_name, sequence_number)
if data.get("error") in ("http_error", "network_error", "parse_error"):
msg = data.get("message", "Unknown error")
raise RuntimeError(f"Failed to fetch data: {msg}")
val = data.get(key_name, "")
if key_type == "INT":
return (to_int(val),)
elif key_type == "FLOAT":
return (to_float(val),)
elif isinstance(val, bool):
return (str(val).lower(),)
elif isinstance(val, (int, float)):
return (val,)
else:
return (str(val),)
# --- Mappings --- # --- Mappings ---
PROJECT_NODE_CLASS_MAPPINGS = { PROJECT_NODE_CLASS_MAPPINGS = {
"ProjectLoaderDynamic": ProjectLoaderDynamic, "ProjectLoaderDynamic": ProjectLoaderDynamic,
"ProjectSource": ProjectSource, "ProjectSource": ProjectSource,
"ProjectKey": ProjectKey,
} }
PROJECT_NODE_DISPLAY_NAME_MAPPINGS = { PROJECT_NODE_DISPLAY_NAME_MAPPINGS = {
"ProjectLoaderDynamic": "Project Loader (Dynamic)", "ProjectLoaderDynamic": "Project Loader (Dynamic)",
"ProjectSource": "Project Source", "ProjectSource": "Project Source",
"ProjectKey": "Project Key",
} }
+101 -2
View File
@@ -235,9 +235,108 @@ class TestProjectSource:
assert ProjectSource.CATEGORY == "utils/json/project" assert ProjectSource.CATEGORY == "utils/json/project"
class TestProjectKey:
def test_input_types(self):
from project_loader import ProjectKey
inputs = ProjectKey.INPUT_TYPES()
assert "source_label" in inputs["required"]
assert "key_name" in inputs["required"]
assert "key_type" in inputs["required"]
def test_single_output(self):
from project_loader import ProjectKey
assert len(ProjectKey.RETURN_TYPES) == 1
assert len(ProjectKey.RETURN_NAMES) == 1
def test_fetch_key_string(self):
from project_loader import ProjectKey
node = ProjectKey()
data = {"prompt": "hello", "seed": 42}
with patch("project_loader._fetch_data", return_value=data):
result = node.fetch_key(
source_label="my_source",
key_name="prompt",
key_type="STRING",
manager_url="http://localhost:8080",
project_name="proj1",
file_name="batch_i2v",
sequence_number=1,
)
assert result == ("hello",)
def test_fetch_key_int_coercion(self):
from project_loader import ProjectKey
node = ProjectKey()
data = {"seed": "42"}
with patch("project_loader._fetch_data", return_value=data):
result = node.fetch_key(
source_label="my_source",
key_name="seed",
key_type="INT",
manager_url="http://localhost:8080",
project_name="proj1",
file_name="batch_i2v",
sequence_number=1,
)
assert result == (42,)
def test_fetch_key_float_coercion(self):
from project_loader import ProjectKey
node = ProjectKey()
data = {"cfg": "1.5"}
with patch("project_loader._fetch_data", return_value=data):
result = node.fetch_key(
source_label="my_source",
key_name="cfg",
key_type="FLOAT",
manager_url="http://localhost:8080",
project_name="proj1",
file_name="batch_i2v",
sequence_number=1,
)
assert result == (1.5,)
def test_fetch_key_missing_key(self):
from project_loader import ProjectKey
node = ProjectKey()
with patch("project_loader._fetch_data", return_value={}):
result = node.fetch_key(
source_label="my_source",
key_name="nonexistent",
key_type="STRING",
manager_url="http://localhost:8080",
project_name="proj1",
file_name="batch_i2v",
sequence_number=1,
)
assert result == ("",)
def test_fetch_key_network_error(self):
from project_loader import ProjectKey
node = ProjectKey()
error_resp = {"error": "network_error", "message": "Connection refused"}
with patch("project_loader._fetch_data", return_value=error_resp):
with pytest.raises(RuntimeError, match="Failed to fetch"):
node.fetch_key(
source_label="my_source",
key_name="prompt",
key_type="STRING",
manager_url="http://localhost:8080",
project_name="proj1",
file_name="batch_i2v",
sequence_number=1,
)
def test_category(self):
from project_loader import ProjectKey
assert ProjectKey.CATEGORY == "utils/json/project"
class TestNodeMappings: class TestNodeMappings:
def test_mappings_exist(self): def test_mappings_exist(self):
from project_loader import PROJECT_NODE_CLASS_MAPPINGS, PROJECT_NODE_DISPLAY_NAME_MAPPINGS from project_loader import PROJECT_NODE_CLASS_MAPPINGS, PROJECT_NODE_DISPLAY_NAME_MAPPINGS
assert "ProjectLoaderDynamic" in PROJECT_NODE_CLASS_MAPPINGS assert "ProjectLoaderDynamic" in PROJECT_NODE_CLASS_MAPPINGS
assert len(PROJECT_NODE_CLASS_MAPPINGS) == 1 assert "ProjectSource" in PROJECT_NODE_CLASS_MAPPINGS
assert len(PROJECT_NODE_DISPLAY_NAME_MAPPINGS) == 1 assert "ProjectKey" in PROJECT_NODE_CLASS_MAPPINGS
assert len(PROJECT_NODE_CLASS_MAPPINGS) == 3
assert len(PROJECT_NODE_DISPLAY_NAME_MAPPINGS) == 3