From c2ef2b624f9fb9038dd9d97dc5f09776297aea2d Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:23:58 +0000 Subject: [PATCH 1/2] CG-17442: Add safe_add_reaction utility to handle already_reacted Slack API errors --- src/codegen/extensions/slack/utils/README.md | 37 +++++++++ .../extensions/slack/utils/__init__.py | 49 ++++++++++++ .../extensions/slack/utils/__init__.py | 0 .../extensions/slack/utils/test_utils.py | 78 +++++++++++++++++++ 4 files changed, 164 insertions(+) create mode 100644 src/codegen/extensions/slack/utils/README.md create mode 100644 src/codegen/extensions/slack/utils/__init__.py create mode 100644 tests/unit/codegen/extensions/slack/utils/__init__.py create mode 100644 tests/unit/codegen/extensions/slack/utils/test_utils.py diff --git a/src/codegen/extensions/slack/utils/README.md b/src/codegen/extensions/slack/utils/README.md new file mode 100644 index 000000000..9a8399812 --- /dev/null +++ b/src/codegen/extensions/slack/utils/README.md @@ -0,0 +1,37 @@ +# Slack Utilities + +This module provides utility functions for working with the Slack API in a safe and reliable way. + +## Features + +### `safe_add_reaction` + +A utility function that safely adds a reaction to a Slack message, handling the case where the reaction already exists. + +This function is particularly useful for preventing the `SlackApiError` with the error message `already_reacted` that can occur when trying to add a reaction that already exists on a message. + +### Usage + +```python +from slack_sdk import WebClient +from codegen.extensions.slack.utils import safe_add_reaction + +client = WebClient(token="your-token") + +# Safely add a reaction +response = safe_add_reaction( + client=client, + channel="C12345", + timestamp="1234567890.123456", + name="thumbsup" +) + +# The function will not raise an exception if the reaction already exists +``` + +## Error Handling + +The `safe_add_reaction` function handles the following error cases: + +- If the reaction already exists (`already_reacted` error), it logs the information and returns a success response. +- For all other errors, it logs the error and re-raises the exception for proper handling upstream. diff --git a/src/codegen/extensions/slack/utils/__init__.py b/src/codegen/extensions/slack/utils/__init__.py new file mode 100644 index 000000000..76ab8fd15 --- /dev/null +++ b/src/codegen/extensions/slack/utils/__init__.py @@ -0,0 +1,49 @@ +"""Utility functions for Slack integration.""" + +import logging +from typing import Any, Dict, Optional + +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) +logger.setLevel(logging.INFO) + + +def safe_add_reaction( + client: WebClient, + channel: str, + timestamp: str, + name: str, +) -> Dict[str, Any]: + """ + Safely add a reaction to a Slack message, handling the case where the reaction already exists. + + Args: + client: The Slack WebClient instance + channel: The channel ID where the message is located + timestamp: The timestamp of the message + name: The name of the reaction emoji to add + + Returns: + The response from the Slack API or an error response + """ + try: + return client.reactions_add( + channel=channel, + timestamp=timestamp, + name=name, + ) + except SlackApiError as e: + # If the error is "already_reacted", just log it and continue + if e.response["error"] == "already_reacted": + logger.info( + f"Reaction '{name}' already exists on message in channel {channel} at {timestamp}. Ignoring." + ) + # Return a success response to prevent further error handling + return {"ok": True, "already_exists": True} + # For other errors, re-raise + logger.error(f"Error adding reaction: {e}") + raise diff --git a/tests/unit/codegen/extensions/slack/utils/__init__.py b/tests/unit/codegen/extensions/slack/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/codegen/extensions/slack/utils/test_utils.py b/tests/unit/codegen/extensions/slack/utils/test_utils.py new file mode 100644 index 000000000..ac6bcebdc --- /dev/null +++ b/tests/unit/codegen/extensions/slack/utils/test_utils.py @@ -0,0 +1,78 @@ +"""Tests for Slack utility functions.""" + +import pytest +from unittest.mock import MagicMock, patch + +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +from codegen.extensions.slack.utils import safe_add_reaction + + +def test_safe_add_reaction_success(): + """Test that safe_add_reaction works correctly when the API call succeeds.""" + mock_client = MagicMock(spec=WebClient) + mock_client.reactions_add.return_value = {"ok": True} + + result = safe_add_reaction( + client=mock_client, + channel="C12345", + timestamp="1234567890.123456", + name="thumbsup" + ) + + assert result == {"ok": True} + mock_client.reactions_add.assert_called_once_with( + channel="C12345", + timestamp="1234567890.123456", + name="thumbsup" + ) + + +def test_safe_add_reaction_already_reacted(): + """Test that safe_add_reaction handles the 'already_reacted' error correctly.""" + mock_client = MagicMock(spec=WebClient) + + # Create a mock SlackApiError with the 'already_reacted' error + mock_response = {"ok": False, "error": "already_reacted"} + mock_error = SlackApiError(message="already_reacted", response=mock_response) + mock_client.reactions_add.side_effect = mock_error + + result = safe_add_reaction( + client=mock_client, + channel="C12345", + timestamp="1234567890.123456", + name="thumbsup" + ) + + # Should return a success response with already_exists=True + assert result == {"ok": True, "already_exists": True} + mock_client.reactions_add.assert_called_once_with( + channel="C12345", + timestamp="1234567890.123456", + name="thumbsup" + ) + + +def test_safe_add_reaction_other_error(): + """Test that safe_add_reaction re-raises other errors.""" + mock_client = MagicMock(spec=WebClient) + + # Create a mock SlackApiError with a different error + mock_response = {"ok": False, "error": "invalid_auth"} + mock_error = SlackApiError(message="invalid_auth", response=mock_response) + mock_client.reactions_add.side_effect = mock_error + + with pytest.raises(SlackApiError): + safe_add_reaction( + client=mock_client, + channel="C12345", + timestamp="1234567890.123456", + name="thumbsup" + ) + + mock_client.reactions_add.assert_called_once_with( + channel="C12345", + timestamp="1234567890.123456", + name="thumbsup" + ) From 59662be9e2110fd03e82aed7c55f63a9e814caf6 Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:26:19 +0000 Subject: [PATCH 2/2] Automated pre-commit update --- src/codegen/extensions/slack/utils/README.md | 7 +-- .../extensions/slack/utils/__init__.py | 15 ++--- .../extensions/slack/utils/test_utils.py | 59 +++++-------------- 3 files changed, 23 insertions(+), 58 deletions(-) diff --git a/src/codegen/extensions/slack/utils/README.md b/src/codegen/extensions/slack/utils/README.md index 9a8399812..f404809cf 100644 --- a/src/codegen/extensions/slack/utils/README.md +++ b/src/codegen/extensions/slack/utils/README.md @@ -19,12 +19,7 @@ from codegen.extensions.slack.utils import safe_add_reaction client = WebClient(token="your-token") # Safely add a reaction -response = safe_add_reaction( - client=client, - channel="C12345", - timestamp="1234567890.123456", - name="thumbsup" -) +response = safe_add_reaction(client=client, channel="C12345", timestamp="1234567890.123456", name="thumbsup") # The function will not raise an exception if the reaction already exists ``` diff --git a/src/codegen/extensions/slack/utils/__init__.py b/src/codegen/extensions/slack/utils/__init__.py index 76ab8fd15..f6d3db86c 100644 --- a/src/codegen/extensions/slack/utils/__init__.py +++ b/src/codegen/extensions/slack/utils/__init__.py @@ -17,16 +17,15 @@ def safe_add_reaction( channel: str, timestamp: str, name: str, -) -> Dict[str, Any]: - """ - Safely add a reaction to a Slack message, handling the case where the reaction already exists. - +) -> dict[str, Any]: + """Safely add a reaction to a Slack message, handling the case where the reaction already exists. + Args: client: The Slack WebClient instance channel: The channel ID where the message is located timestamp: The timestamp of the message name: The name of the reaction emoji to add - + Returns: The response from the Slack API or an error response """ @@ -39,11 +38,9 @@ def safe_add_reaction( except SlackApiError as e: # If the error is "already_reacted", just log it and continue if e.response["error"] == "already_reacted": - logger.info( - f"Reaction '{name}' already exists on message in channel {channel} at {timestamp}. Ignoring." - ) + logger.info(f"Reaction '{name}' already exists on message in channel {channel} at {timestamp}. Ignoring.") # Return a success response to prevent further error handling return {"ok": True, "already_exists": True} # For other errors, re-raise - logger.error(f"Error adding reaction: {e}") + logger.exception(f"Error adding reaction: {e}") raise diff --git a/tests/unit/codegen/extensions/slack/utils/test_utils.py b/tests/unit/codegen/extensions/slack/utils/test_utils.py index ac6bcebdc..8aa877904 100644 --- a/tests/unit/codegen/extensions/slack/utils/test_utils.py +++ b/tests/unit/codegen/extensions/slack/utils/test_utils.py @@ -1,8 +1,8 @@ """Tests for Slack utility functions.""" -import pytest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock +import pytest from slack_sdk import WebClient from slack_sdk.errors import SlackApiError @@ -13,66 +13,39 @@ def test_safe_add_reaction_success(): """Test that safe_add_reaction works correctly when the API call succeeds.""" mock_client = MagicMock(spec=WebClient) mock_client.reactions_add.return_value = {"ok": True} - - result = safe_add_reaction( - client=mock_client, - channel="C12345", - timestamp="1234567890.123456", - name="thumbsup" - ) - + + result = safe_add_reaction(client=mock_client, channel="C12345", timestamp="1234567890.123456", name="thumbsup") + assert result == {"ok": True} - mock_client.reactions_add.assert_called_once_with( - channel="C12345", - timestamp="1234567890.123456", - name="thumbsup" - ) + mock_client.reactions_add.assert_called_once_with(channel="C12345", timestamp="1234567890.123456", name="thumbsup") def test_safe_add_reaction_already_reacted(): """Test that safe_add_reaction handles the 'already_reacted' error correctly.""" mock_client = MagicMock(spec=WebClient) - + # Create a mock SlackApiError with the 'already_reacted' error mock_response = {"ok": False, "error": "already_reacted"} mock_error = SlackApiError(message="already_reacted", response=mock_response) mock_client.reactions_add.side_effect = mock_error - - result = safe_add_reaction( - client=mock_client, - channel="C12345", - timestamp="1234567890.123456", - name="thumbsup" - ) - + + result = safe_add_reaction(client=mock_client, channel="C12345", timestamp="1234567890.123456", name="thumbsup") + # Should return a success response with already_exists=True assert result == {"ok": True, "already_exists": True} - mock_client.reactions_add.assert_called_once_with( - channel="C12345", - timestamp="1234567890.123456", - name="thumbsup" - ) + mock_client.reactions_add.assert_called_once_with(channel="C12345", timestamp="1234567890.123456", name="thumbsup") def test_safe_add_reaction_other_error(): """Test that safe_add_reaction re-raises other errors.""" mock_client = MagicMock(spec=WebClient) - + # Create a mock SlackApiError with a different error mock_response = {"ok": False, "error": "invalid_auth"} mock_error = SlackApiError(message="invalid_auth", response=mock_response) mock_client.reactions_add.side_effect = mock_error - + with pytest.raises(SlackApiError): - safe_add_reaction( - client=mock_client, - channel="C12345", - timestamp="1234567890.123456", - name="thumbsup" - ) - - mock_client.reactions_add.assert_called_once_with( - channel="C12345", - timestamp="1234567890.123456", - name="thumbsup" - ) + safe_add_reaction(client=mock_client, channel="C12345", timestamp="1234567890.123456", name="thumbsup") + + mock_client.reactions_add.assert_called_once_with(channel="C12345", timestamp="1234567890.123456", name="thumbsup")