From 9db1f7816a277eac4f5e2c63675e38e9c50b424b Mon Sep 17 00:00:00 2001 From: MohammadIqbalAD-NHS <127403145+MohammadIqbalAD-NHS@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:48:40 +0000 Subject: [PATCH 1/3] [PRMP-1353] - Read all files as UTF-8 encoded (#484) --- lambdas/services/bulk_upload_metadata_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lambdas/services/bulk_upload_metadata_service.py b/lambdas/services/bulk_upload_metadata_service.py index a4c55b72b..4a31120a1 100644 --- a/lambdas/services/bulk_upload_metadata_service.py +++ b/lambdas/services/bulk_upload_metadata_service.py @@ -74,7 +74,9 @@ def csv_to_staging_metadata(csv_file_path: str) -> list[StagingMetadata]: logger.info("Parsing bulk upload metadata") patients = {} - with open(csv_file_path, mode="r") as csv_file_handler: + with open( + csv_file_path, mode="r", encoding="utf-8", errors="replace" + ) as csv_file_handler: csv_reader: Iterable[dict] = csv.DictReader(csv_file_handler) for row in csv_reader: file_metadata = MetadataFile.model_validate(row) From 8bdeea0ba9025acde28833765210220c3c0e5e69 Mon Sep 17 00:00:00 2001 From: MohammadIqbalAD-NHS <127403145+MohammadIqbalAD-NHS@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:19:37 +0000 Subject: [PATCH 2/3] [PRMP-1354] - Historic names take into account Usual names (#483) --- lambdas/tests/unit/utils/test_lloyd_george_validator.py | 4 ++-- lambdas/utils/lloyd_george_validator.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lambdas/tests/unit/utils/test_lloyd_george_validator.py b/lambdas/tests/unit/utils/test_lloyd_george_validator.py index f6ab44300..9019ae3b0 100644 --- a/lambdas/tests/unit/utils/test_lloyd_george_validator.py +++ b/lambdas/tests/unit/utils/test_lloyd_george_validator.py @@ -331,7 +331,7 @@ def test_validate_name_with_wrong_first_name(mocker, mock_pds_patient): validate_patient_name_using_full_name_history( lg_file_patient_name, mock_pds_patient ) - assert mock_validate_name.call_count == 2 + assert mock_validate_name.call_count == 3 def test_validate_name_with_wrong_family_name(mocker, mock_pds_patient): @@ -344,7 +344,7 @@ def test_validate_name_with_wrong_family_name(mocker, mock_pds_patient): validate_patient_name_using_full_name_history( lg_file_patient_name, mock_pds_patient ) - assert mock_validate_name.call_count == 2 + assert mock_validate_name.call_count == 3 def test_validate_name_with_historical_name(mocker, mock_pds_patient): diff --git a/lambdas/utils/lloyd_george_validator.py b/lambdas/utils/lloyd_george_validator.py index 805696173..d44ff154c 100644 --- a/lambdas/utils/lloyd_george_validator.py +++ b/lambdas/utils/lloyd_george_validator.py @@ -190,8 +190,6 @@ def validate_patient_name_using_full_name_history( ) for name in pds_patient_details.name: - if name.use == "usual": - continue historic_first_name_in_pds: str = name.given[0] historic_family_name_in_pds = name.family if validate_patient_name( From 2bc003e16eba996a58949f655b267175fbdfc572 Mon Sep 17 00:00:00 2001 From: NogaNHS <127490765+NogaNHS@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:03:01 +0000 Subject: [PATCH 3/3] [PRMP-1360] - Update to nrl pointer request (#488) * [PRMP-1360] add missing fields in NRL request * [PRMP-1360] fix typo * [PRMP-1360] change to snomedcode --- lambdas/enums/snomed_codes.py | 19 +++++++++---- lambdas/models/nrl_fhir_document_reference.py | 27 ++++++++++++++----- lambdas/models/nrl_sqs_message.py | 12 ++++++--- lambdas/services/document_deletion_service.py | 8 +++--- lambdas/services/nrl_api_service.py | 13 +++++---- .../test_manage_nrl_pointer_handler.py | 26 +++++++++--------- .../test_document_deletion_service.py | 13 +++++---- .../unit/services/test_nrl_api_service.py | 23 ++++++++-------- 8 files changed, 88 insertions(+), 53 deletions(-) diff --git a/lambdas/enums/snomed_codes.py b/lambdas/enums/snomed_codes.py index 9b1f3f995..299441fa5 100644 --- a/lambdas/enums/snomed_codes.py +++ b/lambdas/enums/snomed_codes.py @@ -1,9 +1,18 @@ -from enum import StrEnum +from enum import Enum +from pydantic import BaseModel -class SnomedCodesType(StrEnum): - LLOYD_GEORGE = "16521000000101" +class SnomedCode(BaseModel): + code: str + display_name: str -class SnomedCodesCategory(StrEnum): - CARE_PLAN = "734163000" + +class SnomedCodes(Enum): + LLOYD_GEORGE = SnomedCode( + code="16521000000101", display_name="Lloyd George record folder" + ) + CARE_PLAN = SnomedCode(code="734163000", display_name="Care plan") + GENERAL_MEDICAL_PRACTICE = SnomedCode( + code="1060971000000108", display_name="General practice service" + ) diff --git a/lambdas/models/nrl_fhir_document_reference.py b/lambdas/models/nrl_fhir_document_reference.py index ed1f67901..196d03ecf 100644 --- a/lambdas/models/nrl_fhir_document_reference.py +++ b/lambdas/models/nrl_fhir_document_reference.py @@ -1,5 +1,6 @@ from typing import Optional +from enums.snomed_codes import SnomedCode, SnomedCodes from fhir.resources.R4B.documentreference import DocumentReference from models.nrl_sqs_message import NrlAttachment from pydantic import BaseModel, ConfigDict @@ -10,9 +11,11 @@ class FhirDocumentReference(BaseModel): model_config = ConfigDict(alias_generator=to_camel) nhs_number: str custodian: str - snomed_code_doc_type: str = "None" - snomed_code_category: str = "None" - snomed_code_category_display: str = "Care plan" + snomed_code_doc_type: SnomedCode = SnomedCodes.LLOYD_GEORGE.value + snomed_code_category: SnomedCode = SnomedCodes.CARE_PLAN.value + snomed_code_practice_setting: SnomedCode = ( + SnomedCodes.GENERAL_MEDICAL_PRACTICE.value + ) attachment: Optional[NrlAttachment] = NrlAttachment() def build_fhir_dict(self): @@ -36,7 +39,8 @@ def build_fhir_dict(self): "coding": [ { "system": snomed_url, - "code": self.snomed_code_doc_type, + "code": self.snomed_code_doc_type.code, + "display": self.snomed_code_doc_type.display_name, } ] }, @@ -45,8 +49,8 @@ def build_fhir_dict(self): "coding": [ { "system": snomed_url, - "code": self.snomed_code_category, - "display": self.snomed_code_category_display, + "code": self.snomed_code_category.code, + "display": self.snomed_code_category.display_name, } ] } @@ -71,5 +75,16 @@ def build_fhir_dict(self): }, } ], + "context": { + "practiceSetting": { + "coding": [ + { + "system": snomed_url, + "code": self.snomed_code_practice_setting.code, + "display": self.snomed_code_practice_setting.display_name, + } + ] + } + }, } return DocumentReference(**structure_json) diff --git a/lambdas/models/nrl_sqs_message.py b/lambdas/models/nrl_sqs_message.py index c6c82c22d..aa3b9dbdd 100644 --- a/lambdas/models/nrl_sqs_message.py +++ b/lambdas/models/nrl_sqs_message.py @@ -1,6 +1,6 @@ from typing import Optional -from enums.snomed_codes import SnomedCodesCategory, SnomedCodesType +from enums.snomed_codes import SnomedCode, SnomedCodes from pydantic import AliasGenerator, BaseModel, ConfigDict from pydantic.alias_generators import to_camel @@ -17,12 +17,16 @@ class NrlAttachment(BaseModel): class NrlSqsMessage(BaseModel): model_config = ConfigDict( - alias_generator=AliasGenerator(serialization_alias=to_camel) + alias_generator=AliasGenerator(serialization_alias=to_camel), + use_enum_values=True, ) nhs_number: str - snomed_code_doc_type: str = SnomedCodesType.LLOYD_GEORGE - snomed_code_category: str = SnomedCodesCategory.CARE_PLAN + snomed_code_doc_type: SnomedCode = SnomedCodes.LLOYD_GEORGE.value + snomed_code_category: SnomedCode = SnomedCodes.CARE_PLAN.value + snomed_code_practice_setting: SnomedCode = ( + SnomedCodes.GENERAL_MEDICAL_PRACTICE.value + ) description: str = "" attachment: Optional[NrlAttachment] = None action: str diff --git a/lambdas/services/document_deletion_service.py b/lambdas/services/document_deletion_service.py index 09ba07e2d..8f634d780 100644 --- a/lambdas/services/document_deletion_service.py +++ b/lambdas/services/document_deletion_service.py @@ -6,7 +6,7 @@ from enums.lambda_error import LambdaError from enums.nrl_sqs_upload import NrlActionTypes from enums.s3_lifecycle_tags import S3LifecycleTags -from enums.snomed_codes import SnomedCodesCategory, SnomedCodesType +from enums.snomed_codes import SnomedCodes from enums.supported_document_types import SupportedDocumentTypes from models.document_reference import DocumentReference from models.nrl_sqs_message import NrlSqsMessage @@ -91,13 +91,13 @@ def send_sqs_message_to_remove_pointer(self, nhs_number: str): delete_nrl_message = NrlSqsMessage( nhs_number=nhs_number, action=NrlActionTypes.DELETE, - snomed_code_doc_type=SnomedCodesType.LLOYD_GEORGE, - snomed_code_category=SnomedCodesCategory.CARE_PLAN, + snomed_code_doc_type=SnomedCodes.LLOYD_GEORGE.value, + snomed_code_category=SnomedCodes.CARE_PLAN.value, ) sqs_group_id = f"NRL_delete_{uuid.uuid4()}" nrl_queue_url = os.environ["NRL_SQS_QUEUE_URL"] self.sqs_service.send_message_fifo( queue_url=nrl_queue_url, - message_body=delete_nrl_message.model_dump_json(), + message_body=delete_nrl_message.model_dump_json(exclude_unset=True), group_id=sqs_group_id, ) diff --git a/lambdas/services/nrl_api_service.py b/lambdas/services/nrl_api_service.py index 9e33d01bd..52f3ed5bb 100644 --- a/lambdas/services/nrl_api_service.py +++ b/lambdas/services/nrl_api_service.py @@ -2,8 +2,9 @@ import uuid import requests -from requests import HTTPError +from enums.snomed_codes import SnomedCode from requests.adapters import HTTPAdapter +from requests.exceptions import ConnectionError, HTTPError, Timeout from urllib3 import Retry from utils.audit_logging_setup import LoggingService from utils.exceptions import NrlApiException @@ -46,7 +47,7 @@ def create_new_pointer(self, body, retry_on_expired: bool = True): ) response.raise_for_status() logger.info("Successfully created new pointer") - except HTTPError as e: + except (ConnectionError, Timeout, HTTPError) as e: logger.error(e.response.content) if e.response.status_code == 401 and retry_on_expired: self.headers["Authorization"] = ( @@ -56,14 +57,16 @@ def create_new_pointer(self, body, retry_on_expired: bool = True): else: raise NrlApiException("Error while creating new NRL Pointer") - def get_pointer(self, nhs_number, record_type=None, retry_on_expired: bool = True): + def get_pointer( + self, nhs_number, record_type: SnomedCode = None, retry_on_expired: bool = True + ): try: self.set_x_request_id() params = { "subject:identifier": f"https://fhir.nhs.uk/Id/nhs-number|{nhs_number}" } if record_type: - params["type"] = f"http://snomed.info/sct|{record_type}" + params["type"] = f"http://snomed.info/sct|{record_type.code}" response = self.session.get( url=self.endpoint, params=params, headers=self.headers ) @@ -79,7 +82,7 @@ def get_pointer(self, nhs_number, record_type=None, retry_on_expired: bool = Tru else: raise NrlApiException("Error while getting NRL Pointer") - def delete_pointer(self, nhs_number, record_type): + def delete_pointer(self, nhs_number, record_type: SnomedCode = None): search_results = self.get_pointer(nhs_number, record_type).get("entry", []) for entry in search_results: self.set_x_request_id() diff --git a/lambdas/tests/unit/handlers/test_manage_nrl_pointer_handler.py b/lambdas/tests/unit/handlers/test_manage_nrl_pointer_handler.py index c115a2202..b7a91c5c0 100644 --- a/lambdas/tests/unit/handlers/test_manage_nrl_pointer_handler.py +++ b/lambdas/tests/unit/handlers/test_manage_nrl_pointer_handler.py @@ -1,7 +1,7 @@ -import json - import pytest +from enums.snomed_codes import SnomedCodes from handlers.manage_nrl_pointer_handler import lambda_handler +from models.nrl_sqs_message import NrlAttachment, NrlSqsMessage from utils.exceptions import NrlApiException @@ -14,18 +14,18 @@ def mock_service(mocker): def build_test_sqs_message(action="create"): - SQS_Message = { - "nhs_number": "123456789", - "snomed_code_doc_type": "16521000000101", - "snomed_code_category": "734163000", - "action": action, - "attachment": { - "contentType": "application/pdf", - "url": "https://example.org/my-doc.pdf", - }, - } + doc_details = NrlAttachment( + url="https://example.org/my-doc.pdf", + ) + sqs_message = NrlSqsMessage( + nhs_number="123456789", + action=action, + snomed_code_doc_type=SnomedCodes.LLOYD_GEORGE.value, + snomed_code_category=SnomedCodes.CARE_PLAN.value, + attachment=doc_details, + ).model_dump_json() return { - "body": json.dumps(SQS_Message), + "body": sqs_message, "eventSource": "aws:sqs", } diff --git a/lambdas/tests/unit/services/test_document_deletion_service.py b/lambdas/tests/unit/services/test_document_deletion_service.py index e68c7e1a4..4a690c443 100644 --- a/lambdas/tests/unit/services/test_document_deletion_service.py +++ b/lambdas/tests/unit/services/test_document_deletion_service.py @@ -2,6 +2,7 @@ import pytest from enums.s3_lifecycle_tags import S3LifecycleTags +from enums.snomed_codes import SnomedCodes from enums.supported_document_types import SupportedDocumentTypes from services.document_deletion_service import DocumentDeletionService from tests.unit.conftest import ( @@ -227,12 +228,14 @@ def test_send_sqs_message_to_remove_pointer(mocker, mock_deletion_service): expected_message_body = ( '{{"nhs_number":"{}",' - '"snomed_code_doc_type":"16521000000101",' - '"snomed_code_category":"734163000",' - '"description":"",' - '"attachment":null,' + '"snomed_code_doc_type":{},' + '"snomed_code_category":{},' '"action":"delete"}}' - ).format(TEST_NHS_NUMBER) + ).format( + TEST_NHS_NUMBER, + SnomedCodes.LLOYD_GEORGE.value.model_dump_json(), + SnomedCodes.CARE_PLAN.value.model_dump_json(), + ) mock_deletion_service.send_sqs_message_to_remove_pointer(TEST_NHS_NUMBER) diff --git a/lambdas/tests/unit/services/test_nrl_api_service.py b/lambdas/tests/unit/services/test_nrl_api_service.py index 997a3d839..0c0241bd4 100644 --- a/lambdas/tests/unit/services/test_nrl_api_service.py +++ b/lambdas/tests/unit/services/test_nrl_api_service.py @@ -1,4 +1,5 @@ import pytest +from enums.snomed_codes import SnomedCodes from requests import Response from services.nrl_api_service import NrlApiService from tests.unit.conftest import FAKE_URL, TEST_NHS_NUMBER @@ -43,12 +44,12 @@ def test_get_end_user_ods_code(nrl_service): def test_get_pointer_with_record_type(mocker, nrl_service): - mock_type = 11111111 + mock_type = SnomedCodes.LLOYD_GEORGE.value mocker.patch("uuid.uuid4", return_value="test_uuid") mock_params = { "subject:identifier": f"https://fhir.nhs.uk/Id/nhs-number|{TEST_NHS_NUMBER}", - "type": f"http://snomed.info/sct|{mock_type}", + "type": f"http://snomed.info/sct|{mock_type.code}", } mock_headers = { "Authorization": f"Bearer {ACCESS_TOKEN}", @@ -64,11 +65,11 @@ def test_get_pointer_with_record_type(mocker, nrl_service): def test_get_pointer_with_record_type_no_retry(mocker, nrl_service): - mock_type = 11111111 + mock_type = SnomedCodes.LLOYD_GEORGE.value mocker.patch("uuid.uuid4", return_value="test_uuid") mock_params = { "subject:identifier": f"https://fhir.nhs.uk/Id/nhs-number|{TEST_NHS_NUMBER}", - "type": f"http://snomed.info/sct|{mock_type}", + "type": f"http://snomed.info/sct|{mock_type.code}", } mock_headers = { "Authorization": f"Bearer {ACCESS_TOKEN}", @@ -90,11 +91,11 @@ def test_get_pointer_with_record_type_no_retry(mocker, nrl_service): def test_get_pointer_with_record_type_with_retry(mocker, nrl_service): - mock_type = 11111111 + mock_type = SnomedCodes.LLOYD_GEORGE.value mocker.patch("uuid.uuid4", return_value="test_uuid") mock_params = { "subject:identifier": f"https://fhir.nhs.uk/Id/nhs-number|{TEST_NHS_NUMBER}", - "type": f"http://snomed.info/sct|{mock_type}", + "type": f"http://snomed.info/sct|{mock_type.code}", } mock_headers = { "Authorization": f"Bearer {ACCESS_TOKEN}", @@ -120,7 +121,7 @@ def test_get_pointer_raise_error(nrl_service): response.status_code = 400 response._content = b"{}" - mock_type = 11111111 + mock_type = SnomedCodes.LLOYD_GEORGE.value nrl_service.session.get.return_value = response pytest.raises(NrlApiException, nrl_service.get_pointer, TEST_NHS_NUMBER, mock_type) @@ -129,7 +130,7 @@ def test_get_pointer_raise_error(nrl_service): def test_delete_pointer_with_record_type_no_record(mocker, nrl_service): - mock_type = 11111111 + mock_type = SnomedCodes.LLOYD_GEORGE.value mocker.patch("uuid.uuid4", return_value="test_uuid") nrl_response = { @@ -145,7 +146,7 @@ def test_delete_pointer_with_record_type_no_record(mocker, nrl_service): def test_delete_pointer_with_record_type_one_record(mocker, nrl_service): - mock_type = 11111111 + mock_type = SnomedCodes.LLOYD_GEORGE.value mocker.patch("uuid.uuid4", return_value="test_uuid") mock_pointer_id = "ODSCODE-1111bfb1-1111-2222-3333-4444555c666f" mock_headers = { @@ -176,7 +177,7 @@ def test_delete_pointer_with_record_type_one_record(mocker, nrl_service): def test_delete_pointer_with_record_type_more_than_one_record(mocker, nrl_service): - mock_type = 11111111 + mock_type = SnomedCodes.LLOYD_GEORGE.value mocker.patch("uuid.uuid4", return_value="test_uuid") mock_pointer_id = "ODSCODE-1111bfb1-1111-2222-3333-4444555c666" @@ -209,7 +210,7 @@ def test_delete_pointer_not_raise_error(mocker, nrl_service): response = Response() response.status_code = 400 response._content = b"{}" - mock_type = 11111111 + mock_type = SnomedCodes.LLOYD_GEORGE.value nrl_response = { "resourceType": "Bundle", "type": "searchset",