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:
+12
-14
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
Executable
+23
@@ -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 "$@"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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=
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -18,6 +18,3 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
|
||||
frontend:
|
||||
container_name: !reset null
|
||||
|
||||
+15
-67
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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": [{
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user