Files
app/backend/fluksio/flow/nodes/ntfy.py
T
stroblmeandClaude Opus 5 640654bd66 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>
2026-08-21 21:48:05 +02:00

88 lines
2.7 KiB
Python

"""Ntfy: push a notification to a phone.
The flow-level counterpart to the engine's own alerting — this is a flow
deciding something is worth saying, rather than the engine reporting that it
broke.
"""
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
from fluksio.flow.nodes.http import shared_client
logger = logging.getLogger(__name__)
class NtfyNode(Node):
"""Publish an incoming value as a notification on an ntfy topic."""
# A notification sent twice is read twice.
idempotent = False
class Params(BaseModel):
model_config = ConfigDict(extra="allow")
server: str = Field(
default="https://ntfy.sh", description="Base URL of the ntfy server."
)
topic: str = Field(description="Topic to publish to.")
title: str = Field(default="", description="Notification title.")
priority: str = Field(
default="default",
description="min, low, default, high or urgent.",
)
tags: str = Field(default="", description="Comma-separated ntfy tags.")
token: str = Field(
default="",
description="Access token, for a server that needs one.",
json_schema_extra={"x-secret": True},
)
timeout: float = Field(default=10.0, gt=0)
__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._notify,
requires=requires,
provides=provides,
params=params,
name=name or "ntfy",
)
def _notify(self, params: dict[str, Any], **kwargs: Any) -> dict[str, Any] | None:
if not kwargs:
return None
body = str(next(iter(kwargs.values())))
headers = {"Priority": self.cfg.priority}
if self.cfg.title:
headers["Title"] = self.cfg.title
if self.cfg.tags:
headers["Tags"] = self.cfg.tags
if self.cfg.token:
headers["Authorization"] = f"Bearer {self.cfg.token}"
response = shared_client().post(
f"{self.cfg.server.rstrip('/')}/{self.cfg.topic}",
content=body.encode(),
headers=headers,
timeout=self.cfg.timeout,
)
response.raise_for_status()
return None