From 93d7ca405254e53492d4cc8947344c5cf46fb355 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 6 May 2024 17:23:38 -0700 Subject: [PATCH 01/43] added pending and requested status to monthly stats dict --- app/main/views/dashboard.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 8453ef369..89d05d6a6 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -24,6 +24,7 @@ from app.utils import ( DELIVERED_STATUSES, FAILURE_STATUSES, REQUESTED_STATUSES, + SENDING_STATUSES, service_has_permission, ) from app.utils.csv import Spreadsheet @@ -341,6 +342,9 @@ def get_dashboard_partials(service_id): service_id, free_sms_fragment_limit=free_sms_allowance ) + monthly_stats = format_monthly_stats_to_list( + service_api_client.get_monthly_notification_stats(service_id, get_current_financial_year())["data"] + ) yearly_usage = billing_api_client.get_annual_usage_for_service( service_id, get_current_financial_year(), @@ -366,6 +370,7 @@ def get_dashboard_partials(service_id): ), "usage": render_template( "views/dashboard/_usage.html", + monthly_stats=monthly_stats, **get_annual_usage_breakdown(yearly_usage, free_sms_allowance), ), } @@ -424,6 +429,8 @@ def aggregate_status_types(counts_dict): "{}_counts".format(message_type): { "failed": sum(stats.get(status, 0) for status in FAILURE_STATUSES), "requested": sum(stats.get(status, 0) for status in REQUESTED_STATUSES), + "delivered": sum(stats.get(status, 0) for status in DELIVERED_STATUSES), + "pending": sum(stats.get(status, 0) for status in SENDING_STATUSES), } for message_type, stats in counts_dict.items() } From d045ecfa21bac5af1741b17db9e85d3e28c8d197 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Mon, 20 May 2024 10:45:34 -0700 Subject: [PATCH 02/43] remove easy targets --- app/__init__.py | 3 +- app/extensions.py | 2 - .../clients/antivirus/__init__.py | 0 .../clients/antivirus/antivirus_client.py | 55 ----- .../clients/encryption/__init__.py | 0 .../clients/encryption/encryption_client.py | 86 ------- .../clients/zendesk/__init__.py | 0 .../clients/zendesk/zendesk_client.py | 150 ------------ .../antivirus/test_antivirus_client.py | 71 ------ .../encryption/test_encryption_client.py | 88 ------- .../clients/zendesk/test_zendesk_client.py | 227 ------------------ 11 files changed, 1 insertion(+), 681 deletions(-) delete mode 100644 notifications_utils/clients/antivirus/__init__.py delete mode 100644 notifications_utils/clients/antivirus/antivirus_client.py delete mode 100644 notifications_utils/clients/encryption/__init__.py delete mode 100644 notifications_utils/clients/encryption/encryption_client.py delete mode 100644 notifications_utils/clients/zendesk/__init__.py delete mode 100644 notifications_utils/clients/zendesk/zendesk_client.py delete mode 100644 tests/notifications_utils/clients/antivirus/test_antivirus_client.py delete mode 100644 tests/notifications_utils/clients/encryption/test_encryption_client.py delete mode 100644 tests/notifications_utils/clients/zendesk/test_zendesk_client.py diff --git a/app/__init__.py b/app/__init__.py index 5c9283c79..6b0584a20 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -30,7 +30,7 @@ from werkzeug.local import LocalProxy from app import proxy_fix from app.asset_fingerprinter import asset_fingerprinter from app.config import configs -from app.extensions import redis_client, zendesk_client +from app.extensions import redis_client from app.formatters import ( convert_markdown_template, convert_to_boolean, @@ -202,7 +202,6 @@ def create_app(application): user_api_client, # External API clients redis_client, - zendesk_client, ): client.init_app(application) diff --git a/app/extensions.py b/app/extensions.py index 8bbb874a3..e322e46d0 100644 --- a/app/extensions.py +++ b/app/extensions.py @@ -1,5 +1,3 @@ from notifications_utils.clients.redis.redis_client import RedisClient -from notifications_utils.clients.zendesk.zendesk_client import ZendeskClient -zendesk_client = ZendeskClient() redis_client = RedisClient() diff --git a/notifications_utils/clients/antivirus/__init__.py b/notifications_utils/clients/antivirus/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/notifications_utils/clients/antivirus/antivirus_client.py b/notifications_utils/clients/antivirus/antivirus_client.py deleted file mode 100644 index affe8f27e..000000000 --- a/notifications_utils/clients/antivirus/antivirus_client.py +++ /dev/null @@ -1,55 +0,0 @@ -import requests -from flask import current_app - - -class AntivirusError(Exception): - def __init__(self, message=None, status_code=None): - self.message = message - self.status_code = status_code - - @classmethod - def from_exception(cls, e): - try: - message = e.response.json()["error"] - status_code = e.response.status_code - except (TypeError, ValueError, AttributeError, KeyError): - message = "connection error" - status_code = 503 - - return cls(message, status_code) - - -class AntivirusClient: - def __init__(self, api_host=None, auth_token=None): - self.api_host = api_host - self.auth_token = auth_token - - def init_app(self, app): - self.api_host = app.config["ANTIVIRUS_API_HOST"] - self.auth_token = app.config["ANTIVIRUS_API_KEY"] - - def scan(self, document_stream): - try: - response = requests.post( - "{}/scan".format(self.api_host), - headers={ - "Authorization": "Bearer {}".format(self.auth_token), - }, - files={"document": document_stream}, - ) - - response.raise_for_status() - - except requests.RequestException as e: - error = AntivirusError.from_exception(e) - current_app.logger.warning( - "Notify Antivirus API request failed with error: {}".format( - error.message - ) - ) - - raise error - finally: - document_stream.seek(0) - - return response.json()["ok"] diff --git a/notifications_utils/clients/encryption/__init__.py b/notifications_utils/clients/encryption/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/notifications_utils/clients/encryption/encryption_client.py b/notifications_utils/clients/encryption/encryption_client.py deleted file mode 100644 index cf5283208..000000000 --- a/notifications_utils/clients/encryption/encryption_client.py +++ /dev/null @@ -1,86 +0,0 @@ -from base64 import urlsafe_b64encode -from json import dumps, loads - -from cryptography.fernet import Fernet, InvalidToken -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from itsdangerous import BadSignature, URLSafeSerializer - - -class EncryptionError(Exception): - pass - - -class SaltLengthError(Exception): - pass - - -class Encryption: - def init_app(self, app): - self._serializer = URLSafeSerializer(app.config.get("SECRET_KEY")) - self._salt = app.config.get("DANGEROUS_SALT") - self._password = app.config.get("SECRET_KEY").encode() - - try: - self._shared_encryptor = Fernet(self._derive_key(self._salt)) - except SaltLengthError as reason: - raise EncryptionError( - "DANGEROUS_SALT must be at least 16 bytes" - ) from reason - - def encrypt(self, thing_to_encrypt, salt=None): - """Encrypt a string or object - - thing_to_encrypt must be serializable as JSON - Returns a UTF-8 string - """ - serialized_bytes = dumps(thing_to_encrypt).encode("utf-8") - encrypted_bytes = self._encryptor(salt).encrypt(serialized_bytes) - return encrypted_bytes.decode("utf-8") - - def decrypt(self, thing_to_decrypt, salt=None): - """Decrypt a UTF-8 string or bytes. - - Once decrypted, thing_to_decrypt must be deserializable from JSON. - """ - try: - return loads(self._encryptor(salt).decrypt(thing_to_decrypt)) - except InvalidToken as reason: - raise EncryptionError from reason - - def sign(self, thing_to_sign, salt=None): - return self._serializer.dumps(thing_to_sign, salt=(salt or self._salt)) - - def verify_signature(self, thing_to_verify, salt=None): - try: - return self._serializer.loads(thing_to_verify, salt=(salt or self._salt)) - except BadSignature as reason: - raise EncryptionError from reason - - def _encryptor(self, salt=None): - if salt is None: - return self._shared_encryptor - else: - try: - return Fernet(self._derive_key(salt)) - except SaltLengthError as reason: - raise EncryptionError( - "Custom salt value must be at least 16 bytes" - ) from reason - - def _derive_key(self, salt): - """Derive a key suitable for use within Fernet from the SECRET_KEY and salt - - * For the salt to be secure, it must be 16 bytes or longer and randomly generated. - * 600_000 was chosen for the iterations because it is what OWASP recommends as - * of [February 2023](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) - * For more information, see https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#pbkdf2 - * and https://cryptography.io/en/latest/fernet/#using-passwords-with-fernet - """ - salt_bytes = salt.encode() - if len(salt_bytes) < 16: - raise SaltLengthError - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), length=32, salt=salt_bytes, iterations=600_000 - ) - return urlsafe_b64encode(kdf.derive(self._password)) diff --git a/notifications_utils/clients/zendesk/__init__.py b/notifications_utils/clients/zendesk/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/notifications_utils/clients/zendesk/zendesk_client.py b/notifications_utils/clients/zendesk/zendesk_client.py deleted file mode 100644 index c5c2c5d02..000000000 --- a/notifications_utils/clients/zendesk/zendesk_client.py +++ /dev/null @@ -1,150 +0,0 @@ -import requests -from flask import current_app - - -class ZendeskError(Exception): - def __init__(self, response): - self.response = response - - -class ZendeskClient: - # the account used to authenticate with. If no requester is provided, the ticket will come from this account. - NOTIFY_ZENDESK_EMAIL = "zd-api-notify@digital.cabinet-office.gov.uk" - - ZENDESK_TICKET_URL = "https://govuk.zendesk.com/api/v2/tickets.json" - - def __init__(self): - self.api_key = None - - def init_app(self, app, *args, **kwargs): - self.api_key = app.config.get("ZENDESK_API_KEY") - - def send_ticket_to_zendesk(self, ticket): - response = requests.post( - self.ZENDESK_TICKET_URL, - json=ticket.request_data, - auth=(f"{self.NOTIFY_ZENDESK_EMAIL}/token", self.api_key), - ) - - if response.status_code != 201: - current_app.logger.error( - f"Zendesk create ticket request failed with {response.status_code} '{response.json()}'" - ) - raise ZendeskError(response) - - ticket_id = response.json()["ticket"]["id"] - - current_app.logger.info(f"Zendesk create ticket {ticket_id} succeeded") - - -class NotifySupportTicket: - PRIORITY_URGENT = "urgent" - PRIORITY_HIGH = "high" - PRIORITY_NORMAL = "normal" - PRIORITY_LOW = "low" - - TAGS_P2 = "govuk_notify_support" - TAGS_P1 = "govuk_notify_emergency" - - TYPE_PROBLEM = "problem" - TYPE_INCIDENT = "incident" - TYPE_QUESTION = "question" - TYPE_TASK = "task" - - # Group: 3rd Line--Notify Support - NOTIFY_GROUP_ID = 360000036529 - # Organization: GDS - NOTIFY_ORG_ID = 21891972 - NOTIFY_TICKET_FORM_ID = 1900000284794 - - def __init__( - self, - subject, - message, - ticket_type, - p1=False, - user_name=None, - user_email=None, - requester_sees_message_content=True, - technical_ticket=False, - ticket_categories=None, - org_id=None, - org_type=None, - service_id=None, - email_ccs=None, - ): - self.subject = subject - self.message = message - self.ticket_type = ticket_type - self.p1 = p1 - self.user_name = user_name - self.user_email = user_email - self.requester_sees_message_content = requester_sees_message_content - self.technical_ticket = technical_ticket - self.ticket_categories = ticket_categories or [] - self.org_id = org_id - self.org_type = org_type - self.service_id = service_id - self.email_ccs = email_ccs - - @property - def request_data(self): - data = { - "ticket": { - "subject": self.subject, - "comment": { - "body": self.message, - "public": self.requester_sees_message_content, - }, - "group_id": self.NOTIFY_GROUP_ID, - "organization_id": self.NOTIFY_ORG_ID, - "ticket_form_id": self.NOTIFY_TICKET_FORM_ID, - "priority": self.PRIORITY_URGENT if self.p1 else self.PRIORITY_NORMAL, - "tags": [self.TAGS_P1 if self.p1 else self.TAGS_P2], - "type": self.ticket_type, - "custom_fields": self._get_custom_fields(), - } - } - - if self.email_ccs: - data["ticket"]["email_ccs"] = [ - {"user_email": email, "action": "put"} for email in self.email_ccs - ] - - # if no requester provided, then the call came from within Notify đŸ‘» - if self.user_email: - data["ticket"]["requester"] = { - "email": self.user_email, - "name": self.user_name or "(no name supplied)", - } - - return data - - def _get_custom_fields(self): - technical_ticket_tag = ( - f'notify_ticket_type_{"" if self.technical_ticket else "non_"}technical' - ) - org_type_tag = f"notify_org_type_{self.org_type}" if self.org_type else None - - return [ - { - "id": "1900000744994", - "value": technical_ticket_tag, - }, # Notify Ticket type field - { - "id": "360022836500", - "value": self.ticket_categories, - }, # Notify Ticket category field - { - "id": "360022943959", - "value": self.org_id, - }, # Notify Organisation ID field - { - "id": "360022943979", - "value": org_type_tag, - }, # Notify Organisation type field - { - "id": "1900000745014", - "value": self.service_id, - }, # Notify Service ID field - ] diff --git a/tests/notifications_utils/clients/antivirus/test_antivirus_client.py b/tests/notifications_utils/clients/antivirus/test_antivirus_client.py deleted file mode 100644 index e19327e55..000000000 --- a/tests/notifications_utils/clients/antivirus/test_antivirus_client.py +++ /dev/null @@ -1,71 +0,0 @@ -import io - -import pytest -import requests - -from notifications_utils.clients.antivirus.antivirus_client import ( - AntivirusClient, - AntivirusError, -) - - -@pytest.fixture() -def antivirus(app, mocker): - client = AntivirusClient() - app.config["ANTIVIRUS_API_HOST"] = "https://antivirus" - app.config["ANTIVIRUS_API_KEY"] = "test-antivirus-key" - client.init_app(app) - return client - - -def test_scan_document(antivirus, rmock): - document = io.BytesIO(b"filecontents") - rmock.request( - "POST", - "https://antivirus/scan", - json={"ok": True}, - request_headers={ - "Authorization": "Bearer test-antivirus-key", - }, - status_code=200, - ) - - resp = antivirus.scan(document) - - assert resp - assert "filecontents" in rmock.last_request.text - assert document.tell() == 0 - - -def test_should_raise_for_status(antivirus, rmock): - with pytest.raises(AntivirusError) as excinfo: - _test_one_statement_for_status(antivirus, rmock) - - assert excinfo.value.message == "Antivirus error" - assert excinfo.value.status_code == 400 - - -def _test_one_statement_for_status(antivirus, rmock): - rmock.request( - "POST", - "https://antivirus/scan", - json={"error": "Antivirus error"}, - status_code=400, - ) - - antivirus.scan(io.BytesIO(b"document")) - - -def test_should_raise_for_connection_errors(antivirus, rmock): - with pytest.raises(AntivirusError) as excinfo: - _test_one_statement_for_connection_errors(antivirus, rmock) - - assert excinfo.value.message == "connection error" - assert excinfo.value.status_code == 503 - - -def _test_one_statement_for_connection_errors(antivirus, rmock): - rmock.request( - "POST", "https://antivirus/scan", exc=requests.exceptions.ConnectTimeout - ) - antivirus.scan(io.BytesIO(b"document")) diff --git a/tests/notifications_utils/clients/encryption/test_encryption_client.py b/tests/notifications_utils/clients/encryption/test_encryption_client.py deleted file mode 100644 index c392ba529..000000000 --- a/tests/notifications_utils/clients/encryption/test_encryption_client.py +++ /dev/null @@ -1,88 +0,0 @@ -import pytest - -from notifications_utils.clients.encryption.encryption_client import ( - Encryption, - EncryptionError, -) - - -@pytest.fixture() -def encryption_client(app): - client = Encryption() - - app.config["SECRET_KEY"] = "test-notify-secret-key" - app.config["DANGEROUS_SALT"] = "test-notify-salt" - - client.init_app(app) - - return client - - -def test_should_ensure_shared_salt_security(app): - client = Encryption() - app.config["SECRET_KEY"] = "test-notify-secret-key" - app.config["DANGEROUS_SALT"] = "too-short" - with pytest.raises(EncryptionError): - client.init_app(app) - - -def test_should_ensure_custom_salt_security(encryption_client): - with pytest.raises(EncryptionError): - encryption_client.encrypt("this", salt="too-short") - - -def test_should_encrypt_strings(encryption_client): - encrypted = encryption_client.encrypt("this") - assert encrypted != "this" - assert isinstance(encrypted, str) - - -def test_should_encrypt_dicts(encryption_client): - to_encrypt = {"hello": "world"} - encrypted = encryption_client.encrypt(to_encrypt) - assert encrypted != to_encrypt - assert encryption_client.decrypt(encrypted) == to_encrypt - - -def test_encryption_is_nondeterministic(encryption_client): - first_run = encryption_client.encrypt("this") - second_run = encryption_client.encrypt("this") - assert first_run != second_run - - -def test_should_decrypt_content(encryption_client): - encrypted = encryption_client.encrypt("this") - assert encryption_client.decrypt(encrypted) == "this" - - -def test_should_decrypt_content_with_custom_salt(encryption_client): - salt = "different-salt-value" - encrypted = encryption_client.encrypt("this", salt=salt) - assert encryption_client.decrypt(encrypted, salt=salt) == "this" - - -def test_should_verify_decryption(encryption_client): - encrypted = encryption_client.encrypt("this") - with pytest.raises(EncryptionError): - encryption_client.decrypt(encrypted, salt="different-salt-value") - - -def test_should_sign_and_serialize_string(encryption_client): - signed = encryption_client.sign("this") - assert signed != "this" - - -def test_should_verify_signature_and_deserialize_string(encryption_client): - signed = encryption_client.sign("this") - assert encryption_client.verify_signature(signed) == "this" - - -def test_should_raise_encryption_error_on_bad_salt(encryption_client): - signed = encryption_client.sign("this") - with pytest.raises(EncryptionError): - encryption_client.verify_signature(signed, salt="different-salt-value") - - -def test_should_sign_and_serialize_json(encryption_client): - signed = encryption_client.sign({"this": "that"}) - assert encryption_client.verify_signature(signed) == {"this": "that"} diff --git a/tests/notifications_utils/clients/zendesk/test_zendesk_client.py b/tests/notifications_utils/clients/zendesk/test_zendesk_client.py deleted file mode 100644 index d89bf466f..000000000 --- a/tests/notifications_utils/clients/zendesk/test_zendesk_client.py +++ /dev/null @@ -1,227 +0,0 @@ -from base64 import b64decode - -import pytest - -from notifications_utils.clients.zendesk.zendesk_client import ( - NotifySupportTicket, - ZendeskClient, - ZendeskError, -) - - -@pytest.fixture() -def zendesk_client(app): - client = ZendeskClient() - - app.config["ZENDESK_API_KEY"] = "testkey" - - client.init_app(app) - - return client - - -def test_zendesk_client_send_ticket_to_zendesk(zendesk_client, app, mocker, rmock): - rmock.request( - "POST", - ZendeskClient.ZENDESK_TICKET_URL, - status_code=201, - json={ - "ticket": { - "id": 12345, - "subject": "Something is wrong", - } - }, - ) - mock_logger = mocker.patch.object(app.logger, "info") - - ticket = NotifySupportTicket("subject", "message", "incident") - zendesk_client.send_ticket_to_zendesk(ticket) - - assert rmock.last_request.headers["Authorization"][:6] == "Basic " - b64_auth = rmock.last_request.headers["Authorization"][6:] - assert ( - b64decode(b64_auth.encode()).decode() - == "zd-api-notify@digital.cabinet-office.gov.uk/token:testkey" - ) - assert rmock.last_request.json() == ticket.request_data - mock_logger.assert_called_once_with("Zendesk create ticket 12345 succeeded") - - -def test_zendesk_client_send_ticket_to_zendesk_error( - zendesk_client, app, mocker, rmock -): - rmock.request( - "POST", ZendeskClient.ZENDESK_TICKET_URL, status_code=401, json={"foo": "bar"} - ) - - mock_logger = mocker.patch.object(app.logger, "error") - - ticket = NotifySupportTicket("subject", "message", "incident") - - with pytest.raises(ZendeskError): - zendesk_client.send_ticket_to_zendesk(ticket) - - mock_logger.assert_called_with( - "Zendesk create ticket request failed with 401 '{'foo': 'bar'}'" - ) - - -@pytest.mark.parametrize( - ("p1_arg", "expected_tags", "expected_priority"), - [ - ( - {}, - ["govuk_notify_support"], - "normal", - ), - ( - { - "p1": False, - }, - ["govuk_notify_support"], - "normal", - ), - ( - { - "p1": True, - }, - ["govuk_notify_emergency"], - "urgent", - ), - ], -) -def test_notify_support_ticket_request_data(p1_arg, expected_tags, expected_priority): - notify_ticket_form = NotifySupportTicket("subject", "message", "question", **p1_arg) - - assert notify_ticket_form.request_data == { - "ticket": { - "subject": "subject", - "comment": { - "body": "message", - "public": True, - }, - "group_id": NotifySupportTicket.NOTIFY_GROUP_ID, - "organization_id": NotifySupportTicket.NOTIFY_ORG_ID, - "ticket_form_id": NotifySupportTicket.NOTIFY_TICKET_FORM_ID, - "priority": expected_priority, - "tags": expected_tags, - "type": "question", - "custom_fields": [ - {"id": "1900000744994", "value": "notify_ticket_type_non_technical"}, - {"id": "360022836500", "value": []}, - {"id": "360022943959", "value": None}, - {"id": "360022943979", "value": None}, - {"id": "1900000745014", "value": None}, - ], - } - } - - -def test_notify_support_ticket_request_data_with_message_hidden_from_requester(): - notify_ticket_form = NotifySupportTicket( - "subject", "message", "problem", requester_sees_message_content=False - ) - - assert notify_ticket_form.request_data["ticket"]["comment"]["public"] is False - - -@pytest.mark.parametrize( - ("name", "zendesk_name"), [("Name", "Name"), (None, "(no name supplied)")] -) -def test_notify_support_ticket_request_data_with_user_name_and_email( - name, zendesk_name -): - notify_ticket_form = NotifySupportTicket( - "subject", "message", "question", user_name=name, user_email="user@example.com" - ) - - assert ( - notify_ticket_form.request_data["ticket"]["requester"]["email"] - == "user@example.com" - ) - assert ( - notify_ticket_form.request_data["ticket"]["requester"]["name"] == zendesk_name - ) - - -@pytest.mark.parametrize( - ( - "custom_fields", - "tech_ticket_tag", - "categories", - "org_id", - "org_type", - "service_id", - ), - [ - ( - {"technical_ticket": True}, - "notify_ticket_type_technical", - [], - None, - None, - None, - ), - ( - {"technical_ticket": False}, - "notify_ticket_type_non_technical", - [], - None, - None, - None, - ), - ( - {"ticket_categories": ["notify_billing", "notify_bug"]}, - "notify_ticket_type_non_technical", - ["notify_billing", "notify_bug"], - None, - None, - None, - ), - ( - {"org_id": "1234", "org_type": "local"}, - "notify_ticket_type_non_technical", - [], - "1234", - "notify_org_type_local", - None, - ), - ( - {"service_id": "abcd", "org_type": "nhs"}, - "notify_ticket_type_non_technical", - [], - None, - "notify_org_type_nhs", - "abcd", - ), - ], -) -def test_notify_support_ticket_request_data_custom_fields( - custom_fields, - tech_ticket_tag, - categories, - org_id, - org_type, - service_id, -): - notify_ticket_form = NotifySupportTicket( - "subject", "message", "question", **custom_fields - ) - - assert notify_ticket_form.request_data["ticket"]["custom_fields"] == [ - {"id": "1900000744994", "value": tech_ticket_tag}, - {"id": "360022836500", "value": categories}, - {"id": "360022943959", "value": org_id}, - {"id": "360022943979", "value": org_type}, - {"id": "1900000745014", "value": service_id}, - ] - - -def test_notify_support_ticket_request_data_email_ccs(): - notify_ticket_form = NotifySupportTicket( - "subject", "message", "question", email_ccs=["someone@example.com"] - ) - - assert notify_ticket_form.request_data["ticket"]["email_ccs"] == [ - {"user_email": "someone@example.com", "action": "put"}, - ] From 99166321876a5c860a28cb8e808723eeaa684392 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Mon, 20 May 2024 10:55:26 -0700 Subject: [PATCH 03/43] set confidence back to 100% here. The 60% confidence is handled in a different PR --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fe27636d6..24b8862b5 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ py-test: ## Run python unit tests .PHONY: dead-code dead-code: - poetry run vulture ./app ./notifications_utils --min-confidence=60 + poetry run vulture ./app ./notifications_utils --min-confidence=100 .PHONY: e2e-test e2e-test: export NEW_RELIC_ENVIRONMENT=test From ad68c264503ddc9b8b5b6cdc1a9ed5b59aa6a388 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 20 May 2024 16:21:38 -0700 Subject: [PATCH 04/43] installing socketIO and testing the WebSocket connection --- app/__init__.py | 3 +++ app/assets/javascripts/socket.js | 23 ++++++++++++++++++++ app/main/views/dashboard.py | 12 ++++++++++ app/templates/new/components/head.html | 1 + app/templates/views/dashboard/dashboard.html | 3 +++ gulpfile.js | 1 + 6 files changed, 43 insertions(+) create mode 100644 app/assets/javascripts/socket.js diff --git a/app/__init__.py b/app/__init__.py index 5c9283c79..7ecc5dd3b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -18,6 +18,7 @@ from flask import ( ) from flask.globals import request_ctx from flask_login import LoginManager, current_user +from flask_socketio import SocketIO from flask_talisman import Talisman from flask_wtf import CSRFProtect from flask_wtf.csrf import CSRFError @@ -118,6 +119,7 @@ from notifications_utils.recipients import format_phone_number_human_readable login_manager = LoginManager() csrf = CSRFProtect() talisman = Talisman() +socketio = SocketIO() # The current service attached to the request stack. @@ -175,6 +177,7 @@ def create_app(application): init_govuk_frontend(application) init_jinja(application) + socketio.init_app(application) for client in ( csrf, diff --git a/app/assets/javascripts/socket.js b/app/assets/javascripts/socket.js new file mode 100644 index 000000000..608fa0bde --- /dev/null +++ b/app/assets/javascripts/socket.js @@ -0,0 +1,23 @@ + +(function (window) { + document.addEventListener('DOMContentLoaded', (event) => { + var socket = io(); + + socket.on('connect', function() { + console.log('Connected to the server'); + }); + + socket.on('message', function(msg) { + var li = document.createElement("li"); + li.appendChild(document.createTextNode(msg)); + document.getElementById("messages").appendChild(li); + }); + + document.getElementById('sendButton').addEventListener('click', function() { + var message = document.getElementById("message").value; + socket.send(message); + document.getElementById("message").value = ''; + }); + }); + +})(window); diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 3bbf432b3..042cbe94e 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -6,6 +6,7 @@ from itertools import groupby from flask import Response, abort, jsonify, render_template, request, session, url_for from flask_login import current_user +from flask_socketio import send, emit from werkzeug.utils import redirect from app import ( @@ -15,6 +16,7 @@ from app import ( notification_api_client, service_api_client, template_statistics_client, + socketio ) from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left from app.main import main @@ -32,6 +34,16 @@ from app.utils.user import user_has_permissions from notifications_utils.recipients import format_phone_number_human_readable +@socketio.on('message') +def handle_message(msg): + print('''Message: + + + + ''' + msg) + emit('message', msg, broadcast=True) + + @main.route("/services//dashboard") @user_has_permissions("view_activity", "send_messages") def old_service_dashboard(service_id): diff --git a/app/templates/new/components/head.html b/app/templates/new/components/head.html index 51f3c4da3..f7c7153e2 100644 --- a/app/templates/new/components/head.html +++ b/app/templates/new/components/head.html @@ -32,6 +32,7 @@ {# google #} + {% if g.hide_from_search_engines %} diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html index 0a558ad8f..4a2751eb2 100644 --- a/app/templates/views/dashboard/dashboard.html +++ b/app/templates/views/dashboard/dashboard.html @@ -22,6 +22,9 @@ Messages sent + +
    + {{ ajax_block(partials, updates_url, 'inbox') }} {{ ajax_block(partials, updates_url, 'totals') }} diff --git a/gulpfile.js b/gulpfile.js index 98afbbacf..3b7d765a7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -126,6 +126,7 @@ const javascripts = () => { paths.src + 'javascripts/loginAlert.js', paths.src + 'javascripts/main.js', paths.src + 'javascripts/chartDashboard.js', + paths.src + 'javascripts/socket.js', ]) .pipe(plugins.prettyerror()) .pipe(plugins.babel({ From 17fec1c99e0a8e441d1887f2d4c53fa2cb0d7b37 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Tue, 28 May 2024 11:27:57 -0700 Subject: [PATCH 05/43] use moto to mock s3 --- notifications_utils/s3.py | 11 ++++ poetry.lock | 78 +++++++++++++++++++++++++++- pyproject.toml | 1 + tests/app/main/views/test_send.py | 14 +++++ tests/notifications_utils/test_s3.py | 2 + 5 files changed, 105 insertions(+), 1 deletion(-) diff --git a/notifications_utils/s3.py b/notifications_utils/s3.py index cdcc70a5c..a2886b381 100644 --- a/notifications_utils/s3.py +++ b/notifications_utils/s3.py @@ -38,6 +38,17 @@ def s3upload( region_name=region, ) _s3 = session.resource("s3", config=AWS_CLIENT_CONFIG) + # This 'proves' that use of moto in the relevant tests in test_send.py + # mocks everything related to S3. What you will see in the logs is: + # Exception: CREATED AT + # + # raise Exception(f"CREATED AT {_s3.Bucket(bucket_name).creation_date}") + if os.getenv("NOTIFY_ENVIRONMENT") == "test": + teststr = str(_s3.Bucket(bucket_name).creation_date).lower() + if "magicmock" not in teststr: + raise Exception( + f"xxxxxtest not mocked, use @mock_aws creation date is {teststr}" + ) key = _s3.Object(bucket_name, file_location) diff --git a/poetry.lock b/poetry.lock index 80a341491..5a32fc4d4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1297,6 +1297,7 @@ files = [ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, + {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, @@ -1551,6 +1552,50 @@ files = [ {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, ] +[[package]] +name = "moto" +version = "5.0.8" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "moto-5.0.8-py2.py3-none-any.whl", hash = "sha256:7d1035e366434bfa9fcc0621f07d5aa724b6846408071d540137a0554c46f214"}, + {file = "moto-5.0.8.tar.gz", hash = "sha256:517fb808dc718bcbdda54c6ffeaca0adc34cf6e10821bfb01216ce420a31765c"}, +] + +[package.dependencies] +boto3 = ">=1.9.201" +botocore = ">=1.14.0" +cryptography = ">=3.3.1" +Jinja2 = ">=2.10.1" +python-dateutil = ">=2.1,<3.0.0" +requests = ">=2.5" +responses = ">=0.15.0" +werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" +xmltodict = "*" + +[package.extras] +all = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.5)", "pyparsing (>=3.0.7)", "setuptools"] +apigateway = ["PyYAML (>=5.1)", "joserfc (>=0.9.0)", "openapi-spec-validator (>=0.5.0)"] +apigatewayv2 = ["PyYAML (>=5.1)", "openapi-spec-validator (>=0.5.0)"] +appsync = ["graphql-core"] +awslambda = ["docker (>=3.0.0)"] +batch = ["docker (>=3.0.0)"] +cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.5)", "pyparsing (>=3.0.7)", "setuptools"] +cognitoidp = ["joserfc (>=0.9.0)"] +dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.5)"] +dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.5)"] +glue = ["pyparsing (>=3.0.7)"] +iotdata = ["jsondiff (>=1.1.2)"] +proxy = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=2.5.1)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.5)", "pyparsing (>=3.0.7)", "setuptools"] +resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.5)", "pyparsing (>=3.0.7)"] +s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.5.5)"] +s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.5.5)"] +server = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.5)", "pyparsing (>=3.0.7)", "setuptools"] +ssm = ["PyYAML (>=5.1)"] +stepfunctions = ["antlr4-python3-runtime", "jsonpath-ng"] +xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] + [[package]] name = "msgpack" version = "1.0.8" @@ -1613,6 +1658,7 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] @@ -2617,6 +2663,25 @@ requests = ">=2.22,<3" [package.extras] fixture = ["fixtures"] +[[package]] +name = "responses" +version = "0.25.0" +description = "A utility library for mocking out the `requests` Python library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "responses-0.25.0-py3-none-any.whl", hash = "sha256:2f0b9c2b6437db4b528619a77e5d565e4ec2a9532162ac1a131a83529db7be1a"}, + {file = "responses-0.25.0.tar.gz", hash = "sha256:01ae6a02b4f34e39bffceb0fc6786b67a25eae919c6368d05eabc8d9576c2a66"}, +] + +[package.dependencies] +pyyaml = "*" +requests = ">=2.30.0,<3.0" +urllib3 = ">=1.25.10,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] + [[package]] name = "rich" version = "13.7.1" @@ -2960,7 +3025,18 @@ files = [ {file = "xlwt-1.3.0.tar.gz", hash = "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88"}, ] +[[package]] +name = "xmltodict" +version = "0.13.0" +description = "Makes working with XML feel like you are working with JSON" +optional = false +python-versions = ">=3.4" +files = [ + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "6c271d919c3736a844fa3674c1db0891e4c09378e6656b396ff60c594e34a862" +content-hash = "8f58d29f819ca160e10740e9774b5e528675208f0e8f2a51fa88ea0a62ea1dc8" diff --git a/pyproject.toml b/pyproject.toml index 2fc0b3c14..c0da02466 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ flake8-print = "^5.0.0" flake8-pytest-style = "^1.7.2" isort = "^5.13.2" jinja2-cli = {version = "==0.8.2", extras = ["yaml"]} +moto="*" pip-audit = "*" pre-commit = "^3.7.1" pytest = "^8.2.1" diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index 449259d8d..efaee4043 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -11,6 +11,7 @@ from zipfile import BadZipFile import pytest from flask import url_for +from moto import mock_aws from notifications_python_client.errors import HTTPError from xlrd.biffh import XLRDError from xlrd.xldate import XLDateAmbiguous, XLDateError, XLDateNegative, XLDateTooLarge @@ -426,6 +427,7 @@ def test_example_spreadsheet( list(zip(test_spreadsheet_files, repeat(True), repeat(302))) + list(zip(test_non_spreadsheet_files, repeat(False), repeat(200))), ) +@mock_aws def test_upload_files_in_different_formats( filename, acceptable_file, @@ -465,6 +467,7 @@ def test_upload_files_in_different_formats( ) +@mock_aws def test_send_messages_sanitises_and_truncates_file_name_for_metadata( client_request, service_one, @@ -572,6 +575,7 @@ def test_shows_error_if_parsing_exception( ) +@mock_aws def test_upload_csv_file_with_errors_shows_check_page_with_errors( client_request, service_one, @@ -619,6 +623,7 @@ def test_upload_csv_file_with_errors_shows_check_page_with_errors( assert "Upload your file again" in page.text +@mock_aws def test_upload_csv_file_with_empty_message_shows_check_page_with_errors( client_request, service_one, @@ -671,6 +676,7 @@ def test_upload_csv_file_with_empty_message_shows_check_page_with_errors( assert page.select("tbody tr td")[1]["colspan"] == "2" +@mock_aws def test_upload_csv_file_with_very_long_placeholder_shows_check_page_with_errors( client_request, service_one, @@ -807,6 +813,7 @@ def test_upload_csv_file_with_very_long_placeholder_shows_check_page_with_errors ), ], ) +@mock_aws def test_upload_csv_file_with_missing_columns_shows_error( client_request, mocker, @@ -882,6 +889,7 @@ def test_upload_csv_size_too_big( assert "File must be smaller than 10Mb" in page.text +@mock_aws def test_upload_valid_csv_redirects_to_check_page( client_request, mock_get_service_template_with_placeholders, @@ -928,6 +936,7 @@ def test_upload_valid_csv_redirects_to_check_page( ), ], ) +@mock_aws def test_upload_valid_csv_shows_preview_and_table( client_request, mocker, @@ -1021,6 +1030,7 @@ def test_upload_valid_csv_shows_preview_and_table( assert normalize_spaces(str(row.select("td")[index])) == cell +@mock_aws def test_show_all_columns_if_there_are_duplicate_recipient_columns( client_request, mocker, @@ -1071,6 +1081,7 @@ def test_show_all_columns_if_there_are_duplicate_recipient_columns( (5, 404), ], ) +@mock_aws def test_404_for_previewing_a_row_out_of_range( client_request, mocker, @@ -1519,6 +1530,7 @@ def test_send_one_off_redirects_to_end_if_step_out_of_bounds( create_active_caseworking_user(), ], ) +@mock_aws def test_send_one_off_redirects_to_start_if_you_skip_steps( client_request, service_one, @@ -1623,6 +1635,7 @@ def test_send_one_off_sms_message_redirects( create_active_caseworking_user(), ], ) +@mock_aws def test_send_one_off_email_to_self_without_placeholders_redirects_to_check_page( client_request, mocker, @@ -1828,6 +1841,7 @@ def test_download_example_csv( assert "text/csv" in response.headers["Content-Type"] +@mock_aws def test_upload_csvfile_with_valid_phone_shows_all_numbers( client_request, mock_get_service_template, diff --git a/tests/notifications_utils/test_s3.py b/tests/notifications_utils/test_s3.py index 46b863c4f..d05fa8fdc 100644 --- a/tests/notifications_utils/test_s3.py +++ b/tests/notifications_utils/test_s3.py @@ -2,6 +2,7 @@ from urllib.parse import parse_qs import botocore import pytest +from moto import mock_aws from notifications_utils.s3 import S3ObjectNotFound, s3download, s3upload @@ -12,6 +13,7 @@ location = "some_file_location" content_type = "binary/octet-stream" +@mock_aws def test_s3upload_save_file_to_bucket(mocker): mocked = mocker.patch("notifications_utils.s3.Session.resource") s3upload( From 758c1cd8a0f59c337a60106536c7981898d8cc57 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Wed, 29 May 2024 12:13:09 -0700 Subject: [PATCH 06/43] added new api for month, by year, previous 7 day stats --- app/main/views/dashboard.py | 34 ++++++++++++++++++++----- app/notify_client/service_api_client.py | 18 +++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 89d05d6a6..33e98e104 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -19,6 +19,7 @@ from app import ( ) from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left from app.main import main +from app.models.user import User from app.statistics_utils import get_formatted_percentage from app.utils import ( DELIVERED_STATUSES, @@ -324,6 +325,11 @@ def aggregate_notifications_stats(template_statistics): def get_dashboard_partials(service_id): + current_financial_year = get_current_financial_year() + current_month = get_current_month_for_financial_year(current_financial_year) + start_date = datetime.now().strftime('%Y-%m-%d') + days=7 + all_statistics = template_statistics_client.get_template_statistics_for_service( service_id, limit_days=7 ) @@ -336,19 +342,30 @@ def get_dashboard_partials(service_id): ) # These 2 calls will update the dashboard sms allowance count while in trial mode. billing_api_client.get_monthly_usage_for_service( - service_id, get_current_financial_year() + service_id, current_financial_year ) billing_api_client.create_or_update_free_sms_fragment_limit( service_id, free_sms_fragment_limit=free_sms_allowance ) - - monthly_stats = format_monthly_stats_to_list( - service_api_client.get_monthly_notification_stats(service_id, get_current_financial_year())["data"] - ) yearly_usage = billing_api_client.get_annual_usage_for_service( service_id, - get_current_financial_year(), + current_financial_year, ) + + #Previous 7 day stats + daily_stats = service_api_client.get_service_notification_statistics_by_day(service_id, start_date=start_date, days=days) + + #Single month stats + single_month_notification_stats = service_api_client.get_single_month_notification_stats(service_id, year=current_financial_year, month=current_month) + + #monthly stats by year + monthly_stats = format_monthly_stats_to_list( + service_api_client.get_monthly_notification_stats(service_id, current_financial_year)["data"] + ) + + # user=User.from_id(user_id), + # single_month_notification_stats = service_api_client.get_single_month_notification_stats_by_user(service_id, user, year=current_financial_year, month=current_month) + return { "upcoming": render_template( "views/dashboard/_upcoming.html", @@ -441,6 +458,11 @@ def get_months_for_financial_year(year, time_format="%B"): return [month.strftime(time_format) for month in (get_months_for_year(1, 13, year))] +def get_current_month_for_financial_year(year): + current_month = datetime.now().month + return current_month + + def get_months_for_year(start, end, year): return [datetime(year, month, 1) for month in range(start, end)] diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index d34516b8b..f2cc2f934 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -43,6 +43,24 @@ class ServiceAPIClient(NotifyAdminAPIClient): params={"limit_days": limit_days}, )["data"] + def get_service_notification_statistics_by_day(self, service_id, start_date=None, days=None): + if start_date is None: + start_date = datetime.now().strftime('%Y-%m-%d') + + return self.get( + "/service/{0}/statistics/{1}/{2}".format(service_id, start_date, days), + )["data"] + + def get_single_month_notification_stats(self, service_id, year, month): + return self.get( + "/service/{0}/notifications/month?year={1}&month={2}".format(service_id, year, month), + ) + + # def get_single_month_notification_stats(self, service_id, user_id, year, month): + # return self.get( + # "/service/{0}/notifications//month?year={1}&month={2}".format(service_id, user_id, year, month), + # ) + def get_services(self, params_dict=None): """ Retrieve a list of services. From 9368f6a49622c4780d0ed1ddc8c0c9739128d9ad Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 29 May 2024 14:18:08 -0700 Subject: [PATCH 07/43] remove some fixtures --- .ds.baseline | 4 +- app/s3_client/s3_csv_client.py | 1 + notifications_utils/s3.py | 11 ++ tests/app/main/views/test_send.py | 217 ++++++++++++++++++++++-------- tests/conftest.py | 23 ---- 5 files changed, 174 insertions(+), 82 deletions(-) diff --git a/.ds.baseline b/.ds.baseline index cec28396c..859f30b4d 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -675,7 +675,7 @@ "filename": "tests/conftest.py", "hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea", "is_verified": false, - "line_number": 3289, + "line_number": 3266, "is_secret": false } ], @@ -710,5 +710,5 @@ } ] }, - "generated_at": "2024-05-20T16:03:05Z" + "generated_at": "2024-05-29T21:18:03Z" } diff --git a/app/s3_client/s3_csv_client.py b/app/s3_client/s3_csv_client.py index 21c329887..752f054a4 100644 --- a/app/s3_client/s3_csv_client.py +++ b/app/s3_client/s3_csv_client.py @@ -28,6 +28,7 @@ def get_csv_upload(service_id, upload_id): def s3upload(service_id, filedata): + upload_id = str(uuid.uuid4()) bucket_name, file_location, access_key, secret_key, region = get_csv_location( service_id, upload_id diff --git a/notifications_utils/s3.py b/notifications_utils/s3.py index a2886b381..b0a86844a 100644 --- a/notifications_utils/s3.py +++ b/notifications_utils/s3.py @@ -93,6 +93,17 @@ def s3download( ) s3 = session.resource("s3", config=AWS_CLIENT_CONFIG) key = s3.Object(bucket_name, filename) + # This 'proves' that use of moto in the relevant tests in test_send.py + # mocks everything related to S3. What you will see in the logs is: + # Exception: CREATED AT + # + # raise Exception(f"CREATED AT {_s3.Bucket(bucket_name).creation_date}") + if os.getenv("NOTIFY_ENVIRONMENT") == "test": + teststr = str(s3.Bucket(bucket_name).creation_date).lower() + if "magicmock" not in teststr: + raise Exception( + f"xxxxxtest not mocked, use @mock_aws creation date is {teststr}" + ) return key.get()["Body"] except botocore.exceptions.ClientError as error: raise S3ObjectNotFound(error.response, error.operation_name) diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index efaee4043..46240de5a 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -6,6 +6,7 @@ from io import BytesIO from itertools import repeat from os import path from random import randbytes +from unittest.mock import ANY from uuid import uuid4 from zipfile import BadZipFile @@ -18,7 +19,11 @@ from xlrd.xldate import XLDateAmbiguous, XLDateError, XLDateNegative, XLDateTooL from notifications_utils.recipients import RecipientCSV from notifications_utils.template import SMSPreviewTemplate -from tests import validate_route_permission, validate_route_permission_with_client +from tests import ( + sample_uuid, + validate_route_permission, + validate_route_permission_with_client, +) from tests.conftest import ( SERVICE_ONE_ID, create_active_caseworking_user, @@ -436,10 +441,15 @@ def test_upload_files_in_different_formats( service_one, mocker, mock_get_service_template, - mock_s3_set_metadata, - mock_s3_upload, fake_uuid, ): + + mock_s3_set_metadata = mocker.patch( + "app.main.views.send.set_metadata_on_csv_upload" + ) + + mock_s3_upload = mocker.patch("app.main.views.send.s3upload") + with open(filename, "rb") as uploaded: page = client_request.post( "main.send_messages", @@ -458,7 +468,7 @@ def test_upload_files_in_different_formats( "202 205 8823,Still Not Pete,Crimson,Pear" ) mock_s3_set_metadata.assert_called_once_with( - SERVICE_ONE_ID, fake_uuid, original_file_name=filename + SERVICE_ONE_ID, ANY, original_file_name=filename ) else: assert not mock_s3_upload.called @@ -473,13 +483,17 @@ def test_send_messages_sanitises_and_truncates_file_name_for_metadata( service_one, mocker, mock_get_service_template_with_placeholders, - mock_s3_set_metadata, - mock_s3_get_metadata, - mock_s3_upload, mock_s3_download, mock_get_job_doesnt_exist, fake_uuid, ): + + mock_s3_set_metadata = mocker.patch( + "app.main.views.send.set_metadata_on_csv_upload" + ) + + mocker.patch("app.main.views.send.s3upload") + filename = f"😁{'a' * 2000}.csv" client_request.post( @@ -581,15 +595,20 @@ def test_upload_csv_file_with_errors_shows_check_page_with_errors( service_one, mocker, mock_get_service_template_with_placeholders, - mock_s3_set_metadata, - mock_s3_get_metadata, - mock_s3_upload, mock_get_users_by_service, mock_get_service_statistics, mock_get_job_doesnt_exist, mock_get_jobs, fake_uuid, ): + + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) mocker.patch( "app.main.views.send.s3download", return_value=""" @@ -629,15 +648,21 @@ def test_upload_csv_file_with_empty_message_shows_check_page_with_errors( service_one, mocker, mock_get_empty_service_template_with_optional_placeholder, - mock_s3_set_metadata, - mock_s3_get_metadata, - mock_s3_upload, mock_get_users_by_service, mock_get_service_statistics, mock_get_job_doesnt_exist, mock_get_jobs, fake_uuid, ): + + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) + mocker.patch( "app.main.views.send.s3download", return_value=""" @@ -682,15 +707,20 @@ def test_upload_csv_file_with_very_long_placeholder_shows_check_page_with_errors service_one, mocker, mock_get_service_template_with_placeholders, - mock_s3_set_metadata, - mock_s3_get_metadata, - mock_s3_upload, mock_get_users_by_service, mock_get_service_statistics, mock_get_job_doesnt_exist, mock_get_jobs, fake_uuid, ): + + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) big_placeholder = " ".join(["not ok"] * 402) mocker.patch( "app.main.views.send.s3download", @@ -818,9 +848,6 @@ def test_upload_csv_file_with_missing_columns_shows_error( client_request, mocker, mock_get_service_template_with_placeholders, - mock_s3_set_metadata, - mock_s3_get_metadata, - mock_s3_upload, mock_get_users_by_service, mock_get_service_statistics, mock_get_job_doesnt_exist, @@ -830,6 +857,15 @@ def test_upload_csv_file_with_missing_columns_shows_error( file_contents, expected_error, ): + + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) + mocker.patch("app.main.views.send.s3download", return_value=file_contents) page = client_request.post( @@ -893,10 +929,13 @@ def test_upload_csv_size_too_big( def test_upload_valid_csv_redirects_to_check_page( client_request, mock_get_service_template_with_placeholders, - mock_s3_upload, - mock_s3_set_metadata, fake_uuid, + mocker, ): + + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) client_request.post( "main.send_messages", service_id=SERVICE_ONE_ID, @@ -936,7 +975,6 @@ def test_upload_valid_csv_redirects_to_check_page( ), ], ) -@mock_aws def test_upload_valid_csv_shows_preview_and_table( client_request, mocker, @@ -946,13 +984,16 @@ def test_upload_valid_csv_shows_preview_and_table( mock_get_service_statistics, mock_get_job_doesnt_exist, mock_get_jobs, - mock_s3_get_metadata, - mock_s3_set_metadata, fake_uuid, extra_args, expected_link_in_first_row, expected_message, ): + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) with client_request.session_transaction() as session: session["file_uploads"] = {fake_uuid: {"template_id": fake_uuid}} @@ -1041,8 +1082,12 @@ def test_show_all_columns_if_there_are_duplicate_recipient_columns( mock_get_job_doesnt_exist, mock_get_jobs, fake_uuid, - mock_s3_get_metadata, ): + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) with client_request.session_transaction() as session: session["file_uploads"] = {fake_uuid: {"template_id": fake_uuid}} @@ -1091,12 +1136,17 @@ def test_404_for_previewing_a_row_out_of_range( mock_get_service_statistics, mock_get_job_doesnt_exist, mock_get_jobs, - mock_s3_get_metadata, - mock_s3_set_metadata, fake_uuid, row_index, expected_status, ): + + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) with client_request.session_transaction() as session: session["file_uploads"] = {fake_uuid: {"template_id": fake_uuid}} @@ -1535,7 +1585,6 @@ def test_send_one_off_redirects_to_start_if_you_skip_steps( client_request, service_one, fake_uuid, - mock_s3_upload, mock_get_users_by_service, mock_get_service_statistics, mock_has_no_jobs, @@ -1641,7 +1690,6 @@ def test_send_one_off_email_to_self_without_placeholders_redirects_to_check_page mocker, service_one, mock_get_service_email_template_without_placeholders, - mock_s3_upload, mock_get_users_by_service, mock_get_service_statistics, mock_has_no_jobs, @@ -1849,13 +1897,20 @@ def test_upload_csvfile_with_valid_phone_shows_all_numbers( mock_get_live_service, mock_get_job_doesnt_exist, mock_get_jobs, - mock_s3_get_metadata, - mock_s3_set_metadata, service_one, fake_uuid, - mock_s3_upload, mocker, ): + + mock_s3_set_metadata = mocker.patch( + "app.main.views.send.set_metadata_on_csv_upload" + ) + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) mocker.patch( "app.main.views.send.s3download", return_value="\n".join( @@ -1909,9 +1964,6 @@ def test_upload_csvfile_with_international_validates( api_user_active, client_request, mock_get_service_template, - mock_s3_set_metadata, - mock_s3_get_metadata, - mock_s3_upload, mock_has_permissions, mock_get_users_by_service, mock_get_service_statistics, @@ -1922,6 +1974,14 @@ def test_upload_csvfile_with_international_validates( should_allow_international, service_one, ): + + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) if international_sms_permission: service_one["permissions"] += ("sms", "international_sms") mocker.patch( @@ -1961,10 +2021,15 @@ def test_test_message_can_only_be_sent_now( mock_get_service_statistics, mock_get_job_doesnt_exist, mock_get_jobs, - mock_s3_get_metadata, - mock_s3_set_metadata, fake_uuid, ): + + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) content = client_request.get( "main.check_messages", service_id=service_one["id"], @@ -1986,8 +2051,12 @@ def test_preview_button_is_correctly_labelled( mock_get_job_doesnt_exist, mock_get_jobs, fake_uuid, - mock_s3_get_metadata, ): + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) mocker.patch( "app.main.views.send.s3download", return_value="\n".join(["phone_number"] + (["2028670123"] * 1000)), @@ -2072,7 +2141,6 @@ def test_route_permissions( mock_get_jobs, mock_get_notifications, mock_create_job, - mock_s3_upload, fake_uuid, route, response_code, @@ -2106,8 +2174,8 @@ def test_route_permissions_send_check_notifications( response_code, method, mock_create_job, - mock_s3_upload, ): + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) with client_request.session_transaction() as session: session["recipient"] = "2028675301" session["placeholders"] = {"name": "a"} @@ -2187,8 +2255,6 @@ def test_check_messages_back_link( mock_get_job_doesnt_exist, mock_get_jobs, mock_s3_download, - mock_s3_get_metadata, - mock_s3_set_metadata, fake_uuid, mocker, template_type, @@ -2196,6 +2262,13 @@ def test_check_messages_back_link( extra_args, expected_url, ): + + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) content = "Hi there ((name))" if has_placeholders else "Hi there" template_data = create_template( template_id=fake_uuid, template_type=template_type, content=content @@ -2250,8 +2323,12 @@ def test_check_messages_shows_too_many_messages_errors( fake_uuid, num_requested, expected_msg, - mock_s3_get_metadata, ): + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) # csv with 100 phone numbers mocker.patch( "app.main.views.send.s3download", @@ -2295,7 +2372,6 @@ def test_check_messages_shows_too_many_messages_errors( def test_check_messages_shows_trial_mode_error( client_request, - mock_s3_get_metadata, mock_get_users_by_service, mock_get_service_template, mock_has_permissions, @@ -2305,6 +2381,11 @@ def test_check_messages_shows_trial_mode_error( fake_uuid, mocker, ): + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) mocker.patch( "app.main.views.send.s3download", return_value=("phone number,\n2028675209"), # Not in team @@ -2440,8 +2521,12 @@ def test_check_messages_column_error_doesnt_show_optional_columns( mock_get_service_statistics, mock_get_job_doesnt_exist, mock_get_jobs, - mock_s3_get_metadata, ): + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) mocker.patch( "app.main.views.send.s3download", return_value="\n".join( @@ -2482,10 +2567,17 @@ def test_check_messages_adds_sender_id_in_session_to_metadata( mock_get_service_statistics, mock_get_job_doesnt_exist, mock_get_jobs, - mock_s3_get_metadata, - mock_s3_set_metadata, fake_uuid, ): + + mock_s3_set_metadata = mocker.patch( + "app.main.views.send.set_metadata_on_csv_upload" + ) + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) mocker.patch( "app.main.views.send.s3download", return_value=("phone number,\n2028675209") ) @@ -2522,11 +2614,15 @@ def test_check_messages_shows_over_max_row_error( mock_get_service_statistics, mock_get_job_doesnt_exist, mock_get_jobs, - mock_s3_get_metadata, mock_s3_download, fake_uuid, mocker, ): + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) mock_recipients = mocker.patch("app.main.views.send.RecipientCSV").return_value mock_recipients.max_rows = 11111 mock_recipients.__len__.return_value = 99999 @@ -2676,8 +2772,8 @@ def test_send_notification_submits_data( expected_personalisation, mocker, mock_create_job, - mock_s3_upload, ): + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) with client_request.session_transaction() as session: session["recipient"] = recipient session["placeholders"] = placeholders @@ -2704,8 +2800,8 @@ def test_send_notification_clears_session( mock_get_service_template, mocker, mock_create_job, - mock_s3_upload, ): + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) with client_request.session_transaction() as session: session["recipient"] = "2028675301" session["placeholders"] = {"a": "b"} @@ -2766,8 +2862,8 @@ def test_send_notification_redirects_to_view_page( extra_redirect_args, mocker, mock_create_job, - mock_s3_upload, ): + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) with client_request.session_transaction() as session: session["recipient"] = "2028675301" session["placeholders"] = {"a": "b"} @@ -2826,8 +2922,9 @@ def test_send_notification_shows_error_if_400( exception_msg, expected_h1, expected_err_details, - mock_s3_upload, ): + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) + class MockHTTPError(HTTPError): message = exception_msg @@ -2865,8 +2962,9 @@ def test_send_notification_shows_email_error_in_trial_mode( mocker, mock_get_service_email_template, mock_create_job, - mock_s3_upload, ): + mocker.patch("app.main.views.send.s3upload", return_value=sample_uuid()) + class MockHTTPError(HTTPError): message = TRIAL_MODE_MSG status_code = 400 @@ -2912,8 +3010,6 @@ def test_reply_to_is_previewed_if_chosen( mocker, mock_get_service_email_template, mock_s3_download, - mock_s3_get_metadata, - mock_s3_set_metadata, mock_get_users_by_service, mock_get_service_statistics, mock_get_job_doesnt_exist, @@ -2924,6 +3020,8 @@ def test_reply_to_is_previewed_if_chosen( extra_args, reply_to_address, ): + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + mocker.patch( "app.main.views.send.s3download", return_value=""" @@ -2967,8 +3065,6 @@ def test_sms_sender_is_previewed( mocker, mock_get_service_template, mock_s3_download, - mock_s3_get_metadata, - mock_s3_set_metadata, mock_get_users_by_service, mock_get_service_statistics, mock_get_job_doesnt_exist, @@ -2979,6 +3075,13 @@ def test_sms_sender_is_previewed( extra_args, sms_sender, ): + + mocker.patch("app.main.views.send.set_metadata_on_csv_upload") + + mocker.patch( + "app.main.views.send.get_csv_metadata", + return_value={"original_file_name": "example.csv"}, + ) mocker.patch( "app.main.views.send.s3download", return_value=""" diff --git a/tests/conftest.py b/tests/conftest.py index 00451bdf7..592ec9403 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1843,14 +1843,6 @@ def mock_get_users_by_service(mocker): ) -@pytest.fixture() -def mock_s3_upload(mocker): - def _upload(service_id, filedata): - return sample_uuid() - - return mocker.patch("app.main.views.send.s3upload", side_effect=_upload) - - @pytest.fixture() def mock_s3_download(mocker): def _download(service_id, upload_id): @@ -1863,21 +1855,6 @@ def mock_s3_download(mocker): return mocker.patch("app.main.views.send.s3download", side_effect=_download) -@pytest.fixture() -def mock_s3_get_metadata(mocker): - def _get_metadata(service_id, upload_id): - return {"original_file_name": "example.csv"} - - return mocker.patch( - "app.main.views.send.get_csv_metadata", side_effect=_get_metadata - ) - - -@pytest.fixture() -def mock_s3_set_metadata(mocker): - return mocker.patch("app.main.views.send.set_metadata_on_csv_upload") - - @pytest.fixture() def sample_invite(mocker, service_one): id_ = USER_ONE_ID From e694b9a5f60f6bb67199902df7cecc75f5ffa548 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 29 May 2024 14:49:06 -0700 Subject: [PATCH 08/43] clean up mock_s3_download --- tests/app/main/views/test_send.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index 46240de5a..fd55ae20b 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -483,7 +483,6 @@ def test_send_messages_sanitises_and_truncates_file_name_for_metadata( service_one, mocker, mock_get_service_template_with_placeholders, - mock_s3_download, mock_get_job_doesnt_exist, fake_uuid, ): @@ -1620,7 +1619,6 @@ def test_send_one_off_redirects_to_start_if_index_out_of_bounds_and_some_placeho service_one, fake_uuid, mock_get_service_email_template, - mock_s3_download, mock_get_users_by_service, mock_get_service_statistics, mock_has_no_jobs, @@ -2016,11 +2014,11 @@ def test_test_message_can_only_be_sent_now( mocker, service_one, mock_get_service_template, - mock_s3_download, mock_get_users_by_service, mock_get_service_statistics, mock_get_job_doesnt_exist, mock_get_jobs, + mock_s3_download, fake_uuid, ): @@ -3009,7 +3007,6 @@ def test_reply_to_is_previewed_if_chosen( client_request, mocker, mock_get_service_email_template, - mock_s3_download, mock_get_users_by_service, mock_get_service_statistics, mock_get_job_doesnt_exist, @@ -3064,7 +3061,6 @@ def test_sms_sender_is_previewed( client_request, mocker, mock_get_service_template, - mock_s3_download, mock_get_users_by_service, mock_get_service_statistics, mock_get_job_doesnt_exist, From df032ba76df80a23941b19a2f978ff1acbeb87f3 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Thu, 30 May 2024 07:58:30 -0700 Subject: [PATCH 09/43] remove mock_aws where its not useful --- tests/app/main/views/test_send.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index fd55ae20b..24403e895 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -12,7 +12,6 @@ from zipfile import BadZipFile import pytest from flask import url_for -from moto import mock_aws from notifications_python_client.errors import HTTPError from xlrd.biffh import XLRDError from xlrd.xldate import XLDateAmbiguous, XLDateError, XLDateNegative, XLDateTooLarge @@ -432,7 +431,6 @@ def test_example_spreadsheet( list(zip(test_spreadsheet_files, repeat(True), repeat(302))) + list(zip(test_non_spreadsheet_files, repeat(False), repeat(200))), ) -@mock_aws def test_upload_files_in_different_formats( filename, acceptable_file, @@ -477,7 +475,6 @@ def test_upload_files_in_different_formats( ) -@mock_aws def test_send_messages_sanitises_and_truncates_file_name_for_metadata( client_request, service_one, @@ -588,7 +585,6 @@ def test_shows_error_if_parsing_exception( ) -@mock_aws def test_upload_csv_file_with_errors_shows_check_page_with_errors( client_request, service_one, @@ -641,7 +637,6 @@ def test_upload_csv_file_with_errors_shows_check_page_with_errors( assert "Upload your file again" in page.text -@mock_aws def test_upload_csv_file_with_empty_message_shows_check_page_with_errors( client_request, service_one, @@ -700,7 +695,6 @@ def test_upload_csv_file_with_empty_message_shows_check_page_with_errors( assert page.select("tbody tr td")[1]["colspan"] == "2" -@mock_aws def test_upload_csv_file_with_very_long_placeholder_shows_check_page_with_errors( client_request, service_one, @@ -842,7 +836,6 @@ def test_upload_csv_file_with_very_long_placeholder_shows_check_page_with_errors ), ], ) -@mock_aws def test_upload_csv_file_with_missing_columns_shows_error( client_request, mocker, @@ -924,7 +917,6 @@ def test_upload_csv_size_too_big( assert "File must be smaller than 10Mb" in page.text -@mock_aws def test_upload_valid_csv_redirects_to_check_page( client_request, mock_get_service_template_with_placeholders, @@ -1070,7 +1062,6 @@ def test_upload_valid_csv_shows_preview_and_table( assert normalize_spaces(str(row.select("td")[index])) == cell -@mock_aws def test_show_all_columns_if_there_are_duplicate_recipient_columns( client_request, mocker, @@ -1125,7 +1116,6 @@ def test_show_all_columns_if_there_are_duplicate_recipient_columns( (5, 404), ], ) -@mock_aws def test_404_for_previewing_a_row_out_of_range( client_request, mocker, @@ -1579,7 +1569,6 @@ def test_send_one_off_redirects_to_end_if_step_out_of_bounds( create_active_caseworking_user(), ], ) -@mock_aws def test_send_one_off_redirects_to_start_if_you_skip_steps( client_request, service_one, @@ -1682,7 +1671,6 @@ def test_send_one_off_sms_message_redirects( create_active_caseworking_user(), ], ) -@mock_aws def test_send_one_off_email_to_self_without_placeholders_redirects_to_check_page( client_request, mocker, @@ -1887,7 +1875,6 @@ def test_download_example_csv( assert "text/csv" in response.headers["Content-Type"] -@mock_aws def test_upload_csvfile_with_valid_phone_shows_all_numbers( client_request, mock_get_service_template, From a4fc04a65927f3841477cd87dda74725e26ee701 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Thu, 30 May 2024 08:25:00 -0700 Subject: [PATCH 10/43] test that S3 not getting hit in tests --- app/s3_client/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/s3_client/__init__.py b/app/s3_client/__init__.py index e0933b464..047ae0bb8 100644 --- a/app/s3_client/__init__.py +++ b/app/s3_client/__init__.py @@ -1,3 +1,5 @@ +import os + import botocore from boto3 import Session from botocore.config import Config @@ -29,6 +31,17 @@ def get_s3_object( ) s3 = session.resource("s3", config=AWS_CLIENT_CONFIG) obj = s3.Object(bucket_name, filename) + # This 'proves' that use of moto in the relevant tests in test_send.py + # mocks everything related to S3. What you will see in the logs is: + # Exception: CREATED AT + # + # raise Exception(f"CREATED AT {_s3.Bucket(bucket_name).creation_date}") + if os.getenv("NOTIFY_ENVIRONMENT") == "test": + teststr = str(s3.Bucket(bucket_name).creation_date).lower() + if "magicmock" not in teststr: + raise Exception( + f"xxxxxtest not mocked, use @mock_aws creation date is {teststr}" + ) return obj From 108e889ac564dae4478c61a0a21e6e02c233c190 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 30 May 2024 14:48:25 -0700 Subject: [PATCH 11/43] added example of fetch --- app/assets/javascripts/socket.js | 14 ++++++-------- app/main/views/dashboard.py | 19 ++++++++++++------- app/templates/views/dashboard/dashboard.html | 4 ++-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/socket.js b/app/assets/javascripts/socket.js index 608fa0bde..c4a54198d 100644 --- a/app/assets/javascripts/socket.js +++ b/app/assets/javascripts/socket.js @@ -7,16 +7,14 @@ console.log('Connected to the server'); }); - socket.on('message', function(msg) { - var li = document.createElement("li"); - li.appendChild(document.createTextNode(msg)); - document.getElementById("messages").appendChild(li); + // Listen for job updates from the server + socket.on('job_update', function(data) { + console.log('Received job update:', data); }); - document.getElementById('sendButton').addEventListener('click', function() { - var message = document.getElementById("message").value; - socket.send(message); - document.getElementById("message").value = ''; + document.getElementById('fetchJobsButton').addEventListener('click', function() { + const serviceId = 'b1226555-1f1a-472c-9086-043b0a69f4ec'; // Example service ID + socket.emit('fetch_jobs', serviceId); }); }); diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 042cbe94e..60c44ee13 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -6,7 +6,7 @@ from itertools import groupby from flask import Response, abort, jsonify, render_template, request, session, url_for from flask_login import current_user -from flask_socketio import send, emit +from flask_socketio import SocketIO, emit from werkzeug.utils import redirect from app import ( @@ -34,14 +34,20 @@ from app.utils.user import user_has_permissions from notifications_utils.recipients import format_phone_number_human_readable -@socketio.on('message') -def handle_message(msg): - print('''Message: +# @socketio.on('connect') +# def handle_connect(): +# print('Client connected') +# @socketio.on('disconnect') +# def handle_disconnect(): +# print('Client disconnected') - ''' + msg) - emit('message', msg, broadcast=True) + +@socketio.on('fetch_jobs') +def handle_fetch_jobs(service_id): + job_response = job_api_client.get_jobs(service_id)["data"] + emit('job_update', job_response) @main.route("/services//dashboard") @@ -71,7 +77,6 @@ def service_dashboard(service_id): job_id = notification.get("job", {}).get("id", None) if job_id: aggregate_notifications_by_job[job_id].append(notification) - job_and_notifications = [ { "job_id": job["id"], diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html index 4a2751eb2..1361576e6 100644 --- a/app/templates/views/dashboard/dashboard.html +++ b/app/templates/views/dashboard/dashboard.html @@ -22,8 +22,8 @@ Messages sent - -
      +

      Job Dashboard

      + {{ ajax_block(partials, updates_url, 'inbox') }} From 01321aeddc3926b0454837e2464d495830ea65e9 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 30 May 2024 21:17:10 -0700 Subject: [PATCH 12/43] connecting api data to socket and setting up chartjs to use the data --- app/assets/javascripts/chartDashboard.js | 78 +++++++++++++++++--- app/main/views/dashboard.py | 58 ++++++++------- app/templates/views/dashboard/dashboard.html | 5 +- 3 files changed, 101 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/chartDashboard.js b/app/assets/javascripts/chartDashboard.js index fc40936f8..adc9ea255 100644 --- a/app/assets/javascripts/chartDashboard.js +++ b/app/assets/javascripts/chartDashboard.js @@ -1,24 +1,78 @@ (function (window) { - const ctx = document.getElementById('myChart'); + var socket = io(); + var serviceId = chart.getAttribute('data-service-id'); - new Chart(ctx, { + socket.on('connect', function() { + console.log('Connected to the server'); // Debug log, i'll delete later + socket.emit('fetch_daily_stats', serviceId); + socket.emit('fetch_single_month_notification_stats', serviceId); + socket.emit('fetch_monthly_stats_by_year', serviceId); + }); + + //this is for previous 7 days + socket.on('daily_stats_update', function(data) { + console.log('Received daily_stats_update:', data); + // Process the data + var labels = []; + var deliveredData = []; + // var failureData = []; + // var requestedData = []; + + for (var date in data) { + labels.push(date); + deliveredData.push(data[date].sms.delivered); + // failureData.push(data[date].sms.failure); + // requestedData.push(data[date].sms.requested); + } + + // Update Chart.js + myBarChart.data.labels = labels; + myBarChart.data.datasets[0].data = deliveredData; + myBarChart.update(); + }); + //this is for a single month + socket.on('single_month_notification_stats_update', function(data) { + console.log('Received single_month_notification_stats_update:', data); + // Update Chart.js with new data here + }); + //this is for monthly stats by year + socket.on('monthly_stats_by_year_update', function(data) { + console.log('Received monthly_stats_by_year_update:', data); + // Update Chart.js with new data here + }); + + socket.on('error', function(data) { + console.log('Error:', data); + }); + + sevenDaysButton.addEventListener('click', function() { + socket.emit('fetch_monthly_stats_by_year', serviceId); + console.log('button click'); // Debug log, i'll delete later + }); + + // Initialize Chart.js bar chart + var ctx = document.getElementById('myChart').getContext('2d'); + var myBarChart = new Chart(ctx, { type: 'bar', data: { - labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], - datasets: [{ - label: '# of Votes', - data: [12, 19, 3, 5, 2, 3], - borderWidth: 1 - }] + labels: [], // Initialize with empty data + datasets: [ + { + label: 'Delivered', + data: [], + backgroundColor: '#0076d6', + stack: 'Stack 0' + }, + ] }, options: { - scales: { - y: { - beginAtZero: true + scales: { + y: { + beginAtZero: true + } } } - } }); })(window); diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 9d857dbab..59add85f3 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -20,7 +20,6 @@ from app import ( ) from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left from app.main import main -from app.models.user import User from app.statistics_utils import get_formatted_percentage from app.utils import ( DELIVERED_STATUSES, @@ -36,20 +35,30 @@ from app.utils.user import user_has_permissions from notifications_utils.recipients import format_phone_number_human_readable -# @socketio.on('connect') -# def handle_connect(): -# print('Client connected') +@socketio.on('fetch_daily_stats') +def handle_fetch_daily_stats(service_id): + if service_id: + date_range = get_stats_date_range() + daily_stats = service_api_client.get_service_notification_statistics_by_day(service_id, start_date=date_range['start_date'], days=date_range['days']) + emit('daily_stats_update', daily_stats) + else: + emit('error', {'error': 'No service_id provided'}) -# @socketio.on('disconnect') -# def handle_disconnect(): -# print('Client disconnected') +@socketio.on('fetch_single_month_notification_stats') +def handle_fetch_single_month_notification_stats(service_id): + date_range = get_stats_date_range() + single_month_notification_stats = service_api_client.get_single_month_notification_stats(service_id, year=date_range['current_financial_year'], month=date_range['current_month']) + emit('single_month_notification_stats_update', single_month_notification_stats) -@socketio.on('fetch_jobs') -def handle_fetch_jobs(service_id): - job_response = job_api_client.get_jobs(service_id)["data"] - emit('job_update', job_response) +@socketio.on('fetch_monthly_stats_by_year') +def handle_fetch_monthly_stats(service_id): + date_range = get_stats_date_range() + monthly_stats_by_year_stats = format_monthly_stats_to_list( + service_api_client.get_monthly_notification_stats(service_id, year=date_range['current_financial_year'])["data"] + ) + emit('monthly_stats_by_year_update', monthly_stats_by_year_stats) @main.route("/services//dashboard") @@ -103,6 +112,7 @@ def service_dashboard(service_id): partials=get_dashboard_partials(service_id), job_and_notifications=job_and_notifications, service_data_retention_days=service_data_retention_days, + service_id=service_id ) @@ -343,10 +353,6 @@ def aggregate_notifications_stats(template_statistics): def get_dashboard_partials(service_id): current_financial_year = get_current_financial_year() - current_month = get_current_month_for_financial_year(current_financial_year) - start_date = datetime.now().strftime('%Y-%m-%d') - days=7 - all_statistics = template_statistics_client.get_template_statistics_for_service( service_id, limit_days=7 ) @@ -368,21 +374,10 @@ def get_dashboard_partials(service_id): service_id, current_financial_year, ) - - #Previous 7 day stats - daily_stats = service_api_client.get_service_notification_statistics_by_day(service_id, start_date=start_date, days=days) - - #Single month stats - single_month_notification_stats = service_api_client.get_single_month_notification_stats(service_id, year=current_financial_year, month=current_month) - - #monthly stats by year monthly_stats = format_monthly_stats_to_list( service_api_client.get_monthly_notification_stats(service_id, current_financial_year)["data"] ) - # user=User.from_id(user_id), - # single_month_notification_stats = service_api_client.get_single_month_notification_stats_by_user(service_id, user, year=current_financial_year, month=current_month) - return { "upcoming": render_template( "views/dashboard/_upcoming.html", @@ -479,6 +474,17 @@ def get_current_month_for_financial_year(year): current_month = datetime.now().month return current_month +def get_stats_date_range(): + current_financial_year = get_current_financial_year() + current_month = get_current_month_for_financial_year(current_financial_year) + start_date = datetime.now().strftime('%Y-%m-%d') + days = 7 + return { + "current_financial_year": current_financial_year, + "current_month": current_month, + "start_date": start_date, + "days": days, + } def get_months_for_year(start, end, year): return [datetime(year, month, 1) for month in range(start, end)] diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html index 1361576e6..d73e6bd9a 100644 --- a/app/templates/views/dashboard/dashboard.html +++ b/app/templates/views/dashboard/dashboard.html @@ -23,8 +23,9 @@

      Job Dashboard

      - - + + +
      {{ ajax_block(partials, updates_url, 'inbox') }} {{ ajax_block(partials, updates_url, 'totals') }} From 6c44982a5e29802415953e0c9974753d6b0594f2 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 30 May 2024 21:20:09 -0700 Subject: [PATCH 13/43] removed socket.js --- app/assets/javascripts/socket.js | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 app/assets/javascripts/socket.js diff --git a/app/assets/javascripts/socket.js b/app/assets/javascripts/socket.js deleted file mode 100644 index c4a54198d..000000000 --- a/app/assets/javascripts/socket.js +++ /dev/null @@ -1,21 +0,0 @@ - -(function (window) { - document.addEventListener('DOMContentLoaded', (event) => { - var socket = io(); - - socket.on('connect', function() { - console.log('Connected to the server'); - }); - - // Listen for job updates from the server - socket.on('job_update', function(data) { - console.log('Received job update:', data); - }); - - document.getElementById('fetchJobsButton').addEventListener('click', function() { - const serviceId = 'b1226555-1f1a-472c-9086-043b0a69f4ec'; // Example service ID - socket.emit('fetch_jobs', serviceId); - }); - }); - -})(window); From a834166f41061d302443da99cb83c51a9446fd43 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Fri, 31 May 2024 08:56:11 -0700 Subject: [PATCH 14/43] merge from main --- poetry.lock | 575 ++++++++++++++++++++++++---------------------------- 1 file changed, 270 insertions(+), 305 deletions(-) diff --git a/poetry.lock b/poetry.lock index d01c10d73..2025fafb4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -171,17 +171,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.106" +version = "1.34.116" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.106-py3-none-any.whl", hash = "sha256:d3be4e1dd5d546a001cd4da805816934cbde9d395316546e9411fec341ade5cf"}, - {file = "boto3-1.34.106.tar.gz", hash = "sha256:6165b8cf1c7e625628ab28b32f9027064c8f5e5fca1c38d7fc228cd22069a19f"}, + {file = "boto3-1.34.116-py3-none-any.whl", hash = "sha256:e7f5ab2d1f1b90971a2b9369760c2c6bae49dae98c084a5c3f5c78e3968ace15"}, + {file = "boto3-1.34.116.tar.gz", hash = "sha256:53cb8aeb405afa1cd2b25421e27a951aeb568026675dec020587861fac96ac87"}, ] [package.dependencies] -botocore = ">=1.34.106,<1.35.0" +botocore = ">=1.34.116,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -190,13 +190,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.106" +version = "1.34.116" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.106-py3-none-any.whl", hash = "sha256:4baf0e27c2dfc4f4d0dee7c217c716e0782f9b30e8e1fff983fce237d88f73ae"}, - {file = "botocore-1.34.106.tar.gz", hash = "sha256:921fa5202f88c3e58fdcb4b3acffd56d65b24bca47092ee4b27aa988556c0be6"}, + {file = "botocore-1.34.116-py3-none-any.whl", hash = "sha256:ec4d42c816e9b2d87a2439ad277e7dda16a4a614ef6839cf66f4c1a58afa547c"}, + {file = "botocore-1.34.116.tar.gz", hash = "sha256:269cae7ba99081519a9f87d7298e238d9e68ba94eb4f8ddfa906224c34cb8b6c"}, ] [package.dependencies] @@ -462,63 +462,63 @@ files = [ [[package]] name = "coverage" -version = "7.5.1" +version = "7.5.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, - {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, - {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, - {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, - {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, - {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, - {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, - {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, - {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, - {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, - {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, - {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, - {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, - {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, - {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, - {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, - {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, - {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, - {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, - {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, + {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, + {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, + {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, + {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, + {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, + {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, + {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, + {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, + {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, + {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, + {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, + {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, ] [package.extras] @@ -580,19 +580,19 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "cyclonedx-python-lib" -version = "6.4.4" +version = "7.4.0" description = "Python library for CycloneDX" optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "cyclonedx_python_lib-6.4.4-py3-none-any.whl", hash = "sha256:c366619cc4effd528675f1f7a7a00be30b6695ff03f49c64880ad15acbebc341"}, - {file = "cyclonedx_python_lib-6.4.4.tar.gz", hash = "sha256:1b6f9109b6b9e91636dff822c2de90a05c0c8af120317713c1b879dbfdebdff8"}, + {file = "cyclonedx_python_lib-7.4.0-py3-none-any.whl", hash = "sha256:fc423e7f46d772e5ded29a48cb0743233e692e5853c49b829efc0f59014efde1"}, + {file = "cyclonedx_python_lib-7.4.0.tar.gz", hash = "sha256:09b10736a7f440262578fa40f470b448de1ebf3c7a71e2ff0a4af0781d3a3b42"}, ] [package.dependencies] license-expression = ">=30,<31" packageurl-python = ">=0.11,<2" -py-serializable = ">=0.16,<2" +py-serializable = ">=1.0.3,<2" sortedcontainers = ">=2.4.0,<3.0.0" [package.extras] @@ -1206,165 +1206,149 @@ files = [ [[package]] name = "lxml" -version = "5.2.1" +version = "5.2.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" files = [ - {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1"}, - {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1"}, - {file = "lxml-5.2.1-cp310-cp310-win32.whl", hash = "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5"}, - {file = "lxml-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f"}, - {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"}, - {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"}, - {file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"}, - {file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"}, - {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"}, - {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"}, - {file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"}, - {file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"}, - {file = "lxml-5.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461"}, - {file = "lxml-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0"}, - {file = "lxml-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289"}, - {file = "lxml-5.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8"}, - {file = "lxml-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd"}, - {file = "lxml-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c"}, - {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5"}, - {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637"}, - {file = "lxml-5.2.1-cp38-cp38-win32.whl", hash = "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da"}, - {file = "lxml-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806"}, - {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd"}, - {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188"}, - {file = "lxml-5.2.1-cp39-cp39-win32.whl", hash = "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708"}, - {file = "lxml-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"}, - {file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"}, + {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632"}, + {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526"}, + {file = "lxml-5.2.2-cp310-cp310-win32.whl", hash = "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30"}, + {file = "lxml-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7"}, + {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545"}, + {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b"}, + {file = "lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438"}, + {file = "lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be"}, + {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391"}, + {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836"}, + {file = "lxml-5.2.2-cp312-cp312-win32.whl", hash = "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a"}, + {file = "lxml-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48"}, + {file = "lxml-5.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56"}, + {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9"}, + {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264"}, + {file = "lxml-5.2.2-cp36-cp36m-win32.whl", hash = "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3"}, + {file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"}, + {file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"}, + {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"}, + {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"}, + {file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"}, + {file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"}, + {file = "lxml-5.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1"}, + {file = "lxml-5.2.2-cp38-cp38-win32.whl", hash = "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30"}, + {file = "lxml-5.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6"}, + {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30"}, + {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9"}, + {file = "lxml-5.2.2-cp39-cp39-win32.whl", hash = "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf"}, + {file = "lxml-5.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324"}, + {file = "lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87"}, ] [package.extras] @@ -1594,6 +1578,7 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] @@ -1610,40 +1595,40 @@ files = [ [[package]] name = "newrelic" -version = "9.9.1" +version = "9.10.0" description = "New Relic Python Agent" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "newrelic-9.9.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:474499f482da7f58b5039f2c42dea2880d878b30729ae563bb1498a0bb30be44"}, - {file = "newrelic-9.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3c99cc368a3cfd9ce40ca4bbe2fe3bdd5f7d37865ea5e4bf811ba6fd0d00152d"}, - {file = "newrelic-9.9.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3ef567a779b068297c040f7410153135fb12e51e4a82084675b0cf142c407551"}, - {file = "newrelic-9.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:303117d3402659afac45174dfe7c595b7d4b3c0812a76b712c251c91ef95c430"}, - {file = "newrelic-9.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c813e9c7bdb1381cb0eda4925e07aa8ee21e111b5025d02261605eaabb129f1"}, - {file = "newrelic-9.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5d688917307d083d7fa6f3b31eec40c5a3782b160383230f5f644e2d4ae2a26"}, - {file = "newrelic-9.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5710910ceb847f8806540e6934764fff6823d7dcc6d30955e9ecb012e20efbfd"}, - {file = "newrelic-9.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aefa66f59d62ec22a6d347afa73c24bd723521c4cc0fdce7f51c71bfe85c42bc"}, - {file = "newrelic-9.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afdb30c4f89d0f089ac05ca50a383f94cfcdb07aab0b9722d2d5af09626ab304"}, - {file = "newrelic-9.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6361af2a60ab60a5757b13ce0b9b4efeee577a228637b9b8b449d47ec81fdd"}, - {file = "newrelic-9.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7aa1be0d0530d0c566dee2c4d43765aba9fc5fae256fac110ba57aae6ae8d8c4"}, - {file = "newrelic-9.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8ad34b8eb60f33b0eab9ed7727cdb9452ad7d4381a2c5397e6ed3d4895833fd1"}, - {file = "newrelic-9.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e613f1ffd0d35b1f866382eeee52d8aa9576d82f3de818a84aa2e56c08f1868"}, - {file = "newrelic-9.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3264e305ae0e973f3a02f7394460f4c7366822e8a3509cd08b2093f9cb5def5"}, - {file = "newrelic-9.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2b165328c05fd2c006cf1f476bebb281579944418a13903e802344660b13332c"}, - {file = "newrelic-9.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e3226ac2c0c57955a00a11f6cf982dd6747490254ed322d6fcf36077bfc37386"}, - {file = "newrelic-9.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673ed069516fa4d168cd12b7319bcadf75fbc9f0ebcd147916e281b2bc16c551"}, - {file = "newrelic-9.9.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40820a3dff89cc8e242f0543fabd1692333458f627ebad6f2e56f6c9db7d2efe"}, - {file = "newrelic-9.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ddb2d4a2fc3f88c5d1c0b4dec2f8eb89907541501f2ec7ac14e5506ea702e0f5"}, - {file = "newrelic-9.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d50fa347584967c15e574a2503fdcafcd13c86c17e589021eae5432d4aad1cca"}, - {file = "newrelic-9.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbca7a8749eadb05eacdfb68af938dc1045c6be8bcc83375d15a840172b5f40e"}, - {file = "newrelic-9.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d6feba8968662c7a84ee6fe837d3be8c53a7126398ded3283634bb51dc43e94"}, - {file = "newrelic-9.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:eec85620708aea387b602db61fb43504efc5b5fcb7b627d2cbe0a33c3fe10ab9"}, - {file = "newrelic-9.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:21e280c027835062f54be2df48f32834dcc98f382b049c14ee35b80aa7b48ea0"}, - {file = "newrelic-9.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb0e56324df855c3079d7d86fd6b35e79727759de8c8517be9c06d482092c3b"}, - {file = "newrelic-9.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c43a14c48dd8f752da348c3ec80cb500b9ead12abcd40d29d39a0bb8a62a3a0d"}, - {file = "newrelic-9.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:763faab4868b0226906c17ef0419dab527964f489cb2e3818d57d0484762cb2e"}, - {file = "newrelic-9.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7f41343548aad28b7722c85d00079b4e61ef48d5a6bdf757c458a5fe860bb099"}, - {file = "newrelic-9.9.1.tar.gz", hash = "sha256:e49c734058c7b6a6c199e8c2657187143061a6eda92cc8ba67739de88a9e203d"}, + {file = "newrelic-9.10.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:a4d4e5670082225ca7ef0ee986ef8e6588f4e530a05d43d66f9368459c0b1f18"}, + {file = "newrelic-9.10.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f4605bc4feb114235e242dfe260b75ec85d0894f5400aa7f30e75fbbc0423b3f"}, + {file = "newrelic-9.10.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d3be6c97d007ceb142f908f5ab2444807b44dc600a0b7f3254dc685b5b03fd10"}, + {file = "newrelic-9.10.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4e573d49c1543a488d6567906a9b2cb0c748cdbf80724c322b06874f8e47c789"}, + {file = "newrelic-9.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae0515f7ab19f1a5dd14e31506420d1b86014c5e1340c2a210833248bc765dae"}, + {file = "newrelic-9.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acf5cdcafd2971933ad2f9e836284957f4a3eababe88f063cf53b1b1f67f1a16"}, + {file = "newrelic-9.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5d18236bf4a80fca4eb1db03448ed72bf8e16b84b3a4ed5fcc29bb91c2d05d54"}, + {file = "newrelic-9.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:744c815f15ec06e441c11a6c57042d2eca8c41401c11de6f47b3e105d952b9bd"}, + {file = "newrelic-9.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:524ed5bfa09d330746b45e0087765da994ca34802cce032063041e404e58414c"}, + {file = "newrelic-9.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad9cd5459b8c620ab7a876bd5d920c3ef2943948d1262a42289d4f8d16dadab"}, + {file = "newrelic-9.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4404c649b5e6165dcdd59091092c19b292a43cc96520d5ffd718b628fb866096"}, + {file = "newrelic-9.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2576bbec0b640d9b76454dcfd5b2f03078e0bb062a7ea3952a8db7b9972c352"}, + {file = "newrelic-9.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77537a020ce84033f39210e46cc43bb3927cec3fb4b34b5c4df802e96fddaedf"}, + {file = "newrelic-9.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2236f70b8c6aa79635f2175e7315d032f3a80dfd65ad9c9ed12a921f5df4c655"}, + {file = "newrelic-9.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b8201a33caf7632b2e55e3f9687584ad6956aaf5751485cdb2bad7c428a9b400"}, + {file = "newrelic-9.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6ed4bc2c9a44dfe59958eeecf1f327f0a0fb6324b5e609515bc511944d12db74"}, + {file = "newrelic-9.10.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cc3ddb26c0615ba4e18f87453bca57f0688a43d2fcdd50e2771a77515cfc3ba"}, + {file = "newrelic-9.10.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09912303e04bee6aa1fe1c671e87b4e8e55461081a96210895828798f5ba8c3f"}, + {file = "newrelic-9.10.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:40368dca0d423efe40b210686d7018787d4365a24ee1deca136b3b7c9d850325"}, + {file = "newrelic-9.10.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56f4c309a07a2c66243b12d18056c32aa704735469741495642c31be4a1c77fa"}, + {file = "newrelic-9.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d68fc707d896dc7da8d6939bcc1f995bf9e463c2b911fc63250a10e1502a234"}, + {file = "newrelic-9.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cd462804a6ede617fb3b4b126e9083b3ee8b4ed1250f7cc12299ebacb785432"}, + {file = "newrelic-9.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ceef4fef2a5cffb69e9e1742bd18a35625ca62c3856c7016c22be68ec876753d"}, + {file = "newrelic-9.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1f11d9c17b50982fcc39de71f6592a61920ec5e5c29b9105edc9f8fb7f2480b9"}, + {file = "newrelic-9.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf6757d422954e61082715dbba4208cae17bf3720006bc337c3f87f19ede2876"}, + {file = "newrelic-9.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae84bacfdc60792bd04e681027cc5c58e6737a04c652e9be2eda84abe21f57f5"}, + {file = "newrelic-9.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:667722cf1f4ed9f6cd99f4fbe247fc2bdb941935528e14a93659ba2c651dc889"}, + {file = "newrelic-9.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0c18210648889416da3de61aa282248e012cb507ba9841511407f922fff9a52"}, + {file = "newrelic-9.10.0.tar.gz", hash = "sha256:02db25b0fd2fc835efe4a7f1c92dbc5bbb95125341aba07152041aa6a5666cda"}, ] [package.extras] @@ -1651,18 +1636,15 @@ infinite-tracing = ["grpcio", "protobuf"] [[package]] name = "nodeenv" -version = "1.8.0" +version = "1.9.0" description = "Node.js virtual environment builder" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, + {file = "nodeenv-1.9.0-py2.py3-none-any.whl", hash = "sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a"}, + {file = "nodeenv-1.9.0.tar.gz", hash = "sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1"}, ] -[package.dependencies] -setuptools = "*" - [[package]] name = "notifications-python-client" version = "9.0.0" @@ -1803,13 +1785,13 @@ files = [ [[package]] name = "phonenumbers" -version = "8.13.36" +version = "8.13.37" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." optional = false python-versions = "*" files = [ - {file = "phonenumbers-8.13.36-py2.py3-none-any.whl", hash = "sha256:68e06d20ae2f8fe5c7c7fd5b433f4257bc3cc747dc5196a029c7898ea449b012"}, - {file = "phonenumbers-8.13.36.tar.gz", hash = "sha256:b4e2371e35a1172aa2c91c9200b1e48e87b9355eb575768dd38058fc8d72c9ff"}, + {file = "phonenumbers-8.13.37-py2.py3-none-any.whl", hash = "sha256:4ea00ef5012422c08c7955c21131e7ae5baa9a3ef52cf2d561e963f023006b80"}, + {file = "phonenumbers-8.13.37.tar.gz", hash = "sha256:bd315fed159aea0516f7c367231810fe8344d5bec26156b88fa18374c11d1cf2"}, ] [[package]] @@ -1886,13 +1868,13 @@ testing = ["aboutcode-toolkit (>=6.0.0)", "black", "pytest (>=6,!=7.0.0)", "pyte [[package]] name = "platformdirs" -version = "4.2.1" +version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] @@ -1902,18 +1884,18 @@ type = ["mypy (>=1.8)"] [[package]] name = "playwright" -version = "1.43.0" +version = "1.44.0" description = "A high-level API to automate web browsers" optional = false python-versions = ">=3.8" files = [ - {file = "playwright-1.43.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b03b12bd4da9c2cfb78dff820deac8b52892fe3c2f89a4d95d6f08c59e41deb9"}, - {file = "playwright-1.43.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e9ec21b141727392f630761c7f4dec46d80c98243614257cc501b64ff636d337"}, - {file = "playwright-1.43.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:e05a8d8fb2040c630429cca07e843c8fa33059717837c8f50c01b7d1fc651ce1"}, - {file = "playwright-1.43.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:50d9a5c07c76456945a2296d63f78fdf6eb11aed3e8d39bb5ccbda760a8d6d41"}, - {file = "playwright-1.43.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87191272c40b4c282cf2c9449ca3acaf705f38ac6e2372270f1617ce16b661b8"}, - {file = "playwright-1.43.0-py3-none-win32.whl", hash = "sha256:bd8b818904b17e2914be23e7bc2a340b203f57fe81678520b10f908485b056ea"}, - {file = "playwright-1.43.0-py3-none-win_amd64.whl", hash = "sha256:9b7bd707eeeaebee47f656b2de90aa9bd85e9ca2c6af7a08efd73896299e4d50"}, + {file = "playwright-1.44.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:c2317a80896796fdeb03d60f06cc229e775ff2e19b80c64b1bb9b29c8a59d992"}, + {file = "playwright-1.44.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54d44fb634d870839301c2326e1e12a178a1be0de76d0caaec230ab075c2e077"}, + {file = "playwright-1.44.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:64b67194e73b47ae72acf25f1a9cfacfef38ca2b52e4bb8b0abd385c5deeaadf"}, + {file = "playwright-1.44.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:29161b1fae71f7c402df5b15f0bd3deaeecd8b3d1ecd9ff01271700c66210e7b"}, + {file = "playwright-1.44.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8c8a3bfea17576d3f94a2363eee195cbda8dbba86975588c7eaac7792b25eee"}, + {file = "playwright-1.44.0-py3-none-win32.whl", hash = "sha256:235e37832deaa9af8a629d09955396259ab757533cc1922f9b0308b4ee0d9cdf"}, + {file = "playwright-1.44.0-py3-none-win_amd64.whl", hash = "sha256:5b8a4a1d4d50f4ff99b47965576322a8c4e34631854b862a25c1feb824be22a8"}, ] [package.dependencies] @@ -2121,17 +2103,16 @@ files = [ [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] @@ -2206,13 +2187,13 @@ certifi = "*" [[package]] name = "pytest" -version = "8.2.0" +version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, - {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, + {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, + {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] @@ -2562,13 +2543,13 @@ files = [ [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -2647,22 +2628,6 @@ botocore = ">=1.33.2,<2.0a.0" [package.extras] crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] -[[package]] -name = "setuptools" -version = "69.5.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, - {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "shapely" version = "2.0.4" @@ -2812,13 +2777,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, + {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, ] [[package]] @@ -2840,13 +2805,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.1" +version = "20.26.2" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"}, - {file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"}, + {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, + {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, ] [package.dependencies] From 8c85d9944923e8e0d3e3872aaec1610e116dcdb7 Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Fri, 31 May 2024 12:09:22 -0400 Subject: [PATCH 15/43] Update dependencies and fix E2E test This changeset updates a couple of dependencies flagged by Dependabot and fixes an end-to-end test that needed to be updated with the one-off send filename changes. Signed-off-by: Carlo Costino --- poetry.lock | 16 +++++++--------- pyproject.toml | 4 ++-- .../test_send_message_from_existing_template.py | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/poetry.lock b/poetry.lock index a3b6f718b..7107f3089 100644 --- a/poetry.lock +++ b/poetry.lock @@ -171,17 +171,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.114" +version = "1.34.116" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.114-py3-none-any.whl", hash = "sha256:4460958d2b0c53bd2195b23ed5d45db2350e514486fe8caeb38b285b30742280"}, - {file = "boto3-1.34.114.tar.gz", hash = "sha256:eeb11bca9b19d12baf93436fb8a16b8b824f1f7e8b9bcc722607e862c46b1b08"}, + {file = "boto3-1.34.116-py3-none-any.whl", hash = "sha256:e7f5ab2d1f1b90971a2b9369760c2c6bae49dae98c084a5c3f5c78e3968ace15"}, + {file = "boto3-1.34.116.tar.gz", hash = "sha256:53cb8aeb405afa1cd2b25421e27a951aeb568026675dec020587861fac96ac87"}, ] [package.dependencies] -botocore = ">=1.34.114,<1.35.0" +botocore = ">=1.34.116,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -190,13 +190,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.115" +version = "1.34.116" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.115-py3-none-any.whl", hash = "sha256:15b8ad1ee0e9cd57884fb0bcaf3a9551d2552e44a02c2ffb55ec583eebdb888e"}, - {file = "botocore-1.34.115.tar.gz", hash = "sha256:a5d5e28b9c847b17a1ecb7660b46b83d9512b125f671e03e93d14bf6f0b274c2"}, + {file = "botocore-1.34.116-py3-none-any.whl", hash = "sha256:ec4d42c816e9b2d87a2439ad277e7dda16a4a614ef6839cf66f4c1a58afa547c"}, + {file = "botocore-1.34.116.tar.gz", hash = "sha256:269cae7ba99081519a9f87d7298e238d9e68ba94eb4f8ddfa906224c34cb8b6c"}, ] [package.dependencies] @@ -1297,7 +1297,6 @@ files = [ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, @@ -1614,7 +1613,6 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] diff --git a/pyproject.toml b/pyproject.toml index 59c1a23b5..9a1eb25ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ wtforms = "~=3.1" markdown = "^3.5.2" async-timeout = "^4.0.3" bleach = "^6.1.0" -boto3 = "^1.34.113" -botocore = "^1.34.115" +boto3 = "^1.34.115" +botocore = "^1.34.116" cachetools = "^5.3.3" cffi = "^1.16.0" cryptography = "^42.0.7" diff --git a/tests/end_to_end/test_send_message_from_existing_template.py b/tests/end_to_end/test_send_message_from_existing_template.py index 535ad6c5f..56ced9b62 100644 --- a/tests/end_to_end/test_send_message_from_existing_template.py +++ b/tests/end_to_end/test_send_message_from_existing_template.py @@ -191,7 +191,7 @@ def handle_no_existing_template_case(page): in content ) assert "12025555555" in content - assert "one-off-e2e_test_user" in content + assert "one-off-" in content os.remove("download_test_file") From 2414b09c0ffd8059834b49ec1c75ba78cd234570 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Fri, 31 May 2024 09:47:19 -0700 Subject: [PATCH 16/43] fix flake8 --- app/s3_client/__init__.py | 2 +- notifications_utils/s3.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/s3_client/__init__.py b/app/s3_client/__init__.py index d5fa38189..7de3509d2 100644 --- a/app/s3_client/__init__.py +++ b/app/s3_client/__init__.py @@ -40,7 +40,7 @@ def get_s3_object( teststr = str(s3.Bucket(bucket_name).creation_date).lower() if "magicmock" not in teststr: raise Exception( - f"Test is not mocked, use @mock_aws or the relevant mocker.patch to avoid accessing S3" + "Test is not mocked, use @mock_aws or the relevant mocker.patch to avoid accessing S3" ) return obj diff --git a/notifications_utils/s3.py b/notifications_utils/s3.py index 6f7f2ce1d..d33cbe25a 100644 --- a/notifications_utils/s3.py +++ b/notifications_utils/s3.py @@ -47,7 +47,7 @@ def s3upload( teststr = str(_s3.Bucket(bucket_name).creation_date).lower() if "magicmock" not in teststr: raise Exception( - f"Test is not mocked, use @mock_aws or the relevant mocker.patch to avoid accessing S3" + "Test is not mocked, use @mock_aws or the relevant mocker.patch to avoid accessing S3" ) key = _s3.Object(bucket_name, file_location) @@ -102,7 +102,7 @@ def s3download( teststr = str(s3.Bucket(bucket_name).creation_date).lower() if "magicmock" not in teststr: raise Exception( - f"Test is not mocked, use @mock_aws or the relevant mocker.patch to avoid accessing S3" + "Test is not mocked, use @mock_aws or the relevant mocker.patch to avoid accessing S3" ) return key.get()["Body"] except botocore.exceptions.ClientError as error: From 29dbe45cbe32625cd7280318c9f361293be9fbab Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Fri, 31 May 2024 11:27:39 -0700 Subject: [PATCH 17/43] code review feedback --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7ca7c4c49..2fd078feb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ flake8-print = "^5.0.0" flake8-pytest-style = "^1.7.2" isort = "^5.13.2" jinja2-cli = {version = "==0.8.2", extras = ["yaml"]} -moto="*" +moto = "*" pip-audit = "*" pre-commit = "^3.7.1" pytest = "^8.2.1" From f6cf4932666bd41f67c2623cd761412840fbd86d Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Fri, 31 May 2024 17:20:08 -0400 Subject: [PATCH 18/43] Update expired and cancelled service invite handling This changeset adds a bit of extra handling for expired and cancelled service invites so that users can no longer accept them and are provided with more detailed error messages. Signed-off-by: Carlo Costino --- app/main/views/register.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/main/views/register.py b/app/main/views/register.py index 7f50c6a19..df1282abc 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -251,10 +251,21 @@ def get_invited_user_email_address(invited_user_id): def invited_user_accept_invite(invited_user_id): invited_user = InvitedUser.by_id(invited_user_id) + if invited_user.status == "expired": current_app.logger.error("User invitation has expired") - flash("Your invitation has expired.") + flash( + "Your invitation has expired; please contact the person who invited you for additional help." + ) abort(401) + + if invited_user.status == "cancelled": + current_app.logger.error("User invitation has been cancelled") + flash( + "Your invitation is no longer valid; please contact the person who invited you for additional help." + ) + abort(401) + invited_user.accept_invite() From f0fcb8f21d09f520a05a2df4e7a0d843bf9c9547 Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Mon, 3 Jun 2024 11:03:29 -0400 Subject: [PATCH 19/43] Update Python dependencies - 6/3/24 This changeset updates several Python dependencies to stay on top of Dependabot alerts. Signed-off-by: Carlo Costino --- poetry.lock | 35 +++++++++++++++++------------------ pyproject.toml | 4 ++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index 78ba0826d..8cb15a59d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -171,17 +171,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.116" +version = "1.34.117" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.116-py3-none-any.whl", hash = "sha256:e7f5ab2d1f1b90971a2b9369760c2c6bae49dae98c084a5c3f5c78e3968ace15"}, - {file = "boto3-1.34.116.tar.gz", hash = "sha256:53cb8aeb405afa1cd2b25421e27a951aeb568026675dec020587861fac96ac87"}, + {file = "boto3-1.34.117-py3-none-any.whl", hash = "sha256:1506589e30566bbb2f4997b60968ff7d4ef8a998836c31eedd36437ac3b7408a"}, + {file = "boto3-1.34.117.tar.gz", hash = "sha256:c8a383b904d6faaf7eed0c06e31b423db128e4c09ce7bd2afc39d1cd07030a51"}, ] [package.dependencies] -botocore = ">=1.34.116,<1.35.0" +botocore = ">=1.34.117,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -190,13 +190,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.116" +version = "1.34.117" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.116-py3-none-any.whl", hash = "sha256:ec4d42c816e9b2d87a2439ad277e7dda16a4a614ef6839cf66f4c1a58afa547c"}, - {file = "botocore-1.34.116.tar.gz", hash = "sha256:269cae7ba99081519a9f87d7298e238d9e68ba94eb4f8ddfa906224c34cb8b6c"}, + {file = "botocore-1.34.117-py3-none-any.whl", hash = "sha256:26a431997f882bcdd1e835f44c24b2a1752b1c4e5183c2ce62999ce95d518d6c"}, + {file = "botocore-1.34.117.tar.gz", hash = "sha256:4637ca42e6c51aebc4d9a2d92f97bf4bdb042e3f7985ff31a659a11e4c170e73"}, ] [package.dependencies] @@ -241,13 +241,13 @@ files = [ [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] @@ -1537,13 +1537,13 @@ files = [ [[package]] name = "moto" -version = "5.0.8" +version = "5.0.9" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "moto-5.0.8-py2.py3-none-any.whl", hash = "sha256:7d1035e366434bfa9fcc0621f07d5aa724b6846408071d540137a0554c46f214"}, - {file = "moto-5.0.8.tar.gz", hash = "sha256:517fb808dc718bcbdda54c6ffeaca0adc34cf6e10821bfb01216ce420a31765c"}, + {file = "moto-5.0.9-py2.py3-none-any.whl", hash = "sha256:21a13e02f83d6a18cfcd99949c96abb2e889f4bd51c4c6a3ecc8b78765cb854e"}, + {file = "moto-5.0.9.tar.gz", hash = "sha256:eb71f1cba01c70fff1f16086acb24d6d9aeb32830d646d8989f98a29aeae24ba"}, ] [package.dependencies] @@ -1641,7 +1641,6 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] @@ -2859,13 +2858,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.12.0" +version = "4.12.1" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, - {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, + {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, + {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, ] [[package]] @@ -3002,4 +3001,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "ebecc7a1869605e491b4cb168a621e9e8b64a69bca74189857782c51c9fd7b9c" +content-hash = "91a817ed33b8f8182673b00dd8b340c4aff9697cfa093051a86360c6cef4ec49" diff --git a/pyproject.toml b/pyproject.toml index 9762f7140..8b77d4bec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ wtforms = "~=3.1" markdown = "^3.5.2" async-timeout = "^4.0.3" bleach = "^6.1.0" -boto3 = "^1.34.115" -botocore = "^1.34.116" +boto3 = "^1.34.117" +botocore = "^1.34.117" cachetools = "^5.3.3" cffi = "^1.16.0" cryptography = "^42.0.7" From 4ebd5734d6ddd84d77552d0e32a4aa12e8bf0cbc Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 3 Jun 2024 11:32:56 -0700 Subject: [PATCH 20/43] removed socket.js --- app/templates/new/components/head.html | 1 - gulpfile.js | 1 - 2 files changed, 2 deletions(-) diff --git a/app/templates/new/components/head.html b/app/templates/new/components/head.html index f7c7153e2..51f3c4da3 100644 --- a/app/templates/new/components/head.html +++ b/app/templates/new/components/head.html @@ -32,7 +32,6 @@ {# google #} - {% if g.hide_from_search_engines %} diff --git a/gulpfile.js b/gulpfile.js index 3b7d765a7..98afbbacf 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -126,7 +126,6 @@ const javascripts = () => { paths.src + 'javascripts/loginAlert.js', paths.src + 'javascripts/main.js', paths.src + 'javascripts/chartDashboard.js', - paths.src + 'javascripts/socket.js', ]) .pipe(plugins.prettyerror()) .pipe(plugins.babel({ From 72e82c818b9a21a4166bd2f66179adc0fdd9ca3b Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 3 Jun 2024 11:39:02 -0700 Subject: [PATCH 21/43] added socket.io.min.js --- .ds.baseline | 4 ++-- app/templates/new/components/head.html | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.ds.baseline b/.ds.baseline index 859f30b4d..13a4125fe 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -423,7 +423,7 @@ "filename": "app/templates/new/components/head.html", "hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327", "is_verified": false, - "line_number": 35, + "line_number": 36, "is_secret": false } ], @@ -710,5 +710,5 @@ } ] }, - "generated_at": "2024-05-29T21:18:03Z" + "generated_at": "2024-06-03T18:37:18Z" } diff --git a/app/templates/new/components/head.html b/app/templates/new/components/head.html index 51f3c4da3..f7c7153e2 100644 --- a/app/templates/new/components/head.html +++ b/app/templates/new/components/head.html @@ -32,6 +32,7 @@ {# google #} + {% if g.hide_from_search_engines %} From 2e41b752f5ef082c42812d5efde42da9f94e6184 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 3 Jun 2024 12:50:05 -0700 Subject: [PATCH 22/43] Remove .ds.baseline from repository --- .ds.baseline | 714 --------------------------------------------------- 1 file changed, 714 deletions(-) delete mode 100644 .ds.baseline diff --git a/.ds.baseline b/.ds.baseline deleted file mode 100644 index 13a4125fe..000000000 --- a/.ds.baseline +++ /dev/null @@ -1,714 +0,0 @@ -{ - "version": "1.5.0", - "plugins_used": [ - { - "name": "ArtifactoryDetector" - }, - { - "name": "AWSKeyDetector" - }, - { - "name": "AzureStorageKeyDetector" - }, - { - "name": "Base64HighEntropyString", - "limit": 4.5 - }, - { - "name": "BasicAuthDetector" - }, - { - "name": "CloudantDetector" - }, - { - "name": "DiscordBotTokenDetector" - }, - { - "name": "GitHubTokenDetector" - }, - { - "name": "GitLabTokenDetector" - }, - { - "name": "HexHighEntropyString", - "limit": 3.0 - }, - { - "name": "IbmCloudIamDetector" - }, - { - "name": "IbmCosHmacDetector" - }, - { - "name": "IPPublicDetector" - }, - { - "name": "JwtTokenDetector" - }, - { - "name": "KeywordDetector", - "keyword_exclude": "" - }, - { - "name": "MailchimpDetector" - }, - { - "name": "NpmDetector" - }, - { - "name": "OpenAIDetector" - }, - { - "name": "PrivateKeyDetector" - }, - { - "name": "PypiTokenDetector" - }, - { - "name": "SendGridDetector" - }, - { - "name": "SlackDetector" - }, - { - "name": "SoftlayerDetector" - }, - { - "name": "SquareOAuthDetector" - }, - { - "name": "StripeDetector" - }, - { - "name": "TelegramBotTokenDetector" - }, - { - "name": "TwilioKeyDetector" - } - ], - "filters_used": [ - { - "path": "detect_secrets.filters.allowlist.is_line_allowlisted" - }, - { - "path": "detect_secrets.filters.common.is_baseline_file", - "filename": ".ds.baseline" - }, - { - "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", - "min_level": 2 - }, - { - "path": "detect_secrets.filters.heuristic.is_indirect_reference" - }, - { - "path": "detect_secrets.filters.heuristic.is_likely_id_string" - }, - { - "path": "detect_secrets.filters.heuristic.is_lock_file" - }, - { - "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" - }, - { - "path": "detect_secrets.filters.heuristic.is_potential_uuid" - }, - { - "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" - }, - { - "path": "detect_secrets.filters.heuristic.is_sequential_string" - }, - { - "path": "detect_secrets.filters.heuristic.is_swagger_file" - }, - { - "path": "detect_secrets.filters.heuristic.is_templated_secret" - } - ], - "results": { - ".github/workflows/checks.yml": [ - { - "type": "Secret Keyword", - "filename": ".github/workflows/checks.yml", - "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", - "is_verified": false, - "line_number": 61, - "is_secret": false - }, - { - "type": "Basic Auth Credentials", - "filename": ".github/workflows/checks.yml", - "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", - "is_verified": false, - "line_number": 95, - "is_secret": false - } - ], - "app/assets/js/uswds.min.js": [ - { - "type": "Secret Keyword", - "filename": "app/assets/js/uswds.min.js", - "hashed_secret": "372ea08cab33e71c02c651dbc83a474d32c676ea", - "is_verified": false, - "line_number": 85, - "is_secret": false - }, - { - "type": "Secret Keyword", - "filename": "app/assets/js/uswds.min.js", - "hashed_secret": "53e07a32bf191d6917ee6fd863f0b52632a86798", - "is_verified": false, - "line_number": 85, - "is_secret": false - } - ], - "app/config.py": [ - { - "type": "Secret Keyword", - "filename": "app/config.py", - "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", - "is_verified": false, - "line_number": 111, - "is_secret": false - } - ], - "app/main/_commonly_used_passwords.py": [ - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "82e19fa12aab7cfc718a002fc82c0f074bf070e7", - "is_verified": false, - "line_number": 123, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "a172ffc990129fe6f68b50f6037c54a1894ee3fd", - "is_verified": false, - "line_number": 240, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "4de69ee6b12b7fc91070873b71ba6e2929b90619", - "is_verified": false, - "line_number": 244, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "370194ff6e0f93a7432e16cc9badd9427e8b4e13", - "is_verified": false, - "line_number": 284, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "3dd635a808ddb6dd4b6731f7c409d53dd4b14df2", - "is_verified": false, - "line_number": 356, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "67a74306b06d0c01624fe0d0249a570f4d093747", - "is_verified": false, - "line_number": 374, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "61d6504733ca7757e259c644acd085c4dd471019", - "is_verified": false, - "line_number": 910, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "4ea872dfd7eefbde0036da7f0780826353dc7477", - "is_verified": false, - "line_number": 940, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "b214f706bb602c1cc2adc5c6165e73622305f4bb", - "is_verified": false, - "line_number": 1010, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "5cbabd43e49a1fedbbc3b86311aa6c8fe446abf9", - "is_verified": false, - "line_number": 1195, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "18ad10fd4a67f21fc07b1aa5046b410f6b2bedf1", - "is_verified": false, - "line_number": 1213, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "10470c3b4b1fed12c3baac014be15fac67c6e815", - "is_verified": false, - "line_number": 1263, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "65e1946c8f102eca8ba0af291f7c5e807516d94c", - "is_verified": false, - "line_number": 1346, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "0075df0a74c07ee295c98238c018401c9a80183b", - "is_verified": false, - "line_number": 1397, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "ca0023d7b345802fbc227b902cb9c57a3e02195f", - "is_verified": false, - "line_number": 1442, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "c8c6ca2e11c2dfd2a40914585b5944bffea15c8c", - "is_verified": false, - "line_number": 1555, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "b85b97a99eab8c809570c61d6404c1e49bdefbb4", - "is_verified": false, - "line_number": 1596, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "dec7dd342a499dfd4d283d872ccf598d8a7b6039", - "is_verified": false, - "line_number": 1789, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "2dc5053699a351121bf839c446bd4a878dda5735", - "is_verified": false, - "line_number": 1939, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "e5d54f0ac13abbdaa94b696c2469148b96dd11ab", - "is_verified": false, - "line_number": 2242, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "6059f42e2bbae78141e8a9e6286755ee691d5ce0", - "is_verified": false, - "line_number": 2305, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "fe703d258c7ef5f50b71e06565a65aa07194907f", - "is_verified": false, - "line_number": 2348, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "c229b68e1c3ffd9874838b5cb5354a0ee1367ddc", - "is_verified": false, - "line_number": 2349, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "756de479126e911b6f3400ae686d663d9d26b509", - "is_verified": false, - "line_number": 2920, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "6b174322afcdb440ee9cc3cc11eb16f9a00dec04", - "is_verified": false, - "line_number": 2975, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "9860783bfb510cbb2bf34471ec0b84a7ea587695", - "is_verified": false, - "line_number": 3359, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "b227cbd22eaa96019ebfc4aff35ad2add2a47439", - "is_verified": false, - "line_number": 3590, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "381d48209aecab8834eb495c5b5406100da07882", - "is_verified": false, - "line_number": 3811, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "app/main/_commonly_used_passwords.py", - "hashed_secret": "508b38590a90d32990aadd7350d160b795c3ab41", - "is_verified": false, - "line_number": 3850, - "is_secret": false - } - ], - "app/main/views/sign_in.py": [ - { - "type": "Private Key", - "filename": "app/main/views/sign_in.py", - "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", - "is_verified": false, - "line_number": 27, - "is_secret": false - } - ], - "app/templates/new/components/head.html": [ - { - "type": "Base64 High Entropy String", - "filename": "app/templates/new/components/head.html", - "hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327", - "is_verified": false, - "line_number": 36, - "is_secret": false - } - ], - "app/templates/old/admin_template.html": [ - { - "type": "Base64 High Entropy String", - "filename": "app/templates/old/admin_template.html", - "hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327", - "is_verified": false, - "line_number": 18, - "is_secret": false - } - ], - "deploy-config/sandbox.yml": [ - { - "type": "Secret Keyword", - "filename": "deploy-config/sandbox.yml", - "hashed_secret": "113151dd10316fcb0d5507b6215d78e2f3fe9e54", - "is_verified": false, - "line_number": 8, - "is_secret": false - } - ], - "pytest.ini": [ - { - "type": "Secret Keyword", - "filename": "pytest.ini", - "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", - "is_verified": false, - "line_number": 7, - "is_secret": false - }, - { - "type": "Base64 High Entropy String", - "filename": "pytest.ini", - "hashed_secret": "d347784b1ab6074a65cda7bc42f1561bed85493f", - "is_verified": false, - "line_number": 7, - "is_secret": false - }, - { - "type": "Base64 High Entropy String", - "filename": "pytest.ini", - "hashed_secret": "ed1754d5cc82c8fd83205ebfb8c43fe4e88415a4", - "is_verified": false, - "line_number": 9, - "is_secret": false - }, - { - "type": "Secret Keyword", - "filename": "pytest.ini", - "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", - "is_verified": false, - "line_number": 11, - "is_secret": false - } - ], - "tests/__init__.py": [ - { - "type": "Secret Keyword", - "filename": "tests/__init__.py", - "hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea", - "is_verified": false, - "line_number": 388, - "is_secret": false - } - ], - "tests/app/main/forms/test_register_user_form.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/main/forms/test_register_user_form.py", - "hashed_secret": "8c6c978dc8e08771c7dea1ea2370fdf2446e5ba5", - "is_verified": false, - "line_number": 38, - "is_secret": false - } - ], - "tests/app/main/test_errorhandlers.py": [ - { - "type": "Base64 High Entropy String", - "filename": "tests/app/main/test_errorhandlers.py", - "hashed_secret": "005fa73b3f2be8f0d71d361c1f0a9d787cd09b4e", - "is_verified": false, - "line_number": 33, - "is_secret": false - } - ], - "tests/app/main/test_request_header.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/main/test_request_header.py", - "hashed_secret": "6866ef97a972ba3a2c6ff8bb2812981054770162", - "is_verified": false, - "line_number": 21, - "is_secret": false - } - ], - "tests/app/main/views/organizations/test_organization_invites.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/organizations/test_organization_invites.py", - "hashed_secret": "bdbb156d25d02fd7792865824201dda1c60f4473", - "is_verified": false, - "line_number": 265, - "is_secret": false - }, - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/organizations/test_organization_invites.py", - "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", - "is_verified": false, - "line_number": 273, - "is_secret": false - } - ], - "tests/app/main/views/test_accept_invite.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/test_accept_invite.py", - "hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71", - "is_verified": false, - "line_number": 607, - "is_secret": false - } - ], - "tests/app/main/views/test_new_password.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/test_new_password.py", - "hashed_secret": "a41d5c3bbcd0b39c627b9cbf4897c6d25efa694f", - "is_verified": false, - "line_number": 89, - "is_secret": false - } - ], - "tests/app/main/views/test_register.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/test_register.py", - "hashed_secret": "bdbb156d25d02fd7792865824201dda1c60f4473", - "is_verified": false, - "line_number": 116, - "is_secret": false - }, - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/test_register.py", - "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", - "is_verified": false, - "line_number": 192, - "is_secret": false - }, - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/test_register.py", - "hashed_secret": "bb5b7caa27d005d38039e3797c3ddb9bcd22c3c8", - "is_verified": false, - "line_number": 260, - "is_secret": false - } - ], - "tests/app/main/views/test_sign_in.py": [ - { - "type": "Private Key", - "filename": "tests/app/main/views/test_sign_in.py", - "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", - "is_verified": false, - "line_number": 31, - "is_secret": false - }, - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/test_sign_in.py", - "hashed_secret": "8b8b69116ee882b5e987e330f55db81aba0636f9", - "is_verified": false, - "line_number": 104, - "is_secret": false - } - ], - "tests/app/main/views/test_two_factor.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/test_two_factor.py", - "hashed_secret": "dc66ad927c29e31c6c374231f57a4684b0687bfe", - "is_verified": false, - "line_number": 267, - "is_secret": false - } - ], - "tests/app/main/views/test_user_profile.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/test_user_profile.py", - "hashed_secret": "8072d7aad32964ec43fbcb699c75dc38890792f7", - "is_verified": false, - "line_number": 350, - "is_secret": false - }, - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/test_user_profile.py", - "hashed_secret": "4c9dbb972da179e4f66f023eaa5fb9451d835030", - "is_verified": false, - "line_number": 351, - "is_secret": false - } - ], - "tests/app/main/views/test_verify.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/main/views/test_verify.py", - "hashed_secret": "faafcfa63e128929409bf310b7ea5a415f2331ce", - "is_verified": false, - "line_number": 160, - "is_secret": false - } - ], - "tests/app/notify_client/test_user_client.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/notify_client/test_user_client.py", - "hashed_secret": "f2c57870308dc87f432e5912d4de6f8e322721ba", - "is_verified": false, - "line_number": 55, - "is_secret": false - } - ], - "tests/app/test_cloudfoundry_config.py": [ - { - "type": "Secret Keyword", - "filename": "tests/app/test_cloudfoundry_config.py", - "hashed_secret": "5e44dae2de8b6e57c797b968035265c9f2cd2b3e", - "is_verified": false, - "line_number": 12, - "is_secret": false - }, - { - "type": "Secret Keyword", - "filename": "tests/app/test_cloudfoundry_config.py", - "hashed_secret": "e5e178db7317356946d13e5d2da037d39ac61c71", - "is_verified": false, - "line_number": 27, - "is_secret": false - } - ], - "tests/conftest.py": [ - { - "type": "Secret Keyword", - "filename": "tests/conftest.py", - "hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea", - "is_verified": false, - "line_number": 3266, - "is_secret": false - } - ], - "tests/notifications_utils/clients/antivirus/test_antivirus_client.py": [ - { - "type": "Secret Keyword", - "filename": "tests/notifications_utils/clients/antivirus/test_antivirus_client.py", - "hashed_secret": "932b25270abe1301c22c709a19082dff07d469ff", - "is_verified": false, - "line_number": 16, - "is_secret": false - } - ], - "tests/notifications_utils/clients/encryption/test_encryption_client.py": [ - { - "type": "Secret Keyword", - "filename": "tests/notifications_utils/clients/encryption/test_encryption_client.py", - "hashed_secret": "f1e923a9667de11be6a210849a8651c1bfd81605", - "is_verified": false, - "line_number": 13, - "is_secret": false - } - ], - "tests/notifications_utils/clients/zendesk/test_zendesk_client.py": [ - { - "type": "Secret Keyword", - "filename": "tests/notifications_utils/clients/zendesk/test_zendesk_client.py", - "hashed_secret": "913a73b565c8e2c8ed94497580f619397709b8b6", - "is_verified": false, - "line_number": 16, - "is_secret": false - } - ] - }, - "generated_at": "2024-06-03T18:37:18Z" -} From d12b798bba97e4b0b9115888c9678f2340a4ba62 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 3 Jun 2024 13:17:31 -0700 Subject: [PATCH 23/43] removed sending status --- .ds.baseline | 714 ++++++++++++++++++++++++++++++++++++ app/main/views/dashboard.py | 5 +- 2 files changed, 715 insertions(+), 4 deletions(-) create mode 100644 .ds.baseline diff --git a/.ds.baseline b/.ds.baseline new file mode 100644 index 000000000..859f30b4d --- /dev/null +++ b/.ds.baseline @@ -0,0 +1,714 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".ds.baseline" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": { + ".github/workflows/checks.yml": [ + { + "type": "Secret Keyword", + "filename": ".github/workflows/checks.yml", + "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "is_verified": false, + "line_number": 61, + "is_secret": false + }, + { + "type": "Basic Auth Credentials", + "filename": ".github/workflows/checks.yml", + "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "is_verified": false, + "line_number": 95, + "is_secret": false + } + ], + "app/assets/js/uswds.min.js": [ + { + "type": "Secret Keyword", + "filename": "app/assets/js/uswds.min.js", + "hashed_secret": "372ea08cab33e71c02c651dbc83a474d32c676ea", + "is_verified": false, + "line_number": 85, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "app/assets/js/uswds.min.js", + "hashed_secret": "53e07a32bf191d6917ee6fd863f0b52632a86798", + "is_verified": false, + "line_number": 85, + "is_secret": false + } + ], + "app/config.py": [ + { + "type": "Secret Keyword", + "filename": "app/config.py", + "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", + "is_verified": false, + "line_number": 111, + "is_secret": false + } + ], + "app/main/_commonly_used_passwords.py": [ + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "82e19fa12aab7cfc718a002fc82c0f074bf070e7", + "is_verified": false, + "line_number": 123, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "a172ffc990129fe6f68b50f6037c54a1894ee3fd", + "is_verified": false, + "line_number": 240, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "4de69ee6b12b7fc91070873b71ba6e2929b90619", + "is_verified": false, + "line_number": 244, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "370194ff6e0f93a7432e16cc9badd9427e8b4e13", + "is_verified": false, + "line_number": 284, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "3dd635a808ddb6dd4b6731f7c409d53dd4b14df2", + "is_verified": false, + "line_number": 356, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "67a74306b06d0c01624fe0d0249a570f4d093747", + "is_verified": false, + "line_number": 374, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "61d6504733ca7757e259c644acd085c4dd471019", + "is_verified": false, + "line_number": 910, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "4ea872dfd7eefbde0036da7f0780826353dc7477", + "is_verified": false, + "line_number": 940, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "b214f706bb602c1cc2adc5c6165e73622305f4bb", + "is_verified": false, + "line_number": 1010, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "5cbabd43e49a1fedbbc3b86311aa6c8fe446abf9", + "is_verified": false, + "line_number": 1195, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "18ad10fd4a67f21fc07b1aa5046b410f6b2bedf1", + "is_verified": false, + "line_number": 1213, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "10470c3b4b1fed12c3baac014be15fac67c6e815", + "is_verified": false, + "line_number": 1263, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "65e1946c8f102eca8ba0af291f7c5e807516d94c", + "is_verified": false, + "line_number": 1346, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "0075df0a74c07ee295c98238c018401c9a80183b", + "is_verified": false, + "line_number": 1397, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "ca0023d7b345802fbc227b902cb9c57a3e02195f", + "is_verified": false, + "line_number": 1442, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "c8c6ca2e11c2dfd2a40914585b5944bffea15c8c", + "is_verified": false, + "line_number": 1555, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "b85b97a99eab8c809570c61d6404c1e49bdefbb4", + "is_verified": false, + "line_number": 1596, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "dec7dd342a499dfd4d283d872ccf598d8a7b6039", + "is_verified": false, + "line_number": 1789, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "2dc5053699a351121bf839c446bd4a878dda5735", + "is_verified": false, + "line_number": 1939, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "e5d54f0ac13abbdaa94b696c2469148b96dd11ab", + "is_verified": false, + "line_number": 2242, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "6059f42e2bbae78141e8a9e6286755ee691d5ce0", + "is_verified": false, + "line_number": 2305, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "fe703d258c7ef5f50b71e06565a65aa07194907f", + "is_verified": false, + "line_number": 2348, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "c229b68e1c3ffd9874838b5cb5354a0ee1367ddc", + "is_verified": false, + "line_number": 2349, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "756de479126e911b6f3400ae686d663d9d26b509", + "is_verified": false, + "line_number": 2920, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "6b174322afcdb440ee9cc3cc11eb16f9a00dec04", + "is_verified": false, + "line_number": 2975, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "9860783bfb510cbb2bf34471ec0b84a7ea587695", + "is_verified": false, + "line_number": 3359, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "b227cbd22eaa96019ebfc4aff35ad2add2a47439", + "is_verified": false, + "line_number": 3590, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "381d48209aecab8834eb495c5b5406100da07882", + "is_verified": false, + "line_number": 3811, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "app/main/_commonly_used_passwords.py", + "hashed_secret": "508b38590a90d32990aadd7350d160b795c3ab41", + "is_verified": false, + "line_number": 3850, + "is_secret": false + } + ], + "app/main/views/sign_in.py": [ + { + "type": "Private Key", + "filename": "app/main/views/sign_in.py", + "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", + "is_verified": false, + "line_number": 27, + "is_secret": false + } + ], + "app/templates/new/components/head.html": [ + { + "type": "Base64 High Entropy String", + "filename": "app/templates/new/components/head.html", + "hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327", + "is_verified": false, + "line_number": 35, + "is_secret": false + } + ], + "app/templates/old/admin_template.html": [ + { + "type": "Base64 High Entropy String", + "filename": "app/templates/old/admin_template.html", + "hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327", + "is_verified": false, + "line_number": 18, + "is_secret": false + } + ], + "deploy-config/sandbox.yml": [ + { + "type": "Secret Keyword", + "filename": "deploy-config/sandbox.yml", + "hashed_secret": "113151dd10316fcb0d5507b6215d78e2f3fe9e54", + "is_verified": false, + "line_number": 8, + "is_secret": false + } + ], + "pytest.ini": [ + { + "type": "Secret Keyword", + "filename": "pytest.ini", + "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", + "is_verified": false, + "line_number": 7, + "is_secret": false + }, + { + "type": "Base64 High Entropy String", + "filename": "pytest.ini", + "hashed_secret": "d347784b1ab6074a65cda7bc42f1561bed85493f", + "is_verified": false, + "line_number": 7, + "is_secret": false + }, + { + "type": "Base64 High Entropy String", + "filename": "pytest.ini", + "hashed_secret": "ed1754d5cc82c8fd83205ebfb8c43fe4e88415a4", + "is_verified": false, + "line_number": 9, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "pytest.ini", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "is_verified": false, + "line_number": 11, + "is_secret": false + } + ], + "tests/__init__.py": [ + { + "type": "Secret Keyword", + "filename": "tests/__init__.py", + "hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea", + "is_verified": false, + "line_number": 388, + "is_secret": false + } + ], + "tests/app/main/forms/test_register_user_form.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/main/forms/test_register_user_form.py", + "hashed_secret": "8c6c978dc8e08771c7dea1ea2370fdf2446e5ba5", + "is_verified": false, + "line_number": 38, + "is_secret": false + } + ], + "tests/app/main/test_errorhandlers.py": [ + { + "type": "Base64 High Entropy String", + "filename": "tests/app/main/test_errorhandlers.py", + "hashed_secret": "005fa73b3f2be8f0d71d361c1f0a9d787cd09b4e", + "is_verified": false, + "line_number": 33, + "is_secret": false + } + ], + "tests/app/main/test_request_header.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/main/test_request_header.py", + "hashed_secret": "6866ef97a972ba3a2c6ff8bb2812981054770162", + "is_verified": false, + "line_number": 21, + "is_secret": false + } + ], + "tests/app/main/views/organizations/test_organization_invites.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/organizations/test_organization_invites.py", + "hashed_secret": "bdbb156d25d02fd7792865824201dda1c60f4473", + "is_verified": false, + "line_number": 265, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/organizations/test_organization_invites.py", + "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "is_verified": false, + "line_number": 273, + "is_secret": false + } + ], + "tests/app/main/views/test_accept_invite.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/test_accept_invite.py", + "hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71", + "is_verified": false, + "line_number": 607, + "is_secret": false + } + ], + "tests/app/main/views/test_new_password.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/test_new_password.py", + "hashed_secret": "a41d5c3bbcd0b39c627b9cbf4897c6d25efa694f", + "is_verified": false, + "line_number": 89, + "is_secret": false + } + ], + "tests/app/main/views/test_register.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/test_register.py", + "hashed_secret": "bdbb156d25d02fd7792865824201dda1c60f4473", + "is_verified": false, + "line_number": 116, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/test_register.py", + "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "is_verified": false, + "line_number": 192, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/test_register.py", + "hashed_secret": "bb5b7caa27d005d38039e3797c3ddb9bcd22c3c8", + "is_verified": false, + "line_number": 260, + "is_secret": false + } + ], + "tests/app/main/views/test_sign_in.py": [ + { + "type": "Private Key", + "filename": "tests/app/main/views/test_sign_in.py", + "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", + "is_verified": false, + "line_number": 31, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/test_sign_in.py", + "hashed_secret": "8b8b69116ee882b5e987e330f55db81aba0636f9", + "is_verified": false, + "line_number": 104, + "is_secret": false + } + ], + "tests/app/main/views/test_two_factor.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/test_two_factor.py", + "hashed_secret": "dc66ad927c29e31c6c374231f57a4684b0687bfe", + "is_verified": false, + "line_number": 267, + "is_secret": false + } + ], + "tests/app/main/views/test_user_profile.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/test_user_profile.py", + "hashed_secret": "8072d7aad32964ec43fbcb699c75dc38890792f7", + "is_verified": false, + "line_number": 350, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/test_user_profile.py", + "hashed_secret": "4c9dbb972da179e4f66f023eaa5fb9451d835030", + "is_verified": false, + "line_number": 351, + "is_secret": false + } + ], + "tests/app/main/views/test_verify.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/main/views/test_verify.py", + "hashed_secret": "faafcfa63e128929409bf310b7ea5a415f2331ce", + "is_verified": false, + "line_number": 160, + "is_secret": false + } + ], + "tests/app/notify_client/test_user_client.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/notify_client/test_user_client.py", + "hashed_secret": "f2c57870308dc87f432e5912d4de6f8e322721ba", + "is_verified": false, + "line_number": 55, + "is_secret": false + } + ], + "tests/app/test_cloudfoundry_config.py": [ + { + "type": "Secret Keyword", + "filename": "tests/app/test_cloudfoundry_config.py", + "hashed_secret": "5e44dae2de8b6e57c797b968035265c9f2cd2b3e", + "is_verified": false, + "line_number": 12, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "tests/app/test_cloudfoundry_config.py", + "hashed_secret": "e5e178db7317356946d13e5d2da037d39ac61c71", + "is_verified": false, + "line_number": 27, + "is_secret": false + } + ], + "tests/conftest.py": [ + { + "type": "Secret Keyword", + "filename": "tests/conftest.py", + "hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea", + "is_verified": false, + "line_number": 3266, + "is_secret": false + } + ], + "tests/notifications_utils/clients/antivirus/test_antivirus_client.py": [ + { + "type": "Secret Keyword", + "filename": "tests/notifications_utils/clients/antivirus/test_antivirus_client.py", + "hashed_secret": "932b25270abe1301c22c709a19082dff07d469ff", + "is_verified": false, + "line_number": 16, + "is_secret": false + } + ], + "tests/notifications_utils/clients/encryption/test_encryption_client.py": [ + { + "type": "Secret Keyword", + "filename": "tests/notifications_utils/clients/encryption/test_encryption_client.py", + "hashed_secret": "f1e923a9667de11be6a210849a8651c1bfd81605", + "is_verified": false, + "line_number": 13, + "is_secret": false + } + ], + "tests/notifications_utils/clients/zendesk/test_zendesk_client.py": [ + { + "type": "Secret Keyword", + "filename": "tests/notifications_utils/clients/zendesk/test_zendesk_client.py", + "hashed_secret": "913a73b565c8e2c8ed94497580f619397709b8b6", + "is_verified": false, + "line_number": 16, + "is_secret": false + } + ] + }, + "generated_at": "2024-05-29T21:18:03Z" +} diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 59add85f3..780bb8218 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -25,7 +25,6 @@ from app.utils import ( DELIVERED_STATUSES, FAILURE_STATUSES, REQUESTED_STATUSES, - SENDING_STATUSES, service_has_permission, ) from app.utils.csv import Spreadsheet @@ -88,6 +87,7 @@ def service_dashboard(service_id): job_id = notification.get("job", {}).get("id", None) if job_id: aggregate_notifications_by_job[job_id].append(notification) + job_and_notifications = [ { "job_id": job["id"], @@ -377,7 +377,6 @@ def get_dashboard_partials(service_id): monthly_stats = format_monthly_stats_to_list( service_api_client.get_monthly_notification_stats(service_id, current_financial_year)["data"] ) - return { "upcoming": render_template( "views/dashboard/_upcoming.html", @@ -458,8 +457,6 @@ def aggregate_status_types(counts_dict): "{}_counts".format(message_type): { "failed": sum(stats.get(status, 0) for status in FAILURE_STATUSES), "requested": sum(stats.get(status, 0) for status in REQUESTED_STATUSES), - "delivered": sum(stats.get(status, 0) for status in DELIVERED_STATUSES), - "pending": sum(stats.get(status, 0) for status in SENDING_STATUSES), } for message_type, stats in counts_dict.items() } From 89f19bb63de83f4d722d91a3cd64ee0627f4cfd5 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 3 Jun 2024 13:35:30 -0700 Subject: [PATCH 24/43] cleared up style checks --- app/main/views/dashboard.py | 18 +++++++++++++++--- app/notify_client/service_api_client.py | 5 ----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 780bb8218..64ef2965c 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -38,7 +38,11 @@ from notifications_utils.recipients import format_phone_number_human_readable def handle_fetch_daily_stats(service_id): if service_id: date_range = get_stats_date_range() - daily_stats = service_api_client.get_service_notification_statistics_by_day(service_id, start_date=date_range['start_date'], days=date_range['days']) + daily_stats = service_api_client.get_service_notification_statistics_by_day( + service_id, + start_date=date_range['start_date'], + days=date_range['days'] + ) emit('daily_stats_update', daily_stats) else: emit('error', {'error': 'No service_id provided'}) @@ -47,7 +51,11 @@ def handle_fetch_daily_stats(service_id): @socketio.on('fetch_single_month_notification_stats') def handle_fetch_single_month_notification_stats(service_id): date_range = get_stats_date_range() - single_month_notification_stats = service_api_client.get_single_month_notification_stats(service_id, year=date_range['current_financial_year'], month=date_range['current_month']) + single_month_notification_stats = service_api_client.get_single_month_notification_stats( + service_id, + year=date_range['current_financial_year'], + month=date_range['current_month'] + ) emit('single_month_notification_stats_update', single_month_notification_stats) @@ -55,7 +63,9 @@ def handle_fetch_single_month_notification_stats(service_id): def handle_fetch_monthly_stats(service_id): date_range = get_stats_date_range() monthly_stats_by_year_stats = format_monthly_stats_to_list( - service_api_client.get_monthly_notification_stats(service_id, year=date_range['current_financial_year'])["data"] + service_api_client.get_monthly_notification_stats( + service_id, + year=date_range['current_financial_year'])["data"] ) emit('monthly_stats_by_year_update', monthly_stats_by_year_stats) @@ -471,6 +481,7 @@ def get_current_month_for_financial_year(year): current_month = datetime.now().month return current_month + def get_stats_date_range(): current_financial_year = get_current_financial_year() current_month = get_current_month_for_financial_year(current_financial_year) @@ -483,6 +494,7 @@ def get_stats_date_range(): "days": days, } + def get_months_for_year(start, end, year): return [datetime(year, month, 1) for month in range(start, end)] diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index f2cc2f934..6b9e1d43c 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -56,11 +56,6 @@ class ServiceAPIClient(NotifyAdminAPIClient): "/service/{0}/notifications/month?year={1}&month={2}".format(service_id, year, month), ) - # def get_single_month_notification_stats(self, service_id, user_id, year, month): - # return self.get( - # "/service/{0}/notifications//month?year={1}&month={2}".format(service_id, user_id, year, month), - # ) - def get_services(self, params_dict=None): """ Retrieve a list of services. From d3bee617426e12a578e4f456d665f2c373131ba5 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 3 Jun 2024 13:45:36 -0700 Subject: [PATCH 25/43] cleared up style checks --- app/main/views/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 64ef2965c..d1f68448c 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -6,7 +6,7 @@ from itertools import groupby from flask import Response, abort, jsonify, render_template, request, session, url_for from flask_login import current_user -from flask_socketio import SocketIO, emit +from flask_socketio import emit from werkzeug.utils import redirect from app import ( From 8554df58f8a3fce3c563e5296aebc3033fed4f0d Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 3 Jun 2024 13:51:59 -0700 Subject: [PATCH 26/43] cleared up style checks --- app/main/views/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index d1f68448c..48a36dbb4 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -15,8 +15,8 @@ from app import ( job_api_client, notification_api_client, service_api_client, + socketio, template_statistics_client, - socketio ) from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left from app.main import main From 7a9d2607173bdf7407e1f7f8c1be7be2371456bf Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 3 Jun 2024 15:34:43 -0700 Subject: [PATCH 27/43] fix testing --- app/main/views/dashboard.py | 46 +++++++++----------- app/notify_client/service_api_client.py | 10 +++-- app/templates/views/dashboard/dashboard.html | 1 - 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 48a36dbb4..7ad899a53 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -34,40 +34,40 @@ from app.utils.user import user_has_permissions from notifications_utils.recipients import format_phone_number_human_readable -@socketio.on('fetch_daily_stats') +@socketio.on("fetch_daily_stats") def handle_fetch_daily_stats(service_id): if service_id: date_range = get_stats_date_range() daily_stats = service_api_client.get_service_notification_statistics_by_day( - service_id, - start_date=date_range['start_date'], - days=date_range['days'] + service_id, start_date=date_range["start_date"], days=date_range["days"] ) - emit('daily_stats_update', daily_stats) + emit("daily_stats_update", daily_stats) else: - emit('error', {'error': 'No service_id provided'}) + emit("error", {"error": "No service_id provided"}) -@socketio.on('fetch_single_month_notification_stats') +@socketio.on("fetch_single_month_notification_stats") def handle_fetch_single_month_notification_stats(service_id): date_range = get_stats_date_range() - single_month_notification_stats = service_api_client.get_single_month_notification_stats( - service_id, - year=date_range['current_financial_year'], - month=date_range['current_month'] + single_month_notification_stats = ( + service_api_client.get_single_month_notification_stats( + service_id, + year=date_range["current_financial_year"], + month=date_range["current_month"], + ) ) - emit('single_month_notification_stats_update', single_month_notification_stats) + emit("single_month_notification_stats_update", single_month_notification_stats) -@socketio.on('fetch_monthly_stats_by_year') +@socketio.on("fetch_monthly_stats_by_year") def handle_fetch_monthly_stats(service_id): date_range = get_stats_date_range() monthly_stats_by_year_stats = format_monthly_stats_to_list( service_api_client.get_monthly_notification_stats( - service_id, - year=date_range['current_financial_year'])["data"] + service_id, year=date_range["current_financial_year"] + )["data"] ) - emit('monthly_stats_by_year_update', monthly_stats_by_year_stats) + emit("monthly_stats_by_year_update", monthly_stats_by_year_stats) @main.route("/services//dashboard") @@ -122,7 +122,7 @@ def service_dashboard(service_id): partials=get_dashboard_partials(service_id), job_and_notifications=job_and_notifications, service_data_retention_days=service_data_retention_days, - service_id=service_id + service_id=service_id, ) @@ -362,7 +362,6 @@ def aggregate_notifications_stats(template_statistics): def get_dashboard_partials(service_id): - current_financial_year = get_current_financial_year() all_statistics = template_statistics_client.get_template_statistics_for_service( service_id, limit_days=7 ) @@ -375,17 +374,15 @@ def get_dashboard_partials(service_id): ) # These 2 calls will update the dashboard sms allowance count while in trial mode. billing_api_client.get_monthly_usage_for_service( - service_id, current_financial_year + service_id, get_current_financial_year() ) billing_api_client.create_or_update_free_sms_fragment_limit( service_id, free_sms_fragment_limit=free_sms_allowance ) + yearly_usage = billing_api_client.get_annual_usage_for_service( service_id, - current_financial_year, - ) - monthly_stats = format_monthly_stats_to_list( - service_api_client.get_monthly_notification_stats(service_id, current_financial_year)["data"] + get_current_financial_year(), ) return { "upcoming": render_template( @@ -408,7 +405,6 @@ def get_dashboard_partials(service_id): ), "usage": render_template( "views/dashboard/_usage.html", - monthly_stats=monthly_stats, **get_annual_usage_breakdown(yearly_usage, free_sms_allowance), ), } @@ -485,7 +481,7 @@ def get_current_month_for_financial_year(year): def get_stats_date_range(): current_financial_year = get_current_financial_year() current_month = get_current_month_for_financial_year(current_financial_year) - start_date = datetime.now().strftime('%Y-%m-%d') + start_date = datetime.now().strftime("%Y-%m-%d") days = 7 return { "current_financial_year": current_financial_year, diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index 6b9e1d43c..627bd0ccd 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -43,9 +43,11 @@ class ServiceAPIClient(NotifyAdminAPIClient): params={"limit_days": limit_days}, )["data"] - def get_service_notification_statistics_by_day(self, service_id, start_date=None, days=None): + def get_service_notification_statistics_by_day( + self, service_id, start_date=None, days=None + ): if start_date is None: - start_date = datetime.now().strftime('%Y-%m-%d') + start_date = datetime.now().strftime("%Y-%m-%d") return self.get( "/service/{0}/statistics/{1}/{2}".format(service_id, start_date, days), @@ -53,7 +55,9 @@ class ServiceAPIClient(NotifyAdminAPIClient): def get_single_month_notification_stats(self, service_id, year, month): return self.get( - "/service/{0}/notifications/month?year={1}&month={2}".format(service_id, year, month), + "/service/{0}/notifications/month?year={1}&month={2}".format( + service_id, year, month + ), ) def get_services(self, params_dict=None): diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html index cb00254dc..a776ce83c 100644 --- a/app/templates/views/dashboard/dashboard.html +++ b/app/templates/views/dashboard/dashboard.html @@ -22,7 +22,6 @@ Messages sent -

      Job Dashboard

      From fec6c2ff72366dbf60367d36b3e9f973ce615edd Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Tue, 4 Jun 2024 16:27:35 -0700 Subject: [PATCH 28/43] removed comments --- app/assets/javascripts/chartDashboard.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/chartDashboard.js b/app/assets/javascripts/chartDashboard.js index adc9ea255..98378137a 100644 --- a/app/assets/javascripts/chartDashboard.js +++ b/app/assets/javascripts/chartDashboard.js @@ -6,14 +6,12 @@ socket.on('connect', function() { console.log('Connected to the server'); // Debug log, i'll delete later socket.emit('fetch_daily_stats', serviceId); - socket.emit('fetch_single_month_notification_stats', serviceId); - socket.emit('fetch_monthly_stats_by_year', serviceId); }); //this is for previous 7 days socket.on('daily_stats_update', function(data) { console.log('Received daily_stats_update:', data); - // Process the data + var labels = []; var deliveredData = []; // var failureData = []; @@ -31,23 +29,13 @@ myBarChart.data.datasets[0].data = deliveredData; myBarChart.update(); }); - //this is for a single month - socket.on('single_month_notification_stats_update', function(data) { - console.log('Received single_month_notification_stats_update:', data); - // Update Chart.js with new data here - }); - //this is for monthly stats by year - socket.on('monthly_stats_by_year_update', function(data) { - console.log('Received monthly_stats_by_year_update:', data); - // Update Chart.js with new data here - }); socket.on('error', function(data) { console.log('Error:', data); }); sevenDaysButton.addEventListener('click', function() { - socket.emit('fetch_monthly_stats_by_year', serviceId); + socket.emit('fetch_daily_stats', serviceId); console.log('button click'); // Debug log, i'll delete later }); @@ -56,7 +44,7 @@ var myBarChart = new Chart(ctx, { type: 'bar', data: { - labels: [], // Initialize with empty data + labels: [], datasets: [ { label: 'Delivered', From 0a4cf290ac1c8f2e41e247599114ac4c20dbca0f Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Tue, 4 Jun 2024 16:33:55 -0700 Subject: [PATCH 29/43] added flask-socketio --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 9762f7140..d7f3e5c3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ requests = "^2.32.3" six = "^1.16.0" urllib3 = "^2.2.1" webencodings = "^0.5.1" +flask-socketio = "^5.3.6" [tool.poetry.group.dev.dependencies] From fed2aff43c684f59f5e3545a1cd310ef42d61049 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Tue, 4 Jun 2024 16:52:32 -0700 Subject: [PATCH 30/43] cleaned up code --- app/assets/javascripts/chartDashboard.js | 12 +----------- app/main/views/dashboard.py | 24 ------------------------ app/notify_client/service_api_client.py | 7 ------- 3 files changed, 1 insertion(+), 42 deletions(-) diff --git a/app/assets/javascripts/chartDashboard.js b/app/assets/javascripts/chartDashboard.js index 98378137a..e73f655f9 100644 --- a/app/assets/javascripts/chartDashboard.js +++ b/app/assets/javascripts/chartDashboard.js @@ -1,30 +1,21 @@ (function (window) { - + // Initialize flask-socketio var socket = io(); var serviceId = chart.getAttribute('data-service-id'); socket.on('connect', function() { - console.log('Connected to the server'); // Debug log, i'll delete later socket.emit('fetch_daily_stats', serviceId); }); - //this is for previous 7 days socket.on('daily_stats_update', function(data) { - console.log('Received daily_stats_update:', data); - var labels = []; var deliveredData = []; - // var failureData = []; - // var requestedData = []; for (var date in data) { labels.push(date); deliveredData.push(data[date].sms.delivered); - // failureData.push(data[date].sms.failure); - // requestedData.push(data[date].sms.requested); } - // Update Chart.js myBarChart.data.labels = labels; myBarChart.data.datasets[0].data = deliveredData; myBarChart.update(); @@ -36,7 +27,6 @@ sevenDaysButton.addEventListener('click', function() { socket.emit('fetch_daily_stats', serviceId); - console.log('button click'); // Debug log, i'll delete later }); // Initialize Chart.js bar chart diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 7ad899a53..a64444dc2 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -46,30 +46,6 @@ def handle_fetch_daily_stats(service_id): emit("error", {"error": "No service_id provided"}) -@socketio.on("fetch_single_month_notification_stats") -def handle_fetch_single_month_notification_stats(service_id): - date_range = get_stats_date_range() - single_month_notification_stats = ( - service_api_client.get_single_month_notification_stats( - service_id, - year=date_range["current_financial_year"], - month=date_range["current_month"], - ) - ) - emit("single_month_notification_stats_update", single_month_notification_stats) - - -@socketio.on("fetch_monthly_stats_by_year") -def handle_fetch_monthly_stats(service_id): - date_range = get_stats_date_range() - monthly_stats_by_year_stats = format_monthly_stats_to_list( - service_api_client.get_monthly_notification_stats( - service_id, year=date_range["current_financial_year"] - )["data"] - ) - emit("monthly_stats_by_year_update", monthly_stats_by_year_stats) - - @main.route("/services//dashboard") @user_has_permissions("view_activity", "send_messages") def old_service_dashboard(service_id): diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index 627bd0ccd..42f54572f 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -53,13 +53,6 @@ class ServiceAPIClient(NotifyAdminAPIClient): "/service/{0}/statistics/{1}/{2}".format(service_id, start_date, days), )["data"] - def get_single_month_notification_stats(self, service_id, year, month): - return self.get( - "/service/{0}/notifications/month?year={1}&month={2}".format( - service_id, year, month - ), - ) - def get_services(self, params_dict=None): """ Retrieve a list of services. From 43d67a4821c64326e9d8735eabe7ae90f265d00a Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Wed, 5 Jun 2024 16:55:17 -0400 Subject: [PATCH 31/43] Update Python dependencies - 6/5/2024 This changeset updates several Python dependencies flagged by Dependabot. Signed-off-by: Carlo Costino --- poetry.lock | 100 ++++++++++++++++++++++++------------------------- pyproject.toml | 10 ++--- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8cb15a59d..5fa01c2b7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -171,17 +171,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.117" +version = "1.34.119" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.117-py3-none-any.whl", hash = "sha256:1506589e30566bbb2f4997b60968ff7d4ef8a998836c31eedd36437ac3b7408a"}, - {file = "boto3-1.34.117.tar.gz", hash = "sha256:c8a383b904d6faaf7eed0c06e31b423db128e4c09ce7bd2afc39d1cd07030a51"}, + {file = "boto3-1.34.119-py3-none-any.whl", hash = "sha256:8f9c43c54b3dfaa36c4a0d7b42c417227a515bc7a2e163e62802780000a5a3e2"}, + {file = "boto3-1.34.119.tar.gz", hash = "sha256:cea2365a25b2b83a97e77f24ac6f922ef62e20636b42f9f6ee9f97188f9c1c03"}, ] [package.dependencies] -botocore = ">=1.34.117,<1.35.0" +botocore = ">=1.34.119,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -190,13 +190,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.117" +version = "1.34.119" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.117-py3-none-any.whl", hash = "sha256:26a431997f882bcdd1e835f44c24b2a1752b1c4e5183c2ce62999ce95d518d6c"}, - {file = "botocore-1.34.117.tar.gz", hash = "sha256:4637ca42e6c51aebc4d9a2d92f97bf4bdb042e3f7985ff31a659a11e4c170e73"}, + {file = "botocore-1.34.119-py3-none-any.whl", hash = "sha256:4bdf7926a1290b2650d62899ceba65073dd2693e61c35f5cdeb3a286a0aaa27b"}, + {file = "botocore-1.34.119.tar.gz", hash = "sha256:b253f15b24b87b070e176af48e8ef146516090429d30a7d8b136a4c079b28008"}, ] [package.dependencies] @@ -526,43 +526,43 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "42.0.7" +version = "42.0.8" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"}, - {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"}, - {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"}, - {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"}, - {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"}, - {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"}, - {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"}, - {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"}, - {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"}, - {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"}, - {file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"}, - {file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"}, - {file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"}, - {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"}, - {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"}, - {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"}, - {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"}, - {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"}, - {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"}, - {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"}, - {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"}, - {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"}, - {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"}, - {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, + {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, + {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, + {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, + {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, + {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, + {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, ] [package.dependencies] @@ -1698,13 +1698,13 @@ infinite-tracing = ["grpcio", "protobuf"] [[package]] name = "nodeenv" -version = "1.9.0" +version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "nodeenv-1.9.0-py2.py3-none-any.whl", hash = "sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a"}, - {file = "nodeenv-1.9.0.tar.gz", hash = "sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1"}, + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] [[package]] @@ -1847,13 +1847,13 @@ files = [ [[package]] name = "phonenumbers" -version = "8.13.37" +version = "8.13.38" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." optional = false python-versions = "*" files = [ - {file = "phonenumbers-8.13.37-py2.py3-none-any.whl", hash = "sha256:4ea00ef5012422c08c7955c21131e7ae5baa9a3ef52cf2d561e963f023006b80"}, - {file = "phonenumbers-8.13.37.tar.gz", hash = "sha256:bd315fed159aea0516f7c367231810fe8344d5bec26156b88fa18374c11d1cf2"}, + {file = "phonenumbers-8.13.38-py2.py3-none-any.whl", hash = "sha256:d22aa747fb591ef2a18afec13cab5a0e294ab20fce5a1560e4949e459e70eeef"}, + {file = "phonenumbers-8.13.38.tar.gz", hash = "sha256:2822c74ee9334e9d8ad792fc352cc8d21004307349b6b1bb61da12937fa2eaba"}, ] [[package]] @@ -2249,13 +2249,13 @@ certifi = "*" [[package]] name = "pytest" -version = "8.2.1" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, - {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] @@ -3001,4 +3001,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "91a817ed33b8f8182673b00dd8b340c4aff9697cfa093051a86360c6cef4ec49" +content-hash = "e96b87c048826ecb23d826c45b516aee60532cd2677a22332dc86fef498a411d" diff --git a/pyproject.toml b/pyproject.toml index 8b77d4bec..5c7c88eaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,18 +39,18 @@ wtforms = "~=3.1" markdown = "^3.5.2" async-timeout = "^4.0.3" bleach = "^6.1.0" -boto3 = "^1.34.117" -botocore = "^1.34.117" +boto3 = "^1.34.119" +botocore = "^1.34.119" cachetools = "^5.3.3" cffi = "^1.16.0" -cryptography = "^42.0.7" +cryptography = "^42.0.8" flask-redis = "^0.4.0" geojson = "^3.1.0" jmespath = "^1.0.1" mistune = "0.8.4" numpy = "^1.26.4" ordered-set = "^4.1.0" -phonenumbers = "^8.13.37" +phonenumbers = "^8.13.38" pycparser = "^2.22" python-json-logger = "^2.0.7" redis = "^5.0.4" @@ -85,7 +85,7 @@ jinja2-cli = {version = "==0.8.2", extras = ["yaml"]} moto = "*" pip-audit = "*" pre-commit = "^3.7.1" -pytest = "^8.2.1" +pytest = "^8.2.2" pytest-env = "^1.1.3" pytest-mock = "^3.14.0" pytest-playwright = "^0.5.0" From e5a9eed626f17352ec92b54ff348a8b20f5aa98d Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Wed, 5 Jun 2024 14:56:22 -0700 Subject: [PATCH 32/43] installing socketio --- app/templates/new/components/head.html | 1 - gulpfile.js | 3 +- package-lock.json | 84 +++++++++++++++++++++++++- package.json | 1 + 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/app/templates/new/components/head.html b/app/templates/new/components/head.html index f7c7153e2..51f3c4da3 100644 --- a/app/templates/new/components/head.html +++ b/app/templates/new/components/head.html @@ -32,7 +32,6 @@ {# google #} - {% if g.hide_from_search_engines %} diff --git a/gulpfile.js b/gulpfile.js index 98afbbacf..836596165 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -96,7 +96,8 @@ const javascripts = () => { paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js', paths.npm + 'timeago/jquery.timeago.js', paths.npm + 'textarea-caret/index.js', - paths.npm + 'cbor-js/cbor.js' + paths.npm + 'cbor-js/cbor.js', + paths.npm + 'socket.io-client/dist/socket.io.min.js' ])); // JS local to this application diff --git a/package-lock.json b/package-lock.json index c96f68f8a..30688bfde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "python": "^0.0.4", "query-command-supported": "1.0.0", "sass-embedded": "^1.69.5", + "socket.io-client": "^4.7.5", "textarea-caret": "3.1.0", "timeago": "1.6.7" }, @@ -2592,6 +2593,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4644,7 +4650,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -5039,6 +5044,46 @@ "once": "^1.4.0" } }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -10779,8 +10824,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multipipe": { "version": "0.1.2", @@ -13127,6 +13171,32 @@ "urix": "^0.1.0" } }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -14511,6 +14581,14 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", diff --git a/package.json b/package.json index b60893b57..38cc83797 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "python": "^0.0.4", "query-command-supported": "1.0.0", "sass-embedded": "^1.69.5", + "socket.io-client": "^4.7.5", "textarea-caret": "3.1.0", "timeago": "1.6.7" }, From 9d3dc047ea20cbd92fadf256d8601c301b6f0a62 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Wed, 5 Jun 2024 15:03:19 -0700 Subject: [PATCH 33/43] added chartjs to path in gulp --- .ds.baseline | 4 ++-- app/templates/new/components/head.html | 1 - gulpfile.js | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ds.baseline b/.ds.baseline index 859f30b4d..ec87d9c30 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -423,7 +423,7 @@ "filename": "app/templates/new/components/head.html", "hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327", "is_verified": false, - "line_number": 35, + "line_number": 34, "is_secret": false } ], @@ -710,5 +710,5 @@ } ] }, - "generated_at": "2024-05-29T21:18:03Z" + "generated_at": "2024-06-05T22:01:56Z" } diff --git a/app/templates/new/components/head.html b/app/templates/new/components/head.html index 51f3c4da3..dd7519cf4 100644 --- a/app/templates/new/components/head.html +++ b/app/templates/new/components/head.html @@ -31,7 +31,6 @@ {# google #} - {% if g.hide_from_search_engines %} diff --git a/gulpfile.js b/gulpfile.js index 836596165..b68da089f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -97,7 +97,8 @@ const javascripts = () => { paths.npm + 'timeago/jquery.timeago.js', paths.npm + 'textarea-caret/index.js', paths.npm + 'cbor-js/cbor.js', - paths.npm + 'socket.io-client/dist/socket.io.min.js' + paths.npm + 'socket.io-client/dist/socket.io.min.js', + paths.npm + 'chart.js/dist/chart.umd.js' ])); // JS local to this application From 61253963f746ed24469532c142b139ec58a2fa55 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 6 Jun 2024 00:48:08 -0700 Subject: [PATCH 34/43] added socketiotestclent --- tests/app/main/views/test_dashboard.py | 68 +++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index 926163dbd..f92efbb19 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -3,15 +3,21 @@ import json from datetime import datetime import pytest -from flask import url_for +from flask import Flask, url_for +from flask_socketio import SocketIOTestClient from freezegun import freeze_time +from app import ( + create_app, + socketio +) from app.main.views.dashboard import ( aggregate_notifications_stats, aggregate_status_types, aggregate_template_usage, format_monthly_stats_to_list, get_dashboard_totals, + get_stats_date_range, get_tuples_of_financial_years, ) from tests import ( @@ -23,6 +29,7 @@ from tests import ( from tests.conftest import ( ORGANISATION_ID, SERVICE_ONE_ID, + SERVICE_TWO_ID, create_active_caseworking_user, create_active_user_view_permissions, normalize_spaces, @@ -1875,3 +1882,62 @@ def test_service_dashboard_shows_batched_jobs( assert job_table_body is not None assert len(rows) == 1 + + +@pytest.fixture +def app_with_socketio(): + app = Flask("app") + create_app(app) + return app, socketio + + +@pytest.mark.parametrize( + "service_id, date_range, expected_call_args", + [ + (SERVICE_ONE_ID, {"start_date": "2024-01-01", "days": 7}, {"service_id": SERVICE_ONE_ID, "start_date": "2024-01-01", "days": 7}), + (SERVICE_TWO_ID, {"start_date": "2023-06-01", "days": 7}, {"service_id": SERVICE_TWO_ID, "start_date": "2023-06-01", "days": 7}), + ] +) +def test_fetch_daily_stats( + app_with_socketio, mocker, + service_id, + date_range, + expected_call_args +): + app, socketio = app_with_socketio + + mocker.patch( + "app.main.views.dashboard.get_stats_date_range", + return_value=date_range + ) + + mock_service_api = mocker.patch( + "app.service_api_client.get_service_notification_statistics_by_day", + return_value={ + date_range["start_date"]: {"email": {"delivered": 0, "failure": 0, "requested": 0}, "sms": {"delivered": 0, "failure": 1, "requested": 1}}, + } + ) + + client = SocketIOTestClient(app, socketio) + try: + connected = client.is_connected() + assert connected, "Client should be connected" + + client.emit('fetch_daily_stats', service_id) + + received = client.get_received() + assert received, "Should receive a response message" + assert received[0]['name'] == 'daily_stats_update' + assert received[0]['args'][0] == { + date_range["start_date"]: {"email": {"delivered": 0, "failure": 0, "requested": 0}, "sms": {"delivered": 0, "failure": 1, "requested": 1}}, + } + + mock_service_api.assert_called_once_with( + service_id, + start_date=expected_call_args["start_date"], + days=expected_call_args["days"] + ) + finally: + client.disconnect() + disconnected = not client.is_connected() + assert disconnected, "Client should be disconnected" From ae7a14f3f2570d90ef85e36c4fdd914eae8fa89c Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 6 Jun 2024 01:10:09 -0700 Subject: [PATCH 35/43] fixed js chart referenced --- app/assets/javascripts/chartDashboard.js | 104 ++++++++++--------- app/templates/views/dashboard/dashboard.html | 3 +- 2 files changed, 58 insertions(+), 49 deletions(-) diff --git a/app/assets/javascripts/chartDashboard.js b/app/assets/javascripts/chartDashboard.js index e73f655f9..d191abef1 100644 --- a/app/assets/javascripts/chartDashboard.js +++ b/app/assets/javascripts/chartDashboard.js @@ -1,56 +1,66 @@ (function (window) { - // Initialize flask-socketio - var socket = io(); - var serviceId = chart.getAttribute('data-service-id'); - socket.on('connect', function() { - socket.emit('fetch_daily_stats', serviceId); - }); - - socket.on('daily_stats_update', function(data) { - var labels = []; - var deliveredData = []; - - for (var date in data) { - labels.push(date); - deliveredData.push(data[date].sms.delivered); + function initializeChartAndSocket() { + var ctx = document.getElementById('myChart'); + if (!ctx) { + return; } - myBarChart.data.labels = labels; - myBarChart.data.datasets[0].data = deliveredData; - myBarChart.update(); - }); - - socket.on('error', function(data) { - console.log('Error:', data); - }); - - sevenDaysButton.addEventListener('click', function() { - socket.emit('fetch_daily_stats', serviceId); - }); - - // Initialize Chart.js bar chart - var ctx = document.getElementById('myChart').getContext('2d'); - var myBarChart = new Chart(ctx, { - type: 'bar', - data: { - labels: [], - datasets: [ - { - label: 'Delivered', - data: [], - backgroundColor: '#0076d6', - stack: 'Stack 0' - }, - ] - }, - options: { - scales: { - y: { - beginAtZero: true + var myBarChart = new Chart(ctx.getContext('2d'), { + type: 'bar', + data: { + labels: [], + datasets: [ + { + label: 'Delivered', + data: [], + backgroundColor: '#0076d6', + stack: 'Stack 0' + }, + ] + }, + options: { + scales: { + y: { + beginAtZero: true + } } } + }); + + var socket = io(); + var serviceId = ctx.getAttribute('data-service-id'); + + socket.on('connect', function() { + socket.emit('fetch_daily_stats', serviceId); + }); + + socket.on('daily_stats_update', function(data) { + var labels = []; + var deliveredData = []; + + for (var date in data) { + labels.push(date); + deliveredData.push(data[date].sms.delivered); + } + + myBarChart.data.labels = labels; + myBarChart.data.datasets[0].data = deliveredData; + myBarChart.update(); + }); + + socket.on('error', function(data) { + console.log('Error:', data); + }); + + var sevenDaysButton = document.getElementById('sevenDaysButton'); + if (sevenDaysButton) { + sevenDaysButton.addEventListener('click', function() { + socket.emit('fetch_daily_stats', serviceId); + }); } - }); + } + + document.addEventListener('DOMContentLoaded', initializeChartAndSocket); })(window); diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html index a776ce83c..7b04f9baf 100644 --- a/app/templates/views/dashboard/dashboard.html +++ b/app/templates/views/dashboard/dashboard.html @@ -23,8 +23,7 @@ - -
      + {{ ajax_block(partials, updates_url, 'inbox') }} {{ ajax_block(partials, updates_url, 'totals') }} From 82690d6e3d3ccb70f5176271685bd4ad237dbd2c Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Thu, 6 Jun 2024 09:34:49 -0700 Subject: [PATCH 36/43] add debug for user issue --- app/main/views/send.py | 35 ++++++++++++++++++++++++++++++++-- app/utils/csv.py | 18 +++++++++++++++++ notifications_utils/logging.py | 13 +++++++++++++ poetry.lock | 1 + 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/app/main/views/send.py b/app/main/views/send.py index aab2db104..8bb6ce24c 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -3,7 +3,16 @@ import uuid from string import ascii_uppercase from zipfile import BadZipFile -from flask import abort, flash, redirect, render_template, request, session, url_for +from flask import ( + abort, + current_app, + flash, + redirect, + render_template, + request, + session, + url_for, +) from flask_login import current_user from markupsafe import Markup from notifications_python_client.errors import HTTPError @@ -31,12 +40,18 @@ from app.s3_client.s3_csv_client import ( s3upload, set_metadata_on_csv_upload, ) -from app.utils import PermanentRedirect, should_skip_template_page, unicode_truncate +from app.utils import ( + PermanentRedirect, + hilite, + should_skip_template_page, + unicode_truncate, +) from app.utils.csv import Spreadsheet, get_errors_for_csv from app.utils.templates import get_template from app.utils.user import user_has_permissions from notifications_utils import SMS_CHAR_COUNT_LIMIT from notifications_utils.insensitive_dict import InsensitiveDict +from notifications_utils.logging import scrub from notifications_utils.recipients import RecipientCSV, first_column_headings from notifications_utils.sanitise_text import SanitiseASCII @@ -938,6 +953,9 @@ def send_notification(service_id, template_id): ) ) + current_app.logger.info( + hilite(scrub(f"Recipient for the one-off will be {recipient}")) + ) keys = [] values = [] for k, v in session["placeholders"].items(): @@ -971,6 +989,19 @@ def send_notification(service_id, template_id): valid="True", ) + # Here we are attempting to cleverly link the job id to the one-off recipient + # If we know the partial phone number of the recipient, we can search + # on that initially and find this, which will give us the job_id + # And once we know the job_id, we can search on that and it might tell us something + # about report generation. + current_app.logger.info( + hilite( + scrub( + f"Created job to send one-off, recipient is {recipient}, job_id is {upload_id}" + ) + ) + ) + session.pop("recipient") session.pop("placeholders") diff --git a/app/utils/csv.py b/app/utils/csv.py index c3c27ec18..a8d743adc 100644 --- a/app/utils/csv.py +++ b/app/utils/csv.py @@ -1,10 +1,13 @@ import datetime import pytz +from flask import current_app, json from flask_login import current_user from app.models.spreadsheet import Spreadsheet +from app.utils import hilite from app.utils.templates import get_sample_template +from notifications_utils.logging import scrub from notifications_utils.recipients import RecipientCSV @@ -71,7 +74,22 @@ def generate_notifications_csv(**kwargs): # This generates the "batch" csv report if kwargs.get("job_id"): + # The kwargs contain the job id, which is linked to the recipient's partial phone number in other debug + try: + current_app.logger.info( + hilite(f"Setting up report with kwargs {scrub(json.dumps(kwargs))}") + ) + except TypeError: + pass + original_file_contents = s3download(kwargs["service_id"], kwargs["job_id"]) + # This will verify that the user actually did successfully upload a csv for a one-off. Limit the size + # we display to 999 characters, because we don't want to show the contents for reports with thousands of rows. + current_app.logger.info( + hilite( + f"Original csv for job_id {kwargs['job_id']}: {scrub(original_file_contents[0:999])}" + ) + ) original_upload = RecipientCSV( original_file_contents, template=get_sample_template(kwargs["template_type"]), diff --git a/notifications_utils/logging.py b/notifications_utils/logging.py index 6a209cdd3..7c56a00ad 100644 --- a/notifications_utils/logging.py +++ b/notifications_utils/logging.py @@ -1,5 +1,6 @@ import logging import logging.handlers +import re import sys from itertools import product @@ -131,3 +132,15 @@ class JSONFormatter(BaseJSONFormatter): except (KeyError, IndexError) as e: logger.exception("failed to format log message: {} not found".format(e)) return log_record + + +def scrub(msg): + # Eventually we want to scrub all messages in all logs for phone numbers + # and email addresses, masking them. Ultimately this will probably get + # refactored into a 'SafeLogger' subclass or something, but let's start here + # with phones. + phones = re.findall("(?:\\+ *)?\\d[\\d\\- ]{7,}\\d", msg) + phones = [phone.replace("-", "").replace(" ", "") for phone in phones] + for phone in phones: + msg = msg.replace(phone, f"1XXXXX{phone[-5:]}") + return msg diff --git a/poetry.lock b/poetry.lock index 8cb15a59d..331962043 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1641,6 +1641,7 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] From f98cff8d40546509d6c5c8c5e5a2fd34ea7d638a Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 6 Jun 2024 10:01:58 -0700 Subject: [PATCH 37/43] fixed linting issues --- tests/app/main/views/test_dashboard.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index f92efbb19..d46235668 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -1884,7 +1884,7 @@ def test_service_dashboard_shows_batched_jobs( assert len(rows) == 1 -@pytest.fixture +@pytest.fixture() def app_with_socketio(): app = Flask("app") create_app(app) @@ -1892,10 +1892,18 @@ def app_with_socketio(): @pytest.mark.parametrize( - "service_id, date_range, expected_call_args", + ("service_id", "date_range", "expected_call_args"), [ - (SERVICE_ONE_ID, {"start_date": "2024-01-01", "days": 7}, {"service_id": SERVICE_ONE_ID, "start_date": "2024-01-01", "days": 7}), - (SERVICE_TWO_ID, {"start_date": "2023-06-01", "days": 7}, {"service_id": SERVICE_TWO_ID, "start_date": "2023-06-01", "days": 7}), + ( + SERVICE_ONE_ID, + {"start_date": "2024-01-01", "days": 7}, + {"service_id": SERVICE_ONE_ID, "start_date": "2024-01-01", "days": 7} + ), + ( + SERVICE_TWO_ID, + {"start_date": "2023-06-01", "days": 7}, + {"service_id": SERVICE_TWO_ID, "start_date": "2023-06-01", "days": 7} + ), ] ) def test_fetch_daily_stats( @@ -1914,7 +1922,10 @@ def test_fetch_daily_stats( mock_service_api = mocker.patch( "app.service_api_client.get_service_notification_statistics_by_day", return_value={ - date_range["start_date"]: {"email": {"delivered": 0, "failure": 0, "requested": 0}, "sms": {"delivered": 0, "failure": 1, "requested": 1}}, + date_range["start_date"]: { + "email": {"delivered": 0, "failure": 0, "requested": 0}, + "sms": {"delivered": 0, "failure": 1, "requested": 1} + }, } ) @@ -1929,7 +1940,10 @@ def test_fetch_daily_stats( assert received, "Should receive a response message" assert received[0]['name'] == 'daily_stats_update' assert received[0]['args'][0] == { - date_range["start_date"]: {"email": {"delivered": 0, "failure": 0, "requested": 0}, "sms": {"delivered": 0, "failure": 1, "requested": 1}}, + date_range["start_date"]: { + "email": {"delivered": 0, "failure": 0, "requested": 0}, + "sms": {"delivered": 0, "failure": 1, "requested": 1} + }, } mock_service_api.assert_called_once_with( From e413f8c640c96c625edec8469a6cd57b53ee7d3d Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 6 Jun 2024 10:05:36 -0700 Subject: [PATCH 38/43] added sampleChartDashboard.js --- .../{chartDashboard.js => sampleChartDashboard.js} | 0 app/templates/views/dashboard/dashboard.html | 4 ++-- gulpfile.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename app/assets/javascripts/{chartDashboard.js => sampleChartDashboard.js} (100%) diff --git a/app/assets/javascripts/chartDashboard.js b/app/assets/javascripts/sampleChartDashboard.js similarity index 100% rename from app/assets/javascripts/chartDashboard.js rename to app/assets/javascripts/sampleChartDashboard.js diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html index 7b04f9baf..c1ba46caf 100644 --- a/app/templates/views/dashboard/dashboard.html +++ b/app/templates/views/dashboard/dashboard.html @@ -22,8 +22,8 @@ Messages sent - - + {{ ajax_block(partials, updates_url, 'inbox') }} {{ ajax_block(partials, updates_url, 'totals') }} diff --git a/gulpfile.js b/gulpfile.js index b68da089f..0e2c1eaac 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -127,7 +127,7 @@ const javascripts = () => { paths.src + 'javascripts/date.js', paths.src + 'javascripts/loginAlert.js', paths.src + 'javascripts/main.js', - paths.src + 'javascripts/chartDashboard.js', + paths.src + 'javascripts/sampleChartDashboard.js', ]) .pipe(plugins.prettyerror()) .pipe(plugins.babel({ From c0b2c42473cb3d1ab15bd63d029d7d9ea5e6f424 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 6 Jun 2024 10:42:49 -0700 Subject: [PATCH 39/43] removed imported unused style issue --- tests/app/main/views/test_dashboard.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index d46235668..5e4e73f62 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -17,7 +17,6 @@ from app.main.views.dashboard import ( aggregate_template_usage, format_monthly_stats_to_list, get_dashboard_totals, - get_stats_date_range, get_tuples_of_financial_years, ) from tests import ( From 83f5b6f0ab4ac6dda7532a5d1a7bbb84497f593f Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Thu, 6 Jun 2024 10:43:41 -0700 Subject: [PATCH 40/43] add comment --- app/utils/csv.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/utils/csv.py b/app/utils/csv.py index a8d743adc..5c5b794de 100644 --- a/app/utils/csv.py +++ b/app/utils/csv.py @@ -75,6 +75,8 @@ def generate_notifications_csv(**kwargs): # This generates the "batch" csv report if kwargs.get("job_id"): # The kwargs contain the job id, which is linked to the recipient's partial phone number in other debug + # Some unit tests are mocking the kwargs and turning them into a function instead of dict, + # hence the try/except. try: current_app.logger.info( hilite(f"Setting up report with kwargs {scrub(json.dumps(kwargs))}") From 934f8927c65e927d69419a97d473219c57801253 Mon Sep 17 00:00:00 2001 From: Jonathan Bobel Date: Thu, 6 Jun 2024 13:50:08 -0400 Subject: [PATCH 41/43] Updated content, removed test no longer needed --- app/templates/views/message-status.html | 40 +++++++++++++++++++------ app/templates/views/trial-mode.html | 2 +- tests/app/main/views/test_index.py | 20 ------------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/app/templates/views/message-status.html b/app/templates/views/message-status.html index 089a29f49..d9dae439f 100644 --- a/app/templates/views/message-status.html +++ b/app/templates/views/message-status.html @@ -9,9 +9,13 @@

      Delivery status

      -

      Notify’s real-time dashboard lets you check the status of any message.

      -

      For security, this information is only available for seven days after a message has been sent. You can download a report, including a list of sent messages, for your own records.

      -

      This page describes the statuses you'll see when you're signed in to Notify.

      +

      Notify starts sending your text messages immediately. Each message is sent to its cell phone carrier, which attempts + delivery to the recipient. This process is often almost instantaneous, but can sometimes take a while if a phone is + unavailable. The carrier will continue to try to deliver the message for up to 72 hours.

      +

      The Notify dashboard provides a high-level view of the number of messages Sent, Pending, Delivered, and Failed. The + dashboard data starts to update about five minutes after a message is sent and will continue to update for four hours.

      +

      Delivery statuses reflect delivery to a specific cell phone number, not to a person. No status can confirm that a + specific person actually received or read the message.

      Text messages

      -
      +
      {% call mapping_table( caption='Message statuses – text messages', field_headings=['Status', 'Description'], @@ -53,12 +57,14 @@ caption_visible=False ) %} {% for message_status, description in [ - ('Total', 'The total number of messages that have been sent during the last seven days.'), - ('Pending', 'Notify has sent the message to the provider. The provider will try to deliver the message to the recipient for up to 72 hours. Notify is waiting for delivery information.'), + ('Scheduled', 'The total number of messages that have been scheduled to be sent at some future time.'), + ('Canceled', 'Messages that were created and scheduled, but canceled prior to Sending.'), + ('Total', 'The total number of messages that have been sent during the specified time.'), + ('Pending', 'Notify has sent the message to the provider. The provider will try to deliver the message to the recipient for up to 72 + hours. “Pending” indicates that Notify is waiting for delivery information.'), ('Delivered', 'The message was successfully delivered. Notify cannot tell you if a user has opened or read a message.'), - ('Failed', 'The provider could not deliver the message. This can happen if the phone number was wrong or if the network operator - rejects the message. If you’re sure that these phone numbers are correct, you should contact us. If not, you should remove them from your database. You’ll still be charged for text messages that - cannot be delivered.' | safe), + ('Failed', 'The message could not be delivered.'), + ('Process error / Delivery not attempted', 'If you receive a large number of process errors, please contact the Notify team.'), ] %} {% call row() %} {{ text_field(message_status) }} @@ -67,5 +73,21 @@ {% endfor %} {% endcall %}
      +

      About carrier statuses

      +

      Sometimes Notify receives more detailed information from the carriers on the status of messages, and these can be found + in the downloadable reports. Not all carriers provide the same level of detail regarding delivery and some delivery + statutes have a slight variation in word choice. Notify includes this information in the reports to provide you as much + detail as possible. Remember, for security purposes, detailed information is only available for seven days after a + message has been sent.

      + +

      Opting out

      +

      A text recipient can opt out of receiving text messages from your phone number at any time by responding “STOP” or + “QUIT” or one of several other keywords. If they opt out,

      +
        +
      • They may receive a response from their carrier stating that they will no longer receive text messages from that number.
      • +
      • Any subsequent messages sent to that number will not be delivered (unless the recipient opts back in by texting “START” + or “OPT-IN”).
      • +
      +

      Notify.gov does not yet have a way to pull opt-out status and make it available in the UI for agencies to download.

      {% endblock %} diff --git a/app/templates/views/trial-mode.html b/app/templates/views/trial-mode.html index 7ca6d2b97..cb0dca0ec 100644 --- a/app/templates/views/trial-mode.html +++ b/app/templates/views/trial-mode.html @@ -21,7 +21,7 @@
    • A text message of 160-306 characters is two parts.
    • For more information on how message parts are calculated, see - Pricing.

      + Tracking usage.

      Before going Live

      Before you request to make your service live so you can send messages to clients:

      diff --git a/tests/app/main/views/test_index.py b/tests/app/main/views/test_index.py index 4ee5a817f..57bdfaa70 100644 --- a/tests/app/main/views/test_index.py +++ b/tests/app/main/views/test_index.py @@ -179,26 +179,6 @@ def test_old_static_pages_redirect(client_request, view, expected_view): ) -def test_message_status_page_contains_message_status_ids(client_request): - # The 'email-statuses' and 'sms-statuses' id are linked to when we display a message status, - # so this test ensures we don't accidentally remove them - page = client_request.get("main.message_status") - - # email-statuses is commented out in view - # assert page.find(id='email-statuses') - assert page.find(id="text-message-statuses") - - -def test_message_status_page_contains_link_to_support(client_request): - page = client_request.get("main.message_status") - sms_status_table = page.find(id="text-message-statuses").findNext("tbody") - - temp_fail_details_cell = sms_status_table.select_one( - "tr:nth-child(4) > td:nth-child(2)" - ) - assert temp_fail_details_cell.find("a").attrs["href"] == url_for("main.support") - - def test_old_using_notify_page(client_request): client_request.get("main.using_notify", _expected_status=410) From 33f8f4c0000161ce0415c2e8fd6a87c4e5ce24a9 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 6 Jun 2024 10:50:24 -0700 Subject: [PATCH 42/43] fix import sort --- tests/app/main/views/test_dashboard.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index 5e4e73f62..285444b1b 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -7,10 +7,7 @@ from flask import Flask, url_for from flask_socketio import SocketIOTestClient from freezegun import freeze_time -from app import ( - create_app, - socketio -) +from app import create_app, socketio from app.main.views.dashboard import ( aggregate_notifications_stats, aggregate_status_types, From 63bd660403f60e81dad05fed8d41a68e17440fda Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Fri, 7 Jun 2024 08:03:43 -0700 Subject: [PATCH 43/43] code review feedback --- tests/notifications_utils/test_logging.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/notifications_utils/test_logging.py b/tests/notifications_utils/test_logging.py index cc7f5ee34..858b9352b 100644 --- a/tests/notifications_utils/test_logging.py +++ b/tests/notifications_utils/test_logging.py @@ -49,3 +49,13 @@ def test_base_json_formatter_contains_service_id(): == "message to log" ) assert service_id_filter.filter(record).service_id == "no-service-id" + + +def test_scrub(): + result = logging.scrub( + "This is a message with 17775554324, and also 18884449323 and also 17775554324" + ) + assert ( + result + == "This is a message with 1XXXXX54324, and also 1XXXXX49323 and also 1XXXXX54324" + )