Files
proxy-scraper-checker/proxy_scraper_checker/settings.py
T

353 lines
11 KiB
Python
Raw Normal View History

2024-01-21 17:45:01 +00:00
from __future__ import annotations
import asyncio
import enum
2024-01-21 21:46:01 +03:00
import json
2024-01-21 17:45:01 +00:00
import logging
import math
2024-02-08 09:12:13 +03:00
import stat
2024-01-21 17:45:01 +00:00
import sys
from pathlib import Path
2024-05-06 22:55:17 +05:00
from types import MappingProxyType
2024-07-09 03:08:30 +03:00
from typing import TYPE_CHECKING
2024-01-21 17:45:01 +00:00
from urllib.parse import urlparse
import attrs
2024-02-01 07:24:26 +00:00
import platformdirs
2024-07-09 03:08:30 +03:00
from aiohttp import ClientTimeout, hdrs
2024-01-21 17:45:01 +00:00
from aiohttp_socks import ProxyType
2024-11-22 14:32:29 +03:00
from proxy_scraper_checker import fs, sort
from proxy_scraper_checker.http import get_response_text
from proxy_scraper_checker.null_context import NullContext
from proxy_scraper_checker.parsers import parse_ipv4
from proxy_scraper_checker.utils import IS_DOCKER
2024-01-21 17:45:01 +00:00
if TYPE_CHECKING:
2024-10-17 14:18:27 +03:00
from collections.abc import Iterable, Mapping
from typing import Callable
2024-07-09 03:08:30 +03:00
from aiohttp import ClientSession
from typing_extensions import Any, Literal, Self
2024-11-22 14:32:29 +03:00
from proxy_scraper_checker.proxy import Proxy
2024-01-21 17:45:01 +00:00
2024-11-06 15:05:34 +03:00
_logger = logging.getLogger(__name__)
2024-01-21 17:45:01 +00:00
2024-06-03 14:08:42 +05:00
def _get_supported_max_connections() -> int | None:
2024-01-21 17:45:01 +00:00
if sys.platform == "win32":
if isinstance(
asyncio.get_event_loop_policy(),
asyncio.WindowsSelectorEventLoopPolicy,
):
return 512
return None
2024-05-06 22:55:17 +05:00
import resource # type: ignore[unreachable, unused-ignore] # noqa: PLC0415
2024-01-21 17:45:01 +00:00
soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
2024-11-06 15:05:34 +03:00
_logger.debug(
2024-01-21 17:45:01 +00:00
"max_connections: soft limit = %d, hard limit = %d, infinity = %d",
soft_limit,
hard_limit,
resource.RLIM_INFINITY,
)
if soft_limit != hard_limit:
try:
resource.setrlimit(resource.RLIMIT_NOFILE, (hard_limit, hard_limit))
except ValueError as e:
2024-11-06 15:05:34 +03:00
_logger.warning("Failed setting max_connections: %s", e)
2024-01-21 17:45:01 +00:00
else:
soft_limit = hard_limit
if soft_limit == resource.RLIM_INFINITY:
return None
return soft_limit
2024-06-03 14:08:42 +05:00
def _get_max_connections(value: int, /) -> int | None:
2024-01-21 17:45:01 +00:00
if value < 0:
msg = "max_connections must be non-negative"
raise ValueError(msg)
max_supported = _get_supported_max_connections()
if not value:
2024-11-06 15:05:34 +03:00
_logger.info("Using %d as max_connections value", max_supported or 0)
2024-01-21 17:45:01 +00:00
return max_supported
if not max_supported or value <= max_supported:
return value
2024-11-06 15:05:34 +03:00
_logger.warning(
"max_connections value is too high for your OS. "
"The config value will be ignored and %d will be used.%s",
2024-01-21 17:45:01 +00:00
max_supported,
" To make max_connections unlimited, install the winloop library."
if sys.version_info >= (3, 9)
and sys.platform in {"cygwin", "win32"}
and sys.implementation.name == "cpython"
else "",
2024-01-21 17:45:01 +00:00
)
return max_supported
2024-06-03 14:08:42 +05:00
def _semaphore_converter(value: int, /) -> asyncio.Semaphore | NullContext:
2024-01-21 17:45:01 +00:00
v = _get_max_connections(value)
return asyncio.Semaphore(v) if v else NullContext()
def _timeout_converter(value: float, /) -> ClientTimeout:
return ClientTimeout(total=value, sock_connect=math.inf)
def _sources_converter(
2024-06-03 14:08:42 +05:00
value: Mapping[ProxyType, Iterable[str] | None], /
) -> dict[ProxyType, frozenset[str]]:
2024-01-21 17:45:01 +00:00
return {
proxy_type: frozenset(sources)
for proxy_type, sources in value.items()
if sources is not None
}
class CheckWebsiteType(enum.Enum):
2024-05-06 22:55:17 +05:00
UNKNOWN = enum.auto(), False, False, None
PLAIN_IP = enum.auto(), True, True, None
2024-01-21 17:45:01 +00:00
"""https://checkip.amazonaws.com"""
2024-05-06 22:55:17 +05:00
HTTPBIN_IP = (
enum.auto(),
True,
True,
MappingProxyType({hdrs.ACCEPT: "application/json"}),
)
2024-01-21 17:45:01 +00:00
"""https://httpbin.org/ip"""
2024-05-06 22:55:17 +05:00
def __init__(
self,
_: object,
supports_geolocation: bool, # noqa: FBT001
supports_anonymity: bool, # noqa: FBT001
2024-06-03 14:08:42 +05:00
headers: Mapping[str, str] | None,
2024-05-06 22:55:17 +05:00
/,
) -> None:
self.supports_geolocation = supports_geolocation
self.supports_anonymity = supports_anonymity
self.headers = headers
def __new__(cls, value: object, *_: Any) -> Self:
member = object.__new__(cls)
member._value_ = value
return member
2024-01-21 17:45:01 +00:00
async def _get_check_website_type_and_real_ip(
*, check_website: str, session: ClientSession
2024-06-03 14:08:42 +05:00
) -> (
tuple[Literal[CheckWebsiteType.UNKNOWN], None]
| tuple[
Literal[CheckWebsiteType.PLAIN_IP, CheckWebsiteType.HTTPBIN_IP], str
]
):
2024-09-18 18:30:59 +03:00
if not check_website:
return CheckWebsiteType.UNKNOWN, None
2024-01-21 17:45:01 +00:00
try:
async with session.get(check_website) as response:
2024-01-23 09:37:29 +03:00
content = await response.read()
text = get_response_text(response=response, content=content)
2024-01-21 17:45:01 +00:00
except Exception:
2024-11-06 15:05:34 +03:00
_logger.exception(
2024-01-21 17:45:01 +00:00
"Error when opening check_website without proxy, it will be "
"impossible to determine anonymity and geolocation of proxies"
)
return CheckWebsiteType.UNKNOWN, None
try:
2024-01-21 21:46:01 +03:00
js = json.loads(text)
except json.JSONDecodeError:
2024-01-21 17:45:01 +00:00
try:
2024-01-21 21:46:01 +03:00
return CheckWebsiteType.PLAIN_IP, parse_ipv4(text)
2024-01-21 17:45:01 +00:00
except ValueError:
pass
else:
try:
2024-01-21 21:46:01 +03:00
return CheckWebsiteType.HTTPBIN_IP, parse_ipv4(js["origin"])
2024-01-21 17:45:01 +00:00
except (KeyError, TypeError, ValueError):
pass
2024-11-06 15:05:34 +03:00
_logger.warning(
2024-01-21 17:45:01 +00:00
"Check_website is not httpbin and does not return plain ip, so it will"
" be impossible to determine the anonymity and geolocation of proxies"
)
return CheckWebsiteType.UNKNOWN, None
@attrs.define(
repr=False,
weakref_slot=False,
kw_only=True,
eq=False,
getstate_setstate=False,
match_args=False,
)
class Settings:
check_website: str = attrs.field(
validator=attrs.validators.instance_of(str)
)
check_website_type: CheckWebsiteType = attrs.field(
validator=attrs.validators.instance_of(CheckWebsiteType)
)
enable_geolocation: bool = attrs.field(
validator=attrs.validators.instance_of(bool)
)
output_json: bool = attrs.field(
validator=attrs.validators.instance_of(bool)
)
output_path: Path = attrs.field(converter=Path)
output_txt: bool = attrs.field(validator=attrs.validators.instance_of(bool))
2024-06-03 14:08:42 +05:00
real_ip: str | None = attrs.field(
2024-01-21 17:45:01 +00:00
validator=attrs.validators.optional(attrs.validators.instance_of(str))
)
2024-06-03 14:08:42 +05:00
semaphore: asyncio.Semaphore | NullContext = attrs.field(
2024-01-21 17:45:01 +00:00
converter=_semaphore_converter
)
sort_by_speed: bool = attrs.field(
validator=attrs.validators.instance_of(bool)
)
source_timeout: float = attrs.field(validator=attrs.validators.gt(0))
2024-06-03 14:08:42 +05:00
sources: dict[ProxyType, frozenset[str]] = attrs.field(
2024-01-21 17:45:01 +00:00
validator=attrs.validators.and_(
attrs.validators.instance_of(dict),
attrs.validators.min_len(1),
attrs.validators.deep_mapping(
attrs.validators.instance_of(ProxyType),
attrs.validators.and_(
attrs.validators.min_len(1),
attrs.validators.deep_iterable(
attrs.validators.and_(
attrs.validators.instance_of(str),
attrs.validators.min_len(1),
)
),
),
),
),
converter=_sources_converter,
)
timeout: ClientTimeout = attrs.field(converter=_timeout_converter)
@property
def sorting_key(
self,
2024-06-03 14:08:42 +05:00
) -> Callable[[Proxy], float] | Callable[[Proxy], tuple[int, ...]]:
2024-01-21 17:45:01 +00:00
return (
sort.timeout_sort_key
if self.sort_by_speed
else sort.natural_sort_key
)
def __attrs_post_init__(self) -> None:
if not self.output_json and not self.output_txt:
msg = "both json and txt outputs are disabled"
raise ValueError(msg)
if not self.output_json and self.enable_geolocation:
msg = "geolocation can not be enabled if json output is disabled"
raise ValueError(msg)
2024-09-18 18:35:56 +03:00
if not self.check_website and self.sort_by_speed:
2024-11-06 15:05:34 +03:00
_logger.warning(
2024-09-18 18:35:56 +03:00
"Proxy checking is disabled, so sorting by speed is not"
" possible. Alphabetical sorting will be used instead."
)
self.sort_by_speed = False
2024-01-21 17:45:01 +00:00
@check_website.validator
2024-09-06 06:06:06 +00:00
def _validate_check_website(
2024-01-21 17:45:01 +00:00
self,
attribute: attrs.Attribute[str], # noqa: ARG002
value: str,
/,
) -> None:
2024-09-18 18:30:59 +03:00
if value:
parsed_url = urlparse(value)
if (
parsed_url.scheme not in {"http", "https"}
or not parsed_url.netloc
):
msg = f"invalid check_website: {value}"
raise ValueError(msg)
if parsed_url.scheme == "http":
2024-11-06 15:05:34 +03:00
_logger.warning(
2024-09-18 18:30:59 +03:00
"check_website uses the http protocol. "
"It is recommended to use https for correct checking."
)
2024-01-21 17:45:01 +00:00
@timeout.validator
def _validate_timeout(
self,
attribute: attrs.Attribute[str], # noqa: ARG002
value: float, # noqa: ARG002
/,
) -> None:
if self.timeout.total is None or self.timeout.total <= 0:
msg = "timeout must be positive"
raise ValueError(msg)
@classmethod
async def from_mapping(
cls, cfg: Mapping[str, Any], /, *, session: ClientSession
) -> Self:
2024-02-01 07:24:26 +00:00
output_path = (
platformdirs.user_data_path("proxy_scraper_checker")
if IS_DOCKER
2024-02-08 09:12:13 +03:00
else Path(cfg["output"]["path"])
2024-01-21 17:45:01 +00:00
)
2024-02-01 07:24:26 +00:00
2024-11-04 11:00:52 +03:00
output_path_future = asyncio.to_thread(
fs.create_or_fix_dir,
output_path,
permission=stat.S_IXUSR | stat.S_IWUSR,
)
check_website_type, real_ip = await _get_check_website_type_and_real_ip(
check_website=cfg["check_website"], session=session
)
enable_geolocation = (
cfg["enable_geolocation"]
and check_website_type.supports_geolocation
)
if enable_geolocation:
2024-11-04 11:00:52 +03:00
await asyncio.to_thread(
fs.create_or_fix_dir,
2024-02-08 09:12:13 +03:00
fs.CACHE_PATH,
2024-02-08 12:01:37 +03:00
permission=stat.S_IRUSR | stat.S_IXUSR | stat.S_IWUSR,
)
2024-03-01 07:28:43 +03:00
await output_path_future
2024-02-01 07:24:26 +00:00
2024-01-21 17:45:01 +00:00
return cls(
check_website=cfg["check_website"],
check_website_type=check_website_type,
enable_geolocation=enable_geolocation,
2024-01-21 17:45:01 +00:00
output_json=cfg["output"]["json"],
2024-02-01 07:24:26 +00:00
output_path=output_path,
2024-01-21 17:45:01 +00:00
output_txt=cfg["output"]["txt"],
real_ip=real_ip,
semaphore=cfg["max_connections"],
sort_by_speed=cfg["sort_by_speed"],
source_timeout=cfg["source_timeout"],
sources={
ProxyType.HTTP: (
cfg["http"]["sources"] if cfg["http"]["enabled"] else None
),
ProxyType.SOCKS4: (
cfg["socks4"]["sources"]
if cfg["socks4"]["enabled"]
else None
),
ProxyType.SOCKS5: (
cfg["socks5"]["sources"]
if cfg["socks5"]["enabled"]
else None
),
},
timeout=cfg["timeout"],
)