Rename the import package app to fluksio

A wheel whose top-level module is `app` collides with anything else in a
user's venv, so the package that is about to be published takes the name
it is published under. Only the Python package moves; the repo, the
Docker WORKDIR and the compose project keep theirs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 21:48:05 +02:00
co-authored by Claude Opus 5
parent 97785ee590
commit 60d7ec81c0
170 changed files with 629 additions and 619 deletions
+106
View File
@@ -0,0 +1,106 @@
"""Exec: run a command and hand back what it said.
The command runs inside the backend container, not on the host. That matters
when porting: a flow that read the host's journal or poked a host script needs
either a mount or a small listener on the host side, not this node.
"""
from __future__ import annotations
import logging
import shlex
import subprocess
from collections.abc import Iterable
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from fluksio.flow.messages import MessageSpec
from fluksio.flow.nodes.base import Node
logger = logging.getLogger(__name__)
class ExecNode(Node):
"""Run a command, returning its output, error text and exit code."""
# Running a command twice is running it twice.
idempotent = False
class Params(BaseModel):
model_config = ConfigDict(extra="allow")
command: str = Field(description="The command to run.")
append_payload: bool = Field(
default=False,
description="Add the incoming value to the command as one argument.",
)
timeout: float = Field(
default=30.0, gt=0, description="Give up after this many seconds."
)
fail_on_error: bool = Field(
default=False,
description="Treat a non-zero exit as a node failure rather than output.",
)
__slots__ = ("cfg",)
def __init__(
self,
requires: MessageSpec | Iterable[MessageSpec] = (),
provides: MessageSpec | Iterable[MessageSpec] = (),
params: dict[str, Any] | None = None,
name: str | None = None,
):
self.cfg = self.Params.model_validate(params or {})
super().__init__(
f=self._run,
requires=requires,
provides=provides,
params=params,
name=name or "exec",
)
def _run(self, params: dict[str, Any], **kwargs: Any) -> dict[str, Any] | None:
argv = shlex.split(self.cfg.command)
if not argv:
raise ValueError("exec node has no command")
if self.cfg.append_payload and kwargs:
argv.append(str(next(iter(kwargs.values()))))
try:
# No shell: the command is a list, so a value carrying a semicolon
# is an argument rather than a second command.
completed = subprocess.run(
argv,
capture_output=True,
text=True,
timeout=self.cfg.timeout,
check=False,
)
except subprocess.TimeoutExpired as exc:
raise TimeoutError(
f"'{argv[0]}' did not finish within {self.cfg.timeout}s"
) from exc
except FileNotFoundError as exc:
raise FileNotFoundError(
f"'{argv[0]}' is not available in this container"
) from exc
if self.cfg.fail_on_error and completed.returncode != 0:
raise RuntimeError(
f"'{argv[0]}' exited {completed.returncode}: "
f"{completed.stderr.strip()[:200]}"
)
available = {
"stdout": completed.stdout,
"stderr": completed.stderr,
"code": completed.returncode,
}
# Ports named after one of those get it; anything else gets stdout,
# which is what a single-output exec node is almost always after.
return {
spec.port: available.get(spec.port, completed.stdout)
for spec in self.output_ports
} or None