Files
flow/scripts/test-api-examples-local.sh

286 lines
8.3 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
HOST="${FLOW_HOST:-127.0.0.1}"
PORT="${FLOW_PORT:-7860}"
SUITES="${SUITES:-curl,python,javascript}"
EXECUTE_MODE="${EXECUTE_MODE:-true}"
export FLOW_AUTO_LOGIN="${FLOW_AUTO_LOGIN:-true}"
# /v1/workflows (docs Python workflow examples) requires this. Always enable for this
# harness so a user-wide FLOW_DEVELOPER_API_ENABLED=false does not break the suite.
export FLOW_DEVELOPER_API_ENABLED=true
cleanup() {
if [[ -f /tmp/flow-server.pid ]]; then
SERVER_PID="$(< /tmp/flow-server.pid)"
if kill -0 "$SERVER_PID" 2>/dev/null; then
kill "$SERVER_PID" || true
for _ in {1..15}; do
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
break
fi
sleep 1
done
kill -9 "$SERVER_PID" 2>/dev/null || true
fi
rm -f /tmp/flow-server.pid
fi
}
trap cleanup EXIT
# If PORT is already taken, Hanzo Flow may bind to PORT+1 while this script still uses PORT for
# curl and examples — you then hit the wrong server (e.g. 403 on /v1/workflows without dev API).
port_is_in_use() {
local host="$1" port="$2"
uv run python - "$host" "$port" <<'PY'
import socket
import sys
host, port = sys.argv[1], int(sys.argv[2])
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.5)
try:
sock.connect((host, port))
except (ConnectionRefusedError, TimeoutError, OSError):
sys.exit(1)
else:
sys.exit(0)
finally:
sock.close()
PY
}
pick_free_port() {
uv run python - "$HOST" <<'PY'
import socket
import sys
host = sys.argv[1]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((host, 0))
port = sock.getsockname()[1]
sock.close()
print(port)
PY
}
REQUESTED_PORT="$PORT"
if port_is_in_use "$HOST" "$PORT"; then
PORT="$(pick_free_port)"
echo "Port $REQUESTED_PORT was in use; using $PORT for this run (set FLOW_PORT to pin a port)."
fi
echo "Starting Hanzo Flow on http://$HOST:$PORT (developer API enabled for /v1/workflows)"
# Set on the command line so the server process always sees it (macOS launcher/exec paths).
FLOW_DEVELOPER_API_ENABLED=true uv run flow run --backend-only --host "$HOST" --port "$PORT" >/tmp/flow-server.log 2>&1 &
echo $! >/tmp/flow-server.pid
echo "Waiting for Hanzo Flow readiness (DB + services via /health_check)..."
# /health can respond before the Hanzo Flow app is fully up (uvicorn default).
for _ in {1..60}; do
if curl -sf "http://$HOST:$PORT/health_check" >/dev/null; then
break
fi
sleep 2
done
if ! curl -sf "http://$HOST:$PORT/health_check" >/dev/null; then
echo "Hanzo Flow did not become ready in time."
echo "See /tmp/flow-server.log for details."
exit 1
fi
echo "Creating API key for local examples..."
# Use HTTP only: a second process opening the same SQLite DB while the server runs
# causes "database is locked" during Alembic/initialize_services.
export BASE_URL="http://$HOST:$PORT"
FLOW_API_KEY="$(
uv run python - <<'PY'
import os
import time
import requests
from requests import RequestException
base = os.environ["BASE_URL"]
session = requests.Session()
user = os.environ.get("FLOW_SUPERUSER", "flow")
password = os.environ.get("FLOW_SUPERUSER_PASSWORD", "flow")
token = None
last_status = None
# Fail fast: short timeouts and small retry budget.
for attempt in range(1, 9):
try:
resp = session.get(f"{base}/v1/auto_login", timeout=8)
last_status = ("auto_login", resp.status_code)
if resp.status_code == 200:
token = resp.json()["access_token"]
break
resp2 = session.post(
f"{base}/v1/login",
data={"username": user, "password": password},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=8,
)
last_status = ("login", resp2.status_code)
if resp2.status_code == 200:
token = resp2.json()["access_token"]
break
except RequestException as exc:
last_status = ("request_error", str(exc))
print(f"Auth attempt {attempt}/8 failed: {last_status}", flush=True)
time.sleep(1)
if not token:
raise RuntimeError(f"Could not log in after retries (last {last_status})")
key_resp = session.post(
f"{base}/v1/api_key/",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"name": "local-docs-examples"},
timeout=8,
)
key_resp.raise_for_status()
print(key_resp.json()["api_key"])
PY
)"
if [[ -z "$FLOW_API_KEY" ]]; then
echo "Failed to create API key."
exit 1
fi
export FLOW_URL="http://$HOST:$PORT"
export FLOW_SERVER_URL="http://$HOST:$PORT"
export FLOW_API_KEY
if [[ "$EXECUTE_MODE" == "true" ]]; then
echo "Bootstrapping PROJECT_ID/FLOW_ID/FOLDER_ID for examples..."
BOOTSTRAP_VARS="$(
uv run python - <<'PY'
import json
import os
import sys
import uuid
from pathlib import Path
import requests
base_url = os.environ["FLOW_URL"]
api_key = os.environ["FLOW_API_KEY"]
headers = {"accept": "application/json", "Content-Type": "application/json", "x-api-key": api_key}
try:
project_name = f"api-example-project-{uuid.uuid4().hex[:8]}"
project_resp = requests.post(
f"{base_url}/v1/projects/",
headers=headers,
json={"name": project_name, "description": "Local docs examples bootstrap", "components_list": [], "flows_list": []},
timeout=20,
)
project_resp.raise_for_status()
project_data = project_resp.json()
project_id = project_data.get("id")
if not project_id:
raise RuntimeError(f"Project creation returned no id: {project_data}")
flow_name = f"api-example-flow-{uuid.uuid4().hex[:8]}"
flow_resp = requests.post(
f"{base_url}/v1/flows/",
headers=headers,
json={
"name": flow_name,
"description": "Local docs examples bootstrap",
"data": {"nodes": [], "edges": []},
},
timeout=20,
)
flow_resp.raise_for_status()
flow_data = flow_resp.json()
flow_id = flow_data.get("id")
if not flow_id:
raise RuntimeError(f"Flow creation returned no id: {flow_data}")
build_resp = requests.post(
f"{base_url}/v1/build/{flow_id}/flow",
headers=headers,
json={},
timeout=20,
)
build_resp.raise_for_status()
build_data = build_resp.json()
job_id = build_data.get("job_id")
if not job_id:
raise RuntimeError(f"Build start returned no job_id: {build_data}")
project_zip_path = Path("/tmp/flow-project-import.zip")
try:
export_resp = requests.get(
f"{base_url}/v1/projects/download/{project_id}",
headers={"accept": "application/json", "x-api-key": api_key},
timeout=30,
)
export_resp.raise_for_status()
project_zip_path.write_bytes(export_resp.content)
except Exception:
# Keep bootstrap resilient; some local instances can fail project export.
project_zip_path = Path("docs/docs/API-Reference/fixtures/project-import.zip")
# Many examples use FOLDER_ID and PROJECT_ID interchangeably for project-scoped routes.
print(f"PROJECT_ID={project_id}")
print(f"FOLDER_ID={project_id}")
print(f"FLOW_ID={flow_id}")
print(f"JOB_ID={job_id}")
print(f"PROJECT_IMPORT_FILE={project_zip_path}")
except Exception as exc:
print(f"ERROR={exc}", file=sys.stderr)
raise
PY
)"
while IFS='=' read -r key value; do
if [[ -n "$key" && -n "$value" ]]; then
export "$key=$value"
fi
done <<< "$BOOTSTRAP_VARS"
echo "Using PROJECT_ID=$PROJECT_ID FLOW_ID=$FLOW_ID FOLDER_ID=$FOLDER_ID"
fi
EXAMPLE_MODE_ARGS=""
if [[ "$EXECUTE_MODE" == "true" ]]; then
EXAMPLE_MODE_ARGS="--execute"
fi
echo "Running suites: $SUITES (execute=$EXECUTE_MODE)"
IFS=',' read -ra SUITE_LIST <<< "$SUITES"
for suite in "${SUITE_LIST[@]}"; do
suite="$(echo "$suite" | xargs)"
case "$suite" in
curl)
bash docs/docs/API-Reference/curl-examples/test-curl-examples.sh $EXAMPLE_MODE_ARGS
;;
python)
bash docs/docs/API-Reference/python-examples/test-python-examples.sh $EXAMPLE_MODE_ARGS
;;
javascript)
bash docs/docs/API-Reference/javascript-examples/test-javascript-examples.sh $EXAMPLE_MODE_ARGS
;;
"")
;;
*)
echo "Unknown suite: '$suite'. Valid values: curl, python, javascript"
exit 1
;;
esac
done
echo "Done. Server log: /tmp/flow-server.log"