⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cc6d5c3
[GPCAPIM-275]: Remove duplicate lines.
davidhamill1-nhs Feb 5, 2026
4ebe273
[GPCAPIM-275]: Import modules consistently.
davidhamill1-nhs Feb 5, 2026
742f2fd
[GPCAPIM-275]: Enable other test modules to use a valid Bundle.
davidhamill1-nhs Feb 5, 2026
41437ac
[GPCAPIM-275]: Reduce mocking complexity. Move common required header…
davidhamill1-nhs Feb 5, 2026
dd8c318
[GPCAPIM-275]: Resolve spurious Sonar issue
davidhamill1-nhs Feb 5, 2026
4f1534d
[GPCAPIM-275]: Use single assertion in unit tests
davidhamill1-nhs Feb 5, 2026
3580a2b
[GPCAPIM-275]: Content-type header is a required header.
davidhamill1-nhs Feb 5, 2026
22adc40
[GPCAPIM-275]: Use static datetimes for unit tests.
davidhamill1-nhs Feb 5, 2026
4b9dbec
[GPCAPIM-275]: Move towards using a common error class
davidhamill1-nhs Feb 9, 2026
80a90ed
[GPCAPIM-275]: Move towards using a common error class.
davidhamill1-nhs Feb 9, 2026
72af28c
[GPCAPIM-275]: Rework unit tests to only assert once in a test method
davidhamill1-nhs Feb 9, 2026
a41f241
[GPCAPIM-275]: Have a "catch all" error within the common error class
davidhamill1-nhs Feb 9, 2026
701038b
[GPCAPIM-275]: Run request handling all in one try-except as exceptio…
davidhamill1-nhs Feb 9, 2026
5e55b85
[GPCAPIM-275]: Run request handling all in one try-except as exceptio…
davidhamill1-nhs Feb 9, 2026
5d924a8
[GPCAPIM-275]: Enable additional details to be passed to errors.
davidhamill1-nhs Feb 9, 2026
e5fecaa
[GPCAPIM-275]: We do not send the ODS code to PDS as an End User Org
davidhamill1-nhs Feb 9, 2026
8354daf
[GPCAPIM-275]: Given we are accessing PDS as an application, we do no…
davidhamill1-nhs Feb 9, 2026
36d9812
[GPCAPIM-275]: PDS sandbox can receive auth header.
davidhamill1-nhs Feb 9, 2026
ddd5b94
[GPCAPIM-275]: Move towards common error class
davidhamill1-nhs Feb 10, 2026
750e4d4
[GPCAPIM-275]: Use http client's status code definitions for clarity
davidhamill1-nhs Feb 10, 2026
7aa9fa2
[GPCAPIM-275]: Move towards common error class
davidhamill1-nhs Feb 10, 2026
5af0f0a
[GPCAPIM-275]: Remove unnecessary imports.
davidhamill1-nhs Feb 10, 2026
366f6d4
[GPCAPIM-275]: Move modules to their own directory
davidhamill1-nhs Feb 10, 2026
4169586
[GPCAPIM-275]: Add example Patient resource - taken from PDS FHIR's s…
davidhamill1-nhs Feb 10, 2026
be94937
[GPCAPIM-275]: Move modules to their own directory
davidhamill1-nhs Feb 10, 2026
0c3c8cd
[GPCAPIM-275]: Write a test to make number go up
davidhamill1-nhs Feb 10, 2026
30fdbaa
[GPCAPIM-275]: Behaviour being testted already covered by integration…
davidhamill1-nhs Feb 10, 2026
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
32 changes: 25 additions & 7 deletions gateway-api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions gateway-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ clinical-data-common = { git = "https://github.com/NHSDigital/clinical-data-comm
flask = "^3.1.2"
types-flask = "^1.1.6"
requests = "^2.32.5"
pytest-mock = "^3.15.1"

[tool.poetry]
packages = [{include = "gateway_api", from = "src"},
Expand Down Expand Up @@ -55,6 +56,7 @@ dev = [
"schemathesis>=4.4.1",
"types-requests (>=2.32.4.20250913,<3.0.0.0)",
"types-pyyaml (>=6.0.12.20250915,<7.0.0.0)",
"pytest-mock (>=3.15.1,<4.0.0)",
]

[tool.mypy]
Expand Down
27 changes: 8 additions & 19 deletions gateway-api/src/gateway_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from flask import Flask, request
from flask.wrappers import Response

from gateway_api.common.error import BaseError
from gateway_api.controller import Controller
from gateway_api.get_structured_record import (
GetStructuredRecordRequest,
RequestValidationError,
)

app = Flask(__name__)
Expand Down Expand Up @@ -38,27 +38,16 @@ def get_app_port() -> int:
def get_structured_record() -> Response:
try:
get_structured_record_request = GetStructuredRecordRequest(request)
except RequestValidationError as e:
response = Response(
response=str(e),
status=400,
content_type="text/plain",
)
return response
except Exception as e:
response = Response(
response=f"Internal Server Error: {e}",
status=500,
content_type="text/plain",
)
return response

try:
controller = Controller()
flask_response = controller.run(request=get_structured_record_request)
get_structured_record_request.set_response_from_flaskresponse(flask_response)
except Exception as e:
get_structured_record_request.set_negative_response(str(e))
except BaseError as e:
e.log()
return e.build_response()
except Exception:
error = BaseError()
error.log()
return error.build_response()

return get_structured_record_request.build_response()

Expand Down
99 changes: 99 additions & 0 deletions gateway-api/src/gateway_api/common/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import json
from dataclasses import dataclass
from http.client import BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND
from typing import TYPE_CHECKING

from flask import Response

if TYPE_CHECKING:
from fhir.operation_outcome import OperationOutcome


@dataclass
class BaseError(Exception):
_message = "Internal Server Error"
status_code: int = INTERNAL_SERVER_ERROR
severity: str = "error"
error_code: str = "exception"

def __init__(self, **additional_details: str):
self.additional_details = additional_details
super().__init__(self)

def build_response(self) -> Response:
operation_outcome: OperationOutcome = {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": self.severity,
"code": self.error_code,
"diagnostics": self.message,
}
],
}
response = Response(
response=json.dumps(operation_outcome),
status=self.status_code,
content_type="application/fhir+json",
)
return response

def log(self) -> None:
print(self)

@property
def message(self) -> str:
return self._message.format(**self.additional_details)

def __str__(self) -> str:
return self.message


class NoPatientFound(BaseError):
_message = "No PDS patient found for NHS number {nhs_number}"
status_code = BAD_REQUEST


class InvalidRequestJSON(BaseError):
_message = "Invalid JSON body sent in request"
status_code = BAD_REQUEST


class MissingOrEmptyHeader(BaseError):
_message = 'Missing or empty required header "{header}"'
status_code = BAD_REQUEST


class NoCurrentProvider(BaseError):
_message = "PDS patient {nhs_number} did not contain a current provider ODS code"
status_code = NOT_FOUND


class NoOrganisationFound(BaseError):
_message = "No SDS org found for {org_type} ODS code {ods_code}"
status_code = NOT_FOUND


class NoAsidFound(BaseError):
_message = (
"SDS result for {org_type} ODS code {ods_code} did not contain a current ASID"
)
status_code = NOT_FOUND


class NoCurrentEndpoint(BaseError):
_message = (
"SDS result for provider ODS code {provider_ods} did not contain "
"a current endpoint"
)
status_code = NOT_FOUND


class PdsRequestFailed(BaseError):
_message = "PDS FHIR API request failed: {error_reason}"
status_code = INTERNAL_SERVER_ERROR


class SdsRequestFailed(BaseError):
_message = "SDS FHIR API request failed: {error_reason}"
status_code = INTERNAL_SERVER_ERROR
40 changes: 40 additions & 0 deletions gateway-api/src/gateway_api/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Pytest configuration and shared fixtures for gateway API tests."""

import pytest
from fhir.bundle import Bundle
from fhir.parameters import Parameters


Expand All @@ -18,3 +19,42 @@ def valid_simple_request_payload() -> Parameters:
},
],
}


@pytest.fixture
def valid_simple_response_payload() -> Bundle:
return {
"resourceType": "Bundle",
"id": "example-patient-bundle",
"type": "collection",
"timestamp": "2026-02-05T22:45:42.766330+00:00",
"entry": [
{
"fullUrl": "https://example.com/Patient/9999999999",
"resource": {
"name": [{"family": "Alice", "given": ["Johnson"], "use": "Ally"}],
"gender": "female",
"birthDate": "1990-05-15",
"resourceType": "Patient",
"id": "9999999999",
"identifier": [
{"value": "9999999999", "system": "urn:nhs:numbers"}
],
},
}
],
}


@pytest.fixture
def valid_headers() -> dict[str, str]:
return {
"Ssp-TraceID": "test-trace-id",
"ODS-from": "test-ods",
"Content-type": "application/fhir+json",
}


@pytest.fixture
def auth_token() -> str:
return "AUTH_TOKEN123"
Loading