Skip to content

Commit c2f9ae7

Browse files
eli-313 actions properties returned in snake case (#213)
* WIP * WIP * factory created for suggested actions * unit tests * exclude none test * urllink is httpurl everywhere * test for not null * code refactoring * code cleanup --------- Co-authored-by: Robert <[email protected]>
1 parent bb47fcd commit c2f9ae7

File tree

11 files changed

+265
-77
lines changed

11 files changed

+265
-77
lines changed

src/eligibility_signposting_api/model/eligibility.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from functools import total_ordering
77
from typing import NewType, Self
88

9+
from pydantic import HttpUrl
10+
911
NHSNumber = NewType("NHSNumber", str)
1012
DateOfBirth = NewType("DateOfBirth", date)
1113
Postcode = NewType("Postcode", str)
@@ -17,7 +19,7 @@
1719
ActionType = NewType("ActionType", str)
1820
ActionCode = NewType("ActionCode", str)
1921
ActionDescription = NewType("ActionDescription", str)
20-
UrlLink = NewType("UrlLink", str)
22+
UrlLink = NewType("UrlLink", HttpUrl)
2123
UrlLabel = NewType("UrlLabel", str)
2224

2325

@@ -79,17 +81,12 @@ class SuggestedAction:
7981
url_label: UrlLabel | None
8082

8183

82-
@dataclass
83-
class SuggestedActions:
84-
actions: list[SuggestedAction]
85-
86-
8784
@dataclass
8885
class Condition:
8986
condition_name: ConditionName
9087
status: Status
9188
cohort_results: list[CohortGroupResult]
92-
actions: SuggestedActions | None = None
89+
actions: list[SuggestedAction] | None = None
9390

9491

9592
@dataclass
@@ -104,7 +101,7 @@ class CohortGroupResult:
104101
class IterationResult:
105102
status: Status
106103
cohort_results: list[CohortGroupResult]
107-
actions: SuggestedActions | None
104+
actions: list[SuggestedAction] | None
108105

109106

110107
@dataclass

src/eligibility_signposting_api/model/rules.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from operator import attrgetter
1010
from typing import Literal, NewType
1111

12-
from pydantic import BaseModel, Field, RootModel, field_serializer, field_validator, model_validator
12+
from pydantic import BaseModel, Field, HttpUrl, RootModel, field_serializer, field_validator, model_validator
1313

1414
from eligibility_signposting_api.config.contants import MAGIC_COHORT_LABEL, RULE_STOP_DEFAULT
1515

@@ -132,7 +132,7 @@ class AvailableAction(BaseModel):
132132
action_type: str = Field(..., alias="ActionType")
133133
action_code: str = Field(..., alias="ExternalRoutingCode")
134134
action_description: str | None = Field(None, alias="ActionDescription")
135-
url_link: str | None = Field(None, alias="UrlLink")
135+
url_link: HttpUrl | None = Field(None, alias="UrlLink")
136136
url_label: str | None = Field(None, alias="UrlLabel")
137137

138138
model_config = {"populate_by_name": True}

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
IterationResult,
2424
Status,
2525
SuggestedAction,
26-
SuggestedActions,
2726
UrlLabel,
2827
UrlLink,
2928
)
@@ -129,7 +128,7 @@ def get_redirect_rules(
129128
def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibility.EligibilityStatus:
130129
"""Iterates over campaign groups, evaluates eligibility, and returns a consolidated status."""
131130
condition_results: dict[ConditionName, IterationResult] = {}
132-
actions: SuggestedActions | None = SuggestedActions([])
131+
actions: list[SuggestedAction] | None = []
133132

134133
for condition_name, campaign_group in self.campaigns_grouped_by_condition_name:
135134
iteration_results: dict[str, tuple[Iteration, IterationResult]] = {}
@@ -165,12 +164,12 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil
165164
final_result = self.build_condition_results(condition_results)
166165
return eligibility.EligibilityStatus(conditions=final_result)
167166

168-
def handle_redirect_rules(self, best_active_iteration: Iteration) -> SuggestedActions | None:
167+
def handle_redirect_rules(self, best_active_iteration: Iteration) -> list[SuggestedAction] | None:
169168
redirect_rules, action_mapper, default_comms = self.get_redirect_rules(best_active_iteration)
170169
priority_getter = attrgetter("priority")
171170
sorted_rules_by_priority = sorted(redirect_rules, key=priority_getter)
172171

173-
actions: SuggestedActions | None = self.get_actions_from_comms(action_mapper, default_comms)
172+
actions: list[SuggestedAction] | None = self.get_actions_from_comms(action_mapper, default_comms)
174173
for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter):
175174
rule_group_list = list(rule_group)
176175
matcher_matched_list = [
@@ -181,7 +180,7 @@ def handle_redirect_rules(self, best_active_iteration: Iteration) -> SuggestedAc
181180
comms_routing = rule_group_list[0].comms_routing
182181
if comms_routing and all(matcher_matched_list):
183182
rule_actions = self.get_actions_from_comms(action_mapper, comms_routing)
184-
if rule_actions and len(rule_actions.actions) > 0:
183+
if rule_actions and len(rule_actions) > 0:
185184
actions = rule_actions
186185
break
187186

@@ -330,12 +329,12 @@ def evaluate_rules_priority_group(
330329
return best_status, inclusion_reasons, exclusion_reasons, is_rule_stop
331330

332331
@staticmethod
333-
def get_actions_from_comms(action_mapper: ActionsMapper, comms: str) -> SuggestedActions | None:
334-
suggested_actions: SuggestedActions = SuggestedActions([])
332+
def get_actions_from_comms(action_mapper: ActionsMapper, comms: str) -> list[SuggestedAction] | None:
333+
suggested_actions: list[SuggestedAction] = []
335334
for comm in comms.split("|"):
336335
action = action_mapper.get(comm)
337336
if action is not None:
338-
suggested_actions.actions.append(
337+
suggested_actions.append(
339338
SuggestedAction(
340339
action_type=ActionType(action.action_type),
341340
action_code=ActionCode(action.action_code),

src/eligibility_signposting_api/views/eligibility.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def check_eligibility(nhs_number: NHSNumber, eligibility_service: Injected[Eligi
3939
except UnknownPersonError:
4040
return handle_unknown_person_error(nhs_number)
4141
else:
42-
eligibility_response = build_eligibility_response(eligibility_status)
42+
eligibility_response: eligibility.EligibilityResponse = build_eligibility_response(eligibility_status)
4343
return make_response(
4444
eligibility_response.model_dump(by_alias=True, mode="json", exclude_none=True), HTTPStatus.OK
4545
)
@@ -103,11 +103,7 @@ def build_eligibility_response(eligibility_status: EligibilityStatus) -> eligibi
103103
statusText=eligibility.StatusText(f"{condition.status}"), # pyright: ignore[reportCallIssue]
104104
eligibilityCohorts=build_eligibility_cohorts(condition), # pyright: ignore[reportCallIssue]
105105
suitabilityRules=build_suitability_results(condition), # pyright: ignore[reportCallIssue]
106-
actions=(
107-
condition.actions.actions
108-
if condition.actions is not None and condition.actions.actions is not None
109-
else None
110-
),
106+
actions=build_actions(condition),
111107
)
112108

113109
processed_suggestions.append(suggestions)
@@ -120,6 +116,24 @@ def build_eligibility_response(eligibility_status: EligibilityStatus) -> eligibi
120116
)
121117

122118

119+
def build_actions(condition: Condition) -> list[eligibility.Action] | None:
120+
if condition.actions is not None:
121+
return [
122+
eligibility.Action(
123+
actionType=eligibility.ActionType(action.action_type),
124+
actionCode=eligibility.ActionCode(action.action_code),
125+
description=eligibility.Description(action.action_description)
126+
if action.action_description is not None
127+
else None,
128+
urlLink=eligibility.HttpUrl(action.url_link) if action.url_link is not None else None,
129+
urlLabel=eligibility.UrlLabel(action.url_label) if action.url_label is not None else None,
130+
)
131+
for action in condition.actions
132+
]
133+
134+
return None
135+
136+
123137
def build_eligibility_cohorts(condition: Condition) -> list[eligibility.EligibilityCohort]:
124138
"""Group Iteration cohorts and make only one entry per cohort group"""
125139

src/eligibility_signposting_api/views/response_model/eligibility.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
from pydantic import UUID4, BaseModel, Field, HttpUrl, field_serializer
66
from pydantic_core.core_schema import SerializationInfo
77

8-
from eligibility_signposting_api.model.eligibility import SuggestedAction
9-
108
LastUpdated = NewType("LastUpdated", datetime)
119
ConditionName = NewType("ConditionName", str)
1210
StatusText = NewType("StatusText", str)
@@ -17,6 +15,7 @@
1715
RuleText = NewType("RuleText", str)
1816
CohortCode = NewType("CohortCode", str)
1917
CohortText = NewType("CohortText", str)
18+
UrlLabel = NewType("UrlLabel", str)
2019

2120

2221
class Status(StrEnum):
@@ -50,8 +49,9 @@ class SuitabilityRule(BaseModel):
5049
class Action(BaseModel):
5150
action_type: ActionType = Field(..., alias="actionType")
5251
action_code: ActionCode = Field(..., alias="actionCode")
53-
description: Description
54-
url_link: HttpUrl = Field(..., alias="urlLink")
52+
description: Description | None
53+
url_link: HttpUrl | None = Field(..., alias="urlLink")
54+
url_label: UrlLabel | None = Field(..., alias="urlLabel")
5555

5656
model_config = {"populate_by_name": True}
5757

@@ -62,7 +62,7 @@ class ProcessedSuggestion(BaseModel):
6262
status_text: StatusText = Field(..., alias="statusText")
6363
eligibility_cohorts: list[EligibilityCohort] = Field(..., alias="eligibilityCohorts")
6464
suitability_rules: list[SuitabilityRule] = Field(..., alias="suitabilityRules")
65-
actions: list[SuggestedAction] | None
65+
actions: list[Action] | None
6666

6767
model_config = {"populate_by_name": True}
6868

tests/fixtures/builders/model/eligibility.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,25 @@
33

44
from polyfactory import Use
55
from polyfactory.factories import DataclassFactory
6+
from pydantic import HttpUrl
67

7-
from eligibility_signposting_api.model.eligibility import CohortGroupResult, Condition, EligibilityStatus
8+
from eligibility_signposting_api.model import eligibility
9+
from eligibility_signposting_api.model.eligibility import UrlLink
810

911

10-
class ConditionFactory(DataclassFactory[Condition]): ...
12+
class SuggestedActionFactory(DataclassFactory[eligibility.SuggestedAction]):
13+
url_link = UrlLink(HttpUrl("https://test-example.com"))
1114

1215

13-
class EligibilityStatusFactory(DataclassFactory[EligibilityStatus]):
14-
condition = Use(ConditionFactory.batch, size=2)
16+
class ConditionFactory(DataclassFactory[eligibility.Condition]):
17+
actions = Use(SuggestedActionFactory.batch, size=2)
1518

1619

17-
class CohortResultFactory(DataclassFactory[CohortGroupResult]): ...
20+
class EligibilityStatusFactory(DataclassFactory[eligibility.EligibilityStatus]):
21+
conditions = Use(ConditionFactory.batch, size=2)
22+
23+
24+
class CohortResultFactory(DataclassFactory[eligibility.CohortGroupResult]): ...
1825

1926

2027
def random_str(length: int) -> str:

tests/fixtures/matchers/eligibility.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from hamcrest.core.matcher import Matcher
22

33
from eligibility_signposting_api.model.eligibility import CohortGroupResult, Condition, EligibilityStatus, Reason
4-
from eligibility_signposting_api.views.response_model.eligibility import EligibilityCohort, SuitabilityRule
4+
from eligibility_signposting_api.views.response_model.eligibility import Action, EligibilityCohort, SuitabilityRule
55

66
from .meta import BaseAutoMatcher
77

@@ -24,6 +24,9 @@ class EligibilityCohortMatcher(BaseAutoMatcher[EligibilityCohort]): ...
2424
class SuitabilityRuleMatcher(BaseAutoMatcher[SuitabilityRule]): ...
2525

2626

27+
class ActionMatcher(BaseAutoMatcher[Action]): ...
28+
29+
2730
def is_eligibility_status() -> Matcher[EligibilityStatus]:
2831
return EligibilityStatusMatcher()
2932

@@ -46,3 +49,7 @@ def is_eligibility_cohort() -> Matcher[EligibilityCohort]:
4649

4750
def is_suitability_rule() -> Matcher[SuitabilityRule]:
4851
return SuitabilityRuleMatcher()
52+
53+
54+
def is_action() -> Matcher[Action]:
55+
return ActionMatcher()

tests/integration/in_process/test_eligibility_endpoint.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def test_actionable(
218218
"cohortText": "positive_description",
219219
}
220220
],
221-
"actions": [{"action_type": "defaultcomms", "action_code": "action_code"}],
221+
"actions": [{"actionType": "defaultcomms", "actionCode": "action_code"}],
222222
"suitabilityRules": [],
223223
"statusText": "Status.actionable",
224224
}
@@ -355,7 +355,7 @@ def test_actionable_when_only_magic_cohort_is_present(
355355
"cohortText": "magic positive description",
356356
}
357357
],
358-
"actions": [{"action_type": "defaultcomms", "action_code": "action_code"}],
358+
"actions": [{"actionType": "defaultcomms", "actionCode": "action_code"}],
359359
"suitabilityRules": [],
360360
"statusText": "Status.actionable",
361361
}
@@ -511,7 +511,7 @@ def test_actionable(
511511
"condition": "FLU",
512512
"status": "Actionable",
513513
"eligibilityCohorts": [],
514-
"actions": [{"action_code": "action_code", "action_type": "defaultcomms"}],
514+
"actions": [{"actionCode": "action_code", "actionType": "defaultcomms"}],
515515
"suitabilityRules": [],
516516
"statusText": "Status.actionable",
517517
}

tests/integration/lambda/test_app_running_as_lambda.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def get_log_messages(flask_function: str, logs_client: BaseClient) -> list[str]:
153153
return [e["message"] for e in log_events["events"]]
154154

155155

156-
def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers( # noqa: PLR0913
156+
def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_check_if_audited( # noqa: PLR0913
157157
lambda_client: BaseClient, # noqa:ARG001
158158
persisted_person: NHSNumber,
159159
campaign_config: CampaignConfig, # noqa:ARG001
@@ -176,6 +176,7 @@ def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers( # noqa: P
176176
is_response().with_status_code(HTTPStatus.OK).and_body(is_json_that(has_key("processedSuggestions"))),
177177
)
178178

179+
# Then - check if audited
179180
objects = s3_client.list_objects_v2(Bucket=audit_bucket).get("Contents", [])
180181
object_keys = [obj["Key"] for obj in objects]
181182
latest_key = sorted(object_keys)[-1]

0 commit comments

Comments
 (0)