"""Загрузчик кварталов dataset_СДЕЛКИ через SSH-tunnel на localhost:15432. Эквивалент 02_load_all_quarters.sh, но через psycopg вместо docker — удобнее запускать с Windows / любой машины с уже открытым SSH-tunnel. Idempotent: пропускает кварталы, уже залитые в rosreestr_deals. Usage: cd backend uv run python ../data/sql/02b_load_via_tunnel.py 2024Q1 2024Q2 uv run python ../data/sql/02b_load_via_tunnel.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" # (source_quarter, period_start, csv_filename, delimiter) JOBS: list[tuple[str, str, str, str]] = [ ("2024Q1", "2024-01-01", "dataset_СДЕЛКИ_r-r_01-92_y_2024_q_1.csv", ";"), ("2024Q2", "2024-04-01", "dataset_СДЕЛКИ_r-r_01-92_y_2024_q_2.csv", ";"), # Q3+ уже залиты, но оставляем как fallback / re-run target. ("2024Q3", "2024-07-01", "dataset_СДЕЛКИ_r_all_q_3.csv", ";"), ("2024Q4", "2024-10-01", "dataset_СДЕЛКИ_r-r_01-92_y_2024_q_4.csv", "~"), ("2025Q1", "2025-01-01", "dataset_СДЕЛКИ_r-r_01-92_y_2025_q_1.csv", "~"), ("2025Q2", "2025-04-01", "dataset_СДЕЛКИ_r-r_01-92_y_2025_q_2.csv", "~"), ("2025Q3", "2025-07-01", "dataset_СДЕЛКИ_r-r_01-92_y_2025_q_3.csv", "~"), ("2025Q4", "2025-10-01", "dataset_СДЕЛКИ_r-r_01-92_y_2025_q_4.csv", "~"), ("2026Q1", "2026-01-01", "dataset_СДЕЛКИ_r-r_01-92_y_2026_q_1.csv", "~"), ] COPY_SQL = """ COPY rosreestr_deals_staging FROM STDIN (FORMAT csv, HEADER true, DELIMITER %r, QUOTE '"', NULL '') """ INSERT_SQL = """ INSERT INTO rosreestr_deals ( source_quarter, period_start_date, region_code, okato, district, city, quarter_cad_number, street, realestate_type_code, wall_material_code, purpose_code, year_build, floor, area, doc_type, deal_price, currency, deal_count ) SELECT %s, period_start_date, region_code, NULLIF(okato, '-'), NULLIF(district, ''), NULLIF(city, ''), quarter_cad_number, NULLIF(street, ''), NULLIF(realestate_type_code, ''), NULLIF(wall_material_code, ''), NULLIF(purpose_code, ''), CASE WHEN year_build ~ '^[12][0-9]{3}$' THEN year_build::INT ELSE NULL END, NULLIF(floor, ''), CASE WHEN area > 0 AND area < 1e8 THEN area ELSE NULL END, NULLIF(doc_type, ''), CASE WHEN deal_price > 0 AND deal_price < 1e15 THEN deal_price ELSE NULL END, COALESCE(NULLIF(currency, ''), 'рубль'), COALESCE(deal_count, 1) FROM rosreestr_deals_staging WHERE region_code IS NOT NULL AND quarter_cad_number IS NOT NULL AND quarter_cad_number <> '' AND doc_type IS NOT NULL AND doc_type <> '' AND period_start_date 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, period, fname, sep in selected: with conn.cursor() as cur: cur.execute( "SELECT COUNT(*) FROM rosreestr_deals WHERE source_quarter = %s", (sq,), ) already = cur.fetchone()[0] if already: print(f"=== {sq} SKIP — уже {already} строк (DELETE FROM rosreestr_deals WHERE source_quarter='{sq}' чтобы перезалить)") continue csv_path = RAW / fname if not csv_path.exists(): print(f"=== {sq} SKIP — нет файла {csv_path}") continue print(f"=== {sq} ({fname}, sep='{sep}', {csv_path.stat().st_size/1e6:.1f} MB) ===") t0 = time.time() with conn.cursor() as cur: cur.execute("TRUNCATE rosreestr_deals_staging") copy_sql = ( f"COPY rosreestr_deals_staging FROM STDIN " f"(FORMAT csv, HEADER true, DELIMITER '{sep}', 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 {inserted} rows, total {t_total:.0f}s") # Final summary with conn.cursor() as cur: cur.execute( """ SELECT source_quarter, COUNT(*) AS rows, SUM(deal_count) AS deals FROM rosreestr_deals GROUP BY source_quarter ORDER BY source_quarter """ ) print("\n=== Итоги по rosreestr_deals ===") for sq, rows, deals in cur.fetchall(): print(f" {sq} rows={rows:>9} deals={deals:>10}") finally: conn.close() return 0 if __name__ == "__main__": sys.exit(main())