Skip to content

Commit d47a14c

Browse files
authored
Merge branch 'main' into feature/eja-eli-579-adding-derivation-for-previous-dose-calculations
2 parents c9ce52b + 990a9ef commit d47a14c

24 files changed

Lines changed: 383 additions & 238 deletions

.github/dependabot.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,10 @@ updates:
1919

2020
- package-ecosystem: "pip"
2121
directory: "/"
22+
cooldown:
23+
default-days: 5
24+
semver-major-days: 7
25+
semver-minor-days: 7
26+
semver-patch-days: 3
2227
schedule:
2328
interval: "daily"

index.html

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4-
<meta charset="UTF-8"/>
4+
<meta charset="UTF-8" />
55
<title>Campaign Config Validator - NHS Digital</title>
66
<script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
77
<style>
@@ -216,18 +216,22 @@ <h3>Visualiser Output</h3>
216216
const jsonInput = document.getElementById("jsonfile");
217217
const runBtn = document.getElementById("run");
218218

219-
function log(text) {
220-
let cleanText = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
221-
// Handle ANSI Colors for Pydantic output
222-
cleanText = cleanText
223-
.replace(/\x1b\[92m/g, '<span class="ansi-green">')
224-
.replace(/\x1b\[93m/g, '<span class="ansi-yellow">')
225-
.replace(/\x1b\[91m/g, '<span class="ansi-red">')
226-
.replace(/\x1b\[0m/g, '</span>');
227-
228-
output.innerHTML += cleanText + "\n";
229-
output.scrollTop = output.scrollHeight;
219+
function log(text) {
220+
let cleanText = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
221+
// ANSI color replacements
222+
cleanText = cleanText
223+
.replace(/\x1b\[92m/g, '<span class="ansi-grey">') // validator/method green
224+
.replace(/\x1b\[93m/g, '<span class="ansi-yellow">') // general yellow
225+
.replace(/\x1b\[34m/g, '<span style="color:#005eb8;font-weight:bold">') // blue
226+
.replace(/\x1b\[33m/g, '<span class="ansi-yellow">') // colon yellow
227+
.replace(/\x1b\[0m/g, '</span>'); // reset
228+
229+
if (cleanText.includes("Valid Config")) {
230+
cleanText = cleanText.replace(/Valid Config/g, '<span style="font-size:2em;font-weight:bold;color:#007f3b">Valid Config</span>');
230231
}
232+
output.innerHTML += cleanText + "\n";
233+
output.scrollTop = output.scrollHeight;
234+
}
231235

232236
function clearLog() {
233237
output.innerHTML = "";
@@ -248,6 +252,8 @@ <h3>Visualiser Output</h3>
248252
"src/eligibility_signposting_api/model/campaign_config.py",
249253
"src/eligibility_signposting_api/config/__init__.py",
250254
"src/eligibility_signposting_api/config/constants.py",
255+
"src/rules_validation_api/decorators/__init__.py",
256+
"src/rules_validation_api/decorators/tracker.py",
251257
"src/rules_validation_api/__init__.py",
252258
"src/rules_validation_api/validators/__init__.py",
253259
"src/rules_validation_api/validators/rules_validator.py",

poetry.lock

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

pyproject.toml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,27 @@ httpx = "^0.28.1"
2626
yarl = "^1.18.3"
2727
pydantic = "^2.12.5"
2828
asgiref = "^3.11.0"
29-
eval-type-backport = "^0.2.2"
29+
eval-type-backport = "^0.3.1"
3030
mangum = "^0.19.0"
31-
wireup = "^2.1.0"
32-
python-json-logger = "^3.3.0"
31+
wireup = "^2.2.2"
32+
python-json-logger = "^4.0.0"
3333
python-dateutil = "^2.9.0"
3434
pyhamcrest = "^2.1.0"
3535
boto3 = "^1.40.57"
36-
botocore = "^1.40.57"
36+
botocore = "^1.40.76"
3737
aws-xray-sdk = "2.15.0"
3838

3939
[tool.poetry.group.dev.dependencies]
40-
ruff = "^0.11.13"
40+
ruff = "^0.14.10"
4141
docopt = "^0.6.2"
4242
jsonpath-rw = "^1.4.0"
4343
semver = "^3.0.4"
4444
gitpython = "^3.1.45"
4545
pytest = "^8.4.2"
46-
pytest-asyncio = "^1.2.0"
46+
pytest-asyncio = "^1.3.0"
4747
pytest-cov = "^7.0.0"
4848
pytest-nhsd-apim = "^5.0.14"
49-
aiohttp = "^3.12.15"
49+
aiohttp = "^3.13.2"
5050
awscli = "^1.37.24"
5151
awscli-local = "^0.22.2"
5252
polyfactory = "^3.2.0"
@@ -56,13 +56,13 @@ localstack = "^4.12.0"
5656
pytest-docker = "^3.2.3"
5757
stamina = "^25.2.0"
5858
pytest-freezer = "^0.4.9"
59-
moto = "^5.1.13"
59+
moto = "^5.1.19"
6060
requests = "^2.32.5"
6161
jsonschema = "^4.25.1"
6262
behave = "^1.3.3"
6363
python-dotenv = "^1.2.1"
6464
openapi-spec-validator = "^0.7.2"
65-
pip-licenses = "^5.0.0"
65+
pip-licenses = "^5.5.0"
6666

6767

6868
[tool.poetry-plugin-lambda-build]

scripts/config/pre-commit.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ repos:
5151
language: script
5252
pass_filenames: false
5353
- repo: https://github.com/astral-sh/ruff-pre-commit
54-
rev: v0.9.7
54+
rev: v0.14.10
5555
hooks:
5656
- id: ruff
5757
args: [--fix]

src/eligibility_signposting_api/logging/logs_manager.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ def wrapper(event: LambdaEvent, context: LambdaContext) -> dict[str, Any] | None
3030

3131

3232
class EnrichedJsonFormatter(JsonFormatter):
33-
def add_fields(self, log_record: dict[str, Any], record: logging.LogRecord, message_dict: dict[str, Any]) -> None:
34-
log_record["request_id"] = request_id_context_var.get() or "-"
35-
super().add_fields(log_record, record, message_dict)
33+
def add_fields(
34+
self,
35+
log_data: dict[str, Any],
36+
record: logging.LogRecord,
37+
message_dict: dict[str, Any],
38+
) -> None:
39+
log_data["request_id"] = request_id_context_var.get() or "-"
40+
super().add_fields(log_data, record, message_dict)
3641

3742

3843
def init_logging(quieten: Sequence[str] = ("asyncio", "botocore", "boto3", "mangum", "urllib3")) -> None:

src/eligibility_signposting_api/model/campaign_config.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class IterationRule(BaseModel):
158158
model_config = {"populate_by_name": True, "extra": "ignore"}
159159

160160
@field_validator("rule_stop", mode="before")
161-
def parse_yn_to_bool(cls, v: str | bool) -> bool: # noqa: N805
161+
def parse_yn_to_bool(cls, v: str | bool) -> bool: # noqa: N805, FBT001
162162
if isinstance(v, str):
163163
return v.upper() == "Y"
164164
return v
@@ -238,7 +238,7 @@ class StatusText(BaseModel):
238238
not_actionable: str | None = Field(None, alias="NotActionable")
239239
actionable: str | None = Field(None, alias="Actionable")
240240

241-
model_config = {"populate_by_name": True}
241+
model_config = {"populate_by_name": True, "extra": "ignore"}
242242

243243

244244
class RuleEntry(BaseModel):
@@ -277,6 +277,12 @@ class Iteration(BaseModel):
277277

278278
model_config = {"populate_by_name": True, "arbitrary_types_allowed": True, "extra": "ignore"}
279279

280+
def __init__(self, **data: dict[str, typing.Any]) -> None:
281+
super().__init__(**data)
282+
# Ensure each rule knows its parent iteration
283+
for rule in self.iteration_rules:
284+
rule.set_parent(self)
285+
280286
@field_validator("iteration_date", mode="before")
281287
@classmethod
282288
def parse_dates(cls, v: str | date) -> date:
@@ -300,12 +306,6 @@ def parse_dates(cls, v: str | date) -> date:
300306
def serialize_dates(v: date, _info: SerializationInfo) -> str:
301307
return v.strftime("%Y%m%d")
302308

303-
@model_validator(mode="after")
304-
def attach_rule_parents(self) -> Iteration:
305-
for rule in self.iteration_rules:
306-
rule.set_parent(self)
307-
return self
308-
309309
def __str__(self) -> str:
310310
return json.dumps(self.model_dump(by_alias=True), indent=2)
311311

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def get_best_iteration_result(self, campaign_group: list[CampaignConfig]) -> Bes
129129
if not iteration_results:
130130
return None
131131

132-
(best_iteration_name, best_iteration_result) = max(
132+
(_best_iteration_name, best_iteration_result) = max(
133133
iteration_results.items(),
134134
key=lambda item: next(iter(item[1].cohort_results.values())).status.value
135135
# Below handles the case where there are no cohort results

src/rules_validation_api/app.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
import json
33
import logging
44
import sys
5+
from collections import defaultdict
56
from pathlib import Path
67

78
from pydantic import ValidationError
89

10+
from rules_validation_api.decorators.tracker import VALIDATORS_CALLED
911
from rules_validation_api.validators.rules_validator import RulesValidation
1012

1113
logging.basicConfig(
@@ -18,11 +20,12 @@
1820
RESET = "\033[0m"
1921
YELLOW = "\033[93m"
2022
RED = "\033[91m"
23+
BLUE = "\033[34m"
2124

2225

2326
def refine_error(e: ValidationError) -> str:
2427
"""Return a very short, single-line error message."""
25-
lines = [f"Validation Error: {len(e.errors())} validation error(s)"]
28+
lines = [f"Validation Error: {len(e.errors())} validation error(s)"]
2629

2730
for err in e.errors():
2831
loc = ".".join(str(x) for x in err["loc"])
@@ -34,21 +37,43 @@ def refine_error(e: ValidationError) -> str:
3437
return "\n".join(lines)
3538

3639

37-
def main() -> None:
40+
def main() -> None: # pragma: no cover
3841
parser = argparse.ArgumentParser(description="Validate campaign configuration.")
3942
parser.add_argument("--config_path", required=True, help="Path to the campaign config JSON file")
4043
args = parser.parse_args()
4144

4245
try:
4346
with Path(args.config_path).open() as file:
4447
json_data = json.load(file)
45-
RulesValidation(**json_data)
48+
result = RulesValidation(**json_data)
4649
sys.stdout.write(f"{GREEN}Valid Config{RESET}\n")
50+
sys.stdout.write(
51+
f"{YELLOW}Current Iteration Number is {RESET}{GREEN}"
52+
f"{result.campaign_config.current_iteration.iteration_number}{RESET}\n"
53+
)
54+
55+
# Group by class
56+
grouped = defaultdict(list)
57+
for v in VALIDATORS_CALLED:
58+
cls, method = v.split(":", 1)
59+
grouped[cls].append(method.strip())
60+
61+
# Print grouped
62+
for cls_name in sorted(grouped.keys(), reverse=True):
63+
methods = sorted(grouped[cls_name])
64+
# First method prints class name
65+
first = methods[0]
66+
colored = f"{BLUE}{cls_name}{RESET}{YELLOW}:{RESET}{GREEN}{first}{RESET}\n"
67+
sys.stdout.write(colored)
68+
# Rest methods indented
69+
for method_name in methods[1:]:
70+
colored = f"{' ' * len(cls_name)}{YELLOW}:{RESET}{GREEN}{method_name}{RESET}\n"
71+
sys.stdout.write(colored)
4772

4873
except ValidationError as e:
4974
clean = refine_error(e)
5075
sys.stderr.write(f"{YELLOW}{clean}{RESET}\n")
5176

5277

53-
if __name__ == "__main__":
78+
if __name__ == "__main__": # pragma: no cover
5479
main()

src/rules_validation_api/decorators/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)