ae9ccaec79
Since we are using Py3.10 as minimum version now, we remove Union when possible
151 lines
5.1 KiB
Python
151 lines
5.1 KiB
Python
from msgspec import Struct, convert, ValidationError
|
|
from urllib.parse import urlparse
|
|
from pathlib import Path
|
|
|
|
from scrapling.core._types import (
|
|
Optional,
|
|
Dict,
|
|
Callable,
|
|
List,
|
|
SelectorWaitStates,
|
|
)
|
|
from scrapling.engines.toolbelt import construct_proxy_dict
|
|
|
|
|
|
class PlaywrightConfig(Struct, kw_only=True, frozen=False):
|
|
"""Configuration struct for validation"""
|
|
|
|
max_pages: int = 1
|
|
cdp_url: Optional[str] = None
|
|
headless: bool = True
|
|
google_search: bool = True
|
|
hide_canvas: bool = False
|
|
disable_webgl: bool = False
|
|
real_chrome: bool = False
|
|
stealth: bool = False
|
|
wait: int | float = 0
|
|
page_action: Optional[Callable] = None
|
|
proxy: Optional[str | Dict[str, str]] = (
|
|
None # The default value for proxy in Playwright's source is `None`
|
|
)
|
|
locale: str = "en-US"
|
|
extra_headers: Optional[Dict[str, str]] = None
|
|
useragent: Optional[str] = None
|
|
timeout: int | float = 30000
|
|
disable_resources: bool = False
|
|
wait_selector: Optional[str] = None
|
|
cookies: Optional[List[Dict]] = None
|
|
network_idle: bool = False
|
|
wait_selector_state: SelectorWaitStates = "attached"
|
|
selector_config: 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 self.cdp_url:
|
|
self.__validate_cdp(self.cdp_url)
|
|
if not self.cookies:
|
|
self.cookies = []
|
|
if not self.selector_config:
|
|
self.selector_config = {}
|
|
|
|
@staticmethod
|
|
def __validate_cdp(cdp_url):
|
|
try:
|
|
# Check the scheme
|
|
if not cdp_url.startswith(("ws://", "wss://")):
|
|
raise ValueError("CDP URL must use 'ws://' or 'wss://' scheme")
|
|
|
|
# Validate hostname and port
|
|
if not urlparse(cdp_url).netloc:
|
|
raise ValueError("Invalid hostname for the CDP URL")
|
|
|
|
except AttributeError as e:
|
|
raise ValueError(f"Malformed CDP URL: {cdp_url}: {str(e)}")
|
|
|
|
except Exception as e:
|
|
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: bool = True # noqa: F821
|
|
block_images: bool = False
|
|
disable_resources: bool = False
|
|
block_webrtc: bool = False
|
|
allow_webgl: bool = True
|
|
network_idle: bool = False
|
|
humanize: bool | float = True
|
|
solve_cloudflare: bool = False
|
|
wait: int | float = 0
|
|
timeout: 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[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
|
|
selector_config: Optional[Dict] = None
|
|
additional_args: 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:
|
|
addon_path = Path(addon)
|
|
if not addon_path.exists():
|
|
raise FileNotFoundError(f"Addon's path not found: {addon}")
|
|
elif not addon_path.is_dir():
|
|
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.selector_config:
|
|
self.selector_config = {}
|
|
if not self.additional_args:
|
|
self.additional_args = {}
|
|
|
|
|
|
def validate(params, model):
|
|
try:
|
|
config = convert(params, model)
|
|
except ValidationError as e:
|
|
raise TypeError(f"Invalid argument type: {e}")
|
|
|
|
return config
|