Metadata-Version: 2.4
Name: loop-node
Version: 0.2.0
Summary: Shared Node protocol and runtime for Loop and external integrations.
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: eclipse-zenoh==1.9.0
Requires-Dist: protobuf>=7.36.0
Requires-Dist: pydantic>=2.12.0
Provides-Extra: numpy
Requires-Dist: numpy>=1.26; extra == "numpy"

# loop-node

`loop-node` is the low-level runtime and wire protocol shared by Loop and
`loop-sdk`. Most integrations should use `loop-sdk` directly; use `loop-node`
when building a lower-level Node API or integration layer.

## Installation

```bash
pip install loop-node
```

Published wheels support CPython 3.10–3.14 on Linux x86_64 (glibc 2.28+).
They contain compiled implementation modules and type stubs; installation
does not require a compiler.

## Minimal example

```python
import time

from loop_node import (
    EmptyConfig,
    FieldContract,
    LifecycleState,
    Node,
    NodeConnectionConfig,
    PayloadContract,
    ValueKind,
)

VALUE = PayloadContract(fields={"value": FieldContract(kind=ValueKind.SCALAR)})

node = Node(config_type=EmptyConfig)
output = node.declare_stream_output("value", payload_contract=VALUE)
node.start(
    node_id="example_node",
    connection=NodeConnectionConfig(loop_endpoint="tcp/127.0.0.1:7448"),
)

try:
    while node.is_running:
        node.process_control()
        if node.status.lifecycle is LifecycleState.ACTIVE:
            output.publish(timestamp_ns=time.time_ns(), payload={"value": 1.0})
        time.sleep(0.1)
finally:
    if node.status.lifecycle is not LifecycleState.FINALIZED:
        node.shutdown()
    node.close()
```

A Node declares its Config and Ports before connecting. Loop supplies the
validated Config, Port bindings, and lifecycle operations.
Call `process_control()` regularly to handle lifecycle requests from Loop.
Use the Node lifecycle state to decide when your application should exchange
Graph data. In most cases, only exchange data while the Node is `ACTIVE`.

## Lifecycle

All Nodes use `IDLE → Configure → CONFIGURED → Start → ACTIVE`.
Configure validates Config and calls optional `on_configure(config)` before
Port binding. Start takes only bindings and calls optional `on_start()`.
Stop and ResetFault return to IDLE; each restart requires Configure again.
Configure may update existing payload contracts. The Orchestrator validates
the resulting graph before starting Nodes. Port names and kinds stay fixed.

Describe returns the Config schema and current Port contracts for inspection.
Loop uses Configure results for payload compatibility checks; contracts read
through Describe are not treated as finalized for a run.

## Shared-memory streams

Use the `shared_memory` communication profile for Stream ports between Nodes on
the same machine. It supports every payload value type: scalars, booleans,
strings, tensors, images, and audio. A message may contain multiple binary
fields, optional fields, or no binary fields. Logical types and payload
contract validation still apply. Request/Reply continues to use `protobuf`.

In a Cell Config, select `communication_profile: shared_memory` on the consumer
binding. In a low-level Graph Config, define a bounded profile:

```yaml
communication_profiles:
  local_shared:
    transport: zenoh
    stream_mode: shared_memory
    shared_memory_pool_size_bytes: 67108864
```

Reference `local_shared` from the consumer binding. The pool limit must cover
all binary fields in a message and messages retained by subscribers, with room
for allocation overhead. Allocation exhaustion raises an error; the transport
does not silently fall back to ordinary bytes. Profile selection is explicit;
this change does not automatically switch small messages to SHM.

The wire format has a Protobuf metadata attachment and one contiguous
SHM allocation. Each image, tensor, or audio field has an offset and byte length
in that allocation. Binary bytes are copied directly into their destination
ranges without first building a combined Protobuf byte string. Scalar fields
remain in the attachment. Messages with no binary bytes use one padding byte
because Zenoh requires a nonempty allocation; SHM offers no bulk-copy advantage
for those messages.

The receiver validates the message and all buffer references, layout, and sizes.
Image/audio buffers retain the original Zenoh Sample, including after
the subscriber closes. Zenoh Python 1.9.0 does not expose received SHM through
the Python buffer protocol: packed ranges share a lazy Python snapshot, and
tensor decoding materializes that snapshot to preserve `TensorValue.data` as
a read-only `memoryview`. This is not end-to-end zero-copy NumPy access.
