diff --git a/.ds.baseline b/.ds.baseline index ac9497189..bf8b9631a 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -349,7 +349,7 @@ "filename": "tests/app/user/test_rest.py", "hashed_secret": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33", "is_verified": false, - "line_number": 962, + "line_number": 810, "is_secret": false } ], @@ -384,5 +384,5 @@ } ] }, - "generated_at": "2024-07-10T20:12:22Z" + "generated_at": "2024-07-22T21:27:35Z" } diff --git a/Makefile b/Makefile index 9cb8bdac9..175f630c4 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ run-celery: ## Run celery, TODO remove purge for staging/prod .PHONY: dead-code -dead-code: +dead-code: ## Use 60 to look for suspected dead code poetry run vulture ./app --min-confidence=100 .PHONY: run-celery-beat diff --git a/app/celery/nightly_tasks.py b/app/celery/nightly_tasks.py index dd063be64..4ff56d44b 100644 --- a/app/celery/nightly_tasks.py +++ b/app/celery/nightly_tasks.py @@ -51,7 +51,13 @@ def cleanup_unfinished_jobs(): for job in jobs: # The query already checks that the processing_finished time is null, so here we are saying # if it started more than 4 hours ago, that's too long - acceptable_finish_time = job.processing_started + timedelta(minutes=5) + try: + acceptable_finish_time = job.processing_started + timedelta(minutes=5) + except TypeError: + current_app.logger.error( + f"Job ID {job.id} processing_started is {job.processing_started}." + ) + raise if now > acceptable_finish_time: remove_csv_object(job.original_file_name) dao_archive_job(job) diff --git a/app/celery/provider_tasks.py b/app/celery/provider_tasks.py index b68ac9f44..b79902ced 100644 --- a/app/celery/provider_tasks.py +++ b/app/celery/provider_tasks.py @@ -23,7 +23,7 @@ from app.utils import utc_now # This is the amount of time to wait after sending an sms message before we check the aws logs and look for delivery # receipts -DELIVERY_RECEIPT_DELAY_IN_SECONDS = 120 +DELIVERY_RECEIPT_DELAY_IN_SECONDS = 30 @notify_celery.task( diff --git a/app/commands.py b/app/commands.py index b3732e638..c2a2d70e7 100644 --- a/app/commands.py +++ b/app/commands.py @@ -36,12 +36,14 @@ from app.dao.organization_dao import ( dao_get_organization_by_email_address, dao_get_organization_by_id, ) +from app.dao.service_sms_sender_dao import dao_get_sms_senders_by_service_id from app.dao.services_dao import ( dao_fetch_all_services_by_user, dao_fetch_all_services_created_by_user, dao_fetch_service_by_id, dao_update_service, delete_service_and_all_associated_db_objects, + get_services_by_partial_name, ) from app.dao.templates_dao import dao_get_template_by_id from app.dao.users_dao import ( @@ -600,6 +602,24 @@ def download_csv_file_by_name(csv_filename): print(s3.get_s3_file(bucket_name, csv_filename, access_key, secret, region)) +@notify_command(name="dump-sms-senders") +@click.argument("service_name") +def dump_sms_senders(service_name): + + # poetry run flask command dump-sms-senders MyServiceName + # cf run-task notify-api-production --command "flask command dump-sms-senders MyServiceName" + services = get_services_by_partial_name(service_name) + if len(services) > 1: + raise ValueError( + f"Please use a unique and complete service name instead of {service_name}" + ) + + senders = dao_get_sms_senders_by_service_id(services[0].id) + for sender in senders: + # Not PII, okay to put in logs + click.echo(sender.serialize()) + + @notify_command(name="populate-annual-billing-with-the-previous-years-allowance") @click.option( "-y", diff --git a/app/delivery/send_to_providers.py b/app/delivery/send_to_providers.py index c0d32bf86..4f811de22 100644 --- a/app/delivery/send_to_providers.py +++ b/app/delivery/send_to_providers.py @@ -10,6 +10,7 @@ from app.celery.test_key_tasks import send_email_response, send_sms_response from app.dao.email_branding_dao import dao_get_email_branding_by_id from app.dao.notifications_dao import dao_update_notification from app.dao.provider_details_dao import get_provider_details_by_notification_type +from app.dao.service_sms_sender_dao import dao_get_sms_senders_by_service_id from app.enums import BrandType, KeyType, NotificationStatus, NotificationType from app.exceptions import NotificationTechnicalFailureException from app.serialised_models import SerialisedService, SerialisedTemplate @@ -101,6 +102,13 @@ def send_sms_to_provider(notification): raise Exception( f"The recipient for (Service ID: {si}; Job ID: {ji}; Job Row Number {jrn} was not found." ) + + sender_numbers = get_sender_numbers(notification) + if notification.reply_to_text not in sender_numbers: + raise ValueError( + f"{notification.reply_to_text} not in {sender_numbers} #notify-admin-1701" + ) + send_sms_kwargs = { "to": recipient, "content": str(template), @@ -130,6 +138,14 @@ def send_sms_to_provider(notification): return message_id +def get_sender_numbers(notification): + possible_senders = dao_get_sms_senders_by_service_id(notification.service_id) + sender_numbers = [] + for possible_sender in possible_senders: + sender_numbers.append(possible_sender.sms_sender) + return sender_numbers + + def send_email_to_provider(notification): # Someone needs an email, possibly new registration recipient = redis_store.get(f"email-address-{notification.id}") diff --git a/app/errors.py b/app/errors.py index 6ed311dba..41c760da6 100644 --- a/app/errors.py +++ b/app/errors.py @@ -10,11 +10,6 @@ from app.exceptions import ArchiveValidationError from notifications_utils.recipients import InvalidEmailError -class VirusScanError(Exception): - def __init__(self, message): - super().__init__(message) - - class InvalidRequest(Exception): code = None fields = [] diff --git a/app/notifications/notifications_ses_callback.py b/app/notifications/notifications_ses_callback.py index 0b41c64db..2e0465a25 100644 --- a/app/notifications/notifications_ses_callback.py +++ b/app/notifications/notifications_ses_callback.py @@ -1,5 +1,3 @@ -from datetime import timedelta - from flask import Blueprint, jsonify, request from app.celery.process_ses_receipts_tasks import process_ses_results @@ -8,7 +6,6 @@ from app.errors import InvalidRequest from app.notifications.sns_handlers import sns_notification_handler ses_callback_blueprint = Blueprint("notifications_ses_callback", __name__) -DEFAULT_MAX_AGE = timedelta(days=10000) # 400 counts as a permanent failure so SNS will not retry. diff --git a/app/notifications/sns_handlers.py b/app/notifications/sns_handlers.py index 96ae4e6bf..6353b43f4 100644 --- a/app/notifications/sns_handlers.py +++ b/app/notifications/sns_handlers.py @@ -1,5 +1,4 @@ import enum -from datetime import timedelta from json import decoder import requests @@ -8,8 +7,6 @@ from flask import current_app, json from app.errors import InvalidRequest from app.notifications.sns_cert_validator import validate_sns_cert -DEFAULT_MAX_AGE = timedelta(days=10000) - class SNSMessageType(enum.Enum): SubscriptionConfirmation = "SubscriptionConfirmation" diff --git a/app/service/rest.py b/app/service/rest.py index 7d98cd91c..687cf5a23 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -201,6 +201,8 @@ def get_service_by_id(service_id): fetched = dao_fetch_service_by_id(service_id) data = service_schema.dump(fetched) + + current_app.logger.info(f'>> SERVICE: {data["id"]}; {data}') return jsonify(data=data) diff --git a/app/user/rest.py b/app/user/rest.py index 365cf66b6..52335dce1 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -30,7 +30,6 @@ from app.dao.users_dao import ( reset_failed_login_count, save_model_user, save_user_attribute, - update_user_password, use_user_code, ) from app.enums import CodeType, KeyType, NotificationType, TemplateType @@ -45,7 +44,6 @@ from app.schemas import ( create_user_schema, email_data_request_schema, partial_email_data_request_schema, - user_update_password_schema_load_json, user_update_schema_load_json, ) from app.user.users_schema import ( @@ -628,57 +626,6 @@ def get_all_users(): return jsonify(data=result), 200 -@user_blueprint.route("/reset-password", methods=["POST"]) -def send_user_reset_password(): - request_json = request.get_json() - email = email_data_request_schema.load(request_json) - - user_to_send_to = get_user_by_email(email["email"]) - template = dao_get_template_by_id(current_app.config["PASSWORD_RESET_TEMPLATE_ID"]) - service = Service.query.get(current_app.config["NOTIFY_SERVICE_ID"]) - personalisation = { - "user_name": user_to_send_to.name, - "url": _create_reset_password_url( - user_to_send_to.email_address, - base_url=request_json.get("admin_base_url"), - next_redirect=request_json.get("next"), - ), - } - saved_notification = persist_notification( - template_id=template.id, - template_version=template.version, - recipient=email["email"], - service=service, - personalisation=None, - notification_type=template.template_type, - api_key_id=None, - key_type=KeyType.NORMAL, - reply_to_text=service.get_default_reply_to_email_address(), - ) - saved_notification.personalisation = personalisation - - redis_store.set( - f"email-personalisation-{saved_notification.id}", - json.dumps(personalisation), - ex=60 * 60, - ) - send_notification_to_queue(saved_notification, queue=QueueNames.NOTIFY) - - return jsonify({}), 204 - - -@user_blueprint.route("//update-password", methods=["POST"]) -def update_password(user_id): - user = get_user_by_id(user_id=user_id) - req_json = request.get_json() - password = req_json.get("_password") - - user_update_password_schema_load_json.load(req_json) - - update_user_password(user, password) - return jsonify(data=user.serialize()), 200 - - @user_blueprint.route("/report-all-users", methods=["GET"]) def report_all_users(): users = dao_report_users() @@ -692,17 +639,6 @@ def get_organizations_and_services_for_user(user_id): return jsonify(data) -def _create_reset_password_url(email, next_redirect, base_url=None): - data = json.dumps({"email": email, "created_at": str(utc_now())}) - static_url_part = "/new-password/" - full_url = url_with_token( - data, static_url_part, current_app.config, base_url=base_url - ) - if next_redirect: - full_url += "?{}".format(urlencode({"next": next_redirect})) - return full_url - - def _create_verification_url(user, base_url): data = json.dumps({"user_id": str(user.id), "email": user.email_address}) url = "/verify-email/" diff --git a/app/utils.py b/app/utils.py index e286d6539..df2f4a3f9 100644 --- a/app/utils.py +++ b/app/utils.py @@ -60,11 +60,6 @@ def get_midnight_in_utc(date): return datetime.combine(date, datetime.min.time()) -def get_midnight_for_day_before(date): - day_before = date - timedelta(1) - return get_midnight_in_utc(day_before) - - def get_month_from_utc_column(column): """ Where queries need to count notifications by month it needs to be @@ -112,20 +107,6 @@ def get_dt_string_or_none(val): return val.strftime(DATETIME_FORMAT) if val else None -def get_uuid_string_or_none(val): - return str(val) if val else None - - -def format_sequential_number(sequential_number): - return format(sequential_number, "x").zfill(8) - - -def get_reference_from_personalisation(personalisation): - if personalisation: - return personalisation.get("reference") - return None - - # Function used for debugging. # Do print(hilite(message)) while debugging, then remove your print statements def hilite(message): diff --git a/deploy-config/production.yml b/deploy-config/production.yml index fc5be0451..2a7b7799d 100644 --- a/deploy-config/production.yml +++ b/deploy-config/production.yml @@ -1,7 +1,7 @@ env: production web_instances: 2 web_memory: 2G -worker_instances: 1 +worker_instances: 2 worker_memory: 2G scheduler_memory: 256M public_api_route: notify-api.app.cloud.gov diff --git a/docs/all.md b/docs/all.md index 5f9be2a30..140fa6ef3 100644 --- a/docs/all.md +++ b/docs/all.md @@ -438,23 +438,31 @@ Rules for use: If this is the first time you have used Terraform in this repository, you will first have to hook your copy of Terraform up to our remote state. Follow [Retrieving existing bucket credentials](https://github.com/GSA/notifications-api/tree/main/terraform#retrieving-existing-bucket-credentials). +:anchor: The Admin app depends upon the API app, so set up the API first. + 1. Set up services: - ``` + ```bash $ cd terraform/sandbox $ ../create_service_account.sh -s notify-sandbox -u -terraform -m > secrets.auto.tfvars $ terraform init $ terraform plan $ terraform apply ``` + Check [Terraform troubleshooting](https://github.com/GSA/notifications-api/tree/main/terraform#troubleshooting) if you encounter problems. 1. Change back to the project root directory: `cd ../..` -1. start a poetry shell as a shortcut to load `.env` file variables: `$ poetry shell` +1. Start a poetry shell as a shortcut to load `.env` file variables by running `poetry shell`. (You'll have to restart this any time you change the file.) 1. Output requirements.txt file: `poetry export --without-hashes --format=requirements.txt > requirements.txt` -1. Deploy the application: +1. Ensure you are using the correct CloudFoundry target + ```bash + cf target -o gsa-tts-benefits-studio -s notify-sandbox ``` +1. Deploy the application: + ```bash cf push --vars-file deploy-config/sandbox.yml --var NEW_RELIC_LICENSE_KEY=$NEW_RELIC_LICENSE_KEY ``` The real `push` command has more var arguments than the single one above. Get their values from a Notify team member. -1. Visit the URL of the app you just deployed + +1. Visit the URL(s) of the app you just deployed * Admin https://notify-sandbox.app.cloud.gov/ * API https://notify-api-sandbox.app.cloud.gov/ @@ -1392,7 +1400,24 @@ After pushing the Admin app, you might see this in the logs {"name": "app", "levelname": "ERROR", "message": "API unknown failed with status 503 message Request failed", "pathname": "/home/vcap/app/app/__init__.py", ... ``` -This indicates that the Admin and API apps are unable to talk to each other because of either a missing route or a missing network policy. The apps require [container-to-container networking](https://cloud.gov/docs/management/container-to-container/) to communicate. List `cf network-policies` and compare the output to our other deployed envs. If you find a policy is missing, you might have to create a network policy with something like: +And you would also see this in the Admin web UI + ``` +Sorry, we can't deliver what you asked for right now. +``` + +This indicates that the Admin and API apps are unable to talk to each other because of either a missing route or a missing network policy. The apps require [container-to-container networking](https://cloud.gov/docs/management/container-to-container/) to communicate. List `cf network-policies`; you should see one connecting API and Admin on port 61443. If not, you can create one manually: + +```bash cf add-network-policy notify-admin-sandbox notify-api-sandbox --protocol tcp --port 61443 ``` + +### Service instance not found + +This error encounted after `cf push` indicates you may be using the wrong CloudFoundry target + +``` +For application 'notify-api-sandbox': Service instance 'notify-api-rds-sandbox' not found +``` + +Run `cf target -o gsa-tts-benefits-studio -s notify-sandbox` before pushing to the Sandbox diff --git a/poetry.lock b/poetry.lock index 98b7a4343..33d0907b2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -204,17 +204,17 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "awscli" -version = "1.33.26" +version = "1.33.32" description = "Universal Command Line Environment for AWS." optional = false python-versions = ">=3.8" files = [ - {file = "awscli-1.33.26-py3-none-any.whl", hash = "sha256:e542f2ae56dc8ac0bb1774f835031e818af2e3a0e3fd133363d5932621e8e66c"}, - {file = "awscli-1.33.26.tar.gz", hash = "sha256:b3047b2b8ab6d4a8b5bb04e28e4531be8e72fc87e5fb8f2588196eca9dd11dd0"}, + {file = "awscli-1.33.32-py3-none-any.whl", hash = "sha256:94e0e348b4e98d949d01f3c9bba817ef9c0da4aabea53a5d966445618339c7dd"}, + {file = "awscli-1.33.32.tar.gz", hash = "sha256:90137622dcd7695939d64f57172901376d8ca5eb995c12e5fce50f55316c5b62"}, ] [package.dependencies] -botocore = "1.34.144" +botocore = "1.34.150" colorama = ">=0.2.5,<0.4.7" docutils = ">=0.10,<0.17" PyYAML = ">=3.10,<6.1" @@ -403,17 +403,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.144" +version = "1.34.150" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.144-py3-none-any.whl", hash = "sha256:b8433d481d50b68a0162c0379c0dd4aabfc3d1ad901800beb5b87815997511c1"}, - {file = "boto3-1.34.144.tar.gz", hash = "sha256:2f3e88b10b8fcc5f6100a9d74cd28230edc9d4fa226d99dd40a3ab38ac213673"}, + {file = "boto3-1.34.150-py3-none-any.whl", hash = "sha256:ad648c89a4935590a69341e5430fc42a021489a22de171ee3fd7bb204f9ef0fa"}, + {file = "boto3-1.34.150.tar.gz", hash = "sha256:894b222f7850b870a7ac63d7e378ac36c5c34375da24ddc30e131d9fafe369dc"}, ] [package.dependencies] -botocore = ">=1.34.144,<1.35.0" +botocore = ">=1.34.150,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -422,13 +422,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.144" +version = "1.34.150" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.144-py3-none-any.whl", hash = "sha256:a2cf26e1bf10d5917a2285e50257bc44e94a1d16574f282f3274f7a5d8d1f08b"}, - {file = "botocore-1.34.144.tar.gz", hash = "sha256:4215db28d25309d59c99507f1f77df9089e5bebbad35f6e19c7c44ec5383a3e8"}, + {file = "botocore-1.34.150-py3-none-any.whl", hash = "sha256:b988d47f4d502df85befce11a48002421e4e6ea4289997b5e0261bac5fa76ce6"}, + {file = "botocore-1.34.150.tar.gz", hash = "sha256:4d23387e0f076d87b637a2a35c0ff2b8daca16eace36b63ce27f65630c6b375a"}, ] [package.dependencies] @@ -939,43 +939,38 @@ files = [ [[package]] name = "cryptography" -version = "42.0.8" +version = "43.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, - {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, - {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, - {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, - {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, - {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, - {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, - {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, + {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, + {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, + {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, + {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, + {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, + {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, + {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, ] [package.dependencies] @@ -988,7 +983,7 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] @@ -2657,12 +2652,12 @@ files = [ [[package]] name = "notifications-python-client" -version = "9.1.0" +version = "10.0.0" description = "Python API client for GOV.UK Notify." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "notifications_python_client-9.1.0-py3-none-any.whl", hash = "sha256:43b8f738dfa81ae3aa656d91e361dbc51316194f8b9a430706b03347fa7a01bc"}, + {file = "notifications_python_client-10.0.0-py3-none-any.whl", hash = "sha256:0f152a4d23b7f7b827dae6b45ac63568a2bc56044b1aa57b66bc68b137c40e57"}, ] [package.dependencies] @@ -3038,13 +3033,13 @@ files = [ [[package]] name = "pre-commit" -version = "3.7.1" +version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, - {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, ] [package.dependencies] @@ -3296,20 +3291,20 @@ files = [ [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -3661,90 +3656,90 @@ rpds-py = ">=0.7.0" [[package]] name = "regex" -version = "2024.5.15" +version = "2024.7.24" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" files = [ - {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"}, - {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"}, - {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"}, - {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"}, - {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"}, - {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"}, - {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"}, - {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"}, - {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"}, - {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"}, - {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"}, - {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"}, - {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"}, - {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, + {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, + {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, + {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, + {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, + {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, + {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, + {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, + {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, + {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, + {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, + {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, ] [[package]] @@ -4017,18 +4012,19 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "70.3.0" +version = "72.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, - {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, + {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, + {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, ] [package.extras] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shapely" @@ -4743,4 +4739,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "ed5c093f930c1b1efbb3a8ec8e3d4b44932bee3b1ae5fad237912cc97857b05d" +content-hash = "4ef9a352a137fe091b1084b06959b9b674c30635771abc23010d2ba560f6bfd6" diff --git a/pyproject.toml b/pyproject.toml index 70285e3b6..3bfff266b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,8 +11,8 @@ python = "^3.12.2" alembic = "==1.13.2" amqp = "==5.2.0" beautifulsoup4 = "==4.12.3" -boto3 = "^1.34.144" -botocore = "^1.34.144" +boto3 = "^1.34.150" +botocore = "^1.34.150" cachetools = "==5.4.0" celery = {version = "==5.4.0", extras = ["redis"]} certifi = ">=2022.12.7" @@ -39,7 +39,7 @@ lxml = "==5.2.2" marshmallow = "==3.21.3" marshmallow-sqlalchemy = "==1.0.0" newrelic = "*" -notifications-python-client = "==9.1.0" +notifications-python-client = "==10.0.0" oscrypto = "==1.3.0" packaging = "==24.1" poetry-dotenv-plugin = "==0.2.0" @@ -58,12 +58,12 @@ ordered-set = "^4.1.0" phonenumbers = "^8.13.40" python-json-logger = "^2.0.7" pytz = "^2024.1" -regex = "^2024.5.15" +regex = "^2024.7.24" shapely = "^2.0.5" smartypants = "^2.0.1" mistune = "0.8.4" blinker = "^1.8.2" -cryptography = "^42.0.8" +cryptography = "^43.0.0" idna = "^3.7" jmespath = "^1.0.1" markupsafe = "^2.1.5" @@ -81,7 +81,7 @@ requests = "^2.32.3" [tool.poetry.group.dev.dependencies] -awscli = "^1.29.74" +awscli = "^1.33.32" bandit = "*" black = "^24.3.0" cloudfoundry-client = "*" @@ -94,15 +94,15 @@ isort = "^5.13.2" jinja2-cli = {version = "==0.8.2", extras = ["yaml"]} moto = "==5.0.11" pip-audit = "*" -pre-commit = "^3.7.1" -pytest = "^8.2.2" +pre-commit = "^3.8.0" +pytest = "^8.3.2" pytest-env = "^1.1.3" pytest-mock = "^3.14.0" pytest-cov = "^5.0.0" pytest-xdist = "^3.5.0" radon = "^6.0.1" requests-mock = "^1.11.0" -setuptools = "^70.3.0" +setuptools = "^72.1.0" sqlalchemy-utils = "^0.41.2" vulture = "^2.10" detect-secrets = "^1.5.0" diff --git a/runtime.txt b/runtime.txt index 4ddc7cd66..64f28603a 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.12.3 +python-3.12.x diff --git a/terraform/README.md b/terraform/README.md index bbb63424a..24e9a55f3 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -1,12 +1,17 @@ # Terraform +illustration of space exploration + This directory holds the Terraform modules for maintaining Notify.gov's API infrastructure. You might want to: * [Set up](#retrieving-existing-bucket-credentials) the Sandbox and develop Terraform, +* [Maintain](#maintenance) software versions or CI/CD, * [Learn](#structure) about the directory structure, or * [Troubleshoot](#troubleshooting) error messages The Admin app repo [has its own terraform directory](https://github.com/GSA/notifications-admin/tree/main/terraform) but a lot of the below instructions apply to both apps. +:tv: [Video introduction](https://drive.google.com/file/d/13SR3M8IowYBa4Wp_YEcuAURZ74EcCYoc/) to Notify infrastructure + ## Retrieving existing bucket credentials :green_book: New developers start here! @@ -31,52 +36,6 @@ Assuming [initial setup](#initial-setup) is complete — which it should be These credentials will allow Terraform to access the AWS/Cloud.gov bucket in which developers share Terraform state files. Now you are ready to develop Terraform using the [Workflow for deployed environments](#workflow-for-deployed-environments). -## Initial setup - -These instructions were used for deploying the project for the first time, years ago. We should not have to perform these steps again. They are provided here for reference. - -1. Manually run the bootstrap module following instructions under [Terraform State Credentials](#terraform-state-credentials) -1. Setup CI/CD Pipeline to run Terraform - 1. Copy bootstrap credentials to your CI/CD secrets using the instructions in the base README - 1. Create a cloud.gov SpaceDeployer by following the instructions under [SpaceDeployers](#spacedeployers) - 1. Copy SpaceDeployer credentials to your CI/CD secrets using the instructions in the base README -1. Manually Running Terraform - 1. Follow instructions under [Workflow for deployed environments](#workflow-for-deployed-environments) to create your infrastructure - -### Terraform state credentials - -The bootstrap module is used to create an s3 bucket for later terraform runs to store their state in. (If the bucket is already created, you should [Use bootstrap credentials](#use-bootstrap-credentials)) - -#### Bootstrapping the state storage s3 buckets for the first time - -1. Within the `bootstrap` directory, run `terraform init` -1. Run `./run.sh plan` to verify that the changes are what you expect -1. Run `./run.sh apply` to set up the bucket -1. Follow instructions under [Use bootstrap credentials](#use-bootstrap-credentials) -1. Ensure that `import.sh` includes a line and correct IDs for any resources created -1. Run `./teardown_creds.sh` to remove the space deployer account used to create the s3 bucket -1. Copy `bucket` from `bucket_credentials` output to the backend block of `staging/providers.tf` and `production/providers.tf` - -#### To make changes to the bootstrap module - -*This should not be necessary in most cases* - -1. Run `terraform init` -1. If you don't have terraform state locally: - 1. run `./import.sh` - 1. optionally run `./run.sh apply` to include the existing outputs in the state file -1. Make your changes -1. Continue from step 2 of the boostrapping instructions - -## SpaceDeployers - -A [SpaceDeployer](https://cloud.gov/docs/services/cloud-gov-service-account/) account is required to run terraform or -deploy the application from the CI/CD pipeline. Create a new account by running: - -`./create_service_account.sh -s -u ` - -SpaceDeployers are also needed to run Terraform locally — they fill user and password input variables (via `deployers` within `main.tf`) that some of our Terraform modules require when they start running. Using a SpaceDeployer account locally is covered in [the next section](#workflow-for-deployed-environments). - ## Workflow for deployed environments These are the steps for developing Terraform code for our deployed environment modules (`sandbox`, `demo`, `staging` and `production`) locally on your laptop. Or for setting up a new deployment environment, or otherwise for running Terraform manually in any module that uses remote state. You don't need to do all this to run code in the `development` module, because it is not a deployed environment and it does not use remote state. @@ -119,7 +78,7 @@ These steps assume shared [Terraform state credentials](#terraform-state-credent This will show you any pending changes that Terraform is ready to make. - :pencil: Now is the time to write any HCL code you are planning to write, re-running `terraform plan` to confirm that the code works as you develop. Keep in mind that any changes to the codebase that you commit will be run by the CI/CD pipeline. + :pencil: Now is the time to write any HCL code (aka Terraform code) you are planning to write, re-running `terraform plan` to confirm that the code works as you develop. Keep in mind that any changes to the codebase that you commit will be run by the CI/CD pipeline. 1. **Only if it is safe to do so**, apply your changes. @@ -146,6 +105,30 @@ These steps assume shared [Terraform state credentials](#terraform-state-credent Optionally, you can also `rm secrets.auto.tfvars` +## Maintenance + +### Version upgrade checklist + +These version numbers are hardcoded in Terraform or shell scripts. We should periodically check them for upgrades. + +* Cloud Foundry Terraform plugin in every module in the API and Admin apps, [here for example](sandbox/providers.tf#L6). +* The [terraform-cloudgov module](https://github.com/GSA-TTS/terraform-cloudgov/), the version of which is referred to serveral times in most modules, [here for example](sandbox/main.tf#L16). +* Cloud Service Broker (CSB) version in [the SMS](https://github.com/GSA/usnotify-ssb/blob/main/app-setup-sms.sh) and [the SMTP](https://github.com/GSA/usnotify-ssb/blob/main/app-setup-smtp.sh) download scripts of the usnotify-ssb repo. +* SMS and SMTP brokerpak versions, also in the download scripts of the usnotify-ssb repo. (And we may have to help maintain the [SMTP brokerpak project](https://github.com/GSA-TTS/datagov-brokerpak-smtp) itself.) +* The version of Redis used in deployed environment modules, [here for example](sandbox/main.tf#L33). To upgrade, the resource must be destroyed and replaced. The versions supported are limited by Cloud.gov. +* A required minimum version of Terraform is noted in every providers.tf file, [here for example](sandbox/providers.tf#L2). It would be best to keep it in sync with the version used by our CI/CD deployment pipeline. But, it does not need to be updated with every new Terraform release. + +:tv: Some of these version upgrades are discussed in our [video introduction](https://drive.google.com/file/d/13SR3M8IowYBa4Wp_YEcuAURZ74EcCYoc/). + +### SpaceDeployers + +A [SpaceDeployer](https://cloud.gov/docs/services/cloud-gov-service-account/) account is required to run terraform or +deploy the application from the CI/CD pipeline. During CI/CD maintenance you might need to create a new account: + +`./create_service_account.sh -s -u ` + +SpaceDeployers are also needed to run Terraform locally — they fill user and password input variables (via `deployers` within `main.tf`) that some of our Terraform modules require when they start running. Using a SpaceDeployer account locally is covered in [Workflow for deployed environments](#workflow-for-deployed-environments). + ## Structure The `terraform` directory contains sub-directories (`staging`, `production`, etc.) named for deployment environments. Each of these is a *module*, which is just Terraform's word for a directory with some .tf files in it. Each module governs the infrastructure of the environment for which it is named. This directory structure forms "[bulkheads](https://blog.gruntwork.io/how-to-manage-terraform-state-28f5697e68fa)" which isolate Terraform commands to a single environment, limiting accidental damage. diff --git a/terraform/development/main.tf b/terraform/development/main.tf index 0d8920c6f..91719db81 100644 --- a/terraform/development/main.tf +++ b/terraform/development/main.tf @@ -1,7 +1,6 @@ locals { cf_org_name = "gsa-tts-benefits-studio" cf_space_name = "notify-local-dev" - recursive_delete = true key_name = "${var.username}-api-dev-key" } diff --git a/terraform/production/main.tf b/terraform/production/main.tf index 45cf7a5b8..e3bd10a26 100644 --- a/terraform/production/main.tf +++ b/terraform/production/main.tf @@ -74,23 +74,3 @@ module "sns_sms" { aws_region = "us-gov-west-1" monthly_spend_limit = 1000 } - -########################################################################### -# The following lines need to be commented out for the initial `terraform apply` -# It can be re-enabled after: -# TODO: decide on public API domain name -# 1) the app has first been deployed -# 2) the route has been manually created by an OrgManager: -# `cf create-domain gsa-tts-benefits-studio api.notify.gov` -########################################################################### -# module "domain" { -# source = "github.com/18f/terraform-cloudgov//domain?ref=v0.7.1" -# -# cf_org_name = local.cf_org_name -# cf_space_name = local.cf_space_name -# app_name_or_id = "${local.app_name}-${local.env}" -# name = "${local.app_name}-domain-${local.env}" -# recursive_delete = local.recursive_delete -# cdn_plan_name = "domain" -# domain_name = "api.notify.gov" -# } diff --git a/tests/app/delivery/test_send_to_providers.py b/tests/app/delivery/test_send_to_providers.py index af65dd766..7f16a799a 100644 --- a/tests/app/delivery/test_send_to_providers.py +++ b/tests/app/delivery/test_send_to_providers.py @@ -318,6 +318,9 @@ def test_should_send_sms_with_downgraded_content(notify_db_session, mocker): # é, o, and u are in GSM. # ī, grapes, tabs, zero width space and ellipsis are not # ó isn't in GSM, but it is in the welsh alphabet so will still be sent + mocker.patch( + "app.delivery.send_to_providers.get_sender_numbers", return_value=["testing"] + ) msg = "a é ī o u 🍇 foo\tbar\u200bbaz((misc))…" placeholder = "∆∆∆abc" gsm_message = "?ódz Housing Service: a é i o u ? foo barbaz???abc..." @@ -327,6 +330,7 @@ def test_should_send_sms_with_downgraded_content(notify_db_session, mocker): template=template, ) db_notification.personalisation = {"misc": placeholder} + db_notification.reply_to_text = 'testing' mocker.patch("app.aws_sns_client.send_sms") @@ -609,11 +613,16 @@ def __update_notification(notification_to_update, research_mode, expected_status def test_should_update_billable_units_and_status_according_to_research_mode_and_key_type( sample_template, mocker, research_mode, key_type, billable_units, expected_status ): + + mocker.patch( + "app.delivery.send_to_providers.get_sender_numbers", return_value=["testing"] + ) notification = create_notification( template=sample_template, billable_units=0, status=NotificationStatus.CREATED, key_type=key_type, + reply_to_text='testing', ) mocker.patch("app.aws_sns_client.send_sms") mocker.patch( @@ -771,9 +780,13 @@ def test_send_email_to_provider_uses_reply_to_from_notification( def test_send_sms_to_provider_should_use_normalised_to(mocker, client, sample_template): + + mocker.patch( + "app.delivery.send_to_providers.get_sender_numbers", return_value=["testing"] + ) send_mock = mocker.patch("app.aws_sns_client.send_sms") notification = create_notification( - template=sample_template, to_field="+12028675309", normalised_to="2028675309" + template=sample_template, to_field="+12028675309", normalised_to="2028675309", reply_to_text='testing' ) mock_s3 = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3") @@ -826,6 +839,10 @@ def test_send_email_to_provider_should_user_normalised_to( def test_send_sms_to_provider_should_return_template_if_found_in_redis( mocker, client, sample_template ): + + mocker.patch( + "app.delivery.send_to_providers.get_sender_numbers", return_value=["testing"] + ) from app.schemas import service_schema, template_schema service_dict = service_schema.dump(sample_template.service) @@ -845,7 +862,7 @@ def test_send_sms_to_provider_should_return_template_if_found_in_redis( send_mock = mocker.patch("app.aws_sns_client.send_sms") notification = create_notification( - template=sample_template, to_field="+447700900855", normalised_to="447700900855" + template=sample_template, to_field="+447700900855", normalised_to="447700900855", reply_to_text='testing' ) mock_s3 = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3") diff --git a/tests/app/test_errors.py b/tests/app/test_errors.py deleted file mode 100644 index 2cdbc0b33..000000000 --- a/tests/app/test_errors.py +++ /dev/null @@ -1,6 +0,0 @@ -from app.errors import VirusScanError - - -def test_virus_scan_error(): - vse = VirusScanError("a message") - assert "a message" in vse.args diff --git a/tests/app/test_utils.py b/tests/app/test_utils.py index b39d3a2c0..89c66cfc2 100644 --- a/tests/app/test_utils.py +++ b/tests/app/test_utils.py @@ -1,4 +1,3 @@ -import uuid from datetime import date, datetime import pytest @@ -6,12 +5,8 @@ from freezegun import freeze_time from app.enums import ServicePermissionType from app.utils import ( - format_sequential_number, - get_midnight_for_day_before, get_midnight_in_utc, get_public_notify_type_text, - get_reference_from_personalisation, - get_uuid_string_or_none, midnight_n_days_ago, ) @@ -31,18 +26,6 @@ def test_get_midnight_in_utc_returns_expected_date(date, expected_date): assert get_midnight_in_utc(date) == expected_date -@pytest.mark.parametrize( - "date, expected_date", - [ - (datetime(2016, 1, 15, 0, 30), datetime(2016, 1, 14, 0, 0)), - (datetime(2016, 7, 15, 0, 0), datetime(2016, 7, 14, 0, 0)), - (datetime(2016, 8, 23, 11, 59), datetime(2016, 8, 22, 0, 0)), - ], -) -def test_get_midnight_for_day_before_returns_expected_date(date, expected_date): - assert get_midnight_for_day_before(date) == expected_date - - @pytest.mark.parametrize( "current_time, arg, expected_datetime", [ @@ -65,40 +48,7 @@ def test_midnight_n_days_ago(current_time, arg, expected_datetime): assert midnight_n_days_ago(arg) == expected_datetime -def test_format_sequential_number(): - assert format_sequential_number(123) == "0000007b" - - -@pytest.mark.parametrize( - "personalisation, expected_response", - [ - ({"nothing": "interesting"}, None), - ({"reference": "something"}, "something"), - (None, None), - ], -) -def test_get_reference_from_personalisation(personalisation, expected_response): - assert get_reference_from_personalisation(personalisation) == expected_response - - -def test_get_uuid_string_or_none(): - my_uuid = uuid.uuid4() - assert str(my_uuid) == get_uuid_string_or_none(my_uuid) - - assert get_uuid_string_or_none(None) is None - - def test_get_public_notify_type_text(): assert ( get_public_notify_type_text(ServicePermissionType.UPLOAD_DOCUMENT) == "document" ) - - -# This method is used for simulating bulk sends. We use localstack and run on a developer's machine to do the -# simulation. Please see docs->bulk_testing.md for instructions. -# def test_generate_csv_for_bulk_testing(): -# f = open("bulktest_100000.csv", "w") -# f.write("phone number\n") -# for _ in range(0, 100000): -# f.write("16615555555\n") -# f.close() diff --git a/tests/app/user/test_rest.py b/tests/app/user/test_rest.py index dc7b78e82..07dcc3570 100644 --- a/tests/app/user/test_rest.py +++ b/tests/app/user/test_rest.py @@ -632,137 +632,6 @@ def test_remove_user_folder_permissions(admin_request, sample_user, sample_servi assert service_user.folders == [] -@freeze_time("2016-01-01 11:09:00.061258") -def test_send_user_reset_password_should_send_reset_password_link( - admin_request, sample_user, mocker, password_reset_email_template -): - mocked = mocker.patch("app.celery.provider_tasks.deliver_email.apply_async") - data = {"email": sample_user.email_address} - notify_service = password_reset_email_template.service - - admin_request.post( - "user.send_user_reset_password", - _data=data, - _expected_status=204, - ) - - notification = Notification.query.first() - mocked.assert_called_once_with( - [str(notification.id)], queue="notify-internal-tasks" - ) - assert ( - notification.reply_to_text - == notify_service.get_default_reply_to_email_address() - ) - - -@pytest.mark.parametrize( - "data, expected_url", - ( - ( - { - "email": "notify@digital.fake.gov", - }, - ("http://localhost:6012/new-password/"), - ), - ( - { - "email": "notify@digital.fake.gov", - "admin_base_url": "https://different.example.com", - }, - ("https://different.example.com/new-password/"), - ), - ), -) -@freeze_time("2016-01-01 11:09:00.061258") -def test_send_user_reset_password_should_use_provided_base_url( - admin_request, - sample_user, - password_reset_email_template, - mocker, - data, - expected_url, -): - mocker.patch("app.celery.provider_tasks.deliver_email.apply_async") - - admin_request.post( - "user.send_user_reset_password", - _data=data, - _expected_status=204, - ) - - assert Notification.query.first().personalisation["url"].startswith(expected_url) - - -@freeze_time("2016-01-01 11:09:00.061258") -def test_send_user_reset_password_reset_password_link_contains_redirect_link_if_present_in_request( - admin_request, sample_user, mocker, password_reset_email_template -): - mocked = mocker.patch("app.celery.provider_tasks.deliver_email.apply_async") - data = {"email": sample_user.email_address, "next": "blob"} - - admin_request.post( - "user.send_user_reset_password", - _data=data, - _expected_status=204, - ) - - notification = Notification.query.first() - assert "?next=blob" in notification.content - mocked.assert_called_once_with( - [str(notification.id)], queue="notify-internal-tasks" - ) - - -def test_send_user_reset_password_should_return_400_when_email_is_missing( - admin_request, mocker -): - mocked = mocker.patch("app.celery.provider_tasks.deliver_email.apply_async") - data = {} - - json_resp = admin_request.post( - "user.send_user_reset_password", - _data=data, - _expected_status=400, - ) - assert json_resp["message"] == {"email": ["Missing data for required field."]} - assert mocked.call_count == 0 - - -def test_send_user_reset_password_should_return_400_when_user_doesnot_exist( - admin_request, mocker -): - mocked = mocker.patch("app.celery.provider_tasks.deliver_email.apply_async") - bad_email_address = "bad@email.gov.uk" - data = {"email": bad_email_address} - - json_resp = admin_request.post( - "user.send_user_reset_password", - _data=data, - _expected_status=404, - ) - - assert json_resp["message"] == "No result found" - assert mocked.call_count == 0 - - -def test_send_user_reset_password_should_return_400_when_data_is_not_email_address( - admin_request, mocker -): - mocked = mocker.patch("app.celery.provider_tasks.deliver_email.apply_async") - bad_email_address = "bad.email.gov.uk" - data = {"email": bad_email_address} - - json_resp = admin_request.post( - "user.send_user_reset_password", - _data=data, - _expected_status=400, - ) - - assert json_resp["message"] == {"email": ["Not a valid email address"]} - assert mocked.call_count == 0 - - def test_send_already_registered_email( admin_request, sample_user, already_registered_template, mocker ): @@ -842,27 +711,6 @@ def test_send_user_confirm_new_email_returns_400_when_email_missing( mocked.assert_not_called() -@freeze_time("2020-02-14T12:00:00") -def test_update_user_password_saves_correctly(admin_request, sample_service): - sample_user = sample_service.users[0] - new_password = "1234567890" - data = {"_password": "1234567890"} - - json_resp = admin_request.post( - "user.update_password", user_id=str(sample_user.id), _data=data - ) - - assert json_resp["data"]["password_changed_at"] is not None - data = {"password": new_password} - - admin_request.post( - "user.verify_user_password", - user_id=str(sample_user.id), - _data=data, - _expected_status=204, - ) - - def test_activate_user(admin_request, sample_user): sample_user.state = "pending"