def iter_rate(response: SlackResponse): """ Rate-limited aware iterator generator ... iterator rate ... iter rate. Puns are important in life. """ # SlackResponse needs to initialize the internal iterator count and initial data response.__iter__() while True: try: yield next(response) except StopIteration: # Reached the end of the Slack data, time to end the loop break except SlackApiError as ex: # According to the internals of SlackResponse, the internal data and iterator counter # will not be updated if the reaponse._client._request raises an exception. Thus, it should # be possible to just wait the required cool-off period and the next iteration of the loop # should still be at the same spot. if ex.response.status_code == 429 and "Retry-After" in ex.response.headers: # If Slack is rate-limiting our calls, wait the required time wait_period = float(ex.response.headers["Retry-After"]) warning( "Rate limited, waiting for {}s".format(wait_period)) time.sleep(wait_period) else: # Only suppress the SlackApiError when we're rate-limited raise
def mock_rtm_response(): coro = Mock(name="RTMResponse") data = { "client": ANY, "http_verb": ANY, "api_url": ANY, "req_args": ANY, "data": { "ok": True, "url": "ws://localhost:8765", "self": { "id": "U01234ABC", "name": "robotoverlord" }, "team": { "domain": "exampledomain", "id": "T123450FP", "name": "ExampleName", }, }, "headers": ANY, "status_code": 200, } coro.return_value = SlackResponse(**data) corofunc = Mock(name="mock_rtm_response", side_effect=asyncio.coroutine(coro)) corofunc.coro = coro return corofunc
async def _send(self, http_verb, api_url, req_args): """Sends the request out for transmission. Args: http_verb (str): The HTTP verb. e.g. 'GET' or 'POST'. api_url (str): The Slack API url. e.g. 'https://slack.com/api/chat.postMessage' req_args (dict): The request arguments to be attached to the request. e.g. { json: { 'attachments': [{"pretext": "pre-hello", "text": "text-world"}], 'channel': '#random' } } Returns: The response parsed into a SlackResponse object. """ res = await self._request(http_verb=http_verb, api_url=api_url, req_args=req_args) data = { "client": self, "http_verb": http_verb, "api_url": api_url, "req_args": req_args, } return SlackResponse(**{**data, **res}).validate()
def test_send_message_raise_error_false(): channel = "#fake_channel_name" message = "This is a test message." response_data = { "type": "SlackError", "text": f'The message: "{message}" failed to send to channel: {channel}', "details": "channel_not_found", } mock_client = WebClient() mock_response = SlackResponse( client=mock_client, http_verb="POST", api_url="http://localhost:3000/api.test", req_args={}, data=response_data, headers={}, status_code=200, ) mock_client.chat_postMessage = MagicMock(return_value=mock_response) response = notifications.slack.send_message( slack_client=mock_client, channel=channel, message=message, raise_error=False, ) assert response == { "type": "SlackError", "text": f'The message: "{message}" failed to send to channel: {channel}', "details": "channel_not_found", }
def test_roulette_endpoint(): mock_client = Mock(LazySlackClient) response_data = { "ok": True, "members": ["U023BECGF", "U061F7AUR", "W012A3CDE"], "response_metadata": { "next_cursor": "e3VzZXJfaWQ6IFcxMjM0NTY3fQ==" } } response = SlackResponse(client=mock_client, http_verb="POST", api_url="", req_args={}, data=response_data, headers={}, status_code=200) app.slack_client = mock_client mock_client.conversations_members.return_value = response app.current_request = www_request_of( data={"channel_id": "1234"}, headers={"Content-Type": "application/x-www-form-urlencoded"}) resp = roulette() assert resp text = resp["text"] assert "How about chats between" in text assert BULLET in text
def _slack_response(token, data): return SlackResponse(client=WebClient(token=token), http_verb="POST", api_url="http://localhost:3000/api.test", req_args={}, data=data, headers={}, status_code=200)
def build_slack_response(self, data: Dict[str, Any]) -> SlackResponse: """Build a fake SlackResponse containing the given data.""" response_data = copy.deepcopy(data) if "ok" not in response_data: response_data["ok"] = True return SlackResponse( client=self, http_verb="GET", api_url="/mock", req_args={}, data=response_data, headers={}, status_code=200, )
async def post_message_to_slack(self, message: SlackMessage) -> SlackResponse: if not message: return None request_content = { "token": self.options.slack_bot_token, "channel": message.channel, "text": message.text, } if message.thread_ts: request_content["thread_ts"] = message.thread_ts if message.blocks: request_content["blocks"] = json.dumps(message.blocks) session = aiohttp.ClientSession( timeout=aiohttp.ClientTimeout(total=30), ) http_verb = "POST" api_url = POST_EPHEMERAL_MESSAGE_URL if message.ephemeral else POST_MESSAGE_URL req_args = {"data": request_content} async with session.request(http_verb, api_url, **req_args) as res: response_content = {} try: response_content = await res.json() except aiohttp.ContentTypeError: pass response_data = { "data": response_content, "headers": res.headers, "status_code": res.status, } data = { "client": self, "http_verb": http_verb, "api_url": api_url, "req_args": req_args, } response = SlackResponse(**{**data, **response_data}).validate() await session.close() return response
async def _send(self, http_verb, api_url, req_args): """Sends the request out for transmission. Args: http_verb (str): The HTTP verb. e.g. 'GET' or 'POST'. api_url (str): The Slack API url. e.g. 'https://slack.com/api/chat.postMessage' req_args (dict): The request arguments to be attached to the request. e.g. { json: { 'attachments': [{"pretext": "pre-hello", "text": "text-world"}], 'channel': '#random' } } Returns: The response parsed into a SlackResponse object. """ open_files = [] files = req_args.pop("files", None) if files is not None: for k, v in files.items(): if isinstance(v, str): f = open(v.encode("utf-8", "ignore"), "rb") open_files.append(f) req_args["data"].update({k: f}) else: req_args["data"].update({k: v}) if "params" in req_args: # True/False -> "1"/"0" req_args["params"] = convert_bool_to_0_or_1(req_args["params"]) res = await self._request( http_verb=http_verb, api_url=api_url, req_args=req_args ) for f in open_files: f.close() data = { "client": self, "http_verb": http_verb, "api_url": api_url, "req_args": req_args, "use_sync_aiohttp": self.use_sync_aiohttp, } return SlackResponse(**{**data, **res}).validate()
def _mock_response(channel: str, text: str) -> SlackResponse: """ Simulate response for a call to chat_PostMessage() in the Slack WebClient https://python-slackclient.readthedocs.io/en/latest/basic_usage.html Args: channel: The slack channel to post to (ignored in mock response) text: The body of the message in the response Returns: A JSON SlackResponse object """ mock_client = WebClient() response = { "bot_id": "BOT_ID", "type": "message", "text": text, "user": "******", "team": "TEAM", "bot_profile": { "id": "BOT_ID", "deleted": False, "name": "Tamr Jobs Watch", "updated": 1593130492, "app_id": "APP_ID", "icons": { "image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png", "image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png", "image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png", }, "team_id": "TEAM", }, } response_data = {"message": response} mock_response = SlackResponse( client=mock_client, http_verb="POST", api_url="http://localhost:3000/api.test", req_args={}, data=response_data, headers={}, status_code=200, ) return mock_response
def test_issue_559(self): response = SlackResponse( client=WebClient(token="xoxb-dummy"), http_verb="POST", api_url="http://localhost:3000/api.test", req_args={}, data={ "ok": True, "args": { "hello": "world" } }, headers={}, status_code=200, ) self.assertTrue("ok" in response.data) self.assertTrue("args" in response.data) self.assertFalse("error" in response.data)
async def _send( self, http_verb: str, api_url: str, req_args: dict ) -> SlackResponse: """Sends the request out for transmission. Args: http_verb (str): The HTTP verb. e.g. 'GET' or 'POST'. api_url (str): The Slack API url. e.g. 'https://slack.com/api/chat.postMessage' req_args (dict): The request arguments to be attached to the request. e.g. { json: { 'attachments': [{"pretext": "pre-hello", "text": "text-world"}], 'channel': '#random' } } Returns: The response parsed into a SlackResponse object. """ open_files = _files_to_data(req_args) try: if "params" in req_args: # True/False -> "1"/"0" req_args["params"] = convert_bool_to_0_or_1(req_args["params"]) res = await self._request( http_verb=http_verb, api_url=api_url, req_args=req_args ) finally: for f in open_files: f.close() data = { "client": self, "http_verb": http_verb, "api_url": api_url, "req_args": req_args, "use_sync_aiohttp": self.use_sync_aiohttp, } return SlackResponse(**{**data, **res}).validate()
def _urllib_api_call( self, *, token: str = None, url: str, query_params: Dict[str, str] = {}, json_body: Dict = {}, body_params: Dict[str, str] = {}, files: Dict[str, io.BytesIO] = {}, additional_headers: Dict[str, str] = {}, ) -> SlackResponse: """Performs a Slack API request and returns the result. :param token: Slack API Token (either bot token or user token) :param url: a complete URL (e.g., https://www.slack.com/api/chat.postMessage) :param query_params: query string :param json_body: json data structure (it's still a dict at this point), if you give this argument, body_params and files will be skipped :param body_params: form params :param files: files to upload :param additional_headers: request headers to append :return: API response """ files_to_close: List[BinaryIO] = [] try: # True/False -> "1"/"0" query_params = convert_bool_to_0_or_1(query_params) body_params = convert_bool_to_0_or_1(body_params) if self._logger.level <= logging.DEBUG: def convert_params(values: dict) -> dict: if not values or not isinstance(values, dict): return {} return { k: ("(bytes)" if isinstance(v, bytes) else v) for k, v in values.items() } headers = { k: "(redacted)" if k.lower() == "authorization" else v for k, v in additional_headers.items() } self._logger.debug( f"Sending a request - url: {url}, " f"query_params: {convert_params(query_params)}, " f"body_params: {convert_params(body_params)}, " f"files: {convert_params(files)}, " f"json_body: {json_body}, " f"headers: {headers}") request_data = {} if files is not None and isinstance(files, dict) and len(files) > 0: if body_params: for k, v in body_params.items(): request_data.update({k: v}) for k, v in files.items(): if isinstance(v, str): f: BinaryIO = open(v.encode("utf-8", "ignore"), "rb") files_to_close.append(f) request_data.update({k: f}) elif isinstance(v, (bytearray, bytes)): request_data.update({k: io.BytesIO(v)}) else: request_data.update({k: v}) request_headers = self._build_urllib_request_headers( token=token or self.token, has_json=json is not None, has_files=files is not None, additional_headers=additional_headers, ) request_args = { "headers": request_headers, "data": request_data, "params": body_params, "files": files, "json": json_body, } if query_params: q = urlencode(query_params) url = f"{url}&{q}" if "?" in url else f"{url}?{q}" response = self._perform_urllib_http_request(url=url, args=request_args) if response.get("body"): try: response_body_data: dict = json.loads(response["body"]) except json.decoder.JSONDecodeError as e: message = f"Failed to parse the response body: {str(e)}" raise err.SlackApiError(message, response) else: response_body_data: dict = None if query_params: all_params = copy.copy(body_params) all_params.update(query_params) else: all_params = body_params request_args["params"] = all_params # for backward-compatibility return SlackResponse( client=self, http_verb="POST", # you can use POST method for all the Web APIs api_url=url, req_args=request_args, data=response_body_data, headers=dict(response["headers"]), status_code=response["status"], use_sync_aiohttp=False, ).validate() finally: for f in files_to_close: if not f.closed: f.close()
def api_call( self, *, token: str = None, url: str, query_params: Dict[str, str] = dict(), json_body: Dict = dict(), body_params: Dict[str, str] = dict(), files: Dict[str, io.BytesIO] = dict(), additional_headers: Dict[str, str] = dict(), ) -> SlackResponse: """Performs a Slack API request and returns the result. :param token: Slack API Token (either bot token or user token) :param url: a complete URL (e.g., https://www.slack.com/api/chat.postMessage) :param query_params: query string :param json_body: json data structure (it's still a dict at this point), if you give this argument, body_params and files will be skipped :param body_params: form params :param files: files to upload :param additional_headers: request headers to append :return: API response """ show_2020_01_deprecation(self._to_api_method(url)) files_to_close: List[BinaryIO] = [] try: # True/False -> "1"/"0" query_params = convert_bool_to_0_or_1(query_params) body_params = convert_bool_to_0_or_1(body_params) if self.logger.level <= logging.DEBUG: self.logger.debug(f"Slack API Request - url: {url}, " f"query_params: {query_params}, " f"json_body: {json_body}, " f"body_params: {body_params}, " f"files: {files}, " f"additional_headers: {additional_headers}") request_data = {} if files: if body_params: for k, v in body_params.items(): request_data.update({k: v}) for k, v in files.items(): if isinstance(v, str): f: BinaryIO = open(v.encode("ascii", "ignore"), "rb") files_to_close.append(f) request_data.update({k: f}) else: request_data.update({k: v}) request_headers = self._build_request_headers( token=token or self.token, has_json=json is not None, has_files=files is not None, additional_headers=additional_headers, ) request_args = { "headers": request_headers, "data": request_data, "params": body_params, "files": files, "json": json_body, } if query_params: q = urlencode(query_params) url = f"{url}&{q}" if "?" in url else f"{url}?{q}" response, response_body = self._perform_http_request( url=url, args=request_args) if response_body: response_body_data: dict = json.loads(response_body) else: response_body_data: dict = None if query_params: all_params = copy.copy(body_params) all_params.update(query_params) else: all_params = body_params request_args["params"] = all_params # for backward-compatibility return SlackResponse( client=self.web_client, http_verb="POST", # you can use POST method for all the Web APIs api_url=url, req_args=request_args, data=response_body_data, headers=dict(response.headers), status_code=response.status, use_sync_aiohttp=False, ).validate() finally: for f in files_to_close: if not f.closed: f.close()