mirror of
https://github.com/GSA/notifications-api.git
synced 2026-08-22 23:36:08 -04:00
Compare commits
10 Commits
dynamic-sc
...
03-27-2025
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d47c829ab6 | ||
|
|
385b342bf2 | ||
|
|
379913fa13 | ||
|
|
b40c2327f7 | ||
|
|
e1601b7c5e | ||
|
|
ff10aa9d4c | ||
|
|
21e61ea221 | ||
|
|
1b37fa9b3a | ||
|
|
42536d5cb4 | ||
|
|
00f39e5c7b |
2
.github/workflows/checks.yml
vendored
2
.github/workflows/checks.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
- name: Check coverage threshold
|
||||
# TODO get this back up to 95
|
||||
run: poetry run coverage report -m --fail-under=93
|
||||
run: poetry run coverage report -m --fail-under=94
|
||||
|
||||
validate-new-relic-config:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
22
.github/workflows/deploy-demo.yml
vendored
22
.github/workflows/deploy-demo.yml
vendored
@@ -74,25 +74,9 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_REGISTRATION_URL="$LOGIN_DOT_GOV_REGISTRATION_URL"
|
||||
--strategy rolling
|
||||
|
||||
- name: Update templates
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
env:
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
LOGIN_DOT_GOV_REGISTRATION_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-demo.app.cloud.gov/set-up-your-profile&response_type=code&scope=openid+email&state=STATE"
|
||||
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-demo
|
||||
cf_command: >-
|
||||
run-task notify-api-demo --command "flask command update-templates"
|
||||
|
||||
# TODO FIX
|
||||
# - name: Update templates
|
||||
# run: cf run-task notify-api-demo --command "flask command update-templates"
|
||||
|
||||
- name: Deploy egress proxy
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
|
||||
21
.github/workflows/deploy-prod.yml
vendored
21
.github/workflows/deploy-prod.yml
vendored
@@ -78,24 +78,9 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_REGISTRATION_URL="$LOGIN_DOT_GOV_REGISTRATION_URL"
|
||||
--strategy rolling
|
||||
|
||||
- name: Update templates
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
env:
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
LOGIN_DOT_GOV_REGISTRATION_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://beta.notify.gov/set-up-your-profile&response_type=code&scope=openid+email&state=STATE"
|
||||
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-production
|
||||
cf_command: >-
|
||||
run-task notify-api-production --command "flask command update-templates"
|
||||
# TODO FIX
|
||||
# - name: Update templates
|
||||
# run: cf run-task notify-api-production --command "flask command update-templates"
|
||||
|
||||
- name: Deploy egress proxy
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
|
||||
20
.github/workflows/deploy.yml
vendored
20
.github/workflows/deploy.yml
vendored
@@ -80,23 +80,9 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_REGISTRATION_URL="$LOGIN_DOT_GOV_REGISTRATION_URL"
|
||||
--strategy rolling
|
||||
|
||||
- name: Update templates
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
env:
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
LOGIN_DOT_GOV_REGISTRATION_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/set-up-your-profile&response_type=code&scope=openid+email&state=STATE"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-staging
|
||||
cf_command: >-
|
||||
run-task notify-api-staging --command "flask command update-templates"
|
||||
# TODO FIX
|
||||
# - name: Update templates
|
||||
# run: cf run-task notify-api-staging --command "flask command update-templates"
|
||||
|
||||
- name: Deploy egress proxy
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
|
||||
34
.github/workflows/restage-apps.yml
vendored
34
.github/workflows/restage-apps.yml
vendored
@@ -18,37 +18,6 @@ jobs:
|
||||
matrix:
|
||||
app: ["api", "admin"]
|
||||
steps:
|
||||
- name: Update env variables api
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-${{ inputs.environment }}
|
||||
command: |
|
||||
cf set-env notify-api-${{inputs.environment}} DANGEROUS_SALT "${{secrets.DANGEROUS_SALT}}"
|
||||
cf set-env notify-api-${{inputs.environment}} ADMIN_CLIENT_SECRET "${{secrets.ADMIN_CLIENT_SECRET}}"
|
||||
cf set-env notify-api-${{inputs.environment}} SECRET_KEY "${{secrets.SECRET_KEY}}"
|
||||
cf set-env notify-api-${{inputs.environment}} LOGIN_PEM "${{secrets.LOGIN_PEM}}"
|
||||
cf set-env notify-api-${{inputs.environment}} NOTIFY_E2E_TEST_EMAIL "${{secrets.NOTIFY_E2E_TEST_EMAIL}}"
|
||||
cf set-env notify-api-${{inputs.environment}} NOTIFY_E2E_TEST_PASSWORD "${{secrets.NOTIFY_E2E_TEST_PASSWORD}}"
|
||||
|
||||
- name: Update env variables admin
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-${{ inputs.environment }}
|
||||
command: |
|
||||
cf set-env notify-admin-${{inputs.environment}} DANGEROUS_SALT "${{secrets.DANGEROUS_SALT}}"
|
||||
cf set-env notify-admin-${{inputs.environment}} ADMIN_CLIENT_SECRET "${{secrets.ADMIN_CLIENT_SECRET}}"
|
||||
cf set-env notify-admin-${{inputs.environment}} SECRET_KEY "${{secrets.SECRET_KEY}}"
|
||||
cf set-env notify-admin-${{inputs.environment}} LOGIN_PEM "${{secrets.LOGIN_PEM}}"
|
||||
cf set-env notify-admin-${{inputs.environment}} NOTIFY_E2E_TEST_EMAIL "${{secrets.NOTIFY_E2E_TEST_EMAIL}}"
|
||||
cf set-env notify-admin-${{inputs.environment}} NOTIFY_E2E_TEST_PASSWORD "${{secrets.NOTIFY_E2E_TEST_PASSWORD}}"
|
||||
cf set-env notify-api-${{inputs.environment}} E2E_BUCKET_NAME "${{secrets.E2E_BUCKET_NAME}}"
|
||||
|
||||
- name: Restage ${{matrix.app}}
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
@@ -56,8 +25,7 @@ jobs:
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-${{ inputs.environment }}
|
||||
command: |
|
||||
cf restage --strategy rolling notify-${{matrix.app}}-${{inputs.environment}}
|
||||
command: "cf restage --strategy rolling notify-${{matrix.app}}-${{inputs.environment}}"
|
||||
- name: Restage ${{matrix.app}} egress
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
|
||||
2
Makefile
2
Makefile
@@ -95,7 +95,7 @@ test: ## Run tests and create coverage report
|
||||
poetry run coverage run --omit=*/migrations/*,*/tests/* -m pytest --maxfail=10
|
||||
|
||||
## TODO set this back to 95 asap
|
||||
poetry run coverage report -m --fail-under=93
|
||||
poetry run coverage report -m --fail-under=94
|
||||
poetry run coverage html -d .coverage_cache
|
||||
|
||||
.PHONY: py-lock
|
||||
|
||||
@@ -33,7 +33,6 @@ class DocumentDownloadClient:
|
||||
"document": file_contents,
|
||||
"is_csv": is_csv or False,
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -29,7 +29,6 @@ class PerformancePlatformClient:
|
||||
self.performance_platform_url + payload["dataType"],
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
|
||||
@@ -48,10 +48,7 @@ class AwsSnsClient(SmsClient):
|
||||
|
||||
def send_sms(self, to, content, reference, sender=None, international=False):
|
||||
matched = False
|
||||
if "+" not in to:
|
||||
to = f"+{to}"
|
||||
|
||||
for match in phonenumbers.PhoneNumberMatcher(to, None):
|
||||
for match in phonenumbers.PhoneNumberMatcher(to, "US"):
|
||||
matched = True
|
||||
to = phonenumbers.format_number(
|
||||
match.number, phonenumbers.PhoneNumberFormat.E164
|
||||
|
||||
@@ -35,7 +35,6 @@ def cronitor(task_name):
|
||||
params={
|
||||
"host": current_app.config["API_HOST_NAME"],
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
|
||||
@@ -604,7 +604,7 @@ def dao_fetch_stats_for_service_from_days_for_user(
|
||||
|
||||
total_substmt = (
|
||||
select(
|
||||
func.date_trunc("hour", NotificationAllTimeView.created_at).label("hour"),
|
||||
func.date_trunc("day", NotificationAllTimeView.created_at).label("day"),
|
||||
Job.notification_count.label("notification_count"),
|
||||
)
|
||||
.join(Job, NotificationAllTimeView.job_id == Job.id)
|
||||
@@ -618,26 +618,25 @@ def dao_fetch_stats_for_service_from_days_for_user(
|
||||
.group_by(
|
||||
Job.id,
|
||||
Job.notification_count,
|
||||
func.date_trunc("hour", NotificationAllTimeView.created_at),
|
||||
func.date_trunc("day", NotificationAllTimeView.created_at),
|
||||
)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
total_stmt = select(
|
||||
total_substmt.c.hour,
|
||||
total_substmt.c.day,
|
||||
func.sum(total_substmt.c.notification_count).label("total_notifications"),
|
||||
).group_by(total_substmt.c.hour)
|
||||
).group_by(total_substmt.c.day)
|
||||
|
||||
total_notifications = {
|
||||
row.hour: row.total_notifications
|
||||
for row in db.session.execute(total_stmt).all()
|
||||
row.day: row.total_notifications for row in db.session.execute(total_stmt).all()
|
||||
}
|
||||
|
||||
stmt = (
|
||||
select(
|
||||
NotificationAllTimeView.notification_type,
|
||||
NotificationAllTimeView.status,
|
||||
func.date_trunc("hour", NotificationAllTimeView.created_at).label("hour"),
|
||||
func.date_trunc("day", NotificationAllTimeView.created_at).label("day"),
|
||||
func.count(NotificationAllTimeView.id).label("count"),
|
||||
)
|
||||
.where(
|
||||
@@ -650,7 +649,7 @@ def dao_fetch_stats_for_service_from_days_for_user(
|
||||
.group_by(
|
||||
NotificationAllTimeView.notification_type,
|
||||
NotificationAllTimeView.status,
|
||||
func.date_trunc("hour", NotificationAllTimeView.created_at),
|
||||
func.date_trunc("day", NotificationAllTimeView.created_at),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -185,10 +185,8 @@ def send_email_to_provider(notification):
|
||||
recipient = recipient.decode("utf-8")
|
||||
personalisation = redis_store.get(f"email-personalisation-{notification.id}")
|
||||
if personalisation:
|
||||
p = personalisation.decode("utf-8")
|
||||
|
||||
p = json.loads(p)
|
||||
notification.personalisation = p
|
||||
personalisation = personalisation.decode("utf-8")
|
||||
notification.personalisation = json.loads(personalisation)
|
||||
|
||||
service = SerialisedService.from_id(notification.service_id)
|
||||
if not service.active:
|
||||
@@ -212,12 +210,6 @@ def send_email_to_provider(notification):
|
||||
template_dict, values=notification.personalisation
|
||||
)
|
||||
|
||||
html_email = str(html_email)
|
||||
html_email = html_email.replace("%5B", "")
|
||||
html_email = html_email.replace("%5D", "")
|
||||
html_email = html_email.replace("(", "")
|
||||
html_email = html_email.replace(")", "")
|
||||
|
||||
if notification.key_type == KeyType.TEST:
|
||||
notification.reference = str(create_uuid())
|
||||
update_notification_to_sending(notification, provider)
|
||||
@@ -233,7 +225,7 @@ def send_email_to_provider(notification):
|
||||
recipient,
|
||||
plain_text_email.subject,
|
||||
body=str(plain_text_email),
|
||||
html_body=html_email,
|
||||
html_body=str(html_email),
|
||||
reply_to_address=notification.reply_to_text,
|
||||
)
|
||||
notification.reference = reference
|
||||
|
||||
@@ -30,7 +30,7 @@ def get_certificate(url):
|
||||
res = redis_store.get(url)
|
||||
if res is not None:
|
||||
return res
|
||||
res = requests.get(url, timeout=30).text
|
||||
res = requests.get(url).text
|
||||
redis_store.set(url, res, ex=60 * 60) # 60 minutes
|
||||
_signing_cert_cache[url] = res
|
||||
return res
|
||||
|
||||
@@ -58,7 +58,7 @@ def sns_notification_handler(data, headers):
|
||||
if "SubscribeUrl" in message
|
||||
else message.get("SubscribeURL")
|
||||
)
|
||||
response = requests.get(url, timeout=30)
|
||||
response = requests.get(url)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
|
||||
@@ -8,7 +8,7 @@ def confirm_subscription(confirmation_request):
|
||||
current_app.logger.warning("SubscribeURL does not exist or empty")
|
||||
return
|
||||
|
||||
response = requests.get(url, timeout=30)
|
||||
response = requests.get(url)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
|
||||
@@ -282,12 +282,10 @@ def get_service_statistics_for_specific_days_by_user(
|
||||
service_id, start_date, end_date, user_id
|
||||
)
|
||||
|
||||
hours = days * 24
|
||||
|
||||
stats = get_specific_hours_stats(
|
||||
stats = get_specific_days_stats(
|
||||
results,
|
||||
start_date,
|
||||
hours=hours,
|
||||
days=days,
|
||||
total_notifications=total_notifications,
|
||||
)
|
||||
return stats
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import Blueprint, jsonify, make_response, request
|
||||
from flask import Blueprint, jsonify, request
|
||||
from sqlalchemy import text
|
||||
|
||||
from app import db, version
|
||||
@@ -14,7 +14,7 @@ def show_status():
|
||||
if request.args.get("simple", None):
|
||||
return jsonify(status="ok"), 200
|
||||
else:
|
||||
response = make_response(
|
||||
return (
|
||||
jsonify(
|
||||
status="ok", # This should be considered part of the public API
|
||||
git_commit=version.__git_commit__,
|
||||
@@ -23,21 +23,17 @@ def show_status():
|
||||
),
|
||||
200,
|
||||
)
|
||||
response.headers["Content-Type"] = "application/json"
|
||||
return response
|
||||
|
||||
|
||||
@status.route("/_status/live-service-and-organization-counts")
|
||||
def live_service_and_organization_counts():
|
||||
response = make_response(
|
||||
return (
|
||||
jsonify(
|
||||
organizations=dao_count_organizations_with_live_services(),
|
||||
services=dao_count_live_services(),
|
||||
),
|
||||
200,
|
||||
)
|
||||
response.headers["Content-Type"] = "application/json"
|
||||
return response
|
||||
|
||||
|
||||
def get_db_version():
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Revision ID: 0216_remove_colours
|
||||
Revises: 0215_email_brand_type
|
||||
Create Date: 2018-08-24 13:36:49.346156
|
||||
"""
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Revision ID: 0219_default_email_branding
|
||||
Revises: 0216_remove_colours
|
||||
Create Date: 2018-08-24 13:36:49.346156
|
||||
"""
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Revision ID: 0220_email_brand_type_non_null
|
||||
Revises: 0219_default_email_branding
|
||||
Create Date: 2018-08-24 13:36:49.346156
|
||||
"""
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Revision ID: 0221_nullable_service_branding
|
||||
Revises: 0220_email_brand_type_non_null
|
||||
Create Date: 2018-08-24 13:36:49.346156
|
||||
"""
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Revision ID: 0222_drop_service_branding
|
||||
Revises: 0221_nullable_service_branding
|
||||
Create Date: 2018-08-24 13:36:49.346156
|
||||
"""
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Revision ID: 0223_add_domain_constraint
|
||||
Revises: 0222_drop_service_branding
|
||||
Create Date: 2018-08-24 13:36:49.346156
|
||||
"""
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ def upgrade():
|
||||
conn = op.get_bind()
|
||||
|
||||
# delete the old user because
|
||||
delete_sql = f"""
|
||||
delete from users where email_address='{email_address}'
|
||||
delete_sql = """
|
||||
delete from users where email_address='e2e-test-notify-user@fake.gov'
|
||||
"""
|
||||
|
||||
insert_sql = """
|
||||
|
||||
@@ -12,15 +12,15 @@ from notifications_utils.sanitise_text import SanitiseSMS
|
||||
from . import email_with_smart_quotes_regex
|
||||
|
||||
OBSCURE_ZERO_WIDTH_WHITESPACE = (
|
||||
"\u180e" # Mongolian vowel separator
|
||||
"\u200b" # zero width space
|
||||
"\u200c" # zero width non-joiner
|
||||
"\u200d" # zero width joiner
|
||||
"\u180E" # Mongolian vowel separator
|
||||
"\u200B" # zero width space
|
||||
"\u200C" # zero width non-joiner
|
||||
"\u200D" # zero width joiner
|
||||
"\u2060" # word joiner
|
||||
"\ufeff" # zero width non-breaking space
|
||||
"\uFEFF" # zero width non-breaking space
|
||||
)
|
||||
|
||||
OBSCURE_FULL_WIDTH_WHITESPACE = "\u00a0" # non breaking space
|
||||
OBSCURE_FULL_WIDTH_WHITESPACE = "\u00A0" # non breaking space
|
||||
|
||||
ALL_WHITESPACE = (
|
||||
string.whitespace + OBSCURE_ZERO_WIDTH_WHITESPACE + OBSCURE_FULL_WIDTH_WHITESPACE
|
||||
@@ -61,7 +61,7 @@ more_than_two_newlines_in_a_row = re.compile(r"\n{3,}")
|
||||
def unlink_govuk_escaped(message):
|
||||
return re.sub(
|
||||
govuk_not_a_link,
|
||||
r"\1\2\3" + ".\u200b" + r"\4", # Unicode zero-width space
|
||||
r"\1\2\3" + ".\u200B" + r"\4", # Unicode zero-width space
|
||||
message,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,229 +1,308 @@
|
||||
import html
|
||||
import re
|
||||
from itertools import count
|
||||
|
||||
import mistune
|
||||
from flask import current_app
|
||||
from ordered_set import OrderedSet
|
||||
|
||||
from notifications_utils import MAGIC_SEQUENCE, magic_sequence_regex
|
||||
from notifications_utils.formatters import create_sanitised_html_for_url
|
||||
|
||||
LINK_STYLE = "word-wrap: break-word; color: #1D70B8;"
|
||||
|
||||
|
||||
def escape_plus_lists(markdown_text):
|
||||
return re.sub(r"(?m)^(\+)(?=\s)", r"\\\1", markdown_text)
|
||||
|
||||
|
||||
def autolinkify(text):
|
||||
# url_pattern = re.compile(r"""(?<!\]\()(?<!["'])\b(https?://[^\s<>()]+)""")
|
||||
url_pattern = re.compile(
|
||||
r"""(?<!\]\()
|
||||
(?<!href=["'])
|
||||
\b(https?://[^\s<>"')\]]+)""",
|
||||
re.VERBOSE,
|
||||
mistune._block_quote_leading_pattern = re.compile(r"^ *\^ ?", flags=re.M)
|
||||
mistune.BlockGrammar.block_quote = re.compile(r"^( *\^[^\n]+(\n[^\n]+)*\n*)+")
|
||||
mistune.BlockGrammar.list_block = re.compile(
|
||||
r"^( *)([•*-]|\d+\.)[\s\S]+?"
|
||||
r"(?:"
|
||||
r"\n+(?=\1?(?:[-*_] *){3,}(?:\n+|$))" # hrule
|
||||
r"|\n+(?=%s)" # def links
|
||||
r"|\n+(?=%s)" # def footnotes
|
||||
r"|\n{2,}"
|
||||
r"(?! )"
|
||||
r"(?!\1(?:[•*-]|\d+\.) )\n*"
|
||||
r"|"
|
||||
r"\s*$)"
|
||||
% (
|
||||
mistune._pure_pattern(mistune.BlockGrammar.def_links),
|
||||
mistune._pure_pattern(mistune.BlockGrammar.def_footnotes),
|
||||
)
|
||||
)
|
||||
mistune.BlockGrammar.list_item = re.compile(
|
||||
r"^(( *)(?:[•*-]|\d+\.)[^\n]*" r"(?:\n(?!\2(?:[•*-]|\d+\.))[^\n]*)*)", flags=re.M
|
||||
)
|
||||
mistune.BlockGrammar.list_bullet = re.compile(r"^ *(?:[•*-]|\d+\.)")
|
||||
mistune.InlineGrammar.url = re.compile(r"""^(https?:\/\/[^\s<]+[^<.,:"')\]\s])""")
|
||||
|
||||
def replacer(match):
|
||||
url = match.group(0)
|
||||
return f"[{url}]({url})"
|
||||
|
||||
return url_pattern.sub(replacer, text)
|
||||
|
||||
|
||||
class EmailRenderer(mistune.HTMLRenderer):
|
||||
|
||||
def table(self, header, body):
|
||||
return ""
|
||||
|
||||
def table_row(self, content):
|
||||
return ""
|
||||
|
||||
def table_cell(self, content, **kwargs):
|
||||
return ""
|
||||
|
||||
def heading(self, text, level):
|
||||
if level == 1:
|
||||
return (
|
||||
'<h2 style="Margin: 0 0 20px 0; padding: 0; '
|
||||
'font-size: 27px; line-height: 35px; font-weight: bold; color: #0B0C0C;">'
|
||||
f"{text}</h2>"
|
||||
)
|
||||
return self.paragraph(text)
|
||||
|
||||
def paragraph(self, text):
|
||||
if text.strip():
|
||||
text = html.unescape(text)
|
||||
return (
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; '
|
||||
'line-height: 25px; color: #0B0C0C;">' + text + "</p>"
|
||||
)
|
||||
|
||||
def emphasis(self, text):
|
||||
return f"*{text}*"
|
||||
|
||||
def strong(self, text):
|
||||
return f"**{text}**"
|
||||
|
||||
def block_code(self, code, info=None):
|
||||
return code.strip()
|
||||
|
||||
def block_quote(self, text):
|
||||
return (
|
||||
'<blockquote style="Margin: 0 0 20px 0; border-left: 10px solid #B1B4B6; '
|
||||
'padding: 15 px 0 0.1px 15 px; font-size: 19px; line-height: 25px;">'
|
||||
f"{text}</blockquote>"
|
||||
mistune.InlineLexer.default_rules = list(
|
||||
OrderedSet(mistune.InlineLexer.default_rules)
|
||||
- set(
|
||||
(
|
||||
"emphasis",
|
||||
"double_emphasis",
|
||||
"strikethrough",
|
||||
"code",
|
||||
)
|
||||
|
||||
def thematic_break(self):
|
||||
return '<hr style="border: 0; height: 1px; background: #B1B4B6; Margin: 30px 0 30px 0;">'
|
||||
|
||||
def codespan(self, text):
|
||||
return f"`{text}`"
|
||||
|
||||
def linebreak(self):
|
||||
return "<br />"
|
||||
|
||||
def newline(self):
|
||||
return self.linebreak()
|
||||
|
||||
def list(self, text, ordered, level=None, **kwargs):
|
||||
tag = "ol" if ordered else "ul"
|
||||
style = "list-style-type: decimal;" if ordered else "list-style-type: disc;"
|
||||
return (
|
||||
'<table role="presentation" style="padding 0 0 20px 0;">'
|
||||
'<tr><td style="font-family: Helvetica, Arial, sans-serif;">'
|
||||
f'<{tag} style="Margin: 0 0 0 20px; padding: 0; {style}">{text}</{tag}>'
|
||||
"</td></tr></table>"
|
||||
)
|
||||
|
||||
def list_item(self, text, level=None):
|
||||
return (
|
||||
'<li style="Margin: 5px 0 5px; padding: 0 0 0 5px; font-size: 19px;'
|
||||
'line-height: 25px; color: #0B0C0C;">' + text.strip() + "</li>"
|
||||
)
|
||||
|
||||
def link(self, link=None, text=None, title=None, url=None, **kwargs):
|
||||
|
||||
href = html.escape(
|
||||
url or (link if link and link.startswith("http://", "https://") else "")
|
||||
)
|
||||
display_text = text or link or href or ""
|
||||
title_attr = f' title="{title}"' if title else ""
|
||||
return f'<a style="{LINK_STYLE}" href="{href}"{title_attr}>{display_text}</a>'
|
||||
|
||||
def autolink(self, link, is_email=False): # noqa
|
||||
|
||||
return create_sanitised_html_for_url(link, style=LINK_STYLE)
|
||||
|
||||
def image(self, src, alt="", title=None, url=None): # noqa
|
||||
current_app.logger.debug(f"src={src} alt={alt} title={title} url={url}")
|
||||
return ""
|
||||
|
||||
def strikethrough(self, text):
|
||||
return (
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">'
|
||||
f"~~{text}~~"
|
||||
"</p>"
|
||||
)
|
||||
)
|
||||
mistune.InlineLexer.inline_html_rules = list(
|
||||
set(mistune.InlineLexer.inline_html_rules)
|
||||
- set(
|
||||
(
|
||||
"emphasis",
|
||||
"double_emphasis",
|
||||
"strikethrough",
|
||||
"code",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PlainTextRenderer(mistune.HTMLRenderer):
|
||||
COLUMN_WIDTH = 65
|
||||
|
||||
def heading(self, text, level):
|
||||
if level == 1:
|
||||
return f"\n\n\n{text}\n{'-' * self.COLUMN_WIDTH}"
|
||||
return self.paragraph(text)
|
||||
|
||||
def paragraph(self, text):
|
||||
if text.strip():
|
||||
return f"\n\n{text}"
|
||||
return ""
|
||||
|
||||
def thematic_break(self):
|
||||
return f"\n\n{'=' * self.COLUMN_WIDTH}"
|
||||
class NotifyLetterMarkdownPreviewRenderer(mistune.Renderer):
|
||||
# TODO if we start removing the dead code detected by
|
||||
# the vulture tool (such as the parameter 'language' here)
|
||||
# it will break all the tests. Need to do some massive
|
||||
# cleanup apparently, although it's not clear why vulture
|
||||
# only recently started detecting this.
|
||||
def block_code(self, code, language=None): # noqa
|
||||
return code
|
||||
|
||||
def block_quote(self, text):
|
||||
return text
|
||||
|
||||
def block_code(self, code, info=None):
|
||||
return code.strip()
|
||||
def header(self, text, level, raw=None): # noqa
|
||||
if level == 1:
|
||||
return super().header(text, 2)
|
||||
return self.paragraph(text)
|
||||
|
||||
def hrule(self):
|
||||
return '<div class="page-break"> </div>'
|
||||
|
||||
def paragraph(self, text):
|
||||
if text.strip():
|
||||
return "<p>{}</p>".format(text)
|
||||
return ""
|
||||
|
||||
def table(self, header, body):
|
||||
return ""
|
||||
|
||||
def autolink(self, link, is_email=False):
|
||||
return "<strong>{}</strong>".format(
|
||||
link.replace("http://", "").replace("https://", "")
|
||||
)
|
||||
|
||||
def image(self, src, title, alt_text): # noqa
|
||||
return ""
|
||||
|
||||
def linebreak(self):
|
||||
return "<br>"
|
||||
|
||||
def newline(self):
|
||||
return self.linebreak()
|
||||
|
||||
def list_item(self, text):
|
||||
return "<li>{}</li>\n".format(text.strip())
|
||||
|
||||
def link(self, link, title, content):
|
||||
return "{}: {}".format(content, self.autolink(link))
|
||||
|
||||
def footnote_ref(self, key, index):
|
||||
return ""
|
||||
|
||||
def footnote_item(self, key, text):
|
||||
return text
|
||||
|
||||
def footnotes(self, text):
|
||||
return text
|
||||
|
||||
|
||||
class NotifyEmailMarkdownRenderer(NotifyLetterMarkdownPreviewRenderer):
|
||||
def header(self, text, level, raw=None): # noqa
|
||||
if level == 1:
|
||||
return (
|
||||
'<h2 style="Margin: 0 0 20px 0; padding: 0; '
|
||||
'font-size: 27px; line-height: 35px; font-weight: bold; color: #0B0C0C;">'
|
||||
"{}"
|
||||
"</h2>"
|
||||
).format(text)
|
||||
return self.paragraph(text)
|
||||
|
||||
def hrule(self):
|
||||
return '<hr style="border: 0; height: 1px; background: #B1B4B6; Margin: 30px 0 30px 0;">'
|
||||
|
||||
def linebreak(self):
|
||||
return "<br />"
|
||||
|
||||
def list(self, body, ordered=True):
|
||||
return (
|
||||
(
|
||||
'<table role="presentation" style="padding: 0 0 20px 0;">'
|
||||
"<tr>"
|
||||
'<td style="font-family: Helvetica, Arial, sans-serif;">'
|
||||
'<ol style="Margin: 0 0 0 20px; padding: 0; list-style-type: decimal;">'
|
||||
"{}"
|
||||
"</ol>"
|
||||
"</td>"
|
||||
"</tr>"
|
||||
"</table>"
|
||||
).format(body)
|
||||
if ordered
|
||||
else (
|
||||
'<table role="presentation" style="padding: 0 0 20px 0;">'
|
||||
"<tr>"
|
||||
'<td style="font-family: Helvetica, Arial, sans-serif;">'
|
||||
'<ul style="Margin: 0 0 0 20px; padding: 0; list-style-type: disc;">'
|
||||
"{}"
|
||||
"</ul>"
|
||||
"</td>"
|
||||
"</tr>"
|
||||
"</table>"
|
||||
).format(body)
|
||||
)
|
||||
|
||||
def list_item(self, text):
|
||||
return (
|
||||
'<li style="Margin: 5px 0 5px; padding: 0 0 0 5px; font-size: 19px;'
|
||||
'line-height: 25px; color: #0B0C0C;">'
|
||||
"{}"
|
||||
"</li>"
|
||||
).format(text.strip())
|
||||
|
||||
def paragraph(self, text):
|
||||
if text.strip():
|
||||
return (
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">{}</p>'
|
||||
).format(text)
|
||||
return ""
|
||||
|
||||
def block_quote(self, text):
|
||||
return (
|
||||
"<blockquote "
|
||||
'style="Margin: 0 0 20px 0; border-left: 10px solid #B1B4B6;'
|
||||
'padding: 15px 0 0.1px 15px; font-size: 19px; line-height: 25px;"'
|
||||
">"
|
||||
"{}"
|
||||
"</blockquote>"
|
||||
).format(text)
|
||||
|
||||
def link(self, link, title, content):
|
||||
return ('<a style="{}"{}{}>{}</a>').format(
|
||||
LINK_STYLE,
|
||||
' href="{}"'.format(link),
|
||||
' title="{}"'.format(title) if title else "",
|
||||
content,
|
||||
)
|
||||
|
||||
def autolink(self, link, is_email=False):
|
||||
if is_email:
|
||||
return link
|
||||
return create_sanitised_html_for_url(link, style=LINK_STYLE)
|
||||
|
||||
|
||||
class NotifyPlainTextEmailMarkdownRenderer(NotifyEmailMarkdownRenderer):
|
||||
COLUMN_WIDTH = 65
|
||||
|
||||
def header(self, text, level, raw=None): # noqa
|
||||
if level == 1:
|
||||
return "".join(
|
||||
(
|
||||
self.linebreak() * 3,
|
||||
text,
|
||||
self.linebreak(),
|
||||
"-" * self.COLUMN_WIDTH,
|
||||
)
|
||||
)
|
||||
return self.paragraph(text)
|
||||
|
||||
def hrule(self):
|
||||
return self.paragraph("=" * self.COLUMN_WIDTH)
|
||||
|
||||
def linebreak(self):
|
||||
return "\n"
|
||||
|
||||
def list(self, text, ordered, level=None, **kwargs):
|
||||
def list(self, body, ordered=True):
|
||||
def _get_list_marker():
|
||||
decimal = count(1)
|
||||
return lambda _: "{}.".format(next(decimal)) if ordered else "•"
|
||||
|
||||
if ordered is True:
|
||||
text = text.replace("•", "1.", 1)
|
||||
text = text.replace("•", "2.", 1)
|
||||
text = text.replace("•", "3.", 1)
|
||||
return "".join(
|
||||
(
|
||||
self.linebreak(),
|
||||
re.sub(
|
||||
magic_sequence_regex,
|
||||
_get_list_marker(),
|
||||
body,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# print(f"LIST ordered={ordered} text={text}")
|
||||
return f"\n{text}"
|
||||
def list_item(self, text):
|
||||
return "".join(
|
||||
(
|
||||
self.linebreak(),
|
||||
MAGIC_SEQUENCE,
|
||||
" ",
|
||||
text.strip(),
|
||||
)
|
||||
)
|
||||
|
||||
def list_item(self, text, ordered=None, level=None):
|
||||
# print(f"LIST ITEM = {text} ordered={ordered} level {level}")
|
||||
return f"\n• {text.strip()}"
|
||||
def paragraph(self, text):
|
||||
if text.strip():
|
||||
return "".join(
|
||||
(
|
||||
self.linebreak() * 2,
|
||||
text,
|
||||
)
|
||||
)
|
||||
return ""
|
||||
|
||||
def link(self, link=None, text=None, title=None, url=None, **kwargs):
|
||||
display_text = text or link or url or ""
|
||||
href = url or link or ""
|
||||
output = display_text
|
||||
if title:
|
||||
output += f" ({title})"
|
||||
if href:
|
||||
output += f": {href}"
|
||||
return output
|
||||
def block_quote(self, text):
|
||||
return text
|
||||
|
||||
def autolink(self, link, is_email=False):
|
||||
def link(self, link, title, content):
|
||||
return "".join(
|
||||
(
|
||||
content,
|
||||
" ({})".format(title) if title else "",
|
||||
": ",
|
||||
link,
|
||||
)
|
||||
)
|
||||
|
||||
def autolink(self, link, is_email=False): # noqa
|
||||
return link
|
||||
|
||||
def image(self, src, alt="", title=None, url=None):
|
||||
|
||||
class NotifyEmailPreheaderMarkdownRenderer(NotifyPlainTextEmailMarkdownRenderer):
|
||||
def header(self, text, level, raw=None): # noqa
|
||||
return self.paragraph(text)
|
||||
|
||||
def hrule(self):
|
||||
return ""
|
||||
|
||||
def emphasis(self, text):
|
||||
return f"*{text}*"
|
||||
|
||||
def strong(self, text):
|
||||
return f"**{text}**"
|
||||
|
||||
def codespan(self, text):
|
||||
return f"`{text}`"
|
||||
|
||||
def strikethrough(self, text):
|
||||
return f"~~{text}~~"
|
||||
def link(self, link, title, content):
|
||||
return "".join(
|
||||
(
|
||||
content,
|
||||
" ({})".format(title) if title else "",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PreheaderRenderer(PlainTextRenderer):
|
||||
|
||||
def heading(self, text, level):
|
||||
return html.unescape(self.paragraph(text))
|
||||
|
||||
def thematic_break(self):
|
||||
return ""
|
||||
|
||||
def link(self, link, text=None, title=None, url=None):
|
||||
return text or link
|
||||
|
||||
def image(self, src, alt="", title=None, url=None):
|
||||
current_app.logger.debug("src={src} alt={alt} title={title} url={url}")
|
||||
return ""
|
||||
|
||||
|
||||
_notify_email_markdown = mistune.create_markdown(
|
||||
renderer=EmailRenderer(), hard_wrap=True
|
||||
notify_email_markdown = mistune.Markdown(
|
||||
renderer=NotifyEmailMarkdownRenderer(),
|
||||
hard_wrap=True,
|
||||
use_xhtml=False,
|
||||
)
|
||||
notify_email_preheader_markdown = mistune.create_markdown(renderer=PreheaderRenderer())
|
||||
_notify_plain_text_email_markdown = mistune.create_markdown(
|
||||
renderer=PlainTextRenderer()
|
||||
notify_plain_text_email_markdown = mistune.Markdown(
|
||||
renderer=NotifyPlainTextEmailMarkdownRenderer(),
|
||||
hard_wrap=True,
|
||||
)
|
||||
notify_email_preheader_markdown = mistune.Markdown(
|
||||
renderer=NotifyEmailPreheaderMarkdownRenderer(),
|
||||
hard_wrap=True,
|
||||
)
|
||||
notify_letter_preview_markdown = mistune.Markdown(
|
||||
renderer=NotifyLetterMarkdownPreviewRenderer(),
|
||||
hard_wrap=True,
|
||||
use_xhtml=False,
|
||||
)
|
||||
|
||||
|
||||
def notify_email_markdown(text):
|
||||
text = escape_plus_lists(text)
|
||||
return _notify_email_markdown(autolinkify(text))
|
||||
|
||||
|
||||
def notify_plain_text_email_markdown(text):
|
||||
text = escape_plus_lists(text)
|
||||
return _notify_plain_text_email_markdown(text)
|
||||
|
||||
@@ -628,6 +628,8 @@ def validate_phone_number(number, international=False):
|
||||
|
||||
try:
|
||||
parsed = phonenumbers.parse(number, None)
|
||||
if parsed.country_code != 1:
|
||||
raise InvalidPhoneError("Invalid country code")
|
||||
number = f"{parsed.country_code}{parsed.national_number}"
|
||||
if len(number) < 8:
|
||||
raise InvalidPhoneError("Not enough digits")
|
||||
|
||||
@@ -15,13 +15,13 @@ class SanitiseText:
|
||||
"’": "'", # RIGHT SINGLE QUOTATION MARK (U+2019)
|
||||
"“": '"', # LEFT DOUBLE QUOTATION MARK (U+201C)
|
||||
"”": '"', # RIGHT DOUBLE QUOTATION MARK (U+201D)
|
||||
"\u180e": "", # Mongolian vowel separator
|
||||
"\u200b": "", # zero width space
|
||||
"\u200c": "", # zero width non-joiner
|
||||
"\u200d": "", # zero width joiner
|
||||
"\u180E": "", # Mongolian vowel separator
|
||||
"\u200B": "", # zero width space
|
||||
"\u200C": "", # zero width non-joiner
|
||||
"\u200D": "", # zero width joiner
|
||||
"\u2060": "", # word joiner
|
||||
"\ufeff": "", # zero width non-breaking space
|
||||
"\u00a0": " ", # NON BREAKING WHITE SPACE (U+200B)
|
||||
"\uFEFF": "", # zero width non-breaking space
|
||||
"\u00A0": " ", # NON BREAKING WHITE SPACE (U+200B)
|
||||
"\t": " ", # TAB
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,20 @@ from os import path
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from markupsafe import Markup
|
||||
|
||||
from notifications_utils import MAGIC_SEQUENCE, SMS_CHAR_COUNT_LIMIT
|
||||
from notifications_utils import (
|
||||
LETTER_MAX_PAGE_COUNT,
|
||||
MAGIC_SEQUENCE,
|
||||
SMS_CHAR_COUNT_LIMIT,
|
||||
utc_now,
|
||||
)
|
||||
from notifications_utils.countries.data import Postage
|
||||
from notifications_utils.field import Field, PlainTextField
|
||||
from notifications_utils.formatters import (
|
||||
add_prefix,
|
||||
add_trailing_newline,
|
||||
autolink_urls,
|
||||
escape_html,
|
||||
formatted_list,
|
||||
make_quotes_smart,
|
||||
nl2br,
|
||||
normalise_multiple_newlines,
|
||||
@@ -23,6 +30,7 @@ from notifications_utils.formatters import (
|
||||
remove_smart_quotes_from_email_addresses,
|
||||
remove_whitespace_before_punctuation,
|
||||
replace_hyphens_with_en_dashes,
|
||||
replace_hyphens_with_non_breaking_hyphens,
|
||||
sms_encode,
|
||||
strip_leading_whitespace,
|
||||
strip_unsupported_characters,
|
||||
@@ -32,8 +40,10 @@ from notifications_utils.insensitive_dict import InsensitiveDict
|
||||
from notifications_utils.markdown import (
|
||||
notify_email_markdown,
|
||||
notify_email_preheader_markdown,
|
||||
notify_letter_preview_markdown,
|
||||
notify_plain_text_email_markdown,
|
||||
)
|
||||
from notifications_utils.postal_address import PostalAddress, address_lines_1_to_7_keys
|
||||
from notifications_utils.sanitise_text import SanitiseSMS
|
||||
from notifications_utils.take import Take
|
||||
from notifications_utils.template_change import TemplateChange
|
||||
@@ -704,6 +714,231 @@ class EmailPreviewTemplate(BaseEmailTemplate):
|
||||
)
|
||||
|
||||
|
||||
class BaseLetterTemplate(SubjectMixin, Template):
|
||||
template_type = "letter"
|
||||
|
||||
address_block = "\n".join(
|
||||
f'(({line.replace("_", " ")}))' for line in address_lines_1_to_7_keys
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
template,
|
||||
values=None,
|
||||
contact_block=None,
|
||||
admin_base_url="http://localhost:6012",
|
||||
logo_file_name=None,
|
||||
redact_missing_personalisation=False,
|
||||
date=None,
|
||||
):
|
||||
self.contact_block = (contact_block or "").strip()
|
||||
super().__init__(
|
||||
template,
|
||||
values,
|
||||
redact_missing_personalisation=redact_missing_personalisation,
|
||||
)
|
||||
self.admin_base_url = admin_base_url
|
||||
self.logo_file_name = logo_file_name
|
||||
self.date = date or utc_now()
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
return (
|
||||
Take(
|
||||
Field(
|
||||
self._subject,
|
||||
self.values,
|
||||
redact_missing_personalisation=self.redact_missing_personalisation,
|
||||
html="escape",
|
||||
)
|
||||
)
|
||||
.then(do_nice_typography)
|
||||
.then(normalise_whitespace)
|
||||
)
|
||||
|
||||
@property
|
||||
def placeholders(self):
|
||||
return get_placeholders(self.contact_block) | super().placeholders
|
||||
|
||||
@property
|
||||
def postal_address(self):
|
||||
return PostalAddress.from_personalisation(InsensitiveDict(self.values))
|
||||
|
||||
@property
|
||||
def _address_block(self):
|
||||
if (
|
||||
self.postal_address.has_enough_lines
|
||||
and not self.postal_address.has_too_many_lines
|
||||
):
|
||||
return self.postal_address.normalised_lines
|
||||
|
||||
if "address line 7" not in self.values and "postcode" in self.values:
|
||||
self.values["address line 7"] = self.values["postcode"]
|
||||
|
||||
return Field(
|
||||
self.address_block,
|
||||
self.values,
|
||||
html="escape",
|
||||
with_brackets=False,
|
||||
).splitlines()
|
||||
|
||||
@property
|
||||
def _contact_block(self):
|
||||
return (
|
||||
Take(
|
||||
Field(
|
||||
"\n".join(line.strip() for line in self.contact_block.split("\n")),
|
||||
self.values,
|
||||
redact_missing_personalisation=self.redact_missing_personalisation,
|
||||
html="escape",
|
||||
)
|
||||
)
|
||||
.then(remove_whitespace_before_punctuation)
|
||||
.then(nl2br)
|
||||
)
|
||||
|
||||
@property
|
||||
def _date(self):
|
||||
return self.date.strftime("%-d %B %Y")
|
||||
|
||||
@property
|
||||
def _message(self):
|
||||
return (
|
||||
Take(
|
||||
Field(
|
||||
self.content,
|
||||
self.values,
|
||||
html="escape",
|
||||
markdown_lists=True,
|
||||
redact_missing_personalisation=self.redact_missing_personalisation,
|
||||
)
|
||||
)
|
||||
.then(add_trailing_newline)
|
||||
.then(notify_letter_preview_markdown)
|
||||
.then(do_nice_typography)
|
||||
.then(replace_hyphens_with_non_breaking_hyphens)
|
||||
)
|
||||
|
||||
|
||||
class LetterPreviewTemplate(BaseLetterTemplate):
|
||||
jinja_template = template_env.get_template("letter_pdf/preview.jinja2")
|
||||
|
||||
def __str__(self):
|
||||
return Markup(
|
||||
self.jinja_template.render(
|
||||
{
|
||||
"admin_base_url": self.admin_base_url,
|
||||
"logo_file_name": self.logo_file_name,
|
||||
# logo_class should only ever be None, svg or png
|
||||
"logo_class": (
|
||||
self.logo_file_name.lower()[-3:]
|
||||
if self.logo_file_name
|
||||
else None
|
||||
),
|
||||
"subject": self.subject,
|
||||
"message": self._message,
|
||||
"address": self._address_block,
|
||||
"contact_block": self._contact_block,
|
||||
"date": self._date,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class LetterPrintTemplate(LetterPreviewTemplate):
|
||||
jinja_template = template_env.get_template("letter_pdf/print.jinja2")
|
||||
|
||||
|
||||
class LetterImageTemplate(BaseLetterTemplate):
|
||||
jinja_template = template_env.get_template("letter_image_template.jinja2")
|
||||
first_page_number = 1
|
||||
allowed_postage_types = (
|
||||
Postage.FIRST,
|
||||
Postage.SECOND,
|
||||
Postage.EUROPE,
|
||||
Postage.REST_OF_WORLD,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
template,
|
||||
values=None,
|
||||
image_url=None,
|
||||
page_count=None,
|
||||
contact_block=None,
|
||||
postage=None,
|
||||
):
|
||||
super().__init__(template, values, contact_block=contact_block)
|
||||
if not image_url:
|
||||
raise TypeError("image_url is required")
|
||||
if not page_count:
|
||||
raise TypeError("page_count is required")
|
||||
if postage not in [None] + list(self.allowed_postage_types):
|
||||
raise TypeError(
|
||||
"postage must be None, {}".format(
|
||||
formatted_list(
|
||||
self.allowed_postage_types,
|
||||
conjunction="or",
|
||||
before_each="'",
|
||||
after_each="'",
|
||||
)
|
||||
)
|
||||
)
|
||||
self.image_url = image_url
|
||||
self.page_count = int(page_count)
|
||||
self._postage = postage
|
||||
|
||||
@property
|
||||
def postage(self):
|
||||
if self.postal_address.international:
|
||||
return self.postal_address.postage
|
||||
return self._postage
|
||||
|
||||
@property
|
||||
def last_page_number(self):
|
||||
return min(self.page_count, LETTER_MAX_PAGE_COUNT) + self.first_page_number
|
||||
|
||||
@property
|
||||
def page_numbers(self):
|
||||
return list(range(self.first_page_number, self.last_page_number))
|
||||
|
||||
@property
|
||||
def postage_description(self):
|
||||
return {
|
||||
Postage.FIRST: "first class",
|
||||
Postage.SECOND: "second class",
|
||||
Postage.EUROPE: "international",
|
||||
Postage.REST_OF_WORLD: "international",
|
||||
}.get(self.postage)
|
||||
|
||||
@property
|
||||
def postage_class_value(self):
|
||||
return {
|
||||
Postage.FIRST: "letter-postage-first",
|
||||
Postage.SECOND: "letter-postage-second",
|
||||
Postage.EUROPE: "letter-postage-international",
|
||||
Postage.REST_OF_WORLD: "letter-postage-international",
|
||||
}.get(self.postage)
|
||||
|
||||
def __str__(self):
|
||||
return Markup(
|
||||
self.jinja_template.render(
|
||||
{
|
||||
"image_url": self.image_url,
|
||||
"page_numbers": self.page_numbers,
|
||||
"address": self._address_block,
|
||||
"contact_block": self._contact_block,
|
||||
"date": self._date,
|
||||
"subject": self.subject,
|
||||
"message": self._message,
|
||||
"show_postage": bool(self.postage),
|
||||
"postage_description": self.postage_description,
|
||||
"postage_class_value": self.postage_class_value,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_sms_fragment_count(character_count, non_gsm_characters):
|
||||
if non_gsm_characters:
|
||||
return 1 if character_count <= 70 else math.ceil(float(character_count) / 67)
|
||||
|
||||
421
poetry.lock
generated
421
poetry.lock
generated
@@ -201,13 +201,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "5.0.1"
|
||||
version = "4.0.3"
|
||||
description = "Timeout context manager for asyncio programs"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
|
||||
{file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
|
||||
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
|
||||
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -346,33 +346,33 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "25.1.0"
|
||||
version = "24.10.0"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"},
|
||||
{file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"},
|
||||
{file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"},
|
||||
{file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"},
|
||||
{file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"},
|
||||
{file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"},
|
||||
{file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"},
|
||||
{file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"},
|
||||
{file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"},
|
||||
{file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"},
|
||||
{file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"},
|
||||
{file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"},
|
||||
{file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"},
|
||||
{file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"},
|
||||
{file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"},
|
||||
{file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"},
|
||||
{file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"},
|
||||
{file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"},
|
||||
{file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"},
|
||||
{file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"},
|
||||
{file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"},
|
||||
{file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"},
|
||||
{file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
|
||||
{file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
|
||||
{file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
|
||||
{file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
|
||||
{file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
|
||||
{file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
|
||||
{file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
|
||||
{file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
|
||||
{file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
|
||||
{file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
|
||||
{file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
|
||||
{file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
|
||||
{file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
|
||||
{file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
|
||||
{file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
|
||||
{file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
|
||||
{file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"},
|
||||
{file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"},
|
||||
{file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"},
|
||||
{file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"},
|
||||
{file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
|
||||
{file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -680,103 +680,116 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.1"
|
||||
version = "3.4.0"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"},
|
||||
{file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
|
||||
{file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"},
|
||||
{file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"},
|
||||
{file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -810,18 +823,20 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "click-datetime"
|
||||
version = "0.4.0"
|
||||
version = "0.2"
|
||||
description = "Datetime type support for click."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.12"
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "click_datetime-0.4.0-py3-none-any.whl", hash = "sha256:dfd2f54142f0f899535e400946d724cc8319b4cf70d110d1451f892a6f010b04"},
|
||||
{file = "click_datetime-0.4.0.tar.gz", hash = "sha256:9f35ed3fab13f553221ce8c5a895e5185d7360993c51dd7f8628b9b4f636930b"},
|
||||
{file = "click-datetime-0.2.tar.gz", hash = "sha256:c562ad24b3711784a655a49141b4a87933a78608fe66296259acae95fda5e115"},
|
||||
{file = "click_datetime-0.2-py2.py3-none-any.whl", hash = "sha256:7256ca518e648ada8e2550239ab328de125906e5b7199a5bd5bcbb4dfe28f946"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0,<9.0.0"
|
||||
wheel = ">=0.44.0,<0.45.0"
|
||||
click = "*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "click-didyoumean"
|
||||
@@ -1355,33 +1370,33 @@ typing = ["typing-extensions (>=4.12.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "flake8"
|
||||
version = "7.2.0"
|
||||
version = "7.1.1"
|
||||
description = "the modular source code checker: pep8 pyflakes and co"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8.1"
|
||||
files = [
|
||||
{file = "flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343"},
|
||||
{file = "flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"},
|
||||
{file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"},
|
||||
{file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mccabe = ">=0.7.0,<0.8.0"
|
||||
pycodestyle = ">=2.13.0,<2.14.0"
|
||||
pyflakes = ">=3.3.0,<3.4.0"
|
||||
pycodestyle = ">=2.12.0,<2.13.0"
|
||||
pyflakes = ">=3.2.0,<3.3.0"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-bugbear"
|
||||
version = "24.12.12"
|
||||
version = "24.8.19"
|
||||
description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
|
||||
optional = false
|
||||
python-versions = ">=3.8.1"
|
||||
files = [
|
||||
{file = "flake8_bugbear-24.12.12-py3-none-any.whl", hash = "sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e"},
|
||||
{file = "flake8_bugbear-24.12.12.tar.gz", hash = "sha256:46273cef0a6b6ff48ca2d69e472f41420a42a46e24b2a8972e4f0d6733d12a64"},
|
||||
{file = "flake8_bugbear-24.8.19-py3-none-any.whl", hash = "sha256:25bc3867f7338ee3b3e0916bf8b8a0b743f53a9a5175782ddc4325ed4f386b89"},
|
||||
{file = "flake8_bugbear-24.8.19.tar.gz", hash = "sha256:9b77627eceda28c51c27af94560a72b5b2c97c016651bdce45d8f56c180d2d32"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
attrs = ">=22.2.0"
|
||||
attrs = ">=19.2.0"
|
||||
flake8 = ">=6.0.0"
|
||||
|
||||
[package.extras]
|
||||
@@ -2434,13 +2449,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mistune"
|
||||
version = "3.1.3"
|
||||
description = "A sane and fast Markdown parser with useful plugins and renderers"
|
||||
version = "0.8.4"
|
||||
description = "The fastest markdown parser in pure Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9"},
|
||||
{file = "mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0"},
|
||||
{file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"},
|
||||
{file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2686,40 +2701,40 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "newrelic"
|
||||
version = "10.8.1"
|
||||
version = "10.2.0"
|
||||
description = "New Relic Python Agent"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "newrelic-10.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c1c8438138ca530ecbb4a8d52805520b6815e65e93856739494aa657e1b464a"},
|
||||
{file = "newrelic-10.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:824d36a9be37c43f7d66bed46689c1c2168eba60c591299e932684e03eb10e42"},
|
||||
{file = "newrelic-10.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e2d02e6edd5a9c0e2229e6ca1fa114d9ee95e7c137d3eaa9de3ce8b1ce73b62b"},
|
||||
{file = "newrelic-10.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0c5f96c2ec732258980ea1c2ae8e66ec834ff4c40a815ffdacf3154f1e53d3d5"},
|
||||
{file = "newrelic-10.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03552deef7ee3d9d40b579beca702ac65105d7b5ebd603da0520961b96acadfe"},
|
||||
{file = "newrelic-10.8.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:600aa9b4dafcc4e0d16cc9c0bd40e2fe07a5fa3aa03f4293ba69045738885473"},
|
||||
{file = "newrelic-10.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:75dbdceb1601368becaf0e37af98eadcd5c41d0fb362b906d11eb6697528b52b"},
|
||||
{file = "newrelic-10.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a928b17f4c4d729f0522b3029da5ece1d32d2c5f0a89d500a037619bc53a0b06"},
|
||||
{file = "newrelic-10.8.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:055a8316e3eb1ddb230495928c5d4ad71ad18d11a295092123a1de9f01efa385"},
|
||||
{file = "newrelic-10.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a25ce0228eb104274b71eadf110971c51c90f80c7c6f31f62b3fda4d732eb0a3"},
|
||||
{file = "newrelic-10.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c025d52c45b17abaaeb1907e18104567e65c0d36f855a8fe9834cf105e0ca46"},
|
||||
{file = "newrelic-10.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:28c26c54b6be6a4827ce0cbd056bbb12c5aec148a7de0b664438cc35bb93afdb"},
|
||||
{file = "newrelic-10.8.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1688c9dcfb101b30acb4baf2e2f45159c3d298cdd585204eedf4642e2850fe3"},
|
||||
{file = "newrelic-10.8.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b68067a89eca771e766370fdf3a7f4dd5e02372957051e54fad23096b93e1819"},
|
||||
{file = "newrelic-10.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59055c2265183f7ec9fff9c657451accba6f84eb0ceb5dca9a6dcaa012f2df6b"},
|
||||
{file = "newrelic-10.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3c70ef477e91e9c4419905940769c8223a9d21bf388473687aa500998f23543"},
|
||||
{file = "newrelic-10.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1ce54252d6e3c0b02887b284b58011c9b5a0d82deafc2dc9313be9cb757a720"},
|
||||
{file = "newrelic-10.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9a053f1ba62b89c2a1543a8a55e36bc1cd3c77a39a98744927ba298ce4e3af3"},
|
||||
{file = "newrelic-10.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5444474ba9aa6a08e0235b931c4506c9d78911d9bdbb3a68132107186bd53b35"},
|
||||
{file = "newrelic-10.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89d75646853ce429ba7fe6d7a0d50100107040525094d8f14b965660a6ea41f7"},
|
||||
{file = "newrelic-10.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9a3410ea09e70af92181db485b7772ae7c9243d4605dba20e9dd6cea08e0a1"},
|
||||
{file = "newrelic-10.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7396a8e0b52ce7cb8a6f5cbef5196da7356b3197421617648b9c6d861aa9fe3"},
|
||||
{file = "newrelic-10.8.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:457c74a3fbc4484111f0e5473dc8186da5cd371ced820fb28107fed9790ddc55"},
|
||||
{file = "newrelic-10.8.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:935fdb99883636da4f8aca3d44646c0262c74e1322342e86f0ab0016ecd4cb37"},
|
||||
{file = "newrelic-10.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b823bd4ef787e47f4a3c39a03ba9634dfadee609bb11d050634316f376a9368b"},
|
||||
{file = "newrelic-10.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17a696256c76af9f186af93aa4ef97e9b3e2bd832bec263f5e0b69f47e7ad2f6"},
|
||||
{file = "newrelic-10.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c63924d7748555af011de1d04c0f51069d78fb45112ded407cec39d171763cd3"},
|
||||
{file = "newrelic-10.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d0b01b8a7ff3596825d99ef88b5c04ad6a312d0c3969e97651fda3a35b6212c2"},
|
||||
{file = "newrelic-10.8.1.tar.gz", hash = "sha256:4eac63a95a4565198c36a4ffacb3798bd53affb6fb188531e201f8eb239ee86b"},
|
||||
{file = "newrelic-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f822e6a43151af13a748fb2de6ff298aeb6eee03bf6512afba6aaa79211172"},
|
||||
{file = "newrelic-10.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501cc575b3fd702a21542a0f5dac59b83f47e2806f5b7c0f4e4510b5474ed77f"},
|
||||
{file = "newrelic-10.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f15a940b6794b4008ab983e7ac3b4d179efe609e040ee96ed5744723fc580c8"},
|
||||
{file = "newrelic-10.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60d01303807228718c4099d8550f72d21ee8b61a33555d8974800f6868f2144a"},
|
||||
{file = "newrelic-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5809b4111ef3b1d0b5fa66ad06a81de512842370707863d44888c9439f16c4c"},
|
||||
{file = "newrelic-10.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3415b1c7cab5e586e72cca467dd80cd0507f23a3139c02911cf75892fdbb48a6"},
|
||||
{file = "newrelic-10.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3d9c8297ba158ce4570fc48cfea7bdf3678b2054baaf0cad4debcbca33c2af3a"},
|
||||
{file = "newrelic-10.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bee0b9ce1eccf6ac63e51113781743853b1b84c98ae48ed17d0410c352ccb4d"},
|
||||
{file = "newrelic-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6ff022c7556b61b067e8e6b729fe60f437a8356f319ae3b8342858792f3930d"},
|
||||
{file = "newrelic-10.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3521d646c0032db53b7320fe6b6859eebd863f1b47d7c7dd480073727091e50e"},
|
||||
{file = "newrelic-10.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b4220b97669d214e75d2039fea9e0505fde5bc450832210abbc76b9a635785ed"},
|
||||
{file = "newrelic-10.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6aa9cf936b16d13b65c1b7aa6c722a76a0702469f91bcfc3c5b39f3293181eb"},
|
||||
{file = "newrelic-10.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36a2218c9e79897d9b5671cdeac30c467d7fbac10cda4f2d79062f2bd0fcaed8"},
|
||||
{file = "newrelic-10.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f021eac4c2e3b14eab90c608d8bd25b4e3c6b0b0d40796ec1c1260cc47b5e83"},
|
||||
{file = "newrelic-10.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:69aa68cae47c595bdeb95f275d78693ec27a9fd9353bf81257e21f8607134db6"},
|
||||
{file = "newrelic-10.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ef5d27001d3b5ca53f19d150c60b570c1b0c774d082ab9bf8349f4430ed85b48"},
|
||||
{file = "newrelic-10.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00df1aa613294cb592a52157f789e75166dbf439cfa9e6cf59f6cf4a265dada9"},
|
||||
{file = "newrelic-10.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6d4094d19db924c51ca35da603344907bcbca030822f7a78d5d9c6ad361d419"},
|
||||
{file = "newrelic-10.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:62d521a5d7269c8a5c5838c4ca3b757ef63a13257302c901223c75510cc6f9f5"},
|
||||
{file = "newrelic-10.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3cd5aeade6462519328fc42f4e98948a45571f3d22360a0559e19a6525c723a6"},
|
||||
{file = "newrelic-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6333aa7051544ddc7f8a85f344bc3f401ddd8635540878da34de7bfd91f5d95"},
|
||||
{file = "newrelic-10.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b449546ebb89feaadfd36fda7735ce06023fc90979b838e244f98369aba5ccb"},
|
||||
{file = "newrelic-10.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc3d34db12133b481636384663f45b9ccd7f0f41554a59c15ec37aeb4f77227d"},
|
||||
{file = "newrelic-10.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a15df23effd09bb1d1f5c38866b75cc5f380b6aa953efbca9e95e79b72744db4"},
|
||||
{file = "newrelic-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc693e0db87ab4cf6623847c3949debbcc991554edfb4dd8c02c136e0770b367"},
|
||||
{file = "newrelic-10.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c57e79d37ed87e2790c5e66253f9a5d91ed8cc218f160d5a4d062fc759791a78"},
|
||||
{file = "newrelic-10.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23400846dad2283693eade90b6d3c3462301a4b7735c7f76009b1fa445660aeb"},
|
||||
{file = "newrelic-10.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bd34e4cd73c2435472c0b67869fd2db914d6c99d3e1e404e09affe61a8551e"},
|
||||
{file = "newrelic-10.2.0.tar.gz", hash = "sha256:03ab987eae0452aeb5aed8571c100d1735613a3a227387f99fe54ed38f1ae0e9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -2880,13 +2895,13 @@ test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
version = "24.1"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3142,13 +3157,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.2.0"
|
||||
version = "3.8.0"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"},
|
||||
{file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"},
|
||||
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
|
||||
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3418,13 +3433,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pycodestyle"
|
||||
version = "2.13.0"
|
||||
version = "2.12.1"
|
||||
description = "Python style guide checker"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9"},
|
||||
{file = "pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"},
|
||||
{file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"},
|
||||
{file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3440,13 +3455,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pyflakes"
|
||||
version = "3.3.2"
|
||||
version = "3.2.0"
|
||||
description = "passive checker of Python programs"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a"},
|
||||
{file = "pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b"},
|
||||
{file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"},
|
||||
{file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3527,17 +3542,17 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "6.1.1"
|
||||
version = "5.0.0"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"},
|
||||
{file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"},
|
||||
{file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
|
||||
{file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
coverage = {version = ">=7.5", extras = ["toml"]}
|
||||
coverage = {version = ">=5.2.1", extras = ["toml"]}
|
||||
pytest = ">=4.6"
|
||||
|
||||
[package.extras]
|
||||
@@ -4575,13 +4590,13 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.3.0"
|
||||
version = "2.2.3"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"},
|
||||
{file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
|
||||
{file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
|
||||
{file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -4702,20 +4717,6 @@ MarkupSafe = ">=2.1.1"
|
||||
[package.extras]
|
||||
watchdog = ["watchdog (>=2.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "wheel"
|
||||
version = "0.44.0"
|
||||
description = "A built-package format for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"},
|
||||
{file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6.0.0)", "setuptools (>=65)"]
|
||||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "1.16.0"
|
||||
@@ -4978,4 +4979,4 @@ propcache = ">=0.2.0"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12.2"
|
||||
content-hash = "3076c0b4315416685218586e928e6c1658a6a06d1830403a3051013843dc88be"
|
||||
content-hash = "c791f133fcf2db01b7dadc9aca104cabd4c11331342bf936e0f7ebdb62bbec72"
|
||||
|
||||
@@ -17,9 +17,9 @@ cachetools = "==5.4.0"
|
||||
celery = {version = "==5.4.0", extras = ["redis"]}
|
||||
certifi = ">=2022.12.7"
|
||||
cffi = "==1.17.1"
|
||||
charset-normalizer = "^3.4.1"
|
||||
charset-normalizer = "^3.1.0"
|
||||
click = "==8.1.7"
|
||||
click-datetime = "==0.4.0"
|
||||
click-datetime = "==0.2"
|
||||
click-didyoumean = "==0.3.1"
|
||||
click-plugins = "==1.1.1"
|
||||
click-repl = "==0.3.0"
|
||||
@@ -41,7 +41,7 @@ marshmallow-sqlalchemy = "==1.0.0"
|
||||
newrelic = "*"
|
||||
notifications-python-client = "==10.0.0"
|
||||
oscrypto = { git = "https://github.com/wbond/oscrypto.git", rev = "1547f53" }
|
||||
packaging = "==24.2"
|
||||
packaging = "==24.1"
|
||||
poetry-dotenv-plugin = "==0.2.0"
|
||||
psycopg2-binary = "==2.9.9"
|
||||
pyjwt = "==2.10.1"
|
||||
@@ -49,7 +49,7 @@ python-dotenv = "==1.0.1"
|
||||
sqlalchemy = "==2.0.31"
|
||||
werkzeug = "^3.0.6"
|
||||
faker = "^26.0.0"
|
||||
async-timeout = "^5.0.1"
|
||||
async-timeout = "^4.0.3"
|
||||
bleach = "^6.1.0"
|
||||
geojson = "^3.2.0"
|
||||
numpy = "^2.2.3"
|
||||
@@ -59,7 +59,7 @@ python-json-logger = "^2.0.7"
|
||||
regex = "^2024.7.24"
|
||||
shapely = "^2.0.5"
|
||||
smartypants = "^2.0.1"
|
||||
mistune = "^3.1.3"
|
||||
mistune = "0.8.4"
|
||||
blinker = "^1.9.0"
|
||||
cryptography = "^44.0.1"
|
||||
idna = "^3.7"
|
||||
@@ -70,7 +70,7 @@ python-dateutil = "^2.9.0.post0"
|
||||
pyyaml = "^6.0.2"
|
||||
s3transfer = "^0.10.2"
|
||||
six = "^1.16.0"
|
||||
urllib3 = "^2.3.0"
|
||||
urllib3 = "^2.2.2"
|
||||
webencodings = "^0.5.1"
|
||||
itsdangerous = "^2.2.0"
|
||||
jinja2 = "^3.1.6"
|
||||
@@ -81,22 +81,22 @@ requests = "^2.32.3"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
awscli = "^1.33.32"
|
||||
bandit = "*"
|
||||
black = "^25.1.0"
|
||||
black = "^24.8.0"
|
||||
cloudfoundry-client = "*"
|
||||
exceptiongroup = "==1.2.2"
|
||||
flake8 = "^7.2.0"
|
||||
flake8-bugbear = "^24.12.12"
|
||||
flake8 = "^7.1.1"
|
||||
flake8-bugbear = "^24.1.17"
|
||||
freezegun = "^1.5.1"
|
||||
honcho = "*"
|
||||
isort = "^5.13.2"
|
||||
jinja2-cli = {version = "==0.8.2", extras = ["yaml"]}
|
||||
moto = "==5.1.0"
|
||||
pip-audit = "*"
|
||||
pre-commit = "^4.2.0"
|
||||
pre-commit = "^3.8.0"
|
||||
pytest = "^8.3.2"
|
||||
pytest-env = "^1.1.3"
|
||||
pytest-mock = "^3.14.0"
|
||||
pytest-cov = "^6.1.1"
|
||||
pytest-cov = "^5.0.0"
|
||||
pytest-xdist = "^3.5.0"
|
||||
radon = "^6.0.1"
|
||||
requests-mock = "^1.11.0"
|
||||
|
||||
@@ -104,7 +104,7 @@ def test_notifications_ses_200_autoconfirms_subscription(client, mocker):
|
||||
],
|
||||
)
|
||||
|
||||
requests_mock.assert_called_once_with("https://foo", timeout=30)
|
||||
requests_mock.assert_called_once_with("https://foo")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from app import aws_sns_client
|
||||
|
||||
def test_send_sms_successful_returns_aws_sns_response(notify_api, mocker):
|
||||
boto_mock = mocker.patch.object(aws_sns_client, "_client", create=True)
|
||||
to = "16135555555"
|
||||
to = "6135555555"
|
||||
content = reference = "foo"
|
||||
with notify_api.app_context():
|
||||
aws_sns_client.send_sms(to, content, reference)
|
||||
|
||||
@@ -278,7 +278,7 @@ def test_send_notification_to_queue_throws_exception_deletes_notification(
|
||||
with pytest.raises(Boto3Error):
|
||||
send_notification_to_queue(sample_notification, False)
|
||||
mocked.assert_called_once_with(
|
||||
[str(sample_notification.id)], queue="send-sms-tasks", countdown=60
|
||||
[(str(sample_notification.id))], queue="send-sms-tasks", countdown=60
|
||||
)
|
||||
|
||||
assert _get_notification_query_count() == 0
|
||||
|
||||
@@ -75,7 +75,7 @@ def test_create_invited_org_user(
|
||||
# assert len(notification.personalisation["url"]) > len(expected_start_of_invite_url)
|
||||
|
||||
mocked.assert_called_once_with(
|
||||
[str(notification.id)], queue="notify-internal-tasks", countdown=60
|
||||
[(str(notification.id))], queue="notify-internal-tasks", countdown=60
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ def test_create_invited_user(
|
||||
)
|
||||
|
||||
mocked.assert_called_once_with(
|
||||
[str(notification.id)], queue="notify-internal-tasks", countdown=60
|
||||
[(str(notification.id))], queue="notify-internal-tasks", countdown=60
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ def test_what_will_trigger_conditional_placeholder(value):
|
||||
"list: ",
|
||||
),
|
||||
(
|
||||
{"placeholder": [" ", " \t ", "\u180e"]},
|
||||
{"placeholder": [" ", " \t ", "\u180E"]},
|
||||
"list: ",
|
||||
"list: ",
|
||||
),
|
||||
|
||||
@@ -66,7 +66,7 @@ def test_HTML_template_has_URLs_replaced_with_links():
|
||||
|
||||
def test_escaping_govuk_in_email_templates():
|
||||
template_content = "GOV.UK"
|
||||
expected = "GOV.\u200bUK"
|
||||
expected = "GOV.\u200BUK"
|
||||
assert unlink_govuk_escaped(template_content) == expected
|
||||
template_json = {
|
||||
"content": template_content,
|
||||
@@ -81,23 +81,23 @@ def test_escaping_govuk_in_email_templates():
|
||||
("template_content", "expected"),
|
||||
[
|
||||
# Cases that we add the breaking space
|
||||
("GOV.UK", "GOV.\u200bUK"),
|
||||
("gov.uk", "gov.\u200buk"),
|
||||
("GOV.UK", "GOV.\u200BUK"),
|
||||
("gov.uk", "gov.\u200Buk"),
|
||||
(
|
||||
"content with space infront GOV.UK",
|
||||
"content with space infront GOV.\u200bUK",
|
||||
"content with space infront GOV.\u200BUK",
|
||||
),
|
||||
("content with tab infront\tGOV.UK", "content with tab infront\tGOV.\u200bUK"),
|
||||
("content with tab infront\tGOV.UK", "content with tab infront\tGOV.\u200BUK"),
|
||||
(
|
||||
"content with newline infront\nGOV.UK",
|
||||
"content with newline infront\nGOV.\u200bUK",
|
||||
"content with newline infront\nGOV.\u200BUK",
|
||||
),
|
||||
("*GOV.UK", "*GOV.\u200bUK"),
|
||||
("#GOV.UK", "#GOV.\u200bUK"),
|
||||
("^GOV.UK", "^GOV.\u200bUK"),
|
||||
(" #GOV.UK", " #GOV.\u200bUK"),
|
||||
("GOV.UK with CONTENT after", "GOV.\u200bUK with CONTENT after"),
|
||||
("#GOV.UK with CONTENT after", "#GOV.\u200bUK with CONTENT after"),
|
||||
("*GOV.UK", "*GOV.\u200BUK"),
|
||||
("#GOV.UK", "#GOV.\u200BUK"),
|
||||
("^GOV.UK", "^GOV.\u200BUK"),
|
||||
(" #GOV.UK", " #GOV.\u200BUK"),
|
||||
("GOV.UK with CONTENT after", "GOV.\u200BUK with CONTENT after"),
|
||||
("#GOV.UK with CONTENT after", "#GOV.\u200BUK with CONTENT after"),
|
||||
# Cases that we don't add the breaking space
|
||||
("https://gov.uk", "https://gov.uk"),
|
||||
("https://www.gov.uk", "https://www.gov.uk"),
|
||||
@@ -366,7 +366,7 @@ def test_unicode_dash_lookup():
|
||||
"""
|
||||
\t bar
|
||||
""",
|
||||
" \u180e\u200b \u200c bar \u200d \u2060\ufeff ",
|
||||
" \u180E\u200B \u200C bar \u200D \u2060\uFEFF ",
|
||||
],
|
||||
)
|
||||
def test_strip_all_whitespace(value):
|
||||
@@ -378,7 +378,7 @@ def test_strip_all_whitespace(value):
|
||||
[
|
||||
"notifications-email",
|
||||
" \tnotifications-email \x0c ",
|
||||
"\rn\u200coti\u200dfi\u200bcati\u2060ons-\u180eemai\ufeffl\ufeff",
|
||||
"\rn\u200Coti\u200Dfi\u200Bcati\u2060ons-\u180Eemai\uFEFFl\uFEFF",
|
||||
],
|
||||
)
|
||||
def test_strip_and_remove_obscure_whitespace(value):
|
||||
@@ -421,12 +421,12 @@ def test_strip_unsupported_characters():
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
[
|
||||
"\u200c Your tax is\ndue\n\n",
|
||||
"\u200C Your tax is\ndue\n\n",
|
||||
" Your tax is due ",
|
||||
# Non breaking spaces replaced by single spaces
|
||||
"\u00a0Your\u00a0tax\u00a0 is\u00a0\u00a0due\u00a0",
|
||||
"\u00A0Your\u00A0tax\u00A0 is\u00A0\u00A0due\u00A0",
|
||||
# zero width spaces are removed
|
||||
"\u180eYour \u200btax\u200c is \u200d\u2060due \ufeff",
|
||||
"\u180EYour \u200Btax\u200C is \u200D\u2060due \uFEFF",
|
||||
# tabs are replaced by single spaces
|
||||
"\tYour tax\tis due ",
|
||||
],
|
||||
|
||||
@@ -2,8 +2,10 @@ import pytest
|
||||
|
||||
from notifications_utils.markdown import (
|
||||
notify_email_markdown,
|
||||
notify_letter_preview_markdown,
|
||||
notify_plain_text_email_markdown,
|
||||
)
|
||||
from notifications_utils.template import HTMLEmailTemplate
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -42,9 +44,9 @@ def test_makes_links_out_of_URLs(url):
|
||||
),
|
||||
),
|
||||
(
|
||||
("this link is in parenthesis (http://example.com)"),
|
||||
("this link is in brackets (http://example.com)"),
|
||||
(
|
||||
"this link is in parenthesis "
|
||||
"this link is in brackets "
|
||||
'(<a style="word-wrap: break-word; color: #1D70B8;" href="http://example.com">http://example.com</a>)'
|
||||
),
|
||||
),
|
||||
@@ -77,52 +79,49 @@ def test_doesnt_make_links_out_of_invalid_urls(url):
|
||||
).format(url)
|
||||
|
||||
|
||||
# TODO broke after mistune upgrade 0.8.4->3.1.3
|
||||
# def test_handles_placeholders_in_urls():
|
||||
# assert notify_email_markdown(
|
||||
# "http://example.com/?token=<span class='placeholder'>((token))</span>&key=1"
|
||||
# ) == (
|
||||
# '<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">'
|
||||
# '<a style="word-wrap: break-word; color: #1D70B8;" href="http://example.com/?token=">'
|
||||
# "http://example.com/?token="
|
||||
# "</a>"
|
||||
# "<span class='placeholder'>((token))</span>&key=1"
|
||||
# "</p>"
|
||||
# )
|
||||
def test_handles_placeholders_in_urls():
|
||||
assert notify_email_markdown(
|
||||
"http://example.com/?token=<span class='placeholder'>((token))</span>&key=1"
|
||||
) == (
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">'
|
||||
'<a style="word-wrap: break-word; color: #1D70B8;" href="http://example.com/?token=">'
|
||||
"http://example.com/?token="
|
||||
"</a>"
|
||||
"<span class='placeholder'>((token))</span>&key=1"
|
||||
"</p>"
|
||||
)
|
||||
|
||||
|
||||
# TODO broke after mistune upgrade 0.8.4->3.1.3
|
||||
# @pytest.mark.parametrize(
|
||||
# ("url", "expected_html", "expected_html_in_template"),
|
||||
# [
|
||||
# (
|
||||
# """https://example.com"onclick="alert('hi')""",
|
||||
# """<a style="word-wrap: break-word; color: #1D70B8;" href="https://example.com">https://example.com</a>"onclick="alert('hi')""", # noqa
|
||||
# """<a style="word-wrap: break-word; color: #1D70B8;" href="https://example.com">https://example.com</a>"onclick="alert('hi‘)""", # noqa
|
||||
# ),
|
||||
# (
|
||||
# """https://example.com"style='text-decoration:blink'""",
|
||||
# """<a style="word-wrap: break-word; color: #1D70B8;" href="https://example.com%22style=%27text-decoration:blink">https://example.com"style='text-decoration:blink</a>'""", # noqa
|
||||
# """<a style="word-wrap: break-word; color: #1D70B8;" href="https://example.com%22style=%27text-decoration:blink">https://example.com"style='text-decoration:blink</a>’""", # noqa
|
||||
# ),
|
||||
# ],
|
||||
# )
|
||||
# def test_URLs_get_escaped(url, expected_html, expected_html_in_template):
|
||||
# assert notify_email_markdown(url) == (
|
||||
# '<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">'
|
||||
# "{}"
|
||||
# "</p>"
|
||||
# ).format(expected_html)
|
||||
# TODO need template expertise to fix these
|
||||
# assert expected_html_in_template in str(
|
||||
# HTMLEmailTemplate(
|
||||
# {
|
||||
# "content": url,
|
||||
# "subject": "",
|
||||
# "template_type": "email",
|
||||
# }
|
||||
# )
|
||||
# )
|
||||
@pytest.mark.parametrize(
|
||||
("url", "expected_html", "expected_html_in_template"),
|
||||
[
|
||||
(
|
||||
"""https://example.com"onclick="alert('hi')""",
|
||||
"""<a style="word-wrap: break-word; color: #1D70B8;" href="https://example.com%22onclick=%22alert%28%27hi">https://example.com"onclick="alert('hi</a>')""", # noqa
|
||||
"""<a style="word-wrap: break-word; color: #1D70B8;" href="https://example.com%22onclick=%22alert%28%27hi">https://example.com"onclick="alert('hi</a>‘)""", # noqa
|
||||
),
|
||||
(
|
||||
"""https://example.com"style='text-decoration:blink'""",
|
||||
"""<a style="word-wrap: break-word; color: #1D70B8;" href="https://example.com%22style=%27text-decoration:blink">https://example.com"style='text-decoration:blink</a>'""", # noqa
|
||||
"""<a style="word-wrap: break-word; color: #1D70B8;" href="https://example.com%22style=%27text-decoration:blink">https://example.com"style='text-decoration:blink</a>’""", # noqa
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_URLs_get_escaped(url, expected_html, expected_html_in_template):
|
||||
assert notify_email_markdown(url) == (
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">'
|
||||
"{}"
|
||||
"</p>"
|
||||
).format(expected_html)
|
||||
assert expected_html_in_template in str(
|
||||
HTMLEmailTemplate(
|
||||
{
|
||||
"content": url,
|
||||
"subject": "",
|
||||
"template_type": "email",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -157,7 +156,7 @@ def test_preserves_whitespace_when_making_links(markdown_function, expected_outp
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (notify_letter_preview_markdown, 'print("hello")'),
|
||||
(notify_letter_preview_markdown, 'print("hello")'),
|
||||
(notify_email_markdown, 'print("hello")'),
|
||||
(notify_plain_text_email_markdown, 'print("hello")'),
|
||||
],
|
||||
@@ -166,43 +165,42 @@ def test_block_code(markdown_function, expected):
|
||||
assert markdown_function('```\nprint("hello")\n```') == expected
|
||||
|
||||
|
||||
# TODO broke in mistune upgrade 0.8.4 -> 3.1.3
|
||||
# @pytest.mark.parametrize(
|
||||
# ("markdown_function", "expected"),
|
||||
# [
|
||||
# # (notify_letter_preview_markdown, ("<p>inset text</p>")),
|
||||
# (
|
||||
# notify_email_markdown,
|
||||
# (
|
||||
# "<blockquote "
|
||||
# 'style="Margin: 0 0 20px 0; border-left: 10px solid #B1B4B6;'
|
||||
# "padding: 15px 0 0.1px 15px; font-size: 19px; line-height: 25px;"
|
||||
# '">'
|
||||
# '<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">inset text</p>'
|
||||
# "</blockquote>"
|
||||
# ),
|
||||
# ),
|
||||
# (
|
||||
# notify_plain_text_email_markdown,
|
||||
# ("\n" "\ninset text"),
|
||||
# ),
|
||||
# ],
|
||||
# )
|
||||
# def test_block_quote(markdown_function, expected):
|
||||
# assert markdown_function("^ inset text") == expected
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
(notify_letter_preview_markdown, ("<p>inset text</p>")),
|
||||
(
|
||||
notify_email_markdown,
|
||||
(
|
||||
"<blockquote "
|
||||
'style="Margin: 0 0 20px 0; border-left: 10px solid #B1B4B6;'
|
||||
"padding: 15px 0 0.1px 15px; font-size: 19px; line-height: 25px;"
|
||||
'">'
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">inset text</p>'
|
||||
"</blockquote>"
|
||||
),
|
||||
),
|
||||
(
|
||||
notify_plain_text_email_markdown,
|
||||
("\n" "\ninset text"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_block_quote(markdown_function, expected):
|
||||
assert markdown_function("^ inset text") == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"heading",
|
||||
[
|
||||
"# heading",
|
||||
# "#heading", # This worked in mistune 0.8.4 but is not correct markdown syntax
|
||||
"#heading",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (notify_letter_preview_markdown, "<h2>heading</h2>\n"),
|
||||
(notify_letter_preview_markdown, "<h2>heading</h2>\n"),
|
||||
(
|
||||
notify_email_markdown,
|
||||
(
|
||||
@@ -230,7 +228,7 @@ def test_level_1_header(markdown_function, heading, expected):
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (notify_letter_preview_markdown, "<p>inset text</p>"),
|
||||
(notify_letter_preview_markdown, "<p>inset text</p>"),
|
||||
(
|
||||
notify_email_markdown,
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">inset text</p>',
|
||||
@@ -248,10 +246,10 @@ def test_level_2_header(markdown_function, expected):
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (
|
||||
# notify_letter_preview_markdown,
|
||||
# ("<p>a</p>" '<div class="page-break"> </div>' "<p>b</p>"),
|
||||
# ),
|
||||
(
|
||||
notify_letter_preview_markdown,
|
||||
("<p>a</p>" '<div class="page-break"> </div>' "<p>b</p>"),
|
||||
),
|
||||
(
|
||||
notify_email_markdown,
|
||||
(
|
||||
@@ -278,66 +276,64 @@ def test_hrule(markdown_function, expected):
|
||||
assert markdown_function("a\n\n---\n\nb") == expected
|
||||
|
||||
|
||||
# TODO broke on mistune upgrade 0.8.4->3.1.3
|
||||
# @pytest.mark.parametrize(
|
||||
# ("markdown_function", "expected"),
|
||||
# [
|
||||
# # (
|
||||
# # notify_letter_preview_markdown,
|
||||
# # ("<ol>\n" "<li>one</li>\n" "<li>two</li>\n" "<li>three</li>\n" "</ol>\n"),
|
||||
# # ),
|
||||
# (
|
||||
# notify_email_markdown,
|
||||
# (
|
||||
# '<table role="presentation" style="padding: 0 0 20px 0;">'
|
||||
# "<tr>"
|
||||
# '<td style="font-family: Helvetica, Arial, sans-serif;">'
|
||||
# '<ol style="Margin: 0 0 0 20px; padding: 0; list-style-type: decimal;">'
|
||||
# '<li style="Margin: 5px 0 5px; padding: 0 0 0 5px; font-size: 19px;'
|
||||
# 'line-height: 25px; color: #0B0C0C;">one</li>'
|
||||
# '<li style="Margin: 5px 0 5px; padding: 0 0 0 5px; font-size: 19px;'
|
||||
# 'line-height: 25px; color: #0B0C0C;">two</li>'
|
||||
# '<li style="Margin: 5px 0 5px; padding: 0 0 0 5px; font-size: 19px;'
|
||||
# 'line-height: 25px; color: #0B0C0C;">three</li>'
|
||||
# "</ol>"
|
||||
# "</td>"
|
||||
# "</tr>"
|
||||
# "</table>"
|
||||
# ),
|
||||
# ),
|
||||
# (
|
||||
# notify_plain_text_email_markdown,
|
||||
# ("\n" "\n1. one" "\n2. two" "\n3. three"),
|
||||
# ),
|
||||
# ],
|
||||
# )
|
||||
# def test_ordered_list(markdown_function, expected):
|
||||
# assert markdown_function("1. one\n" "2. two\n" "3. three\n") == expected
|
||||
# assert markdown_function("1.one\n" "2.two\n" "3.three\n") == expected
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
(
|
||||
notify_letter_preview_markdown,
|
||||
("<ol>\n" "<li>one</li>\n" "<li>two</li>\n" "<li>three</li>\n" "</ol>\n"),
|
||||
),
|
||||
(
|
||||
notify_email_markdown,
|
||||
(
|
||||
'<table role="presentation" style="padding: 0 0 20px 0;">'
|
||||
"<tr>"
|
||||
'<td style="font-family: Helvetica, Arial, sans-serif;">'
|
||||
'<ol style="Margin: 0 0 0 20px; padding: 0; list-style-type: decimal;">'
|
||||
'<li style="Margin: 5px 0 5px; padding: 0 0 0 5px; font-size: 19px;'
|
||||
'line-height: 25px; color: #0B0C0C;">one</li>'
|
||||
'<li style="Margin: 5px 0 5px; padding: 0 0 0 5px; font-size: 19px;'
|
||||
'line-height: 25px; color: #0B0C0C;">two</li>'
|
||||
'<li style="Margin: 5px 0 5px; padding: 0 0 0 5px; font-size: 19px;'
|
||||
'line-height: 25px; color: #0B0C0C;">three</li>'
|
||||
"</ol>"
|
||||
"</td>"
|
||||
"</tr>"
|
||||
"</table>"
|
||||
),
|
||||
),
|
||||
(
|
||||
notify_plain_text_email_markdown,
|
||||
("\n" "\n1. one" "\n2. two" "\n3. three"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_ordered_list(markdown_function, expected):
|
||||
assert markdown_function("1. one\n" "2. two\n" "3. three\n") == expected
|
||||
assert markdown_function("1.one\n" "2.two\n" "3.three\n") == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"markdown",
|
||||
[
|
||||
# TODO these broke on mistune upgrade from 0.8.4 to 3.1.3
|
||||
# ("*one\n" "*two\n" "*three\n"), # no space
|
||||
# ("* one\n" "* two\n" "* three\n"), # single space
|
||||
# ("* one\n" "* two\n" "* three\n"), # two spaces
|
||||
# ("- one\n" "- two\n" "- three\n"), # dash as bullet
|
||||
("*one\n" "*two\n" "*three\n"), # no space
|
||||
("* one\n" "* two\n" "* three\n"), # single space
|
||||
("* one\n" "* two\n" "* three\n"), # two spaces
|
||||
("- one\n" "- two\n" "- three\n"), # dash as bullet
|
||||
pytest.param(
|
||||
("+ one\n" "+ two\n" "+ three\n"), # plus as bullet
|
||||
marks=pytest.mark.xfail(raises=AssertionError),
|
||||
),
|
||||
# ("• one\n" "• two\n" "• three\n"), # bullet as bullet
|
||||
("• one\n" "• two\n" "• three\n"), # bullet as bullet
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (
|
||||
# notify_letter_preview_markdown,
|
||||
# ("<ul>\n" "<li>one</li>\n" "<li>two</li>\n" "<li>three</li>\n" "</ul>\n"),
|
||||
# ),
|
||||
(
|
||||
notify_letter_preview_markdown,
|
||||
("<ul>\n" "<li>one</li>\n" "<li>two</li>\n" "<li>three</li>\n" "</ul>\n"),
|
||||
),
|
||||
(
|
||||
notify_email_markdown,
|
||||
(
|
||||
@@ -370,20 +366,21 @@ def test_unordered_list(markdown, markdown_function, expected):
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (
|
||||
# notify_letter_preview_markdown,
|
||||
# "<p>+ one</p><p>+ two</p><p>+ three</p>",
|
||||
# ),
|
||||
(
|
||||
notify_letter_preview_markdown,
|
||||
"<p>+ one</p><p>+ two</p><p>+ three</p>",
|
||||
),
|
||||
(
|
||||
notify_email_markdown,
|
||||
(
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; '
|
||||
'line-height: 25px; color: #0B0C0C;">+ one<br />+ two<br />+ three</p>'
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">+ one</p>'
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">+ two</p>'
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">+ three</p>'
|
||||
),
|
||||
),
|
||||
(
|
||||
notify_plain_text_email_markdown,
|
||||
("\n\n+ one" "\n+ two" "\n+ three"),
|
||||
("\n\n+ one" "\n\n+ two" "\n\n+ three"),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -394,10 +391,10 @@ def test_pluses_dont_render_as_lists(markdown_function, expected):
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (
|
||||
# notify_letter_preview_markdown,
|
||||
# ("<p>" "line one<br>" "line two" "</p>" "<p>" "new paragraph" "</p>"),
|
||||
# ),
|
||||
(
|
||||
notify_letter_preview_markdown,
|
||||
("<p>" "line one<br>" "line two" "</p>" "<p>" "new paragraph" "</p>"),
|
||||
),
|
||||
(
|
||||
notify_email_markdown,
|
||||
(
|
||||
@@ -419,7 +416,7 @@ def test_paragraphs(markdown_function, expected):
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (notify_letter_preview_markdown, ("<p>before</p>" "<p>after</p>")),
|
||||
(notify_letter_preview_markdown, ("<p>before</p>" "<p>after</p>")),
|
||||
(
|
||||
notify_email_markdown,
|
||||
(
|
||||
@@ -437,63 +434,62 @@ def test_multiple_newlines_get_truncated(markdown_function, expected):
|
||||
assert markdown_function("before\n\n\n\n\n\nafter") == expected
|
||||
|
||||
|
||||
# This worked with mistune 0.8.4 but mistune 3.1.3 dropped table support
|
||||
# @pytest.mark.parametrize(
|
||||
# "markdown_function",
|
||||
# [
|
||||
# #notify_letter_preview_markdown,
|
||||
# notify_email_markdown,
|
||||
# notify_plain_text_email_markdown,
|
||||
# ],
|
||||
# )
|
||||
# def test_table(markdown_function):
|
||||
# assert markdown_function("col | col\n" "----|----\n" "val | val\n") == ("")
|
||||
@pytest.mark.parametrize(
|
||||
"markdown_function",
|
||||
[
|
||||
notify_letter_preview_markdown,
|
||||
notify_email_markdown,
|
||||
notify_plain_text_email_markdown,
|
||||
],
|
||||
)
|
||||
def test_table(markdown_function):
|
||||
assert markdown_function("col | col\n" "----|----\n" "val | val\n") == ("")
|
||||
|
||||
# TODO broke on mistune upgrad 0.8.4->3.1.3
|
||||
# @pytest.mark.parametrize(
|
||||
# ("markdown_function", "link", "expected"),
|
||||
# [
|
||||
# # (
|
||||
# # notify_letter_preview_markdown,
|
||||
# # "http://example.com",
|
||||
# # "<p><strong>example.com</strong></p>",
|
||||
# # ),
|
||||
# (
|
||||
# notify_email_markdown,
|
||||
# "http://example.com",
|
||||
# (
|
||||
# '<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">'
|
||||
# '<a style="word-wrap: break-word; color: #1D70B8;" href="http://example.com">http://example.com</a>'
|
||||
# "</p>"
|
||||
# ),
|
||||
# ),
|
||||
# (
|
||||
# notify_email_markdown,
|
||||
# """https://example.com"onclick="alert('hi')""",
|
||||
# (
|
||||
# '<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">'
|
||||
# '<a style="word-wrap: break-word; color: #1D70B8;" '
|
||||
# 'href="https://example.com%22onclick=%22alert%28%27hi">'
|
||||
# 'https://example.com"onclick="alert(\'hi'
|
||||
# "</a>')"
|
||||
# "</p>"
|
||||
# ),
|
||||
# ),
|
||||
# (
|
||||
# notify_plain_text_email_markdown,
|
||||
# "http://example.com",
|
||||
# ("\n" "\nhttp://example.com"),
|
||||
# ),
|
||||
# ],
|
||||
# )
|
||||
# def test_autolink(markdown_function, link, expected):
|
||||
# assert markdown_function(link) == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "link", "expected"),
|
||||
[
|
||||
(
|
||||
notify_letter_preview_markdown,
|
||||
"http://example.com",
|
||||
"<p><strong>example.com</strong></p>",
|
||||
),
|
||||
(
|
||||
notify_email_markdown,
|
||||
"http://example.com",
|
||||
(
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">'
|
||||
'<a style="word-wrap: break-word; color: #1D70B8;" href="http://example.com">http://example.com</a>'
|
||||
"</p>"
|
||||
),
|
||||
),
|
||||
(
|
||||
notify_email_markdown,
|
||||
"""https://example.com"onclick="alert('hi')""",
|
||||
(
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">'
|
||||
'<a style="word-wrap: break-word; color: #1D70B8;" '
|
||||
'href="https://example.com%22onclick=%22alert%28%27hi">'
|
||||
'https://example.com"onclick="alert(\'hi'
|
||||
"</a>')"
|
||||
"</p>"
|
||||
),
|
||||
),
|
||||
(
|
||||
notify_plain_text_email_markdown,
|
||||
"http://example.com",
|
||||
("\n" "\nhttp://example.com"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_autolink(markdown_function, link, expected):
|
||||
assert markdown_function(link) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (notify_letter_preview_markdown, "<p>variable called `thing`</p>"),
|
||||
(notify_letter_preview_markdown, "<p>variable called `thing`</p>"),
|
||||
(
|
||||
notify_email_markdown,
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">variable called `thing`</p>', # noqa E501
|
||||
@@ -511,7 +507,7 @@ def test_codespan(markdown_function, expected):
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (notify_letter_preview_markdown, "<p>something **important**</p>"),
|
||||
(notify_letter_preview_markdown, "<p>something **important**</p>"),
|
||||
(
|
||||
notify_email_markdown,
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">something **important**</p>', # noqa E501
|
||||
@@ -523,17 +519,17 @@ def test_codespan(markdown_function, expected):
|
||||
],
|
||||
)
|
||||
def test_double_emphasis(markdown_function, expected):
|
||||
assert markdown_function("something __important__") == expected
|
||||
assert markdown_function("something **important**") == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "text", "expected"),
|
||||
[
|
||||
# (
|
||||
# notify_letter_preview_markdown,
|
||||
# "something *important*",
|
||||
# "<p>something *important*</p>",
|
||||
# ),
|
||||
(
|
||||
notify_letter_preview_markdown,
|
||||
"something *important*",
|
||||
"<p>something *important*</p>",
|
||||
),
|
||||
(
|
||||
notify_email_markdown,
|
||||
"something *important*",
|
||||
@@ -547,7 +543,7 @@ def test_double_emphasis(markdown_function, expected):
|
||||
(
|
||||
notify_plain_text_email_markdown,
|
||||
"something _important_",
|
||||
"\n\nsomething *important*",
|
||||
"\n\nsomething _important_",
|
||||
),
|
||||
(
|
||||
notify_plain_text_email_markdown,
|
||||
@@ -582,26 +578,25 @@ def test_nested_emphasis(markdown_function, expected):
|
||||
assert markdown_function("foo ****** bar") == expected
|
||||
|
||||
|
||||
# TODO broke in mistune upgrade 0.8.4->3.1.3
|
||||
# @pytest.mark.parametrize(
|
||||
# "markdown_function",
|
||||
# [
|
||||
# # notify_letter_preview_markdown,
|
||||
# notify_email_markdown,
|
||||
# notify_plain_text_email_markdown,
|
||||
# ],
|
||||
# )
|
||||
# def test_image(markdown_function):
|
||||
# assert markdown_function("") == ("")
|
||||
@pytest.mark.parametrize(
|
||||
"markdown_function",
|
||||
[
|
||||
notify_letter_preview_markdown,
|
||||
notify_email_markdown,
|
||||
notify_plain_text_email_markdown,
|
||||
],
|
||||
)
|
||||
def test_image(markdown_function):
|
||||
assert markdown_function("") == ("")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (
|
||||
# notify_letter_preview_markdown,
|
||||
# ("<p>Example: <strong>example.com</strong></p>"),
|
||||
# ),
|
||||
(
|
||||
notify_letter_preview_markdown,
|
||||
("<p>Example: <strong>example.com</strong></p>"),
|
||||
),
|
||||
(
|
||||
notify_email_markdown,
|
||||
(
|
||||
@@ -624,10 +619,10 @@ def test_link(markdown_function, expected):
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (
|
||||
# notify_letter_preview_markdown,
|
||||
# ("<p>Example: <strong>example.com</strong></p>"),
|
||||
# ),
|
||||
(
|
||||
notify_letter_preview_markdown,
|
||||
("<p>Example: <strong>example.com</strong></p>"),
|
||||
),
|
||||
(
|
||||
notify_email_markdown,
|
||||
(
|
||||
@@ -654,7 +649,7 @@ def test_link_with_title(markdown_function, expected):
|
||||
@pytest.mark.parametrize(
|
||||
("markdown_function", "expected"),
|
||||
[
|
||||
# (notify_letter_preview_markdown, "<p>~~Strike~~</p>"),
|
||||
(notify_letter_preview_markdown, "<p>~~Strike~~</p>"),
|
||||
(
|
||||
notify_email_markdown,
|
||||
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">~~Strike~~</p>',
|
||||
|
||||
@@ -559,7 +559,7 @@ def test_normalise_postcode(postcode, normalised_postcode):
|
||||
("N5 1AA", True),
|
||||
("SO14 6WB", True),
|
||||
("so14 6wb", True),
|
||||
("so14\u00a06wb", True),
|
||||
("so14\u00A06wb", True),
|
||||
# invalida / incomplete postcodes
|
||||
("N5", False),
|
||||
("SO144 6WB", False),
|
||||
@@ -600,7 +600,7 @@ def test_if_postcode_is_a_real_uk_postcode_normalises_before_checking_postcode(m
|
||||
("N5 3EF", "N5 3EF"),
|
||||
("N53EF ", "N5 3EF"),
|
||||
("n53Ef", "N5 3EF"),
|
||||
("n5 \u00a0 \t 3Ef", "N5 3EF"),
|
||||
("n5 \u00A0 \t 3Ef", "N5 3EF"),
|
||||
("SO146WB", "SO14 6WB"),
|
||||
("BFPO2", "BFPO 2"),
|
||||
("BFPO232", "BFPO 232"),
|
||||
|
||||
@@ -9,6 +9,7 @@ import pytest
|
||||
from ordered_set import OrderedSet
|
||||
|
||||
from notifications_utils import SMS_CHAR_COUNT_LIMIT
|
||||
from notifications_utils.countries import Country
|
||||
from notifications_utils.formatters import strip_and_remove_obscure_whitespace
|
||||
from notifications_utils.recipients import (
|
||||
Cell,
|
||||
@@ -16,7 +17,11 @@ from notifications_utils.recipients import (
|
||||
Row,
|
||||
first_column_headings,
|
||||
)
|
||||
from notifications_utils.template import EmailPreviewTemplate, SMSMessageTemplate
|
||||
from notifications_utils.template import (
|
||||
EmailPreviewTemplate,
|
||||
LetterImageTemplate,
|
||||
SMSMessageTemplate,
|
||||
)
|
||||
|
||||
|
||||
def _sample_template(template_type, content="foo"):
|
||||
@@ -25,6 +30,11 @@ def _sample_template(template_type, content="foo"):
|
||||
{"content": content, "subject": "bar", "template_type": "email"}
|
||||
),
|
||||
"sms": SMSMessageTemplate({"content": content, "template_type": "sms"}),
|
||||
"letter": LetterImageTemplate(
|
||||
{"content": content, "subject": "bar", "template_type": "letter"},
|
||||
image_url="https://example.com",
|
||||
page_count=1,
|
||||
),
|
||||
}.get(template_type)
|
||||
|
||||
|
||||
@@ -37,6 +47,19 @@ def _index_rows(rows):
|
||||
[
|
||||
("email", ["email address"]),
|
||||
("sms", ["phone number"]),
|
||||
(
|
||||
"letter",
|
||||
[
|
||||
"address line 1",
|
||||
"address line 2",
|
||||
"address line 3",
|
||||
"address line 4",
|
||||
"address line 5",
|
||||
"address line 6",
|
||||
"postcode",
|
||||
"address line 7",
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_recipient_column_headers(template_type, expected):
|
||||
@@ -143,6 +166,34 @@ def test_recipient_column_headers(template_type, expected):
|
||||
],
|
||||
],
|
||||
),
|
||||
(
|
||||
"""
|
||||
address_line_1
|
||||
Alice
|
||||
Bob
|
||||
""",
|
||||
"letter",
|
||||
[[("address_line_1", "Alice")], [("address_line_1", "Bob")]],
|
||||
),
|
||||
(
|
||||
"""
|
||||
address line 1,address line 2,address line 5,address line 6,postcode,name,thing
|
||||
A. Name,,,,XM4 5HQ,example,example
|
||||
""",
|
||||
"letter",
|
||||
[
|
||||
[
|
||||
("addressline1", "A. Name"),
|
||||
("addressline2", None),
|
||||
# optional address rows 3 and 4 not in file
|
||||
("addressline5", None),
|
||||
("addressline5", None),
|
||||
("postcode", "XM4 5HQ"),
|
||||
("name", "example"),
|
||||
("thing", "example"),
|
||||
]
|
||||
],
|
||||
),
|
||||
(
|
||||
"""
|
||||
phone number, list, list, list
|
||||
@@ -510,6 +561,14 @@ def test_get_recipient_respects_order(
|
||||
["email address", "name", "colour"],
|
||||
set(),
|
||||
),
|
||||
(
|
||||
"""
|
||||
address_line_1, address_line_2, postcode, name
|
||||
""",
|
||||
"letter",
|
||||
["address_line_1", "address_line_2", "postcode", "name"],
|
||||
set(),
|
||||
),
|
||||
(
|
||||
"""
|
||||
email address,colour
|
||||
@@ -518,6 +577,14 @@ def test_get_recipient_respects_order(
|
||||
["email address", "colour"],
|
||||
set(["name"]),
|
||||
),
|
||||
(
|
||||
"""
|
||||
address_line_1, address_line_2, name
|
||||
""",
|
||||
"letter",
|
||||
["address_line_1", "address_line_2", "name"],
|
||||
set(),
|
||||
),
|
||||
(
|
||||
"""
|
||||
phone number,list,list,name,list
|
||||
@@ -550,12 +617,40 @@ def test_column_headers(file_contents, template_type, expected, expected_missing
|
||||
pytest.param("", "sms", marks=pytest.mark.xfail),
|
||||
pytest.param("name", "sms", marks=pytest.mark.xfail),
|
||||
pytest.param("email address", "sms", marks=pytest.mark.xfail),
|
||||
pytest.param(
|
||||
"address_line_1",
|
||||
"letter",
|
||||
marks=pytest.mark.xfail,
|
||||
),
|
||||
pytest.param(
|
||||
"address_line_1, address_line_2",
|
||||
"letter",
|
||||
marks=pytest.mark.xfail,
|
||||
),
|
||||
pytest.param(
|
||||
"address_line_6, postcode",
|
||||
"letter",
|
||||
marks=pytest.mark.xfail,
|
||||
),
|
||||
pytest.param(
|
||||
"address_line_1, postcode, address_line_7",
|
||||
"letter",
|
||||
marks=pytest.mark.xfail,
|
||||
),
|
||||
("phone number", "sms"),
|
||||
("phone number,name", "sms"),
|
||||
("email address", "email"),
|
||||
("email address,name", "email"),
|
||||
("PHONENUMBER", "sms"),
|
||||
("email_address", "email"),
|
||||
("address_line_1, address_line_2, postcode", "letter"),
|
||||
("address_line_1, address_line_2, address_line_7", "letter"),
|
||||
("address_line_1, address_line_2, address_line_3", "letter"),
|
||||
("address_line_4, address_line_5, address_line_6", "letter"),
|
||||
(
|
||||
"address_line_1, address_line_2, address_line_3, address_line_4, address_line_5, address_line_6, postcode",
|
||||
"letter",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_recipient_column(content, file_contents, template_type):
|
||||
@@ -603,6 +698,48 @@ def test_recipient_column(content, file_contents, template_type):
|
||||
set(),
|
||||
set(),
|
||||
),
|
||||
(
|
||||
# missing postcode
|
||||
"""
|
||||
address_line_1,address_line_2,address_line_3,address_line_4,address_line_5,postcode,date
|
||||
name, building, street, town, county, SE1 7LS,today
|
||||
name, building, street, town, county, , today
|
||||
""",
|
||||
"letter",
|
||||
{1},
|
||||
set(),
|
||||
),
|
||||
(
|
||||
# not enough address fields
|
||||
"""
|
||||
address_line_1, postcode, date
|
||||
name, SE1 7LS, today
|
||||
""",
|
||||
"letter",
|
||||
{0},
|
||||
set(),
|
||||
),
|
||||
(
|
||||
# optional address fields not filled in
|
||||
"""
|
||||
address_line_1,address_line_2,address_line_3,address_line_4,address_line_5,postcode,date
|
||||
name ,123 fake st. , , , ,SE1 7LS,today
|
||||
name , , , , ,SE1 7LS,today
|
||||
""",
|
||||
"letter",
|
||||
{1},
|
||||
set(),
|
||||
),
|
||||
(
|
||||
# Can use any address columns
|
||||
"""
|
||||
address_line_3, address_line_4, address_line_7, date
|
||||
name , 123 fake st., SE1 7LS, today
|
||||
""",
|
||||
"letter",
|
||||
set(),
|
||||
set(),
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
@@ -628,8 +765,6 @@ def test_bad_or_missing_data(
|
||||
assert recipients.has_errors is True
|
||||
|
||||
|
||||
# TODO, original test for number one had {0, 1, 2}, but it has morphed to {0, 1}
|
||||
# Is +447900123 legit or not? What changed?
|
||||
@pytest.mark.parametrize(
|
||||
("file_contents", "rows_with_bad_recipients"),
|
||||
[
|
||||
@@ -640,7 +775,7 @@ def test_bad_or_missing_data(
|
||||
1234
|
||||
+447900123
|
||||
""",
|
||||
{0, 1},
|
||||
{0, 1, 2},
|
||||
),
|
||||
(
|
||||
"""
|
||||
@@ -649,7 +784,7 @@ def test_bad_or_missing_data(
|
||||
+12022340104, USA
|
||||
+23051234567, Mauritius
|
||||
""",
|
||||
set(),
|
||||
{2},
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -879,14 +1014,14 @@ def test_ignores_spaces_and_case_in_placeholders(key, expected):
|
||||
("\n", None), # newline
|
||||
("\r", None), # carriage return
|
||||
("\t", None), # tab
|
||||
("\u180e", "MONGOLIAN VOWEL SEPARATOR"),
|
||||
("\u200b", "ZERO WIDTH SPACE"),
|
||||
("\u200c", "ZERO WIDTH NON-JOINER"),
|
||||
("\u200d", "ZERO WIDTH JOINER"),
|
||||
("\u180E", "MONGOLIAN VOWEL SEPARATOR"),
|
||||
("\u200B", "ZERO WIDTH SPACE"),
|
||||
("\u200C", "ZERO WIDTH NON-JOINER"),
|
||||
("\u200D", "ZERO WIDTH JOINER"),
|
||||
("\u2060", "WORD JOINER"),
|
||||
("\ufeff", "ZERO WIDTH NO-BREAK SPACE"),
|
||||
("\uFEFF", "ZERO WIDTH NO-BREAK SPACE"),
|
||||
# all the things
|
||||
(" \n\r\t\u000a\u000d\u180e\u200b\u200c\u200d\u2060\ufeff", None),
|
||||
(" \n\r\t\u000A\u000D\u180E\u200B\u200C\u200D\u2060\uFEFF", None),
|
||||
],
|
||||
)
|
||||
def test_ignores_leading_whitespace_in_file(character, name):
|
||||
@@ -1064,6 +1199,23 @@ def test_multiple_email_recipient_columns():
|
||||
assert recipients.has_errors
|
||||
|
||||
|
||||
def test_multiple_letter_recipient_columns():
|
||||
recipients = RecipientCSV(
|
||||
"""
|
||||
address line 1, Address Line 2, address line 1, address_line_2
|
||||
1,2,3,4
|
||||
""",
|
||||
template=_sample_template("letter"),
|
||||
)
|
||||
assert recipients.rows[0].get("addressline1").data == ("3")
|
||||
assert recipients.rows[0].get("addressline1").error is None
|
||||
assert recipients.has_errors
|
||||
assert recipients.duplicate_recipient_column_headers == OrderedSet(
|
||||
["address line 1", "Address Line 2", "address line 1", "address_line_2"]
|
||||
)
|
||||
assert recipients.has_errors
|
||||
|
||||
|
||||
def test_displayed_rows_when_some_rows_have_errors():
|
||||
recipients = RecipientCSV(
|
||||
"""
|
||||
@@ -1109,6 +1261,59 @@ def test_multi_line_placeholders_work():
|
||||
assert recipients.rows[0].personalisation["data"] == "a\nb\n\nc"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("extra_args", "expected_errors", "expected_bad_rows"),
|
||||
[
|
||||
({}, True, {0}),
|
||||
({"allow_international_letters": False}, True, {0}),
|
||||
({"allow_international_letters": True}, False, set()),
|
||||
],
|
||||
)
|
||||
def test_accepts_international_addresses_when_allowed(
|
||||
extra_args, expected_errors, expected_bad_rows
|
||||
):
|
||||
recipients = RecipientCSV(
|
||||
"""
|
||||
address line 1, address line 2, address line 3
|
||||
First Lastname, 123 Example St, Fiji
|
||||
First Lastname, 123 Example St, SW1A 1AA
|
||||
""",
|
||||
template=_sample_template("letter"),
|
||||
**extra_args,
|
||||
)
|
||||
assert recipients.has_errors is expected_errors
|
||||
assert _index_rows(recipients.rows_with_bad_recipients) == expected_bad_rows
|
||||
# Prove that the error isn’t because the given country is unknown
|
||||
assert recipients[0].as_postal_address.country == Country("Fiji")
|
||||
|
||||
|
||||
def test_address_validation_speed():
|
||||
# We should be able to validate 1000 lines of address data in about
|
||||
# a second – if it starts to get slow, something is inefficient
|
||||
number_of_lines = 1000
|
||||
uk_addresses_with_valid_postcodes = "\n".join(
|
||||
(
|
||||
"{n} Example Street, London, {a}{b} {c}{d}{e}".format(
|
||||
n=randrange(1000),
|
||||
a=choice(["n", "e", "sw", "se", "w"]),
|
||||
b=choice(range(1, 10)),
|
||||
c=choice(range(1, 10)),
|
||||
d=choice("ABDefgHJLNPqrstUWxyZ"),
|
||||
e=choice("ABDefgHJLNPqrstUWxyZ"),
|
||||
)
|
||||
for i in range(number_of_lines)
|
||||
)
|
||||
)
|
||||
recipients = RecipientCSV(
|
||||
"address line 1, address line 2, address line 3\n"
|
||||
+ (uk_addresses_with_valid_postcodes),
|
||||
template=_sample_template("letter"),
|
||||
allow_international_letters=False,
|
||||
)
|
||||
for row in recipients:
|
||||
assert not row.has_bad_postal_address
|
||||
|
||||
|
||||
def test_email_validation_speed():
|
||||
email_addresses = "\n".join(
|
||||
(
|
||||
|
||||
@@ -24,16 +24,21 @@ valid_us_phone_numbers = [
|
||||
"(202) 555-0104",
|
||||
]
|
||||
|
||||
# TODO
|
||||
# International phone number tests are commented out as a result of issue #943 in notifications-admin. We are
|
||||
# deliberately eliminating the ability to send to numbers outside of country code 1. These tests should
|
||||
# be removed at some point when we are sure we are never going to support international numbers
|
||||
|
||||
valid_international_phone_numbers = [
|
||||
"+71234567890", # Russia
|
||||
"+447123456789", # UK
|
||||
"+4407123456789", # UK
|
||||
"+4407123 456789", # UK
|
||||
"+4407123-456-789", # UK
|
||||
"+23051234567", # Mauritius,
|
||||
"+682 12345", # Cook islands
|
||||
"+3312345678",
|
||||
"+9-2345-12345-12345", # 15 digits
|
||||
# "+71234567890", # Russia
|
||||
# "+447123456789", # UK
|
||||
# "+4407123456789", # UK
|
||||
# "+4407123 456789", # UK
|
||||
# "+4407123-456-789", # UK
|
||||
# "+23051234567", # Mauritius,
|
||||
# "+682 12345", # Cook islands
|
||||
# "+3312345678",
|
||||
# "+9-2345-12345-12345", # 15 digits
|
||||
]
|
||||
|
||||
|
||||
@@ -80,7 +85,7 @@ invalid_us_phone_numbers = sum(
|
||||
invalid_phone_numbers = [
|
||||
("+80233456789", "Not a valid country prefix"),
|
||||
("1234567", "Not enough digits"),
|
||||
("+682 1234", "Not enough digits"), # Cook Islands phone numbers are 5 digits
|
||||
("+682 1234", "Invalid country code"), # Cook Islands phone numbers can be 5 digits
|
||||
("+12345 12345 12345 6", "Too many digits"),
|
||||
]
|
||||
|
||||
@@ -151,46 +156,46 @@ def test_detect_us_phone_numbers(phone_number):
|
||||
@pytest.mark.parametrize(
|
||||
("phone_number", "expected_info"),
|
||||
[
|
||||
(
|
||||
"+4407900900123",
|
||||
international_phone_info(
|
||||
international=True,
|
||||
country_prefix="44", # UK
|
||||
billable_units=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"+4407700900123",
|
||||
international_phone_info(
|
||||
international=True,
|
||||
country_prefix="44", # Number in TV range
|
||||
billable_units=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"+4407700800123",
|
||||
international_phone_info(
|
||||
international=True,
|
||||
country_prefix="44", # UK Crown dependency, so prefix same as UK
|
||||
billable_units=1,
|
||||
),
|
||||
),
|
||||
( #
|
||||
"+20-12-1234-1234",
|
||||
international_phone_info(
|
||||
international=True,
|
||||
country_prefix="20", # Egypt
|
||||
billable_units=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"+201212341234",
|
||||
international_phone_info(
|
||||
international=True,
|
||||
country_prefix="20", # Egypt
|
||||
billable_units=1,
|
||||
),
|
||||
),
|
||||
# (
|
||||
# "+4407900900123",
|
||||
# international_phone_info(
|
||||
# international=True,
|
||||
# country_prefix="44", # UK
|
||||
# billable_units=1,
|
||||
# ),
|
||||
# ),
|
||||
# (
|
||||
# "+4407700900123",
|
||||
# international_phone_info(
|
||||
# international=True,
|
||||
# country_prefix="44", # Number in TV range
|
||||
# billable_units=1,
|
||||
# ),
|
||||
# ),
|
||||
# (
|
||||
# "+4407700800123",
|
||||
# international_phone_info(
|
||||
# international=True,
|
||||
# country_prefix="44", # UK Crown dependency, so prefix same as UK
|
||||
# billable_units=1,
|
||||
# ),
|
||||
# ),
|
||||
# ( #
|
||||
# "+20-12-1234-1234",
|
||||
# international_phone_info(
|
||||
# international=True,
|
||||
# country_prefix="20", # Egypt
|
||||
# billable_units=1,
|
||||
# ),
|
||||
# ),
|
||||
# (
|
||||
# "+201212341234",
|
||||
# international_phone_info(
|
||||
# international=True,
|
||||
# country_prefix="20", # Egypt
|
||||
# billable_units=1,
|
||||
# ),
|
||||
# ),
|
||||
(
|
||||
"+1 664-491-3434",
|
||||
international_phone_info(
|
||||
@@ -199,14 +204,14 @@ def test_detect_us_phone_numbers(phone_number):
|
||||
billable_units=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"+71234567890",
|
||||
international_phone_info(
|
||||
international=True,
|
||||
country_prefix="7", # Russia
|
||||
billable_units=1,
|
||||
),
|
||||
),
|
||||
# (
|
||||
# "+71234567890",
|
||||
# international_phone_info(
|
||||
# international=True,
|
||||
# country_prefix="7", # Russia
|
||||
# billable_units=1,
|
||||
# ),
|
||||
# ),
|
||||
(
|
||||
"1-202-555-0104",
|
||||
international_phone_info(
|
||||
@@ -223,14 +228,14 @@ def test_detect_us_phone_numbers(phone_number):
|
||||
billable_units=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"+23051234567",
|
||||
international_phone_info(
|
||||
international=True,
|
||||
country_prefix="230", # Mauritius
|
||||
billable_units=1,
|
||||
),
|
||||
),
|
||||
# (
|
||||
# "+23051234567",
|
||||
# international_phone_info(
|
||||
# international=True,
|
||||
# country_prefix="230", # Mauritius
|
||||
# billable_units=1,
|
||||
# ),
|
||||
# ),
|
||||
],
|
||||
)
|
||||
def test_get_international_info(phone_number, expected_info):
|
||||
@@ -283,11 +288,11 @@ def test_valid_us_phone_number_can_be_formatted_consistently(phone_number):
|
||||
@pytest.mark.parametrize(
|
||||
("phone_number", "expected_formatted"),
|
||||
[
|
||||
("+44071234567890", "+4471234567890"),
|
||||
# ("+44071234567890", "+4471234567890"),
|
||||
("1-202-555-0104", "+12025550104"),
|
||||
("+12025550104", "+12025550104"),
|
||||
("12025550104", "+12025550104"),
|
||||
("+23051234567", "+23051234567"),
|
||||
# ("+23051234567", "+23051234567"),
|
||||
],
|
||||
)
|
||||
def test_valid_international_phone_number_can_be_formatted_consistently(
|
||||
@@ -339,7 +344,7 @@ def test_validate_email_address_accepts_valid(email_address):
|
||||
" email@domain.com ",
|
||||
"\temail@domain.com",
|
||||
"\temail@domain.com\n",
|
||||
"\u200bemail@domain.com\u200b",
|
||||
"\u200Bemail@domain.com\u200B",
|
||||
],
|
||||
)
|
||||
def test_validate_email_address_strips_whitespace(email):
|
||||
@@ -363,17 +368,17 @@ def test_validates_against_guestlist_of_phone_numbers(phone_number):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recipient_number, allowlist_number",
|
||||
[
|
||||
["+4407123-456-789", "+4407123456789"],
|
||||
["+4407123456789", "+4407123-456-789"],
|
||||
],
|
||||
)
|
||||
def test_validates_against_guestlist_of_international_phone_numbers(
|
||||
recipient_number, allowlist_number
|
||||
):
|
||||
assert allowed_to_send_to(recipient_number, [allowlist_number])
|
||||
# @pytest.mark.parametrize(
|
||||
# "recipient_number, allowlist_number",
|
||||
# [
|
||||
# ["+4407123-456-789", "+4407123456789"],
|
||||
# ["+4407123456789", "+4407123-456-789"],
|
||||
# ],
|
||||
# )
|
||||
# def test_validates_against_guestlist_of_international_phone_numbers(
|
||||
# recipient_number, allowlist_number
|
||||
# ):
|
||||
# assert allowed_to_send_to(recipient_number, [allowlist_number])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("email_address", valid_email_addresses)
|
||||
@@ -383,21 +388,19 @@ def test_validates_against_guestlist_of_email_addresses(email_address):
|
||||
)
|
||||
|
||||
|
||||
# TODO something wrong with formatting Egyptian numbers, doesn't seem
|
||||
# like this would affect sendability need to confirm with AWS simulated numbers.
|
||||
@pytest.mark.parametrize(
|
||||
("phone_number", "expected_formatted"),
|
||||
[
|
||||
("+4407900900123", "+44 7900 900123"), # UK
|
||||
("+44(0)7900900123", "+44 7900 900123"), # UK
|
||||
("+447900900123", "+44 7900 900123"), # UK
|
||||
# ("+4407900900123", "+44 7900 900123"), # UK
|
||||
# ("+44(0)7900900123", "+44 7900 900123"), # UK
|
||||
# ("+447900900123", "+44 7900 900123"), # UK
|
||||
# ("+20-12-1234-1234", "+20 121 234 1234"), # Egypt
|
||||
# ("+201212341234", "+20 121 234 1234"), # Egypt
|
||||
("+1 664 491-3434", "+1 664-491-3434"), # Montserrat
|
||||
("+7 499 1231212", "+7 499 123-12-12"), # Moscow (Russia)
|
||||
# ("+7 499 1231212", "+7 499 123-12-12"), # Moscow (Russia)
|
||||
("1-202-555-0104", "(202) 555-0104"), # Washington DC (USA)
|
||||
("+23051234567", "+230 5123 4567"), # Mauritius
|
||||
("+33(0)1 12345678", "+33 1 12 34 56 78"), # Paris (France)
|
||||
# ("+23051234567", "+230 5123 4567"), # Mauritius
|
||||
# ("+33(0)1 12345678", "+33 1 12 34 56 78"), # Paris (France)
|
||||
],
|
||||
)
|
||||
def test_format_us_and_international_phone_numbers(phone_number, expected_formatted):
|
||||
@@ -414,7 +417,7 @@ def test_format_us_and_international_phone_numbers(phone_number, expected_format
|
||||
(None, ""),
|
||||
("foo", "foo"),
|
||||
("TeSt@ExAmPl3.com", "test@exampl3.com"),
|
||||
("+4407900 900 123", "+447900900123"),
|
||||
# ("+4407900 900 123", "+447900900123"),
|
||||
("+1 800 555 5555", "+18005555555"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ params, ids = zip(
|
||||
("…", "..."),
|
||||
"compatibility transform unicode char (HORIZONTAL ELLIPSIS (U+2026)",
|
||||
),
|
||||
(("\u200b", ""), "compatibility transform unicode char (ZERO WIDTH SPACE (U+200B)"),
|
||||
(("\u200B", ""), "compatibility transform unicode char (ZERO WIDTH SPACE (U+200B)"),
|
||||
(
|
||||
("‘", "'"),
|
||||
"compatibility transform unicode char (LEFT SINGLE QUOTATION MARK (U+2018)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user