Replaces tradein.cad_buildings manual snapshot (36k rows, frozen 2026-05-22) with live postgres_fdw foreign table reading gendesign.v_tradein_cad_buildings directly. Also reverts PR #492 HTTP-based cadastral.py — superseded by FDW. Architecture: - gendesign DB: new role tradein_fdw_reader + flat view v_tradein_cad_buildings (EKB-only slice of cad_buildings with lat/lon flattened from geom) - networks: gendesign-postgres added to gendesign_shared (alias); tradein-postgres added to gendesign_shared for FDW connect - tradein DB: postgres_fdw extension + FOREIGN TABLE gendesign_cad_buildings - tradein backend: startup hook creates/refreshes USER MAPPING with password from env GENDESIGN_FDW_PASSWORD (password rotation handled via restart) - geocoder.py: cadastral primary for forward + reverse + suggest; Yandex/Nominatim fallback. reverse_geocode wraps Nominatim in try/except — no more HTTPStatusError → 500. - house_metadata.py and trade_in.py admin stats switched to foreign table - DROP TABLE cad_buildings in tradein (legacy snapshot removed entirely) - import-cadastre.sh deleted (no manual sync needed) Fixes: - /trade-in/api/v1/geocode/reverse 500 (Nominatim ban → no fallback) - estimate confidence_explanation=address_not_geocoded for addresses in our cadastre (e.g. Хохрякова 81) that Nominatim doesn't return Deploy ordering: main migration 100_tradein_fdw_role.sql adds role+view first (strict deploy.yml). Tradein next deploy applies 060_postgres_fdw_extension.sql and 061_drop_legacy_cad_buildings.sql (idempotent, errors ignored). Tradein backend startup creates USER MAPPING when env var present. Verify post-deploy with: docker exec tradein-postgres psql -U tradein -d tradein -c \ "SELECT count(*) FROM gendesign_cad_buildings" # expect ~36k EKB buildings.
96 lines
3.4 KiB
Python
96 lines
3.4 KiB
Python
"""Trade-In MVP — FastAPI entry point.
|
|
|
|
Standalone версия, выделена из основного gendesign repo для локальной разработки.
|
|
Только trade-in фича; никаких других роутеров (concepts/parcels/analytics/etc) нет.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from collections.abc import AsyncGenerator
|
|
from contextlib import asynccontextmanager
|
|
|
|
import sentry_sdk
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
from app.api.v1 import admin, brand, geocode, search, trade_in
|
|
from app.core.config import settings
|
|
from app.core.db import SessionLocal
|
|
from app.core.fdw import ensure_fdw_user_mapping
|
|
from app.core.ratelimit import RateLimitMiddleware
|
|
from app.services.scheduler import scheduler_loop
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
)
|
|
|
|
# Мониторинг ошибок — GlitchTip (Sentry-совместимый, #396).
|
|
# DSN из env GLITCHTIP_DSN; пусто (dev) → мониторинг не инициализируется.
|
|
if settings.glitchtip_dsn:
|
|
sentry_sdk.init(
|
|
dsn=settings.glitchtip_dsn,
|
|
environment=settings.environment,
|
|
traces_sample_rate=0.0, # только ошибки, без performance-трейсов
|
|
send_default_pii=False, # не шлём client_name / client_phone в отчёты
|
|
)
|
|
logging.getLogger("app.main").info("GlitchTip monitoring enabled")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
# FDW bootstrap: create/refresh USER MAPPING for gendesign_remote postgres_fdw server.
|
|
# Best-effort: failure does not abort startup, just logs.
|
|
try:
|
|
with SessionLocal() as db:
|
|
ensure_fdw_user_mapping(db)
|
|
except Exception:
|
|
logger.exception(
|
|
"FDW user mapping bootstrap failed — cadastral queries may fail"
|
|
)
|
|
|
|
# Startup: launch scheduler background task
|
|
scheduler_task = asyncio.create_task(scheduler_loop())
|
|
logger.info("FastAPI lifespan: scheduler task spawned")
|
|
yield
|
|
# Shutdown: cancel scheduler
|
|
scheduler_task.cancel()
|
|
try:
|
|
await scheduler_task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
logger.info("FastAPI lifespan: scheduler cancelled cleanly")
|
|
|
|
|
|
app = FastAPI(
|
|
title="Trade-In MVP API",
|
|
description="Оценка вторичного жилья (выкупная стоимость) — копия trade-in feature из gendesign", # noqa: E501
|
|
version="0.1.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.cors_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
# Rate-limit публичного API (per-IP sliding window) — защита от абуза.
|
|
app.add_middleware(RateLimitMiddleware)
|
|
|
|
|
|
@app.get("/health")
|
|
def health() -> dict[str, str]:
|
|
return {"status": "ok", "environment": settings.environment}
|
|
|
|
|
|
app.include_router(geocode.router, prefix="/api/v1/geocode", tags=["geocode"])
|
|
app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"])
|
|
app.include_router(brand.router, prefix="/api/v1/brand", tags=["brand"])
|
|
app.include_router(trade_in.router, prefix="/api/v1/trade-in", tags=["trade-in"])
|
|
app.include_router(search.router, prefix="/api/v1", tags=["search"])
|