#!/usr/bin/env python3 """ Скрипт для подключения к SDK ФПГ (GigaDoc) и тестирования всех этапов. Этапы: 1. Авторизация (получение access token) 2. Создание сессии 3. Подключение к WebSocket 4. Отправка изображений 5. Получение результатов 6. Удаление сессии Логирование ведётся в файл fpg_test.log """ import argparse import asyncio import glob import json import logging import os import sys import time from datetime import datetime from pathlib import Path import requests # ─── Конфигурация ───────────────────────────────────────────── OAUTH_URL = "https://oauth.sbermed.ai/token" CLIENT_ID = "client_fpg_front" CLIENT_SECRET = os.environ.get("FPG_CLIENT_SECRET", "") REST_HOST = "gigadoc-sdk.sbermed.ai" REST_BASE = f"https://{REST_HOST}" # WebSocket (рабочий хост подтверждён: gigadoc-sdk.sbermed.ai:8443) WS_HOST = "gigadoc-sdk.sbermed.ai" WS_PORT = 8443 WS_PATH_TEMPLATE = "/api/v2/ws/predict/{uid}/image" DEVICE_ID = "web_prod" FORM_FACTOR = "web_prod" LOG_FILE = "fpg_test.log" # ─── Логирование ────────────────────────────────────────────── def setup_logging(log_file: str = LOG_FILE): """Настроить логирование в файл и консоль.""" logger = logging.getLogger("fpg") logger.setLevel(logging.DEBUG) # Формат formatter = logging.Formatter( "%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) # Файловый хендлер fh = logging.FileHandler(log_file, encoding="utf-8") fh.setLevel(logging.DEBUG) fh.setFormatter(formatter) logger.addHandler(fh) # Консольный хендлер ch = logging.StreamHandler(sys.stdout) ch.setLevel(logging.INFO) ch.setFormatter(formatter) logger.addHandler(ch) # Включаем логирование библиотек (requests, websockets, urllib3) logging.getLogger("urllib3").setLevel(logging.DEBUG) logging.getLogger("urllib3").addHandler(fh) logging.getLogger("websockets").setLevel(logging.DEBUG) logging.getLogger("websockets").addHandler(fh) logging.getLogger("asyncio").setLevel(logging.DEBUG) logging.getLogger("asyncio").addHandler(fh) return logger logger = logging.getLogger("fpg") def log_headers(headers: dict, prefix: str = " "): """Залогировать HTTP-заголовки (маскируя секреты).""" for k, v in headers.items(): if k.lower() in ("authorization", "client_secret", "cookie", "set-cookie"): # Маскируем секретные заголовки if isinstance(v, str) and len(v) > 20: v = v[:20] + "..." + v[-5:] else: v = "***" logger.debug(f"{prefix}{k}: {v}") def log_response(resp, prefix: str = " "): """Залогировать HTTP-ответ подробно.""" logger.debug(f"{prefix}HTTP {resp.status_code} {resp.reason}") logger.debug(f"{prefix}Headers:") log_headers(dict(resp.headers), prefix + " ") body = resp.text if len(body) > 2000: logger.debug(f"{prefix}Body ({len(body)} bytes): {body[:2000]}...") else: logger.debug(f"{prefix}Body: {body}") # ─── Этап 1: Авторизация ────────────────────────────────────── def get_token() -> str: """Получить access token через client_credentials.""" logger.info("=" * 60) logger.info("ЭТАП 1: Авторизация") logger.info("=" * 60) logger.info(f"POST {OAUTH_URL}") logger.debug(" Запрос:") logger.debug(f" grant_type=client_credentials") logger.debug(f" client_id={CLIENT_ID}") logger.debug(f" client_secret={'*' * len(CLIENT_SECRET)}") try: resp = requests.post( OAUTH_URL, data={ "grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, }, timeout=15, ) logger.info(f"HTTP {resp.status_code}") logger.debug(" Ответ:") log_response(resp, " ") resp.raise_for_status() data = resp.json() token = data["access_token"] expires_in = data.get("expires_in", "?") logger.info(f"✅ Токен получен (действует {expires_in} сек)") logger.debug(f" token_type: {data.get('token_type')}") logger.debug(f" scope: {data.get('scope')}") logger.debug(f" refresh_expires_in: {data.get('refresh_expires_in')}") logger.debug(f" not-before-policy: {data.get('not-before-policy')}") logger.debug(f" access_token: {token[:80]}...") logger.debug(f" access_token (полный, {len(token)} chars): {token}") return token except requests.exceptions.RequestException as e: logger.error(f"❌ Ошибка авторизации: {e}") if hasattr(e, "response") and e.response is not None: logger.error(f" Статус: {e.response.status_code}") logger.error(f" Ответ: {e.response.text}") raise # ─── Этап 2: Создание сессии ────────────────────────────────── def create_session(token: str) -> str: """Создать сессию и вернуть uid.""" logger.info("=" * 60) logger.info("ЭТАП 2: Создание сессии") logger.info("=" * 60) url = f"{REST_BASE}/api/v1/predict/new" logger.info(f"POST {url}") logger.debug(" Запрос:") logger.debug(f" device_id={DEVICE_ID}") logger.debug(f" form_factor={FORM_FACTOR}") logger.debug(f" Authorization: Bearer {token[:30]}...") try: resp = requests.post( url, params={"device_id": DEVICE_ID, "form_factor": FORM_FACTOR}, headers={ "Authorization": f"Bearer {token}", "Accept": "application/json", }, timeout=15, ) logger.info(f"HTTP {resp.status_code}") logger.debug(" Ответ:") log_response(resp, " ") resp.raise_for_status() uid = resp.json()["uid"] logger.info(f"✅ Сессия создана: {uid}") return uid except requests.exceptions.RequestException as e: logger.error(f"❌ Ошибка создания сессии: {e}") if hasattr(e, "response") and e.response is not None: logger.error(f" Статус: {e.response.status_code}") logger.error(f" Ответ: {e.response.text}") raise # ─── Этап 3: WebSocket ──────────────────────────────────────── async def ws_scan(token: str, uid: str, ws_host: str, ws_port: int, image_dir: str): """Подключиться по WebSocket, отправить изображения, получить результат.""" import socket import ssl import websockets logger.info("=" * 60) logger.info("ЭТАП 3: WebSocket подключение") logger.info("=" * 60) # ── DNS резолв ── logger.info(f"DNS резолв: {ws_host}") try: ips = socket.getaddrinfo(ws_host, ws_port) for addr in ips: logger.debug(f" {addr[0].name} -> {addr[4]}") logger.info(f"✅ DNS резолв успешен: {len(ips)} записей") except socket.gaierror as e: logger.warning(f"⚠️ DNS резолв не удался: {e}") logger.warning(f" Хост {ws_host} не найден. Пробуем другие варианты...") # ── Собираем изображения ── patterns = ["*.jpg", "*.jpeg", "*.png"] images = [] for p in patterns: images.extend(glob.glob(os.path.join(image_dir, p))) images.sort() if not images: logger.warning(f"Нет изображений в {image_dir}, использую тестовое") images = ["/tmp/test_face.jpg"] if not os.path.exists(images[0]): logger.error("Тестовое изображение не найдено") return None logger.info(f"Найдено изображений: {len(images)}") for img in images: size = os.path.getsize(img) logger.debug(f" {img} ({size} bytes)") # ── Пробуем несколько вариантов URI ── uris = [ f"wss://{ws_host}:{ws_port}{WS_PATH_TEMPLATE.format(uid=uid)}", f"wss://{REST_HOST}:{ws_port}{WS_PATH_TEMPLATE.format(uid=uid)}", f"wss://{REST_HOST}:443{WS_PATH_TEMPLATE.format(uid=uid)}", ] last_error = None for uri in uris: logger.info(f"Попытка подключения: {uri}") logger.info(" ── Заголовки WebSocket-запроса ──") logger.info(f" GET {WS_PATH_TEMPLATE.format(uid=uid)} HTTP/1.1") logger.info(f" Host: {ws_host}") logger.info(f" Upgrade: websocket") logger.info(f" Connection: Upgrade") logger.info(f" Sec-WebSocket-Version: 13") logger.info(f" Sec-WebSocket-Protocol: bearer. (авторизация через subprotocol)") logger.info(f" Origin: https://gigadoc.sbermed.ai") logger.info(f" (полный токен: {token})") logger.info(" ────────────────────────────────") try: async with websockets.connect( uri, open_timeout=10, subprotocols=[f"bearer.{token}"], origin="https://gigadoc.sbermed.ai", extra_headers={ "Cache-Control": "no-cache", "Pragma": "no-cache", }, compression="deflate", ping_interval=20, ping_timeout=20, max_size=None, ) as ws: logger.info(f"✅ WebSocket подключен: {uri}") logger.debug(f" Протокол: {ws.subprotocol}") logger.debug(f" Расширения: {ws.extensions if hasattr(ws, 'extensions') else 'n/a'}") # Отправляем изображения logger.info("=" * 60) logger.info("ЭТАП 4: Отправка изображений") logger.info("=" * 60) result = None for i, img_path in enumerate(images): with open(img_path, "rb") as f: img_data = f.read() logger.info(f" [{i+1}/{len(images)}] WS → отправка изображения: {img_path} ({len(img_data)} bytes, binary)") t_start = time.time() await ws.send(img_data) t_send = time.time() - t_start logger.debug(f" Отправлено за {t_send*1000:.1f} мс") # Читаем ответ try: resp = await asyncio.wait_for(ws.recv(), timeout=5) t_recv = time.time() - t_start logger.debug(f" Ответ получен за {t_recv*1000:.1f} мс") try: data = json.loads(resp) progress = data.get("progress", "?") face_ok = data.get("face_ok", "?") # Логируем прогресс if i % 10 == 0 or (isinstance(progress, (int, float)) and progress >= 1.0): logger.info(f" [{i+1}/{len(images)}] progress={progress} face_ok={face_ok}") # Логируем полный ответ на DEBUG logger.debug(f" Полный ответ: {json.dumps(data, ensure_ascii=False)}") # Финальный результат if isinstance(progress, (int, float)) and progress >= 1.0: logger.info("=" * 60) logger.info("ЭТАП 5: ФИНАЛЬНЫЙ РЕЗУЛЬТАТ") logger.info("=" * 60) logger.info(json.dumps(data, indent=2, ensure_ascii=False)) result = data break except json.JSONDecodeError: logger.debug(f" Ответ не JSON: {resp[:200]}") except asyncio.TimeoutError: logger.debug(f" [{i+1}/{len(images)}] Таймаут ожидания ответа (5 сек)") if result is None: logger.warning("Финальный результат не получен (прогресс не достиг 1.0)") return result except Exception as e: last_error = e logger.warning(f"❌ Не удалось подключиться к {uri}: {type(e).__name__}: {e}") logger.debug(f" Детали ошибки: {e}") logger.error(f"❌ Все варианты WebSocket подключения не удались") logger.error(f" Последняя ошибка: {type(last_error).__name__}: {last_error}") return None # ─── Этап 6: Удаление сессии ────────────────────────────────── def delete_session(token: str, uid: str): """Удалить сессию.""" logger.info("=" * 60) logger.info("ЭТАП 6: Удаление сессии") logger.info("=" * 60) url = f"{REST_BASE}/api/v1/predict/{uid}" logger.info(f"DELETE {url}") logger.debug(f" Authorization: Bearer {token[:30]}...") try: resp = requests.delete( url, headers={"Authorization": f"Bearer {token}"}, timeout=15, ) logger.info(f"HTTP {resp.status_code}") logger.debug(" Ответ:") log_response(resp, " ") if resp.status_code == 200: logger.info(f"✅ Сессия {uid} удалена") else: logger.warning(f"⚠️ Сессия {uid} не удалена: {resp.status_code} {resp.text}") except requests.exceptions.RequestException as e: logger.error(f"❌ Ошибка удаления сессии: {e}") if hasattr(e, "response") and e.response is not None: logger.error(f" Статус: {e.response.status_code}") logger.error(f" Ответ: {e.response.text}") # ─── Создание тестового изображения ────────────────────────── def create_test_image(): """Создать тестовое JPEG-изображение если нет своих.""" try: from PIL import Image img = Image.new("RGB", (1280, 720), color=(128, 128, 128)) img.save("/tmp/test_face.jpg", "JPEG", quality=80) logger.info("Создано тестовое изображение /tmp/test_face.jpg") except ImportError: logger.warning("PIL не установлен, тестовое изображение не создано") # ─── Main ───────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser(description="Тест SDK ФПГ (GigaDoc)") parser.add_argument("--ws-host", default=WS_HOST, help=f"WebSocket host (default: {WS_HOST})") parser.add_argument("--ws-port", type=int, default=WS_PORT, help=f"WebSocket port (default: {WS_PORT})") parser.add_argument("--image-dir", default=".", help="Директория с JPEG-файлами (default: .)") parser.add_argument("--log-file", default=LOG_FILE, help=f"Файл лога (default: {LOG_FILE})") parser.add_argument("--rest-only", action="store_true", help="Только REST API (без WebSocket)") args = parser.parse_args() # Настройка логирования setup_logging(args.log_file) logger.info("=" * 60) logger.info("🏥 Тестирование SDK ФПГ (GigaDoc)") logger.info(f"Дата: {datetime.now().isoformat()}") logger.info(f"REST: {REST_BASE}") logger.info(f"WS: {args.ws_host}:{args.ws_port}") logger.info("=" * 60) # 0. Тестовое изображение create_test_image() # 1. Авторизация try: token = get_token() except Exception: logger.error("Тест прерван: не удалось авторизоваться") return 1 # 2. Создание сессии try: uid = create_session(token) except Exception: logger.error("Тест прерван: не удалось создать сессию") return 1 # 3-5. WebSocket result = None if not args.rest_only: try: result = asyncio.run( ws_scan(token, uid, args.ws_host, args.ws_port, args.image_dir) ) except Exception as e: logger.error(f"Ошибка WebSocket: {type(e).__name__}: {e}") else: logger.info("WebSocket тест пропущен (--rest-only)") # 6. Удаление сессии delete_session(token, uid) # Итог logger.info("=" * 60) if result: logger.info("✅ Тест завершён успешно — результат получен") else: logger.info("⚠️ REST API работает, WebSocket недоступен") logger.info(f"Лог сохранён в: {args.log_file}") logger.info("=" * 60) return 0 if result else 2 if __name__ == "__main__": sys.exit(main())