Skip to content

Commit 1556a2e

Browse files
authored
Merge branch 'master' into VED-711-terraform-changes-for-preprod
2 parents 2527c1f + f1ca34a commit 1556a2e

24 files changed

+1027
-313
lines changed

.github/workflows/sonarcloud.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ jobs:
6464
continue-on-error: true
6565
run: |
6666
poetry install
67-
poetry run coverage run -m unittest discover -s "./tests" -p "*batch*.py" || echo "recordforwarder tests failed" >> ../failed_tests.txt
67+
poetry run coverage run -m unittest discover -p "*batch*.py" || echo "recordforwarder tests failed" >> ../failed_tests.txt
6868
poetry run coverage xml -o ../recordforwarder-coverage.xml
6969
7070
- name: Run unittest with coverage-ack-lambda

ack_backend/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ package: build
66
docker run --rm -v $(shell pwd)/build:/build ack-lambda-build
77

88
test:
9-
python -m unittest
9+
@PYTHONPATH=src:tests python -m unittest
1010

1111
.PHONY: build package

ack_backend/poetry.lock

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

ack_backend/src/ack_processor.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ def lambda_handler(event, context):
2020
file_key = None
2121
created_at_formatted_string = None
2222
message_id = None
23-
supplier_queue = None
2423

2524
ack_data_rows = []
2625

@@ -40,12 +39,11 @@ def lambda_handler(event, context):
4039
message_id = (incoming_message_body[0].get("row_id", "")).split("^")[0]
4140
vaccine_type = incoming_message_body[0].get("vaccine_type")
4241
supplier = incoming_message_body[0].get("supplier")
43-
supplier_queue = f"{supplier}_{vaccine_type}"
4442
created_at_formatted_string = incoming_message_body[0].get("created_at_formatted_string")
4543

4644
for message in incoming_message_body:
4745
ack_data_rows.append(convert_message_to_ack_row(message, created_at_formatted_string))
4846

49-
update_ack_file(file_key, message_id, supplier_queue, created_at_formatted_string, ack_data_rows)
47+
update_ack_file(file_key, message_id, supplier, vaccine_type, created_at_formatted_string, ack_data_rows)
5048

5149
return {"statusCode": 200, "body": json.dumps("Lambda function executed successfully!")}

ack_backend/src/clients.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55

66
REGION_NAME = "eu-west-2"
77

8-
s3_client = boto3_client("s3", region_name=REGION_NAME)
98
firehose_client = boto3_client("firehose", region_name=REGION_NAME)
109
lambda_client = boto3_client('lambda', region_name=REGION_NAME)
1110
dynamodb_client = boto3_client("dynamodb", region_name=REGION_NAME)
1211

1312
dynamodb_resource = boto3_resource("dynamodb", region_name=REGION_NAME)
1413

14+
s3_client = None
15+
def get_s3_client():
16+
global s3_client
17+
if s3_client is None:
18+
s3_client = boto3_client("s3", region_name=REGION_NAME)
19+
return s3_client
1520

1621
# Logger
1722
logging.basicConfig(level="INFO")

ack_backend/src/constants.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22

33
import os
44

5-
SOURCE_BUCKET_NAME = os.getenv("SOURCE_BUCKET_NAME")
6-
ACK_BUCKET_NAME = os.getenv("ACK_BUCKET_NAME")
75
AUDIT_TABLE_NAME = os.getenv("AUDIT_TABLE_NAME")
6+
FILE_NAME_PROC_LAMBDA_NAME = os.getenv("FILE_NAME_PROC_LAMBDA_NAME")
87
AUDIT_TABLE_FILENAME_GSI = "filename_index"
98
AUDIT_TABLE_QUEUE_NAME_GSI = "queue_name_index"
10-
FILE_NAME_PROC_LAMBDA_NAME = os.getenv("FILE_NAME_PROC_LAMBDA_NAME")
119

10+
def get_source_bucket_name() -> str:
11+
"""Get the SOURCE_BUCKET_NAME environment from environment variables."""
12+
return os.getenv("SOURCE_BUCKET_NAME")
13+
14+
def get_ack_bucket_name() -> str:
15+
"""Get the ACK_BUCKET_NAME environment from environment variables."""
16+
return os.getenv("ACK_BUCKET_NAME")
1217

1318
class FileStatus:
1419
"""File status constants"""

ack_backend/src/logging_decorators.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,27 @@ def wrapper(message, created_at_formatted_string):
7777
return wrapper
7878

7979

80+
def upload_ack_file_logging_decorator(func):
81+
"""This decorator logs when record processing is complete."""
82+
83+
@wraps(func)
84+
def wrapper(*args, **kwargs):
85+
86+
base_log_data = {"function_name": f"ack_processor_{func.__name__}", "date_time": str(datetime.now())}
87+
start_time = time.time()
88+
89+
# NB this doesn't require a try-catch block as the wrapped function never throws an exception
90+
result = func(*args, **kwargs)
91+
if result is not None:
92+
message_for_logs = "Record processing complete"
93+
base_log_data.update(result)
94+
additional_log_data = {"status": "success", "statusCode": 200, "message": message_for_logs}
95+
generate_and_send_logs(start_time, base_log_data, additional_log_data)
96+
return result
97+
98+
return wrapper
99+
100+
80101
def ack_lambda_handler_logging_decorator(func):
81102
"""This decorator logs the execution info for the ack lambda handler."""
82103

ack_backend/src/update_ack_file.py

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
import json
44
from io import StringIO, BytesIO
5-
from typing import Union
5+
from typing import Union, Optional
66
from botocore.exceptions import ClientError
7-
from constants import ACK_HEADERS, SOURCE_BUCKET_NAME, ACK_BUCKET_NAME, FILE_NAME_PROC_LAMBDA_NAME
7+
from constants import ACK_HEADERS, get_source_bucket_name, get_ack_bucket_name, FILE_NAME_PROC_LAMBDA_NAME
88
from audit_table import change_audit_table_status_to_processed, get_next_queued_file_details
9-
from clients import s3_client, logger, lambda_client
9+
from clients import get_s3_client, logger, lambda_client
1010
from utils_for_ack_lambda import get_row_count
11+
from logging_decorators import upload_ack_file_logging_decorator
1112

1213

1314
def create_ack_data(
@@ -49,7 +50,7 @@ def obtain_current_ack_content(temp_ack_file_key: str) -> StringIO:
4950
"""Returns the current ack file content if the file exists, or else initialises the content with the ack headers."""
5051
try:
5152
# If ack file exists in S3 download the contents
52-
existing_ack_file = s3_client.get_object(Bucket=ACK_BUCKET_NAME, Key=temp_ack_file_key)
53+
existing_ack_file = get_s3_client().get_object(Bucket=get_ack_bucket_name(), Key=temp_ack_file_key)
5354
existing_content = existing_ack_file["Body"].read().decode("utf-8")
5455
except ClientError as error:
5556
# If ack file does not exist in S3 create a new file containing the headers only
@@ -65,43 +66,61 @@ def obtain_current_ack_content(temp_ack_file_key: str) -> StringIO:
6566
return accumulated_csv_content
6667

6768

69+
@upload_ack_file_logging_decorator
6870
def upload_ack_file(
6971
temp_ack_file_key: str,
7072
message_id: str,
71-
supplier_queue: str,
73+
supplier: str,
74+
vaccine_type: str,
7275
accumulated_csv_content: StringIO,
7376
ack_data_rows: list,
7477
archive_ack_file_key: str,
7578
file_key: str,
76-
) -> None:
79+
) -> Optional[dict]:
7780
"""Adds the data row to the uploaded ack file"""
7881
for row in ack_data_rows:
7982
data_row_str = [str(item) for item in row.values()]
8083
cleaned_row = "|".join(data_row_str).replace(" |", "|").replace("| ", "|").strip()
8184
accumulated_csv_content.write(cleaned_row + "\n")
8285
csv_file_like_object = BytesIO(accumulated_csv_content.getvalue().encode("utf-8"))
83-
s3_client.upload_fileobj(csv_file_like_object, ACK_BUCKET_NAME, temp_ack_file_key)
8486

85-
row_count_source = get_row_count(SOURCE_BUCKET_NAME, f"processing/{file_key}")
86-
row_count_destination = get_row_count(ACK_BUCKET_NAME, temp_ack_file_key)
87+
ack_bucket_name = get_ack_bucket_name()
88+
source_bucket_name = get_source_bucket_name()
89+
90+
get_s3_client().upload_fileobj(csv_file_like_object, ack_bucket_name, temp_ack_file_key)
91+
92+
row_count_source = get_row_count(source_bucket_name, f"processing/{file_key}")
93+
row_count_destination = get_row_count(ack_bucket_name, temp_ack_file_key)
8794
# TODO: Should we check for > and if so what handling is required
8895
if row_count_destination == row_count_source:
89-
move_file(ACK_BUCKET_NAME, temp_ack_file_key, archive_ack_file_key)
90-
move_file(SOURCE_BUCKET_NAME, f"processing/{file_key}", f"archive/{file_key}")
96+
move_file(ack_bucket_name, temp_ack_file_key, archive_ack_file_key)
97+
move_file(source_bucket_name, f"processing/{file_key}", f"archive/{file_key}")
9198

9299
# Update the audit table and invoke the filename lambda with next file in the queue (if one exists)
93100
change_audit_table_status_to_processed(file_key, message_id)
101+
supplier_queue = f"{supplier}_{vaccine_type}"
94102
next_queued_file_details = get_next_queued_file_details(supplier_queue)
95103
if next_queued_file_details:
96104
invoke_filename_lambda(next_queued_file_details["filename"], next_queued_file_details["message_id"])
97-
98-
logger.info("Ack file updated to %s: %s", ACK_BUCKET_NAME, archive_ack_file_key)
105+
# Ingestion of this file is complete
106+
result = {
107+
"message_id": message_id,
108+
"file_key": file_key,
109+
"supplier": supplier,
110+
"vaccine_type": vaccine_type,
111+
"row_count": row_count_source - 1,
112+
}
113+
else:
114+
result = None
115+
logger.info("Ack file updated to %s: %s", ack_bucket_name, archive_ack_file_key)
116+
return result
99117

100118

101119
def update_ack_file(
102120
file_key: str,
103121
message_id: str,
104-
supplier_queue: str,
122+
supplier: str,
123+
vaccine_type: str,
105124
created_at_formatted_string: str,
106125
ack_data_rows: list,
107126
) -> None:
@@ -113,7 +132,8 @@ def update_ack_file(
113132
upload_ack_file(
114133
temp_ack_file_key,
115134
message_id,
116-
supplier_queue,
135+
supplier,
136+
vaccine_type,
117137
accumulated_csv_content,
118138
ack_data_rows,
119139
archive_ack_file_key,
@@ -123,6 +143,7 @@ def update_ack_file(
123143

124144
def move_file(bucket_name: str, source_file_key: str, destination_file_key: str) -> None:
125145
"""Moves a file from one location to another within a single S3 bucket by copying and then deleting the file."""
146+
s3_client = get_s3_client()
126147
s3_client.copy_object(
127148
Bucket=bucket_name, CopySource={"Bucket": bucket_name, "Key": source_file_key}, Key=destination_file_key
128149
)
@@ -135,7 +156,15 @@ def invoke_filename_lambda(file_key: str, message_id: str) -> None:
135156
try:
136157
lambda_payload = {
137158
"Records": [
138-
{"s3": {"bucket": {"name": SOURCE_BUCKET_NAME}, "object": {"key": file_key}}, "message_id": message_id}
159+
{"s3":
160+
{
161+
"bucket": {
162+
"name": get_source_bucket_name()
163+
},
164+
"object": {"key": file_key}
165+
},
166+
"message_id": message_id
167+
}
139168
]
140169
}
141170
lambda_client.invoke(
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"""Utils for ack lambda"""
22

3-
from clients import s3_client
3+
from clients import get_s3_client
44

55

66
def get_row_count(bucket_name: str, file_key: str) -> int:
77
"""
88
Looks in the given bucket and returns the count of the number of lines in the given file.
99
NOTE: Blank lines are not included in the count.
1010
"""
11-
response = s3_client.get_object(Bucket=bucket_name, Key=file_key)
11+
response = get_s3_client().get_object(Bucket=bucket_name, Key=file_key)
1212
return sum(1 for line in response["Body"].iter_lines() if line.strip())

ack_backend/tests/test_ack_processor.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@
2424
with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT):
2525
from ack_processor import lambda_handler
2626

27-
s3_client = boto3_client("s3", region_name=REGION_NAME)
28-
firehose_client = boto3_client("firehose", region_name=REGION_NAME)
29-
3027
BASE_SUCCESS_MESSAGE = MOCK_MESSAGE_DETAILS.success_message
3128
BASE_FAILURE_MESSAGE = {
3229
**{k: v for k, v in BASE_SUCCESS_MESSAGE.items() if k != "imms_id"},
@@ -41,19 +38,24 @@ class TestAckProcessor(unittest.TestCase):
4138
"""Tests for the ack processor lambda handler."""
4239

4340
def setUp(self) -> None:
44-
GenericSetUp(s3_client, firehose_client)
41+
self.s3_client = boto3_client("s3", region_name=REGION_NAME)
42+
self.firehose_client = boto3_client("firehose", region_name=REGION_NAME)
43+
GenericSetUp(self.s3_client, self.firehose_client)
4544

4645
# MOCK SOURCE FILE WITH 100 ROWS TO SIMULATE THE SCENARIO WHERE THE ACK FILE IS NO FULL.
4746
# TODO: Test all other scenarios.
4847
mock_source_file_with_100_rows = StringIO("\n".join(f"Row {i}" for i in range(1, 101)))
49-
s3_client.put_object(
48+
self.s3_client.put_object(
5049
Bucket=BucketNames.SOURCE,
5150
Key=f"processing/{MOCK_MESSAGE_DETAILS.file_key}",
5251
Body=mock_source_file_with_100_rows.getvalue(),
5352
)
53+
self.logger_info_patcher = patch('logging_decorators.logger.info')
54+
self.mock_logger_info = self.logger_info_patcher.start()
5455

5556
def tearDown(self) -> None:
56-
GenericTearDown(s3_client, firehose_client)
57+
GenericTearDown(self.s3_client, self.firehose_client)
58+
self.mock_logger_info.stop()
5759

5860
@staticmethod
5961
def generate_event(test_messages: list[dict]) -> dict:
@@ -111,7 +113,7 @@ def test_lambda_handler_main_multiple_records(self):
111113
response = lambda_handler(event=event, context={})
112114

113115
self.assertEqual(response, EXPECTED_ACK_LAMBDA_RESPONSE_FOR_SUCCESS)
114-
validate_ack_file_content(
116+
validate_ack_file_content(self.s3_client,
115117
[*array_of_success_messages, *array_of_failure_messages, *array_of_mixed_success_and_failure_messages],
116118
existing_file_content=ValidValues.ack_headers,
117119
)
@@ -159,20 +161,20 @@ def test_lambda_handler_main(self):
159161
with self.subTest(msg=f"No existing ack file: {test_case['description']}"):
160162
response = lambda_handler(event=self.generate_event(test_case["messages"]), context={})
161163
self.assertEqual(response, EXPECTED_ACK_LAMBDA_RESPONSE_FOR_SUCCESS)
162-
validate_ack_file_content(test_case["messages"])
164+
validate_ack_file_content(self.s3_client, test_case["messages"])
163165

164-
s3_client.delete_object(Bucket=BucketNames.DESTINATION, Key=MOCK_MESSAGE_DETAILS.temp_ack_file_key)
166+
self.s3_client.delete_object(Bucket=BucketNames.DESTINATION, Key=MOCK_MESSAGE_DETAILS.temp_ack_file_key)
165167

166168
# Test scenario where there is an existing ack file
167169
# TODO: None of the test cases have any existing ack file content?
168170
with self.subTest(msg=f"Existing ack file: {test_case['description']}"):
169171
existing_ack_file_content = test_case.get("existing_ack_file_content", "")
170-
setup_existing_ack_file(MOCK_MESSAGE_DETAILS.temp_ack_file_key, existing_ack_file_content)
172+
setup_existing_ack_file(MOCK_MESSAGE_DETAILS.temp_ack_file_key, existing_ack_file_content, self.s3_client)
171173
response = lambda_handler(event=self.generate_event(test_case["messages"]), context={})
172174
self.assertEqual(response, EXPECTED_ACK_LAMBDA_RESPONSE_FOR_SUCCESS)
173-
validate_ack_file_content(test_case["messages"], existing_ack_file_content)
175+
validate_ack_file_content(self.s3_client, test_case["messages"], existing_ack_file_content)
174176

175-
s3_client.delete_object(Bucket=BucketNames.DESTINATION, Key=MOCK_MESSAGE_DETAILS.temp_ack_file_key)
177+
self.s3_client.delete_object(Bucket=BucketNames.DESTINATION, Key=MOCK_MESSAGE_DETAILS.temp_ack_file_key)
176178

177179
def test_lambda_handler_error_scenarios(self):
178180
"""Test that the lambda handler raises appropriate exceptions for malformed event data."""

0 commit comments

Comments
 (0)