diff --git a/.ds.baseline b/.ds.baseline index b7e7b6c11..076016c38 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -469,7 +469,7 @@ "filename": "tests/app/main/test_errorhandlers.py", "hashed_secret": "005fa73b3f2be8f0d71d361c1f0a9d787cd09b4e", "is_verified": false, - "line_number": 33, + "line_number": 34, "is_secret": false } ], @@ -634,5 +634,5 @@ } ] }, - "generated_at": "2025-06-04T16:12:20Z" + "generated_at": "2025-06-10T18:39:51Z" } diff --git a/app/__init__.py b/app/__init__.py index a6a4a615b..e47a263d8 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -24,7 +24,6 @@ from flask_talisman import Talisman from flask_wtf import CSRFProtect from flask_wtf.csrf import CSRFError from itsdangerous import BadSignature -from notifications_python_client.errors import HTTPError from werkzeug.exceptions import HTTPException as WerkzeugHTTPException from werkzeug.exceptions import abort from werkzeug.local import LocalProxy @@ -111,6 +110,7 @@ from app.notify_client.user_api_client import user_api_client from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter from app.utils.api_health import is_api_down from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend +from notifications_python_client.errors import HTTPError from notifications_utils import logging, request_helper from notifications_utils.formatters import ( formatted_list, diff --git a/app/main/views/add_service.py b/app/main/views/add_service.py index 339fd9e7c..f6ec2fe50 100644 --- a/app/main/views/add_service.py +++ b/app/main/views/add_service.py @@ -1,12 +1,12 @@ from flask import current_app, redirect, render_template, session, url_for from flask_login import current_user -from notifications_python_client.errors import HTTPError from app import service_api_client from app.formatters import email_safe from app.main import main from app.main.forms import CreateServiceForm from app.utils.user import user_is_gov_user, user_is_logged_in +from notifications_python_client.errors import HTTPError def _create_service(service_name, organization_type, email_from, form): diff --git a/app/main/views/conversation.py b/app/main/views/conversation.py index a3ac47da7..35d82fe09 100644 --- a/app/main/views/conversation.py +++ b/app/main/views/conversation.py @@ -1,12 +1,12 @@ from flask import jsonify, redirect, render_template, session, url_for from flask_login import current_user -from notifications_python_client.errors import HTTPError from app import current_service, notification_api_client, service_api_client from app.main import main from app.main.forms import SearchByNameForm from app.models.template_list import TemplateList from app.utils.user import user_has_permissions +from notifications_python_client.errors import HTTPError from notifications_utils.recipients import format_phone_number_human_readable from notifications_utils.template import SMSPreviewTemplate diff --git a/app/main/views/find_users.py b/app/main/views/find_users.py index 19ec5dce7..ef55ad2a4 100644 --- a/app/main/views/find_users.py +++ b/app/main/views/find_users.py @@ -1,6 +1,5 @@ from flask import flash, redirect, render_template, request, url_for from flask_login import current_user -from notifications_python_client.errors import HTTPError from app import user_api_client from app.event_handlers import create_archive_user_event @@ -8,6 +7,7 @@ from app.main import main from app.main.forms import AdminSearchUsersByEmailForm, AuthTypeForm from app.models.user import User from app.utils.user import user_is_platform_admin +from notifications_python_client.errors import HTTPError @main.route("/find-users-by-email", methods=["GET", "POST"]) diff --git a/app/main/views/forgot_password.py b/app/main/views/forgot_password.py index 0aa72e0de..360d495b5 100644 --- a/app/main/views/forgot_password.py +++ b/app/main/views/forgot_password.py @@ -1,9 +1,9 @@ from flask import render_template, request -from notifications_python_client.errors import HTTPError from app import user_api_client from app.main import main from app.main.forms import ForgotPasswordForm +from notifications_python_client.errors import HTTPError @main.route("/forgot-password", methods=["GET", "POST"]) diff --git a/app/main/views/manage_users.py b/app/main/views/manage_users.py index b55341e08..66e00c48e 100644 --- a/app/main/views/manage_users.py +++ b/app/main/views/manage_users.py @@ -1,6 +1,5 @@ from flask import abort, flash, redirect, render_template, request, session, url_for from flask_login import current_user -from notifications_python_client.errors import HTTPError from app import current_service, service_api_client from app.event_handlers import ( @@ -24,6 +23,7 @@ from app.main.forms import ( from app.models.user import InvitedUser, User from app.utils.user import is_gov_user, user_has_permissions from app.utils.user_permissions import permission_options +from notifications_python_client.errors import HTTPError @main.route("/services//users") diff --git a/app/main/views/organizations.py b/app/main/views/organizations.py index 14ccc9de2..5ad8756fa 100644 --- a/app/main/views/organizations.py +++ b/app/main/views/organizations.py @@ -4,7 +4,6 @@ from functools import partial from flask import flash, redirect, render_template, request, url_for from flask_login import current_user -from notifications_python_client.errors import HTTPError from app import current_organization, org_invite_api_client, organizations_client from app.main import main @@ -27,6 +26,7 @@ from app.models.organization import AllOrganizations, Organization from app.models.user import InvitedOrgUser, User from app.utils.csv import Spreadsheet from app.utils.user import user_has_permissions, user_is_platform_admin +from notifications_python_client.errors import HTTPError @main.route("/organizations", methods=["GET"]) diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py index bfd3749f3..fb9ea7fbe 100644 --- a/app/main/views/platform_admin.py +++ b/app/main/views/platform_admin.py @@ -15,7 +15,6 @@ from flask import ( session, url_for, ) -from notifications_python_client.errors import HTTPError from app import ( billing_api_client, @@ -46,6 +45,7 @@ from app.utils.pagination import ( get_page_from_request, ) from app.utils.user import user_is_platform_admin +from notifications_python_client.errors import HTTPError COMPLAINT_THRESHOLD = 0.02 FAILURE_THRESHOLD = 3 diff --git a/app/main/views/send.py b/app/main/views/send.py index b1b393a90..8de4663fe 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -17,7 +17,6 @@ from flask import ( ) from flask_login import current_user from markupsafe import Markup -from notifications_python_client.errors import HTTPError from xlrd.biffh import XLRDError from xlrd.xldate import XLDateError @@ -52,6 +51,7 @@ from app.utils import ( 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_python_client.errors import HTTPError from notifications_utils import SMS_CHAR_COUNT_LIMIT from notifications_utils.insensitive_dict import InsensitiveDict from notifications_utils.recipients import RecipientCSV, first_column_headings diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 628ac59e5..fd15b1854 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -12,7 +12,6 @@ from flask import ( url_for, ) from flask_login import current_user -from notifications_python_client.errors import HTTPError from app import ( billing_api_client, @@ -52,6 +51,7 @@ from app.main.forms import ( from app.utils import DELIVERED_STATUSES, FAILURE_STATUSES, SENDING_STATUSES from app.utils.time import parse_naive_dt from app.utils.user import user_has_permissions, user_is_platform_admin +from notifications_python_client.errors import HTTPError PLATFORM_ADMIN_SERVICE_PERMISSIONS = OrderedDict( [ diff --git a/app/main/views/templates.py b/app/main/views/templates.py index 4ac898cb4..bb82c6d38 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -3,7 +3,6 @@ from functools import partial from flask import abort, flash, jsonify, redirect, render_template, request, url_for from flask_login import current_user from markupsafe import Markup -from notifications_python_client.errors import HTTPError from app import ( current_service, @@ -29,6 +28,7 @@ from app.models.template_list import TemplateList, TemplateLists from app.utils import NOTIFICATION_TYPES, should_skip_template_page from app.utils.templates import get_template from app.utils.user import user_has_permissions +from notifications_python_client.errors import HTTPError from notifications_utils import SMS_CHAR_COUNT_LIMIT form_objects = { diff --git a/app/models/user.py b/app/models/user.py index 468208d6e..88babe2fa 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -3,7 +3,6 @@ from datetime import datetime from flask import abort, current_app, request, session from flask_login import AnonymousUserMixin, UserMixin, login_user, logout_user -from notifications_python_client.errors import HTTPError from werkzeug.utils import cached_property from app.event_handlers import ( @@ -22,6 +21,7 @@ from app.utils.user_permissions import ( all_ui_permissions, translate_permissions_from_db_to_ui, ) +from notifications_python_client.errors import HTTPError def _get_service_id_from_view_args(): diff --git a/app/notify_client/__init__.py b/app/notify_client/__init__.py index 8db3425f2..e05a6124a 100644 --- a/app/notify_client/__init__.py +++ b/app/notify_client/__init__.py @@ -2,10 +2,10 @@ import os from flask import abort, current_app, has_request_context, request from flask_login import current_user -from notifications_python_client import __version__ -from notifications_python_client.base import BaseAPIClient from app.extensions import redis_client +from notifications_python_client import __version__ +from notifications_python_client.base import BaseAPIClient from notifications_utils.clients.redis import RequestCache cache = RequestCache(redis_client) diff --git a/app/notify_client/organizations_api_client.py b/app/notify_client/organizations_api_client.py index b37acda74..10d121a41 100644 --- a/app/notify_client/organizations_api_client.py +++ b/app/notify_client/organizations_api_client.py @@ -1,9 +1,8 @@ from itertools import chain -from notifications_python_client.errors import HTTPError - from app.extensions import redis_client from app.notify_client import NotifyAdminAPIClient, cache +from notifications_python_client.errors import HTTPError class OrganizationsClient(NotifyAdminAPIClient): diff --git a/app/notify_client/user_api_client.py b/app/notify_client/user_api_client.py index 1aab20a90..2a96d96db 100644 --- a/app/notify_client/user_api_client.py +++ b/app/notify_client/user_api_client.py @@ -1,9 +1,9 @@ from flask import current_app -from notifications_python_client.errors import HTTPError from app.notify_client import NotifyAdminAPIClient, cache from app.utils import hilite from app.utils.user_permissions import translate_permissions_from_ui_to_db +from notifications_python_client.errors import HTTPError ALLOWED_ATTRIBUTES = { "name", diff --git a/app/status/views/healthcheck.py b/app/status/views/healthcheck.py index b0c11091a..6bb62cf03 100644 --- a/app/status/views/healthcheck.py +++ b/app/status/views/healthcheck.py @@ -2,12 +2,12 @@ import time import traceback from flask import current_app, jsonify, request -from notifications_python_client.errors import HTTPError from redis import RedisError from app import status_api_client, version from app.extensions import redis_client from app.status import status +from notifications_python_client.errors import HTTPError @status.route("/_status", methods=["GET"]) diff --git a/notifications_python_client/__init__.py b/notifications_python_client/__init__.py new file mode 100644 index 000000000..436df44f5 --- /dev/null +++ b/notifications_python_client/__init__.py @@ -0,0 +1,17 @@ +# Version numbering follows Semantic Versionning: +# +# Given a version number MAJOR.MINOR.PATCH, increment the: +# - MAJOR version when you make incompatible API changes, +# - MINOR version when you add functionality in a backwards-compatible manner, and +# - PATCH version when you make backwards-compatible bug fixes. +# +# -- http://semver.org/ + +__version__ = "10.0.1" + +from notifications_python_client.errors import ( # noqa + REQUEST_ERROR_MESSAGE, + REQUEST_ERROR_STATUS_CODE, +) +from notifications_python_client.notifications import NotificationsAPIClient # noqa +from notifications_python_client.utils import prepare_upload # noqa diff --git a/notifications_python_client/authentication.py b/notifications_python_client/authentication.py new file mode 100644 index 000000000..7e700e7db --- /dev/null +++ b/notifications_python_client/authentication.py @@ -0,0 +1,153 @@ +import calendar +import time + +import jwt + +from notifications_python_client.errors import ( + TokenAlgorithmError, + TokenDecodeError, + TokenError, + TokenExpiredError, + TokenIssuedAtError, + TokenIssuerError, +) + +__algorithm__ = "HS256" +__type__ = "JWT" +__bound__ = 30 + +INVALID_FUTURE_TOKEN_ERROR_MESSAGE = "Token can not be in the future" + + +def create_jwt_token(secret, client_id): + """ + Create JWT token for GOV.UK Notify + + Tokens have standard header: + { + "typ": "JWT", + "alg": "HS256" + } + + Claims consist of: + iss: identifier for the client + iat: issued at in epoch seconds (UTC) + + :param secret: Application signing secret + :param client_id: Identifier for the client + :return: JWT token for this request + """ + assert secret, "Missing secret key" + assert client_id, "Missing client id" + + headers = {"typ": __type__, "alg": __algorithm__} + + claims = {"iss": client_id, "iat": epoch_seconds()} + t = jwt.encode(payload=claims, key=secret, headers=headers) + if isinstance(t, str): + return t + else: + return t.decode() + + +def get_token_issuer(token): + """ + Issuer of a token is the identifier used to recover the secret + Need to extract this from token to ensure we can proceed to the signature validation stage + Does not check validity of the token + :param token: signed JWT token + :return issuer: iss field of the JWT token + :raises TokenIssuerError: if iss field not present + :raises TokenDecodeError: if token does not conform to JWT spec + """ + try: + unverified = decode_token(token) + + if "iss" not in unverified: + raise TokenIssuerError + + return unverified.get("iss") + except jwt.DecodeError as e: + raise TokenDecodeError from e + + +def decode_jwt_token(token, secret): + """ + Validates and decodes the JWT token + Token checked for + - signature of JWT token + - token issued date is valid + + :param token: jwt token + :param secret: client specific secret + :return boolean: True if valid token, False otherwise + :raises TokenIssuerError: if iss field not present + :raises TokenIssuedAtError: if iat field not present + :raises TokenExpiredError: If the iat value expires this token + :raises TokenDecodeError: If the token cannot be decoded because it failed validation + :raises TokenAlgorithmError: If the algorithm is not recognised + :raises TokenError: If any other type of jwt exception is raised when trying jwt.decode + """ + try: + # check signature of the token + decoded_token = jwt.decode( + token, + key=secret, + options={"verify_signature": True}, + algorithms=[__algorithm__], + leeway=__bound__, + ) + return validate_jwt_token(decoded_token) + except jwt.InvalidIssuedAtError as e: + raise TokenExpiredError( + "Token has invalid iat field", decode_token(token) + ) from e + except jwt.ImmatureSignatureError as e: + raise TokenExpiredError( + INVALID_FUTURE_TOKEN_ERROR_MESSAGE, decode_token(token) + ) from e + except jwt.DecodeError as e: + raise TokenDecodeError from e + except jwt.InvalidAlgorithmError as e: + raise TokenAlgorithmError from e + except jwt.InvalidTokenError as e: + # At this point, we have not caught a specific exception we care about enough to show + # a precise error message (ie something to do with the iat, iss or alg fields). + # If there is a different reason our token is invalid we will throw a generic error as we + # don't wish to provide exact messages for every type of error that jwt might encounter. + # https://github.com/jpadilla/pyjwt/blob/master/jwt/exceptions.py + # https://pyjwt.readthedocs.io/en/latest/api.html#exceptions + raise TokenError from e + + +def validate_jwt_token(decoded_token): + # token has all the required fields + if "iss" not in decoded_token: + raise TokenIssuerError + if "iat" not in decoded_token: + raise TokenIssuedAtError + + # check iat time is within bounds + now = epoch_seconds() + iat = int(decoded_token["iat"]) + if now > (iat + __bound__): + raise TokenExpiredError("Token has expired", decoded_token) + if iat > (now + __bound__): + raise TokenExpiredError(INVALID_FUTURE_TOKEN_ERROR_MESSAGE, decoded_token) + + return True + + +def decode_token(token): + """ + Decode token but don;t check the signature + :param token: + :return decoded token: + """ + return jwt.decode( + token, options={"verify_signature": False}, algorithms=[__algorithm__] + ) + + +def epoch_seconds(): + return calendar.timegm(time.gmtime()) diff --git a/notifications_python_client/base.py b/notifications_python_client/base.py new file mode 100644 index 000000000..32f76808d --- /dev/null +++ b/notifications_python_client/base.py @@ -0,0 +1,119 @@ +import json +import logging +import urllib.parse + +import requests + +from notifications_python_client import __version__ +from notifications_python_client.authentication import create_jwt_token +from notifications_python_client.errors import HTTPError, InvalidResponse + +logger = logging.getLogger(__name__) + + +class BaseAPIClient: + """ + Base class for GOV.UK Notify API client. + + This class is not thread-safe. + """ + + def __init__( + self, api_key, base_url="https://api.notifications.service.gov.uk", timeout=30 + ): + """ + Initialise the client + Error if either of base_url or secret missing + :param base_url - base URL of GOV.UK Notify API: + :param secret - application secret - used to sign the request: + :param timeout - request timeout on the client + :return: + """ + service_id = api_key[-73:-37] + api_key = api_key[-36:] + + assert base_url, "Missing base url" + assert service_id, "Missing service ID" + assert api_key, "Missing API key" + self.base_url = base_url + self.service_id = service_id + self.api_key = api_key + self.timeout = timeout + self.request_session = requests.Session() + + def put(self, url, data): + return self.request("PUT", url, data=data) + + def get(self, url, params=None): + return self.request("GET", url, params=params) + + def post(self, url, data): + return self.request("POST", url, data=data) + + def delete(self, url, data=None): + return self.request("DELETE", url, data=data) + + def generate_headers(self, api_token): + return { + "Content-type": "application/json", + "Authorization": f"Bearer {api_token}", + "User-agent": f"NOTIFY-API-PYTHON-CLIENT/{__version__}", + } + + def request(self, method, url, data=None, params=None): + logger.debug("API request %s %s", method, url) + url, kwargs = self._create_request_objects(url, data, params) + + response = self._perform_request(method, url, kwargs) + + return self._process_json_response(response) + + def _create_request_objects(self, url, data, params): + api_token = create_jwt_token(self.api_key, self.service_id) + + kwargs = {"headers": self.generate_headers(api_token), "timeout": self.timeout} + + if data is not None: + kwargs.update(data=self._serialize_data(data)) + + if params is not None: + kwargs.update(params=params) + + url = urllib.parse.urljoin(str(self.base_url), str(url)) + + return url, kwargs + + def _serialize_data(self, data): + return json.dumps(data, default=self._extended_json_encoder) + + def _extended_json_encoder(self, obj): + if isinstance(obj, set): + return list(obj) + + raise TypeError + + def _perform_request(self, method, url, kwargs): + + try: + response = self.request_session.request(method, url, **kwargs) + response.raise_for_status() + return response + except requests.RequestException as e: + api_error = HTTPError.create(e) + logger.warning( + "API %s request failed with %s '%s'", + method, + api_error.status_code, + api_error.message, + ) + raise api_error from e + + def _process_json_response(self, response): + try: + if response.status_code == 204: + return + return response.json() + except ValueError as e: + raise InvalidResponse( + response, message="No JSON response object could be decoded" + ) from e diff --git a/notifications_python_client/errors.py b/notifications_python_client/errors.py new file mode 100644 index 000000000..430123684 --- /dev/null +++ b/notifications_python_client/errors.py @@ -0,0 +1,90 @@ +from typing import List, Union # noqa: UP035 – Python <3.10 compatibility + +from requests import RequestException, Response + +REQUEST_ERROR_STATUS_CODE = 503 +REQUEST_ERROR_MESSAGE = "Request failed" + +TOKEN_ERROR_GUIDANCE = "See our requirements for JSON Web Tokens \ + at https://docs.notifications.service.gov.uk/rest-api.html#authorisation-header" +TOKEN_ERROR_DEFAULT_ERROR_MESSAGE = "Invalid token: " + TOKEN_ERROR_GUIDANCE + + +class TokenError(Exception): + def __init__(self, message=None, token=None): + self.message = ( + message + ". " + TOKEN_ERROR_GUIDANCE + if message + else TOKEN_ERROR_DEFAULT_ERROR_MESSAGE + ) + self.token = token + + +class TokenExpiredError(TokenError): + pass + + +class TokenAlgorithmError(TokenError): + def __init__(self): + super().__init__("Invalid token: algorithm used is not HS256") + + +class TokenDecodeError(TokenError): + def __init__(self, message=None): + super().__init__(message or "Invalid token: signature") + + +class TokenIssuerError(TokenDecodeError): + def __init__(self): + super().__init__("Invalid token: iss field not provided") + + +class TokenIssuedAtError(TokenDecodeError): + def __init__(self): + super().__init__("Invalid token: iat field not provided") + + +class APIError(Exception): + def __init__(self, response: Response = None, message: str = None): + self.response = response + self._message = message + + def __str__(self): + return f"{self.status_code} - {self.message}" + + @property + def message( + self, + ) -> Union[str, List[dict]]: # noqa: UP006, UP007 – Python <3.10 compatibility + try: + json_resp = self.response.json() # type: ignore + return json_resp.get("message", json_resp.get("errors")) + except (TypeError, ValueError, AttributeError, KeyError): + return self._message or REQUEST_ERROR_MESSAGE + + @property + def status_code(self) -> int: + try: + return self.response.status_code # type: ignore + except AttributeError: + return REQUEST_ERROR_STATUS_CODE + + +class HTTPError(APIError): + @staticmethod + def create(e: RequestException) -> "HTTPError": + error = HTTPError(e.response) + if error.status_code == 503: + error = HTTP503Error(e.response) + return error + + +class HTTP503Error(HTTPError): + """Specific instance of HTTPError for 503 errors + + Used for detecting whether failed requests should be retried. + """ + + +class InvalidResponse(APIError): + pass diff --git a/notifications_python_client/notifications.py b/notifications_python_client/notifications.py new file mode 100644 index 000000000..8b0118d0f --- /dev/null +++ b/notifications_python_client/notifications.py @@ -0,0 +1,154 @@ +import base64 +import logging +import re +from io import BytesIO + +from notifications_python_client.base import BaseAPIClient + +logger = logging.getLogger(__name__) + + +class NotificationsAPIClient(BaseAPIClient): + def send_sms_notification( + self, + phone_number, + template_id, + personalisation=None, + reference=None, + sms_sender_id=None, + ): + notification = {"phone_number": phone_number, "template_id": template_id} + if personalisation: + notification.update({"personalisation": personalisation}) + if reference: + notification.update({"reference": reference}) + if sms_sender_id: + notification.update({"sms_sender_id": sms_sender_id}) + return self.post("/v2/notifications/sms", data=notification) + + def send_email_notification( + self, + email_address, + template_id, + personalisation=None, + reference=None, + email_reply_to_id=None, + one_click_unsubscribe_url=None, + ): + notification = {"email_address": email_address, "template_id": template_id} + if personalisation: + notification.update({"personalisation": personalisation}) + if reference: + notification.update({"reference": reference}) + if email_reply_to_id: + notification.update({"email_reply_to_id": email_reply_to_id}) + if one_click_unsubscribe_url: + notification.update( + {"one_click_unsubscribe_url": one_click_unsubscribe_url} + ) + + return self.post("/v2/notifications/email", data=notification) + + def send_letter_notification(self, template_id, personalisation, reference=None): + notification = {"template_id": template_id, "personalisation": personalisation} + if reference: + notification.update({"reference": reference}) + return self.post("/v2/notifications/letter", data=notification) + + def send_precompiled_letter_notification(self, reference, pdf_file, postage=None): + content = base64.b64encode(pdf_file.read()).decode("utf-8") + notification = {"reference": reference, "content": content} + + if postage: + notification["postage"] = postage + + return self.post("/v2/notifications/letter", data=notification) + + def get_received_texts(self, older_than=None): + if older_than: + query_string = f"?older_than={older_than}" + else: + query_string = "" + + return self.get(f"/v2/received-text-messages{query_string}") + + def get_received_texts_iterator(self, older_than=None): + result = self.get_received_texts(older_than=older_than) + received_texts = result.get("received_text_messages") + while received_texts: + yield from received_texts + next_link = result["links"].get("next") + received_text_id = re.search( + "[0-F]{8}-[0-F]{4}-[0-F]{4}-[0-F]{4}-[0-F]{12}", next_link, re.I + ).group(0) + result = self.get_received_texts(older_than=received_text_id) + received_texts = result.get("received_text_messages") + + def get_notification_by_id(self, id): + return self.get(f"/v2/notifications/{id}") + + def get_pdf_for_letter(self, id): + url = f"/v2/notifications/{id}/pdf" + logger.debug("API request %s %s", "GET", url) + url, kwargs = self._create_request_objects(url, data=None, params=None) + + response = self._perform_request("GET", url, kwargs) + + return BytesIO(response.content) + + def get_all_notifications( + self, + status=None, + template_type=None, + reference=None, + older_than=None, + include_jobs=None, + ): + data = {} + if status: + data.update({"status": status}) + if template_type: + data.update({"template_type": template_type}) + if reference: + data.update({"reference": reference}) + if older_than: + data.update({"older_than": older_than}) + if include_jobs: + data.update({"include_jobs": include_jobs}) + return self.get("/v2/notifications", params=data) + + def get_all_notifications_iterator( + self, status=None, template_type=None, reference=None, older_than=None + ): + result = self.get_all_notifications( + status, template_type, reference, older_than + ) + notifications = result.get("notifications") + while notifications: + yield from notifications + next_link = result["links"].get("next") + notification_id = re.search( + "[0-F]{8}-[0-F]{4}-[0-F]{4}-[0-F]{4}-[0-F]{12}", next_link, re.I + ).group(0) + result = self.get_all_notifications( + status, template_type, reference, notification_id + ) + notifications = result.get("notifications") + + def post_template_preview(self, template_id, personalisation): + template = {"personalisation": personalisation} + return self.post(f"/v2/template/{template_id}/preview", data=template) + + def get_template(self, template_id): + return self.get(f"/v2/template/{template_id}") + + def get_template_version(self, template_id, version): + return self.get(f"/v2/template/{template_id}/version/{version}") + + def get_all_template_versions(self, template_id): + return self.get(f"service/{self.service_id}/template/{template_id}/versions") + + def get_all_templates(self, template_type=None): + _template_type = f"?type={template_type}" if template_type else "" + + return self.get(f"/v2/templates{_template_type}") diff --git a/notifications_python_client/py.typed b/notifications_python_client/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/notifications_python_client/utils.py b/notifications_python_client/utils.py new file mode 100644 index 000000000..a93faa959 --- /dev/null +++ b/notifications_python_client/utils.py @@ -0,0 +1,21 @@ +import base64 + +DOCUMENT_UPLOAD_SIZE_LIMIT = 2 * 1024 * 1024 + + +def prepare_upload( + f, filename=None, confirm_email_before_download=None, retention_period=None +): + contents = f.read() + + if len(contents) > DOCUMENT_UPLOAD_SIZE_LIMIT: + raise ValueError("File is larger than 2MB") + + file_data = { + "file": base64.b64encode(contents).decode("ascii"), + "filename": filename, + "confirm_email_before_download": confirm_email_before_download, + "retention_period": retention_period, + } + + return file_data diff --git a/poetry.lock b/poetry.lock index 0addb42ea..641e506d6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "ago" @@ -827,17 +827,6 @@ idna = ["idna (>=3.7)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] -[[package]] -name = "docopt" -version = "0.6.2" -description = "Pythonic argument parser, that will make you smile" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, -] - [[package]] name = "docutils" version = "0.16" @@ -2049,76 +2038,71 @@ xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] [[package]] name = "msgpack" -version = "1.1.0" +version = "1.1.1" description = "MessagePack serializer" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, - {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, - {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, - {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, - {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, - {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, - {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, - {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, - {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, - {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, - {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, - {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, - {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, - {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, + {file = "msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed"}, + {file = "msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338"}, + {file = "msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd"}, + {file = "msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8"}, + {file = "msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558"}, + {file = "msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752"}, + {file = "msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295"}, + {file = "msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458"}, + {file = "msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238"}, + {file = "msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a"}, + {file = "msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c"}, + {file = "msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4"}, + {file = "msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0"}, + {file = "msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5"}, + {file = "msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323"}, + {file = "msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba1be28247e68994355e028dcd668316db30c1f758d3241a7b903ac78dcd285"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f93dcddb243159c9e4109c9750ba5b335ab8d48d9522c5308cd05d7e3ce600"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fbbc0b906a24038c9958a1ba7ae0918ad35b06cb449d398b76a7d08470b0ed9"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:61e35a55a546a1690d9d09effaa436c25ae6130573b6ee9829c37ef0f18d5e78"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1abfc6e949b352dadf4bce0eb78023212ec5ac42f6abfd469ce91d783c149c2a"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:996f2609ddf0142daba4cefd767d6db26958aac8439ee41db9cc0db9f4c4c3a6"}, + {file = "msgpack-1.1.1-cp38-cp38-win32.whl", hash = "sha256:4d3237b224b930d58e9d83c81c0dba7aacc20fcc2f89c1e5423aa0529a4cd142"}, + {file = "msgpack-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:da8f41e602574ece93dbbda1fab24650d6bf2a24089f9e9dbb4f5730ec1e58ad"}, + {file = "msgpack-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b"}, + {file = "msgpack-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478"}, + {file = "msgpack-1.1.1-cp39-cp39-win32.whl", hash = "sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57"}, + {file = "msgpack-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084"}, + {file = "msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd"}, ] [[package]] @@ -2187,22 +2171,6 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] -[[package]] -name = "notifications-python-client" -version = "10.0.1" -description = "Python API client for GOV.UK Notify." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "notifications_python_client-10.0.1-py3-none-any.whl", hash = "sha256:00d88eacb6fd6eb0467d7396a7e23677194cfebe0ebd88de2efa031fb51eb23f"}, -] - -[package.dependencies] -docopt = ">=0.3.0" -PyJWT = ">=1.5.1" -requests = ">=2.0.0" - [[package]] name = "numpy" version = "2.3.0" @@ -2296,14 +2264,14 @@ dev = ["black", "mypy", "pytest"] [[package]] name = "packageurl-python" -version = "0.17.0" +version = "0.17.1" description = "A purl aka. Package URL parser and builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "packageurl_python-0.17.0-py3-none-any.whl", hash = "sha256:c2a8e48d7b393b18806fd096abe326260b3aed82a07eda6c6628689b36ba52c4"}, - {file = "packageurl_python-0.17.0.tar.gz", hash = "sha256:3cc44a01c9549c237f14c34981bae1008eb57c858c0af9039f4f13091807e1f3"}, + {file = "packageurl_python-0.17.1-py3-none-any.whl", hash = "sha256:59b0862ae0b216994f847e05b4c6e870e0d16e1ddd706feefb19d79810f22cbd"}, + {file = "packageurl_python-0.17.1.tar.gz", hash = "sha256:5db592a990b60bc02446033c50fb1803a26c5124cd72c5a2cd1b8ea1ae741969"}, ] [package.extras] @@ -2353,14 +2321,14 @@ setuptools = "*" [[package]] name = "pbs-installer" -version = "2025.5.29" +version = "2025.6.12" description = "Installer for Python Build Standalone" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pbs_installer-2025.5.29-py3-none-any.whl", hash = "sha256:bfb281f850e95fbad62e872359b055e3c161d18048be3f5748b2b0b2b42f8922"}, - {file = "pbs_installer-2025.5.29.tar.gz", hash = "sha256:11849df09c13c37330ac54c86645e13e8ff5d07a2a1c63d3489d4774a52c9d05"}, + {file = "pbs_installer-2025.6.12-py3-none-any.whl", hash = "sha256:438e75de131a2114ac5e86156fc51da7dadd6734844de329ad162cca63709297"}, + {file = "pbs_installer-2025.6.12.tar.gz", hash = "sha256:ae2d3990848652dca699a680b00ea8e19b970cb6172967cb00539bfeed5a7465"}, ] [package.dependencies] @@ -4161,4 +4129,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "^3.12.2" -content-hash = "db3796fb772645fe32882c281001ad9b5db776660a58bb8d2e1ceff6447cc00e" +content-hash = "acc8897a5d775035e535d20a130a984636c954bb4a597f4f479dccd1bf172ba2" diff --git a/pyproject.toml b/pyproject.toml index ebc5d0cb3..e591c3fe7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ humanize = "~=4.12" itsdangerous = "~=2.2" jinja2 = "^3.1.6" newrelic = "*" -notifications-python-client = "==10.0.1" pyexcel = "==0.7.3" pyexcel-io = "==0.6.7" pyexcel-ods3 = "==0.6.1" @@ -53,6 +52,7 @@ numpy = "^2.3.0" ordered-set = "^4.1.0" phonenumbers = "^9.0.7" pycparser = "^2.22" +pyjwt = "^2.10.1" python-json-logger = "^3.3.0" redis = "^6.2.0" regex = "^2024.11.6" diff --git a/tests/app/main/test_errorhandlers.py b/tests/app/main/test_errorhandlers.py index 57537762a..6b0178382 100644 --- a/tests/app/main/test_errorhandlers.py +++ b/tests/app/main/test_errorhandlers.py @@ -1,6 +1,7 @@ import pytest from flask import Response, url_for from flask_wtf.csrf import CSRFError + from notifications_python_client.errors import HTTPError diff --git a/tests/app/main/views/organizations/test_organizations.py b/tests/app/main/views/organizations/test_organizations.py index d85352522..bb952d661 100644 --- a/tests/app/main/views/organizations/test_organizations.py +++ b/tests/app/main/views/organizations/test_organizations.py @@ -1,8 +1,8 @@ import pytest from flask import url_for from freezegun import freeze_time -from notifications_python_client.errors import HTTPError +from notifications_python_client.errors import HTTPError from tests import organization_json, service_json from tests.conftest import ( ORGANISATION_ID, diff --git a/tests/app/main/views/service_settings/test_service_settings.py b/tests/app/main/views/service_settings/test_service_settings.py index 1f04b1315..603e914b5 100644 --- a/tests/app/main/views/service_settings/test_service_settings.py +++ b/tests/app/main/views/service_settings/test_service_settings.py @@ -5,9 +5,9 @@ from uuid import uuid4 import pytest from flask import url_for from freezegun import freeze_time -from notifications_python_client.errors import HTTPError import app +from notifications_python_client.errors import HTTPError from tests import ( find_element_by_tag_and_partial_text, organization_json, diff --git a/tests/app/main/views/test_accept_invite.py b/tests/app/main/views/test_accept_invite.py index 297dc7603..77df3a832 100644 --- a/tests/app/main/views/test_accept_invite.py +++ b/tests/app/main/views/test_accept_invite.py @@ -3,9 +3,9 @@ from unittest.mock import ANY, Mock, call import pytest from flask import url_for from freezegun import freeze_time -from notifications_python_client.errors import HTTPError import app +from notifications_python_client.errors import HTTPError from tests import service_json from tests.conftest import ( SERVICE_ONE_ID, diff --git a/tests/app/main/views/test_add_service.py b/tests/app/main/views/test_add_service.py index 4f2ab9964..3a781bda5 100644 --- a/tests/app/main/views/test_add_service.py +++ b/tests/app/main/views/test_add_service.py @@ -1,9 +1,9 @@ import pytest from flask import url_for from freezegun import freeze_time -from notifications_python_client.errors import HTTPError from app.utils.user import is_gov_user +from notifications_python_client.errors import HTTPError from tests import organization_json diff --git a/tests/app/main/views/test_conversation.py b/tests/app/main/views/test_conversation.py index 15a5ec587..389af5930 100644 --- a/tests/app/main/views/test_conversation.py +++ b/tests/app/main/views/test_conversation.py @@ -6,9 +6,9 @@ from unittest.mock import Mock import pytest from flask import url_for from freezegun import freeze_time -from notifications_python_client.errors import HTTPError from app.main.views.conversation import get_user_number +from notifications_python_client.errors import HTTPError from tests.conftest import ( SERVICE_ONE_ID, _template, diff --git a/tests/app/main/views/test_find_users.py b/tests/app/main/views/test_find_users.py index b8056c4e2..6b073d377 100644 --- a/tests/app/main/views/test_find_users.py +++ b/tests/app/main/views/test_find_users.py @@ -2,8 +2,8 @@ import uuid import pytest from flask import url_for -from notifications_python_client.errors import HTTPError +from notifications_python_client.errors import HTTPError from tests import user_json from tests.conftest import normalize_spaces diff --git a/tests/app/main/views/test_forgot_password.py b/tests/app/main/views/test_forgot_password.py index c23650990..538966f47 100644 --- a/tests/app/main/views/test_forgot_password.py +++ b/tests/app/main/views/test_forgot_password.py @@ -1,8 +1,8 @@ import pytest from flask import Response, url_for -from notifications_python_client.errors import HTTPError import app +from notifications_python_client.errors import HTTPError from tests import user_json from tests.conftest import SERVICE_ONE_ID diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index 11104325a..4a70b01b6 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -12,10 +12,10 @@ from zipfile import BadZipFile import pytest from flask import url_for -from notifications_python_client.errors import HTTPError from xlrd.biffh import XLRDError from xlrd.xldate import XLDateAmbiguous, XLDateError, XLDateNegative, XLDateTooLarge +from notifications_python_client.errors import HTTPError from notifications_utils.recipients import RecipientCSV from notifications_utils.template import SMSPreviewTemplate from tests import ( diff --git a/tests/app/main/views/test_template_folders.py b/tests/app/main/views/test_template_folders.py index f418b2c42..3363b7652 100644 --- a/tests/app/main/views/test_template_folders.py +++ b/tests/app/main/views/test_template_folders.py @@ -2,9 +2,9 @@ import uuid import pytest from flask import abort, url_for -from notifications_python_client.errors import HTTPError from app.models.user import User +from notifications_python_client.errors import HTTPError from tests import sample_uuid from tests.conftest import ( SERVICE_ONE_ID, diff --git a/tests/app/main/views/test_templates.py b/tests/app/main/views/test_templates.py index 250815269..6e4ed0ea9 100644 --- a/tests/app/main/views/test_templates.py +++ b/tests/app/main/views/test_templates.py @@ -5,8 +5,8 @@ import pytest from bs4 import BeautifulSoup from flask import url_for from freezegun import freeze_time -from notifications_python_client.errors import HTTPError +from notifications_python_client.errors import HTTPError from tests import template_json, validate_route_permission from tests.app.main.views.test_template_folders import ( CHILD_FOLDER_ID, diff --git a/tests/app/main/views/test_verify.py b/tests/app/main/views/test_verify.py index 4f2e39feb..b082a5c82 100644 --- a/tests/app/main/views/test_verify.py +++ b/tests/app/main/views/test_verify.py @@ -6,9 +6,9 @@ import pytest from flask import session as flask_session from flask import url_for from itsdangerous import SignatureExpired -from notifications_python_client.errors import HTTPError from app.main.views.verify import activate_user +from notifications_python_client.errors import HTTPError from tests.conftest import create_user diff --git a/tests/conftest.py b/tests/conftest.py index de1ff28a3..c1bfbb7ef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,9 +11,9 @@ import pytest from bs4 import BeautifulSoup from dotenv import load_dotenv from flask import Flask, url_for -from notifications_python_client.errors import HTTPError from app import create_app +from notifications_python_client.errors import HTTPError from notifications_utils.url_safe_token import generate_token from . import (