|
@@ -10,9 +10,20 @@ import logging
|
|
|
import sys
|
|
|
from collections import namedtuple
|
|
|
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.models import AuditLogEntryEvent, Identity, OrganizationIntegration
|
|
|
+from sentry.models import (
|
|
|
+ AuditLogEntryEvent,
|
|
|
+ Identity,
|
|
|
+ Integration,
|
|
|
+ Organization,
|
|
|
+ OrganizationIntegration,
|
|
|
+)
|
|
|
from sentry.pipeline import PipelineProvider
|
|
|
from sentry.shared_integrations.constants import (
|
|
|
ERR_INTERNAL,
|
|
@@ -52,9 +63,9 @@ IntegrationMetadata = namedtuple(
|
|
|
)
|
|
|
|
|
|
|
|
|
-class IntegrationMetadata(IntegrationMetadata):
|
|
|
+class IntegrationMetadata(IntegrationMetadata): # type: ignore
|
|
|
@staticmethod
|
|
|
- def feature_flag_name(f):
|
|
|
+ def feature_flag_name(f: Optional[str]) -> Optional[str]:
|
|
|
"""
|
|
|
FeatureDescriptions are set using the IntegrationFeatures constants,
|
|
|
however we expose them here as mappings to organization feature flags, thus
|
|
@@ -62,8 +73,9 @@ class IntegrationMetadata(IntegrationMetadata):
|
|
|
"""
|
|
|
if f is not None:
|
|
|
return f"integrations-{f}"
|
|
|
+ return None
|
|
|
|
|
|
- def _asdict(self):
|
|
|
+ def _asdict(self) -> Dict[str, Sequence[Any]]:
|
|
|
metadata = super()._asdict()
|
|
|
metadata["features"] = [
|
|
|
{
|
|
@@ -103,7 +115,7 @@ class IntegrationFeatures(Enum):
|
|
|
DEPLOYMENT = "deployment"
|
|
|
|
|
|
|
|
|
-class IntegrationProvider(PipelineProvider):
|
|
|
+class IntegrationProvider(PipelineProvider): # type: ignore
|
|
|
"""
|
|
|
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').
|
|
|
# Used to lookup sibling classes and the ``key`` used when creating
|
|
|
# Integration objects.
|
|
|
- key = None
|
|
|
+ key: Optional[str] = None
|
|
|
|
|
|
# a unique identifier to use when creating the ``Integration`` object.
|
|
|
# Only needed when you want to create the above object with something other
|
|
|
# than ``key``. See: VstsExtensionIntegrationProvider.
|
|
|
- _integration_key = None
|
|
|
+ _integration_key: Optional[str] = None
|
|
|
|
|
|
# Whether this integration should show up in the list on the Organization
|
|
|
# Integrations page.
|
|
|
visible = True
|
|
|
|
|
|
# a human readable name (e.g. 'Slack')
|
|
|
- name = None
|
|
|
+ name: Optional[str] = None
|
|
|
|
|
|
# an IntegrationMetadata object, used to provide extra details in the
|
|
|
# configuration interface of the integration.
|
|
|
- metadata = None
|
|
|
+ metadata: Optional[IntegrationMetadata] = None
|
|
|
|
|
|
# an Integration class that will manage the functionality once installed
|
|
|
- integration_cls = None
|
|
|
+ integration_cls: Optional[Any] = None
|
|
|
|
|
|
# configuration for the setup dialog
|
|
|
setup_dialog_config = {"width": 600, "height": 600}
|
|
@@ -157,29 +169,38 @@ class IntegrationProvider(PipelineProvider):
|
|
|
needs_default_identity = False
|
|
|
|
|
|
# can be any number of IntegrationFeatures
|
|
|
- features = frozenset()
|
|
|
+ features: FrozenSet[IntegrationFeatures] = frozenset()
|
|
|
|
|
|
# if this is hidden without the feature flag
|
|
|
requires_feature_flag = False
|
|
|
|
|
|
@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:
|
|
|
raise NotImplementedError
|
|
|
|
|
|
return cls.integration_cls(model, organization_id, **kwargs)
|
|
|
|
|
|
@property
|
|
|
- def integration_key(self):
|
|
|
+ def integration_key(self) -> Optional[str]:
|
|
|
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}")
|
|
|
|
|
|
- def post_install(self, integration, organization, extra=None):
|
|
|
+ def post_install(
|
|
|
+ self, integration: Integration, organization: Organization, extra: Optional[Any] = None
|
|
|
+ ) -> None:
|
|
|
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.
|
|
|
"""
|
|
@@ -192,7 +213,7 @@ class IntegrationProvider(PipelineProvider):
|
|
|
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
|
|
|
configuration pipeline.
|
|
@@ -202,7 +223,7 @@ class IntegrationProvider(PipelineProvider):
|
|
|
"""
|
|
|
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
|
|
|
of configuration and metadata to store with this integration.
|
|
@@ -221,7 +242,7 @@ class IntegrationProvider(PipelineProvider):
|
|
|
>>> return {
|
|
|
>>> 'external_id': state['id'],
|
|
|
>>> 'name': state['name'],
|
|
|
- >>> 'metadata': {url': state['url']},
|
|
|
+ >>> 'metadata': {'url': state['url']},
|
|
|
>>> }
|
|
|
|
|
|
This can return the 'expect_exists' flag, and this method will expect
|
|
@@ -237,7 +258,7 @@ class IntegrationProvider(PipelineProvider):
|
|
|
"""
|
|
|
raise NotImplementedError
|
|
|
|
|
|
- def setup(self):
|
|
|
+ def setup(self) -> None:
|
|
|
"""
|
|
|
Executed once Sentry has been initialized at runtime.
|
|
|
|
|
@@ -245,7 +266,7 @@ class IntegrationProvider(PipelineProvider):
|
|
|
>>> bindings.add('repository.provider', GitHubRepositoryProvider, key='github')
|
|
|
"""
|
|
|
|
|
|
- def has_feature(self, feature):
|
|
|
+ def has_feature(self, feature: IntegrationFeatures) -> bool:
|
|
|
return feature in self.features
|
|
|
|
|
|
|
|
@@ -257,20 +278,20 @@ class IntegrationInstallation:
|
|
|
|
|
|
logger = logging.getLogger("sentry.integrations")
|
|
|
|
|
|
- def __init__(self, model, organization_id):
|
|
|
+ def __init__(self, model: M, organization_id: int) -> None:
|
|
|
self.model = model
|
|
|
self.organization_id = organization_id
|
|
|
self._org_integration = None
|
|
|
|
|
|
@property
|
|
|
- def org_integration(self):
|
|
|
+ def org_integration(self) -> OrganizationIntegration:
|
|
|
if self._org_integration is None:
|
|
|
self._org_integration = OrganizationIntegration.objects.get(
|
|
|
organization_id=self.organization_id, integration_id=self.model.id
|
|
|
)
|
|
|
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
|
|
|
configure the integration per-organization. This simply represents the
|
|
@@ -280,7 +301,7 @@ class IntegrationInstallation:
|
|
|
"""
|
|
|
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.
|
|
|
"""
|
|
@@ -288,28 +309,26 @@ class IntegrationInstallation:
|
|
|
config.update(data)
|
|
|
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
|
|
|
|
|
|
- def get_client(self):
|
|
|
+ def get_client(self) -> Any:
|
|
|
# Return the api client for a given provider
|
|
|
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")
|
|
|
|
|
|
- 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
|
|
|
format and return them, allowing an IntegrationFormError to be raised.
|
|
@@ -319,11 +338,13 @@ class IntegrationInstallation:
|
|
|
"""
|
|
|
return None
|
|
|
|
|
|
- def message_from_error(self, exc):
|
|
|
+ def message_from_error(self, exc: ApiError) -> str:
|
|
|
if isinstance(exc, ApiUnauthorized):
|
|
|
return ERR_UNAUTHORIZED
|
|
|
elif isinstance(exc, ApiHostError):
|
|
|
- return exc.text
|
|
|
+ # Explicitly typing to satisfy mypy.
|
|
|
+ message: str = exc.text
|
|
|
+ return message
|
|
|
elif isinstance(exc, UnsupportedResponseType):
|
|
|
return ERR_UNSUPPORTED_RESPONSE_TYPE.format(content_type=exc.content_type)
|
|
|
elif isinstance(exc, ApiError):
|
|
@@ -335,7 +356,7 @@ class IntegrationInstallation:
|
|
|
else:
|
|
|
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):
|
|
|
raise InvalidIdentity(self.message_from_error(exc), identity=identity).with_traceback(
|
|
|
sys.exc_info()[2]
|
|
@@ -354,10 +375,12 @@ class IntegrationInstallation:
|
|
|
raise IntegrationError(self.message_from_error(exc)).with_traceback(sys.exc_info()[2])
|
|
|
|
|
|
@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
|
|
|
that are not covered in the `delete_organization_integration`
|