gendesign/tradein-mvp/backend/app/tasks/gar_flats_load.py
bot-backend c729a39446
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(tradein): канонический матч ГАР↔houses (0%→41% yield) + tradein_canon_addr + --match-only
2026-06-28 18:44:22 +03:00

139 lines
5.3 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.

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