gendesign/_wt-cadval/data/sql/178b_load_cadastral_value.py
bot-backend 542ff1c9ca docs(tradein/domclick): два комментария описывали пул, которого нет с миграции 253
Оба места утверждали, что у Домклика один выделенный резидентный прокси и
пула нет. Это перестало быть правдой ещё в #2800 (миграция 253 сняла
резервацию узла), но текст остался — и именно на него опирался тикет #3189,
поставленный под «калибровку» ограничения, которого не существует.
Свип к тому же ходит через пул давно (serp.py:359), а бэкфилл подключён к
нему в PR #3222.

Заодно докстринг называл не тот ограничитель: свип кладёт не счётчик
провалов lease, а break по первому DomClickBlockedError (#2854) — до
ротации дело не доходит ни при каком счётчике, отсюда buckets_completed=0.

Только комментарии, поведение не меняется. Миграция 175 уже применена, а
_schema_migrations трекает по имени файла без checksum — правка текста
её не перезапустит.
2026-08-29 18:57:25 +03:00

157 lines
6.1 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.

"""Загрузчик кварталов 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())