Skip to content

Commit 4e3e0f2

Browse files
committed
Cleanup after each test
1 parent 6e89857 commit 4e3e0f2

File tree

4 files changed

+74
-80
lines changed

4 files changed

+74
-80
lines changed

delta_backend/README.md

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -68,74 +68,4 @@ You can now:
6868
- Open `output.csv` in Excel or Google Sheets to view cleanly structured records
6969
- Inspect `output.json` to validate the flat key-value output programmatically
7070

71-
---# 🩺 FHIR to Flat JSON Conversion Engine
72-
73-
This project is designed to convert FHIR-compliant JSON data (e.g., Immunization records) into a flat JSON format based on a configurable schema layout. It is intended to support synchronization of Immunisation API generated data from external sources to DPS (Data Processing System) data system
74-
75-
---
76-
77-
## 📁 File Structure Overview
78-
79-
| File Name | What It Does |
80-
|------------------------|---------------|
81-
| **`converter.py`** | 🧠 The main brain — applies the schema, runs conversions, handles errors. |
82-
| **`FHIRParser.py`** | 🪜 Knows how to dig into nested FHIR structures and pull out values like dates, IDs, and patient names. |
83-
| **`SchemaParser.py`** | 📐 Reads your schema layout and tells the converter which FHIR fields to extract and how to rename/format them. |
84-
| **`ConversionLayout.py`** | ✍️ A plain Python list that defines which fields you want, and how they should be formatted (e.g. date format, renaming rules). |
85-
| **`ConversionChecker.py`** | 🔧 Handles transformation logic — e.g. turning a FHIR datetime into `YYYY-MM-DD`, applying lookups, gender codes, defaults, etc. |
86-
| **`Extractor.py`** | 🎣 Specialized logic to pull practitioner names, site codes, addresses, and apply time-aware rules. |
87-
| **`ExceptionMessages.py`** | 🚨 Holds reusable error messages and codes for clean debugging and validation feedback. |
88-
89-
---
90-
91-
92-
## 🛠️ Key Features
93-
94-
- Schema-driven field extraction and formatting
95-
- Support for custom date formats like `YYYYMMDD`, and CSV-safe UTC timestamps
96-
- Robust handling of patient, practitioner, and address data using time-aware logic
97-
- Extendable structure with static helper methods and modular architecture
98-
99-
---
100-
101-
## 📦 Example Use Case
102-
103-
- Input: FHIR `Immunization` resource (with nested fields)
104-
- Output: Flat JSON object with 34 standardized key-value pairs
105-
- Purpose: To export into CSV or push into downstream ETL systems
106-
107-
---
108-
109-
## ✅ Getting Started with `check_conversion.py`
110-
111-
To quickly test your conversion, use the provided `check_conversion.py` script.
112-
This script loads sample FHIR data, runs it through the converter, and automatically saves the output in both JSON and CSV formats.
113-
114-
### 🔄 How to Use It
115-
116-
1. Add your FHIR data (e.g., a dictionary or sample JSON) into the `fhir_sample` variable inside `check_conversion.py`
117-
2. Ensure the field mapping in `ConversionLayout.py` matches your desired output
118-
3. Run the script from the `tests` folder:
119-
120-
```bash
121-
python check_conversion.py
122-
```
123-
124-
### 📁 Output Location
125-
When the script runs, it will automatically:
126-
- Save a **flat JSON file** as `output.json`
127-
- Save a **CSV file** as `output.csv`
128-
129-
These will be located one level up from the `src/` folder:
130-
131-
```
132-
/mnt/c/Users/USER/desktop/shn/immunisation-fhir-api/delta_backend/output.json
133-
/mnt/c/Users/USER/desktop/shn/immunisation-fhir-api/delta_backend/output.csv
134-
```
135-
136-
### 👀 Visualization
137-
You can now:
138-
- Open `output.csv` in Excel or Google Sheets to view cleanly structured records
139-
- Inspect `output.json` to validate the flat key-value output programmatically
140-
14171
---

e2e/utils/base_test.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
make_apigee_product,
2121
)
2222
from utils.immunisation_api import ImmunisationApi, parse_location
23-
from utils.resource import generate_imms_resource
23+
from utils.resource import generate_imms_resource, delete_imms_records
2424

2525

2626
class ImmunizationBaseTest(unittest.TestCase):
@@ -35,7 +35,11 @@ class ImmunizationBaseTest(unittest.TestCase):
3535
apps: List[ApigeeApp]
3636
# an ImmunisationApi with default auth-type: ApplicationRestricted
3737
default_imms_api: ImmunisationApi
38+
generated_imms_records: List[str]
3839

40+
# Called once before any test methods in the class are run.
41+
# The purpose of setUpClass is to prepare shared resources that
42+
# All tests in the class can use
3943
@classmethod
4044
def setUpClass(cls):
4145
cls.apps = []
@@ -87,13 +91,25 @@ def make_app_data() -> ApigeeApp:
8791
cls.tearDownClass()
8892
raise e
8993

94+
# Class method that runs once after all test methods in the class have finished.
95+
# It is used to clean up resources that were shared across multiple tests
9096
@classmethod
9197
def tearDownClass(cls):
9298
for app in cls.apps:
9399
cls.apigee_service.delete_application(app.name)
94100
if hasattr(cls, "product") and cls:
95101
cls.apigee_service.delete_product(cls.product.name)
96102

103+
# Runs after each individual test method in a test class.
104+
# It’s used to clean up resources that were initialized specifically for a single test,
105+
# such as temporary files or mock objects.
106+
def tearDown(cls):
107+
for api in cls.imms_apis:
108+
if api.generated_test_records:
109+
imms_deleted = delete_imms_records(api.generated_test_records)
110+
print(imms_deleted)
111+
api.generated_test_records.clear()
112+
97113
@staticmethod
98114
def create_immunization_resource(imms_api: ImmunisationApi, resource: dict = None) -> str:
99115
"""creates an Immunization resource and returns the resource url"""

e2e/utils/immunisation_api.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import time
44
import random
55
import requests
6-
from typing import Optional, Literal
6+
from typing import Optional, Literal, List
77
from datetime import datetime
88

99
from lib.authentication import BaseAuthentication
@@ -26,6 +26,7 @@ class ImmunisationApi:
2626
url: str
2727
headers: dict
2828
auth: BaseAuthentication
29+
generated_test_records: List[str]
2930

3031
def __init__(self, url, auth: BaseAuthentication):
3132
self.url = url
@@ -38,6 +39,7 @@ def __init__(self, url, auth: BaseAuthentication):
3839
"Authorization": f"Bearer {token}",
3940
"Content-Type": "application/fhir+json",
4041
"Accept": "application/fhir+json"}
42+
self.generated_test_records = []
4143

4244
def __str__(self):
4345
return f"ImmunizationApi: AuthType: {self.auth}"
@@ -69,7 +71,7 @@ def _make_request_with_backoff(
6971
return response
7072

7173
except Exception as e:
72-
if attempt == self.MAX_RETRIES:
74+
if attempt == self.MAX_RETRIES - 1:
7375
raise
7476

7577
wait = (2 ** attempt) + random.uniform(0, 0.5)
@@ -91,14 +93,26 @@ def get_immunization_by_id(self, event_id, expected_status_code: int = 200):
9193
)
9294

9395
def create_immunization(self, imms, expected_status_code: int = 201):
94-
return self._make_request_with_backoff(
96+
response = self._make_request_with_backoff(
9597
"POST",
9698
f"{self.url}/Immunization",
9799
expected_status_code,
98100
headers=self._update_headers(),
99101
json=imms
100102
)
101103

104+
if response.status_code == 201:
105+
if "Location" not in response.headers:
106+
raise ValueError("Missing 'Location' header in response")
107+
108+
imms_id = response.headers["Location"].split("Immunization/")[-1]
109+
if not self._is_valid_uuid4(imms_id):
110+
raise ValueError(f"Invalid UUID4: {imms_id}")
111+
112+
self.generated_test_records.append(imms_id)
113+
114+
return response
115+
102116
def update_immunization(self, imms_id, imms, expected_status_code: int = 200):
103117
return self._make_request_with_backoff(
104118
"PUT",
@@ -154,3 +168,10 @@ def _update_headers(self, headers=None):
154168
"E-Tag": "1"
155169
}}
156170
return {**updated, **headers}
171+
172+
def _is_valid_uuid4(self, imms_id):
173+
try:
174+
val = uuid.UUID(imms_id, version=4)
175+
return str(val) == imms_id
176+
except ValueError:
177+
return False

e2e/utils/resource.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
from copy import deepcopy
66
from decimal import Decimal
77
from typing import Union, Literal
8-
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource
8+
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource, Table
99
from botocore.config import Config
1010
from .mappings import vaccine_type_mappings, VaccineTypes
11-
11+
from functools import lru_cache
1212
from .constants import valid_nhs_number1
1313

1414
current_directory = os.path.dirname(os.path.realpath(__file__))
@@ -133,9 +133,36 @@ def get_patient_postal_code(imms: dict):
133133

134134

135135
def get_full_row_from_identifier(identifier: str) -> dict:
136-
"""Get the full record from the dynamodb table using the identifier"""
137-
config = Config(connect_timeout=1, read_timeout=1, retries={"max_attempts": 1})
138-
db: DynamoDBServiceResource = boto3.resource("dynamodb", region_name="eu-west-2", config=config)
139-
table = db.Table(os.getenv("DYNAMODB_TABLE_NAME"))
136+
table = get_dynamodb_table()
140137

141138
return table.get_item(Key={"PK": f"Immunization#{identifier}"}).get("Item")
139+
140+
141+
@lru_cache()
142+
def get_dynamodb_table() -> Table:
143+
config = Config(connect_timeout=2, read_timeout=2, retries={"max_attempts": 1})
144+
db: DynamoDBServiceResource = boto3.resource("dynamodb", region_name="eu-west-2", config=config)
145+
return db.Table(os.getenv("DYNAMODB_TABLE_NAME"))
146+
147+
148+
def delete_imms_records(identifiers: list[str]) -> dict:
149+
"""Batch delete immunization records from the DynamoDB table."""
150+
table = get_dynamodb_table()
151+
deleted = []
152+
errors = []
153+
154+
try:
155+
with table.batch_writer(overwrite_by_pkeys=["PK"]) as batch:
156+
for identifier in identifiers:
157+
key = {"PK": f"Immunization#{identifier}"}
158+
try:
159+
batch.delete_item(Key=key)
160+
deleted.append(identifier)
161+
except Exception as e:
162+
print(f"Failed to delete record with key {key}: {e}")
163+
errors.append({"identifier": identifier, "error": str(e)})
164+
except Exception as e:
165+
print(f"Batch writer failed: {e}")
166+
return {"Error": str(e), "Deleted": deleted, "Failures": errors}
167+
168+
return {"Deleted": deleted, "Failures": errors}

0 commit comments

Comments
 (0)