Skip to content

Commit 1ae026a

Browse files
committed
reduce cognitive complexity and use utils and constants
1 parent a0381b7 commit 1ae026a

File tree

5 files changed

+165
-58
lines changed

5 files changed

+165
-58
lines changed

lambdas/id_sync/src/ieds_db_operations.py

Lines changed: 69 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from os_vars import get_ieds_table_name
33
from common.aws_dynamodb import get_dynamodb_table
44
from common.clients import logger, dynamodb_client
5+
from utils import log_status, BATCH_SIZE
56
from exceptions.id_sync_exception import IdSyncException
67

78
ieds_table = None
@@ -16,23 +17,18 @@ def get_ieds_table():
1617
return ieds_table
1718

1819

19-
BATCH_SIZE = 25
20-
21-
2220
def ieds_update_patient_id(old_id: str, new_id: str, items_to_update: list | None = None) -> dict:
2321
"""Update the patient ID in the IEDS table."""
2422
logger.info(f"ieds_update_patient_id. Update patient ID from {old_id} to {new_id}")
2523
if not old_id or not new_id or not old_id.strip() or not new_id.strip():
26-
return {"status": "error", "message": "Old ID and New ID cannot be empty"}
24+
return log_status("Old ID and New ID cannot be empty", old_id, "error")
2725

2826
if old_id == new_id:
29-
return {"status": "success", "message": f"No change in patient ID: {old_id}"}
27+
return log_status(f"No change in patient ID: {old_id}", old_id)
3028

3129
try:
3230
logger.info(f"Updating patient ID in IEDS from {old_id} to {new_id}")
3331

34-
new_patient_pk = f"Patient#{new_id}"
35-
3632
if items_to_update is None:
3733
logger.info("Getting items to update in IEDS table...")
3834
items_to_update = get_items_from_patient_id(old_id)
@@ -41,64 +37,27 @@ def ieds_update_patient_id(old_id: str, new_id: str, items_to_update: list | Non
4137

4238
if not items_to_update:
4339
logger.warning(f"No items found to update for patient ID: {old_id}")
44-
return {
45-
"status": "success",
46-
"message": f"No items found to update for patient ID: {old_id}"
47-
}
48-
49-
transact_items = []
40+
return log_status(f"No items found to update for patient ID: {old_id}", old_id)
5041

5142
logger.info(f"Items to update: {len(items_to_update)}")
52-
ieds_table_name = get_ieds_table_name()
53-
for item in items_to_update:
54-
transact_items.append({
55-
'Update': {
56-
'TableName': ieds_table_name,
57-
'Key': {
58-
'PK': {'S': item['PK']},
59-
},
60-
'UpdateExpression': 'SET PatientPK = :new_val',
61-
'ExpressionAttributeValues': {
62-
':new_val': {'S': new_patient_pk}
63-
}
64-
}
65-
})
66-
67-
logger.info("Transacting items in IEDS table...")
68-
# success tracking
69-
all_batches_successful = True
70-
total_batches = 0
7143

72-
# Batch transact in chunks of BATCH_SIZE
73-
for i in range(0, len(transact_items), BATCH_SIZE):
74-
batch = transact_items[i:i+BATCH_SIZE]
75-
total_batches += 1
76-
logger.info(f"Transacting batch {total_batches} of size: {len(batch)}")
44+
# Build transact items and execute them in batches via helpers to keep
45+
# the top-level function easy to read and test.
46+
transact_items = build_transact_items(old_id, new_id, items_to_update)
7747

78-
response = dynamodb_client.transact_write_items(TransactItems=batch)
79-
logger.info("Batch update complete. Response: %s", response)
80-
81-
# Check each batch response
82-
if response['ResponseMetadata']['HTTPStatusCode'] != 200:
83-
all_batches_successful = False
84-
logger.error(
85-
f"Batch {total_batches} failed with status: {response['ResponseMetadata']['HTTPStatusCode']}")
48+
all_batches_successful, total_batches = execute_transaction_in_batches(transact_items)
8649

8750
# Consolidated response handling
8851
logger.info(
8952
f"All batches complete. Total batches: {total_batches}, All successful: {all_batches_successful}")
9053

9154
if all_batches_successful:
92-
return {
93-
"status": "success",
94-
"message":
95-
f"IEDS update, patient ID: {old_id}=>{new_id}. {len(items_to_update)} updated {total_batches}."
96-
}
55+
return log_status(
56+
f"IEDS update, patient ID: {old_id}=>{new_id}. {len(items_to_update)} updated {total_batches}.",
57+
old_id,
58+
)
9759
else:
98-
return {
99-
"status": "error",
100-
"message": f"Failed to update some batches for patient ID: {old_id}"
101-
}
60+
return log_status(f"Failed to update some batches for patient ID: {old_id}", old_id, "error")
10261

10362
except Exception as e:
10463
logger.exception("Error updating patient ID")
@@ -184,3 +143,59 @@ def extract_patient_resource_from_item(item: dict) -> dict | None:
184143
return response
185144

186145
return None
146+
147+
148+
def build_transact_items(old_id: str, new_id: str, items_to_update: list) -> list:
149+
"""Construct the list of TransactItems for DynamoDB TransactWriteItems.
150+
151+
Each item uses a conditional expression to ensure PatientPK hasn't changed
152+
since it was read.
153+
"""
154+
transact_items = []
155+
ieds_table_name = get_ieds_table_name()
156+
new_patient_pk = f"Patient#{new_id}"
157+
158+
for item in items_to_update:
159+
old_patient_pk = item.get('PatientPK', f"Patient#{old_id}")
160+
161+
transact_items.append({
162+
'Update': {
163+
'TableName': ieds_table_name,
164+
'Key': {
165+
'PK': {'S': item['PK']},
166+
},
167+
'UpdateExpression': 'SET PatientPK = :new_val',
168+
"ConditionExpression": "PatientPK = :expected_old",
169+
'ExpressionAttributeValues': {
170+
':new_val': {'S': new_patient_pk},
171+
':expected_old': {'S': old_patient_pk}
172+
}
173+
}
174+
})
175+
176+
return transact_items
177+
178+
179+
def execute_transaction_in_batches(transact_items: list) -> tuple:
180+
"""Execute transact write items in batches of BATCH_SIZE.
181+
182+
Returns (all_batches_successful: bool, total_batches: int).
183+
"""
184+
all_batches_successful = True
185+
total_batches = 0
186+
187+
for i in range(0, len(transact_items), BATCH_SIZE):
188+
batch = transact_items[i:i+BATCH_SIZE]
189+
total_batches += 1
190+
logger.info(f"Transacting batch {total_batches} of size: {len(batch)}")
191+
192+
response = dynamodb_client.transact_write_items(TransactItems=batch)
193+
logger.info("Batch update complete. Response: %s", response)
194+
195+
# Check each batch response
196+
if response['ResponseMetadata']['HTTPStatusCode'] != 200:
197+
all_batches_successful = False
198+
logger.error(
199+
f"Batch {total_batches} failed with status: {response['ResponseMetadata']['HTTPStatusCode']}")
200+
201+
return all_batches_successful, total_batches

lambdas/id_sync/src/record_processor.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
extract_patient_resource_from_item,
77
get_items_from_patient_id,
88
)
9+
from utils import log_status
910
import json
1011
import ast
1112

@@ -162,7 +163,3 @@ def normalize_strings(item: Any) -> str | None:
162163
logger.exception("demographics_match: comparison failed with exception")
163164
return False
164165

165-
166-
def log_status(msg: str, nhs_number: str, status: str = "success") -> Dict[str, Any]:
167-
message = {"status": status, "message": msg, "nhs_number": nhs_number}
168-
return message

lambdas/id_sync/src/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Dict, Any
2+
3+
4+
def log_status(msg: str, nhs_number: str | None = None, status: str = "success") -> Dict[str, Any]:
5+
"""Return a simple status dict used by record processing for observability.
6+
7+
If `nhs_number` is None the key is omitted which keeps the output shape
8+
compatible with callers that expect only a status/message.
9+
"""
10+
result = {"status": status, "message": msg}
11+
if nhs_number is not None:
12+
result["nhs_number"] = nhs_number
13+
return result
14+
15+
BATCH_SIZE = 25

lambdas/id_sync/tests/test_ieds_db_operations.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ def test_ieds_update_patient_id_success(self):
312312
"status": "success",
313313
"message": f"IEDS update, patient ID: {old_id}=>{new_id}. {len(mock_items)} updated 1."
314314
}
315+
expected_result["nhs_number"] = old_id
315316
self.assertEqual(result, expected_result)
316317

317318
# Verify get_items_from_patient_id was called
@@ -342,6 +343,7 @@ def test_ieds_update_patient_id_non_200_response(self):
342343
"status": "error",
343344
"message": f"Failed to update some batches for patient ID: {old_id}"
344345
}
346+
expected_result["nhs_number"] = old_id
345347
self.assertEqual(result, expected_result)
346348

347349
# Verify transact_write_items was called (not update_item)
@@ -364,6 +366,7 @@ def test_ieds_update_patient_id_no_items_found(self):
364366
"status": "success",
365367
"message": f"No items found to update for patient ID: {old_id}"
366368
}
369+
expected_result["nhs_number"] = old_id
367370
self.assertEqual(result, expected_result)
368371

369372
# Verify get_items_from_patient_id was called
@@ -386,6 +389,7 @@ def test_ieds_update_patient_id_empty_old_id(self):
386389
"status": "error",
387390
"message": "Old ID and New ID cannot be empty"
388391
}
392+
expected_result["nhs_number"] = old_id
389393
self.assertEqual(result, expected_result)
390394

391395
# Verify no update was attempted
@@ -406,6 +410,7 @@ def test_ieds_update_patient_id_empty_new_id(self):
406410
"status": "error",
407411
"message": "Old ID and New ID cannot be empty"
408412
}
413+
expected_result["nhs_number"] = old_id
409414
self.assertEqual(result, expected_result)
410415

411416
# Verify no update was attempted
@@ -425,6 +430,7 @@ def test_ieds_update_patient_id_same_old_and_new_id(self):
425430
"status": "success",
426431
"message": f"No change in patient ID: {patient_id}"
427432
}
433+
expected_result["nhs_number"] = patient_id
428434
self.assertEqual(result, expected_result)
429435

430436
# Verify no update was attempted
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock
3+
4+
import ieds_db_operations
5+
6+
7+
class TestIedsDbOperationsConditional(unittest.TestCase):
8+
def setUp(self):
9+
# Patch logger to suppress output
10+
self.logger_patcher = patch('ieds_db_operations.logger')
11+
self.mock_logger = self.logger_patcher.start()
12+
13+
# Patch get_ieds_table_name and get_ieds_table
14+
self.get_ieds_table_name_patcher = patch('ieds_db_operations.get_ieds_table_name')
15+
self.mock_get_ieds_table_name = self.get_ieds_table_name_patcher.start()
16+
self.mock_get_ieds_table_name.return_value = 'test-table'
17+
18+
self.get_ieds_table_patcher = patch('ieds_db_operations.get_ieds_table')
19+
self.mock_get_ieds_table = self.get_ieds_table_patcher.start()
20+
21+
# Patch dynamodb client
22+
self.dynamodb_client_patcher = patch('ieds_db_operations.dynamodb_client')
23+
self.mock_dynamodb_client = self.dynamodb_client_patcher.start()
24+
25+
def tearDown(self):
26+
patch.stopall()
27+
28+
def test_ieds_update_patient_id_empty_inputs(self):
29+
res = ieds_db_operations.ieds_update_patient_id('', '')
30+
self.assertEqual(res['status'], 'error')
31+
32+
def test_ieds_update_patient_id_same_ids(self):
33+
res = ieds_db_operations.ieds_update_patient_id('a', 'a')
34+
self.assertEqual(res['status'], 'success')
35+
36+
def test_ieds_update_with_items_to_update_uses_provided_list(self):
37+
items = [{'PK': 'Patient#1'}, {'PK': 'Patient#1#r2'}]
38+
# patch transact_write_items to return success
39+
self.mock_dynamodb_client.transact_write_items = MagicMock(return_value={'ResponseMetadata': {'HTTPStatusCode': 200}})
40+
41+
res = ieds_db_operations.ieds_update_patient_id('1', '2', items_to_update=items)
42+
self.assertEqual(res['status'], 'success')
43+
# ensure transact called at least once
44+
self.mock_dynamodb_client.transact_write_items.assert_called()
45+
46+
def test_ieds_update_batches_multiple_calls(self):
47+
# create 60 items to force 3 batches (25,25,10)
48+
items = [{'PK': f'Patient#old#{i}'} for i in range(60)]
49+
called = []
50+
51+
def fake_transact(TransactItems):
52+
called.append(len(TransactItems))
53+
return {'ResponseMetadata': {'HTTPStatusCode': 200}}
54+
55+
self.mock_dynamodb_client.transact_write_items = MagicMock(side_effect=fake_transact)
56+
57+
res = ieds_db_operations.ieds_update_patient_id('old', 'new', items_to_update=items)
58+
self.assertEqual(res['status'], 'success')
59+
# should have been called 3 times
60+
self.assertEqual(len(called), 3)
61+
self.assertEqual(called[0], 25)
62+
self.assertEqual(called[1], 25)
63+
self.assertEqual(called[2], 10)
64+
65+
def test_ieds_update_non_200_response(self):
66+
items = [{'PK': 'Patient#1'}]
67+
self.mock_dynamodb_client.transact_write_items = MagicMock(return_value={'ResponseMetadata': {'HTTPStatusCode': 500}})
68+
69+
res = ieds_db_operations.ieds_update_patient_id('1', '2', items_to_update=items)
70+
self.assertEqual(res['status'], 'error')
71+
72+
73+
if __name__ == '__main__':
74+
unittest.main()

0 commit comments

Comments
 (0)