"""Загрузчик кварталов dataset_КАДАСТРСТОИМОСТЬ (кадастровая стоимость) в gendesign. Region-66-scoped: при INSERT фильтруем region_code='66' (Свердловская обл.), хотя исходный CSV покрывает регионы 60-92. Источник — открытый квартальный датасет Росреестра. Заливается через psycopg v3 (staging COPY → typed INSERT). Назначение: cost_per_area (кадастровая ₽/m²) даёт «пол» цены на квартал, по которому denoise-миграция 179 отбрасывает зашумлённые сделки rosreestr_deals дешевле 0.7x кадастрового медианного ₽/m². Idempotent: пропускает source_quarter, уже залитый в rosreestr_cadastral_value. Запускается ВРУЧНУЮ оператором после деплоя; затем REFRESH двух MV (см. 179). Usage: cd backend uv run python ../data/sql/178b_load_cadastral_value.py 2026Q1 uv run python ../data/sql/178b_load_cadastral_value.py # все из JOBS """ from __future__ import annotations import os import sys import time from pathlib import Path import psycopg PG_DSN = os.environ.get( "TUNNEL_DSN", "postgresql://gendesign:2J2SBPMKuS998fiwhtQqDhMI@localhost:15432/gendesign", ) RAW = Path(__file__).resolve().parent.parent / "raw" # Датасет всегда UTF-8, delimiter '~', header columns в порядке staging-таблицы. DELIMITER = "~" # (source_quarter, csv_filename) JOBS: list[tuple[str, str]] = [ ("2026Q1", "dataset_КАДАСТРСТОИМОСТЬ_r-r_60-92_y_2026_q_1.csv"), ] # Region-66-scoped typed INSERT. Staging — всё TEXT, поэтому каждое числовое поле # проходит regex-guard + NULLIF перед ::cast (мусорные строки → NULL, не падаем). INSERT_SQL = """ INSERT INTO rosreestr_cadastral_value ( source_quarter, number, realestate_type_code, purpose_code, area, region_code, district, city, quarter_cad_number, street, registration_quarter, application_quarter, cost, cost_per_area, period ) SELECT %s, CASE WHEN number ~ '^[0-9]+$' THEN number::INTEGER ELSE NULL END, NULLIF(realestate_type_code, ''), NULLIF(purpose_code, ''), CASE WHEN area ~ '^[0-9]+(\\.[0-9]+)?$' AND area::NUMERIC > 0 THEN area::NUMERIC ELSE NULL END, region_code::SMALLINT, NULLIF(district, ''), NULLIF(city, ''), NULLIF(quarter_cad_number, ''), NULLIF(street, ''), NULLIF(registration_quarter, ''), NULLIF(application_quarter, ''), CASE WHEN cost ~ '^[0-9]+(\\.[0-9]+)?$' AND cost::NUMERIC > 0 THEN cost::NUMERIC ELSE NULL END, CASE WHEN cost_per_area ~ '^[0-9]+(\\.[0-9]+)?$' AND cost_per_area::NUMERIC > 0 THEN cost_per_area::NUMERIC ELSE NULL END, NULLIF(period, '') FROM rosreestr_cadastral_value_staging WHERE region_code = '66' AND NULLIF(quarter_cad_number, '') IS NOT NULL """ def main() -> int: targets = sys.argv[1:] or [j[0] for j in JOBS] selected = [j for j in JOBS if j[0] in targets] if not selected: print(f"Нет matching кварталов в JOBS. Targets: {targets}") return 1 conn = psycopg.connect(PG_DSN, connect_timeout=15) try: for sq, fname in selected: with conn.cursor() as cur: cur.execute( "SELECT COUNT(*) FROM rosreestr_cadastral_value " "WHERE source_quarter = %s", (sq,), ) already = cur.fetchone()[0] if already: print( f"=== {sq} SKIP — уже {already} строк " f"(DELETE FROM rosreestr_cadastral_value " f"WHERE source_quarter='{sq}' чтобы перезалить)" ) continue csv_path = RAW / fname if not csv_path.exists(): print(f"=== {sq} SKIP — нет файла {csv_path}") continue size_mb = csv_path.stat().st_size / 1e6 print(f"=== {sq} ({fname}, sep='{DELIMITER}', {size_mb:.1f} MB) ===") t0 = time.time() with conn.cursor() as cur: cur.execute("TRUNCATE rosreestr_cadastral_value_staging") copy_sql = ( f"COPY rosreestr_cadastral_value_staging FROM STDIN " f"(FORMAT csv, HEADER true, DELIMITER '{DELIMITER}', " f"QUOTE '\"', NULL '')" ) with cur.copy(copy_sql) as copy: with open(csv_path, "rb") as f: while True: chunk = f.read(8 * 1024 * 1024) if not chunk: break copy.write(chunk) t_copy = time.time() - t0 cur.execute(INSERT_SQL, (sq,)) inserted = cur.rowcount conn.commit() t_total = time.time() - t0 print( f" COPY {t_copy:.0f}s, INSERT (region 66) {inserted} rows, " f"total {t_total:.0f}s" ) # Final summary with conn.cursor() as cur: cur.execute( """ SELECT source_quarter, COUNT(*) AS rows, COUNT(*) FILTER ( WHERE realestate_type_code = '002001003000' ) AS apt_rows FROM rosreestr_cadastral_value GROUP BY source_quarter ORDER BY source_quarter """ ) print("\n=== Итоги по rosreestr_cadastral_value (region 66) ===") for sq, rows, apt_rows in cur.fetchall(): print(f" {sq} rows={rows:>9} apartments={apt_rows:>9}") finally: conn.close() return 0 if __name__ == "__main__": sys.exit(main())