Skip to content

Commit fb9982d

Browse files
committed
paginate table query
1 parent a9e07ff commit fb9982d

24 files changed

+146
-207
lines changed

.github/workflows/sonarcloud.yml

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,6 @@ jobs:
2424
- name: Install poetry
2525
run: pip install poetry==2.1.4
2626

27-
- name: Debug PR Info
28-
if: github.event_name == 'pull_request'
29-
run: |
30-
echo "PR Number: ${{ github.event.pull_request.number }}"
31-
echo "PR Head SHA: ${{ github.event.pull_request.head.sha }}"
32-
echo "PR Base SHA: ${{ github.event.pull_request.base.sha }}"
33-
echo "Current SHA: ${{ github.sha }}"
34-
35-
36-
- name: Debug Trigger Info
37-
run: |
38-
echo "=== WORKFLOW TRIGGER DEBUG ==="
39-
echo "Event Name: ${{ github.event_name }}"
40-
echo "Current SHA: ${{ github.sha }}"
41-
42-
if [ "${{ github.event_name }}" = "pull_request" ]; then
43-
echo "=== PULL REQUEST DETAILS ==="
44-
echo "PR Number: ${{ github.event.pull_request.number }}"
45-
echo "PR Head SHA: ${{ github.event.pull_request.head.sha }}"
46-
echo "PR Base SHA: ${{ github.event.pull_request.base.sha }}"
47-
echo "PR Head Ref: ${{ github.event.pull_request.head.ref }}"
48-
echo "PR Base Ref: ${{ github.event.pull_request.base.ref }}"
49-
echo "✅ Using PR HEAD commit for analysis"
50-
elif [ "${{ github.event_name }}" = "push" ]; then
51-
echo "=== PUSH DETAILS ==="
52-
echo "Branch: ${{ github.ref_name }}"
53-
echo "Pushed SHA: ${{ github.sha }}"
54-
echo "Before SHA: ${{ github.event.before }}"
55-
echo "After SHA: ${{ github.event.after }}"
56-
echo "✅ Using pushed commit for analysis"
57-
else
58-
echo "=== OTHER EVENT ==="
59-
echo "Unknown event type: ${{ github.event_name }}"
60-
fi
61-
62-
63-
- name: Install poetry
64-
run: pip install poetry==2.1.2
65-
6627
- uses: actions/setup-python@v5
6728
with:
6829
python-version: 3.11

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,3 @@ openapi.json
3030

3131
devtools/volume/
3232
**/.coverage
33-
backend/src/1.json

.tool-versions

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
poetry 2.1.2
32
nodejs 23.11.0
43
python 3.11.12

backend/poetry.lock

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

backend/src/fhir_batch_repository.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import boto3
44
import time
55
import simplejson as json
6+
from clients import logger
67
from dataclasses import dataclass
78
import botocore.exceptions
89
from boto3.dynamodb.conditions import Key, Attr
@@ -34,7 +35,7 @@ def _query_identifier(table, index, pk, identifier, is_present):
3435
return queryresponse
3536

3637
if retries > 6:
37-
print(f"{identifier}: Crossed {retries} retries")
38+
logger.info(f"{identifier}: Crossed {retries} retries")
3839

3940
retries += 1
4041
# Delay time in milliseconds

backend/src/fhir_controller.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -392,29 +392,20 @@ def delete_immunization(self, aws_event):
392392
return self.create_response(403, unauthorized.to_operation_outcome())
393393

394394
def search_immunizations(self, aws_event: APIGatewayProxyEventV1) -> dict:
395-
logger.info("SAW: search_immunizations.")
396-
397395
if response := self.authorize_request(aws_event):
398396
return response
399-
logger.info("SAW: Authorised request")
397+
400398
try:
401-
logger.info("SAW: Processing search parameters")
402399
search_params = process_search_params(process_params(aws_event))
403400
except ParameterException as e:
404401
return self._create_bad_request(e.message)
405402
if search_params is None:
406403
raise Exception("Failed to parse parameters.")
407404

408-
logger.info("SAW: fhir_controller. search_params: %s", search_params)
409405
# Check vaxx type permissions- start
410406
try:
411-
logger.info("SAW: fhir_controller. search_immunizations...5")
412-
logger.info("SAW: aws_event=%s", aws_event)
413407
if aws_event.get("headers"):
414-
logger.info("SAW: fhir_controller. search_immunizations...6")
415408
supplier_system = self._identify_supplier_system(aws_event)
416-
logger.info("SAW: Supplier system identified: %s", supplier_system)
417-
logger.info("SAW: Get supplier permissions for: %s", supplier_system)
418409
imms_vax_type_perms = get_supplier_permissions(supplier_system)
419410
if len(imms_vax_type_perms) == 0:
420411
raise UnauthorizedVaxError()
@@ -437,7 +428,7 @@ def search_immunizations(self, aws_event: APIGatewayProxyEventV1) -> dict:
437428
except UnauthorizedVaxError as unauthorized:
438429
return self.create_response(403, unauthorized.to_operation_outcome())
439430
# Check vaxx type permissions on the existing record - end
440-
logger.info("SAW: Searching immunizations...")
431+
441432
result = self.fhir_service.search_immunizations(
442433
search_params.patient_identifier,
443434
vax_type_perm,
@@ -635,7 +626,7 @@ def check_vaccine_type_permissions(self, aws_event):
635626
if len(supplier_system) == 0:
636627
raise UnauthorizedSystemError()
637628
imms_vax_type_perms = get_supplier_permissions(supplier_system)
638-
print(f" update imms = {imms_vax_type_perms}")
629+
logger.info(f" update imms = {imms_vax_type_perms}")
639630
if len(imms_vax_type_perms) == 0:
640631
raise UnauthorizedVaxError()
641632
# Return the values needed for later use
@@ -650,10 +641,8 @@ def check_vaccine_type_permissions(self, aws_event):
650641

651642
@staticmethod
652643
def create_response(status_code, body=None, headers=None):
653-
logger.info("SAW: Creating response with status code: %d", status_code)
654644
if body:
655645
if isinstance(body, dict):
656-
logger.info("SAW: return body : %s", body)
657646
body = json.dumps(body)
658647
if headers:
659648
headers["Content-Type"] = "application/fhir+json"
@@ -668,10 +657,7 @@ def create_response(status_code, body=None, headers=None):
668657

669658
@staticmethod
670659
def _identify_supplier_system(aws_event):
671-
logger.info("SAW: fhir_controller. _identify_supplier_system...1")
672-
headers = aws_event.get("headers", {})
673-
logger.info("SAW: headers=%s", headers)
674-
supplier_system = headers.get("SupplierSystem")
660+
supplier_system = aws_event["headers"]["SupplierSystem"]
675661
if not supplier_system:
676662
raise UnauthorizedError("SupplierSystem header is missing")
677663
return supplier_system

backend/src/fhir_repository.py

Lines changed: 34 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from urllib import response
2+
from responses import logger
13
import simplejson as json
24
import os
35
import time
@@ -394,70 +396,49 @@ def delete_immunization(
394396

395397
def find_immunizations(self, patient_identifier: str, vaccine_types: list):
396398
"""it should find all of the specified patient's Immunization events for all of the specified vaccine_types"""
397-
398-
# ✅ Add debug logging
399-
import logging
400-
logger = logging.getLogger(__name__)
401-
402-
logger.info("SAW fi...1: find_immunizations called with patient_identifier: '%s', vaccine_types: %s",
403-
patient_identifier, vaccine_types)
404-
405-
# Create the patient PK and log it
406-
patient_pk = _make_patient_pk(patient_identifier)
407-
logger.info("SAW fi...2: patient_pk created: '%s'", patient_pk)
408-
409-
condition = Key("PatientPK").eq(patient_pk)
399+
condition = Key("PatientPK").eq(_make_patient_pk(patient_identifier))
410400
is_not_deleted = Attr("DeletedAt").not_exists() | Attr("DeletedAt").eq("reinstated")
411401

412-
logger.info("SAW fi...2.1: is_not_deleted condition created: %s", is_not_deleted)
402+
raw_items = self.get_all_items(condition, is_not_deleted)
413403

414-
logger.info("SAW fi...3: executing DynamoDB query on PatientGSI index")
415-
416-
response = self.table.query(
417-
IndexName="PatientGSI",
418-
KeyConditionExpression=condition,
419-
FilterExpression=is_not_deleted,
420-
)
421-
422-
# ✅ Log the raw DynamoDB response
423-
logger.info("SAW fi...4: DynamoDB query response - Count: %s, ScannedCount: %s",
424-
response.get("Count", 0), response.get("ScannedCount", 0))
425-
426-
if "Items" in response:
427-
raw_items = response["Items"]
428-
logger.info("SAW fi...5: total items returned from DynamoDB: %d", len(raw_items))
429-
430-
# Log first few items for debugging
431-
if raw_items:
432-
logger.info("SAW fi...6: sample raw item keys: %s", list(raw_items[0].keys()))
433-
logger.info("SAW fi...7: first few PatientSK values: %s",
434-
[item.get("PatientSK", "MISSING") for item in raw_items[:3]])
435-
404+
if raw_items:
436405
# Filter the response to contain only the requested vaccine types
437406
items = [x for x in raw_items if x["PatientSK"].split("#")[0] in vaccine_types]
438407

439-
logger.info("SAW fi...8: after vaccine_types filtering (%s): %d items", vaccine_types, len(items))
440-
441-
if items:
442-
# Log the vaccine types found
443-
found_vaccine_types = [item["PatientSK"].split("#")[0] for item in items]
444-
logger.info("SAW fi...9: found vaccine types: %s", found_vaccine_types)
445-
else:
446-
# Debug why no items matched
447-
all_vaccine_types = [item["PatientSK"].split("#")[0] for item in raw_items]
448-
logger.warning("SAW fi...10: no items matched vaccine_types filter!")
449-
logger.warning("SAW fi...11: requested vaccine_types: %s", vaccine_types)
450-
logger.warning("SAW fi...12: available vaccine_types in data: %s", list(set(all_vaccine_types)))
451-
452408
# Return a list of the FHIR immunization resource JSON items
453409
final_resources = [json.loads(item["Resource"]) for item in items]
454-
logger.info("SAW fi...13: returning %d FHIR resources", len(final_resources))
455410

456411
return final_resources
457412
else:
458-
logger.error("SAW fi...14: No 'Items' key in DynamoDB response!")
459-
logger.error("SAW fi...15: Response keys: %s", list(response.keys()))
460-
raise UnhandledResponseError(message=f"Unhandled error. Query failed", response=response)
413+
logger.warning("no items matched patient_identifier filter!")
414+
return []
415+
416+
def get_all_items(self, condition, is_not_deleted):
417+
"""Query DynamoDB and paginate through all results."""
418+
all_items = []
419+
last_evaluated_key = None
420+
421+
while True:
422+
query_args = {
423+
"IndexName": "PatientGSI",
424+
"KeyConditionExpression": condition,
425+
"FilterExpression": is_not_deleted,
426+
}
427+
if last_evaluated_key:
428+
query_args["ExclusiveStartKey"] = last_evaluated_key
429+
430+
response = self.table.query(**query_args)
431+
if "Items" not in response:
432+
raise UnhandledResponseError(message="No Items in DynamoDB response", response=response)
433+
434+
items = response.get("Items", [])
435+
all_items.extend(items)
436+
437+
last_evaluated_key = response.get("LastEvaluatedKey")
438+
if not last_evaluated_key:
439+
break
440+
441+
return all_items
461442

462443
@staticmethod
463444
def _handle_dynamo_response(response):

backend/src/fhir_service.py

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -288,59 +288,40 @@ def search_immunizations(
288288
Finds all instances of Immunization(s) for a specified patient which are for the specified vaccine type(s).
289289
Bundles the resources with the relevant patient resource and returns the bundle.
290290
"""
291-
logger.info("SAW: search_immunizations...1 nhs_number: %s, vaccine_types: %s, date_from: %s, date_to: %s",
292-
nhs_number, vaccine_types, date_from, date_to)
293291
# TODO: is disease type a mandatory field? (I assumed it is)
294292
# i.e. Should we provide a search option for getting Patient's entire imms history?
295-
296-
logger.info("SAW: search_immunizations...2 check nhs_number_mod11_check for nhs_number: %s", nhs_number)
297293
if not nhs_number_mod11_check(nhs_number):
298294
return create_diagnostics()
299295

300-
logger.info("SAW: search_immunizations...3 obtain resources for nhs_number: %s, vaccine_types: %s, date_from: %s, date_to: %s",
301-
nhs_number, vaccine_types, date_from, date_to)
302296
# Obtain all resources which are for the requested nhs number and vaccine type(s) and within the date range
303297
resources = [
304298
r
305299
for r in self.immunization_repo.find_immunizations(nhs_number, vaccine_types)
306300
if self.is_valid_date_from(r, date_from) and self.is_valid_date_to(r, date_to)
307301
]
308-
logger.info("SAW: search_immunizations...4 no of resources found: %d", len(resources))
302+
309303
# Create the patient URN for the fullUrl field.
310304
# NOTE: This UUID is assigned when a SEARCH request is received and used only for referencing the patient
311305
# resource from immunisation resources within the bundle. The fullUrl value we are using is a urn (hence the
312306
# FHIR key name of "fullUrl" is somewhat misleading) which cannot be used to locate any externally stored
313307
# patient resource. This is as agreed with VDS team for backwards compatibility with Immunisation History API.
314308
patient_full_url = f"urn:uuid:{str(uuid4())}"
315309

316-
logger.info("SAW: search_immunizations...5 call get_contained_patient")
317310
imms_patient_record = get_contained_patient(resources[-1]) if resources else None
318311

319-
logger.info("SAW: search_immunizations...6 filter resources for search")
320-
# log pretty resources for debug
321-
logger.info("SAW: search_immunizations...7 resources: %s", [
322-
Immunization.parse_obj(r).json(indent=2) for r in resources
323-
])
324-
# Filter and amend the immunization resources for the SEARCH response
312+
# Filter and amend the immunization resources for the SEARCH response
325313
resources_filtered_for_search = [Filter.search(imms, patient_full_url) for imms in resources]
326-
logger.info("SAW: search_immunizations...8 no of items in filtered resources: %d", len(resources_filtered_for_search))
314+
327315
# Add bundle entries for each of the immunization resources
328316
entries = [
329317
BundleEntry(
330318
resource=Immunization.parse_obj(imms),
331319
search=BundleEntrySearch(mode="match"),
332320
fullUrl=f"https://api.service.nhs.uk/immunisation-fhir-api/Immunization/{imms['id']}",
333321
)
334-
# get imms and log for debug
335322
for imms in resources_filtered_for_search
336323
]
337324

338-
339-
logger.info("SAW: search_immunizations...9 entries created for bundle: %d", len(entries))
340-
341-
for entry in entries:
342-
logger.debug("SAW: search_immunizations...10 entry: %s", entry.resource.json(indent=2))
343-
344325
# Add patient resource if there is at least one immunization resource
345326
if len(resources) > 0:
346327
entries.append(

backend/src/log_structure.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def function_info(func):
2020
@wraps(func)
2121
def wrapper(*args, **kwargs):
2222
event = args[0] if args else {}
23-
print(f"Event: {event}")
23+
logger.info(f"Event: {event}")
2424
headers = event.get("headers", {})
2525
correlation_id = headers.get("X-Correlation-ID", "X-Correlation-ID not passed")
2626
request_id = headers.get("X-Request-ID", "X-Request-ID not passed")
@@ -40,7 +40,7 @@ def wrapper(*args, **kwargs):
4040
start = time.time()
4141
try:
4242
result = func(*args, **kwargs)
43-
print(f"Result:{result}")
43+
logger.info(f"Result:{result}")
4444
end = time.time()
4545
log_data["time_taken"] = f"{round(end - start, 5)}s"
4646
status = "500"
@@ -56,7 +56,7 @@ def wrapper(*args, **kwargs):
5656
record = result_headers["Location"]
5757
if result.get("body"):
5858
ops_outcome = json.loads(result["body"])
59-
print(f"ops_outcome: {ops_outcome}")
59+
logger.info(f"ops_outcome: {ops_outcome}")
6060
if ops_outcome.get("issue"):
6161
outcome_body = ops_outcome["issue"][0]
6262
status_code = outcome_body["code"]

0 commit comments

Comments
 (0)