gendesign/backend/app/schemas/own_project.py
bot-backend cdf493f345
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
chore(format): нормализация под ruff 0.15.20 — 161 файл, только формат (#2864) (#3022)
2026-08-21 12:01:52 +00:00

120 lines
5.5 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.

"""Pydantic schemas для own_planned_project (#1169 PR1, ТЗ §25.3).
OwnPlannedProjectCreate — тело POST (name обязателен).
OwnPlannedProjectUpdate — тело PUT (все поля опциональны — partial update).
OwnPlannedProjectOut — ответ API (включает id, created_by, created_at, updated_at).
created_by НЕ принимается из тела — проставляется роутером из X-Authenticated-User
(зеркало insight #948). Это «наш будущий/планируемый проект» приватного пайплайна:
второй источник §25.3-движка каннибализации (парный к «текущим» из domrf).
"""
from __future__ import annotations
from datetime import date, datetime
from typing import Literal
from pydantic import BaseModel, Field, field_validator, model_validator
# Класс проекта — мягкий enum (зеркалит class-вокабуляр forecasting SegmentSpec
# и domrf obj_class). None допустим (класс может быть ещё не определён). Матчинг в
# PR2 регистронезависимый, поэтому здесь фиксируем «человеческий» Title-case-набор.
OwnProjectClass = Literal["Стандарт", "Комфорт", "Бизнес", "Премиум"]
def _validate_unit_mix(value: dict[str, float] | None) -> dict[str, float] | None:
"""Проверить, что доли квартирографии — числа в [0,1]. None допустим.
Сумму ≈1 НЕ навязываем жёстко (округления / нежилая часть) — зеркалит CHECK
миграции 148. Возвращает значение как есть (после проверки).
"""
if value is None:
return None
for key, share in value.items():
if not isinstance(share, int | float) or isinstance(share, bool):
raise ValueError(
f"unit_mix['{key}'] должно быть числом, получено {type(share).__name__}"
)
if share < 0 or share > 1:
raise ValueError(f"unit_mix['{key}']={share} вне диапазона [0,1]")
return value
class OwnPlannedProjectCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=300, description="Название проекта/ЖК")
district: str | None = Field(None, max_length=200, description="Район (location ref)")
obj_class: OwnProjectClass | None = Field(
None, description="Класс: Стандарт|Комфорт|Бизнес|Премиум"
)
planned_release_month: date | None = Field(
None, description="Планируемый месяц выхода в продажу (нормализуется к 1-му числу)"
)
price_min_per_m2: float | None = Field(None, ge=0, description="Нижняя граница цены, ₽/м² (≥0)")
price_max_per_m2: float | None = Field(
None, ge=0, description="Верхняя граница цены, ₽/м² (≥0)"
)
unit_mix: dict[str, float] | None = Field(
None,
description='Квартирография: доли по форматам {"studio":0.3,"1k":0.4,...} (каждая в [0,1])',
)
lon: float | None = Field(None, ge=-180.0, le=180.0, description="Долгота WGS-84 (центроид)")
lat: float | None = Field(None, ge=-90.0, le=90.0, description="Широта WGS-84 (центроид)")
@field_validator("unit_mix")
@classmethod
def _check_unit_mix(cls, value: dict[str, float] | None) -> dict[str, float] | None:
return _validate_unit_mix(value)
@model_validator(mode="after")
def _check_price_range(self) -> OwnPlannedProjectCreate:
if (
self.price_min_per_m2 is not None
and self.price_max_per_m2 is not None
and self.price_min_per_m2 > self.price_max_per_m2
):
raise ValueError("price_min_per_m2 не может быть больше price_max_per_m2")
return self
class OwnPlannedProjectUpdate(BaseModel):
"""Partial update (PUT) — переданные поля заменяются, отсутствующие не трогаются."""
name: str | None = Field(None, min_length=1, max_length=300)
district: str | None = Field(None, max_length=200)
obj_class: OwnProjectClass | None = None
planned_release_month: date | None = None
price_min_per_m2: float | None = Field(None, ge=0)
price_max_per_m2: float | None = Field(None, ge=0)
unit_mix: dict[str, float] | None = None
lon: float | None = Field(None, ge=-180.0, le=180.0)
lat: float | None = Field(None, ge=-90.0, le=90.0)
@field_validator("unit_mix")
@classmethod
def _check_unit_mix(cls, value: dict[str, float] | None) -> dict[str, float] | None:
return _validate_unit_mix(value)
class OwnPlannedProjectOut(BaseModel):
id: int
created_by: str
name: str
district: str | None
obj_class: str | None
planned_release_month: date | None
price_min_per_m2: float | None
price_max_per_m2: float | None
unit_mix: dict[str, float] | None
lon: float | None
lat: float | None
created_at: datetime
updated_at: datetime
class OwnPlannedProjectList(BaseModel):
"""Ответ list-эндпоинта: total + страница строк (как insight)."""
total: int
limit: int
offset: int
rows: list[OwnPlannedProjectOut]