Skip to content

Commit 4519ee6

Browse files
committed
ELI-223: Fixes review comments
1 parent 90202e4 commit 4519ee6

6 files changed

Lines changed: 72 additions & 60 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from typing import Literal
2+
13
MAGIC_COHORT_LABEL = "elid_all_people"
24
RULE_STOP_DEFAULT = False
35
NHS_NUMBER_HEADER = "nhs-login-nhs-number"
6+
ALLOWED_CONDITIONS = Literal["COVID", "FLU", "MMR", "RSV"]

src/eligibility_signposting_api/model/campaign_config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from pydantic import BaseModel, Field, HttpUrl, RootModel, field_serializer, field_validator, model_validator
1313

14-
from eligibility_signposting_api.config.contants import MAGIC_COHORT_LABEL, RULE_STOP_DEFAULT
14+
from eligibility_signposting_api.config.contants import ALLOWED_CONDITIONS, MAGIC_COHORT_LABEL, RULE_STOP_DEFAULT
1515

1616
if typing.TYPE_CHECKING: # pragma: no cover
1717
from pydantic import SerializationInfo
@@ -184,7 +184,7 @@ class CampaignConfig(BaseModel):
184184
version: CampaignVersion = Field(..., alias="Version")
185185
name: CampaignName = Field(..., alias="Name")
186186
type: Literal["V", "S"] = Field(..., alias="Type")
187-
target: Literal["COVID", "FLU", "MMR", "RSV"] = Field(..., alias="Target")
187+
target: ALLOWED_CONDITIONS = Field(..., alias="Target")
188188
manager: list[str] | None = Field(None, alias="Manager")
189189
approver: list[str] | None = Field(None, alias="Approver")
190190
reviewer: list[str] | None = Field(None, alias="Reviewer")

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from collections import defaultdict
55
from dataclasses import dataclass, field
66
from itertools import chain
7-
from typing import TYPE_CHECKING, TypeVar
7+
from typing import TYPE_CHECKING
88

99
from wireup import service
1010

@@ -37,7 +37,6 @@
3737

3838

3939
logger = logging.getLogger(__name__)
40-
T = TypeVar("T")
4140

4241

4342
@service

src/eligibility_signposting_api/services/processors/token_parser.py

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,70 @@
22
from dataclasses import dataclass
33

44

5-
class InvalidTokenError(ValueError):
6-
def __init__(self, message: str = "Invalid token.") -> None:
7-
super().__init__(message)
8-
9-
10-
class InvalidTokenFormatError(ValueError):
11-
def __init__(self, message: str = "Invalid token format.") -> None:
12-
super().__init__(message)
13-
14-
155
@dataclass
166
class ParsedToken:
17-
attribute_level: str # example: "PERSON" or "TARGET"
18-
attribute_name: str # example: "POSTCODE" or "RSV"
19-
attribute_value: str | None # example: "LAST_SUCCESSFUL_DATE" if attribute_level is TARGET
20-
format: str | None # example: "%d %B %Y" if DATE formatting is used
7+
"""
8+
A class to represent a parsed token.
9+
...
10+
Attributes
11+
----------
12+
attribute_level : str
13+
Example: "PERSON" or "TARGET"
14+
attribute_name : str
15+
Example: "POSTCODE" or "RSV"
16+
attribute_value : int
17+
Example: "LAST_SUCCESSFUL_DATE" if attribute_level is TARGET
18+
format : str
19+
Example: "%d %B %Y" if DATE formatting is used
20+
"""
21+
22+
attribute_level: str
23+
attribute_name: str
24+
attribute_value: str | None
25+
format: str | None
2126

2227

2328
class TokenParser:
2429
MIN_TOKEN_PARTS = 2
2530

2631
@staticmethod
2732
def parse(token: str) -> ParsedToken:
28-
token_body = token[2:-2] # Strip the surrounding [[ ]]
29-
# Check for empty body after stripping, e.g., '[[]]'
33+
"""Parses a token into its parts.
34+
Steps:
35+
Strip the surrounding [[ ]]
36+
Check for empty body after stripping, e.g., '[[]]'
37+
Check for empty parts created by leading/trailing dots or tokens with no dot
38+
Check if the name contains a date format
39+
Return a ParsedToken object
40+
"""
41+
42+
token_body = token[2:-2]
3043
if not token_body:
31-
raise InvalidTokenError
44+
message = "Invalid token."
45+
raise ValueError(message)
3246

3347
token_parts = token_body.split(".")
3448

35-
# Check for empty parts created by leading/trailing dots or tokens with no dot
3649
if len(token_parts) < TokenParser.MIN_TOKEN_PARTS or not all(token_parts):
37-
raise InvalidTokenError
50+
message = "Invalid token."
51+
raise ValueError(message)
3852

3953
token_level = token_parts[0].upper()
4054
token_name = token_parts[-1]
4155

42-
# Check if the name contains a date format
4356
format_match = re.search(r":DATE\(([^()]*)\)", token_name, re.IGNORECASE)
4457
if not format_match and len(token_name.split(":")) > 1:
45-
raise InvalidTokenFormatError
58+
message = "Invalid token format."
59+
raise ValueError(message)
4660

4761
format_str = format_match.group(1) if format_match else None
4862

49-
# Remove the date format from the last part
5063
last_part = re.sub(r":DATE\(.*?\)", "", token_name, flags=re.IGNORECASE)
5164

5265
if len(token_parts) == TokenParser.MIN_TOKEN_PARTS:
53-
# Person token, example, [[PERSON.AGE]]
5466
name = last_part.upper()
5567
value = None
5668
else:
57-
# Target token, example, [[TARGET.RSV.LAST_SUCCESSFUL_DATE]]
5869
name = token_parts[1].upper()
5970
value = last_part.upper()
6071

src/eligibility_signposting_api/services/processors/token_processor.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from wireup import service
77

8+
from eligibility_signposting_api.config.contants import ALLOWED_CONDITIONS
89
from eligibility_signposting_api.model.person import Person
910
from eligibility_signposting_api.services.processors.token_parser import ParsedToken, TokenParser
1011

@@ -13,7 +14,17 @@
1314

1415
TARGET_ATTRIBUTE_LEVEL = "TARGET"
1516
PERSON_ATTRIBUTE_LEVEL = "PERSON"
16-
ALLOWED_CONDITION = "RSV"
17+
ALLOWED_TARGET_ATTRIBUTES = {
18+
"ATTRIBUTE_TYPE",
19+
"VALID_DOSES_COUNT",
20+
"INVALID_DOSES_COUNT",
21+
"LAST_SUCCESSFUL_DATE",
22+
"LAST_VALID_DOSE_DATE",
23+
"BOOKED_APPOINTMENT_DATE",
24+
"BOOKED_APPOINTMENT_PROVIDER",
25+
"LAST_INVITE_DATE",
26+
"LAST_INVITE_STATUS",
27+
}
1728

1829

1930
@service
@@ -59,18 +70,6 @@ def replace_token(text: str, person: Person) -> str:
5970

6071
pattern = r"\[\[.*?\]\]"
6172
all_tokens = re.findall(pattern, text, re.IGNORECASE)
62-
allowed_target_attributes = {
63-
"NHS_NUMBER",
64-
"ATTRIBUTE_TYPE",
65-
"VALID_DOSES_COUNT",
66-
"INVALID_DOSES_COUNT",
67-
"LAST_SUCCESSFUL_DATE",
68-
"LAST_VALID_DOSE_DATE",
69-
"BOOKED_APPOINTMENT_DATE",
70-
"BOOKED_APPOINTMENT_PROVIDER",
71-
"LAST_INVITE_DATE",
72-
"LAST_INVITE_STATUS",
73-
}
7473
present_attributes = [attribute.get("ATTRIBUTE_TYPE") for attribute in person.data]
7574

7675
for token in all_tokens:
@@ -86,18 +85,18 @@ def replace_token(text: str, person: Person) -> str:
8685

8786
if (
8887
parsed_token.attribute_level == TARGET_ATTRIBUTE_LEVEL
89-
and parsed_token.attribute_name == ALLOWED_CONDITION
90-
and parsed_token.attribute_value in allowed_target_attributes
88+
and parsed_token.attribute_name in ALLOWED_CONDITIONS.__args__
89+
and parsed_token.attribute_value in ALLOWED_TARGET_ATTRIBUTES
9190
and parsed_token.attribute_name not in present_attributes
9291
):
9392
replace_with = ""
9493

9594
if replace_with != "":
9695
for attribute in person.data:
9796
is_person_attribute = attribute.get("ATTRIBUTE_TYPE") == PERSON_ATTRIBUTE_LEVEL
98-
is_target_rsv = parsed_token.attribute_name.upper() == ALLOWED_CONDITION
97+
is_allowed_target = parsed_token.attribute_name.upper() in ALLOWED_CONDITIONS.__args__
9998

100-
if (is_target_rsv or is_person_attribute) and key_to_find in attribute:
99+
if (is_allowed_target or is_person_attribute) and key_to_find in attribute:
101100
found_attribute = attribute
102101
key_to_replace = key_to_find
103102
break

tests/unit/services/processors/test_token_processor.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,16 @@ def test_missing_patient_vaccine_data_on_target_attribute_should_replace_with_em
166166

167167
assert actual.condition_name == "Last successful date: "
168168

169-
def test_non_rsv_target_token_should_raise_error(self):
169+
def test_not_allowed_target_conditions_token_should_raise_error(self):
170170
person = Person(
171171
[
172172
{"ATTRIBUTE_TYPE": "PERSON", "AGE": "30"},
173-
{"ATTRIBUTE_TYPE": "COVID", "CONDITION_NAME": "COVID", "LAST_SUCCESSFUL_DATE": "20250101"},
173+
{"ATTRIBUTE_TYPE": "YELLOW_FEVER", "CONDITION_NAME": "COVID", "LAST_SUCCESSFUL_DATE": "20250101"},
174174
]
175175
)
176176

177177
condition = Condition(
178-
condition_name=ConditionName("Last successful date: [[TARGET.COVID.LAST_SUCCESSFUL_DATE]]"),
178+
condition_name=ConditionName("Last successful date: [[TARGET.YELLOW_FEVER.LAST_SUCCESSFUL_DATE]]"),
179179
status=Status.actionable,
180180
status_text=StatusText("Some status"),
181181
cohort_results=[],
@@ -184,7 +184,7 @@ def test_non_rsv_target_token_should_raise_error(self):
184184
)
185185

186186
expected_error = re.escape(
187-
"Invalid attribute name 'LAST_SUCCESSFUL_DATE' in token '[[TARGET.COVID.LAST_SUCCESSFUL_DATE]]'."
187+
"Invalid attribute name 'LAST_SUCCESSFUL_DATE' in token '[[TARGET.YELLOW_FEVER.LAST_SUCCESSFUL_DATE]]'."
188188
)
189189
with pytest.raises(ValueError, match=expected_error):
190190
TokenProcessor.find_and_replace_tokens(person, condition)
@@ -266,7 +266,7 @@ def test_valid_token_valid_format_should_replace_with_date_formatting(self):
266266

267267
condition = Condition(
268268
condition_name=ConditionName(
269-
"You had your RSV vaccine on [[TARGET.RSV.LAST_SUCCESSFUL_DATE:DATE(%d %B %Y)]]"
269+
"You had your COVID vaccine on [[TARGET.COVID.LAST_SUCCESSFUL_DATE:DATE(%d %B %Y)]]"
270270
),
271271
status=Status.actionable,
272272
status_text=StatusText("Your birthday is on [[PERSON.DATE_OF_BIRTH:DATE(%-d %B %Y)]]"),
@@ -276,7 +276,7 @@ def test_valid_token_valid_format_should_replace_with_date_formatting(self):
276276
)
277277

278278
expected = Condition(
279-
condition_name=ConditionName("You had your RSV vaccine on 01 January 2025"),
279+
condition_name=ConditionName("You had your COVID vaccine on 01 January 2025"),
280280
status=Status.actionable,
281281
status_text=StatusText("Your birthday is on 27 March 1990"),
282282
cohort_results=[],
@@ -334,12 +334,12 @@ def test_valid_token_invalid_format_should_raise_error(self, token_format: str):
334334
def test_valid_date_format(self, token_format: str, expected: str):
335335
person = Person(
336336
[
337-
{"ATTRIBUTE_TYPE": "RSV", "CONDITION_NAME": "RSV", "LAST_SUCCESSFUL_DATE": "19900327"},
337+
{"ATTRIBUTE_TYPE": "MMR", "CONDITION_NAME": "MMR", "LAST_SUCCESSFUL_DATE": "19900327"},
338338
]
339339
)
340340

341341
condition = Condition(
342-
condition_name=ConditionName(f"Date: [[TARGET.RSV.LAST_SUCCESSFUL_DATE{token_format}]]"),
342+
condition_name=ConditionName(f"Date: [[TARGET.MMR.LAST_SUCCESSFUL_DATE{token_format}]]"),
343343
status=Status.actionable,
344344
status_text=StatusText("Some text"),
345345
cohort_results=[],
@@ -358,22 +358,22 @@ def test_valid_date_format(self, token_format: str, expected: str):
358358
("[[PERSON.date_of_birth:DATE(%d %B %Y)]]", "27 March 1990"),
359359
("[[PERSON.DATE_OF_BIRTH:date(%d %B %Y)]]", "27 March 1990"),
360360
("[[pErSoN.DATE_OF_BIRTH:DATE(%d %B %Y)]]", "27 March 1990"),
361-
("[[target.RSV.LAST_SUCCESSFUL_DATE:DATE(%-d %B %Y)]]", "1 January 2025"),
362-
("[[TARGET.rsv.LAST_SUCCESSFUL_DATE:DATE(%-d %B %Y)]]", "1 January 2025"),
363-
("[[TARGET.RSV.last_successful_date:DATE(%-d %B %Y)]]", "1 January 2025"),
364-
("[[TARGET.RSV.last_successful_date:date(%-d %B %Y)]]", "1 January 2025"),
361+
("[[target.FLU.LAST_SUCCESSFUL_DATE:DATE(%-d %B %Y)]]", "1 January 2025"),
362+
("[[TARGET.FLU.LAST_SUCCESSFUL_DATE:DATE(%-d %B %Y)]]", "1 January 2025"),
363+
("[[TARGET.FLU.last_successful_date:DATE(%-d %B %Y)]]", "1 January 2025"),
364+
("[[TARGET.FLU.last_successful_date:date(%-d %B %Y)]]", "1 January 2025"),
365365
],
366366
)
367367
def test_token_replace_is_case_insensitive(self, token: str, expected: str):
368368
person = Person(
369369
[
370370
{"ATTRIBUTE_TYPE": "PERSON", "AGE": "30", "DATE_OF_BIRTH": "19900327"},
371-
{"ATTRIBUTE_TYPE": "RSV", "CONDITION_NAME": "RSV", "LAST_SUCCESSFUL_DATE": "20250101"},
371+
{"ATTRIBUTE_TYPE": "FLU", "CONDITION_NAME": "FLU", "LAST_SUCCESSFUL_DATE": "20250101"},
372372
]
373373
)
374374

375375
condition = Condition(
376-
condition_name=ConditionName(f"RSV vaccine on: {token}."),
376+
condition_name=ConditionName(f"FLU vaccine on: {token}."),
377377
status=Status.actionable,
378378
status_text=StatusText(f"Your DOB is: {token}."),
379379
cohort_results=[],
@@ -384,4 +384,4 @@ def test_token_replace_is_case_insensitive(self, token: str, expected: str):
384384
result = TokenProcessor.find_and_replace_tokens(person, condition)
385385

386386
assert result.status_text == f"Your DOB is: {expected}."
387-
assert result.condition_name == f"RSV vaccine on: {expected}."
387+
assert result.condition_name == f"FLU vaccine on: {expected}."

0 commit comments

Comments
 (0)