From edf209e1f15cb92d2e2f8c08c049d998c008c130 Mon Sep 17 00:00:00 2001 From: Alexandr Kuryanov Date: Thu, 2 Apr 2026 14:40:19 +0300 Subject: [PATCH 1/4] add http/socks proxy --- ymdantic/client/yandex_music.py | 99 +++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/ymdantic/client/yandex_music.py b/ymdantic/client/yandex_music.py index a44121a..182280c 100644 --- a/ymdantic/client/yandex_music.py +++ b/ymdantic/client/yandex_music.py @@ -1,4 +1,4 @@ -from typing import Sequence, Unpack +from typing import Sequence, Unpack, Optional from dataclass_rest import get, post from dataclass_rest.client_protocol import FactoryProtocol @@ -53,6 +53,11 @@ from ymdantic.models.landing.artist import LandingArtistItemData +from aiohttp import ClientSession, TCPConnector +# Для поддержки SOCKS прокси +from aiohttp_socks import ProxyConnector +import ssl + class YMClient(AiohttpClient): """Клиент для работы с API Яндекс Музыки.""" @@ -61,17 +66,99 @@ def __init__( token: str, user_id: int | None = None, base_url: str = "https://api.music.yandex.net/", + proxy: str | None = None, + # Формат: scheme://login:password@ip:port + check_certs: bool = True, # Проверка SSL сертификатов ) -> None: self.user_id = user_id + self.proxy = proxy + self.check_certs = check_certs + headers = { + "Accept": "application/json", + "Authorization": f"OAuth {token}", + "X-Yandex-Music-Client": "YandexMusicAndroid/24023621", + } + session = self._create_session_with_proxy(headers=headers) if self.proxy else None super().__init__( base_url=base_url, - headers={ - "Accept": "application/json", - "Authorization": f"OAuth {token}", - "X-Yandex-Music-Client": "YandexMusicAndroid/24023621", - }, + headers=headers, + session=session ) + def _create_session_with_proxy(self, headers: dict[str,str]) -> ClientSession: + """ + Создает ClientSession с учетом прокси и настроек SSL. + Поддерживает HTTP, HTTPS и SOCKS прокси (4, 4a, 5). + """ + # Настройка SSL контекста + if not self.check_certs: + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + else: + ssl_context = None # Используем стандартную проверку + + # Определяем тип прокси по схеме + proxy_lower = self.proxy.lower() + + # SOCKS прокси (поддерживаются socks4, socks4a, socks5, socks5h) + if any(scheme in proxy_lower for scheme in + ['socks4', 'socks5', 'socks5h', 'socks4a']): + + # Определяем точный тип SOCKS + if 'socks5h' in proxy_lower: + proxy_type = 'socks5h' + elif 'socks5' in proxy_lower: + proxy_type = 'socks5' + elif 'socks4a' in proxy_lower: + proxy_type = 'socks4a' + elif 'socks4' in proxy_lower: + proxy_type = 'socks4' + else: + proxy_type = 'socks5' + + # Используем ProxyConnector для SOCKS + connector = ProxyConnector.from_url( + self.proxy, + ssl=ssl_context, + ) + return ClientSession(connector=connector,headers=headers) + + # HTTP/HTTPS прокси + elif proxy_lower.startswith('http://') or proxy_lower.startswith('https://'): + connector = TCPConnector(ssl=ssl_context) + return ClientSession( + connector=connector, + proxy=self.proxy, + proxy_auth=self._get_proxy_auth(), + headers=headers + ) + + # Если схема не распознана, предполагаем HTTP + else: + connector = TCPConnector(ssl=ssl_context) + return ClientSession( + connector=connector, + proxy=f"http://{self.proxy}", + proxy_auth=self._get_proxy_auth(), + headers=headers + ) + + def _get_proxy_auth(self) -> Optional[tuple]: + """ + Извлекает логин и пароль из строки прокси. + Возвращает кортеж (login, password) или None. + """ + try: + # Ищем логин и пароль в формате login:password@ + import re + match = re.search(r'://([^:]+):([^@]+)@', self.proxy) + if match: + return (match.group(1), match.group(2)) + except Exception: + pass + return None + def _init_request_body_factory(self) -> FactoryProtocol: return PydanticFactory() From 7f67adfba42f59fdf203468d6a5501aaee028b80 Mon Sep 17 00:00:00 2001 From: Alexandr Kuryanov Date: Thu, 2 Apr 2026 19:05:44 +0300 Subject: [PATCH 2/4] refactor: proxy logic to aiohttp_client --- ymdantic/client/session/aiohttp_client.py | 64 +++++++++++++++- ymdantic/client/yandex_music.py | 92 ++--------------------- 2 files changed, 65 insertions(+), 91 deletions(-) diff --git a/ymdantic/client/session/aiohttp_client.py b/ymdantic/client/session/aiohttp_client.py index bb83190..70562ba 100644 --- a/ymdantic/client/session/aiohttp_client.py +++ b/ymdantic/client/session/aiohttp_client.py @@ -9,6 +9,7 @@ from dataclass_rest.http_request import HttpRequest from ymdantic.client.session.aiohttp_method import YMHttpMethod +from aiohttp import ClientSession, TCPConnector class AiohttpClient(BaseClient): @@ -22,15 +23,70 @@ def __init__( session: Optional[ClientSession] = None, headers: Optional[dict[str, Any]] = None, timeout: Optional[ClientTimeout] = None, + proxy: Optional[str] = None ) -> None: super().__init__() self.base_url = base_url self.headers = headers or {} + self.proxy = proxy + self.timeout = timeout - self._session = session or ClientSession( - headers=headers, - timeout=timeout or ClientTimeout(total=0), - ) + self._session = session or self._create_session(headers=headers) + + + + def _create_session(self, headers: dict[str,str]) -> ClientSession: + """ + Создает ClientSession с учетом прокси. + Поддерживает HTTP, HTTPS и SOCKS прокси (4, 4a, 5). + """ + if self.proxy is None: + return ClientSession( + headers=headers, + timeout=self.timeout or ClientTimeout(total=0) + ) + + # Определяем тип прокси по схеме + proxy_lower = self.proxy.lower() + + # SOCKS прокси (поддерживаются socks4, socks4a, socks5, socks5h) + if any(scheme in proxy_lower for scheme in + ['socks4', 'socks5', 'socks5h', 'socks4a']): + + try: + from aiohttp_socks import ProxyConnector + except ImportError: + raise ImportError( + "Warning: aiohttp_socks not installed. " + "SOCKS proxies will not work. " + "Install with: pip install aiohttp_socks") + + # Используем ProxyConnector для SOCKS + connector = ProxyConnector.from_url(self.proxy) + return ClientSession(connector=connector, + headers=headers, + timeout=self.timeout or ClientTimeout(total=0) + ) + + # HTTP/HTTPS прокси + elif proxy_lower.startswith('http://') or proxy_lower.startswith('https://'): + connector = TCPConnector() + return ClientSession( + connector=connector, + proxy=self.proxy, + headers=headers, + timeout=self.timeout or ClientTimeout(total=0) + ) + + # Если схема не распознана, предполагаем HTTP + else: + connector = TCPConnector() + return ClientSession( + connector=connector, + proxy=f"http://{self.proxy}", + headers=headers, + timeout=self.timeout or ClientTimeout(total=0) + ) async def close(self) -> None: """Этот метод используется для закрытия текущей сессии, если она открыта.""" diff --git a/ymdantic/client/yandex_music.py b/ymdantic/client/yandex_music.py index 182280c..94c6e0c 100644 --- a/ymdantic/client/yandex_music.py +++ b/ymdantic/client/yandex_music.py @@ -1,4 +1,4 @@ -from typing import Sequence, Unpack, Optional +from typing import Sequence, Unpack from dataclass_rest import get, post from dataclass_rest.client_protocol import FactoryProtocol @@ -53,11 +53,6 @@ from ymdantic.models.landing.artist import LandingArtistItemData -from aiohttp import ClientSession, TCPConnector -# Для поддержки SOCKS прокси -from aiohttp_socks import ProxyConnector -import ssl - class YMClient(AiohttpClient): """Клиент для работы с API Яндекс Музыки.""" @@ -66,99 +61,22 @@ def __init__( token: str, user_id: int | None = None, base_url: str = "https://api.music.yandex.net/", - proxy: str | None = None, - # Формат: scheme://login:password@ip:port - check_certs: bool = True, # Проверка SSL сертификатов + proxy: str | None = None, # Формат: scheme://login:password@ip:port ) -> None: self.user_id = user_id - self.proxy = proxy - self.check_certs = check_certs + headers = { "Accept": "application/json", "Authorization": f"OAuth {token}", "X-Yandex-Music-Client": "YandexMusicAndroid/24023621", } - session = self._create_session_with_proxy(headers=headers) if self.proxy else None + super().__init__( base_url=base_url, headers=headers, - session=session + proxy=proxy ) - def _create_session_with_proxy(self, headers: dict[str,str]) -> ClientSession: - """ - Создает ClientSession с учетом прокси и настроек SSL. - Поддерживает HTTP, HTTPS и SOCKS прокси (4, 4a, 5). - """ - # Настройка SSL контекста - if not self.check_certs: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - else: - ssl_context = None # Используем стандартную проверку - - # Определяем тип прокси по схеме - proxy_lower = self.proxy.lower() - - # SOCKS прокси (поддерживаются socks4, socks4a, socks5, socks5h) - if any(scheme in proxy_lower for scheme in - ['socks4', 'socks5', 'socks5h', 'socks4a']): - - # Определяем точный тип SOCKS - if 'socks5h' in proxy_lower: - proxy_type = 'socks5h' - elif 'socks5' in proxy_lower: - proxy_type = 'socks5' - elif 'socks4a' in proxy_lower: - proxy_type = 'socks4a' - elif 'socks4' in proxy_lower: - proxy_type = 'socks4' - else: - proxy_type = 'socks5' - - # Используем ProxyConnector для SOCKS - connector = ProxyConnector.from_url( - self.proxy, - ssl=ssl_context, - ) - return ClientSession(connector=connector,headers=headers) - - # HTTP/HTTPS прокси - elif proxy_lower.startswith('http://') or proxy_lower.startswith('https://'): - connector = TCPConnector(ssl=ssl_context) - return ClientSession( - connector=connector, - proxy=self.proxy, - proxy_auth=self._get_proxy_auth(), - headers=headers - ) - - # Если схема не распознана, предполагаем HTTP - else: - connector = TCPConnector(ssl=ssl_context) - return ClientSession( - connector=connector, - proxy=f"http://{self.proxy}", - proxy_auth=self._get_proxy_auth(), - headers=headers - ) - - def _get_proxy_auth(self) -> Optional[tuple]: - """ - Извлекает логин и пароль из строки прокси. - Возвращает кортеж (login, password) или None. - """ - try: - # Ищем логин и пароль в формате login:password@ - import re - match = re.search(r'://([^:]+):([^@]+)@', self.proxy) - if match: - return (match.group(1), match.group(2)) - except Exception: - pass - return None - def _init_request_body_factory(self) -> FactoryProtocol: return PydanticFactory() From e23194f25637c2846caeb5a9bd6c49cf4547632e Mon Sep 17 00:00:00 2001 From: Alexandr Kuryanov Date: Thu, 2 Apr 2026 20:36:59 +0300 Subject: [PATCH 3/4] refactor: fix ruff mypy errors --- ymdantic/client/session/aiohttp_client.py | 67 ++++++++++++----------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/ymdantic/client/session/aiohttp_client.py b/ymdantic/client/session/aiohttp_client.py index 70562ba..1e2badc 100644 --- a/ymdantic/client/session/aiohttp_client.py +++ b/ymdantic/client/session/aiohttp_client.py @@ -3,13 +3,21 @@ from types import TracebackType from typing import Any, Optional, Self -from aiohttp import ClientError, ClientSession, ClientTimeout, FormData +from aiohttp import ClientError, ClientSession, ClientTimeout, FormData, TCPConnector from dataclass_rest.base_client import BaseClient from dataclass_rest.exceptions import ClientLibraryError from dataclass_rest.http_request import HttpRequest from ymdantic.client.session.aiohttp_method import YMHttpMethod -from aiohttp import ClientSession, TCPConnector + +try: + from aiohttp_socks import ProxyConnector +except ImportError as e: + raise ImportError( + "Warning: aiohttp_socks not installed. " + "SOCKS proxies will not work. " + "Install with: pip install aiohttp_socks", + ) from e class AiohttpClient(BaseClient): @@ -23,7 +31,7 @@ def __init__( session: Optional[ClientSession] = None, headers: Optional[dict[str, Any]] = None, timeout: Optional[ClientTimeout] = None, - proxy: Optional[str] = None + proxy: Optional[str] = None, ) -> None: super().__init__() self.base_url = base_url @@ -33,61 +41,56 @@ def __init__( self._session = session or self._create_session(headers=headers) - - - def _create_session(self, headers: dict[str,str]) -> ClientSession: + def _create_session(self, headers: dict[str, Any] | None) -> ClientSession: """ Создает ClientSession с учетом прокси. + Поддерживает HTTP, HTTPS и SOCKS прокси (4, 4a, 5). + + :param headers: + :return: aiohttp ClientSession. """ if self.proxy is None: return ClientSession( headers=headers, - timeout=self.timeout or ClientTimeout(total=0) + timeout=self.timeout or ClientTimeout(total=0), ) # Определяем тип прокси по схеме proxy_lower = self.proxy.lower() # SOCKS прокси (поддерживаются socks4, socks4a, socks5, socks5h) - if any(scheme in proxy_lower for scheme in - ['socks4', 'socks5', 'socks5h', 'socks4a']): - - try: - from aiohttp_socks import ProxyConnector - except ImportError: - raise ImportError( - "Warning: aiohttp_socks not installed. " - "SOCKS proxies will not work. " - "Install with: pip install aiohttp_socks") - + if any( + scheme in proxy_lower + for scheme in ["socks4", "socks5", "socks5h", "socks4a"] + ): # Используем ProxyConnector для SOCKS connector = ProxyConnector.from_url(self.proxy) - return ClientSession(connector=connector, - headers=headers, - timeout=self.timeout or ClientTimeout(total=0) - ) - - # HTTP/HTTPS прокси - elif proxy_lower.startswith('http://') or proxy_lower.startswith('https://'): - connector = TCPConnector() return ClientSession( connector=connector, - proxy=self.proxy, headers=headers, - timeout=self.timeout or ClientTimeout(total=0) + timeout=self.timeout or ClientTimeout(total=0), ) - # Если схема не распознана, предполагаем HTTP - else: + # HTTP/HTTPS прокси + if proxy_lower.startswith(("http://", "https://")): connector = TCPConnector() return ClientSession( connector=connector, - proxy=f"http://{self.proxy}", + proxy=self.proxy, headers=headers, - timeout=self.timeout or ClientTimeout(total=0) + timeout=self.timeout or ClientTimeout(total=0), ) + # Если схема не распознана, предполагаем HTTP + connector = TCPConnector() + return ClientSession( + connector=connector, + proxy=f"http://{self.proxy}", + headers=headers, + timeout=self.timeout or ClientTimeout(total=0), + ) + async def close(self) -> None: """Этот метод используется для закрытия текущей сессии, если она открыта.""" if self._session and not self._session.closed: From dd8f7144d404affdc9e4802e84a7f4d1b06529f9 Mon Sep 17 00:00:00 2001 From: Alexandr Kuryanov Date: Thu, 2 Apr 2026 20:58:20 +0300 Subject: [PATCH 4/4] refactor: fix ruff mypy errors --- ymdantic/client/yandex_music.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ymdantic/client/yandex_music.py b/ymdantic/client/yandex_music.py index 94c6e0c..5d2fd47 100644 --- a/ymdantic/client/yandex_music.py +++ b/ymdantic/client/yandex_music.py @@ -61,7 +61,7 @@ def __init__( token: str, user_id: int | None = None, base_url: str = "https://api.music.yandex.net/", - proxy: str | None = None, # Формат: scheme://login:password@ip:port + proxy: str | None = None, # Формат: scheme://login:password@ip:port ) -> None: self.user_id = user_id @@ -74,7 +74,7 @@ def __init__( super().__init__( base_url=base_url, headers=headers, - proxy=proxy + proxy=proxy, ) def _init_request_body_factory(self) -> FactoryProtocol: