chore(tradein): полное выключение источника n1 — миграция 165 + вычистка backend/ops (#2204)
This commit is contained in:
parent
bbe596747d
commit
dd050af16d
13 changed files with 183 additions and 24 deletions
|
|
@ -411,8 +411,6 @@ class Settings(BaseSettings):
|
||||||
"cian.ru",
|
"cian.ru",
|
||||||
"ekb.cian.ru",
|
"ekb.cian.ru",
|
||||||
"realty.yandex.ru",
|
"realty.yandex.ru",
|
||||||
"ekaterinburg.n1.ru",
|
|
||||||
"n1.ru",
|
|
||||||
"domclick.ru",
|
"domclick.ru",
|
||||||
"www.domclick.ru",
|
"www.domclick.ru",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ class SearchParams(BaseModel):
|
||||||
|
|
||||||
# --- Quality / cross-source ---
|
# --- Quality / cross-source ---
|
||||||
has_kadastr: bool = False
|
has_kadastr: bool = False
|
||||||
sources: list[Literal["avito", "cian", "yandex_realty", "n1"]] | None = None
|
sources: list[Literal["avito", "cian", "yandex_realty"]] | None = None
|
||||||
multi_source_only: bool = False
|
multi_source_only: bool = False
|
||||||
require_avito: bool = False
|
require_avito: bool = False
|
||||||
require_cian: bool = False
|
require_cian: bool = False
|
||||||
|
|
|
||||||
|
|
@ -506,7 +506,7 @@ class SalesListingPair(BaseModel):
|
||||||
deal_address: str | None = None
|
deal_address: str | None = None
|
||||||
|
|
||||||
listing_id: int | None = None
|
listing_id: int | None = None
|
||||||
listing_source: str | None = None # 'avito' / 'cian' / 'yandex' / 'n1' / 'domklik'
|
listing_source: str | None = None # 'avito' / 'cian' / 'yandex' / 'domklik' (+ inactive 'n1')
|
||||||
listing_source_url: str | None = None
|
listing_source_url: str | None = None
|
||||||
listing_date: date | None = None
|
listing_date: date | None = None
|
||||||
listing_price_rub: int | None = None
|
listing_price_rub: int | None = None
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,6 @@ _ALLOWED_URL_DOMAINS: frozenset[str] = frozenset(
|
||||||
"www.cian.ru",
|
"www.cian.ru",
|
||||||
"cian.ru",
|
"cian.ru",
|
||||||
"realty.yandex.ru",
|
"realty.yandex.ru",
|
||||||
"n1.ru",
|
|
||||||
"ekaterinburg.n1.ru",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -70,7 +68,6 @@ _ALLOWED_PHOTO_CDN: frozenset[str] = frozenset(
|
||||||
"images.avito.st",
|
"images.avito.st",
|
||||||
"cdn-p.cian.site",
|
"cdn-p.cian.site",
|
||||||
"avatars.mds.yandex.net",
|
"avatars.mds.yandex.net",
|
||||||
"n1ru.cdn-cw.com",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -176,12 +173,14 @@ def _make_safe_url_fetcher(): # type: ignore[no-untyped-def]
|
||||||
|
|
||||||
|
|
||||||
# ── Source pseudo-logos (текстовые pill-badges как у Брусники с логотипами) ──
|
# ── Source pseudo-logos (текстовые pill-badges как у Брусники с логотипами) ──
|
||||||
|
# Неизвестный/снятый source (напр. историческое inactive 'n1', #2204) рендерится
|
||||||
|
# через безопасный fallback в _source_pill: серый фон + source.title() — код на
|
||||||
|
# исторических строках НЕ падает.
|
||||||
_SOURCE_LOGO_COLORS: dict[str, tuple[str, str]] = {
|
_SOURCE_LOGO_COLORS: dict[str, tuple[str, str]] = {
|
||||||
"avito": ("#00aaff", "#fff"), # Avito brand blue (упрощённо)
|
"avito": ("#00aaff", "#fff"), # Avito brand blue (упрощённо)
|
||||||
"cian": ("#0468ff", "#fff"), # Циан фирменный синий
|
"cian": ("#0468ff", "#fff"), # Циан фирменный синий
|
||||||
"domklik": ("#1ab248", "#fff"), # ДомКлик зелёный Сбер
|
"domklik": ("#1ab248", "#fff"), # ДомКлик зелёный Сбер
|
||||||
"yandex": ("#fc3f1d", "#fff"), # Я.Недвижимость красный
|
"yandex": ("#fc3f1d", "#fff"), # Я.Недвижимость красный
|
||||||
"n1": ("#ff6b00", "#fff"), # N1 оранжевый
|
|
||||||
"rosreestr": ("#003d82", "#fff"), # Росреестр тёмно-синий
|
"rosreestr": ("#003d82", "#fff"), # Росреестр тёмно-синий
|
||||||
"etazhi": ("#e30613", "#fff"), # Этажи красный
|
"etazhi": ("#e30613", "#fff"), # Этажи красный
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +190,6 @@ _SOURCE_DISPLAY_NAMES: dict[str, str] = {
|
||||||
"cian": "Циан",
|
"cian": "Циан",
|
||||||
"domklik": "Домклик · Сбер",
|
"domklik": "Домклик · Сбер",
|
||||||
"yandex": "Я.Недвижимость",
|
"yandex": "Я.Недвижимость",
|
||||||
"n1": "N1",
|
|
||||||
"rosreestr": "Росреестр",
|
"rosreestr": "Росреестр",
|
||||||
"etazhi": "Этажи",
|
"etazhi": "Этажи",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ def deactivate_stale_listings(
|
||||||
|
|
||||||
Параметры:
|
Параметры:
|
||||||
listing_source: значение поля source в таблице listings ('avito', 'yandex',
|
listing_source: значение поля source в таблице listings ('avito', 'yandex',
|
||||||
'cian', 'domklik', 'n1').
|
'cian', 'domklik').
|
||||||
ttl_days: количество дней TTL; объявления старше этого порога деактивируются.
|
ttl_days: количество дней TTL; объявления старше этого порога деактивируются.
|
||||||
segments: если задан -- деактивировать только объявления с указанными
|
segments: если задан -- деактивировать только объявления с указанными
|
||||||
listing_segment значениями. None -> все сегменты (поведение avito по умолчанию).
|
listing_segment значениями. None -> все сегменты (поведение avito по умолчанию).
|
||||||
|
|
|
||||||
60
tradein-mvp/backend/data/sql/165_remove_n1_source.sql
Normal file
60
tradein-mvp/backend/data/sql/165_remove_n1_source.sql
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
-- 165_remove_n1_source.sql
|
||||||
|
--
|
||||||
|
-- ПРОДУКТОВОЕ РЕШЕНИЕ (user, 2026-07-03, #2204):
|
||||||
|
-- Источник n1 ВЫКЛЮЧАЕМ И ВЫЧИЩАЕМ из всех продуктовых поверхностей trade-in
|
||||||
|
-- («удалить отовсюду, чтобы не появлялся»). Кода скрейпера n1 давно нет
|
||||||
|
-- (см. 114 — портал переехал в cian ещё 2026-06); 381 активная строка
|
||||||
|
-- listings source='n1' деактивируется TTL-джобой deactivate_stale_n1
|
||||||
|
-- (заведена 160) сегодня в 07:00 UTC. Эта миграция закрепляет выключение
|
||||||
|
-- декларативно и убирает n1 из scheduler/settings, чтобы источник не мог
|
||||||
|
-- ожить и не считался в active-пуле.
|
||||||
|
--
|
||||||
|
-- ЧТО ДЕЛАЕТ:
|
||||||
|
-- 1. UPDATE listings — страховка: если TTL-джоба ещё не отработала на момент
|
||||||
|
-- деплоя, добиваем оставшиеся is_active строки source='n1'. Строки НЕ
|
||||||
|
-- удаляем — остаются в БД с is_active=false как история (аналитика/аудит).
|
||||||
|
-- 2. DELETE из scrape_schedules — убираем обе n1-строки расписаний:
|
||||||
|
-- - 'n1_city_sweep' (сеяна 084, выключена 114) — активный свип;
|
||||||
|
-- - 'deactivate_stale_n1'(сеяна 160) — TTL-деактивация: после того как
|
||||||
|
-- инвентарь деактивирован, ежедневный прогон больше не нужен.
|
||||||
|
-- Row'ы n1 нужны только историческим run'ам scrape_runs (audit сохранён
|
||||||
|
-- там); сами scrape_schedules — конфиг, не история, чистить безопасно.
|
||||||
|
-- 3. DELETE из scraper_settings — убираем per-source knob 'n1' (сеян 054):
|
||||||
|
-- anti-ban delay мёртвого источника не нужен.
|
||||||
|
--
|
||||||
|
-- ИДЕМПОТЕНТНОСТЬ:
|
||||||
|
-- - UPDATE ограничен `AND is_active` -> повторный прогон трогает 0 строк.
|
||||||
|
-- - DELETE ... WHERE source IN (...) -> повторный прогон удаляет 0 строк.
|
||||||
|
-- Безопасно применять многократно.
|
||||||
|
--
|
||||||
|
-- НЕДЕСТРУКТИВНОСТЬ К ДАННЫМ:
|
||||||
|
-- Никакого `DELETE FROM listings` — исторические объявления source='n1'
|
||||||
|
-- сохраняются (только is_active=false). Удаляются лишь конфиг-строки
|
||||||
|
-- расписаний/настроек скрейпера.
|
||||||
|
--
|
||||||
|
-- ЗАВИСИМОСТИ: 052_scrape_schedules.sql, 053/054 scraper_settings, 160 (seed).
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- (1) Страховка-деактивация оставшихся активных n1-объявлений (обычно no-op:
|
||||||
|
-- TTL-джоба deactivate_stale_n1 отрабатывает раньше). Строки НЕ удаляем.
|
||||||
|
UPDATE listings
|
||||||
|
SET is_active = false
|
||||||
|
WHERE source = 'n1'
|
||||||
|
AND is_active;
|
||||||
|
|
||||||
|
-- (2) Убрать n1-расписания из scheduler-конфига (свип + TTL-деактивация).
|
||||||
|
DELETE FROM scrape_schedules
|
||||||
|
WHERE source IN ('n1_city_sweep', 'deactivate_stale_n1');
|
||||||
|
|
||||||
|
-- (3) Убрать per-source anti-ban knob мёртвого источника.
|
||||||
|
DELETE FROM scraper_settings
|
||||||
|
WHERE source = 'n1';
|
||||||
|
|
||||||
|
-- Освежить справочный комментарий таблицы (n1 больше не источник свипа).
|
||||||
|
COMMENT ON TABLE scrape_schedules IS
|
||||||
|
'In-app scheduler config: avito/yandex/cian city sweep, rosreestr import, '
|
||||||
|
'listing_source_snapshot, deactivate_stale_* family. Источник n1 удалён '
|
||||||
|
'(#2204, 2026-07-03 — портал мёртв, переехал в cian).';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
@ -169,3 +169,4 @@
|
||||||
162_seed_deals_freshness_monitor.sql
|
162_seed_deals_freshness_monitor.sql
|
||||||
163_disable_deactivate_stale_domklik.sql
|
163_disable_deactivate_stale_domklik.sql
|
||||||
164_yandex_url_canonicalize_active_dups.sql
|
164_yandex_url_canonicalize_active_dups.sql
|
||||||
|
165_remove_n1_source.sql
|
||||||
|
|
|
||||||
|
|
@ -450,8 +450,8 @@ def run_backfill(
|
||||||
and gives a checkpoint every commit.
|
and gives a checkpoint every commit.
|
||||||
limit: stop after processing this many total rows (--limit). None =
|
limit: stop after processing this many total rows (--limit). None =
|
||||||
run to exhaustion.
|
run to exhaustion.
|
||||||
source: filter to one ext_source ('avito'/'cian'/'yandex'/'n1').
|
source: filter to one ext_source ('avito'/'cian'/'yandex').
|
||||||
None = all sources.
|
None = all sources (historical 'n1' rows still processed in a full run).
|
||||||
dry_run: skip all writes, just count.
|
dry_run: skip all writes, just count.
|
||||||
"""
|
"""
|
||||||
stats = Stats()
|
stats = Stats()
|
||||||
|
|
@ -569,7 +569,7 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
)
|
)
|
||||||
p.add_argument(
|
p.add_argument(
|
||||||
"--source",
|
"--source",
|
||||||
choices=("avito", "cian", "yandex", "n1"),
|
choices=("avito", "cian", "yandex"),
|
||||||
default=None,
|
default=None,
|
||||||
help=(
|
help=(
|
||||||
"Restrict to one ext_source for staged rollout. Default: all sources mixed in id order."
|
"Restrict to one ext_source for staged rollout. Default: all sources mixed in id order."
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,11 @@ def test_absolute_yandex_realty_allowed() -> None:
|
||||||
_assert_allowed_url("https://realty.yandex.ru/ekaterinburg/kupit/kvartira/123/")
|
_assert_allowed_url("https://realty.yandex.ru/ekaterinburg/kupit/kvartira/123/")
|
||||||
|
|
||||||
|
|
||||||
def test_absolute_n1_allowed() -> None:
|
def test_absolute_n1_blocked() -> None:
|
||||||
_assert_allowed_url("https://ekaterinburg.n1.ru/kupit/kvartiry/vtorichka/")
|
"""n1 удалён из allowlist (#2204, источник мёртв) → абсолютный n1-URL блокируется."""
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
_assert_allowed_url("https://ekaterinburg.n1.ru/kupit/kvartiry/vtorichka/")
|
||||||
|
assert exc_info.value.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -100,7 +103,13 @@ def test_allowlist_contains_required_scraper_hosts() -> None:
|
||||||
"ekb.cian.ru",
|
"ekb.cian.ru",
|
||||||
"www.cian.ru",
|
"www.cian.ru",
|
||||||
"realty.yandex.ru",
|
"realty.yandex.ru",
|
||||||
"ekaterinburg.n1.ru",
|
|
||||||
}
|
}
|
||||||
missing = required - settings.scrape_allowed_hosts
|
missing = required - settings.scrape_allowed_hosts
|
||||||
assert not missing, f"Missing hosts in scrape_allowed_hosts: {missing}"
|
assert not missing, f"Missing hosts in scrape_allowed_hosts: {missing}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_allowlist_excludes_removed_n1_hosts() -> None:
|
||||||
|
"""n1 полностью удалён из allowlist (#2204) — ни один n1-хост не должен остаться."""
|
||||||
|
n1_hosts = {"n1.ru", "ekaterinburg.n1.ru"}
|
||||||
|
leftover = n1_hosts & settings.scrape_allowed_hosts
|
||||||
|
assert not leftover, f"n1 hosts still in scrape_allowed_hosts: {leftover}"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""Offline unit tests for run_cian_city_sweep (#860).
|
"""Offline unit tests for run_cian_city_sweep (#860).
|
||||||
|
|
||||||
Pure & fast: NO live network, NO DB. Mirrors test_n1_city_sweep / test_city_sweep.
|
Pure & fast: NO live network, NO DB. Mirrors test_city_sweep.
|
||||||
Asserts orchestration semantics only.
|
Asserts orchestration semantics only.
|
||||||
|
|
||||||
Coverage:
|
Coverage:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
"""Static guards for migration 165 (issue #2204 — полное выключение источника n1).
|
||||||
|
|
||||||
|
Прод применяет data/sql построчно строго (ON_ERROR_STOP). Полный DB-прогон
|
||||||
|
требует живой БД; здесь фиксируем структурные инварианты, которые ГАРАНТИРУЮТ
|
||||||
|
идемпотентность и НЕдеструктивность к самим listings-строкам по построению:
|
||||||
|
|
||||||
|
1. Транзакционность (BEGIN/COMMIT) — атомарный апдейт.
|
||||||
|
2. НЕдеструктивность к данным: строки listings source='n1' НЕ удаляются, только
|
||||||
|
деактивируются (is_active=false) — исторические объявления остаются в БД.
|
||||||
|
Никакого `DELETE FROM listings` в коде.
|
||||||
|
3. Идемпотентность по построению:
|
||||||
|
- страховочный UPDATE ограничен `AND is_active` -> повторный прогон no-op;
|
||||||
|
- `DELETE ... WHERE source IN (...)` / `WHERE source = 'n1'` -> повторный
|
||||||
|
прогон удаляет 0 строк.
|
||||||
|
4. Скоуп чистки конфига: обе n1-строки расписаний (n1_city_sweep +
|
||||||
|
deactivate_stale_n1) и per-source knob scraper_settings 'n1'.
|
||||||
|
|
||||||
|
Комментарий-заголовок в прозе объясняет намерение (в т.ч. фразу «Никакого
|
||||||
|
DELETE FROM listings»), поэтому destructive-проверки смотрят на ИСПОЛНЯЕМЫЙ SQL
|
||||||
|
(`--`-комменты вырезаны), а не на прозу.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_SQL_DIR = Path(__file__).resolve().parents[1] / "data" / "sql"
|
||||||
|
_MIGRATION_165 = _SQL_DIR / "165_remove_n1_source.sql"
|
||||||
|
|
||||||
|
|
||||||
|
def _sql() -> str:
|
||||||
|
return _MIGRATION_165.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _executable_sql() -> str:
|
||||||
|
"""SQL без комментариев — только исполняемые строки (`--`-комменты вырезаны).
|
||||||
|
|
||||||
|
В этой миграции `--` не встречается внутри строковых литералов, поэтому
|
||||||
|
split по первому `--` безопасно отделяет код от построчного комментария.
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
for raw in _sql().splitlines():
|
||||||
|
code = raw.split("--", 1)[0]
|
||||||
|
if code.strip():
|
||||||
|
lines.append(code)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _flat(text: str) -> str:
|
||||||
|
return re.sub(r"\s+", " ", text).strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_165_exists() -> None:
|
||||||
|
assert _MIGRATION_165.exists(), f"missing migration: {_MIGRATION_165}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_165_is_transactional() -> None:
|
||||||
|
sql = _sql()
|
||||||
|
assert "BEGIN;" in sql
|
||||||
|
assert "COMMIT;" in sql
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_165_does_not_delete_listings() -> None:
|
||||||
|
"""Строки listings source='n1' НЕ удаляются — только деактивируются."""
|
||||||
|
flat = _flat(_executable_sql())
|
||||||
|
assert "delete from listings" not in flat
|
||||||
|
# Деактивация вместо удаления.
|
||||||
|
assert "update listings" in flat
|
||||||
|
assert "set is_active = false" in flat
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_165_listings_update_idempotent_and_scoped() -> None:
|
||||||
|
"""UPDATE ограничен source='n1' AND is_active -> повторный прогон no-op."""
|
||||||
|
flat = _flat(_executable_sql())
|
||||||
|
assert "where source = 'n1'" in flat
|
||||||
|
assert "and is_active" in flat
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_165_deletes_both_n1_schedules() -> None:
|
||||||
|
flat = _flat(_executable_sql())
|
||||||
|
assert "delete from scrape_schedules" in flat
|
||||||
|
assert "'n1_city_sweep'" in flat
|
||||||
|
assert "'deactivate_stale_n1'" in flat
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_165_deletes_n1_scraper_settings() -> None:
|
||||||
|
flat = _flat(_executable_sql())
|
||||||
|
assert "delete from scraper_settings" in flat
|
||||||
|
assert "where source = 'n1'" in flat
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_165_no_psycopg_trap() -> None:
|
||||||
|
"""Никаких :param::type — psycopg v3 требует CAST(... AS type)."""
|
||||||
|
assert not re.search(r":\w+::", _sql())
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
# Запрос к backend идёт через `docker exec tradein-backend curl localhost:8000`
|
# Запрос к backend идёт через `docker exec tradein-backend curl localhost:8000`
|
||||||
# — не зависит от Caddy и не выставляет admin/scrape наружу.
|
# — не зависит от Caddy и не выставляет admin/scrape наружу.
|
||||||
#
|
#
|
||||||
# Usage: ./cron-scrape.sh [all|avito|cian|yandex|n1] [deep|fast]
|
# Usage: ./cron-scrape.sh [all|avito|cian|yandex] [deep|fast]
|
||||||
#
|
#
|
||||||
# Crontab пример:
|
# Crontab пример:
|
||||||
# 0 */3 * * * /opt/gendesign/tradein-mvp/deploy/cron-scrape.sh all deep
|
# 0 */3 * * * /opt/gendesign/tradein-mvp/deploy/cron-scrape.sh all deep
|
||||||
|
|
@ -48,13 +48,12 @@ build_payload() {
|
||||||
yandex) sources_json='["yandex"]' ;;
|
yandex) sources_json='["yandex"]' ;;
|
||||||
avito) sources_json='["avito"]' ;;
|
avito) sources_json='["avito"]' ;;
|
||||||
cian) sources_json='["cian"]' ;;
|
cian) sources_json='["cian"]' ;;
|
||||||
n1) sources_json='["n1"]' ;;
|
all|*) sources_json='["avito","cian","yandex"]' ;;
|
||||||
all|*) sources_json='["avito","cian","yandex","n1"]' ;;
|
|
||||||
esac
|
esac
|
||||||
if [[ "$mode" == "deep" ]]; then
|
if [[ "$mode" == "deep" ]]; then
|
||||||
echo "{\"lat\":$lat,\"lon\":$lon,\"radius_m\":1500,\"sources\":$sources_json,\"multi_room_yandex\":true,\"deep_yandex\":true,\"multi_room_cian\":true,\"multi_room_n1\":true}"
|
echo "{\"lat\":$lat,\"lon\":$lon,\"radius_m\":1500,\"sources\":$sources_json,\"multi_room_yandex\":true,\"deep_yandex\":true,\"multi_room_cian\":true}"
|
||||||
else
|
else
|
||||||
echo "{\"lat\":$lat,\"lon\":$lon,\"radius_m\":1500,\"sources\":$sources_json,\"multi_room_yandex\":true,\"multi_room_cian\":true,\"multi_room_n1\":true}"
|
echo "{\"lat\":$lat,\"lon\":$lon,\"radius_m\":1500,\"sources\":$sources_json,\"multi_room_yandex\":true,\"multi_room_cian\":true}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,7 +73,7 @@ for anchor in "${ANCHORS[@]}"; do
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
|
|
||||||
# Геокодинг свежих лотов без координат (Cian/N1 отдают только адрес).
|
# Геокодинг свежих лотов без координат (Cian отдаёт только адрес).
|
||||||
# Чанками по 100 (~200с/чанк, под таймаутом), пока remaining > 0.
|
# Чанками по 100 (~200с/чанк, под таймаутом), пока remaining > 0.
|
||||||
# Макс 15 чанков = 1500 адресов. geocode_tried_at не даёт зациклиться.
|
# Макс 15 чанков = 1500 адресов. geocode_tried_at не даёт зациклиться.
|
||||||
log "→ geocode-missing listings (чанками)"
|
log "→ geocode-missing listings (чанками)"
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ log = logging.getLogger(__name__)
|
||||||
_SOURCE_TO_PATH: dict[str, str] = {
|
_SOURCE_TO_PATH: dict[str, str] = {
|
||||||
"avito_city_sweep": "/api/v1/admin/scrape/avito-city-sweep",
|
"avito_city_sweep": "/api/v1/admin/scrape/avito-city-sweep",
|
||||||
"yandex_city_sweep": "/api/v1/admin/scrape/yandex-city-sweep",
|
"yandex_city_sweep": "/api/v1/admin/scrape/yandex-city-sweep",
|
||||||
"n1_city_sweep": "/api/v1/admin/scrape/n1",
|
|
||||||
"cian_history_backfill": "/api/v1/admin/scrape/cian-backfill-history",
|
"cian_history_backfill": "/api/v1/admin/scrape/cian-backfill-history",
|
||||||
"yandex_address_backfill": "/api/v1/admin/scrape/yandex-address-backfill",
|
"yandex_address_backfill": "/api/v1/admin/scrape/yandex-address-backfill",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue