From a12bfe88aeda28e80f2dea331a1386252a7ccfc9 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Tue, 23 Sep 2025 07:15:18 -0700 Subject: [PATCH 1/4] fix tests --- tests/app/main/views/test_templates.py | 32 ++++++++++++-------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/tests/app/main/views/test_templates.py b/tests/app/main/views/test_templates.py index 488736a95..67f58d8c9 100644 --- a/tests/app/main/views/test_templates.py +++ b/tests/app/main/views/test_templates.py @@ -830,6 +830,7 @@ def test_choose_a_template_to_copy_from_folder_within_service( "sms", "Should appear in list (at same level)", parent=PARENT_FOLDER_ID, + template_id=TEMPLATE_ONE_ID, ), _template( "sms", @@ -878,23 +879,20 @@ def test_choose_a_template_to_copy_from_folder_within_service( service_id=SERVICE_ONE_ID, from_folder=FOLDER_TWO_ID, ) - # assert links[1]["href"] == url_for( - # "main.choose_template_to_copy", - # service_id=SERVICE_ONE_ID, - # from_service=SERVICE_ONE_ID, - # from_folder=PARENT_FOLDER_ID, - # ) - # assert links[2]["href"] == url_for( - # "main.choose_template_to_copy", - # service_id=SERVICE_ONE_ID, - # from_folder=FOLDER_TWO_ID, - # ) - # assert links[3]["href"] == url_for( - # "main.copy_template", - # service_id=SERVICE_ONE_ID, - # template_id=TEMPLATE_ONE_ID, - # from_service=SERVICE_ONE_ID, - # ) + + assert links[1]["href"] == url_for( + "main.copy_template", + service_id=SERVICE_ONE_ID, + template_id=TEMPLATE_ONE_ID, + from_service=SERVICE_ONE_ID, + ) + + assert links[2]["href"] == url_for( + "main.copy_template", + service_id=SERVICE_ONE_ID, + template_id=TEMPLATE_ONE_ID, + from_service=SERVICE_ONE_ID, + ) @pytest.mark.parametrize( From a40c8861bf74feec4ef78ab1c2d69a99c953887c Mon Sep 17 00:00:00 2001 From: Alex Janousek Date: Fri, 26 Sep 2025 06:57:18 -0400 Subject: [PATCH 2/4] Optimizing polling (#2946) * Optimizing polling * Fixed formatting issue --- .ds.baseline | 4 +- .gitignore | 1 + app/assets/javascripts/job-status-polling.js | 186 ++++++++++++++++++ app/assets/javascripts/socketio.js | 132 ------------- app/config.py | 4 +- app/main/views/jobs.py | 46 ++++- app/main/views/organizations.py | 11 +- app/main/views/templates.py | 12 +- app/templates/views/jobs/job.html | 5 +- app/utils/login.py | 2 +- gulpfile.js | 2 +- .../views/test_job_notification_updates.py | 141 +++++++++++++ tests/app/main/views/test_jobs.py | 140 +++++++++++++ tests/app/main/views/test_template_folders.py | 4 +- tests/app/test_navigation.py | 1 + 15 files changed, 528 insertions(+), 163 deletions(-) create mode 100644 app/assets/javascripts/job-status-polling.js delete mode 100644 app/assets/javascripts/socketio.js create mode 100644 tests/app/main/views/test_job_notification_updates.py diff --git a/.ds.baseline b/.ds.baseline index 5afd27402..78a916432 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -161,7 +161,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 124, + "line_number": 122, "is_secret": false } ], @@ -634,5 +634,5 @@ } ] }, - "generated_at": "2025-09-24T22:03:22Z" + "generated_at": "2025-09-25T20:25:57Z" } diff --git a/.gitignore b/.gitignore index 09973207f..ba86027ad 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ coverage/ coverage.xml test_results.xml *,cover +.hypothesis/ # Translations *.mo diff --git a/app/assets/javascripts/job-status-polling.js b/app/assets/javascripts/job-status-polling.js new file mode 100644 index 000000000..47858a040 --- /dev/null +++ b/app/assets/javascripts/job-status-polling.js @@ -0,0 +1,186 @@ +document.addEventListener('DOMContentLoaded', function () { + // Verify we are on the job page + const isJobPage = window.location.pathname.includes('/jobs/'); + if (!isJobPage) return; + + // Check if polling elements exist + const hasPollingElements = document.querySelector('[data-key="counts"]'); + if (!hasPollingElements) return; + + // Extract job info from URL path: /services/{serviceId}/jobs/{jobId} + const pathParts = window.location.pathname.split('/'); + if (pathParts.length < 5 || pathParts[1] !== 'services' || pathParts[3] !== 'jobs') return; + + const serviceId = pathParts[2]; + const jobId = pathParts[4]; + + // Validate service and job IDs to prevent path injection + function isValidUuid(id) { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(id); + } + + // Validate both IDs are UUIDs to prevent path injection attacks + if (!isValidUuid(serviceId) || !isValidUuid(jobId)) { + console.warn('Invalid service or job ID format detected'); + return; + } + + const DEFAULT_INTERVAL_MS = 10000; + const MIN_INTERVAL_MS = 1000; + const MAX_INTERVAL_MS = 30000; + + let pollInterval; + let currentInterval = DEFAULT_INTERVAL_MS; + let isPolling = false; + let lastProcessedCount = 0; + + function calculateBackoff(responseTime) { + return Math.min( + MAX_INTERVAL_MS, + Math.max( + MIN_INTERVAL_MS, + Math.floor((250 * Math.sqrt(responseTime)) - 1000) + ) + ); + } + + async function updateNotifications() { + const notificationsUrl = `/services/${serviceId}/jobs/${jobId}.json`; + + try { + const response = await fetch(notificationsUrl); + if (!response.ok) { + throw new Error(`Failed to fetch notifications: ${response.status}`); + } + + const data = await response.json(); + + // Update notifications container if it exists + const notificationsContainer = document.querySelector('[data-key="notifications"]'); + if (notificationsContainer && data.notifications) { + notificationsContainer.innerHTML = data.notifications; + } + } catch (error) { + console.warn('Failed to update notifications:', error.message); + } + } + + async function updateAllJobSections(retryCount = 0) { + if (isPolling || document.hidden) { + return; + } + + isPolling = true; + + const pollStatusUrl = `/services/${serviceId}/jobs/${jobId}/status.json`; + + try { + const response = await fetch(pollStatusUrl); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + const countsContainer = document.querySelector('[data-key="counts"]'); + if (countsContainer) { + // Get all big-number elements in order: total, pending, delivered, failed + const countElements = countsContainer.querySelectorAll('.big-number-number'); + + if (countElements.length >= 4) { + if (data.total_count !== undefined) { + countElements[0].textContent = data.total_count.toLocaleString(); + } + + if (data.pending_count !== undefined) { + countElements[1].textContent = data.pending_count.toLocaleString(); + } + + if (data.sent_count !== undefined) { + countElements[2].textContent = data.sent_count.toLocaleString(); + } + + if (data.failed_count !== undefined) { + countElements[3].textContent = data.failed_count.toLocaleString(); + } + } + } + + currentInterval = calculateBackoff(DEFAULT_INTERVAL_MS); + + // Calculate how many messages have been processed + const processedCount = (data.sent_count || 0) + (data.failed_count || 0); + + // Update notifications conditionally: + // 1. If we have new messages and still under 50 total + // 2. Always when job is finished + if (processedCount > lastProcessedCount && processedCount <= 50 && !data.finished) { + // Update notifications for first 50 messages to show early results + await updateNotifications(); + lastProcessedCount = processedCount; + } + + if (data.finished === true) { + await updateNotifications(); + stopPolling(); + } + + } catch (error) { + if (retryCount < 3) { + console.debug(`Job polling retry ${retryCount}`, error.message); + isPolling = false; + + const retryDelay = Math.pow(2, retryCount) * 1000; + setTimeout(() => { + updateAllJobSections(retryCount + 1); + }, retryDelay); + return; + } + + console.warn('Job polling failed after 3 retries:', { + error: error.message, + url: pollStatusUrl, + jobId: jobId, + timestamp: new Date().toISOString() + }); + currentInterval = Math.min(currentInterval * 2, MAX_INTERVAL_MS); + } finally { + isPolling = false; + } + } + + function startPolling() { + updateAllJobSections(); + + function scheduleNext() { + if (pollInterval) clearTimeout(pollInterval); + pollInterval = setTimeout(() => { + updateAllJobSections(); + scheduleNext(); + }, currentInterval); + } + + scheduleNext(); + } + + function stopPolling() { + if (pollInterval) { + clearTimeout(pollInterval); + pollInterval = null; + } + } + + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + stopPolling(); + } else { + startPolling(); + } + }); + + window.addEventListener('beforeunload', stopPolling); + + startPolling(); +}); diff --git a/app/assets/javascripts/socketio.js b/app/assets/javascripts/socketio.js deleted file mode 100644 index 77d6a1028..000000000 --- a/app/assets/javascripts/socketio.js +++ /dev/null @@ -1,132 +0,0 @@ -document.addEventListener('DOMContentLoaded', function () { - const isJobPage = window.location.pathname.includes('/jobs/'); - if (!isJobPage) return; - - const jobEl = document.querySelector('[data-job-id]'); - const jobId = jobEl?.dataset?.jobId; - const featureEnabled = jobEl?.dataset?.feature === 'true'; - const apiHost = jobEl?.dataset?.host; - - if (!jobId || !featureEnabled) return; - - const DEFAULT_INTERVAL_MS = 10000; - const MIN_INTERVAL_MS = 1000; - const MAX_INTERVAL_MS = 30000; - - let pollInterval; - let currentInterval = DEFAULT_INTERVAL_MS; - let isPolling = false; - - function calculateBackoff(responseTime) { - return Math.min( - MAX_INTERVAL_MS, - Math.max( - MIN_INTERVAL_MS, - Math.floor((250 * Math.sqrt(responseTime)) - 1000) - ) - ); - } - - async function updateAllJobSections(retryCount = 0) { - if (isPolling || document.hidden) { - return; - } - - isPolling = true; - const startTime = Date.now(); - - const resourceEl = document.querySelector('[data-socket-update="status"]'); - const url = resourceEl?.dataset?.resource; - - if (!url) { - isPolling = false; - return; - } - - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - const data = await response.json(); - - const sections = { - status: document.querySelector('[data-socket-update="status"]'), - counts: document.querySelector('[data-socket-update="counts"]'), - notifications: document.querySelector('[data-socket-update="notifications"]'), - }; - - if (data.status && sections.status) { - sections.status.innerHTML = data.status; - } - if (data.counts && sections.counts) { - sections.counts.innerHTML = data.counts; - } - if (data.notifications && sections.notifications) { - sections.notifications.innerHTML = data.notifications; - } - - const responseTime = Date.now() - startTime; - currentInterval = calculateBackoff(responseTime); - - if (data.finished === true) { - stopPolling(); - } - - } catch (error) { - if (retryCount < 3) { - console.debug(`Job polling retry ${retryCount}`, error.message); - isPolling = false; - - const retryDelay = Math.pow(2, retryCount) * 1000; - setTimeout(() => { - updateAllJobSections(retryCount + 1); - }, retryDelay); - return; - } - - console.warn('Job polling failed after 3 retries:', { - error: error.message, - url: url, - jobId: jobId, - timestamp: new Date().toISOString() - }); - currentInterval = Math.min(currentInterval * 2, MAX_INTERVAL_MS); - } finally { - isPolling = false; - } - } - - function startPolling() { - updateAllJobSections(); - - function scheduleNext() { - if (pollInterval) clearTimeout(pollInterval); - pollInterval = setTimeout(() => { - updateAllJobSections(); - scheduleNext(); - }, currentInterval); - } - - scheduleNext(); - } - - function stopPolling() { - if (pollInterval) { - clearTimeout(pollInterval); - pollInterval = null; - } - } - - document.addEventListener('visibilitychange', () => { - if (document.hidden) { - stopPolling(); - } else { - startPolling(); - } - }); - - window.addEventListener('beforeunload', stopPolling); - - startPolling(); -}); diff --git a/app/config.py b/app/config.py index 556d455e5..6fc83946f 100644 --- a/app/config.py +++ b/app/config.py @@ -90,9 +90,7 @@ class Config(object): ], } - # TODO FIX!!! - # FEATURE_SOCKET_ENABLED = getenv("FEATURE_SOCKET_ENABLED", "true") == "true" - FEATURE_SOCKET_ENABLED = False + FEATURE_SOCKET_ENABLED = getenv("FEATURE_SOCKET_ENABLED", "true") == "true" def _s3_credentials_from_env(bucket_prefix): diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index 263f04e56..da562dc3a 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import json import os from functools import partial @@ -67,12 +68,6 @@ def view_job(service_id, job_id): FEATURE_SOCKET_ENABLED=current_app.config["FEATURE_SOCKET_ENABLED"], job=job, status=request.args.get("status", ""), - updates_url=url_for( - ".view_job_updates", - service_id=service_id, - job_id=job.id, - status=request.args.get("status", ""), - ), partials=get_job_partials(job), ) @@ -112,11 +107,48 @@ def cancel_job(service_id, job_id): return redirect(url_for("main.service_dashboard", service_id=service_id)) +@main.route("/services//jobs//status.json") +@user_has_permissions() +def view_job_status_poll(service_id, job_id): + """ + Poll status endpoint that only queries jobs table. + Returns minimal data needed for polling. + """ + import time + + start_time = time.time() + + job = Job.from_id(job_id, service_id=service_id) + + processed_count = job.notifications_delivered + job.notifications_failed + total_count = job.notification_count + + response_data = { + "sent_count": job.notifications_delivered, + "failed_count": job.notifications_failed, + "pending_count": job.notifications_sending, + "total_count": total_count, + "finished": job.finished_processing, + } + + response_time_ms = round((time.time() - start_time) * 1000, 2) + response_json = json.dumps(response_data) + response_size_bytes = len(response_json.encode("utf-8")) + + current_app.logger.info( + f"Poll status request - job_id={job_id[:8]} " + f"response_size={response_size_bytes}b " + f"response_time={response_time_ms}ms " + f"progress={processed_count}/{total_count}" + ) + + return jsonify(response_data) + + @main.route("/services//jobs/.json") @user_has_permissions() def view_job_updates(service_id, job_id): job = Job.from_id(job_id, service_id=service_id) - return jsonify(**get_job_partials(job)) diff --git a/app/main/views/organizations.py b/app/main/views/organizations.py index 5ed38db1d..3c1e447ff 100644 --- a/app/main/views/organizations.py +++ b/app/main/views/organizations.py @@ -92,7 +92,11 @@ def organization_dashboard(org_id): def download_organization_usage_report(org_id): selected_year_input = request.args.get("selected_year") # Validate selected_year to prevent header injection - if selected_year_input and selected_year_input.isdigit() and len(selected_year_input) == 4: + if ( + selected_year_input + and selected_year_input.isdigit() + and len(selected_year_input) == 4 + ): selected_year = selected_year_input else: selected_year = str(datetime.now().year) @@ -128,8 +132,9 @@ def download_organization_usage_report(org_id): # Sanitize organization name for filename to prevent header injection import re - safe_org_name = re.sub(r'[^\w\s-]', '', current_organization.name).strip() - safe_org_name = re.sub(r'[-\s]+', '-', safe_org_name) + + safe_org_name = re.sub(r"[^\w\s-]", "", current_organization.name).strip() + safe_org_name = re.sub(r"[-\s]+", "-", safe_org_name) return ( Spreadsheet.from_rows(org_usage_data).as_csv_data, diff --git a/app/main/views/templates.py b/app/main/views/templates.py index bd5895315..518dd1207 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -198,16 +198,14 @@ def process_folder_management_form(form, current_folder_id): # Use request.full_path which includes query string but not host # This avoids host header injection while preserving all parameters # Hardened redirect: only allow relative URLs, and strip any backslashes - target = request.full_path.replace('\\', '') + target = request.full_path.replace("\\", "") parts = urlparse(target) - if not parts.scheme and not parts.netloc and target.startswith('/'): + if not parts.scheme and not parts.netloc and target.startswith("/"): return redirect(target) # Fallback to main template list for this service - return redirect(url_for( - '.choose_template', - service_id=current_service.id, - template_type='all' - )) + return redirect( + url_for(".choose_template", service_id=current_service.id, template_type="all") + ) def get_template_nav_label(value): diff --git a/app/templates/views/jobs/job.html b/app/templates/views/jobs/job.html index 38ff39162..755f6b3bc 100644 --- a/app/templates/views/jobs/job.html +++ b/app/templates/views/jobs/job.html @@ -14,10 +14,9 @@ {% if not job.finished_processing %}

This page refreshes automatically to show the latest message activity delivery rates, details, and reports.
You can watch it in progress or check back later.

{% endif %} -
+
{% if not job.finished_processing and FEATURE_SOCKET_ENABLED %}
{% if not job.processing_finished and FEATURE_SOCKET_ENABLED %}
{ paths.src + 'javascripts/activityChart.js', paths.src + 'javascripts/sidenav.js', paths.src + 'javascripts/validation.js', - paths.src + 'javascripts/socketio.js', + paths.src + 'javascripts/job-status-polling.js', paths.src + 'javascripts/scrollPosition.js', ]) .pipe(plugins.prettyerror()) diff --git a/tests/app/main/views/test_job_notification_updates.py b/tests/app/main/views/test_job_notification_updates.py new file mode 100644 index 000000000..a06d9f03a --- /dev/null +++ b/tests/app/main/views/test_job_notification_updates.py @@ -0,0 +1,141 @@ +""" +Tests for job notification update logic during polling. + +These tests verify the poll status endpoint behavior and document +the JavaScript notification refresh logic: +1. Notifications update for first 50 messages +2. Notifications stop updating after 50 messages (to prevent performance issues) +3. Notifications always update when job finishes +""" + +import json + +import pytest + +from tests import job_json, user_json + + +@pytest.mark.parametrize( + ("delivered", "failed", "pending", "finished", "js_should_update_notifications", "reason"), + [ + (20, 10, 70, False, True, "30 messages processed (≤50 threshold)"), + (40, 10, 50, False, True, "50 messages processed (exactly at threshold)"), + (45, 15, 40, False, False, "60 messages processed (>50 threshold)"), + (450, 50, 0, True, True, "500 messages but job finished (always updates)"), + ], +) +def test_poll_status_notification_update_logic( + client_request, + service_one, + active_user_with_permissions, + mock_get_service_data_retention, + mocker, + fake_uuid, + delivered, + failed, + pending, + finished, + js_should_update_notifications, + reason, +): + """ + Test poll status endpoint for various scenarios. + + The JavaScript updates notifications when: + processedCount ≤ 50 AND job not finished + job is finished (regardless of count) + """ + total = delivered + failed + pending + job_status = "finished" if finished else "sending" + + mock_job = mocker.patch("app.job_api_client.get_job") + mock_job.return_value = { + "data": { + **job_json( + service_one["id"], + created_by=user_json(), + job_id=fake_uuid, + job_status=job_status, + notification_count=total, + notifications_requested=total, + ), + "statistics": [ + {"status": "delivered", "count": delivered}, + {"status": "failed", "count": failed}, + {"status": "pending", "count": pending}, + ], + } + } + + response = client_request.get_response( + "main.view_job_status_poll", + service_id=service_one["id"], + job_id=fake_uuid, + ) + + assert response.status_code == 200 + data = json.loads(response.get_data(as_text=True)) + + # Verify the response + assert data["sent_count"] == delivered + assert data["failed_count"] == failed + assert data["pending_count"] == pending + assert data["total_count"] == total + assert data["finished"] is finished + + processed_count = delivered + failed + + if js_should_update_notifications: + # JavaScript would call: await updateNotifications() + if finished: + assert finished, f"JS updates notifications: {reason}" + else: + assert processed_count <= 50, f"JS updates notifications: {reason}" + assert not finished, f"JS updates notifications: {reason}" + else: + # JavaScript would NOT update notifications + assert processed_count > 50, f"JS skips notification update: {reason}" + assert not finished, f"JS skips notification update: {reason}" + + +def test_poll_status_provides_required_fields( + client_request, + service_one, + active_user_with_permissions, + mock_get_service_data_retention, + mocker, + fake_uuid, +): + """Verify poll status endpoint returns all fields needed for notification update logic.""" + mock_job = mocker.patch("app.job_api_client.get_job") + mock_job.return_value = { + "data": { + **job_json( + service_one["id"], + created_by=user_json(), + job_id=fake_uuid, + job_status="sending", + notification_count=25, + notifications_requested=25, + ), + "statistics": [ + {"status": "delivered", "count": 15}, + {"status": "failed", "count": 5}, + {"status": "pending", "count": 5}, + ], + } + } + + response = client_request.get_response( + "main.view_job_status_poll", + service_id=service_one["id"], + job_id=fake_uuid, + ) + + data = json.loads(response.get_data(as_text=True)) + + required_fields = {"sent_count", "failed_count", "finished", "pending_count", "total_count"} + assert set(data.keys()) == required_fields + + response_size = len(response.get_data(as_text=True)) + assert response_size < 200, f"Response too large: {response_size} bytes" diff --git a/tests/app/main/views/test_jobs.py b/tests/app/main/views/test_jobs.py index dd04ec348..8dc508444 100644 --- a/tests/app/main/views/test_jobs.py +++ b/tests/app/main/views/test_jobs.py @@ -499,3 +499,143 @@ def test_should_show_message_note( 'Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear ' "back from the carrier they have received it and attempted deliver. More information on delivery status." ) + + +def test_poll_status_endpoint( + client_request, + service_one, + active_user_with_permissions, + mock_get_service_data_retention, + mocker, + fake_uuid, +): + """Test that the poll status endpoint returns only required data without notifications""" + mock_job = mocker.patch("app.job_api_client.get_job") + mock_job.return_value = { + "data": { + **job_json( + service_one["id"], + created_by=user_json(), + job_id=fake_uuid, + job_status="finished", + notification_count=100, + notifications_requested=100, + ), + "statistics": [ + {"status": "delivered", "count": 90}, + {"status": "failed", "count": 10}, + {"status": "pending", "count": 0}, + ], + } + } + + response = client_request.get_response( + "main.view_job_status_poll", + service_id=service_one["id"], + job_id=fake_uuid, + ) + + assert response.status_code == 200 + data = json.loads(response.get_data(as_text=True)) + + expected_keys = { + "sent_count", + "failed_count", + "pending_count", + "total_count", + "finished", + } + assert set(data.keys()) == expected_keys + + assert data["sent_count"] == 90 + assert data["failed_count"] == 10 + assert data["pending_count"] == 0 + assert data["total_count"] == 100 + assert data["finished"] is True + + +def test_poll_status_with_zero_notifications( + client_request, + service_one, + active_user_with_permissions, + mock_get_service_data_retention, + mocker, + fake_uuid, +): + """Test poll status endpoint handles edge case of no notifications""" + mock_job = mocker.patch("app.job_api_client.get_job") + mock_job.return_value = { + "data": { + **job_json( + service_one["id"], + created_by=user_json(), + job_id=fake_uuid, + job_status="pending", + notification_count=0, + notifications_requested=0, + ), + "statistics": [], + } + } + + response = client_request.get_response( + "main.view_job_status_poll", + service_id=service_one["id"], + job_id=fake_uuid, + ) + + assert response.status_code == 200 + data = json.loads(response.get_data(as_text=True)) + + assert data["total_count"] == 0 + assert ( + data["finished"] is True + ) + + +def test_poll_status_endpoint_does_not_query_notifications_table( + client_request, + service_one, + active_user_with_permissions, + mock_get_service_data_retention, + mocker, + fake_uuid, +): + """Critical regression test: ensure poll status endpoint never queries notifications""" + mock_job = mocker.patch("app.job_api_client.get_job") + mock_job.return_value = { + "data": { + **job_json( + service_one["id"], + created_by=user_json(), + job_id=fake_uuid, + job_status="sending", + notification_count=500, + notifications_requested=500, + ), + "statistics": [ + {"status": "delivered", "count": 300}, + {"status": "failed", "count": 50}, + {"status": "pending", "count": 150}, + ], + } + } + + mock_get_notifications = mocker.patch( + "app.notification_api_client.get_notifications_for_service" + ) + + response = client_request.get_response( + "main.view_job_status_poll", + service_id=service_one["id"], + job_id=fake_uuid, + ) + + assert response.status_code == 200 + + # Verify no notifications were fetched + mock_get_notifications.assert_not_called() + + data = json.loads(response.get_data(as_text=True)) + assert data["total_count"] == 500 + assert data["sent_count"] == 300 diff --git a/tests/app/main/views/test_template_folders.py b/tests/app/main/views/test_template_folders.py index 19ad5f935..e72bc7ff6 100644 --- a/tests/app/main/views/test_template_folders.py +++ b/tests/app/main/views/test_template_folders.py @@ -1533,9 +1533,7 @@ def test_should_be_able_to_move_to_new_folder( ], }, _expected_status=302, - _expected_redirect=url_for( - "main.choose_template", service_id=SERVICE_ONE_ID - ), + _expected_redirect=url_for("main.choose_template", service_id=SERVICE_ONE_ID), ) mock_create_template_folder.assert_called_once_with( diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index 4e9ef54c0..7d00769b9 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -242,6 +242,7 @@ EXCLUDED_ENDPOINTS = tuple( "verify_email", "view_job", "view_job_csv", + "view_job_status_poll", "view_job_updates", "view_jobs", "view_notification", From bb24117f008f9ec718d32300ebb9196107ae423d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 06:58:40 -0400 Subject: [PATCH 3/4] Bump newrelic from 10.17.0 to 11.0.0 (#2948) Bumps [newrelic](https://github.com/newrelic/newrelic-python-agent) from 10.17.0 to 11.0.0. - [Release notes](https://github.com/newrelic/newrelic-python-agent/releases) - [Commits](https://github.com/newrelic/newrelic-python-agent/compare/v10.17.0...v11.0.0) --- updated-dependencies: - dependency-name: newrelic dependency-version: 11.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 60 +++++++++++++++++++++++--------------------------- pyproject.toml | 2 +- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/poetry.lock b/poetry.lock index 39f84c2ab..2a7597ef0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2206,41 +2206,37 @@ files = [ [[package]] name = "newrelic" -version = "10.17.0" +version = "11.0.0" description = "New Relic Python Agent" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "newrelic-10.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91ebeaae460beaaeb3113474873bc5fc9c6c13ab6b0fd8936f584c508fec373d"}, - {file = "newrelic-10.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30b8b6a9f447a7276fd19a41ccc6dba264f9ef569e4bed92200a58b76e7f7754"}, - {file = "newrelic-10.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:214bd42ef8fb1c2046727bfe15c8f374ebf4ff2d5091ca1ac870abd2a7574ffe"}, - {file = "newrelic-10.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36a14fd92b44324d9bb33e1c1698c2cd15ffa0e291d222365e929ee17cd9d6da"}, - {file = "newrelic-10.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f62f70f1c01aa527de7e6ce2eb4c0285a7c40a89b42107be92bcc5dfd0fa8bb4"}, - {file = "newrelic-10.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54898db2b9eb81db9f3421302d2d03451630c033f2001fbe9f21d0e068f0418f"}, - {file = "newrelic-10.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65f7d08f90caf6dd3708068a0eda8328c49ec18e51d514a43afaa5a68180e9c"}, - {file = "newrelic-10.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dd61153b82b1e265754820f3e91c96c16b1b75508b6c169e27b5c196f1b7b95"}, - {file = "newrelic-10.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ab9cbce3f8da5bfcd085b8a59591cfb653d75834b362e3eccb86bdf21eea917"}, - {file = "newrelic-10.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:889969407457a9344927b1a9e9d5afde77345857db4d2ea75b119d02c02d9174"}, - {file = "newrelic-10.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d74c06863356b19c1fcf19900f85057ab33ef779aa3e0de3cb7e3d3f37ca8e20"}, - {file = "newrelic-10.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:083777458f93d16ae9e605fd66a6298d856e32deea11cf3270ed237858dcbfe6"}, - {file = "newrelic-10.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:165c7045da474342d4ceab67776e8aeb66e77674fab7484b4e2e4ea68d02ed4d"}, - {file = "newrelic-10.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3c3c82c95b57fe30fc1f684bf920bd8b0ecae70a16cc11f55d0867ffb7520d"}, - {file = "newrelic-10.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5789f739e6ca1c4a49649e406b538d67c063ec33ab4658d01049303dfad9398b"}, - {file = "newrelic-10.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4144dd96fa55772456326b3a6032a53d54aa9245ffc5041b002ce9eb5dbd0992"}, - {file = "newrelic-10.17.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c444e96918508decee6f532f04f0fa79027db0728770669ace4a7008c5dd52ca"}, - {file = "newrelic-10.17.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5171f1020bc834b01faae8466b5375e8c2162fa219c98430e42af1c6e5077b14"}, - {file = "newrelic-10.17.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0b43fb53f8a63c1e792a9ba4439680265b481c44efd64f7d92069e974c38b199"}, - {file = "newrelic-10.17.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:78a673440037ad61e0073da69f2549fac92271dadd7a2087c22a20ddc7f91260"}, - {file = "newrelic-10.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e81b726f3fab5fed155407f157ed67931d2d83403cc0ae473327e0b7b42bc5"}, - {file = "newrelic-10.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed6f549b1ab42ad31d5ee34c8041f1484ab769e045ad3b9f321c9588339079f3"}, - {file = "newrelic-10.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3a34df04ff6315619a8d098c59751f766de57f14e5cd5c09427dde47bfbc5012"}, - {file = "newrelic-10.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d92b43c07cf137d8a6d8dc73c87fbef99ca449564c25385e66727f156c2c34d8"}, - {file = "newrelic-10.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7669597193d8f6a3512fd5e8728a7b2b244db9694b3a03b3b99930ba9efc9e3b"}, - {file = "newrelic-10.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1027039eae1b4d3ffee3077135900a41ffdc41bd2bee3162d6cf36bd990cf3b"}, - {file = "newrelic-10.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:af28eb5e7f368f6f4fcde9593e7dd189b68e39a2e7409874a0cdea3a4bb4e904"}, - {file = "newrelic-10.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:328162a61e30f7b3e76241830989110d8f86d8392805a78034f8109e4f5434b4"}, - {file = "newrelic-10.17.0.tar.gz", hash = "sha256:f092109ac024f9524fafdd06126924c0fbd2af54684571167c0ee1d1cc1bcb7d"}, + {file = "newrelic-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d2e4dd14a07c18976112ae06512d9c306cea29ed28a86a985f85ddeb2d531c"}, + {file = "newrelic-11.0.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a75441bc2d3a2462fb6e1bd249b161883b8f26bb89b0a4c85cce082013827f3"}, + {file = "newrelic-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f527edd175b591d220d5c1f79ddaf45def2a0dc7c61d889d96a8c45655166968"}, + {file = "newrelic-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c88af2362743da139d597253c0b2e7252c6693c804cf236163aa9879f09c938d"}, + {file = "newrelic-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ff5e4304d7cf1f668b498bf84e8639ae7f55bb88cb83e3f0f950c02d401d46"}, + {file = "newrelic-11.0.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d516aba469dccb410b0ba8585694be65e40b17383f4a14a834713a316cc77b"}, + {file = "newrelic-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd7ea98c575206062c45662a67dab9ab849cdbe1ec85af77a464a5c38e1ce345"}, + {file = "newrelic-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a9da74aa99aaec48f73a0cf758e1fc14a39805fcca33057abddf0130f8d2b149"}, + {file = "newrelic-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b42fe1c4c9e35a9fdf4c264faa1e92f6918d0014c31dad69fede2798bc4482bb"}, + {file = "newrelic-11.0.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:927b150eefd9843a5abfc407a9eeac02f4119a842ea0597cd30e33c4b1d5bc61"}, + {file = "newrelic-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1ca9deba4f44e13eab9eb9866757135a88544b33d971e0929c6884821286bb5"}, + {file = "newrelic-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee444805ea0df9bcd3bb62873e16f8c5fe3848e0169fc90f8f74b611a51ae2d0"}, + {file = "newrelic-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a060dd3918e19e629c9e0e5c16b8c97c4ea1989c7fc12e1845abec2b688e80f2"}, + {file = "newrelic-11.0.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5bbdc9c1062d8178dd47aaf26bda8c1ebc2700ed82927973ac005cdd1668a8"}, + {file = "newrelic-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c27e089768c8883d4df35f5f1f406ec475ff5053f1463082e01115b25881e4a6"}, + {file = "newrelic-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eb16e55ac25781812bfde1e7e4c181857f9578957919106e049e1a784b333942"}, + {file = "newrelic-11.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0499267e59934fe3df4de00335619d71344e9ac585c28a654a13e68d34a5c8c"}, + {file = "newrelic-11.0.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765ba1fb3fe139c3ece956a9ab6e69519b88b886b6a97cd637fb3d75bdb4c6d3"}, + {file = "newrelic-11.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:94413632777d261a61aacabc5d3771b482155f58cf59524205bd2969daec888d"}, + {file = "newrelic-11.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:577a8aa37c1f3ea2bc82298f27056fd5e32a917933748374de998e09a4baeb4d"}, + {file = "newrelic-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acf814050a7dc3d8631d9bde2a9af67c08a80f7e8fc6374e3488276f58ad1f12"}, + {file = "newrelic-11.0.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17eb9301ef6761e2e541b57d1a36b8514cc44d5be26564942a8822b1d7bebb17"}, + {file = "newrelic-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e4c755d0e8a0e95c9e424fe3820c1e6720b71c3bfac763d826dbd70bf0cd29ac"}, + {file = "newrelic-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:56794a746560a64fdcd49ca0ced7da941a13db6a60da33f971aa7a9ad488c2ef"}, + {file = "newrelic-11.0.0.tar.gz", hash = "sha256:3419599597dfcb5c7dd78dd46d12097d20c72b19cc1c89218783804976d0931e"}, ] [package.extras] @@ -4354,4 +4350,4 @@ cffi = ["cffi (>=1.17) ; python_version >= \"3.13\" and platform_python_implemen [metadata] lock-version = "2.1" python-versions = "^3.13.2" -content-hash = "fd9f0f2545909be9453dbfb68f0770e6323c1ed8c874a204a686fab6f544c668" +content-hash = "59316fd4958902662de50fc951fabae4b5f16694858a4291d22aab1ef6845045" diff --git a/pyproject.toml b/pyproject.toml index c9870eb2e..5a5144ade 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ hypothesis = "^6.140.2" humanize = "~=4.13" itsdangerous = "~=2.2" jinja2 = "^3.1.6" -newrelic = "^10.17.0" +newrelic = "^11.0.0" pyexcel = "==0.7.3" pyexcel-io = "==0.6.7" pyexcel-ods3 = "==0.6.1" From 60b7f9149e654dd5ee95dc8aef285cce57dcb787 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:06:53 +0000 Subject: [PATCH 4/4] Bump pyyaml from 6.0.2 to 6.0.3 (#2947) Bumps [pyyaml](https://github.com/yaml/pyyaml) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/yaml/pyyaml/releases) - [Changelog](https://github.com/yaml/pyyaml/blob/6.0.3/CHANGES) - [Commits](https://github.com/yaml/pyyaml/compare/6.0.2...6.0.3) --- updated-dependencies: - dependency-name: pyyaml dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 123 +++++++++++++++++++++++++++---------------------- pyproject.toml | 2 +- 2 files changed, 69 insertions(+), 56 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2a7597ef0..32f9e42d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3197,65 +3197,78 @@ files = [ [[package]] name = "pyyaml" -version = "6.0.2" +version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] [[package]] @@ -4350,4 +4363,4 @@ cffi = ["cffi (>=1.17) ; python_version >= \"3.13\" and platform_python_implemen [metadata] lock-version = "2.1" python-versions = "^3.13.2" -content-hash = "59316fd4958902662de50fc951fabae4b5f16694858a4291d22aab1ef6845045" +content-hash = "17ca104123a6b1e9cfa8b4ae2bb1a5fa7bdf16ab9714e46dad078dc1e02c322f" diff --git a/pyproject.toml b/pyproject.toml index 5a5144ade..681a35e75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ click = "^8.3.0" idna = "^3.7" markupsafe = "^3.0.2" python-dateutil = "^2.9.0.post0" -pyyaml = "^6.0.1" +pyyaml = "^6.0.3" requests = "^2.32.5" six = "^1.16.0" urllib3 = "^2.5.0"