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
+97
View File
@@ -0,0 +1,97 @@
"""Trigger: send one thing now, another if nothing follows.
Node-RED's *trigger*. The shape it exists for is "the door opened — turn the
light on, and off again in two minutes unless it opens again", which is
awkward to express any other way.
"""
from __future__ import annotations
import logging
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 TriggerNode(Node):
"""Emit ``first`` on arrival, then ``then`` once the wait runs out.
A value arriving during the wait extends it, so the second emission only
happens when things have actually gone quiet.
"""
class Params(BaseModel):
model_config = ConfigDict(extra="allow")
first: Any = Field(default=True, description="Sent as soon as a value arrives.")
then: Any = Field(
default=False,
description="Sent when the wait expires. Empty sends nothing.",
)
wait: float = Field(
default=60.0, gt=0, description="Seconds of quiet before the second value."
)
extend: bool = Field(
default=True,
description="A value arriving during the wait starts it over.",
)
__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._trigger,
requires=requires,
provides=provides,
params=params,
name=name or "trigger",
)
def _outputs(self, value: Any) -> dict[str, Any]:
return {spec.port: value for spec in self.output_ports}
def _trigger(self, params: dict[str, Any], **kwargs: Any) -> dict[str, Any] | None:
if not kwargs or not self.output_ports:
return None
armed = self.recall("armed", 0)
if armed and not self.cfg.extend:
# Already counting down and not extending: ignore the new value.
return None
# A generation counter, so a value arriving mid-wait invalidates the
# deferred emission that was already scheduled.
generation = int(self.recall("generation", 0)) + 1
self.remember("generation", generation)
self.remember("armed", 1)
if self.cfg.then is not None and self._pipeline is not None:
deferred = self._outputs(self.cfg.then)
scheduled = self._pipeline.defer(
self,
self._to_messages(deferred) or {},
self.cfg.wait,
guard=("generation", generation),
)
if not scheduled:
logger.warning(
"Node '%s' cannot wait without a work queue; sending only "
"its first value",
self.name,
)
self.remember("armed", 0)
return self._outputs(self.cfg.first)