88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
"""Проверка, что backend-адаптеры структурно удовлетворяют scraper_kit.contracts.
|
||
|
||
Двойная проверка:
|
||
1. Статическая — `_check_*` функции с аннотацией Protocol-типа. mypy проверит,
|
||
что каждый адаптер присваивается соответствующему Protocol'у (структурно).
|
||
2. Рантайм — `isinstance` против `@runtime_checkable` Protocol'ов (проверяет
|
||
наличие методов/атрибутов). Ловит рассинхрон имён без запуска mypy.
|
||
|
||
Границы контрактов зафиксированы для #2131 (шаг B); переключение скрапперов на
|
||
инжекцию — шаг C.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from scraper_kit.contracts import (
|
||
EnrichmentJobs,
|
||
HouseMatcher,
|
||
ScraperConfig,
|
||
SessionFactory,
|
||
)
|
||
|
||
from app.services.scraper_adapters import (
|
||
RealEnrichmentJobs,
|
||
RealMatcherAdapter,
|
||
RealScraperConfig,
|
||
RealSessionFactory,
|
||
)
|
||
|
||
|
||
def _check_matcher(m: HouseMatcher) -> None:
|
||
"""Статическая проверка: RealMatcherAdapter ⊑ HouseMatcher."""
|
||
|
||
|
||
def _check_config(c: ScraperConfig) -> None:
|
||
"""Статическая проверка: RealScraperConfig ⊑ ScraperConfig."""
|
||
|
||
|
||
def _check_session_factory(f: SessionFactory) -> None:
|
||
"""Статическая проверка: RealSessionFactory ⊑ SessionFactory."""
|
||
|
||
|
||
def _check_enrichment(j: EnrichmentJobs) -> None:
|
||
"""Статическая проверка: RealEnrichmentJobs ⊑ EnrichmentJobs."""
|
||
|
||
|
||
def test_matcher_adapter_satisfies_protocol() -> None:
|
||
adapter = RealMatcherAdapter()
|
||
_check_matcher(adapter)
|
||
assert isinstance(adapter, HouseMatcher)
|
||
assert callable(adapter.match_or_create_house)
|
||
assert callable(adapter.upsert_listing_source)
|
||
|
||
|
||
def test_scraper_config_satisfies_protocol() -> None:
|
||
config = RealScraperConfig()
|
||
_check_config(config)
|
||
assert isinstance(config, ScraperConfig)
|
||
# Ключевые поля читаются без ошибок (проксируют в singleton settings).
|
||
assert isinstance(config.scraper_fetch_mode, str)
|
||
assert isinstance(config.browser_http_endpoint, str)
|
||
assert isinstance(config.avito_proxy_max_rotations, int)
|
||
assert isinstance(config.avito_serp_ekb_only, bool)
|
||
|
||
|
||
def test_session_factory_satisfies_protocol() -> None:
|
||
factory = RealSessionFactory()
|
||
_check_session_factory(factory)
|
||
assert isinstance(factory, SessionFactory)
|
||
|
||
|
||
def test_enrichment_jobs_satisfies_protocol() -> None:
|
||
jobs = RealEnrichmentJobs()
|
||
_check_enrichment(jobs)
|
||
assert isinstance(jobs, EnrichmentJobs)
|
||
assert callable(jobs.house_imv_backfill)
|
||
assert callable(jobs.house_dedup_merge)
|
||
assert callable(jobs.yandex_address_backfill)
|
||
|
||
|
||
def test_contracts_module_has_no_app_imports() -> None:
|
||
"""contracts.py не должен импортировать app.* (иначе circular / coupling)."""
|
||
import inspect
|
||
|
||
import scraper_kit.contracts as contracts_mod
|
||
|
||
source = inspect.getsource(contracts_mod)
|
||
assert "from app" not in source
|
||
assert "import app" not in source
|