Skip to content

Commit e084351

Browse files
Merge branch 'main' into dependabot/github_actions/nhs-england-tools/notify-msteams-action-1.0.5
2 parents c118984 + b873ef2 commit e084351

26 files changed

Lines changed: 915 additions & 58 deletions

File tree

.github/README.md

Whitespace-only changes.

.github/agents/docs.agent.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
name: Documentation Writer Agent
3+
description: Expert technical writer for this project
4+
---
5+
6+
# Documentation Writer Agent
7+
8+
You are an expert technical writer for this project.
9+
10+
## Your role
11+
12+
- You are fluent in Markdown and can read and understand Python code, the Flask framework, OpenAPI, pytest, Pact, and Schemathesis,
13+
- You write for a software developer audience, focusing on clarity and practical examples
14+
- Your task: read all files, and generate or update documentation in `**/README.md` where you feel appropriate and necessary.
15+
16+
## Project knowledge
17+
18+
- **Tech Stack:** Flask, Python, OpenAPI, pytest, Pact, Schemathesis
19+
- **File Structure:**
20+
- Files and folders that require documentation (you READ from here)
21+
- `gateway-api` - Code relating to the project
22+
- `gateway-api/src/` – All source code
23+
- `gateway-api/stubs` – API stubs and mock definitions used for testing or examples
24+
- `gateway-api/tests` – Automated tests for the gateway API
25+
- `infrastructure/` – All infrastructure code (e.g. Terraform, Dockerfiles, CI/CD pipelines)
26+
- `proxygen` - Code relating to the deployment of the API proxy
27+
- `**/README.md` – All documentation (you WRITE to here)
28+
29+
## Documentation practices
30+
31+
Be concise and specific.
32+
Write so that a new developer to this codebase can understand your writing, don’t assume your audience are experts in the topic/area you are writing about.
33+
Use mermaid to create diagrams where helpful to explain complex concepts or workflows.
34+
Provide examples where helpful to clarify concepts or usage.
35+
36+
## Boundaries
37+
38+
-**Always do:** Amend or create `**/README.md` only
39+
- ⚠️ **Ask first:** Before modifying existing documents in a major way, or before creating new README files.
40+
- 🚫 **Never do:** Delete a file, nor create or modify any files other than `**/README.md`

.github/agents/unit_test.agent.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
name: Unit Test Writer Agent
3+
description: Expert unit test writer for this project
4+
---
5+
6+
# Unit Test Writer Agent
7+
8+
You are an expert unit test writer for this project.
9+
10+
## Your role
11+
12+
- You are fluent in Python, and can understand the Flask framework and pytest
13+
- You write unit tests to improve the stability and reliability of the codebase by ensuring that all code is exercised by unit tests
14+
- Your task: read all files in `gateway-api/` and generate or update unit tests in `gateway-api/src/**/test_*.py`
15+
16+
## Project knowledge
17+
18+
- **Tech Stack:** Flask, Python, pytest
19+
- **File Structure:**
20+
- `gateway-api/src/**/*.py` – Files and folders that require unit tests (you READ from here)
21+
- `gateway-api/src/**/test_*.py` – All unit tests (you WRITE to here)
22+
23+
## Unit test practices
24+
25+
Where possible, write unit tests that
26+
27+
- Are independent and can be run in isolation
28+
- Cover edge cases and error handling, not just the happy path
29+
- Are well-named to clearly indicate what they are testing and the expected outcome
30+
- Use `pytest` fixtures to set up any necessary test data or state, and to clean up after tests if needed
31+
- Pass a message to the assertion to provide additional context when a test fails, making it easier to diagnose issues
32+
33+
## Boundaries
34+
35+
-**Always do:** Create or amend `gateway-api/src/**/test_*.py` only
36+
- ⚠️ **Ask first:** Before modifying more than one test file in a single PR; and before deleting a file.
37+
- 🚫 **Never do:** Modify any files other than `gateway-api/src/**/test_*.py`
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# NHSE Clinical Data Gateway API
2+
3+
Our core programming language is Python.
4+
5+
Our docs are in README.md files next to or in the parent directories of the files they are documenting.
6+
7+
This repository is for handling HTTP requests from "Consumer systems" and forwarding them on to "Provider systems", while performing a number of checks on and additions to the request. The response from the "Provider system", which is sent straight back to the "Consumer system" unchanged, will contain a patient's medical details.
8+
9+
We use other NHSE services to assist in the validation and processing of the requests including PDS FHIR API for obtaining GP practice codes for the patient, SDS FHIR API for obtaining the "Provider system" details of that GP practice and Healthcare Worker FHIR API for obtaining details of the requesting practitioner using the "Consumer System" that will then be added to the forwarded request.
10+
11+
`make deploy` will build and start a container running Gateway API at `localhost:5000`.
12+
13+
After deploying the container locally, `make test` will run all tests and capture their coverage. Note: env variables control the use of stubs for the PDS FHIR API, SDS FHIR API, Healthcare Worker FHIR API and Provider system services.
14+
15+
Individual test suites can be run with:
16+
17+
- Unit tests: `make unit`
18+
- Acceptance tests: `make acceptance`
19+
- Integration tests: `make integration`
20+
- Schema tests: `make schema`
21+
- Contract tests: `make contract`
22+
23+
The container must be running in order to successfully run any of the test suites other than the unit tests.
24+
25+
The schema for this API can be found in `gateway-api/openapi.yaml`.
26+
27+
## Docstrings and comments
28+
29+
- Use precise variable and function names to reduce the need for comments
30+
- Use docstrings on high-level functions and classes to explain their purpose, inputs, outputs, and any side effects
31+
- Avoid comments that state the obvious or repeat what the code does; instead, focus on explaining the intent behind the code, the reasons for non-obvious decisions, and any important trade-offs or constraints
32+
33+
## Commits
34+
35+
Prepend `[AI-generated]` to the commit message when committing changes made by an AI agent.
36+
37+
## Security
38+
39+
This repository is public. Do not commit any secrets, tokens or credentials.

gateway-api/poetry.lock

Lines changed: 26 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gateway-api/pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ authors = [
66
{name = "Your Name", email = "you@example.com"}
77
]
88
readme = "README.md"
9-
requires-python = ">3.13,<4.0.0"
9+
requires-python = ">=3.14,<4.0.0"
1010

1111
[tool.poetry.dependencies]
1212
clinical-data-common = { git = "https://github.com/NHSDigital/clinical-data-common.git", tag = "v0.1.0" }
13-
flask = "^3.1.2"
13+
flask = "^3.1.3"
1414
types-flask = "^1.1.6"
1515
requests = "^2.32.5"
16+
pyjwt = "^2.11.0"
1617

1718
[tool.poetry]
1819
packages = [{include = "gateway_api", from = "src"},
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .device import Device
2+
from .jwt import JWT
3+
from .practitioner import Practitioner
4+
5+
__all__ = ["JWT", "Device", "Practitioner"]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass(frozen=True, kw_only=True)
5+
class Device:
6+
system: str
7+
value: str
8+
model: str
9+
version: str
10+
11+
@property
12+
def json(self) -> str:
13+
outstr = f"""
14+
{{
15+
"resourceType": "Device",
16+
"identifier": [
17+
{{
18+
"system": "{self.system}",
19+
"value": "{self.value}"
20+
}}
21+
],
22+
"model": "{self.model}",
23+
"version": "{self.version}"
24+
}}
25+
"""
26+
return outstr.strip()
27+
28+
def __str__(self) -> str:
29+
return self.json
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from dataclasses import dataclass, field
2+
from datetime import UTC, datetime
3+
from time import time
4+
from typing import Any
5+
6+
import jwt as pyjwt
7+
8+
9+
@dataclass(frozen=True, kw_only=True)
10+
class JWT:
11+
issuer: str
12+
subject: str
13+
audience: str
14+
requesting_device: str
15+
requesting_organization: str
16+
requesting_practitioner: str
17+
18+
# Time fields
19+
issued_at: int = field(default_factory=lambda: int(time()))
20+
expiration: int = field(default_factory=lambda: int(time()) + 300)
21+
22+
# These are here for future proofing but are not expected ever to be changed
23+
algorithm: str | None = None
24+
type: str = "JWT"
25+
reason_for_request: str = "directcare"
26+
requested_scope: str = "patient/*.read"
27+
28+
@property
29+
def issue_time(self) -> str:
30+
return datetime.fromtimestamp(self.issued_at, tz=UTC).isoformat()
31+
32+
@property
33+
def exp_time(self) -> str:
34+
return datetime.fromtimestamp(self.expiration, tz=UTC).isoformat()
35+
36+
def encode(self) -> str:
37+
return pyjwt.encode(
38+
self.payload(),
39+
key=None,
40+
algorithm=self.algorithm,
41+
headers={"typ": self.type},
42+
)
43+
44+
@staticmethod
45+
def decode(token: str) -> "JWT":
46+
token_dict = pyjwt.decode(
47+
token,
48+
options={"verify_signature": False}, # NOSONAR S5659 (not signed)
49+
)
50+
51+
return JWT(
52+
issuer=token_dict["iss"],
53+
subject=token_dict["sub"],
54+
audience=token_dict["aud"],
55+
expiration=token_dict["exp"],
56+
issued_at=token_dict["iat"],
57+
requesting_device=token_dict["requesting_device"],
58+
requesting_organization=token_dict["requesting_organization"],
59+
requesting_practitioner=token_dict["requesting_practitioner"],
60+
)
61+
62+
def payload(self) -> dict[str, Any]:
63+
return {
64+
"iss": self.issuer,
65+
"sub": self.subject,
66+
"aud": self.audience,
67+
"exp": self.expiration,
68+
"iat": self.issued_at,
69+
"requesting_device": self.requesting_device,
70+
"requesting_organization": self.requesting_organization,
71+
"requesting_practitioner": self.requesting_practitioner,
72+
"reason_for_request": self.reason_for_request,
73+
"requested_scope": self.requested_scope,
74+
}
75+
76+
def __str__(self) -> str:
77+
return self.encode()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass(kw_only=True)
5+
class Practitioner:
6+
id: str
7+
sds_userid: str
8+
role_profile_id: str
9+
userid_url: str
10+
userid_value: str
11+
family_name: str
12+
given_name: str | None = None
13+
prefix: str | None = None
14+
15+
def __post_init__(self) -> None:
16+
given = "" if self.given_name is None else f',"given":["{self.given_name}"]'
17+
prefix = "" if self.prefix is None else f',"prefix":["{self.prefix}"]'
18+
self._name_str = f'[{{"family": "{self.family_name}"{given}{prefix}}}]'
19+
20+
@property
21+
def json(self) -> str:
22+
user_id_system = "https://fhir.nhs.uk/Id/sds-user-id"
23+
role_id_system = "https://fhir.nhs.uk/Id/sds-role-profile-id"
24+
25+
outstr = f"""
26+
{{
27+
"resourceType": "Practitioner",
28+
"id": "{self.id}",
29+
"identifier": [
30+
{{
31+
"system": "{user_id_system}",
32+
"value": "{self.sds_userid}"
33+
}},
34+
{{
35+
"system": "{role_id_system}",
36+
"value": "{self.role_profile_id}"
37+
}},
38+
{{
39+
"system": "{self.userid_url}",
40+
"value": "{self.userid_value}"
41+
}}
42+
],
43+
"name": {self._name_str}
44+
}}
45+
"""
46+
return outstr.strip()
47+
48+
def __str__(self) -> str:
49+
return self.json

0 commit comments

Comments
 (0)