oncall-engine/engine/apps/base/utils.py
Joey Orlando 9dde1805aa
add mypy static type checker to backend codebase (#2151)
# What this PR does

- Adds [`mypy` static type checking](https://mypy-lang.org/) to our CI
pipeline. Currently there is still a **ton** of errors being returned by
the tool, as we'll need to fix pre-existing errors. I think we can
slowly chip away at these errors in small PRs, doing them all in one
large PR is likely very risky.
- Also, this PR starts chipping away at one of the main type errors that
we have which is accessing the `datetime` class (from the `datetime`
library) or `timedelta` function on the `django.utils.timezone` module.
Basically we should be instead accessing these two objects from the
native `datetime` module. This makes sense because the [`__all__`
attribute](https://github.com/django/django/blob/main/django/utils/timezone.py#L14-L30)
in `django.utils.timezone` does not re-export `datetime` or `timedelta`.
- splits `engine` dependencies out into `requirements.txt` and
`requirements-dev.txt`

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated (N/A)
- [ ] Documentation added (or `pr:no public docs` PR label added if not
required) (N/A)
- [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required) (N/A)
2023-06-12 12:50:33 -04:00

180 lines
6.1 KiB
Python

import json
import re
from urllib.parse import urlparse
import phonenumbers
from django.apps import apps
from phonenumbers import NumberParseException
from telegram import Bot
from twilio.base.exceptions import TwilioException
from twilio.rest import Client
from common.api_helpers.utils import create_engine_url
class LiveSettingProxy:
def __dir__(self):
LiveSetting = apps.get_model("base", "LiveSetting")
return LiveSetting.AVAILABLE_NAMES
def __getattr__(self, item):
LiveSetting = apps.get_model("base", "LiveSetting")
value = LiveSetting.get_setting(item)
return value
def __setattr__(self, key, value):
LiveSetting = apps.get_model("base", "LiveSetting")
LiveSetting.objects.update_or_create(name=key, defaults={"value": value})
live_settings = LiveSettingProxy()
class LiveSettingValidator:
EMPTY_VALID_NAMES = (
"TWILIO_AUTH_TOKEN",
"TWILIO_API_KEY_SID",
"TWILIO_API_KEY_SECRET",
)
def __init__(self, live_setting):
self.live_setting = live_setting
def get_error(self):
check_fn_name = f"_check_{self.live_setting.name.lower()}"
if self.live_setting.value in (None, "") and self.live_setting.name not in self.EMPTY_VALID_NAMES:
return "Empty"
# skip validation if there's no handler for it
if not hasattr(self, check_fn_name):
return None
check_fn = getattr(self, check_fn_name)
return check_fn(self.live_setting.value)
@classmethod
def _check_twilio_api_key_sid(cls, twilio_api_key_sid):
if live_settings.TWILIO_AUTH_TOKEN:
return
try:
Client(
twilio_api_key_sid, live_settings.TWILIO_API_KEY_SECRET, live_settings.TWILIO_ACCOUNT_SID
).api.applications.list(limit=1)
except Exception as e:
return cls._prettify_twilio_error(e)
@classmethod
def _check_twilio_api_key_secret(cls, twilio_api_key_secret):
if live_settings.TWILIO_AUTH_TOKEN:
return
try:
Client(
live_settings.TWILIO_API_KEY_SID, twilio_api_key_secret, live_settings.TWILIO_ACCOUNT_SID
).api.applications.list(limit=1)
except Exception as e:
return cls._prettify_twilio_error(e)
@classmethod
def _check_twilio_account_sid(cls, twilio_account_sid):
try:
if live_settings.TWILIO_API_KEY_SID and live_settings.TWILIO_API_KEY_SECRET:
Client(
live_settings.TWILIO_API_KEY_SID, live_settings.TWILIO_API_KEY_SECRET, twilio_account_sid
).api.applications.list(limit=1)
else:
Client(twilio_account_sid, live_settings.TWILIO_AUTH_TOKEN).api.applications.list(limit=1)
except Exception as e:
return cls._prettify_twilio_error(e)
@classmethod
def _check_twilio_auth_token(cls, twilio_auth_token):
if live_settings.TWILIO_API_KEY_SID and live_settings.TWILIO_API_KEY_SECRET:
return
try:
Client(live_settings.TWILIO_ACCOUNT_SID, twilio_auth_token).api.applications.list(limit=1)
except Exception as e:
return cls._prettify_twilio_error(e)
@classmethod
def _check_twilio_verify_service_sid(cls, twilio_verify_service_sid):
try:
if live_settings.TWILIO_API_KEY_SID and live_settings.TWILIO_API_KEY_SECRET:
twilio_client = Client(
live_settings.TWILIO_API_KEY_SID,
live_settings.TWILIO_API_KEY_SECRET,
live_settings.TWILIO_ACCOUNT_SID,
)
else:
twilio_client = Client(live_settings.TWILIO_ACCOUNT_SID, live_settings.TWILIO_AUTH_TOKEN)
twilio_client.verify.services(twilio_verify_service_sid).rate_limits.list(limit=1)
except Exception as e:
return cls._prettify_twilio_error(e)
@classmethod
def _check_twilio_number(cls, twilio_number):
if not cls._is_phone_number_valid(twilio_number):
return "Please specify a valid phone number in the following format: +XXXXXXXXXXX"
@classmethod
def _check_slack_install_return_redirect_host(cls, slack_install_return_redirect_host):
scheme = urlparse(slack_install_return_redirect_host).scheme
if scheme != "https":
return "Must use https"
@classmethod
def _check_telegram_token(cls, telegram_token):
try:
bot = Bot(telegram_token)
bot.get_me()
except Exception as e:
return f"Telegram error: {str(e)}"
@classmethod
def _check_telegram_webhook_host(cls, telegram_webhook_host):
try:
# avoid circular import
from apps.telegram.client import TelegramClient
url = create_engine_url("/telegram/", override_base=telegram_webhook_host)
TelegramClient().register_webhook(url)
except Exception as e:
return f"Telegram error: {str(e)}"
@classmethod
def _check_grafana_cloud_oncall_token(cls, grafana_oncall_token):
from apps.oss_installation.models import CloudConnector
_, err = CloudConnector.sync_with_cloud(grafana_oncall_token)
return err
@staticmethod
def _is_email_valid(email):
return re.match(r"^[^@]+@[^@]+\.[^@]+$", email)
@staticmethod
def _is_phone_number_valid(phone_number):
try:
ph_num = phonenumbers.parse(phone_number)
return phonenumbers.is_valid_number(ph_num)
except NumberParseException:
return False
@staticmethod
def _prettify_twilio_error(exc):
if isinstance(exc, TwilioException):
if len(exc.args) > 1:
response_content = exc.args[1].content
content = json.loads(response_content)
error_code = content["code"]
more_info = content["more_info"]
return f"Twilio error: code {error_code}. Learn more: {more_info}"
else:
return f"Twilio error: {exc.args[0]}"
else:
return f"Twilio error: {str(exc)}"