"""Проверка, что 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, ProxyProvider, ScraperConfig, SessionFactory, ) from app.services.scraper_adapters import ( RealEnrichmentJobs, RealMatcherAdapter, RealProxyProvider, 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 _check_proxy_provider(p: ProxyProvider) -> None: """Статическая проверка: RealProxyProvider ⊑ ProxyProvider.""" 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) # #2163: новый флаг proxy-пула присутствует и bool. assert isinstance(config.use_proxy_pool_curl, bool) # #2164 P4: флаг browser-пула присутствует и bool. assert isinstance(config.use_proxy_pool_browser, bool) def test_proxy_provider_satisfies_protocol() -> None: provider = RealProxyProvider() _check_proxy_provider(provider) assert isinstance(provider, ProxyProvider) assert callable(provider.acquire) assert callable(provider.release) assert callable(provider.mark_health) 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