All checks were successful
CI Trade-In / changes (pull_request) Successful in 15s
CI / changes (pull_request) Successful in 15s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m56s
CI Trade-In / backend-tests (pull_request) Successful in 5m25s
CI / backend-tests (pull_request) Successful in 18m18s
Почему: файлы в main были отформатированы старым ruff (хук 0.7.4), а после #3021 хук, CI и `uv run ruff format` — на 0.15.20. Без нормализации каждый коммит, касающийся одного из этих 161 файлов, тащил бы посторонние хунки на нетронутых строках (длинные assert-сообщения и т.п.). Что: `ruff format` 0.15.20 по backend/ и tradein-mvp/backend/; ничего, кроме форматирования (958+/1034−). `ruff check` 0.15.20 зелёный на обоих проектах, повторный `format --check` — 1001 файл без изменений (идемпотентно), compileall чистый. Refs #2864
141 lines
7.2 KiB
Python
141 lines
7.2 KiB
Python
"""Generative Design — Stage 3a (#1965): каталог типовых домов (house-type catalog).
|
||
|
||
Контракт Stage 3a: вместо max-FAR жадной раскладки (Stage 1b) пользователь выбирает
|
||
ТИПОВЫЕ дома и говорит, сколько секций каждого типа поставить. Этот модуль — справочник
|
||
таких типов: для каждого ``section_type`` он несёт габариты пятна секции (ширина × глубина,
|
||
метры), дефолтную этажность и подходящий класс жилья.
|
||
|
||
ИСТОЧНИК: это РАЗУМНЫЙ ДЕФОЛТНЫЙ каталог (sane-default), а не выгрузка из БД. Габариты —
|
||
типовые размеры секций массового жилья РФ (панель/монолит/башня/малоэтажка), округлённые
|
||
до реалистичных значений. Каталог намеренно захардкожен в коде на Stage 3a: миграции БД
|
||
сейчас нет (см. эпик #1953). Когда понадобится редактируемый застройщиком каталог — его
|
||
ПРОДВИГАЮТ в БД-таблицу с тем же контрактом (``section_type`` → footprint/floors/class),
|
||
а этот модуль станет seed'ом/фолбэком. До тех пор — single source of truth по типам.
|
||
|
||
Детерминированно, без LLM / внешних API / БД.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from dataclasses import dataclass
|
||
from typing import Literal
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Класс жилья — то же Literal-множество, что у ConceptInput / compute_teap (single
|
||
# source of truth по допустимым значениям; рассинхрон тут = ошибка типов в mypy-strict).
|
||
HousingClass = Literal["econom", "comfort", "business"]
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class HouseType:
|
||
"""Один типовой дом каталога (тип секции МКД / малоэтажки).
|
||
|
||
section_type — стабильный машинный КЛЮЧ типа (латиница, snake_case); это и есть
|
||
значение, которое фронт кладёт в ``BuildingProgramItem.section_type``.
|
||
label_ru — человекочитаемый русский лейбл для UI (Stage 3b его показывает).
|
||
footprint_w_m — ширина пятна секции, метры.
|
||
footprint_d_m — глубина пятна секции, метры.
|
||
default_floors — дефолтная этажность типа (UI подставляет, пользователь может менять
|
||
в пределах контракта BuildingProgramItem [1, 40]).
|
||
housing_class — подходящий класс жилья (драйвит ТЭП/финмодель ниже по конвейеру).
|
||
"""
|
||
|
||
section_type: str
|
||
label_ru: str
|
||
footprint_w_m: float
|
||
footprint_d_m: float
|
||
default_floors: int
|
||
housing_class: HousingClass
|
||
|
||
@property
|
||
def footprint_sqm(self) -> float:
|
||
"""Площадь пятна секции, кв.м (ширина × глубина)."""
|
||
return self.footprint_w_m * self.footprint_d_m
|
||
|
||
|
||
# ── Каталог типовых домов (sane-default, см. модульный docstring про источник) ──────
|
||
# Покрываем основные форматы массового жилья РФ:
|
||
# * панель-эконом — длинная неглубокая секция, средняя этажность, эконом-класс;
|
||
# * монолит-комфорт— чуть шире/глубже, комфорт-класс, типовая «свечка» 14 этажей;
|
||
# * башня-бизнес — компактное квадратное пятно, высотная (точечная) застройка;
|
||
# * малоэтажка-комфорт — широкая невысокая секция (3 этажа), низкоплотная застройка;
|
||
# * таунхаус — узкое неглубокое пятно блокированной застройки, 3 этажа.
|
||
# Габариты — реалистичные типовые размеры; этажность — характерная для формата.
|
||
HOUSE_TYPES: tuple[HouseType, ...] = (
|
||
HouseType(
|
||
section_type="panel_econom",
|
||
label_ru="Панельная секция (эконом)",
|
||
footprint_w_m=24.0,
|
||
footprint_d_m=15.0,
|
||
default_floors=9,
|
||
housing_class="econom",
|
||
),
|
||
HouseType(
|
||
section_type="monolith_comfort",
|
||
label_ru="Монолитная секция (комфорт)",
|
||
footprint_w_m=21.0,
|
||
footprint_d_m=18.0,
|
||
default_floors=14,
|
||
housing_class="comfort",
|
||
),
|
||
HouseType(
|
||
section_type="tower_business",
|
||
label_ru="Башня (бизнес)",
|
||
footprint_w_m=18.0,
|
||
footprint_d_m=18.0,
|
||
default_floors=25,
|
||
housing_class="business",
|
||
),
|
||
HouseType(
|
||
section_type="lowrise_comfort",
|
||
label_ru="Малоэтажная секция (комфорт)",
|
||
footprint_w_m=30.0,
|
||
footprint_d_m=14.0,
|
||
default_floors=3,
|
||
housing_class="comfort",
|
||
),
|
||
HouseType(
|
||
section_type="townhouse",
|
||
label_ru="Таунхаус",
|
||
footprint_w_m=12.0,
|
||
footprint_d_m=10.0,
|
||
default_floors=3,
|
||
housing_class="comfort",
|
||
),
|
||
)
|
||
|
||
# Индекс по ключу для O(1)-лукапа. Построен один раз при импорте; ключи уникальны
|
||
# (assert ниже ловит дубликат типа на старте, а не молча затирает запись).
|
||
_BY_KEY: dict[str, HouseType] = {ht.section_type: ht for ht in HOUSE_TYPES}
|
||
assert len(_BY_KEY) == len(HOUSE_TYPES), "duplicate section_type key in HOUSE_TYPES"
|
||
|
||
|
||
def get_house_type(section_type: str) -> HouseType:
|
||
"""Найти тип дома по ключу ``section_type``. Бросает :class:`KeyError`, если нет.
|
||
|
||
Вызывающий слой (placement) обязан валидировать ключи программы заранее (см.
|
||
:func:`available_section_types`) — неизвестный ключ здесь это программная ошибка,
|
||
а не пользовательский ввод, поэтому KeyError, а не тихий None.
|
||
"""
|
||
try:
|
||
return _BY_KEY[section_type]
|
||
except KeyError as exc:
|
||
raise KeyError(
|
||
f"unknown house type {section_type!r}; available: {', '.join(sorted(_BY_KEY))}"
|
||
) from exc
|
||
|
||
|
||
def available_section_types() -> frozenset[str]:
|
||
"""Множество допустимых ключей ``section_type`` каталога (для валидации программы)."""
|
||
return frozenset(_BY_KEY)
|
||
|
||
|
||
__all__ = [
|
||
"HOUSE_TYPES",
|
||
"HouseType",
|
||
"HousingClass",
|
||
"available_section_types",
|
||
"get_house_type",
|
||
]
|