feat(sf): включить enable_ird_analyze + latency-hardening (#1115)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 1m29s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m31s
Deploy / deploy (push) Successful in 1m12s

Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
This commit is contained in:
lekss361 2026-06-07 10:00:48 +00:00 committed by bot-reviewer
parent 54a501b90a
commit 4cc0b6da8c
8 changed files with 80 additions and 22 deletions

View file

@ -1379,12 +1379,8 @@ def export_parcel_forecast(
if format == "docx":
return Response(
content=render_report_docx(run.result),
media_type=(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
),
headers={
"Content-Disposition": f'attachment; filename="{base_name}.docx"'
},
media_type=("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
headers={"Content-Disposition": f'attachment; filename="{base_name}.docx"'},
)
# pptx — байты (PowerPoint OOXML), отдельной веткой (зеркало docx early-return),
@ -1395,9 +1391,7 @@ def export_parcel_forecast(
media_type=(
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
),
headers={
"Content-Disposition": f'attachment; filename="{base_name}.pptx"'
},
headers={"Content-Disposition": f'attachment; filename="{base_name}.pptx"'},
)
if format == "json":
@ -3003,7 +2997,11 @@ def analyze_parcel(
db, geom_wkt, centroid_lon, centroid_lat
)
except Exception:
logger.warning("ird block failed for cad=%s — analyze response unaffected", cad_num)
logger.warning(
"ird block failed for cad=%s — analyze response unaffected",
cad_num,
exc_info=True,
)
return result_payload

View file

@ -135,11 +135,11 @@ class Settings(BaseSettings):
openrouteservice_api_key: str = ""
# ИРД-слой в analyze (#1067 D9b «GG-форсайт»): поле `ird` в ответе analyze —
# parcel_ird_overlaps (м.132, incl opportunity) + функц.зона/КРТ (геопортал WFS) +
# ПЗЗ-регламент зоны (C8b). Default OFF: источники 2-3 — live-зависимость от внешнего
# ЕКБ-геопортала (reverse-eng, без SLA) в hot-пути analyze. Включать после прогона
# harvest на проде (B6) и заполнения zone_regulation_cache.
enable_ird_analyze: bool = False
# parcel_ird_overlaps (м.132, incl opportunity) + КРТ (геопортал WFS) +
# ПЗЗ-регламент зоны (C8b). Включён 2026-06-07 после B6-harvest + прогрева
# zone_regulation_cache (#1102). Override через env ENABLE_IRD_ANALYZE=false остаётся
# возможен. functional_zone выключен — слой пуст в геопортале ЕКБ (0 фич, #1058).
enable_ird_analyze: bool = True
settings = Settings()

View file

@ -33,7 +33,7 @@ from app.services.site_finder.zone_regulation import get_or_fetch_zone_regulatio
logger = logging.getLogger(__name__)
# Короткий timeout для geoportal-вызовов в hot-пути analyze (default клиента — 20s, слишком долго).
_GEOPORTAL_TIMEOUT_S = 8
_GEOPORTAL_TIMEOUT_S = 4
def _functional_zone_at(
@ -105,10 +105,14 @@ def build_ird_analyze_block(
"krt": [],
"zone_regulation": None,
}
# guard активен только в unit-тестах; в prod centroid_lon/lat всегда float
# (EKB-fallback в parcels.py).
# Geoportal-источники — только при валидном centroid (network, опц.).
if lon is not None and lat is not None:
client = client or EKBGeoportalClient(timeout=_GEOPORTAL_TIMEOUT_S)
block["functional_zone"] = _functional_zone_at(client, lon, lat)
# Слой генплан-функц.зон пуст в геопортале ЕКБ (0 фич, 2026-06); источник = ФГИС ТП (#1058).
# Не дёргаем geoportal впустую — вернём _functional_zone_at когда появится ingest.
block["functional_zone"] = None
block["krt"] = _krt_at(client, lon, lat)
block["zone_regulation"] = _zone_regulation_at(db, client, lon, lat)
return block

View file

@ -141,6 +141,19 @@ _PATCHES = [
),
patch("app.api.v1.parcels.compute_velocity", return_value=None),
patch("app.api.v1.parcels.compute_gate_verdict", return_value={"verdict": "unknown"}),
# enable_ird_analyze=True by default — мокаем чтобы не делать сетевые вызовы в тестах
patch(
"app.api.v1.parcels.build_ird_analyze_block",
return_value={
"ird_overlaps": [],
"ird_by_kind": {},
"opportunity_overlaps": [],
"planning_projects": [],
"functional_zone": None,
"krt": [],
"zone_regulation": None,
},
),
]
@ -270,9 +283,9 @@ def test_inline_weights_rejects_nan() -> None:
content=raw_body,
headers={"Content-Type": "application/json"},
)
assert resp.status_code == 422, (
f"Ожидали 422 для NaN-weight, получили {resp.status_code}: {resp.text}"
)
assert (
resp.status_code == 422
), f"Ожидали 422 для NaN-weight, получили {resp.status_code}: {resp.text}"
finally:
app.dependency_overrides.clear()
_stop_patches()

View file

@ -140,6 +140,19 @@ _PATCHES = [
),
patch("app.api.v1.parcels.compute_velocity", return_value=None),
patch("app.api.v1.parcels.compute_gate_verdict", return_value={"verdict": "unknown"}),
# enable_ird_analyze=True by default — мокаем чтобы не делать сетевые вызовы в тестах
patch(
"app.api.v1.parcels.build_ird_analyze_block",
return_value={
"ird_overlaps": [],
"ird_by_kind": {},
"opportunity_overlaps": [],
"planning_projects": [],
"functional_zone": None,
"krt": [],
"zone_regulation": None,
},
),
]

View file

@ -147,6 +147,19 @@ _PATCHES = [
),
patch("app.api.v1.parcels.compute_velocity", return_value=None),
patch("app.api.v1.parcels.compute_gate_verdict", return_value={"verdict": "unknown"}),
# enable_ird_analyze=True by default — мокаем чтобы не делать сетевые вызовы в тестах
patch(
"app.api.v1.parcels.build_ird_analyze_block",
return_value={
"ird_overlaps": [],
"ird_by_kind": {},
"opportunity_overlaps": [],
"planning_projects": [],
"functional_zone": None,
"krt": [],
"zone_regulation": None,
},
),
]

View file

@ -129,6 +129,19 @@ _PATCHES = [
),
patch("app.api.v1.parcels.compute_velocity", return_value=None),
patch("app.api.v1.parcels.compute_gate_verdict", return_value={"verdict": "unknown"}),
# enable_ird_analyze=True by default — мокаем чтобы не делать сетевые вызовы в тестах
patch(
"app.api.v1.parcels.build_ird_analyze_block",
return_value={
"ird_overlaps": [],
"ird_by_kind": {},
"opportunity_overlaps": [],
"planning_projects": [],
"functional_zone": None,
"krt": [],
"zone_regulation": None,
},
),
]

View file

@ -50,7 +50,9 @@ def _patch_overlay(monkeypatch: Any, overlay: dict[str, Any]) -> None:
def test_full_block_assembly(monkeypatch: Any) -> None:
"""Все источники → overlaps + split opportunity + функц.зона + КРТ + регламент."""
"""Все источники → overlaps + split opportunity + КРТ + регламент.
functional_zone всегда None: слой пуст в геопортале ЕКБ (0 фич, 2026-06) WFS не дёргается.
"""
_patch_overlay(monkeypatch, _OVERLAY)
monkeypatch.setattr(
ird_analyze,
@ -67,7 +69,9 @@ def test_full_block_assembly(monkeypatch: Any) -> None:
"opportunity_auction",
"opportunity_krt",
]
assert block["functional_zone"] == {"name": "Ж-зона"}
# functional_zone WFS выключен (слой пуст в геопортале ЕКБ, #1058) — всегда None
assert block["functional_zone"] is None
assert "functional_zone" not in [layer for layer, _, _ in client.calls]
assert block["krt"] == [{"num_oks_krt": "КРТ-7"}]
assert block["zone_regulation"] == {"zone_index": "Ц-1"}
assert block["planning_projects"] == _PLANNING