Миграция 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>
135 lines
7.1 KiB
Python
135 lines
7.1 KiB
Python
"""Тесты числового экстракта предельных параметров ПЗЗ (#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]
|