mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-27 09:43:58 -04:00
Compare commits
72 Commits
2282-p2-ho
...
0df04cfd89
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0df04cfd89 | ||
|
|
34f7c6a4da | ||
|
|
cd78710753 | ||
|
|
102d005e0d | ||
|
|
3455235a8b | ||
|
|
c93d00dcbe | ||
|
|
a524ed0dc5 | ||
|
|
c37bb44e0a | ||
|
|
0b3177e3ff | ||
|
|
baf9b69fbb | ||
|
|
df3e2264ec | ||
|
|
79192e29b6 | ||
|
|
5cede78661 | ||
|
|
5c9d190021 | ||
|
|
61fb59f1cb | ||
|
|
02f39bd077 | ||
|
|
1eb12b93f1 | ||
|
|
f9fd580c58 | ||
|
|
fe6921e243 | ||
|
|
d20f887f0b | ||
|
|
eadd8dddb5 | ||
|
|
77248b8bf0 | ||
|
|
a8d036a3e5 | ||
|
|
05a746a042 | ||
|
|
84acce48c4 | ||
|
|
1bbc49c0ad | ||
|
|
bcd7ead5ec | ||
|
|
0405c92f69 | ||
|
|
ad1f83e80e | ||
|
|
44778349f0 | ||
|
|
5b46a4e1d7 | ||
|
|
91bea21607 | ||
|
|
ab497be1f8 | ||
|
|
5b88eefe27 | ||
|
|
34e8647fba | ||
|
|
2fdedc1b4f | ||
|
|
db1f929dba | ||
|
|
e7953262a3 | ||
|
|
89cfbd989d | ||
|
|
479cca3adb | ||
|
|
cc6a66f790 | ||
|
|
0c1dd129e0 | ||
|
|
d70a80f4a6 | ||
|
|
78cfafae94 | ||
|
|
3016aff185 | ||
|
|
4726143fb7 | ||
|
|
a4963b8263 | ||
|
|
22f729d652 | ||
|
|
38fad44a15 | ||
|
|
c1fa099c12 | ||
|
|
208891a71a | ||
|
|
afd3d94790 | ||
|
|
515419e0a2 | ||
|
|
d16eb70765 | ||
|
|
39fd87064b | ||
|
|
5c6b4132a9 | ||
|
|
23e10b1529 | ||
|
|
545bf10e0d | ||
|
|
12a2290ea9 | ||
|
|
bd619af421 | ||
|
|
2d896daa0e | ||
|
|
6ccf1bb983 | ||
|
|
5020964fb1 | ||
|
|
5f8d5996aa | ||
|
|
229842b8c9 | ||
|
|
4feace4cdc | ||
|
|
48b317ed52 | ||
|
|
709b7049c6 | ||
|
|
9630d085f4 | ||
|
|
5d565ab88b | ||
|
|
cfb966ce53 | ||
|
|
a4f6dd7100 |
@@ -527,7 +527,7 @@
|
||||
"filename": "tests/app/main/views/test_accept_invite.py",
|
||||
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
|
||||
"is_verified": false,
|
||||
"line_number": 643,
|
||||
"line_number": 631,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -684,5 +684,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2025-01-16T16:38:48Z"
|
||||
"generated_at": "2025-02-26T18:19:37Z"
|
||||
}
|
||||
|
||||
8
.github/workflows/checks.yml
vendored
8
.github/workflows/checks.yml
vendored
@@ -10,7 +10,7 @@ env:
|
||||
FLASK_APP: application.py
|
||||
WERKZEUG_DEBUG_PIN: off
|
||||
REDIS_ENABLED: 0
|
||||
NODE_VERSION: 16.15.1
|
||||
NODE_VERSION: 22.3.0
|
||||
AWS_US_TOLL_FREE_NUMBER: "+18556438890"
|
||||
ADMIN_BASE_URL: http://localhost:6012
|
||||
|
||||
@@ -38,10 +38,10 @@ jobs:
|
||||
output: report-markdown
|
||||
annotations: failed-tests
|
||||
prnumber: ${{ steps.findPr.outputs.number }}
|
||||
- name: Run style checks
|
||||
run: poetry run flake8 .
|
||||
- name: Check imports alphabetized
|
||||
run: poetry run isort --check-only ./app ./tests
|
||||
- name: Run style checks
|
||||
run: poetry run flake8 .
|
||||
- name: Check dead code
|
||||
run: make dead-code
|
||||
- name: Run js tests
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
- uses: pypa/gh-action-pip-audit@v1.0.8
|
||||
- uses: pypa/gh-action-pip-audit@v1.1.0
|
||||
with:
|
||||
inputs: requirements.txt
|
||||
ignore-vulns: |
|
||||
|
||||
4
.github/workflows/daily_checks.yml
vendored
4
.github/workflows/daily_checks.yml
vendored
@@ -16,7 +16,7 @@ env:
|
||||
FLASK_APP: application.py
|
||||
WERKZEUG_DEBUG_PIN: off
|
||||
REDIS_ENABLED: 0
|
||||
NODE_VERSION: 16.15.1
|
||||
NODE_VERSION: 22.3.0
|
||||
|
||||
jobs:
|
||||
dependency-audits:
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
- uses: pypa/gh-action-pip-audit@v1.0.6
|
||||
- uses: pypa/gh-action-pip-audit@v1.1.0
|
||||
with:
|
||||
inputs: requirements.txt
|
||||
- name: Run npm audit
|
||||
|
||||
12
.github/workflows/deploy-demo.yml
vendored
12
.github/workflows/deploy-demo.yml
vendored
@@ -16,9 +16,17 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
# Looks like we need to install Terraform ourselves now!
|
||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
terraform/demo/**
|
||||
@@ -93,7 +101,7 @@ jobs:
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-demo.*.acl
|
||||
|
||||
12
.github/workflows/deploy-prod.yml
vendored
12
.github/workflows/deploy-prod.yml
vendored
@@ -16,9 +16,17 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
# Looks like we need to install Terraform ourselves now!
|
||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
terraform/production/**
|
||||
@@ -93,7 +101,7 @@ jobs:
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-production.*.acl
|
||||
|
||||
12
.github/workflows/deploy.yml
vendored
12
.github/workflows/deploy.yml
vendored
@@ -21,9 +21,17 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
# Looks like we need to install Terraform ourselves now!
|
||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
terraform/staging/**
|
||||
@@ -100,7 +108,7 @@ jobs:
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-staging.*.acl
|
||||
|
||||
24
.github/workflows/drift.yml
vendored
24
.github/workflows/drift.yml
vendored
@@ -15,6 +15,14 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Looks like we need to install Terraform ourselves now!
|
||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for drift
|
||||
uses: dflook/terraform-check@v1
|
||||
env:
|
||||
@@ -35,6 +43,14 @@ jobs:
|
||||
with:
|
||||
ref: 'production'
|
||||
|
||||
# Looks like we need to install Terraform ourselves now!
|
||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for drift
|
||||
uses: dflook/terraform-check@v1
|
||||
env:
|
||||
@@ -55,6 +71,14 @@ jobs:
|
||||
with:
|
||||
ref: 'production'
|
||||
|
||||
# Looks like we need to install Terraform ourselves now!
|
||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for drift
|
||||
uses: dflook/terraform-check@v1
|
||||
env:
|
||||
|
||||
10
.github/workflows/terraform-demo.yml
vendored
10
.github/workflows/terraform-demo.yml
vendored
@@ -18,6 +18,14 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Looks like we need to install Terraform ourselves now!
|
||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Terraform format
|
||||
id: format
|
||||
run: terraform fmt -check
|
||||
@@ -51,7 +59,7 @@ jobs:
|
||||
|
||||
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
||||
- name: Update PR
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
# we would like to update the PR even when a prior step failed
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
|
||||
10
.github/workflows/terraform-production.yml
vendored
10
.github/workflows/terraform-production.yml
vendored
@@ -18,6 +18,14 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Looks like we need to install Terraform ourselves now!
|
||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Terraform format
|
||||
id: format
|
||||
run: terraform fmt -check
|
||||
@@ -51,7 +59,7 @@ jobs:
|
||||
|
||||
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
||||
- name: Update PR
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
# we would like to update the PR even when a prior step failed
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
|
||||
10
.github/workflows/terraform-staging.yml
vendored
10
.github/workflows/terraform-staging.yml
vendored
@@ -18,6 +18,14 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Looks like we need to install Terraform ourselves now!
|
||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Terraform format
|
||||
id: format
|
||||
run: terraform fmt -check
|
||||
@@ -51,7 +59,7 @@ jobs:
|
||||
|
||||
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
||||
- name: Update PR
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
# we would like to update the PR even when a prior step failed
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
|
||||
12
Makefile
12
Makefile
@@ -62,6 +62,13 @@ py-lint: ## Run python linting scanners and black
|
||||
poetry run flake8 .
|
||||
poetry run isort --check-only ./app ./tests
|
||||
|
||||
.PHONY: tada
|
||||
tada: ## Run python linting scanners and black
|
||||
poetry run isort ./app ./tests
|
||||
poetry run black .
|
||||
poetry run flake8 .
|
||||
|
||||
|
||||
.PHONY: avg-complexity
|
||||
avg-complexity:
|
||||
echo "*** Shows average complexity in radon of all code ***"
|
||||
@@ -155,3 +162,8 @@ upload-static:
|
||||
# @cf map-route notify-admin ${DNS_NAME} --hostname www
|
||||
# @cf unmap-route notify-admin-failwhale ${DNS_NAME} --hostname www
|
||||
# @echo "Failwhale is disabled"
|
||||
|
||||
.PHONY: test-single
|
||||
test-single: export NEW_RELIC_ENVIRONMENT=test
|
||||
test-single: ## Run a single test file
|
||||
poetry run pytest $(TEST_FILE)
|
||||
|
||||
@@ -130,11 +130,12 @@
|
||||
.append('g')
|
||||
.attr('class', 'bar-group')
|
||||
.attr('fill', d => color(d.key));
|
||||
|
||||
const minBarHeight = 5;
|
||||
barGroups.selectAll('rect')
|
||||
.data(d => d)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.filter(d => d[1] - d[0] > 0)
|
||||
.attr('x', d => x(d.data.label))
|
||||
.attr('y', height)
|
||||
.attr('height', 0)
|
||||
@@ -155,8 +156,10 @@
|
||||
.transition()
|
||||
.duration(1000)
|
||||
.attr('y', d => y(d[1]))
|
||||
.attr('height', d => y(d[0]) - y(d[1]));
|
||||
};
|
||||
.attr('height', d => {
|
||||
const calculatedHeight = y(d[0]) - y(d[1]);
|
||||
return calculatedHeight < minBarHeight ? minBarHeight : calculatedHeight;
|
||||
}); };
|
||||
|
||||
// Function to create an accessible table
|
||||
const createTable = function(tableId, chartType, labels, deliveredData, failedData, pendingData) {
|
||||
@@ -213,7 +216,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var url = type === 'service' ? `/services/${currentServiceId}/daily-stats.json` : `/services/${currentServiceId}/daily-stats-by-user.json`;
|
||||
var userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
var url = type === 'service'
|
||||
? `/services/${currentServiceId}/daily-stats.json?timezone=${encodeURIComponent(userTimezone)}`
|
||||
: `/services/${currentServiceId}/daily-stats-by-user.json`;
|
||||
|
||||
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
@@ -238,9 +247,6 @@
|
||||
failedData.push(data[dateString].sms.failure);
|
||||
pendingData.push(data[dateString].sms.pending || 0);
|
||||
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure + data[dateString].sms.pending;
|
||||
|
||||
// Calculate the total number of messages
|
||||
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
var chartTitle = document.getElementById('chartTitle').textContent;
|
||||
|
||||
// Access data attributes from the HTML
|
||||
var sms_sent = parseInt(chartContainer.getAttribute('data-sms-sent'));
|
||||
var sms_remaining_messages = parseInt(chartContainer.getAttribute('data-sms-allowance-remaining'));
|
||||
var totalMessages = sms_sent + sms_remaining_messages;
|
||||
var messagesSent = parseInt(chartContainer.getAttribute('data-messages-sent'));
|
||||
var messagesRemaining = parseInt(chartContainer.getAttribute('data-messages-remaining'));
|
||||
var totalMessages = messagesSent + messagesRemaining;
|
||||
|
||||
// Update the message below the chart
|
||||
document.getElementById('message').innerText = `${sms_sent.toLocaleString()} sent / ${sms_remaining_messages.toLocaleString()} remaining`;
|
||||
document.getElementById('message').innerText = `${messagesSent.toLocaleString()} sent / ${messagesRemaining.toLocaleString()} remaining`;
|
||||
|
||||
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
|
||||
var minSentPercentage = (sms_sent === 0) ? 0 : 0.02;
|
||||
var minSentPercentage = (messagesSent === 0) ? 0 : 0.02;
|
||||
var minSentValue = totalMessages * minSentPercentage;
|
||||
var displaySent = Math.max(sms_sent, minSentValue);
|
||||
var displaySent = Math.max(messagesSent, minSentValue);
|
||||
var displayRemaining = totalMessages - displaySent;
|
||||
|
||||
var svg = d3.select("#totalMessageChart");
|
||||
@@ -48,7 +48,7 @@
|
||||
.attr("width", 0) // Start with width 0 for animation
|
||||
.on('mouseover', function(event) {
|
||||
tooltip.style('display', 'block')
|
||||
.html(`Messages Sent: ${sms_sent.toLocaleString()}`);
|
||||
.html(`Messages Sent: ${messagesSent.toLocaleString()}`);
|
||||
})
|
||||
.on('mousemove', function(event) {
|
||||
tooltip.style('left', `${event.pageX + 10}px`)
|
||||
@@ -66,7 +66,7 @@
|
||||
.attr("width", 0) // Start with width 0 for animation
|
||||
.on('mouseover', function(event) {
|
||||
tooltip.style('display', 'block')
|
||||
.html(`Remaining: ${sms_remaining_messages.toLocaleString()}`);
|
||||
.html(`Remaining: ${messagesRemaining.toLocaleString()}`);
|
||||
})
|
||||
.on('mousemove', function(event) {
|
||||
tooltip.style('left', `${event.pageX + 10}px`)
|
||||
@@ -115,9 +115,9 @@
|
||||
var tbodyRow = document.createElement('tr');
|
||||
|
||||
var tdMessagesSent = document.createElement('td');
|
||||
tdMessagesSent.textContent = sms_sent.toLocaleString(); // Value for Messages Sent
|
||||
tdMessagesSent.textContent = messagesSent.toLocaleString(); // Value for Messages Sent
|
||||
var tdRemaining = document.createElement('td');
|
||||
tdRemaining.textContent = sms_remaining_messages.toLocaleString(); // Value for Remaining
|
||||
tdRemaining.textContent = messagesRemaining.toLocaleString(); // Value for Remaining
|
||||
|
||||
tbodyRow.appendChild(tdMessagesSent);
|
||||
tbodyRow.appendChild(tdRemaining);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import calendar
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from flask import Response, abort, jsonify, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
@@ -48,48 +49,99 @@ def service_dashboard(service_id):
|
||||
if not current_user.has_permissions("view_activity"):
|
||||
return redirect(url_for("main.choose_template", service_id=service_id))
|
||||
|
||||
yearly_usage = billing_api_client.get_annual_usage_for_service(
|
||||
service_id,
|
||||
get_current_financial_year(),
|
||||
)
|
||||
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||
current_service.id,
|
||||
)
|
||||
usage_data = get_annual_usage_breakdown(yearly_usage, free_sms_allowance)
|
||||
sms_sent = usage_data["sms_sent"]
|
||||
sms_allowance_remaining = usage_data["sms_allowance_remaining"]
|
||||
|
||||
job_response = job_api_client.get_jobs(service_id)["data"]
|
||||
service_data_retention_days = 7
|
||||
|
||||
filtered_jobs = [job for job in job_response if job["job_status"] != "cancelled"]
|
||||
sorted_jobs = sorted(filtered_jobs, key=lambda job: job["created_at"], reverse=True)
|
||||
active_jobs = [job for job in job_response if job["job_status"] != "cancelled"]
|
||||
sorted_jobs = sorted(active_jobs, key=lambda job: job["created_at"], reverse=True)
|
||||
job_lists = [
|
||||
{**job_dict, "finished_processing": job_is_finished(job_dict)}
|
||||
for job_dict in sorted_jobs
|
||||
]
|
||||
|
||||
total_messages = service_api_client.get_service_message_ratio(service_id)
|
||||
messages_remaining = total_messages.get("messages_remaining", 0)
|
||||
messages_sent = total_messages.get("messages_sent", 0)
|
||||
return render_template(
|
||||
"views/dashboard/dashboard.html",
|
||||
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
|
||||
partials=get_dashboard_partials(service_id),
|
||||
jobs=sorted_jobs,
|
||||
jobs=job_lists,
|
||||
service_data_retention_days=service_data_retention_days,
|
||||
sms_sent=sms_sent,
|
||||
sms_allowance_remaining=sms_allowance_remaining,
|
||||
messages_remaining=messages_remaining,
|
||||
messages_sent=messages_sent,
|
||||
)
|
||||
|
||||
|
||||
def job_is_finished(job_dict):
|
||||
done_statuses = [
|
||||
"delivered",
|
||||
"sent",
|
||||
"failed",
|
||||
"technical-failure",
|
||||
"temporary-failure",
|
||||
"permanent-failure",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
processed_count = sum(
|
||||
stat["count"]
|
||||
for stat in job_dict["statistics"]
|
||||
if stat["status"] in done_statuses
|
||||
)
|
||||
return job_dict["notification_count"] == processed_count
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/daily-stats.json")
|
||||
@user_has_permissions()
|
||||
def get_daily_stats(service_id):
|
||||
date_range = get_stats_date_range()
|
||||
stats = service_api_client.get_service_notification_statistics_by_day(
|
||||
service_id, start_date=date_range["start_date"], days=date_range["days"]
|
||||
days = date_range["days"]
|
||||
user_timezone = request.args.get("timezone", "UTC")
|
||||
|
||||
stats_utc = service_api_client.get_service_notification_statistics_by_day(
|
||||
service_id,
|
||||
start_date=date_range["start_date"],
|
||||
days=days,
|
||||
)
|
||||
return jsonify(stats)
|
||||
|
||||
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days)
|
||||
return jsonify(local_stats)
|
||||
|
||||
|
||||
def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days):
|
||||
tz = ZoneInfo(user_timezone)
|
||||
today_local = datetime.now(tz).date()
|
||||
start_local = today_local - timedelta(days=days - 1)
|
||||
|
||||
# Generate exactly days local dates, each with zeroed stats
|
||||
days_list = [
|
||||
(start_local + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(days)
|
||||
]
|
||||
aggregator = {
|
||||
d: {
|
||||
"sms": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
}
|
||||
for d in days_list
|
||||
}
|
||||
|
||||
# Convert each UTC timestamp to local date and iterate
|
||||
for utc_ts, data in stats_utc.items():
|
||||
utc_dt = datetime.strptime(utc_ts, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=ZoneInfo("UTC"))
|
||||
local_day = utc_dt.astimezone(tz).strftime("%Y-%m-%d")
|
||||
|
||||
if local_day in aggregator:
|
||||
for msg_type in ["sms", "email"]:
|
||||
for status in ["delivered", "failure", "pending", "requested"]:
|
||||
aggregator[local_day][msg_type][status] += data[msg_type][status]
|
||||
|
||||
return aggregator
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/daily-stats-by-user.json")
|
||||
@user_has_permissions()
|
||||
def get_daily_stats_by_user(service_id):
|
||||
service_id = session.get("service_id")
|
||||
date_range = get_stats_date_range()
|
||||
stats = service_api_client.get_user_service_notification_statistics_by_day(
|
||||
service_id,
|
||||
|
||||
@@ -116,6 +116,70 @@ def download_all_users():
|
||||
return response
|
||||
|
||||
|
||||
@main.route("/platform-admin/get-redis-report")
|
||||
@user_is_platform_admin
|
||||
def get_redis_report():
|
||||
|
||||
memory_info = redis_client.info("memory")
|
||||
memory_used = memory_info.get("used_memory_human", "N/A")
|
||||
max_memory = memory_info.get("maxmemory_human", "N/A")
|
||||
if max_memory == "0B":
|
||||
max_memory = "No set limit"
|
||||
mem_fragmentation = memory_info.get("mem_fragmentation_ratio", "N/A")
|
||||
frag_quality = "Swapping (bad)"
|
||||
if mem_fragmentation >= 1.0:
|
||||
frag_quality = "Healthy"
|
||||
if mem_fragmentation > 1.5:
|
||||
frag_quality = "Problematic"
|
||||
if mem_fragmentation > 2.0:
|
||||
frag_quality = "Severe fragmentation"
|
||||
|
||||
frag_note = ""
|
||||
if mem_fragmentation > 2.0:
|
||||
frag_note = "Use MEMORY PURGE.\nReplace multiple small keys with hashes.\nAvoid long keys.\nSet max_memory."
|
||||
elif mem_fragmentation < 1.0:
|
||||
frag_note = "Allocate more RAM.\nSet max_memory."
|
||||
|
||||
keys = redis_client.keys("*")
|
||||
key_details = []
|
||||
|
||||
for key in keys:
|
||||
key_type = redis_client.type(key).decode("utf-8")
|
||||
ttl = redis_client.ttl(key)
|
||||
ttl_str = "No Expiry" if ttl == -1 else f"{ttl} seconds"
|
||||
key_details.append(
|
||||
{"Key": key.decode("utf-8"), "Type": key_type, "TTL": ttl_str}
|
||||
)
|
||||
output = StringIO()
|
||||
writer = csv.writer(
|
||||
output,
|
||||
)
|
||||
writer.writerow(["Redis Report"])
|
||||
writer.writerow([])
|
||||
|
||||
writer.writerow(["Memory"])
|
||||
writer.writerow(["", "Memory Used", memory_used])
|
||||
writer.writerow(["", "Max Memory", max_memory])
|
||||
writer.writerow(["", "Memory Fragmentation Ratio", mem_fragmentation])
|
||||
writer.writerow(["", "Memory Fragmentation Quality", frag_quality, frag_note])
|
||||
writer.writerow([])
|
||||
|
||||
writer.writerow(["Keys Overview"])
|
||||
writer.writerow(["", "TTL", "Type", "Key"])
|
||||
for key_detail in key_details:
|
||||
writer.writerow(
|
||||
["", key_detail["TTL"], key_detail["Type"], key_detail["Key"][0:50]]
|
||||
)
|
||||
|
||||
csv_data = output.getvalue()
|
||||
|
||||
# Create a direct download response with the CSV data and appropriate headers
|
||||
response = Response(csv_data, content_type="text/csv; charset=utf-8")
|
||||
response.headers["Content-Disposition"] = "attachment; filename=redis.csv"
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def is_over_threshold(number, total, threshold):
|
||||
percentage = number / total * 100 if total else 0
|
||||
return percentage > threshold
|
||||
|
||||
@@ -68,11 +68,12 @@ def _get_access_token(code): # pragma: no cover
|
||||
id_token = get_id_token(response_json)
|
||||
nonce = id_token["nonce"]
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
stored_nonce = redis_client.get(nonce_key).decode("utf8")
|
||||
if not os.getenv("NOTIFY_ENVIRONMENT") == "development":
|
||||
stored_nonce = redis_client.get(nonce_key).decode("utf8")
|
||||
|
||||
if nonce != stored_nonce:
|
||||
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
|
||||
abort(403)
|
||||
if nonce != stored_nonce:
|
||||
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
|
||||
abort(403)
|
||||
|
||||
try:
|
||||
access_token = response_json["access_token"]
|
||||
@@ -112,7 +113,7 @@ def _do_login_dot_gov(): # $ pragma: no cover
|
||||
verify_key = f"login-verify_email-{unquote(state)}"
|
||||
verify_path = bool(redis_client.get(verify_key))
|
||||
|
||||
if not verify_path:
|
||||
if not verify_path and not os.getenv("NOTIFY_ENVIRONMENT") == "development":
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
|
||||
if state != stored_state:
|
||||
|
||||
@@ -537,6 +537,11 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
"""
|
||||
return self.get("/service/invite/redis/{0}".format(redis_key))
|
||||
|
||||
def get_service_message_ratio(self, service_id):
|
||||
return self.get(
|
||||
url="service/get-service-message-ratio?service_id={0}".format(service_id),
|
||||
)
|
||||
|
||||
|
||||
service_api_client = ServiceAPIClient()
|
||||
|
||||
|
||||
@@ -14,10 +14,8 @@
|
||||
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
||||
{% block bodyStart %}
|
||||
{% block extra_javascripts_before_body %}
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WX5NGWF"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -145,10 +143,8 @@
|
||||
{% block bodyEnd %}
|
||||
{% block extra_javascripts %}
|
||||
{% endblock %}
|
||||
<!--[if gt IE 8]><!-->
|
||||
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
|
||||
<!--<![endif]-->
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
{% if job.scheduled_for %}
|
||||
<div class="usa-alert usa-alert--info">
|
||||
<div class="usa-alert__body">
|
||||
<h2 class="usa-alert__heading">Your message has been scheduled</h2>
|
||||
<h2 class="usa-alert__heading">Your {{ 'message has' if job.notification_count == 1 else 'messages have' }} been scheduled</h2>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }} was scheduled on {{ job.scheduled_for|format_datetime_normal }} by {{ job.created_by.name }}
|
||||
</p>
|
||||
@@ -37,7 +37,7 @@
|
||||
<div class="usa-alert usa-alert--success">
|
||||
<div class="usa-alert__body">
|
||||
<h2 class="usa-alert__heading">
|
||||
Your message is sending
|
||||
Your {{ 'message is' if job.notification_count == 1 else 'messages are' }} sending
|
||||
</h2>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }}
|
||||
@@ -49,7 +49,7 @@
|
||||
<div class="usa-alert usa-alert--info">
|
||||
<div class="usa-alert__body">
|
||||
<h2 class="usa-alert__heading">
|
||||
Your message is pending
|
||||
Your {{ 'message is' if job.notification_count == 1 else 'messages are' }} pending
|
||||
</h2>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }}
|
||||
@@ -64,7 +64,7 @@
|
||||
<div class="usa-alert usa-alert--success">
|
||||
<div class="usa-alert__body">
|
||||
<h2 class="usa-alert__heading">
|
||||
Your message has been sent
|
||||
Your {{ 'message has' if job.notification_count == 1 else 'messages have' }} been sent
|
||||
</h2>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }}
|
||||
|
||||
@@ -46,14 +46,16 @@
|
||||
</td>
|
||||
<td class="table-field template">{{ job.template_name }}</td>
|
||||
<td class="table-field time-sent">
|
||||
{% if job.scheduled_for and not job.processing_finished %}
|
||||
Scheduled for {{ job.scheduled_for|format_datetime_table }}
|
||||
{% elif job.processing_finished and not job.statistics|selectattr('status', 'equalto', 'sending')|list %}
|
||||
Sent on {{ job.processing_finished|format_datetime_table }}
|
||||
{% elif job.processing_started %}
|
||||
Sending since {{ job.processing_started|format_datetime_table }}
|
||||
{% if not job.finished_processing %}
|
||||
{% if job.scheduled_for%}
|
||||
Scheduled for {{ job.scheduled_for|format_datetime_table }}
|
||||
{% elif job.processing_started %}
|
||||
Sending since {{ job.processing_started|format_datetime_table }}
|
||||
{% else %}
|
||||
Pending since {{ job.created_at|format_datetime_table }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Pending since {{ job.created_at|format_datetime_table }}
|
||||
Sent on {{ job.processing_started|format_datetime_table }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="table-field sender sender-column">{{ job.created_by.name }}</td>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'inbox') }}
|
||||
|
||||
<div id="totalMessageChartContainer" data-sms-sent="{{ sms_sent }}" data-sms-allowance-remaining="{{ sms_allowance_remaining }}">
|
||||
<div id="totalMessageChartContainer" data-messages-sent="{{ messages_sent }}" data-messages-remaining="{{ messages_remaining }}">
|
||||
<h2 id="chartTitle">Total messages</h2>
|
||||
<svg id="totalMessageChart"></svg>
|
||||
<div id="message"></div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header("Message status") }}
|
||||
{% if not job.processing_finished %}
|
||||
{% if not job.finished_processing %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
@@ -20,7 +20,7 @@
|
||||
>
|
||||
{% endif %}
|
||||
{{ partials['status']|safe }}
|
||||
{% if not job.processing_finished %}
|
||||
{% if not job.finished_processing %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not finished %}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{% set product_highlights = [
|
||||
{
|
||||
"svg_src": "#chat",
|
||||
"card_heading": "Up to 100,000 messages to use over your first year*",
|
||||
"card_heading": "Up to 250,000 messages to use over your first year*",
|
||||
},
|
||||
{
|
||||
"svg_src": "#phone",
|
||||
|
||||
@@ -34,5 +34,8 @@
|
||||
<p>
|
||||
<a class="usa-link" href="{{ url_for('main.download_all_users') }}">Download All Users</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="usa-link" href="{{ url_for('main.get_redis_report') }}">Get Redis Report</a>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -128,6 +128,13 @@ def generate_notifications_csv(**kwargs):
|
||||
notifications_resp = notification_api_client.get_notifications_for_service(
|
||||
**kwargs
|
||||
)
|
||||
# Stop if we are finished
|
||||
if (
|
||||
notifications_resp.get("notifications") is None
|
||||
or len(notifications_resp["notifications"]) == 0
|
||||
):
|
||||
return
|
||||
|
||||
for notification in notifications_resp["notifications"]:
|
||||
preferred_tz_created_at = convert_report_date_to_preferred_timezone(
|
||||
notification["created_at"]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from flask import Flask
|
||||
from werkzeug.serving import WSGIRequestHandler
|
||||
|
||||
from app import create_app
|
||||
|
||||
WSGIRequestHandler.version_string = lambda self: "SecureServer"
|
||||
|
||||
application = Flask("app")
|
||||
|
||||
create_app(application)
|
||||
|
||||
136
docs/manual-a11y-checklist.md
Normal file
136
docs/manual-a11y-checklist.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Manual Accessibility Testing Checklist
|
||||
|
||||
## 1. Structure and Semantics
|
||||
|
||||
### Headings:
|
||||
- Verify that headings are used logically (`<h1>` to `<h6>`) to create a clear content hierarchy.
|
||||
- Ensure there is only one `<h1>` per page (unless it’s a valid use case, like within `<section>` landmarks).
|
||||
|
||||
### Landmarks:
|
||||
- Confirm the presence of ARIA landmarks (e.g., `<header>`, `<main>`, `<footer>`) for navigation.
|
||||
- Use tools (e.g., Accessibility Insights, Axe) to ensure proper landmark roles.
|
||||
|
||||
### HTML Validity:
|
||||
- Check for semantic HTML usage (e.g., `<button>` for buttons, `<a>` for links).
|
||||
- Avoid using `<div>` or `<span>` for interactive elements.
|
||||
|
||||
|
||||
## 2. Navigation and Focus
|
||||
|
||||
### Keyboard Navigation:
|
||||
- Ensure all functionality is accessible via the keyboard (e.g., Tab, Enter, Space, Arrow Keys).
|
||||
- Test for logical tab order that follows the visual reading order.
|
||||
|
||||
### Focus States:
|
||||
- Ensure focus is visible and distinct on all interactive elements.
|
||||
- Check that focus is not trapped in modal dialogs or components.
|
||||
|
||||
### Skip Links:
|
||||
- Verify that “Skip to content” links are present and functional.
|
||||
|
||||
|
||||
## 3. Forms and Inputs
|
||||
|
||||
### Labels:
|
||||
- Confirm all form fields have accessible, descriptive labels (`<label>` or `aria-label`/`aria-labelledby`).
|
||||
- Ensure placeholder text is not used as a label substitute.
|
||||
|
||||
### Error Messages:
|
||||
- Check that error messages are programmatically associated with inputs and conveyed to assistive technologies.
|
||||
- Verify that error messages are specific and provide actionable guidance.
|
||||
|
||||
### Fieldset and Legend:
|
||||
- Group related inputs with `<fieldset>` and `<legend>` where appropriate.
|
||||
|
||||
|
||||
## 4. Media and Non-Text Content
|
||||
|
||||
### Images:
|
||||
- Ensure all meaningful images have appropriate alt text.
|
||||
- Decorative images should have `alt=""` or be hidden with `role="presentation"`.
|
||||
|
||||
### Videos:
|
||||
- Verify captions are available and accurate for all video content.
|
||||
- Provide audio descriptions for videos with critical visual information.
|
||||
|
||||
### Audio:
|
||||
- Confirm there is a way to stop, pause, or adjust the volume of any audio that plays automatically.
|
||||
|
||||
|
||||
## 5. Color and Contrast
|
||||
|
||||
### Color Contrast:
|
||||
- Use a contrast checker to ensure text and interactive elements meet WCAG AA requirements (4.5:1 for text, 3:1 for large text).
|
||||
|
||||
### Color Independence:
|
||||
- Verify that color is not the sole means of conveying information (e.g., “errors in red”).
|
||||
|
||||
### High Contrast Modes:
|
||||
- Test with browser or OS high-contrast modes to ensure proper readability.
|
||||
|
||||
---
|
||||
|
||||
## 6. Dynamic Content and Interactions
|
||||
|
||||
### ARIA Live Regions:
|
||||
- Verify that dynamic updates are announced using `aria-live` (e.g., success messages).
|
||||
|
||||
### Modals and Dialogs:
|
||||
- Ensure modals have proper focus management (focus should move to the modal on open and back to the trigger on close).
|
||||
- Test that modals are announced properly by screen readers.
|
||||
|
||||
### Tooltips and Popovers:
|
||||
- Ensure tooltips are accessible via keyboard and announced by assistive technologies.
|
||||
|
||||
|
||||
## 7. Assistive Technology Compatibility
|
||||
|
||||
### Screen Reader Testing:
|
||||
- Test the site with a screen reader (e.g., NVDA, JAWS, VoiceOver) to ensure content is announced logically.
|
||||
- Verify that dynamic content (e.g., dropdowns, modals) is announced appropriately.
|
||||
|
||||
### Zoom and Magnification:
|
||||
- Ensure the site is usable at 200% zoom without loss of functionality or content.
|
||||
|
||||
### Responsive Design:
|
||||
- Test on different devices and orientations to verify responsive behavior is accessible.
|
||||
|
||||
|
||||
## 8. Performance and Usability
|
||||
|
||||
### Loading Indicators:
|
||||
- Confirm that loading indicators are announced (e.g., using `aria-live`).
|
||||
|
||||
### Page Titles:
|
||||
- Ensure page titles are unique and descriptive of the page’s content.
|
||||
|
||||
### Time Limits:
|
||||
- Check if users can extend or disable time limits where applicable.
|
||||
|
||||
|
||||
## 9. WCAG Success Criteria Coverage
|
||||
|
||||
### Review WCAG 2.1 (or 2.2 if applicable) Success Criteria:
|
||||
- **Level A**: Must-have minimum requirements.
|
||||
- **Level AA**: Commonly required for legal compliance.
|
||||
- **Level AAA**: Optional for enhanced accessibility.
|
||||
|
||||
|
||||
## Tools to Assist Manual Testing
|
||||
|
||||
### Browser Extensions:
|
||||
- [Axe DevTools](https://www.deque.com/axe/)
|
||||
- [WAVE](https://wave.webaim.org/)
|
||||
- [Lighthouse](https://developers.google.com/web/tools/lighthouse)
|
||||
|
||||
### Screen Readers:
|
||||
- **NVDA** (Windows)
|
||||
- **VoiceOver** (macOS/iOS)
|
||||
- **TalkBack** (Android)
|
||||
|
||||
### Contrast Checkers:
|
||||
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||
- [Color Contrast Analyzer](https://developer.paciellogroup.com/resources/contrastanalyser/)
|
||||
|
||||
### Keyboard Navigation:
|
||||
- Test tabbing through the site manually.
|
||||
@@ -149,6 +149,22 @@ class RedisClient:
|
||||
except Exception as e:
|
||||
self.__handle_exception(e, raise_exception, "incr", key)
|
||||
|
||||
def info(self, key):
|
||||
if self.active:
|
||||
return self.redis_store.info(key)
|
||||
|
||||
def keys(self, pattern):
|
||||
if self.active:
|
||||
return self.redis_store.keys(pattern)
|
||||
|
||||
def type(self, key):
|
||||
if self.active:
|
||||
return self.redis_store.type(key)
|
||||
|
||||
def ttl(self, key):
|
||||
if self.active:
|
||||
return self.redis_store.ttl(key)
|
||||
|
||||
def get(self, key, raise_exception=False):
|
||||
key = prepare_value(key)
|
||||
if self.active:
|
||||
|
||||
@@ -75,6 +75,24 @@ class ResponseHeaderMiddleware(object):
|
||||
if SPAN_ID_HEADER.lower() not in lower_existing_header_names:
|
||||
headers.append((SPAN_ID_HEADER, str(req.span_id)))
|
||||
|
||||
# Some dynamic scan findings
|
||||
headers.append(("Cross-Origin-Opener-Policy", "same-origin"))
|
||||
headers.append(("Cross-Origin-Embedder-Policy", "require-corp"))
|
||||
headers.append(("Cross-Origin-Resource-Policy", "same-origin"))
|
||||
headers.append(("Cross-Origin-Opener-Policy", "same-origin"))
|
||||
|
||||
# svg content type should not contain charset
|
||||
found_svg = False
|
||||
for _, v in headers:
|
||||
if "svg+xml" in v:
|
||||
found_svg = True
|
||||
if found_svg:
|
||||
new_headers = [
|
||||
(k, v) for k, v in headers if k.lower() != "content-type"
|
||||
]
|
||||
new_headers.append(("Content-Type", "image/svg+xml"))
|
||||
return start_response(status, new_headers, exc_info)
|
||||
|
||||
return start_response(status, headers, exc_info)
|
||||
|
||||
return self._app(environ, rewrite_response_headers)
|
||||
|
||||
572
package-lock.json
generated
572
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -40,15 +40,15 @@
|
||||
"playwright": "^1.50.1",
|
||||
"python": "^0.0.4",
|
||||
"query-command-supported": "1.0.0",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"sass-embedded": "^1.85.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"timeago": "1.6.7",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.7",
|
||||
"@babel/preset-env": "^7.26.7",
|
||||
"@babel/core": "^7.26.9",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@uswds/compile": "^1.2.1",
|
||||
"backstopjs": "^6.3.25",
|
||||
"better-npm-audit": "^3.11.0",
|
||||
@@ -66,7 +66,7 @@
|
||||
"jest-environment-jsdom": "^29.2.2",
|
||||
"jshint": "2.13.6",
|
||||
"jshint-stylish": "2.2.1",
|
||||
"rollup": "^4.34.4",
|
||||
"rollup": "^4.34.8",
|
||||
"rollup-plugin-commonjs": "10.1.0",
|
||||
"rollup-plugin-node-resolve": "5.2.0"
|
||||
}
|
||||
|
||||
220
poetry.lock
generated
220
poetry.lock
generated
@@ -42,17 +42,17 @@ tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
||||
|
||||
[[package]]
|
||||
name = "awscli"
|
||||
version = "1.35.17"
|
||||
version = "1.36.40"
|
||||
description = "Universal Command Line Environment for AWS."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "awscli-1.35.17-py3-none-any.whl", hash = "sha256:67906511b138bb6b241136c0ba6bee854f522b7b506887911e6ad34877a822c3"},
|
||||
{file = "awscli-1.35.17.tar.gz", hash = "sha256:9469a1924c6987dd0553ad0d0fd2a37717d0f7b55104e99f5789d3678c9706c4"},
|
||||
{file = "awscli-1.36.40-py3-none-any.whl", hash = "sha256:971c3b150c06068bc26867fe295753547780f63fcf8256d41cd38760e44d46ca"},
|
||||
{file = "awscli-1.36.40.tar.gz", hash = "sha256:e2a88f88dc16d5c0f26379afd6f254097e53e8b34c82164e59f4165db6ee6dfa"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = "1.35.51"
|
||||
botocore = "1.35.99"
|
||||
colorama = ">=0.2.5,<0.4.7"
|
||||
docutils = ">=0.10,<0.17"
|
||||
PyYAML = ">=3.10,<6.1"
|
||||
@@ -219,13 +219,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.35.51"
|
||||
version = "1.35.99"
|
||||
description = "Low-level, data-driven core of boto 3."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "botocore-1.35.51-py3-none-any.whl", hash = "sha256:4d65b00111bd12b98e9f920ecab602cf619cc6a6d0be6e5dd53f517e4b92901c"},
|
||||
{file = "botocore-1.35.51.tar.gz", hash = "sha256:a9b3d1da76b3e896ad74605c01d88f596324a3337393d4bfbfa0d6c35822ca9c"},
|
||||
{file = "botocore-1.35.99-py3-none-any.whl", hash = "sha256:b22d27b6b617fc2d7342090d6129000af2efd20174215948c0d7ae2da0fab445"},
|
||||
{file = "botocore-1.35.99.tar.gz", hash = "sha256:1eab44e969c39c5f3d9a3104a0836c24715579a455f12b3979a31d7cde51b3c3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -595,51 +595,55 @@ toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "43.0.3"
|
||||
version = "44.0.1"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
|
||||
files = [
|
||||
{file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
|
||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
|
||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
|
||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
|
||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
|
||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
|
||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
|
||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
|
||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
|
||||
{file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"},
|
||||
{file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"},
|
||||
{file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"},
|
||||
{file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"},
|
||||
{file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"},
|
||||
{file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"},
|
||||
{file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"},
|
||||
{file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"},
|
||||
{file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"},
|
||||
{file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
||||
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||
nox = ["nox"]
|
||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
||||
sdist = ["build"]
|
||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"]
|
||||
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"]
|
||||
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
|
||||
sdist = ["build (>=1.0.0)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
@@ -1746,40 +1750,40 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "newrelic"
|
||||
version = "10.2.0"
|
||||
version = "10.6.0"
|
||||
description = "New Relic Python Agent"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "newrelic-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f822e6a43151af13a748fb2de6ff298aeb6eee03bf6512afba6aaa79211172"},
|
||||
{file = "newrelic-10.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501cc575b3fd702a21542a0f5dac59b83f47e2806f5b7c0f4e4510b5474ed77f"},
|
||||
{file = "newrelic-10.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f15a940b6794b4008ab983e7ac3b4d179efe609e040ee96ed5744723fc580c8"},
|
||||
{file = "newrelic-10.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60d01303807228718c4099d8550f72d21ee8b61a33555d8974800f6868f2144a"},
|
||||
{file = "newrelic-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5809b4111ef3b1d0b5fa66ad06a81de512842370707863d44888c9439f16c4c"},
|
||||
{file = "newrelic-10.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3415b1c7cab5e586e72cca467dd80cd0507f23a3139c02911cf75892fdbb48a6"},
|
||||
{file = "newrelic-10.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3d9c8297ba158ce4570fc48cfea7bdf3678b2054baaf0cad4debcbca33c2af3a"},
|
||||
{file = "newrelic-10.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bee0b9ce1eccf6ac63e51113781743853b1b84c98ae48ed17d0410c352ccb4d"},
|
||||
{file = "newrelic-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6ff022c7556b61b067e8e6b729fe60f437a8356f319ae3b8342858792f3930d"},
|
||||
{file = "newrelic-10.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3521d646c0032db53b7320fe6b6859eebd863f1b47d7c7dd480073727091e50e"},
|
||||
{file = "newrelic-10.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b4220b97669d214e75d2039fea9e0505fde5bc450832210abbc76b9a635785ed"},
|
||||
{file = "newrelic-10.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6aa9cf936b16d13b65c1b7aa6c722a76a0702469f91bcfc3c5b39f3293181eb"},
|
||||
{file = "newrelic-10.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36a2218c9e79897d9b5671cdeac30c467d7fbac10cda4f2d79062f2bd0fcaed8"},
|
||||
{file = "newrelic-10.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f021eac4c2e3b14eab90c608d8bd25b4e3c6b0b0d40796ec1c1260cc47b5e83"},
|
||||
{file = "newrelic-10.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:69aa68cae47c595bdeb95f275d78693ec27a9fd9353bf81257e21f8607134db6"},
|
||||
{file = "newrelic-10.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ef5d27001d3b5ca53f19d150c60b570c1b0c774d082ab9bf8349f4430ed85b48"},
|
||||
{file = "newrelic-10.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00df1aa613294cb592a52157f789e75166dbf439cfa9e6cf59f6cf4a265dada9"},
|
||||
{file = "newrelic-10.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6d4094d19db924c51ca35da603344907bcbca030822f7a78d5d9c6ad361d419"},
|
||||
{file = "newrelic-10.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:62d521a5d7269c8a5c5838c4ca3b757ef63a13257302c901223c75510cc6f9f5"},
|
||||
{file = "newrelic-10.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3cd5aeade6462519328fc42f4e98948a45571f3d22360a0559e19a6525c723a6"},
|
||||
{file = "newrelic-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6333aa7051544ddc7f8a85f344bc3f401ddd8635540878da34de7bfd91f5d95"},
|
||||
{file = "newrelic-10.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b449546ebb89feaadfd36fda7735ce06023fc90979b838e244f98369aba5ccb"},
|
||||
{file = "newrelic-10.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc3d34db12133b481636384663f45b9ccd7f0f41554a59c15ec37aeb4f77227d"},
|
||||
{file = "newrelic-10.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a15df23effd09bb1d1f5c38866b75cc5f380b6aa953efbca9e95e79b72744db4"},
|
||||
{file = "newrelic-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc693e0db87ab4cf6623847c3949debbcc991554edfb4dd8c02c136e0770b367"},
|
||||
{file = "newrelic-10.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c57e79d37ed87e2790c5e66253f9a5d91ed8cc218f160d5a4d062fc759791a78"},
|
||||
{file = "newrelic-10.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23400846dad2283693eade90b6d3c3462301a4b7735c7f76009b1fa445660aeb"},
|
||||
{file = "newrelic-10.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bd34e4cd73c2435472c0b67869fd2db914d6c99d3e1e404e09affe61a8551e"},
|
||||
{file = "newrelic-10.2.0.tar.gz", hash = "sha256:03ab987eae0452aeb5aed8571c100d1735613a3a227387f99fe54ed38f1ae0e9"},
|
||||
{file = "newrelic-10.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0192ae5b6d624b1dbd5bb5808e0b0846e109d614b38ddfab6328e87bb261d889"},
|
||||
{file = "newrelic-10.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f2cfa6aee9668f6e19c3eba77c851dddbd0bc4c87af2551ad6a56f17a3d5fbe"},
|
||||
{file = "newrelic-10.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c532a2ec39b1f8fbeba7b557ae2d0cb3440012a0ecbbd138b6a137ecd90224c9"},
|
||||
{file = "newrelic-10.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b282a069984e3bf3bc810eaf988165eb0e8dccc99b3c34d7c0f34c7649093fbe"},
|
||||
{file = "newrelic-10.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5682142c7f94ca1294b51e3c49cb3ef64580ce36ff223efa23fbc2f34e7fb392"},
|
||||
{file = "newrelic-10.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cc7d1df935239613582d592253fbe3a961f372c9f2568e4e8b0937caf8f072b"},
|
||||
{file = "newrelic-10.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:659991fc2f5149b4f7711e7f51c0a76ac1bdef7ca0b5f1b4806c21ed05553963"},
|
||||
{file = "newrelic-10.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c657a50cc831d80bff310022106e1086c224b50a114241cb61194921498bcd5c"},
|
||||
{file = "newrelic-10.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8af1c6cea8ffdb185fcfe871f61ab2de8471c71b1d417f07a93e6fe7713f012a"},
|
||||
{file = "newrelic-10.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3af6f16cfa95d187a37be26ef44f73b15c33b3600c14d4936fc77666a26192e6"},
|
||||
{file = "newrelic-10.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a64e99fe3bb0a5bb9f942053291214659345064f79355f1cb1753b2ae3989792"},
|
||||
{file = "newrelic-10.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:120bce9635c39e8f444e127132e08cf536a042138c5688c6bce27dbfd44caf6f"},
|
||||
{file = "newrelic-10.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b539ebb8e624b797a4dd35ef742a11148f16d8d9b4da5b4b4c77c33a354e9206"},
|
||||
{file = "newrelic-10.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe82936cd60d9ebfb3573311c9ca4c22682f3ca5a27baa2bf5f9e26a7a4ed7e"},
|
||||
{file = "newrelic-10.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f3a9c60cd1b4b76acab30306d30b95b90fd19eaa38c34ef84dcaade7a6894426"},
|
||||
{file = "newrelic-10.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eb69b15f2f917fd7874e05f0444752ae1cc1eb63028814ab77033babfc6329e5"},
|
||||
{file = "newrelic-10.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ca468375f5959867fda7bbe90f2d80ca32aae6c5d632e3234e32064013e439"},
|
||||
{file = "newrelic-10.6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319c4df59515d214ae91fd6c46cc7fc120217ffc38a080689fd9930b3b2a7530"},
|
||||
{file = "newrelic-10.6.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:23fc4539240bb8a5c131d56f0d2d982c253ee2adf6e2dff945daaab07ef5e527"},
|
||||
{file = "newrelic-10.6.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89b304474705254d94cfc82d58a505f18c5bba029aaec32fba15ba582cb2c849"},
|
||||
{file = "newrelic-10.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:465f4f8be765121a6e1d215184b61b8c6c5bb24106f7e3f1443cffe14f647e8f"},
|
||||
{file = "newrelic-10.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:579d146ff0309e93af712759df8732e7d23a7c676dc1e38c2ac32af46be15f5d"},
|
||||
{file = "newrelic-10.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:07b31b0970ad4b160a9481f1bd908250aa39ee8d7f2a4fa1d88b13c360361241"},
|
||||
{file = "newrelic-10.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2c9f39b6b1154dfa21831d7632b53cd1fa1a2b4069a273dead1a40186621688e"},
|
||||
{file = "newrelic-10.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ba77efcbf1aaa902cdc546480fa05af6e907e4334798b53ab08f34be1d13a9c"},
|
||||
{file = "newrelic-10.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf6f29b82845355faa32e719d246a63b29d2b8cbd406cc31a4991dc213c4813"},
|
||||
{file = "newrelic-10.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:275d930c60cb534e26dd2aaa61e5c25b6312d63fef011055203d3f6a26e20863"},
|
||||
{file = "newrelic-10.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43a8aafc4a5d844192872a94d9033f4721693a6c0950750f5a0b0e448821831b"},
|
||||
{file = "newrelic-10.6.0.tar.gz", hash = "sha256:46d6f39a9cb89cca2440211e272e6a13bb89802865a4c2b2cd9d613c1488c3ba"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -2310,38 +2314,44 @@ diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pyproj"
|
||||
version = "3.6.1"
|
||||
version = "3.7.1"
|
||||
description = "Python interface to PROJ (cartographic projections and coordinate transformations library)"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "pyproj-3.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab7aa4d9ff3c3acf60d4b285ccec134167a948df02347585fdd934ebad8811b4"},
|
||||
{file = "pyproj-3.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc0472302919e59114aa140fd7213c2370d848a7249d09704f10f5b062031fe"},
|
||||
{file = "pyproj-3.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5279586013b8d6582e22b6f9e30c49796966770389a9d5b85e25a4223286cd3f"},
|
||||
{file = "pyproj-3.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fafd1f3eb421694857f254a9bdbacd1eb22fc6c24ca74b136679f376f97d35"},
|
||||
{file = "pyproj-3.6.1-cp310-cp310-win32.whl", hash = "sha256:c41e80ddee130450dcb8829af7118f1ab69eaf8169c4bf0ee8d52b72f098dc2f"},
|
||||
{file = "pyproj-3.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:db3aedd458e7f7f21d8176f0a1d924f1ae06d725228302b872885a1c34f3119e"},
|
||||
{file = "pyproj-3.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ebfbdbd0936e178091309f6cd4fcb4decd9eab12aa513cdd9add89efa3ec2882"},
|
||||
{file = "pyproj-3.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:447db19c7efad70ff161e5e46a54ab9cc2399acebb656b6ccf63e4bc4a04b97a"},
|
||||
{file = "pyproj-3.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e13c40183884ec7f94eb8e0f622f08f1d5716150b8d7a134de48c6110fee85"},
|
||||
{file = "pyproj-3.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65ad699e0c830e2b8565afe42bd58cc972b47d829b2e0e48ad9638386d994915"},
|
||||
{file = "pyproj-3.6.1-cp311-cp311-win32.whl", hash = "sha256:8b8acc31fb8702c54625f4d5a2a6543557bec3c28a0ef638778b7ab1d1772132"},
|
||||
{file = "pyproj-3.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:38a3361941eb72b82bd9a18f60c78b0df8408416f9340521df442cebfc4306e2"},
|
||||
{file = "pyproj-3.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1e9fbaf920f0f9b4ee62aab832be3ae3968f33f24e2e3f7fbb8c6728ef1d9746"},
|
||||
{file = "pyproj-3.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d227a865356f225591b6732430b1d1781e946893789a609bb34f59d09b8b0f8"},
|
||||
{file = "pyproj-3.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83039e5ae04e5afc974f7d25ee0870a80a6bd6b7957c3aca5613ccbe0d3e72bf"},
|
||||
{file = "pyproj-3.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb059ba3bced6f6725961ba758649261d85ed6ce670d3e3b0a26e81cf1aa8d"},
|
||||
{file = "pyproj-3.6.1-cp312-cp312-win32.whl", hash = "sha256:2d6ff73cc6dbbce3766b6c0bce70ce070193105d8de17aa2470009463682a8eb"},
|
||||
{file = "pyproj-3.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:7a27151ddad8e1439ba70c9b4b2b617b290c39395fa9ddb7411ebb0eb86d6fb0"},
|
||||
{file = "pyproj-3.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ba1f9b03d04d8cab24d6375609070580a26ce76eaed54631f03bab00a9c737b"},
|
||||
{file = "pyproj-3.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18faa54a3ca475bfe6255156f2f2874e9a1c8917b0004eee9f664b86ccc513d3"},
|
||||
{file = "pyproj-3.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd43bd9a9b9239805f406fd82ba6b106bf4838d9ef37c167d3ed70383943ade1"},
|
||||
{file = "pyproj-3.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50100b2726a3ca946906cbaa789dd0749f213abf0cbb877e6de72ca7aa50e1ae"},
|
||||
{file = "pyproj-3.6.1-cp39-cp39-win32.whl", hash = "sha256:9274880263256f6292ff644ca92c46d96aa7e57a75c6df3f11d636ce845a1877"},
|
||||
{file = "pyproj-3.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:36b64c2cb6ea1cc091f329c5bd34f9c01bb5da8c8e4492c709bda6a09f96808f"},
|
||||
{file = "pyproj-3.6.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd93c1a0c6c4aedc77c0fe275a9f2aba4d59b8acf88cebfc19fe3c430cfabf4f"},
|
||||
{file = "pyproj-3.6.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6420ea8e7d2a88cb148b124429fba8cd2e0fae700a2d96eab7083c0928a85110"},
|
||||
{file = "pyproj-3.6.1.tar.gz", hash = "sha256:44aa7c704c2b7d8fb3d483bbf75af6cb2350d30a63b144279a09b75fead501bf"},
|
||||
{file = "pyproj-3.7.1-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:bf09dbeb333c34e9c546364e7df1ff40474f9fddf9e70657ecb0e4f670ff0b0e"},
|
||||
{file = "pyproj-3.7.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:6575b2e53cc9e3e461ad6f0692a5564b96e7782c28631c7771c668770915e169"},
|
||||
{file = "pyproj-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cb516ee35ed57789b46b96080edf4e503fdb62dbb2e3c6581e0d6c83fca014b"},
|
||||
{file = "pyproj-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e47c4e93b88d99dd118875ee3ca0171932444cdc0b52d493371b5d98d0f30ee"},
|
||||
{file = "pyproj-3.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3e8d276caeae34fcbe4813855d0d97b9b825bab8d7a8b86d859c24a6213a5a0d"},
|
||||
{file = "pyproj-3.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f173f851ee75e54acdaa053382b6825b400cb2085663a9bb073728a59c60aebb"},
|
||||
{file = "pyproj-3.7.1-cp310-cp310-win32.whl", hash = "sha256:f550281ed6e5ea88fcf04a7c6154e246d5714be495c50c9e8e6b12d3fb63e158"},
|
||||
{file = "pyproj-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3537668992a709a2e7f068069192138618c00d0ba113572fdd5ee5ffde8222f3"},
|
||||
{file = "pyproj-3.7.1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:a94e26c1a4950cea40116775588a2ca7cf56f1f434ff54ee35a84718f3841a3d"},
|
||||
{file = "pyproj-3.7.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:263b54ba5004b6b957d55757d846fc5081bc02980caa0279c4fc95fa0fff6067"},
|
||||
{file = "pyproj-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6d6a2ccd5607cd15ef990c51e6f2dd27ec0a741e72069c387088bba3aab60fa"},
|
||||
{file = "pyproj-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c5dcf24ede53d8abab7d8a77f69ff1936c6a8843ef4fcc574646e4be66e5739"},
|
||||
{file = "pyproj-3.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c2e7449840a44ce860d8bea2c6c1c4bc63fa07cba801dcce581d14dcb031a02"},
|
||||
{file = "pyproj-3.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0829865c1d3a3543f918b3919dc601eea572d6091c0dd175e1a054db9c109274"},
|
||||
{file = "pyproj-3.7.1-cp311-cp311-win32.whl", hash = "sha256:6181960b4b812e82e588407fe5c9c68ada267c3b084db078f248db5d7f45d18a"},
|
||||
{file = "pyproj-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ad0ff443a785d84e2b380869fdd82e6bfc11eba6057d25b4409a9bbfa867970"},
|
||||
{file = "pyproj-3.7.1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:2781029d90df7f8d431e29562a3f2d8eafdf233c4010d6fc0381858dc7373217"},
|
||||
{file = "pyproj-3.7.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d61bf8ab04c73c1da08eedaf21a103b72fa5b0a9b854762905f65ff8b375d394"},
|
||||
{file = "pyproj-3.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04abc517a8555d1b05fcee768db3280143fe42ec39fdd926a2feef31631a1f2f"},
|
||||
{file = "pyproj-3.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084c0a475688f934d386c2ab3b6ce03398a473cd48adfda70d9ab8f87f2394a0"},
|
||||
{file = "pyproj-3.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a20727a23b1e49c7dc7fe3c3df8e56a8a7acdade80ac2f5cca29d7ca5564c145"},
|
||||
{file = "pyproj-3.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bf84d766646f1ebd706d883755df4370aaf02b48187cedaa7e4239f16bc8213d"},
|
||||
{file = "pyproj-3.7.1-cp312-cp312-win32.whl", hash = "sha256:5f0da2711364d7cb9f115b52289d4a9b61e8bca0da57f44a3a9d6fc9bdeb7274"},
|
||||
{file = "pyproj-3.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:aee664a9d806612af30a19dba49e55a7a78ebfec3e9d198f6a6176e1d140ec98"},
|
||||
{file = "pyproj-3.7.1-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:5f8d02ef4431dee414d1753d13fa82a21a2f61494737b5f642ea668d76164d6d"},
|
||||
{file = "pyproj-3.7.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0b853ae99bda66cbe24b4ccfe26d70601d84375940a47f553413d9df570065e0"},
|
||||
{file = "pyproj-3.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83db380c52087f9e9bdd8a527943b2e7324f275881125e39475c4f9277bdeec4"},
|
||||
{file = "pyproj-3.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b35ed213892e211a3ce2bea002aa1183e1a2a9b79e51bb3c6b15549a831ae528"},
|
||||
{file = "pyproj-3.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8b15b0463d1303bab113d1a6af2860a0d79013c3a66fcc5475ce26ef717fd4f"},
|
||||
{file = "pyproj-3.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:87229e42b75e89f4dad6459200f92988c5998dfb093c7c631fb48524c86cd5dc"},
|
||||
{file = "pyproj-3.7.1-cp313-cp313-win32.whl", hash = "sha256:d666c3a3faaf3b1d7fc4a544059c4eab9d06f84a604b070b7aa2f318e227798e"},
|
||||
{file = "pyproj-3.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:d3caac7473be22b6d6e102dde6c46de73b96bc98334e577dfaee9886f102ea2e"},
|
||||
{file = "pyproj-3.7.1.tar.gz", hash = "sha256:60d72facd7b6b79853f19744779abcd3f804c4e0d4fa8815469db20c9f640a47"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3039,13 +3049,13 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
|
||||
|
||||
[[package]]
|
||||
name = "vulture"
|
||||
version = "2.13"
|
||||
version = "2.14"
|
||||
description = "Find dead code"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "vulture-2.13-py2.py3-none-any.whl", hash = "sha256:34793ba60488e7cccbecdef3a7fe151656372ef94fdac9fe004c52a4000a6d44"},
|
||||
{file = "vulture-2.13.tar.gz", hash = "sha256:78248bf58f5eaffcc2ade306141ead73f437339950f80045dce7f8b078e5a1aa"},
|
||||
{file = "vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9"},
|
||||
{file = "vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3134,4 +3144,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12.2"
|
||||
content-hash = "3035525f7fc44291ea02b011d9fa6c0e84501d567417144e4d8fd6beebb11928"
|
||||
content-hash = "ad24005d65a2f8cff39fc4a1aa543f5aebb103c0748b3dadd9cb5ee5c692846c"
|
||||
|
||||
@@ -30,7 +30,7 @@ pyexcel-ods3 = "==0.6.1"
|
||||
pyexcel-xls = "==0.7.0"
|
||||
pyexcel-xlsx = "==0.6.0"
|
||||
openpyxl = "==3.0.10"
|
||||
pyproj = "==3.6.1"
|
||||
pyproj = "==3.7.1"
|
||||
python-dotenv = "==1.0.1"
|
||||
pytz = "^2024.1"
|
||||
rtreelib = "==0.2.0"
|
||||
@@ -40,10 +40,10 @@ markdown = "^3.5.2"
|
||||
async-timeout = "^4.0.3"
|
||||
bleach = "^6.1.0"
|
||||
boto3 = "^1.34.156"
|
||||
botocore = "^1.34.156"
|
||||
botocore = "^1.35.99"
|
||||
cachetools = "^5.4.0"
|
||||
cffi = "^1.16.0"
|
||||
cryptography = "^43.0.1"
|
||||
cryptography = "^44.0.1"
|
||||
flask-redis = "^0.4.0"
|
||||
geojson = "^3.1.0"
|
||||
jmespath = "^1.0.1"
|
||||
@@ -94,7 +94,7 @@ pytest-playwright = "^0.5.1"
|
||||
pytest-xdist = "^3.5.0"
|
||||
radon = "^6.0.1"
|
||||
requests-mock = "^1.11.0"
|
||||
vulture = "^2.11"
|
||||
vulture = "^2.14"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -300,25 +300,13 @@ def test_accepting_invite_removes_invite_from_session(
|
||||
|
||||
client_request.login(user)
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
date_range = {"start_date": "2024-01-01", "days": 7}
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats",
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value={
|
||||
date_range["start_date"]: {
|
||||
"email": {"delivered": 0, "failure": 0, "requested": 0},
|
||||
"sms": {"delivered": 0, "failure": 1, "requested": 1},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value={
|
||||
date_range["start_date"]: {
|
||||
"email": {"delivered": 1, "failure": 0, "requested": 1},
|
||||
"sms": {"delivered": 1, "failure": 0, "requested": 1},
|
||||
},
|
||||
"messages_remaining": 71919,
|
||||
"messages_sent": 28081,
|
||||
"total_message_limit": 100000,
|
||||
},
|
||||
)
|
||||
page = client_request.get(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import copy
|
||||
import json
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
from flask import url_for
|
||||
@@ -12,6 +14,7 @@ from app.main.views.dashboard import (
|
||||
aggregate_template_usage,
|
||||
format_monthly_stats_to_list,
|
||||
get_dashboard_totals,
|
||||
get_local_daily_stats_for_last_x_days,
|
||||
get_tuples_of_financial_years,
|
||||
)
|
||||
from tests import (
|
||||
@@ -193,6 +196,12 @@ mock_daily_stats_by_user = {
|
||||
},
|
||||
}
|
||||
|
||||
mock_service_message_ratio = {
|
||||
"messages_remaining": 71919,
|
||||
"messages_sent": 28081,
|
||||
"total_message_limit": 100000,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user",
|
||||
@@ -252,12 +261,8 @@ def test_get_started(
|
||||
)
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get(
|
||||
"main.service_dashboard",
|
||||
@@ -286,12 +291,8 @@ def test_get_started_is_hidden_once_templates_exist(
|
||||
)
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get(
|
||||
"main.service_dashboard",
|
||||
@@ -317,12 +318,8 @@ def test_inbound_messages_not_visible_to_service_without_permissions(
|
||||
service_one["permissions"] = []
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get(
|
||||
"main.service_dashboard",
|
||||
@@ -348,12 +345,8 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_messages(
|
||||
):
|
||||
service_one["permissions"] = ["inbound_sms"]
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get(
|
||||
"main.service_dashboard",
|
||||
@@ -384,12 +377,8 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
|
||||
):
|
||||
service_one["permissions"] = ["inbound_sms"]
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get(
|
||||
"main.service_dashboard",
|
||||
@@ -638,12 +627,8 @@ def test_should_show_recent_templates_on_dashboard(
|
||||
)
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get(
|
||||
"main.service_dashboard",
|
||||
@@ -690,12 +675,8 @@ def test_should_not_show_recent_templates_on_dashboard_if_only_one_template_used
|
||||
)
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||
main = page.select_one("main").text
|
||||
@@ -849,12 +830,8 @@ def test_should_show_upcoming_jobs_on_dashboard(
|
||||
mock_get_inbound_sms_summary,
|
||||
):
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get(
|
||||
"main.service_dashboard",
|
||||
@@ -896,12 +873,8 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_count_is_0(
|
||||
)
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get(
|
||||
"main.service_dashboard",
|
||||
@@ -926,12 +899,8 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_service_has_no_jobs(
|
||||
):
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get(
|
||||
"main.service_dashboard",
|
||||
@@ -1016,12 +985,8 @@ def test_should_not_show_jobs_on_dashboard_for_users_with_uploads_page(
|
||||
mock_get_inbound_sms_summary,
|
||||
):
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get(
|
||||
"main.service_dashboard",
|
||||
@@ -1284,12 +1249,8 @@ def test_menu_send_messages(
|
||||
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = _test_dashboard_menu(
|
||||
client_request,
|
||||
@@ -1325,12 +1286,8 @@ def test_menu_manage_service(
|
||||
):
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = _test_dashboard_menu(
|
||||
client_request,
|
||||
@@ -1366,12 +1323,8 @@ def test_menu_main_settings(
|
||||
):
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = _test_settings_menu(
|
||||
client_request,
|
||||
@@ -1406,12 +1359,8 @@ def test_menu_manage_api_keys(
|
||||
):
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = _test_dashboard_menu(
|
||||
client_request,
|
||||
@@ -1447,12 +1396,8 @@ def test_menu_all_services_for_platform_admin_user(
|
||||
):
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = _test_dashboard_menu(
|
||||
client_request, mocker, platform_admin_user, service_one, []
|
||||
@@ -1490,12 +1435,8 @@ def test_route_for_service_permissions(
|
||||
)
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
validate_route_permission(
|
||||
mocker,
|
||||
@@ -1639,12 +1580,8 @@ def test_org_breadcrumbs_do_not_show_if_service_has_no_org(
|
||||
):
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||
|
||||
@@ -1710,12 +1647,8 @@ def test_org_breadcrumbs_show_if_user_is_a_member_of_the_services_org(
|
||||
)
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
|
||||
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||
@@ -1750,12 +1683,8 @@ def test_org_breadcrumbs_do_not_show_if_user_is_a_member_of_the_services_org_but
|
||||
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
|
||||
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||
@@ -1792,12 +1721,8 @@ def test_org_breadcrumbs_show_if_user_is_platform_admin(
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
|
||||
client_request.login(platform_admin_user, service_one_json)
|
||||
@@ -1829,14 +1754,9 @@ def test_breadcrumb_shows_if_service_is_suspended(
|
||||
)
|
||||
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
|
||||
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||
@@ -1869,12 +1789,8 @@ def test_service_dashboard_shows_usage(
|
||||
)
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
|
||||
service_one["permissions"] = permissions
|
||||
@@ -1909,12 +1825,8 @@ def test_service_dashboard_shows_free_allowance(
|
||||
)
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
|
||||
|
||||
@@ -1930,12 +1842,8 @@ def test_service_dashboard_shows_batched_jobs(
|
||||
):
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value=mock_daily_stats_by_user,
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value=mock_service_message_ratio,
|
||||
)
|
||||
|
||||
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||
@@ -1945,3 +1853,155 @@ def test_service_dashboard_shows_batched_jobs(
|
||||
assert len(rows) == 1
|
||||
|
||||
assert job_table_body is not None
|
||||
|
||||
|
||||
@patch("app.main.views.dashboard.datetime", wraps=datetime)
|
||||
def test_simple_local_conversion(mock_dt):
|
||||
stats_utc = {
|
||||
"2025-02-24T15:00:00Z": {
|
||||
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
"2025-02-25T07:00:00Z": {
|
||||
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
}
|
||||
|
||||
# Mock today's date in local time: 2025-02-25 at 10:00
|
||||
mock_dt.now.return_value = datetime(2025, 2, 25, 10, 0, 0, tzinfo=ZoneInfo("America/New_York"))
|
||||
|
||||
local_stats = get_local_daily_stats_for_last_x_days(
|
||||
stats_utc,
|
||||
"America/New_York",
|
||||
days=2
|
||||
)
|
||||
|
||||
assert len(local_stats) == 2
|
||||
assert "2025-02-24" in local_stats
|
||||
assert "2025-02-25" in local_stats
|
||||
|
||||
assert local_stats["2025-02-24"]["sms"]["delivered"] == 1
|
||||
assert local_stats["2025-02-24"]["sms"]["requested"] == 1
|
||||
assert local_stats["2025-02-25"]["sms"]["delivered"] == 2
|
||||
assert local_stats["2025-02-25"]["sms"]["requested"] == 2
|
||||
|
||||
|
||||
def test_no_timestamps_returns_zeroed_days():
|
||||
stats_utc = {}
|
||||
|
||||
class MockDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return cls(2025, 2, 26, 8, 0, 0, tzinfo=tz)
|
||||
|
||||
with patch("app.main.views.dashboard.datetime", MockDateTime):
|
||||
local_stats = get_local_daily_stats_for_last_x_days(
|
||||
stats_utc, "America/New_York", days=3
|
||||
)
|
||||
assert list(local_stats.keys()) == ["2025-02-24", "2025-02-25", "2025-02-26"]
|
||||
for day in local_stats:
|
||||
for msg_type in ["sms", "email"]:
|
||||
for status in ["delivered", "failure", "pending", "requested"]:
|
||||
assert local_stats[day][msg_type][status] == 0
|
||||
|
||||
|
||||
def test_timestamp_in_future_time_zone():
|
||||
stats_utc = {
|
||||
"2025-02-25T01:00:00Z": {
|
||||
"sms": {"delivered": 5, "failure": 0, "pending": 0, "requested": 5},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
}
|
||||
}
|
||||
|
||||
class MockDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return cls(2025, 2, 25, 10, 0, 0, tzinfo=tz)
|
||||
|
||||
with patch("app.main.views.dashboard.datetime", MockDateTime):
|
||||
local_stats = get_local_daily_stats_for_last_x_days(
|
||||
stats_utc, "Asia/Shanghai", days=1
|
||||
)
|
||||
|
||||
assert list(local_stats.keys()) == ["2025-02-25"]
|
||||
assert local_stats["2025-02-25"]["sms"]["delivered"] == 5
|
||||
|
||||
|
||||
def test_many_timestamps_one_local_day():
|
||||
stats_utc = {
|
||||
"2025-02-24T05:00:00Z": {
|
||||
"sms": {"delivered": 2, "failure": 1, "pending": 0, "requested": 3},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
"2025-02-24T09:30:00Z": {
|
||||
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
|
||||
"email": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
|
||||
},
|
||||
"2025-02-24T16:59:59Z": {
|
||||
"sms": {"delivered": 4, "failure": 0, "pending": 0, "requested": 4},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
}
|
||||
|
||||
class MockDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return cls(2025, 2, 24, 20, 0, 0, tzinfo=tz)
|
||||
|
||||
with patch("app.main.views.dashboard.datetime", MockDateTime):
|
||||
local_stats = get_local_daily_stats_for_last_x_days(
|
||||
stats_utc, "America/New_York", days=1
|
||||
)
|
||||
|
||||
assert list(local_stats.keys()) == ["2025-02-24"]
|
||||
assert local_stats["2025-02-24"]["sms"]["delivered"] == 7
|
||||
assert local_stats["2025-02-24"]["sms"]["requested"] == 8
|
||||
assert local_stats["2025-02-24"]["email"]["delivered"] == 2
|
||||
assert local_stats["2025-02-24"]["email"]["requested"] == 2
|
||||
|
||||
|
||||
def test_local_conversion_phoenix():
|
||||
"""Test aggregator logic in Mountain Time, no DST (America/Phoenix)."""
|
||||
stats_utc = {
|
||||
"2025-02-25T01:00:00Z": {
|
||||
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
"2025-02-25T12:00:00Z": {
|
||||
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
|
||||
"email": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
|
||||
},
|
||||
}
|
||||
|
||||
with patch("app.main.views.dashboard.datetime", wraps=datetime) as mock_dt:
|
||||
mock_dt.now.return_value = datetime(2025, 2, 25, 12, 0, 0, tzinfo=ZoneInfo("America/Phoenix"))
|
||||
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, "America/Phoenix", days=1)
|
||||
|
||||
assert len(local_stats) == 1
|
||||
(day_key,) = local_stats.keys()
|
||||
sms_delivered = local_stats[day_key]["sms"]["delivered"]
|
||||
assert sms_delivered in (2, 3, 4)
|
||||
|
||||
|
||||
def test_local_conversion_honolulu():
|
||||
"""Test aggregator logic in Hawaii (Pacific/Honolulu)."""
|
||||
stats_utc = {
|
||||
"2025-02-25T03:00:00Z": {
|
||||
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
"2025-02-25T21:00:00Z": {
|
||||
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
|
||||
"email": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
|
||||
},
|
||||
}
|
||||
|
||||
with patch("app.main.views.dashboard.datetime", wraps=datetime) as mock_dt:
|
||||
mock_dt.now.return_value = datetime(2025, 2, 25, 12, 0, 0, tzinfo=ZoneInfo("Pacific/Honolulu"))
|
||||
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, "Pacific/Honolulu", days=1)
|
||||
|
||||
assert len(local_stats) == 1
|
||||
(day_key,) = local_stats.keys()
|
||||
total_requested = local_stats[day_key]["sms"]["requested"]
|
||||
assert total_requested in (3, 1, 4)
|
||||
|
||||
@@ -128,25 +128,12 @@ def test_sign_out_user(
|
||||
# Check we are logged in
|
||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||
|
||||
date_range = {"start_date": "2024-01-01", "days": 7}
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats",
|
||||
"app.service_api_client.get_service_message_ratio",
|
||||
return_value={
|
||||
date_range["start_date"]: {
|
||||
"email": {"delivered": 0, "failure": 0, "requested": 0},
|
||||
"sms": {"delivered": 0, "failure": 1, "requested": 1},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||
return_value={
|
||||
date_range["start_date"]: {
|
||||
"email": {"delivered": 1, "failure": 0, "requested": 1},
|
||||
"sms": {"delivered": 1, "failure": 0, "requested": 1},
|
||||
},
|
||||
"messages_remaining": 71919,
|
||||
"messages_sent": 28081,
|
||||
"total_message_limit": 100000,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@ def test_should_show_empty_text_box(
|
||||
# data-module=autofocus is set on a containing element so it
|
||||
# shouldn’t also be set on the textbox itself
|
||||
assert "data-module" not in textbox
|
||||
|
||||
assert normalize_spaces(page.select_one("label[for=phone-number]").text) == "one"
|
||||
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ EXCLUDED_ENDPOINTS = tuple(
|
||||
"get_volumes_by_service",
|
||||
"get_example_csv",
|
||||
"get_notifications_as_json",
|
||||
"get_redis_report",
|
||||
"get_started",
|
||||
"get_started_old",
|
||||
"go_to_dashboard_after_tour",
|
||||
|
||||
@@ -199,7 +199,7 @@ test('Fetches data and creates chart and table correctly', async () => {
|
||||
|
||||
const data = await fetchData('service');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(`/services/${currentServiceId}/daily-stats.json`);
|
||||
expect(global.fetch).toHaveBeenCalledWith(`/services/${currentServiceId}/daily-stats.json?timezone=UTC`);
|
||||
expect(data).toEqual(mockResponse);
|
||||
|
||||
const labels = Object.keys(mockResponse).map(dateString => {
|
||||
|
||||
@@ -22,7 +22,7 @@ Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
||||
beforeEach(() => {
|
||||
// Set up the DOM with the D3 script included
|
||||
document.body.innerHTML = `
|
||||
<div id="totalMessageChartContainer" data-sms-sent="100" data-sms-allowance-remaining="249900" style="width: 600px;">
|
||||
<div id="totalMessageChartContainer" data-messages-sent="28081" data-messages-remaining="71919" style="width: 600px;">
|
||||
<h1 id="chartTitle">Total Messages</h1>
|
||||
<svg id="totalMessageChart"></svg>
|
||||
</div>
|
||||
@@ -76,8 +76,8 @@ test('Populates the accessible table correctly', () => {
|
||||
expect(headers[1].textContent).toBe('Remaining');
|
||||
|
||||
const firstRowCells = rows[1].getElementsByTagName('td');
|
||||
expect(firstRowCells[0].textContent).toBe('100');
|
||||
expect(firstRowCells[1].textContent).toBe('249,900');
|
||||
expect(firstRowCells[0].textContent).toBe('28,081');
|
||||
expect(firstRowCells[1].textContent).toBe('71,919');
|
||||
});
|
||||
|
||||
// Test to check if the chart title is correctly set
|
||||
@@ -114,7 +114,7 @@ test('Chart resizes correctly on window resize', done => {
|
||||
// Testing the tooltip
|
||||
test('Tooltip displays on hover', () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="totalMessageChartContainer" data-sms-sent="100" data-sms-allowance-remaining="249900" style="width: 600px;">
|
||||
<div id="totalMessageChartContainer" data-messages-sent="100" data-messages-remaining="249900" style="width: 600px;">
|
||||
<h1 id="chartTitle">Total Messages</h1>
|
||||
<svg id="totalMessageChart"></svg>
|
||||
</div>
|
||||
|
||||
1
zap.conf
1
zap.conf
@@ -53,6 +53,7 @@
|
||||
10096 WARN (Timestamp Disclosure - Passive/release)
|
||||
10097 WARN (Hash Disclosure - Passive/beta)
|
||||
10098 WARN (Cross-Domain Misconfiguration - Passive/release)
|
||||
10099 IGNORE (Source Code Disclosure - Java)
|
||||
10104 WARN (User Agent Fuzzer - Active/beta)
|
||||
10105 WARN (Weak Authentication Method - Passive/release)
|
||||
10106 IGNORE (HTTP Only Site - Active/beta)
|
||||
|
||||
Reference in New Issue
Block a user