gendesign/backend/app/main.py
lekss361 bd6b8a2e76 feat(tradein): cadastral reverse geocode via main backend cad_buildings
Fixes 500 on /trade-in/api/v1/geocode/reverse when Nominatim bans prod IP
after geocode_missing batch. Cadastral lookup via main backend new
endpoint /api/v1/cadastral/reverse is primary (37k buildings in ЕКБ,
<10ms, no external API). Nominatim is fallback wrapped in try/except
so HTTPStatusError no longer bubbles to FastAPI 500.

Architecture:
- main backend: new GET /api/v1/cadastral/reverse — cad_buildings nearest
  within radius, filters out гаражи/СНТ. Added to gendesign_shared network
  with alias gendesign-backend for cross-stack DNS.
- tradein backend: _cadastral_reverse() calls main backend, returns None
  on any error. reverse_geocode prefers cadastral → falls back to
  Nominatim → returns None (endpoint becomes 404 instead of 500).

Env: MAIN_BACKEND_URL=http://gendesign-backend:8000 in tradein prod.
Tests: 7 new cases covering cadastral/nominatim/fallback/error paths.
2026-05-24 10:22:06 +03:00

114 lines
4 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.

import logging
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import sentry_sdk
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.httpx import HttpxIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration
from app.api.v1 import (
admin_cadastre,
admin_etl,
admin_jobs,
admin_leads,
admin_scrape,
admin_weight_profiles,
analytics,
cadastral,
concepts,
custom_pois,
landing,
parcels,
photos,
pilot,
trade_in,
users,
)
from app.core.config import settings
from app.observability.sentry_scrub import scrub_sensitive_query
logger = logging.getLogger(__name__)
# Инициализируем SDK до создания FastAPI app, чтобы все инструменты
# (middleware, маршруты) видели активный client с самого старта процесса.
# GlitchTip не поддерживает profiling — profiles_sample_rate=0.0.
if settings.glitchtip_dsn:
sentry_sdk.init(
dsn=settings.glitchtip_dsn,
environment=settings.environment,
release=os.getenv("GIT_SHA") or os.getenv("SENTRY_RELEASE") or "unknown",
traces_sample_rate=settings.glitchtip_traces_sample_rate,
profiles_sample_rate=0.0,
send_default_pii=False,
before_send_transaction=scrub_sensitive_query,
integrations=[
StarletteIntegration(),
FastApiIntegration(),
CeleryIntegration(monitor_beat_tasks=True),
SqlalchemyIntegration(),
HttpxIntegration(),
LoggingIntegration(level=logging.INFO, event_level=logging.ERROR),
],
)
logger.info(
"GlitchTip SDK initialised (env=%s, traces=%.2f)",
settings.environment,
settings.glitchtip_traces_sample_rate,
)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
yield
app = FastAPI(title="GenDesign API", version="0.1.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(cadastral.router, prefix="/api/v1/cadastral", tags=["cadastral"])
app.include_router(concepts.router, prefix="/api/v1/concepts", tags=["concepts"])
app.include_router(parcels.router, prefix="/api/v1/parcels", tags=["parcels"])
app.include_router(analytics.router, prefix="/api/v1/analytics", tags=["analytics"])
app.include_router(admin_scrape.router, prefix="/api/v1/admin/scrape", tags=["admin"])
app.include_router(admin_jobs.router, prefix="/api/v1/admin/jobs", tags=["admin"])
app.include_router(admin_leads.router, prefix="/api/v1/admin/leads", tags=["admin"])
app.include_router(
admin_weight_profiles.router,
prefix="/api/v1/admin/site-finder/weight-profiles",
tags=["admin", "site-finder"],
)
app.include_router(photos.router, prefix="/api/v1/photos", tags=["photos"])
app.include_router(custom_pois.router, prefix="/api/v1/custom-pois", tags=["custom-pois"])
app.include_router(
admin_cadastre.router,
prefix="/api/v1/admin/cadastre",
tags=["admin", "cadastre"],
)
app.include_router(
admin_etl.router,
prefix="/api/v1/admin/etl",
tags=["admin", "etl"],
)
app.include_router(trade_in.router, prefix="/api/v1/trade-in", tags=["trade-in"])
app.include_router(landing.router, prefix="/api/v1", tags=["landing"])
app.include_router(pilot.router, prefix="/api/v1/pilot", tags=["pilot"])
app.include_router(users.router, prefix="/api/v1", tags=["users"])
@app.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok", "environment": settings.environment}