All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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
CI Trade-In / backend-tests (pull_request) Successful in 5m14s
Открытые данные АИС ППК «ФРТ» (бывш. Реформа ЖКХ), node 110 = реестр МКД региона 66: 41 790 строк, houseguid (ФИАС GUID) заполнен на 100% → join к houses.gar_house_guid без канонизации адреса. Анонимный GET, без ЕСИА. Заполняет ТОЛЬКО NULL-поля houses: year_built, material_walls, material_floors, total_floors, entrances, is_emergency, flat_count, heat_supply_type, gas_supply_type, hot_water + новые area_land, foundation_type, elevators_total. Замер по ЕКБ: wall_material 92.5% (было 75% от ДОМ.РФ), built_year 92.0% (было 86%), area_land 86.8% и is_emergency — признаков, которых не давал ни один из текущих источников. Осознанно НЕ мапится: - project_type → series_name: свободный текст и фактически дубль материала стен (пусто у 10 635 строк, «кирпичный» 1754, «нет данных» 1496); - energy_efficiency: решение миграции 284 + реальный класс лишь у ~10% домов (у 25 595 из 41 790 значение «Не присвоен»); - elevators_count → passenger_elevators: в источнике это ОБЩЕЕ число лифтов, в houses раздельно пассажирские и грузовые → отдельная колонка; - playground/sportsground: в источнике id справочника (498/499/500), не флаг. Дубликаты houseguid реальны и взаимодополняющи: 912 guid'ов, 968 лишних строк, у одной строки пары заполнен area_total, у парной нет. Строки СЛИВАЮТСЯ по полям (первое непустое побеждает), иначе бэкфилл терял бы данные ~2% домов. estimator.py не тронут: аналоги подбираются FROM listings без JOIN к houses, встраивание признаков в подбор когорты — отдельная задача. Миграции 290 (staging frt_mkd + колонки houses) и 291 (сид расписания, enabled=false, interval_days 30). robots.txt источника требует Crawl-delay 10.
127 lines
5 KiB
Python
127 lines
5 KiB
Python
"""CLI: АИС ППК ФРТ (реестр МКД) → houses.area_land/foundation_type/elevators_total +
|
||
добор year_built/material_walls/total_floors/entrances/is_emergency/flat_count/
|
||
heat_supply_type/gas_supply_type/hot_water (issue #frt-mkd).
|
||
|
||
Тянет CSV реестра МКД (export/{node_id}, по умолчанию 110 = region 66) с открытых
|
||
данных АИС ППК ФРТ, UPSERT'ит в staging `frt_mkd` (мигр. 290), затем backfill_houses —
|
||
COALESCE-добор перечисленных houses.* полей (только NULL, никогда не перезаписывает).
|
||
|
||
Запуск из контейнера tradein-scraper (у него есть интернет к источнику):
|
||
|
||
python -m app.tasks.frt_mkd_load # полный прогон (node 110, region 66)
|
||
python -m app.tasks.frt_mkd_load --dry-run # без записи, только подсчёт
|
||
python -m app.tasks.frt_mkd_load --load-only # только staging, без backfill
|
||
python -m app.tasks.frt_mkd_load --backfill-only # staging уже загружена — только backfill
|
||
python -m app.tasks.frt_mkd_load --src-path /tmp/frt.csv # локальный CSV, без скачивания
|
||
|
||
В режиме --dry-run скачивание/парс (если применимо) происходят, но НИ ОДНОЙ записи
|
||
в БД не делается.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import logging
|
||
|
||
from app.core.db import SessionLocal
|
||
from app.services.frt_mkd_loader import (
|
||
DEFAULT_NODE_ID,
|
||
DEFAULT_REGION_CODE,
|
||
backfill_houses,
|
||
load_frt_mkd,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def build_parser() -> argparse.ArgumentParser:
|
||
"""Парсер CLI (вынесен для тестируемости флагов без запуска main)."""
|
||
parser = argparse.ArgumentParser(
|
||
description=(
|
||
"АИС ППК ФРТ loader: реестр МКД → frt_mkd staging → "
|
||
"houses.area_land/foundation_type/elevators_total + добор смежных полей"
|
||
)
|
||
)
|
||
parser.add_argument(
|
||
"--src-path", default=None, help="локальный CSV реестра МКД (пропустить скачивание)"
|
||
)
|
||
parser.add_argument(
|
||
"--work-dir",
|
||
default=None,
|
||
help="каталог для скачанного/распакованного CSV (по умолчанию — временный, "
|
||
"удаляется после)",
|
||
)
|
||
parser.add_argument(
|
||
"--node-id",
|
||
type=int,
|
||
default=DEFAULT_NODE_ID,
|
||
help=f"id ноды export АИС ФРТ (по умолчанию {DEFAULT_NODE_ID} = реестр МКД region 66)",
|
||
)
|
||
parser.add_argument(
|
||
"--region-code",
|
||
type=int,
|
||
default=DEFAULT_REGION_CODE,
|
||
help=f"код региона выгрузки для тега staging-строк (по умолчанию {DEFAULT_REGION_CODE})",
|
||
)
|
||
parser.add_argument(
|
||
"--dry-run", action="store_true", help="без записи в БД — только скачивание/парс/подсчёт"
|
||
)
|
||
parser.add_argument(
|
||
"--load-only",
|
||
action="store_true",
|
||
help="только скачать+распарсить+UPSERT staging, без backfill houses",
|
||
)
|
||
parser.add_argument(
|
||
"--backfill-only",
|
||
action="store_true",
|
||
help="пропустить скачивание/load staging (считаем что уже загружена этим же "
|
||
"или предыдущим прогоном) — только backfill houses",
|
||
)
|
||
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()
|
||
|
||
if args.load_only and args.backfill_only:
|
||
parser.error("--load-only и --backfill-only взаимоисключающи")
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
load_counts: dict[str, int] = {}
|
||
if not args.backfill_only:
|
||
load_counts = load_frt_mkd(
|
||
db,
|
||
src_path=args.src_path,
|
||
work_dir=args.work_dir,
|
||
node_id=args.node_id,
|
||
region_code=args.region_code,
|
||
dry_run=args.dry_run,
|
||
)
|
||
if not args.dry_run:
|
||
db.commit()
|
||
logger.info("frt_mkd_load: load stage done: %s", load_counts)
|
||
|
||
houses_counts: dict[str, int] = {}
|
||
if not args.load_only:
|
||
houses_counts = backfill_houses(db, dry_run=args.dry_run)
|
||
if not args.dry_run:
|
||
db.commit()
|
||
|
||
logger.info(
|
||
"frt_mkd_load DONE: dry_run=%s load=%s houses=%s",
|
||
args.dry_run,
|
||
load_counts,
|
||
houses_counts,
|
||
)
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|