Skip to content
Open
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
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"
)


@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,
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
Loading