Repository: slackapi/python-slack-sdk
Pull request #1367 (2023-05-11 18:02:16)

Add built in server error retry handlers
## Summary

This pull request resolves https://github.com/slackapi/python-slack-sdk/issues/1364

### Category (place an `x` in each of the `[ ]`)

- [x] **slack_sdk.web.WebClient (sync/async)** (Web API client)
- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender)
- [ ] **slack_sdk.socket_mode** (Socket Mode client)
- [ ] **slack_sdk.signature** (Request Signature Verifier)
- [ ] **slack_sdk.oauth** (OAuth Flow Utilities)
- [ ] **slack_sdk.models** (UI component builders)
- [ ] **slack_sdk.scim** (SCIM API client)
- [ ] **slack_sdk.audit_logs** (Audit Logs API client)
- [ ] **slack_sdk.rtm_v2** (RTM client)
- [ ] `/docs-src` (Documents, have you run `./scripts/docs.sh`?)
- [ ] `/docs-src-v2` (Documents, have you run `./scripts/docs-v2.sh`?)
- [ ] `/tutorial` (PythOnBoardingBot tutorial)
- [ ] `tests`/`integration_tests` (Automated tests for this library)

## Requirements (place an `x` in each `[ ]`)

- [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and have done my best effort to follow them.
- [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
- [x] I've run `python3 -m venv .venv && source .venv/bin/activate && ./scripts/run_validation.sh` after making the changes.


salesforce-cla[bot]: Thanks for the contribution!  Before we can merge this, we need @digitalnomd to [sign the Salesforce Inc. Contributor License Agreement](https://cla.salesforce.com/sign-cla?prUrl=https%3A%2F%2Fgithub.com%2Fslackapi%2Fpython-slack-sdk%2Fpull%2F1367).
seratch: We already have a few unit tests for the built-in retry handlers. If you could add some for the new handler, it'd be greatly appreciated. Otherwise, I can add some tests when merging your changes.

* https://github.com/slackapi/python-slack-sdk/blob/main/tests/slack_sdk/web/test_web_client_http_retry_server_error.py
* https://github.com/slackapi/python-slack-sdk/blob/main/tests/slack_sdk_async/web/test_async_web_client_http_retry.py
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/slackapi/python-slack-sdk/pull/1367?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) Report
> Merging [#1367](https://app.codecov.io/gh/slackapi/python-slack-sdk/pull/1367?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) (b0a9e1e) into [main](https://app.codecov.io/gh/slackapi/python-slack-sdk/commit/7eec685f630e0b9ffdeda11ff5e91998d4203055?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) (7eec685) will **decrease** coverage by `0.03%`.
> The diff coverage is `60.00%`.

> :exclamation: Current head b0a9e1e differs from pull request most recent head 3738060. Consider uploading reports for the commit 3738060 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main    #1367      +/-   ##
==========================================
- Coverage   85.58%   85.56%   -0.03%     
==========================================
  Files         111      111              
  Lines       11559    11569      +10     
==========================================
+ Hits         9893     9899       +6     
- Misses       1666     1670       +4     
```


| [Impacted Files](https://app.codecov.io/gh/slackapi/python-slack-sdk/pull/1367?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) | Coverage Δ | |
|---|---|---|
| [slack\_sdk/http\_retry/builtin\_async\_handlers.py](https://app.codecov.io/gh/slackapi/python-slack-sdk/pull/1367?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi#diff-c2xhY2tfc2RrL2h0dHBfcmV0cnkvYnVpbHRpbl9hc3luY19oYW5kbGVycy5weQ==) | `91.30% <60.00%> (-3.82%)` | :arrow_down: |
| [slack\_sdk/http\_retry/builtin\_handlers.py](https://app.codecov.io/gh/slackapi/python-slack-sdk/pull/1367?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi#diff-c2xhY2tfc2RrL2h0dHBfcmV0cnkvYnVpbHRpbl9oYW5kbGVycy5weQ==) | `91.48% <60.00%> (-3.75%)` | :arrow_down: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi)


Patch:
diff --git a/slack_sdk/http_retry/builtin_async_handlers.py b/slack_sdk/http_retry/builtin_async_handlers.py
index 9993f5e..b1e1095 100644
--- a/slack_sdk/http_retry/builtin_async_handlers.py
+++ b/slack_sdk/http_retry/builtin_async_handlers.py
@@ -86,5 +86,26 @@ class AsyncRateLimitErrorRetryHandler(AsyncRetryHandler):
         state.increment_current_attempt()
 
 
+class AsyncServerErrorRetryHandler(AsyncRetryHandler):
+    """RetryHandler that does retries for server errors."""
+
+    def __init__(
+        self,
+        max_retry_count: int = 1,
+        interval_calculator: RetryIntervalCalculator = default_interval_calculator,
+    ):
+        super().__init__(max_retry_count, interval_calculator)
+
+    async def _can_retry_async(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse],
+        error: Optional[Exception],
+    ) -> bool:
+        return response is not None and response.status_code in [500, 503]
+
+
 def async_default_handlers() -> List[AsyncRetryHandler]:
     return [AsyncConnectionErrorRetryHandler()]
diff --git a/slack_sdk/http_retry/builtin_handlers.py b/slack_sdk/http_retry/builtin_handlers.py
index d8ac13d..98ba7de 100644
--- a/slack_sdk/http_retry/builtin_handlers.py
+++ b/slack_sdk/http_retry/builtin_handlers.py
@@ -87,3 +87,24 @@ class RateLimitErrorRetryHandler(RetryHandler):
             duration = int(response.headers.get(retry_after_header_name)[0]) + random.random()
         time.sleep(duration)
         state.increment_current_attempt()
+
+
+class ServerErrorRetryHandler(RetryHandler):
+    """RetryHandler that does retries for server errors."""
+
+    def __init__(
+        self,
+        max_retry_count: int = 1,
+        interval_calculator: RetryIntervalCalculator = default_interval_calculator,
+    ):
+        super().__init__(max_retry_count, interval_calculator)
+
+    def _can_retry_async(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse],
+        error: Optional[Exception],
+    ) -> bool:
+        return response is not None and response.status_code in [500, 503]
diff --git a/slack_sdk/models/attachments/__init__.py b/slack_sdk/models/attachments/__init__.py
index fbcac21..c4ffe79 100644
--- a/slack_sdk/models/attachments/__init__.py
+++ b/slack_sdk/models/attachments/__init__.py
@@ -266,6 +266,7 @@ class Attachment(JsonObject):
         "author_icon",
         "author_link",
         "author_name",
+        "author_subname",
         "color",
         "fallback",
         "fields",
@@ -298,6 +299,7 @@ class Attachment(JsonObject):
         title_link: Optional[str] = None,
         pretext: Optional[str] = None,
         author_name: Optional[str] = None,
+        author_subname: Optional[str] = None,
         author_link: Optional[str] = None,
         author_icon: Optional[str] = None,
         image_url: Optional[str] = None,
@@ -333,6 +335,7 @@ class Attachment(JsonObject):
                 be formatted as plain text, or with markdown by including it in the
                 markdown_in parameter.
             author_name: Small text used to display the author's name.
+            author_subname: Small text used to display the author's sub name.
             author_link: A valid URL that will hyperlink the author_name text.
                 Will only work if author_name is present.
             author_icon: A valid URL that displays a small 16px by 16px image to
@@ -371,6 +374,7 @@ class Attachment(JsonObject):
         self.title_link = title_link
         self.color = color
         self.author_name = author_name
+        self.author_subname = author_subname
         self.author_link = author_link
         self.author_icon = author_icon
         self.image_url = image_url
@@ -484,6 +488,7 @@ class InteractiveAttachment(Attachment):
         title_link: Optional[str] = None,
         pretext: Optional[str] = None,
         author_name: Optional[str] = None,
+        author_subname: Optional[str] = None,
         author_link: Optional[str] = None,
         author_icon: Optional[str] = None,
         image_url: Optional[str] = None,
@@ -525,6 +530,7 @@ class InteractiveAttachment(Attachment):
                 be formatted as plain text, or with markdown by including it in the
                 markdown_in parameter.
             author_name: Small text used to display the author's name.
+            author_subname: Small text used to display the author's sub name.
             author_link: A valid URL that will hyperlink the author_name text.
                 Will only work if author_name is present.
             author_icon: A valid URL that displays a small 16px by 16px image to
@@ -565,6 +571,7 @@ class InteractiveAttachment(Attachment):
             title_link=title_link,
             color=color,
             author_name=author_name,
+            author_subname=author_subname,
             author_link=author_link,
             author_icon=author_icon,
             image_url=image_url,
diff --git a/slack_sdk/web/async_client.py b/slack_sdk/web/async_client.py
index caf45c4..4ed813d 100644
--- a/slack_sdk/web/async_client.py
+++ b/slack_sdk/web/async_client.py
@@ -4170,6 +4170,8 @@ class AsyncWebClient(AsyncBaseClient):
         count: Optional[Union[int, str]] = None,
         page: Optional[Union[int, str]] = None,
         team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
         **kwargs,
     ) -> AsyncSlackResponse:
         """Gets the access logs for the current team.
@@ -4181,6 +4183,8 @@ class AsyncWebClient(AsyncBaseClient):
                 "count": count,
                 "page": page,
                 "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
             }
         )
         return await self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
diff --git a/slack_sdk/web/client.py b/slack_sdk/web/client.py
index b0200f6..3964381 100644
--- a/slack_sdk/web/client.py
+++ b/slack_sdk/web/client.py
@@ -4161,6 +4161,8 @@ class WebClient(BaseClient):
         count: Optional[Union[int, str]] = None,
         page: Optional[Union[int, str]] = None,
         team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
         **kwargs,
     ) -> SlackResponse:
         """Gets the access logs for the current team.
@@ -4172,6 +4174,8 @@ class WebClient(BaseClient):
                 "count": count,
                 "page": page,
                 "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
             }
         )
         return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
diff --git a/slack_sdk/web/legacy_client.py b/slack_sdk/web/legacy_client.py
index 6c8dd59..c61b5f4 100644
--- a/slack_sdk/web/legacy_client.py
+++ b/slack_sdk/web/legacy_client.py
@@ -4172,6 +4172,8 @@ class LegacyWebClient(LegacyBaseClient):
         count: Optional[Union[int, str]] = None,
         page: Optional[Union[int, str]] = None,
         team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
         **kwargs,
     ) -> Union[Future, SlackResponse]:
         """Gets the access logs for the current team.
@@ -4183,6 +4185,8 @@ class LegacyWebClient(LegacyBaseClient):
                 "count": count,
                 "page": page,
                 "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
             }
         )
         return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)


Test patch:
diff --git a/tests/slack_sdk/models/test_attachments.py b/tests/slack_sdk/models/test_attachments.py
index 875302d..7fbce0e 100644
--- a/tests/slack_sdk/models/test_attachments.py
+++ b/tests/slack_sdk/models/test_attachments.py
@@ -38,6 +38,7 @@ class AttachmentTests(unittest.TestCase):
                 fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)],
                 color="#FFFF00",
                 author_name="John Doe",
+                author_subname="@jd",
                 author_link="http://johndoeisthebest.com",
                 author_icon="http://johndoeisthebest.com/avatar.jpg",
                 thumb_url="thumbnail URL",
@@ -49,6 +50,7 @@ class AttachmentTests(unittest.TestCase):
             {
                 "text": "attachment text",
                 "author_name": "John Doe",
+                "author_subname": "@jd",
                 "author_link": "http://johndoeisthebest.com",
                 "author_icon": "http://johndoeisthebest.com/avatar.jpg",
                 "footer": "and a footer",
@@ -155,6 +157,7 @@ class InteractiveAttachmentTests(unittest.TestCase):
                 fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)],
                 color="#FFFF00",
                 author_name="John Doe",
+                author_subname="@jd",
                 author_link="http://johndoeisthebest.com",
                 author_icon="http://johndoeisthebest.com/avatar.jpg",
                 thumb_url="thumbnail URL",
@@ -168,6 +171,7 @@ class InteractiveAttachmentTests(unittest.TestCase):
                 "callback_id": "cb_123",
                 "actions": [a.to_dict() for a in actions],
                 "author_name": "John Doe",
+                "author_subname": "@jd",
                 "author_link": "http://johndoeisthebest.com",
                 "author_icon": "http://johndoeisthebest.com/avatar.jpg",
                 "footer": "and a footer",