Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PRMDR-327 subtask - Connect search to PDS API #100

Merged
merged 6 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions lambdas/handlers/search_patient_details_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from json import JSONDecodeError

from pydantic import ValidationError
from requests import HTTPError
from services.pds_api_service import PdsApiService
from utils.exceptions import (
InvalidResourceIdException,
Expand All @@ -16,18 +15,21 @@

from services.mock_pds_service import MockPdsApiService

from utils.decorators.validate_patient_id import validate_patient_id

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def get_pds_service():
return (
PdsApiService
if (os.getenv("PDS_FHIR_IS_STUBBED") == 'false')
if (os.getenv("PDS_FHIR_IS_STUBBED") == "false")
SRAlexander marked this conversation as resolved.
Show resolved Hide resolved
else MockPdsApiService
)


@validate_patient_id
def lambda_handler(event, context):
logger.info("API Gateway event received - processing starts")
logger.info(event)
Expand All @@ -43,23 +45,31 @@ def lambda_handler(event, context):
return ApiGatewayResponse(200, response, "GET").create_api_gateway_response()

except PatientNotFoundException as e:
return ApiGatewayResponse(404, f"{str(e)}", "GET").create_api_gateway_response()
logger.error(f"PDS not found: {str(e)}")
return ApiGatewayResponse(
404, f"Patient does not exist for given NHS number", "GET"
).create_api_gateway_response()

except (InvalidResourceIdException, PdsErrorException) as e:
return ApiGatewayResponse(400, f"{str(e)}", "GET").create_api_gateway_response()
logger.error(f"PDS Error: {str(e)}")
return ApiGatewayResponse(
400, f"An error occurred while searching for patient", "GET"
).create_api_gateway_response()

except ValidationError as e:
logger.error(f"Failed to parse PDS data:{str(e)}")
return ApiGatewayResponse(
400, f"Failed to parse PDS data: {str(e)}", "GET"
400, f"Failed to parse PDS data", "GET"
).create_api_gateway_response()

except JSONDecodeError as e:
logger.error(f"Error while decoding Json:{str(e)}")
return ApiGatewayResponse(
400, f"Invalid json in body: {str(e)}", "GET"
400, f"Invalid json in body", "GET"
).create_api_gateway_response()

except KeyError as e:
logger.info(f"Error parsing patientId from json: {str(e)}")
logger.error(f"Error parsing patientId from json: {str(e)}")
return ApiGatewayResponse(
400, "No NHS number found in request parameters.", "GET"
).create_api_gateway_response()
31 changes: 5 additions & 26 deletions lambdas/services/mock_pds_service.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,20 @@
import json

from models.pds_models import PatientDetails
from requests import Response
from utils.utilities import validate_id
from utils.exceptions import (
PdsErrorException,
PatientNotFoundException,
InvalidResourceIdException,
)
from models.pds_models import Patient

from services.patient_search_service import PatientSearch

class MockPdsApiService:

class MockPdsApiService(PatientSearch):
def __init__(self, *args, **kwargs):
pass

def fetch_patient_details(self, nhs_number: str) -> PatientDetails:
validate_id(nhs_number)

response = self.fake_pds_request(nhs_number)

if response.status_code == 200:
patient = Patient.model_validate(response.content)
patient_details = patient.get_patient_details(nhs_number)
return patient_details

if response.status_code == 404:
raise PatientNotFoundException(
"Patient does not exist for given NHS number"
)

if response.status_code == 400:
raise InvalidResourceIdException("Invalid NHS number")

raise PdsErrorException("Error when requesting patient from PDS")

def fake_pds_request(self, nhsNumber: str) -> Response:
def pds_request(self, nhsNumber: str, *args, **kwargs) -> Response:
mock_pds_results: list[dict] = []

try:
Expand All @@ -59,7 +38,7 @@ def fake_pds_request(self, nhsNumber: str) -> Response:

if bool(pds_patient):
response.status_code = 200
response._content = pds_patient
response._content = json.dumps(pds_patient).encode("utf-8")
SRAlexander marked this conversation as resolved.
Show resolved Hide resolved
else:
response.status_code = 404

Expand Down
36 changes: 36 additions & 0 deletions lambdas/services/patient_search_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from models.pds_models import PatientDetails, Patient
from requests import Response

from utils.exceptions import (
PdsErrorException,
PatientNotFoundException,
InvalidResourceIdException,
)


class PatientSearch:
def fetch_patient_details(
self,
nhs_number: str,
) -> PatientDetails:
response = self.pds_request(nhs_number, retry_on_expired=True)
return self.handle_response(response, nhs_number)

def handle_response(self, response: Response, nhs_number: str) -> PatientDetails:
if response.status_code == 200:
patient = Patient.model_validate(response.json())
patient_details = patient.get_patient_details(nhs_number)
return patient_details

if response.status_code == 404:
raise PatientNotFoundException(
"Patient does not exist for given NHS number"
)

if response.status_code == 400:
raise InvalidResourceIdException("Invalid NHS number")

raise PdsErrorException("Error when requesting patient from PDS")

def pds_request(self, nhsNumber: str, *args, **kwargs) -> Response:
pass
77 changes: 27 additions & 50 deletions lambdas/services/pds_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,77 +6,54 @@
import jwt
import requests
from botocore.exceptions import ClientError
from models.pds_models import Patient, PatientDetails
from requests.models import Response, HTTPError
from requests.models import HTTPError
from utils.exceptions import (
InvalidResourceIdException,
PatientNotFoundException,
PdsErrorException,
)
from utils.utilities import validate_id

from enums.pds_ssm_parameters import SSMParameter

from services.patient_search_service import PatientSearch

logger = logging.getLogger()
logger.setLevel(logging.INFO)


class PdsApiService:
class PdsApiService(PatientSearch):
def __init__(self, ssm_service):
self.ssm_service = ssm_service

def fetch_patient_details(
self,
nhs_number: str,
) -> PatientDetails:
def pds_request(self, nshNumber: str, retry_on_expired: bool):
try:
logger.info("Using real pds service")
validate_id(nhs_number)
response = self.pds_request(nhs_number, retry_on_expired=True)
return self.handle_response(response, nhs_number)
except ClientError as e:
logger.error(f"Error when getting ssm parameters {e}")
raise PdsErrorException("Failed to preform patient search")

def handle_response(self, response: Response, nhs_number: str) -> PatientDetails:
if response.status_code == 200:
patient = Patient.model_validate(response.json())
patient_details = patient.get_patient_details(nhs_number)
return patient_details

if response.status_code == 404:
raise PatientNotFoundException(
"Patient does not exist for given NHS number"
endpoint, access_token_response = self.get_parameters_for_pds_api_request()
access_token_response = json.loads(access_token_response)
access_token = access_token_response["access_token"]
access_token_expiration = (
int(access_token_response["expires_in"])
+ int(access_token_response["issued_at"]) / 1000
)
time_safety_margin_seconds = 10
if time.time() - access_token_expiration > time_safety_margin_seconds:
access_token = self.get_new_access_token()

if response.status_code == 400:
raise InvalidResourceIdException("Invalid NHS number")
x_request_id = str(uuid.uuid4())

raise PdsErrorException("Error when requesting patient from PDS")
authorization_header = {
"Authorization": f"Bearer {access_token}",
"X-Request-ID": x_request_id,
}

def pds_request(self, nshNumber: str, retry_on_expired: bool):
endpoint, access_token_response = self.get_parameters_for_pds_api_request()
access_token_response = json.loads(access_token_response)
access_token = access_token_response["access_token"]
access_token_expiration = int(access_token_response["expires_in"]) + int(
access_token_response["issued_at"]
)
time_safety_margin_seconds = 10
if time.time() - access_token_expiration < time_safety_margin_seconds:
access_token = self.get_new_access_token()

x_request_id = str(uuid.uuid4())

authorization_header = {
"Authorization": f"Bearer {access_token}",
"X-Request-ID": x_request_id,
}
url_endpoint = endpoint + "Patient/" + nshNumber
pds_response = requests.get(url=url_endpoint, headers=authorization_header)
if pds_response.status_code == 401 and retry_on_expired:
return self.pds_request(nshNumber, retry_on_expired=False)
return pds_response

url_endpoint = endpoint + "Patient/" + nshNumber
pds_response = requests.get(url=url_endpoint, headers=authorization_header)
if pds_response.status_code == 401 & retry_on_expired:
return self.pds_request(nshNumber, retry_on_expired=False)
return pds_response
except ClientError as e:
logger.error(f"Error when getting ssm parameters {e}")
raise PdsErrorException("Failed to preform patient search")

def get_new_access_token(self):
logger.info("Getting new PDS access token")
Expand Down
19 changes: 6 additions & 13 deletions lambdas/tests/unit/handlers/test_search_patient_details_handler.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import os
from unittest.mock import patch

Expand All @@ -21,10 +22,10 @@ def test_lambda_handler_valid_id_returns_200(
):
response = Response()
response.status_code = 200
response._content = PDS_PATIENT
response._content = json.dumps(PDS_PATIENT).encode("utf-8")

mocker.patch(
"services.mock_pds_service.MockPdsApiService.fake_pds_request",
"services.mock_pds_service.MockPdsApiService.pds_request",
return_value=response,
)

Expand Down Expand Up @@ -53,7 +54,7 @@ def test_lambda_handler_invalid_id_returns_400(
response.status_code = 400

mocker.patch(
"services.mock_pds_service.MockPdsApiService.fake_pds_request",
"services.mock_pds_service.MockPdsApiService.pds_request",
return_value=response,
)

Expand All @@ -80,7 +81,7 @@ def test_lambda_handler_valid_id_not_in_pds_returns_404(
response.status_code = 404

mocker.patch(
"services.mock_pds_service.MockPdsApiService.fake_pds_request",
"services.mock_pds_service.MockPdsApiService.pds_request",
return_value=response,
)

Expand All @@ -103,18 +104,10 @@ def test_lambda_handler_valid_id_not_in_pds_returns_404(
def test_lambda_handler_missing_id_in_query_params_returns_400(
missing_id_event, context, mocker, patch_env_vars
):
response = Response()
response.status_code = 400

mocker.patch(
"services.mock_pds_service.MockPdsApiService.fake_pds_request",
return_value=response,
)

actual = lambda_handler(missing_id_event, context)

expected = {
"body": "No NHS number found in request parameters.",
"body": "An error occurred due to missing key: 'patientId'",
"headers": {
"Content-Type": "application/fhir+json",
"Access-Control-Allow-Origin": "*",
Expand Down
6 changes: 6 additions & 0 deletions lambdas/tests/unit/helpers/data/pds/access_token_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
RESPONSE_TOKEN = {
"access_token": "Sr5PGv19wTEHJdDr2wx2f7IGd0cw",
"expires_in": "599",
"token_type": "Bearer",
"issued_at": "1650000006000",
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
]
},
},
{"url": "interpreterRequired", "valueBoolean": True},
{"url": "interpreterRequired", "valueBoolean": "True"},
],
},
{
Expand Down
19 changes: 4 additions & 15 deletions lambdas/tests/unit/services/test_mock_pds_api_service.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import pytest
import json

from models.pds_models import PatientDetails
from requests.models import Response
from tests.unit.helpers.data.pds.pds_patient_response import PDS_PATIENT
from utils.exceptions import (
InvalidResourceIdException,
PatientNotFoundException,
PdsErrorException,
)

from services.mock_pds_service import MockPdsApiService

Expand All @@ -18,10 +14,10 @@ def test_fetch_patient_details_valid_returns_PatientDetails(mocker):

response = Response()
response.status_code = 200
response._content = PDS_PATIENT
response._content = json.dumps(PDS_PATIENT).encode("utf-8")

mocker.patch(
"services.mock_pds_service.MockPdsApiService.fake_pds_request",
"services.mock_pds_service.MockPdsApiService.pds_request",
return_value=response,
)

Expand All @@ -38,10 +34,3 @@ def test_fetch_patient_details_valid_returns_PatientDetails(mocker):
)

assert actual == expected


def test_fetch_patient_details_invalid_nhs_number_raises_InvalidResourceIdException():
nhs_number = "000000000"

with pytest.raises(InvalidResourceIdException):
pds_service.fetch_patient_details(nhs_number)
Loading
Loading