gendesign/backend/app/schemas/parcel.py
lekss361 3c8bea38d0
Some checks failed
CI / backend (pull_request) Failing after 1m0s
CI / frontend (pull_request) Successful in 2m19s
feat(parcels): GET /{cad_num}/connection-points — engineering points (Forgejo #115)
KILLER feature от Макса — точки подключения к инженерным сетям + охранные
зоны как proxy для подбора инвест-участков.

Phase 1 — публичные NSPD данные (cat 36328 + 37578).

- ConnectionPointsResponse pydantic + 3 nested models
- get_connection_points() + helpers (parcel WKT, structures by boundary,
  zouit overlaps) с ST_Distance(::geography) для метров
- GET endpoint с Query(radius_m, ge=50, le=2000, default=500)
- 7 tests

Closes part of #115 (Phase 1 backend).
2026-05-16 01:03:42 +03:00

91 lines
2.3 KiB
Python

from typing import Any
from pydantic import BaseModel, Field
# ── Connection points schemas (issue #115) ────────────────────────────────────
class EngineeringStructure(BaseModel):
name: str | None
type: str | None
cad_num: str | None
distance_to_boundary_m: float
geometry_geojson: dict[str, Any]
readable_address: str | None
raw_props: dict[str, Any]
source: str
class ZouitOverlap(BaseModel):
reg_numb_border: str | None
type_zone: str | None
subcategory: int | None
intersects_parcel: bool
geometry_geojson: dict[str, Any]
raw_props: dict[str, Any]
source: str
class ConnectionPointsSummary(BaseModel):
nearest_structure_distance_m: float | None
in_protection_zone: bool
protection_zones_intersecting: int
total_structures_in_radius: int
class ConnectionPointsResponse(BaseModel):
engineering_structures: list[EngineeringStructure]
zouit_engineering_overlaps: list[ZouitOverlap]
summary: ConnectionPointsSummary
dump_available: bool
dump_fetched_at: str | None
# ── Parcel search/detail schemas ──────────────────────────────────────────────
class ParcelFilter(BaseModel):
vri: list[str] | None = None
area_min_sqm: float | None = None
area_max_sqm: float | None = None
region: str | None = None
polygon_geojson: dict[str, Any] | None = None
text_search: str | None = None
class ScoringWeights(BaseModel):
distance_to_center: float = 0.4
poi_density: float = 0.3
neighborhood_price: float = 0.3
class ParcelSearchRequest(BaseModel):
filters: ParcelFilter = Field(default_factory=ParcelFilter)
weights: ScoringWeights = Field(default_factory=ScoringWeights)
limit: int = 50
class ScoreBreakdown(BaseModel):
total: float
distance_to_center: float
poi_density: float
neighborhood_price: float
class ParcelSummary(BaseModel):
id: str
cadastral_number: str
area_sqm: float
vri: str
address: str | None
score: ScoreBreakdown
class ParcelSearchResponse(BaseModel):
items: list[ParcelSummary]
total: int
class ParcelDetail(ParcelSummary):
geometry_geojson: dict[str, Any]
enrichment: dict[str, Any]