From 4e9b265ee3d52c94bb88ce34433b70629c192219 Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 11 Apr 2024 14:25:39 -0500 Subject: [PATCH 01/11] Add ruff for Python linting --- .circleci/config.yml | 12 ++++++++++-- .circleci/python_job.bash | 21 ++++++++++++++++++--- requirements.txt | 1 + 3 files changed, 29 insertions(+), 5 deletions(-) mode change 100644 => 100755 .circleci/python_job.bash diff --git a/.circleci/config.yml b/.circleci/config.yml index 6b11421cc2..501469abd2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -378,7 +378,7 @@ jobs: description: "What command should the job run?" default: "pytest" type: enum - enum: ["pytest", "black", "mypy", "build_email_tracker_lists", "build_glean", "check_glean"] + enum: ["pytest", "black", "mypy", "build_email_tracker_lists", "build_glean", "check_glean", "ruff"] test_results_filename: description: "What is the name of the jUnit XML test output? (Optional)" default: "" @@ -494,6 +494,7 @@ jobs: echo "export TEST_DB_NAME=$(printf '%q' "${TEST_DB_NAME}")" >> "$TMP_ENV" echo "export TEST_DB_URL=$(printf '%q' "${TEST_DB_URL}")" >> "$TMP_ENV" echo "export DATABASE_ENGINE=$(printf '%q' "${DATABASE_ENGINE}")" >> "$TMP_ENV" + echo "export TEST_RESULTS_DIR=job-results" >> "$TMP_ENV" cat "$TMP_ENV" | tee --append "$BASH_ENV" cat /home/circleci/project/.circleci/python_job.bash >> "$BASH_ENV" @@ -504,7 +505,7 @@ jobs: steps: - run: name: Create job-results directory - command: mkdir job-results + command: mkdir -p "$TEST_RESULTS_DIR" - run: name: Set test defaults command: cp .env-dist .env @@ -608,6 +609,13 @@ workflows: command: black filters: *default_filters + - python_job: + name: ruff linting check + command: ruff + test_results_filename: "ruff.xml" + allow_fail: true + filters: *default_filters + - python_job: name: mypy type check command: mypy diff --git a/.circleci/python_job.bash b/.circleci/python_job.bash old mode 100644 new mode 100755 index 34d84e6aaf..f131aeb240 --- a/.circleci/python_job.bash +++ b/.circleci/python_job.bash @@ -8,6 +8,7 @@ : ${PHONES_ENABLED:=0} : ${PYTEST_FAIL_FAST:=0} : ${PYTEST_MIGRATIONS_MODE:=0} +: ${TEST_RESULTS_DIR:=tmp} : ${TEST_RESULTS_FILENAME:=} : ${DATABASE_URL:=sqlite:///db.sqlite3} : ${TEST_DB_NAME:=test.sqlite3} @@ -28,7 +29,7 @@ function run_mypy { if [ $MYPY_STRICT -ne 0 ]; then MYPY_ARGS+=("--strict"); fi if [ -n "$TEST_RESULTS_FILENAME" ] then - MYPY_ARGS+=("--junit-xml" "job-results/${TEST_RESULTS_FILENAME}") + MYPY_ARGS+=("--junit-xml" "${TEST_RESULTS_DIR}/${TEST_RESULTS_FILENAME}") fi MYPY_ARGS+=(".") @@ -56,7 +57,7 @@ function run_pytest { if [ $CREATE_DB -ne 0 ]; then PYTEST_ARGS+=("--create-db"); fi if [ -n "$TEST_RESULTS_FILENAME" ] && [ $SKIP_RESULTS != 1 ] then - PYTEST_ARGS+=("--junit-xml=job-results/$TEST_RESULTS_FILENAME") + PYTEST_ARGS+=("--junit-xml=${TEST_RESULTS_DIR}/$TEST_RESULTS_FILENAME") fi PYTEST_ARGS+=(".") @@ -169,6 +170,20 @@ function run_check_glean { fi } +# Run ruff to lint python code +function run_ruff { + if [ -n "$TEST_RESULTS_FILENAME" ] + then + # Run with output to a test results file instead of stdout + set -x + ruff check --exit-zero --output-format junit --output-file "${TEST_RESULTS_DIR}/${TEST_RESULTS_FILENAME}" . + { set +x; } 2>/dev/null + fi + + set -x + ruff check . +} + # Run a command by name # $1 - The command to run - black, mypy, pytest, or build_email_tracker_lists @@ -176,7 +191,7 @@ function run_check_glean { function run_command { local COMMAND=${1:-} case $COMMAND in - black | mypy | pytest | build_email_tracker_lists | build_glean | check_glean) + black | mypy | pytest | build_email_tracker_lists | build_glean | check_glean | ruff) :;; "") echo "No command passed - '$COMMAND'" diff --git a/requirements.txt b/requirements.txt index f0325ff23e..14da5d88d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,6 +47,7 @@ responses==0.25.0 # linting black==24.4.0 +ruff==0.3.6 # type hinting boto3-stubs==1.34.88 From 663a0807463a4e2887cd1071b189624b96ba81ff Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 11 Apr 2024 15:06:38 -0500 Subject: [PATCH 02/11] Fix F401 - unused imports --- api/renderers.py | 1 - emails/management/commands/deactivate_user_by_token.py | 2 +- emails/management/commands/get_latest_email_tracker_lists.py | 1 - emails/migrations/0004_auto_20190612_2047.py | 1 - .../migrations/0028_copy_subdomain_to_registeredsubdomain.py | 2 +- emails/tests/mgmt_check_health_tests.py | 3 --- mypy_stubs/decouple.pyi | 2 +- privaterelay/cleaners.py | 2 +- privaterelay/management/commands/cleanup_data.py | 2 +- privaterelay/views.py | 2 +- 10 files changed, 6 insertions(+), 12 deletions(-) diff --git a/api/renderers.py b/api/renderers.py index a838dc87f7..6b04839a10 100644 --- a/api/renderers.py +++ b/api/renderers.py @@ -1,6 +1,5 @@ import vobject -from django.conf import settings from rest_framework import renderers diff --git a/emails/management/commands/deactivate_user_by_token.py b/emails/management/commands/deactivate_user_by_token.py index 574137aa1f..04df05b9f6 100644 --- a/emails/management/commands/deactivate_user_by_token.py +++ b/emails/management/commands/deactivate_user_by_token.py @@ -1,4 +1,4 @@ -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from ...models import Profile diff --git a/emails/management/commands/get_latest_email_tracker_lists.py b/emails/management/commands/get_latest_email_tracker_lists.py index e125f729fc..57a54d256d 100644 --- a/emails/management/commands/get_latest_email_tracker_lists.py +++ b/emails/management/commands/get_latest_email_tracker_lists.py @@ -1,4 +1,3 @@ -import json import pathlib from django.core.management.base import BaseCommand diff --git a/emails/migrations/0004_auto_20190612_2047.py b/emails/migrations/0004_auto_20190612_2047.py index 2913ce26d6..52f9ab08d1 100644 --- a/emails/migrations/0004_auto_20190612_2047.py +++ b/emails/migrations/0004_auto_20190612_2047.py @@ -1,5 +1,4 @@ # Generated by Django 2.2.2 on 2019-06-12 20:47 -import uuid from django.db import migrations diff --git a/emails/migrations/0028_copy_subdomain_to_registeredsubdomain.py b/emails/migrations/0028_copy_subdomain_to_registeredsubdomain.py index f635991c2c..981d9cceed 100644 --- a/emails/migrations/0028_copy_subdomain_to_registeredsubdomain.py +++ b/emails/migrations/0028_copy_subdomain_to_registeredsubdomain.py @@ -1,6 +1,6 @@ # Generated by Django 2.2.24 on 2021-10-15 05:22 -from django.db import migrations, models +from django.db import migrations from emails.models import hash_subdomain diff --git a/emails/tests/mgmt_check_health_tests.py b/emails/tests/mgmt_check_health_tests.py index 012de94519..f610ae0b42 100644 --- a/emails/tests/mgmt_check_health_tests.py +++ b/emails/tests/mgmt_check_health_tests.py @@ -1,5 +1,4 @@ from datetime import datetime, timezone, timedelta -from unittest.mock import ANY, patch import json import logging @@ -7,8 +6,6 @@ from django.core.management import call_command, CommandError -from emails.management.commands.check_health import Command - @pytest.fixture(autouse=True) def test_settings(settings, tmp_path): diff --git a/mypy_stubs/decouple.pyi b/mypy_stubs/decouple.pyi index 287a2ac8dc..87c4b0ed65 100644 --- a/mypy_stubs/decouple.pyi +++ b/mypy_stubs/decouple.pyi @@ -11,7 +11,7 @@ Changes: """ from collections.abc import Sequence -from typing import Any, Callable, Generic, Optional, TypeVar, Union, overload +from typing import Any, Callable, Generic, TypeVar, Union, overload # Unreleased as of 3.6 - accepts a bool # def strtobool(value: Union[str, bool]) -> bool: ... diff --git a/privaterelay/cleaners.py b/privaterelay/cleaners.py index f9f4f6348f..a2fc1341ab 100644 --- a/privaterelay/cleaners.py +++ b/privaterelay/cleaners.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any Counts = dict[str, dict[str, int]] CleanupData = dict[str, Any] diff --git a/privaterelay/management/commands/cleanup_data.py b/privaterelay/management/commands/cleanup_data.py index a456554f51..401005ed6f 100644 --- a/privaterelay/management/commands/cleanup_data.py +++ b/privaterelay/management/commands/cleanup_data.py @@ -1,7 +1,7 @@ from __future__ import annotations from argparse import RawDescriptionHelpFormatter from shutil import get_terminal_size -from typing import Any, Optional, TYPE_CHECKING +from typing import Any, TYPE_CHECKING import textwrap import logging diff --git a/privaterelay/views.py b/privaterelay/views.py index a188b52cd2..fffbb55c8a 100644 --- a/privaterelay/views.py +++ b/privaterelay/views.py @@ -1,7 +1,7 @@ from datetime import datetime, timezone from functools import lru_cache from hashlib import sha256 -from typing import Any, Optional, TypedDict +from typing import Any, TypedDict from collections.abc import Iterable import json import logging From ea5080f87c42746c5aefbea71da6116a5bb07f2f Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 11 Apr 2024 15:12:48 -0500 Subject: [PATCH 03/11] fix E402 Module level import not at top of file --- api/tests/iq_views_tests.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/tests/iq_views_tests.py b/api/tests/iq_views_tests.py index ecb3196bea..b18274a8a4 100644 --- a/api/tests/iq_views_tests.py +++ b/api/tests/iq_views_tests.py @@ -3,10 +3,6 @@ from django.conf import settings -pytestmark = pytest.mark.skipif( - not settings.PHONES_ENABLED, reason="PHONES_ENABLED is False" -) -pytestmark = pytest.mark.skipif(not settings.IQ_ENABLED, reason="IQ_ENABLED is False") import responses from unittest.mock import Mock, patch @@ -24,6 +20,11 @@ from api.tests.phones_views_tests import _make_real_phone, _make_relay_number +pytestmark = pytest.mark.skipif( + not settings.PHONES_ENABLED, reason="PHONES_ENABLED is False" +) +pytestmark = pytest.mark.skipif(not settings.IQ_ENABLED, reason="IQ_ENABLED is False") + API_ROOT = "http://127.0.0.1:8000" INBOUND_SMS_PATH = f"{API_ROOT}/api/v1/inbound_sms_iq/" From 4473241cea11f37a1274ab8dc5a54b498895d8a6 Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 11 Apr 2024 15:16:59 -0500 Subject: [PATCH 04/11] fix E712 Avoid equality comparisons to True / False --- api/tests/serializers_tests.py | 12 ++++++------ emails/tests/mgmt_send_welcome_emails_tests.py | 2 +- privaterelay/tests/signals_tests.py | 4 ++-- pyproject.toml | 8 ++++++++ 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/api/tests/serializers_tests.py b/api/tests/serializers_tests.py index 4a1cc0140d..8391f443bc 100644 --- a/api/tests/serializers_tests.py +++ b/api/tests/serializers_tests.py @@ -16,7 +16,7 @@ class PremiumValidatorsTest(APITestCase): def test_non_premium_cant_set_block_list_emails(self): free_user = make_free_test_user() free_alias = baker.make(RelayAddress, user=free_user) - assert free_alias.block_list_emails == False + assert free_alias.block_list_emails is False url = reverse("relayaddress-detail", args=[free_alias.id]) data = {"block_list_emails": True} @@ -25,12 +25,12 @@ def test_non_premium_cant_set_block_list_emails(self): response = self.client.patch(url, data, format="json") assert response.status_code == 401 - assert free_alias.block_list_emails == False + assert free_alias.block_list_emails is False def test_non_premium_can_clear_block_list_emails(self): free_user = make_free_test_user() free_alias = baker.make(RelayAddress, user=free_user) - assert free_alias.block_list_emails == False + assert free_alias.block_list_emails is False url = reverse("relayaddress-detail", args=[free_alias.id]) data = {"block_list_emails": False} @@ -40,12 +40,12 @@ def test_non_premium_can_clear_block_list_emails(self): assert response.status_code == 200 free_alias.refresh_from_db() - assert free_alias.block_list_emails == False + assert free_alias.block_list_emails is False def test_premium_can_set_block_list_emails(self): premium_user = make_premium_test_user() premium_alias = baker.make(RelayAddress, user=premium_user) - assert premium_alias.block_list_emails == False + assert premium_alias.block_list_emails is False url = reverse("relayaddress-detail", args=[premium_alias.id]) data = {"block_list_emails": True} @@ -55,7 +55,7 @@ def test_premium_can_set_block_list_emails(self): assert response.status_code == 200 premium_alias.refresh_from_db() - assert premium_alias.block_list_emails == True + assert premium_alias.block_list_emails is True @pytest.fixture diff --git a/emails/tests/mgmt_send_welcome_emails_tests.py b/emails/tests/mgmt_send_welcome_emails_tests.py index d448190f15..96b62edd56 100644 --- a/emails/tests/mgmt_send_welcome_emails_tests.py +++ b/emails/tests/mgmt_send_welcome_emails_tests.py @@ -105,7 +105,7 @@ def test_invalid_email_address_skips_invalid( call_command(COMMAND_NAME) invalid_email_user.profile.refresh_from_db() - assert invalid_email_user.profile.sent_welcome_email == False + assert invalid_email_user.profile.sent_welcome_email is False rec1, rec2, rec3, rec4, rec5 = caplog.records assert "Starting" in rec1.getMessage() diff --git a/privaterelay/tests/signals_tests.py b/privaterelay/tests/signals_tests.py index 70d6c9fc7e..69e3bd5070 100644 --- a/privaterelay/tests/signals_tests.py +++ b/privaterelay/tests/signals_tests.py @@ -33,5 +33,5 @@ def get_response(_: HttpRequest): middleware.process_request(sign_up_request) record_user_signed_up(sign_up_request, user) - assert sign_up_request.session["user_created"] == True - assert sign_up_request.session.modified == True + assert sign_up_request.session["user_created"] is True + assert sign_up_request.session.modified is True diff --git a/pyproject.toml b/pyproject.toml index 5f6ac951b9..4eb1a6dcf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,3 +78,11 @@ testpaths = [ "phones", "privaterelay", ] + +[tool.ruff.lint] +extend-safe-fixes = [ + # E712 Avoid equality comparisons to True / False + # Changes '== True' to 'is True' + "E712", +] + From 1c430eeda0adf0abfacf5bf01725b6bbd2a300dc Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 11 Apr 2024 15:19:50 -0500 Subject: [PATCH 05/11] fix E711 Comparision to None --- privaterelay/apps.py | 2 +- pyproject.toml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/privaterelay/apps.py b/privaterelay/apps.py index af96eed7d4..5dba499eb2 100644 --- a/privaterelay/apps.py +++ b/privaterelay/apps.py @@ -48,7 +48,7 @@ def ready(self) -> None: ): # Set up Google Cloud Profiler service, version = get_profiler_startup_data() - if service != None: + if service is not None: gcp_key_json_path = Path(settings.GOOGLE_APPLICATION_CREDENTIALS) if not gcp_key_json_path.exists(): write_gcp_key_json_file(gcp_key_json_path) diff --git a/pyproject.toml b/pyproject.toml index 4eb1a6dcf1..895473b507 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,9 @@ testpaths = [ [tool.ruff.lint] extend-safe-fixes = [ + # E711 Comparison to `None` should be `cond is / is not None` + # Changes '== None' to 'is None' + "E711", # E712 Avoid equality comparisons to True / False # Changes '== True' to 'is True' "E712", From b5cffdda3b79ea7117f6a32784aceb3fc56d833e Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 11 Apr 2024 15:22:02 -0500 Subject: [PATCH 06/11] fix E721 Do not compare types, use `isinstance()` --- api/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/authentication.py b/api/authentication.py index c268407523..9151e3dea8 100644 --- a/api/authentication.py +++ b/api/authentication.py @@ -95,7 +95,7 @@ def get_fxa_uid_from_oauth_token(token: str, use_cache=True) -> str: # cache valid access_token and fxa_resp_data until access_token expiration # TODO: revisit this since the token can expire before its time - if type(fxa_resp_data.get("json", {}).get("exp")) is int: + if isinstance(fxa_resp_data.get("json", {}).get("exp"), int): # Note: FXA iat and exp are timestamps in *milliseconds* fxa_token_exp_time = int(fxa_resp_data["json"]["exp"] / 1000) now_time = int(datetime.now(timezone.utc).timestamp()) From 01d227e95ee34c8530f423576234d803e8c92bf8 Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 11 Apr 2024 16:27:47 -0500 Subject: [PATCH 07/11] Enable remaining pycodestyle rules, fix or ignore The remaining rules check line length and for trailing spaces --- api/renderers.py | 5 +++-- emails/migrations/0050_profile_store_phone_log.py | 1 + ...r_day_and_num_email_forwarded_per_day_fields.py | 1 + .../0054_profile_forwarded_first_reply.py | 1 + .../migrations/0057_profile_sent_welcome_email.py | 1 + .../0058_profile_onboarding_free_state.py | 1 + ..._and_num_deleted_domain_addresses_to_profile.py | 1 + emails/tests/sns_tests.py | 5 ++++- .../0020_inboundcontact_last_inbound_type.py | 1 + .../0021_add_relaynumber_stats_20220913_1959.py | 1 + ..._relaynumber_remaining_seconds_20220921_1829.py | 1 + ...023_relaynumber_deprecated_remaining_minutes.py | 2 +- phones/migrations/0024_add_country_code.py | 2 +- phones/migrations/0027_relaynumber_vendor.py | 4 ++-- phones/models.py | 3 ++- privaterelay/apps.py | 4 +++- .../sync_phone_related_dates_on_profile.py | 14 +++++++++++--- .../commands/update_phone_remaining_stats.py | 3 ++- pyproject.toml | 8 ++++++++ 19 files changed, 46 insertions(+), 13 deletions(-) diff --git a/api/renderers.py b/api/renderers.py index 6b04839a10..e6f400bd0e 100644 --- a/api/renderers.py +++ b/api/renderers.py @@ -12,8 +12,9 @@ def render(self, data, accepted_media_type=None, renderer_context=None): vCard = vobject.vCard() vCard.add("FN").value = "Firefox Relay" # TODO: fix static urls - # photo_url is a base64 encoding of /static/images/email-images/relay-logo-vcard.png - photo_url = "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDcuMS1jMDAwIDc5LjljY2M0ZGU5MywgMjAyMi8wMy8xNC0xNDowNzoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjMgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QjgxNjQwMjQwNzIxMTFFRDgyREFEMkI1QjYxNzQ1RjAiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QjgxNjQwMjUwNzIxMTFFRDgyREFEMkI1QjYxNzQ1RjAiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpCODE2NDAyMjA3MjExMUVEODJEQUQyQjVCNjE3NDVGMCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpCODE2NDAyMzA3MjExMUVEODJEQUQyQjVCNjE3NDVGMCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkTS+8oAAD2QSURBVHja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" + # photo_url is a base64 encoding of + # /static/images/email-images/relay-logo-vcard.png + photo_url = "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDcuMS1jMDAwIDc5LjljY2M0ZGU5MywgMjAyMi8wMy8xNC0xNDowNzoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjMgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QjgxNjQwMjQwNzIxMTFFRDgyREFEMkI1QjYxNzQ1RjAiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QjgxNjQwMjUwNzIxMTFFRDgyREFEMkI1QjYxNzQ1RjAiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpCODE2NDAyMjA3MjExMUVEODJEQUQyQjVCNjE3NDVGMCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpCODE2NDAyMzA3MjExMUVEODJEQUQyQjVCNjE3NDVGMCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkTS+8oAAD2QSURBVHja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" # noqa: E501 vCard.add("PHOTO;ENCODING=BASE64;TYPE=PNG").value = photo_url vCard.add("LOGO").value = photo_url vCard.add("tel") diff --git a/emails/migrations/0050_profile_store_phone_log.py b/emails/migrations/0050_profile_store_phone_log.py index 33e6217d51..ca0edcbb63 100644 --- a/emails/migrations/0050_profile_store_phone_log.py +++ b/emails/migrations/0050_profile_store_phone_log.py @@ -1,4 +1,5 @@ # Generated by Django 3.2.14 on 2022-08-05 14:28 +# ruff: noqa: E501 from django.db import migrations, models diff --git a/emails/migrations/0051_add_email_size_per_day_and_num_email_forwarded_per_day_fields.py b/emails/migrations/0051_add_email_size_per_day_and_num_email_forwarded_per_day_fields.py index 156a455038..ba492dda4d 100644 --- a/emails/migrations/0051_add_email_size_per_day_and_num_email_forwarded_per_day_fields.py +++ b/emails/migrations/0051_add_email_size_per_day_and_num_email_forwarded_per_day_fields.py @@ -1,4 +1,5 @@ # Generated by Django 3.2.14 on 2022-08-30 15:34 +# ruff: noqa: E501 from django.db import migrations, models diff --git a/emails/migrations/0054_profile_forwarded_first_reply.py b/emails/migrations/0054_profile_forwarded_first_reply.py index f359f3e5c8..ce9b26c802 100644 --- a/emails/migrations/0054_profile_forwarded_first_reply.py +++ b/emails/migrations/0054_profile_forwarded_first_reply.py @@ -1,4 +1,5 @@ # Generated by Django 3.2.16 on 2022-12-22 21:23 +# ruff: noqa: E501 from django.db import migrations, models diff --git a/emails/migrations/0057_profile_sent_welcome_email.py b/emails/migrations/0057_profile_sent_welcome_email.py index 659d209acb..ca02e8df8a 100644 --- a/emails/migrations/0057_profile_sent_welcome_email.py +++ b/emails/migrations/0057_profile_sent_welcome_email.py @@ -1,4 +1,5 @@ # Generated by Django 3.2.19 on 2023-08-07 20:13 +# ruff: noqa: E501 from django.db import migrations, models diff --git a/emails/migrations/0058_profile_onboarding_free_state.py b/emails/migrations/0058_profile_onboarding_free_state.py index ae473612b2..b6b07569bb 100644 --- a/emails/migrations/0058_profile_onboarding_free_state.py +++ b/emails/migrations/0058_profile_onboarding_free_state.py @@ -1,4 +1,5 @@ # Generated by Django 3.2.19 on 2023-10-19 23:46 +# ruff: noqa: W291,E501 from django.db import migrations, models diff --git a/emails/migrations/0060_add_num_deleted_relay_addresses_and_num_deleted_domain_addresses_to_profile.py b/emails/migrations/0060_add_num_deleted_relay_addresses_and_num_deleted_domain_addresses_to_profile.py index 6a5ef1e224..d8a4fd6668 100644 --- a/emails/migrations/0060_add_num_deleted_relay_addresses_and_num_deleted_domain_addresses_to_profile.py +++ b/emails/migrations/0060_add_num_deleted_relay_addresses_and_num_deleted_domain_addresses_to_profile.py @@ -1,4 +1,5 @@ # Generated by Django 4.2.8 on 2024-01-02 20:18 +# ruff: noqa: W291,E501 from django.db import migrations, models diff --git a/emails/tests/sns_tests.py b/emails/tests/sns_tests.py index 8f2e5f3007..092c57a005 100644 --- a/emails/tests/sns_tests.py +++ b/emails/tests/sns_tests.py @@ -9,7 +9,10 @@ class GrabKeyfileTest(TestCase): @patch("emails.sns.urlopen") def test_grab_keyfile_checks_cert_url_origin(self, mock_urlopen): - cert_url = "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem" + cert_url = ( + "https://sns.us-east-1.amazonaws.com/" + "SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem" + ) assert mock_urlopen.called_once_with(cert_url) with self.assertRaises(SuspiciousOperation): diff --git a/phones/migrations/0020_inboundcontact_last_inbound_type.py b/phones/migrations/0020_inboundcontact_last_inbound_type.py index 9a413cf233..975871aa67 100644 --- a/phones/migrations/0020_inboundcontact_last_inbound_type.py +++ b/phones/migrations/0020_inboundcontact_last_inbound_type.py @@ -1,4 +1,5 @@ # Generated by Django 3.2.15 on 2022-08-15 17:42 +# ruff: noqa: E501 from django.db import migrations, models diff --git a/phones/migrations/0021_add_relaynumber_stats_20220913_1959.py b/phones/migrations/0021_add_relaynumber_stats_20220913_1959.py index 5a005beb4f..8ffb171d9a 100644 --- a/phones/migrations/0021_add_relaynumber_stats_20220913_1959.py +++ b/phones/migrations/0021_add_relaynumber_stats_20220913_1959.py @@ -1,4 +1,5 @@ # Generated by Django 3.2.15 on 2022-09-13 19:59 +# ruff: noqa: E501 from django.db import migrations, models diff --git a/phones/migrations/0022_relaynumber_remaining_seconds_20220921_1829.py b/phones/migrations/0022_relaynumber_remaining_seconds_20220921_1829.py index d042e16921..0881b83279 100644 --- a/phones/migrations/0022_relaynumber_remaining_seconds_20220921_1829.py +++ b/phones/migrations/0022_relaynumber_remaining_seconds_20220921_1829.py @@ -1,4 +1,5 @@ # Generated by Django 3.2.15 on 2022-09-21 18:29 +# ruff: noqa: E501 from django.db import migrations, models diff --git a/phones/migrations/0023_relaynumber_deprecated_remaining_minutes.py b/phones/migrations/0023_relaynumber_deprecated_remaining_minutes.py index 47929b6b2d..9813db07b1 100644 --- a/phones/migrations/0023_relaynumber_deprecated_remaining_minutes.py +++ b/phones/migrations/0023_relaynumber_deprecated_remaining_minutes.py @@ -29,7 +29,7 @@ def add_db_default_forward_func(apps, schema_editor): ' "remaining_texts" integer NOT NULL DEFAULT 75,' ' "texts_blocked" integer NOT NULL DEFAULT 0,' ' "texts_forwarded" integer NOT NULL DEFAULT 0,' - ' "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED,' + ' "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED,' # noqa: E501 ' "vcard_lookup_key" varchar(6) NOT NULL UNIQUE);' ) schema_editor.execute( diff --git a/phones/migrations/0024_add_country_code.py b/phones/migrations/0024_add_country_code.py index dd4b58cba3..63f541eee8 100644 --- a/phones/migrations/0024_add_country_code.py +++ b/phones/migrations/0024_add_country_code.py @@ -59,7 +59,7 @@ def add_db_default_forward_func(apps, schema_editor): ' ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,' ' "number" varchar(15) NOT NULL,' ' "location" varchar(255) NOT NULL,' - ' "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED,' + ' "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED,' # noqa: E501 ' "vcard_lookup_key" varchar(6) NOT NULL UNIQUE,' ' "enabled" bool NOT NULL,' ' "calls_blocked" integer NOT NULL,' diff --git a/phones/migrations/0027_relaynumber_vendor.py b/phones/migrations/0027_relaynumber_vendor.py index 063583ffc4..343f8066f4 100644 --- a/phones/migrations/0027_relaynumber_vendor.py +++ b/phones/migrations/0027_relaynumber_vendor.py @@ -22,7 +22,7 @@ def add_db_default_forward_func(apps, schema_editor): ' ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,' ' "number" varchar(15) NOT NULL,' ' "location" varchar(255) NOT NULL,' - ' "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED,' + ' "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED,' # noqa: E501 ' "vcard_lookup_key" varchar(6) NOT NULL UNIQUE,' ' "enabled" bool NOT NULL,' ' "calls_blocked" integer NOT NULL,' @@ -44,7 +44,7 @@ def add_db_default_forward_func(apps, schema_editor): ' SELECT "id", "number", "location", "user_id", "vcard_lookup_key",' ' "enabled", "calls_blocked", "calls_forwarded", "remaining_texts",' ' "texts_blocked", "texts_forwarded", "remaining_seconds",' - ' "remaining_minutes", "country_code", \'twilio\' FROM "phones_relaynumber";' + ' "remaining_minutes", "country_code", \'twilio\' FROM "phones_relaynumber";' # noqa: E501 ) schema_editor.execute('DROP TABLE "phones_relaynumber";') schema_editor.execute( diff --git a/phones/models.py b/phones/models.py index 45c11b1880..7d07c61a60 100644 --- a/phones/models.py +++ b/phones/models.py @@ -392,7 +392,8 @@ def send_welcome_message(user, relay_number): client = twilio_client() client.messages.create( body=( - "Welcome to Relay phone masking! 🎉 Please add your number to your contacts." + "Welcome to Relay phone masking!" + " 🎉 Please add your number to your contacts." " This will help you identify your Relay messages and calls." ), from_=settings.TWILIO_MAIN_NUMBER, diff --git a/privaterelay/apps.py b/privaterelay/apps.py index 5dba499eb2..91f700b811 100644 --- a/privaterelay/apps.py +++ b/privaterelay/apps.py @@ -31,7 +31,9 @@ def get_profiler_startup_data() -> tuple[str | None, str | None]: def write_gcp_key_json_file(gcp_key_json_path: Path) -> None: - # create the gcp key json file from contents of GOOGLE_CLOUD_PROFILER_CREDENTIALS_B64 + """ + Create the gcp key json file from contents of GOOGLE_CLOUD_PROFILER_CREDENTIALS_B64 + """ google_app_creds = base64.b64decode(settings.GOOGLE_CLOUD_PROFILER_CREDENTIALS_B64) if not google_app_creds == b"": with open(gcp_key_json_path, "w+") as gcp_key_file: diff --git a/privaterelay/management/commands/sync_phone_related_dates_on_profile.py b/privaterelay/management/commands/sync_phone_related_dates_on_profile.py index 123740c1d3..5a3b953d4f 100644 --- a/privaterelay/management/commands/sync_phone_related_dates_on_profile.py +++ b/privaterelay/management/commands/sync_phone_related_dates_on_profile.py @@ -52,7 +52,8 @@ def sync_phone_related_dates_on_profile(group: str) -> int: profile.date_phone_subscription_start = start_date profile.date_phone_subscription_end = end_date if profile.date_phone_subscription_reset is None: - # initialize the reset date for phone subscription users to the start of the subscription + # initialize the reset date for phone subscription users to the start of the + # subscription profile.date_phone_subscription_reset = start_date thirtyone_days_ago = datetime_now - timedelta(settings.MAX_DAYS_IN_MONTH) while profile.date_phone_subscription_reset < thirtyone_days_ago: @@ -65,14 +66,21 @@ def sync_phone_related_dates_on_profile(group: str) -> int: class Command(BaseCommand): - help = "Sync date_subscribed_phone, date_phone_limits_reset, date_phone_subscription_end fields on Profile by syncing with Mozilla Accounts data" + help = ( + "Sync date_subscribed_phone, date_phone_limits_reset," + " date_phone_subscription_end fields on Profile by syncing with" + " Mozilla Accounts data" + ) def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "--group", default="subscription", choices=["subscription", "free", "both"], - help="Choose phone subscription users, free phone users, or both. Defaults to subscription users.", + help=( + "Choose phone subscription users, free phone users, or both." + " Defaults to subscription users." + ), ) def handle(self, *args, **options): diff --git a/privaterelay/management/commands/update_phone_remaining_stats.py b/privaterelay/management/commands/update_phone_remaining_stats.py index 954695865f..981bfd358b 100644 --- a/privaterelay/management/commands/update_phone_remaining_stats.py +++ b/privaterelay/management/commands/update_phone_remaining_stats.py @@ -93,5 +93,6 @@ class Command(BaseCommand): def handle(self, *args, **options): num_profiles_w_phones, num_profiles_updated = update_phone_remaining_stats() print( - f"Out of {num_profiles_w_phones} profiles, {num_profiles_updated} limits were reset" + f"Out of {num_profiles_w_phones} profiles," + f" {num_profiles_updated} limits were reset" ) diff --git a/pyproject.toml b/pyproject.toml index 895473b507..f081a044cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,11 @@ testpaths = [ ] [tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "W", # pycodestyle warnings +] extend-safe-fixes = [ # E711 Comparison to `None` should be `cond is / is not None` # Changes '== None' to 'is None' @@ -89,3 +94,6 @@ extend-safe-fixes = [ "E712", ] +[tool.ruff.lint.per-file-ignores] +# Ignore line length in generated file +"privaterelay/glean/server_events.py" = ["E501"] From c7cfa28dd82f458e069f2c418924f7bd43ee84a2 Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Mon, 15 Apr 2024 10:37:08 -0500 Subject: [PATCH 08/11] Enable isort, fix import order `ruff` isort is mostly compatible with `isort`, with one exception in emails/policy.py The check for changes in privaterelay/glean/server_events.py now includes running `ruff check --fix --ignore E501`. --- .circleci/python_job.bash | 1 + api/authentication.py | 8 ++- api/permissions.py | 4 +- api/renderers.py | 2 - api/serializers/__init__.py | 2 +- api/tests/authentication_tests.py | 14 ++--- api/tests/iq_views_tests.py | 15 ++---- api/tests/phones_views_tests.py | 16 +++--- api/tests/serializers_tests.py | 5 +- api/tests/views_tests.py | 20 ++++---- api/urls.py | 17 ++++--- api/views/__init__.py | 51 ++++++++----------- api/views/phones.py | 30 ++++------- emails/apps.py | 9 ++-- .../command_from_django_settings.py | 2 +- emails/management/commands/check_health.py | 2 +- .../commands/delete_old_reply_records.py | 2 +- .../process_delayed_emails_from_sqs.py | 9 ++-- .../commands/process_emails_from_sqs.py | 16 +++--- .../commands/send_welcome_emails.py | 8 ++- emails/migrations/0001_initial.py | 3 +- emails/migrations/0002_auto_20190606_0249.py | 3 +- emails/migrations/0003_profile.py | 5 +- emails/migrations/0007_auto_20200310_2203.py | 2 +- emails/migrations/0015_domainaddress.py | 2 +- ..._reply_squashed_0022_auto_20210817_0327.py | 2 +- emails/migrations/0020_reply_created_at.py | 2 +- .../0024_increase_subdomain_length.py | 1 + ...metric_and_changeserver_storage_default.py | 1 + emails/migrations/0032_abusemetrics.py | 2 +- .../0034_relay_address_default_domain.py | 1 + emails/migrations/0053_alter_profile_user.py | 2 +- emails/models.py | 14 ++--- emails/policy.py | 17 +++---- emails/signals.py | 4 +- emails/sns.py | 5 +- emails/templatetags/email_extras.py | 2 +- emails/tests/cleaners_tests.py | 4 +- emails/tests/mgmt_check_health_tests.py | 6 +-- .../mgmt_process_emails_from_sqs_tests.py | 18 +++---- .../tests/mgmt_send_welcome_emails_tests.py | 8 ++- emails/tests/models_tests.py | 23 ++++----- emails/tests/policy_tests.py | 4 +- emails/tests/utils_tests.py | 9 ++-- emails/tests/views_tests.py | 35 +++++++------ emails/urls.py | 1 - emails/utils.py | 34 ++++++------- emails/views.py | 43 ++++++++-------- mypy_stubs/waffle/__init__.pyi | 3 +- mypy_stubs/waffle/managers.pyi | 2 + mypy_stubs/waffle/models.pyi | 1 + mypy_stubs/waffle/testutils.pyi | 3 +- mypy_stubs/waffle/utils.pyi | 1 + phones/admin.py | 1 - phones/apps.py | 9 ++-- phones/iq_utils.py | 3 +- phones/migrations/0006_realphone.py | 2 +- phones/migrations/0010_auto_20220529_1600.py | 1 + phones/migrations/0011_auto_20220530_1726.py | 1 + phones/migrations/0012_relaynumber.py | 2 +- .../0013_relaynumber_vcard_lookup_key.py | 1 + ..._alter_realphone_verification_sent_date.py | 1 + phones/migrations/0018_inboundcontact.py | 3 +- phones/models.py | 13 ++--- phones/tests/mgmt_delete_phone_data_tests.py | 10 ++-- phones/tests/models_tests.py | 13 ++--- privaterelay/allauth.py | 3 +- privaterelay/apps.py | 4 +- privaterelay/fxa_utils.py | 5 +- privaterelay/glean/server_events.py | 3 +- privaterelay/glean_interface.py | 5 +- .../management/commands/cleanup_data.py | 12 ++--- .../commands/get_or_create_user_group.py | 3 +- .../sync_phone_related_dates_on_profile.py | 3 +- .../commands/update_phone_remaining_stats.py | 5 +- privaterelay/middleware.py | 6 +-- .../migrations/0009_remove_duplicate_index.py | 1 - privaterelay/plans.py | 2 +- privaterelay/settings.py | 17 +++---- privaterelay/signals.py | 2 +- privaterelay/tests/allauth_tests.py | 4 +- privaterelay/tests/conftest.py | 6 +-- privaterelay/tests/fxa_utils_tests.py | 5 +- privaterelay/tests/glean_tests.py | 14 ++--- ...nc_phone_related_dates_on_profile_tests.py | 2 +- ...mgmt_update_phone_remaining_stats_tests.py | 4 +- privaterelay/tests/plans_tests.py | 2 +- privaterelay/tests/signals_tests.py | 2 +- privaterelay/tests/utils.py | 2 +- privaterelay/tests/utils_tests.py | 4 +- privaterelay/tests/views_tests.py | 12 ++--- privaterelay/utils.py | 19 ++++--- privaterelay/views.py | 16 +++--- pyproject.toml | 7 +++ 94 files changed, 347 insertions(+), 379 deletions(-) diff --git a/.circleci/python_job.bash b/.circleci/python_job.bash index f131aeb240..0bcc5b8089 100755 --- a/.circleci/python_job.bash +++ b/.circleci/python_job.bash @@ -85,6 +85,7 @@ function run_build_glean { set -x glean_parser translate --format python_server --output ${OUTPUT_FOLDER} ${INPUT_YAML} black ${OUTPUT_FOLDER} + ruff check --fix --ignore E501 ${OUTPUT_FOLDER} { set +x; } 2>/dev/null case "$OSTYPE" in darwin* | bsd*) diff --git a/api/authentication.py b/api/authentication.py index 9151e3dea8..c586ce4b2d 100644 --- a/api/authentication.py +++ b/api/authentication.py @@ -1,13 +1,12 @@ -from datetime import datetime, timezone -from typing import Any import logging import shlex - -import requests +from datetime import datetime, timezone +from typing import Any from django.conf import settings from django.core.cache import cache +import requests from allauth.socialaccount.models import SocialAccount from rest_framework.authentication import BaseAuthentication, get_authorization_header from rest_framework.exceptions import ( @@ -18,7 +17,6 @@ PermissionDenied, ) - logger = logging.getLogger("events") INTROSPECT_TOKEN_URL = ( "%s/introspect" % settings.SOCIALACCOUNT_PROVIDERS["fxa"]["OAUTH_ENDPOINT"] diff --git a/api/permissions.py b/api/permissions.py index e8fa50c607..5609c2aa00 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -1,9 +1,7 @@ from rest_framework import permissions - -from emails.models import Profile - from waffle import flag_is_active +from emails.models import Profile READ_METHODS = ["GET", "HEAD"] diff --git a/api/renderers.py b/api/renderers.py index e6f400bd0e..0633b72af9 100644 --- a/api/renderers.py +++ b/api/renderers.py @@ -1,6 +1,4 @@ import vobject - - from rest_framework import renderers diff --git a/api/serializers/__init__.py b/api/serializers/__init__.py index e2458448da..e906a5f955 100644 --- a/api/serializers/__init__.py +++ b/api/serializers/__init__.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import User from django.db.models import prefetch_related_objects -from rest_framework import serializers, exceptions +from rest_framework import exceptions, serializers from waffle import get_waffle_flag_model from emails.models import DomainAddress, Profile, RelayAddress diff --git a/api/tests/authentication_tests.py b/api/tests/authentication_tests.py index 8851af0686..c4ec9ac5ab 100644 --- a/api/tests/authentication_tests.py +++ b/api/tests/authentication_tests.py @@ -1,28 +1,22 @@ from datetime import datetime -from model_bakery import baker -import responses - from django.core.cache import cache from django.test import RequestFactory, TestCase +import responses from allauth.socialaccount.models import SocialAccount -from rest_framework.exceptions import ( - APIException, - AuthenticationFailed, - NotFound, -) +from model_bakery import baker +from rest_framework.exceptions import APIException, AuthenticationFailed, NotFound from rest_framework.test import APIClient from ..authentication import ( + INTROSPECT_TOKEN_URL, FxaTokenAuthentication, get_cache_key, get_fxa_uid_from_oauth_token, introspect_token, - INTROSPECT_TOKEN_URL, ) - MOCK_BASE = "api.authentication" diff --git a/api/tests/iq_views_tests.py b/api/tests/iq_views_tests.py index b18274a8a4..ce33a7417e 100644 --- a/api/tests/iq_views_tests.py +++ b/api/tests/iq_views_tests.py @@ -1,24 +1,19 @@ -from allauth.socialaccount.models import SocialAccount -import pytest +from unittest.mock import Mock, patch from django.conf import settings - +import pytest import responses -from unittest.mock import Mock, patch - -from twilio.rest import Client - - +from allauth.socialaccount.models import SocialAccount from rest_framework.test import RequestsClient +from twilio.rest import Client if settings.PHONES_ENABLED: from api.views.phones import compute_iq_mac from phones.models import InboundContact, iq_fmt -from phones.tests.models_tests import make_phone_test_user from api.tests.phones_views_tests import _make_real_phone, _make_relay_number - +from phones.tests.models_tests import make_phone_test_user pytestmark = pytest.mark.skipif( not settings.PHONES_ENABLED, reason="PHONES_ENABLED is False" diff --git a/api/tests/phones_views_tests.py b/api/tests/phones_views_tests.py index fa32883281..3b8a8063d1 100644 --- a/api/tests/phones_views_tests.py +++ b/api/tests/phones_views_tests.py @@ -1,28 +1,26 @@ +import re +from collections.abc import Iterator from dataclasses import dataclass from datetime import datetime, timezone from typing import Literal -from collections.abc import Iterator -from unittest.mock import Mock, patch, call -import re - -from twilio.request_validator import RequestValidator -from twilio.rest import Client +from unittest.mock import Mock, call, patch from django.conf import settings from django.contrib.auth.models import User from django.test.utils import override_settings +import pytest from model_bakery import baker from rest_framework.test import APIClient from twilio.base.exceptions import TwilioRestException +from twilio.request_validator import RequestValidator +from twilio.rest import Client from waffle.testutils import override_flag -import pytest from emails.models import Profile - if settings.PHONES_ENABLED: - from api.views.phones import _match_by_prefix, MatchByPrefix + from api.views.phones import MatchByPrefix, _match_by_prefix from phones.models import InboundContact, RealPhone, RelayNumber from phones.tests.models_tests import make_phone_test_user diff --git a/api/tests/serializers_tests.py b/api/tests/serializers_tests.py index 8391f443bc..5aa914068e 100644 --- a/api/tests/serializers_tests.py +++ b/api/tests/serializers_tests.py @@ -1,16 +1,15 @@ from django.urls import reverse +import pytest from model_bakery import baker from rest_framework.authtoken.models import Token from rest_framework.test import APITestCase from waffle.models import Flag -import pytest +from api.serializers import FlagSerializer from emails.models import RelayAddress from emails.tests.models_tests import make_free_test_user, make_premium_test_user -from api.serializers import FlagSerializer - class PremiumValidatorsTest(APITestCase): def test_non_premium_cant_set_block_list_emails(self): diff --git a/api/tests/views_tests.py b/api/tests/views_tests.py index c834d82c83..453d8abf84 100644 --- a/api/tests/views_tests.py +++ b/api/tests/views_tests.py @@ -1,34 +1,34 @@ -from datetime import datetime import logging -from allauth.account.models import EmailAddress -import pytest -from model_bakery import baker -import responses +from datetime import datetime from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.core.cache import cache from django.test import RequestFactory, TestCase -from django.utils import timezone from django.urls import reverse -from rest_framework.test import APIClient +from django.utils import timezone +import pytest +import responses +from allauth.account.models import EmailAddress from allauth.socialaccount.models import SocialAccount, SocialApp +from model_bakery import baker from pytest_django.fixtures import SettingsWrapper +from rest_framework.test import APIClient from waffle.testutils import override_flag -from api.authentication import get_cache_key, INTROSPECT_TOKEN_URL +from api.authentication import INTROSPECT_TOKEN_URL, get_cache_key from api.tests.authentication_tests import ( _setup_fxa_response, _setup_fxa_response_no_json, ) from api.views import FXA_PROFILE_URL -from emails.models import Profile, RelayAddress, DomainAddress +from emails.models import DomainAddress, Profile, RelayAddress from emails.tests.models_tests import make_free_test_user, make_premium_test_user from privaterelay.tests.utils import ( create_expected_glean_event, - log_extra, get_glean_event, + log_extra, ) diff --git a/api/urls.py b/api/urls.py index 8fe27fa414..8b296e92b1 100644 --- a/api/urls.py +++ b/api/urls.py @@ -9,12 +9,13 @@ from rest_framework import routers from privaterelay.utils import enable_if_setting + from .views import ( DomainAddressViewSet, - RelayAddressViewSet, + FlagViewSet, ProfileViewSet, + RelayAddressViewSet, UserViewSet, - FlagViewSet, first_forwarded_email, report_webcompat_issue, runtime_data, @@ -93,18 +94,18 @@ def to_url(self, value): if settings.PHONES_ENABLED: from .views.phones import ( - outbound_call, - list_messages, - outbound_sms, + InboundContactViewSet, RealPhoneViewSet, RelayNumberViewSet, - InboundContactViewSet, inbound_call, inbound_sms, - vCard, + list_messages, + outbound_call, + outbound_sms, + resend_welcome_sms, sms_status, + vCard, voice_status, - resend_welcome_sms, ) if settings.PHONES_ENABLED: diff --git a/api/views/__init__.py b/api/views/__init__.py index 7f1f8c0100..b226d4a8b1 100644 --- a/api/views/__init__.py +++ b/api/views/__init__.py @@ -9,36 +9,24 @@ """ import logging -from django.core.exceptions import ObjectDoesNotExist -from django.template.loader import render_to_string -from django.urls.exceptions import NoReverseMatch -import requests from typing import Any, Generic, TypeVar from django.apps import apps from django.conf import settings from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist from django.db.models.query import QuerySet +from django.template.loader import render_to_string +from django.urls.exceptions import NoReverseMatch import django_ftl -from drf_spectacular.utils import OpenApiResponse, extend_schema -from rest_framework.authentication import get_authorization_header -from rest_framework.exceptions import ( - AuthenticationFailed, - ErrorDetail, - ParseError, -) -from rest_framework.response import Response -from rest_framework.serializers import BaseSerializer -from rest_framework.views import exception_handler - +import requests from allauth.account.adapter import get_adapter as get_account_adapter from allauth.socialaccount.adapter import get_adapter as get_social_adapter -from allauth.socialaccount.models import SocialAccount from allauth.socialaccount.helpers import complete_social_login +from allauth.socialaccount.models import SocialAccount from django_filters import rest_framework as filters -from waffle import flag_is_active, get_waffle_flag_model -from waffle.models import Switch, Sample +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework import ( decorators, permissions, @@ -47,38 +35,39 @@ throttling, viewsets, ) +from rest_framework.authentication import get_authorization_header +from rest_framework.exceptions import AuthenticationFailed, ErrorDetail, ParseError +from rest_framework.response import Response +from rest_framework.serializers import BaseSerializer +from rest_framework.views import exception_handler +from waffle import flag_is_active, get_waffle_flag_model +from waffle.models import Sample, Switch + from emails.apps import EmailsConfig +from emails.models import DomainAddress, Profile, RelayAddress from emails.utils import generate_from_header, incr_if_enabled, ses_message_props -from emails.views import wrap_html_email, _get_address - +from emails.views import _get_address, wrap_html_email +from privaterelay.ftl_bundles import main as ftl_bundle from privaterelay.plans import ( get_bundle_country_language_mapping, - get_premium_country_language_mapping, get_phone_country_language_mapping, + get_premium_country_language_mapping, ) from privaterelay.utils import get_countries_info_from_request_and_mapping, glean_logger -from emails.models import ( - DomainAddress, - Profile, - RelayAddress, -) - from ..authentication import get_fxa_uid_from_oauth_token from ..exceptions import RelayAPIException -from ..permissions import IsOwner, CanManageFlags +from ..permissions import CanManageFlags, IsOwner from ..serializers import ( DomainAddressSerializer, FirstForwardedEmailSerializer, + FlagSerializer, ProfileSerializer, RelayAddressSerializer, UserSerializer, - FlagSerializer, WebcompatIssueSerializer, ) -from privaterelay.ftl_bundles import main as ftl_bundle - logger = logging.getLogger("events") info_logger = logging.getLogger("eventsinfo") FXA_PROFILE_URL = ( diff --git a/api/views/phones.py b/api/views/phones.py index 8141ab456a..57a2195927 100644 --- a/api/views/phones.py +++ b/api/views/phones.py @@ -1,70 +1,62 @@ -from dataclasses import asdict, dataclass, field -from datetime import datetime, timezone import hashlib import logging import re import string +from dataclasses import asdict, dataclass, field +from datetime import datetime, timezone from typing import Any, Literal -from waffle import get_waffle_flag_model -import django_ftl -import phonenumbers - from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.db.models.query import QuerySet from django.forms import model_to_dict -from drf_spectacular.utils import extend_schema, OpenApiParameter +import django_ftl +import phonenumbers +from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework import ( decorators, + exceptions, permissions, response, throttling, viewsets, - exceptions, ) from rest_framework.generics import get_object_or_404 from rest_framework.request import Request - from twilio.base.exceptions import TwilioRestException -from waffle import flag_is_active +from waffle import flag_is_active, get_waffle_flag_model from api.views import SaveToRequestUser from emails.utils import incr_if_enabled -from phones.iq_utils import send_iq_sms - from phones.apps import phones_config, twilio_client +from phones.iq_utils import send_iq_sms from phones.models import ( InboundContact, RealPhone, RelayNumber, + area_code_numbers, get_last_text_sender, get_pending_unverified_realphone_records, get_valid_realphone_verification_record, get_verified_realphone_record, get_verified_realphone_records, + location_numbers, send_welcome_message, suggested_numbers, - location_numbers, - area_code_numbers, ) from privaterelay.ftl_bundles import main as ftl_bundle from ..exceptions import ConflictError, ErrorContextType from ..permissions import HasPhoneService -from ..renderers import ( - TemplateTwiMLRenderer, - vCardRenderer, -) +from ..renderers import TemplateTwiMLRenderer, vCardRenderer from ..serializers.phones import ( InboundContactSerializer, RealPhoneSerializer, RelayNumberSerializer, ) - logger = logging.getLogger("events") info_logger = logging.getLogger("eventsinfo") diff --git a/emails/apps.py b/emails/apps.py index d4f3ea9ffd..d974e9a831 100644 --- a/emails/apps.py +++ b/emails/apps.py @@ -1,15 +1,14 @@ import logging import os +from django.apps import AppConfig, apps +from django.conf import settings +from django.utils.functional import cached_property + import boto3 from botocore.config import Config -from django.utils.functional import cached_property from mypy_boto3_ses.client import SESClient -from django.apps import apps, AppConfig -from django.conf import settings - - logger = logging.getLogger("events") diff --git a/emails/management/command_from_django_settings.py b/emails/management/command_from_django_settings.py index b9172bc223..9753e6e417 100644 --- a/emails/management/command_from_django_settings.py +++ b/emails/management/command_from_django_settings.py @@ -3,10 +3,10 @@ settings. """ +import textwrap from argparse import RawDescriptionHelpFormatter from collections import namedtuple from shutil import get_terminal_size -import textwrap from django.conf import settings from django.core.management.base import BaseCommand, CommandError, DjangoHelpFormatter diff --git a/emails/management/commands/check_health.py b/emails/management/commands/check_health.py index 8dc9b0e856..493b92f203 100644 --- a/emails/management/commands/check_health.py +++ b/emails/management/commands/check_health.py @@ -11,9 +11,9 @@ https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ """ -from datetime import datetime, timezone import json import logging +from datetime import datetime, timezone from django.core.management.base import CommandError diff --git a/emails/management/commands/delete_old_reply_records.py b/emails/management/commands/delete_old_reply_records.py index dda09bfb9b..e7037b0f69 100644 --- a/emails/management/commands/delete_old_reply_records.py +++ b/emails/management/commands/delete_old_reply_records.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta, timezone -from django.db import transaction from django.core.management.base import BaseCommand +from django.db import transaction from ...models import Reply diff --git a/emails/management/commands/process_delayed_emails_from_sqs.py b/emails/management/commands/process_delayed_emails_from_sqs.py index fbf8d0dce9..a5c98a573a 100644 --- a/emails/management/commands/process_delayed_emails_from_sqs.py +++ b/emails/management/commands/process_delayed_emails_from_sqs.py @@ -4,16 +4,15 @@ import sys import time -import boto3 -from botocore.exceptions import ClientError - from django.conf import settings from django.core.management.base import BaseCommand +import boto3 +from botocore.exceptions import ClientError + from emails.sns import verify_from_sns -from emails.views import _sns_inbound_logic, validate_sns_arn_and_type from emails.utils import incr_if_enabled - +from emails.views import _sns_inbound_logic, validate_sns_arn_and_type logger = logging.getLogger("events") info_logger = logging.getLogger("eventsinfo") diff --git a/emails/management/commands/process_emails_from_sqs.py b/emails/management/commands/process_emails_from_sqs.py index 09ddbe7fdd..52ad3a1ed5 100644 --- a/emails/management/commands/process_emails_from_sqs.py +++ b/emails/management/commands/process_emails_from_sqs.py @@ -9,29 +9,29 @@ https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sqs.html#SQS.Queue.receive_messages """ -from datetime import datetime, timezone -from urllib.parse import urlsplit import gc import json import logging import shlex import time +from datetime import datetime, timezone +from urllib.parse import urlsplit + +from django.core.management.base import CommandError import boto3 +import OpenSSL from botocore.exceptions import ClientError from codetiming import Timer from markus.utils import generate_tag -import OpenSSL - -from django.core.management.base import CommandError -from emails.sns import verify_from_sns -from emails.views import _sns_inbound_logic, validate_sns_arn_and_type -from emails.utils import incr_if_enabled, gauge_if_enabled from emails.management.command_from_django_settings import ( CommandFromDjangoSettings, SettingToLocal, ) +from emails.sns import verify_from_sns +from emails.utils import gauge_if_enabled, incr_if_enabled +from emails.views import _sns_inbound_logic, validate_sns_arn_and_type logger = logging.getLogger("eventsinfo.process_emails_from_sqs") diff --git a/emails/management/commands/send_welcome_emails.py b/emails/management/commands/send_welcome_emails.py index e804e93fdc..2940ff19cb 100644 --- a/emails/management/commands/send_welcome_emails.py +++ b/emails/management/commands/send_welcome_emails.py @@ -1,15 +1,13 @@ import logging -from mypy_boto3_ses.type_defs import ContentTypeDef - -from botocore.exceptions import ClientError - from django.apps import apps from django.conf import settings from django.core.management.base import BaseCommand -from allauth.socialaccount.models import SocialAccount import django_ftl +from allauth.socialaccount.models import SocialAccount +from botocore.exceptions import ClientError +from mypy_boto3_ses.type_defs import ContentTypeDef from emails.apps import EmailsConfig from emails.models import Profile diff --git a/emails/migrations/0001_initial.py b/emails/migrations/0001_initial.py index 2d9efd054f..ee08febadf 100644 --- a/emails/migrations/0001_initial.py +++ b/emails/migrations/0001_initial.py @@ -1,8 +1,9 @@ # Generated by Django 2.2.2 on 2019-06-05 12:08 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion + import emails.models diff --git a/emails/migrations/0002_auto_20190606_0249.py b/emails/migrations/0002_auto_20190606_0249.py index 7244053dcd..d1b149ecfb 100644 --- a/emails/migrations/0002_auto_20190606_0249.py +++ b/emails/migrations/0002_auto_20190606_0249.py @@ -1,7 +1,8 @@ # Generated by Django 2.2.2 on 2019-06-06 02:49 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models + import emails.models diff --git a/emails/migrations/0003_profile.py b/emails/migrations/0003_profile.py index 4b4147d186..9eb30e2e5a 100644 --- a/emails/migrations/0003_profile.py +++ b/emails/migrations/0003_profile.py @@ -1,9 +1,10 @@ # Generated by Django 2.2.2 on 2019-06-11 04:08 +import uuid + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import uuid class Migration(migrations.Migration): diff --git a/emails/migrations/0007_auto_20200310_2203.py b/emails/migrations/0007_auto_20200310_2203.py index 28263c28da..f180723806 100644 --- a/emails/migrations/0007_auto_20200310_2203.py +++ b/emails/migrations/0007_auto_20200310_2203.py @@ -1,7 +1,7 @@ # Generated by Django 2.2.10 on 2020-03-10 22:03 -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/emails/migrations/0015_domainaddress.py b/emails/migrations/0015_domainaddress.py index 3628ab7ef5..3cd57f32ed 100644 --- a/emails/migrations/0015_domainaddress.py +++ b/emails/migrations/0015_domainaddress.py @@ -1,8 +1,8 @@ # Generated by Django 2.2.13 on 2021-04-13 19:00 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/emails/migrations/0018_reply_squashed_0022_auto_20210817_0327.py b/emails/migrations/0018_reply_squashed_0022_auto_20210817_0327.py index 5c8d80e95f..8c0e89d640 100644 --- a/emails/migrations/0018_reply_squashed_0022_auto_20210817_0327.py +++ b/emails/migrations/0018_reply_squashed_0022_auto_20210817_0327.py @@ -1,7 +1,7 @@ # Generated by Django 2.2.24 on 2021-08-17 03:30 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/emails/migrations/0020_reply_created_at.py b/emails/migrations/0020_reply_created_at.py index 54b9808126..0f472bdf69 100644 --- a/emails/migrations/0020_reply_created_at.py +++ b/emails/migrations/0020_reply_created_at.py @@ -1,7 +1,7 @@ # Generated by Django 2.2.24 on 2021-08-31 20:29 -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/emails/migrations/0024_increase_subdomain_length.py b/emails/migrations/0024_increase_subdomain_length.py index c4c9406280..7f900571f1 100644 --- a/emails/migrations/0024_increase_subdomain_length.py +++ b/emails/migrations/0024_increase_subdomain_length.py @@ -1,6 +1,7 @@ # Generated by Django 2.2.24 on 2021-10-05 18:48 from django.db import migrations, models + import emails.models diff --git a/emails/migrations/0029_profile_add_deleted_metric_and_changeserver_storage_default.py b/emails/migrations/0029_profile_add_deleted_metric_and_changeserver_storage_default.py index 1d93c53a48..daea3632bd 100644 --- a/emails/migrations/0029_profile_add_deleted_metric_and_changeserver_storage_default.py +++ b/emails/migrations/0029_profile_add_deleted_metric_and_changeserver_storage_default.py @@ -1,6 +1,7 @@ # Generated by Django 2.2.24 on 2021-10-19 15:38 from django.db import migrations, models + import emails.models diff --git a/emails/migrations/0032_abusemetrics.py b/emails/migrations/0032_abusemetrics.py index 82a38e4458..6297a55655 100644 --- a/emails/migrations/0032_abusemetrics.py +++ b/emails/migrations/0032_abusemetrics.py @@ -1,8 +1,8 @@ # Generated by Django 2.2.24 on 2021-11-05 19:16 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/emails/migrations/0034_relay_address_default_domain.py b/emails/migrations/0034_relay_address_default_domain.py index f784a94df2..4a35e4a56b 100644 --- a/emails/migrations/0034_relay_address_default_domain.py +++ b/emails/migrations/0034_relay_address_default_domain.py @@ -1,6 +1,7 @@ # Generated by Django 2.2.24 on 2021-11-10 22:15 from django.db import migrations, models + import emails.models diff --git a/emails/migrations/0053_alter_profile_user.py b/emails/migrations/0053_alter_profile_user.py index 7a4d7b609f..86d61cdf59 100644 --- a/emails/migrations/0053_alter_profile_user.py +++ b/emails/migrations/0053_alter_profile_user.py @@ -1,8 +1,8 @@ # Generated by Django 3.2.16 on 2022-10-28 02:35 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/emails/models.py b/emails/models.py index 5dfc887ecf..1c797f5aad 100644 --- a/emails/models.py +++ b/emails/models.py @@ -1,14 +1,16 @@ from __future__ import annotations -from collections import namedtuple -from datetime import datetime, timedelta, timezone -from hashlib import sha256 -from typing import Literal, cast -from collections.abc import Iterable + import logging import random import re import string import uuid +from collections import namedtuple +from collections.abc import Iterable +from datetime import datetime, timedelta, timezone +from hashlib import sha256 +from typing import Literal, cast + from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import BadRequest @@ -17,8 +19,8 @@ from django.db.models.query import QuerySet from django.dispatch import receiver from django.utils.translation.trans_real import ( - parse_accept_lang_header, get_supported_language_variant, + parse_accept_lang_header, ) from allauth.socialaccount.models import SocialAccount diff --git a/emails/policy.py b/emails/policy.py index 8999307202..e6afe32ef5 100644 --- a/emails/policy.py +++ b/emails/policy.py @@ -18,18 +18,13 @@ https://github.com/python/cpython/blob/main/Lib/email/policy.py """ -from email._header_value_parser import get_unstructured, InvalidMessageID -from email.headerregistry import ( - BaseHeader, - MessageIDHeader as PythonMessageIDHeader, - HeaderRegistry as PythonHeaderRegistry, - UnstructuredHeader, -) -from email.policy import EmailPolicy - from email import errors - -from typing import cast, TYPE_CHECKING +from email._header_value_parser import InvalidMessageID, get_unstructured +from email.headerregistry import BaseHeader, UnstructuredHeader +from email.headerregistry import HeaderRegistry as PythonHeaderRegistry +from email.headerregistry import MessageIDHeader as PythonMessageIDHeader +from email.policy import EmailPolicy +from typing import TYPE_CHECKING, cast if TYPE_CHECKING: # _HeaderParser is a protocol from mypy's typeshed diff --git a/emails/signals.py b/emails/signals.py index 480e6507c9..03f83fbf17 100644 --- a/emails/signals.py +++ b/emails/signals.py @@ -1,15 +1,13 @@ -from hashlib import sha256 import logging +from hashlib import sha256 from django.contrib.auth.models import User - from django.db.models.signals import post_save, pre_save from django.dispatch import receiver from emails.models import Profile from emails.utils import incr_if_enabled, set_user_group - info_logger = logging.getLogger("eventsinfo") diff --git a/emails/sns.py b/emails/sns.py index baba2c7e93..90e604d8b2 100644 --- a/emails/sns.py +++ b/emails/sns.py @@ -3,16 +3,15 @@ import base64 import logging -import pem from urllib.request import urlopen -from OpenSSL import crypto - from django.conf import settings from django.core.cache import caches from django.core.exceptions import SuspiciousOperation from django.utils.encoding import smart_bytes +import pem +from OpenSSL import crypto logger = logging.getLogger("events") diff --git a/emails/templatetags/email_extras.py b/emails/templatetags/email_extras.py index 8f2a651ff0..1636b033f0 100644 --- a/emails/templatetags/email_extras.py +++ b/emails/templatetags/email_extras.py @@ -1,7 +1,7 @@ from django import template from django.template.defaultfilters import stringfilter from django.utils.html import conditional_escape -from django.utils.safestring import mark_safe, SafeString +from django.utils.safestring import SafeString, mark_safe register = template.Library() diff --git a/emails/tests/cleaners_tests.py b/emails/tests/cleaners_tests.py index 37c575f974..154a8fdb6d 100644 --- a/emails/tests/cleaners_tests.py +++ b/emails/tests/cleaners_tests.py @@ -4,10 +4,10 @@ from django.contrib.auth.models import User -from model_bakery import baker import pytest +from model_bakery import baker -from emails.cleaners import ServerStorageCleaner, MissingProfileCleaner +from emails.cleaners import MissingProfileCleaner, ServerStorageCleaner from emails.models import DomainAddress, RelayAddress from .models_tests import make_premium_test_user, make_storageless_test_user diff --git a/emails/tests/mgmt_check_health_tests.py b/emails/tests/mgmt_check_health_tests.py index f610ae0b42..d3ef6d48ab 100644 --- a/emails/tests/mgmt_check_health_tests.py +++ b/emails/tests/mgmt_check_health_tests.py @@ -1,10 +1,10 @@ -from datetime import datetime, timezone, timedelta import json import logging +from datetime import datetime, timedelta, timezone -import pytest +from django.core.management import CommandError, call_command -from django.core.management import call_command, CommandError +import pytest @pytest.fixture(autouse=True) diff --git a/emails/tests/mgmt_process_emails_from_sqs_tests.py b/emails/tests/mgmt_process_emails_from_sqs_tests.py index f328b4e57e..4068d0b3d4 100644 --- a/emails/tests/mgmt_process_emails_from_sqs_tests.py +++ b/emails/tests/mgmt_process_emails_from_sqs_tests.py @@ -1,18 +1,18 @@ -from datetime import datetime, timezone -from typing import Any, TYPE_CHECKING +import json from collections.abc import Generator -from unittest.mock import patch, Mock +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any +from unittest.mock import Mock, patch from uuid import uuid4 -import json - -from botocore.exceptions import ClientError -from markus.testing import MetricsMock -import pytest -import OpenSSL from django.core.management import call_command from django.core.management.base import CommandError +import OpenSSL +import pytest +from botocore.exceptions import ClientError +from markus.testing import MetricsMock + from emails.tests.views_tests import EMAIL_SNS_BODIES from privaterelay.tests.utils import log_extra diff --git a/emails/tests/mgmt_send_welcome_emails_tests.py b/emails/tests/mgmt_send_welcome_emails_tests.py index 96b62edd56..0fabfc2418 100644 --- a/emails/tests/mgmt_send_welcome_emails_tests.py +++ b/emails/tests/mgmt_send_welcome_emails_tests.py @@ -1,20 +1,18 @@ -import pytest from unittest.mock import MagicMock, patch -from botocore.exceptions import ClientError - from django.conf import settings from django.contrib.auth.models import User from django.core.management import call_command -from allauth.socialaccount.models import SocialAccount import django_ftl +import pytest +from allauth.socialaccount.models import SocialAccount +from botocore.exceptions import ClientError from emails.models import Profile from emails.tests.models_tests import make_free_test_user from privaterelay.ftl_bundles import main as ftl_bundle - COMMAND_NAME = "send_welcome_emails" diff --git a/emails/tests/models_tests.py b/emails/tests/models_tests.py index ef86c8f3c4..9ded7f3853 100644 --- a/emails/tests/models_tests.py +++ b/emails/tests/models_tests.py @@ -1,39 +1,38 @@ +import random from datetime import datetime, timedelta, timezone from hashlib import sha256 -import random from unittest import skip -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch from uuid import uuid4 from django.conf import settings from django.contrib.auth.models import User -from django.test import override_settings, TestCase +from django.test import TestCase, override_settings -from allauth.socialaccount.models import SocialAccount -from waffle.testutils import override_flag import pytest - +from allauth.socialaccount.models import SocialAccount from model_bakery import baker +from waffle.testutils import override_flag from ..models import ( AbuseMetrics, - address_hash, CannotMakeAddressException, CannotMakeSubdomainException, DeletedAddress, - DomainAddress, DomainAddrDuplicateException, + DomainAddress, DomainAddrUnavailableException, + Profile, + RegisteredSubdomain, + RelayAddress, + address_hash, get_domain_numerical, has_bad_words, hash_subdomain, is_blocklisted, - Profile, - RegisteredSubdomain, - RelayAddress, - valid_available_subdomain, valid_address, valid_address_pattern, + valid_available_subdomain, ) from ..utils import get_domains_from_settings diff --git a/emails/tests/policy_tests.py b/emails/tests/policy_tests.py index 835980704b..ed8933a40d 100644 --- a/emails/tests/policy_tests.py +++ b/emails/tests/policy_tests.py @@ -1,9 +1,9 @@ """Tests for emails.policy""" -from email import message_from_string, errors -from typing_extensions import TypedDict +from email import errors, message_from_string import pytest +from typing_extensions import TypedDict from emails.policy import relay_policy diff --git a/emails/tests/utils_tests.py b/emails/tests/utils_tests.py index 02b79f4c15..61c8abbc48 100644 --- a/emails/tests/utils_tests.py +++ b/emails/tests/utils_tests.py @@ -1,19 +1,22 @@ +import json from base64 import b64encode from typing import Literal +from unittest.mock import patch from urllib.parse import quote_plus + from django.test import TestCase, override_settings -from unittest.mock import patch -import json + import pytest from emails.utils import ( + InvalidFromHeader, generate_from_header, get_domains_from_settings, get_email_domain_from_settings, parse_email_header, remove_trackers, - InvalidFromHeader, ) + from .models_tests import make_free_test_user, make_premium_test_user # noqa: F401 diff --git a/emails/tests/views_tests.py b/emails/tests/views_tests.py index 079ec48ecd..4fc09cc242 100644 --- a/emails/tests/views_tests.py +++ b/emails/tests/views_tests.py @@ -1,36 +1,28 @@ +import glob +import json +import logging +import os +import re from copy import deepcopy from datetime import datetime, timedelta, timezone from email import message_from_string from email.message import EmailMessage from typing import Any, cast from unittest._log import _LoggingWatcher -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch from uuid import uuid4 -import glob -import json -import logging -import os -import re from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponse -from django.test import override_settings, Client, SimpleTestCase, TestCase +from django.test import Client, SimpleTestCase, TestCase, override_settings +import pytest from allauth.socialaccount.models import SocialAccount from botocore.exceptions import ClientError from markus.main import MetricsRecord from markus.testing import MetricsMock from model_bakery import baker -import pytest - -from privaterelay.ftl_bundles import main -from privaterelay.tests.utils import ( - create_expected_glean_event, - get_glean_event, - log_extra, -) -from privaterelay.glean.server_events import GLEAN_EVENT_MOZLOG_TYPE as GLEAN_LOG from emails.models import ( DeletedAddress, @@ -40,15 +32,16 @@ Reply, address_hash, ) +from emails.policy import relay_policy from emails.types import AWS_SNSMessageJSON, OutgoingHeaders from emails.utils import ( + InvalidFromHeader, b64_lookup_key, decrypt_reply_metadata, derive_reply_keys, encrypt_reply_metadata, get_domains_from_settings, get_message_id_bytes, - InvalidFromHeader, ) from emails.views import ( EmailDroppedReason, @@ -66,7 +59,13 @@ validate_sns_arn_and_type, wrapped_email_test, ) -from emails.policy import relay_policy +from privaterelay.ftl_bundles import main +from privaterelay.glean.server_events import GLEAN_EVENT_MOZLOG_TYPE as GLEAN_LOG +from privaterelay.tests.utils import ( + create_expected_glean_event, + get_glean_event, + log_extra, +) from .models_tests import ( make_free_test_user, diff --git a/emails/urls.py b/emails/urls.py index 8f6b131b4b..a00efe2cfe 100644 --- a/emails/urls.py +++ b/emails/urls.py @@ -3,7 +3,6 @@ from . import views - urlpatterns = [ path("sns-inbound", views.sns_inbound), ] diff --git a/emails/utils.py b/emails/utils.py index 65b06ffba8..8f68813003 100644 --- a/emails/utils.py +++ b/emails/utils.py @@ -1,35 +1,35 @@ from __future__ import annotations + import base64 import contextlib +import json +import logging +import pathlib +import re +from collections.abc import Callable from email.errors import InvalidHeaderDefect from email.headerregistry import Address, AddressHeader from email.message import EmailMessage from email.utils import formataddr, parseaddr from functools import cache -from typing import cast, Any, TypeVar, Literal -from collections.abc import Callable -import json -import pathlib -import re -from django.template.loader import render_to_string -from django.utils.text import Truncator -import requests - -from botocore.exceptions import ClientError -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.hkdf import HKDFExpand -from mypy_boto3_ses.type_defs import ContentTypeDef, SendRawEmailResponseTypeDef -import jwcrypto.jwe -import jwcrypto.jwk -import markus -import logging +from typing import Any, Literal, TypeVar, cast from urllib.parse import quote_plus, urlparse from django.conf import settings from django.contrib.auth.models import Group, User from django.template.defaultfilters import linebreaksbr, urlize +from django.template.loader import render_to_string +from django.utils.text import Truncator +import jwcrypto.jwe +import jwcrypto.jwk +import markus +import requests from allauth.socialaccount.models import SocialAccount +from botocore.exceptions import ClientError +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDFExpand +from mypy_boto3_ses.type_defs import ContentTypeDef, SendRawEmailResponseTypeDef from privaterelay.plans import get_bundle_country_language_mapping from privaterelay.utils import get_countries_info_from_lang_and_mapping diff --git a/emails/views.py b/emails/views.py index 0ca8a5172f..721fd0d781 100644 --- a/emails/views.py +++ b/emails/views.py @@ -1,3 +1,8 @@ +import html +import json +import logging +import re +import shlex from collections import defaultdict from copy import deepcopy from datetime import datetime, timezone @@ -5,37 +10,36 @@ from email.iterators import _structure from email.message import EmailMessage from email.utils import parseaddr -import html from io import StringIO -import json from json import JSONDecodeError -import logging -import re -import shlex from textwrap import dedent from typing import Any, Literal from urllib.parse import urlencode -from botocore.exceptions import ClientError -from codetiming import Timer -from decouple import strtobool -from django.shortcuts import render -from sentry_sdk import capture_message -from markus.utils import generate_tag -from waffle import sample_is_active - from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from django.db.models import prefetch_related_objects from django.http import HttpRequest, HttpResponse +from django.shortcuts import render from django.template.loader import render_to_string from django.utils.html import escape from django.views.decorators.csrf import csrf_exempt -from privaterelay.utils import get_subplat_upgrade_link_by_language, glean_logger +from botocore.exceptions import ClientError +from codetiming import Timer +from decouple import strtobool +from markus.utils import generate_tag +from sentry_sdk import capture_message +from waffle import sample_is_active +from privaterelay.ftl_bundles import main as ftl_bundle +from privaterelay.utils import ( + flag_is_active_in_task, + get_subplat_upgrade_link_by_language, + glean_logger, +) from .models import ( CannotMakeAddressException, @@ -48,14 +52,16 @@ get_domain_numerical, ) from .policy import relay_policy +from .sns import SUPPORTED_SNS_TYPES, verify_from_sns from .types import ( AWS_MailJSON, AWS_SNSMessageJSON, - OutgoingHeaders, EmailForwardingIssues, EmailHeaderIssues, + OutgoingHeaders, ) from .utils import ( + InvalidFromHeader, _get_bucket_and_key_from_s3_json, b64_lookup_key, count_all_trackers, @@ -69,17 +75,12 @@ get_reply_to_address, histogram_if_enabled, incr_if_enabled, + parse_email_header, remove_message_from_s3, remove_trackers, ses_send_raw_email, urlize_and_linebreaks, - InvalidFromHeader, - parse_email_header, ) -from .sns import verify_from_sns, SUPPORTED_SNS_TYPES - -from privaterelay.ftl_bundles import main as ftl_bundle -from privaterelay.utils import flag_is_active_in_task logger = logging.getLogger("events") info_logger = logging.getLogger("eventsinfo") diff --git a/mypy_stubs/waffle/__init__.pyi b/mypy_stubs/waffle/__init__.pyi index 3283253a82..996fdbd66e 100644 --- a/mypy_stubs/waffle/__init__.pyi +++ b/mypy_stubs/waffle/__init__.pyi @@ -5,7 +5,8 @@ from typing import Literal, overload from django.http import HttpRequest -from waffle.models import Flag, Switch, Sample + +from waffle.models import Flag, Sample, Switch VERSION: tuple[int, ...] diff --git a/mypy_stubs/waffle/managers.pyi b/mypy_stubs/waffle/managers.pyi index 18583ec22f..855f18bed9 100644 --- a/mypy_stubs/waffle/managers.pyi +++ b/mypy_stubs/waffle/managers.pyi @@ -3,7 +3,9 @@ # Can be removed once type hints ship in the release after v3.0.0 from typing import TypeVar + from django.db import models + from waffle.models import BaseModel _BASE_T = TypeVar("_BASE_T", bound=BaseModel) diff --git a/mypy_stubs/waffle/models.pyi b/mypy_stubs/waffle/models.pyi index e8d401fed5..d83f71f43e 100644 --- a/mypy_stubs/waffle/models.pyi +++ b/mypy_stubs/waffle/models.pyi @@ -9,6 +9,7 @@ from django.contrib.auth.models import AbstractBaseUser from django.db import models from django.http import HttpRequest from django.utils.translation import gettext_lazy as _ + from django_stubs_ext.db.models import TypedModelMeta from waffle import managers diff --git a/mypy_stubs/waffle/testutils.pyi b/mypy_stubs/waffle/testutils.pyi index b7e7d8104f..3743222202 100644 --- a/mypy_stubs/waffle/testutils.pyi +++ b/mypy_stubs/waffle/testutils.pyi @@ -2,9 +2,10 @@ # https://github.com/django-waffle/django-waffle/blob/v3.0.0/waffle/testutils.py # Can be removed once type hints ship in the release after v3.0.0 -from django.test.utils import TestContextDecorator from typing import Generic, TypeVar +from django.test.utils import TestContextDecorator + _T = TypeVar("_T") _FlagActive = bool | None _SampleActive = bool | float | None diff --git a/mypy_stubs/waffle/utils.pyi b/mypy_stubs/waffle/utils.pyi index 35a256a9fc..fca30a1237 100644 --- a/mypy_stubs/waffle/utils.pyi +++ b/mypy_stubs/waffle/utils.pyi @@ -3,6 +3,7 @@ # Can be removed once type hints ship in the release after v3.0.0 from typing import Any + from django.core.cache.backends.base import BaseCache def get_setting(name: str, default: Any | None = None) -> Any: ... diff --git a/phones/admin.py b/phones/admin.py index ce4f30399d..161d1b9b54 100644 --- a/phones/admin.py +++ b/phones/admin.py @@ -2,7 +2,6 @@ from .models import InboundContact, RealPhone, RelayNumber - admin.site.register(InboundContact, admin.ModelAdmin) admin.site.register(RealPhone, admin.ModelAdmin) admin.site.register(RelayNumber, admin.ModelAdmin) diff --git a/phones/apps.py b/phones/apps.py index 29dc89d2e3..e35115bfd5 100644 --- a/phones/apps.py +++ b/phones/apps.py @@ -1,13 +1,12 @@ import logging -from twilio.base.instance_resource import InstanceResource -from twilio.request_validator import RequestValidator -from twilio.rest import Client - -from django.apps import apps, AppConfig +from django.apps import AppConfig, apps from django.conf import settings from django.utils.functional import cached_property +from twilio.base.instance_resource import InstanceResource +from twilio.request_validator import RequestValidator +from twilio.rest import Client logger = logging.getLogger("events") diff --git a/phones/iq_utils.py b/phones/iq_utils.py index 47f2ef4954..3651c4611e 100644 --- a/phones/iq_utils.py +++ b/phones/iq_utils.py @@ -1,9 +1,8 @@ import json -import requests - from django.conf import settings +import requests from rest_framework import exceptions diff --git a/phones/migrations/0006_realphone.py b/phones/migrations/0006_realphone.py index 164f376e1b..ee3bdc03cf 100644 --- a/phones/migrations/0006_realphone.py +++ b/phones/migrations/0006_realphone.py @@ -1,8 +1,8 @@ # Generated by Django 3.2.13 on 2022-05-28 14:48 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/phones/migrations/0010_auto_20220529_1600.py b/phones/migrations/0010_auto_20220529_1600.py index 7bb4b91c87..9282d89c7e 100644 --- a/phones/migrations/0010_auto_20220529_1600.py +++ b/phones/migrations/0010_auto_20220529_1600.py @@ -1,6 +1,7 @@ # Generated by Django 3.2.13 on 2022-05-29 16:00 from django.db import migrations, models + import phones.models diff --git a/phones/migrations/0011_auto_20220530_1726.py b/phones/migrations/0011_auto_20220530_1726.py index 46611a97ca..9fd18ee17d 100644 --- a/phones/migrations/0011_auto_20220530_1726.py +++ b/phones/migrations/0011_auto_20220530_1726.py @@ -1,6 +1,7 @@ # Generated by Django 3.2.13 on 2022-05-30 17:26 from django.db import migrations, models + import phones.models diff --git a/phones/migrations/0012_relaynumber.py b/phones/migrations/0012_relaynumber.py index 9124633675..fc7c23eb9e 100644 --- a/phones/migrations/0012_relaynumber.py +++ b/phones/migrations/0012_relaynumber.py @@ -1,8 +1,8 @@ # Generated by Django 3.2.13 on 2022-06-04 16:37 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/phones/migrations/0013_relaynumber_vcard_lookup_key.py b/phones/migrations/0013_relaynumber_vcard_lookup_key.py index d2d78728e2..4a42ee09a4 100644 --- a/phones/migrations/0013_relaynumber_vcard_lookup_key.py +++ b/phones/migrations/0013_relaynumber_vcard_lookup_key.py @@ -1,6 +1,7 @@ # Generated by Django 3.2.13 on 2022-07-01 19:08 from django.db import migrations, models + import phones.models diff --git a/phones/migrations/0015_alter_realphone_verification_sent_date.py b/phones/migrations/0015_alter_realphone_verification_sent_date.py index 5d610c7090..af1ceeeec3 100644 --- a/phones/migrations/0015_alter_realphone_verification_sent_date.py +++ b/phones/migrations/0015_alter_realphone_verification_sent_date.py @@ -1,6 +1,7 @@ # Generated by Django 3.2.13 on 2022-07-08 19:40 from django.db import migrations, models + import phones.models diff --git a/phones/migrations/0018_inboundcontact.py b/phones/migrations/0018_inboundcontact.py index cbaa4b8456..24ac081c90 100644 --- a/phones/migrations/0018_inboundcontact.py +++ b/phones/migrations/0018_inboundcontact.py @@ -1,7 +1,8 @@ # Generated by Django 3.2.14 on 2022-08-07 21:30 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models + import phones.models diff --git a/phones/models.py b/phones/models.py index 7d07c61a60..05ba881df5 100644 --- a/phones/models.py +++ b/phones/models.py @@ -1,22 +1,23 @@ from __future__ import annotations -from datetime import datetime, timedelta, timezone -from math import floor -from collections.abc import Iterator + import logging -import phonenumbers import secrets import string +from collections.abc import Iterator +from datetime import datetime, timedelta, timezone +from math import floor -from django.contrib.auth.models import User from django.conf import settings +from django.contrib.auth.models import User from django.core.cache import cache from django.core.exceptions import BadRequest, ValidationError -from django.db.migrations.recorder import MigrationRecorder from django.db import models +from django.db.migrations.recorder import MigrationRecorder from django.db.models.signals import post_save from django.dispatch.dispatcher import receiver from django.urls import reverse +import phonenumbers from twilio.base.exceptions import TwilioRestException from twilio.rest import Client diff --git a/phones/tests/mgmt_delete_phone_data_tests.py b/phones/tests/mgmt_delete_phone_data_tests.py index 0f75e3410e..8a18d8025f 100644 --- a/phones/tests/mgmt_delete_phone_data_tests.py +++ b/phones/tests/mgmt_delete_phone_data_tests.py @@ -1,18 +1,18 @@ -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from io import StringIO from unittest.mock import patch -from django.contrib.auth.models import User -from django.core.management import call_command, CommandError from django.conf import settings +from django.contrib.auth.models import User +from django.core.management import CommandError, call_command +import pytest from model_bakery import baker from pytest_django.fixtures import SettingsWrapper -import pytest if settings.PHONES_ENABLED: - from .models_tests import make_phone_test_user from ..models import InboundContact, RealPhone, RelayNumber + from .models_tests import make_phone_test_user pytestmark = pytest.mark.skipif( not settings.PHONES_ENABLED, reason="PHONES_ENABLED is False" diff --git a/phones/tests/models_tests.py b/phones/tests/models_tests.py index 2a969b4b92..f6ebd5a496 100644 --- a/phones/tests/models_tests.py +++ b/phones/tests/models_tests.py @@ -1,10 +1,8 @@ +import random from datetime import datetime, timedelta, timezone from types import SimpleNamespace -import pytest -import random -import responses +from unittest.mock import Mock, call, patch from uuid import uuid4 -from unittest.mock import Mock, patch, call from django.conf import settings from django.contrib.auth.models import User @@ -12,9 +10,12 @@ from django.core.exceptions import BadRequest, ValidationError from django.test import override_settings +import pytest +import responses from allauth.socialaccount.models import SocialAccount, SocialToken from model_bakery import baker from twilio.base.exceptions import TwilioRestException + from emails.models import Profile if settings.PHONES_ENABLED: @@ -24,11 +25,11 @@ RelayNumber, area_code_numbers, get_expired_unverified_realphone_records, - get_valid_realphone_verification_record, get_last_text_sender, + get_valid_realphone_verification_record, + iq_fmt, location_numbers, suggested_numbers, - iq_fmt, ) diff --git a/privaterelay/allauth.py b/privaterelay/allauth.py index 082dcec48e..b38e0aa6ce 100644 --- a/privaterelay/allauth.py +++ b/privaterelay/allauth.py @@ -1,5 +1,5 @@ -from urllib.parse import urlencode, urlparse import logging +from urllib.parse import urlencode, urlparse from django.http import Http404 from django.shortcuts import resolve_url @@ -9,7 +9,6 @@ from .middleware import RelayStaticFilesMiddleware - logger = logging.getLogger("events") diff --git a/privaterelay/apps.py b/privaterelay/apps.py index 91f700b811..1ebef5b29e 100644 --- a/privaterelay/apps.py +++ b/privaterelay/apps.py @@ -1,14 +1,14 @@ import base64 import json +import os from pathlib import Path from typing import Any -import requests -import os from django.apps import AppConfig from django.conf import settings from django.utils.functional import cached_property +import requests ROOT_DIR = os.path.abspath(os.curdir) diff --git a/privaterelay/fxa_utils.py b/privaterelay/fxa_utils.py index 498ee14b51..12c50a8d31 100644 --- a/privaterelay/fxa_utils.py +++ b/privaterelay/fxa_utils.py @@ -1,18 +1,17 @@ +import logging from datetime import datetime, timedelta, timezone from typing import Any, cast from django.conf import settings +import sentry_sdk from allauth.socialaccount.models import SocialAccount, SocialToken from allauth.socialaccount.providers.fxa.views import FirefoxAccountsOAuth2Adapter from oauthlib.oauth2.rfc6749.errors import CustomOAuth2Error, TokenExpiredError from requests_oauthlib import OAuth2Session -import logging -import sentry_sdk from .utils import flag_is_active_in_task - logger = logging.getLogger("events") diff --git a/privaterelay/glean/server_events.py b/privaterelay/glean/server_events.py index cbd4797dd1..b130480a9c 100644 --- a/privaterelay/glean/server_events.py +++ b/privaterelay/glean/server_events.py @@ -9,10 +9,11 @@ """ from __future__ import annotations + +import json from datetime import datetime, timezone from typing import Any from uuid import uuid4 -import json GLEAN_EVENT_MOZLOG_TYPE = "glean-server-event" diff --git a/privaterelay/glean_interface.py b/privaterelay/glean_interface.py index 3855ace64c..365a291f8e 100644 --- a/privaterelay/glean_interface.py +++ b/privaterelay/glean_interface.py @@ -1,6 +1,7 @@ """Relay interface to EventsServerEventLogger generated by glean_parser.""" from __future__ import annotations + from datetime import datetime from logging import getLogger from typing import Any, Literal, NamedTuple @@ -12,9 +13,9 @@ from ipware import get_client_ip from emails.models import DomainAddress, RelayAddress -from .glean.server_events import EventsServerEventLogger, GLEAN_EVENT_MOZLOG_TYPE -from .types import RELAY_CHANNEL_NAME +from .glean.server_events import GLEAN_EVENT_MOZLOG_TYPE, EventsServerEventLogger +from .types import RELAY_CHANNEL_NAME # Enumerate the mask setting that caused an email to not be forwarded. EmailBlockedReason = Literal[ diff --git a/privaterelay/management/commands/cleanup_data.py b/privaterelay/management/commands/cleanup_data.py index 401005ed6f..7234a9c2f5 100644 --- a/privaterelay/management/commands/cleanup_data.py +++ b/privaterelay/management/commands/cleanup_data.py @@ -1,20 +1,20 @@ from __future__ import annotations -from argparse import RawDescriptionHelpFormatter -from shutil import get_terminal_size -from typing import Any, TYPE_CHECKING -import textwrap import logging +import textwrap +from argparse import RawDescriptionHelpFormatter +from shutil import get_terminal_size +from typing import TYPE_CHECKING, Any from django.core.management.base import BaseCommand, DjangoHelpFormatter from codetiming import Timer -from emails.cleaners import ServerStorageCleaner, MissingProfileCleaner - +from emails.cleaners import MissingProfileCleaner, ServerStorageCleaner if TYPE_CHECKING: # pragma: no cover from argparse import ArgumentParser + from privaterelay.cleaners import DataIssueTask diff --git a/privaterelay/management/commands/get_or_create_user_group.py b/privaterelay/management/commands/get_or_create_user_group.py index 1879806efa..b140494b91 100644 --- a/privaterelay/management/commands/get_or_create_user_group.py +++ b/privaterelay/management/commands/get_or_create_user_group.py @@ -1,6 +1,5 @@ -from django.core.management.base import BaseCommand - from django.contrib.auth.models import Group +from django.core.management.base import BaseCommand class Command(BaseCommand): diff --git a/privaterelay/management/commands/sync_phone_related_dates_on_profile.py b/privaterelay/management/commands/sync_phone_related_dates_on_profile.py index 5a3b953d4f..307eaa63ac 100644 --- a/privaterelay/management/commands/sync_phone_related_dates_on_profile.py +++ b/privaterelay/management/commands/sync_phone_related_dates_on_profile.py @@ -1,4 +1,6 @@ +import logging from datetime import datetime, timedelta, timezone + from django.conf import settings from django.core.management.base import BaseCommand, CommandParser @@ -7,7 +9,6 @@ get_free_phone_social_accounts, get_phone_subscriber_social_accounts, ) -import logging logger = logging.getLogger("events") diff --git a/privaterelay/management/commands/update_phone_remaining_stats.py b/privaterelay/management/commands/update_phone_remaining_stats.py index 981bfd358b..af465b77ef 100644 --- a/privaterelay/management/commands/update_phone_remaining_stats.py +++ b/privaterelay/management/commands/update_phone_remaining_stats.py @@ -1,10 +1,9 @@ +import logging from datetime import datetime, timedelta, timezone from django.conf import settings -from django.core.management.base import BaseCommand from django.contrib.auth.models import User - -import logging +from django.core.management.base import BaseCommand from emails.models import Profile from privaterelay.management.utils import ( diff --git a/privaterelay/middleware.py b/privaterelay/middleware.py index a945a6eeb7..8c0f362ab8 100644 --- a/privaterelay/middleware.py +++ b/privaterelay/middleware.py @@ -1,14 +1,12 @@ -from datetime import datetime, timezone import time - -import markus +from datetime import datetime, timezone from django.conf import settings from django.shortcuts import redirect +import markus from whitenoise.middleware import WhiteNoiseMiddleware - metrics = markus.get_metrics("fx-private-relay") diff --git a/privaterelay/migrations/0009_remove_duplicate_index.py b/privaterelay/migrations/0009_remove_duplicate_index.py index 3e5644bde3..5383dd6616 100644 --- a/privaterelay/migrations/0009_remove_duplicate_index.py +++ b/privaterelay/migrations/0009_remove_duplicate_index.py @@ -8,7 +8,6 @@ from django.db import migrations from django.db.backends.base.schema import BaseDatabaseSchemaEditor - INDEX_NAME = "account_emailaddress_email_upper" diff --git a/privaterelay/plans.py b/privaterelay/plans.py index 4ca12cb407..1afa521bdf 100644 --- a/privaterelay/plans.py +++ b/privaterelay/plans.py @@ -62,7 +62,7 @@ from copy import deepcopy from functools import lru_cache -from typing import get_args, Literal, TypedDict +from typing import Literal, TypedDict, get_args from django.conf import settings diff --git a/privaterelay/settings.py b/privaterelay/settings.py index 0c9b66357f..c1ac4cfc29 100644 --- a/privaterelay/settings.py +++ b/privaterelay/settings.py @@ -11,25 +11,24 @@ """ from __future__ import annotations -from pathlib import Path -from typing import Any, TYPE_CHECKING, cast, get_args + +import base64 import ipaddress import os import sys +from hashlib import sha256 +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast, get_args +from django.conf.global_settings import LANGUAGES as DEFAULT_LANGUAGES -from decouple import config, Choices, Csv +import dj_database_url import django_stubs_ext import markus import sentry_sdk +from decouple import Choices, Csv, config from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.logging import ignore_logger -from hashlib import sha256 -import base64 - -from django.conf.global_settings import LANGUAGES as DEFAULT_LANGUAGES - -import dj_database_url from .types import RELAY_CHANNEL_NAME diff --git a/privaterelay/signals.py b/privaterelay/signals.py index 29898f26b5..40cd860430 100644 --- a/privaterelay/signals.py +++ b/privaterelay/signals.py @@ -1,6 +1,6 @@ from django.dispatch import receiver -from allauth.account.signals import user_signed_up, user_logged_in +from allauth.account.signals import user_logged_in, user_signed_up from emails.utils import incr_if_enabled diff --git a/privaterelay/tests/allauth_tests.py b/privaterelay/tests/allauth_tests.py index e1a3d005eb..88c68cc649 100644 --- a/privaterelay/tests/allauth_tests.py +++ b/privaterelay/tests/allauth_tests.py @@ -1,7 +1,7 @@ from collections.abc import Iterator -from unittest.mock import patch, Mock -import pytest +from unittest.mock import Mock, patch +import pytest from allauth.core.context import request_context from ..allauth import AccountAdapter diff --git a/privaterelay/tests/conftest.py b/privaterelay/tests/conftest.py index 8917249f52..a1f54f71d9 100644 --- a/privaterelay/tests/conftest.py +++ b/privaterelay/tests/conftest.py @@ -1,11 +1,11 @@ """Shared fixtures for privaterelay tests""" -from pathlib import Path -from collections.abc import Iterator import json +from collections.abc import Iterator +from pathlib import Path -from pytest_django.fixtures import SettingsWrapper import pytest +from pytest_django.fixtures import SettingsWrapper from privaterelay.utils import get_version_info diff --git a/privaterelay/tests/fxa_utils_tests.py b/privaterelay/tests/fxa_utils_tests.py index 7bc94d0102..79980864f4 100644 --- a/privaterelay/tests/fxa_utils_tests.py +++ b/privaterelay/tests/fxa_utils_tests.py @@ -4,14 +4,15 @@ from datetime import datetime, timedelta, timezone from unittest.mock import patch -import pytest from django.conf import settings +import pytest from allauth.socialaccount.models import SocialAccount -from privaterelay.fxa_utils import get_phone_subscription_dates from waffle.testutils import override_flag +from privaterelay.fxa_utils import get_phone_subscription_dates + if settings.PHONES_ENABLED: from phones.tests.models_tests import make_phone_test_user diff --git a/privaterelay/tests/glean_tests.py b/privaterelay/tests/glean_tests.py index e81fe3e753..49d062236d 100644 --- a/privaterelay/tests/glean_tests.py +++ b/privaterelay/tests/glean_tests.py @@ -1,16 +1,16 @@ +import json from datetime import datetime, timezone -from typing import Any, NamedTuple from logging import LogRecord +from typing import Any, NamedTuple from uuid import UUID, uuid4 -import json -from django.test import RequestFactory from django.contrib.auth.models import User +from django.test import RequestFactory +import pytest from allauth.socialaccount.models import SocialAccount from model_bakery import baker from pytest_django.fixtures import SettingsWrapper -import pytest from api.serializers import RelayAddressSerializer from emails.models import RelayAddress @@ -20,9 +20,6 @@ phone_subscription, vpn_subscription, ) -from privaterelay.types import RELAY_CHANNEL_NAME -from privaterelay.utils import glean_logger as utils_glean_logger -from privaterelay.tests.utils import create_expected_glean_event from privaterelay.glean_interface import ( EmailBlockedReason, EmailMaskData, @@ -30,6 +27,9 @@ RequestData, UserData, ) +from privaterelay.tests.utils import create_expected_glean_event +from privaterelay.types import RELAY_CHANNEL_NAME +from privaterelay.utils import glean_logger as utils_glean_logger @pytest.fixture diff --git a/privaterelay/tests/mgmt_sync_phone_related_dates_on_profile_tests.py b/privaterelay/tests/mgmt_sync_phone_related_dates_on_profile_tests.py index 3c15aebbfd..a2d1da3cee 100644 --- a/privaterelay/tests/mgmt_sync_phone_related_dates_on_profile_tests.py +++ b/privaterelay/tests/mgmt_sync_phone_related_dates_on_profile_tests.py @@ -4,12 +4,12 @@ from datetime import datetime, timedelta, timezone from unittest.mock import patch -import pytest from django.conf import settings from django.contrib.auth.models import User from django.core.management import call_command +import pytest from allauth.socialaccount.models import SocialAccount from model_bakery import baker from waffle.models import Flag diff --git a/privaterelay/tests/mgmt_update_phone_remaining_stats_tests.py b/privaterelay/tests/mgmt_update_phone_remaining_stats_tests.py index f089cf2672..c054c0f515 100644 --- a/privaterelay/tests/mgmt_update_phone_remaining_stats_tests.py +++ b/privaterelay/tests/mgmt_update_phone_remaining_stats_tests.py @@ -4,11 +4,11 @@ from datetime import datetime, timedelta, timezone from unittest.mock import patch -import pytest from django.conf import settings from django.core.management import call_command +import pytest from allauth.socialaccount.models import SocialAccount from model_bakery import baker from waffle.models import Flag @@ -20,8 +20,8 @@ if settings.PHONES_ENABLED: from api.tests.phones_views_tests import mocked_twilio_client # noqa: F401 - from phones.tests.models_tests import make_phone_test_user from phones.models import RealPhone, RelayNumber + from phones.tests.models_tests import make_phone_test_user pytestmark = pytest.mark.skipif( not settings.PHONES_ENABLED, reason="PHONES_ENABLED is False" diff --git a/privaterelay/tests/plans_tests.py b/privaterelay/tests/plans_tests.py index 68d6a52637..d8acb5f5c1 100644 --- a/privaterelay/tests/plans_tests.py +++ b/privaterelay/tests/plans_tests.py @@ -1,7 +1,7 @@ """Tests for privaterelay/plans.py""" -from pytest_django.fixtures import SettingsWrapper import pytest +from pytest_django.fixtures import SettingsWrapper from privaterelay.plans import ( CountryStr, diff --git a/privaterelay/tests/signals_tests.py b/privaterelay/tests/signals_tests.py index 69e3bd5070..787f3b7d6a 100644 --- a/privaterelay/tests/signals_tests.py +++ b/privaterelay/tests/signals_tests.py @@ -1,4 +1,3 @@ -import pytest from unittest.mock import patch from django.contrib.auth.models import User @@ -7,6 +6,7 @@ from django.http.response import HttpResponse from django.test.client import RequestFactory +import pytest from model_bakery import baker from privaterelay.signals import record_user_signed_up diff --git a/privaterelay/tests/utils.py b/privaterelay/tests/utils.py index 438c4640e3..b240dd1300 100644 --- a/privaterelay/tests/utils.py +++ b/privaterelay/tests/utils.py @@ -1,8 +1,8 @@ """Helper functions for tests""" +import json from logging import LogRecord from typing import Any -import json from unittest._log import _LoggingWatcher from django.contrib.auth.models import User diff --git a/privaterelay/tests/utils_tests.py b/privaterelay/tests/utils_tests.py index 36da65949f..4749ffd46d 100644 --- a/privaterelay/tests/utils_tests.py +++ b/privaterelay/tests/utils_tests.py @@ -1,18 +1,18 @@ +import logging from collections.abc import Iterator from unittest.mock import patch -import logging from django.contrib.auth.models import AbstractBaseUser, Group, User from django.core.cache.backends.base import BaseCache from django.test import RequestFactory +import pytest from _pytest.fixtures import SubRequest from _pytest.logging import LogCaptureFixture from pytest_django.fixtures import SettingsWrapper from waffle.models import Flag from waffle.testutils import override_flag from waffle.utils import get_cache as get_waffle_cache -import pytest from ..plans import get_premium_country_language_mapping from ..utils import ( diff --git a/privaterelay/tests/views_tests.py b/privaterelay/tests/views_tests.py index 9db8265c74..d703de1e4c 100644 --- a/privaterelay/tests/views_tests.py +++ b/privaterelay/tests/views_tests.py @@ -1,28 +1,28 @@ import json import logging +from collections.abc import Iterator from copy import deepcopy from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any, Literal -from collections.abc import Iterator -from uuid import uuid4 from unittest.mock import Mock, patch +from uuid import uuid4 from django.contrib.auth.models import User from django.test import Client, TestCase from django.utils import timezone -from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken +import jwt +import pytest +import responses from allauth.account.models import EmailAddress +from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken from cryptography.hazmat.primitives.asymmetric.rsa import ( RSAPrivateKey, generate_private_key, ) from markus.testing import MetricsMock from model_bakery import baker -import jwt -import pytest -import responses from emails.models import ( DeletedAddress, diff --git a/privaterelay/utils.py b/privaterelay/utils.py index 553f218809..b4f5c0ef41 100644 --- a/privaterelay/utils.py +++ b/privaterelay/utils.py @@ -1,13 +1,14 @@ from __future__ import annotations + +import json +import logging +import random +from collections.abc import Callable from decimal import Decimal from functools import cache, wraps from pathlib import Path from string import ascii_uppercase -from typing import TypedDict, cast, TYPE_CHECKING -from collections.abc import Callable -import json -import logging -import random +from typing import TYPE_CHECKING, TypedDict, cast from django.conf import settings from django.contrib.auth.models import AbstractBaseUser @@ -16,16 +17,14 @@ from waffle import get_waffle_flag_model from waffle.models import logger as waffle_logger -from waffle.utils import ( - get_cache as get_waffle_cache, - get_setting as get_waffle_setting, -) +from waffle.utils import get_cache as get_waffle_cache +from waffle.utils import get_setting as get_waffle_setting from .plans import ( + CountryStr, LanguageStr, PeriodStr, PlanCountryLangMapping, - CountryStr, get_premium_country_language_mapping, ) diff --git a/privaterelay/views.py b/privaterelay/views.py index fffbb55c8a..ddb8cc65cf 100644 --- a/privaterelay/views.py +++ b/privaterelay/views.py @@ -1,10 +1,10 @@ +import json +import logging +from collections.abc import Iterable from datetime import datetime, timezone from functools import lru_cache from hashlib import sha256 from typing import Any, TypedDict -from collections.abc import Iterable -import json -import logging from django.apps import apps from django.conf import settings @@ -14,18 +14,17 @@ from django.urls import reverse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods -from rest_framework.decorators import api_view, schema +import jwt +import sentry_sdk from allauth.socialaccount.models import SocialAccount, SocialApp from allauth.socialaccount.providers.fxa.views import FirefoxAccountsOAuth2Adapter from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from google_measurement_protocol import event, report from oauthlib.oauth2.rfc6749.errors import CustomOAuth2Error -import jwt -import sentry_sdk +from rest_framework.decorators import api_view, schema # from silk.profiling.profiler import silk_profile - from emails.models import ( CannotMakeSubdomainException, DomainAddress, @@ -35,8 +34,7 @@ from emails.utils import incr_if_enabled from .apps import PrivateRelayConfig -from .fxa_utils import _get_oauth2_session, NoSocialToken - +from .fxa_utils import NoSocialToken, _get_oauth2_session FXA_PROFILE_CHANGE_EVENT = "https://schemas.accounts.firefox.com/event/profile-change" FXA_SUBSCRIPTION_CHANGE_EVENT = ( diff --git a/pyproject.toml b/pyproject.toml index f081a044cb..0748c798de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ testpaths = [ select = [ "E", # pycodestyle errors "F", # pyflakes + "I", # isort "W", # pycodestyle warnings ] extend-safe-fixes = [ @@ -94,6 +95,12 @@ extend-safe-fixes = [ "E712", ] +[tool.ruff.lint.isort] +section-order = ["future", "standard-library", "django", "third-party", "first-party", "local-folder"] + +[tool.ruff.lint.isort.sections] +"django"= ["django"] + [tool.ruff.lint.per-file-ignores] # Ignore line length in generated file "privaterelay/glean/server_events.py" = ["E501"] From 10f5205905527dc416e566263b63eac1c6045d9a Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Mon, 15 Apr 2024 11:12:32 -0500 Subject: [PATCH 09/11] Add ruff to precommit --- .lintstagedrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lintstagedrc.js b/.lintstagedrc.js index 959559b31e..aa879d96e2 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -6,5 +6,5 @@ module.exports = { .map((file) => file.split(process.cwd())[1]) .join(" --file ")}`, "*.md": "prettier --write", - "*.py": ["black", "mypy"], + "*.py": ["black", "mypy", "ruff check --fix"], } From 8912bcd30a760a9c0fdfcb099c5a7b07d44433f2 Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Mon, 15 Apr 2024 11:18:31 -0500 Subject: [PATCH 10/11] Enable pyupgrade, fix issues --- api/authentication.py | 4 +- api/schema.py | 2 +- api/tests/phones_views_tests.py | 24 +++---- api/views/phones.py | 4 +- emails/management/commands/check_health.py | 4 +- .../commands/delete_old_reply_records.py | 4 +- .../commands/process_emails_from_sqs.py | 4 +- emails/models.py | 40 +++++------ emails/tests/mgmt_check_health_tests.py | 4 +- .../mgmt_process_emails_from_sqs_tests.py | 4 +- emails/tests/models_tests.py | 68 +++++++------------ emails/tests/views_tests.py | 44 ++++++------ emails/views.py | 18 ++--- mypy_stubs/decouple.pyi | 6 +- phones/models.py | 14 ++-- phones/tests/mgmt_delete_phone_data_tests.py | 4 +- phones/tests/models_tests.py | 28 ++++---- privaterelay/fxa_utils.py | 14 ++-- privaterelay/glean/server_events.py | 4 +- .../sync_phone_related_dates_on_profile.py | 4 +- .../commands/update_phone_remaining_stats.py | 6 +- privaterelay/middleware.py | 4 +- privaterelay/tests/fxa_utils_tests.py | 4 +- privaterelay/tests/glean_tests.py | 4 +- ...nc_phone_related_dates_on_profile_tests.py | 14 ++-- ...mgmt_update_phone_remaining_stats_tests.py | 8 +-- privaterelay/views.py | 14 ++-- pyproject.toml | 1 + 28 files changed, 165 insertions(+), 188 deletions(-) diff --git a/api/authentication.py b/api/authentication.py index c586ce4b2d..a177d169c9 100644 --- a/api/authentication.py +++ b/api/authentication.py @@ -1,6 +1,6 @@ import logging import shlex -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from django.conf import settings @@ -96,7 +96,7 @@ def get_fxa_uid_from_oauth_token(token: str, use_cache=True) -> str: if isinstance(fxa_resp_data.get("json", {}).get("exp"), int): # Note: FXA iat and exp are timestamps in *milliseconds* fxa_token_exp_time = int(fxa_resp_data["json"]["exp"] / 1000) - now_time = int(datetime.now(timezone.utc).timestamp()) + now_time = int(datetime.now(UTC).timestamp()) fxa_token_exp_cache_timeout = fxa_token_exp_time - now_time if fxa_token_exp_cache_timeout > cache_timeout: # cache until access_token expires (matched Relay user) diff --git a/api/schema.py b/api/schema.py index 0849d08b2d..339913e1cc 100644 --- a/api/schema.py +++ b/api/schema.py @@ -1,6 +1,6 @@ """Schema Extensions for drf-spectacular""" -from typing import Callable +from collections.abc import Callable from drf_spectacular.extensions import OpenApiAuthenticationExtension from drf_spectacular.openapi import AutoSchema diff --git a/api/tests/phones_views_tests.py b/api/tests/phones_views_tests.py index 3b8a8063d1..5144e1cab6 100644 --- a/api/tests/phones_views_tests.py +++ b/api/tests/phones_views_tests.py @@ -1,7 +1,7 @@ import re from collections.abc import Iterator from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Literal from unittest.mock import Mock, call, patch @@ -96,55 +96,55 @@ def user_with_sms_activity(outbound_phone_user, mocked_twilio_client): InboundContact.objects.create( relay_number=relay_number, inbound_number="+13015550001", - last_inbound_date=datetime(2023, 3, 1, 12, 5, tzinfo=timezone.utc), + last_inbound_date=datetime(2023, 3, 1, 12, 5, tzinfo=UTC), last_inbound_type="text", - last_text_date=datetime(2023, 3, 1, 12, 5, tzinfo=timezone.utc), + last_text_date=datetime(2023, 3, 1, 12, 5, tzinfo=UTC), ) # Second SMS contact InboundContact.objects.create( relay_number=relay_number, inbound_number="+13015550002", - last_inbound_date=datetime(2023, 3, 2, 13, 5, tzinfo=timezone.utc), + last_inbound_date=datetime(2023, 3, 2, 13, 5, tzinfo=UTC), last_inbound_type="text", - last_text_date=datetime(2023, 3, 2, 13, 5, tzinfo=timezone.utc), + last_text_date=datetime(2023, 3, 2, 13, 5, tzinfo=UTC), ) # Voice contact InboundContact.objects.create( relay_number=relay_number, inbound_number="+13015550003", - last_inbound_date=datetime(2023, 3, 3, 8, 30, tzinfo=timezone.utc), + last_inbound_date=datetime(2023, 3, 3, 8, 30, tzinfo=UTC), last_inbound_type="call", - last_call_date=datetime(2023, 3, 3, 8, 30, tzinfo=timezone.utc), + last_call_date=datetime(2023, 3, 3, 8, 30, tzinfo=UTC), ) twilio_messages = [ MockTwilioMessage( from_="+13015550001", to=relay_number.number, - date_sent=datetime(2023, 3, 1, 12, 0, tzinfo=timezone.utc), + date_sent=datetime(2023, 3, 1, 12, 0, tzinfo=UTC), body="Send Y to confirm appointment", ), MockTwilioMessage( from_=relay_number.number, to="+13015550001", - date_sent=datetime(2023, 3, 1, 12, 5, tzinfo=timezone.utc), + date_sent=datetime(2023, 3, 1, 12, 5, tzinfo=UTC), body="Y", ), MockTwilioMessage( from_="+13015550002", to=relay_number.number, - date_sent=datetime(2023, 3, 2, 13, 0, tzinfo=timezone.utc), + date_sent=datetime(2023, 3, 2, 13, 0, tzinfo=UTC), body="Donate $100 to Senator Smith?", ), MockTwilioMessage( from_=relay_number.number, to="+13015550002", - date_sent=datetime(2023, 3, 2, 13, 5, tzinfo=timezone.utc), + date_sent=datetime(2023, 3, 2, 13, 5, tzinfo=UTC), body="STOP STOP STOP", ), MockTwilioMessage( from_=relay_number.number, to="+13015550004", - date_sent=datetime(2023, 3, 4, 20, 55, tzinfo=timezone.utc), + date_sent=datetime(2023, 3, 4, 20, 55, tzinfo=UTC), body="U Up?", ), ] diff --git a/api/views/phones.py b/api/views/phones.py index 57a2195927..827d3aaa82 100644 --- a/api/views/phones.py +++ b/api/views/phones.py @@ -3,7 +3,7 @@ import re import string from dataclasses import asdict, dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any, Literal from django.conf import settings @@ -1285,7 +1285,7 @@ def _check_and_update_contact(inbound_contact, contact_type, relay_number): relay_number.save() raise exceptions.ValidationError(f"Number is not accepting {contact_type}.") - inbound_contact.last_inbound_date = datetime.now(timezone.utc) + inbound_contact.last_inbound_date = datetime.now(UTC) singular_contact_type = contact_type[:-1] # strip trailing "s" inbound_contact.last_inbound_type = singular_contact_type attr = f"num_{contact_type}" diff --git a/emails/management/commands/check_health.py b/emails/management/commands/check_health.py index 493b92f203..f2365b8ca8 100644 --- a/emails/management/commands/check_health.py +++ b/emails/management/commands/check_health.py @@ -13,7 +13,7 @@ import json import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from django.core.management.base import CommandError @@ -95,7 +95,7 @@ def check_healthcheck(self, healthcheck_file, max_age): context["data"] = data raw_timestamp = data["timestamp"] timestamp = datetime.fromisoformat(raw_timestamp) - age = (datetime.now(tz=timezone.utc) - timestamp).total_seconds() + age = (datetime.now(tz=UTC) - timestamp).total_seconds() context["age_s"] = round(age, 3) if age > max_age: diff --git a/emails/management/commands/delete_old_reply_records.py b/emails/management/commands/delete_old_reply_records.py index e7037b0f69..e1b922d2a6 100644 --- a/emails/management/commands/delete_old_reply_records.py +++ b/emails/management/commands/delete_old_reply_records.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from django.core.management.base import BaseCommand from django.db import transaction @@ -13,7 +13,7 @@ def add_arguments(self, parser): parser.add_argument("days_old", nargs=1, type=int) def handle(self, *args, **options): - delete_date = datetime.now(timezone.utc) - timedelta(options["days_old"][0]) + delete_date = datetime.now(UTC) - timedelta(options["days_old"][0]) replies_to_delete = Reply.objects.filter(created_at__lt=delete_date).only("id") print( f"Deleting {len(replies_to_delete)} reply records " diff --git a/emails/management/commands/process_emails_from_sqs.py b/emails/management/commands/process_emails_from_sqs.py index 52ad3a1ed5..abf058c639 100644 --- a/emails/management/commands/process_emails_from_sqs.py +++ b/emails/management/commands/process_emails_from_sqs.py @@ -14,7 +14,7 @@ import logging import shlex import time -from datetime import datetime, timezone +from datetime import UTC, datetime from urllib.parse import urlsplit from django.core.management.base import CommandError @@ -441,7 +441,7 @@ def process_message(self, message): def write_healthcheck(self): """Update the healthcheck file with operations data, if path is set.""" data = { - "timestamp": datetime.now(tz=timezone.utc).isoformat(), + "timestamp": datetime.now(tz=UTC).isoformat(), "cycles": self.cycles, "total_messages": self.total_messages, "failed_messages": self.failed_messages, diff --git a/emails/models.py b/emails/models.py index 1c797f5aad..c8f257dbc5 100644 --- a/emails/models.py +++ b/emails/models.py @@ -7,7 +7,7 @@ import uuid from collections import namedtuple from collections.abc import Iterable -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from hashlib import sha256 from typing import Literal, cast @@ -236,7 +236,7 @@ def at_mask_limit(self) -> bool: def check_bounce_pause(self) -> BounceStatus: if self.last_hard_bounce: - last_hard_bounce_allowed = datetime.now(timezone.utc) - timedelta( + last_hard_bounce_allowed = datetime.now(UTC) - timedelta( days=settings.HARD_BOUNCE_ALLOWED_DAYS ) if self.last_hard_bounce > last_hard_bounce_allowed: @@ -244,7 +244,7 @@ def check_bounce_pause(self) -> BounceStatus: self.last_hard_bounce = None self.save() if self.last_soft_bounce: - last_soft_bounce_allowed = datetime.now(timezone.utc) - timedelta( + last_soft_bounce_allowed = datetime.now(UTC) - timedelta( days=settings.SOFT_BOUNCE_ALLOWED_DAYS ) if self.last_soft_bounce > last_soft_bounce_allowed: @@ -262,7 +262,7 @@ def next_email_try(self) -> datetime: bounce_pause, bounce_type = self.check_bounce_pause() if not bounce_pause: - return datetime.now(timezone.utc) + return datetime.now(UTC) if bounce_type == "soft": assert self.last_soft_bounce @@ -444,8 +444,8 @@ def update_abuse_metric( # look for abuse metrics created on the same UTC date, regardless of time. midnight_utc_today = datetime.combine( - datetime.now(timezone.utc).date(), datetime.min.time() - ).astimezone(timezone.utc) + datetime.now(UTC).date(), datetime.min.time() + ).astimezone(UTC) midnight_utc_tomorow = midnight_utc_today + timedelta(days=1) abuse_metric = self.user.abusemetrics_set.filter( first_recorded__gte=midnight_utc_today, @@ -464,7 +464,7 @@ def update_abuse_metric( abuse_metric.num_email_forwarded_per_day += 1 if forwarded_email_size > 0: abuse_metric.forwarded_email_size_per_day += forwarded_email_size - abuse_metric.last_recorded = datetime.now(timezone.utc) + abuse_metric.last_recorded = datetime.now(UTC) abuse_metric.save() # check user should be flagged for abuse @@ -493,7 +493,7 @@ def update_abuse_metric( or hit_max_forwarded or hit_max_forwarded_email_size ): - self.last_account_flagged = datetime.now(timezone.utc) + self.last_account_flagged = datetime.now(UTC) self.save() data = { "uid": self.fxa.uid if self.fxa else None, @@ -514,7 +514,7 @@ def is_flagged(self): account_premium_feature_resumed = self.last_account_flagged + timedelta( days=settings.PREMIUM_FEATURE_PAUSED_DAYS ) - if datetime.now(timezone.utc) > account_premium_feature_resumed: + if datetime.now(UTC) > account_premium_feature_resumed: # premium feature has been resumed return False # user was flagged and the premium feature pause period is not yet over @@ -771,7 +771,7 @@ def delete(self, *args, **kwargs): ) deleted_address.save() profile = Profile.objects.get(user=self.user) - profile.address_last_deleted = datetime.now(timezone.utc) + profile.address_last_deleted = datetime.now(UTC) profile.num_address_deleted += 1 profile.num_email_forwarded_in_deleted_address += self.num_forwarded profile.num_email_blocked_in_deleted_address += self.num_blocked @@ -781,7 +781,7 @@ def delete(self, *args, **kwargs): profile.num_email_replied_in_deleted_address += self.num_replied profile.num_email_spam_in_deleted_address += self.num_spam profile.num_deleted_relay_addresses += 1 - profile.last_engagement = datetime.now(timezone.utc) + profile.last_engagement = datetime.now(UTC) profile.save() return super().delete(*args, **kwargs) @@ -803,7 +803,7 @@ def save( break self.address = address_default() locked_profile.update_abuse_metric(address_created=True) - locked_profile.last_engagement = datetime.now(timezone.utc) + locked_profile.last_engagement = datetime.now(UTC) locked_profile.save() if (not self.user.profile.server_storage) and any( (self.description, self.generated_for, self.used_on) @@ -953,7 +953,7 @@ def save( raise DomainAddrDuplicateException(duplicate_address=self.address) user_profile.update_abuse_metric(address_created=True) - user_profile.last_engagement = datetime.now(timezone.utc) + user_profile.last_engagement = datetime.now(UTC) user_profile.save(update_fields=["last_engagement"]) incr_if_enabled("domainaddress.create") if self.first_emailed_at: @@ -999,7 +999,7 @@ def make_domain_address( # Only check for bad words if randomly generated assert isinstance(address, str) - first_emailed_at = datetime.now(timezone.utc) if made_via_email else None + first_emailed_at = datetime.now(UTC) if made_via_email else None domain_address = DomainAddress.objects.create( user=user_profile.user, address=address, first_emailed_at=first_emailed_at ) @@ -1020,7 +1020,7 @@ def delete(self, *args, **kwargs): # self.user_profile is a property and should not be used to # update values on the user's profile profile = Profile.objects.get(user=self.user) - profile.address_last_deleted = datetime.now(timezone.utc) + profile.address_last_deleted = datetime.now(UTC) profile.num_address_deleted += 1 profile.num_email_forwarded_in_deleted_address += self.num_forwarded profile.num_email_blocked_in_deleted_address += self.num_blocked @@ -1030,7 +1030,7 @@ def delete(self, *args, **kwargs): profile.num_email_replied_in_deleted_address += self.num_replied profile.num_email_spam_in_deleted_address += self.num_spam profile.num_deleted_domain_addresses += 1 - profile.last_engagement = datetime.now(timezone.utc) + profile.last_engagement = datetime.now(UTC) profile.save() return super().delete(*args, **kwargs) @@ -1043,11 +1043,7 @@ def domain_value(self) -> str: @property def full_address(self) -> str: - return "{}@{}.{}".format( - self.address, - self.user_profile.subdomain, - self.domain_value, - ) + return f"{self.address}@{self.user_profile.subdomain}.{self.domain_value}" @property def metrics_id(self) -> str: @@ -1084,7 +1080,7 @@ def increment_num_replied(self): address = self.relay_address or self.domain_address assert address address.num_replied += 1 - address.last_used_at = datetime.now(timezone.utc) + address.last_used_at = datetime.now(UTC) address.save(update_fields=["num_replied", "last_used_at"]) return address.num_replied diff --git a/emails/tests/mgmt_check_health_tests.py b/emails/tests/mgmt_check_health_tests.py index d3ef6d48ab..ff8e663b67 100644 --- a/emails/tests/mgmt_check_health_tests.py +++ b/emails/tests/mgmt_check_health_tests.py @@ -1,6 +1,6 @@ import json import logging -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from django.core.management import CommandError, call_command @@ -25,7 +25,7 @@ def write_healthcheck(path, age=0): Returns the path to the healthcheck file """ - timestamp = (datetime.now(tz=timezone.utc) - timedelta(seconds=age)).isoformat() + timestamp = (datetime.now(tz=UTC) - timedelta(seconds=age)).isoformat() data = {"timestamp": timestamp, "testing": True} with path.open("w", encoding="utf8") as f: json.dump(data, f) diff --git a/emails/tests/mgmt_process_emails_from_sqs_tests.py b/emails/tests/mgmt_process_emails_from_sqs_tests.py index 4068d0b3d4..f8fe6723d1 100644 --- a/emails/tests/mgmt_process_emails_from_sqs_tests.py +++ b/emails/tests/mgmt_process_emails_from_sqs_tests.py @@ -1,6 +1,6 @@ import json from collections.abc import Generator -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING, Any from unittest.mock import Mock, patch from uuid import uuid4 @@ -389,7 +389,7 @@ def test_writes_healthcheck_file(test_settings): "queue_count_not_visible": 3, } ts = datetime.fromisoformat(content["timestamp"]) - duration = (datetime.now(tz=timezone.utc) - ts).total_seconds() + duration = (datetime.now(tz=UTC) - ts).total_seconds() assert 0.0 < duration < 0.5 diff --git a/emails/tests/models_tests.py b/emails/tests/models_tests.py index 9ded7f3853..b67d572e47 100644 --- a/emails/tests/models_tests.py +++ b/emails/tests/models_tests.py @@ -1,5 +1,5 @@ import random -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from hashlib import sha256 from unittest import skip from unittest.mock import Mock, patch @@ -60,7 +60,7 @@ def make_free_test_user(email: str = "") -> User: def make_premium_test_user() -> User: premium_user = baker.make(User, email="premium@email.com") premium_user.profile.server_storage = True - premium_user.profile.date_subscribed = datetime.now(tz=timezone.utc) + premium_user.profile.date_subscribed = datetime.now(tz=UTC) premium_user.profile.save() upgrade_test_user_to_premium(premium_user) return premium_user @@ -71,7 +71,7 @@ def make_storageless_test_user() -> User: storageless_user_profile = storageless_user.profile storageless_user_profile.server_storage = False storageless_user_profile.subdomain = "mydomain" - storageless_user_profile.date_subscribed = datetime.now(tz=timezone.utc) + storageless_user_profile.date_subscribed = datetime.now(tz=UTC) storageless_user_profile.save() upgrade_test_user_to_premium(storageless_user) return storageless_user @@ -450,7 +450,7 @@ def test_clear_storage_with_update_fields(self) -> None: assert relay_address.used_on == "https://example.com" # Update a different field with update_fields to avoid full model save - new_last_used_at = datetime(2024, 1, 11, tzinfo=timezone.utc) + new_last_used_at = datetime(2024, 1, 11, tzinfo=UTC) relay_address.last_used_at = new_last_used_at relay_address.save(update_fields={"last_used_at"}) @@ -474,7 +474,7 @@ def test_clear_block_list_emails_with_update_fields(self) -> None: assert relay_address.block_list_emails # Update a different field with update_fields to avoid full model save - new_last_used_at = datetime(2024, 1, 12, tzinfo=timezone.utc) + new_last_used_at = datetime(2024, 1, 12, tzinfo=UTC) relay_address.last_used_at = new_last_used_at relay_address.save(update_fields={"last_used_at"}) @@ -543,7 +543,7 @@ def set_hard_bounce(self) -> datetime: This happens when the user's email server reports a hard bounce, such as saying the email does not exist. """ - self.profile.last_hard_bounce = datetime.now(timezone.utc) - timedelta( + self.profile.last_hard_bounce = datetime.now(UTC) - timedelta( days=settings.HARD_BOUNCE_ALLOWED_DAYS - 1 ) self.profile.save() @@ -556,7 +556,7 @@ def set_soft_bounce(self) -> datetime: This happens when the user's email server reports a soft bounce, such as saying the user's mailbox is full. """ - self.profile.last_soft_bounce = datetime.now(timezone.utc) - timedelta( + self.profile.last_soft_bounce = datetime.now(UTC) - timedelta( days=settings.SOFT_BOUNCE_ALLOWED_DAYS - 1 ) self.profile.save() @@ -592,7 +592,7 @@ def test_hard_and_soft_bounce_pending_shows_hard(self) -> None: assert bounce_type == "hard" def test_hard_bounce_over_resets_timer(self) -> None: - self.profile.last_hard_bounce = datetime.now(timezone.utc) - timedelta( + self.profile.last_hard_bounce = datetime.now(UTC) - timedelta( days=settings.HARD_BOUNCE_ALLOWED_DAYS + 1 ) self.profile.save() @@ -605,7 +605,7 @@ def test_hard_bounce_over_resets_timer(self) -> None: assert self.profile.last_hard_bounce is None def test_soft_bounce_over_resets_timer(self) -> None: - self.profile.last_soft_bounce = datetime.now(timezone.utc) - timedelta( + self.profile.last_soft_bounce = datetime.now(UTC) - timedelta( days=settings.SOFT_BOUNCE_ALLOWED_DAYS + 1 ) self.profile.save() @@ -622,7 +622,7 @@ class ProfileNextEmailTryDateTest(ProfileBounceTestCase): """Tests for Profile.next_email_try""" def test_no_bounces_returns_today(self) -> None: - assert self.profile.next_email_try.date() == datetime.now(timezone.utc).date() + assert self.profile.next_email_try.date() == datetime.now(UTC).date() def test_hard_bounce_returns_proper_datemath(self) -> None: last_hard_bounce = self.set_hard_bounce() @@ -716,7 +716,7 @@ def test_default_None(self) -> None: def test_real_phone_no_relay_number_returns_verified_date(self) -> None: self.upgrade_to_phone() - datetime_now = datetime.now(timezone.utc) + datetime_now = datetime.now(UTC) RealPhone.objects.create( user=self.profile.user, number="+12223334444", @@ -729,7 +729,7 @@ def test_real_phone_and_relay_number_w_created_at_returns_created_at_date( self, ) -> None: self.upgrade_to_phone() - datetime_now = datetime.now(timezone.utc) + datetime_now = datetime.now(UTC) phone_user = self.profile.user RealPhone.objects.create( user=phone_user, @@ -744,7 +744,7 @@ def test_real_phone_and_relay_number_wo_created_at_returns_verified_date( self, ) -> None: self.upgrade_to_phone() - datetime_now = datetime.now(timezone.utc) + datetime_now = datetime.now(UTC) phone_user = self.profile.user real_phone = RealPhone.objects.create( user=phone_user, @@ -878,7 +878,7 @@ def test_lowercases_subdomain_value_with_update_fields(self) -> None: assert self.profile.subdomain == "mIxEdcAsE" # Update a different field with update_fields to avoid a full model save - new_date_subscribed = datetime(2023, 3, 3, tzinfo=timezone.utc) + new_date_subscribed = datetime(2023, 3, 3, tzinfo=UTC) self.profile.date_subscribed = new_date_subscribed self.profile.save(update_fields={"date_subscribed"}) @@ -1189,9 +1189,9 @@ def setUp(self) -> None: mocked_datetime = patcher.start() self.addCleanup(patcher.stop) - self.expected_now = datetime.now(timezone.utc) + self.expected_now = datetime.now(UTC) mocked_datetime.combine.return_value = datetime.combine( - datetime.now(timezone.utc).date(), datetime.min.time() + datetime.now(UTC).date(), datetime.min.time() ) mocked_datetime.now.return_value = self.expected_now mocked_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw) @@ -1302,24 +1302,16 @@ def test_phone_user(self) -> None: def test_phone_user_1_month(self) -> None: self.upgrade_to_phone() - self.profile.date_phone_subscription_start = datetime( - 2024, 1, 1, tzinfo=timezone.utc - ) + self.profile.date_phone_subscription_start = datetime(2024, 1, 1, tzinfo=UTC) - self.profile.date_phone_subscription_end = datetime( - 2024, 2, 1, tzinfo=timezone.utc - ) + self.profile.date_phone_subscription_end = datetime(2024, 2, 1, tzinfo=UTC) assert self.profile.plan_term == "1_month" def test_phone_user_1_year(self) -> None: self.upgrade_to_phone() - self.profile.date_phone_subscription_start = datetime( - 2024, 1, 1, tzinfo=timezone.utc - ) + self.profile.date_phone_subscription_start = datetime(2024, 1, 1, tzinfo=UTC) - self.profile.date_phone_subscription_end = datetime( - 2025, 1, 1, tzinfo=timezone.utc - ) + self.profile.date_phone_subscription_end = datetime(2025, 1, 1, tzinfo=UTC) assert self.profile.plan_term == "1_year" def test_vpn_bundle_user(self) -> None: @@ -1341,24 +1333,16 @@ def test_phone_user(self) -> None: def test_phone_user_1_month(self) -> None: self.upgrade_to_phone() - self.profile.date_phone_subscription_start = datetime( - 2024, 1, 1, tzinfo=timezone.utc - ) + self.profile.date_phone_subscription_start = datetime(2024, 1, 1, tzinfo=UTC) - self.profile.date_phone_subscription_end = datetime( - 2024, 2, 1, tzinfo=timezone.utc - ) + self.profile.date_phone_subscription_end = datetime(2024, 2, 1, tzinfo=UTC) assert self.profile.metrics_premium_status == "phone_1_month" def test_phone_user_1_year(self) -> None: self.upgrade_to_phone() - self.profile.date_phone_subscription_start = datetime( - 2024, 1, 1, tzinfo=timezone.utc - ) + self.profile.date_phone_subscription_start = datetime(2024, 1, 1, tzinfo=UTC) - self.profile.date_phone_subscription_end = datetime( - 2025, 1, 1, tzinfo=timezone.utc - ) + self.profile.date_phone_subscription_end = datetime(2025, 1, 1, tzinfo=UTC) assert self.profile.metrics_premium_status == "phone_1_year" def test_vpn_bundle_user(self) -> None: @@ -1609,7 +1593,7 @@ def test_clear_storage_with_update_fields(self) -> None: assert domain_address.used_on == "https://example.com" # Update a different field with update_fields to avoid full model save - new_last_used_at = datetime(2024, 1, 11, tzinfo=timezone.utc) + new_last_used_at = datetime(2024, 1, 11, tzinfo=UTC) domain_address.last_used_at = new_last_used_at domain_address.save(update_fields={"last_used_at"}) @@ -1635,7 +1619,7 @@ def test_clear_block_list_emails_with_update_fields(self) -> None: assert domain_address.block_list_emails # Update a different field with update_fields to avoid full model save - new_last_used_at = datetime(2024, 1, 12, tzinfo=timezone.utc) + new_last_used_at = datetime(2024, 1, 12, tzinfo=UTC) assert domain_address.last_used_at != new_last_used_at domain_address.last_used_at = new_last_used_at domain_address.save(update_fields={"last_used_at"}) diff --git a/emails/tests/views_tests.py b/emails/tests/views_tests.py index 4fc09cc242..9529a4493f 100644 --- a/emails/tests/views_tests.py +++ b/emails/tests/views_tests.py @@ -4,7 +4,7 @@ import os import re from copy import deepcopy -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from email import message_from_string from email.message import EmailMessage from typing import Any, cast @@ -386,7 +386,7 @@ def setUp(self) -> None: super().setUp() self.user = baker.make(User, email="user@example.com") self.profile = self.user.profile - self.profile.last_engagement = datetime.now(timezone.utc) + self.profile.last_engagement = datetime.now(UTC) self.profile.save() self.sa: SocialAccount = baker.make( SocialAccount, user=self.user, provider="fxa" @@ -397,7 +397,7 @@ def setUp(self) -> None: self.premium_user = make_premium_test_user() self.premium_profile = Profile.objects.get(user=self.premium_user) self.premium_profile.subdomain = "subdomain" - self.premium_profile.last_engagement = datetime.now(timezone.utc) + self.premium_profile.last_engagement = datetime.now(UTC) self.premium_profile.save() def test_single_recipient_sns_notification(self) -> None: @@ -413,7 +413,7 @@ def test_single_recipient_sns_notification(self) -> None: self.ra.refresh_from_db() assert self.ra.num_forwarded == 1 assert self.ra.last_used_at is not None - assert (datetime.now(tz=timezone.utc) - self.ra.last_used_at).seconds < 2.0 + assert (datetime.now(tz=UTC) - self.ra.last_used_at).seconds < 2.0 self.ra.user.profile.refresh_from_db() assert self.ra.user.profile.last_engagement is not None assert ( @@ -437,7 +437,7 @@ def test_single_french_recipient_sns_notification(self) -> None: self.ra.refresh_from_db() assert self.ra.num_forwarded == 1 assert self.ra.last_used_at is not None - assert (datetime.now(tz=timezone.utc) - self.ra.last_used_at).seconds < 2.0 + assert (datetime.now(tz=UTC) - self.ra.last_used_at).seconds < 2.0 def test_list_email_sns_notification(self) -> None: """By default, list emails should still forward.""" @@ -447,7 +447,7 @@ def test_list_email_sns_notification(self) -> None: self.ra.refresh_from_db() assert self.ra.num_forwarded == 1 assert self.ra.last_used_at is not None - assert (datetime.now(tz=timezone.utc) - self.ra.last_used_at).seconds < 2.0 + assert (datetime.now(tz=UTC) - self.ra.last_used_at).seconds < 2.0 def test_block_list_email_sns_notification(self) -> None: """When an alias is blocking list emails, list emails should not forward.""" @@ -524,7 +524,7 @@ def test_domain_recipient(self) -> None: da = DomainAddress.objects.get(user=self.premium_user, address="wildcard") assert da.num_forwarded == 1 assert da.last_used_at - assert (datetime.now(tz=timezone.utc) - da.last_used_at).seconds < 2.0 + assert (datetime.now(tz=UTC) - da.last_used_at).seconds < 2.0 mask_event = get_glean_event(caplog, "email_mask", "created") assert mask_event is not None @@ -561,7 +561,7 @@ def test_successful_email_relay_message_removed_from_s3(self) -> None: self.ra.refresh_from_db() assert self.ra.num_forwarded == 1 assert self.ra.last_used_at is not None - assert (datetime.now(tz=timezone.utc) - self.ra.last_used_at).seconds < 2.0 + assert (datetime.now(tz=UTC) - self.ra.last_used_at).seconds < 2.0 def test_unsuccessful_email_relay_message_not_removed_from_s3(self) -> None: self.mock_send_raw_email.side_effect = SEND_RAW_EMAIL_FAILED @@ -606,7 +606,7 @@ def test_inline_image(self) -> None: self.ra.refresh_from_db() assert self.ra.num_forwarded == 1 assert self.ra.last_used_at - assert (datetime.now(tz=timezone.utc) - self.ra.last_used_at).seconds < 2.0 + assert (datetime.now(tz=UTC) - self.ra.last_used_at).seconds < 2.0 def test_russian_spam(self) -> None: """ @@ -628,7 +628,7 @@ def test_russian_spam(self) -> None: self.ra.refresh_from_db() assert self.ra.num_forwarded == 1 assert self.ra.last_used_at - assert (datetime.now(tz=timezone.utc) - self.ra.last_used_at).seconds < 2.0 + assert (datetime.now(tz=UTC) - self.ra.last_used_at).seconds < 2.0 @patch("emails.views.info_logger") def test_plain_text(self, mock_logger: Mock) -> None: @@ -644,7 +644,7 @@ def test_plain_text(self, mock_logger: Mock) -> None: self.ra.refresh_from_db() assert self.ra.num_forwarded == 1 assert self.ra.last_used_at - assert (datetime.now(tz=timezone.utc) - self.ra.last_used_at).seconds < 2.0 + assert (datetime.now(tz=UTC) - self.ra.last_used_at).seconds < 2.0 mock_logger.warning.assert_not_called() @patch("emails.views.info_logger") @@ -744,8 +744,8 @@ def setUp(self) -> None: # Create a premium user matching the s3_stored_replies sender self.user = baker.make(User, email="source@sender.com") self.user.profile.server_storage = True - self.user.profile.date_subscribed = datetime.now(tz=timezone.utc) - self.user.profile.last_engagement = datetime.now(tz=timezone.utc) + self.user.profile.date_subscribed = datetime.now(tz=UTC) + self.user.profile.last_engagement = datetime.now(tz=UTC) self.user.profile.save() self.pre_reply_last_engagement = self.user.profile.last_engagement upgrade_test_user_to_premium(self.user) @@ -802,7 +802,7 @@ def successful_reply_test_implementation( assert self.relay_address.num_replied == 1 last_used_at = self.relay_address.last_used_at assert last_used_at - assert (datetime.now(tz=timezone.utc) - last_used_at).seconds < 2.0 + assert (datetime.now(tz=UTC) - last_used_at).seconds < 2.0 assert (last_en := self.relay_address.user.profile.last_engagement) is not None assert last_en > self.pre_reply_last_engagement @@ -911,7 +911,7 @@ def setUp(self): ) def test_sns_message_with_hard_bounce(self) -> None: - pre_request_datetime = datetime.now(timezone.utc) + pre_request_datetime = datetime.now(UTC) with self.assertLogs(INFO_LOG) as logs, MetricsMock() as mm: _sns_notification(BOUNCE_SNS_BODIES["hard"]) @@ -946,7 +946,7 @@ def test_sns_message_with_hard_bounce(self) -> None: ) def test_sns_message_with_soft_bounce(self) -> None: - pre_request_datetime = datetime.now(timezone.utc) + pre_request_datetime = datetime.now(UTC) with self.assertLogs(INFO_LOG) as logs, MetricsMock() as mm: _sns_notification(BOUNCE_SNS_BODIES["soft"]) @@ -1401,7 +1401,7 @@ def test_auto_block_spam_true_email_in_s3_deleted(self) -> None: mm.assert_incr_once("fx.private.relay.email_auto_suppressed_for_spam") def test_user_bounce_soft_paused_email_in_s3_deleted(self) -> None: - self.profile.last_soft_bounce = datetime.now(timezone.utc) + self.profile.last_soft_bounce = datetime.now(UTC) self.profile.save() with self.assertLogs(INFO_LOG) as caplog, MetricsMock() as mm: @@ -1413,7 +1413,7 @@ def test_user_bounce_soft_paused_email_in_s3_deleted(self) -> None: mm.assert_incr_once("fx.private.relay.email_suppressed_for_soft_bounce") def test_user_bounce_hard_paused_email_in_s3_deleted(self) -> None: - self.profile.last_hard_bounce = datetime.now(timezone.utc) + self.profile.last_hard_bounce = datetime.now(UTC) self.profile.save() with self.assertLogs(INFO_LOG) as caplog, MetricsMock() as mm: @@ -1443,8 +1443,8 @@ def test_reply_not_allowed_email_in_s3_deleted( def test_flagged_user_email_in_s3_deleted(self) -> None: profile = self.address.user.profile - profile.last_account_flagged = datetime.now(timezone.utc) - profile.last_engagement = datetime.now(timezone.utc) + profile.last_account_flagged = datetime.now(UTC) + profile.last_engagement = datetime.now(UTC) profile.save() pre_flagged_last_engagement = profile.last_engagement @@ -1461,7 +1461,7 @@ def test_relay_address_disabled_email_in_s3_deleted(self) -> None: self.address.enabled = False self.address.save() profile = self.address.user.profile - profile.last_engagement = datetime.now(timezone.utc) + profile.last_engagement = datetime.now(UTC) profile.save() pre_blocked_email_last_engagement = profile.last_engagement @@ -1486,7 +1486,7 @@ def test_blocked_list_email_in_s3_deleted( self.address.block_list_emails = True self.address.save() profile = self.address.user.profile - profile.last_engagement = datetime.now(timezone.utc) + profile.last_engagement = datetime.now(UTC) profile.save() pre_blocked_email_last_engagement = profile.last_engagement mocked_email_is_from_list.return_value = True diff --git a/emails/views.py b/emails/views.py index 721fd0d781..c5a6f429af 100644 --- a/emails/views.py +++ b/emails/views.py @@ -5,7 +5,7 @@ import shlex from collections import defaultdict from copy import deepcopy -from datetime import datetime, timezone +from datetime import UTC, datetime from email import message_from_bytes from email.iterators import _structure from email.message import EmailMessage @@ -676,7 +676,7 @@ def _handle_received(message_json: AWS_SNSMessageJSON) -> HttpResponse: address.num_blocked += 1 address.save(update_fields=["num_blocked"]) _record_receipt_verdicts(receipt, "disabled_alias") - user_profile.last_engagement = datetime.now(timezone.utc) + user_profile.last_engagement = datetime.now(UTC) user_profile.save() glean_logger().log_email_blocked(mask=address, reason="block_all") return HttpResponse("Address is temporarily disabled.") @@ -694,7 +694,7 @@ def _handle_received(message_json: AWS_SNSMessageJSON) -> HttpResponse: incr_if_enabled("list_email_for_address_blocking_lists", 1) address.num_blocked += 1 address.save(update_fields=["num_blocked"]) - user_profile.last_engagement = datetime.now(timezone.utc) + user_profile.last_engagement = datetime.now(UTC) user_profile.save() glean_logger().log_email_blocked(mask=address, reason="block_promotional") return HttpResponse("Address is not accepting list emails.") @@ -793,10 +793,10 @@ def _handle_received(message_json: AWS_SNSMessageJSON) -> HttpResponse: user_profile.update_abuse_metric( email_forwarded=True, forwarded_email_size=len(incoming_email_bytes) ) - user_profile.last_engagement = datetime.now(timezone.utc) + user_profile.last_engagement = datetime.now(UTC) user_profile.save() address.num_forwarded += 1 - address.last_used_at = datetime.now(timezone.utc) + address.last_used_at = datetime.now(UTC) if level_one_trackers_removed: address.num_level_one_trackers_blocked = ( address.num_level_one_trackers_blocked or 0 @@ -1085,7 +1085,7 @@ def _convert_html_content( now: datetime | None = None, ) -> tuple[str, int]: # frontend expects a timestamp in milliseconds - now = now or datetime.now(timezone.utc) + now = now or datetime.now(UTC) datetime_now_ms = int(now.timestamp() * 1000) # scramble alias so that clients don't recognize it @@ -1320,7 +1320,7 @@ def _handle_reply( reply_record.increment_num_replied() profile = address.user.profile profile.update_abuse_metric(replied=True) - profile.last_engagement = datetime.now(timezone.utc) + profile.last_engagement = datetime.now(UTC) profile.save() glean_logger().log_email_forwarded(mask=address, is_reply=True) return HttpResponse("Sent email to final recipient.", status=200) @@ -1364,7 +1364,7 @@ def _get_domain_address(local_portion: str, domain_portion: str) -> DomainAddres mask=domain_address, created_by_api=False, ) - domain_address.last_used_at = datetime.now(timezone.utc) + domain_address.last_used_at = datetime.now(UTC) domain_address.save() return domain_address except Profile.DoesNotExist as e: @@ -1447,7 +1447,7 @@ def _handle_bounce(message_json: AWS_SNSMessageJSON) -> HttpResponse: bounce_subtype = bounce.get("bounceSubType", "none") bounced_recipients = bounce.get("bouncedRecipients", []) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) bounce_data = [] for recipient in bounced_recipients: recipient_address = recipient.pop("emailAddress", None) diff --git a/mypy_stubs/decouple.pyi b/mypy_stubs/decouple.pyi index 87c4b0ed65..3764fd23fd 100644 --- a/mypy_stubs/decouple.pyi +++ b/mypy_stubs/decouple.pyi @@ -10,8 +10,8 @@ Changes: * Simplified interfaces of Csv and Choices to our usage """ -from collections.abc import Sequence -from typing import Any, Callable, Generic, TypeVar, Union, overload +from collections.abc import Callable, Sequence +from typing import Any, Generic, TypeVar, overload # Unreleased as of 3.6 - accepts a bool # def strtobool(value: Union[str, bool]) -> bool: ... @@ -25,7 +25,7 @@ def config(option: str) -> str: ... @overload def config(option: str, default: str) -> str: ... @overload -def config(option: str, default: _DefaultType) -> Union[str, _DefaultType]: ... +def config(option: str, default: _DefaultType) -> str | _DefaultType: ... @overload def config( option: str, default: _DefaultType, cast: Callable[[_DefaultType], _CastReturnType] diff --git a/phones/models.py b/phones/models.py index 05ba881df5..f3f57d21bd 100644 --- a/phones/models.py +++ b/phones/models.py @@ -4,7 +4,7 @@ import secrets import string from collections.abc import Iterator -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from math import floor from django.conf import settings @@ -41,7 +41,7 @@ def verification_code_default(): def verification_sent_date_default(): - return datetime.now(timezone.utc) + return datetime.now(UTC) def get_expired_unverified_realphone_records(number): @@ -49,7 +49,7 @@ def get_expired_unverified_realphone_records(number): number=number, verified=False, verification_sent_date__lt=( - datetime.now(timezone.utc) + datetime.now(UTC) - timedelta(0, 60 * settings.MAX_MINUTES_TO_VERIFY_REAL_PHONE) ), ) @@ -60,7 +60,7 @@ def get_pending_unverified_realphone_records(number): number=number, verified=False, verification_sent_date__gt=( - datetime.now(timezone.utc) + datetime.now(UTC) - timedelta(0, 60 * settings.MAX_MINUTES_TO_VERIFY_REAL_PHONE) ), ) @@ -80,7 +80,7 @@ def get_valid_realphone_verification_record(user, number, verification_code): number=number, verification_code=verification_code, verification_sent_date__gt=( - datetime.now(timezone.utc) + datetime.now(UTC) - timedelta(0, 60 * settings.MAX_MINUTES_TO_VERIFY_REAL_PHONE) ), ).first() @@ -178,7 +178,7 @@ def save(self, *args, **kwargs): def mark_verified(self): incr_if_enabled("phones_RealPhone.mark_verified") self.verified = True - self.verified_date = datetime.now(timezone.utc) + self.verified_date = datetime.now(UTC) self.save(force_update=True) return self @@ -404,7 +404,7 @@ def send_welcome_message(user, relay_number): def last_inbound_date_default(): - return datetime.now(timezone.utc) + return datetime.now(UTC) class InboundContact(models.Model): diff --git a/phones/tests/mgmt_delete_phone_data_tests.py b/phones/tests/mgmt_delete_phone_data_tests.py index 8a18d8025f..f42e23811b 100644 --- a/phones/tests/mgmt_delete_phone_data_tests.py +++ b/phones/tests/mgmt_delete_phone_data_tests.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from io import StringIO from unittest.mock import patch @@ -31,7 +31,7 @@ def test_settings(settings: SettingsWrapper) -> SettingsWrapper: def phone_user(db: None, test_settings: SettingsWrapper) -> User: """Return a Relay user with phone setup and phone usage.""" # Create the user - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) phone_user = make_phone_test_user() phone_user.profile.date_subscribed = now - timedelta(days=15) phone_user.profile.save() diff --git a/phones/tests/models_tests.py b/phones/tests/models_tests.py index f6ebd5a496..6d50c3a7e9 100644 --- a/phones/tests/models_tests.py +++ b/phones/tests/models_tests.py @@ -1,5 +1,5 @@ import random -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from types import SimpleNamespace from unittest.mock import Mock, call, patch from uuid import uuid4 @@ -78,9 +78,7 @@ def mock_twilio_client(twilio_number_sid: str): def make_phone_test_user() -> User: phone_user = baker.make(User, email="phone_user@example.com") phone_user_profile = Profile.objects.get(user=phone_user) - phone_user_profile.date_subscribed = datetime.now(tz=timezone.utc) - timedelta( - days=15 - ) + phone_user_profile.date_subscribed = datetime.now(tz=UTC) - timedelta(days=15) phone_user_profile.save() upgrade_test_user_to_phone(phone_user) return phone_user @@ -98,7 +96,7 @@ def upgrade_test_user_to_phone(user): baker.make( SocialToken, account=account, - expires_at=datetime.now(timezone.utc) + timedelta(1), + expires_at=datetime.now(UTC) + timedelta(1), ) return user @@ -121,7 +119,7 @@ def test_get_valid_realphone_verification_record_returns_object(phone_user): real_phone = RealPhone.objects.create( user=phone_user, number=number, - verification_sent_date=datetime.now(timezone.utc), + verification_sent_date=datetime.now(UTC), ) record = get_valid_realphone_verification_record( phone_user, number, real_phone.verification_code @@ -136,7 +134,7 @@ def test_get_valid_realphone_verification_record_returns_none(phone_user): user=phone_user, number=number, verification_sent_date=( - datetime.now(timezone.utc) + datetime.now(UTC) - timedelta(0, 60 * settings.MAX_MINUTES_TO_VERIFY_REAL_PHONE + 1) ), ) @@ -203,7 +201,7 @@ def test_create_realphone_deletes_expired_unverified_records( number=number, verified=False, verification_sent_date=( - datetime.now(timezone.utc) + datetime.now(UTC) - timedelta(0, 60 * settings.MAX_MINUTES_TO_VERIFY_REAL_PHONE + 1) ), ) @@ -320,7 +318,7 @@ def real_phone_us(phone_user, mock_twilio_client): user=phone_user, number="+12223334444", verified=True, - verification_sent_date=datetime.now(timezone.utc), + verification_sent_date=datetime.now(UTC), ) mock_twilio_client.messages.create.assert_called_once() mock_twilio_client.messages.create.reset_mock() @@ -564,7 +562,7 @@ def real_phone_ca(phone_user, mock_twilio_client): user=phone_user, number="+14035551234", verified=True, - verification_sent_date=datetime.now(timezone.utc), + verification_sent_date=datetime.now(UTC), country_code="CA", ) mock_twilio_client.messages.create.assert_called_once() @@ -772,31 +770,31 @@ def test_get_last_text_sender_lots_of_inbound_returns_one(): InboundContact, relay_number=relay_number, last_inbound_type="call", - last_inbound_date=datetime.now(timezone.utc) - timedelta(days=4), + last_inbound_date=datetime.now(UTC) - timedelta(days=4), ) baker.make( InboundContact, relay_number=relay_number, last_inbound_type="text", - last_inbound_date=datetime.now(timezone.utc) - timedelta(days=3), + last_inbound_date=datetime.now(UTC) - timedelta(days=3), ) baker.make( InboundContact, relay_number=relay_number, last_inbound_type="call", - last_inbound_date=datetime.now(timezone.utc) - timedelta(days=2), + last_inbound_date=datetime.now(UTC) - timedelta(days=2), ) baker.make( InboundContact, relay_number=relay_number, last_inbound_type="text", - last_inbound_date=datetime.now(timezone.utc) - timedelta(days=1), + last_inbound_date=datetime.now(UTC) - timedelta(days=1), ) inbound_contact = baker.make( InboundContact, relay_number=relay_number, last_inbound_type="text", - last_inbound_date=datetime.now(timezone.utc), + last_inbound_date=datetime.now(UTC), ) assert get_last_text_sender(relay_number) == inbound_contact diff --git a/privaterelay/fxa_utils.py b/privaterelay/fxa_utils.py index 12c50a8d31..1672c4d237 100644 --- a/privaterelay/fxa_utils.py +++ b/privaterelay/fxa_utils.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from typing import Any, cast from django.conf import settings @@ -34,7 +34,7 @@ def update_social_token( ) -> None: existing_social_token.token = new_oauth2_token["access_token"] existing_social_token.token_secret = new_oauth2_token["refresh_token"] - existing_social_token.expires_at = datetime.now(timezone.utc) + timedelta( + existing_social_token.expires_at = datetime.now(UTC) + timedelta( seconds=int(new_oauth2_token["expires_in"]) ) existing_social_token.save() @@ -59,7 +59,7 @@ def _token_updater(new_token): "client_secret": client_secret, } - expires_in = (social_token.expires_at - datetime.now(timezone.utc)).total_seconds() + expires_in = (social_token.expires_at - datetime.now(UTC)).total_seconds() token = { "access_token": social_token.token, "refresh_token": social_token.token_secret, @@ -171,10 +171,8 @@ def get_phone_subscription_dates(social_account): return None, None, None date_subscribed_phone = datetime.fromtimestamp( - subscription_created_timestamp, tz=timezone.utc + subscription_created_timestamp, tz=UTC ) - start_date = datetime.fromtimestamp( - subscription_start_timestamp, tz=timezone.utc - ) - end_date = datetime.fromtimestamp(subscription_end_timestamp, tz=timezone.utc) + start_date = datetime.fromtimestamp(subscription_start_timestamp, tz=UTC) + end_date = datetime.fromtimestamp(subscription_end_timestamp, tz=UTC) return date_subscribed_phone, start_date, end_date diff --git a/privaterelay/glean/server_events.py b/privaterelay/glean/server_events.py index b130480a9c..c7101957cc 100644 --- a/privaterelay/glean/server_events.py +++ b/privaterelay/glean/server_events.py @@ -11,7 +11,7 @@ from __future__ import annotations import json -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from uuid import uuid4 @@ -34,7 +34,7 @@ def __init__( self._channel = channel def _record(self, user_agent: str, ip_address: str, event: dict[str, Any]) -> None: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) timestamp = now.isoformat() event["timestamp"] = int(1000.0 * now.timestamp()) # Milliseconds since epoch event_payload = { diff --git a/privaterelay/management/commands/sync_phone_related_dates_on_profile.py b/privaterelay/management/commands/sync_phone_related_dates_on_profile.py index 307eaa63ac..cd2468952d 100644 --- a/privaterelay/management/commands/sync_phone_related_dates_on_profile.py +++ b/privaterelay/management/commands/sync_phone_related_dates_on_profile.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from django.conf import settings from django.core.management.base import BaseCommand, CommandParser @@ -25,7 +25,7 @@ def sync_phone_related_dates_on_profile(group: str) -> int: return 0 num_updated_accounts = 0 - datetime_now = datetime.now(timezone.utc) + datetime_now = datetime.now(UTC) for social_account in social_accounts_with_phones: date_subscribed_phone, start_date, end_date = get_phone_subscription_dates( social_account diff --git a/privaterelay/management/commands/update_phone_remaining_stats.py b/privaterelay/management/commands/update_phone_remaining_stats.py index af465b77ef..b8b690145a 100644 --- a/privaterelay/management/commands/update_phone_remaining_stats.py +++ b/privaterelay/management/commands/update_phone_remaining_stats.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from django.conf import settings from django.contrib.auth.models import User @@ -50,7 +50,7 @@ def get_next_reset_date(profile: Profile) -> datetime: "date_phone_subscription_end": profile.date_phone_subscription_end, }, ) - return datetime.now(timezone.utc) - timedelta(minutes=15) + return datetime.now(UTC) - timedelta(minutes=15) calculated_next_reset_date = profile.date_phone_subscription_reset + timedelta( settings.MAX_DAYS_IN_MONTH @@ -72,7 +72,7 @@ def update_phone_remaining_stats() -> tuple[int, int]: return 0, 0 updated_profiles = [] - datetime_now = datetime.now(timezone.utc) + datetime_now = datetime.now(UTC) for social_account in social_accounts_with_phones: profile = social_account.user.profile next_reset_date = get_next_reset_date(profile) diff --git a/privaterelay/middleware.py b/privaterelay/middleware.py index 8c0f362ab8..02d2bf1a13 100644 --- a/privaterelay/middleware.py +++ b/privaterelay/middleware.py @@ -1,5 +1,5 @@ import time -from datetime import datetime, timezone +from datetime import UTC, datetime from django.conf import settings from django.shortcuts import redirect @@ -90,7 +90,7 @@ def __call__(self, request): response = self.get_response(request) first_visit = request.COOKIES.get("first_visit") if first_visit is None and not request.user.is_anonymous: - response.set_cookie("first_visit", datetime.now(timezone.utc)) + response.set_cookie("first_visit", datetime.now(UTC)) return response diff --git a/privaterelay/tests/fxa_utils_tests.py b/privaterelay/tests/fxa_utils_tests.py index 79980864f4..51ad726506 100644 --- a/privaterelay/tests/fxa_utils_tests.py +++ b/privaterelay/tests/fxa_utils_tests.py @@ -2,7 +2,7 @@ Tests for private_relay/fxa_utils.py """ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from unittest.mock import patch from django.conf import settings @@ -156,7 +156,7 @@ def test_get_phone_subscription_dates_subscription_has_invalid_phone_susbscripti def test_get_phone_subscription_dates_subscription_has_phone_subscription_data( mocked_logger, mocked_data_from_fxa, phone_user ): - first_day_this_month = datetime.now(timezone.utc).replace(day=1) + first_day_this_month = datetime.now(UTC).replace(day=1) first_day_next_month = (first_day_this_month + timedelta(31)).replace(day=1) social_account = SocialAccount.objects.get(user=phone_user) sample_subscription_data = { diff --git a/privaterelay/tests/glean_tests.py b/privaterelay/tests/glean_tests.py index 49d062236d..54bd4dcc8e 100644 --- a/privaterelay/tests/glean_tests.py +++ b/privaterelay/tests/glean_tests.py @@ -1,5 +1,5 @@ import json -from datetime import datetime, timezone +from datetime import UTC, datetime from logging import LogRecord from typing import Any, NamedTuple from uuid import UUID, uuid4 @@ -268,7 +268,7 @@ def extract_parts_from_payload(payload: dict[str, Any]) -> PayloadVariedParts: start_time_iso = payload["ping_info"]["start_time"] start_time = datetime.fromisoformat(start_time_iso) # The start_time is in ISO 8601 format with timezone data. Check the conversion. - assert 0 < (datetime.now(timezone.utc) - start_time).total_seconds() < 0.5 + assert 0 < (datetime.now(UTC) - start_time).total_seconds() < 0.5 telemetry_sdk_build = payload["client_info"]["telemetry_sdk_build"] # The version will change with glean_parser releases, so only check prefix diff --git a/privaterelay/tests/mgmt_sync_phone_related_dates_on_profile_tests.py b/privaterelay/tests/mgmt_sync_phone_related_dates_on_profile_tests.py index a2d1da3cee..b4d494f199 100644 --- a/privaterelay/tests/mgmt_sync_phone_related_dates_on_profile_tests.py +++ b/privaterelay/tests/mgmt_sync_phone_related_dates_on_profile_tests.py @@ -2,7 +2,7 @@ Tests for private_relay/management/commands/sync_phone_related_dates_on_profile.py """ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from unittest.mock import patch from django.conf import settings @@ -50,7 +50,7 @@ def patch_datetime_now(): https://docs.python.org/3/library/unittest.mock-examples.html#partial-mocking """ with patch(f"{MOCK_BASE}.datetime") as mocked_datetime: - expected_now = datetime.now(timezone.utc) + expected_now = datetime.now(UTC) mocked_datetime.combine.return_value = datetime.combine( expected_now.date(), datetime.min.time() ) @@ -144,7 +144,7 @@ def test_monthly_phone_subscriber_profile_date_fields_all_updated( mocked_dates, patch_datetime_now, phone_user ): profile = Profile.objects.get(user=phone_user) - date_subscribed_phone = datetime.now(timezone.utc) - timedelta(3) + date_subscribed_phone = datetime.now(UTC) - timedelta(3) profile.date_subscribed_phone = date_subscribed_phone profile.save() mocked_dates.return_value = ( @@ -168,7 +168,7 @@ def test_monthly_phone_subscriber_renewed_subscription_profile_date_phone_subscr mocked_dates, patch_datetime_now, phone_user ): profile = Profile.objects.get(user=phone_user) - first_day_of_current_month = datetime.now(timezone.utc).replace(day=1) + first_day_of_current_month = datetime.now(UTC).replace(day=1) # get first of the last month first_day_of_last_month = (first_day_of_current_month - timedelta(1)).replace(day=1) # get first of the next month @@ -204,7 +204,7 @@ def test_yearly_phone_subscriber_profile_date_fields_all_updated( mocked_dates, patch_datetime_now, phone_user ): profile = Profile.objects.get(user=phone_user) - date_subscribed_phone = datetime.now(timezone.utc) - timedelta(3) + date_subscribed_phone = datetime.now(UTC) - timedelta(3) mocked_dates.return_value = ( date_subscribed_phone, date_subscribed_phone, @@ -226,7 +226,7 @@ def test_yearly_phone_subscriber_with_subscription_date_older_than_31_days_profi mocked_dates, patch_datetime_now, phone_user ): profile = Profile.objects.get(user=phone_user) - date_subscribed_phone = datetime.now(timezone.utc) - timedelta(90) + date_subscribed_phone = datetime.now(UTC) - timedelta(90) profile.date_subscribed_phone = date_subscribed_phone profile.save() mocked_dates.return_value = ( @@ -254,7 +254,7 @@ def test_command_with_one_update( capsys, ): profile = Profile.objects.get(user=phone_user) - date_subscribed_phone = datetime.now(timezone.utc) + date_subscribed_phone = datetime.now(UTC) profile.date_subscribed_phone = date_subscribed_phone profile.save() mocked_dates.return_value = ( diff --git a/privaterelay/tests/mgmt_update_phone_remaining_stats_tests.py b/privaterelay/tests/mgmt_update_phone_remaining_stats_tests.py index c054c0f515..45bef7024d 100644 --- a/privaterelay/tests/mgmt_update_phone_remaining_stats_tests.py +++ b/privaterelay/tests/mgmt_update_phone_remaining_stats_tests.py @@ -2,7 +2,7 @@ Tests for private_relay/management/commands/cleanup_data.py """ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from unittest.mock import patch from django.conf import settings @@ -51,7 +51,7 @@ def patch_datetime_now(): https://docs.python.org/3/library/unittest.mock-examples.html#partial-mocking """ with patch(f"{MOCK_BASE}.datetime") as mocked_datetime: - expected_now = datetime.now(timezone.utc) + expected_now = datetime.now(UTC) mocked_datetime.combine.return_value = datetime.combine( expected_now.date(), datetime.min.time() ) @@ -233,7 +233,7 @@ def test_phone_subscriber_with_phones_reset_31_day_ago_phone_limits_updated( def test_phone_subscriber_with_subscription_end_date_sooner_than_31_days_since_reset_phone_limits_updated( # noqa: E501 phone_user, ): - datetime_now = datetime.now(timezone.utc) + datetime_now = datetime.now(UTC) datetime_first_of_march = datetime_now.replace(month=3, day=1) profile = Profile.objects.get(user=phone_user) profile.date_subscribed_phone = datetime_now.replace(month=1, day=1) @@ -273,7 +273,7 @@ def test_phone_subscriber_with_subscription_end_date_sooner_than_31_days_since_r def test_phone_subscriber_with_subscription_end_date_after_reset_phone_limits_updated( phone_user, ): - datetime_now = datetime.now(timezone.utc) + datetime_now = datetime.now(UTC) datetime_fourth_of_march = datetime_now.replace(month=3, day=4) profile = Profile.objects.get(user=phone_user) profile.date_subscribed_phone = datetime_now.replace(month=1, day=2) diff --git a/privaterelay/views.py b/privaterelay/views.py index ddb8cc65cf..a10c85ebb5 100644 --- a/privaterelay/views.py +++ b/privaterelay/views.py @@ -1,8 +1,8 @@ import json import logging from collections.abc import Iterable -from datetime import datetime, timezone -from functools import lru_cache +from datetime import UTC, datetime +from functools import cache from hashlib import sha256 from typing import Any, TypedDict @@ -47,7 +47,7 @@ info_logger = logging.getLogger("eventsinfo") -@lru_cache(maxsize=None) +@cache def _get_fxa(request): return request.user.socialaccount_set.filter(provider="fxa").first() @@ -225,7 +225,7 @@ def _verify_jwt_with_fxa_key( iat = claims.get("iat") iat_age = None if iat: - iat_age = round(datetime.now(tz=timezone.utc).timestamp() - iat, 3) + iat_age = round(datetime.now(tz=UTC).timestamp() - iat, 3) info_logger.warning( "fxa_rp_event.future_iat", extra={"iat": iat, "iat_age_s": iat_age} ) @@ -305,7 +305,7 @@ def _update_all_data( no_longer_premium = had_premium and not now_has_premium if newly_premium: incr_if_enabled("user_purchased_premium", 1) - profile.date_subscribed = datetime.now(timezone.utc) + profile.date_subscribed = datetime.now(UTC) profile.save() if no_longer_premium: incr_if_enabled("user_has_downgraded", 1) @@ -314,8 +314,8 @@ def _update_all_data( no_longer_phone = had_phone and not now_has_phone if newly_phone: incr_if_enabled("user_purchased_phone", 1) - profile.date_subscribed_phone = datetime.now(timezone.utc) - profile.date_phone_subscription_reset = datetime.now(timezone.utc) + profile.date_subscribed_phone = datetime.now(UTC) + profile.date_phone_subscription_reset = datetime.now(UTC) profile.save() if no_longer_phone: incr_if_enabled("user_has_dropped_phone", 1) diff --git a/pyproject.toml b/pyproject.toml index 0748c798de..9e0bbba5e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ select = [ "E", # pycodestyle errors "F", # pyflakes "I", # isort + "UP", # pyupgrade "W", # pycodestyle warnings ] extend-safe-fixes = [ From 50826c65d04992e0a4aecdaf071a82fdfa2f8b5b Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Mon, 15 Apr 2024 11:57:27 -0500 Subject: [PATCH 11/11] Enable most bandit checks Ignore two rules with production code changes, for now. --- emails/models.py | 6 +++++- emails/sns.py | 2 +- emails/templatetags/email_extras.py | 2 +- privaterelay/allauth.py | 2 +- privaterelay/settings.py | 2 +- privaterelay/utils.py | 2 +- pyproject.toml | 11 +++++++++++ 7 files changed, 21 insertions(+), 6 deletions(-) diff --git a/emails/models.py b/emails/models.py index c8f257dbc5..057d77eeac 100644 --- a/emails/models.py +++ b/emails/models.py @@ -591,7 +591,11 @@ def address_hash(address, subdomain=None, domain=None): def address_default(): - return "".join(random.choices(string.ascii_lowercase + string.digits, k=9)) + return "".join( + random.choices( # noqa: S311 (standard pseudo-random generator used) + string.ascii_lowercase + string.digits, k=9 + ) + ) def has_bad_words(value) -> bool: diff --git a/emails/sns.py b/emails/sns.py index 90e604d8b2..0c49678989 100644 --- a/emails/sns.py +++ b/emails/sns.py @@ -97,7 +97,7 @@ def _grab_keyfile(cert_url): pemfile = key_cache.get(cert_url) if not pemfile: - response = urlopen(cert_url) + response = urlopen(cert_url) # noqa: S310 (check for custom scheme) pemfile = response.read() # Extract the first certificate in the file and confirm it's a valid # PEM certificate diff --git a/emails/templatetags/email_extras.py b/emails/templatetags/email_extras.py index 1636b033f0..b45bf04069 100644 --- a/emails/templatetags/email_extras.py +++ b/emails/templatetags/email_extras.py @@ -50,4 +50,4 @@ def convert_fsi_to_span(text: str | SafeString, autoescape=True) -> str | SafeSt ) else: result = f'{pre_fsi}{middle}{post_pdi}' - return mark_safe(result) + return mark_safe(result) # noqa: S308 (use of mark_safe) diff --git a/privaterelay/allauth.py b/privaterelay/allauth.py index b38e0aa6ce..df69ed6167 100644 --- a/privaterelay/allauth.py +++ b/privaterelay/allauth.py @@ -40,7 +40,7 @@ def is_safe_url(self, url: str | None) -> bool: # Is this a known frontend path? try: middleware = RelayStaticFilesMiddleware() - except Exception: + except Exception: # noqa: S110 (exception pass without log) # Staticfiles are not available pass else: diff --git a/privaterelay/settings.py b/privaterelay/settings.py index c1ac4cfc29..fda1500e4a 100644 --- a/privaterelay/settings.py +++ b/privaterelay/settings.py @@ -485,7 +485,7 @@ def _get_initial_middleware() -> list[str]: # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators # only needed when admin UI is enabled if ADMIN_ENABLED: - _DJANGO_PWD_VALIDATION = "django.contrib.auth.password_validation" + _DJANGO_PWD_VALIDATION = "django.contrib.auth.password_validation" # noqa: E501, S105 (long line, possible password) AUTH_PASSWORD_VALIDATORS = [ {"NAME": _DJANGO_PWD_VALIDATION + ".UserAttributeSimilarityValidator"}, {"NAME": _DJANGO_PWD_VALIDATION + ".MinimumLengthValidator"}, diff --git a/privaterelay/utils.py b/privaterelay/utils.py index b4f5c0ef41..7f0fc69337 100644 --- a/privaterelay/utils.py +++ b/privaterelay/utils.py @@ -477,7 +477,7 @@ def flag_is_active_in_task(flag_name: str, user: AbstractBaseUser | None) -> boo # Removed - check for cookie setting for flag # Removed - check for read-only mode - if Decimal(str(random.uniform(0, 100))) <= flag.percent: + if Decimal(str(random.uniform(0, 100))) <= flag.percent: # noqa: S311 # Removed - setting the flag for future checks return True diff --git a/pyproject.toml b/pyproject.toml index 9e0bbba5e7..cc5ebef433 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,13 @@ testpaths = [ ] [tool.ruff.lint] +ignore = [ + # TODO MPP-3802: Enable more bandit security checks + "S101", # https://docs.astral.sh/ruff/rules/assert/ + "S113", # https://docs.astral.sh/ruff/rules/request-without-timeout/ +] select = [ + "S", # flake8-bandit "E", # pycodestyle errors "F", # pyflakes "I", # isort @@ -105,3 +111,8 @@ section-order = ["future", "standard-library", "django", "third-party", "first-p [tool.ruff.lint.per-file-ignores] # Ignore line length in generated file "privaterelay/glean/server_events.py" = ["E501"] +# S101: Allow assert in tests, since it is correct usage for pytest +# S105: Allow hardcoded passwords in tests +# S311: Allow pseudo-random generators in tests +"**/tests/*_tests.py" = ["S101", "S105", "S311"] +"**/tests/utils.py" = ["S101", "S311"]