Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c4ca999
[PRMP-1079] test out paginator for with dynamodb client
steph-torres-nhs Dec 15, 2025
5fb5ec6
[PRMP-1079] key needs to be a string for keycondition
steph-torres-nhs Dec 16, 2025
842d728
[PRMP-1079] change key expression format
steph-torres-nhs Dec 16, 2025
9d3ad51
[PRMP-1079] use build full result
steph-torres-nhs Dec 16, 2025
14be2df
[PRMP-1079] serialize condition attribute values
steph-torres-nhs Dec 16, 2025
dd2a861
[PRMP-1079] add dynamo_utils tests
steph-torres-nhs Dec 16, 2025
38e48fd
[PRMP-1079] add negative test for dynamo util
steph-torres-nhs Dec 17, 2025
1b4433f
[PRMP-1079] amend service tests to call query docs pending review wit…
steph-torres-nhs Dec 17, 2025
20cb592
[PRMP-1079] add paginator query filter test
steph-torres-nhs Dec 17, 2025
5ac066e
[PRMP-1079] format
steph-torres-nhs Dec 17, 2025
be2a8f2
[PRMP-1079] add test for querying with paginator
steph-torres-nhs Dec 17, 2025
bad0827
[PRMP-1079] extract query with paginator to dynamo service
steph-torres-nhs Dec 17, 2025
3caa1b0
[PRMP-1079] document service uses dynamo service to query with paginator
steph-torres-nhs Dec 17, 2025
c4df7cc
[PRMP-1079] format
steph-torres-nhs Dec 17, 2025
b6215ed
Merge branch 'main' into PRMP-1079
steph-torres-nhs Dec 17, 2025
bec65fa
[PRMP-1079] update method used to align with actual method definition
steph-torres-nhs Dec 18, 2025
fdc677c
[PRMP-1079] update type hint on argument
steph-torres-nhs Dec 18, 2025
d873389
[PRMP-1079] fix conflicts
steph-torres-nhs Dec 18, 2025
024fc72
[PRMP-1079] test refactoring
steph-torres-nhs Dec 19, 2025
1016a68
[PRMP-1079] remove unnecessary method
steph-torres-nhs Dec 19, 2025
4695061
[PRMP-1079] merge in main
steph-torres-nhs Dec 19, 2025
0fa9a8c
[PRMP-1079] address SonarCloud issue
steph-torres-nhs Dec 19, 2025
41cbcee
[PRMP-1079] move method into parent class
steph-torres-nhs Dec 19, 2025
a8fd436
[PRMP-1079] git merge main
steph-torres-nhs Dec 19, 2025
2c9e22e
Merge branch 'main' into PRMP-1079
steph-torres-nhs Dec 22, 2025
b187108
Merge branch 'main' into PRMP-1079
steph-torres-nhs Dec 24, 2025
c625f38
Merge branch 'main' into PRMP-1079
steph-torres-nhs Jan 2, 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
9 changes: 2 additions & 7 deletions lambdas/handlers/generate_document_manifest_handler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from boto3.dynamodb.types import TypeDeserializer
from enums.lambda_error import LambdaError
from enums.logging_app_interaction import LoggingAppInteraction
from models.zip_trace import DocumentManifestZipTrace
Expand All @@ -9,6 +8,7 @@
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
from utils.decorators.override_error_check import override_error_check
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.dynamo_utils import deserialize_dynamodb_object
from utils.lambda_exceptions import GenerateManifestZipException
from utils.lambda_response import ApiGatewayResponse
from utils.request_context import request_context
Expand Down Expand Up @@ -71,9 +71,4 @@ def manifest_zip_handler(zip_trace_item):


def prepare_zip_trace_data(new_zip_trace: dict) -> dict:
deserialize = TypeDeserializer().deserialize
parsed_dynamodb_items = {
key: deserialize(dynamodb_value)
for key, dynamodb_value in new_zip_trace.items()
}
return parsed_dynamodb_items
return deserialize_dynamodb_object(new_zip_trace)
57 changes: 57 additions & 0 deletions lambdas/services/base/dynamo_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
create_expression_attribute_values,
create_expressions,
create_update_expression,
deserialize_dynamodb_object,
serialize_dict_to_dynamodb_object,
)
from utils.exceptions import DynamoServiceException

Expand All @@ -29,6 +31,7 @@ def __new__(cls):
def __init__(self):
if not self.initialised:
self.dynamodb = boto3.resource("dynamodb", region_name="eu-west-2")
self.client = boto3.client("dynamodb")
self.initialised = True

def get_table(self, table_name: str):
Expand Down Expand Up @@ -429,3 +432,57 @@ def build_update_transaction_item(
},
}
}

def query_table_with_paginator(
self,
table_name: str,
index_name: str,
key: str,
condition: str,
filter_expression: str | None = None,
expression_attribute_names: str | None = None,
expression_attribute_values: dict | None = None,
limit: int = 20,
page_size: int = 1,
start_key: str | None = None,
) -> dict:

try:
query_params = {
"TableName": table_name,
"IndexName": index_name,
"KeyConditionExpression": f"{key}=:i",
"PaginationConfig": {
"MaxItems": limit,
"PageSize": page_size,
"StartingToken": start_key,
},
}

if expression_attribute_values is None:
expression_attribute_values = {}

expression_attribute_values[":i"] = condition

if filter_expression:
query_params["FilterExpression"] = filter_expression

if expression_attribute_names:
query_params["ExpressionAttributeNames"] = expression_attribute_names

if expression_attribute_values:
query_params["ExpressionAttributeValues"] = (
serialize_dict_to_dynamodb_object(expression_attribute_values)
)

paginator = self.client.get_paginator("query")
response = paginator.paginate(**query_params).build_full_result()

response["Items"] = [
deserialize_dynamodb_object(item) for item in response["Items"]
]
return response

except Exception as e:
logger.error("Failed to query DynamoDB")
raise e
40 changes: 40 additions & 0 deletions lambdas/services/document_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,43 @@ def create_dynamo_entry(
except (ValidationError, ClientError) as e:
logger.error(e)
raise e

def query_table_with_paginator(
self,
index_name: str,
search_key: str,
search_condition: str,
table_name: str | None = None,
filter_expression: str | None = None,
expression_attribute_names: dict | None = None,
expression_attribute_values: dict | None = None,
limit: int | None = None,
page_size: int = 1,
start_key: str | None = None,
model_class: BaseModel | None = None,
) -> tuple[list[BaseModel], str | None]:

try:
table_name = table_name or self.table_name
model_class = model_class or self.model_class

response = self.dynamo_service.query_table_with_paginator(
table_name=table_name,
index_name=index_name,
key=search_key,
condition=search_condition,
filter_expression=filter_expression,
expression_attribute_names=expression_attribute_names,
expression_attribute_values=expression_attribute_values,
limit=limit,
page_size=page_size,
start_key=start_key
)

references=[model_class.model_validate(item) for item in response["Items"]]

return references, response.get("NextToken")

except (ValidationError, ClientError) as e:
logger.error(e)
raise e
77 changes: 51 additions & 26 deletions lambdas/services/document_upload_review_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
from utils.audit_logging_setup import LoggingService
from utils.aws_transient_error_check import is_transient_error
from utils.dynamo_query_filter_builder import DynamoQueryFilterBuilder
from utils.dynamo_utils import build_transaction_item
from utils.dynamo_utils import (
build_mixed_condition_expression,
build_transaction_item,
)
from utils.exceptions import DocumentReviewException

logger = LoggingService(__name__)
Expand Down Expand Up @@ -40,40 +43,32 @@ def model_class(self) -> type:
def s3_bucket(self) -> str:
return self._s3_bucket

def query_docs_pending_review_by_custodian_with_limit(
def query_docs_pending_review_with_paginator(
self,
ods_code: str,
limit: int = DEFAULT_QUERY_LIMIT,
start_key: dict | None = None,
start_key: str | None = None,
nhs_number: str | None = None,
uploader: str | None = None,
) -> tuple[list[DocumentUploadReviewReference], dict | None]:
) -> tuple[list[DocumentUploadReviewReference], str | None]:

logger.info(f"Getting review document references for custodian: {ods_code}")

filter_expression = self.build_review_dynamo_filter(
nhs_number=nhs_number, uploader=uploader
filter_expression, condition_attribute_names, condition_attribute_values = (
self.build_paginator_query_filter(nhs_number=nhs_number, uploader=uploader)
)
references, last_evaluated_key = self.query_table_with_paginator(
index_name="CustodianIndex",
search_key="Custodian",
search_condition=ods_code,
filter_expression=filter_expression,
expression_attribute_names=condition_attribute_names,
expression_attribute_values=condition_attribute_values,
limit=limit,
start_key=start_key,
)

try:
response = self.dynamo_service.query_table_single(
table_name=self.table_name,
search_key="Custodian",
search_condition=ods_code,
index_name="CustodianIndex",
limit=limit,
start_key=start_key,
query_filter=filter_expression,
)

references = self._validate_review_references(response["Items"])

last_evaluated_key = response.get("LastEvaluatedKey", None)

return references, last_evaluated_key

except ClientError as e:
logger.error(e)
raise DocumentReviewException("Error querying document review references")
return references, last_evaluated_key

def _validate_review_references(
self, items: list[dict]
Expand All @@ -88,6 +83,36 @@ def _validate_review_references(
logger.error(e)
raise DocumentReviewException("Error validating document review references")

def build_paginator_query_filter(
self, nhs_number: str | None = None, uploader: str | None = None
):
conditions = [
{
"field": "ReviewStatus",
"operator": "=",
"value": DocumentReviewStatus.PENDING_REVIEW.value,
}
]
if nhs_number:
conditions.append(
{
"field": "NhsNumber",
"operator": "=",
"value": nhs_number,
}
)

if uploader:
conditions.append(
{
"field": "Author",
"operator": "=",
"value": uploader,
}
)

return build_mixed_condition_expression(conditions)

def get_document(
self, document_id: str, version: int | None
) -> DocumentUploadReviewReference | None:
Expand Down
36 changes: 5 additions & 31 deletions lambdas/services/search_document_review_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import base64
import decimal
import json

from enums.lambda_error import LambdaError
from pydantic import ValidationError
from services.document_upload_review_service import DocumentUploadReviewService
Expand All @@ -20,14 +16,13 @@ def process_request(
self, ods_code: str, params: dict
) -> tuple[list[str], str | None]:
try:

decoded_start_key = self.decode_start_key(params.get("nextPageToken", None))
start_key = params.get("nextPageToken", None)

str_limit = params.get("limit", self.document_service.DEFAULT_QUERY_LIMIT)
limit = int(str_limit)

references, last_evaluated_key = self.get_review_document_references(
start_key=decoded_start_key,
start_key=start_key,
ods_code=ods_code,
limit=limit,
nhs_number=params.get("nhsNumber", None),
Expand All @@ -50,9 +45,7 @@ def process_request(
for reference in references
]

encoded_exclusive_start_key = self.encode_start_key(last_evaluated_key)

return output_refs, encoded_exclusive_start_key
return output_refs, last_evaluated_key

except ValidationError as e:
logger.error(e)
Expand All @@ -69,33 +62,14 @@ def get_review_document_references(
self,
ods_code: str,
limit: int | None = None,
start_key: dict | None = None,
start_key: str | None = None,
nhs_number: str | None = None,
uploader: str | None = None,
):
return self.document_service.query_docs_pending_review_by_custodian_with_limit(
return self.document_service.query_docs_pending_review_with_paginator(
ods_code=ods_code,
limit=limit,
start_key=start_key,
nhs_number=nhs_number,
uploader=uploader,
)

def decode_start_key(self, encoded_start_key: str | None) -> dict[str, str] | None:
return (
json.loads(
base64.b64decode(encoded_start_key.encode("ascii")).decode("utf-8")
)
if encoded_start_key
else None
)

def encode_start_key(self, start_key: dict) -> str | None:
if start_key:
for key, value in start_key.items():
if isinstance(value, decimal.Decimal):
start_key[key] = int(value)
return base64.b64encode(json.dumps(start_key).encode("ascii")).decode(
"utf-8"
)
return None
Loading
Loading