Skip to content

Commit 5cc0daa

Browse files
authored
[GPDAPIM-258] SDS access module (#79)
Creates the SDS access module and integrates it with the controller. CDG requires the ability to obtain organisation ASIDs and endpoints from SDS. This provides that capability.
1 parent 848f4ed commit 5cc0daa

File tree

18 files changed

+1603
-175
lines changed

18 files changed

+1603
-175
lines changed

.github/workflows/stage-2-test.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ name: "Test stage"
33
env:
44
BASE_URL: "http://localhost:5000"
55
HOST: "localhost"
6+
STUB_SDS: "true"
7+
STUB_PDS: "true"
8+
STUB_PROVIDER: "true"
69

710
on:
811
workflow_call:

.tool-versions

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is for you! Please, updated to the versions agreed by your team.
22

3-
terraform 1.14.0
3+
terraform 1.14.5
44
pre-commit 3.6.0
55
gitleaks 8.18.4
66

@@ -15,7 +15,7 @@ gitleaks 8.18.4
1515
# docker/ghcr.io/make-ops-tools/gocloc latest@sha256:6888e62e9ae693c4ebcfed9f1d86c70fd083868acb8815fe44b561b9a73b5032 # SEE: https://github.com/make-ops-tools/gocloc/pkgs/container/gocloc
1616
# 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
1717
# docker/hadolint/hadolint 2.12.0-alpine@sha256:7dba9a9f1a0350f6d021fb2f6f88900998a4fb0aaf8e4330aa8c38544f04db42 # SEE: https://hub.docker.com/r/hadolint/hadolint/tags
18-
# docker/hashicorp/terraform 1.12.2@sha256:b3d13c9037d2bd858fe10060999aa7ca56d30daafe067d7715b29b3d4f5b162f # SEE: https://hub.docker.com/r/hashicorp/terraform/tags
18+
# docker/hashicorp/terraform 1.14.5@sha256:96d2bc440714bf2b2f2998ac730fd4612f30746df43fca6f0892b2e2035b11bc # SEE: https://hub.docker.com/r/hashicorp/terraform/tags
1919
# docker/koalaman/shellcheck latest@sha256:e40388688bae0fcffdddb7e4dea49b900c18933b452add0930654b2dea3e7d5c # SEE: https://hub.docker.com/r/koalaman/shellcheck/tags
2020
# docker/mstruebing/editorconfig-checker 2.7.1@sha256:dd3ca9ea50ef4518efe9be018d669ef9cf937f6bb5cfe2ef84ff2a620b5ddc24 # SEE: https://hub.docker.com/r/mstruebing/editorconfig-checker/tags
2121
# docker/sonarsource/sonar-scanner-cli 10.0@sha256:0bc49076468d2955948867620b2d98d67f0d59c0fd4a5ef1f0afc55cf86f2079 # SEE: https://hub.docker.com/r/sonarsource/sonar-scanner-cli/tags

gateway-api/src/gateway_api/controller.py

Lines changed: 10 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from gateway_api.common.common import FlaskResponse
2020
from gateway_api.pds_search import PdsClient, PdsSearchResults
21+
from gateway_api.sds_search import SdsClient, SdsSearchResults
2122

2223

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

4647

47-
@dataclass
48-
class SdsSearchResults:
49-
"""
50-
Stub SDS search results dataclass.
51-
52-
Replace this with the real one once it's implemented.
53-
54-
:param asid: Accredited System ID.
55-
:param endpoint: Endpoint URL associated with the organisation, if applicable.
56-
"""
57-
58-
asid: str
59-
endpoint: str | None
60-
61-
62-
class SdsClient:
63-
"""
64-
Stub SDS client for obtaining ASID from ODS code.
65-
66-
Replace this with the real one once it's implemented.
67-
"""
68-
69-
SANDBOX_URL = "https://example.invalid/sds"
70-
71-
def __init__(
72-
self,
73-
auth_token: str,
74-
base_url: str = SANDBOX_URL,
75-
timeout: int = 10,
76-
) -> None:
77-
"""
78-
Create an SDS client.
79-
80-
:param auth_token: Authentication token to present to SDS.
81-
:param base_url: Base URL for SDS.
82-
:param timeout: Timeout in seconds for SDS calls.
83-
"""
84-
self.auth_token = auth_token
85-
self.base_url = base_url
86-
self.timeout = timeout
87-
88-
def get_org_details(self, ods_code: str) -> SdsSearchResults | None:
89-
"""
90-
Retrieve SDS org details for a given ODS code.
91-
92-
This is a placeholder implementation that always returns an ASID and endpoint.
93-
94-
:param ods_code: ODS code to look up.
95-
:returns: SDS search results or ``None`` if not found.
96-
"""
97-
# Placeholder implementation
98-
return SdsSearchResults(
99-
asid=f"asid_{ods_code}", endpoint="https://example-provider.org/endpoint"
100-
)
101-
102-
10348
class Controller:
10449
"""
10550
Orchestrates calls to PDS -> SDS -> GP provider.
@@ -113,7 +58,7 @@ class Controller:
11358
def __init__(
11459
self,
11560
pds_base_url: str = PdsClient.SANDBOX_URL,
116-
sds_base_url: str = "https://example.invalid/sds",
61+
sds_base_url: str = SdsClient.SANDBOX_URL,
11762
nhsd_session_urid: str | None = None,
11863
timeout: int = 10,
11964
) -> None:
@@ -159,7 +104,7 @@ def run(self, request: GetStructuredRecordRequest) -> FlaskResponse:
159104

160105
try:
161106
consumer_asid, provider_asid, provider_endpoint = self._get_sds_details(
162-
auth_token, request.ods_from.strip(), provider_ods
107+
request.ods_from.strip(), provider_ods
163108
)
164109
except RequestError as err:
165110
return FlaskResponse(status_code=err.status_code, data=str(err))
@@ -243,7 +188,7 @@ def _get_pds_details(
243188
return provider_ods_code
244189

245190
def _get_sds_details(
246-
self, auth_token: str, consumer_ods: str, provider_ods: str
191+
self, consumer_ods: str, provider_ods: str
247192
) -> tuple[str, str, str]:
248193
"""
249194
Call SDS to obtain consumer ASID, provider ASID, and provider endpoint.
@@ -252,20 +197,20 @@ def _get_sds_details(
252197
- provider details (ASID + endpoint)
253198
- consumer details (ASID)
254199
255-
:param auth_token: Authorization token to use for SDS.
256200
:param consumer_ods: Consumer organisation ODS code (from request headers).
257201
:param provider_ods: Provider organisation ODS code (from PDS).
258202
:returns: Tuple of (consumer_asid, provider_asid, provider_endpoint).
259203
:raises RequestError: If SDS data is missing or incomplete for provider/consumer
260204
"""
261205
# SDS: Get provider details (ASID + endpoint) for provider ODS
262206
sds = SdsClient(
263-
auth_token=auth_token,
264207
base_url=self.sds_base_url,
265208
timeout=self.timeout,
266209
)
267210

268-
provider_details: SdsSearchResults | None = sds.get_org_details(provider_ods)
211+
provider_details: SdsSearchResults | None = sds.get_org_details(
212+
provider_ods, get_endpoint=True
213+
)
269214
if provider_details is None:
270215
raise RequestError(
271216
status_code=404,
@@ -293,7 +238,9 @@ def _get_sds_details(
293238
)
294239

295240
# SDS: Get consumer details (ASID) for consumer ODS
296-
consumer_details: SdsSearchResults | None = sds.get_org_details(consumer_ods)
241+
consumer_details: SdsSearchResults | None = sds.get_org_details(
242+
consumer_ods, get_endpoint=False
243+
)
297244
if consumer_details is None:
298245
raise RequestError(
299246
status_code=404,
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
"""Get Structured Record module."""
22

33
from gateway_api.get_structured_record.request import (
4+
ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
45
GetStructuredRecordRequest,
56
RequestValidationError,
67
)
78

8-
__all__ = ["RequestValidationError", "GetStructuredRecordRequest"]
9+
__all__ = [
10+
"RequestValidationError",
11+
"GetStructuredRecordRequest",
12+
"ACCESS_RECORD_STRUCTURED_INTERACTION_ID",
13+
]

gateway-api/src/gateway_api/get_structured_record/request.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,26 @@
55
from fhir.operation_outcome import OperationOutcomeIssue
66
from flask.wrappers import Request, Response
77

8-
from gateway_api.common.common import FlaskResponse
8+
from gateway_api.common.common import (
9+
FlaskResponse,
10+
)
911

1012
if TYPE_CHECKING:
1113
from fhir.bundle import Bundle
1214

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

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

1725

1826
class GetStructuredRecordRequest:
19-
INTERACTION_ID: str = "urn:nhs:names:services:gpconnect:gpc.getstructuredrecord-1"
27+
INTERACTION_ID: str = ACCESS_RECORD_STRUCTURED_INTERACTION_ID
2028
RESOURCE: str = "patient"
2129
FHIR_OPERATION: str = "$gpc.getstructuredrecord"
2230

gateway-api/src/gateway_api/provider_request.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,10 @@
2626
from urllib.parse import urljoin
2727

2828
from requests import HTTPError, Response, post
29-
from stubs.stub_provider import stub_post
29+
from stubs.stub_provider import GpProviderStub
30+
31+
from gateway_api.get_structured_record import ACCESS_RECORD_STRUCTURED_INTERACTION_ID
3032

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

4847

4948
class ExternalServiceError(Exception):
@@ -94,7 +93,7 @@ def _build_headers(self, trace_id: str) -> dict[str, str]:
9493
return {
9594
"Content-Type": "application/fhir+json",
9695
"Accept": "application/fhir+json",
97-
"Ssp-InteractionID": ARS_INTERACTION_ID,
96+
"Ssp-InteractionID": ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
9897
"Ssp-To": self.provider_asid,
9998
"Ssp-From": self.consumer_asid,
10099
"Ssp-TraceID": trace_id,

0 commit comments

Comments
 (0)