|
@@ -1,22 +1,27 @@
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import abc
|
|
|
import logging
|
|
|
from types import LambdaType
|
|
|
-from typing import Any, Dict, Optional, Sequence
|
|
|
+from typing import Any, Mapping, Sequence, Type
|
|
|
|
|
|
from django.http.response import HttpResponseBase
|
|
|
from django.views import View
|
|
|
from rest_framework.request import Request
|
|
|
|
|
|
from sentry import analytics
|
|
|
+from sentry.db.models import Model
|
|
|
from sentry.models import Organization
|
|
|
from sentry.utils.hashlib import md5_text
|
|
|
from sentry.web.helpers import render_to_response
|
|
|
|
|
|
+from . import PipelineProvider
|
|
|
from .constants import INTEGRATION_EXPIRATION_TTL
|
|
|
from .store import PipelineSessionStore
|
|
|
from .types import PipelineAnalyticsEntry, PipelineRequestState
|
|
|
|
|
|
|
|
|
-class Pipeline:
|
|
|
+class Pipeline(abc.ABC):
|
|
|
"""
|
|
|
Pipeline provides a mechanism to guide the user through a request
|
|
|
'pipeline', where each view may be completed by calling the ``next_step``
|
|
@@ -42,13 +47,13 @@ class Pipeline:
|
|
|
using the ``update_config`` method.
|
|
|
"""
|
|
|
|
|
|
- pipeline_name = None
|
|
|
- provider_manager = None
|
|
|
- provider_model_cls = None
|
|
|
+ pipeline_name: str
|
|
|
+ provider_manager: Any
|
|
|
+ provider_model_cls: Type[Model]
|
|
|
session_store_cls = PipelineSessionStore
|
|
|
|
|
|
@classmethod
|
|
|
- def get_for_request(cls, request):
|
|
|
+ def get_for_request(cls, request: Request) -> Pipeline | None:
|
|
|
req_state = cls.unpack_state(request)
|
|
|
if not req_state:
|
|
|
return None
|
|
@@ -63,7 +68,7 @@ class Pipeline:
|
|
|
)
|
|
|
|
|
|
@classmethod
|
|
|
- def unpack_state(cls, request) -> Optional[PipelineRequestState]:
|
|
|
+ def unpack_state(cls, request: Request) -> PipelineRequestState | None:
|
|
|
state = cls.session_store_cls(request, cls.pipeline_name, ttl=INTEGRATION_EXPIRATION_TTL)
|
|
|
if not state.is_valid():
|
|
|
return None
|
|
@@ -80,12 +85,18 @@ class Pipeline:
|
|
|
|
|
|
return PipelineRequestState(state, provider_model, organization, provider_key)
|
|
|
|
|
|
- def get_provider(self, provider_key: str):
|
|
|
- return self.provider_manager.get(provider_key)
|
|
|
+ def get_provider(self, provider_key: str) -> PipelineProvider:
|
|
|
+ provider: PipelineProvider = self.provider_manager.get(provider_key)
|
|
|
+ return provider
|
|
|
|
|
|
def __init__(
|
|
|
- self, request: Request, provider_key, organization=None, provider_model=None, config=None
|
|
|
- ):
|
|
|
+ self,
|
|
|
+ request: Request,
|
|
|
+ provider_key: str,
|
|
|
+ organization: Organization | None = None,
|
|
|
+ provider_model: Model | None = None,
|
|
|
+ config: Mapping[str, Any] | None = None,
|
|
|
+ ) -> None:
|
|
|
self.request = request
|
|
|
self.organization = organization
|
|
|
self.state = self.session_store_cls(
|
|
@@ -114,17 +125,20 @@ class Pipeline:
|
|
|
providers should inherit, or customize the provider method called to
|
|
|
retrieve the views.
|
|
|
"""
|
|
|
- return self.provider.get_pipeline_views()
|
|
|
+ views: Sequence[View] = self.provider.get_pipeline_views()
|
|
|
+ return views
|
|
|
|
|
|
def is_valid(self) -> bool:
|
|
|
- return self.state.is_valid() and self.state.signature == self.signature
|
|
|
+ _is_valid: bool = self.state.is_valid() and self.state.signature == self.signature
|
|
|
+ return _is_valid
|
|
|
|
|
|
def initialize(self) -> None:
|
|
|
self.state.regenerate(self.get_initial_state())
|
|
|
|
|
|
- def get_initial_state(self) -> Dict[str, Any]:
|
|
|
+ def get_initial_state(self) -> Mapping[str, Any]:
|
|
|
+ user: Any = self.request.user
|
|
|
return {
|
|
|
- "uid": self.request.user.id if self.request.user.is_authenticated else None,
|
|
|
+ "uid": user.id if user.is_authenticated else None,
|
|
|
"provider_model_id": self.provider_model.id if self.provider_model else None,
|
|
|
"provider_key": self.provider.key,
|
|
|
"org_id": self.organization.id if self.organization else None,
|
|
@@ -134,10 +148,10 @@ class Pipeline:
|
|
|
"data": {},
|
|
|
}
|
|
|
|
|
|
- def clear_session(self):
|
|
|
+ def clear_session(self) -> None:
|
|
|
self.state.clear()
|
|
|
|
|
|
- def current_step(self):
|
|
|
+ def current_step(self) -> HttpResponseBase:
|
|
|
"""
|
|
|
Render the current step.
|
|
|
"""
|
|
@@ -154,8 +168,9 @@ class Pipeline:
|
|
|
|
|
|
return self.dispatch_to(step)
|
|
|
|
|
|
- def dispatch_to(self, step: View):
|
|
|
- """Dispatch to a view expected by this pipeline.
|
|
|
+ def dispatch_to(self, step: View) -> HttpResponseBase:
|
|
|
+ """
|
|
|
+ Dispatch to a view expected by this pipeline.
|
|
|
|
|
|
A subclass may override this if its views take other parameters.
|
|
|
"""
|
|
@@ -177,22 +192,21 @@ class Pipeline:
|
|
|
request=self.request,
|
|
|
)
|
|
|
|
|
|
- def render_warning(self, message):
|
|
|
- """For situations when we want to display an error without triggering an issue"""
|
|
|
+ def render_warning(self, message: str) -> HttpResponseBase:
|
|
|
+ """For situations when we want to display an error without triggering an issue."""
|
|
|
context = {"error": message}
|
|
|
return render_to_response("sentry/pipeline-provider-error.html", context, self.request)
|
|
|
|
|
|
- def next_step(self, step_size=1):
|
|
|
- """
|
|
|
- Render the next step.
|
|
|
- """
|
|
|
+ def next_step(self, step_size: int = 1) -> HttpResponseBase:
|
|
|
+ """Render the next step."""
|
|
|
self.state.step_index += step_size
|
|
|
|
|
|
analytics_entry = self.get_analytics_entry()
|
|
|
if analytics_entry and self.organization:
|
|
|
+ user: Any = self.request.user
|
|
|
analytics.record(
|
|
|
analytics_entry.event_type,
|
|
|
- user_id=self.request.user.id,
|
|
|
+ user_id=user.id,
|
|
|
organization_id=self.organization.id,
|
|
|
integration=self.provider.key,
|
|
|
step_index=self.state.step_index,
|
|
@@ -201,27 +215,26 @@ class Pipeline:
|
|
|
|
|
|
return self.current_step()
|
|
|
|
|
|
- def get_analytics_entry(self) -> Optional[PipelineAnalyticsEntry]:
|
|
|
+ def get_analytics_entry(self) -> PipelineAnalyticsEntry | None:
|
|
|
"""Return analytics attributes for this pipeline."""
|
|
|
return None
|
|
|
|
|
|
- def finish_pipeline(self):
|
|
|
- """
|
|
|
- Called when the pipeline completes the final step.
|
|
|
- """
|
|
|
- raise NotImplementedError
|
|
|
+ @abc.abstractmethod
|
|
|
+ def finish_pipeline(self) -> HttpResponseBase:
|
|
|
+ """Called when the pipeline completes the final step."""
|
|
|
+ pass
|
|
|
|
|
|
- def bind_state(self, key, value):
|
|
|
+ def bind_state(self, key: str, value: Any) -> None:
|
|
|
data = self.state.data or {}
|
|
|
data[key] = value
|
|
|
|
|
|
self.state.data = data
|
|
|
|
|
|
- def fetch_state(self, key=None):
|
|
|
+ def fetch_state(self, key: str | None = None) -> Any | None:
|
|
|
data = self.state.data
|
|
|
if not data:
|
|
|
return None
|
|
|
return data if key is None else data.get(key)
|
|
|
|
|
|
- def get_logger(self):
|
|
|
+ def get_logger(self) -> logging.Logger:
|
|
|
return logging.getLogger(f"sentry.integration.{self.provider.key}")
|