"""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 python -m app.tasks.gar_flats_load --dir /data/gar/77 --region 77 --version 2026-09-11 python -m app.tasks.gar_flats_load --dir /data/gar/50 --region 50 --version 2026-09-11 Делает два шага в одной транзакционной сессии: 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 python -m app.tasks.gar_flats_load --match-only --region 77 python -m app.tasks.gar_flats_load --match-only --region 50 В режиме `--match-only` шаг парса/загрузки пропускается целиком; `--dir` не требуется. Многогигабайтный ДАМП качается/распаковывается отдельно (ops-шаг, см. docs/gar-flats-runbook.md) — этот лоадер потребляет уже распакованные XML локально. Город-фильтр матча (--city) по умолчанию НЕ вводится руками на каждый запуск — берётся ПО РЕГИОНУ (см. app.services.gar_flats_loader.default_city_filter_for_region): region 66 получает byte-for-byte прежний фильтр «Екатеринбург», остальные регионы (77, 50 и любой новый) — без фильтра. `--city ""` явно отключает фильтр для ЛЮБОГО региона (в т.ч. 66); `--city "Имя"` — явный override. """ 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 ( CITY_FILTER_AUTO, CityFilterAutoType, default_city_filter_for_region, load_gar_region, match_houses_to_gar, ) logger = logging.getLogger(__name__) def run_gar_flats_load( dir_path: str, region_code: str, gar_version: str, *, city_filter: str | CityFilterAutoType | None = CITY_FILTER_AUTO, ) -> 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 | CityFilterAutoType | None = CITY_FILTER_AUTO ) -> 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=None, help=( "город-фильтр матча (ILIKE по full_address); по умолчанию берётся ПО РЕГИОНУ " "(--region) — см. default_city_filter_for_region; пустая строка явно отключает " "фильтр для ЛЮБОГО региона" ), ) 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 не передан явно (argparse default=None) → дефолт ПО РЕГИОНУ (66 → «Екатеринбург» # byte-for-byte как раньше, остальные — без фильтра). Передан явно (в т.ч. "") → # уважаем волю вызывающего: "" → None (фильтр отключён), непустая строка → override. if args.city is None: city_filter: str | None = default_city_filter_for_region(args.region) else: 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()