Metadata-Version: 2.5
Name: pyflowred
Version: 0.1.0
Summary: A Python port of Node-RED: the same editor, the same flow semantics, a Python runtime.
Project-URL: Homepage, https://github.com/DataVoyage/pyred
Project-URL: Documentation, https://github.com/DataVoyage/pyred/blob/main/docs/operations.md
Project-URL: Source, https://github.com/DataVoyage/pyred
Project-URL: Changelog, https://github.com/DataVoyage/pyred/blob/main/CHANGELOG.md
Author: Melvin Neffle
License: Apache-2.0
Keywords: automation,flow,iot,low-code,node-red,streaming
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.13
Requires-Dist: bcrypt>=4.2
Requires-Dist: beautifulsoup4>=4.12
Requires-Dist: chevron>=0.14
Requires-Dist: confluent-kafka>=2.5
Requires-Dist: croniter>=6.0
Requires-Dist: cryptography>=44.0
Requires-Dist: google-auth>=2.35
Requires-Dist: httpx>=0.28
Requires-Dist: jsonata-python>=0.7.0
Requires-Dist: jsonschema>=4.23
Requires-Dist: lxml>=5.3
Requires-Dist: obstore>=0.11
Requires-Dist: packaging>=24.0
Requires-Dist: paho-mqtt>=2.1
Requires-Dist: prometheus-client>=0.21
Requires-Dist: python-dateutil>=2.9
Requires-Dist: python-multipart>=0.0.20
Requires-Dist: pyyaml>=6.0
Requires-Dist: starlette>=0.41
Requires-Dist: uvicorn[standard]>=0.32
Requires-Dist: watchfiles>=1.0
Requires-Dist: websockets>=14.0
Provides-Extra: dev
Requires-Dist: playwright>=1.49; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# PyRED

A port of [Node-RED](https://nodered.org) 5.0.7 to Python.

The same editor, the same flow file, the same node set, the same HTTP admin
API — with a Python runtime and nodes written in Python.

![The PyRED editor](docs/editor.png)

## What "port" means here

| Layer | Origin |
|---|---|
| Editor client | Node-RED source, vendored and built with the same pipeline |
| Node edit dialogs and message catalogues | Node-RED source, adapted only where the runtime language shows through |
| HTTP admin API | Re-implemented in Python, wire-compatible with the editor |
| Flow runtime, node registry, context, credentials, storage | Re-implemented in Python |
| Projects (git-backed flows), multiplayer, palette catalogue | Re-implemented in Python |
| Prometheus metrics endpoint and exporter nodes | New; the nodes ported from `node-red-contrib-prometheus-exporter` |
| Core nodes | Re-implemented in Python — all 50 types |

The editor is not a reimplementation. It is Node-RED's own editor, talking to
a Python implementation of the API it expects. Flows exported from Node-RED
import into PyRED and back again: the flow file is the same JSON, and the
credentials file uses the same encryption.

See [docs/porting.md](docs/porting.md) for the full correspondence.

## Where it deliberately differs

Two things cannot be carried over one-to-one, because they *are* the
JavaScript runtime:

1. **The Function node runs Python.** The same three tabs (*On Start*, *On
   Message*, *On Stop*), the same `node` / `flow` / `env` objects, the same
   send and done semantics — but the body is Python:

   ```python
   msg["payload"] = msg["payload"] * 2
   return msg
   ```

   The body is compiled into an `async def`, so `await` works directly.
   `global` is a Python keyword, so the global context object is `global_`.

2. **Packages come from PyPI.** The Function node's *Setup* tab and the
   palette manager install Python distributions. A PyRED node package is an
   ordinary distribution that advertises a `pyred.nodes` entry point.

## Installing

The distribution is **`pyflowred`** on PyPI — `pyred` was already taken there
by an unrelated project. The import package, the command and the editor are
all still PyRED.

```bash
pip install pyflowred     # or: uv pip install pyflowred
pyred                     # pyflowred runs the same thing
```

The wheel carries the built editor, so this needs Python and nothing else —
no Node, no build step. The editor is then at <http://127.0.0.1:1880/>.

## In a container

Two images, same application. `Dockerfile` installs the released package with
`uv` and carries no build tooling; `Dockerfile.source` builds this checkout in
three stages, Node included.

```bash
docker build -t pyred:0.1.0 .                        # released
docker build -f Dockerfile.source -t pyred:source .  # this checkout
docker run -d -p 1880:1880 -v pyred-data:/data pyred:0.1.0
```

Your own CA is a build argument or a mount, and reaches every library in the
runtime. See [docker/README.md](docker/README.md).

## Building it

Node.js is needed once, to build the editor. It is never needed at runtime.

```bash
npm install          # editor build dependencies
npm run build        # builds src/pyred/editor_client/public
uv build --wheel     # ~10 MB, py3-none-any
```

That build output is a git-ignored artifact, so the wheel has to pull it back
in deliberately (`tool.hatch.build.targets.wheel.artifacts`). A build hook
refuses to produce a wheel when the editor has not been built, because the
failure mode otherwise is a package that installs, starts, and serves a blank
page. The editor *source* is excluded from the wheel and kept in the sdist,
where it can be rebuilt.

## Working on it

```bash
npm install && npm run build
uv venv --python 3.13
uv pip install -e ".[dev]"
.venv/bin/pyred
```

> Without `adminAuth`, anyone who can reach the editor can run arbitrary
> Python as the user running the process. See
> [docs/operations.md](docs/operations.md#protecting-the-editor).

## Writing a node

```python
def register(RED):
    class LowerCaseNode(RED.Node):
        def __init__(self, config):
            super().__init__(config)
            self.on("input", self._on_input)

        def _on_input(self, msg, send, done):
            msg["payload"] = msg["payload"].lower()
            send(msg)
            done()

    RED.nodes.register_type("lower-case", LowerCaseNode)
```

with the matching `.html` next to it, exactly as in Node-RED. A complete
example package is in `examples/pyred-node-lowercase`; the details are in
[docs/writing-nodes.md](docs/writing-nodes.md).

## Layout

```
src/pyred/
  util/            property expressions, JSONata, mustache, cloning, logging, i18n
  runtime/         flow engine, node registry, context, credentials, storage
  registry/        node discovery, loading, palette installation
  api/             HTTP admin API, editor routes, comms WebSocket (Starlette)
  metrics/         the Prometheus registry, runtime instrumentation, endpoint
  nodes/core/      the core node set: Python implementations + Node-RED dialogs
  editor_client/   the Node-RED editor, vendored and built
tools/
  editor-build/    the Node-RED editor build pipeline, re-pointed at this repo
  devserver.sh     start/stop a throwaway instance
  uicheck.py       load the editor in a real browser and report on it
  uiflow.py        deploy and run a flow through the editor, in a real browser
  uiprojects.py    commit a deploy through the project UI, in a real browser
  benchflows.py    seven benchmark flows, 82 nodes across 33 node types
  loadtest.py      drive them and sample the runtime's CPU, RSS and fds
  rawbaseline.py   a bare Starlette endpoint, to measure the runtime against
Dockerfile         the released package, installed with uv
Dockerfile.source  this checkout, editor and all
docker/            entrypoint, compose, CA drop-in
examples/          an example node package
docs/              porting notes, node authoring, operations, benchmarks
tests/             the test suite, including a browser test of the editor
```

## Tests

```bash
.venv/bin/python -m pytest tests
```

The suite covers every node group against real sockets (including an
in-process MQTT broker), the runtime (subflows, groups, context, credentials,
deploy types), the admin API, projects against a real `git` binary, and — when
Playwright is available — the actual editor in a headless browser: import a
flow, press Deploy, open the Function node, click Inject, read the debug
sidebar.

## Licence

Apache-2.0, as Node-RED is. The vendored editor and node dialogs keep their
original copyright headers.
