One image for the whole app: engine, API and dashboard on one port

The backend image now builds the SPA and serves it on the API's own origin
(fluksio/spa.py), so an instance is a single container with no proxy, DNS,
port 80 or .env: `docker compose up -d` with the new compose.yml runs it on
localhost:8080. It starts on fluksio serve (persisted secret key, first
admin printed once) and brings its own Redis when REDIS_HOST is unset.

Slimmer: python:3.13-slim, no dev group, uv once, fastapi without the cloud
CLI. Optional features are per-feature extras (mqtt, influx, email, sentry,
ml, server for all) picked with the EXTRAS build argument; numpy moved to
`ml`. ffmpeg left the image for imageio-ffmpeg via the Modules screen.

The operator stack runs the same image with app. and api. routed to it; the
nginx frontend service, the LAN nginx proxy and DOMAIN in the backend are
gone, and the MCP issuer is FRONTEND_HOST.
This commit is contained in:
2026-10-16 11:40:02 +02:00
parent 9d9dac0bd0
commit dea25ec08d
39 changed files with 573 additions and 728 deletions
+12 -14
View File
@@ -5,24 +5,19 @@
# Domain Traefik routes on and acquires TLS certificates for.
DOMAIN=localhost
# Used by the backend to build links in outgoing emails.
# The address people and devices reach this instance at: panel links, mails
# and the MCP issuer are built from it.
FRONTEND_HOST=http://localhost:5173
# local, staging, production
ENVIRONMENT=local
# Baked into the SPA at build time: the address the interface calls. The
# workspace root's scripts/setup.sh derives it from DOMAIN and ENVIRONMENT —
# plain http for a local target, where nothing terminates TLS.
VITE_API_URL=http://api.localhost
PROJECT_NAME="Fluksio"
STACK_NAME=fluksio-app
# Backend
# `http://frontend` is the compose-network origin the Playwright job loads the
# SPA from (docker/compose.ci.yml); without it every spec fails CORS at login.
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,http://app.localhost,https://localhost,https://localhost:5173,http://frontend"
# Backend. The image serves the dashboard on the API's own origin, so these
# only matter to a dashboard served from elsewhere, such as the Vite server.
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,http://app.localhost,https://localhost,https://localhost:5173"
SECRET_KEY=changethis
FIRST_SUPERUSER=admin@fluksio.com
FIRST_SUPERUSER_PASSWORD=changethis
@@ -49,16 +44,19 @@ POSTGRES_DB=app
POSTGRES_USER=postgres
POSTGRES_PASSWORD=changethis
# Redis — flow engine state. Empty keeps the state in memory; the compose stack
# points the backend at its own `redis` service.
# Redis — flow engine state. Empty keeps the state in memory in a checkout, and
# the image starts a Redis of its own; the compose stack points the backend at
# its own `redis` service.
REDIS_HOST=
REDIS_PORT=6379
SENTRY_DSN=
# Docker registry images
# Docker registry image
DOCKER_IMAGE_BACKEND=fluksio-backend
DOCKER_IMAGE_FRONTEND=fluksio-frontend
# Optional features built into it, comma separated: mqtt, influx, email,
# sentry, ml, or server for all of them. Empty is the minimal engine.
EXTRAS=server
# The MCP endpoint agents connect to, and the OAuth server behind it.
MCP_ENABLED=true
+4 -6
View File
@@ -30,8 +30,8 @@ concurrency:
env:
# The playwright runner service comes from the dev overlay. It loads the SPA
# from the nginx `frontend` service and reaches the API as http://backend:8000
# on the project network — both wired up in compose.ci.yml.
# and reaches the API on http://backend:8000, one origin on the project
# network, wired up in compose.ci.yml.
COMPOSE_FILE: docker/compose.yml:docker/compose.dev.yml:docker/compose.ci.yml
# frontend/playwright.config.ts keys the blob reporter, the retries and
# forbidOnly off CI, and compose.dev.yml forwards it into the container.
@@ -63,11 +63,9 @@ jobs:
# Not committed; compose interpolates it and the runner container reads
# FIRST_SUPERUSER / FIRST_SUPERUSER_PASSWORD out of it (tests/config.ts).
run: cp .env.example .env
- run: docker compose build backend frontend playwright
- run: docker compose build backend playwright
- run: docker compose down -v --remove-orphans
# PLAYWRIGHT_BASE_URL points the browser at `frontend`, which is not a
# dependency of the playwright service, so bring it up explicitly.
- run: docker compose up -d --wait backend frontend
- run: docker compose up -d --wait backend
# Named rather than --rm: the report is written inside the container, and
# compose.ci.yml drops the mount that would have put it on this
# filesystem — under a sibling daemon that mount lands somewhere this job
+12 -14
View File
@@ -48,24 +48,22 @@ jobs:
uses: actions/checkout@v4
- name: Write .env
run: cp .env.example .env
# The Playwright job runs the SPA off a Vite server inside its own
# container, so the nginx image is only ever built there. This job is what
# covers it — the SPA fallback in frontend/nginx.conf is the part that
# breaks silently.
- run: docker compose build backend frontend
# The image serves the dashboard too; its SPA fallback (fluksio/spa.py)
# is the part that breaks silently.
- run: docker compose build backend
- run: docker compose down -v --remove-orphans
# Redis comes along as a depends_on; --wait blocks on the backend
# healthcheck in docker/compose.yml, which is also what proves the
# schema was created before anything served a request.
- run: docker compose up -d --wait backend frontend
# Redis comes along as a depends_on; --wait blocks on the image's
# healthcheck (backend/Dockerfile), which is also what proves the schema
# was created before anything served a request.
- run: docker compose up -d --wait backend
# Nothing is published on the host (compose.ci.yml), so the checks run
# from inside the backend container against the project network.
# from inside the backend container, which has python and no curl.
- name: Backend is up
run: docker compose exec -T backend curl -f http://localhost:8000/api/v1/utils/health-check/
- name: Frontend is up
run: docker compose exec -T backend curl -f http://frontend/
run: docker compose exec -T backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/v1/utils/health-check/')"
- name: Dashboard is up
run: docker compose exec -T backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/')"
- name: An authenticated route serves the SPA shell
run: docker compose exec -T backend curl -f http://frontend/flows
run: docker compose exec -T backend python -c "import urllib.request; assert b'<div id=\"root\"' in urllib.request.urlopen('http://localhost:8000/flows').read()"
- name: Container logs
# `up --wait` reports only that a container exited, never why. Without
# this the whole failure is one line and the reason dies with the stack
+18 -22
View File
@@ -17,13 +17,13 @@ COMPOSE_PROD := $(COMPOSE) -f docker/compose.yml
# Production also runs autoheal, which restarts the backend when its deep
# health check fails. It is profile-gated because it mounts the Docker socket.
COMPOSE_PROD_RUN := $(COMPOSE_PROD) --profile autoheal
# Host port `make dev-lan` publishes the frontend on.
# Host port `make dev-lan` publishes the app on.
APP_PORT ?= 8080
COMPOSE_DEV := $(COMPOSE_PROD) -f docker/compose.dev.yml
# Integrated local stack: dev stack wired onto the shared `proxy` network.
COMPOSE_LOCAL := $(COMPOSE_DEV) -f docker/compose.local.yml
# Same stack, with the frontend also published on APP_PORT and proxying the API
# there, for clients that cannot resolve app.$(DOMAIN).
# Same stack, with the app also published on APP_PORT, for clients that cannot
# resolve app.$(DOMAIN).
COMPOSE_LAN := $(COMPOSE_LOCAL) -f docker/compose.lan.yml
# The frontend suite's own stack, a project of its own behind the local Traefik
# (compose.test.yml). The hostname is fixed, never read from a file or the
@@ -79,7 +79,7 @@ dev: ## Start the full dev stack (includes a local Traefik proxy)
dev-local: ## Start the local stack at http://app.localhost (also run by the root `make dev`)
DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} \
$(COMPOSE_LOCAL) up --build -d proxy backend frontend docs mailcatcher
$(COMPOSE_LOCAL) up --build -d --remove-orphans proxy backend docs mailcatcher
# The host's LAN address is the source of its default route. Without `ip`
# (macOS) it stays empty and the instance keeps calling itself app.$(DOMAIN).
@@ -87,21 +87,18 @@ dev-lan: ## Same, plus the app on http://<host-ip>:$(APP_PORT) (no DNS needed)
@ip=$$(ip route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p'); \
DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} APP_PORT=$(APP_PORT) \
LAN_URL=$${ip:+http://$$ip:$(APP_PORT)} \
$(COMPOSE_LAN) up --build -d proxy backend frontend mailcatcher
$(COMPOSE_LAN) up --build -d --remove-orphans proxy backend mailcatcher
# The frontend is an nginx image, so a UI change needs a rebuild. Both the
# domain and the overlay set come off the *running* stack rather than from a
# file: VITE_API_URL is baked in at build time, so rebuilding under a different
# domain — or without the lan overlay a stack was started with — leaves the SPA
# calling an API that answers elsewhere. The lan overlay is what publishes the
# host port and builds with an empty VITE_API_URL, so dropping it takes a wall
# panel reaching http://<host-ip>:$(APP_PORT) off the air entirely and bakes a
# name it cannot resolve into the bundle. A published host port is how that
# overlay is recognised, because it is the thing only that overlay adds.
rebuild-frontend: ## Rebuild and restart the local frontend (after a UI change)
@domain=$$(docker inspect fluksio-app 2>/dev/null \
# The dashboard is built into the app image, so a UI change needs a rebuild;
# only the image's frontend stage runs again. The domain and the overlay set
# come off the *running* stack rather than from a file, so the rebuilt
# container keeps the hostname and, on a LAN stack, the published port it was
# started with. A published host port is how the lan overlay is recognised,
# because it is the thing only that overlay adds.
rebuild-frontend: ## Rebuild the app image and restart it (after a UI change)
@domain=$$(docker inspect fluksio-api 2>/dev/null \
| grep -o 'Host(`app\.[^`]*`)' | head -1 | sed 's/Host(`app\.//;s/`)//'); \
port=$$(docker inspect fluksio-app \
port=$$(docker inspect fluksio-api \
--format '{{range $$p, $$c := .NetworkSettings.Ports}}{{range $$c}}{{.HostPort}} {{end}}{{end}}' \
2>/dev/null | awk '{print $$1}'); \
if [ -n "$$port" ]; then \
@@ -112,7 +109,7 @@ rebuild-frontend: ## Rebuild and restart the local frontend (after a UI change)
fi; \
DOMAIN=$${domain:-$$DOMAIN} ENVIRONMENT=$${ENVIRONMENT:-local} \
APP_PORT=$${port:-$(APP_PORT)} \
$$compose up --build -d frontend
$$compose up --build -d backend
up: ## Start the production stack
$(COMPOSE_PROD_RUN) up --build -d
@@ -204,14 +201,13 @@ PW_VERSION = $(shell sed -n 's/.*"@playwright\/test": "[^0-9]*\([0-9.]*\)".*/\1/
test-stack: ## Start or update the frontend suite's own stack at http://app.test.localhost
@docker ps -q -f name='^fluksio-app-proxy-1$$' | grep -q . \
|| { echo " ✗ the local Traefik is not running, 'make dev' first"; exit 1; }
$(COMPOSE_TEST) up --build -d --wait redis backend frontend
$(COMPOSE_TEST) up --build -d --wait --remove-orphans redis backend
# The Playwright container, minus its network. tests/guard.ts refuses a public
# address on top of the fixed hostname.
PW_RUN = docker run --rm --ipc=host --user $$(id -u):$$(id -g) -e HOME=/tmp \
-v $(COMPOSE_ROOT):/app -w /app/frontend \
-e PLAYWRIGHT_BASE_URL=http://app.$(TEST_DOMAIN) \
-e PLAYWRIGHT_API_URL=http://api.$(TEST_DOMAIN) \
-e CI=$${CI:-1} \
-e FIRST_SUPERUSER="$$(sed -n 's/^FIRST_SUPERUSER=//p' $(COMPOSE_ROOT)/.env | head -1)" \
-e FIRST_SUPERUSER_PASSWORD="$$(sed -n 's/^FIRST_SUPERUSER_PASSWORD=//p' $(COMPOSE_ROOT)/.env | head -1)"
@@ -225,8 +221,8 @@ test-frontend: test-stack ## Run frontend tests (Playwright e2e) against the te
2>/dev/null | cut -d/ -f1); \
[ -n "$$ip" ] || { echo " ✗ the local Traefik is not on the proxy network"; exit 1; }; \
$(PW_RUN) --network proxy \
--add-host app.$(TEST_DOMAIN):$$ip --add-host api.$(TEST_DOMAIN):$$ip \
-e HOST_RESOLVER_RULES="MAP app.$(TEST_DOMAIN) $$ip, MAP api.$(TEST_DOMAIN) $$ip" \
--add-host app.$(TEST_DOMAIN):$$ip \
-e HOST_RESOLVER_RULES="MAP app.$(TEST_DOMAIN) $$ip" \
$(PW_IMAGE) npx playwright test $(PLAYWRIGHT_ARGS)
# WebKit takes *.localhost to loopback through GLib and ignores /etc/hosts, so
+16 -5
View File
@@ -4,8 +4,8 @@ Fluksio is a node-based automation software that brings trust and reliability to
It just works and looks good.
The core of Fluksio: a node-based, test-driven automation software built to scale. This
repo holds the FastAPI backend, the flow engine, and the dashboard SPA. It is served on
`app.${DOMAIN}` (SPA) and `api.${DOMAIN}` (API)
repo holds the FastAPI backend, the flow engine, and the dashboard SPA, built into one
image that serves all three on one port.
## Layout
@@ -16,7 +16,8 @@ backend/ FastAPI + SQLModel + Alembic + SQLite
worker/ the `fluksio-worker` distribution: the agent and the node runner
frontend/ React 19 + TanStack Router + Tailwind 4 + shadcn/ui
docs/ the public documentation site (zensical), served on docs.${DOMAIN}
docker/ compose.yml → compose.dev.yml → compose.local.yml (+ compose.traefik.yml)
compose.yml the single container, as the getting-started guide runs it
docker/ the operator stack: compose.yml → compose.dev.yml → compose.local.yml (+ compose.traefik.yml)
scripts/ generate-client.sh, test.sh
```
@@ -48,11 +49,21 @@ FLUKSIO_WORKER_TOKEN=… fluksio-worker --url wss://api.fluksio.com/api/v1/worke
## Getting started
Needs Docker, `make` and `openssl`. Port 80 must be free.
Needs Docker with Compose v2.
```sh
docker compose up -d # http://localhost:8080
docker compose logs fluksio # the admin password, printed once
```
Optional features (MQTT, InfluxDB, mail, …) are build-time extras; see `EXTRAS` in
`compose.yml`.
For development, with hot reload behind Traefik on port 80 (also needs `make` and `openssl`):
```sh
make init # once: .env with generated secrets, `proxy` network, prints the admin login
make dev-local # full stack at http://app.localhost (API at http://api.localhost)
make dev-local # full stack at http://app.localhost
make down # stop it
```
+73 -41
View File
@@ -1,47 +1,44 @@
FROM python:3.13
# The whole instance in one image: engine, API and dashboard on port 8000.
#
# docker build -f backend/Dockerfile -t fluksio .
# docker run -d -p 8080:8000 -v fluksio-data:/data fluksio
#
# The build context is the app repository root. EXTRAS picks the optional
# features (mqtt, influx, email, sentry, ml, or server for all of them); the
# default is the minimal engine.
ENV PYTHONUNBUFFERED=1
# ── Dashboard ────────────────────────────────────────────────────
FROM oven/bun:1 AS frontend
# Install uv
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/
WORKDIR /app
COPY package.json bun.lock /app/
COPY frontend/package.json /app/frontend/
WORKDIR /app/frontend
RUN bun install
# Compile bytecode
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
ENV UV_COMPILE_BYTECODE=1
COPY ./frontend /app/frontend
# Empty: the dashboard calls the API as a path on whichever origin served it,
# so one build works behind any port, hostname or proxy.
ENV VITE_API_URL=
RUN bun run build
# uv Cache
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
ENV UV_LINK_MODE=copy
# ── Python environment ───────────────────────────────────────────
FROM python:3.13-slim AS backend
# Ref: https://docs.astral.sh/uv/guides/integration/docker/
COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
WORKDIR /app/
# Place executables in the environment at the front of the path
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
ENV PATH="/app/.venv/bin:$PATH"
ARG EXTRAS=""
# /app/.venv holds the app and nothing of anybody else's, so there is nothing
# here to adopt: node code gets a venv of its own on the data volume, which is
# what the Modules screen installs into. A `pip install fluksio` into an
# environment somebody already works in is the case that adopts instead.
ENV NODE_VENV=managed
# ffmpeg for node code that transcodes, such as the clip recipe in
# docs/code/nodes.md. Off by default: Debian's package and what it pulls in add
# about 400 MB. Set WITH_FFMPEG=true in .env and rebuild to include it.
ARG WITH_FFMPEG=false
RUN if [ "$WITH_FFMPEG" = true ]; then \
apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Install dependencies
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
# Dependencies first, so a source change does not reinstall them.
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-workspace --package fluksio --extra server
uv sync --frozen --no-dev --no-install-workspace --package fluksio \
$(for e in $(echo "$EXTRAS" | tr , ' '); do printf -- '--extra %s ' "$e"; done)
COPY ./backend/scripts /app/backend/scripts
@@ -57,12 +54,11 @@ COPY ./backend/fluksio /app/backend/fluksio
# node code with.
COPY ./worker /app/worker
# Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --package fluksio --extra server
uv sync --frozen --no-dev --package fluksio \
$(for e in $(echo "$EXTRAS" | tr , ' '); do printf -- '--extra %s ' "$e"; done)
# Connectors are ordinary installed packages found through the
# `fluksio.node_types` entry point. `make connectors` builds them into here;
@@ -73,9 +69,45 @@ RUN --mount=type=cache,target=/root/.cache/uv \
&& uv pip install --python /app/.venv/bin/python /tmp/connector-wheels/*.whl \
|| echo "no connectors bundled"'
WORKDIR /app/backend/
# ── Runtime ──────────────────────────────────────────────────────
FROM python:3.13-slim
# Single worker on purpose: the process hosts the flow engine, and a second
# worker would be a second engine — duplicated subscriptions, cron ticks and
# webhooks. Scaling out is the M5 worker split, not more uvicorn processes.
CMD ["fastapi", "run", "fluksio/main.py"]
# git versions the flows. redis-server keeps message state when no external
# Redis is named. tini reaps the python node workers that outlive their pool,
# which would otherwise pile up as zombies under PID 1. procps is uptime, ps
# and free for Command nodes, the demo's among them.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git procps redis-server tini \
&& rm -rf /var/lib/apt/lists/*
# /app/.venv holds the app and nothing of anybody else's, so there is nothing
# here to adopt: node code gets a venv of its own on the data volume, which is
# what the Modules screen installs into.
ENV PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH" \
NODE_VENV=managed \
FRONTEND_DIST=/app/frontend/dist \
FRONTEND_HOST=http://localhost:8000
# The venv installs the app editable, so the source stays at the path it was
# synced from. uv comes with it: the Modules screen installs with it.
COPY --from=backend /app /app
COPY --from=frontend /app/frontend/dist /app/frontend/dist
COPY ./backend/docker-entrypoint.sh /usr/local/bin/
WORKDIR /app/backend/
VOLUME /data
EXPOSE 8000
# Deep health: fails when the event loop is wedged or Redis is gone, not just
# when the process is dead.
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/v1/utils/health/', timeout=4)"
ENTRYPOINT ["tini", "--", "docker-entrypoint.sh"]
# Single process on purpose: it hosts the flow engine, and a second worker
# would be a second engine — duplicated subscriptions, cron ticks and webhooks.
# `serve` makes the secret key and the first admin on first start, and prints
# the admin's password once.
CMD ["fluksio", "serve", "--plain", "--host", "0.0.0.0", "--port", "8000", "--data-dir", "/data"]
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
# Starts the bundled Redis unless an external one is named, then the command.
#
# Redis holds the last value of every message, node state and the work queue,
# so without it a restart forgets all three. In the background on purpose: its
# appendonly file is written on every change, so the SIGKILL it gets when the
# container stops loses nothing that was acknowledged.
set -e
if [ -z "$REDIS_HOST" ]; then
mkdir -p /data/redis
redis-server --bind 127.0.0.1 --port 6379 --dir /data/redis \
--appendonly yes --loglevel warning &
i=0
until redis-cli -h 127.0.0.1 ping >/dev/null 2>&1; do
i=$((i + 1))
[ "$i" -lt 100 ] || { echo "bundled redis did not start" >&2; exit 1; }
sleep 0.1
done
export REDIS_HOST=127.0.0.1
fi
exec "$@"
+10 -6
View File
@@ -54,7 +54,12 @@ class Settings(BaseSettings):
SECRET_KEY: str = secrets.token_urlsafe(32)
# 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
#: The address people and devices reach this instance at: panel links,
#: mails and the MCP issuer are built from it.
FRONTEND_HOST: str = "http://localhost:5173"
#: The built dashboard, served on the API's own origin when set. The image
#: sets it; a checkout runs Vite instead and leaves it empty.
FRONTEND_DIST: Path | None = None
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
#: The proxies whose X-Forwarded-For is believed: uvicorn's own setting,
#: read here too so a call replayed off the portal tunnel resolves its
@@ -111,7 +116,6 @@ class Settings(BaseSettings):
# unless asked for: an account can change what flows do, so who gets one
# is a superuser's call (Admin → Users).
SIGNUP_ENABLED: bool = False
DOMAIN: str = "localhost"
OAUTH_PRIVATE_KEY_FILE: Path = Path("flow-data/oauth-key.pem")
# Written only when someone enrols this instance with a portal.
# Its absence is what keeps remote access off.
@@ -237,14 +241,14 @@ class Settings(BaseSettings):
@computed_field # type: ignore[prop-decorator]
@property
def oauth_issuer(self) -> str:
"""Who issues MCP tokens — this app, on its API host.
"""Who issues MCP tokens — this app, at the address it is reached on.
Kept separate from the app's own URL because a hosted deployment can
later point agents at a different issuer without the resource server
The API is a path on the dashboard's origin, so that one address is
both. Kept a property of its own because a hosted deployment can later
point agents at a different issuer without the resource server
changing: it validates whatever issuer it is configured to trust.
"""
scheme = "http" if self.ENVIRONMENT == "local" else "https"
return f"{scheme}://api.{self.DOMAIN}"
return self.FRONTEND_HOST.rstrip("/")
@computed_field # type: ignore[prop-decorator]
@property
+3 -2
View File
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
def _influxdb() -> Any:
"""The client library, which is a `fluksio[server]` extra.
"""The client library, which is the `influx` extra.
Imported per use rather than at module level, because the node type is
registered at boot and an instance with no InfluxDB behind it should
@@ -25,7 +25,8 @@ def _influxdb() -> Any:
import influxdb_client
except ImportError:
raise RuntimeError(
"the influxdb node needs the server extra: pip install 'fluksio[server]'"
"the influxdb node needs the influx extra: pip install 'fluksio[influx]', "
"or build the image with EXTRAS=influx"
) from None
return influxdb_client
+18 -1
View File
@@ -7,7 +7,6 @@ import threading
from collections.abc import Iterable
from typing import Any
import numpy as np
from pydantic import BaseModel, ConfigDict
from fluksio.flow.messages import MessageSpec
@@ -16,6 +15,22 @@ from fluksio.flow.nodes.base import Node
logger = logging.getLogger(__name__)
def _numpy() -> Any:
"""numpy, which is the `ml` extra.
Imported when a node is built rather than at boot, so an instance without
it still offers every other type, and a node placed without it says so.
"""
try:
import numpy
except ImportError:
raise RuntimeError(
"the mlp node needs the ml extra: pip install 'fluksio[ml]', "
"or build the image with EXTRAS=ml"
) from None
return numpy
class MLPNode(Node):
"""
Multi-Layer Perceptron node for neural network processing in pipelines.
@@ -72,6 +87,7 @@ class MLPNode(Node):
)
cfg = self.Params.model_validate(self.params)
np = self._np = _numpy()
rng = np.random.default_rng(seed=cfg.seed)
num_inputs = max(1, len(self.input_ports))
num_outputs = max(1, len(self.output_ports))
@@ -91,6 +107,7 @@ class MLPNode(Node):
self.id,
)
np = self._np
if kwargs:
input_array = np.array([float(v) for v in kwargs.values()])
else:
+3 -2
View File
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
def _aiomqtt() -> Any:
"""The client library, which is a `fluksio[server]` extra.
"""The client library, which is the `mqtt` extra.
Imported per use rather than at module level, because the node type is
registered at boot and an instance that talks to no broker should not
@@ -31,7 +31,8 @@ def _aiomqtt() -> Any:
import aiomqtt
except ImportError:
raise RuntimeError(
"the mqtt node needs the server extra: pip install 'fluksio[server]'"
"the mqtt node needs the mqtt extra: pip install 'fluksio[mqtt]', "
"or build the image with EXTRAS=mqtt"
) from None
return aiomqtt
+6 -1
View File
@@ -90,7 +90,7 @@ def custom_generate_unique_id(route: APIRoute) -> str:
if settings.SENTRY_DSN and settings.ENVIRONMENT != "local":
# Imported here rather than at the top: it is a `fluksio[server]` extra, so
# Imported here rather than at the top: it is the `sentry` extra, so
# a pip install without one has no sentry to import — and no DSN either.
import sentry_sdk
@@ -506,6 +506,11 @@ if settings.all_cors_origins:
app.add_middleware(DocsForThisMachine)
if settings.FRONTEND_DIST:
from fluksio.spa import Dashboard
app.add_middleware(Dashboard, dist=settings.FRONTEND_DIST)
# The caller's address, for every per-address limit and the docs check above:
# the rightmost X-Forwarded-For hop that is not a trusted proxy. uvicorn does
# this for what it serves, but a call the portal tunnel replays arrives over an
+88
View File
@@ -0,0 +1,88 @@
"""The dashboard, served by the API on its own origin.
One process and one port: the built SPA comes off disk, and every path the API
does not own answers with ``index.html``, so a client-side route survives a
reload. Same origin also means the dashboard calls the API as a path, needs no
CORS allowance and no second hostname, which is what lets the whole instance
sit behind a single published port.
"""
from __future__ import annotations
import base64
import hashlib
import mimetypes
import re
from pathlib import Path
from starlette.middleware.gzip import GZipMiddleware
from starlette.responses import FileResponse
from starlette.types import ASGIApp, Receive, Scope, Send
#: What the API answers itself. Everything else is the dashboard's.
API_PREFIXES = ("/api/", "/hooks/", "/mcp", "/.well-known/", "/docs", "/redoc")
# Older mime tables predate the manifest, and it would go out as a download.
mimetypes.add_type("application/manifest+json", ".webmanifest")
def content_security_policy(index_html: str) -> str:
"""The policy for the dashboard, admitting its inline scripts by hash.
The hash is taken from the file being served, so editing the theme script
in ``index.html`` cannot leave a stale one behind.
style-src 'unsafe-inline': Monaco, Radix's scroll lock and the toasts insert
<style> elements at runtime, which no hash can name. img/frame-src take any
http(s): a camera stream or website widget points wherever its dashboard
says.
"""
hashes = "".join(
f" 'sha256-{base64.b64encode(hashlib.sha256(script.encode()).digest()).decode()}'"
for script in re.findall(r"<script>([\s\S]*?)</script>", index_html)
)
return (
f"default-src 'self'; script-src 'self'{hashes}; "
"style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; "
"media-src 'self' blob:; font-src 'self'; connect-src 'self'; "
"worker-src 'self'; frame-src http: https:; frame-ancestors 'self'; "
"object-src 'none'; base-uri 'self'; form-action 'self'"
)
class Dashboard:
"""Serve the built SPA for every GET the API does not own."""
def __init__(self, app: ASGIApp, dist: Path) -> None:
self.app = app
self.dist = dist.resolve()
self.index = self.dist / "index.html"
self.headers = {
"Content-Security-Policy": content_security_policy(self.index.read_text()),
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
}
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
path: str = scope.get("path", "")
if (
scope["type"] != "http"
or scope["method"] not in ("GET", "HEAD")
or path.startswith(API_PREFIXES)
):
await self.app(scope, receive, send)
return
file = (self.dist / path.lstrip("/")).resolve()
if file.is_file() and file.is_relative_to(self.dist) and file != self.index:
headers = dict(self.headers)
# Vite puts a content hash in every asset's name, so one never
# changes under its URL.
if path.startswith("/assets/"):
headers["Cache-Control"] = "public, max-age=31536000, immutable"
else:
# The one file that must not be cached: it names the current
# assets, so a stale copy pins the browser to the previous build.
file = self.index
headers = {**self.headers, "Cache-Control": "no-cache"}
response = FileResponse(file, headers=headers)
await GZipMiddleware(response, minimum_size=1024)(scope, receive, send)
+2 -1
View File
@@ -44,7 +44,8 @@ def send_email(
import emails # type: ignore
except ImportError:
raise RuntimeError(
"sending mail needs the server extra: pip install 'fluksio[server]'"
"sending mail needs the email extra: pip install 'fluksio[email]', "
"or build the image with EXTRAS=email"
) from None
message = emails.Message(
subject=subject,
+15 -15
View File
@@ -26,8 +26,10 @@ classifiers = [
# email-validator, jinja2 and httpx — also arrive inside `fastapi[standard]`,
# so those lines look redundant and install nothing extra; they are here so a
# direct import does not rest on somebody else's extra keeping its contents.
# The variant without fastapi-cloud-cli, which brings sentry-sdk and a deploy
# command nothing here runs.
dependencies = [
"fastapi[standard]<1.0.0,>=0.114.2",
"fastapi[standard-no-fastapi-cloud-cli]<1.0.0,>=0.114.2",
"pydantic>2.0",
"pydantic-settings<3.0.0,>=2.2.1",
"sqlmodel<1.0.0,>=0.0.21",
@@ -56,9 +58,6 @@ dependencies = [
# the journal, the worker pipe. Engine-side only: `fluksio-worker` is
# copied onto other people's machines and stays dependency-free.
"orjson>=3.10",
# The `mlp` node, which is registered at boot. Kept out of the extras
# because a data-science environment has it already.
"numpy>=2.2.6",
# The `wren` node runs its model on one BLAS thread, which answers in about
# half the time. Scoped to the call: OPENBLAS_NUM_THREADS would bind every
# other numpy call in the engine too, and only counts before numpy loads.
@@ -94,17 +93,18 @@ dependencies = [
# extra rather than a dependency: csv and jsonl need nothing, and pyarrow is
# tens of megabytes for whoever wants dtypes kept.
parquet = ["pyarrow>=17"]
# What a deployment has and a laptop does not: the device connectors, outbound
# mail and error reporting. The engine, the CLI and every python node work
# without them — `pip install fluksio` in a data-science environment is the
# case this exists for, and lxml and the aiohttp stack are most of its wait.
# Each import is guarded and names this extra. The image installs it.
server = [
"emails<1.0,>=0.6",
"sentry-sdk[fastapi]>=2.20.0",
"aiomqtt>=2.0.0",
"influxdb-client[async]>=1.40.0",
]
# One per feature, so an instance carries only what it talks to. The engine,
# the CLI and every python node work without any of them. Each import is
# guarded and names its extra; the image takes them as EXTRAS=mqtt,influx.
mqtt = ["aiomqtt>=2.0.0"]
influx = ["influxdb-client[async]>=1.40.0"]
# Outbound mail. lxml and its stylesheet inliner are most of this.
email = ["emails<1.0,>=0.6"]
sentry = ["sentry-sdk[fastapi]>=2.20.0"]
# The `mlp` node. A data-science environment has it already.
ml = ["numpy>=2.2.6"]
# All of the above: what a full deployment runs.
server = ["fluksio[mqtt,influx,email,sentry,ml]"]
[project.urls]
Homepage = "https://fluksio.com"
+2 -3
View File
@@ -40,7 +40,6 @@ os.environ["NODE_VENV"] = "managed"
# (with the dev stack) where they are meant to work.
os.environ["PRIVATE_API_ENABLED"] = "true"
# The suite is the development configuration by definition: `/private` is
# gated on it (app/api/routes/private.py) and the MCP host allow-list is
# built from DOMAIN (app/mcp/http.py), while the tests speak to api.localhost.
# gated on it (app/api/routes/private.py) and the MCP host allow-list admits
# localhost under it (app/mcp/http.py).
os.environ["ENVIRONMENT"] = "local"
os.environ["DOMAIN"] = "localhost"
+1 -1
View File
@@ -59,7 +59,7 @@ def over_mcp(monkeypatch: pytest.MonkeyPatch):
async with mcp_server.mcp.session_manager.run():
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=mcp_app),
base_url="http://api.localhost",
base_url=settings.oauth_issuer,
) as client:
return await block(client)
+39
View File
@@ -0,0 +1,39 @@
import base64
import hashlib
from pathlib import Path
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route
from starlette.testclient import TestClient
from fluksio.spa import Dashboard
THEME = "document.documentElement.classList.add('dark')"
def _client(dist: Path) -> TestClient:
(dist / "assets").mkdir(parents=True)
(dist / "index.html").write_text(f"<html><script>{THEME}</script></html>")
(dist / "assets" / "app-1a2b.js").write_text("console.log(1)")
api = Starlette(routes=[Route("/api/v1/ping", lambda _: PlainTextResponse("api"))])
return TestClient(Dashboard(api, dist))
def test_the_dashboard_and_the_api_share_one_origin(tmp_path: Path) -> None:
client = _client(tmp_path)
# A client-side route survives a reload, with the inline script admitted.
page = client.get("/flows/house")
digest = base64.b64encode(hashlib.sha256(THEME.encode()).digest()).decode()
assert THEME in page.text
assert page.headers["cache-control"] == "no-cache"
assert f"'sha256-{digest}'" in page.headers["content-security-policy"]
asset = client.get("/assets/app-1a2b.js")
assert asset.text == "console.log(1)"
assert "immutable" in asset.headers["cache-control"]
assert client.get("/api/v1/ping").text == "api"
# Nothing outside the build directory is served, only the shell.
assert THEME in client.get("/assets/../../etc/passwd").text
+35
View File
@@ -0,0 +1,35 @@
# Fluksio in one container. From a clone of this repository:
#
# docker compose up -d
# docker compose logs fluksio # the admin password, printed once
#
# then open http://localhost:8080. Change the left side of `ports` to use
# another host port. The operator stack under docker/ is a different file.
name: fluksio
services:
fluksio:
build:
context: .
dockerfile: backend/Dockerfile
args:
# Optional features, comma separated: mqtt, influx, email, sentry, ml,
# or server for all of them. Rebuild after a change: `docker compose up -d --build`.
EXTRAS: ""
image: fluksio:latest
restart: unless-stopped
ports:
- "8080:8000"
volumes:
- data:/data
environment:
# The address people and devices reach this instance at. Panel links and
# mails are built from it.
FRONTEND_HOST: http://localhost:8080
# Schedules run in this timezone, such as Europe/Berlin.
TZ: UTC
# Where frames a flow only shows live are held.
shm_size: 128mb
volumes:
data:
+19 -19
View File
@@ -7,7 +7,8 @@ How to run, test and lint the `app` stack.
**Integrated — the normal way.** From the workspace root, once: `make init` (submodules,
secrets, the per-stack `.env` files, the shared external `proxy` docker network). Then
`make dev` brings both stacks up detached behind a single Traefik — which is what lets the
SPA, the API and the marketing site answer on one port. `make status` lists the containers,
app and the marketing site answer on one port. The app is one container
(`backend/Dockerfile`): the engine, its API and the built dashboard on port 8000. `make status` lists the containers,
`make down` stops everything.
The hostname comes from `DOMAIN` in `app/.env`, which `scripts/setup.sh` copies out of the
@@ -22,6 +23,11 @@ port 80. `make dev` starts this stack on its own Traefik with all host ports
published, in the foreground. `make dev-utils` starts just proxy and mailcatcher — the
useful half when the backend runs on the host.
**Single container.** `docker compose up -d` in this repo's root uses `compose.yml`, the
setup the getting-started guide gives: the image on `localhost:8080` with its own Redis, no
Traefik, no `.env`. It is a separate project from the stack below and shares nothing
with it.
**No Docker.** `make install` once, then `make dev-backend` (FastAPI on :8000) and
`make dev-frontend` (Vite on :5173) in two terminals. The backend needs nothing running: its
database is SQLite in `DATA_DIR` (`backend/flow-data` by default), created on first start.
@@ -45,9 +51,9 @@ already being taken would stop the whole stack from starting.
| URL | Service |
|---|---|
| http://app.localhost | dashboard SPA |
| http://api.localhost | backend API |
| http://api.localhost/docs | OpenAPI / Swagger UI |
| http://app.localhost | dashboard, with the API under `/api/v1` |
| http://api.localhost | the same container, for callers configured with the API host |
| http://app.localhost/docs | OpenAPI / Swagger UI |
| http://localhost | marketing site (the `index` stack) |
Mailcatcher has no route here, so use the container directly:
@@ -59,8 +65,7 @@ Standalone (`cd app && make dev`) publishes the full set:
| URL | Service |
|---|---|
| http://app.localhost, http://api.localhost | this stack's own Traefik on :80 |
| http://localhost:5173 | SPA (the nginx image, not the Vite dev server) |
| http://localhost:8000 | backend API |
| http://localhost:8000 | dashboard and API |
| http://localhost:8090 | Traefik dashboard |
| http://localhost:1080 | mailcatcher web UI (SMTP on 1025) |
@@ -74,14 +79,9 @@ syncs the same directories, but only under `docker compose watch`, which no targ
before the mount existed keeps serving the source baked into its image and has to be
recreated once.
The frontend is built into an nginx image, so a UI change needs an explicit rebuild, from
`app/`:
```sh
docker compose -p fluksio-app --env-file "$PWD/.env" \
-f docker/compose.yml -f docker/compose.dev.yml -f docker/compose.local.yml \
up --build -d frontend
```
The dashboard is built into the app image, so a UI change needs a rebuild:
`make rebuild-frontend` from `app/`. Only the image's frontend stage runs again, and the
container is recreated with the domain and overlays the running one was started with.
## Services the dev stack adds
@@ -154,8 +154,8 @@ The specs are `frontend/tests/*.spec.ts`; `playwright.config.ts` defines `setup`
in once into `playwright/.auth/user.json`, which the browser projects reuse; `mobile` runs
`mobile.spec.ts` only, on a Pixel 5 at 393px.
Both origins come from `PLAYWRIGHT_BASE_URL` and `PLAYWRIGHT_API_URL`, deliberately never
from `VITE_API_URL` — that one belongs to the app build and in a deployment checkout it
The origin comes from `PLAYWRIGHT_BASE_URL`, with the API on it unless `PLAYWRIGHT_API_URL`
names another, deliberately never from `VITE_API_URL` — that one belongs to the app build and in a deployment checkout it
names the deployment, which would mean driving a browser at the local stack while sending
teardown `DELETE`s to the live instance. `tests/guard.ts` runs first and refuses the whole
run when either origin resolves outside loopback or the private ranges, because the suite
@@ -166,7 +166,7 @@ creates and deletes flows, dashboards and users. `PLAYWRIGHT_ALLOW_PUBLIC=1` ove
always happens in the Playwright container, which pins the browser by reading
`frontend/package.json`. Running Playwright on the host is unsupported — the workspace pins a
browser revision whose install is incomplete. The domain comes off the running stack (the
frontend container's Traefik rule), so the shots are of whatever `make dev` last started.
app container's Traefik rule), so the shots are of whatever `make dev` last started.
## Lint, hooks and the generated client
@@ -186,7 +186,7 @@ contract between the two frontends, and that target is the only thing checking i
`.gitea/workflows/` runs four jobs:
`test-backend.yml` (pytest on Python 3.12 and 3.14, plus a check that both wheels build), `playwright.yml` (the e2e suite in
two shards, one compose project each, the browser loading the SPA from the nginx `frontend`
two shards, one compose project each, the browser loading the SPA from the `backend`
service), `pre-commit.yml` (the hooks above, plus strict mypy, which they do not cover) and
`test-compose.yml` (a smoke test that the production images actually come up). All but
`test-compose.yml` (a smoke test that the production image actually comes up). All but
`test-backend.yml` write `.env` from `.env.example` first. The backend suite needs none.
+4 -19
View File
@@ -42,20 +42,6 @@ services:
volumes: !override
- app-flow-data:/data
frontend:
container_name: !reset null
ports: !reset []
# Baked at build time. compose.dev.yml bakes http://localhost:8000, which
# resolves to the playwright container itself; the browser has to reach the
# API by service name on the project network instead.
build:
args:
- VITE_API_URL=http://backend:8000
# Image tags are daemon-global too, and this one no longer holds the same
# bytes as the smoke job's: without a per-job tag the two jobs overwrite
# each other's `fluksio-frontend:latest` and the SPA calls the wrong API.
image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${COMPOSE_PROJECT_NAME:-ci}'
playwright:
ports: !reset []
# Same reason as backend above. These two exist so a local run leaves its
@@ -64,9 +50,8 @@ services:
# found with the provided path: frontend/blob-report" was. The job copies
# the report out of the container instead.
volumes: !reset []
# frontend/playwright.config.ts defaults baseURL to http://app.localhost,
# which is right for a local run against the integrated stack but resolves
# to 127.0.0.1 in here. In CI the nginx `frontend` service serves the SPA.
# This origin must stay in BACKEND_CORS_ORIGINS (.env.example).
# frontend/playwright.config.ts defaults baseURL to the test stack's
# hostname, which resolves to nothing in here. The backend serves the SPA
# and its API on one origin.
environment:
- PLAYWRIGHT_BASE_URL=http://frontend
- PLAYWRIGHT_BASE_URL=http://backend:8000
-11
View File
@@ -147,17 +147,6 @@ services:
- "1080:1080"
- "1025:1025"
frontend:
restart: "no"
ports:
- "5173:80"
build:
context: ..
dockerfile: frontend/Dockerfile
args:
- VITE_API_URL=http://localhost:8000
- NODE_ENV=development
# Reachable at http://docs.${DOMAIN}, but a container rebuild per edit is a
# poor authoring loop — use `make docs-serve` for that.
docs:
+4 -15
View File
@@ -2,26 +2,15 @@
#
# For reaching the app from a machine that cannot be given a DNS record or a
# hosts entry. Traefik routes on the Host header and a bare IP matches no
# router, so the frontend is published on a host port instead and proxies the
# API and the hooks on that same port: http://<host-ip>:${APP_PORT} is the
# whole app.
# router, so the app container is published on a host port as well:
# http://<host-ip>:${APP_PORT} is the whole app, dashboard, API and hooks.
services:
backend:
ports: !override
- "${APP_PORT:-8080}:8000"
environment:
# The address the instance gives out: the link the Panels dialog shows a
# screen and the one its emails carry. app.${DOMAIN} resolves on this host
# only, so `make dev-lan` passes the published port on the LAN address.
- FRONTEND_HOST=${LAN_URL:-http://app.${DOMAIN:-localhost}}
frontend:
ports: !override
- "${APP_PORT:-8080}:80"
volumes:
- ./nginx-api-proxy.conf:/etc/nginx/extra-conf.d/backend-not-found.conf:ro
build:
args:
# Empty on purpose: the client then addresses the API as a path on
# whichever origin served the page, so one build works through
# app.${DOMAIN}, the published port, and an ssh tunnel alike.
- VITE_API_URL=
-7
View File
@@ -33,13 +33,6 @@ services:
# talks to this backend rather than running one of its own.
- BACKEND_CORS_ORIGINS=http://${DOMAIN:-localhost},http://app.${DOMAIN:-localhost},http://localhost:5173
frontend:
ports: !reset []
build:
args:
- VITE_API_URL=http://api.${DOMAIN:-localhost}
- NODE_ENV=development
networks:
# Shared with the website stack; created by the root scripts/setup.sh.
proxy:
-3
View File
@@ -18,6 +18,3 @@ services:
redis:
condition: service_healthy
restart: true
frontend:
container_name: !reset null
+15 -67
View File
@@ -1,4 +1,5 @@
# Production stack. Layered by the Makefile:
# Production stack: one image (backend/Dockerfile) serves the engine, the API
# and the dashboard on port 8000. Layered by the Makefile:
# compose.yml → production (Traefik + TLS, restart always)
# + compose.dev.yml → local dev (published ports, hot reload)
# + compose.local.yml → integrated stack on the shared `proxy` net
@@ -75,13 +76,6 @@ services:
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
container_name: fluksio-api
restart: always
# PID 1 that reaps orphans. A python node's worker processes outlive the
# process holding their handle whenever that one goes without stopping the
# pool -- which `--reload` does on every source edit -- and reparent onto
# PID 1, which is the app itself and waits for nobody else's children. The
# container filled up with zombie `python`. Declared here rather than in
# compose.dev.yml because an init closes the whole class, not just reload.
init: true
security_opt:
- no-new-privileges:true
networks:
@@ -94,14 +88,13 @@ services:
env_file:
- ../.env
environment:
- DOMAIN=${DOMAIN}
- FRONTEND_HOST=${FRONTEND_HOST?Variable not set}
- ENVIRONMENT=${ENVIRONMENT}
- BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS}
# Whose X-Forwarded-For is believed: Traefik's and the frontend's nginx,
# which reach this from the compose networks (Docker's default pools sit
# in 172.16.0.0/12). Both write the header themselves, so the hop they
# add is the caller. No port is published here, so nothing else can.
# Whose X-Forwarded-For is believed: the reverse proxy's, which reaches
# this from the compose networks (Docker's default pools sit in
# 172.16.0.0/12) and writes the header itself, so the hop it adds is the
# caller. No port is published here, so nothing else can.
- FORWARDED_ALLOW_IPS=${FORWARDED_ALLOW_IPS:-127.0.0.1,172.16.0.0/12}
- SECRET_KEY=${SECRET_KEY?Variable not set}
- FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set}
@@ -111,7 +104,8 @@ services:
- SMTP_PASSWORD=${SMTP_PASSWORD}
- EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL}
- SENTRY_DSN=${SENTRY_DSN}
# Flow state survives a restart in Redis; without a host it stays in memory.
# This stack's own Redis rather than the one bundled in the image, so the
# app-redis-data volume keeps holding the flow state.
- REDIS_HOST=redis
# Everything this instance keeps — the database, the flows, secrets,
# artifacts and the user venv — on one volume. The paths below derive
@@ -151,21 +145,15 @@ services:
# together (ARTIFACT_VOLATILE_BYTES) for more cameras or bigger frames.
shm_size: 128mb
# Deep health: fails when the event loop is wedged or Redis is gone, not
# just when the process is dead. Autoheal restarts on unhealthy.
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/utils/health/"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# The image's deep health check fails when the event loop is wedged or
# Redis is gone, not just when the process is dead. Autoheal restarts on
# unhealthy.
build:
context: ..
dockerfile: backend/Dockerfile
args:
# Opt-in: ffmpeg costs the image about 400 MB (backend/Dockerfile).
- WITH_FFMPEG=${WITH_FFMPEG:-false}
# Every optional feature by default; EXTRAS=mqtt,influx in .env trims it.
- EXTRAS=${EXTRAS-server}
labels:
- autoheal=true
- traefik.enable=true
@@ -174,56 +162,16 @@ services:
- traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=8000
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=Host(`api.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=Host(`app.${DOMAIN?Variable not set}`) || Host(`api.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.entrypoints=http
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.rule=Host(`api.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.rule=Host(`app.${DOMAIN?Variable not set}`) || Host(`api.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.entrypoints=https
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls=true
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls.certresolver=le
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-redirect
frontend:
image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${TAG-latest}'
container_name: fluksio-app
restart: always
security_opt:
- no-new-privileges:true
networks:
- proxy
- default
build:
context: ..
dockerfile: frontend/Dockerfile
args:
# The address baked into the bundle. `scripts/setup.sh` already derives
# it from ENVIRONMENT and writes it to .env — http for a local target,
# because nothing terminates TLS there — so take it from the environment
# and keep the https form only as the default for a checkout that
# predates that key. Hardcoding https here made every `up --build` that
# did not also layer compose.local.yml ship a bundle calling
# https://api.localhost, which fails the certificate check and with it
# the whole login.
- VITE_API_URL=${VITE_API_URL:-https://api.${DOMAIN?Variable not set}}
- NODE_ENV=production
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.constraint-label=proxy
- traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=Host(`app.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.entrypoints=http
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.rule=Host(`app.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.entrypoints=https
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls=true
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls.certresolver=le
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.middlewares=https-redirect
# Static documentation site (zensical), served at docs.${DOMAIN}. Content and
# config live in ../docs/ and ../zensical.toml; `make docs-serve` previews it.
docs:
-46
View File
@@ -1,46 +0,0 @@
# Serves the API from the same origin as the SPA, replacing the 404 stub the
# image ships. Traefik routes on the Host header, which a bare IP never
# matches, so this is what makes http://<host-ip>:<port> a complete app: the
# client asks for /api/v1/... on whatever origin served it, and never needs a
# resolvable api.${DOMAIN} or a CORS allowance.
# /hooks is where the flows' webhooks and served sockets live, next to the API
# on the backend. One regex takes both, and a regex outranks `location /`,
# which would otherwise answer /hooks with the SPA.
location ~ ^/(api|hooks) {
# Docker's embedded DNS, re-resolved: nginx otherwise pins the backend's
# address at startup and 502s for good once that container is recreated.
resolver 127.0.0.11 valid=10s ipv6=off;
# The container name, which only the dev API carries. The test stack's
# backend answers to the service name `backend` on the shared proxy
# network too.
set $backend_upstream http://fluksio-api:8000;
proxy_pass $backend_upstream$request_uri;
proxy_http_version 1.1;
proxy_set_header Host $host;
# Replaced, not appended to: whatever the client sent would otherwise sit
# in front of the one hop the backend believes, and anything reading the
# header naively would take it for the caller.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
# The flow websocket (/api/v1/flows/ws) and a served socket upgrade through
# here; passing the client's own Connection header covers both and
# ordinary requests.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
# A live socket outlives the 60s default by design.
proxy_read_timeout 1d;
# nginx's 1 MB default would 413 any larger artifact upload; the backend
# enforces MAX_ARTIFACT_BYTES itself, so the body streams through as is.
client_max_body_size 0;
proxy_request_buffering off;
}
# Unchanged from the stub: the API docs stay off the frontend's origin.
location /docs {
return 404;
}
location /redoc {
return 404;
}
+7 -4
View File
@@ -5,13 +5,16 @@ and run flows with the permissions of the user who authorizes it.
## Connect an agent
Set `MCP_ENABLED=true` in the engine's environment and restart it. The Python
package defaults to off; the Docker template enables it.
Set `MCP_ENABLED=true` in the engine's environment and restart it, under
`environment` in `compose.yml` for the Docker container. It defaults to off.
Configure your MCP client with an HTTP endpoint:
- Local Docker stack: `http://api.localhost/mcp`
- Deployment: `https://api.<your-domain>/mcp`
- Docker container: `http://localhost:8080/mcp`
- Deployment: `https://<your-domain>/mcp`
The endpoint has to be the address in `FRONTEND_HOST`, which is also the
issuer of the client's tokens.
The client registers, opens browser authorization, and receives an OAuth
token after you approve access. The issuer and browser redirects use
+2 -2
View File
@@ -10,8 +10,8 @@ export FLUKSIO=http://127.0.0.1:8000/api/v1
```
Use the port printed by `fluksio serve`. For Docker, use
`http://api.localhost/api/v1`, `http://<host-ip>:8080/api/v1` on a LAN stack,
or `https://api.<your-domain>/api/v1` for a deployment.
`http://localhost:8080/api/v1`, or `https://<your-domain>/api/v1` for a
deployment.
All paths below are relative to this base. Timestamps in responses are
ISO 8601 and always carry their UTC offset.
+5 -4
View File
@@ -11,8 +11,9 @@ For the browser dashboard, sign up at [fluksio.com](https://fluksio.com), add
an instance, then run `fluksio enroll <code>` in the same directory.
Use `fluksio --help` or `fluksio COMMAND --help` for command options.
`pip install 'fluksio[server]'` adds MQTT, InfluxDB, outbound mail and error
reporting. A machine that only executes remote nodes can install
`pip install 'fluksio[server]'` adds MQTT, InfluxDB, outbound mail, error
reporting and numpy, or name only what you need, such as `fluksio[mqtt]`. A
machine that only executes remote nodes can install
[`fluksio-worker`](workers.md).
## Where an instance lives
@@ -123,7 +124,7 @@ Starts the same agent as the standalone `fluksio-worker` command:
```sh
FLUKSIO_WORKER_TOKEN=your-token fluksio worker \
--url wss://api.example.com/api/v1/workers/attach --labels gpu
--url wss://fluksio.example.com/api/v1/workers/attach --labels gpu
```
Create the worker token on the engine first. See [Remote workers](workers.md)
@@ -140,7 +141,7 @@ is a fallback.
### `fluksio login`
```sh
fluksio login --url https://api.example.com
fluksio login --url https://fluksio.example.com
```
Prompts for credentials and saves an API token for a remote engine.
+6 -5
View File
@@ -144,6 +144,7 @@ import subprocess
import tempfile
import fluksio
import imageio_ffmpeg
# H.264 Constrained Baseline, at most 640x360 and 15 fps, mono AAC.
PANEL = [
@@ -157,10 +158,11 @@ PANEL = [
def process(clip):
source = fluksio.load_artifact(clip)
ffmpeg = imageio_ffmpeg.get_ffmpeg_exe()
with tempfile.TemporaryDirectory() as tmp:
target = os.path.join(tmp, "panel.mp4")
subprocess.run(
["ffmpeg", "-nostdin", "-loglevel", "error", "-i", source, *PANEL, target],
[ffmpeg, "-nostdin", "-loglevel", "error", "-i", source, *PANEL, target],
check=True,
timeout=600,
)
@@ -169,10 +171,9 @@ def process(clip):
```
Use it only for clips headed for such a panel. Browsers play the originals,
and the result plays there too, just smaller. The node needs `ffmpeg` on the
`PATH`. The Docker image includes it only when built with
[`WITH_FFMPEG=true`](../reference/configuration.md#image-options) in `.env`.
Otherwise run the node on a [remote worker](workers.md) that has it.
and the result plays there too, just smaller. Add `imageio-ffmpeg` on the
**Modules** screen first. It brings a static `ffmpeg` build for Linux on x86
and ARM, so nothing has to be installed into the image.
### Speech to and from a panel
+3 -3
View File
@@ -9,7 +9,7 @@ WebSocket. Keep one engine; add workers where the hardware lives.
On the engine, use a superuser access token. In these examples:
- `API_URL` is the engine's API base, including `/api/v1`, such as
`https://api.example.com/api/v1` or `http://localhost:8080/api/v1` for a LAN stack.
`https://fluksio.example.com/api/v1` or `http://localhost:8080/api/v1` for a Docker container.
- `TOKEN` is your superuser bearer token. The login endpoint
`POST /api/v1/login/access-token` accepts form fields `username` and `password`.
@@ -36,7 +36,7 @@ pip install fluksio-worker
export FLUKSIO_WORKER_TOKEN='<token from the engine>'
fluksio-worker \
--url wss://api.example.com/api/v1/workers/attach \
--url wss://fluksio.example.com/api/v1/workers/attach \
--labels gpu,cuda12 \
--python /opt/torch-venv/bin/python
```
@@ -163,7 +163,7 @@ in `$DATA_DIR/provisioners.json`, replacing the example addresses and paths:
"name": "hpc",
"login": "me@login.cluster",
"ssh_key": "/secrets/hpc_ed25519",
"engine_url": "wss://api.example.com/api/v1/workers/attach",
"engine_url": "wss://fluksio.example.com/api/v1/workers/attach",
"max_idle_s": 300,
"provision_timeout_s": 900,
"profiles": [{
+49 -63
View File
@@ -3,40 +3,43 @@
Build a small temperature flow, then connect a real sensor when you're ready.
No hardware required for the first lap.
## Start the Docker stack
## Start Fluksio
You need Git, Docker with Compose v2, `make`, and `openssl`. Port 80 must be
free. Clone the public app repository and run its commands from the repo root:
You need Git and Docker with Compose v2. Clone the app repository and start
its single container:
```sh
git clone https://git.stroblme.de/Fluksio/app.git fluksio
cd fluksio
make init
make dev-local
docker compose up -d
docker compose logs fluksio
```
`make init` creates `.env`, generates secrets, creates the `proxy` network,
and prints your admin login. It is safe to run again: existing secrets stay
as they are.
The first build takes a few minutes. The log shows the admin account and its
password once, on the first start. Open [http://localhost:8080](http://localhost:8080)
and sign in. Adding Python code requires that superuser account.
Give the containers a moment to start on the first build, then open
[http://app.localhost](http://app.localhost). Sign in with the admin login:
adding Python code requires a superuser account. The API is at
[http://api.localhost](http://api.localhost), and the docs are at
[http://docs.localhost](http://docs.localhost).
Everything runs in that one container on one port: the engine, its API and the
dashboard. To use another port, change the left side of `ports` in
`compose.yml`, for example `"9000:8000"`. From another machine on your network,
open `http://<host-ip>:8080`. Set `FRONTEND_HOST` in `compose.yml` to the
address you open, since links for wall panels and mails are built from it.
For a Docker instance reached from another machine on your network, use:
The image starts minimal. MQTT, InfluxDB, outbound mail and the rest are
[extras](../reference/configuration.md#image-options). Add the ones you need to
`EXTRAS` in `compose.yml` and rebuild:
```yaml
args:
EXTRAS: "mqtt,influx"
```
```sh
make dev-lan
docker compose up -d --build
```
Open `http://<host-ip>:8080`. Choose another port with
`make dev-lan APP_PORT=8081`. This is a development stack; see
[running it long term](#run-it-long-term) before using it for a deployment.
When you are done, stop the stack with `make down`. Your data volumes stay
in place for the next start.
Stop the container with `docker compose down`. Your data stays on the `data`
volume for the next start.
## Your first flow
@@ -97,10 +100,8 @@ checking the payload format your device expects. A relay may need `ON` and
and **Step** let you inspect queued messages one at a time. A real heating
controller also needs appropriate limits and hysteresis.
Docker development stacks include a test broker at `mosquitto:1883`; it has
no published host port, so your physical sensors cannot reach it directly.
For real sensors, use your own broker. The Docker image includes the MQTT
and InfluxDB connector dependencies.
The MQTT node needs the `mqtt` extra. Fluksio does not run a broker, so point
the node at your own, such as Mosquitto.
## Show it on a dashboard
@@ -112,53 +113,38 @@ See [Dashboards and panels](../interface/dashboards.md) for widgets and pairing
a wall tablet.
For longer-term history, add an **InfluxDB** node that consumes
`living_temperature`. Configure your server URL, organization, bucket, token
`living_temperature`. It needs the `influx` extra. Configure your server URL, organization, bucket, token
and `writes` mapping. Store the token under **Secrets** and select it in the
node's credential field. [Node types](../reference/node-types.md#influxdb)
has the write and query formats.
The Docker development stack includes an InfluxDB test service at
`http://influxdb:8086`, with organization and bucket `fluksio` and token
`fluksio-dev-token`. It is for testing; configure a persistent InfluxDB server
for data you want to keep. The production stack includes neither InfluxDB nor
an MQTT broker.
Fluksio does not run an InfluxDB server either. Use your own.
## Run it long term
For a Docker deployment, run `make init`, then edit the repository's `.env`:
The container from `compose.yml` is fit for long-term use. It restarts with
Docker, and `/data` holds everything it keeps: the database, the flows,
secrets, artifacts and its Redis. Back up that volume. Compose names it
`fluksio_data`.
| Setting | Example |
|---|---|
| `DOMAIN` | `example.com` |
| `ENVIRONMENT` | `production` |
| `FRONTEND_HOST` | `https://app.example.com` |
| `VITE_API_URL` | `https://api.example.com` |
| `BACKEND_CORS_ORIGINS` | `https://app.example.com` |
| `FIRST_SUPERUSER` | your administrator email |
| `TZ` | your timezone, such as `Europe/Berlin` |
The container speaks plain HTTP. For access from outside your network, put a
reverse proxy with HTTPS and WebSocket support in front of it, such as Caddy,
Traefik or nginx, or [enroll with the portal](../interface/portal.md). A
Caddyfile for `fluksio.example.com` is one line:
Keep the generated secrets. Stop any development stack with `make down`.
Set up DNS and a reverse proxy with HTTPS and WebSocket support before running
`make up`. That target starts the application services; it requires a proxy
on the Docker `proxy` network. Route
`app.example.com` to `fluksio-app:80`, `api.example.com` to `fluksio-api:8000`
and, optionally, `docs.example.com` to `fluksio-docs:80`.
```text
fluksio.example.com {
reverse_proxy localhost:8080
}
```
`docker/compose.traefik.yml` is an optional proxy configuration. It also needs
`EMAIL`, `USERNAME` and `HASHED_PASSWORD` for certificates and dashboard
authentication; `make init` does not set these up.
Set `FRONTEND_HOST` to `https://fluksio.example.com` and `TZ` to your
timezone, such as `Europe/Berlin`. See [Configuration](../reference/configuration.md)
for the other settings, which go under `environment` in `compose.yml`.
The published port from `make dev-lan` uses HTTP. For access outside your
network, use HTTPS or [enroll with the portal](../interface/portal.md).
To update, pull the repository and rebuild:
Back up `.env` and both persistent volumes: `app-flow-data` holds the database,
flows, secrets and artifacts; `app-redis-data` holds message values, node state
and queued work. Compose prefixes volume names with the project name
`fluksio-app`. Also back up any external MQTT or InfluxDB data you need.
## Next steps
- [Flows, nodes and messages](../concepts/flows.md): how the graph works.
- [Secrets, modules and alerts](../interface/operations.md): credentials and notifications.
- [Remote workers](../code/workers.md): run node code on another machine.
- [Configuration](../reference/configuration.md): settings and storage paths.
```sh
git pull
docker compose up -d --build
```
+32 -31
View File
@@ -42,9 +42,10 @@ Docker stores instance data under `/data` on the `app-flow-data` volume.
Docker uses `managed`. When Fluksio adopts your own venv, the Modules screen
is read-only; install packages into that environment yourself.
Back up the whole data directory and the persistent `SECRET_KEY`. In Docker,
keep `.env` and `app-flow-data`; back up `app-redis-data` too when preserving
live state. Compose prefixes volume names with the project name `fluksio-app`.
Back up the whole data directory and the persistent `SECRET_KEY`. The single
container keeps both, and its Redis, on the `/data` volume. The operator stack
under `docker/` keeps `.env` and `app-flow-data`, plus `app-redis-data` for
live state. Compose prefixes volume names with the project name.
Keep runtime files on persistent storage so rebuilds preserve credentials,
pairings and subscriptions.
@@ -52,13 +53,15 @@ pairings and subscriptions.
| Variable | Default | Purpose |
|---|---|---|
| `REDIS_HOST` | unset | unset uses memory; a Redis host enables persistent flow state |
| `REDIS_HOST` | unset | unset uses memory, or the image's own Redis in Docker; a Redis host enables persistent flow state |
| `REDIS_PORT` | `6379` | Redis port |
Live state includes message values, node memory and queued work. In-memory
state is lost on restart. The Docker stack uses Redis with append-only
persistence on its own `app-redis-data` volume. Its default sync policy may
lose about a second of recent writes after an unclean shutdown.
state is lost on restart. The Docker image starts a Redis of its own with
append-only persistence in `/data/redis` whenever `REDIS_HOST` is unset. Set
`REDIS_HOST` to use an external one instead, as the operator stack does with
its `app-redis-data` volume. The default sync policy may lose about a second of
recent writes after a power cut.
## Accounts and access
@@ -68,8 +71,7 @@ lose about a second of recent writes after an unclean shutdown.
| `ACCESS_TOKEN_EXPIRE_MINUTES` | `11520` (8 days) | session-token lifetime |
| `FIRST_SUPERUSER` | unset | initial administrator email; the CLI creates an account if needed |
| `FIRST_SUPERUSER_PASSWORD` | unset | initial password; the CLI generates and prints one if needed |
| `DOMAIN` | `localhost` | API hostname and OAuth issuer; Docker also uses it for routing |
| `FRONTEND_HOST` | `http://localhost:5173` | interface URL for links and metadata |
| `FRONTEND_HOST` | `http://localhost:5173`, `http://localhost:8000` in Docker | the address people and devices reach the instance at; panel links, mails and the OAuth issuer use it |
| `BACKEND_CORS_ORIGINS` | `[]` | allowed origins, as comma-separated URLs or a JSON list; `FRONTEND_HOST` is also allowed |
| `FORWARDED_ALLOW_IPS` | `127.0.0.1` | trusted proxies; Docker adds `172.16.0.0/12` |
| `SIGNUP_ENABLED` | `false` | allows account creation from the login page; otherwise use **Admin → Users** |
@@ -79,32 +81,31 @@ lose about a second of recent writes after an unclean shutdown.
Changing `SECRET_KEY` signs everyone out and makes existing encrypted
secrets unreadable. Back it up with your data.
## Interface API address
## Interface address
`VITE_API_URL` is a frontend **build argument**, so changing it requires
rebuilding the frontend image.
| Setup | Value |
|---|---|
| Production Docker | set `.env` to `https://api.<your-domain>` |
| `make dev-local` | overlay sets `http://api.localhost` by default |
| `make dev-lan` | overlay sets an empty value, using `/api` on the page's own origin |
Without an explicit value, the production Compose file falls back to
`https://api.${DOMAIN}`. Set `FRONTEND_HOST` and `BACKEND_CORS_ORIGINS` to
match your deployment as well; `make init` generates secrets, not deployment
URLs.
The LAN overlay also configures nginx to proxy `/api` and WebSocket requests
to the backend. An empty value needs that proxy when the frontend and backend
are served separately. See [facility automation](../getting-started/facility-automation.md#run-it-long-term)
for deployment settings.
The image serves the interface and the API on one origin, so the interface
calls the API as a path on whatever address served it. One build works behind
any port, hostname or reverse proxy. `FRONTEND_DIST` points at the built
interface and is set by the image. A checkout leaves it empty and runs the Vite
dev server, which needs its origin in `BACKEND_CORS_ORIGINS`.
## Image options
`WITH_FFMPEG=true` in `.env` installs `ffmpeg` into the backend image on the
next build, for node code that transcodes video. It is off by default because
it adds about 400 MB.
The `EXTRAS` build argument picks the optional features built into the image,
comma separated. Empty, the default, is the minimal engine.
| Extra | Adds |
|---|---|
| `mqtt` | the MQTT node |
| `influx` | the InfluxDB node |
| `email` | outbound mail, such as password resets |
| `sentry` | error reporting to `SENTRY_DSN` |
| `ml` | numpy, for the `mlp` node |
| `server` | all of the above |
A node whose extra is missing says so on the canvas. The same names work as
pip extras, such as `pip install 'fluksio[mqtt]'`. Connector packages built
with `make connectors` are installed into the image too.
## Environment
+3 -3
View File
@@ -6,9 +6,9 @@ settings in the node panel; use a **Function** node for your own Python.
`GET /flows/node-types` returns this list with each type's full parameter
schema, including any [connectors](#connectors) installed on your instance.
MQTT and InfluxDB need `pip install 'fluksio[server]'` on a Python installation.
The Docker image includes these dependencies. Run the broker and database
separately, or use the development stack's test services.
MQTT and InfluxDB need their [extras](configuration.md#image-options): `mqtt`
and `influx` in the image's `EXTRAS`, or `pip install 'fluksio[mqtt,influx]'`
on a Python installation. Run the broker and database separately.
## Function
+1 -1
View File
@@ -30,7 +30,7 @@ export default defineConfig({
/* Base URL to use in actions like `await page.goto('/')`. Defaults to the
suite's own stack (`make test-stack`), never the dev instance's data.
Point PLAYWRIGHT_BASE_URL elsewhere to test another running server; the
API origin follows it (see tests/config.ts). */
API is on the same origin (see tests/config.ts). */
baseURL: appUrl,
/* Chromium pins *.localhost to loopback whatever /etc/hosts says, so a
+13 -21
View File
@@ -124,27 +124,19 @@ async function captureRuns(page, dir) {
await page.waitForTimeout(1500)
await page.screenshot({ path: `${dir}/app-runs-compare.png`, fullPage: true })
// The API is its own host here, unless the stack serves it under the SPA's
// own origin (a LAN stack), whose CSP then refuses the other host.
const apiUrl = APP_URL.replace("//app.", "//api.")
const results = await page.evaluate(
async (bases) => {
for (const base of bases) {
const answer = await fetch(`${base}/api/v1/dashboards/`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
},
}).catch(() => null)
if (!answer?.ok) continue
const body = await answer.json()
return (body.data || [])
.map((one) => one.name)
.find((n) => n.endsWith("_results"))
}
return null
},
[apiUrl, ""],
)
// The API is a path on the SPA's own origin.
const results = await page.evaluate(async () => {
const answer = await fetch("/api/v1/dashboards/", {
headers: {
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
},
}).catch(() => null)
if (!answer?.ok) return null
const body = await answer.json()
return (body.data || [])
.map((one) => one.name)
.find((n) => n.endsWith("_results"))
})
// A dashboard read against two runs: the same page a live run is watched
// on, showing finished ones. This is the seam the feature exists for — and
+2 -14
View File
@@ -32,17 +32,5 @@ export const firstSuperuserPassword = getEnvVar("FIRST_SUPERUSER_PASSWORD")
export const appUrl =
process.env.PLAYWRIGHT_BASE_URL || "http://app.test.localhost"
export const apiUrl = process.env.PLAYWRIGHT_API_URL || apiOrigin(appUrl)
/** `app.<domain>` serves the SPA, `api.<domain>` serves its API. */
function apiOrigin(app: string): string {
const url = new URL(app)
if (!url.hostname.startsWith("app.")) {
throw new Error(
`Cannot derive the API origin from PLAYWRIGHT_BASE_URL=${app}. ` +
"Set PLAYWRIGHT_API_URL to name it.",
)
}
url.hostname = `api.${url.hostname.slice("app.".length)}`
return url.origin
}
/** The API is a path on the origin that serves the dashboard. */
export const apiUrl = process.env.PLAYWRIGHT_API_URL || new URL(appUrl).origin
Generated
+28 -256
View File
@@ -699,15 +699,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/ec/bb273b7208c606890dc36540fe667d06ce840a6f62f9fae7e658fcdc90fb/cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1", size = 385747, upload-time = "2024-06-04T15:51:37.499Z" },
]
[[package]]
name = "detect-installer"
version = "0.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" },
]
[[package]]
name = "dnspython"
version = "2.8.0"
@@ -764,10 +755,9 @@ wheels = [
]
[package.optional-dependencies]
standard = [
standard-no-fastapi-cloud-cli = [
{ name = "email-validator" },
{ name = "fastapi-cli", extra = ["standard"] },
{ name = "fastar" },
{ name = "fastapi-cli", extra = ["standard-no-fastapi-cloud-cli"] },
{ name = "httpx" },
{ name = "jinja2" },
{ name = "pydantic-extra-types" },
@@ -791,135 +781,10 @@ wheels = [
]
[package.optional-dependencies]
standard = [
{ name = "fastapi-cloud-cli" },
standard-no-fastapi-cloud-cli = [
{ name = "uvicorn", extra = ["standard"] },
]
[[package]]
name = "fastapi-cloud-cli"
version = "0.23.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "detect-installer" },
{ name = "fastar" },
{ name = "httpx" },
{ name = "pydantic", extra = ["email"] },
{ name = "rich-toolkit" },
{ name = "rignore" },
{ name = "sentry-sdk" },
{ name = "typer" },
{ name = "uvicorn", extra = ["standard"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/00/dc/63aaf9913f455e39a7027c27140edd887a87d47d65ac43532d77a51718e5/fastapi_cloud_cli-0.23.0.tar.gz", hash = "sha256:840895bb8d14309aeffc905e0dcd1334d18c6f5da54b735413a8f1cb385e581e", size = 95295, upload-time = "2026-07-28T14:03:33.463Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/96/7e9aba6fabce3cb05f320abeef5b81efd5134823ae85d1a517872cb83cbc/fastapi_cloud_cli-0.23.0-py3-none-any.whl", hash = "sha256:1cd2ffa56e92e92c1fc63acc426c214dd928cbeed2a4c7c6a9a5fc85ea73de16", size = 78058, upload-time = "2026-07-28T14:03:34.386Z" },
]
[[package]]
name = "fastar"
version = "0.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/52/5bee9a672f418008d34c708d66e89e8f6fed8f0812f406a1c92fb5e393a8/fastar-0.12.0.tar.gz", hash = "sha256:bba71522eae6a7627a5514ffdd4ac9645ef27d82e23931d79fd974bb49c3f2ad", size = 89647, upload-time = "2026-08-20T09:11:25.212Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/25/24eb7918c62a3053d1b7ac7c79262ddb50f942f599da263be865eaca9b8f/fastar-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d7a37d672233031bd41b000852e2520d34a1dd362c2e3b9000b6400a16b3bba4", size = 697081, upload-time = "2026-08-20T09:09:52.043Z" },
{ url = "https://files.pythonhosted.org/packages/23/03/856c091b035b9672c435c3f71e25d3fb5b9151693ca434f8804865bc3a93/fastar-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:787e3c8018848b56b7e47918f9a9ec0e13a68e39af6e4d386cf083b2973d254b", size = 624788, upload-time = "2026-08-20T09:09:34.712Z" },
{ url = "https://files.pythonhosted.org/packages/41/15/649c243ca97cbc2a36e9f650ff37d4491c444b580578fa54f2e6452cfdb8/fastar-0.12.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8fa10e692ca23aafb88ab441bb280724328da7b8cebddf3296c751b4e06324ed", size = 855861, upload-time = "2026-08-20T09:08:59.762Z" },
{ url = "https://files.pythonhosted.org/packages/2f/c8/0cbb9f59d598ff312f3781f8e2cc69101c2a843427c965df8dfe43436d51/fastar-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76115050e8f076fb96f1aa7662dc502b5232d21b610aec08035957a93a830cb1", size = 753839, upload-time = "2026-08-20T09:07:29.507Z" },
{ url = "https://files.pythonhosted.org/packages/e9/fb/54a3b1a4afb643157d16b518dc5c7d6b34d9715a7cbb055d7022c456e8a7/fastar-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0df33b22848b96beb8b229869abcc6592592d654f25b6cb1856c08717aaf1544", size = 748226, upload-time = "2026-08-20T09:07:47.228Z" },
{ url = "https://files.pythonhosted.org/packages/78/72/464ab962c9e0ead4e1cd48ede8805d07ea7ac11f3830673a9377def041b6/fastar-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27142e096eb5765b4dca66e4dbd29a9c61a0a4bc4c0ca649f6e661475da2bf8d", size = 916573, upload-time = "2026-08-20T09:08:05.488Z" },
{ url = "https://files.pythonhosted.org/packages/62/49/b14461a0edf407bf4b1be5674870798dad3f36196ac1e95d5ebfc2210019/fastar-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eaf9306c98e22aa7650c6ec02522d77b61c539bc81583897ca351de125ade14", size = 807627, upload-time = "2026-08-20T09:08:41.384Z" },
{ url = "https://files.pythonhosted.org/packages/0c/dc/a58f3c205d2934453ba643d0064d2735f359f736af2d15bc00dbf774e4d4/fastar-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deb0b3882626832361dd0fcee1f4ef1c6e2a7aaa4165e51c86ae0bab95caccd6", size = 808091, upload-time = "2026-08-20T09:09:17.721Z" },
{ url = "https://files.pythonhosted.org/packages/a4/ae/f55472d62e0185f9850b536947ec0772e8dad0dbe675ff99d814a9eab629/fastar-0.12.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:47739deed9e4ccf6514821d1c7a79c9188e4a5029d80b618078739d91e7ed9f0", size = 874371, upload-time = "2026-08-20T09:08:23.917Z" },
{ url = "https://files.pythonhosted.org/packages/d1/c8/a2156f8b274f241f8290e0fee56c3ad82f694b1776ea5557e4871a262824/fastar-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ec2100cea7274ec4b1ec77cd5613318a2d9fc9f7a291a6acf642a26e3856a93", size = 964805, upload-time = "2026-08-20T09:10:10.332Z" },
{ url = "https://files.pythonhosted.org/packages/42/d7/05913a98ba24c843c0c1765e48cc81523bf24f19db928d0af9da4ea23f3f/fastar-0.12.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f95a59a11ee59701a6c0048c90601063894102343c4bb09462eed5df0fab0077", size = 1024567, upload-time = "2026-08-20T09:10:30.236Z" },
{ url = "https://files.pythonhosted.org/packages/60/09/7a8ca1592f758c8e63e10c4cdb948a19c972fc16e136804b60a7aa68c978/fastar-0.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8383404eebbff9832652ddcd32f91bb4acd8e038b27597dc12aef33c4e72f7b7", size = 1062271, upload-time = "2026-08-20T09:10:49.969Z" },
{ url = "https://files.pythonhosted.org/packages/7b/d8/5f60c801b1cbb8bf18f1d33d8ebe7af6b7b2eaf966fce02d157391cdb726/fastar-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60de8b81a04cda78e5281d72ff6d0c50e4925621cc5eb55d99c2e344d1b291f4", size = 1018508, upload-time = "2026-08-20T09:11:09.771Z" },
{ url = "https://files.pythonhosted.org/packages/28/90/c8e6a51fffac4d4c6db21354b0a36c00b24d11ca2dee896185be7d15fef3/fastar-0.12.0-cp312-cp312-win32.whl", hash = "sha256:f62411993976d5c5a0d1090f8c809f96e0671d824ee0d844e5b5256bf31fe7a9", size = 450557, upload-time = "2026-08-20T09:11:59.143Z" },
{ url = "https://files.pythonhosted.org/packages/cf/26/453fc6e1de62a7e79a2f636424cbc46a38297a2fd172f2b24349ec8be620/fastar-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ed84522ddffb4c41c247f3e6823c763faf45af2ba1bf887eaab7f907e74e0bbe", size = 483015, upload-time = "2026-08-20T09:11:41.669Z" },
{ url = "https://files.pythonhosted.org/packages/1a/93/f5742b59178f8767ffc8913b584310784d0b6db4f0fe696f19b44cbd45c0/fastar-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:c03a61a149eaa857a4af8bf6c0bc0895bf668b8d5685130337a1ca42e60f9828", size = 458770, upload-time = "2026-08-20T09:11:28.165Z" },
{ url = "https://files.pythonhosted.org/packages/93/ee/bfce95bdf2bd61a1e311c7181e0ff99c39a6eebe4ca2bfd2d04eb403970b/fastar-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:654165090cdcac7ff13d43ee4012c366f0f2061ddf46658bc0ad248c8aa3960a", size = 697138, upload-time = "2026-08-20T09:09:53.682Z" },
{ url = "https://files.pythonhosted.org/packages/87/b0/dd24d87b58b4e99257ff0b0a53b60c89b0a32e89bd33c1f966e9bda58ad3/fastar-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:685d3d45943b43c32c71c8470552a615c90e06ca532db1b7a5633f01aa108f0d", size = 623873, upload-time = "2026-08-20T09:09:36.361Z" },
{ url = "https://files.pythonhosted.org/packages/5a/00/abacbfa2e94c1ff4b717e07ae2e5521ba84804884ff51f18f01706d024cf/fastar-0.12.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f8da75b5eca0d3b540e50ab6b7dfe4b148d7ae8a0b444a9781fd1219392859a0", size = 855719, upload-time = "2026-08-20T09:09:01.612Z" },
{ url = "https://files.pythonhosted.org/packages/8a/27/2781690ebabbce0d2a25b9efd359a13a5df2b0098120167e58b013c9f65a/fastar-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c2df8db1b7a4d783429ffa1125c53f7dd9534baae7218eb49273797691e2a8", size = 754159, upload-time = "2026-08-20T09:07:31.365Z" },
{ url = "https://files.pythonhosted.org/packages/1b/9b/1e6262fe31b2e8efc90feacf3f2213c9d2dff5fcb6e88f9084f26f18d62e/fastar-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:042fd43c4e0c3f3ba3f7b8a083694d1c4bd77d60ce266090d4eb96cb8a8021c2", size = 748002, upload-time = "2026-08-20T09:07:48.724Z" },
{ url = "https://files.pythonhosted.org/packages/4f/8e/b4792568d3e544b4e2b00b918744e4edb7e8c6be3c4fce268514febeb844/fastar-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6958a332c55052dd8090b03de238ca59190d625f4ba9c292b34e938ac64105ea", size = 915919, upload-time = "2026-08-20T09:08:07.102Z" },
{ url = "https://files.pythonhosted.org/packages/03/3f/0460223969f5dae9a49d09a29a93d5ccc359e701b0a18c0972a0ebce29b1/fastar-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b05c0c739c43b9228e57fc59eb68c38660c62030bcdea3a032b269df71f5bb8", size = 807788, upload-time = "2026-08-20T09:08:42.967Z" },
{ url = "https://files.pythonhosted.org/packages/0c/7b/68e681a12232ca2dc46d7d6de0c0fbe770137a204e4a3c0b5864a9548d67/fastar-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2bfad69679111e4567d4bad41fd795071c9335cd94bc0f26e24b7d19e95c9b1", size = 807587, upload-time = "2026-08-20T09:09:19.304Z" },
{ url = "https://files.pythonhosted.org/packages/81/8f/f93e981114034eb901d301690ad8e21edca6a2307b380d5e20e4e4862c7d/fastar-0.12.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:1155e1dd9c60cf636b6b3d35edfe242348f47286724fa84b5b4055c03d7fdbf6", size = 874095, upload-time = "2026-08-20T09:08:25.707Z" },
{ url = "https://files.pythonhosted.org/packages/2c/8e/74671e6cc6d9056347806ed9eefd0ea0bda25292848b32cc8cc321f956d8/fastar-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:29b87474b2e7c9e64549b87aeb2c1d68a94e78c887a3a8d88bf7b804ddbcc0dc", size = 965017, upload-time = "2026-08-20T09:10:12.048Z" },
{ url = "https://files.pythonhosted.org/packages/7f/55/44d5c532bfdbff48f3f6ce957744a612f7a169af9e4a673f00e21f8c83ed/fastar-0.12.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4587a08d6de2e62611278fc4cd36186a3ebbf6609d9e49df34d79a407d66f599", size = 1024377, upload-time = "2026-08-20T09:10:31.936Z" },
{ url = "https://files.pythonhosted.org/packages/a8/35/73d04733a06175211fef985f4e89604882b849cf1125c4a792ef022aa85c/fastar-0.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e7d7512b5c747edfce129448a72c8f6223323748ca3e98cc54401241bff70ee5", size = 1061994, upload-time = "2026-08-20T09:10:52.014Z" },
{ url = "https://files.pythonhosted.org/packages/f5/d2/c841b941fad02b5fc6e2277fc8ca35d963685fafc9335b76925bd166d11a/fastar-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ce9a7e7757074a3d0920a8bc3936cf4164f63786d8e1b64e425d2957ddcceace", size = 1018409, upload-time = "2026-08-20T09:11:11.495Z" },
{ url = "https://files.pythonhosted.org/packages/8d/c8/59a1791df4f128e6f8fffa03fd46d9b6c8ca24bcb6b7e9071973648edab3/fastar-0.12.0-cp313-cp313-win32.whl", hash = "sha256:61f1eec258b328182c6b6258641d33264ac3080fb7fcbf40ea1e326fc855d917", size = 450302, upload-time = "2026-08-20T09:12:00.704Z" },
{ url = "https://files.pythonhosted.org/packages/a7/9e/483982c1e60e3d9332c3b870230b34404b5b44340d647a97fe8f91d4fa0f/fastar-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:84caa362865cac75807c51afbeff2e9b313fc45f89e0865f7c8bf627ea721f4b", size = 482104, upload-time = "2026-08-20T09:11:43.655Z" },
{ url = "https://files.pythonhosted.org/packages/1e/e6/ec8ab1d44d73c0cad4f9e0ae4ce7b9330f2d504f835774802845dd465a42/fastar-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:a3de985d942247fa924e185ff2744a6da0005dcedbb39ae1b811bec11710e572", size = 458292, upload-time = "2026-08-20T09:11:29.693Z" },
{ url = "https://files.pythonhosted.org/packages/cf/f9/cf4b63a3b8bfa7dba8de8db364246d9e8390108a789de3f6a9628bb75c8a/fastar-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:7116a770a4e47262734fafe06d3e835a23b833e81b45b3054558301385a0f2a7", size = 698042, upload-time = "2026-08-20T09:09:55.257Z" },
{ url = "https://files.pythonhosted.org/packages/be/3f/c0ef2beffeeb01f9ec27b0ba8ce23697919b2247316d923f5a3bed6fc26b/fastar-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ebe324ffcb3e8efb2308255f60de911bfa4ccf10f087b6e60c62606f091f1807", size = 626452, upload-time = "2026-08-20T09:09:37.853Z" },
{ url = "https://files.pythonhosted.org/packages/e5/32/87c1887bcbe311a913a2b5b2f4481cce9bae7dd05115575c9f6cbbc892f5/fastar-0.12.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4af6be78f7ec6ef8e6da7d162361e4198be10fe81bfc95112f635c2c14e12922", size = 856780, upload-time = "2026-08-20T09:09:03.181Z" },
{ url = "https://files.pythonhosted.org/packages/89/d3/d4299d3c73df485d2bf095cca7e3829036f6a3fce26728ccfe4c3db3798b/fastar-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad38b27e93ba9c7de076ad694d224153bf8d66695acc27a114cba8087078fd54", size = 755380, upload-time = "2026-08-20T09:07:32.822Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ff/768ec3c6898fb20710ab222b8bc7caedcb9765a4c78122bb0267373f8255/fastar-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:968d64c203d10d257c2f2268cfc97d94f273fdfc64b37a4739fdb6e2cf2c3f03", size = 748975, upload-time = "2026-08-20T09:07:50.575Z" },
{ url = "https://files.pythonhosted.org/packages/36/7c/0ea040fdd20fe90c39e008f5c8b54b54058afb78faf261484c67b4ea5e94/fastar-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21256ccb3946730e3d601b6a9c9de61a127855957a7f0852cb14b0dbe15f8aeb", size = 917534, upload-time = "2026-08-20T09:08:08.782Z" },
{ url = "https://files.pythonhosted.org/packages/ce/5d/5a46751dff921b344ce995ee364801a867d80331406a96215336140182ed/fastar-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d89116b102fb7d4c47b6b723dc32d5c12122e7ffeb41b7a8b22cc777c9eca5", size = 808943, upload-time = "2026-08-20T09:08:44.785Z" },
{ url = "https://files.pythonhosted.org/packages/f3/67/6336def57f2b4701f93289a999a42de7743dfb78320a6b8931e9c0472da4/fastar-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1450e0325897e87594c29837fdd9e338b1d281f6c81d14d7a75b6776285b5b6e", size = 809624, upload-time = "2026-08-20T09:09:20.899Z" },
{ url = "https://files.pythonhosted.org/packages/6e/75/bd2540d70c6e4deeae3693cd41725dc42d4a96ced0394a9f845ac6afe148/fastar-0.12.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ca63fee43f07408efec09e1c0ae34a1b29ae52b8c1adc31bd6434ccc9e1741cb", size = 874787, upload-time = "2026-08-20T09:08:27.198Z" },
{ url = "https://files.pythonhosted.org/packages/6e/5c/13d20ec4d2c1e5ee15bf3b01fa43b282ff93f481d162e5ab507d8a341fbf/fastar-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fa5fd057b4f4537f04dd4e4f13be92433bf47bc479309335755531c5af34560", size = 966646, upload-time = "2026-08-20T09:10:13.697Z" },
{ url = "https://files.pythonhosted.org/packages/4a/6e/fcecbb90c6784ca0d5a54a9c3f7edf78ae5393c71b8dd0088130c3a4c82a/fastar-0.12.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4b395c3d4375809d0d55b5ae297f6bc037b90b382f35f056e453732e4f6f523c", size = 1025855, upload-time = "2026-08-20T09:10:33.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/8f/e865bf29f54c93a6fee55c7248ae64de91e0c68c6ce7f9a6d8c4adff471d/fastar-0.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:20d5e52c45e75a55ed27e7952487506269e9a64559d6cbfaf6977529db81298f", size = 1063126, upload-time = "2026-08-20T09:10:53.804Z" },
{ url = "https://files.pythonhosted.org/packages/42/19/4010752bcd7f7f290476fd4eec5f9611324b78a00575ad7224b4375fa124/fastar-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4497098ebdf3c1f89dbeafe44ebcd7f143f8a774641bbfa13d1d793104464c41", size = 1019854, upload-time = "2026-08-20T09:11:13.219Z" },
{ url = "https://files.pythonhosted.org/packages/ad/fe/9dc92104a021396d2cef2d7d3a8a8db2f3409bc19f41a7e8370642e980f8/fastar-0.12.0-cp314-cp314-win32.whl", hash = "sha256:56cb3b3c46edf26f054f2420573c7c69c03997203a7a867ca835bc305b4a0f30", size = 451451, upload-time = "2026-08-20T09:12:02.371Z" },
{ url = "https://files.pythonhosted.org/packages/02/a7/b2f55b50aa446958225360a20a16a87a4717c2deeda4ebc85edc62b58f64/fastar-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:81534df96e775ccaa37fcd1f45e06f48c245ff77e30d3bcb0d3c1101da9399e8", size = 483614, upload-time = "2026-08-20T09:11:45.439Z" },
{ url = "https://files.pythonhosted.org/packages/ae/0b/eb965694e157e09aa92857db73dd46d7cb10c24dae261a50dc46e0d361e2/fastar-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:fce60bd91fd982bf52e9a4c87820a44f92ac0d896bd64544891d6995fa6b8b98", size = 459422, upload-time = "2026-08-20T09:11:31.659Z" },
{ url = "https://files.pythonhosted.org/packages/31/21/58b9e84b20c50d8cbd31d25aa99ce852a463e60c8f88b3a44bfcdecb46da/fastar-0.12.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:22c882f1096199d5e63f3ea4d9313e10799fcbc70166d315aa07576148601326", size = 696156, upload-time = "2026-08-20T09:09:56.953Z" },
{ url = "https://files.pythonhosted.org/packages/05/84/227ad56548f2de419a5fb948cedc866ae0aec6fa0cf057b50e96e50aebb8/fastar-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:74c928183d7ca19056bc0eb24d8c1907c115cfb2382ad4a7c32ccbfb04ca0a0b", size = 624449, upload-time = "2026-08-20T09:09:39.39Z" },
{ url = "https://files.pythonhosted.org/packages/c7/51/8b05253149a568bc62b9a2d774f26a4b963dd91dae3da4e843a2cb7c5b40/fastar-0.12.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f1f3e70447f45e54b488eea8bd401cd3774b7b688d5b507915ae415058d8ac86", size = 855891, upload-time = "2026-08-20T09:09:04.968Z" },
{ url = "https://files.pythonhosted.org/packages/02/77/e62ef58301d2d79d624fc17fdaaee61c1a827d5f4000a8482be6a20bd10c/fastar-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96ae27bbb807e39e05097bdc2a6cba6820f31dd053aaf8cf0a045e5969041778", size = 754529, upload-time = "2026-08-20T09:07:34.489Z" },
{ url = "https://files.pythonhosted.org/packages/af/87/c35f3c3effae445d1b51b56ad6629df6d2001aeb5051646d081904be2081/fastar-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0d60682ed24fc6063b18e76a947f1cc0fcd0777709357b801b3e3458a87c2e5", size = 747923, upload-time = "2026-08-20T09:07:52.301Z" },
{ url = "https://files.pythonhosted.org/packages/3e/85/e9556fbaa8183db72c6d3cffdb56384299b2bd4d7a5e0eb990401ea221a0/fastar-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:644f6d06e312bc47dd315aa36fba723854859601e184113de7b887613ed9a5ad", size = 917040, upload-time = "2026-08-20T09:08:10.474Z" },
{ url = "https://files.pythonhosted.org/packages/53/8f/9a53202c1dcb3a5cd5661c04c201bff358919f23f6506e73533411cd96d4/fastar-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6c5addb01f206bd75f784540a8a11bbddc451293318a823e3fecaf0d62cc3c", size = 808402, upload-time = "2026-08-20T09:08:46.871Z" },
{ url = "https://files.pythonhosted.org/packages/07/aa/6a083aa6f7089f5ee7cfa99f15f299658953074035a6b7ed4dd52e21b8fe/fastar-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc892486eb242ac55dde185d547d2723289da50c6cdc06614868416576ba5c4f", size = 809412, upload-time = "2026-08-20T09:09:22.5Z" },
{ url = "https://files.pythonhosted.org/packages/d4/3e/1c45d7da2e67134f161c25f733ba231df67fdcdd018ceb9aecc50feb01d6/fastar-0.12.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a47a68d1b9bd59062af41d9809a340739ab1ba13cb5b4beb23466a621d6479f2", size = 874673, upload-time = "2026-08-20T09:08:28.874Z" },
{ url = "https://files.pythonhosted.org/packages/22/7f/c56ddd4e7c9035170b4513c2cd24673a4ce7a2a6b6cb9a18970672e14455/fastar-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a7d9fd762e7eab2262ec004aa9c714b53d303e11cca7c814b7b634d9d2424691", size = 965952, upload-time = "2026-08-20T09:10:15.447Z" },
{ url = "https://files.pythonhosted.org/packages/f8/5a/329cca10ae74a8b40791dd59868276e66d9e9321d1c1c4f1284e6c864f9d/fastar-0.12.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3766bf0aeeb6a03d114b185472593a2a3f0ff43f1b56c40fe0cda4283f9f4351", size = 1024392, upload-time = "2026-08-20T09:10:35.46Z" },
{ url = "https://files.pythonhosted.org/packages/42/ec/3c6bda956cd88d22ed9cb3fdc15e43a6fccf58ac64b8856f5aebd23117c8/fastar-0.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:860516a52a3bdcaa746be42fc1bdbbbd48e05c7579f9f61e30d78a7e065835bd", size = 1062506, upload-time = "2026-08-20T09:10:55.553Z" },
{ url = "https://files.pythonhosted.org/packages/b1/e1/bfb903579672ae213f5c81a5d289e3b9ca92ff2b3f789b08577de5f51050/fastar-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb75898c166ff6d232bceb68a5a7dc4a8f17239fb40925d169649a3967b76c4a", size = 1019468, upload-time = "2026-08-20T09:11:14.932Z" },
{ url = "https://files.pythonhosted.org/packages/61/46/c0f6a5b18981425879938fdf8ae38dfe7ef031d7164cb5b40eec29e5a4a3/fastar-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:c5755332572756061b29766a9ceff2c837d52d96828e58f908cb46cc49123bd7", size = 449931, upload-time = "2026-08-20T09:12:08.865Z" },
{ url = "https://files.pythonhosted.org/packages/d9/8f/832c7ca0d28642d5869a1aa194e4fb644bae2013101a1e9357168e6e1383/fastar-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:07d861c7ddf31bccb9615a0ba4c9f06987d1373a6b357183526d68cf9c3f5552", size = 482560, upload-time = "2026-08-20T09:11:52.086Z" },
{ url = "https://files.pythonhosted.org/packages/ad/10/17b9b24e129dcf3f4b4eb57b148888b128930180e9babd0e166cc828b2b6/fastar-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1d56e2a52bebd3e379d0cacc2b018b819a0b99f0dc19b4453f304c4e2fce5b3", size = 458209, upload-time = "2026-08-20T09:11:35.086Z" },
{ url = "https://files.pythonhosted.org/packages/0a/b7/21ec24e28f98554f727ba78a8703009d44ddc8946a91715743e574ed3f01/fastar-0.12.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:6109ec55528a975ab3644dc1c9ccccb2c2315daa66ca34f54e1e3dca60afa757", size = 698704, upload-time = "2026-08-20T09:09:58.576Z" },
{ url = "https://files.pythonhosted.org/packages/a7/f2/9e570204757c36d3ef1c94012294b1921a4c51af55da4684f206d88c09df/fastar-0.12.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6e94e2881c3aa53da5d9161e2e64c0d698c66506a62024d5900943098220ebc7", size = 626785, upload-time = "2026-08-20T09:09:40.933Z" },
{ url = "https://files.pythonhosted.org/packages/c4/d1/dfaf600497cdd4ddf3942c188d27171689f370d6ea2156b7646c606be8a0/fastar-0.12.0-cp315-cp315-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:682c531ac174d63919374eaab6fca91f92432cbf6a2262acb72719ba2e2a694d", size = 857172, upload-time = "2026-08-20T09:09:06.417Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d8/551b13387ae7ce88f165e7201757f031309a0cc9226663245096fba40554/fastar-0.12.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf3d5bfd73516b506a916f6809b90a4ad73ff5840c0af6cbe0142417a03b014", size = 755384, upload-time = "2026-08-20T09:07:36.044Z" },
{ url = "https://files.pythonhosted.org/packages/0e/cb/b8f2f3572e0ec95ca9f0eaf6889e6d7da65782461b5a983379f833d6a453/fastar-0.12.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbeb5fce858248d6b9b1fbef12c3e14d174432728c6e6eb1e2a63447432571c2", size = 750165, upload-time = "2026-08-20T09:07:53.779Z" },
{ url = "https://files.pythonhosted.org/packages/34/66/71d24540a462eb5dbff8993dcf674844ae8355b8f5ea63195c10207c10a0/fastar-0.12.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45164f7138613b76918101ea28842cb69c10ff441c1ba2d56d7c6b28053f28e2", size = 916891, upload-time = "2026-08-20T09:08:12.202Z" },
{ url = "https://files.pythonhosted.org/packages/84/b6/fc72480b7771ee14541b04ece4f94327a6522883ca51ebf46d9112723486/fastar-0.12.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e71715c64695bc80fd8fed2a82af30acb49b6bf085c06876cbfc2116b53cc7c", size = 808828, upload-time = "2026-08-20T09:08:48.413Z" },
{ url = "https://files.pythonhosted.org/packages/88/1e/0cb98e45845e442cb04b4dcc5d3a149184b188ec41425960fec62ee140b9/fastar-0.12.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8056fb0f3ff2213eb00234d32b2d701cd288b763d3430b67033a393b8d33b47", size = 809399, upload-time = "2026-08-20T09:09:23.995Z" },
{ url = "https://files.pythonhosted.org/packages/9b/00/e568dfd06fbb14a70f165a9972672dc21418edbf25590229e8fb2176cd19/fastar-0.12.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a843704912dc3b20e152743bd5fa3e225bf9cc23c34fea0debeceefead477e78", size = 874588, upload-time = "2026-08-20T09:08:30.455Z" },
{ url = "https://files.pythonhosted.org/packages/6e/8a/83ebe531a4a6fc93511c719ce8e505a2b23711e9ac4bed627cd7950eeb60/fastar-0.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:b4a7b6ca4a04e269aa26533ca8bfd0c674e4ee7328b0d3d80d45ab979a7e613c", size = 966301, upload-time = "2026-08-20T09:10:17.137Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7c/8c37e19cc9248b35d1b13cb6d27828755e2b23be9dd06437b77b572c0a01/fastar-0.12.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:883f1e06c0d9649a2e54b767b3384b47306098ea73be3ca288d562c4d73dbcaa", size = 1026601, upload-time = "2026-08-20T09:10:37.159Z" },
{ url = "https://files.pythonhosted.org/packages/de/ac/4fb738d3ab7ede5545ebf8205beae5c9e3fb97088b68b2c2103357349efd/fastar-0.12.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:74858c4648bdc4450a66f3d6ec16a4e61ba48c16c9898a88a061d3272f82c65b", size = 1063319, upload-time = "2026-08-20T09:10:57.318Z" },
{ url = "https://files.pythonhosted.org/packages/e3/8e/c31c84446226f20a217bad3921528bd8214b714ab1aa634afb183c1d852d/fastar-0.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b16451d5b50579e4eb7dc1761946bc6f6186df44fa84310a06f455c26eb4442e", size = 1019807, upload-time = "2026-08-20T09:11:16.731Z" },
{ url = "https://files.pythonhosted.org/packages/02/46/a44dee8cbc14601a91fc1818ea3e172cd5db2cd9af55f29c866bcee4c22d/fastar-0.12.0-cp315-cp315-win32.whl", hash = "sha256:ad8185a7b379e5cd81ef65209d21db4c63e8a62bcaa2d17fe40a8e50fcb28427", size = 451911, upload-time = "2026-08-20T09:12:04.003Z" },
{ url = "https://files.pythonhosted.org/packages/4a/e2/1feebd0c4e7ea7839f250a4ab77663d3a6149124a5ff6ee01859d788b6a3/fastar-0.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:a2819b9061cee89da560156b77230d4ffe81e75b8f64b9732ca34d7bc546e49e", size = 483441, upload-time = "2026-08-20T09:11:47.052Z" },
{ url = "https://files.pythonhosted.org/packages/fc/32/a2eaddc9b4f63d1560d4467df586d427da52717e01e88d9a0202439c07c9/fastar-0.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:a8a8130f236a5dc2ceab88486f77bbdd516d08dc949d0f04194305845cf44c19", size = 459131, upload-time = "2026-08-20T09:11:33.448Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7f/cffe7bae35e9e80789396f03cae4e8c4c761c3bee6003d2e622f6c9d8d6b/fastar-0.12.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:6f25c1aa6d55a457d95dc2163bbc27942e1541ca6792d4bf323a922688b8597e", size = 696600, upload-time = "2026-08-20T09:10:00.071Z" },
{ url = "https://files.pythonhosted.org/packages/ab/75/5e28ef81c3fac04a8d04a1068399069e28e4dbc6df221254c3a75b43af4d/fastar-0.12.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:aeb69fe64537deec4902f45ad9634b85d44ebb42ee1a33725d6584e8d9b33927", size = 625371, upload-time = "2026-08-20T09:09:42.547Z" },
{ url = "https://files.pythonhosted.org/packages/ad/aa/234c34a70d5e9e30420f43a9bad7a0046593ce5d65c2452f147f0a65e86a/fastar-0.12.0-cp315-cp315t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a83ae278bcc718dd155219fbbd552a16bd8c178effc5021600c3be2806a01cf", size = 856582, upload-time = "2026-08-20T09:09:08.082Z" },
{ url = "https://files.pythonhosted.org/packages/79/99/bd327da80d86309f6f5806cb3af70f91473d26fafe53f7cb646aaf35a4e5/fastar-0.12.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048c732e3ca28a132732f83130ccdab58d9b27dd36bb26bdeb42c2d48827da9", size = 754890, upload-time = "2026-08-20T09:07:37.76Z" },
{ url = "https://files.pythonhosted.org/packages/fa/01/a72bf87d4a26ef1bc94fde3b4c37c97500c1db5e2431bbfba865fabd8eb2/fastar-0.12.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:613466f628667af03de8f914de58a07bfb3ee1bd0347e3532ec9790df92a1e72", size = 748828, upload-time = "2026-08-20T09:07:55.27Z" },
{ url = "https://files.pythonhosted.org/packages/fb/63/2880890777d680271115ff6b2e8b891069610463308e7779e0fa9c8d9e75/fastar-0.12.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f59d3243d8913db385ab822be8f111f188218ea73f9f14f5d70c869a33ff4d1", size = 917262, upload-time = "2026-08-20T09:08:14.243Z" },
{ url = "https://files.pythonhosted.org/packages/08/06/b3293a2a8bf7bac81e848fcc1c67411a0770b186a3bfe5232ea6dac5e929/fastar-0.12.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b362e5404dab262e85f0d93bd950933a0935dac6a9f5f0516bba6c703c440ec", size = 808819, upload-time = "2026-08-20T09:08:49.914Z" },
{ url = "https://files.pythonhosted.org/packages/45/a4/0c0e9c1bc422272df07414d137d25eeb4bec8b3a8966c8e2b179967390fe/fastar-0.12.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d1e50c423cd064f29f11c98f6d995b8fa7df7bbc19f3fdb9f081859afd8e00b", size = 809620, upload-time = "2026-08-20T09:09:25.495Z" },
{ url = "https://files.pythonhosted.org/packages/18/34/f10ca8db20176ee9e685da80fcdad2c79e66485ed025ee16881e8113d97f/fastar-0.12.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:6857a79691c5c033a31d76c62ad02f2c92d173a0e1fb2fac7fcb7ac686108bd3", size = 874739, upload-time = "2026-08-20T09:08:31.946Z" },
{ url = "https://files.pythonhosted.org/packages/fd/a8/547881f35496d5b5b64f3e29552475d92d55d7bc213244bd7fb59938a116/fastar-0.12.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3053bb800b5375fbca8f96d256654ae3489c439f1b55766896d1c703d8281804", size = 965821, upload-time = "2026-08-20T09:10:18.83Z" },
{ url = "https://files.pythonhosted.org/packages/3d/57/a4376b6e70e6ca8909b8788b6e0ccc55cb9f0ada4c2bf7f95490d06659fe/fastar-0.12.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:067094312cbea82ef2efa3999dc31318ac539805965c9505d99654f01775cac1", size = 1025655, upload-time = "2026-08-20T09:10:38.892Z" },
{ url = "https://files.pythonhosted.org/packages/9a/ab/48e90600f5c08e7869c8cbd346cb9cf846414f85ca1f9e7aa6976222c6b5/fastar-0.12.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f69400ebb83a8d754aa7735165c12f8029ac577c3c08eb6d174eedc5901b7cb2", size = 1063028, upload-time = "2026-08-20T09:10:58.989Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/e113f8aebfd874d3c78945a094fc803ca295b92e12dc0d80ec81e2a7701f/fastar-0.12.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7d875d99946a11538f7ecb183f0a885d1d0a0495a6f1d6d2aa1de9b5fe6e5e9d", size = 1019546, upload-time = "2026-08-20T09:11:18.523Z" },
{ url = "https://files.pythonhosted.org/packages/51/45/72c2bf5ae3386407009fe51c40e548e41f70181bbe34a6477bb6078be047/fastar-0.12.0-cp315-cp315t-win32.whl", hash = "sha256:39dad3351f1399cd28e2e649b6651299ef857df6c745e598b00b4167dcf93dbc", size = 449932, upload-time = "2026-08-20T09:12:10.589Z" },
{ url = "https://files.pythonhosted.org/packages/7f/74/0adeb47b838c62ebd78bdf11ee2255e6e205ea176f9147978efa022ef855/fastar-0.12.0-cp315-cp315t-win_amd64.whl", hash = "sha256:00cda9a3f11871261a4e77a3b8f0eede85c9730fb7516811bcbcf96a2bb3b75b", size = 482858, upload-time = "2026-08-20T09:11:53.714Z" },
{ url = "https://files.pythonhosted.org/packages/bf/b9/b2ce5a79c57150d36aa8e0514dd65999ea76f091242aba93c403cd20e491/fastar-0.12.0-cp315-cp315t-win_arm64.whl", hash = "sha256:e8e0fb057b5c271f46f3300b539b0d3dab8c8cb2515205a37c818c2f68d16806", size = 458287, upload-time = "2026-08-20T09:11:36.952Z" },
]
[[package]]
name = "fluksio"
version = "0.1.6+dev"
@@ -929,13 +794,12 @@ dependencies = [
{ name = "croniter" },
{ name = "cryptography" },
{ name = "email-validator" },
{ name = "fastapi", extra = ["standard"] },
{ name = "fastapi", extra = ["standard-no-fastapi-cloud-cli"] },
{ name = "fluksio-worker" },
{ name = "http-ece" },
{ name = "httpx" },
{ name = "jinja2" },
{ name = "mcp" },
{ name = "numpy" },
{ name = "orjson" },
{ name = "pwdlib", extra = ["argon2", "bcrypt"] },
{ name = "pydantic" },
@@ -952,13 +816,29 @@ dependencies = [
]
[package.optional-dependencies]
email = [
{ name = "emails" },
]
influx = [
{ name = "influxdb-client", extra = ["async"] },
]
ml = [
{ name = "numpy" },
]
mqtt = [
{ name = "aiomqtt" },
]
parquet = [
{ name = "pyarrow" },
]
sentry = [
{ name = "sentry-sdk", extra = ["fastapi"] },
]
server = [
{ name = "aiomqtt" },
{ name = "emails" },
{ name = "influxdb-client", extra = ["async"] },
{ name = "numpy" },
{ name = "sentry-sdk", extra = ["fastapi"] },
]
@@ -973,20 +853,21 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiomqtt", marker = "extra == 'server'", specifier = ">=2.0.0" },
{ name = "aiomqtt", marker = "extra == 'mqtt'", specifier = ">=2.0.0" },
{ name = "alembic", specifier = ">=1.12.1,<2.0.0" },
{ name = "croniter", specifier = ">=1.3.0" },
{ name = "cryptography", specifier = ">=44.0.0" },
{ name = "email-validator", specifier = ">=2.1.0.post1,<3.0.0.0" },
{ name = "emails", marker = "extra == 'server'", specifier = ">=0.6,<1.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.114.2,<1.0.0" },
{ name = "emails", marker = "extra == 'email'", specifier = ">=0.6,<1.0" },
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.114.2,<1.0.0" },
{ name = "fluksio", extras = ["mqtt", "influx", "email", "sentry", "ml"], marker = "extra == 'server'" },
{ name = "fluksio-worker", editable = "worker" },
{ name = "http-ece", specifier = ">=1.2" },
{ name = "httpx", specifier = ">=0.25.1,<1.0.0" },
{ name = "influxdb-client", extras = ["async"], marker = "extra == 'server'", specifier = ">=1.40.0" },
{ name = "influxdb-client", extras = ["async"], marker = "extra == 'influx'", specifier = ">=1.40.0" },
{ name = "jinja2", specifier = ">=3.1.4,<4.0.0" },
{ name = "mcp", specifier = ">=1.29,<2" },
{ name = "numpy", specifier = ">=2.2.6" },
{ name = "numpy", marker = "extra == 'ml'", specifier = ">=2.2.6" },
{ name = "orjson", specifier = ">=3.10" },
{ name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" },
{ name = "pyarrow", marker = "extra == 'parquet'", specifier = ">=17" },
@@ -996,14 +877,14 @@ requires-dist = [
{ name = "python-multipart", specifier = ">=0.0.7,<1.0.0" },
{ name = "redis", specifier = ">=7.1.0" },
{ name = "rich", specifier = ">=13" },
{ name = "sentry-sdk", extras = ["fastapi"], marker = "extra == 'server'", specifier = ">=2.20.0" },
{ name = "sentry-sdk", extras = ["fastapi"], marker = "extra == 'sentry'", specifier = ">=2.20.0" },
{ name = "sqlmodel", specifier = ">=0.0.21,<1.0.0" },
{ name = "textual", specifier = ">=1.0" },
{ name = "threadpoolctl", specifier = ">=3.5" },
{ name = "uv", specifier = ">=0.5" },
{ name = "websockets", specifier = ">=14" },
]
provides-extras = ["parquet", "server"]
provides-extras = ["parquet", "mqtt", "influx", "email", "sentry", "ml", "server"]
[package.metadata.requires-dev]
dev = [
@@ -2168,11 +2049,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[package.optional-dependencies]
email = [
{ name = "email-validator" },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"
@@ -2491,110 +2367,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/ce/639d0d0ce3d25c5edbd1afecd308bb35dc04883a45ac9f0855c8aee4e919/rich_toolkit-0.20.3-py3-none-any.whl", hash = "sha256:419aa87516d5f3849cca553c6dcf707c02a36d508fcf996946606725d34a3002", size = 36195, upload-time = "2026-07-13T14:38:05.687Z" },
]
[[package]]
name = "rignore"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/7e/aa0640d74f6b4bb68466f5899bd5ed1680480732344c31a408504e215801/rignore-0.8.1.tar.gz", hash = "sha256:2b6cf58501e9ff1b6a71c3fd66c8a105311e1f23237626fd4c9c00606bb3d30f", size = 55535, upload-time = "2026-08-04T22:27:08.237Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/e6/b4230258f4c84be6b21f399c7ddf2c9675ee623d5aa54ae0881a618bed3b/rignore-0.8.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:487fb09f98c1e771f52b8433880c0012c25c5868db6e6f9f740ee0d3a13d4ae1", size = 848374, upload-time = "2026-08-04T22:23:31.79Z" },
{ url = "https://files.pythonhosted.org/packages/3e/6c/1fe281b2e9f8876ee5cd1b02ae891312c6543d60764c2a937dc4a5f28285/rignore-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e7ba47a5bb25ad45d39983047ecfbcba01ecda0145458c548cd3f390b73bb7", size = 815871, upload-time = "2026-08-04T22:23:33.212Z" },
{ url = "https://files.pythonhosted.org/packages/14/df/201b3ec49a4714fcad4515a6e1f2d55d3c9e3d8b43a5eb9307a8dfbff506/rignore-0.8.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac0a7cadcf6154dd60b2f101425644179b34c540a88b84089995c1745e8c623e", size = 884566, upload-time = "2026-08-04T22:23:34.516Z" },
{ url = "https://files.pythonhosted.org/packages/3e/f0/c4a64587c650f8c28c3d8afec3ce6a628a8d0ea41ee8af58389e5882d3fd/rignore-0.8.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab53a0908a1f24d2aaf4201920932134fbeb19be1d4c9619514ef7c781c9f3cf", size = 857414, upload-time = "2026-08-04T22:23:35.737Z" },
{ url = "https://files.pythonhosted.org/packages/00/6c/b8345ef35e5cd672df57a529f44c7f01a38daf3fcbd1afb7046290254c3a/rignore-0.8.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd43540d294fbcb66daf66836b6c57043c0d3af672722099cd4a0c91448d948e", size = 1132587, upload-time = "2026-08-04T22:23:37.109Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/ac83dc97554f183d70a02c202e089ea91aaddabddc173b0cdf9f23ce022d/rignore-0.8.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b4316b266e88c25ac7b4a0c765aa93c436dfa689a29429bd276878db45e17153", size = 913989, upload-time = "2026-08-04T22:23:38.356Z" },
{ url = "https://files.pythonhosted.org/packages/a9/a8/c6e6db62226df8cd3d950889925d826a0e35ac567cc5df548a872944f1ed/rignore-0.8.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e68cfc4ee0a2909952af2aebd608d1cd22f7d1cdce332cb0b5ea3762939865d", size = 927835, upload-time = "2026-08-04T22:23:39.887Z" },
{ url = "https://files.pythonhosted.org/packages/9d/dc/850c839ba4ffef76bd4ead7c3556c5e38d16f4cd9d581d01cb97496d3690/rignore-0.8.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:4d891dbe52b1aa4df22a69346e731ee3df219128956ca4391722773e6baab16c", size = 890590, upload-time = "2026-08-04T22:23:41.358Z" },
{ url = "https://files.pythonhosted.org/packages/9d/a2/a17d83b8923dad2d809c515b6dae89304e4057fc9422c6cc7af0a6ce4c3f/rignore-0.8.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:463f9734f06f2dd939c45f0e58d50c0648f799e9c41fb657e40d236054b14cfc", size = 963504, upload-time = "2026-08-04T22:23:42.804Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d8/a5147a1c60f73dedc61c3af1a1de36f11a4b2322d7482d4ef4306e6915a0/rignore-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97fceadfe03f3b8cd77cceeed92d00fad67f3ab80a0fd4d31e8dec104b721018", size = 1060732, upload-time = "2026-08-04T22:23:44.437Z" },
{ url = "https://files.pythonhosted.org/packages/67/33/7f5e99e2b63725ae843daedfabe02d14c513778ede4dd4ca91a1771d5107/rignore-0.8.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:43342bf37e7bb57d69f766678c2b19fecf2b3ec757be3f1bb2fb774d2be8c81a", size = 1132151, upload-time = "2026-08-04T22:23:45.967Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0a/45348d4292464afaa5d659d3afa64580dd74cd06bb0de327159b1f9ed07f/rignore-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:514000eabd84e8f6f6a589a30d00e76d7cebe0d99b6cacede1223f64dca4b742", size = 1139846, upload-time = "2026-08-04T22:23:47.493Z" },
{ url = "https://files.pythonhosted.org/packages/d2/ac/33e84dc397275787d11849b5be0175c297d6210a9d3058d1c1b56c945363/rignore-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8dfa13e24fc32d3df33d40788cd04e07f6b607f3c5306808ee0c285d2135effe", size = 1139121, upload-time = "2026-08-04T22:23:48.919Z" },
{ url = "https://files.pythonhosted.org/packages/42/33/a1de70bc925d072250371abfdfbeeebda463aca9c049dedd78180f2ead08/rignore-0.8.1-cp312-cp312-win32.whl", hash = "sha256:39b300b82e90f7a6ad102544b777f2aa8a26e9260ebb021399ee5a0a708a7c2d", size = 639566, upload-time = "2026-08-04T22:23:50.2Z" },
{ url = "https://files.pythonhosted.org/packages/2b/65/db4fd0b4b8daff0f9810e2142ec2375122bd756a45cb4f163e1ca3aea905/rignore-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:562055b620844654f0afff4df0a9c83f3679452c276cfa76cc5e49c14ddba481", size = 728384, upload-time = "2026-08-04T22:23:51.733Z" },
{ url = "https://files.pythonhosted.org/packages/78/13/bee47064ef6225ddb15333ec846b8e518b3e83d8654c8496734f8f3eb3c4/rignore-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:19606039e35dea7c1a8360f76fe39c5df6715d593a4f2e0c0c712b1b50985d10", size = 666363, upload-time = "2026-08-04T22:23:52.969Z" },
{ url = "https://files.pythonhosted.org/packages/82/b7/ecb7c0b91c8d68b313e3c41796860450b3f99d70ba2473d35f0cd3d1fc28/rignore-0.8.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:16b17b02d3e6ff3b88583af4809b64e14c77cf4de195763f063e5e77d70cd112", size = 847304, upload-time = "2026-08-04T22:23:54.247Z" },
{ url = "https://files.pythonhosted.org/packages/c2/78/71063269a004ab6759682cdc2c8553d1a42bab56aa4bf7f4ffb1468913a7/rignore-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:58d7172259fd45d8ba00f3e02af5af2113f547f4a7fefab42cab567aa7c999fc", size = 815633, upload-time = "2026-08-04T22:23:55.584Z" },
{ url = "https://files.pythonhosted.org/packages/83/94/d87afacf13f5e32a844ffdd2d906a3c8d5ce24de0a91ff02a605322f34bc/rignore-0.8.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c5ce3b10ce4b716abc535bc0fdd66b0b9fa9f5b1987b36d3f5ece7d0e2a9a81", size = 884581, upload-time = "2026-08-04T22:23:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/cf/ca/ea796f9ad84f8b671d682e27bd1a60a7679b844faaf582b11f7e527967f1/rignore-0.8.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c199fa2d4a898e9b686d846371ead3c8e08d3e29f78e2ecabf4c580820a8f764", size = 856803, upload-time = "2026-08-04T22:23:58.456Z" },
{ url = "https://files.pythonhosted.org/packages/75/71/0f3e6d0c421c7a5f998a21d3314f76f26793bc3af851a5e43b1e82d290aa/rignore-0.8.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7136b7ff29c37c8ec8effa3c59e27839482ba542af94fdc2581a59405e99037", size = 1133897, upload-time = "2026-08-04T22:23:59.923Z" },
{ url = "https://files.pythonhosted.org/packages/20/7a/0861528542be468f493e809064842fbb595706c0adce02d3a6e56f15ad2e/rignore-0.8.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32b0fcd01495cc4f4d10b5307f9f15818c9db752cfbd7a9ecb00b0f129a70dc2", size = 914178, upload-time = "2026-08-04T22:24:01.316Z" },
{ url = "https://files.pythonhosted.org/packages/1d/19/769c0a832d0f0a8d368f09ef7dee4caf53d46f391de489532bf44e1eee1d/rignore-0.8.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ca91c91a53135889945e77b286215ecd41c8f1090398db97ca3459c5a290eb6", size = 928035, upload-time = "2026-08-04T22:24:02.735Z" },
{ url = "https://files.pythonhosted.org/packages/50/e9/76cf08722a4d93836f7d416877f1a0d8e9a456f2e145ab57f12b301ccf70/rignore-0.8.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:67e30c0883f9aef3bcc45e5dc6980bc41f161da6c1b1a460f0bd87064c1d6594", size = 893393, upload-time = "2026-08-04T22:24:04.306Z" },
{ url = "https://files.pythonhosted.org/packages/d4/42/ede8c973b1f979b81a4cc48530107538aa9ec7df97c98759415f958bb0eb/rignore-0.8.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3012fc79b19953f76a67b2e6bae5f456742e96f0ed33d3c6c9390b0586fc2da9", size = 964677, upload-time = "2026-08-04T22:24:05.965Z" },
{ url = "https://files.pythonhosted.org/packages/57/22/70af384dde865285d71f5deccacedc89232a7e04fed558d035dd31d001af/rignore-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3a437c870f1465aba36eb4ac7108c09d5097c436123fdaec980e7a26a4595141", size = 1060291, upload-time = "2026-08-04T22:24:07.354Z" },
{ url = "https://files.pythonhosted.org/packages/9e/93/1c661799fb7270c55122efa0ac6211fb49b066579095765db90faed06af5/rignore-0.8.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:59f4f92ec5165619b3bc58130ab558bc618f9c5e9df06807339a85d965143fd5", size = 1131079, upload-time = "2026-08-04T22:24:08.708Z" },
{ url = "https://files.pythonhosted.org/packages/59/62/95655b6d2c6d2e82414f431b06fd7a3e6198adedfaf60de2b924c8f8d344/rignore-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:304ddf5f807c788c08a8210f9977131e7bfa9aa94704668d127689c00279ef41", size = 1140860, upload-time = "2026-08-04T22:24:10.149Z" },
{ url = "https://files.pythonhosted.org/packages/3e/bd/efd83ece6d828f908c751422849ef219847e084fb749f08aeb294e3a48d8/rignore-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1ce5c6d8f02badc55112b014d4fc9af662f0649869913d25785d7b3676ac19ed", size = 1139561, upload-time = "2026-08-04T22:24:11.633Z" },
{ url = "https://files.pythonhosted.org/packages/49/ae/7609946e2f17f4d820f53a50e683702806a21d313939a6bb2cb5478aebd6/rignore-0.8.1-cp313-cp313-win32.whl", hash = "sha256:711369d017f8959e3a253dedb76f6d2f43eb669a4228b2c1b796e30a5370ca36", size = 637370, upload-time = "2026-08-04T22:24:13.165Z" },
{ url = "https://files.pythonhosted.org/packages/ca/46/b14c894ddc21feda8ca21810c7227de567441d90602f6e4620d623b958be/rignore-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:0585eafcacf15f8f2764b8d03a579761296e8c51dd0558f8eb59040810ba8404", size = 727769, upload-time = "2026-08-04T22:24:14.487Z" },
{ url = "https://files.pythonhosted.org/packages/99/49/cdcd08577d5ddfc31c32f5b14528eba9dcb361ae652eed7f2d0207e313c0/rignore-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8ddf078c4b5da0665a83f7b558dee2d746293ffcce3dfe8786015aed0aac4c2", size = 664478, upload-time = "2026-08-04T22:24:15.804Z" },
{ url = "https://files.pythonhosted.org/packages/3c/13/7ceb0fe7a5a5a67e7f1cee65d8a9b09c52df53a4ff406d14940130a420b7/rignore-0.8.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3054fab93e2d2ad483cb89417f7b883cee025cb22286eb824af0073f2ffd5f7e", size = 849303, upload-time = "2026-08-04T22:24:17.126Z" },
{ url = "https://files.pythonhosted.org/packages/ba/87/fe151f1a3d93483b36e22e8a0e84382cff3b7fe9d7347cfb586727054c56/rignore-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60f1ae9eaa51b50d5afe99bd25ffb2cfc10729701ca08c179489555d2614f3ae", size = 817293, upload-time = "2026-08-04T22:24:18.584Z" },
{ url = "https://files.pythonhosted.org/packages/d4/08/cb82a725f040cc5d106a1fe33dec4031fb9d8863a4874a8dfb60c4db79f9/rignore-0.8.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6012766ea3a5a635d9b79f3e8c3797d5e47ce5e5dd81993c9f92a3ea4ff68b4c", size = 885260, upload-time = "2026-08-04T22:24:20.236Z" },
{ url = "https://files.pythonhosted.org/packages/06/77/6ba8d24fd151513347d9420fb383ec732b54886c6a5500e13f0bd7ec4072/rignore-0.8.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90e60f0073caae0f1d59c993adedafa3a57bc6fac551669677cb25aa7fa9d9b8", size = 857548, upload-time = "2026-08-04T22:24:21.769Z" },
{ url = "https://files.pythonhosted.org/packages/47/66/c54f448b077e89de99e85746677425fbb3a62d3721aeb9afa556dc3ca10f/rignore-0.8.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:121ab7ac93e39fd1d70098461c1ed9a6fb89d54e7bf8c60ae351b23b05cbb8c1", size = 1135817, upload-time = "2026-08-04T22:24:23.179Z" },
{ url = "https://files.pythonhosted.org/packages/c3/92/4b47da84a36687f10c09b43de7600d7dde8f56f2981e68b8822cfadd26aa/rignore-0.8.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:befb772556c8463c640b290f632b57440182edd39996708a33c50bcf437796f9", size = 913513, upload-time = "2026-08-04T22:24:24.719Z" },
{ url = "https://files.pythonhosted.org/packages/2b/a4/8428734c0217b5c0adbe752afc9603ca34434c6033584f63d6ca7ba50331/rignore-0.8.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a184fa45db8cdc7a8604d2df020a107be3fc0adc2e522b4e4eb5cd5b57d5f84", size = 929324, upload-time = "2026-08-04T22:24:26.059Z" },
{ url = "https://files.pythonhosted.org/packages/da/f6/ab3738e42d9d351033b8e8fb46ac4b8eb193545aaf3ed2f52f58fb8d5212/rignore-0.8.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e68a572efc126aa45195f1581a5ea97c4e36eeba6874a6635ae44daa4fbec7a4", size = 891130, upload-time = "2026-08-04T22:24:27.374Z" },
{ url = "https://files.pythonhosted.org/packages/1a/ba/9f374cf332eeea0e26cca6b6550678d0c69208e7ba4aea8ece0dc6b205eb/rignore-0.8.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c75ac1952ca8892422de328a925f4804120b149543042313bb3af7cbfdd65d64", size = 964769, upload-time = "2026-08-04T22:24:28.688Z" },
{ url = "https://files.pythonhosted.org/packages/37/46/7ee48265e34cb90ef4bcf85c63874da6d6a31af1c63fb7bca84fd5dbc188/rignore-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3538084cef4a66ba3fee7c453d17db7cfb32a6653456381b62afd8d53090d6fa", size = 1061779, upload-time = "2026-08-04T22:24:30Z" },
{ url = "https://files.pythonhosted.org/packages/01/23/51c725ba23f06b2809a0a676127ca3613e852bcab0532e58cab9f09b6fbb/rignore-0.8.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:28a3baab3d1b7ea42c38e492d4a100de3e5c67217432364c62b5719e0f04e96a", size = 1132595, upload-time = "2026-08-04T22:24:31.472Z" },
{ url = "https://files.pythonhosted.org/packages/0e/a0/671192e2cc8711694cf564f77e9e59d5e8c41f100bce67fbb384f2befec5/rignore-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:74996d8ed1a494ff8e61d9194a0dd3637e6b19a582cbed6fe0e1a4cc60e7b266", size = 1141117, upload-time = "2026-08-04T22:24:33.148Z" },
{ url = "https://files.pythonhosted.org/packages/01/df/b3a9a821b37361debc29fd009d21dc4af7f8313b76371c1640aff2765a59/rignore-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a2e5df9ba53e502f676b054c4d12371273204e2b9c6ec29afad3a155b9ad3399", size = 1140515, upload-time = "2026-08-04T22:24:34.708Z" },
{ url = "https://files.pythonhosted.org/packages/0c/69/bcf0f38713c86d462002c47b0ad14d655f8766a3b9235d8f6813e8245695/rignore-0.8.1-cp314-cp314-win32.whl", hash = "sha256:69db47c2fa51d88e93b4e4de4f44220ce7a9337e4b7526a16f5660b19974a8ca", size = 637894, upload-time = "2026-08-04T22:24:36.081Z" },
{ url = "https://files.pythonhosted.org/packages/44/f8/0c0e0bb59aea492e288d5c058ad873599d43dd5674495033fe9e8444b80d/rignore-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:1c668a541ecd8af3d7bd06c48c62e1c3e0a755028d57c07cee697a25d9b0678d", size = 728394, upload-time = "2026-08-04T22:24:37.496Z" },
{ url = "https://files.pythonhosted.org/packages/29/7c/c23926385e01d9b99fdf7bb7b86aaa60c2c1eb4de2a6303549f08497a0fa/rignore-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:d63559131efa9cbba82494ec8698e38bea403f214969066c661b3eb4da4ad4f9", size = 667896, upload-time = "2026-08-04T22:24:38.826Z" },
{ url = "https://files.pythonhosted.org/packages/38/9d/a70a895b47fa30446ffadd0a7add23494e18335ddc85c27cbc5a740374c7/rignore-0.8.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:b60f3751f681a12798927d51d1d6a46b81fe7f9923a513dd8fcc7947d785e12f", size = 847278, upload-time = "2026-08-04T22:24:40.329Z" },
{ url = "https://files.pythonhosted.org/packages/d9/cc/24fe37a08fd2083882422509c31bb58c14d85f2c7ab3b4ef3cc6f430d807/rignore-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d7d56bf138418e31991ce17b738c491fa1ac098bfcc8c3ba67fc0faeecfed357", size = 815968, upload-time = "2026-08-04T22:24:41.975Z" },
{ url = "https://files.pythonhosted.org/packages/a9/3d/1a260407a17e062859995d9f557b4ccbb36e6a0ef44a0d99409c836e5942/rignore-0.8.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f020b018577d0081a2b23da199d22c58f4cbd63935c8e991adffbb1a755b467", size = 884941, upload-time = "2026-08-04T22:24:43.414Z" },
{ url = "https://files.pythonhosted.org/packages/b3/80/70291634f2f2257fcabe1156926d7a10a87b0da1a8ba9ab2cafac37a4ded/rignore-0.8.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d4a70c9b857657c3a542fd367e16ae106fb6e3e4448cddb30e4edc604d5a025", size = 856667, upload-time = "2026-08-04T22:24:44.769Z" },
{ url = "https://files.pythonhosted.org/packages/1d/84/3811795d0b6fb3e8865d6a4f4eb3d3184e68a8557d7ee5b6d5a463196f54/rignore-0.8.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab668664e6388afc08fec50186ec21daf17c738ee71bb039d12cf5f14e964dde", size = 1134272, upload-time = "2026-08-04T22:24:46.213Z" },
{ url = "https://files.pythonhosted.org/packages/59/51/6a60ea8291346459e3f788bac4ecc9711b13271804c6eef47dc2ec59e6e7/rignore-0.8.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a3454cdd8bc145fd055dd45f650ccf1509e7b4edfc720152eb2be594b230f03", size = 914263, upload-time = "2026-08-04T22:24:47.604Z" },
{ url = "https://files.pythonhosted.org/packages/50/72/620d4baa44e44ee53ae516efcb714183fd592ba923dc7990bf57947ff610/rignore-0.8.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:425b962f3d68b86ebb785409153708a38b0658542c918ee706ea433e2547c805", size = 928135, upload-time = "2026-08-04T22:24:48.987Z" },
{ url = "https://files.pythonhosted.org/packages/50/60/06495efc96c2edcda647dbf7db770ab9307459d3b2f8598d9ae1f1ce9ea5/rignore-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:5094965794d163e1c4a71dccf3d1c3b3df86802e3afbfa461b300aa923cd7ea5", size = 890677, upload-time = "2026-08-04T22:24:50.318Z" },
{ url = "https://files.pythonhosted.org/packages/ee/92/396f751e69d543bfb7e9faa5b0ae3ee988fcd76d9c743857fdb26db0651b/rignore-0.8.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1467756e8454d3f816131bbad8b0efc52b7f6924e9b151f2962cec9e2f4af706", size = 964291, upload-time = "2026-08-04T22:24:51.836Z" },
{ url = "https://files.pythonhosted.org/packages/4e/0b/642483058dfbcbfc319d9cf48a05d8dbf9f7f6939b410eebdbc73dd9414c/rignore-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b5acf12993258f0eac4db81c30bed5469ebadd1b5bb2863c985fe0d802d94e0f", size = 1060972, upload-time = "2026-08-04T22:24:53.921Z" },
{ url = "https://files.pythonhosted.org/packages/64/2d/69b54a53d94baf2fb16ea1246f96bf093aae6a0e5d5c5cf5099c1fee4fb8/rignore-0.8.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:329ae0bea9598541cb818bbecb1e1f56fc1faae77828ebb3846ef2b24a050d05", size = 1130938, upload-time = "2026-08-04T22:24:55.395Z" },
{ url = "https://files.pythonhosted.org/packages/bf/36/c213d838c3c2237da8c51163a6c55acaeedc59759d4884d81ab6d7250721/rignore-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4069fcdff01999cd2d5a426eccda45a8b31a3e0eeee8b5a9da5a452714cbb2c7", size = 1140646, upload-time = "2026-08-04T22:24:56.741Z" },
{ url = "https://files.pythonhosted.org/packages/96/70/28125520727d72e2878c1a6a3dbd333e57f27730b463d629d170e2da3b62/rignore-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf28097a83e1237f87e11e33fb12def022e103cab2a20340a19753e8e222aebf", size = 1139989, upload-time = "2026-08-04T22:24:58.127Z" },
{ url = "https://files.pythonhosted.org/packages/7d/e6/8c0d1f2fa6d9b2f15e6a4bd20e7bfc8f4af436b8328ced782deda0a46057/rignore-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:c978f26c25e66c118da1e6218f34cc5ce2bf9c5cc3969011cde47bacbfe4a072", size = 640012, upload-time = "2026-08-04T22:24:59.597Z" },
{ url = "https://files.pythonhosted.org/packages/9e/c4/ef25f5b8d65ddb0e5266b13892c0c996dc221faac5b76a7b6fff4295ce44/rignore-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:790e14dba9e1f8a532071d64e1c2fa7529c71db75996e95519d06d3378cbbcd5", size = 728195, upload-time = "2026-08-04T22:25:01.358Z" },
{ url = "https://files.pythonhosted.org/packages/e8/6a/603b522d76a3acd66952e20e2ad7fa84c09d89436d707d6bb9ecb5ae55d3/rignore-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:484c0a4803a1eb9097d875df3200e60e58cbf316d4d40357f92e8e757ce529dd", size = 666282, upload-time = "2026-08-04T22:25:02.75Z" },
{ url = "https://files.pythonhosted.org/packages/ac/fb/b8de8a872bdb9150aa55783273f73e9aa253951d8f82856fe916fce3cae8/rignore-0.8.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1c2a29e7f54e879fa54982887de480b2cfa6e3cb51f01d70dd4e4035f5b79670", size = 849669, upload-time = "2026-08-04T22:25:04.116Z" },
{ url = "https://files.pythonhosted.org/packages/b2/24/4ca7ce428378e7ef938cb36573f22e842dcdb2655e266b32711eea08d16f/rignore-0.8.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:58449c827c36dba68133a5ee4137b67d7d6e050234a95c3a8f5e3cefbd6970ec", size = 817738, upload-time = "2026-08-04T22:25:05.489Z" },
{ url = "https://files.pythonhosted.org/packages/38/5a/78311079595c96e01a2404eff725468e25fad16d2836408046ffbdb33a69/rignore-0.8.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e250726b08957aabcf4e28aef4ea18bcccbd95f240cebb75a74b424f067dce4b", size = 885906, upload-time = "2026-08-04T22:25:06.956Z" },
{ url = "https://files.pythonhosted.org/packages/fe/57/7d456d2dcc881e6bf08275222ae36fff58343ee0fc47e1b26722f97b38ea/rignore-0.8.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62470f4d70d83f124381975614ed7c7db6f5c5fb4d777ee88f7bdb9a7d14b65c", size = 858240, upload-time = "2026-08-04T22:25:08.371Z" },
{ url = "https://files.pythonhosted.org/packages/64/3c/0f9bd7b9b3af37b216096e1dc81f9368d64c13625ddd34b4d595b316b47c/rignore-0.8.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d886d6bbdd0a1a3ef73bd38d6768cabc9b30bfd1be7b157b27a4ac7ec6c5244b", size = 1135145, upload-time = "2026-08-04T22:25:09.81Z" },
{ url = "https://files.pythonhosted.org/packages/83/06/f0087a520e3bf3d9532aa823ea141e5a076432b9d036bb506a99ddb534c2/rignore-0.8.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ed31657a20df59b0bd63283d278e8678749aa0154ba98fae0838a9845599695", size = 914331, upload-time = "2026-08-04T22:25:11.369Z" },
{ url = "https://files.pythonhosted.org/packages/f0/65/8ac37040f162cb965520286ef48472a87f47cb137df4068a0d32634166dc/rignore-0.8.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e425a42f6601bfed266f76f3294bca48efcfdb10c3c0c279fb2f977b1e2cc2bc", size = 929202, upload-time = "2026-08-04T22:25:13.057Z" },
{ url = "https://files.pythonhosted.org/packages/e0/93/19b752fda8a56424ee156ef6a9799c582eb6d2ffb233c7b91e103b79d54e/rignore-0.8.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:ff09db59f672d929bca88ee7089d3697256967df76a2bab8b187208f2b517bc0", size = 891752, upload-time = "2026-08-04T22:25:14.773Z" },
{ url = "https://files.pythonhosted.org/packages/a9/94/a5db364ddb136360c7c4ba75cb56ba2f7cd8321b296602f6ea7438fc1a83/rignore-0.8.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8dbc5898945c027dc0ae451de0c983e20bd8fa8297f40ee38d0ce1a3ed924d4c", size = 964714, upload-time = "2026-08-04T22:25:16.416Z" },
{ url = "https://files.pythonhosted.org/packages/f6/11/63316cd5a87402a42c75b6ada0331745975f0274cabb6a8aa85c043468ab/rignore-0.8.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1160abcd855964a9dd69f3d603ef57be14b1ea51ebaf50b07f737a3f3a8b89a6", size = 1062141, upload-time = "2026-08-04T22:25:18.094Z" },
{ url = "https://files.pythonhosted.org/packages/91/6e/19b4bdde3f53bb0c9427d65544f92f864a892d695559423ce01fd14c1a31/rignore-0.8.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:be994859b2cbbc69338351bc9908dd7d049de232e6eab5e998ec1feac3faf785", size = 1133487, upload-time = "2026-08-04T22:25:19.855Z" },
{ url = "https://files.pythonhosted.org/packages/1d/d3/fc41ea3164001fe84a2ec8b84d7bb48e7a73ebdcac6d68961c80f874dd4e/rignore-0.8.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:f52098f3245c7557229d8253ea3a53a03de03704685aba8a1ccd24a5f004db70", size = 1141434, upload-time = "2026-08-04T22:25:21.316Z" },
{ url = "https://files.pythonhosted.org/packages/34/35/b9d415a7e90c37b28b2d5fd6a074a4a3d572d5a0e0b596df0b4b85af8a78/rignore-0.8.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c676080bb70cbd5429052bb5a3827b35543876a42c792c6d713d9e162bdaa00d", size = 1140492, upload-time = "2026-08-04T22:25:22.763Z" },
{ url = "https://files.pythonhosted.org/packages/81/9b/49ac5f66862c796a04f0b7aad47ace4dea0a3ef52d313d2fb154821dd88a/rignore-0.8.1-cp315-cp315-win32.whl", hash = "sha256:ec800546b960d5044d2468d22dd0689210e846350c42cc9f02519c2a2528b3ef", size = 638365, upload-time = "2026-08-04T22:25:24.34Z" },
{ url = "https://files.pythonhosted.org/packages/38/76/90568f3af555074c5e1936b80573539658cda1dce4e89dd87f3174d88aa8/rignore-0.8.1-cp315-cp315-win_amd64.whl", hash = "sha256:4225f2d2f0b3e3c39a815f15dbe551f39f0d7f82e2a92d16bdbdb07ff4b6718e", size = 728711, upload-time = "2026-08-04T22:25:25.815Z" },
{ url = "https://files.pythonhosted.org/packages/0f/e0/3afb8bb7c6c538615d58c97f6e3b9ff7071f19442cd9cfb6f3ae6bbe0bee/rignore-0.8.1-cp315-cp315-win_arm64.whl", hash = "sha256:42480707cdd5f92d6b1252faada600e03ab22fbfd53b124c6f7b33c21e06b6cf", size = 668102, upload-time = "2026-08-04T22:25:27.251Z" },
{ url = "https://files.pythonhosted.org/packages/26/08/ce533f5c0b677dd7e82a3461e86afdf0cd99db9a73f3f561d1fabcd03fc8/rignore-0.8.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:51e608acd3c1aa834f2b7d956ff5380a0a6b6b595df80e665f2a2889982f1855", size = 846662, upload-time = "2026-08-04T22:25:28.718Z" },
{ url = "https://files.pythonhosted.org/packages/9b/43/4bb308ac5751881d7ba600e93d7f2e4a2c884af5028ac0ced4f1274c09c3/rignore-0.8.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:6602011b6e6ecd157f3b0a0a25b1777b40c541ca21de6ebe2137af713d0efa15", size = 816657, upload-time = "2026-08-04T22:25:30.177Z" },
{ url = "https://files.pythonhosted.org/packages/b3/f1/10a11bf9ebf03f7a8d9876ff023634cd4ce61845be9929f816ece4dfff11/rignore-0.8.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8db2be0d49175d5db4cd503f8dc62cbe687b9ebc71af4cede1c8b40bab8f4f4a", size = 884975, upload-time = "2026-08-04T22:25:31.906Z" },
{ url = "https://files.pythonhosted.org/packages/1b/97/c8dddb2dab738bd1b3a0b1639bc42d696bdd2765515a302620a35795c475/rignore-0.8.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:601ff49d8458a21d745908e35f792c02b45ecae4a37fa3cd1fc03fa065962bc0", size = 856951, upload-time = "2026-08-04T22:25:33.354Z" },
{ url = "https://files.pythonhosted.org/packages/a5/1d/ddcc1cddfb0a79c82eedbcd25ba0b014d346251cc08c1938b90951660cf8/rignore-0.8.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb58e93997d546fad4cfcd730a54d7c12cd7a61fe7a9b31bfc75402403dc559f", size = 1134401, upload-time = "2026-08-04T22:25:34.845Z" },
{ url = "https://files.pythonhosted.org/packages/13/0a/519d2e081e0b89c705bb4b282ed7540c185accc748c97c40dd80a333eb3a/rignore-0.8.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0378cf77b8d64560e0cb433deada18438b36ef7933dd284dd65347d03c56c429", size = 914389, upload-time = "2026-08-04T22:25:36.658Z" },
{ url = "https://files.pythonhosted.org/packages/ed/aa/c1e857c4b8f9dd388c38c8ca430917f24f1de44d9279d4f5d9edbcc94a0c/rignore-0.8.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0097a8c35106997d2b52851f0888777efe10e34772140a9fdb018b2f99238159", size = 928845, upload-time = "2026-08-04T22:25:38.421Z" },
{ url = "https://files.pythonhosted.org/packages/5f/9c/a9651d49fbf2a336074df0a09d01ee2d968dae902e88e51e32d822f5e9fc/rignore-0.8.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:2a809a250f1532b93dcc52e173ee71adcafdb536a125532687666047c4537ac0", size = 891090, upload-time = "2026-08-04T22:25:39.966Z" },
{ url = "https://files.pythonhosted.org/packages/53/98/d3242c45edfdc059fb80b73be9e27dac34639da40002ebbcd5c9b9acb821/rignore-0.8.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:580e019787032b430b857335a66db53d7ae0a200586b3a6b5d3ef227648300d6", size = 965283, upload-time = "2026-08-04T22:25:41.796Z" },
{ url = "https://files.pythonhosted.org/packages/56/37/e19e70395c2e5a06ba67e0a51e86ede1b4269b8d3ba091963c4f0a950f63/rignore-0.8.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f922dcf01e6a7ad26adfcb7f42635ae63066b883d409876129db62297061327e", size = 1061223, upload-time = "2026-08-04T22:25:43.782Z" },
{ url = "https://files.pythonhosted.org/packages/bc/bd/24b737f97c542e4883cfcc557b08565adc7c698e30836b8bc9855676d749/rignore-0.8.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4900bd1ba8938e5a5e601306504c7b8169783a2d44a8396c8fda4a9b659eed51", size = 1131772, upload-time = "2026-08-04T22:25:45.391Z" },
{ url = "https://files.pythonhosted.org/packages/aa/8d/bedc51ca98696998797af67163c039e0adc7835035fb929f10d33fb3d997/rignore-0.8.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:3d581bf107490abae4d40a9aa823a79e18c88a21658514ccaf95e42e85d278f7", size = 1141631, upload-time = "2026-08-04T22:25:46.875Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9c/946e55a7e5bc7b165faed3475451683580ddf8b45d63e67c39a0cdee5ce2/rignore-0.8.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:78490b93aec14a87fa4c23e634b1d1dcb2bf5dcb90923f3b03a3428892a13d9f", size = 1140328, upload-time = "2026-08-04T22:25:48.447Z" },
{ url = "https://files.pythonhosted.org/packages/b9/02/211daf0a7f957386d2223ef7a1601d36b42a6c1a6b93aae32db712868390/rignore-0.8.1-cp315-cp315t-win32.whl", hash = "sha256:e8315151a83b982fe972420372e80c550616095181d1549faaa1ec31f363f5ae", size = 640253, upload-time = "2026-08-04T22:25:50.025Z" },
{ url = "https://files.pythonhosted.org/packages/ca/0e/ccb682455ca380cc640c2a9536390fd907cc5ac0396118643f9d386e4af5/rignore-0.8.1-cp315-cp315t-win_amd64.whl", hash = "sha256:69be37202a052d9e13affed6b774f86d8bf3b417b15d54ad737033e56da07ec0", size = 728332, upload-time = "2026-08-04T22:25:51.937Z" },
{ url = "https://files.pythonhosted.org/packages/eb/ec/a2b634938c39600205dcd37015343e779dc8f112c234d905666dec2a66fd/rignore-0.8.1-cp315-cp315t-win_arm64.whl", hash = "sha256:4268e83bcb88f240ed4f43d73e7de9e4bcd8026bdfcfa836567d6c29863e5361", size = 666611, upload-time = "2026-08-04T22:25:53.857Z" },
]
[[package]]
name = "rpds-py"
version = "2026.6.3"