""" routes/instances_routes.py — эндпоинты инстансов. Blueprint "instances" с префиксом /api/v1/svc/instances. GET /instances — список с пагинацией GET /instances/ — полные данные инстанса (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("/", 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