Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
93d1124
Start of SDS module
Vox-Ben Jan 22, 2026
f6236e8
First-cut SDS access module
Vox-Ben Feb 6, 2026
eccff4e
Fix method signatures
Vox-Ben Feb 6, 2026
d9996ea
Add SDS integration tests
Vox-Ben Feb 6, 2026
eae26cb
Update SdsSearchResults export location
Vox-Ben Feb 9, 2026
3de8f28
Add extra test data to stub.
Vox-Ben Feb 9, 2026
f67258a
Create stub base class
Vox-Ben Feb 9, 2026
afe0f8e
Move Provider stub to base class
Vox-Ben Feb 9, 2026
3320c0d
Refactor stub data to make sonar happy
Vox-Ben Feb 9, 2026
af0024d
That really wasn't a security flaw, sonar
Vox-Ben Feb 9, 2026
8bb8a10
Comment/docstring changes
Vox-Ben Feb 10, 2026
758861b
Remove error handling and testing
Vox-Ben Feb 11, 2026
d523e09
Move interaction ID into common
Vox-Ben Feb 12, 2026
bb7b2fa
Remove error check on no hit on ODS code
Vox-Ben Feb 12, 2026
de073fa
Refactor accidental singleton out of tests
Vox-Ben Feb 13, 2026
08f8d6b
Address review comments
Vox-Ben Feb 13, 2026
db8aa1c
Fix test fake for updated signature
Vox-Ben Feb 13, 2026
bce9d50
Remove unnecessary mocking
Vox-Ben Feb 13, 2026
ff9623a
Mock requests.get as well as the stub
Vox-Ben Feb 13, 2026
e6cf45d
Add check for default interaction ID
Vox-Ben Feb 13, 2026
06920d2
Set terraform version pinning
neil-sproston Feb 13, 2026
a53e4be
Fix container build
Vox-Ben Feb 16, 2026
85fb596
Remove debug line
Vox-Ben Feb 17, 2026
20f757e
Set stub env vars for pipeline
Vox-Ben Feb 17, 2026
708aff2
Obtain API key internally
Vox-Ben Feb 18, 2026
d45a736
Tidy up docstrings
Vox-Ben Feb 18, 2026
72e9736
Code review comments
Vox-Ben Feb 18, 2026
5e364a0
Remove unnecessary env vars
Vox-Ben Feb 18, 2026
53b7f22
Move interaction ID, change env var values
Vox-Ben Feb 18, 2026
984bf41
Change to StrEnum
Vox-Ben Feb 18, 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
3 changes: 3 additions & 0 deletions .github/workflows/stage-2-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: "Test stage"
env:
BASE_URL: "http://localhost:5000"
HOST: "localhost"
STUB_SDS: "true"
STUB_PDS: "true"
STUB_PROVIDER: "true"

on:
workflow_call:
Expand Down
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is for you! Please, updated to the versions agreed by your team.

terraform 1.14.0
terraform 1.14.5
pre-commit 3.6.0
gitleaks 8.18.4

Expand All @@ -15,7 +15,7 @@ gitleaks 8.18.4
# docker/ghcr.io/make-ops-tools/gocloc latest@sha256:6888e62e9ae693c4ebcfed9f1d86c70fd083868acb8815fe44b561b9a73b5032 # SEE: https://github.com/make-ops-tools/gocloc/pkgs/container/gocloc
# docker/ghcr.io/nhs-england-tools/github-runner-image 20230909-321fd1e-rt@sha256:ce4fd6035dc450a50d3cbafb4986d60e77cb49a71ab60a053bb1b9518139a646 # SEE: https://github.com/nhs-england-tools/github-runner-image/pkgs/container/github-runner-image
# docker/hadolint/hadolint 2.12.0-alpine@sha256:7dba9a9f1a0350f6d021fb2f6f88900998a4fb0aaf8e4330aa8c38544f04db42 # SEE: https://hub.docker.com/r/hadolint/hadolint/tags
# docker/hashicorp/terraform 1.12.2@sha256:b3d13c9037d2bd858fe10060999aa7ca56d30daafe067d7715b29b3d4f5b162f # SEE: https://hub.docker.com/r/hashicorp/terraform/tags
# docker/hashicorp/terraform 1.14.5@sha256:96d2bc440714bf2b2f2998ac730fd4612f30746df43fca6f0892b2e2035b11bc # SEE: https://hub.docker.com/r/hashicorp/terraform/tags
# docker/koalaman/shellcheck latest@sha256:e40388688bae0fcffdddb7e4dea49b900c18933b452add0930654b2dea3e7d5c # SEE: https://hub.docker.com/r/koalaman/shellcheck/tags
# docker/mstruebing/editorconfig-checker 2.7.1@sha256:dd3ca9ea50ef4518efe9be018d669ef9cf937f6bb5cfe2ef84ff2a620b5ddc24 # SEE: https://hub.docker.com/r/mstruebing/editorconfig-checker/tags
# docker/sonarsource/sonar-scanner-cli 10.0@sha256:0bc49076468d2955948867620b2d98d67f0d59c0fd4a5ef1f0afc55cf86f2079 # SEE: https://hub.docker.com/r/sonarsource/sonar-scanner-cli/tags
73 changes: 10 additions & 63 deletions gateway-api/src/gateway_api/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from gateway_api.common.common import FlaskResponse
from gateway_api.pds_search import PdsClient, PdsSearchResults
from gateway_api.sds_search import SdsClient, SdsSearchResults


@dataclass
Expand All @@ -44,62 +45,6 @@ def __str__(self) -> str:
return self.message


@dataclass
class SdsSearchResults:
"""
Stub SDS search results dataclass.

Replace this with the real one once it's implemented.

:param asid: Accredited System ID.
:param endpoint: Endpoint URL associated with the organisation, if applicable.
"""

asid: str
endpoint: str | None


class SdsClient:
"""
Stub SDS client for obtaining ASID from ODS code.

Replace this with the real one once it's implemented.
"""

SANDBOX_URL = "https://example.invalid/sds"

def __init__(
self,
auth_token: str,
base_url: str = SANDBOX_URL,
timeout: int = 10,
) -> None:
"""
Create an SDS client.

:param auth_token: Authentication token to present to SDS.
:param base_url: Base URL for SDS.
:param timeout: Timeout in seconds for SDS calls.
"""
self.auth_token = auth_token
self.base_url = base_url
self.timeout = timeout

def get_org_details(self, ods_code: str) -> SdsSearchResults | None:
"""
Retrieve SDS org details for a given ODS code.

This is a placeholder implementation that always returns an ASID and endpoint.

:param ods_code: ODS code to look up.
:returns: SDS search results or ``None`` if not found.
"""
# Placeholder implementation
return SdsSearchResults(
asid=f"asid_{ods_code}", endpoint="https://example-provider.org/endpoint"
)


class Controller:
"""
Orchestrates calls to PDS -> SDS -> GP provider.
Expand All @@ -113,7 +58,7 @@ class Controller:
def __init__(
self,
pds_base_url: str = PdsClient.SANDBOX_URL,
sds_base_url: str = "https://example.invalid/sds",
sds_base_url: str = SdsClient.SANDBOX_URL,
nhsd_session_urid: str | None = None,
timeout: int = 10,
) -> None:
Expand Down Expand Up @@ -159,7 +104,7 @@ def run(self, request: GetStructuredRecordRequest) -> FlaskResponse:

try:
consumer_asid, provider_asid, provider_endpoint = self._get_sds_details(
auth_token, request.ods_from.strip(), provider_ods
request.ods_from.strip(), provider_ods
)
except RequestError as err:
return FlaskResponse(status_code=err.status_code, data=str(err))
Expand Down Expand Up @@ -243,7 +188,7 @@ def _get_pds_details(
return provider_ods_code

def _get_sds_details(
self, auth_token: str, consumer_ods: str, provider_ods: str
self, consumer_ods: str, provider_ods: str
) -> tuple[str, str, str]:
"""
Call SDS to obtain consumer ASID, provider ASID, and provider endpoint.
Expand All @@ -252,20 +197,20 @@ def _get_sds_details(
- provider details (ASID + endpoint)
- consumer details (ASID)

:param auth_token: Authorization token to use for SDS.
:param consumer_ods: Consumer organisation ODS code (from request headers).
:param provider_ods: Provider organisation ODS code (from PDS).
:returns: Tuple of (consumer_asid, provider_asid, provider_endpoint).
:raises RequestError: If SDS data is missing or incomplete for provider/consumer
"""
# SDS: Get provider details (ASID + endpoint) for provider ODS
sds = SdsClient(
auth_token=auth_token,
base_url=self.sds_base_url,
timeout=self.timeout,
)

provider_details: SdsSearchResults | None = sds.get_org_details(provider_ods)
provider_details: SdsSearchResults | None = sds.get_org_details(
provider_ods, get_endpoint=True
)
if provider_details is None:
raise RequestError(
status_code=404,
Expand Down Expand Up @@ -293,7 +238,9 @@ def _get_sds_details(
)

# SDS: Get consumer details (ASID) for consumer ODS
consumer_details: SdsSearchResults | None = sds.get_org_details(consumer_ods)
consumer_details: SdsSearchResults | None = sds.get_org_details(
consumer_ods, get_endpoint=False
)
if consumer_details is None:
raise RequestError(
status_code=404,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""Get Structured Record module."""

from gateway_api.get_structured_record.request import (
ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
GetStructuredRecordRequest,
RequestValidationError,
)

__all__ = ["RequestValidationError", "GetStructuredRecordRequest"]
__all__ = [
"RequestValidationError",
"GetStructuredRecordRequest",
"ACCESS_RECORD_STRUCTURED_INTERACTION_ID",
]
12 changes: 10 additions & 2 deletions gateway-api/src/gateway_api/get_structured_record/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,26 @@
from fhir.operation_outcome import OperationOutcomeIssue
from flask.wrappers import Request, Response

from gateway_api.common.common import FlaskResponse
from gateway_api.common.common import (
FlaskResponse,
)

if TYPE_CHECKING:
from fhir.bundle import Bundle

# Access record structured interaction ID from
# https://developer.nhs.uk/apis/gpconnect/accessrecord_structured_development.html#spine-interactions
ACCESS_RECORD_STRUCTURED_INTERACTION_ID = (
"urn:nhs:names:services:gpconnect:fhir:operation:gpc.getstructuredrecord-1"
)


class RequestValidationError(Exception):
"""Exception raised for errors in the request validation."""


class GetStructuredRecordRequest:
INTERACTION_ID: str = "urn:nhs:names:services:gpconnect:gpc.getstructuredrecord-1"
INTERACTION_ID: str = ACCESS_RECORD_STRUCTURED_INTERACTION_ID
RESOURCE: str = "patient"
FHIR_OPERATION: str = "$gpc.getstructuredrecord"

Expand Down
13 changes: 6 additions & 7 deletions gateway-api/src/gateway_api/provider_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,10 @@
from urllib.parse import urljoin

from requests import HTTPError, Response, post
from stubs.stub_provider import stub_post
from stubs.stub_provider import GpProviderStub

from gateway_api.get_structured_record import ACCESS_RECORD_STRUCTURED_INTERACTION_ID

ARS_INTERACTION_ID = (
"urn:nhs:names:services:gpconnect:structured"
":fhir:operation:gpc.getstructuredrecord-1"
)
ARS_FHIR_BASE = "FHIR/STU3"
FHIR_RESOURCE = "patient"
ARS_FHIR_OPERATION = "$gpc.getstructuredrecord"
Expand All @@ -43,7 +41,8 @@
# Direct all requests to the stub provider for steel threading in dev.
# Replace with `from requests import post` for real requests.
PostCallable = Callable[..., Response]
post: PostCallable = stub_post # type: ignore[no-redef]
_gp_provider_stub = GpProviderStub()
post: PostCallable = _gp_provider_stub.post # type: ignore[no-redef]


class ExternalServiceError(Exception):
Expand Down Expand Up @@ -94,7 +93,7 @@ def _build_headers(self, trace_id: str) -> dict[str, str]:
return {
"Content-Type": "application/fhir+json",
"Accept": "application/fhir+json",
"Ssp-InteractionID": ARS_INTERACTION_ID,
"Ssp-InteractionID": ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
"Ssp-To": self.provider_asid,
"Ssp-From": self.consumer_asid,
"Ssp-TraceID": trace_id,
Expand Down
Loading
Loading