Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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: "1"
STUB_PDS: "1"
STUB_PROVIDER: "1"
Comment on lines +6 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the variable value meaningful to the context.

Suggested change
STUB_SDS: "1"
STUB_PDS: "1"
STUB_PROVIDER: "1"
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
6 changes: 6 additions & 0 deletions gateway-api/src/gateway_api/common/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
# The alias is used to make intent clearer in function signatures.
type json_str = str

# 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"
)

Comment on lines +12 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this didn't need to go into a common class. It is intrinsically linked to the GetStructuredRecordRequest.

Yes it is going to be use in multiple places, and thus it is a common piece of code, but by placing it as it's own constant in the common module, it has lost its tight coupling with the request to which it is related.


@dataclass
class FlaskResponse:
Expand Down
71 changes: 10 additions & 61 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 @@ -252,20 +197,22 @@ def _get_sds_details(
- provider details (ASID + endpoint)
- consumer details (ASID)

:param auth_token: Authorization token to use for SDS.
:param auth_token: Authorization token to use for SDS (used as API key).
: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,
api_key=auth_token,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API key is not the auth token.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think leaving auth journey out the client at the moment makes the most sense. In that way, once we have set up our own mock.

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 +240,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
Expand Up @@ -5,7 +5,10 @@
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 (
ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
FlaskResponse,
)

if TYPE_CHECKING:
from fhir.bundle import Bundle
Expand All @@ -16,7 +19,7 @@ class RequestValidationError(Exception):


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: 7 additions & 6 deletions gateway-api/src/gateway_api/provider_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
from urllib.parse import urljoin

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

ARS_INTERACTION_ID = (
"urn:nhs:names:services:gpconnect:structured"
":fhir:operation:gpc.getstructuredrecord-1"
from gateway_api.common.common import (
ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
)

ARS_FHIR_BASE = "FHIR/STU3"
FHIR_RESOURCE = "patient"
ARS_FHIR_OPERATION = "$gpc.getstructuredrecord"
Expand All @@ -43,7 +43,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 +95,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