mirror of https://github.com/scrapy/scrapy.git
Full typing for scrapy/extensions, part 2. (#6279)
This commit is contained in:
parent
6e84648c07
commit
642af40704
|
|
@ -172,8 +172,8 @@ TELNETCONSOLE_PORT
|
||||||
|
|
||||||
Default: ``[6023, 6073]``
|
Default: ``[6023, 6073]``
|
||||||
|
|
||||||
The port range to use for the telnet console. If set to ``None`` or ``0``, a
|
The port range to use for the telnet console. If set to ``None``, a dynamically
|
||||||
dynamically assigned port is used.
|
assigned port is used.
|
||||||
|
|
||||||
|
|
||||||
.. setting:: TELNETCONSOLE_HOST
|
.. setting:: TELNETCONSOLE_HOST
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from json import JSONEncoder
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from twisted.internet import task
|
from twisted.internet import task
|
||||||
|
|
||||||
from scrapy import signals
|
from scrapy import Spider, signals
|
||||||
|
from scrapy.crawler import Crawler
|
||||||
from scrapy.exceptions import NotConfigured
|
from scrapy.exceptions import NotConfigured
|
||||||
|
from scrapy.statscollectors import StatsCollector
|
||||||
from scrapy.utils.serialize import ScrapyJSONEncoder
|
from scrapy.utils.serialize import ScrapyJSONEncoder
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# typing.Self requires Python 3.11
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,32 +25,34 @@ class PeriodicLog:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
stats,
|
stats: StatsCollector,
|
||||||
interval=60.0,
|
interval: float = 60.0,
|
||||||
ext_stats={},
|
ext_stats: Dict[str, Any] = {},
|
||||||
ext_delta={},
|
ext_delta: Dict[str, Any] = {},
|
||||||
ext_timing_enabled=False,
|
ext_timing_enabled: bool = False,
|
||||||
):
|
):
|
||||||
self.stats = stats
|
self.stats: StatsCollector = stats
|
||||||
self.interval = interval
|
self.interval: float = interval
|
||||||
self.multiplier = 60.0 / self.interval
|
self.multiplier: float = 60.0 / self.interval
|
||||||
self.task = None
|
self.task: Optional[task.LoopingCall] = None
|
||||||
self.encoder = ScrapyJSONEncoder(sort_keys=True, indent=4)
|
self.encoder: JSONEncoder = ScrapyJSONEncoder(sort_keys=True, indent=4)
|
||||||
self.ext_stats_enabled = bool(ext_stats)
|
self.ext_stats_enabled: bool = bool(ext_stats)
|
||||||
self.ext_stats_include = ext_stats.get("include", [])
|
self.ext_stats_include: List[str] = ext_stats.get("include", [])
|
||||||
self.ext_stats_exclude = ext_stats.get("exclude", [])
|
self.ext_stats_exclude: List[str] = ext_stats.get("exclude", [])
|
||||||
self.ext_delta_enabled = bool(ext_delta)
|
self.ext_delta_enabled: bool = bool(ext_delta)
|
||||||
self.ext_delta_include = ext_delta.get("include", [])
|
self.ext_delta_include: List[str] = ext_delta.get("include", [])
|
||||||
self.ext_delta_exclude = ext_delta.get("exclude", [])
|
self.ext_delta_exclude: List[str] = ext_delta.get("exclude", [])
|
||||||
self.ext_timing_enabled = ext_timing_enabled
|
self.ext_timing_enabled: bool = ext_timing_enabled
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler):
|
def from_crawler(cls, crawler: Crawler) -> Self:
|
||||||
interval = crawler.settings.getfloat("LOGSTATS_INTERVAL")
|
interval: float = crawler.settings.getfloat("LOGSTATS_INTERVAL")
|
||||||
if not interval:
|
if not interval:
|
||||||
raise NotConfigured
|
raise NotConfigured
|
||||||
try:
|
try:
|
||||||
ext_stats = crawler.settings.getdict("PERIODIC_LOG_STATS")
|
ext_stats: Optional[Dict[str, Any]] = crawler.settings.getdict(
|
||||||
|
"PERIODIC_LOG_STATS"
|
||||||
|
)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
ext_stats = (
|
ext_stats = (
|
||||||
{"enabled": True}
|
{"enabled": True}
|
||||||
|
|
@ -48,7 +60,9 @@ class PeriodicLog:
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
ext_delta = crawler.settings.getdict("PERIODIC_LOG_DELTA")
|
ext_delta: Optional[Dict[str, Any]] = crawler.settings.getdict(
|
||||||
|
"PERIODIC_LOG_DELTA"
|
||||||
|
)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
ext_delta = (
|
ext_delta = (
|
||||||
{"enabled": True}
|
{"enabled": True}
|
||||||
|
|
@ -56,11 +70,14 @@ class PeriodicLog:
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
ext_timing_enabled = crawler.settings.getbool(
|
ext_timing_enabled: bool = crawler.settings.getbool(
|
||||||
"PERIODIC_LOG_TIMING_ENABLED", False
|
"PERIODIC_LOG_TIMING_ENABLED", False
|
||||||
)
|
)
|
||||||
if not (ext_stats or ext_delta or ext_timing_enabled):
|
if not (ext_stats or ext_delta or ext_timing_enabled):
|
||||||
raise NotConfigured
|
raise NotConfigured
|
||||||
|
assert crawler.stats
|
||||||
|
assert ext_stats is not None
|
||||||
|
assert ext_delta is not None
|
||||||
o = cls(
|
o = cls(
|
||||||
crawler.stats,
|
crawler.stats,
|
||||||
interval,
|
interval,
|
||||||
|
|
@ -72,16 +89,16 @@ class PeriodicLog:
|
||||||
crawler.signals.connect(o.spider_closed, signal=signals.spider_closed)
|
crawler.signals.connect(o.spider_closed, signal=signals.spider_closed)
|
||||||
return o
|
return o
|
||||||
|
|
||||||
def spider_opened(self, spider):
|
def spider_opened(self, spider: Spider) -> None:
|
||||||
self.time_prev = datetime.now(tz=timezone.utc)
|
self.time_prev: datetime = datetime.now(tz=timezone.utc)
|
||||||
self.delta_prev = {}
|
self.delta_prev: Dict[str, Union[int, float]] = {}
|
||||||
self.stats_prev = {}
|
self.stats_prev: Dict[str, Union[int, float]] = {}
|
||||||
|
|
||||||
self.task = task.LoopingCall(self.log)
|
self.task = task.LoopingCall(self.log)
|
||||||
self.task.start(self.interval)
|
self.task.start(self.interval)
|
||||||
|
|
||||||
def log(self):
|
def log(self) -> None:
|
||||||
data = {}
|
data: Dict[str, Any] = {}
|
||||||
if self.ext_timing_enabled:
|
if self.ext_timing_enabled:
|
||||||
data.update(self.log_timing())
|
data.update(self.log_timing())
|
||||||
if self.ext_delta_enabled:
|
if self.ext_delta_enabled:
|
||||||
|
|
@ -90,8 +107,8 @@ class PeriodicLog:
|
||||||
data.update(self.log_crawler_stats())
|
data.update(self.log_crawler_stats())
|
||||||
logger.info(self.encoder.encode(data))
|
logger.info(self.encoder.encode(data))
|
||||||
|
|
||||||
def log_delta(self):
|
def log_delta(self) -> Dict[str, Any]:
|
||||||
num_stats = {
|
num_stats: Dict[str, Union[int, float]] = {
|
||||||
k: v
|
k: v
|
||||||
for k, v in self.stats._stats.items()
|
for k, v in self.stats._stats.items()
|
||||||
if isinstance(v, (int, float))
|
if isinstance(v, (int, float))
|
||||||
|
|
@ -101,7 +118,7 @@ class PeriodicLog:
|
||||||
self.delta_prev = num_stats
|
self.delta_prev = num_stats
|
||||||
return {"delta": delta}
|
return {"delta": delta}
|
||||||
|
|
||||||
def log_timing(self):
|
def log_timing(self) -> Dict[str, Any]:
|
||||||
now = datetime.now(tz=timezone.utc)
|
now = datetime.now(tz=timezone.utc)
|
||||||
time = {
|
time = {
|
||||||
"log_interval": self.interval,
|
"log_interval": self.interval,
|
||||||
|
|
@ -113,7 +130,7 @@ class PeriodicLog:
|
||||||
self.time_prev = now
|
self.time_prev = now
|
||||||
return {"time": time}
|
return {"time": time}
|
||||||
|
|
||||||
def log_crawler_stats(self):
|
def log_crawler_stats(self) -> Dict[str, Any]:
|
||||||
stats = {
|
stats = {
|
||||||
k: v
|
k: v
|
||||||
for k, v in self.stats._stats.items()
|
for k, v in self.stats._stats.items()
|
||||||
|
|
@ -121,7 +138,9 @@ class PeriodicLog:
|
||||||
}
|
}
|
||||||
return {"stats": stats}
|
return {"stats": stats}
|
||||||
|
|
||||||
def param_allowed(self, stat_name, include, exclude):
|
def param_allowed(
|
||||||
|
self, stat_name: str, include: List[str], exclude: List[str]
|
||||||
|
) -> bool:
|
||||||
if not include and not exclude:
|
if not include and not exclude:
|
||||||
return True
|
return True
|
||||||
for p in exclude:
|
for p in exclude:
|
||||||
|
|
@ -134,7 +153,7 @@ class PeriodicLog:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def spider_closed(self, spider, reason):
|
def spider_closed(self, spider: Spider, reason: str) -> None:
|
||||||
self.log()
|
self.log()
|
||||||
if self.task and self.task.running:
|
if self.task and self.task.running:
|
||||||
self.task.stop()
|
self.task.stop()
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from bz2 import BZ2File
|
||||||
from gzip import GzipFile
|
from gzip import GzipFile
|
||||||
from io import IOBase
|
from io import IOBase
|
||||||
from lzma import LZMAFile
|
from lzma import LZMAFile
|
||||||
from typing import Any, BinaryIO, Dict, List
|
from typing import Any, BinaryIO, Dict, List, cast
|
||||||
|
|
||||||
from scrapy.utils.misc import load_object
|
from scrapy.utils.misc import load_object
|
||||||
|
|
||||||
|
|
@ -142,7 +142,7 @@ class PostProcessingManager(IOBase):
|
||||||
:return: returns number of bytes written
|
:return: returns number of bytes written
|
||||||
:rtype: int
|
:rtype: int
|
||||||
"""
|
"""
|
||||||
return self.head_plugin.write(data)
|
return cast(int, self.head_plugin.write(data))
|
||||||
|
|
||||||
def tell(self) -> int:
|
def tell(self) -> int:
|
||||||
return self.file.tell()
|
return self.file.tell()
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,27 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pickle # nosec
|
import pickle # nosec
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from scrapy import signals
|
from scrapy import Spider, signals
|
||||||
|
from scrapy.crawler import Crawler
|
||||||
from scrapy.exceptions import NotConfigured
|
from scrapy.exceptions import NotConfigured
|
||||||
from scrapy.utils.job import job_dir
|
from scrapy.utils.job import job_dir
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# typing.Self requires Python 3.11
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
|
||||||
class SpiderState:
|
class SpiderState:
|
||||||
"""Store and load spider state during a scraping job"""
|
"""Store and load spider state during a scraping job"""
|
||||||
|
|
||||||
def __init__(self, jobdir=None):
|
def __init__(self, jobdir: Optional[str] = None):
|
||||||
self.jobdir = jobdir
|
self.jobdir: Optional[str] = jobdir
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler):
|
def from_crawler(cls, crawler: Crawler) -> Self:
|
||||||
jobdir = job_dir(crawler.settings)
|
jobdir = job_dir(crawler.settings)
|
||||||
if not jobdir:
|
if not jobdir:
|
||||||
raise NotConfigured
|
raise NotConfigured
|
||||||
|
|
@ -23,18 +31,20 @@ class SpiderState:
|
||||||
crawler.signals.connect(obj.spider_opened, signal=signals.spider_opened)
|
crawler.signals.connect(obj.spider_opened, signal=signals.spider_opened)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def spider_closed(self, spider):
|
def spider_closed(self, spider: Spider) -> None:
|
||||||
if self.jobdir:
|
if self.jobdir:
|
||||||
with Path(self.statefn).open("wb") as f:
|
with Path(self.statefn).open("wb") as f:
|
||||||
|
assert hasattr(spider, "state") # set in spider_opened
|
||||||
pickle.dump(spider.state, f, protocol=4)
|
pickle.dump(spider.state, f, protocol=4)
|
||||||
|
|
||||||
def spider_opened(self, spider):
|
def spider_opened(self, spider: Spider) -> None:
|
||||||
if self.jobdir and Path(self.statefn).exists():
|
if self.jobdir and Path(self.statefn).exists():
|
||||||
with Path(self.statefn).open("rb") as f:
|
with Path(self.statefn).open("rb") as f:
|
||||||
spider.state = pickle.load(f) # nosec
|
spider.state = pickle.load(f) # type: ignore[attr-defined] # nosec
|
||||||
else:
|
else:
|
||||||
spider.state = {}
|
spider.state = {} # type: ignore[attr-defined]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def statefn(self) -> str:
|
def statefn(self) -> str:
|
||||||
|
assert self.jobdir
|
||||||
return str(Path(self.jobdir, "spider.state"))
|
return str(Path(self.jobdir, "spider.state"))
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,41 @@ StatsMailer extension sends an email when a spider finishes scraping.
|
||||||
Use STATSMAILER_RCPTS setting to enable and give the recipient mail address
|
Use STATSMAILER_RCPTS setting to enable and give the recipient mail address
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from scrapy import signals
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
|
from twisted.internet.defer import Deferred
|
||||||
|
|
||||||
|
from scrapy import Spider, signals
|
||||||
|
from scrapy.crawler import Crawler
|
||||||
from scrapy.exceptions import NotConfigured
|
from scrapy.exceptions import NotConfigured
|
||||||
from scrapy.mail import MailSender
|
from scrapy.mail import MailSender
|
||||||
|
from scrapy.statscollectors import StatsCollector
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# typing.Self requires Python 3.11
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
|
||||||
class StatsMailer:
|
class StatsMailer:
|
||||||
def __init__(self, stats, recipients, mail):
|
def __init__(self, stats: StatsCollector, recipients: List[str], mail: MailSender):
|
||||||
self.stats = stats
|
self.stats: StatsCollector = stats
|
||||||
self.recipients = recipients
|
self.recipients: List[str] = recipients
|
||||||
self.mail = mail
|
self.mail: MailSender = mail
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler):
|
def from_crawler(cls, crawler: Crawler) -> Self:
|
||||||
recipients = crawler.settings.getlist("STATSMAILER_RCPTS")
|
recipients: List[str] = crawler.settings.getlist("STATSMAILER_RCPTS")
|
||||||
if not recipients:
|
if not recipients:
|
||||||
raise NotConfigured
|
raise NotConfigured
|
||||||
mail = MailSender.from_settings(crawler.settings)
|
mail: MailSender = MailSender.from_settings(crawler.settings)
|
||||||
|
assert crawler.stats
|
||||||
o = cls(crawler.stats, recipients, mail)
|
o = cls(crawler.stats, recipients, mail)
|
||||||
crawler.signals.connect(o.spider_closed, signal=signals.spider_closed)
|
crawler.signals.connect(o.spider_closed, signal=signals.spider_closed)
|
||||||
return o
|
return o
|
||||||
|
|
||||||
def spider_closed(self, spider):
|
def spider_closed(self, spider: Spider) -> Optional[Deferred]:
|
||||||
spider_stats = self.stats.get_stats(spider)
|
spider_stats = self.stats.get_stats(spider)
|
||||||
body = "Global stats\n\n"
|
body = "Global stats\n\n"
|
||||||
body += "\n".join(f"{k:<50} : {v}" for k, v in self.stats.get_stats().items())
|
body += "\n".join(f"{k:<50} : {v}" for k, v in self.stats.get_stats().items())
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,17 @@ Scrapy Telnet Console extension
|
||||||
See documentation in docs/topics/telnetconsole.rst
|
See documentation in docs/topics/telnetconsole.rst
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
import traceback
|
import traceback
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List
|
||||||
|
|
||||||
from twisted.internet import protocol
|
from twisted.internet import protocol
|
||||||
|
from twisted.internet.tcp import Port
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from twisted.conch import manhole, telnet
|
from twisted.conch import manhole, telnet
|
||||||
|
|
@ -22,12 +26,16 @@ except (ImportError, SyntaxError):
|
||||||
TWISTED_CONCH_AVAILABLE = False
|
TWISTED_CONCH_AVAILABLE = False
|
||||||
|
|
||||||
from scrapy import signals
|
from scrapy import signals
|
||||||
|
from scrapy.crawler import Crawler
|
||||||
from scrapy.exceptions import NotConfigured
|
from scrapy.exceptions import NotConfigured
|
||||||
from scrapy.utils.decorators import defers
|
from scrapy.utils.decorators import defers
|
||||||
from scrapy.utils.engine import print_engine_status
|
from scrapy.utils.engine import print_engine_status
|
||||||
from scrapy.utils.reactor import listen_tcp
|
from scrapy.utils.reactor import listen_tcp
|
||||||
from scrapy.utils.trackref import print_live_refs
|
from scrapy.utils.trackref import print_live_refs
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# typing.Self requires Python 3.11
|
||||||
|
from typing_extensions import Self
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# signal to update telnet variables
|
# signal to update telnet variables
|
||||||
|
|
@ -36,7 +44,7 @@ update_telnet_vars = object()
|
||||||
|
|
||||||
|
|
||||||
class TelnetConsole(protocol.ServerFactory):
|
class TelnetConsole(protocol.ServerFactory):
|
||||||
def __init__(self, crawler):
|
def __init__(self, crawler: Crawler):
|
||||||
if not crawler.settings.getbool("TELNETCONSOLE_ENABLED"):
|
if not crawler.settings.getbool("TELNETCONSOLE_ENABLED"):
|
||||||
raise NotConfigured
|
raise NotConfigured
|
||||||
if not TWISTED_CONCH_AVAILABLE:
|
if not TWISTED_CONCH_AVAILABLE:
|
||||||
|
|
@ -44,14 +52,14 @@ class TelnetConsole(protocol.ServerFactory):
|
||||||
"TELNETCONSOLE_ENABLED setting is True but required twisted "
|
"TELNETCONSOLE_ENABLED setting is True but required twisted "
|
||||||
"modules failed to import:\n" + _TWISTED_CONCH_TRACEBACK
|
"modules failed to import:\n" + _TWISTED_CONCH_TRACEBACK
|
||||||
)
|
)
|
||||||
self.crawler = crawler
|
self.crawler: Crawler = crawler
|
||||||
self.noisy = False
|
self.noisy: bool = False
|
||||||
self.portrange = [
|
self.portrange: List[int] = [
|
||||||
int(x) for x in crawler.settings.getlist("TELNETCONSOLE_PORT")
|
int(x) for x in crawler.settings.getlist("TELNETCONSOLE_PORT")
|
||||||
]
|
]
|
||||||
self.host = crawler.settings["TELNETCONSOLE_HOST"]
|
self.host: str = crawler.settings["TELNETCONSOLE_HOST"]
|
||||||
self.username = crawler.settings["TELNETCONSOLE_USERNAME"]
|
self.username: str = crawler.settings["TELNETCONSOLE_USERNAME"]
|
||||||
self.password = crawler.settings["TELNETCONSOLE_PASSWORD"]
|
self.password: str = crawler.settings["TELNETCONSOLE_PASSWORD"]
|
||||||
|
|
||||||
if not self.password:
|
if not self.password:
|
||||||
self.password = binascii.hexlify(os.urandom(8)).decode("utf8")
|
self.password = binascii.hexlify(os.urandom(8)).decode("utf8")
|
||||||
|
|
@ -61,11 +69,11 @@ class TelnetConsole(protocol.ServerFactory):
|
||||||
self.crawler.signals.connect(self.stop_listening, signals.engine_stopped)
|
self.crawler.signals.connect(self.stop_listening, signals.engine_stopped)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler):
|
def from_crawler(cls, crawler: Crawler) -> Self:
|
||||||
return cls(crawler)
|
return cls(crawler)
|
||||||
|
|
||||||
def start_listening(self):
|
def start_listening(self) -> None:
|
||||||
self.port = listen_tcp(self.portrange, self.host, self)
|
self.port: Port = listen_tcp(self.portrange, self.host, self)
|
||||||
h = self.port.getHost()
|
h = self.port.getHost()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Telnet console listening on %(host)s:%(port)d",
|
"Telnet console listening on %(host)s:%(port)d",
|
||||||
|
|
@ -73,10 +81,10 @@ class TelnetConsole(protocol.ServerFactory):
|
||||||
extra={"crawler": self.crawler},
|
extra={"crawler": self.crawler},
|
||||||
)
|
)
|
||||||
|
|
||||||
def stop_listening(self):
|
def stop_listening(self) -> None:
|
||||||
self.port.stopListening()
|
self.port.stopListening()
|
||||||
|
|
||||||
def protocol(self):
|
def protocol(self) -> telnet.TelnetTransport: # type: ignore[override]
|
||||||
class Portal:
|
class Portal:
|
||||||
"""An implementation of IPortal"""
|
"""An implementation of IPortal"""
|
||||||
|
|
||||||
|
|
@ -95,9 +103,10 @@ class TelnetConsole(protocol.ServerFactory):
|
||||||
|
|
||||||
return telnet.TelnetTransport(telnet.AuthenticatingTelnetProtocol, Portal())
|
return telnet.TelnetTransport(telnet.AuthenticatingTelnetProtocol, Portal())
|
||||||
|
|
||||||
def _get_telnet_vars(self):
|
def _get_telnet_vars(self) -> Dict[str, Any]:
|
||||||
# Note: if you add entries here also update topics/telnetconsole.rst
|
# Note: if you add entries here also update topics/telnetconsole.rst
|
||||||
telnet_vars = {
|
assert self.crawler.engine
|
||||||
|
telnet_vars: Dict[str, Any] = {
|
||||||
"engine": self.crawler.engine,
|
"engine": self.crawler.engine,
|
||||||
"spider": self.crawler.engine.spider,
|
"spider": self.crawler.engine.spider,
|
||||||
"slot": self.crawler.engine.slot,
|
"slot": self.crawler.engine.slot,
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,29 @@
|
||||||
import logging
|
from __future__ import annotations
|
||||||
|
|
||||||
from scrapy import signals
|
import logging
|
||||||
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
|
|
||||||
|
from scrapy import Request, Spider, signals
|
||||||
|
from scrapy.core.downloader import Slot
|
||||||
|
from scrapy.crawler import Crawler
|
||||||
from scrapy.exceptions import NotConfigured
|
from scrapy.exceptions import NotConfigured
|
||||||
|
from scrapy.http import Response
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# typing.Self requires Python 3.11
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AutoThrottle:
|
class AutoThrottle:
|
||||||
def __init__(self, crawler):
|
def __init__(self, crawler: Crawler):
|
||||||
self.crawler = crawler
|
self.crawler: Crawler = crawler
|
||||||
if not crawler.settings.getbool("AUTOTHROTTLE_ENABLED"):
|
if not crawler.settings.getbool("AUTOTHROTTLE_ENABLED"):
|
||||||
raise NotConfigured
|
raise NotConfigured
|
||||||
|
|
||||||
self.debug = crawler.settings.getbool("AUTOTHROTTLE_DEBUG")
|
self.debug: bool = crawler.settings.getbool("AUTOTHROTTLE_DEBUG")
|
||||||
self.target_concurrency = crawler.settings.getfloat(
|
self.target_concurrency: float = crawler.settings.getfloat(
|
||||||
"AUTOTHROTTLE_TARGET_CONCURRENCY"
|
"AUTOTHROTTLE_TARGET_CONCURRENCY"
|
||||||
)
|
)
|
||||||
if self.target_concurrency <= 0.0:
|
if self.target_concurrency <= 0.0:
|
||||||
|
|
@ -27,27 +37,29 @@ class AutoThrottle:
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler):
|
def from_crawler(cls, crawler: Crawler) -> Self:
|
||||||
return cls(crawler)
|
return cls(crawler)
|
||||||
|
|
||||||
def _spider_opened(self, spider):
|
def _spider_opened(self, spider: Spider) -> None:
|
||||||
self.mindelay = self._min_delay(spider)
|
self.mindelay = self._min_delay(spider)
|
||||||
self.maxdelay = self._max_delay(spider)
|
self.maxdelay = self._max_delay(spider)
|
||||||
spider.download_delay = self._start_delay(spider)
|
spider.download_delay = self._start_delay(spider) # type: ignore[attr-defined]
|
||||||
|
|
||||||
def _min_delay(self, spider):
|
def _min_delay(self, spider: Spider) -> float:
|
||||||
s = self.crawler.settings
|
s = self.crawler.settings
|
||||||
return getattr(spider, "download_delay", s.getfloat("DOWNLOAD_DELAY"))
|
return getattr(spider, "download_delay", s.getfloat("DOWNLOAD_DELAY"))
|
||||||
|
|
||||||
def _max_delay(self, spider):
|
def _max_delay(self, spider: Spider) -> float:
|
||||||
return self.crawler.settings.getfloat("AUTOTHROTTLE_MAX_DELAY")
|
return self.crawler.settings.getfloat("AUTOTHROTTLE_MAX_DELAY")
|
||||||
|
|
||||||
def _start_delay(self, spider):
|
def _start_delay(self, spider: Spider) -> float:
|
||||||
return max(
|
return max(
|
||||||
self.mindelay, self.crawler.settings.getfloat("AUTOTHROTTLE_START_DELAY")
|
self.mindelay, self.crawler.settings.getfloat("AUTOTHROTTLE_START_DELAY")
|
||||||
)
|
)
|
||||||
|
|
||||||
def _response_downloaded(self, response, request, spider):
|
def _response_downloaded(
|
||||||
|
self, response: Response, request: Request, spider: Spider
|
||||||
|
) -> None:
|
||||||
key, slot = self._get_slot(request, spider)
|
key, slot = self._get_slot(request, spider)
|
||||||
latency = request.meta.get("download_latency")
|
latency = request.meta.get("download_latency")
|
||||||
if latency is None or slot is None:
|
if latency is None or slot is None:
|
||||||
|
|
@ -74,11 +86,16 @@ class AutoThrottle:
|
||||||
extra={"spider": spider},
|
extra={"spider": spider},
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_slot(self, request, spider):
|
def _get_slot(
|
||||||
key = request.meta.get("download_slot")
|
self, request: Request, spider: Spider
|
||||||
|
) -> Tuple[Optional[str], Optional[Slot]]:
|
||||||
|
key: Optional[str] = request.meta.get("download_slot")
|
||||||
|
if key is None:
|
||||||
|
return None, None
|
||||||
|
assert self.crawler.engine
|
||||||
return key, self.crawler.engine.downloader.slots.get(key)
|
return key, self.crawler.engine.downloader.slots.get(key)
|
||||||
|
|
||||||
def _adjust_delay(self, slot, latency, response):
|
def _adjust_delay(self, slot: Slot, latency: float, response: Response) -> None:
|
||||||
"""Define delay adjustment policy"""
|
"""Define delay adjustment policy"""
|
||||||
|
|
||||||
# If a server needs `latency` seconds to respond then
|
# If a server needs `latency` seconds to respond then
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@ from email.mime.nonmultipart import MIMENonMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from twisted import version as twisted_version
|
from twisted import version as twisted_version
|
||||||
from twisted.internet import defer, ssl
|
from twisted.internet import ssl
|
||||||
|
from twisted.internet.defer import Deferred
|
||||||
from twisted.python.versions import Version
|
from twisted.python.versions import Version
|
||||||
|
|
||||||
from scrapy.settings import BaseSettings
|
from scrapy.settings import BaseSettings
|
||||||
|
|
@ -85,9 +86,10 @@ class MailSender:
|
||||||
mimetype="text/plain",
|
mimetype="text/plain",
|
||||||
charset=None,
|
charset=None,
|
||||||
_callback=None,
|
_callback=None,
|
||||||
):
|
) -> Optional[Deferred]:
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
msg: MIMEBase
|
||||||
if attachs:
|
if attachs:
|
||||||
msg = MIMEMultipart()
|
msg = MIMEMultipart()
|
||||||
else:
|
else:
|
||||||
|
|
@ -134,7 +136,7 @@ class MailSender:
|
||||||
"mailattachs": len(attachs),
|
"mailattachs": len(attachs),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return
|
return None
|
||||||
|
|
||||||
dfd = self._sendmail(rcpts, msg.as_string().encode(charset or "utf-8"))
|
dfd = self._sendmail(rcpts, msg.as_string().encode(charset or "utf-8"))
|
||||||
dfd.addCallbacks(
|
dfd.addCallbacks(
|
||||||
|
|
@ -178,7 +180,7 @@ class MailSender:
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
msg = BytesIO(msg)
|
msg = BytesIO(msg)
|
||||||
d = defer.Deferred()
|
d = Deferred()
|
||||||
|
|
||||||
factory = self._create_sender_factory(to_addrs, msg, d)
|
factory = self._create_sender_factory(to_addrs, msg, d)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,19 @@ import asyncio
|
||||||
import sys
|
import sys
|
||||||
from asyncio import AbstractEventLoop, AbstractEventLoopPolicy
|
from asyncio import AbstractEventLoop, AbstractEventLoopPolicy
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from typing import Any, Callable, Dict, Optional, Sequence, Type
|
from typing import Any, Callable, Dict, List, Optional, Sequence, Type
|
||||||
from warnings import catch_warnings, filterwarnings, warn
|
from warnings import catch_warnings, filterwarnings, warn
|
||||||
|
|
||||||
from twisted.internet import asyncioreactor, error
|
from twisted.internet import asyncioreactor, error
|
||||||
from twisted.internet.base import DelayedCall
|
from twisted.internet.base import DelayedCall
|
||||||
|
from twisted.internet.protocol import ServerFactory
|
||||||
|
from twisted.internet.tcp import Port
|
||||||
|
|
||||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||||
from scrapy.utils.misc import load_object
|
from scrapy.utils.misc import load_object
|
||||||
|
|
||||||
|
|
||||||
def listen_tcp(portrange, host, factory):
|
def listen_tcp(portrange: List[int], host: str, factory: ServerFactory) -> Port: # type: ignore[return]
|
||||||
"""Like reactor.listenTCP but tries different ports in a range."""
|
"""Like reactor.listenTCP but tries different ports in a range."""
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
|
@ -20,8 +22,6 @@ def listen_tcp(portrange, host, factory):
|
||||||
raise ValueError(f"invalid portrange: {portrange}")
|
raise ValueError(f"invalid portrange: {portrange}")
|
||||||
if not portrange:
|
if not portrange:
|
||||||
return reactor.listenTCP(0, factory, interface=host)
|
return reactor.listenTCP(0, factory, interface=host)
|
||||||
if not hasattr(portrange, "__iter__"):
|
|
||||||
return reactor.listenTCP(portrange, factory, interface=host)
|
|
||||||
if len(portrange) == 1:
|
if len(portrange) == 1:
|
||||||
return reactor.listenTCP(portrange[0], factory, interface=host)
|
return reactor.listenTCP(portrange[0], factory, interface=host)
|
||||||
for x in range(portrange[0], portrange[1] + 1):
|
for x in range(portrange[0], portrange[1] + 1):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue