Files
app-autotest/site/routes/api_scenario.py
T

242 lines
9.0 KiB
Python

"""
API сценариев: CRUD определений + запуск + поллинг.
GET /api/scenarios — список из БД (для UI)
POST /api/scenario/run — запуск по definition_id (409 если уже RUNNING)
GET /api/scenario/run/<int:run_id> — статус конкретного запуска
GET /api/scenario/status — последние 10 запусков
CRUD определений:
GET /api/scenario/definitions — список
POST /api/scenario/definitions — создать
GET /api/scenario/definitions/<id> — один
PUT /api/scenario/definitions/<id> — обновить (version → 409)
DELETE /api/scenario/definitions/<id> — мягкое удаление
"""
from flask import Blueprint, current_app, jsonify, request
import threading
from api.auth import get_client, get_client_id, get_stand, get_token_info
from operations.scenario import run_scenario
from db.pool import get_conn, put_conn
from db.scenario_defs import (
list_definitions, get_definition, create_definition,
update_definition, delete_definition, lock_check,
)
bp = Blueprint("api_scenario", __name__)
# ── Запуск ──────────────────────────────────────────────
@bp.route("/api/scenarios")
def api_scenarios():
"""Список определений сценариев из БД (для UI запуска)."""
try:
defs = list_definitions(get_client_id(), get_stand())
result = [{"id": d["id"], "name": d["name"], "steps": len(d.get("steps", []))} for d in defs]
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/api/scenario/run", methods=["POST"])
def api_scenario_run():
"""Запустить сценарий по definition_id. 409 если уже RUNNING."""
try:
data = request.get_json()
def_id = data.get("definition_id")
if not def_id:
return jsonify({"error": "definition_id is required"}), 400
cid = get_client_id()
stand = get_stand()
# Lock 409
if not lock_check(cid, stand):
return jsonify({"error": "Another scenario is already running"}), 409
# Загрузить определение
defn = get_definition(def_id, cid, stand)
if not defn:
return jsonify({"error": "Definition not found"}), 404
steps = defn.get("steps", [])
if not steps:
return jsonify({"error": "Definition has no steps"}), 400
scenario_name = defn["name"]
user_email = get_token_info().get("email", "")
app_version = current_app.config.get("VERSION", "")
# Создать запись ДО потока
conn = get_conn()
if not conn:
return jsonify({"error": "no db"}), 500
run_id = None
try:
cur = conn.cursor()
cur.execute("""
INSERT INTO scenario_runs (client_id, stand, user_email, scenario_name,
status, current_step, total_steps, app_version,
definition_id, definition_version)
VALUES (%s, %s, %s, %s, 'RUNNING', 0, %s, %s, %s, %s)
RETURNING id
""", (cid, stand, user_email, scenario_name, len(steps), app_version,
def_id, defn["version"]))
run_id = cur.fetchone()[0]
conn.commit()
cur.close()
except Exception as e:
print(f"[API] create scenario_run error: {e}", flush=True)
conn.rollback()
return jsonify({"error": "Failed to create scenario_run"}), 500
finally:
put_conn(conn)
# Запуск в фоне
client = get_client()
threading.Thread(
target=run_scenario,
args=(client, steps, cid, stand, user_email, app_version, run_id, scenario_name),
daemon=True,
).start()
return jsonify({"status": "RUNNING", "scenario": scenario_name, "run_id": run_id}), 202
except Exception as e:
import traceback
print(f"[SCENARIO API] run error: {traceback.format_exc()}", flush=True)
return jsonify({"error": str(e)}), 500
@bp.route("/api/scenario/run/<int:run_id>")
def api_scenario_run_status(run_id):
"""Статус конкретного запуска."""
conn = get_conn()
if not conn:
return jsonify({"error": "no db"}), 500
try:
cur = conn.cursor()
cur.execute("""
SELECT id, created_at, scenario_name, status, current_step, total_steps,
duration_sec, error_log, app_version
FROM scenario_runs
WHERE id = %s AND client_id = %s AND stand = %s
""", (run_id, get_client_id(), get_stand()))
row = cur.fetchone()
if not row:
return jsonify({"error": "not found"}), 404
cols = [d[0] for d in cur.description]
d = dict(zip(cols, row))
if d["created_at"]:
d["created_at"] = d["created_at"].isoformat()
return jsonify(d)
except Exception as e:
return jsonify({"error": str(e)}), 500
finally:
put_conn(conn)
@bp.route("/api/scenario/status")
def api_scenario_status():
"""Последние 10 запусков."""
conn = get_conn()
if not conn:
return jsonify([])
try:
cur = conn.cursor()
cur.execute("""
SELECT id, created_at, scenario_name, status, current_step, total_steps,
duration_sec, error_log, app_version
FROM scenario_runs
WHERE client_id = %s AND stand = %s
ORDER BY created_at DESC
LIMIT 10
""", (get_client_id(), get_stand()))
rows = cur.fetchall()
cols = [d[0] for d in cur.description]
result = []
for r in rows:
d = dict(zip(cols, r))
if d["created_at"]:
d["created_at"] = d["created_at"].isoformat()
result.append(d)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
finally:
put_conn(conn)
# ── CRUD определений ────────────────────────────────────
@bp.route("/api/scenario/definitions", methods=["GET", "POST"])
def api_definitions():
cid = get_client_id()
stand = get_stand()
if request.method == "GET":
try:
defs = list_definitions(cid, stand)
return jsonify(defs)
except Exception as e:
return jsonify({"error": str(e)}), 500
if request.method == "POST":
try:
data = request.get_json()
name = (data.get("name") or "").strip()
if not name:
return jsonify({"error": "name is required"}), 400
steps = data.get("steps", [])
if not isinstance(steps, list):
return jsonify({"error": "steps must be array"}), 400
user = get_token_info().get("email", "")
result = create_definition(cid, stand, name, steps, user)
if result is None:
return jsonify({"error": "create failed"}), 500
return jsonify(result), 201
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/api/scenario/definitions/<int:def_id>", methods=["GET", "PUT", "DELETE"])
def api_definition(def_id):
cid = get_client_id()
stand = get_stand()
if request.method == "GET":
d = get_definition(def_id, cid, stand)
if not d:
return jsonify({"error": "not found"}), 404
return jsonify(d)
if request.method == "PUT":
try:
data = request.get_json()
name = (data.get("name") or "").strip()
version = data.get("version")
if not name:
return jsonify({"error": "name is required"}), 400
if not isinstance(version, int):
return jsonify({"error": "version (int) is required"}), 400
steps = data.get("steps", [])
if not isinstance(steps, list):
return jsonify({"error": "steps must be array"}), 400
user = get_token_info().get("email", "")
result = update_definition(def_id, cid, stand, name, steps, version, user)
if result.get("conflict"):
return jsonify({"error": "Conflict: definition was modified"}), 409
if result.get("error"):
return jsonify({"error": result["error"]}), 500
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
if request.method == "DELETE":
ok = delete_definition(def_id, cid, stand)
if not ok:
return jsonify({"error": "not found"}), 404
return jsonify({"ok": True})