def _request(self, method, path): kwargs = { 'method': method, 'url': f"{self._base_url}{path}", 'headers': { 'Cookie': f'iksm_session={self.iksm_session}; path=/; ' f'domain=.app.splatoon2.nintendo.net;', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } } try: resp = self._con_pool.request(**kwargs) except urllib3.exceptions.TimeoutError: raise TimedOut() except urllib3.exceptions.HTTPError as error: raise NetworkError('urllib3 HTTPError {0}'.format(error)) if 200 <= resp.status <= 299: return resp.data elif resp.status == 403: raise Splatoon2SessionInvalid() else: message = 'Unknown HTTPError' raise NetworkError('{0} ({1})'.format(message, resp.status))
def decorator(*args, **kwargs): try: return func(*args, **kwargs) except HTTPError as error: # `HTTPError` inherits from `URLError` so `HTTPError` handling must # come first. errcode = error.getcode() if errcode in (401, 403): raise Unauthorized() if errcode == 502: raise NetworkError('Bad Gateway') try: message = _parse(error.read()) except ValueError: message = 'Unknown HTTPError {0}'.format(error.getcode()) raise NetworkError('{0} ({1})'.format(message, errcode)) except URLError as error: raise NetworkError('URLError: {0!r}'.format(error)) except (SSLError, socket.timeout) as error: err_s = str(error) if "operation timed out" in err_s: raise TimedOut() raise NetworkError(err_s) except HTTPException as error: raise NetworkError('HTTPException: {0!r}'.format(error))
def _request_wrapper(self, *args, **kwargs): """Wraps urllib3 request for handling known exceptions. Args: args: unnamed arguments, passed to urllib3 request. kwargs: keyword arguments, passed tp urllib3 request. Returns: str: A non-parsed JSON text. Raises: TelegramError """ # Make sure to hint Telegram servers that we reuse connections by sending # "Connection: keep-alive" in the HTTP headers. if 'headers' not in kwargs: kwargs['headers'] = {} kwargs['headers']['connection'] = 'keep-alive' # Also set our user agent kwargs['headers']['user-agent'] = USER_AGENT try: resp = self._con_pool.request(*args, **kwargs) except urllib3.exceptions.TimeoutError: raise TimedOut() except urllib3.exceptions.HTTPError as error: # HTTPError must come last as its the base urllib3 exception class # TODO: do something smart here; for now just raise NetworkError raise NetworkError('urllib3 HTTPError {}'.format(error)) if 200 <= resp.status <= 299: # 200-299 range are HTTP success statuses return resp.data try: message = self._parse(resp.data) except ValueError: message = 'Unknown HTTPError' if resp.status in (401, 403): raise Unauthorized(message) elif resp.status == 400: raise BadRequest(message) elif resp.status == 404: raise InvalidToken() elif resp.status == 409: raise Conflict(message) elif resp.status == 413: raise NetworkError( 'File too large. Check telegram api limits ' 'https://core.telegram.org/bots/api#senddocument') elif resp.status == 502: raise NetworkError('Bad Gateway') else: raise NetworkError('{} ({})'.format(message, resp.status))
def test_network_error(self): with pytest.raises(NetworkError, match="test message"): raise NetworkError("test message") with pytest.raises(NetworkError, match="^Test message$"): raise NetworkError("Error: test message") with pytest.raises(NetworkError, match="^Test message$"): raise NetworkError("[Error]: test message") with pytest.raises(NetworkError, match="^Test message$"): raise NetworkError("Bad Request: test message")
def _request_wrapper(self, *args, **kwargs): """Wraps urllib3 request for handling known exceptions. Args: args: unnamed arguments, passed to urllib3 request. kwargs: keyword arguments, passed tp urllib3 request. Returns: str: A non-parsed JSON text. Raises: TelegramError """ try: resp = self._con_pool.request(*args, **kwargs) except Exception as e: log.error(str(e) + '\n' + traceback.format_exc()) """ except urllib3.exceptions.TimeoutError: raise TimedOut() except urllib3.exceptions.HTTPError as error: # HTTPError must come last as its the base urllib3 exception class # TODO: do something smart here; for now just raise NetworkError raise NetworkError('urllib3 HTTPError {0}'.format(error)) """ if 200 <= resp.status <= 299: # 200-299 range are HTTP success statuses return resp.data try: message = self._parse(resp.data) except ValueError: raise NetworkError('Unknown HTTPError {0}'.format(resp.status)) if resp.status in (401, 403): raise Unauthorized() elif resp.status == 400: raise BadRequest(repr(message)) elif resp.status == 404: raise InvalidToken() elif resp.status == 502: raise NetworkError('Bad Gateway') else: raise NetworkError('{0} ({1})'.format(message, resp.status))
def test_send_msg_network_error(default_conf, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.rpc.telegram', _CONF=default_conf, init=MagicMock()) default_conf['telegram']['enabled'] = True bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) send_msg('test', bot) # Bot should've tried to send it twice assert len(bot.method_calls) == 2
def test__send_msg_network_error(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) telegram._config['telegram']['enabled'] = True telegram._send_msg('test', bot) # Bot should've tried to send it twice assert len(bot.method_calls) == 2 assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog)
def test_send_msg_network_error(default_conf, mocker, caplog) -> None: """ Test send_msg() method """ patch_coinmarketcap(mocker) mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) conf = deepcopy(default_conf) bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) freqtradebot = FreqtradeBot(conf, create_engine('sqlite://')) telegram = Telegram(freqtradebot) telegram._config['telegram']['enabled'] = True telegram.send_msg('test', bot) # Bot should've tried to send it twice assert len(bot.method_calls) == 2 assert log_has( 'Telegram NetworkError: Oh snap! Trying one more time.', caplog.record_tuples )
def _fakeSendMessage_with429Error(obj, *args, **kwargs): global CURRENT_TIME if CURRENT_TIME < 2: raise NetworkError("Too Many Requests: retry after 1 (429)") else: return "SUCCESS"
def _fakesendDocument_with429Error_unknown(obj, *args, **kwargs): raise NetworkError("Too Many Requests: retry after 666")
class TestErrors: def test_telegram_error(self): with pytest.raises(TelegramError, match="^test message$"): raise TelegramError("test message") with pytest.raises(TelegramError, match="^Test message$"): raise TelegramError("Error: test message") with pytest.raises(TelegramError, match="^Test message$"): raise TelegramError("[Error]: test message") with pytest.raises(TelegramError, match="^Test message$"): raise TelegramError("Bad Request: test message") def test_unauthorized(self): with pytest.raises(Forbidden, match="test message"): raise Forbidden("test message") with pytest.raises(Forbidden, match="^Test message$"): raise Forbidden("Error: test message") with pytest.raises(Forbidden, match="^Test message$"): raise Forbidden("[Error]: test message") with pytest.raises(Forbidden, match="^Test message$"): raise Forbidden("Bad Request: test message") def test_invalid_token(self): with pytest.raises(InvalidToken, match="Invalid token"): raise InvalidToken def test_network_error(self): with pytest.raises(NetworkError, match="test message"): raise NetworkError("test message") with pytest.raises(NetworkError, match="^Test message$"): raise NetworkError("Error: test message") with pytest.raises(NetworkError, match="^Test message$"): raise NetworkError("[Error]: test message") with pytest.raises(NetworkError, match="^Test message$"): raise NetworkError("Bad Request: test message") def test_bad_request(self): with pytest.raises(BadRequest, match="test message"): raise BadRequest("test message") with pytest.raises(BadRequest, match="^Test message$"): raise BadRequest("Error: test message") with pytest.raises(BadRequest, match="^Test message$"): raise BadRequest("[Error]: test message") with pytest.raises(BadRequest, match="^Test message$"): raise BadRequest("Bad Request: test message") def test_timed_out(self): with pytest.raises(TimedOut, match="^Timed out$"): raise TimedOut def test_chat_migrated(self): with pytest.raises( ChatMigrated, match="Group migrated to supergroup. New chat id: 1234"): raise ChatMigrated(1234) try: raise ChatMigrated(1234) except ChatMigrated as e: assert e.new_chat_id == 1234 def test_retry_after(self): with pytest.raises( RetryAfter, match="Flood control exceeded. Retry in 12 seconds"): raise RetryAfter(12) def test_conflict(self): with pytest.raises(Conflict, match="Something something."): raise Conflict("Something something.") @pytest.mark.parametrize( "exception, attributes", [ (TelegramError("test message"), ["message"]), (Forbidden("test message"), ["message"]), (InvalidToken(), ["message"]), (NetworkError("test message"), ["message"]), (BadRequest("test message"), ["message"]), (TimedOut(), ["message"]), (ChatMigrated(1234), ["message", "new_chat_id"]), (RetryAfter(12), ["message", "retry_after"]), (Conflict("test message"), ["message"]), (PassportDecryptionError("test message"), ["message"]), (InvalidCallbackData("test data"), ["callback_data"]), ], ) def test_errors_pickling(self, exception, attributes): pickled = pickle.dumps(exception) unpickled = pickle.loads(pickled) assert type(unpickled) is type(exception) assert str(unpickled) == str(exception) for attribute in attributes: assert getattr(unpickled, attribute) == getattr(exception, attribute) @pytest.mark.parametrize( "inst", [ (TelegramError("test message")), (Forbidden("test message")), (InvalidToken()), (NetworkError("test message")), (BadRequest("test message")), (TimedOut()), (ChatMigrated(1234)), (RetryAfter(12)), (Conflict("test message")), (PassportDecryptionError("test message")), (InvalidCallbackData("test data")), ], ) def test_slot_behaviour(self, inst, mro_slots): for attr in inst.__slots__: assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(inst)) == len(set( mro_slots(inst))), "duplicate slot" def test_coverage(self): """ This test is only here to make sure that new errors will override __reduce__ and set __slots__ properly. Add the new error class to the below covered_subclasses dict, if it's covered in the above test_errors_pickling and test_slots_behavior tests. """ def make_assertion(cls): assert set(cls.__subclasses__()) == covered_subclasses[cls] for subcls in cls.__subclasses__(): make_assertion(subcls) covered_subclasses = defaultdict(set) covered_subclasses.update({ TelegramError: { Forbidden, InvalidToken, NetworkError, ChatMigrated, RetryAfter, Conflict, PassportDecryptionError, InvalidCallbackData, }, NetworkError: {BadRequest, TimedOut}, }) make_assertion(TelegramError) def test_string_representations(self): """We just randomly test a few of the subclasses - should suffice""" e = TelegramError("This is a message") assert repr(e) == "TelegramError('This is a message')" assert str(e) == "This is a message" e = RetryAfter(42) assert repr( e) == "RetryAfter('Flood control exceeded. Retry in 42 seconds')" assert str(e) == "Flood control exceeded. Retry in 42 seconds" e = BadRequest("This is a message") assert repr(e) == "BadRequest('This is a message')" assert str(e) == "This is a message"
async def _request_wrapper( self, url: str, method: str, request_data: RequestData = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, ) -> bytes: """Wraps the real implementation request method. Performs the following tasks: * Handle the various HTTP response codes. * Parse the Telegram server response. Args: url (:obj:`str`): The URL to request. method (:obj:`str`): HTTP method (i.e. 'POST', 'GET', etc.). request_data (:class:`telegram.request.RequestData`, optional): An object containing information about parameters and files to upload for the request. read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a response from Telegram's server instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a write operation to complete (in terms of a network socket; i.e. POSTing a request or uploading a file) instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection attempt to a server to succeed instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection to become available instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. Returns: bytes: The payload part of the HTTP server response. Raises: TelegramError """ # TGs response also has the fields 'ok' and 'error_code'. # However, we rather rely on the HTTP status code for now. try: code, payload = await self.do_request( url=url, method=method, request_data=request_data, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) except asyncio.CancelledError as exc: # TODO: in py3.8+, CancelledError is a subclass of BaseException, so we can drop this # clause when we drop py3.7 raise exc except TelegramError as exc: raise exc except Exception as exc: raise NetworkError( f"Unknown error in HTTP implementation: {repr(exc)}") from exc if HTTPStatus.OK <= code <= 299: # 200-299 range are HTTP success statuses return payload response_data = self.parse_json_payload(payload) description = response_data.get("description") if description: message = description else: message = "Unknown HTTPError" # In some special cases, we can raise more informative exceptions: # see https://core.telegram.org/bots/api#responseparameters and # https://core.telegram.org/bots/api#making-requests parameters = response_data.get("parameters") if parameters: migrate_to_chat_id = parameters.get("migrate_to_chat_id") if migrate_to_chat_id: raise ChatMigrated(migrate_to_chat_id) retry_after = parameters.get("retry_after") if retry_after: raise RetryAfter(retry_after) message += f"\nThe server response contained unknown parameters: {parameters}" if code == HTTPStatus.FORBIDDEN: raise Forbidden(message) if code in (HTTPStatus.NOT_FOUND, HTTPStatus.UNAUTHORIZED): # TG returns 404 Not found for # 1) malformed tokens # 2) correct tokens but non-existing method, e.g. api.tg.org/botTOKEN/unkonwnMethod # We can basically rule out 2) since we don't let users make requests manually # TG returns 401 Unauthorized for correctly formatted tokens that are not valid raise InvalidToken(message) if code == HTTPStatus.BAD_REQUEST: raise BadRequest(message) if code == HTTPStatus.CONFLICT: raise Conflict(message) if code == HTTPStatus.BAD_GATEWAY: raise NetworkError(description or "Bad Gateway") raise NetworkError(f"{message} ({code})")
def _fakeSendMessage_with429Error_unknown(obj, *args, **kwargs): raise NetworkError("Too Many Requests: retry after 666")
def _fakesendDocument_with429Error_deep(obj, *args, **kwargs): global CURRENT_TIME if CURRENT_TIME < 90: raise NetworkError("Too Many Requests: retry after 10 (429)") else: return "SUCCESS_DEEP"
class TestErrors: def test_telegram_error(self): with pytest.raises(TelegramError, match="^test message$"): raise TelegramError("test message") with pytest.raises(TelegramError, match="^Test message$"): raise TelegramError("Error: test message") with pytest.raises(TelegramError, match="^Test message$"): raise TelegramError("[Error]: test message") with pytest.raises(TelegramError, match="^Test message$"): raise TelegramError("Bad Request: test message") def test_unauthorized(self): with pytest.raises(Unauthorized, match="test message"): raise Unauthorized("test message") with pytest.raises(Unauthorized, match="^Test message$"): raise Unauthorized("Error: test message") with pytest.raises(Unauthorized, match="^Test message$"): raise Unauthorized("[Error]: test message") with pytest.raises(Unauthorized, match="^Test message$"): raise Unauthorized("Bad Request: test message") def test_invalid_token(self): with pytest.raises(InvalidToken, match="Invalid token"): raise InvalidToken def test_network_error(self): with pytest.raises(NetworkError, match="test message"): raise NetworkError("test message") with pytest.raises(NetworkError, match="^Test message$"): raise NetworkError("Error: test message") with pytest.raises(NetworkError, match="^Test message$"): raise NetworkError("[Error]: test message") with pytest.raises(NetworkError, match="^Test message$"): raise NetworkError("Bad Request: test message") def test_bad_request(self): with pytest.raises(BadRequest, match="test message"): raise BadRequest("test message") with pytest.raises(BadRequest, match="^Test message$"): raise BadRequest("Error: test message") with pytest.raises(BadRequest, match="^Test message$"): raise BadRequest("[Error]: test message") with pytest.raises(BadRequest, match="^Test message$"): raise BadRequest("Bad Request: test message") def test_timed_out(self): with pytest.raises(TimedOut, match="^Timed out$"): raise TimedOut def test_chat_migrated(self): with pytest.raises(ChatMigrated, match="Group migrated to supergroup. New chat id: 1234"): raise ChatMigrated(1234) try: raise ChatMigrated(1234) except ChatMigrated as e: assert e.new_chat_id == 1234 def test_retry_after(self): with pytest.raises(RetryAfter, match="Flood control exceeded. Retry in 12.0 seconds"): raise RetryAfter(12) def test_conflict(self): with pytest.raises(Conflict, match='Something something.'): raise Conflict('Something something.') @pytest.mark.parametrize( "exception, attributes", [ (TelegramError("test message"), ["message"]), (Unauthorized("test message"), ["message"]), (InvalidToken(), ["message"]), (NetworkError("test message"), ["message"]), (BadRequest("test message"), ["message"]), (TimedOut(), ["message"]), (ChatMigrated(1234), ["message", "new_chat_id"]), (RetryAfter(12), ["message", "retry_after"]), (Conflict("test message"), ["message"]), (TelegramDecryptionError("test message"), ["message"]) ], ) def test_errors_pickling(self, exception, attributes): print(exception) pickled = pickle.dumps(exception) unpickled = pickle.loads(pickled) assert type(unpickled) is type(exception) assert str(unpickled) == str(exception) for attribute in attributes: assert getattr(unpickled, attribute) == getattr(exception, attribute) def test_pickling_test_coverage(self): """ This test is only here to make sure that new errors will override __reduce__ properly. Add the new error class to the below covered_subclasses dict, if it's covered in the above test_errors_pickling test. """ def make_assertion(cls): assert {sc for sc in cls.__subclasses__()} == covered_subclasses[cls] for subcls in cls.__subclasses__(): make_assertion(subcls) covered_subclasses = defaultdict(set) covered_subclasses.update({ TelegramError: {Unauthorized, InvalidToken, NetworkError, ChatMigrated, RetryAfter, Conflict, TelegramDecryptionError}, NetworkError: {BadRequest, TimedOut} }) make_assertion(TelegramError)
def _fakeSendMessage_with500Error(obj, *args, **kwargs): raise NetworkError("Internal Server Error (500)")
async def do_request( self, url: str, method: str, request_data: RequestData = None, read_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, write_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, connect_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, pool_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, ) -> Tuple[int, bytes]: """See :meth:`BaseRequest.do_request`.""" if self._client.is_closed: raise RuntimeError("This HTTPXRequest is not initialized!") # If user did not specify timeouts (for e.g. in a bot method), use the default ones when we # created this instance. if isinstance(read_timeout, DefaultValue): read_timeout = self._client.timeout.read if isinstance(write_timeout, DefaultValue): write_timeout = self._client.timeout.write if isinstance(connect_timeout, DefaultValue): connect_timeout = self._client.timeout.connect if isinstance(pool_timeout, DefaultValue): pool_timeout = self._client.timeout.pool timeout = httpx.Timeout( connect=connect_timeout, read=read_timeout, write=write_timeout, pool=pool_timeout, ) # TODO p0: On Linux, use setsockopt to properly set socket level keepalive. # (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 120) # (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) # (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 8) # TODO p4: Support setsockopt on lesser platforms than Linux. files = request_data.multipart_data if request_data else None data = request_data.json_parameters if request_data else None try: res = await self._client.request( method=method, url=url, headers={"User-Agent": self.USER_AGENT}, timeout=timeout, files=files, data=data, ) except httpx.TimeoutException as err: if isinstance(err, httpx.PoolTimeout): raise TimedOut(message=( "Pool timeout: All connections in the connection pool are occupied. " "Request was *not* sent to Telegram. Consider adjusting the connection " "pool size or the pool timeout.")) from err raise TimedOut from err except httpx.HTTPError as err: # HTTPError must come last as its the base httpx exception class # TODO p4: do something smart here; for now just raise NetworkError raise NetworkError(f"httpx HTTPError: {err}") from err return res.status_code, res.content
def _fakeSendMessage_with429Error_deep_stack_overflow(obj, *args, **kwargs): global CURRENT_TIME if CURRENT_TIME < 1000: raise NetworkError("Too Many Requests: retry after 1 (429)") else: return "SUCCESS_DEEP"
def _fakesendDocument_with500Error(obj, *args, **kwargs): raise NetworkError("Internal Server Error (500)")