Andrey Epifancev 501be971e7 perf(gen): grow не падает на ответах листа без null-полей
form4_ids вычисляет vsp_ids и project_ids из строк FORM_4 через .get
с пропуском отсутствующих ключей, grow_form4_sheet использует её.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:58:39 +04:00

138 lines
6.6 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.

"""Доращивание отдельных листов rf до «большого» размера (для замеров модалок поверх большого листа).
cd perf && .venv/bin/python -m perfkit.gen.grow --scale 1 [--vsp-rows 500] [--form4-rows 300]
Базовая генерация (run.py) даёт на листах, где существуют форм-модалки, 31 (FORM_2 AHR_RENT/UTILITY/
SECURITY) и 110 (FORM_4 AHR/CAP/OPER) строк. Здесь строки донабираются через тот же API (POST .../line
под admin, как в run.py), идемпотентно: если строк уже >= цели - ничего не делается.
Растут: FORM_2 AHR_UTILITY и AHR_SECURITY (до --vsp-rows) и FORM_4 CAP (до --form4-rows) формы rf.
AHR_RENT и FORM_4 AHR остаются малыми: это пара «малый лист» для тех же модалок, а скролл-сценарии
по AHR не меняются. Порядок запуска поэтому не влияет на малые замеры; на скролл-сценарии по
UTILITY/SECURITY/CAP grow влияет - запускать после них. Цели одинаковы на масштабах 1 и 2
(add_line на FORM_4 стоит O(n)). Манифест обновляется (Sheet.rows).
"""
from __future__ import annotations
import argparse
import asyncio
import json
import random
import time
from collections import Counter
from datetime import datetime, timezone
from .. import RESULTS_DIR
from ..manifest import dump, load, manifest_path
from .client import Api
from .run import CONCURRENCY, FormInfo, YEAR, add_lines, count_rows, leaf_items, log, sheet_params
VSP_ROWS = 500
FORM4_ROWS = 300
# (form_type, лист, вид)
TARGETS = [("FORM_2", "AHR_UTILITY", "vsp"), ("FORM_2", "AHR_SECURITY", "vsp"), ("FORM_4", "CAP", "expense")]
def rows_to_add(current: int, target: int) -> int:
"""Сколько строк донабрать, чтобы на листе стало >= target (current уже включает шаблонные и META-строки)."""
return max(0, target - current)
def _target_for(kind: str, vsp_rows: int, form4_rows: int) -> int:
return vsp_rows if kind == "vsp" else form4_rows
async def grow_vsp_sheet(api: Api, form_id: int, sheet: str, need: int, org_index: int) -> int:
path = f"/form/{form_id}/sheet/{sheet}"
rows = (await api.get(path))["result"]
vsp_ids = sorted({r["data"]["vsp_id"] for r in rows if r.get("row_type") == "INPUT" and r["data"].get("vsp_id")})
if not vsp_ids:
raise RuntimeError(f"{sheet}: нет ВСП в существующих строках")
async def one(n: int) -> None:
body = {
"vsp_id": vsp_ids[n % len(vsp_ids)],
"contract_number": f"ДГ-{org_index:02d}/{sheet[-4:]}/G{n:04d}",
"contract_end_date": datetime(YEAR + 1 + n % 3, 1 + n % 12, 1 + n % 27, tzinfo=timezone.utc).isoformat(),
}
await api.post(path + "/line", json=body)
await asyncio.gather(*(one(n) for n in range(need)))
return need
def form4_ids(rows: list[dict]) -> tuple[list[int], list[int]]:
"""(vsp_ids, project_ids) из строк листа FORM_4; отсутствующие ключи (data/header/vsp_id/project_id) пропускаются."""
vsp_ids: set[int] = set()
project_ids: set[int] = set()
for r in rows:
header = (r.get("data") or {}).get("header") or {}
if r.get("row_type") == "INPUT":
vid = header.get("vsp_id")
if vid is not None:
vsp_ids.add(vid)
elif r.get("row_type") == "ITEM":
pid = header.get("project_id")
if pid is not None:
project_ids.add(pid)
return sorted(vsp_ids), sorted(project_ids)
async def grow_form4_sheet(api: Api, form_id: int, sheet: str, need: int) -> int:
path = f"/form/{form_id}/sheet/{sheet}"
rows = (await api.get(path))["result"]
vsp_ids, project_ids = form4_ids(rows)
if not project_ids:
raise RuntimeError(f"{sheet}: нет проектов")
leaves = await leaf_items(api, "FORM_4", sheet)
form = FormInfo(form_id, "FORM_4", 0, "rf", 1, vsp_ids)
stats: Counter = Counter()
ids = await add_lines(api, form, sheet, None, need, leaves, random.Random(f"grow-{form_id}-{sheet}"), stats, project_ids)
if stats:
log(f" статистика {dict(stats)}")
return len(ids)
async def main(scale: int, vsp_rows: int, form4_rows: int) -> None:
path = manifest_path(scale, RESULTS_DIR)
m = load(path)
api = Api("admin", "admin", asyncio.Semaphore(CONCURRENCY))
report = []
try:
await api.login()
for ftype, sheet, kind in TARGETS:
sh = next((s for s in m.sheets if s.owner == "rf" and s.form_type == ftype and s.sheet == sheet and s.direction is None), None)
if sh is None:
raise RuntimeError(f"лист {ftype}/{sheet} (rf) не найден в манифесте")
cur = await count_rows(api, f"/form/{sh.form_id}/sheet/{sheet}", sheet_params(None))
target = _target_for(kind, vsp_rows, form4_rows)
need = rows_to_add(cur, target)
t0 = time.time()
if need:
log(f"{ftype}/{sheet} form {sh.form_id}: {cur} -> {target} (+{need})")
if kind == "vsp":
await grow_vsp_sheet(api, sh.form_id, sheet, need, 1)
else:
await grow_form4_sheet(api, sh.form_id, sheet, need)
else:
log(f"{ftype}/{sheet} form {sh.form_id}: {cur} >= {target}, ничего не делаю")
after = await count_rows(api, f"/form/{sh.form_id}/sheet/{sheet}", sheet_params(None))
sh.rows = after
dt = time.time() - t0
report.append({"sheet": f"{ftype}/{sheet}", "form_id": sh.form_id, "before": cur, "after": after, "added": need, "seconds": round(dt, 1)})
log(f" итого {after} строк за {dt:.0f} c")
dump(m, path)
out = RESULTS_DIR / f"grow-s{scale}.json"
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
log(f"манифест обновлён: {path}; отчёт {out}")
finally:
await api.aclose()
if __name__ == "__main__":
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--scale", type=int, required=True)
ap.add_argument("--vsp-rows", type=int, default=VSP_ROWS)
ap.add_argument("--form4-rows", type=int, default=FORM4_ROWS)
a = ap.parse_args()
asyncio.run(main(a.scale, a.vsp_rows, a.form4_rows))