feat(fetchers): Improve StealthyFetcher + Adding StealthySession/AsyncStealthySession classes

This commit is contained in:
Karim shoair
2025-06-23 03:23:28 +03:00
parent 3754547e58
commit 69b983b42f
6 changed files with 850 additions and 467 deletions
@@ -1,5 +1,6 @@
from msgspec import Struct, convert, ValidationError
from urllib.parse import urlparse
from os.path import exists, isdir
from scrapling.core._types import (
Optional,
@@ -78,6 +79,70 @@ class PlaywrightConfig(Struct, kw_only=True, frozen=False):
raise ValueError(f"Invalid CDP URL '{cdp_url}': {str(e)}")
class CamoufoxConfig(Struct, kw_only=True, frozen=False):
"""Configuration struct for validation"""
max_pages: int = 1
headless: Union[bool, Literal["virtual"]] = True # noqa: F821
block_images: bool = False
disable_resources: bool = False
block_webrtc: bool = False
allow_webgl: bool = True
network_idle: bool = False
humanize: Union[bool, float] = True
solve_cloudflare: bool = False
wait: Union[int, float] = 0
timeout: Union[int, float] = 30000
page_action: Optional[Callable] = None
wait_selector: Optional[str] = None
addons: Optional[List[str]] = None
wait_selector_state: SelectorWaitStates = "attached"
cookies: Optional[List[Dict]] = None
google_search: bool = True
extra_headers: Optional[Dict[str, str]] = None
proxy: Optional[Union[str, Dict[str, str]]] = (
None # The default value for proxy in Playwright's source is `None`
)
os_randomize: bool = False
disable_ads: bool = False
geoip: bool = False
adaptor_arguments: Optional[Dict] = None
additional_arguments: Optional[Dict] = None
def __post_init__(self):
"""Custom validation after msgspec validation"""
if self.max_pages < 1 or self.max_pages > 50:
raise ValueError("max_pages must be between 1 and 50")
if self.timeout < 0:
raise ValueError("timeout must be >= 0")
if self.page_action is not None and not callable(self.page_action):
raise TypeError(
f"page_action must be callable, got {type(self.page_action).__name__}"
)
if self.proxy:
self.proxy = construct_proxy_dict(self.proxy, as_tuple=True)
if not self.addons:
self.addons = []
else:
for addon in self.addons:
if not exists(addon):
raise FileNotFoundError(f"Addon's path not found: {addon}")
elif not isdir(addon):
raise ValueError(
f"Addon's path is not a folder, you need to pass a folder of the extracted addon: {addon}"
)
if not self.cookies:
self.cookies = []
if self.solve_cloudflare and self.timeout < 60_000:
self.timeout = 60_000
if not self.adaptor_arguments:
self.adaptor_arguments = {}
if not self.additional_arguments:
self.additional_arguments = {}
def validate(params, model):
try:
config = convert(params, model)