mirror of https://github.com/scrapy/scrapy.git
157 lines
5.7 KiB
Python
157 lines
5.7 KiB
Python
"""
|
|
Downloader Middleware manager
|
|
|
|
See documentation in docs/topics/downloader-middleware.rst
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import warnings
|
|
from functools import wraps
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from scrapy.exceptions import ScrapyDeprecationWarning, _InvalidOutput
|
|
from scrapy.http import Request, Response
|
|
from scrapy.middleware import MiddlewareManager
|
|
from scrapy.utils.conf import build_component_list
|
|
from scrapy.utils.defer import (
|
|
_defer_sleep_async,
|
|
deferred_from_coro,
|
|
ensure_awaitable,
|
|
maybe_deferred_to_future,
|
|
)
|
|
from scrapy.utils.python import global_object_name
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable, Coroutine
|
|
|
|
from twisted.internet.defer import Deferred
|
|
|
|
from scrapy import Spider
|
|
from scrapy.settings import BaseSettings
|
|
|
|
|
|
class DownloaderMiddlewareManager(MiddlewareManager):
|
|
component_name = "downloader middleware"
|
|
|
|
@classmethod
|
|
def _get_mwlist_from_settings(cls, settings: BaseSettings) -> list[Any]:
|
|
return build_component_list(
|
|
settings.get_component_priority_dict_with_base("DOWNLOADER_MIDDLEWARES")
|
|
)
|
|
|
|
def _add_middleware(self, mw: Any) -> None:
|
|
if hasattr(mw, "process_request"):
|
|
self.methods["process_request"].append(mw.process_request)
|
|
self._check_mw_method_spider_arg(mw.process_request)
|
|
if hasattr(mw, "process_response"):
|
|
self.methods["process_response"].appendleft(mw.process_response)
|
|
self._check_mw_method_spider_arg(mw.process_response)
|
|
if hasattr(mw, "process_exception"):
|
|
self.methods["process_exception"].appendleft(mw.process_exception)
|
|
self._check_mw_method_spider_arg(mw.process_exception)
|
|
|
|
def download(
|
|
self,
|
|
download_func: Callable[[Request, Spider], Deferred[Response]],
|
|
request: Request,
|
|
spider: Spider,
|
|
) -> Deferred[Response | Request]:
|
|
warnings.warn(
|
|
"DownloaderMiddlewareManager.download() is deprecated, use download_async() instead",
|
|
ScrapyDeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
@wraps(download_func)
|
|
async def download_func_wrapped(request: Request) -> Response:
|
|
return await maybe_deferred_to_future(download_func(request, spider))
|
|
|
|
self._set_compat_spider(spider)
|
|
return deferred_from_coro(self.download_async(download_func_wrapped, request))
|
|
|
|
async def download_async(
|
|
self,
|
|
download_func: Callable[[Request], Coroutine[Any, Any, Response]],
|
|
request: Request,
|
|
) -> Response | Request:
|
|
|
|
try:
|
|
result: Response | Request = await self._process_request(
|
|
request, download_func
|
|
)
|
|
except Exception as ex:
|
|
await _defer_sleep_async()
|
|
# either returns a request or response (which we pass to process_response())
|
|
# or reraises the exception
|
|
result = await self._process_exception(ex, request)
|
|
return await self._process_response(result, request)
|
|
|
|
def _handle_mw_method(self, method: Callable[..., Any], **kwargs: Any) -> Any:
|
|
if method in self._mw_methods_requiring_spider:
|
|
kwargs["spider"] = self._spider
|
|
|
|
return method(**kwargs)
|
|
|
|
async def _process_request(
|
|
self,
|
|
request: Request,
|
|
download_func: Callable[[Request], Coroutine[Any, Any, Response]],
|
|
) -> Response | Request:
|
|
for method in self.methods["process_request"]:
|
|
assert method is not None
|
|
response = await ensure_awaitable(
|
|
self._handle_mw_method(method, request=request),
|
|
_warn=global_object_name(method),
|
|
)
|
|
if response is not None and not isinstance(response, (Response, Request)):
|
|
raise _InvalidOutput(
|
|
f"Middleware {method.__qualname__} must return None, Response or "
|
|
f"Request, got {response.__class__.__name__}"
|
|
)
|
|
if response:
|
|
return response
|
|
return await download_func(request)
|
|
|
|
async def _process_response(
|
|
self, response: Response | Request, request: Request
|
|
) -> Response | Request:
|
|
if response is None:
|
|
raise TypeError("Received None in process_response")
|
|
if isinstance(response, Request):
|
|
return response
|
|
|
|
for method in self.methods["process_response"]:
|
|
assert method is not None
|
|
response = await ensure_awaitable(
|
|
self._handle_mw_method(method, request=request, response=response),
|
|
_warn=global_object_name(method),
|
|
)
|
|
|
|
if not isinstance(response, (Response, Request)):
|
|
raise _InvalidOutput(
|
|
f"Middleware {method.__qualname__} must return Response or Request, "
|
|
f"got {type(response)}"
|
|
)
|
|
if isinstance(response, Request):
|
|
return response
|
|
return response
|
|
|
|
async def _process_exception(
|
|
self, exception: Exception, request: Request | Response
|
|
) -> Response | Request:
|
|
for method in self.methods["process_exception"]:
|
|
assert method is not None
|
|
response = await ensure_awaitable(
|
|
self._handle_mw_method(method, request=request, exception=exception),
|
|
_warn=global_object_name(method),
|
|
)
|
|
if response is not None and not isinstance(response, (Response, Request)):
|
|
raise _InvalidOutput(
|
|
f"Middleware {method.__qualname__} must return None, Response or "
|
|
f"Request, got {type(response)}"
|
|
)
|
|
if response:
|
|
return response
|
|
raise exception
|