Skip to content

Commit acd8d0c

Browse files
authored
PRMP-756: Add bulk report output models, additional reports and updated S3 filepaths
1 parent c3cd961 commit acd8d0c

20 files changed

+1463
-322
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from datetime import datetime
2+
from typing import Optional
3+
4+
from enums.metadata_report import MetadataReport
5+
from pydantic import BaseModel, ConfigDict, Field
6+
from pydantic.alias_generators import to_pascal
7+
from utils.audit_logging_setup import LoggingService
8+
from utils.utilities import create_reference_id
9+
10+
logger = LoggingService(__name__)
11+
12+
13+
class BulkUploadReport(BaseModel):
14+
model_config = ConfigDict(alias_generator=to_pascal, populate_by_name=True)
15+
id: str = Field(alias=MetadataReport.ID, default_factory=create_reference_id)
16+
nhs_number: str = Field(alias=MetadataReport.NhsNumber)
17+
upload_status: str = Field(alias=MetadataReport.UploadStatus)
18+
timestamp: int = Field(
19+
alias=MetadataReport.Timestamp,
20+
default_factory=lambda: int(datetime.now().timestamp()),
21+
)
22+
date: str = Field(
23+
alias=MetadataReport.Date,
24+
default_factory=lambda: date_string_yyyymmdd(datetime.now()),
25+
)
26+
file_path: str = Field(alias=MetadataReport.FilePath)
27+
pds_ods_code: str = Field(alias=MetadataReport.PdsOdsCode)
28+
uploader_ods_code: str = Field(alias=MetadataReport.UploaderOdsCode)
29+
failure_reason: Optional[str] = Field(
30+
default="", alias=MetadataReport.FailureReason
31+
)
32+
33+
34+
def date_string_yyyymmdd(time_now: datetime) -> str:
35+
return time_now.strftime("%Y-%m-%d")
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
from enums.metadata_report import MetadataReport
2+
from enums.patient_ods_inactive_status import PatientOdsInactiveStatus
3+
from enums.upload_status import UploadStatus
4+
from inflection import underscore
5+
from models.bulk_upload_report import BulkUploadReport
6+
from utils.audit_logging_setup import LoggingService
7+
8+
logger = LoggingService(__name__)
9+
10+
11+
class ReportBase:
12+
def __init__(
13+
self,
14+
generated_at: str,
15+
):
16+
self.generated_at = generated_at
17+
self.total_successful = set()
18+
self.total_registered_elsewhere = set()
19+
self.total_suspended = set()
20+
self.total_deceased = set()
21+
self.total_restricted = set()
22+
23+
def get_total_successful_nhs_numbers(self) -> list:
24+
if self.total_successful:
25+
return [patient[0] for patient in self.total_successful]
26+
return []
27+
28+
def get_total_successful_count(self) -> int:
29+
return len(self.total_successful)
30+
31+
def get_total_registered_elsewhere_count(self) -> int:
32+
return len(self.total_registered_elsewhere)
33+
34+
def get_total_suspended_count(self) -> int:
35+
return len(self.total_suspended)
36+
37+
def get_total_deceased_count(self) -> int:
38+
return len(self.total_deceased)
39+
40+
def get_total_restricted_count(self) -> int:
41+
return len(self.total_restricted)
42+
43+
@staticmethod
44+
def get_sorted(to_sort: set) -> list:
45+
return sorted(to_sort, key=lambda x: x[0]) if to_sort else []
46+
47+
48+
class OdsReport(ReportBase):
49+
def __init__(
50+
self,
51+
generated_at: str,
52+
uploader_ods_code: str = "",
53+
report_items: list[BulkUploadReport] = [],
54+
):
55+
super().__init__(generated_at)
56+
self.report_items = report_items
57+
self.uploader_ods_code = uploader_ods_code
58+
self.failures_per_patient = {}
59+
self.unique_failures = {}
60+
61+
self.populate_report()
62+
63+
def populate_report(self):
64+
logger.info(f"Generating ODS report file for {self.uploader_ods_code}")
65+
66+
for item in self.report_items:
67+
if item.upload_status == UploadStatus.COMPLETE:
68+
self.process_successful_report_item(item)
69+
elif item.upload_status == UploadStatus.FAILED:
70+
self.process_failed_report_item(item)
71+
72+
self.set_unique_failures()
73+
74+
def process_successful_report_item(self, item: BulkUploadReport):
75+
self.total_successful.add((item.nhs_number, item.date))
76+
77+
if item.pds_ods_code == PatientOdsInactiveStatus.SUSPENDED:
78+
self.total_suspended.add((item.nhs_number, item.date))
79+
elif item.pds_ods_code == PatientOdsInactiveStatus.DECEASED:
80+
self.total_deceased.add((item.nhs_number, item.date, item.failure_reason))
81+
elif item.pds_ods_code == PatientOdsInactiveStatus.RESTRICTED:
82+
self.total_restricted.add((item.nhs_number, item.date))
83+
elif (
84+
item.uploader_ods_code != item.pds_ods_code
85+
and item.pds_ods_code not in PatientOdsInactiveStatus.list()
86+
):
87+
self.total_registered_elsewhere.add((item.nhs_number, item.date))
88+
89+
def process_failed_report_item(self, item: BulkUploadReport):
90+
is_new_failure = item.nhs_number not in self.failures_per_patient
91+
92+
is_timestamp_newer = (
93+
item.nhs_number in self.failures_per_patient
94+
and self.failures_per_patient[item.nhs_number].get(MetadataReport.Timestamp)
95+
< item.timestamp
96+
)
97+
98+
if (item.failure_reason and is_new_failure) or is_timestamp_newer:
99+
self.failures_per_patient.update(
100+
{
101+
item.nhs_number: item.model_dump(
102+
include={
103+
underscore(str(MetadataReport.Date)),
104+
underscore(str(MetadataReport.Timestamp)),
105+
underscore(str(MetadataReport.UploaderOdsCode)),
106+
underscore(str(MetadataReport.FailureReason)),
107+
},
108+
by_alias=True,
109+
)
110+
}
111+
)
112+
113+
def set_unique_failures(self):
114+
patients_to_remove = {
115+
patient
116+
for patient in self.failures_per_patient
117+
if patient in self.get_total_successful_nhs_numbers()
118+
}
119+
for patient in patients_to_remove:
120+
self.failures_per_patient.pop(patient)
121+
122+
for patient_data in self.failures_per_patient.values():
123+
reason = patient_data.get(MetadataReport.FailureReason)
124+
self.unique_failures[reason] = self.unique_failures.get(reason, 0) + 1
125+
126+
def get_unsuccessful_reasons_data_rows(self):
127+
return [
128+
[MetadataReport.FailureReason, failure_reason, count]
129+
for failure_reason, count in self.unique_failures.items()
130+
]
131+
132+
133+
class SummaryReport(ReportBase):
134+
def __init__(self, generated_at: str, ods_reports: list[OdsReport] = []):
135+
super().__init__(generated_at)
136+
self.ods_reports = ods_reports
137+
self.success_summary = []
138+
self.reason_summary = []
139+
140+
self.populate_report()
141+
142+
def populate_report(self):
143+
ods_code_success_total = {}
144+
145+
for report in self.ods_reports:
146+
self.total_successful.update(report.total_successful)
147+
self.total_registered_elsewhere.update(report.total_registered_elsewhere)
148+
self.total_suspended.update(report.total_suspended)
149+
self.total_deceased.update(report.total_deceased)
150+
self.total_restricted.update(report.total_restricted)
151+
ods_code_success_total[report.uploader_ods_code] = report.total_successful
152+
153+
for reason, count in report.unique_failures.items():
154+
self.reason_summary.append(
155+
[
156+
f"{MetadataReport.FailureReason} for {report.uploader_ods_code}",
157+
reason,
158+
count,
159+
]
160+
)
161+
162+
if ods_code_success_total:
163+
for uploader_ods_code, nhs_numbers in ods_code_success_total.items():
164+
self.success_summary.append(
165+
["Success by ODS", uploader_ods_code, len(nhs_numbers)]
166+
)
167+
else:
168+
self.success_summary.append(["Success by ODS", "No ODS codes found", 0])

lambdas/models/bulk_upload_status.py

Lines changed: 0 additions & 40 deletions
This file was deleted.

lambdas/repositories/bulk_upload/bulk_upload_dynamo_repository.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from enums.metadata_field_names import DocumentReferenceMetadataFields
44
from enums.upload_status import UploadStatus
5-
from models.bulk_upload_status import BulkUploadReport
5+
from models.bulk_upload_report import BulkUploadReport
66
from models.nhs_document_reference import NHSDocumentReference
77
from models.staging_metadata import StagingMetadata
88
from services.base.dynamo_service import DynamoDBService

0 commit comments

Comments
 (0)