⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
279ad70
[CDAPI-95]: Added Initial Composition Resource
nhsd-jack-wainwright Jan 28, 2026
6bc114f
[CDAPI-95]: Added initial exception handlers to lambda_handler
nhsd-jack-wainwright Jan 29, 2026
f177416
[CDAPI-95]: Swapped logging implementation to use logger provided by …
nhsd-jack-wainwright Jan 29, 2026
0eb5abb
[CDAPI-95]: Added additional resource classes to FHIR R4 module
nhsd-jack-wainwright Jan 29, 2026
b7f11ec
[CDAPI-95]: Added OperationOutcome resource and exception handling fo…
nhsd-jack-wainwright Jan 30, 2026
8b373a7
[CDAPI-95]: Renamed _ensure_bundle_composition to _validate_composition
nhsd-jack-wainwright Feb 2, 2026
143361e
[CDAPI-95]: Updated resource unit tests
nhsd-jack-wainwright Feb 2, 2026
7158c04
[CDAPI-95]: Updated elements unit tests
nhsd-jack-wainwright Feb 2, 2026
7e2efac
[CDAPI-95]: Updated final unit tests to account for handler and excep…
nhsd-jack-wainwright Feb 3, 2026
9817f23
[CDAPI-95]: Allowed for additional unparsed fields to be included wit…
nhsd-jack-wainwright Feb 4, 2026
967f311
[CDAPI-95]: Updated Integration Tests to account for OperationOutcome…
nhsd-jack-wainwright Feb 4, 2026
4c772d3
[CDAPI-95]: Updated contract tests
nhsd-jack-wainwright Feb 4, 2026
3ac04f9
[CDAPI-95]: Updated Acceptance tests
nhsd-jack-wainwright Feb 4, 2026
dc17f45
[CDAPI-95]: Added additional integration tests
nhsd-jack-wainwright Feb 6, 2026
fed8735
[CDAPI-95]: Added Schemathesis hooks to fix schema tests
nhsd-jack-wainwright Feb 6, 2026
25530c3
[CDAPI-95]: Swapped OAS file back to version 3.0.3 and added initial …
nhsd-jack-wainwright Feb 9, 2026
4489779
[CDAPI-95]: Moved security schemes into main components section of Op…
nhsd-jack-wainwright Feb 9, 2026
3832035
[CDAPI-95]: Minor tidy up before submitting for review
nhsd-jack-wainwright Feb 9, 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
146 changes: 106 additions & 40 deletions pathology-api/lambda_handler.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,111 @@
import json
import logging
from collections.abc import Callable
from functools import reduce
from json import JSONDecodeError
from typing import Any

import pydantic
from aws_lambda_powertools.event_handler import (
APIGatewayHttpResolver,
Response,
)
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
from aws_lambda_powertools.utilities.typing import LambdaContext
from pathology_api.fhir.r4.resources import Bundle
from pathology_api.exception import ValidationError
from pathology_api.fhir.r4.resources import Bundle, OperationOutcome
from pathology_api.handler import handle_request
from pydantic import ValidationError
from pathology_api.logging import get_logger

_INVALID_PAYLOAD_MESSAGE = "Invalid payload provided."

_logger = logging.getLogger(__name__)
_logger = get_logger(__name__)

app = APIGatewayHttpResolver()

type _ExceptionHandler[T: Exception] = Callable[[T], Response[str]]


def _exception_handler[T: Exception](
exception_type: type[T],
) -> Callable[[_ExceptionHandler[T]], _ExceptionHandler[T]]:
"""
Exception handler decorator that registers a function as an exception handler with
the created app whilst maintaining type information.
"""

def decorator(func: _ExceptionHandler[T]) -> _ExceptionHandler[T]:
def wrapper(exception: T) -> Response[str]:
return func(exception)

app.exception_handler(exception_type)(wrapper)
return wrapper

def _with_default_headers(status_code: int, body: str) -> Response[str]:
content_type = "application/fhir+json" if status_code == 200 else "text/plain"
return decorator


def _with_default_headers(status_code: int, body: pydantic.BaseModel) -> Response[str]:
return Response(
status_code=status_code,
headers={"Content-Type": content_type},
body=body,
headers={"Content-Type": "application/fhir+json"},
body=body.model_dump_json(by_alias=True, exclude_none=True),
)


@_exception_handler(ValidationError)
def handle_validation_error(exception: ValidationError) -> Response[str]:
# LOG014: False positive, we are within an exception handler here.
_logger.info(
"ValidationError encountered: %s",
exception,
exc_info=True, # noqa: LOG014
)
return _with_default_headers(
status_code=400,
body=OperationOutcome.create_validation_error(exception.message),
)


@_exception_handler(pydantic.ValidationError)
def handle_pydantic_validation_error(
exception: pydantic.ValidationError,
) -> Response[str]:
# LOG014: False positive, we are within an exception handler here.
_logger.info(
"Pydantic ValidationError encountered: %s",
exception,
exc_info=True, # noqa: LOG014
)

operation_outcome = OperationOutcome.create_validation_error(
reduce(
lambda acc, e: acc + f"{str(e['loc'])} - {e['msg']} \n",
exception.errors(),
"",
)
)
return _with_default_headers(
status_code=400,
body=operation_outcome,
)


@app.not_found
def handle_not_found_error(exception: NotFoundError) -> Response[str]:
_logger.info("NotFoundError encountered: %s", exception, exec_info=True)
return _with_default_headers(
status_code=404,
body=OperationOutcome.create_not_found_error(
"No resource found for requested path. "
f"Path: ({app.current_event.http_method}) {app.current_event.path}"
),
)


@_exception_handler(Exception)
def handle_exception(exception: Exception) -> Response[str]:
_logger.exception("Unhandled Exception encountered: %s", exception)
return _with_default_headers(
status_code=500,
body=OperationOutcome.create_server_error(
"An unexpected error has occurred. Please try again later."
),
)


Expand All @@ -37,42 +118,27 @@ def status() -> Response[str]:
@app.post("/FHIR/R4/Bundle")
def post_result() -> Response[str]:
_logger.debug("Post result endpoint called.")

try:
payload = app.current_event.json_body
except json.JSONDecodeError as err:
_logger.error("Error decoding JSON payload. error: %s", err)
return _with_default_headers(status_code=400, body=_INVALID_PAYLOAD_MESSAGE)
_logger.debug("Payload received: %s", payload)
except JSONDecodeError as e:
raise ValidationError("Invalid payload provided.") from e

if not payload:
_logger.error("No payload provided.")
return _with_default_headers(status_code=400, body="No payload provided.")
_logger.debug("Payload received: %s", payload)

try:
bundle = Bundle.model_validate(payload, by_alias=True)
except ValidationError as err:
_logger.error(
"Error parsing payload. error: %s issues: %s",
err,
reduce(lambda acc, e: acc + "," + str(e), err.errors(), ""),
if payload is None:
raise ValidationError(
"Resources must be provided as a bundle of type 'document'"
)
return _with_default_headers(status_code=400, body=_INVALID_PAYLOAD_MESSAGE)
except TypeError as err:
_logger.error("Error parsing payload. error: %s", err)
return _with_default_headers(status_code=400, body=_INVALID_PAYLOAD_MESSAGE)

try:
response = handle_request(bundle)
bundle = Bundle.model_validate(payload, by_alias=True)

return _with_default_headers(
status_code=200,
body=response.model_dump_json(by_alias=True, exclude_none=True),
)
except ValueError as err:
_logger.error("Error processing payload. error: %s", err)
return _with_default_headers(
status_code=400, body="Error processing provided bundle."
)
response = handle_request(bundle)

return _with_default_headers(
status_code=200,
body=response,
)


def handler(data: dict[str, Any], context: LambdaContext) -> dict[str, Any]:
Expand Down
Loading
Loading