2022-06-03 08:09:47 -06:00
|
|
|
import functools
|
|
|
|
|
import html
|
2023-11-02 10:52:32 +01:00
|
|
|
import json
|
2022-06-03 08:09:47 -06:00
|
|
|
import os
|
|
|
|
|
import random
|
|
|
|
|
import re
|
|
|
|
|
import time
|
|
|
|
|
from functools import reduce
|
|
|
|
|
|
|
|
|
|
import factory
|
|
|
|
|
import markdown2
|
|
|
|
|
from bs4 import BeautifulSoup
|
|
|
|
|
from celery.utils.log import get_task_logger
|
|
|
|
|
from celery.utils.time import get_exponential_backoff_interval
|
|
|
|
|
from django.utils.html import urlize
|
|
|
|
|
|
|
|
|
|
logger = get_task_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Faker that always returns unique values
|
|
|
|
|
class UniqueFaker(factory.Faker):
|
|
|
|
|
@classmethod
|
|
|
|
|
def _get_faker(cls, locale=None):
|
|
|
|
|
return super()._get_faker(locale).unique
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Context manager for tasks that are intended to retry
|
|
|
|
|
# It will rerun the whole task if exception(s) exc has happened
|
|
|
|
|
class OkToRetry:
|
|
|
|
|
def __init__(self, task, exc, num_retries=None, compute_countdown=None, allow_jitter=True):
|
|
|
|
|
self.task = task
|
|
|
|
|
self.num_retries = num_retries
|
|
|
|
|
self.compute_countdown = compute_countdown
|
|
|
|
|
self.allow_jitter = allow_jitter
|
|
|
|
|
|
|
|
|
|
if not isinstance(exc, (list, tuple)):
|
|
|
|
|
exc = [exc]
|
|
|
|
|
self.exc = exc
|
|
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
|
|
if exc_type is not None and any(issubclass(exc_type, exc) for exc in self.exc):
|
|
|
|
|
if self.num_retries is None or self.task.request.retries + 1 <= self.num_retries:
|
|
|
|
|
countdown = self.get_countdown(exc_val)
|
|
|
|
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
f"Retrying task gracefully in {countdown} seconds due to {exc_type.__name__}. "
|
|
|
|
|
f"args: {self.task.request.args}, kwargs: {self.task.request.kwargs}"
|
|
|
|
|
)
|
|
|
|
|
self.rerun_task(countdown)
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def get_countdown(self, exc_val):
|
|
|
|
|
if self.compute_countdown is not None:
|
|
|
|
|
countdown = self.compute_countdown(exc_val)
|
|
|
|
|
if self.allow_jitter is True:
|
|
|
|
|
countdown = countdown + random.uniform(0, 2)
|
|
|
|
|
else:
|
|
|
|
|
countdown = get_exponential_backoff_interval(
|
|
|
|
|
factor=self.task.retry_backoff, retries=self.task.request.retries, maximum=600, full_jitter=True
|
|
|
|
|
)
|
|
|
|
|
return countdown
|
|
|
|
|
|
|
|
|
|
def rerun_task(self, countdown):
|
|
|
|
|
self.task.apply_async(
|
|
|
|
|
self.task.request.args,
|
|
|
|
|
kwargs=self.task.request.kwargs,
|
|
|
|
|
retries=self.task.request.retries + 1,
|
|
|
|
|
countdown=countdown,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# lru cache version with addition of timeout.
|
|
|
|
|
# Timeout added to not to occupy memory with too old values
|
|
|
|
|
def timed_lru_cache(timeout: int, maxsize: int = 128, typed: bool = False):
|
|
|
|
|
def wrapper_cache(func):
|
|
|
|
|
func = functools.lru_cache(maxsize=maxsize, typed=typed)(func)
|
|
|
|
|
func.delta = timeout * 10**9
|
|
|
|
|
func.expiration = time.monotonic_ns() + func.delta
|
|
|
|
|
|
|
|
|
|
@functools.wraps(func)
|
|
|
|
|
def wrapped_func(*args, **kwargs):
|
|
|
|
|
if time.monotonic_ns() >= func.expiration:
|
|
|
|
|
func.cache_clear()
|
|
|
|
|
func.expiration = time.monotonic_ns() + func.delta
|
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
wrapped_func.cache_info = func.cache_info
|
|
|
|
|
wrapped_func.cache_clear = func.cache_clear
|
|
|
|
|
return wrapped_func
|
|
|
|
|
|
|
|
|
|
return wrapper_cache
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def getenv_boolean(variable_name: str, default: bool) -> bool:
|
|
|
|
|
value = os.environ.get(variable_name)
|
|
|
|
|
if value is None:
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
return value.lower() in ("true", "1")
|
|
|
|
|
|
|
|
|
|
|
2022-08-24 11:59:01 +05:00
|
|
|
def getenv_integer(variable_name: str, default: int) -> int:
|
|
|
|
|
value = os.environ.get(variable_name)
|
|
|
|
|
if value is None:
|
|
|
|
|
return default
|
|
|
|
|
try:
|
2023-06-12 18:50:33 +02:00
|
|
|
return int(value)
|
2022-08-24 11:59:01 +05:00
|
|
|
except ValueError:
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
2023-11-02 10:52:32 +01:00
|
|
|
def getenv_list(variable_name: str, default: list) -> list:
|
|
|
|
|
value = os.environ.get(variable_name)
|
|
|
|
|
if value is None:
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
return json.loads(value)
|
|
|
|
|
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
def batch_queryset(qs, batch_size=1000):
|
|
|
|
|
qs_count = qs.count()
|
|
|
|
|
for start in range(0, qs_count, batch_size):
|
|
|
|
|
end = min(start + batch_size, qs_count)
|
|
|
|
|
yield qs[start:end]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_regex_valid(regex) -> bool:
|
|
|
|
|
try:
|
|
|
|
|
re.compile(regex)
|
|
|
|
|
return True
|
|
|
|
|
except re.error:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def isoformat_with_tz_suffix(value):
|
|
|
|
|
"""
|
|
|
|
|
Default python datetime.isoformat() return tz offset like +00:00 instead of military tz suffix (e.g.Z for UTC)".
|
|
|
|
|
On the other hand DRF returns datetime with military tz suffix.
|
|
|
|
|
This utility function exists to return consistent datetime string in api.
|
|
|
|
|
Is is copied from DRF DateTimeField.to_representation
|
|
|
|
|
"""
|
|
|
|
|
value = value.isoformat()
|
|
|
|
|
if value.endswith("+00:00"):
|
|
|
|
|
value = value[:-6] + "Z"
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_string_with_visible_characters(string):
|
|
|
|
|
return type(string) == str and not string.isspace() and not string == ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def str_or_backup(string, backup):
|
|
|
|
|
return string if is_string_with_visible_characters(string) else backup
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clean_html(text):
|
Fix warnings when running backend tests (#2079)
# What this PR does
- update `make test` to always use `settings.ci-test`. Right now it will
use whatever the value of `DJANGO_SETTINGS_MODULE` is in
`./dev/.env.dev`, which causes ~45 tests to fail
- Fix several Python warnings that we see when running the tests
```bash
RemovedInDjango40Warning: The providing_args argument is deprecated. As it is purely documentational, it has no replacement. If you rely on this argument as documentation, you can move the text to a code comment or docstring.
alert_create_signal = django.dispatch.Signal(
```
```bash
PytestCollectionWarning: cannot collect test class 'TestOnlyBackend' because it has a __init__ constructor (from: apps/api/tests/test_alert_receive_channel_template.py)
class TestOnlyBackend(BaseMessagingBackend):
```
```bash
DeprecationWarning: The parameter 'use_aliases' in emoji.emojize() is deprecated and will be removed in version 2.0.0. Use language='alias' instead.
To hide this warning, pin/downgrade the package to 'emoji~=1.6.3'
return emoji.emojize(self.verbal_name, use_aliases=True)
```
```bash
DateTimeField CustomOnCallShift.start received a naive datetime (2023-06-01 12:53:12) while time zone support is active.
warnings.warn("DateTimeField %s received a naive datetime (%s)"
```
```bash
apps/twilioapp/tests/test_phone_calls.py::test_resolve_by_phone
/etc/app/apps/twilioapp/tests/test_phone_calls.py:173: DeprecationWarning: The 'text' argument to find()-type methods is deprecated. Use 'string' instead.
content = BeautifulSoup(content, features="html.parser").findAll(text=True)
```
```bash
apps/twilioapp/tests/test_phone_calls.py::test_resolve_by_phone
apps/twilioapp/tests/test_phone_calls.py::test_wrong_pressed_digit
/usr/local/lib/python3.11/site-packages/bs4/builder/__init__.py:545: XMLParsedAsHTMLWarning: It looks like you're parsing an XML document using an HTML parser. If this really is an HTML document (maybe it's XHTML?), you can ignore or filter this warning. If it's XML, you should know that using an XML parser will be more reliable. To parse this document as XML, make sure you have the lxml package installed, and pass the keyword argument `features="xml"` into the BeautifulSoup constructor.
```
```bash
apps/twilioapp/tests/test_phone_calls.py::test_forbidden_requests
/usr/local/lib/python3.11/site-packages/social_django/urls.py:15: RemovedInDjango40Warning: django.conf.urls.url() is deprecated in favor of django.urls.re_path().
url(r'^login/(?P<backend>[^/]+){0}$'.format(extra), views.auth,
```
```bash
apps/twilioapp/tests/test_phone_calls.py: 66 warnings
/usr/local/lib/python3.11/site-packages/debug_toolbar/utils.py:255: DeprecationWarning: currentThread() is deprecated, use current_thread() instead
thread = threading.currentThread()
```
## Checklist
- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
2023-06-06 20:38:00 +02:00
|
|
|
text = "".join(BeautifulSoup(text, features="html.parser").find_all(string=True))
|
2022-06-03 08:09:47 -06:00
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def convert_slack_md_to_html(text):
|
|
|
|
|
text = re.sub(r"\*", "**", text)
|
|
|
|
|
return convert_md_to_html(text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def convert_md_to_html(text):
|
2023-05-12 11:26:08 +08:00
|
|
|
# Markdown expects two or more spaces at the end of a line to indicate a line break.
|
|
|
|
|
# Adding two spaces to any line break to support templates that were built without this in mind.
|
|
|
|
|
# https://daringfireball.net/projects/markdown/syntax#p
|
|
|
|
|
text = text.replace("\n", " \n")
|
2023-07-11 10:14:52 +01:00
|
|
|
|
|
|
|
|
extras = {
|
|
|
|
|
"cuddled-lists",
|
|
|
|
|
"code-friendly", # Disable _ and __ for em and strong.
|
|
|
|
|
# This gives us <pre> and <code> tags for ```-fenced blocks
|
|
|
|
|
"fenced-code-blocks",
|
|
|
|
|
"pyshell",
|
|
|
|
|
"nl2br",
|
|
|
|
|
"target-blank-links",
|
|
|
|
|
"nofollow",
|
|
|
|
|
"pymdownx.emoji",
|
|
|
|
|
"pymdownx.magiclink",
|
|
|
|
|
"tables",
|
|
|
|
|
}
|
|
|
|
|
try:
|
|
|
|
|
text = markdown2.markdown(
|
|
|
|
|
text,
|
|
|
|
|
extras=extras,
|
|
|
|
|
)
|
|
|
|
|
except AssertionError:
|
|
|
|
|
# markdown2 raises an AssertionError when using the "cuddled-lists" extra and passing strings with "- - " in it.
|
|
|
|
|
# If the initial attempt fails, try again without the "cuddled-lists" extra.
|
|
|
|
|
text = markdown2.markdown(
|
|
|
|
|
text,
|
|
|
|
|
extras=extras - {"cuddled-lists"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return text.strip()
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def clean_markup(text):
|
|
|
|
|
html = markdown2.markdown(text, extras=["cuddled-lists", "fenced-code-blocks", "pyshell"]).strip()
|
|
|
|
|
cleaned = clean_html(html)
|
|
|
|
|
stroke_matches = re.findall(r"~\w+~", cleaned)
|
|
|
|
|
for stroke_match in stroke_matches:
|
|
|
|
|
cleaned_match = stroke_match.strip("~")
|
|
|
|
|
cleaned = cleaned.replace(stroke_match, cleaned_match)
|
|
|
|
|
return cleaned
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def escape_html(text):
|
|
|
|
|
return html.escape(text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def urlize_with_respect_to_a(html):
|
|
|
|
|
"""
|
|
|
|
|
Wrap links into <a> tag if not already
|
|
|
|
|
"""
|
|
|
|
|
soup = BeautifulSoup(html, features="html.parser")
|
Fix warnings when running backend tests (#2079)
# What this PR does
- update `make test` to always use `settings.ci-test`. Right now it will
use whatever the value of `DJANGO_SETTINGS_MODULE` is in
`./dev/.env.dev`, which causes ~45 tests to fail
- Fix several Python warnings that we see when running the tests
```bash
RemovedInDjango40Warning: The providing_args argument is deprecated. As it is purely documentational, it has no replacement. If you rely on this argument as documentation, you can move the text to a code comment or docstring.
alert_create_signal = django.dispatch.Signal(
```
```bash
PytestCollectionWarning: cannot collect test class 'TestOnlyBackend' because it has a __init__ constructor (from: apps/api/tests/test_alert_receive_channel_template.py)
class TestOnlyBackend(BaseMessagingBackend):
```
```bash
DeprecationWarning: The parameter 'use_aliases' in emoji.emojize() is deprecated and will be removed in version 2.0.0. Use language='alias' instead.
To hide this warning, pin/downgrade the package to 'emoji~=1.6.3'
return emoji.emojize(self.verbal_name, use_aliases=True)
```
```bash
DateTimeField CustomOnCallShift.start received a naive datetime (2023-06-01 12:53:12) while time zone support is active.
warnings.warn("DateTimeField %s received a naive datetime (%s)"
```
```bash
apps/twilioapp/tests/test_phone_calls.py::test_resolve_by_phone
/etc/app/apps/twilioapp/tests/test_phone_calls.py:173: DeprecationWarning: The 'text' argument to find()-type methods is deprecated. Use 'string' instead.
content = BeautifulSoup(content, features="html.parser").findAll(text=True)
```
```bash
apps/twilioapp/tests/test_phone_calls.py::test_resolve_by_phone
apps/twilioapp/tests/test_phone_calls.py::test_wrong_pressed_digit
/usr/local/lib/python3.11/site-packages/bs4/builder/__init__.py:545: XMLParsedAsHTMLWarning: It looks like you're parsing an XML document using an HTML parser. If this really is an HTML document (maybe it's XHTML?), you can ignore or filter this warning. If it's XML, you should know that using an XML parser will be more reliable. To parse this document as XML, make sure you have the lxml package installed, and pass the keyword argument `features="xml"` into the BeautifulSoup constructor.
```
```bash
apps/twilioapp/tests/test_phone_calls.py::test_forbidden_requests
/usr/local/lib/python3.11/site-packages/social_django/urls.py:15: RemovedInDjango40Warning: django.conf.urls.url() is deprecated in favor of django.urls.re_path().
url(r'^login/(?P<backend>[^/]+){0}$'.format(extra), views.auth,
```
```bash
apps/twilioapp/tests/test_phone_calls.py: 66 warnings
/usr/local/lib/python3.11/site-packages/debug_toolbar/utils.py:255: DeprecationWarning: currentThread() is deprecated, use current_thread() instead
thread = threading.currentThread()
```
## Checklist
- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
2023-06-06 20:38:00 +02:00
|
|
|
textNodes = soup.find_all(string=True)
|
2022-06-03 08:09:47 -06:00
|
|
|
for textNode in textNodes:
|
|
|
|
|
if textNode.parent and getattr(textNode.parent, "name") == "a":
|
|
|
|
|
continue
|
|
|
|
|
urlizedText = urlize(textNode)
|
|
|
|
|
textNode.replaceWith(BeautifulSoup(urlizedText, features="html.parser"))
|
|
|
|
|
|
|
|
|
|
return str(soup)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
url_re = re.compile(
|
|
|
|
|
r"""(?i)\b((?:https?:(?:/{1,3}|[a-z0-9%])|[a-z0-9.\-]+[.](?:com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)/)(?:[^\s()<>{}\[\]]+|\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\))+(?:\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:(?<!@)[a-z0-9]+(?:[.\-][a-z0-9]+)*[.](?:com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)\b/?(?!@)))""", # noqa: E501
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def trim_if_needed(text, default=150):
|
|
|
|
|
if len(text) > default:
|
|
|
|
|
text = text[:default]
|
|
|
|
|
text += "..."
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NoDefaultProvided(object):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def getattrd(obj, name, default=NoDefaultProvided):
|
|
|
|
|
"""
|
|
|
|
|
Same as getattr(), but allows dot notation lookup
|
|
|
|
|
Discussed in:
|
|
|
|
|
http://stackoverflow.com/questions/11975781
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
return reduce(getattr, name.split("."), obj)
|
|
|
|
|
except AttributeError as e:
|
|
|
|
|
if default != NoDefaultProvided:
|
|
|
|
|
return default
|
|
|
|
|
raise e
|