139 lines
5.3 KiB
Python
139 lines
5.3 KiB
Python
"""CLI: загрузка ГАР-знаменателя «квартир на дом» + матч к houses (мигр. 143/144).
|
||
|
||
Запуск (каталог — УЖЕ распакованный ГАР региона, напр. папка `66/` из gar_xml.zip):
|
||
|
||
python -m app.tasks.gar_flats_load --dir /data/gar/66 --region 66 --version 2026-06-01
|
||
|
||
Делает два шага в одной транзакционной сессии:
|
||
1. load_gar_region — стриминговый парс XML → UPSERT gar_house_flats (коммит).
|
||
2. match_houses_to_gar — UPDATE houses.gar_flat_count по КАНОНИЧЕСКОМУ адресу (мигр. 144).
|
||
|
||
Ре-матч без повторного парса многогигабайтного XML (gar_house_flats уже загружена):
|
||
|
||
python -m app.tasks.gar_flats_load --match-only --region 66
|
||
|
||
В режиме `--match-only` шаг парса/загрузки пропускается целиком; `--dir` не требуется.
|
||
|
||
Многогигабайтный ДАМП качается/распаковывается отдельно (ops-шаг, см.
|
||
docs/gar-flats-runbook.md) — этот лоадер потребляет уже распакованные XML локально.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import logging
|
||
from datetime import date
|
||
|
||
from app.core.db import SessionLocal
|
||
from app.services.gar_flats_loader import load_gar_region, match_houses_to_gar
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Город-фильтр матча по умолчанию (см. match_houses_to_gar): ЕКБ-restricted против
|
||
# cross-town коллизий в region 66. Пустая строка в --city → None (фильтр отключён).
|
||
DEFAULT_CITY_FILTER = "Екатеринбург"
|
||
|
||
|
||
def run_gar_flats_load(
|
||
dir_path: str,
|
||
region_code: str,
|
||
gar_version: str,
|
||
*,
|
||
city_filter: str | None = DEFAULT_CITY_FILTER,
|
||
) -> dict[str, int]:
|
||
"""Парс+UPSERT (load_gar_region) затем матч (match_houses_to_gar). Возвращает счётчики."""
|
||
db = SessionLocal()
|
||
try:
|
||
load = load_gar_region(dir_path, region_code, gar_version, db=db)
|
||
matched = match_houses_to_gar(db, region_code=region_code, city_filter=city_filter)
|
||
db.commit()
|
||
finally:
|
||
db.close()
|
||
|
||
counts = load.to_counts()
|
||
counts["houses_matched"] = matched
|
||
logger.info(
|
||
"gar_flats_load DONE: region=%s version=%s houses=%d apartments=%d upserted=%d "
|
||
"houses_matched=%d duration=%ds",
|
||
region_code,
|
||
gar_version,
|
||
counts["houses"],
|
||
counts["apartments"],
|
||
counts["upserted"],
|
||
matched,
|
||
counts["duration_sec"],
|
||
)
|
||
return counts
|
||
|
||
|
||
def run_gar_match_only(
|
||
region_code: str, *, city_filter: str | None = DEFAULT_CITY_FILTER
|
||
) -> dict[str, int]:
|
||
"""Только ре-матч уже загруженного gar_house_flats → houses (без парса XML).
|
||
|
||
Идемпотентно (match_houses_to_gar — plain UPDATE с IS DISTINCT FROM gate).
|
||
"""
|
||
db = SessionLocal()
|
||
try:
|
||
matched = match_houses_to_gar(db, region_code=region_code, city_filter=city_filter)
|
||
db.commit()
|
||
finally:
|
||
db.close()
|
||
logger.info(
|
||
"gar_flats_load --match-only DONE: region=%s city=%s houses_matched=%d",
|
||
region_code,
|
||
city_filter,
|
||
matched,
|
||
)
|
||
return {"houses_matched": matched}
|
||
|
||
|
||
def build_parser() -> argparse.ArgumentParser:
|
||
"""Парсер CLI (вынесен для тестируемости флагов без запуска main)."""
|
||
parser = argparse.ArgumentParser(
|
||
description="ГАР loader: квартир на дом → gar_house_flats + match к houses"
|
||
)
|
||
# --dir не required: в режиме --match-only парс пропускается (валидируем в main).
|
||
parser.add_argument("--dir", help="каталог с распакованными ГАР XML региона")
|
||
parser.add_argument("--region", default="66", help="код региона (по умолчанию 66)")
|
||
parser.add_argument(
|
||
"--version",
|
||
default=date.today().isoformat(),
|
||
help="версия ГАР-дампа YYYY-MM-DD (по умолчанию сегодня)",
|
||
)
|
||
parser.add_argument(
|
||
"--match-only",
|
||
action="store_true",
|
||
help="пропустить парс/загрузку ГАР, только пере-матчить gar_house_flats к houses",
|
||
)
|
||
parser.add_argument(
|
||
"--city",
|
||
default=DEFAULT_CITY_FILTER,
|
||
help="город-фильтр матча (ILIKE по full_address); пусто = без ограничения (не-ЕКБ)",
|
||
)
|
||
return parser
|
||
|
||
|
||
def main() -> None:
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||
)
|
||
parser = build_parser()
|
||
args = parser.parse_args()
|
||
|
||
# Пустой --city → None (фильтр отключён).
|
||
city_filter = args.city or None
|
||
|
||
if args.match_only:
|
||
run_gar_match_only(args.region, city_filter=city_filter)
|
||
return
|
||
|
||
if not args.dir:
|
||
parser.error("--dir обязателен, кроме режима --match-only")
|
||
|
||
run_gar_flats_load(args.dir, args.region, args.version, city_filter=city_filter)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|