Add the flow API: typed messages, git-backed store, REST and live events

Makes the flow engine reachable from the API, which is what M3 needs before
any of it can reach the browser.

- app/flow is a package now; the prototype's watch-dir scripts and the
  matplotlib/networkx visualiser are gone with their dependencies.
- Messages carry a serializable dtype instead of a live Python type, and a
  port name, so the graph can speak qualified names while node functions keep
  local arguments. Redis state is JSON, not pickle.
- Message names are namespaced per flow ("heating.temp"); a bare name resolves
  to its own flow, a dotted one crosses flows.
- Several nodes may provide the same message: producers are a list, so fan-in
  is a real edge instead of a silently dropped one.
- Flows are stored as flow.json plus node sources in a git repository, one
  commit per save, with identical saves skipped so autosave stays quiet.
- Node failures are isolated and reported per node; validate() returns cycles
  and unconnected inputs instead of raising deep in a run.
- Credentials live in an encrypted store and are referenced as {"$secret": …}.
- Engine events reach websocket clients through a bus, so values, node status
  and execution show up live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WzrvW7rjQbynnhF6pxh6i
This commit is contained in:
Melvin Strobl
2026-08-15 17:23:36 +02:00
co-authored by Claude Fable 5
parent 61be29827d
commit 06a4506767
54 changed files with 2586 additions and 4978 deletions
+16 -11
View File
@@ -19,12 +19,9 @@ dependencies = [
"sentry-sdk[fastapi]<2.0.0,>=1.40.6",
"pyjwt<3.0.0,>=2.8.0",
"pwdlib[argon2,bcrypt]>=0.3.0",
"networkx>=3.4.2",
"numpy>=2.2.6",
"matplotlib>=3.10.8",
"redis>=7.1.0",
"scipy>=1.15.3",
"watchfiles>=1.0.0",
"cryptography>=44.0.0",
"aiomqtt>=2.0.0",
"influxdb-client[async]>=1.40.0",
"croniter>=1.3.0",
@@ -45,16 +42,18 @@ build-backend = "hatchling.build"
[tool.mypy]
strict = true
# app/flow is the standalone flow-engine prototype: it is not importable as a
# package yet (no __init__.py, top-level sibling imports) and nothing in the API
# reaches it. Type-checking it would only report on code that is scheduled to be
# restructured — see NOTEPAD.md. Drop the exclusion when it is wired up.
exclude = ["venv", ".venv", "alembic", "app/flow"]
exclude = ["venv", ".venv", "alembic"]
# app/flow/nodes.py still carries the prototype's node classes, and state.py's
# redis calls type as sync/async unions under the current stubs. Both are being
# revisited; every module around them is checked strictly.
[[tool.mypy.overrides]]
module = ["app.flow.nodes", "app.flow.pipeline", "app.flow.state"]
ignore_errors = true
[tool.ruff]
target-version = "py310"
# See the note on the same exclusion under [tool.mypy].
exclude = ["alembic", "app/flow"]
exclude = ["alembic"]
[tool.ruff.lint]
select = [
@@ -75,6 +74,12 @@ ignore = [
"B904", # Allow raising exceptions without from e, for HTTPException
]
[tool.ruff.lint.per-file-ignores]
# Node functions take `params` whether or not they use it — that is the
# contract the engine calls them with.
"app/flow/nodes.py" = ["ARG001", "ARG002"]
"tests/flow/*" = ["ARG001"]
[tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true