204 lines
6.4 KiB
Python
204 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from crawlee import Request
|
|
from crawlee._types import BasicCrawlingContext
|
|
from crawlee.crawlers import ContextPipeline
|
|
from crawlee.errors import ContextPipelineFinalizationError, ContextPipelineInitializationError, RequestHandlerError
|
|
from crawlee.sessions._session import Session
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import AsyncGenerator
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class EnhancedCrawlingContext(BasicCrawlingContext):
|
|
foo: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MoreEnhancedCrawlingContext(EnhancedCrawlingContext):
|
|
bar: int
|
|
|
|
|
|
async def test_calls_consumer_without_middleware() -> None:
|
|
consumer = AsyncMock()
|
|
|
|
pipeline = ContextPipeline()
|
|
context = BasicCrawlingContext(
|
|
request=Request.from_url(url='https://test.io/'),
|
|
send_request=AsyncMock(),
|
|
add_requests=AsyncMock(),
|
|
session=Session(),
|
|
proxy_info=AsyncMock(),
|
|
push_data=AsyncMock(),
|
|
use_state=AsyncMock(),
|
|
get_key_value_store=AsyncMock(),
|
|
log=logging.getLogger(),
|
|
register_deferred_cleanup=lambda _: None,
|
|
)
|
|
|
|
await pipeline(context, consumer)
|
|
|
|
consumer.assert_called_once_with(context)
|
|
|
|
|
|
async def test_calls_consumers_and_middlewares() -> None:
|
|
events = list[str]()
|
|
|
|
async def consumer(context: MoreEnhancedCrawlingContext) -> None:
|
|
events.append('consumer_called')
|
|
assert context.bar == 4
|
|
|
|
async def middleware_a(context: BasicCrawlingContext) -> AsyncGenerator[EnhancedCrawlingContext, None]:
|
|
events.append('middleware_a_in')
|
|
yield EnhancedCrawlingContext(
|
|
request=context.request,
|
|
foo='foo',
|
|
send_request=AsyncMock(),
|
|
add_requests=AsyncMock(),
|
|
session=context.session,
|
|
proxy_info=AsyncMock(),
|
|
push_data=AsyncMock(),
|
|
use_state=AsyncMock(),
|
|
get_key_value_store=AsyncMock(),
|
|
log=logging.getLogger(),
|
|
register_deferred_cleanup=context.register_deferred_cleanup,
|
|
)
|
|
events.append('middleware_a_out')
|
|
|
|
async def middleware_b(context: EnhancedCrawlingContext) -> AsyncGenerator[MoreEnhancedCrawlingContext, None]:
|
|
events.append('middleware_b_in')
|
|
yield MoreEnhancedCrawlingContext(
|
|
request=context.request,
|
|
foo=context.foo,
|
|
bar=4,
|
|
send_request=AsyncMock(),
|
|
add_requests=AsyncMock(),
|
|
session=context.session,
|
|
proxy_info=AsyncMock(),
|
|
push_data=AsyncMock(),
|
|
use_state=AsyncMock(),
|
|
get_key_value_store=AsyncMock(),
|
|
log=logging.getLogger(),
|
|
register_deferred_cleanup=context.register_deferred_cleanup,
|
|
)
|
|
events.append('middleware_b_out')
|
|
|
|
pipeline = ContextPipeline[BasicCrawlingContext]().compose(middleware_a).compose(middleware_b)
|
|
|
|
context = BasicCrawlingContext(
|
|
request=Request.from_url(url='https://test.io/'),
|
|
send_request=AsyncMock(),
|
|
add_requests=AsyncMock(),
|
|
session=Session(),
|
|
proxy_info=AsyncMock(),
|
|
push_data=AsyncMock(),
|
|
use_state=AsyncMock(),
|
|
get_key_value_store=AsyncMock(),
|
|
log=logging.getLogger(),
|
|
register_deferred_cleanup=lambda _: None,
|
|
)
|
|
await pipeline(context, consumer)
|
|
|
|
assert events == [
|
|
'middleware_a_in',
|
|
'middleware_b_in',
|
|
'consumer_called',
|
|
'middleware_b_out',
|
|
'middleware_a_out',
|
|
]
|
|
|
|
|
|
async def test_wraps_consumer_errors() -> None:
|
|
consumer = AsyncMock(side_effect=RuntimeError('Arbitrary crash for testing purposes'))
|
|
|
|
pipeline = ContextPipeline()
|
|
context = BasicCrawlingContext(
|
|
request=Request.from_url(url='https://test.io/'),
|
|
send_request=AsyncMock(),
|
|
add_requests=AsyncMock(),
|
|
session=Session(),
|
|
proxy_info=AsyncMock(),
|
|
push_data=AsyncMock(),
|
|
use_state=AsyncMock(),
|
|
get_key_value_store=AsyncMock(),
|
|
log=logging.getLogger(),
|
|
register_deferred_cleanup=lambda _: None,
|
|
)
|
|
|
|
with pytest.raises(RequestHandlerError):
|
|
await pipeline(context, consumer)
|
|
|
|
|
|
async def test_handles_exceptions_in_middleware_initialization() -> None:
|
|
consumer = AsyncMock()
|
|
cleanup = AsyncMock()
|
|
|
|
async def step_1(context: BasicCrawlingContext) -> AsyncGenerator[BasicCrawlingContext, None]:
|
|
yield context
|
|
await cleanup()
|
|
|
|
async def step_2(context: BasicCrawlingContext) -> AsyncGenerator[BasicCrawlingContext, None]:
|
|
raise RuntimeError('Crash during middleware initialization')
|
|
yield context
|
|
|
|
pipeline = ContextPipeline().compose(step_1).compose(step_2)
|
|
context = BasicCrawlingContext(
|
|
request=Request.from_url(url='https://test.io/'),
|
|
send_request=AsyncMock(),
|
|
add_requests=AsyncMock(),
|
|
session=Session(),
|
|
proxy_info=AsyncMock(),
|
|
push_data=AsyncMock(),
|
|
use_state=AsyncMock(),
|
|
get_key_value_store=AsyncMock(),
|
|
log=logging.getLogger(),
|
|
register_deferred_cleanup=lambda _: None,
|
|
)
|
|
|
|
with pytest.raises(ContextPipelineInitializationError):
|
|
await pipeline(context, consumer)
|
|
|
|
assert not consumer.called
|
|
assert cleanup.called
|
|
|
|
|
|
async def test_handles_exceptions_in_middleware_finalization() -> None:
|
|
consumer = AsyncMock()
|
|
cleanup = AsyncMock()
|
|
|
|
async def step_1(context: BasicCrawlingContext) -> AsyncGenerator[BasicCrawlingContext, None]:
|
|
yield context
|
|
await cleanup()
|
|
|
|
async def step_2(context: BasicCrawlingContext) -> AsyncGenerator[BasicCrawlingContext, None]:
|
|
yield context
|
|
raise RuntimeError('Crash during middleware finalization')
|
|
|
|
pipeline = ContextPipeline().compose(step_1).compose(step_2)
|
|
context = BasicCrawlingContext(
|
|
request=Request.from_url(url='https://test.io/'),
|
|
send_request=AsyncMock(),
|
|
add_requests=AsyncMock(),
|
|
session=Session(),
|
|
proxy_info=AsyncMock(),
|
|
push_data=AsyncMock(),
|
|
use_state=AsyncMock(),
|
|
get_key_value_store=AsyncMock(),
|
|
log=logging.getLogger(),
|
|
register_deferred_cleanup=lambda _: None,
|
|
)
|
|
|
|
with pytest.raises(ContextPipelineFinalizationError):
|
|
await pipeline(context, consumer)
|
|
|
|
assert consumer.called
|
|
assert not cleanup.called
|