gendesign/backend/tests/services/test_zone_regulation_extract.py
lekss361 e5b12d579a
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
feat(sf): zone_regulation_cache — ПЗЗ-регламент по zone_index + числовой экстракт (#1099)
Миграция 133_zone_regulation_cache.sql + zone_regulation.py: кэш ПЗЗ-регламента по (city, zone_index) — 3 списка ВРИ + сырой текст предельных параметров + regex-экстракт числовых (max_far/застройка/этажность/площадь). get-or-fetch через urbanCard. Резолв один раз на зону (~100/город). Self-contained, wiring в D9b. 10 тестов на реальных geoportal-строках.

Refs #1067.
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-06 19:31:28 +00:00

135 lines
7.1 KiB
Python
Raw Permalink 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.

"""Тесты числового экстракта предельных параметров ПЗЗ (#1067 C8b).
Строки limit_params — РЕАЛЬНЫЕ otherDescription с живого ЕКБ-геопортала urbanCard (probe
2026-06-06: зоны Ц-1, ЦС-1, ЦС-6). Проверяют что regex берёт числа из «– NUN кв.м/%/этаж»,
игнорит «не подлежат установлению», тянет per-ВРИ повторы и десятичную запятую.
"""
from __future__ import annotations
from app.services.scrapers.ekb_geoportal_client import ZoneRegulation
from app.services.site_finder.zone_regulation import (
_headline_scalars,
parse_limit_params,
upsert_zone_regulation,
)
# Реальный otherDescription зоны Ц-1 (среднеэтажная 1500 / многоэтажная 3000 + прочее).
_C1 = (
"минимальная площадь земельного участка - 1500 кв. м;\n"
"минимальные отступы от границ земельных участков ... не подлежат установлению;\n"
"предельное количество этажей или предельная высота зданий, строений, сооружений не "
"подлежат установлению;\n"
"максимальный процент застройки в границах земельного участка, определяемый как отношение "
"суммарной площади земельного участка, которая может быть застроена, ко всей площади "
"земельного участка 100%; Иные предельные параметры:\n"
"коэффициент строительного использования земельного участка 4;\n"
"процент застройки подземного пространства 100%;\n"
"минимальная площадь земельного участка - 3000 кв. м;\n"
"максимальный процент застройки в границах земельного участка ... для многоэтажной жилой "
"застройки 100%;\n"
)
# Зона ЦС-1: КСИ 2,4 / 2,5 / 2 (десятичная запятая), этажность 5, застройка 80%, площадь 5000.
_CS1 = (
"максимальный процент застройки в границах земельного участка, определяемый как отношение "
"суммарной площади земельного участка ... ко всей площади земельного участка 80%;\n"
"коэффициент строительного использования земельного участка (за исключением ...) 2,4;\n"
"коэффициент строительного использования земельного участка (только в отношении ...) - 2,5;\n"
"коэффициент строительного использования земельного участка (только в отношении ...) 2;\n"
'предельная этажность (только в отношении ... "гостиничное обслуживание") 5 этажей;\n'
"максимальная общая площадь ... 5 000 кв.м;\n"
)
# Зона ЦС-6: всё «не подлежат установлению» — числовой экстракт пуст.
_CS6 = (
"предельные (минимальные и (или) максимальные) размеры земельных участков, в том числе их "
"площадь, не подлежат установлению;\n"
"максимальный процент застройки в границах земельного участка ... не подлежит установлению;\n"
"Иные предельные параметры разрешенного строительства ... не подлежат установлению\n"
)
def test_c1_min_area_per_vri() -> None:
"""Обе min-площади (среднеэтажная 1500 + многоэтажная 3000) тянутся как список."""
p = parse_limit_params([_C1])
assert p["min_parcel_area_m2"] == [1500.0, 3000.0]
def test_c1_far_and_building_and_underground() -> None:
p = parse_limit_params([_C1])
assert p["max_far"] == [4.0]
assert p["max_building_pct"] == [100.0, 100.0] # per-ВРИ повтор
assert p["underground_pct"] == [100.0]
def test_c1_no_floors_or_height_when_not_established() -> None:
"""«не подлежат установлению» для этажей/высоты → ключи отсутствуют."""
p = parse_limit_params([_C1])
assert "max_floors" not in p
assert "max_height_m" not in p
def test_cs1_decimal_comma_far() -> None:
"""КСИ с десятичной запятой (2,4 / 2,5 / 2) парсятся в float."""
p = parse_limit_params([_CS1])
assert p["max_far"] == [2.4, 2.5, 2.0]
def test_cs1_floors_and_thousand_separated_area() -> None:
p = parse_limit_params([_CS1])
assert p["max_floors"] == [5.0]
assert p["max_building_pct"] == [80.0]
def test_cs6_all_not_established_is_empty() -> None:
assert parse_limit_params([_CS6]) == {}
def test_empty_inputs() -> None:
assert parse_limit_params(None) == {}
assert parse_limit_params([]) == {}
assert parse_limit_params(["", None]) == {} # type: ignore[list-item]
def test_headline_scalars_most_permissive() -> None:
"""min_parcel_area = наименьший минимум; прочие = max; max_floors = int."""
p = parse_limit_params([_C1, _CS1])
s = _headline_scalars(p)
assert s["min_parcel_area_m2"] == 1500.0 # min из [1500, 3000]
assert s["max_far"] == 4.0 # max из [4, 2.4, 2.5, 2]
assert s["max_building_pct"] == 100.0
assert s["max_floors"] == 5
assert isinstance(s["max_floors"], int)
assert s["max_height_m"] is None # нет ни одного → None
def test_headline_scalars_empty() -> None:
s = _headline_scalars({})
assert all(v is None for v in s.values())
assert set(s) == {
"max_far",
"max_building_pct",
"underground_pct",
"min_parcel_area_m2",
"max_floors",
"max_height_m",
}
def test_upsert_skips_when_no_zone_index() -> None:
"""Регламент без zone_index не пишется (None), БД не трогается."""
reg = ZoneRegulation(
zone_index=None,
zone_full_name=None,
main_vri=[],
conditional_vri=[],
auxiliary_vri=[],
limit_params=[],
)
class _Boom:
def begin_nested(self): # pragma: no cover - не должен вызываться
raise AssertionError("db не должна трогаться при отсутствии zone_index")
assert upsert_zone_regulation(_Boom(), reg) is None # type: ignore[arg-type]