refactor: decouple — blueprint'ы + CSS/HTML разделение (по шаблону app-autotest)

This commit is contained in:
2026-07-31 22:16:22 +04:00
parent 6a0d5d00a2
commit 944238d224
16 changed files with 803 additions and 464 deletions
+89
View File
@@ -0,0 +1,89 @@
"""
routes/instances_routes.py — эндпоинты инстансов.
Blueprint "instances" с префиксом /api/v1/svc/instances.
GET /instances — список с пагинацией
GET /instances/<uid> — полные данные инстанса (state.params + state.out)
POST /instances — создать shell инстанса → 201 + Location: ./{uid}
КРИТИЧНО: POST /instances ОБЯЗАН возвращать заголовок Location.
HttpClient.post() в app-autotest достаёт instanceUid ИМЕННО из Location.
"""
from flask import Blueprint, jsonify, make_response, request
import mock_state
from config.loader import SERVICES
bp = Blueprint("instances", __name__, url_prefix="/api/v1/svc/instances")
@bp.route("", methods=["GET"])
@bp.route("/", methods=["GET"])
def list_instances():
"""GET /api/v1/svc/instances — список инстансов с пагинацией.
Query-параметры:
pageSize — размер страницы (по умолчанию 200, максимум 200)
page — номер страницы (по умолчанию 1)
Возвращает {results, pageSize, page, total}.
Пагинация: стоп по len(batch) < pageSize (как в реальном API).
"""
page_size = request.args.get("pageSize", 200, type=int)
page = request.args.get("page", 1, type=int)
result = mock_state.state.list_instances(page_size=page_size, page=page)
return jsonify(result)
@bp.route("/<uid>", methods=["GET"])
def get_instance(uid):
"""GET /api/v1/svc/instances/{uid} — полные данные инстанса.
Возвращает {instance: {instanceUid, serviceId, displayName, status,
explainedStatus, svc, dtCreate, state: {params: {...}, out: {...}}}}.
Если инстанс не найден — 404 с JSON-ошибкой.
"""
inst = mock_state.state.get_instance(uid)
if not inst:
return jsonify({"error": "instance not found"}), 404
return jsonify({"instance": dict(inst)})
@bp.route("", methods=["POST"])
@bp.route("/", methods=["POST"])
def create_instance():
"""POST /api/v1/svc/instances — создать shell инстанса.
Ожидает JSON: {serviceId: int, displayName: str?}
⛔ КРИТИЧНО: возвращает 201 + заголовок Location: ./{instanceUid}.
HttpClient.post() в app-autotest парсит Location чтобы получить UUID:
- Берёт последний сегмент после /
- Проверяет len(сегмент) >= 32 (длина UUID без дефисов)
Также возвращает {"instanceUid": uid} в теле — двойная защита
на случай если заголовок не дойдёт.
"""
body = request.get_json(silent=True) or {}
service_id = body.get("serviceId")
display_name = body.get("displayName", "unnamed")
# Валидация: serviceId обязателен
if service_id is None:
return jsonify({"error": "serviceId is required"}), 400
# Проверка что сервис существует в конфиге
svc_def = SERVICES.get(service_id)
if not svc_def:
return jsonify({"error": f"service {service_id} not found"}), 404
# Создать shell инстанса (статус "creating")
uid = mock_state.state.create_instance(service_id, display_name, svc_def)
# ⛔ Location ОБЯЗАТЕЛЕН — без него executor не получит instanceUid
resp = make_response(jsonify({"instanceUid": uid}), 201)
resp.headers["Location"] = f"./{uid}"
return resp