Browse Source

types(python): Slack Integration Types (#28236)

Marcos Gaeta 3 years ago
parent
commit
ee9f8c460e

+ 4 - 0
mypy.ini

@@ -22,8 +22,11 @@ files = src/sentry/api/bases/external_actor.py,
         src/sentry/grouping/strategies/base.py,
         src/sentry/grouping/strategies/base.py,
         src/sentry/grouping/strategies/message.py,
         src/sentry/grouping/strategies/message.py,
         src/sentry/grouping/result.py,
         src/sentry/grouping/result.py,
+        src/sentry/integrations/base.py,
+        src/sentry/integrations/slack/*.py,
         src/sentry/integrations/slack/message_builder/**/*.py,
         src/sentry/integrations/slack/message_builder/**/*.py,
         src/sentry/integrations/slack/requests/*.py,
         src/sentry/integrations/slack/requests/*.py,
+        src/sentry/integrations/slack/unfurl/*.py,
         src/sentry/integrations/slack/util/*.py,
         src/sentry/integrations/slack/util/*.py,
         src/sentry/integrations/slack/views/*.py,
         src/sentry/integrations/slack/views/*.py,
         src/sentry/killswitches.py,
         src/sentry/killswitches.py,
@@ -33,6 +36,7 @@ files = src/sentry/api/bases/external_actor.py,
         src/sentry/models/projectoption.py,
         src/sentry/models/projectoption.py,
         src/sentry/models/useroption.py,
         src/sentry/models/useroption.py,
         src/sentry/notifications/**/*.py,
         src/sentry/notifications/**/*.py,
+        src/sentry/shared_integrations/constants.py,
         src/sentry/snuba/outcomes.py,
         src/sentry/snuba/outcomes.py,
         src/sentry/snuba/query_subscription_consumer.py,
         src/sentry/snuba/query_subscription_consumer.py,
         src/sentry/tasks/app_store_connect.py,
         src/sentry/tasks/app_store_connect.py,

+ 67 - 44
src/sentry/integrations/base.py

@@ -10,9 +10,20 @@ import logging
 import sys
 import sys
 from collections import namedtuple
 from collections import namedtuple
 from enum import Enum
 from enum import Enum
+from typing import Any, Dict, FrozenSet, Mapping, Optional, Sequence
+from urllib.request import Request
 
 
+from django.views import View
+
+from sentry.db.models.manager import M
 from sentry.exceptions import InvalidIdentity
 from sentry.exceptions import InvalidIdentity
-from sentry.models import AuditLogEntryEvent, Identity, OrganizationIntegration
+from sentry.models import (
+    AuditLogEntryEvent,
+    Identity,
+    Integration,
+    Organization,
+    OrganizationIntegration,
+)
 from sentry.pipeline import PipelineProvider
 from sentry.pipeline import PipelineProvider
 from sentry.shared_integrations.constants import (
 from sentry.shared_integrations.constants import (
     ERR_INTERNAL,
     ERR_INTERNAL,
@@ -52,9 +63,9 @@ IntegrationMetadata = namedtuple(
 )
 )
 
 
 
 
-class IntegrationMetadata(IntegrationMetadata):
+class IntegrationMetadata(IntegrationMetadata):  # type: ignore
     @staticmethod
     @staticmethod
-    def feature_flag_name(f):
+    def feature_flag_name(f: Optional[str]) -> Optional[str]:
         """
         """
         FeatureDescriptions are set using the IntegrationFeatures constants,
         FeatureDescriptions are set using the IntegrationFeatures constants,
         however we expose them here as mappings to organization feature flags, thus
         however we expose them here as mappings to organization feature flags, thus
@@ -62,8 +73,9 @@ class IntegrationMetadata(IntegrationMetadata):
         """
         """
         if f is not None:
         if f is not None:
             return f"integrations-{f}"
             return f"integrations-{f}"
+        return None
 
 
-    def _asdict(self):
+    def _asdict(self) -> Dict[str, Sequence[Any]]:
         metadata = super()._asdict()
         metadata = super()._asdict()
         metadata["features"] = [
         metadata["features"] = [
             {
             {
@@ -103,7 +115,7 @@ class IntegrationFeatures(Enum):
     DEPLOYMENT = "deployment"
     DEPLOYMENT = "deployment"
 
 
 
 
-class IntegrationProvider(PipelineProvider):
+class IntegrationProvider(PipelineProvider):  # type: ignore
     """
     """
     An integration provider describes a third party that can be registered within Sentry.
     An integration provider describes a third party that can be registered within Sentry.
 
 
@@ -120,26 +132,26 @@ class IntegrationProvider(PipelineProvider):
     # a unique identifier (e.g. 'slack').
     # a unique identifier (e.g. 'slack').
     # Used to lookup sibling classes and the ``key`` used when creating
     # Used to lookup sibling classes and the ``key`` used when creating
     # Integration objects.
     # Integration objects.
-    key = None
+    key: Optional[str] = None
 
 
     # a unique identifier to use when creating the ``Integration`` object.
     # a unique identifier to use when creating the ``Integration`` object.
     # Only needed when you want to create the above object with something other
     # Only needed when you want to create the above object with something other
     # than ``key``. See: VstsExtensionIntegrationProvider.
     # than ``key``. See: VstsExtensionIntegrationProvider.
-    _integration_key = None
+    _integration_key: Optional[str] = None
 
 
     # Whether this integration should show up in the list on the Organization
     # Whether this integration should show up in the list on the Organization
     # Integrations page.
     # Integrations page.
     visible = True
     visible = True
 
 
     # a human readable name (e.g. 'Slack')
     # a human readable name (e.g. 'Slack')
-    name = None
+    name: Optional[str] = None
 
 
     # an IntegrationMetadata object, used to provide extra details in the
     # an IntegrationMetadata object, used to provide extra details in the
     # configuration interface of the integration.
     # configuration interface of the integration.
-    metadata = None
+    metadata: Optional[IntegrationMetadata] = None
 
 
     # an Integration class that will manage the functionality once installed
     # an Integration class that will manage the functionality once installed
-    integration_cls = None
+    integration_cls: Optional[Any] = None
 
 
     # configuration for the setup dialog
     # configuration for the setup dialog
     setup_dialog_config = {"width": 600, "height": 600}
     setup_dialog_config = {"width": 600, "height": 600}
@@ -157,29 +169,38 @@ class IntegrationProvider(PipelineProvider):
     needs_default_identity = False
     needs_default_identity = False
 
 
     # can be any number of IntegrationFeatures
     # can be any number of IntegrationFeatures
-    features = frozenset()
+    features: FrozenSet[IntegrationFeatures] = frozenset()
 
 
     # if this is hidden without the feature flag
     # if this is hidden without the feature flag
     requires_feature_flag = False
     requires_feature_flag = False
 
 
     @classmethod
     @classmethod
-    def get_installation(cls, model, organization_id, **kwargs):
+    def get_installation(cls, model: M, organization_id: int, **kwargs: Any) -> Any:
         if cls.integration_cls is None:
         if cls.integration_cls is None:
             raise NotImplementedError
             raise NotImplementedError
 
 
         return cls.integration_cls(model, organization_id, **kwargs)
         return cls.integration_cls(model, organization_id, **kwargs)
 
 
     @property
     @property
-    def integration_key(self):
+    def integration_key(self) -> Optional[str]:
         return self._integration_key or self.key
         return self._integration_key or self.key
 
 
-    def get_logger(self):
+    def get_logger(self) -> logging.Logger:
         return logging.getLogger(f"sentry.integration.{self.key}")
         return logging.getLogger(f"sentry.integration.{self.key}")
 
 
-    def post_install(self, integration, organization, extra=None):
+    def post_install(
+        self, integration: Integration, organization: Organization, extra: Optional[Any] = None
+    ) -> None:
         pass
         pass
 
 
-    def create_audit_log_entry(self, integration, organization, request, action, extra=None):
+    def create_audit_log_entry(
+        self,
+        integration: Integration,
+        organization: Organization,
+        request: Request,
+        action: str,
+        extra: Optional[Any] = None,
+    ) -> None:
         """
         """
         Creates an audit log entry for the newly installed integration.
         Creates an audit log entry for the newly installed integration.
         """
         """
@@ -192,7 +213,7 @@ class IntegrationProvider(PipelineProvider):
                 data={"provider": integration.provider, "name": integration.name},
                 data={"provider": integration.provider, "name": integration.name},
             )
             )
 
 
-    def get_pipeline_views(self):
+    def get_pipeline_views(self) -> Sequence[View]:
         """
         """
         Return a list of ``View`` instances describing this integration's
         Return a list of ``View`` instances describing this integration's
         configuration pipeline.
         configuration pipeline.
@@ -202,7 +223,7 @@ class IntegrationProvider(PipelineProvider):
         """
         """
         raise NotImplementedError
         raise NotImplementedError
 
 
-    def build_integration(self, state):
+    def build_integration(self, state: Mapping[str, Any]) -> Mapping[str, Any]:
         """
         """
         Given state captured during the setup pipeline, return a dictionary
         Given state captured during the setup pipeline, return a dictionary
         of configuration and metadata to store with this integration.
         of configuration and metadata to store with this integration.
@@ -221,7 +242,7 @@ class IntegrationProvider(PipelineProvider):
         >>>     return {
         >>>     return {
         >>>         'external_id': state['id'],
         >>>         'external_id': state['id'],
         >>>         'name': state['name'],
         >>>         'name': state['name'],
-        >>>         'metadata': {url': state['url']},
+        >>>         'metadata': {'url': state['url']},
         >>>     }
         >>>     }
 
 
         This can return the 'expect_exists' flag, and this method  will expect
         This can return the 'expect_exists' flag, and this method  will expect
@@ -237,7 +258,7 @@ class IntegrationProvider(PipelineProvider):
         """
         """
         raise NotImplementedError
         raise NotImplementedError
 
 
-    def setup(self):
+    def setup(self) -> None:
         """
         """
         Executed once Sentry has been initialized at runtime.
         Executed once Sentry has been initialized at runtime.
 
 
@@ -245,7 +266,7 @@ class IntegrationProvider(PipelineProvider):
         >>>     bindings.add('repository.provider', GitHubRepositoryProvider, key='github')
         >>>     bindings.add('repository.provider', GitHubRepositoryProvider, key='github')
         """
         """
 
 
-    def has_feature(self, feature):
+    def has_feature(self, feature: IntegrationFeatures) -> bool:
         return feature in self.features
         return feature in self.features
 
 
 
 
@@ -257,20 +278,20 @@ class IntegrationInstallation:
 
 
     logger = logging.getLogger("sentry.integrations")
     logger = logging.getLogger("sentry.integrations")
 
 
-    def __init__(self, model, organization_id):
+    def __init__(self, model: M, organization_id: int) -> None:
         self.model = model
         self.model = model
         self.organization_id = organization_id
         self.organization_id = organization_id
         self._org_integration = None
         self._org_integration = None
 
 
     @property
     @property
-    def org_integration(self):
+    def org_integration(self) -> OrganizationIntegration:
         if self._org_integration is None:
         if self._org_integration is None:
             self._org_integration = OrganizationIntegration.objects.get(
             self._org_integration = OrganizationIntegration.objects.get(
                 organization_id=self.organization_id, integration_id=self.model.id
                 organization_id=self.organization_id, integration_id=self.model.id
             )
             )
         return self._org_integration
         return self._org_integration
 
 
-    def get_organization_config(self):
+    def get_organization_config(self) -> Sequence[Any]:
         """
         """
         Returns a list of JSONForm configuration object descriptors used to
         Returns a list of JSONForm configuration object descriptors used to
         configure the integration per-organization. This simply represents the
         configure the integration per-organization. This simply represents the
@@ -280,7 +301,7 @@ class IntegrationInstallation:
         """
         """
         return []
         return []
 
 
-    def update_organization_config(self, data):
+    def update_organization_config(self, data: Mapping[str, Any]) -> None:
         """
         """
         Update the configuration field for an organization integration.
         Update the configuration field for an organization integration.
         """
         """
@@ -288,28 +309,26 @@ class IntegrationInstallation:
         config.update(data)
         config.update(data)
         self.org_integration.update(config=config)
         self.org_integration.update(config=config)
 
 
-    def get_config_data(self):
-        return self.org_integration.config
+    def get_config_data(self) -> Mapping[str, str]:
+        # Explicitly typing to satisfy mypy.
+        config_data: Mapping[str, str] = self.org_integration.config
+        return config_data
 
 
-    def get_dynamic_display_information(self):
+    def get_dynamic_display_information(self) -> Optional[Mapping[str, Any]]:
         return None
         return None
 
 
-    def get_client(self):
+    def get_client(self) -> Any:
         # Return the api client for a given provider
         # Return the api client for a given provider
         raise NotImplementedError
         raise NotImplementedError
 
 
-    def get_default_identity(self):
-        """
-        For Integrations that rely solely on user auth for authentication
-        """
-
-        identity = Identity.objects.get(id=self.org_integration.default_auth_id)
-        return identity
+    def get_default_identity(self) -> Identity:
+        """For Integrations that rely solely on user auth for authentication."""
+        return Identity.objects.get(id=self.org_integration.default_auth_id)
 
 
-    def error_message_from_json(self, data):
+    def error_message_from_json(self, data: Mapping[str, Any]) -> Any:
         return data.get("message", "unknown error")
         return data.get("message", "unknown error")
 
 
-    def error_fields_from_json(self, data):
+    def error_fields_from_json(self, data: Mapping[str, Any]) -> Optional[Any]:
         """
         """
         If we can determine error fields from the response JSON this should
         If we can determine error fields from the response JSON this should
         format and return them, allowing an IntegrationFormError to be raised.
         format and return them, allowing an IntegrationFormError to be raised.
@@ -319,11 +338,13 @@ class IntegrationInstallation:
         """
         """
         return None
         return None
 
 
-    def message_from_error(self, exc):
+    def message_from_error(self, exc: ApiError) -> str:
         if isinstance(exc, ApiUnauthorized):
         if isinstance(exc, ApiUnauthorized):
             return ERR_UNAUTHORIZED
             return ERR_UNAUTHORIZED
         elif isinstance(exc, ApiHostError):
         elif isinstance(exc, ApiHostError):
-            return exc.text
+            # Explicitly typing to satisfy mypy.
+            message: str = exc.text
+            return message
         elif isinstance(exc, UnsupportedResponseType):
         elif isinstance(exc, UnsupportedResponseType):
             return ERR_UNSUPPORTED_RESPONSE_TYPE.format(content_type=exc.content_type)
             return ERR_UNSUPPORTED_RESPONSE_TYPE.format(content_type=exc.content_type)
         elif isinstance(exc, ApiError):
         elif isinstance(exc, ApiError):
@@ -335,7 +356,7 @@ class IntegrationInstallation:
         else:
         else:
             return ERR_INTERNAL
             return ERR_INTERNAL
 
 
-    def raise_error(self, exc, identity=None):
+    def raise_error(self, exc: ApiError, identity: Optional[Identity] = None) -> None:
         if isinstance(exc, ApiUnauthorized):
         if isinstance(exc, ApiUnauthorized):
             raise InvalidIdentity(self.message_from_error(exc), identity=identity).with_traceback(
             raise InvalidIdentity(self.message_from_error(exc), identity=identity).with_traceback(
                 sys.exc_info()[2]
                 sys.exc_info()[2]
@@ -354,10 +375,12 @@ class IntegrationInstallation:
             raise IntegrationError(self.message_from_error(exc)).with_traceback(sys.exc_info()[2])
             raise IntegrationError(self.message_from_error(exc)).with_traceback(sys.exc_info()[2])
 
 
     @property
     @property
-    def metadata(self):
-        return self.model.metadata
+    def metadata(self) -> IntegrationMetadata:
+        # Explicitly typing to satisfy mypy.
+        _metadata: IntegrationMetadata = self.model.metadata
+        return _metadata
 
 
-    def uninstall(self):
+    def uninstall(self) -> None:
         """
         """
         For integrations that need additional steps for uninstalling
         For integrations that need additional steps for uninstalling
         that are not covered in the `delete_organization_integration`
         that are not covered in the `delete_organization_integration`

+ 2 - 1
src/sentry/integrations/slack/__init__.py

@@ -3,6 +3,7 @@ from sentry.utils.imports import import_submodules
 
 
 from .notify_action import SlackNotifyServiceAction
 from .notify_action import SlackNotifyServiceAction
 
 
-import_submodules(globals(), __name__, __path__)
+path = __path__  # type: ignore
+import_submodules(globals(), __name__, path)
 
 
 rules.add(SlackNotifyServiceAction)
 rules.add(SlackNotifyServiceAction)

+ 3 - 3
src/sentry/integrations/slack/analytics.py

@@ -1,13 +1,13 @@
 from sentry import analytics
 from sentry import analytics
 
 
 
 
-class SlackIntegrationAssign(analytics.Event):
+class SlackIntegrationAssign(analytics.Event):  # type: ignore
     type = "integrations.slack.assign"
     type = "integrations.slack.assign"
 
 
     attributes = (analytics.Attribute("actor_id", required=False),)
     attributes = (analytics.Attribute("actor_id", required=False),)
 
 
 
 
-class SlackIntegrationStatus(analytics.Event):
+class SlackIntegrationStatus(analytics.Event):  # type: ignore
     type = "integrations.slack.status"
     type = "integrations.slack.status"
 
 
     attributes = (
     attributes = (
@@ -17,7 +17,7 @@ class SlackIntegrationStatus(analytics.Event):
     )
     )
 
 
 
 
-class SlackIntegrationNotificationSent(analytics.Event):
+class SlackIntegrationNotificationSent(analytics.Event):  # type: ignore
     type = "integrations.slack.notification_sent"
     type = "integrations.slack.notification_sent"
 
 
     attributes = (
     attributes = (

+ 3 - 2
src/sentry/integrations/slack/client.py

@@ -4,13 +4,14 @@ from requests import Response
 from sentry_sdk.tracing import Transaction
 from sentry_sdk.tracing import Transaction
 
 
 from sentry.integrations.client import ApiClient
 from sentry.integrations.client import ApiClient
+from sentry.shared_integrations.client import BaseApiResponse
 from sentry.shared_integrations.exceptions import ApiError
 from sentry.shared_integrations.exceptions import ApiError
 from sentry.utils import metrics
 from sentry.utils import metrics
 
 
 SLACK_DATADOG_METRIC = "integrations.slack.http_response"
 SLACK_DATADOG_METRIC = "integrations.slack.http_response"
 
 
 
 
-class SlackClient(ApiClient):
+class SlackClient(ApiClient):  # type: ignore
     allow_redirects = False
     allow_redirects = False
     integration_name = "slack"
     integration_name = "slack"
     base_url = "https://slack.com/api"
     base_url = "https://slack.com/api"
@@ -74,7 +75,7 @@ class SlackClient(ApiClient):
         params: Optional[Mapping[str, Any]] = None,
         params: Optional[Mapping[str, Any]] = None,
         json: bool = False,
         json: bool = False,
         timeout: Optional[int] = None,
         timeout: Optional[int] = None,
-    ):
+    ) -> BaseApiResponse:
         # TODO(meredith): Slack actually supports json now for the chat.postMessage so we
         # TODO(meredith): Slack actually supports json now for the chat.postMessage so we
         # can update that so we don't have to pass json=False here
         # can update that so we don't have to pass json=False here
         response = self._request(method, path, headers=headers, data=data, params=params, json=json)
         response = self._request(method, path, headers=headers, data=data, params=params, json=json)

+ 14 - 8
src/sentry/integrations/slack/integration.py

@@ -1,6 +1,8 @@
 from collections import namedtuple
 from collections import namedtuple
+from typing import Any, Mapping, Optional, Sequence
 
 
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
+from django.views import View
 
 
 from sentry import features
 from sentry import features
 from sentry.identity.pipeline import IdentityProviderPipeline
 from sentry.identity.pipeline import IdentityProviderPipeline
@@ -12,9 +14,11 @@ from sentry.integrations import (
     IntegrationProvider,
     IntegrationProvider,
 )
 )
 from sentry.integrations.slack import tasks
 from sentry.integrations.slack import tasks
+from sentry.models import Integration, Organization
 from sentry.pipeline import NestedPipelineView
 from sentry.pipeline import NestedPipelineView
 from sentry.shared_integrations.exceptions import ApiError, IntegrationError
 from sentry.shared_integrations.exceptions import ApiError, IntegrationError
 from sentry.utils.http import absolute_uri
 from sentry.utils.http import absolute_uri
+from sentry.utils.json import JSONData
 
 
 from .client import SlackClient
 from .client import SlackClient
 from .utils import get_integration_type, logger
 from .utils import get_integration_type, logger
@@ -61,12 +65,12 @@ metadata = IntegrationMetadata(
 )
 )
 
 
 
 
-class SlackIntegration(IntegrationInstallation):
-    def get_config_data(self):
+class SlackIntegration(IntegrationInstallation):  # type: ignore
+    def get_config_data(self) -> Mapping[str, str]:
         return {"installationType": get_integration_type(self.model)}
         return {"installationType": get_integration_type(self.model)}
 
 
 
 
-class SlackIntegrationProvider(IntegrationProvider):
+class SlackIntegrationProvider(IntegrationProvider):  # type: ignore
     key = "slack"
     key = "slack"
     name = "Slack"
     name = "Slack"
     metadata = metadata
     metadata = metadata
@@ -100,7 +104,7 @@ class SlackIntegrationProvider(IntegrationProvider):
 
 
     setup_dialog_config = {"width": 600, "height": 900}
     setup_dialog_config = {"width": 600, "height": 900}
 
 
-    def get_pipeline_views(self):
+    def get_pipeline_views(self) -> Sequence[View]:
         identity_pipeline_config = {
         identity_pipeline_config = {
             "oauth_scopes": self.identity_oauth_scopes,
             "oauth_scopes": self.identity_oauth_scopes,
             "user_scopes": self.user_scopes,
             "user_scopes": self.user_scopes,
@@ -116,8 +120,8 @@ class SlackIntegrationProvider(IntegrationProvider):
 
 
         return [identity_pipeline_view]
         return [identity_pipeline_view]
 
 
-    def get_team_info(self, access_token):
-        headers = {"Authorization": "Bearer %s" % access_token}
+    def get_team_info(self, access_token: str) -> JSONData:
+        headers = {"Authorization": f"Bearer {access_token}"}
 
 
         client = SlackClient()
         client = SlackClient()
         try:
         try:
@@ -128,7 +132,7 @@ class SlackIntegrationProvider(IntegrationProvider):
 
 
         return resp["team"]
         return resp["team"]
 
 
-    def build_integration(self, state):
+    def build_integration(self, state: Mapping[str, Any]) -> Mapping[str, Any]:
         data = state["identity"]["data"]
         data = state["identity"]["data"]
         assert data["ok"]
         assert data["ok"]
 
 
@@ -164,7 +168,9 @@ class SlackIntegrationProvider(IntegrationProvider):
 
 
         return integration
         return integration
 
 
-    def post_install(self, integration, organization, extra=None):
+    def post_install(
+        self, integration: Integration, organization: Organization, extra: Optional[Any] = None
+    ) -> None:
         """
         """
         Create Identity records for an organization's users if their emails match in Sentry and Slack
         Create Identity records for an organization's users if their emails match in Sentry and Slack
         """
         """

+ 4 - 4
src/sentry/integrations/slack/message_builder/issues.py

@@ -1,5 +1,5 @@
 import re
 import re
-from typing import Any, List, Mapping, Optional, Sequence, Tuple, Union
+from typing import Any, List, Mapping, Optional, Sequence, Set, Tuple, Union
 
 
 from django.core.cache import cache
 from django.core.cache import cache
 
 
@@ -165,7 +165,7 @@ def build_footer(group: Group, project: Project, rules: Optional[Sequence[Rule]]
 
 
 
 
 def build_tag_fields(
 def build_tag_fields(
-    event_for_tags: Any, tags: Optional[Mapping[str, str]] = None
+    event_for_tags: Any, tags: Optional[Set[str]] = None
 ) -> Sequence[Mapping[str, Union[str, bool]]]:
 ) -> Sequence[Mapping[str, Union[str, bool]]]:
     fields = []
     fields = []
     if tags:
     if tags:
@@ -306,7 +306,7 @@ class SlackIssuesMessageBuilder(SlackMessageBuilder):
         self,
         self,
         group: Group,
         group: Group,
         event: Optional[Event] = None,
         event: Optional[Event] = None,
-        tags: Optional[Mapping[str, str]] = None,
+        tags: Optional[Set[str]] = None,
         identity: Optional[Identity] = None,
         identity: Optional[Identity] = None,
         actions: Optional[Sequence[Any]] = None,
         actions: Optional[Sequence[Any]] = None,
         rules: Optional[List[Rule]] = None,
         rules: Optional[List[Rule]] = None,
@@ -367,7 +367,7 @@ class SlackIssuesMessageBuilder(SlackMessageBuilder):
 def build_group_attachment(
 def build_group_attachment(
     group: Group,
     group: Group,
     event: Optional[Event] = None,
     event: Optional[Event] = None,
-    tags: Optional[Mapping[str, str]] = None,
+    tags: Optional[Set[str]] = None,
     identity: Optional[Identity] = None,
     identity: Optional[Identity] = None,
     actions: Optional[Sequence[Any]] = None,
     actions: Optional[Sequence[Any]] = None,
     rules: Optional[List[Rule]] = None,
     rules: Optional[List[Rule]] = None,

+ 3 - 3
src/sentry/integrations/slack/notifications.py

@@ -1,6 +1,6 @@
 import logging
 import logging
 from collections import defaultdict
 from collections import defaultdict
-from typing import AbstractSet, Any, Mapping, Set, Union
+from typing import AbstractSet, Any, Mapping, MutableMapping, Optional, Set, Union
 
 
 from sentry import analytics
 from sentry import analytics
 from sentry.integrations.slack.client import SlackClient  # NOQA
 from sentry.integrations.slack.client import SlackClient  # NOQA
@@ -84,7 +84,7 @@ def get_channel_and_integration_by_team(
 def get_channel_and_token_by_recipient(
 def get_channel_and_token_by_recipient(
     organization: Organization, recipients: AbstractSet[Union[User, Team]]
     organization: Organization, recipients: AbstractSet[Union[User, Team]]
 ) -> Mapping[Union[User, Team], Mapping[str, str]]:
 ) -> Mapping[Union[User, Team], Mapping[str, str]]:
-    output = defaultdict(dict)
+    output: MutableMapping[Union[User, Team], MutableMapping[str, str]] = defaultdict(dict)
     for recipient in recipients:
     for recipient in recipients:
         channels_to_integrations = (
         channels_to_integrations = (
             get_channel_and_integration_by_user(recipient, organization)
             get_channel_and_integration_by_user(recipient, organization)
@@ -123,7 +123,7 @@ def send_notification_as_slack(
     notification: BaseNotification,
     notification: BaseNotification,
     recipients: Union[Set[User], Set[Team]],
     recipients: Union[Set[User], Set[Team]],
     shared_context: Mapping[str, Any],
     shared_context: Mapping[str, Any],
-    extra_context_by_user_id: Mapping[str, Any],
+    extra_context_by_user_id: Optional[Mapping[int, Mapping[str, Any]]],
 ) -> None:
 ) -> None:
     """Send an "activity" or "alert rule" notification to a Slack user or team."""
     """Send an "activity" or "alert rule" notification to a Slack user or team."""
     client = SlackClient()
     client = SlackClient()

+ 18 - 13
src/sentry/integrations/slack/notify_action.py

@@ -1,12 +1,15 @@
 import logging
 import logging
+from typing import Any, Mapping, MutableMapping, Optional, Sequence, Tuple
 
 
 from django import forms
 from django import forms
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
+from sentry.eventstore.models import Event
 from sentry.integrations.slack.message_builder.issues import build_group_attachment
 from sentry.integrations.slack.message_builder.issues import build_group_attachment
 from sentry.models import Integration
 from sentry.models import Integration
 from sentry.rules.actions.base import IntegrationEventAction
 from sentry.rules.actions.base import IntegrationEventAction
+from sentry.rules.processor import RuleFuture
 from sentry.shared_integrations.exceptions import (
 from sentry.shared_integrations.exceptions import (
     ApiError,
     ApiError,
     ApiRateLimitedError,
     ApiRateLimitedError,
@@ -25,13 +28,13 @@ from .utils import (
 logger = logging.getLogger("sentry.rules")
 logger = logging.getLogger("sentry.rules")
 
 
 
 
-class SlackNotifyServiceForm(forms.Form):
+class SlackNotifyServiceForm(forms.Form):  # type: ignore
     workspace = forms.ChoiceField(choices=(), widget=forms.Select())
     workspace = forms.ChoiceField(choices=(), widget=forms.Select())
     channel = forms.CharField(widget=forms.TextInput())
     channel = forms.CharField(widget=forms.TextInput())
     channel_id = forms.HiddenInput()
     channel_id = forms.HiddenInput()
     tags = forms.CharField(required=False, widget=forms.TextInput())
     tags = forms.CharField(required=False, widget=forms.TextInput())
 
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
         # NOTE: Workspace maps directly to the integration ID
         # NOTE: Workspace maps directly to the integration ID
         workspace_list = [(i.id, i.name) for i in kwargs.pop("integrations")]
         workspace_list = [(i.id, i.name) for i in kwargs.pop("integrations")]
         self.channel_transformer = kwargs.pop("channel_transformer")
         self.channel_transformer = kwargs.pop("channel_transformer")
@@ -49,7 +52,7 @@ class SlackNotifyServiceForm(forms.Form):
         # in the task (integrations/slack/tasks.py) if the channel_id is found.
         # in the task (integrations/slack/tasks.py) if the channel_id is found.
         self._pending_save = False
         self._pending_save = False
 
 
-    def clean(self):
+    def clean(self) -> Mapping[str, Any]:
         channel_id = self.data.get("inputChannelId") or self.data.get("input_channel_id")
         channel_id = self.data.get("inputChannelId") or self.data.get("input_channel_id")
         if channel_id:
         if channel_id:
             logger.info(
             logger.info(
@@ -62,9 +65,9 @@ class SlackNotifyServiceForm(forms.Form):
             # default to "#" if they have the channel name without the prefix
             # default to "#" if they have the channel name without the prefix
             channel_prefix = self.data["channel"][0] if self.data["channel"][0] == "@" else "#"
             channel_prefix = self.data["channel"][0] if self.data["channel"][0] == "@" else "#"
 
 
-        cleaned_data = super().clean()
+        cleaned_data: MutableMapping[str, Any] = super().clean()
 
 
-        workspace = cleaned_data.get("workspace")
+        workspace: Optional[int] = cleaned_data.get("workspace")
 
 
         if channel_id:
         if channel_id:
             try:
             try:
@@ -142,14 +145,14 @@ class SlackNotifyServiceForm(forms.Form):
         return cleaned_data
         return cleaned_data
 
 
 
 
-class SlackNotifyServiceAction(IntegrationEventAction):
+class SlackNotifyServiceAction(IntegrationEventAction):  # type: ignore
     form_cls = SlackNotifyServiceForm
     form_cls = SlackNotifyServiceForm
     label = "Send a notification to the {workspace} Slack workspace to {channel} and show tags {tags} in notification"
     label = "Send a notification to the {workspace} Slack workspace to {channel} and show tags {tags} in notification"
     prompt = "Send a Slack notification"
     prompt = "Send a Slack notification"
     provider = "slack"
     provider = "slack"
     integration_key = "workspace"
     integration_key = "workspace"
 
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
         self.form_fields = {
         self.form_fields = {
             "workspace": {
             "workspace": {
@@ -160,7 +163,7 @@ class SlackNotifyServiceAction(IntegrationEventAction):
             "tags": {"type": "string", "placeholder": "i.e environment,user,my_tag"},
             "tags": {"type": "string", "placeholder": "i.e environment,user,my_tag"},
         }
         }
 
 
-    def after(self, event, state):
+    def after(self, event: Event, state: str) -> Any:
         channel = self.get_option("channel_id")
         channel = self.get_option("channel_id")
         tags = set(self.get_tags_list())
         tags = set(self.get_tags_list())
 
 
@@ -170,7 +173,7 @@ class SlackNotifyServiceAction(IntegrationEventAction):
             # Integration removed, rule still active.
             # Integration removed, rule still active.
             return
             return
 
 
-        def send_notification(event, futures):
+        def send_notification(event: Event, futures: Sequence[RuleFuture]) -> None:
             rules = [f.rule for f in futures]
             rules = [f.rule for f in futures]
             attachments = [build_group_attachment(event.group, event=event, tags=tags, rules=rules)]
             attachments = [build_group_attachment(event.group, event=event, tags=tags, rules=rules)]
 
 
@@ -200,7 +203,7 @@ class SlackNotifyServiceAction(IntegrationEventAction):
         metrics.incr("notifications.sent", instance="slack.notification", skip_internal=False)
         metrics.incr("notifications.sent", instance="slack.notification", skip_internal=False)
         yield self.future(send_notification, key=key)
         yield self.future(send_notification, key=key)
 
 
-    def render_label(self):
+    def render_label(self) -> str:
         tags = self.get_tags_list()
         tags = self.get_tags_list()
 
 
         return self.label.format(
         return self.label.format(
@@ -209,13 +212,15 @@ class SlackNotifyServiceAction(IntegrationEventAction):
             tags="[{}]".format(", ".join(tags)),
             tags="[{}]".format(", ".join(tags)),
         )
         )
 
 
-    def get_tags_list(self):
+    def get_tags_list(self) -> Sequence[str]:
         return [s.strip() for s in self.get_option("tags", "").split(",")]
         return [s.strip() for s in self.get_option("tags", "").split(",")]
 
 
-    def get_form_instance(self):
+    def get_form_instance(self) -> Any:
         return self.form_cls(
         return self.form_cls(
             self.data, integrations=self.get_integrations(), channel_transformer=self.get_channel_id
             self.data, integrations=self.get_integrations(), channel_transformer=self.get_channel_id
         )
         )
 
 
-    def get_channel_id(self, integration, name):
+    def get_channel_id(
+        self, integration: Integration, name: str
+    ) -> Tuple[str, Optional[str], bool]:
         return get_channel_id(self.project.organization, integration, name)
         return get_channel_id(self.project.organization, integration, name)

+ 2 - 2
src/sentry/integrations/slack/requests/base.py

@@ -1,4 +1,4 @@
-from typing import Any, Mapping, MutableMapping, Optional
+from typing import Any, Dict, Mapping, MutableMapping, Optional
 
 
 from rest_framework import status as status_
 from rest_framework import status as status_
 from rest_framework.request import Request
 from rest_framework.request import Request
@@ -97,7 +97,7 @@ class SlackRequest:
         return self._data
         return self._data
 
 
     @property
     @property
-    def logging_data(self) -> Mapping[str, str]:
+    def logging_data(self) -> Dict[str, Any]:
         data = {
         data = {
             "slack_team_id": self.team_id,
             "slack_team_id": self.team_id,
             "slack_channel_id": self.data.get("channel", {}).get("id"),
             "slack_channel_id": self.data.get("channel", {}).get("id"),

Some files were not shown because too many files changed in this diff