No description
Find a file
2026-05-30 23:39:24 -04:00
src/nodies Add SocketMap for dot-notation socket access 2026-05-30 23:20:38 -04:00
tests Replace add_input/output_projection() with direct socket .connect() 2026-05-30 21:01:02 -04:00
.gitignore chore: update .gitignore and remove obsolete prompt file 2026-05-30 19:02:54 -04:00
.python-version initial commit 2026-05-29 23:32:06 -04:00
demo.py Add SocketMap for dot-notation socket access 2026-05-30 23:20:38 -04:00
LICENSE initial commit 2026-05-29 23:32:06 -04:00
pyproject.toml Update project description 2026-05-30 23:39:24 -04:00
README.md Add SocketMap for dot-notation socket access 2026-05-30 23:20:38 -04:00
uv.lock initial commit 2026-05-29 23:32:06 -04:00

nodies

A node-graph execution library for Python. Nodes have typed input and output sockets. Outputs connect to inputs, forming a directed acyclic graph that is executed with automatic parallelism and dead-code elimination.

Prepared graph plans are cached on composite nodes after the first execution and reused until the graph topology changes.

Installation

uv pip install nodies

Or add to your pyproject.toml:

[project]
dependencies = [
    "nodies",
]

Usage

Defining nodes

Use Node.from_function for a quick functional style, or subclass Node for reusable types:

import types
from nodies import Node

# Functional style
add = Node.from_function(
    lambda a, b: {"result": a + b},
    input_names=["a", "b"],
    output_names=["result"],
    name="add",
)

# Subclass style
class MultiplyNode(Node):
    def __init__(self):
        super().__init__(name="mul")
        self.add_input("a")
        self.add_input("b")
        self.add_output("result")

    def execute(self, **kw):
        return {"result": kw["a"] * kw["b"]}

Building and running a graph

Graphs are composite nodes. Wire a graph's outer input to an inner node's input by calling .connect() on the outer socket:

from nodies import Node, Executor

# Build nodes
add = Node.from_function(
    lambda a, b: {"result": a + b},
    input_names=["a", "b"],
    output_names=["result"],
    name="add",
)
mul = Node.from_function(
    lambda a, b: {"result": a * b},
    input_names=["a", "b"],
    output_names=["result"],
    name="mul",
)

# Create the graph container
graph = Node(name="my_graph")
graph.add_input("x")
graph.add_input("y")
graph.add_input("z")
graph.add_output("out")
graph.inner_nodes = [add, mul]

# Wire: (x + y) * z -> out
graph.inputs.x.connect(add.inputs.a)
graph.inputs.y.connect(add.inputs.b)
add.outputs.result.connect(mul.inputs.a)
graph.inputs.z.connect(mul.inputs.b)
mul.outputs.result.connect(graph.outputs.out)

# Execute
result = Executor().run(graph, x=2, y=3, z=4)
print(result["out"])  # 20  →  (2 + 3) * 4

Copy semantics

Output sockets automatically apply the right copy strategy when delivering values to downstream inputs. Mark an input readonly=True to promise you won't mutate the value — this allows safe pass-through aliasing instead of deep-copying:

node.add_input("data", readonly=True)   # alias (no copy if safe)
node.add_input("data", readonly=False)  # may mutate → triggers deepcopy

The rules:

  • 1 total consumer → always pass through (no copy)
  • All consumers are read-only → pass through (safe aliasing)
  • Mixed / multiple RW consumers:
    • All RO consumers share one deepcopy
    • Each RW consumer except the last receives a deepcopy
    • The last RW consumer receives the original (no wasted copy)

Side-effect nodes

Set pure=False on a node to ensure it always runs, even if its outputs aren't consumed:

logger = Node(name="logger", pure=False)

Development

# Clone the repository
git clone https://git.trainraider.win/trainraider/nodies.git
cd nodies

# Install dependencies
uv sync --all-extras

# Run tests
uv run pytest

# Linting
uv run ruff check src/nodies

License

MIT