Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4799bc8
[CDAPI-55]: Initial changes to swap lambda to expect test result bundle
nhsd-jack-wainwright Jan 7, 2026
abfcb0c
[CDAPI-55]: Ensured that Resources are correctly deserialized based o…
nhsd-jack-wainwright Jan 9, 2026
9af12f1
[CDAPI-55]: Updated lambda to accept Bundle within curl request
nhsd-jack-wainwright Jan 12, 2026
56ae68c
[CDAPI-55]: Updated Bundle resource to expect 'entry' json field
nhsd-jack-wainwright Jan 13, 2026
e631b3a
[CDAPI-55]: Added additional unit tests for new FHIR elements
nhsd-jack-wainwright Jan 13, 2026
0574e56
[CDAPI-55]: Added unit tests for resources.py
nhsd-jack-wainwright Jan 14, 2026
98a53db
[CDAPI-55]: Added validation to Identifier to check that the provided…
nhsd-jack-wainwright Jan 15, 2026
9646101
[CDAPI-55]: Ensured that returned Bundle includes a last_updated field.
nhsd-jack-wainwright Jan 15, 2026
a0eda68
[CDAPI-55]: Updated integration tests to account for new endpoint def…
nhsd-jack-wainwright Jan 15, 2026
3fd45a9
[CDAPI-55]: Added new API gateway mock for accessing lambda locally.
nhsd-jack-wainwright Jan 16, 2026
d2815e7
[CDAPI-55]: Updated integration tests to account for them accessing t…
nhsd-jack-wainwright Jan 16, 2026
72bdda2
[CDAPI-55]: Updated acceptance tests to account for new Bundle endpoint
nhsd-jack-wainwright Jan 16, 2026
e9c2ebe
[CDAPI-55]: Fixed schema tests
nhsd-jack-wainwright Jan 19, 2026
6069648
[CDAPI-55]: Swapped default health path for local lambda action to po…
nhsd-jack-wainwright Jan 20, 2026
fa686d9
[CDAPI-55]: Updated start-local-lambda action to run command in-line …
nhsd-jack-wainwright Jan 20, 2026
bee57ee
[CDAPI-55]: Ensured lambda container is built for x86 architecture
nhsd-jack-wainwright Jan 20, 2026
2d0b9a1
[CDAPI-55]: Minor sonar fixes
nhsd-jack-wainwright Jan 20, 2026
e6cc96c
[CDAPI-55]: Updated preview-env action to build lambda via the `build…
nhsd-jack-wainwright Jan 20, 2026
e057035
[CDAPI-55]: Added project setup to preview-env workflow
nhsd-jack-wainwright Jan 20, 2026
8e64d2f
[CDAPI-55]: Minor fix to preview-env workflow environment variables
nhsd-jack-wainwright Jan 20, 2026
ec0372c
[CDAPI-55]: Updated preview-env workflow to pick up Python artifact f…
nhsd-jack-wainwright Jan 20, 2026
dc48513
[CDAPI-55]: Updated preview environment workflow to reference correct…
nhsd-jack-wainwright Jan 20, 2026
9cb9e00
[CDAPI-55]: Swapped pathology lambda to utilise APIGatewayRestResolver
nhsd-jack-wainwright Jan 22, 2026
6cdff91
[CDAPI-55]: Swapped to APIGatewayHttpResolver
nhsd-jack-wainwright Jan 22, 2026
b7bd8b2
[CDAPI-55]: Fixed local API gateway mock to send requests in expected…
nhsd-jack-wainwright Jan 22, 2026
0e77237
[CDAPI-55]: Minor tidying around resourceType validation
nhsd-jack-wainwright Jan 23, 2026
3fe0674
[CDAPI-55]: Minor peer review fixes
nhsd-jack-wainwright Jan 23, 2026
974943d
[CDAPI-55]: Minor fix to ensure expected system is retrievable as a p…
nhsd-jack-wainwright Jan 23, 2026
d69a3e7
[CDAPI-55]: Moved logging configuration into root module configuration
nhsd-jack-wainwright Jan 27, 2026
275deaf
[CDAPI-55]: Added unused argument checking to Ruff config
nhsd-jack-wainwright Jan 27, 2026
b7fcfcf
[CDAPI-55]: Removed unused logging.config import from lambda_handler.py
nhsd-jack-wainwright Jan 28, 2026
7ccad15
[CDAPI-55]: Improved docstring for Resource.create method
nhsd-jack-wainwright Jan 28, 2026
0108275
[CDAPI-55]: Minor peer review comments
nhsd-jack-wainwright Jan 30, 2026
28fc00e
[CDAPI-55]: Renamed test name following peer review comment
nhsd-jack-wainwright Jan 30, 2026
39b5886
[CDAPI-55]: Minor sonar fixes
nhsd-jack-wainwright Jan 30, 2026
3a30646
[CDAPI-55]: Further minor sonar fixes
nhsd-jack-wainwright Jan 30, 2026
7210bcc
[CDAPI-55]: Ensure NOSONAR comment formatting is correct
nhsd-jack-wainwright Jan 30, 2026
741bceb
[CDAPI-55]: Moved NOSONAR comment to first line of route declaration
nhsd-jack-wainwright Jan 30, 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
27 changes: 1 addition & 26 deletions .github/actions/start-local-lambda/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ inputs:
description: "Command to start local Lambda"
required: false
default: "make deploy"
health-path:
description: "Health probe path to POST"
required: false
default: "/2015-03-31/functions/function/invocations"
max-seconds:
description: "Maximum seconds to wait for readiness"
required: false
Expand All @@ -26,25 +22,4 @@ runs:
run: |
set -euo pipefail
echo "Starting local Lambda: '${{ inputs.deploy-command }}'"
nohup ${{ inputs.deploy-command }} >/tmp/lambda.log 2>&1 &
echo $! > /tmp/lambda.pid
echo "PID: $(cat /tmp/lambda.pid)"
- name: "Wait for Lambda to be ready"
shell: bash
run: |
set -euo pipefail
BASE_URL="${BASE_URL:-http://localhost:5001}"
HEALTH_URL="${BASE_URL}${{ inputs.health-path }}"
MAX="${{ inputs.max-seconds }}"
echo "Waiting for Lambda at ${HEALTH_URL} (max ${MAX}s)..."
for i in $(seq 1 "${MAX}"); do
if curl -sSf -X POST "${HEALTH_URL}" -d '{}' >/dev/null; then
echo "Lambda is ready"
exit 0
fi
sleep 1
done
echo "Lambda did not become ready in time"
echo "---- recent lambda log ----"
tail -n 200 /tmp/lambda.log || true
exit 1
bash -c "${{ inputs.deploy-command }}"
16 changes: 10 additions & 6 deletions .github/workflows/preview-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ permissions:
env:
AWS_REGION: eu-west-2
PREVIEW_PREFIX: pr-
PYTHON_VERSION: 3.14
LAMBDA_RUNTIME: python3.14
LAMBDA_HANDLER: handler.handler
LAMBDA_HANDLER: lambda_handler.handler

jobs:
pr-preview:
Expand All @@ -34,13 +35,16 @@ jobs:
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
with:
python-version: "3.14"
python-version: "${{ env.PYTHON_VERSION }}"

- name: "Setup Python project"
uses: ./.github/actions/setup-python-project
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Package artifact
run: |
cd infrastructure/environments/preview
rm -f artifact.zip
zip -r artifact.zip .
make build

- name: Select AWS role inputs
id: role-select
Expand Down Expand Up @@ -86,7 +90,7 @@ jobs:
- name: Create or update preview Lambda (on open/sync/reopen)
if: github.event.action != 'closed'
run: |
cd infrastructure/environments/preview
cd pathology-api/target/
FN="${{ steps.names.outputs.function_name }}"
echo "Deploying preview function: $FN"
wait_for_lambda_ready() {
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stage-2-test.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: "Test stage"

env:
BASE_URL: "http://localhost:5001"
BASE_URL: "http://localhost:5002"
HOST: "localhost"

on:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ pathology-api/test-artefacts/
**/.env

**/.DS_Store
**/.coverage
60 changes: 40 additions & 20 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ docker := doas docker
else
docker := docker
endif

dockerNetwork := pathology-local

# ==============================================================================

# Example CI/CD targets are: dependencies, build, publish, deploy, clean, etc.
Expand All @@ -17,49 +20,66 @@ endif
dependencies: # Install dependencies needed to build and test the project @Pipeline
cd pathology-api && poetry sync

.PHONY: build-pathology-api
build-pathology-api: dependencies
.PHONY: build
build: clean-artifacts dependencies
@cd pathology-api
@echo "Running type checks..."
@rm -rf target && rm -rf dist
@poetry run mypy --no-namespace-packages .
@echo "Packaging dependencies..."
@poetry build --format=wheel
@pip install "dist/pathology_api-0.1.0-py3-none-any.whl" --target "./target/pathology-api"
# Copy main file separately as it is not included within the package.
VERSION=$$(poetry version -s)
@pip install "dist/pathology_api-$$VERSION-py3-none-any.whl" --target "./target/pathology-api" --platform manylinux2014_x86_64 --only-binary=:all:
# Copy lambda_handler file separately as it is not included within the package.
@cp lambda_handler.py ./target/pathology-api/
@rm -rf ../infrastructure/images/pathology-api/resources/build/
@mkdir ../infrastructure/images/pathology-api/resources/build/
@cp -r ./target/pathology-api ../infrastructure/images/pathology-api/resources/build/
# Remove temporary build artefacts once build has completed
@rm -rf target && rm -rf dist
@cd ./target/pathology-api
@zip -r "../artifact.zip" .

.PHONY: build-images
build-images: build # Build the project artefact @Pipeline
@mkdir infrastructure/images/pathology-api/resources/build/
@cp pathology-api/target/artifact.zip infrastructure/images/pathology-api/resources/build/
@mkdir infrastructure/images/pathology-api/resources/build/pathology-api
@unzip infrastructure/images/pathology-api/resources/build/artifact.zip -d infrastructure/images/pathology-api/resources/build/pathology-api

.PHONY: build
build: build-pathology-api # Build the project artefact @Pipeline
@echo "Building Docker image using Docker. Utilising python version: ${PYTHON_VERSION} ..."
@$(docker) buildx build --load --provenance=false --build-arg PYTHON_VERSION=${PYTHON_VERSION} -t localhost/pathology-api-image infrastructure/images/pathology-api
@$(docker) buildx build --load --platform=linux/amd64 --provenance=false --build-arg PYTHON_VERSION=${PYTHON_VERSION} -t localhost/pathology-api-image infrastructure/images/pathology-api
@echo "Docker image 'pathology-api-image' built successfully!"

@echo "Building api-gateway-mock using Docker. Utilising python version: ${PYTHON_VERSION} ..."
@$(docker) buildx build --load --build-arg PYTHON_VERSION=${PYTHON_VERSION} -t localhost/api-gateway-mock-image infrastructure/images/api-gateway-mock
@echo "Docker image 'api-gateway-mock-image' built successfully!"

publish: # Publish the project artefact @Pipeline
# TODO: Implement the artefact publishing step

deploy: clean build # Deploy the project artefact to the target environment @Pipeline
@if [[ -n "$${IN_BUILD_CONTAINER}" ]]; then \
echo "Starting using local docker network ..." ; \
$(docker) run --name pathology-api -p 5001:8080 --network pathology-local -d localhost/pathology-api-image ; \
else \
$(docker) run --name pathology-api -p 5001:8080 -d localhost/pathology-api-image ; \
fi
deploy: clean-docker build-images # Deploy the project artefact to the target environment @Pipeline
$(docker) network create $(dockerNetwork) || echo "Docker network '$(dockerNetwork)' already exists."
$(docker) run --platform linux/amd64 --name pathology-api -p 5001:8080 --network $(dockerNetwork) -d localhost/pathology-api-image
$(docker) run --name api-gateway-mock -p 5002:5000 --network $(dockerNetwork) -d localhost/api-gateway-mock-image

clean-artifacts:
@echo "Removing build artefacts..."
@rm -rf infrastructure/images/pathology-api/resources/build/
@rm -rf pathology-api/target && rm -rf pathology-api/dist

clean:: stop # Clean-up project resources (main) @Operations
clean-docker: stop
@echo "Removing pathology API container..."
@$(docker) rm pathology-api || echo "No pathology API container currently exists."

@echo "Removing api-gateway-mock container..."
@$(docker) rm api-gateway-mock || echo "No api-gateway-mock container currently exists."

clean:: clean-artifacts clean-docker # Clean-up project resources (main) @Operations

.PHONY: stop
stop:
@echo "Stopping pathology API container..."
@$(docker) stop pathology-api || echo "No pathology API container currently running."

@echo "Stopping api-gateway-mock container..."
@$(docker) stop api-gateway-mock || echo "No api-gateway-mock container currently running."

config:: # Configure development environment (main) @Configuration
# Configure poetry to trust dev certificate if specified
@if [[ -n "$${DEV_CERTS_INCLUDED}" ]]; then \
Expand Down
28 changes: 0 additions & 28 deletions infrastructure/environments/preview/handler.py

This file was deleted.

16 changes: 16 additions & 0 deletions infrastructure/images/api-gateway-mock/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Retrieve the python version from build arguments, deliberately set to "invalid" by default to highlight when no version is provided when building the container.
ARG PYTHON_VERSION=invalid
# Use the specified python version to retrieve the required base lambda image.
ARG url=python:${PYTHON_VERSION}-alpine3.23
FROM $url

COPY resources/ /resources
WORKDIR /resources

RUN pip install --no-cache-dir -r requirements.txt \
&& addgroup -S nonroot \
&& adduser -S flask -G nonroot

USER flask

ENTRYPOINT ["flask", "--app", "server", "run", "--host=0.0.0.0"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
flask==3.1.2
flask-cors==6.0.2
requests==2.32.5
79 changes: 79 additions & 0 deletions infrastructure/images/api-gateway-mock/resources/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from logging.config import dictConfig

import requests
from flask import Flask, request
from flask_cors import CORS

# Very simple logging configuration taken from https://flask.palletsprojects.com/en/stable/logging/
dictConfig(
{
"version": 1,
"formatters": {
"default": {
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
},
},
"handlers": {
"wsgi": {
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
"formatter": "default",
}
},
"root": {"level": "INFO", "handlers": ["wsgi"]},
}
)

app = Flask(__name__) # NOSONAR python:S4502
cors = CORS(app, resources={r"/*": {"origins": "http://localhost:5002"}})


@app.route( # NOSONAR python:S3752
"/",
methods=["POST", "GET"],
defaults={"path_params": None},
)
@app.route("/<path:path_params>", methods=["POST", "GET"])
def forward_request(path_params):
app.logger.info("received request with data: %s", request.get_data(as_text=True))

response = requests.post(
"http://pathology-api:8080/2015-03-31" # NOSONAR python:S5332
"/functions/function/invocations",
json={
"body": request.get_data(as_text=True).replace("\n", "").replace(" ", ""),
"requestContext": {
"http": {
"path": f"/{path_params}",
"method": request.method,
},
"requestId": "request-id",
"stage": "$default",
},
"httpMethod": request.method,
"rawPath": f"/{path_params}",
"rawQueryString": "",
"pathParameters": {"proxy": path_params},
},
headers={"Content-Type": "application/json"},
timeout=120,
)

app.logger.info(
"response: status_code=%s, body=%s", response.status_code, response.text
)

app.logger.info("response: %s", response.text)
response_data = response.json()

output = (
(
response_data["body"],
response_data["statusCode"],
response_data["headers"],
)
if "body" in response_data
else (response_data, 500, {"Content-Type": "text/plain"})
)

return output
Loading
Loading