feat(proxy): proxy for aiohttp,base sessions (#284)

* feat(proxy): proxy for aiohttp,base sessions

Add support for proxies in aiohttp session with aiohttp_socks library,
edit BaseSession class to support proxies for other sessions in future.

* fix(annotation): missing underscore before "private" typevar

* chore: remove redundant of proxy_url schema for socks version

* test: add missing test

Add missing test, remove BaseSession.cfg and switch to
implementing class' "private" traits, add aiohttp_socks in dependency list as
optional and extra.

* feat(session):

Implement asyncio session for requests [wip]

* feat(proxy chain): Chained proxy support in aiohttp session

Add ChainProxyConnector support, !pin pydantic to "1.4", add
documentation on aiohttp connector.

* style(mypy): apply linter changes

* tests(mock): remove await for magic mock

* fix dangling dependency

* refactor(generic):

get rid of generic behaviour for base session
This commit is contained in:
Martin Winks 2020-05-02 17:12:53 +04:00 committed by GitHub
parent 2553f5f19e
commit 15bcc0ba9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 245 additions and 8 deletions

View file

@ -121,7 +121,7 @@ class Bot(ContextInstanceMixin["Bot"]):
"""
def __init__(
self, token: str, session: Optional[BaseSession] = None, parse_mode: Optional[str] = None
self, token: str, session: Optional[BaseSession] = None, parse_mode: Optional[str] = None,
) -> None:
validate_token(token)

View file

@ -1,29 +1,105 @@
from __future__ import annotations
from typing import AsyncGenerator, Callable, Optional, TypeVar, cast
from typing import (
Any,
AsyncGenerator,
Callable,
Dict,
Iterable,
List,
Optional,
Tuple,
Type,
TypeVar,
Union,
cast,
)
from aiohttp import ClientSession, ClientTimeout, FormData
from aiohttp import BasicAuth, ClientSession, ClientTimeout, FormData, TCPConnector
from aiogram.api.methods import Request, TelegramMethod
from .base import PRODUCTION, BaseSession, TelegramAPIServer
T = TypeVar("T")
_ProxyBasic = Union[str, Tuple[str, BasicAuth]]
_ProxyChain = Iterable[_ProxyBasic]
_ProxyType = Union[_ProxyChain, _ProxyBasic]
def _retrieve_basic(basic: _ProxyBasic) -> Dict[str, Any]:
from aiohttp_socks.utils import parse_proxy_url # type: ignore
proxy_auth: Optional[BasicAuth] = None
if isinstance(basic, str):
proxy_url = basic
else:
proxy_url, proxy_auth = basic
proxy_type, host, port, username, password = parse_proxy_url(proxy_url)
if isinstance(proxy_auth, BasicAuth):
username = proxy_auth.login
password = proxy_auth.password
return dict(
proxy_type=proxy_type,
host=host,
port=port,
username=username,
password=password,
rdns=True,
)
def _prepare_connector(chain_or_plain: _ProxyType) -> Tuple[Type["TCPConnector"], Dict[str, Any]]:
from aiohttp_socks import ProxyInfo, ProxyConnector, ChainProxyConnector # type: ignore
# since tuple is Iterable(compatible with _ProxyChain) object, we assume that
# user wants chained proxies if tuple is a pair of string(url) and BasicAuth
if isinstance(chain_or_plain, str) or (
isinstance(chain_or_plain, tuple) and len(chain_or_plain) == 2
):
chain_or_plain = cast(_ProxyBasic, chain_or_plain)
return ProxyConnector, _retrieve_basic(chain_or_plain)
chain_or_plain = cast(_ProxyChain, chain_or_plain)
infos: List[ProxyInfo] = []
for basic in chain_or_plain:
infos.append(ProxyInfo(**_retrieve_basic(basic)))
return ChainProxyConnector, dict(proxy_infos=infos)
class AiohttpSession(BaseSession):
def __init__(
self,
api: TelegramAPIServer = PRODUCTION,
json_loads: Optional[Callable[..., str]] = None,
json_loads: Optional[Callable[..., Any]] = None,
json_dumps: Optional[Callable[..., str]] = None,
proxy: Optional[_ProxyType] = None,
):
super(AiohttpSession, self).__init__(api=api, json_loads=json_loads, json_dumps=json_dumps)
super(AiohttpSession, self).__init__(
api=api, json_loads=json_loads, json_dumps=json_dumps, proxy=proxy
)
self._session: Optional[ClientSession] = None
self._connector_type: Type[TCPConnector] = TCPConnector
self._connector_init: Dict[str, Any] = {}
if self.proxy:
try:
self._connector_type, self._connector_init = _prepare_connector(
cast(_ProxyType, self.proxy)
)
except ImportError as exc: # pragma: no cover
raise UserWarning(
"In order to use aiohttp client for proxy requests, install "
"https://pypi.org/project/aiohttp-socks/"
) from exc
async def create_session(self) -> ClientSession:
if self._session is None or self._session.closed:
self._session = ClientSession()
self._session = ClientSession(connector=self._connector_type(**self._connector_init))
return self._session

View file

@ -12,14 +12,16 @@ from ...methods import Response, TelegramMethod
from ..telegram import PRODUCTION, TelegramAPIServer
T = TypeVar("T")
PT = TypeVar("PT")
class BaseSession(abc.ABC):
def __init__(
self,
api: Optional[TelegramAPIServer] = None,
json_loads: Optional[Callable[..., str]] = None,
json_loads: Optional[Callable[..., Any]] = None,
json_dumps: Optional[Callable[..., str]] = None,
proxy: Optional[PT] = None,
) -> None:
if api is None:
api = PRODUCTION
@ -31,6 +33,7 @@ class BaseSession(abc.ABC):
self.api = api
self.json_loads = json_loads
self.json_dumps = json_dumps
self.proxy = proxy
def raise_for_status(self, response: Response[T]) -> None:
if response.ok: