gendesign/data/sql/69b_objective_minimal.py
2026-05-07 21:04:38 +03:00

204 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Минимальный 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/<timestamp>/<slug>.json
Запуск:
cd backend
OBJECTIVE_API_KEY=<key> uv run python -X utf8 ../data/sql/69b_objective_minimal.py
OBJECTIVE_API_KEY=<key> uv run python -X utf8 ../data/sql/69b_objective_minimal.py --complex 'ЖК "Сибирский сад"'
OBJECTIVE_API_KEY=<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
# Всегда пишем в <repo>/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())