client.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. from __future__ import annotations
  2. from collections.abc import Mapping
  3. from typing import Literal, overload
  4. from requests import PreparedRequest, Response
  5. from sentry.shared_integrations.client.base import BaseApiClient, BaseApiResponseX
  6. from sentry.shared_integrations.client.internal import BaseInternalApiClient
  7. from sentry.shared_integrations.exceptions import ApiUnauthorized
  8. from sentry.users.services.usersocialauth.service import usersocialauth_service
  9. class ApiClient(BaseApiClient):
  10. integration_type = "plugin"
  11. metrics_prefix = "sentry-plugins"
  12. log_path = "sentry.plugins.client"
  13. plugin_name = "undefined"
  14. class AuthApiClient(ApiClient):
  15. def __init__(self, auth=None, *args, **kwargs):
  16. self.auth = auth
  17. super().__init__(*args, **kwargs)
  18. def has_auth(self):
  19. return self.auth and "access_token" in self.auth.tokens
  20. def exception_means_unauthorized(self, exc):
  21. return isinstance(exc, ApiUnauthorized)
  22. def ensure_auth(self, **kwargs):
  23. headers = kwargs["headers"]
  24. if "Authorization" not in headers and self.has_auth() and "auth" not in kwargs:
  25. kwargs = self.bind_auth(**kwargs)
  26. return kwargs
  27. def bind_auth(self, **kwargs):
  28. assert self.auth is not None
  29. token = self.auth.tokens["access_token"]
  30. kwargs["headers"]["Authorization"] = f"Bearer {token}"
  31. return kwargs
  32. @overload
  33. def _request(
  34. self,
  35. method: str,
  36. path: str,
  37. headers: Mapping[str, str] | None = None,
  38. data: Mapping[str, str] | None = None,
  39. params: Mapping[str, str] | None = None,
  40. auth: tuple[str, str] | str | None = None,
  41. json: bool = True,
  42. allow_text: bool | None = None,
  43. allow_redirects: bool | None = None,
  44. timeout: int | None = None,
  45. ignore_webhook_errors: bool = False,
  46. prepared_request: PreparedRequest | None = None,
  47. raw_response: Literal[True] = ...,
  48. ) -> Response:
  49. ...
  50. @overload
  51. def _request(
  52. self,
  53. method: str,
  54. path: str,
  55. headers: Mapping[str, str] | None = None,
  56. data: Mapping[str, str] | None = None,
  57. params: Mapping[str, str] | None = None,
  58. auth: str | None = None,
  59. json: bool = True,
  60. allow_text: bool | None = None,
  61. allow_redirects: bool | None = None,
  62. timeout: int | None = None,
  63. ignore_webhook_errors: bool = False,
  64. prepared_request: PreparedRequest | None = None,
  65. raw_response: bool = ...,
  66. ) -> BaseApiResponseX:
  67. ...
  68. def _request(self, method, path, **kwargs):
  69. headers = kwargs.setdefault("headers", {})
  70. headers.setdefault("Accept", "application/json, application/xml")
  71. # TODO(dcramer): we could proactively refresh the token if we knew
  72. # about expires
  73. kwargs = self.ensure_auth(**kwargs)
  74. try:
  75. return ApiClient._request(self, method, path, **kwargs)
  76. except Exception as exc:
  77. if not self.exception_means_unauthorized(exc):
  78. raise
  79. if not self.auth:
  80. raise
  81. # refresh token
  82. self.logger.info(
  83. "token.refresh", extra={"auth_id": self.auth.id, "provider": self.auth.provider}
  84. )
  85. usersocialauth_service.refresh_token(filter={"id": self.auth.id})
  86. kwargs = self.bind_auth(**kwargs)
  87. return ApiClient._request(self, method, path, **kwargs)
  88. class InternalApiClient(BaseInternalApiClient):
  89. integration_type = "plugin"
  90. metrics_prefix = "sentry-plugins"
  91. log_path = "sentry.plugins.client"
  92. plugin_name = "undefined"