Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m14s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m53s
pre-commit / pre-commit (push) Failing after 2m13s
Test Backend / test-backend (push) Successful in 2m38s
Compose Smoke Test / test-compose (push) Successful in 38s
Playwright Tests / merge-reports (push) Successful in 1m8s
`GET /api/v1/search/` hands the client one flat index of everything worth jumping to — flows and the nodes inside them, dashboards and the widgets on them, panels, secrets, modules, workers and alert channels — and cmdk matches it in the browser, so results narrow while typing without a round trip per keystroke. A node hit is the one thing no list endpoint could answer: it opens its flow with that node in focus. The panel is reached from **Search** above Documentation in the sidebar, or ⌘K anywhere. The flow canvas palette moves to ⌘P, being the narrower of the two. The panels dialog gains an address (`/dashboards?panels`) so a panel hit has somewhere to land, and the sidebar logo gets `shrink-0`: the rail's width animates while the logo is already back, and a flex item short of room is squeezed rather than clipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016vGH7jqcXxWKP9wZFPyVdU
146 lines
5.0 KiB
Markdown
146 lines
5.0 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 (⌘P),
|
|
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.
|
|
|
|
## If the reading is bytes
|
|
|
|
A camera or a microphone publishes a reference rather than the bytes:
|
|
|
|
```python
|
|
async def poll(self):
|
|
jpeg = await asyncio.to_thread(self._grab)
|
|
return {"frame": self.save_artifact(jpeg, "f.jpg", media_type="image/jpeg")}
|
|
```
|
|
|
|
Place the node with an `image`-typed output port and a Media widget draws each
|
|
frame as it lands. `fluksio-connector-test-media` publishes test frames and
|
|
tones this way, so the whole path can be wired up with no camera in the room —
|
|
copy it if yours is a media device.
|
|
|
|
## 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.
|