Files
app/backend/pyproject.toml
T
stroblmeandClaude Fable 5 eb2d098d7c Remote workers: a GPU box dials in and runs the nodes bound to it
The engine runs where the automations are and the GPU is somewhere else,
usually behind a different network — so the worker connects out and the engine
answers over the socket it was given. Nothing has to expose Redis, and the
same connection works through the tunnel the hosted access will use.

What travels is the protocol the local pool already speaks, so a node cannot
tell which kind of worker it is on. A node declares device: gpu and
device_policy, the label is resolved per call (a worker attaching later needs
no rebuild), and a run whose labels nothing carries waits in the queue saying
what it waits for rather than failing — submit from the couch, the GPU box
picks it up when it is switched on.

Two things had to move with it. Compiling now happens on the machine that will
run the node: a node importing torch is correct on the GPU box and a missing
module on the engine, so checking it here failed nodes that were fine. And the
artifact endpoint accepts a worker's own credential, because storing a
checkpoint is exactly what that credential is for — and only that.

Verified against the real split: the training ran on this host (its checkpoint
names the machine and a numpy the engine does not have), streamed 40 metric
points back mid-run, and the evaluate node read the checkpoint on the engine.
Cancel kills the remote training; pulling the worker fails the run in six
seconds instead of waiting out its ten-minute timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
2026-08-18 17:52:42 +02:00

113 lines
3.3 KiB
TOML

[project]
name = "app"
version = "0.1.0"
description = ""
# Capped below 3.14: the MCP SDK wants a newer starlette there than the
# pinned sentry-sdk allows. Lift it when sentry-sdk moves to 2.x.
requires-python = ">=3.10,<3.14"
dependencies = [
"fastapi[standard]<1.0.0,>=0.114.2",
"python-multipart<1.0.0,>=0.0.7",
"email-validator<3.0.0.0,>=2.1.0.post1",
"tenacity<9.0.0,>=8.2.3",
"pydantic>2.0",
"emails<1.0,>=0.6",
"jinja2<4.0.0,>=3.1.4",
"alembic<2.0.0,>=1.12.1",
"httpx<1.0.0,>=0.25.1",
"psycopg[binary]<4.0.0,>=3.1.13",
"sqlmodel<1.0.0,>=0.0.21",
"pydantic-settings<3.0.0,>=2.2.1",
"sentry-sdk[fastapi]<2.0.0,>=1.40.6",
"pyjwt<3.0.0,>=2.8.0",
"pwdlib[argon2,bcrypt]>=0.3.0",
"numpy>=2.2.6",
"redis>=7.1.0",
"cryptography>=44.0.0",
"aiomqtt>=2.0.0",
"influxdb-client[async]>=1.40.0",
"croniter>=1.3.0",
"mcp>=1.29,<2",
]
[dependency-groups]
dev = [
"pytest<8.0.0,>=7.4.3",
"mypy<2.0.0,>=1.8.0",
"ruff<1.0.0,>=0.2.2",
"prek>=0.2.24,<1.0.0",
"coverage<8.0.0,>=7.4.3",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.mypy]
strict = true
exclude = ["venv", ".venv", "alembic"]
# influxdb_client's Point builder carries no annotations; calling it is not a
# reason to stop checking the caller (app/flow/nodes/influx.py).
untyped_calls_exclude = ["influxdb_client"]
# Every module is checked strictly. influxdb_client is typed but re-exports its
# names implicitly, which strict mode refuses to follow. (croniter, the other
# untyped dependency, is ignored at its two import sites.)
[[tool.mypy.overrides]]
module = ["influxdb_client"]
implicit_reexport = true
[tool.ruff]
target-version = "py310"
exclude = ["alembic"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
"T201", # print statements are not allowed
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"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"]
# Printing is what this one is about: node code is user code, and `print` is
# how it says things.
"tests/flow/test_logs.py" = ["ARG001", "T201"]
# This one takes its own directory off sys.path before the rest of its imports
# run, which is the whole point of doing it there.
"app/flow/worker_main.py" = ["E402"]
# A standalone script copied onto another machine: it has no logger configured
# before it tells you the one dependency it is missing.
"app/worker/fluksio_worker.py" = ["T201"]
[tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true
[tool.coverage.run]
source = ["app"]
dynamic_context = "test_function"
[tool.coverage.report]
show_missing = true
sort = "-Cover"
[tool.coverage.html]
show_contexts = true