Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7b902b3
[NRL-1146] Add report to count pointers for a custodian
mattdean3-nhs Nov 4, 2024
f0157cf
[NRL-1146] Add instructions to README on how to run reports
mattdean3-nhs Nov 4, 2024
6a0cfd9
NRL-474 validate author system, length and value format
eesa456 Nov 4, 2024
0e4a8ee
NRL-474 make author mandatory in model
eesa456 Nov 4, 2024
1db9b70
NRL-474 update model and tests
eesa456 Nov 4, 2024
0cc69a6
NRL-474 fix tests
eesa456 Nov 4, 2024
66f3398
NRL-474 remove redundant test
eesa456 Nov 4, 2024
677debf
[NRL-1150] WIP Fix up errors
mattdean3-nhs Nov 4, 2024
ff0fcfb
Merge pull request #723 from NHSDigital/feature/made14-NRL-1146-repor…
mattdean3-nhs Nov 5, 2024
96c9f7e
[NRL-1150] Switch ParseError to use loc if present
mattdean3-nhs Nov 5, 2024
cb0d3b4
NRL-474 add test back
eesa456 Nov 5, 2024
f72ff7b
NRL-474 lint fix
eesa456 Nov 6, 2024
9fef834
Merge branch 'develop' into feature/eema1-NRL-474-authorBasicValidation
eesa456 Nov 6, 2024
2cbeb7f
Merge pull request #725 from NHSDigital/bugfix/made14-NRL-1150-pydant…
mattdean3-nhs Nov 6, 2024
9c867bd
Merge branch 'develop' into feature/eema1-NRL-474-authorBasicValidation
eesa456 Nov 6, 2024
16f50c6
NRL-474 fix test
eesa456 Nov 6, 2024
dda30e1
Merge pull request #724 from NHSDigital/feature/eema1-NRL-474-authorB…
eesa456 Nov 12, 2024
317a657
[NRL-1104] Update werkzeug dependency to latest version
mattdean3-nhs Nov 12, 2024
a4142f7
[NRL-1104] Update setuptools from 69.2.0 to 75.4.0
mattdean3-nhs Nov 12, 2024
b2f542a
Merge branch 'develop' into feature/made14-NRL-1104-update-setuptools
mattdean3-nhs Nov 12, 2024
4d51682
Merge branch 'develop' into feature/made14-NRL-1104-rm-werkzeug-dep
mattdean3-nhs Nov 12, 2024
444b633
Merge pull request #728 from NHSDigital/feature/made14-NRL-1104-rm-we…
mattdean3-nhs Nov 12, 2024
773eebc
Merge pull request #729 from NHSDigital/feature/made14-NRL-1104-updat…
mattdean3-nhs Nov 12, 2024
196a545
NRL-1177 Fix Sonarqube warnings for snomed urls
axelkrastek1-nhs Nov 12, 2024
0ae824c
Merge pull request #733 from NHSDigital/feature/NRL-1177-fix-snomed-w…
axelkrastek1-nhs Nov 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,3 +511,26 @@ Once you have a new release version ready, you can deploy it through our environ
2. If any issues arise in the deployment, fix the issues, create a new release version and start this process again.
3. Once the deployments are complete, use the "Persistent Environment Deploy" Github Action workflow to deploy the release version to `ref`.
4. Once that is complete, use the "Persistent Environment Deploy" workflow to deploy the release version to `prod`.

## Reports

Reports are provided as scripts in the `reports/` directory. To run a report:

1. Login to your AWS account on the command line, choosing the account that contains the resources you want to report on.
2. Run your chosen report script, giving the script the resource names and parameters it requires. See each report script for details.

For example, to count the number of pointers from X26 in the pointers table in the dev environment:

```
$ poetry run python ./scripts/count_pointers_for_custodian.py \
nhsd-nrlf--dev-pointers-table \
X26
```

### Running reports in the prod environment

The reports scripts may require resources that could affect the performance of the live production system. Because of this, it is recommended that you take steps to minimise this impact before running reports.

If you are running a report against the DynamoDB pointers table in prod, you should create a copy (or restore a PITR backup) of the table and run your report against the copy.

Please ensure any duplicated resource/data is deleted from the prod environment once you have finished using it.
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,21 @@ def test_create_document_reference_invalid_body():
"diagnostics": "Request body could not be parsed (status: Field required)",
"expression": ["status"],
},
{
"severity": "error",
"code": "invalid",
"details": {
"coding": [
{
"code": "MESSAGE_NOT_WELL_FORMED",
"display": "Message not well formed",
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
}
],
},
"diagnostics": "Request body could not be parsed (author: Field required)",
"expression": ["author"],
},
{
"severity": "error",
"code": "invalid",
Expand Down
1 change: 1 addition & 0 deletions api/producer/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,7 @@ components:
- resourceType
- status
- content
- author
Bundle:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,21 @@ def test_create_document_reference_invalid_body():
"diagnostics": "Request body could not be parsed (status: Field required)",
"expression": ["status"],
},
{
"severity": "error",
"code": "invalid",
"details": {
"coding": [
{
"code": "MESSAGE_NOT_WELL_FORMED",
"display": "Message not well formed",
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
}
],
},
"diagnostics": "Request body could not be parsed (author: Field required)",
"expression": ["author"],
},
{
"severity": "error",
"code": "invalid",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,21 @@ def test_upsert_document_reference_invalid_body():
"diagnostics": "Request body could not be parsed (status: Field required)",
"expression": ["status"],
},
{
"severity": "error",
"code": "invalid",
"details": {
"coding": [
{
"code": "MESSAGE_NOT_WELL_FORMED",
"display": "Message not well formed",
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
}
],
},
"diagnostics": "Request body could not be parsed (author: Field required)",
"expression": ["author"],
},
{
"severity": "error",
"code": "invalid",
Expand Down
2 changes: 1 addition & 1 deletion layer/nrlf/consumer/fhir/r4/model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: swagger.yaml
# timestamp: 2024-10-28T10:30:48+00:00
# timestamp: 2024-11-04T11:43:16+00:00

from __future__ import annotations

Expand Down
10 changes: 9 additions & 1 deletion layer/nrlf/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ class Source(Enum):

VALID_SOURCES = frozenset(item.value for item in Source.__members__.values())
EMPTY_VALUES = ("", None, [], {})
REQUIRED_CREATE_FIELDS = ["custodian", "id", "type", "status", "subject", "category"]
REQUIRED_CREATE_FIELDS = [
"custodian",
"id",
"type",
"status",
"subject",
"category",
"author",
]
JSON_TYPES = {dict, list}
NHS_NUMBER_INDEX = "idx_nhs_number_by_id"
ID_SEPARATOR = "-"
Expand Down
16 changes: 14 additions & 2 deletions layer/nrlf/core/errors.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
from typing import List, Optional

from pydantic import ValidationError
from pydantic_core import ErrorDetails

from nrlf.core.response import Response
from nrlf.core.types import CodeableConcept
from nrlf.producer.fhir.r4 import model as producer_model
from nrlf.producer.fhir.r4.model import OperationOutcome, OperationOutcomeIssue


def diag_for_error(error: ErrorDetails) -> str:
if error["loc"]:
return f"{error['loc'][0]}: {error['msg']}"
else:
return f"root: {error['msg']}"


def expression_for_error(error: ErrorDetails) -> Optional[str]:
return str(error["loc"][0] if error["loc"] else "root")


class OperationOutcomeError(Exception):
"""
Will instantly trigger an OperationOutcome error response when raised
Expand Down Expand Up @@ -59,8 +71,8 @@ def from_validation_error(
severity="error",
code="invalid",
details=details, # type: ignore
diagnostics=f"{msg} ({error['loc'][0]}: {error['msg']})",
expression=[str(error["loc"][0])], # type: ignore
diagnostics=f"{msg} ({diag_for_error(error)})",
expression=[expression_for_error(error)], # type: ignore
)
for error in exc.errors()
]
Expand Down
215 changes: 213 additions & 2 deletions layer/nrlf/core/tests/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import pytest

from nrlf.core.errors import OperationOutcomeError
from nrlf.core.request import parse_headers
from nrlf.core.errors import OperationOutcomeError, ParseError
from nrlf.core.request import parse_body, parse_headers
from nrlf.producer.fhir.r4.model import DocumentReference
from nrlf.tests.data import load_document_reference_data


def test_parse_headers_empty_headers():
Expand Down Expand Up @@ -129,3 +131,212 @@ def test_parse_headers_case_insensitive():
assert metadata.client_rp_details.developer_app_name == "TestApp"
assert metadata.client_rp_details.developer_app_id == "12345"
assert metadata.ods_code_parts == ("X26", "001")


def test_parse_body_no_model_no_body():
body = None
model = None

result = parse_body(model, body)

assert result is None


def test_parse_body_valid_docref():
model = DocumentReference
docref_body = load_document_reference_data("Y05868-736253002-Valid")

result = parse_body(model, docref_body)

assert isinstance(result, DocumentReference)


def test_parse_body_no_body():
model = DocumentReference
body = None

with pytest.raises(OperationOutcomeError) as error:
parse_body(model, body)

exc = error.value

assert exc.status_code == "400"
assert exc.operation_outcome.model_dump(exclude_none=True) == {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "invalid",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
"code": "BAD_REQUEST",
"display": "Bad request",
}
],
},
"diagnostics": "Request body is required",
}
],
}


def test_parse_body_invalid_docref_json():
model = DocumentReference
docref_body = load_document_reference_data("Y05868-736253002-Valid")

docref_body = docref_body.replace('unstructured"', "unstructured")

with pytest.raises(ParseError) as error:
parse_body(model, docref_body)

response = error.value.response.model_dump()

assert response["statusCode"] == "400"
assert json.loads(response["body"]) == {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "invalid",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
"code": "MESSAGE_NOT_WELL_FORMED",
"display": "Message not well formed",
}
],
},
"diagnostics": "Request body could not be parsed (root: Invalid JSON: control character (\\u0000-\\u001F) found while parsing a string at line 72 column 0)",
"expression": ["root"],
}
],
}


def test_parse_body_invalid_json():
model = DocumentReference
body = '{ "type": "is-not-a-docref" }'

with pytest.raises(ParseError) as error:
parse_body(model, body)

response = error.value.response.model_dump()

assert response["statusCode"] == "400"
assert json.loads(response["body"]) == {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "invalid",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
"code": "MESSAGE_NOT_WELL_FORMED",
"display": "Message not well formed",
}
]
},
"diagnostics": "Request body could not be parsed (resourceType: Field required)",
"expression": ["resourceType"],
},
{
"severity": "error",
"code": "invalid",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
"code": "MESSAGE_NOT_WELL_FORMED",
"display": "Message not well formed",
}
]
},
"diagnostics": "Request body could not be parsed (status: Field required)",
"expression": ["status"],
},
{
"severity": "error",
"code": "invalid",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
"code": "MESSAGE_NOT_WELL_FORMED",
"display": "Message not well formed",
}
]
},
"diagnostics": "Request body could not be parsed (type: Input should be an object)",
"expression": ["type"],
},
{
"code": "invalid",
"details": {
"coding": [
{
"code": "MESSAGE_NOT_WELL_FORMED",
"display": "Message not well formed",
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
},
],
},
"diagnostics": "Request body could not be parsed (author: Field required)",
"expression": [
"author",
],
"severity": "error",
},
{
"severity": "error",
"code": "invalid",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
"code": "MESSAGE_NOT_WELL_FORMED",
"display": "Message not well formed",
}
]
},
"diagnostics": "Request body could not be parsed (content: Field required)",
"expression": ["content"],
},
],
}


def test_parse_body_not_json():
model = DocumentReference
body = "is not json"

with pytest.raises(ParseError) as error:
parse_body(model, body)

response = error.value.response

assert response.statusCode == "400"
assert json.loads(response.body) == {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "invalid",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
"code": "MESSAGE_NOT_WELL_FORMED",
"display": "Message not well formed",
}
]
},
"diagnostics": "Request body could not be parsed (root: Invalid JSON: expected value at line 1 column 1)",
"expression": ["root"],
}
],
}
Loading
Loading