gendesign/tradein-mvp/backend/app/services/scraper_adapters.py
bot-backend 344292f87b
All checks were successful
CI / changes (pull_request) Successful in 20s
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
fix(scraper-kit): sync RealScraperConfig adapter with ScraperConfig protocol (cookie_encryption_key)
2026-07-02 19:45:23 +03:00

215 lines
6.8 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.

"""Продуктовые адаптеры над `app.services.*`, удовлетворяющие scraper_kit.contracts.
Тонкие врапперы, инжектируемые в скрапперы (после переноса в scraper_kit, шаги C-G
#2131) вместо прямых импортов `app.*`. Каждый класс структурно удовлетворяет
соответствующему Protocol'у из `scraper_kit.contracts`:
RealMatcherAdapter → HouseMatcher
RealScraperConfig → ScraperConfig
RealSessionFactory → SessionFactory
RealEnrichmentJobs → EnrichmentJobs
Пока НИКТО их не использует — скрапперы всё ещё на прямых импортах. Это подготовка:
переключение вызовов на инжекцию — отдельный шаг (C). Держим сигнатуры в точном
соответствии с исходными функциями, чтобы шаг C состыковался без правок контрактов.
"""
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from app.core.config import settings as _settings
from app.core.db import SessionLocal as _SessionLocal
from app.services.house_dedup_merge import run_house_dedup_merge as _run_house_dedup_merge
from app.services.house_imv_backfill import backfill_house_imv as _backfill_house_imv
from app.services.matching import match_or_create_house as _match_or_create_house
from app.services.matching import upsert_listing_source as _upsert_listing_source
from app.services.yandex_address_backfill import (
backfill_yandex_addresses as _backfill_yandex_addresses,
)
if TYPE_CHECKING:
from sqlalchemy.orm import Session
from app.services.house_imv_backfill import HouseIMVBackfillResult
from app.services.yandex_address_backfill import YandexAddressBackfillResult
class RealMatcherAdapter:
"""HouseMatcher-адаптер над `app.services.matching`."""
def match_or_create_house(
self,
db: Session,
ext_source: str,
ext_id: str,
address: str | None = None,
lat: float | None = None,
lon: float | None = None,
*,
year_built: int | None = None,
building_cadastral_number: str | None = None,
cadastral_number: str | None = None,
source_url: str | None = None,
) -> tuple[int, float, str]:
return _match_or_create_house(
db,
ext_source,
ext_id,
address,
lat,
lon,
year_built=year_built,
building_cadastral_number=building_cadastral_number,
cadastral_number=cadastral_number,
source_url=source_url,
)
def upsert_listing_source(
self,
db: Session,
*,
listing_id: int,
ext_source: str,
ext_id: str,
method: str,
confidence: float,
price_rub: float | None = None,
area_m2: float | None = None,
floor: int | None = None,
rooms_count: int | None = None,
source_url: str | None = None,
source_data: dict[str, Any] | None = None,
) -> None:
_upsert_listing_source(
db,
listing_id=listing_id,
ext_source=ext_source,
ext_id=ext_id,
method=method,
confidence=confidence,
price_rub=price_rub,
area_m2=area_m2,
floor=floor,
rooms_count=rooms_count,
source_url=source_url,
source_data=source_data,
)
class RealScraperConfig:
"""ScraperConfig-адаптер: read-only проекция singleton'а `settings`.
Свойства проксируют в `app.core.config.settings`, включая computed-properties
(`scraper_proxy_url`, `cian_proxy_url`), чтобы скрапперы читали ровно те же
значения, что и сейчас через прямой `settings.<name>`.
"""
@property
def scraper_fetch_mode(self) -> str:
return _settings.scraper_fetch_mode
@property
def browser_http_endpoint(self) -> str:
return _settings.browser_http_endpoint
@property
def scraper_proxy_url(self) -> str | None:
return _settings.scraper_proxy_url
@property
def avito_proxy_rotate_url(self) -> str | None:
return _settings.avito_proxy_rotate_url
@property
def avito_proxy_max_rotations(self) -> int:
return _settings.avito_proxy_max_rotations
@property
def avito_serp_ekb_only(self) -> bool:
return _settings.avito_serp_ekb_only
@property
def yandex_proxy_rotate_url(self) -> str | None:
return _settings.yandex_proxy_rotate_url
@property
def cian_proxy_url(self) -> str | None:
return _settings.cian_proxy_url
@property
def cian_valuation_min_rub(self) -> float:
return _settings.cian_valuation_min_rub
@property
def cian_valuation_max_rub(self) -> float:
return _settings.cian_valuation_max_rub
@property
def cookie_encryption_key(self) -> str:
return _settings.cookie_encryption_key
@property
def glitchtip_dsn(self) -> str | None:
return _settings.glitchtip_dsn
class RealSessionFactory:
"""SessionFactory-адаптер над `app.core.db.SessionLocal`."""
def __call__(self) -> Session:
return _SessionLocal()
class RealEnrichmentJobs:
"""EnrichmentJobs-адаптер над enrichment-сервисами `app.services.*`."""
async def house_imv_backfill(
self,
db: Session,
*,
batch_size: int = 50,
request_delay_sec: float = 5.0,
only_status: str = "pending",
house_id: int | None = None,
heartbeat: Callable[[], None] | None = None,
) -> HouseIMVBackfillResult:
return await _backfill_house_imv(
db,
batch_size=batch_size,
request_delay_sec=request_delay_sec,
only_status=only_status,
house_id=house_id,
heartbeat=heartbeat,
)
def house_dedup_merge(
self,
db: Session,
*,
run_id: int,
params: dict[str, Any],
) -> dict[str, int]:
return _run_house_dedup_merge(db, run_id=run_id, params=params)
async def yandex_address_backfill(
self,
db: Session,
*,
limit: int = 200,
request_delay_sec: float = 3.0,
) -> YandexAddressBackfillResult:
return await _backfill_yandex_addresses(
db,
limit=limit,
request_delay_sec=request_delay_sec,
)
__all__ = [
"RealEnrichmentJobs",
"RealMatcherAdapter",
"RealScraperConfig",
"RealSessionFactory",
]