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.
65 lines
2.1 KiB
Python
65 lines
2.1 KiB
Python
"""postgres_fdw USER MAPPING management for gendesign_remote server.
|
|
|
|
USER MAPPING password is in env GENDESIGN_FDW_PASSWORD (not in SQL migration —
|
|
would leak password to disk).
|
|
|
|
This helper applies idempotent CREATE / ALTER USER MAPPING. Reapplied on every
|
|
backend startup so password rotation through .env.runtime is picked up after restart.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from sqlalchemy import text
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def ensure_fdw_user_mapping(db: Session) -> None:
|
|
"""Idempotent CREATE OR ALTER USER MAPPING for tradein → gendesign_remote.
|
|
|
|
Skips silently if password not configured (dev / first deploy).
|
|
"""
|
|
password = settings.gendesign_fdw_password
|
|
if not password:
|
|
logger.warning(
|
|
"GENDESIGN_FDW_PASSWORD not set — skipping FDW user mapping "
|
|
"(gendesign_cad_buildings will fail)"
|
|
)
|
|
return
|
|
if not isinstance(password, str) or not password.strip():
|
|
logger.warning("GENDESIGN_FDW_PASSWORD empty — skipping FDW user mapping")
|
|
return
|
|
|
|
# Check existence
|
|
exists = db.execute(
|
|
text(
|
|
"SELECT 1 FROM pg_user_mappings "
|
|
"WHERE srvname = 'gendesign_remote' AND usename = current_user"
|
|
)
|
|
).first()
|
|
|
|
# Password cannot be a bind parameter in CREATE/ALTER USER MAPPING — must inline.
|
|
# Escape single quotes by doubling.
|
|
escaped_pw = password.replace("'", "''")
|
|
if exists is None:
|
|
db.execute(
|
|
text(
|
|
f"CREATE USER MAPPING FOR CURRENT_USER SERVER gendesign_remote "
|
|
f"OPTIONS (user 'tradein_fdw_reader', password '{escaped_pw}')"
|
|
)
|
|
)
|
|
logger.info("created FDW user mapping for gendesign_remote")
|
|
else:
|
|
db.execute(
|
|
text(
|
|
f"ALTER USER MAPPING FOR CURRENT_USER SERVER gendesign_remote "
|
|
f"OPTIONS (SET password '{escaped_pw}')"
|
|
)
|
|
)
|
|
logger.info("refreshed FDW user mapping password for gendesign_remote")
|
|
|
|
db.commit()
|