funcnodes-basic 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {funcnodes_basic-0.2.2/src/funcnodes_basic.egg-info → funcnodes_basic-0.2.4}/PKG-INFO +2 -2
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/pyproject.toml +9 -2
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/__init__.py +3 -2
- funcnodes_basic-0.2.4/src/funcnodes_basic/dataclass.py +64 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/logic.py +29 -6
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4/src/funcnodes_basic.egg-info}/PKG-INFO +2 -2
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic.egg-info/SOURCES.txt +3 -0
- funcnodes_basic-0.2.4/src/funcnodes_basic.egg-info/requires.txt +2 -0
- funcnodes_basic-0.2.4/tests/test_dataclass.py +141 -0
- funcnodes_basic-0.2.4/tests/test_flows.py +26 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_logic.py +5 -1
- funcnodes_basic-0.2.2/src/funcnodes_basic.egg-info/requires.txt +0 -2
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/LICENSE +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/MANIFEST.in +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/README.md +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/setup.cfg +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/dicts.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/input.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/lists.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/math_nodes.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/strings.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic.egg-info/dependency_links.txt +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic.egg-info/entry_points.txt +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic.egg-info/top_level.txt +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_all_nodes_pytest.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_dict.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_import.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_inputs.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_lists.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_math.py +0 -0
- {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_strings.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: funcnodes-basic
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.4
|
4
4
|
Summary: Basic functionalities for funcnodes
|
5
5
|
Author-email: Julian Kimmig <julian.kimmig@linkdlab.de>
|
6
6
|
License: AGPL-3.0
|
@@ -12,7 +12,7 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or l
|
|
12
12
|
Requires-Python: >=3.11
|
13
13
|
Description-Content-Type: text/markdown
|
14
14
|
License-File: LICENSE
|
15
|
-
Requires-Dist: funcnodes-core>=0.
|
15
|
+
Requires-Dist: funcnodes-core>=0.4.1
|
16
16
|
Requires-Dist: funcnodes
|
17
17
|
Dynamic: license-file
|
18
18
|
|
@@ -1,12 +1,12 @@
|
|
1
1
|
[project]
|
2
2
|
name = "funcnodes-basic"
|
3
|
-
version = "0.2.
|
3
|
+
version = "0.2.4"
|
4
4
|
description = "Basic functionalities for funcnodes"
|
5
5
|
readme = "README.md"
|
6
6
|
classifiers = [ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",]
|
7
7
|
requires-python = ">=3.11"
|
8
8
|
dependencies = [
|
9
|
-
"funcnodes-core>=0.
|
9
|
+
"funcnodes-core>=0.4.1",
|
10
10
|
"funcnodes",
|
11
11
|
]
|
12
12
|
authors = [{name = "Julian Kimmig", email = "julian.kimmig@linkdlab.de"}]
|
@@ -43,3 +43,10 @@ shelf = "funcnodes_basic:NODE_SHELF"
|
|
43
43
|
|
44
44
|
[tool.setuptools.packages.find]
|
45
45
|
where = [ "src",]
|
46
|
+
|
47
|
+
[tool.commitizen]
|
48
|
+
name = "cz_conventional_commits"
|
49
|
+
tag_format = "v$version"
|
50
|
+
version_scheme = "pep440"
|
51
|
+
version_provider = "uv"
|
52
|
+
update_changelog_on_bump = true
|
@@ -5,9 +5,9 @@ from .lists import NODE_SHELF as lists_shelf
|
|
5
5
|
from .strings import NODE_SHELF as strings_shelf
|
6
6
|
from .dicts import NODE_SHELF as dicts_shelf
|
7
7
|
from .input import NODE_SHELF as input_shelf
|
8
|
+
from .dataclass import NODE_SHELF as dataclass_shelf
|
8
9
|
|
9
|
-
|
10
|
-
__version__ = "0.2.2"
|
10
|
+
__version__ = "0.2.3"
|
11
11
|
|
12
12
|
NODE_SHELF = Shelf(
|
13
13
|
nodes=[],
|
@@ -15,6 +15,7 @@ NODE_SHELF = Shelf(
|
|
15
15
|
input_shelf,
|
16
16
|
lists_shelf,
|
17
17
|
dicts_shelf,
|
18
|
+
dataclass_shelf,
|
18
19
|
strings_shelf,
|
19
20
|
math_shelf,
|
20
21
|
logic_shelf,
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import funcnodes_core as fn
|
3
|
+
from typing import Any, Dict
|
4
|
+
|
5
|
+
|
6
|
+
@fn.NodeDecorator(
|
7
|
+
id="dataclass.to_dict",
|
8
|
+
)
|
9
|
+
def dataclass_to_dict(instance: Any) -> Dict[str, Any]:
|
10
|
+
"""
|
11
|
+
Convert a dataclass instance to a dictionary.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
instance (object): The dataclass instance to convert.
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
dict: The dictionary representation of the dataclass instance.
|
18
|
+
"""
|
19
|
+
if not dataclasses.is_dataclass(instance):
|
20
|
+
raise TypeError(f"Expected a dataclass instance, got {type(instance)}")
|
21
|
+
|
22
|
+
return dataclasses.asdict(instance)
|
23
|
+
|
24
|
+
|
25
|
+
@fn.NodeDecorator(
|
26
|
+
id="dataclass.get_field",
|
27
|
+
default_io_options={
|
28
|
+
"instance": {
|
29
|
+
"on": {
|
30
|
+
"after_set_value": fn.decorator.update_other_io_value_options(
|
31
|
+
"field_name",
|
32
|
+
lambda result: {
|
33
|
+
"options": [field.name for field in dataclasses.fields(result)]
|
34
|
+
if dataclasses.is_dataclass(result)
|
35
|
+
else None,
|
36
|
+
},
|
37
|
+
)
|
38
|
+
}
|
39
|
+
}
|
40
|
+
},
|
41
|
+
)
|
42
|
+
def dataclass_get_field(instance: Any, field_name: str) -> Any:
|
43
|
+
"""
|
44
|
+
Get a field value from a dataclass instance.
|
45
|
+
"""
|
46
|
+
if not dataclasses.is_dataclass(instance):
|
47
|
+
raise TypeError(f"Expected a dataclass instance, got {type(instance)}")
|
48
|
+
|
49
|
+
if not hasattr(instance, field_name):
|
50
|
+
raise AttributeError(
|
51
|
+
f"{instance.__class__.__name__} has no field '{field_name}'"
|
52
|
+
)
|
53
|
+
|
54
|
+
return getattr(instance, field_name)
|
55
|
+
|
56
|
+
|
57
|
+
NODE_SHELF = fn.Shelf(
|
58
|
+
nodes=[
|
59
|
+
dataclass_to_dict,
|
60
|
+
dataclass_get_field,
|
61
|
+
],
|
62
|
+
name="dataclass",
|
63
|
+
description="Nodes for working with dataclasses",
|
64
|
+
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Logic Nodes for control flow and decision making."""
|
2
2
|
|
3
|
-
from funcnodes_core.node import Node
|
3
|
+
from funcnodes_core.node import Node
|
4
4
|
from typing import Any, List, Optional
|
5
5
|
from funcnodes_core.io import NodeInput, NodeOutput, NoValue
|
6
6
|
import asyncio
|
@@ -35,8 +35,19 @@ class WhileNode(Node):
|
|
35
35
|
async def func(self, condition: bool, input: Any) -> None:
|
36
36
|
if self.inputs["condition"].value:
|
37
37
|
self.outputs["do"].value = input
|
38
|
-
|
39
|
-
|
38
|
+
datapaths = [
|
39
|
+
ip.datapath
|
40
|
+
for ip in self.outputs["do"].connections
|
41
|
+
if ip.datapath is not None
|
42
|
+
]
|
43
|
+
|
44
|
+
while datapaths:
|
45
|
+
for dp in datapaths:
|
46
|
+
if dp.done(breaking_nodes=[self]):
|
47
|
+
datapaths.remove(dp)
|
48
|
+
break
|
49
|
+
else:
|
50
|
+
await asyncio.sleep(0.1)
|
40
51
|
self.request_trigger()
|
41
52
|
else:
|
42
53
|
self.outputs["done"].value = input
|
@@ -94,9 +105,21 @@ class ForNode(Node):
|
|
94
105
|
pass
|
95
106
|
|
96
107
|
for i in self.progress(input, desc="Iterating", unit="it", total=iplen):
|
97
|
-
self.outputs["do"].set_value(i, does_trigger=
|
98
|
-
|
99
|
-
|
108
|
+
self.outputs["do"].set_value(i, does_trigger=True)
|
109
|
+
datapaths = [
|
110
|
+
ip.datapath
|
111
|
+
for ip in self.outputs["do"].connections
|
112
|
+
if ip.datapath is not None
|
113
|
+
]
|
114
|
+
|
115
|
+
while datapaths:
|
116
|
+
for dp in datapaths:
|
117
|
+
if dp.done(breaking_nodes=[self]):
|
118
|
+
datapaths.remove(dp)
|
119
|
+
break
|
120
|
+
else:
|
121
|
+
await asyncio.sleep(0.1)
|
122
|
+
|
100
123
|
v = self.inputs["collector"].value
|
101
124
|
if v is not NoValue:
|
102
125
|
results.append(v)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: funcnodes-basic
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.4
|
4
4
|
Summary: Basic functionalities for funcnodes
|
5
5
|
Author-email: Julian Kimmig <julian.kimmig@linkdlab.de>
|
6
6
|
License: AGPL-3.0
|
@@ -12,7 +12,7 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or l
|
|
12
12
|
Requires-Python: >=3.11
|
13
13
|
Description-Content-Type: text/markdown
|
14
14
|
License-File: LICENSE
|
15
|
-
Requires-Dist: funcnodes-core>=0.
|
15
|
+
Requires-Dist: funcnodes-core>=0.4.1
|
16
16
|
Requires-Dist: funcnodes
|
17
17
|
Dynamic: license-file
|
18
18
|
|
@@ -3,6 +3,7 @@ MANIFEST.in
|
|
3
3
|
README.md
|
4
4
|
pyproject.toml
|
5
5
|
src/funcnodes_basic/__init__.py
|
6
|
+
src/funcnodes_basic/dataclass.py
|
6
7
|
src/funcnodes_basic/dicts.py
|
7
8
|
src/funcnodes_basic/input.py
|
8
9
|
src/funcnodes_basic/lists.py
|
@@ -16,7 +17,9 @@ src/funcnodes_basic.egg-info/entry_points.txt
|
|
16
17
|
src/funcnodes_basic.egg-info/requires.txt
|
17
18
|
src/funcnodes_basic.egg-info/top_level.txt
|
18
19
|
tests/test_all_nodes_pytest.py
|
20
|
+
tests/test_dataclass.py
|
19
21
|
tests/test_dict.py
|
22
|
+
tests/test_flows.py
|
20
23
|
tests/test_import.py
|
21
24
|
tests/test_inputs.py
|
22
25
|
tests/test_lists.py
|
@@ -0,0 +1,141 @@
|
|
1
|
+
from funcnodes_basic import dataclass as dc_nodes
|
2
|
+
import pytest_funcnodes
|
3
|
+
import pytest
|
4
|
+
import funcnodes_core as fn
|
5
|
+
from dataclasses import dataclass
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass
|
9
|
+
class SimpleDataClass:
|
10
|
+
name: str
|
11
|
+
value: int
|
12
|
+
is_active: bool = True
|
13
|
+
|
14
|
+
|
15
|
+
@dataclass
|
16
|
+
class NestedDataClass:
|
17
|
+
id: int
|
18
|
+
data: SimpleDataClass
|
19
|
+
|
20
|
+
|
21
|
+
@pytest_funcnodes.nodetest(dc_nodes.dataclass_to_dict)
|
22
|
+
async def test_dataclass_to_dict():
|
23
|
+
node = dc_nodes.dataclass_to_dict()
|
24
|
+
|
25
|
+
# Test with a simple dataclass
|
26
|
+
instance1 = SimpleDataClass(name="Test1", value=100)
|
27
|
+
node.inputs["instance"].value = instance1
|
28
|
+
await node
|
29
|
+
assert node.outputs["out"].value == {
|
30
|
+
"name": "Test1",
|
31
|
+
"value": 100,
|
32
|
+
"is_active": True,
|
33
|
+
}
|
34
|
+
|
35
|
+
# Test with a nested dataclass
|
36
|
+
instance2 = NestedDataClass(
|
37
|
+
id=1, data=SimpleDataClass(name="Nested", value=200, is_active=False)
|
38
|
+
)
|
39
|
+
node.inputs["instance"].value = instance2
|
40
|
+
await node
|
41
|
+
assert node.outputs["out"].value == {
|
42
|
+
"id": 1,
|
43
|
+
"data": {"name": "Nested", "value": 200, "is_active": False},
|
44
|
+
}
|
45
|
+
|
46
|
+
# Test with non-dataclass input
|
47
|
+
node.inputs["instance"].value = {"not": "a dataclass"}
|
48
|
+
with pytest.raises(fn.NodeTriggerError) as excinfo:
|
49
|
+
await node
|
50
|
+
assert "Expected a dataclass instance" in str(excinfo.value)
|
51
|
+
|
52
|
+
node.inputs["instance"].value = 123
|
53
|
+
with pytest.raises(fn.NodeTriggerError) as excinfo:
|
54
|
+
await node
|
55
|
+
assert "Expected a dataclass instance" in str(excinfo.value)
|
56
|
+
|
57
|
+
|
58
|
+
@pytest_funcnodes.nodetest(dc_nodes.dataclass_get_field)
|
59
|
+
async def test_dataclass_get_field():
|
60
|
+
node = dc_nodes.dataclass_get_field()
|
61
|
+
|
62
|
+
instance_simple = SimpleDataClass(name="TestSimple", value=123)
|
63
|
+
instance_nested = NestedDataClass(
|
64
|
+
id=1, data=SimpleDataClass(name="Nested", value=200, is_active=False)
|
65
|
+
)
|
66
|
+
|
67
|
+
# Test setting instance updates field_name options
|
68
|
+
node.inputs["instance"].value = instance_simple
|
69
|
+
await node # Initial trigger to process instance and update options
|
70
|
+
assert node.inputs["field_name"].value_options["options"] == [
|
71
|
+
"name",
|
72
|
+
"value",
|
73
|
+
"is_active",
|
74
|
+
]
|
75
|
+
|
76
|
+
# Test getting a valid field 'name'
|
77
|
+
node.inputs["field_name"].value = "name"
|
78
|
+
await node
|
79
|
+
assert node.outputs["out"].value == "TestSimple"
|
80
|
+
|
81
|
+
# Test getting a valid field 'value'
|
82
|
+
node.inputs["field_name"].value = "value"
|
83
|
+
await node
|
84
|
+
assert node.outputs["out"].value == 123
|
85
|
+
|
86
|
+
# Test getting a valid field 'is_active'
|
87
|
+
node.inputs["field_name"].value = "is_active"
|
88
|
+
await node
|
89
|
+
assert node.outputs["out"].value is True
|
90
|
+
|
91
|
+
# Test with nested dataclass, first update options
|
92
|
+
node.inputs["instance"].value = instance_nested
|
93
|
+
|
94
|
+
assert node.inputs["field_name"].value_options["options"] == ["id", "data"]
|
95
|
+
|
96
|
+
# Test getting 'id' from nested
|
97
|
+
node.inputs["field_name"].value = "id"
|
98
|
+
await node
|
99
|
+
assert node.outputs["out"].value == 1
|
100
|
+
|
101
|
+
# Test getting 'data' (which is another dataclass)
|
102
|
+
node.inputs["field_name"].value = "data"
|
103
|
+
await node
|
104
|
+
assert node.outputs["out"].value == SimpleDataClass(
|
105
|
+
name="Nested", value=200, is_active=False
|
106
|
+
)
|
107
|
+
|
108
|
+
# Test getting non-existent field
|
109
|
+
node.inputs["instance"].value = instance_simple
|
110
|
+
node.inputs["field_name"].value = "non_existent_field"
|
111
|
+
with pytest.raises(fn.NodeTriggerError) as excinfo:
|
112
|
+
await node
|
113
|
+
assert "has no field 'non_existent_field'" in str(excinfo.value)
|
114
|
+
|
115
|
+
# Test with non-dataclass input
|
116
|
+
node.inputs["instance"].value = {"not": "a dataclass"}
|
117
|
+
node.inputs["field_name"].value = "some_field" # field_name options will be None
|
118
|
+
assert node.inputs["field_name"].value_options["options"] is None
|
119
|
+
|
120
|
+
with pytest.raises(fn.NodeTriggerError) as excinfo:
|
121
|
+
await node # trigger the func with invalid instance
|
122
|
+
assert "Expected a dataclass instance" in str(excinfo.value)
|
123
|
+
|
124
|
+
# Test dynamic update of options when instance changes
|
125
|
+
node.inputs["instance"].value = instance_nested
|
126
|
+
assert node.inputs["field_name"].value_options["options"] == ["id", "data"]
|
127
|
+
node.inputs["field_name"].value = "id"
|
128
|
+
await node
|
129
|
+
assert node.outputs["out"].value == 1
|
130
|
+
|
131
|
+
node.inputs["instance"].value = instance_simple
|
132
|
+
assert node.inputs["field_name"].value_options["options"] == [
|
133
|
+
"name",
|
134
|
+
"value",
|
135
|
+
"is_active",
|
136
|
+
]
|
137
|
+
node.inputs[
|
138
|
+
"field_name"
|
139
|
+
].value = "name" # previous 'id' is no longer valid for options, but value remains
|
140
|
+
await node
|
141
|
+
assert node.outputs["out"].value == "TestSimple"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from funcnodes_basic import logic, strings
|
2
|
+
import pytest_funcnodes
|
3
|
+
import asyncio
|
4
|
+
|
5
|
+
|
6
|
+
@pytest_funcnodes.nodetest(logic.ForNode)
|
7
|
+
async def test_if_node_flow1():
|
8
|
+
fornode = logic.ForNode()
|
9
|
+
endswith = strings.string_endswith()
|
10
|
+
ifnode = logic.IfNode()
|
11
|
+
|
12
|
+
fornode.outputs["do"].connect(endswith.inputs["s"])
|
13
|
+
endswith.outputs["ends_with"].connect(ifnode.inputs["condition"])
|
14
|
+
fornode.outputs["do"].connect(ifnode.inputs["input"])
|
15
|
+
ifnode.outputs["on_true"].connect(fornode.inputs["collector"])
|
16
|
+
|
17
|
+
endswith.inputs["suffix"].value = ".txt"
|
18
|
+
fornode.inputs["input"].value = ["a.txt", "b.xls", "c.xls", "d.txt", "e.xls"]
|
19
|
+
|
20
|
+
# await fn.run_until_complete(fornode, ifnode, endswith)
|
21
|
+
await asyncio.sleep(1.5)
|
22
|
+
print("fornode:", fornode.in_trigger)
|
23
|
+
print("ifnode:", ifnode.in_trigger)
|
24
|
+
print("endswith:", endswith.in_trigger)
|
25
|
+
|
26
|
+
assert fornode.outputs["done"].value == ["a.txt", "d.txt"]
|
@@ -42,8 +42,12 @@ async def test_wait_node():
|
|
42
42
|
@pytest_funcnodes.nodetest(logic.ForNode)
|
43
43
|
async def test_for_node():
|
44
44
|
node = logic.ForNode()
|
45
|
+
waitnode = logic.WaitNode()
|
46
|
+
waitnode.inputs["delay"].value = 0.5
|
47
|
+
waitnode.inputs["input"].connect(node.outputs["do"])
|
48
|
+
waitnode.outputs["output"].connect(node.inputs["collector"])
|
45
49
|
node.inputs["input"].value = "hello"
|
46
|
-
|
50
|
+
|
47
51
|
await node
|
48
52
|
|
49
53
|
assert node.outputs["done"].value == ["h", "e", "l", "l", "o"]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic.egg-info/dependency_links.txt
RENAMED
File without changes
|
{funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic.egg-info/entry_points.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|