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,280 @@
|
||||
"""The persisted shape of a flow, shared by the store, the API and the editor.
|
||||
|
||||
A flow is structure plus code: this module is the structure. Node logic for
|
||||
``python`` nodes lives beside it as a plain ``.py`` file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from fluksio.flow.messages import MessageSpec
|
||||
|
||||
NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||
|
||||
|
||||
def _validate_name(value: str) -> str:
|
||||
if not NAME_PATTERN.match(value):
|
||||
raise ValueError(
|
||||
"Use lowercase letters, digits and underscores, starting with a letter"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class NodeDef(BaseModel):
|
||||
"""A node as stored: identity, configuration and ports.
|
||||
|
||||
Deliberately no canvas position. The editor lays a flow out itself, so
|
||||
where a node sits is a fact about the drawing rather than about the flow —
|
||||
and a graph nobody can arrange is one worth keeping small.
|
||||
"""
|
||||
|
||||
id: str
|
||||
type: str = "python"
|
||||
title: str = ""
|
||||
#: This node's settings: constants of its function, stored with the flow.
|
||||
#: A function node reads them as keyword arguments beside its ports, so a
|
||||
#: setting cannot share a name with one.
|
||||
params: dict[str, Any] = Field(default_factory=dict)
|
||||
requires: list[MessageSpec] = Field(default_factory=list)
|
||||
provides: list[MessageSpec] = Field(default_factory=list)
|
||||
#: Name of a shared source in the library, instead of this node's own file.
|
||||
#: Editing it edits the copy every flow using it runs.
|
||||
source_ref: str | None = None
|
||||
timeout: float | None = Field(
|
||||
default=None,
|
||||
gt=0,
|
||||
description=(
|
||||
"Seconds this node's code may run before it is stopped. This "
|
||||
"covers the first call's imports, which can be much slower than "
|
||||
"the body. Above 60 the engine may deliver its work again while "
|
||||
"it is still running — in a batch run, which never redelivers, "
|
||||
"it is an idle timeout instead: silence this long is a kill."
|
||||
),
|
||||
)
|
||||
device: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Label of the worker this node's code must run on, such as 'gpu'. "
|
||||
"Empty means the engine's own workers. A run needing a label no "
|
||||
"attached worker carries waits rather than failing."
|
||||
),
|
||||
)
|
||||
device_policy: Literal["require", "prefer"] = Field(
|
||||
default="require",
|
||||
description=(
|
||||
"What to do when no worker carries `device`: wait for one, or run "
|
||||
"locally anyway."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("id")
|
||||
@classmethod
|
||||
def _check_id(cls, value: str) -> str:
|
||||
return _validate_name(value)
|
||||
|
||||
|
||||
class FlowInput(BaseModel):
|
||||
"""A message the flow starts with rather than computes."""
|
||||
|
||||
spec: MessageSpec
|
||||
initial: Any | None = None
|
||||
|
||||
|
||||
class FlowDef(BaseModel):
|
||||
"""One atomic flow."""
|
||||
|
||||
name: str
|
||||
title: str = ""
|
||||
nodes: list[NodeDef] = Field(default_factory=list)
|
||||
inputs: list[FlowInput] = Field(default_factory=list)
|
||||
version: int = 1
|
||||
mode: Literal["live", "batch"] = Field(
|
||||
default="live",
|
||||
description=(
|
||||
"A live flow reacts to what arrives: its subscriptions, schedules "
|
||||
"and webhooks run until it is stopped. A batch flow only runs when "
|
||||
"a run asks it to, from its inputs to its outputs, and is never "
|
||||
"activated."
|
||||
),
|
||||
)
|
||||
outputs: list[str] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"Messages a batch run reports as its result, unqualified. Empty "
|
||||
"means every message the flow ends up holding."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def _check_name(cls, value: str) -> str:
|
||||
return _validate_name(value)
|
||||
|
||||
|
||||
class NodeSource(BaseModel):
|
||||
"""The Python source of a node."""
|
||||
|
||||
code: str
|
||||
|
||||
|
||||
Health = Literal["ok", "degraded", "down"]
|
||||
|
||||
|
||||
class NodeStatusPublic(BaseModel):
|
||||
"""Whether a node loaded, and how its connection is doing."""
|
||||
|
||||
id: str
|
||||
status: str = "active"
|
||||
error: str | None = None
|
||||
health: Health = "ok"
|
||||
health_detail: str | None = None
|
||||
#: The node's last runtime failure, kept after it runs again: a failure
|
||||
#: that fired an alert should leave a trace of what it was.
|
||||
last_error: str = ""
|
||||
last_error_ts: float | None = None
|
||||
|
||||
|
||||
class MessageValue(BaseModel):
|
||||
"""The last payload seen on a message."""
|
||||
|
||||
value: Any = None
|
||||
ts: float | None = None
|
||||
|
||||
|
||||
class HistoryPoint(BaseModel):
|
||||
"""One numeric value a message carried, and when."""
|
||||
|
||||
ts: float
|
||||
value: float
|
||||
|
||||
|
||||
class MessageHistory(BaseModel):
|
||||
"""A message's recent numeric values, oldest first.
|
||||
|
||||
Only numbers are recorded, so ``numeric`` tells the panel whether an empty
|
||||
series means "nothing plottable here" or "nothing has arrived yet".
|
||||
"""
|
||||
|
||||
message: str
|
||||
numeric: bool = False
|
||||
points: list[HistoryPoint] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FlowSummary(BaseModel):
|
||||
name: str
|
||||
title: str = ""
|
||||
node_count: int = 0
|
||||
error_count: int = 0
|
||||
has_draft: bool = False
|
||||
enabled: bool = True
|
||||
paused: bool = False
|
||||
# Its background tasks kept crashing, so the engine stopped restarting them.
|
||||
quarantined: bool = False
|
||||
#: Of the working copy, so publishing from a list needs no second read.
|
||||
version: int = 1
|
||||
|
||||
|
||||
class FlowsPublic(BaseModel):
|
||||
data: list[FlowSummary]
|
||||
count: int
|
||||
|
||||
|
||||
class LibraryNode(BaseModel):
|
||||
"""A node source shared across flows, and who is using it."""
|
||||
|
||||
name: str
|
||||
used_by: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FlowStatePublic(BaseModel):
|
||||
values: dict[str, MessageValue] = Field(default_factory=dict)
|
||||
nodes: list[NodeStatusPublic] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ModulePackage(BaseModel):
|
||||
"""One package installed in the venv node code runs on."""
|
||||
|
||||
name: str
|
||||
version: str
|
||||
|
||||
|
||||
class ModulesInfo(BaseModel):
|
||||
"""The venv node code imports from, and the manifest that describes it."""
|
||||
|
||||
python_version: str = ""
|
||||
venv_path: str = ""
|
||||
requirements: str = ""
|
||||
packages: list[ModulePackage] = Field(default_factory=list)
|
||||
#: Whether what is installed matches the manifest.
|
||||
applied: bool = False
|
||||
|
||||
|
||||
class ApplyRequest(BaseModel):
|
||||
"""A pip manifest, one requirement per line."""
|
||||
|
||||
requirements: str = ""
|
||||
|
||||
|
||||
class ApplyResult(BaseModel):
|
||||
ok: bool
|
||||
output: str = ""
|
||||
|
||||
|
||||
class BrainNode(BaseModel):
|
||||
"""One neuron: a thing the engine talks to, or a node that only computes.
|
||||
|
||||
Nodes of the same type pointing at the same outside thing — one broker
|
||||
topic, one URL, one bucket — are a single entry here, whichever flows they
|
||||
sit in. ``members`` are the ``flow.node_id`` names behind it, which is also
|
||||
what the live events are keyed by.
|
||||
"""
|
||||
|
||||
id: str
|
||||
label: str
|
||||
kind: str
|
||||
members: list[str] = Field(default_factory=list)
|
||||
flows: list[str] = Field(default_factory=list)
|
||||
issue: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Why this neuron cannot run, if validation found something. A "
|
||||
"failure the engine hits while running arrives over the socket "
|
||||
"instead; this is the part that is already true before anything "
|
||||
"fires, and so has to travel with the graph."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class BrainEdge(BaseModel):
|
||||
"""Messages carrying values from one neuron to another."""
|
||||
|
||||
source: str
|
||||
target: str
|
||||
messages: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class BrainGraph(BaseModel):
|
||||
"""Every published flow at once, merged on what its nodes talk to."""
|
||||
|
||||
nodes: list[BrainNode] = Field(default_factory=list)
|
||||
edges: list[BrainEdge] = Field(default_factory=list)
|
||||
|
||||
|
||||
class NodeTypeInfo(BaseModel):
|
||||
"""A node type the editor can offer, with its parameter schema."""
|
||||
|
||||
type: str
|
||||
title: str
|
||||
description: str
|
||||
params_schema: dict[str, Any] = Field(default_factory=dict)
|
||||
has_source: bool = False
|
||||
#: Whether this type takes settings beyond the ones its schema declares.
|
||||
#: A function node's settings are its author's to name, and reach `process`
|
||||
#: as keyword arguments beside its ports.
|
||||
free_params: bool = False
|
||||
#: The package a connector came from; empty for the built-in types.
|
||||
plugin: str | None = None
|
||||
Reference in New Issue
Block a user