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:
@@ -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)
|
||||
Reference in New Issue
Block a user