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 df05e3a62a
commit 640654bd66
170 changed files with 629 additions and 619 deletions
+113
View File
@@ -0,0 +1,113 @@
"""Capture what a node prints, so the editor can show it.
Node code is written by the user and ``print`` is the obvious way to look at a
value, but a node runs on a pool thread and its output would otherwise land
unattributed in the server log. ``sys.stdout`` is replaced once by a tee that
also hands what it is given to whichever capture is active on *this* thread —
so only node executions are captured, and everything else passes through
untouched.
"""
from __future__ import annotations
import io
import sys
import traceback
from collections.abc import Callable, Iterator
from contextlib import contextmanager
from contextvars import ContextVar
from typing import TextIO
# Set for the duration of one node execution, on the thread running it.
_sink: ContextVar[Callable[[str], None] | None] = ContextVar(
"fluksio_node_log_sink", default=None
)
class _Tee(io.TextIOBase):
"""Writes through to the real stream, and to the active capture."""
def __init__(self, real: TextIO) -> None:
self._real = real
def write(self, text: str) -> int:
sink = _sink.get()
if sink is not None and text:
sink(text)
return self._real.write(text)
def flush(self) -> None:
self._real.flush()
def isatty(self) -> bool:
return self._real.isatty()
def install() -> None:
"""Put the tee in place. Safe to call more than once."""
if not isinstance(sys.stdout, _Tee):
sys.stdout = _Tee(sys.stdout)
if not isinstance(sys.stderr, _Tee):
sys.stderr = _Tee(sys.stderr)
@contextmanager
def capture(sink: Callable[[str], None]) -> Iterator[None]:
"""Send everything printed on this thread to ``sink`` for the duration."""
token = _sink.set(sink)
try:
yield
finally:
_sink.reset(token)
def node_traceback() -> str:
"""The exception being handled, from the node's own code onward.
The frames above it are the engine calling the node, which is noise to the
person who wrote it — the same trimming ``_short_error`` does for the one
line shown on the node itself.
"""
exc_type, exc, tb = sys.exc_info()
if exc is None:
return ""
# A node running out of process already trimmed its own; the frames on this
# side are the RPC that carried it.
remote = getattr(exc, "remote_traceback", "")
if remote:
return str(remote)
frames = traceback.extract_tb(tb)
start = next(
(i for i, frame in enumerate(frames) if frame.filename.startswith("<node ")),
0,
)
return "".join(
["Traceback (most recent call last):\n"]
+ traceback.format_list(frames[start:])
+ traceback.format_exception_only(exc_type, exc)
)
class Collector:
"""Gathers one execution's output, with a ceiling on how much it keeps."""
__slots__ = ("chunks", "truncated", "_limit", "_size", "_max_bytes")
def __init__(self, limit: int = 200, max_bytes: int = 8192) -> None:
self.chunks: list[str] = []
self.truncated = False
self._limit = limit
self._max_bytes = max_bytes
self._size = 0
def __call__(self, text: str) -> None:
if len(self.chunks) >= self._limit or self._size >= self._max_bytes:
self.truncated = True
return
self.chunks.append(text)
self._size += len(text)
@property
def text(self) -> str:
out = "".join(self.chunks)
return out[: self._max_bytes] if len(out) > self._max_bytes else out