From 7affc4c739aafcc408430cd3fc09052071f7f286 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Thu, 19 Dec 2024 15:28:25 +0000 Subject: [PATCH 01/10] NRL-786 new parent class that does not allow extra fields for pydantic models --- Makefile | 6 +- layer/nrlf/consumer/fhir/r4/model.py | 60 ++++++++++--------- layer/nrlf/core/parent_model.py | 5 ++ layer/nrlf/core/tests/test_validators.py | 41 +++++++++++-- layer/nrlf/core/validators.py | 24 -------- layer/nrlf/producer/fhir/r4/model.py | 58 +++++++++--------- layer/nrlf/producer/fhir/r4/strict_model.py | 57 +++++++++--------- .../createDocumentReference-failure.feature | 60 +++++++++++++++++++ 8 files changed, 195 insertions(+), 116 deletions(-) create mode 100644 layer/nrlf/core/parent_model.py diff --git a/Makefile b/Makefile index bf2003ca6..cc927c892 100644 --- a/Makefile +++ b/Makefile @@ -198,18 +198,22 @@ generate-models: check-warn ## Generate Pydantic Models --input ./api/producer/swagger.yaml \ --input-file-type openapi \ --output ./layer/nrlf/producer/fhir/r4/model.py \ - --output-model-type "pydantic_v2.BaseModel" + --output-model-type "pydantic_v2.BaseModel" \ + --base-class layer.nrlf.core.parent_model.Parent poetry run datamodel-codegen \ --strict-types {str,bytes,int,float,bool} \ --input ./api/producer/swagger.yaml \ --input-file-type openapi \ --output ./layer/nrlf/producer/fhir/r4/strict_model.py \ + --base-class layer.nrlf.core.parent_model.Parent \ --output-model-type "pydantic_v2.BaseModel" + @echo "Generating consumer model" mkdir -p ./layer/nrlf/consumer/fhir/r4 poetry run datamodel-codegen \ --input ./api/consumer/swagger.yaml \ --input-file-type openapi \ --output ./layer/nrlf/consumer/fhir/r4/model.py \ + --base-class layer.nrlf.core.parent_model.Parent \ --output-model-type "pydantic_v2.BaseModel" diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 2fd597b42..70f60e88d 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,12 +1,14 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-13T11:19:30+00:00 +# timestamp: 2024-12-19T15:23:29+00:00 from __future__ import annotations from typing import Annotated, List, Literal, Optional -from pydantic import BaseModel, Field, RootModel +from pydantic import Field, RootModel + +from layer.nrlf.core.parent_model import Parent class LocationItem(RootModel[str]): @@ -29,7 +31,7 @@ class ExpressionItem(RootModel[str]): ] -class BundleEntryRequest(BaseModel): +class BundleEntryRequest(Parent): id: Annotated[ Optional[str], Field( @@ -81,7 +83,7 @@ class BundleEntryRequest(BaseModel): ] = None -class BundleEntrySearch(BaseModel): +class BundleEntrySearch(Parent): id: Annotated[ Optional[str], Field( @@ -104,7 +106,7 @@ class BundleEntrySearch(BaseModel): ] = None -class BundleLink(BaseModel): +class BundleLink(Parent): id: Annotated[ Optional[str], Field( @@ -124,7 +126,7 @@ class BundleLink(BaseModel): ] -class Attachment(BaseModel): +class Attachment(Parent): id: Annotated[ Optional[str], Field( @@ -186,7 +188,7 @@ class Attachment(BaseModel): ] = None -class Coding(BaseModel): +class Coding(Parent): id: Annotated[ Optional[str], Field( @@ -253,7 +255,7 @@ class NRLFormatCode(Coding): ] -class Period(BaseModel): +class Period(Parent): id: Annotated[ Optional[str], Field( @@ -277,7 +279,7 @@ class Period(BaseModel): ] = None -class Quantity(BaseModel): +class Quantity(Parent): id: Annotated[ Optional[str], Field( @@ -331,7 +333,7 @@ class ProfileItem(RootModel[str]): ] -class Meta(BaseModel): +class Meta(Parent): id: Annotated[ Optional[str], Field( @@ -365,7 +367,7 @@ class Meta(BaseModel): tag: Optional[List[Coding]] = None -class Narrative(BaseModel): +class Narrative(Parent): id: Annotated[ Optional[str], Field( @@ -392,7 +394,7 @@ class DocumentId(RootModel[str]): root: Annotated[str, Field(pattern="[A-Za-z0-9\\-\\.]{1,64}")] -class RequestPathParams(BaseModel): +class RequestPathParams(Parent): id: DocumentId @@ -450,7 +452,7 @@ class RequestHeaderCorrelationId(RootModel[str]): root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] -class CodeableConcept(BaseModel): +class CodeableConcept(Parent): id: Annotated[ Optional[str], Field( @@ -468,7 +470,7 @@ class CodeableConcept(BaseModel): ] = None -class Extension(BaseModel): +class Extension(Parent): valueCodeableConcept: Annotated[ Optional[CodeableConcept], Field( @@ -487,11 +489,11 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] -class RequestHeader(BaseModel): +class RequestHeader(Parent): odsCode: RequestHeaderOdsCode -class RequestParams(BaseModel): +class RequestParams(Parent): subject_identifier: Annotated[ RequestQuerySubject, Field(alias="subject:identifier") ] @@ -505,13 +507,13 @@ class RequestParams(BaseModel): ] = None -class CountRequestParams(BaseModel): +class CountRequestParams(Parent): subject_identifier: Annotated[ RequestQuerySubject, Field(alias="subject:identifier") ] -class OperationOutcomeIssue(BaseModel): +class OperationOutcomeIssue(Parent): id: Annotated[ Optional[str], Field( @@ -557,7 +559,7 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept -class OperationOutcome(BaseModel): +class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ Optional[str], @@ -595,7 +597,7 @@ class OperationOutcome(BaseModel): issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)] -class DocumentReferenceContent(BaseModel): +class DocumentReferenceContent(Parent): id: Annotated[ Optional[str], Field( @@ -620,7 +622,7 @@ class DocumentReferenceContent(BaseModel): ] -class DocumentReference(BaseModel): +class DocumentReference(Parent): resourceType: Literal["DocumentReference"] id: Annotated[ Optional[str], @@ -721,7 +723,7 @@ class DocumentReference(BaseModel): ] = None -class Bundle(BaseModel): +class Bundle(Parent): resourceType: Literal["Bundle"] id: Annotated[ Optional[str], @@ -786,7 +788,7 @@ class Bundle(BaseModel): ] = None -class BundleEntry(BaseModel): +class BundleEntry(Parent): id: Annotated[ Optional[str], Field( @@ -828,7 +830,7 @@ class BundleEntry(BaseModel): ] = None -class BundleEntryResponse(BaseModel): +class BundleEntryResponse(Parent): id: Annotated[ Optional[str], Field( @@ -872,7 +874,7 @@ class BundleEntryResponse(BaseModel): ] = None -class DocumentReferenceContext(BaseModel): +class DocumentReferenceContext(Parent): id: Annotated[ Optional[str], Field( @@ -907,7 +909,7 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceRelatesTo(BaseModel): +class DocumentReferenceRelatesTo(Parent): id: Annotated[ Optional[str], Field( @@ -927,7 +929,7 @@ class DocumentReferenceRelatesTo(BaseModel): ] -class Identifier(BaseModel): +class Identifier(Parent): id: Annotated[ Optional[str], Field( @@ -972,7 +974,7 @@ class Identifier(BaseModel): ] = None -class Reference(BaseModel): +class Reference(Parent): id: Annotated[ Optional[str], Field( @@ -1009,7 +1011,7 @@ class Reference(BaseModel): ] = None -class Signature(BaseModel): +class Signature(Parent): id: Annotated[ Optional[str], Field( diff --git a/layer/nrlf/core/parent_model.py b/layer/nrlf/core/parent_model.py new file mode 100644 index 000000000..f821e1a51 --- /dev/null +++ b/layer/nrlf/core/parent_model.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel, ConfigDict + + +class Parent(BaseModel): + model_config = ConfigDict(regex_engine="python-re", extra="forbid") diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 4b13e5d56..66a2b727e 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -285,12 +285,40 @@ def test_validate_document_reference_extra_fields(): document_ref_data["extra_field"] = "extra_value" - result = validator.validate(document_ref_data) + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) - assert result.is_valid is False - assert result.resource.id == "Y05868-99999-99999-999999" - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (extra_field: Extra inputs are not permitted)", + "expression": ["extra_field"], + } + + +def test_validate_document_reference_extra_fields_content(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["extra_field"] = "extra_value" + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { "severity": "error", "code": "invalid", "details": { @@ -302,7 +330,8 @@ def test_validate_document_reference_extra_fields(): } ] }, - "diagnostics": "The resource contains extra fields", + "diagnostics": "Failed to parse DocumentReference resource (content[0].extra_field: Extra inputs are not permitted)", + "expression": ["content[0].extra_field"], } diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index cb8140f1e..94c086afa 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -129,7 +129,6 @@ def validate(self, data: Dict[str, Any] | DocumentReference): try: self._validate_required_fields(resource) - self._validate_no_extra_fields(resource, data) self._validate_identifiers(resource) self._validate_relates_to(resource) self._validate_ssp_asid(resource) @@ -174,29 +173,6 @@ def _validate_required_fields(self, model: DocumentReference): if not self.result.is_valid: raise StopValidationError() - def _validate_no_extra_fields( - self, resource: DocumentReference, data: Dict[str, Any] | DocumentReference - ): - """ - Validate that there are no extra fields - """ - logger.log(LogReference.VALIDATOR001, step="no_extra_fields") - has_extra_fields = False - - if isinstance(data, DocumentReference): - has_extra_fields = ( - len(set(resource.__dict__) - set(resource.model_fields)) > 0 - ) - else: - has_extra_fields = data != resource.model_dump(exclude_none=True) - - if has_extra_fields: - self.result.add_error( - issue_code="invalid", - error_code="INVALID_RESOURCE", - diagnostics="The resource contains extra fields", - ) - def _validate_identifiers(self, model: DocumentReference): """ """ logger.log(LogReference.VALIDATOR001, step="identifiers") diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 945b220ec..a381dc3b6 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,12 +1,14 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-13T11:19:26+00:00 +# timestamp: 2024-12-19T15:23:23+00:00 from __future__ import annotations from typing import Annotated, List, Literal, Optional -from pydantic import BaseModel, ConfigDict, Field, RootModel +from pydantic import ConfigDict, Field, RootModel + +from layer.nrlf.core.parent_model import Parent class LocationItem(RootModel[str]): @@ -29,7 +31,7 @@ class ExpressionItem(RootModel[str]): ] -class BundleEntryRequest(BaseModel): +class BundleEntryRequest(Parent): id: Annotated[ Optional[str], Field( @@ -81,7 +83,7 @@ class BundleEntryRequest(BaseModel): ] = None -class BundleEntrySearch(BaseModel): +class BundleEntrySearch(Parent): id: Annotated[ Optional[str], Field( @@ -104,7 +106,7 @@ class BundleEntrySearch(BaseModel): ] = None -class BundleLink(BaseModel): +class BundleLink(Parent): id: Annotated[ Optional[str], Field( @@ -124,7 +126,7 @@ class BundleLink(BaseModel): ] -class Attachment(BaseModel): +class Attachment(Parent): id: Annotated[ Optional[str], Field( @@ -186,7 +188,7 @@ class Attachment(BaseModel): ] = None -class Coding(BaseModel): +class Coding(Parent): id: Annotated[ Optional[str], Field( @@ -253,7 +255,7 @@ class NRLFormatCode(Coding): ] -class Period(BaseModel): +class Period(Parent): id: Annotated[ Optional[str], Field( @@ -277,7 +279,7 @@ class Period(BaseModel): ] = None -class Quantity(BaseModel): +class Quantity(Parent): id: Annotated[ Optional[str], Field( @@ -331,7 +333,7 @@ class ProfileItem(RootModel[str]): ] -class Meta(BaseModel): +class Meta(Parent): id: Annotated[ Optional[str], Field( @@ -365,7 +367,7 @@ class Meta(BaseModel): tag: Optional[List[Coding]] = None -class Narrative(BaseModel): +class Narrative(Parent): id: Annotated[ Optional[str], Field( @@ -392,7 +394,7 @@ class DocumentId(RootModel[str]): root: Annotated[str, Field(pattern="[A-Za-z0-9\\-\\.]{1,64}")] -class RequestPathParams(BaseModel): +class RequestPathParams(Parent): id: DocumentId @@ -440,7 +442,7 @@ class RequestHeaderCorrelationId(RootModel[str]): root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] -class CodeableConcept(BaseModel): +class CodeableConcept(Parent): id: Annotated[ Optional[str], Field( @@ -458,7 +460,7 @@ class CodeableConcept(BaseModel): ] = None -class Extension(BaseModel): +class Extension(Parent): valueCodeableConcept: Annotated[ Optional[CodeableConcept], Field( @@ -477,11 +479,11 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] -class RequestHeader(BaseModel): +class RequestHeader(Parent): odsCode: RequestHeaderOdsCode -class RequestParams(BaseModel): +class RequestParams(Parent): subject_identifier: Annotated[ Optional[RequestQuerySubject], Field(alias="subject:identifier") ] = None @@ -492,7 +494,7 @@ class RequestParams(BaseModel): ] = None -class OperationOutcomeIssue(BaseModel): +class OperationOutcomeIssue(Parent): id: Annotated[ Optional[str], Field( @@ -538,7 +540,7 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept -class OperationOutcome(BaseModel): +class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ Optional[str], @@ -576,7 +578,7 @@ class OperationOutcome(BaseModel): issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)] -class DocumentReferenceContent(BaseModel): +class DocumentReferenceContent(Parent): id: Annotated[ Optional[str], Field( @@ -601,7 +603,7 @@ class DocumentReferenceContent(BaseModel): ] -class DocumentReference(BaseModel): +class DocumentReference(Parent): model_config = ConfigDict( regex_engine="python-re", ) @@ -705,7 +707,7 @@ class DocumentReference(BaseModel): ] -class Bundle(BaseModel): +class Bundle(Parent): resourceType: Literal["Bundle"] id: Annotated[ Optional[str], @@ -770,7 +772,7 @@ class Bundle(BaseModel): ] = None -class BundleEntry(BaseModel): +class BundleEntry(Parent): id: Annotated[ Optional[str], Field( @@ -812,7 +814,7 @@ class BundleEntry(BaseModel): ] = None -class BundleEntryResponse(BaseModel): +class BundleEntryResponse(Parent): id: Annotated[ Optional[str], Field( @@ -856,7 +858,7 @@ class BundleEntryResponse(BaseModel): ] = None -class DocumentReferenceContext(BaseModel): +class DocumentReferenceContext(Parent): id: Annotated[ Optional[str], Field( @@ -891,7 +893,7 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceRelatesTo(BaseModel): +class DocumentReferenceRelatesTo(Parent): id: Annotated[ Optional[str], Field( @@ -911,7 +913,7 @@ class DocumentReferenceRelatesTo(BaseModel): ] -class Identifier(BaseModel): +class Identifier(Parent): id: Annotated[ Optional[str], Field( @@ -956,7 +958,7 @@ class Identifier(BaseModel): ] = None -class Reference(BaseModel): +class Reference(Parent): id: Annotated[ Optional[str], Field( @@ -993,7 +995,7 @@ class Reference(BaseModel): ] = None -class Signature(BaseModel): +class Signature(Parent): id: Annotated[ Optional[str], Field( diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index 0344821fc..6244b6900 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,13 +1,12 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-13T11:19:28+00:00 +# timestamp: 2024-12-19T15:23:26+00:00 from __future__ import annotations from typing import Annotated, List, Literal, Optional from pydantic import ( - BaseModel, ConfigDict, Field, RootModel, @@ -17,6 +16,8 @@ StrictStr, ) +from layer.nrlf.core.parent_model import Parent + class LocationItem(RootModel[StrictStr]): root: Annotated[ @@ -36,7 +37,7 @@ class ExpressionItem(RootModel[StrictStr]): ] -class BundleEntryRequest(BaseModel): +class BundleEntryRequest(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -81,7 +82,7 @@ class BundleEntryRequest(BaseModel): ] = None -class BundleEntrySearch(BaseModel): +class BundleEntrySearch(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -102,7 +103,7 @@ class BundleEntrySearch(BaseModel): ] = None -class BundleLink(BaseModel): +class BundleLink(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -118,7 +119,7 @@ class BundleLink(BaseModel): url: Annotated[StrictStr, Field(description="The reference details for the link.")] -class Attachment(BaseModel): +class Attachment(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -168,7 +169,7 @@ class Attachment(BaseModel): ] = None -class Coding(BaseModel): +class Coding(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -230,7 +231,7 @@ class NRLFormatCode(Coding): ] -class Period(BaseModel): +class Period(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -249,7 +250,7 @@ class Period(BaseModel): ] = None -class Quantity(BaseModel): +class Quantity(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -295,7 +296,7 @@ class ProfileItem(RootModel[StrictStr]): ] -class Meta(BaseModel): +class Meta(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -325,7 +326,7 @@ class Meta(BaseModel): tag: Optional[List[Coding]] = None -class Narrative(BaseModel): +class Narrative(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -350,7 +351,7 @@ class DocumentId(RootModel[StrictStr]): root: StrictStr -class RequestPathParams(BaseModel): +class RequestPathParams(Parent): id: DocumentId @@ -388,7 +389,7 @@ class RequestHeaderCorrelationId(RootModel[StrictStr]): root: Annotated[StrictStr, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] -class CodeableConcept(BaseModel): +class CodeableConcept(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -404,7 +405,7 @@ class CodeableConcept(BaseModel): ] = None -class Extension(BaseModel): +class Extension(Parent): valueCodeableConcept: Annotated[ Optional[CodeableConcept], Field( @@ -422,11 +423,11 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] -class RequestHeader(BaseModel): +class RequestHeader(Parent): odsCode: RequestHeaderOdsCode -class RequestParams(BaseModel): +class RequestParams(Parent): subject_identifier: Annotated[ Optional[RequestQuerySubject], Field(alias="subject:identifier") ] = None @@ -437,7 +438,7 @@ class RequestParams(BaseModel): ] = None -class OperationOutcomeIssue(BaseModel): +class OperationOutcomeIssue(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -477,7 +478,7 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept -class OperationOutcome(BaseModel): +class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ Optional[StrictStr], @@ -510,7 +511,7 @@ class OperationOutcome(BaseModel): issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)] -class DocumentReferenceContent(BaseModel): +class DocumentReferenceContent(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -534,7 +535,7 @@ class DocumentReferenceContent(BaseModel): ] -class DocumentReference(BaseModel): +class DocumentReference(Parent): model_config = ConfigDict( regex_engine="python-re", ) @@ -624,7 +625,7 @@ class DocumentReference(BaseModel): ] -class Bundle(BaseModel): +class Bundle(Parent): resourceType: Literal["Bundle"] id: Annotated[ Optional[StrictStr], @@ -682,7 +683,7 @@ class Bundle(BaseModel): ] = None -class BundleEntry(BaseModel): +class BundleEntry(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -722,7 +723,7 @@ class BundleEntry(BaseModel): ] = None -class BundleEntryResponse(BaseModel): +class BundleEntryResponse(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -761,7 +762,7 @@ class BundleEntryResponse(BaseModel): ] = None -class DocumentReferenceContext(BaseModel): +class DocumentReferenceContext(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -795,7 +796,7 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceRelatesTo(BaseModel): +class DocumentReferenceRelatesTo(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -813,7 +814,7 @@ class DocumentReferenceRelatesTo(BaseModel): ] -class Identifier(BaseModel): +class Identifier(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -851,7 +852,7 @@ class Identifier(BaseModel): ] = None -class Reference(BaseModel): +class Reference(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -884,7 +885,7 @@ class Reference(BaseModel): ] = None -class Signature(BaseModel): +class Signature(Parent): id: Annotated[ Optional[StrictStr], Field( diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index af22ae1bc..4bc5aad48 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -900,3 +900,63 @@ Feature: Producer - createDocumentReference - Failure Scenarios ] } """ + + Scenario: Invalid content has extra field + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 1363501000000100 | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "someContact.co.uk" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extra_field": "hello", + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource" + } + ] + }, + "diagnostics": "Invalid content format code: urn:nhs-ic:unstructured format code must be 'urn:nhs-ic:record-contact' for Contact details attachments.", + "expression": [ + "content[0].format.code" + ] + } + """ From 2f6eac9a53dee48e897c4f870629ec3126c2860b Mon Sep 17 00:00:00 2001 From: eesa456 Date: Thu, 19 Dec 2024 15:33:52 +0000 Subject: [PATCH 02/10] NRL-786 fix test --- .../tests/test_update_document_reference.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/producer/updateDocumentReference/tests/test_update_document_reference.py b/api/producer/updateDocumentReference/tests/test_update_document_reference.py index 97b32207a..6825fe6d6 100644 --- a/api/producer/updateDocumentReference/tests/test_update_document_reference.py +++ b/api/producer/updateDocumentReference/tests/test_update_document_reference.py @@ -562,7 +562,6 @@ def test_update_document_reference_immutable_fields(repository): ) ], text=None, - extension=None, ) event = create_test_api_gateway_event( From 7be77386b2c5019593044165bb31b6bc3d6ee616 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Thu, 19 Dec 2024 16:04:03 +0000 Subject: [PATCH 03/10] NRL-786 update import to remove layer --- Makefile | 6 +++--- layer/nrlf/consumer/fhir/r4/model.py | 4 ++-- layer/nrlf/producer/fhir/r4/model.py | 4 ++-- layer/nrlf/producer/fhir/r4/strict_model.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index cc927c892..af0ba0a71 100644 --- a/Makefile +++ b/Makefile @@ -199,13 +199,13 @@ generate-models: check-warn ## Generate Pydantic Models --input-file-type openapi \ --output ./layer/nrlf/producer/fhir/r4/model.py \ --output-model-type "pydantic_v2.BaseModel" \ - --base-class layer.nrlf.core.parent_model.Parent + --base-class nrlf.core.parent_model.Parent poetry run datamodel-codegen \ --strict-types {str,bytes,int,float,bool} \ --input ./api/producer/swagger.yaml \ --input-file-type openapi \ --output ./layer/nrlf/producer/fhir/r4/strict_model.py \ - --base-class layer.nrlf.core.parent_model.Parent \ + --base-class nrlf.core.parent_model.Parent \ --output-model-type "pydantic_v2.BaseModel" @@ -215,5 +215,5 @@ generate-models: check-warn ## Generate Pydantic Models --input ./api/consumer/swagger.yaml \ --input-file-type openapi \ --output ./layer/nrlf/consumer/fhir/r4/model.py \ - --base-class layer.nrlf.core.parent_model.Parent \ + --base-class nrlf.core.parent_model.Parent \ --output-model-type "pydantic_v2.BaseModel" diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 70f60e88d..ea7cf1dd9 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-19T15:23:29+00:00 +# timestamp: 2024-12-19T16:03:12+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from pydantic import Field, RootModel -from layer.nrlf.core.parent_model import Parent +from nrlf.core.parent_model import Parent class LocationItem(RootModel[str]): diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index a381dc3b6..1b9ea6a38 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-19T15:23:23+00:00 +# timestamp: 2024-12-19T16:03:08+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from pydantic import ConfigDict, Field, RootModel -from layer.nrlf.core.parent_model import Parent +from nrlf.core.parent_model import Parent class LocationItem(RootModel[str]): diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index 6244b6900..29bf85721 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-19T15:23:26+00:00 +# timestamp: 2024-12-19T16:03:10+00:00 from __future__ import annotations @@ -16,7 +16,7 @@ StrictStr, ) -from layer.nrlf.core.parent_model import Parent +from nrlf.core.parent_model import Parent class LocationItem(RootModel[StrictStr]): From d50ae24dfeadfbea6351e3331fb7dc0c4d35697d Mon Sep 17 00:00:00 2001 From: eesa456 Date: Thu, 19 Dec 2024 16:12:20 +0000 Subject: [PATCH 04/10] NRL-786 fix error message --- .../producer/createDocumentReference-failure.feature | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 4bc5aad48..ff59663a9 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -944,19 +944,19 @@ Feature: Producer - createDocumentReference - Failure Scenarios """ { "severity": "error", - "code": "value", + "code": "invalid", "details": { "coding": [ { "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource" + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed" } ] }, - "diagnostics": "Invalid content format code: urn:nhs-ic:unstructured format code must be 'urn:nhs-ic:record-contact' for Contact details attachments.", + "diagnostics": "Request body could not be parsed (content[0].extra_field: Extra inputs are not permitted)", "expression": [ - "content[0].format.code" + "content[0].extra_field" ] } """ From 93025f14b0bfc0ae203f5a754c37e1ebfbeb4ec7 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Fri, 20 Dec 2024 09:38:43 +0000 Subject: [PATCH 05/10] NRL-786 fix warning in test --- .../tests/test_search_document_reference_consumer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py b/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py index 88d1757c8..9643dad18 100644 --- a/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py +++ b/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py @@ -3,6 +3,7 @@ from moto import mock_aws from api.consumer.searchDocumentReference.search_document_reference import handler +from nrlf.consumer.fhir.r4.model import CodeableConcept, Identifier from nrlf.core.constants import ( CATEGORY_ATTRIBUTES, TYPE_ATTRIBUTES, @@ -66,7 +67,9 @@ def test_search_document_reference_accession_number_in_pointer( ): doc_ref = load_document_reference("Y05868-736253002-Valid") doc_ref.identifier = [ - {"type": {"text": "Accession-Number"}, "value": "Y05868.123456789"} + Identifier( + type=CodeableConcept(text="Accession-Number"), value="Y05868.123456789" + ) ] doc_pointer = DocumentPointer.from_document_reference(doc_ref) repository.create(doc_pointer) From a4b10ac120c516d2a315e6bf547f862fc4e270b9 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Fri, 7 Feb 2025 14:05:08 +0000 Subject: [PATCH 06/10] NRL-786 add extension to parent model --- layer/nrlf/core/parent_model.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/layer/nrlf/core/parent_model.py b/layer/nrlf/core/parent_model.py index f821e1a51..155c7f5dc 100644 --- a/layer/nrlf/core/parent_model.py +++ b/layer/nrlf/core/parent_model.py @@ -1,5 +1,25 @@ -from pydantic import BaseModel, ConfigDict +from typing import Annotated, List, Optional + +from consumer.fhir.r4.model import CodeableConcept +from pydantic import BaseModel, ConfigDict, Field + + +class Extension(BaseModel): + model_config = ConfigDict(regex_engine="python-re", extra="forbid") + valueCodeableConcept: Annotated[ + Optional[CodeableConcept], + Field( + description="A name which details the functional use for this link – see [http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1](http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1)." + ), + ] = None + url: Annotated[ + Optional[str], + Field(description="The reference details for the link.", pattern="\\S*"), + ] = None class Parent(BaseModel): model_config = ConfigDict(regex_engine="python-re", extra="forbid") + extension: Annotated[ + Optional[List[Extension]], Field(description="A list of relevant extensions") + ] From 5b00fcfbe6e92eb675760f890bf1f56f85eaeb3e Mon Sep 17 00:00:00 2001 From: eesa456 Date: Fri, 7 Feb 2025 14:12:45 +0000 Subject: [PATCH 07/10] NRL-786 update model --- layer/nrlf/consumer/fhir/r4/model.py | 2 +- layer/nrlf/producer/fhir/r4/model.py | 6 +++--- layer/nrlf/producer/fhir/r4/strict_model.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index ea7cf1dd9..5f9b911a6 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-19T16:03:12+00:00 +# timestamp: 2025-02-07T14:10:39+00:00 from __future__ import annotations diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 6af7da1aa..715a7e12e 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-19T16:03:08+00:00 +# timestamp: 2025-02-07T14:10:35+00:00 from __future__ import annotations @@ -232,7 +232,7 @@ class Coding(Parent): ] = None -class NRLCoding(BaseModel): +class NRLCoding(Parent): id: Annotated[ Optional[str], Field( @@ -504,7 +504,7 @@ class CodeableConcept(Parent): ] = None -class NRLCodeableConcept(BaseModel): +class NRLCodeableConcept(Parent): id: Annotated[ Optional[str], Field( diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index 06e82f760..d7849f154 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-19T16:03:10+00:00 +# timestamp: 2025-02-07T14:10:37+00:00 from __future__ import annotations @@ -208,7 +208,7 @@ class Coding(Parent): ] = None -class NRLCoding(BaseModel): +class NRLCoding(Parent): id: Annotated[ Optional[StrictStr], Field( From ccbdfab81b2b7209dec3c2c505dfb2f8147952ef Mon Sep 17 00:00:00 2001 From: eesa456 Date: Fri, 7 Feb 2025 14:21:43 +0000 Subject: [PATCH 08/10] NRL-786 update import --- layer/nrlf/core/parent_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/layer/nrlf/core/parent_model.py b/layer/nrlf/core/parent_model.py index 155c7f5dc..eaf97be0a 100644 --- a/layer/nrlf/core/parent_model.py +++ b/layer/nrlf/core/parent_model.py @@ -1,8 +1,9 @@ from typing import Annotated, List, Optional -from consumer.fhir.r4.model import CodeableConcept from pydantic import BaseModel, ConfigDict, Field +from nrlf.consumer.fhir.r4.model import CodeableConcept + class Extension(BaseModel): model_config = ConfigDict(regex_engine="python-re", extra="forbid") From 03e79f5ae0ab5c62eebec050b389a73bb894babf Mon Sep 17 00:00:00 2001 From: eesa456 Date: Fri, 7 Feb 2025 14:34:03 +0000 Subject: [PATCH 09/10] NRL-786 add other classes needed --- layer/nrlf/core/parent_model.py | 64 ++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/layer/nrlf/core/parent_model.py b/layer/nrlf/core/parent_model.py index eaf97be0a..11376b216 100644 --- a/layer/nrlf/core/parent_model.py +++ b/layer/nrlf/core/parent_model.py @@ -2,7 +2,69 @@ from pydantic import BaseModel, ConfigDict, Field -from nrlf.consumer.fhir.r4.model import CodeableConcept + +class Coding(BaseModel): + model_config = ConfigDict(regex_engine="python-re", extra="forbid") + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + system: Annotated[ + Optional[str], + Field( + description="The identification of the code system that defines the meaning of the symbol in the code.", + pattern="\\S*", + ), + ] = None + version: Annotated[ + Optional[str], + Field( + description="The version of the code system which was used when choosing this code. Note that a well–maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged.", + pattern="[ \\r\\n\\t\\S]+", + ), + ] = None + code: Annotated[ + Optional[str], + Field( + description="A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post–coordination).", + pattern="[^\\s]+(\\s[^\\s]+)*", + ), + ] = None + display: Annotated[ + Optional[str], + Field( + description="A representation of the meaning of the code in the system, following the rules of the system.", + pattern="[ \\r\\n\\t\\S]+", + ), + ] = None + userSelected: Annotated[ + Optional[bool], + Field( + description="Indicates that this coding was chosen by a user directly – e.g. off a pick list of available items (codes or displays)." + ), + ] = None + + +class CodeableConcept(BaseModel): + model_config = ConfigDict(regex_engine="python-re", extra="forbid") + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + coding: Optional[List[Coding]] = None + text: Annotated[ + Optional[str], + Field( + description="A human language representation of the concept as seen/selected/uttered by the user who entered the data and/or which represents the intended meaning of the user.", + pattern="[ \\r\\n\\t\\S]+", + ), + ] = None class Extension(BaseModel): From ec405a5fbff8d8def0aae399b4336a3c2ed6b404 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Fri, 7 Feb 2025 14:57:47 +0000 Subject: [PATCH 10/10] NRL-786 fix tests --- layer/nrlf/core/parent_model.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/layer/nrlf/core/parent_model.py b/layer/nrlf/core/parent_model.py index 11376b216..3c18be72c 100644 --- a/layer/nrlf/core/parent_model.py +++ b/layer/nrlf/core/parent_model.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, ConfigDict, Field -class Coding(BaseModel): +class ParentCoding(BaseModel): model_config = ConfigDict(regex_engine="python-re", extra="forbid") id: Annotated[ Optional[str], @@ -48,7 +48,7 @@ class Coding(BaseModel): ] = None -class CodeableConcept(BaseModel): +class ParentCodeableConcept(BaseModel): model_config = ConfigDict(regex_engine="python-re", extra="forbid") id: Annotated[ Optional[str], @@ -57,7 +57,7 @@ class CodeableConcept(BaseModel): pattern="[A-Za-z0-9\\-\\.]{1,64}", ), ] = None - coding: Optional[List[Coding]] = None + coding: Optional[List[ParentCoding]] = None text: Annotated[ Optional[str], Field( @@ -67,10 +67,9 @@ class CodeableConcept(BaseModel): ] = None -class Extension(BaseModel): - model_config = ConfigDict(regex_engine="python-re", extra="forbid") +class ParentExtension(BaseModel): valueCodeableConcept: Annotated[ - Optional[CodeableConcept], + Optional[ParentCodeableConcept], Field( description="A name which details the functional use for this link – see [http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1](http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1)." ), @@ -84,5 +83,6 @@ class Extension(BaseModel): class Parent(BaseModel): model_config = ConfigDict(regex_engine="python-re", extra="forbid") extension: Annotated[ - Optional[List[Extension]], Field(description="A list of relevant extensions") - ] + Optional[List[ParentExtension]], + Field(description="A list of relevant extensions"), + ] = None