Adds reference/connector-contract.md and code/connectors.md to the site, with the node-type reference finally linking the contract it describes. Strict build passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
131 lines
4.5 KiB
Markdown
131 lines
4.5 KiB
Markdown
# Writing a connector
|
|
|
|
A walkthrough from an empty directory to a node on the canvas. The normative
|
|
surface is in [the connector contract](../reference/connector-contract.md);
|
|
this is how to use it.
|
|
|
|
## Start from the skeleton
|
|
|
|
A working connector, kept as small as a correct one can be, lives at
|
|
`skeleton/` in the [connectors repository](https://git.stroblme.de/Fluksio/connectors).
|
|
Copy it:
|
|
|
|
```bash
|
|
git clone https://git.stroblme.de/Fluksio/connectors.git
|
|
cp -r connectors/skeleton fluksio-connector-mydevice
|
|
cd fluksio-connector-mydevice
|
|
```
|
|
|
|
Rename the package (`src/fluksio_connector_skeleton/` and the `[project]` name
|
|
in `pyproject.toml`), then point the entry point at your class:
|
|
|
|
```toml
|
|
[project.entry-points."fluksio.node_types"]
|
|
my_device = "fluksio_connector_mydevice:MyDevice"
|
|
```
|
|
|
|
## Install it into the engine
|
|
|
|
Connectors are found through installed package metadata, so the engine has to
|
|
have yours installed. During development, install it as an editable path
|
|
dependency of the backend:
|
|
|
|
```bash
|
|
cd app/backend
|
|
uv add --editable ../../fluksio-connector-mydevice
|
|
```
|
|
|
|
Editing your connector's **code** then takes effect on the next engine restart,
|
|
with no reinstall. Changing its **metadata** — the entry point, the version,
|
|
the package name — needs `uv sync` to regenerate the installed metadata before
|
|
a restart picks it up.
|
|
|
|
Two things to know about this:
|
|
|
|
- `uv add` writes the path dependency into `backend/pyproject.toml` and
|
|
`uv.lock`. That path does not exist inside the backend's Docker image, so
|
|
**revert both files before committing**; the dependency belongs on your
|
|
machine, not in the repo. Distribution is a later milestone.
|
|
- For the same reason, develop against the **standalone backend** rather than
|
|
the Docker stack:
|
|
|
|
```bash
|
|
cd app && make dev-backend # FastAPI on :8000
|
|
make dev-frontend # Vite on :5173
|
|
```
|
|
|
|
Restart the backend and your node type appears in the add-node palette (⌘K),
|
|
labelled with the package it came from.
|
|
|
|
## Write the node
|
|
|
|
```python
|
|
from typing import Any
|
|
|
|
from pydantic import Field
|
|
from fluksio.flow.connector import CONTRACT_VERSION, ConnectorNode
|
|
|
|
|
|
class MyDevice(ConnectorNode):
|
|
contract = CONTRACT_VERSION
|
|
title = "My device"
|
|
description = "Reads temperature and humidity over the device's HTTP API."
|
|
|
|
class Params(ConnectorNode.Params):
|
|
host: str = "192.168.1.50"
|
|
api_key: str | None = Field(
|
|
default=None, json_schema_extra={"x-secret": True}
|
|
)
|
|
|
|
async def poll(self) -> dict[str, Any] | None:
|
|
reading = await self._read()
|
|
return {"temperature": reading.temp, "humidity": reading.humidity}
|
|
```
|
|
|
|
Then, on the canvas: place the node, set `poll_interval` to how often the
|
|
device should be read, pick the API key from the secrets store, and add output
|
|
ports named `temperature` and `humidity`. The port names are the wiring — any
|
|
node consuming those messages is downstream of this one.
|
|
|
|
## If the device is told rather than asked
|
|
|
|
A connector that commands something implements `write()` instead of, or
|
|
alongside, `poll()`. Input ports carry the values in:
|
|
|
|
```python
|
|
def write(self, **ports: Any) -> None:
|
|
self._device.set(ports["level"])
|
|
```
|
|
|
|
Place the node with *input* ports named `level`, and whatever publishes that
|
|
message drives the device. Set `idempotent = False` on the class: a write is a
|
|
command, and a redelivery after a crash should not undo a newer one.
|
|
|
|
Keep the first version off the wire. A boolean setting the code checks before
|
|
it sends — `artnet`'s `transmit` is the example — lets a flow be built and
|
|
watched in the logs before anything physically moves.
|
|
|
|
## Try it without a device
|
|
|
|
Give `poll` something predictable first and confirm the values reach the canvas
|
|
before pointing it at hardware:
|
|
|
|
```python
|
|
async def poll(self):
|
|
return {"temperature": 21.5}
|
|
```
|
|
|
|
Press Run, or wait for the poll interval, and the value appears on the edge.
|
|
Anything the connector prints shows up in the logs panel in the dock, and a
|
|
raised exception shows there with its traceback and marks the node down.
|
|
|
|
## Checklist before sharing it
|
|
|
|
- `contract = CONTRACT_VERSION` declared on the class itself.
|
|
- Credentials marked `x-secret`, never read from the environment.
|
|
- `stop()` is safe to call twice, and after a failed `start()`.
|
|
- Blocking I/O wrapped in `asyncio.to_thread`.
|
|
- `poll()` returns `None` rather than raising when there is simply nothing new.
|
|
- `write()` returns quickly; the thread it runs on is the scheduler's.
|
|
- Health reported when the connection comes and goes.
|