All checks were successful
Deploy Trade-In / test (push) Successful in 4m0s
Deploy Trade-In / build-backend (push) Successful in 1m12s
Deploy Trade-In / deploy (push) Successful in 1m47s
Deploy Trade-In / deploy-status (push) Successful in 3s
Deploy Trade-In / perimeter-smoke (push) Successful in 1m46s
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
160 lines
6.7 KiB
Python
160 lines
6.7 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
|
||
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()
|