"""Минимальный probe Objective API: 4 отчёта × минимальная выборка. Цель — посмотреть реальную форму payload-ов, не выкачивая весь Екатеринбург. Ограничения объёма: - period: только последние 7 дней (для Сводных) - complex_name: фильтр по 1 ЖК (по умолчанию «ЖК "Парк Культуры"»; меняется флагом) Что выводит: - HTTP-статус через клиент (raw exception если что) - размер JSON в KB - тип (dict/list) + len/keys - первые 1-2 элемента (для list) или верхние ключи (для dict) — обрезанные до 1KB - сохраняет полный payload в data/raw/objective_test//.json Запуск: cd backend OBJECTIVE_API_KEY= uv run python -X utf8 ../data/sql/69b_objective_minimal.py OBJECTIVE_API_KEY= uv run python -X utf8 ../data/sql/69b_objective_minimal.py --complex 'ЖК "Сибирский сад"' OBJECTIVE_API_KEY= uv run python -X utf8 ../data/sql/69b_objective_minimal.py --no-complex --days 3 Notes: - Если поле ComplexName в Objective отличается ('ЖК "Парк Культуры"' vs 'Парк Культуры' vs 'ЖК Парк Культуры') — поменяй --complex или поставь --no-complex. - --no-complex берёт ВСЕ ЖК группы, но всё ещё с период=N дней (для Сводных). Для Поквартирных без --complex может быть очень большой ответ — будь осторожен. """ from __future__ import annotations import argparse import json import os import sys import time from datetime import date, datetime, timedelta, timezone from pathlib import Path # repo-root → /backend для import'а app.* sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "backend")) from app.services.scrapers.objective import ( # noqa: E402 ObjectiveAPIError, ObjectiveAuthError, ObjectiveClient, ) def _shape(data) -> str: if isinstance(data, list): return f"list[{len(data)}]" if isinstance(data, dict): return f"dict(keys={list(data.keys())[:15]})" return type(data).__name__ def _peek(data, max_chars: int = 1200) -> str: """Короткий человеко-читаемый peek первых элементов / ключей.""" if isinstance(data, list): if not data: return " (пустой список)" sample = data[0] if isinstance(sample, dict): keys = list(sample.keys()) return f" [0] keys ({len(keys)}): {keys[:30]}" return f" [0]: {str(sample)[:max_chars]}" if isinstance(data, dict): out = [] for k, v in list(data.items())[:10]: v_repr = ( f"list[{len(v)}]" if isinstance(v, list) else f"dict({len(v)} keys)" if isinstance(v, dict) else str(v)[:120] ) out.append(f" {k!r} -> {v_repr}") return "\n".join(out) s = str(data) return f" {s[:max_chars]}{'…' if len(s) > max_chars else ''}" def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--complex", default='Парк Культуры', help='ComplexName-фильтр (default: "Парк Культуры", без префикса ЖК и без кавычек — ' 'эмпирически проверено что Объектив требует именно такой формат)') ap.add_argument("--no-complex", action="store_true", help="Не передавать ComplexName (вся группа)") ap.add_argument("--group", default=None, help="GroupName (default: settings.objective_default_group = Екатеринбург)") ap.add_argument("--days", type=int, default=7, help="Период для Сводных отчётов в днях (default: 7)") ap.add_argument("--only", choices=("corp_sum", "lots_sum", "corp_pf", "lots_pf"), help="Запустить только один отчёт") ap.add_argument("--inter-delay", type=int, default=15, help="Пауза в секундах между отчётами (default: 15). " "У Objective жёсткий rate-limit: <1 запрос/30с на дешёвых тарифах.") ap.add_argument("--rate-ms", type=int, default=3000, help="Минимальный интервал между HTTP-запросами в мс (default: 3000)") ap.add_argument("--retries", type=int, default=2, help="Сколько раз ретраить 429/5xx (default: 2). 0 = не ретраить вообще.") args = ap.parse_args() if not os.environ.get("OBJECTIVE_API_KEY"): # Конфиг также читает из backend/.env — попробуем через settings. from app.core.config import settings if not settings.objective_api_key: print("ERROR: OBJECTIVE_API_KEY не задан (env + backend/.env пустые).") return 1 end_date = date.today() start_date = end_date - timedelta(days=args.days) complex_name = None if args.no_complex else args.complex # Всегда пишем в /data/raw/objective_test/ независимо от cwd скрипта # (раньше был относительный путь — попадал в backend/data/raw/ когда юзер # запускал из backend/, и эти файлы превышали 512KB pre-commit лимит). repo_root = Path(__file__).resolve().parent.parent.parent out_dir = repo_root / "data" / "raw" / "objective_test" / datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") out_dir.mkdir(parents=True, exist_ok=True) print(f"=== Objective minimal probe") print(f" group = {args.group or '(default settings)'}") print(f" complex = {complex_name or '(none — вся группа)'}") print(f" period = {start_date} .. {end_date} ({args.days}d)") print(f" output = {out_dir.absolute()}") print() try: client = ObjectiveClient(rate_ms=args.rate_ms, retries=args.retries) except ObjectiveAuthError as e: print(f"AUTH ERROR при создании клиента: {e}") return 2 # 4 канонических отчёта × минимальные параметры plan = [ ("corp_sum", "1. Сводные / Корпуса", lambda: client.report_corpuses_summary( group_name=args.group, complex_name=complex_name, start_date=start_date, end_date=end_date, use_ddu=True, use_dkp=True, )), ("lots_sum", "2. Сводные / Лоты", lambda: client.report_lots_summary( group_name=args.group, complex_name=complex_name, start_date=start_date, end_date=end_date, use_ddu=True, use_dkp=True, )), ("corp_pf", "3. Поквартирные / Корпуса", lambda: client.report_corpuses_per_flat( group_name=args.group, complex_name=complex_name, use_ddu=True, use_dkp=True, )), ("lots_pf", "4. Поквартирные / Лоты", lambda: client.report_lots_per_flat( group_name=args.group, complex_name=complex_name, use_ddu=True, use_dkp=True, )), ] if args.only: plan = [p for p in plan if p[0] == args.only] summary = [] for idx, (slug, label, fn) in enumerate(plan): if idx > 0 and args.inter_delay > 0: print(f"... пауза {args.inter_delay}s перед следующим отчётом (rate-limit Objective)") time.sleep(args.inter_delay) print(f"--- {label}") t0 = time.time() try: data = fn() except ObjectiveAuthError as e: print(f" ❌ AUTH: {e}\n") summary.append((slug, "auth_error", 0)) continue except ObjectiveAPIError as e: print(f" ❌ API: {e}\n") summary.append((slug, "api_error", 0)) continue except Exception as e: # noqa: BLE001 print(f" ❌ {type(e).__name__}: {e}\n") summary.append((slug, type(e).__name__, 0)) continue elapsed = time.time() - t0 text = json.dumps(data, ensure_ascii=False, indent=2) size_kb = len(text) / 1024.0 path = out_dir / f"{slug}.json" path.write_text(text, encoding="utf-8") print(f" ✓ {elapsed:.2f}s size={size_kb:.1f} KB shape={_shape(data)}") print(_peek(data)) print(f" saved → {path.relative_to(Path.cwd()) if path.is_relative_to(Path.cwd()) else path}") print() summary.append((slug, "ok", size_kb)) client.close() print("=== Сводка") for slug, status, size in summary: print(f" {slug:10} {status:12} {size:.1f} KB") print(f"\nDONE. Файлы в: {out_dir.absolute()}") return 0 if __name__ == "__main__": sys.exit(main())