gendesign/backend/app/workers/tasks/opportunity_harvest.py
bot-backend 34616042cf
All checks were successful
CI / changes (push) Successful in 6s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (push) Successful in 5m49s
CI / backend-tests (pull_request) Successful in 5m47s
feat(sf): opportunity-ЗУ harvest → ird_overlays (future-supply, #1086)
2026-06-06 22:23:09 +03:00

105 lines
5.5 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.

"""Celery task: harvest opportunity-ЗУ (future-supply) из НСПД → ``ird_overlays`` (#1086).
Future-supply / land opportunity — свободная и выставляемая под застройку земля. Источник —
НСПД (федеральный, единообразный, vault research/NSPD_IRD_Layers_Automation_Recon_Jun06). В центре
города такие ЗУ редки (точечный probe даёт 0), но по агломерации ЕКБ присутствуют → нужен
СПЛОШНОЙ grid-walk по кварталам, как в ИРД-harvest (#1067 B5).
DIRECT-сигналы «земля под застройку» (ID в nspd_client.LAYERS):
37299 ЗУ на аукционе · 37294 ЗУ по схеме расположения · 37298 ЗУ свободные от прав ·
36473 ЗУ по проекту межевания · 37430 территории ККР (комплексное развитие).
Переиспользует ДВИЖОК ИРД-harvest (один grid-walk + UPSERT-engine, не дублируем): тот же
``ird_overlays`` (м.132), тот же ``_upsert_feature`` (ST_Transform 3857→4326, SAVEPOINT per-row,
ON CONFLICT по (source_layer_id, geom_data_id)), те же кварталы ЕКБ (``_ekb_quarters``). Отличие —
набор слоёв (opportunity вместо ограничений) и ``layer_kind='opportunity_*'`` для фильтра в analyze.
Расписание — ежемесячно (beat_schedule.py: opportunity-harvest-monthly): opportunity-земля
меняется медленнее ограничений, еженедельный прогон избыточен.
"""
from __future__ import annotations
import logging
from datetime import UTC, datetime
from app.core.db import SessionLocal
from app.services.scrapers.nspd_client import LAYERS, NSPDClient, _geojson_bbox_3857
from app.workers.celery_app import celery_app
from app.workers.tasks.ird_harvest import _ekb_quarters, _upsert_feature
logger = logging.getLogger(__name__)
# Opportunity-слои: ключ nspd_client.LAYERS → layer_kind в ird_overlays ('opportunity_*' для
# фильтра в analyze «рядом свободная/аукционная земля»). НЕ ограничения — отдельный бакет.
OPPORTUNITY_LAYER_KINDS: dict[str, str] = {
"auction_parcels": "opportunity_auction",
"scheme_parcels": "opportunity_scheme",
"free_parcels": "opportunity_free",
"future_parcels": "opportunity_future",
"krt_territories": "opportunity_krt",
}
@celery_app.task(name="tasks.opportunity_harvest.harvest_opportunity_overlays")
def harvest_opportunity_overlays(quarters: list[str] | None = None) -> dict[str, int]:
"""Harvest opportunity-ЗУ НСПД по кварталам ЕКБ → ird_overlays (layer_kind='opportunity_*').
Args:
quarters: явный список cad-номеров кварталов ('66:41:NNNNNNN'). None → distinct
кварталы ЕКБ из cad_parcels (``_ekb_quarters``).
Returns:
{'quarters': N, 'features': M} — обработано кварталов / записано opportunity-фич.
"""
fetched_at = datetime.now(UTC).isoformat()
client = NSPDClient()
db = SessionLocal()
n_quarters = 0
n_features = 0
try:
quarter_list = quarters if quarters is not None else _ekb_quarters(db)
logger.info("opportunity_harvest: старт, кварталов=%d", len(quarter_list))
for quarter in quarter_list:
try:
qres = client.search_by_cad(quarter, thematic_id=2)
qfeat = qres.first
if not qfeat or not qfeat.geometry:
logger.info("opportunity_harvest: квартал %s пуст в НСПД, скип", quarter)
continue
bbox = _geojson_bbox_3857(qfeat.geometry)
if bbox is None:
continue
n_quarters += 1
for layer_key, layer_kind in OPPORTUNITY_LAYER_KINDS.items():
layer_id = LAYERS.get(layer_key)
if layer_id is None:
logger.warning("opportunity_harvest: нет layer_id для %s", layer_key)
continue
try:
feats = client.get_features_in_bbox_grid(layer_id, bbox)
except Exception as exc:
logger.warning(
"opportunity_harvest: grid-walk failed quarter=%s layer=%s: %s",
quarter,
layer_key,
exc,
)
continue
for feat in feats:
if _upsert_feature(
db,
layer_id=layer_id,
layer_kind=layer_kind,
feature=feat,
fetched_at=fetched_at,
):
n_features += 1
except Exception as exc:
logger.warning("opportunity_harvest: квартал %s failed: %s", quarter, exc)
continue
db.commit()
logger.info("opportunity_harvest: готово, кварталов=%d фич=%d", n_quarters, n_features)
return {"quarters": n_quarters, "features": n_features}
finally:
db.close()