"""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()