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,98 @@
|
||||
"""File: read a file into the graph, or write one out of it.
|
||||
|
||||
Confined to a directory the engine owns. A flow that could name any path
|
||||
would be a way to read the secrets store or overwrite a node's source.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from fluksio.flow.messages import MessageSpec
|
||||
from fluksio.flow.nodes.base import Node
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def files_root() -> Path:
|
||||
"""Where flow-readable files live: beside the flow store, not inside it."""
|
||||
from fluksio.core.config import settings
|
||||
|
||||
return settings.FLOWS_DIR.parent / "files"
|
||||
|
||||
|
||||
def resolve(name: str) -> Path:
|
||||
"""Turn a flow-supplied name into a path inside the sandbox, or refuse."""
|
||||
root = files_root().resolve()
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
target = (root / name).resolve()
|
||||
if target != root and root not in target.parents:
|
||||
raise ValueError(f"'{name}' is outside the files directory")
|
||||
return target
|
||||
|
||||
|
||||
class FileNode(Node):
|
||||
"""Read or write a file under the engine's files directory."""
|
||||
|
||||
class Params(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
path: str = Field(description="Path relative to the engine's files directory.")
|
||||
mode: Literal["read", "write", "append"] = "read"
|
||||
format: Literal["text", "json"] = Field(
|
||||
default="text", description="Parse or serialize the contents as JSON."
|
||||
)
|
||||
newline: bool = Field(
|
||||
default=True, description="End each written record with a newline."
|
||||
)
|
||||
|
||||
__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._act,
|
||||
requires=requires,
|
||||
provides=provides,
|
||||
params=params,
|
||||
name=name or f"file_{self.cfg.mode}",
|
||||
)
|
||||
|
||||
@property
|
||||
def idempotent(self) -> bool: # type: ignore[override]
|
||||
# Reading twice is harmless; appending twice writes the line twice.
|
||||
return self.cfg.mode == "read"
|
||||
|
||||
def _act(self, params: dict[str, Any], **kwargs: Any) -> dict[str, Any] | None:
|
||||
target = resolve(self.cfg.path)
|
||||
|
||||
if self.cfg.mode == "read":
|
||||
if not target.exists():
|
||||
raise FileNotFoundError(f"'{self.cfg.path}' does not exist")
|
||||
raw = target.read_text()
|
||||
value = json.loads(raw) if self.cfg.format == "json" else raw
|
||||
return {spec.port: value for spec in self.output_ports} or None
|
||||
|
||||
if not kwargs:
|
||||
return None
|
||||
value = next(iter(kwargs.values()))
|
||||
text = json.dumps(value) if self.cfg.format == "json" else str(value)
|
||||
if self.cfg.newline:
|
||||
text += "\n"
|
||||
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with target.open("a" if self.cfg.mode == "append" else "w") as handle:
|
||||
handle.write(text)
|
||||
return None
|
||||
Reference in New Issue
Block a user