Skip to content

Commit 57c177f

Browse files
authored
Merge pull request #781 from NHSDigital/feature/eema1-NRL-786-rejectExtraFields
NRL-786 new parent class that does not allow extra fields for pydantic models
2 parents c2f2340 + f26da9f commit 57c177f

File tree

10 files changed

+286
-122
lines changed

10 files changed

+286
-122
lines changed

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,18 +198,22 @@ generate-models: check-warn ## Generate Pydantic Models
198198
--input ./api/producer/swagger.yaml \
199199
--input-file-type openapi \
200200
--output ./layer/nrlf/producer/fhir/r4/model.py \
201-
--output-model-type "pydantic_v2.BaseModel"
201+
--output-model-type "pydantic_v2.BaseModel" \
202+
--base-class nrlf.core.parent_model.Parent
202203
poetry run datamodel-codegen \
203204
--strict-types {str,bytes,int,float,bool} \
204205
--input ./api/producer/swagger.yaml \
205206
--input-file-type openapi \
206207
--output ./layer/nrlf/producer/fhir/r4/strict_model.py \
208+
--base-class nrlf.core.parent_model.Parent \
207209
--output-model-type "pydantic_v2.BaseModel"
208210

211+
209212
@echo "Generating consumer model"
210213
mkdir -p ./layer/nrlf/consumer/fhir/r4
211214
poetry run datamodel-codegen \
212215
--input ./api/consumer/swagger.yaml \
213216
--input-file-type openapi \
214217
--output ./layer/nrlf/consumer/fhir/r4/model.py \
218+
--base-class nrlf.core.parent_model.Parent \
215219
--output-model-type "pydantic_v2.BaseModel"

api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from moto import mock_aws
44

55
from api.consumer.searchDocumentReference.search_document_reference import handler
6+
from nrlf.consumer.fhir.r4.model import CodeableConcept, Identifier
67
from nrlf.core.constants import (
78
CATEGORY_ATTRIBUTES,
89
TYPE_ATTRIBUTES,
@@ -66,7 +67,9 @@ def test_search_document_reference_accession_number_in_pointer(
6667
):
6768
doc_ref = load_document_reference("Y05868-736253002-Valid")
6869
doc_ref.identifier = [
69-
{"type": {"text": "Accession-Number"}, "value": "Y05868.123456789"}
70+
Identifier(
71+
type=CodeableConcept(text="Accession-Number"), value="Y05868.123456789"
72+
)
7073
]
7174
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
7275
repository.create(doc_pointer)

api/producer/updateDocumentReference/tests/test_update_document_reference.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,6 @@ def test_update_document_reference_immutable_fields(repository):
592592
)
593593
],
594594
text=None,
595-
extension=None,
596595
)
597596

598597
event = create_test_api_gateway_event(

layer/nrlf/consumer/fhir/r4/model.py

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# generated by datamodel-codegen:
22
# filename: swagger.yaml
3-
# timestamp: 2025-01-27T09:26:33+00:00
3+
# timestamp: 2025-02-07T14:10:39+00:00
44

55
from __future__ import annotations
66

77
from typing import Annotated, List, Literal, Optional
88

9-
from pydantic import BaseModel, Field, RootModel
9+
from pydantic import Field, RootModel
10+
11+
from nrlf.core.parent_model import Parent
1012

1113

1214
class LocationItem(RootModel[str]):
@@ -29,7 +31,7 @@ class ExpressionItem(RootModel[str]):
2931
]
3032

3133

32-
class BundleEntryRequest(BaseModel):
34+
class BundleEntryRequest(Parent):
3335
id: Annotated[
3436
Optional[str],
3537
Field(
@@ -81,7 +83,7 @@ class BundleEntryRequest(BaseModel):
8183
] = None
8284

8385

84-
class BundleEntrySearch(BaseModel):
86+
class BundleEntrySearch(Parent):
8587
id: Annotated[
8688
Optional[str],
8789
Field(
@@ -104,7 +106,7 @@ class BundleEntrySearch(BaseModel):
104106
] = None
105107

106108

107-
class BundleLink(BaseModel):
109+
class BundleLink(Parent):
108110
id: Annotated[
109111
Optional[str],
110112
Field(
@@ -124,7 +126,7 @@ class BundleLink(BaseModel):
124126
]
125127

126128

127-
class Attachment(BaseModel):
129+
class Attachment(Parent):
128130
id: Annotated[
129131
Optional[str],
130132
Field(
@@ -186,7 +188,7 @@ class Attachment(BaseModel):
186188
] = None
187189

188190

189-
class Coding(BaseModel):
191+
class Coding(Parent):
190192
id: Annotated[
191193
Optional[str],
192194
Field(
@@ -253,7 +255,7 @@ class NRLFormatCode(Coding):
253255
]
254256

255257

256-
class Period(BaseModel):
258+
class Period(Parent):
257259
id: Annotated[
258260
Optional[str],
259261
Field(
@@ -277,7 +279,7 @@ class Period(BaseModel):
277279
] = None
278280

279281

280-
class Quantity(BaseModel):
282+
class Quantity(Parent):
281283
id: Annotated[
282284
Optional[str],
283285
Field(
@@ -331,7 +333,7 @@ class ProfileItem(RootModel[str]):
331333
]
332334

333335

334-
class Meta(BaseModel):
336+
class Meta(Parent):
335337
id: Annotated[
336338
Optional[str],
337339
Field(
@@ -365,7 +367,7 @@ class Meta(BaseModel):
365367
tag: Optional[List[Coding]] = None
366368

367369

368-
class Narrative(BaseModel):
370+
class Narrative(Parent):
369371
id: Annotated[
370372
Optional[str],
371373
Field(
@@ -392,7 +394,7 @@ class DocumentId(RootModel[str]):
392394
root: Annotated[str, Field(pattern="[A-Za-z0-9\\-\\.]{1,64}")]
393395

394396

395-
class RequestPathParams(BaseModel):
397+
class RequestPathParams(Parent):
396398
id: DocumentId
397399

398400

@@ -450,7 +452,7 @@ class RequestHeaderCorrelationId(RootModel[str]):
450452
root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])]
451453

452454

453-
class CodeableConcept(BaseModel):
455+
class CodeableConcept(Parent):
454456
id: Annotated[
455457
Optional[str],
456458
Field(
@@ -468,7 +470,7 @@ class CodeableConcept(BaseModel):
468470
] = None
469471

470472

471-
class Extension(BaseModel):
473+
class Extension(Parent):
472474
valueCodeableConcept: Annotated[
473475
Optional[CodeableConcept],
474476
Field(
@@ -487,11 +489,11 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept):
487489
]
488490

489491

490-
class RequestHeader(BaseModel):
492+
class RequestHeader(Parent):
491493
odsCode: RequestHeaderOdsCode
492494

493495

494-
class RequestParams(BaseModel):
496+
class RequestParams(Parent):
495497
subject_identifier: Annotated[
496498
RequestQuerySubject, Field(alias="subject:identifier")
497499
]
@@ -505,13 +507,13 @@ class RequestParams(BaseModel):
505507
] = None
506508

507509

508-
class CountRequestParams(BaseModel):
510+
class CountRequestParams(Parent):
509511
subject_identifier: Annotated[
510512
RequestQuerySubject, Field(alias="subject:identifier")
511513
]
512514

513515

514-
class OperationOutcomeIssue(BaseModel):
516+
class OperationOutcomeIssue(Parent):
515517
id: Annotated[
516518
Optional[str],
517519
Field(
@@ -557,7 +559,7 @@ class ContentStabilityExtension(Extension):
557559
valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept
558560

559561

560-
class OperationOutcome(BaseModel):
562+
class OperationOutcome(Parent):
561563
resourceType: Literal["OperationOutcome"]
562564
id: Annotated[
563565
Optional[str],
@@ -595,7 +597,7 @@ class OperationOutcome(BaseModel):
595597
issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)]
596598

597599

598-
class DocumentReferenceContent(BaseModel):
600+
class DocumentReferenceContent(Parent):
599601
id: Annotated[
600602
Optional[str],
601603
Field(
@@ -620,7 +622,7 @@ class DocumentReferenceContent(BaseModel):
620622
]
621623

622624

623-
class DocumentReference(BaseModel):
625+
class DocumentReference(Parent):
624626
resourceType: Literal["DocumentReference"]
625627
id: Annotated[
626628
Optional[str],
@@ -721,7 +723,7 @@ class DocumentReference(BaseModel):
721723
] = None
722724

723725

724-
class Bundle(BaseModel):
726+
class Bundle(Parent):
725727
resourceType: Literal["Bundle"]
726728
id: Annotated[
727729
Optional[str],
@@ -786,7 +788,7 @@ class Bundle(BaseModel):
786788
] = None
787789

788790

789-
class BundleEntry(BaseModel):
791+
class BundleEntry(Parent):
790792
id: Annotated[
791793
Optional[str],
792794
Field(
@@ -828,7 +830,7 @@ class BundleEntry(BaseModel):
828830
] = None
829831

830832

831-
class BundleEntryResponse(BaseModel):
833+
class BundleEntryResponse(Parent):
832834
id: Annotated[
833835
Optional[str],
834836
Field(
@@ -872,7 +874,7 @@ class BundleEntryResponse(BaseModel):
872874
] = None
873875

874876

875-
class DocumentReferenceContext(BaseModel):
877+
class DocumentReferenceContext(Parent):
876878
id: Annotated[
877879
Optional[str],
878880
Field(
@@ -907,7 +909,7 @@ class DocumentReferenceContext(BaseModel):
907909
related: Optional[List[Reference]] = None
908910

909911

910-
class DocumentReferenceRelatesTo(BaseModel):
912+
class DocumentReferenceRelatesTo(Parent):
911913
id: Annotated[
912914
Optional[str],
913915
Field(
@@ -927,7 +929,7 @@ class DocumentReferenceRelatesTo(BaseModel):
927929
]
928930

929931

930-
class Identifier(BaseModel):
932+
class Identifier(Parent):
931933
id: Annotated[
932934
Optional[str],
933935
Field(
@@ -972,7 +974,7 @@ class Identifier(BaseModel):
972974
] = None
973975

974976

975-
class Reference(BaseModel):
977+
class Reference(Parent):
976978
id: Annotated[
977979
Optional[str],
978980
Field(
@@ -1009,7 +1011,7 @@ class Reference(BaseModel):
10091011
] = None
10101012

10111013

1012-
class Signature(BaseModel):
1014+
class Signature(Parent):
10131015
id: Annotated[
10141016
Optional[str],
10151017
Field(

layer/nrlf/core/parent_model.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from typing import Annotated, List, Optional
2+
3+
from pydantic import BaseModel, ConfigDict, Field
4+
5+
6+
class ParentCoding(BaseModel):
7+
model_config = ConfigDict(regex_engine="python-re", extra="forbid")
8+
id: Annotated[
9+
Optional[str],
10+
Field(
11+
description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
12+
pattern="[A-Za-z0-9\\-\\.]{1,64}",
13+
),
14+
] = None
15+
system: Annotated[
16+
Optional[str],
17+
Field(
18+
description="The identification of the code system that defines the meaning of the symbol in the code.",
19+
pattern="\\S*",
20+
),
21+
] = None
22+
version: Annotated[
23+
Optional[str],
24+
Field(
25+
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.",
26+
pattern="[ \\r\\n\\t\\S]+",
27+
),
28+
] = None
29+
code: Annotated[
30+
Optional[str],
31+
Field(
32+
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).",
33+
pattern="[^\\s]+(\\s[^\\s]+)*",
34+
),
35+
] = None
36+
display: Annotated[
37+
Optional[str],
38+
Field(
39+
description="A representation of the meaning of the code in the system, following the rules of the system.",
40+
pattern="[ \\r\\n\\t\\S]+",
41+
),
42+
] = None
43+
userSelected: Annotated[
44+
Optional[bool],
45+
Field(
46+
description="Indicates that this coding was chosen by a user directly – e.g. off a pick list of available items (codes or displays)."
47+
),
48+
] = None
49+
50+
51+
class ParentCodeableConcept(BaseModel):
52+
model_config = ConfigDict(regex_engine="python-re", extra="forbid")
53+
id: Annotated[
54+
Optional[str],
55+
Field(
56+
description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
57+
pattern="[A-Za-z0-9\\-\\.]{1,64}",
58+
),
59+
] = None
60+
coding: Optional[List[ParentCoding]] = None
61+
text: Annotated[
62+
Optional[str],
63+
Field(
64+
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.",
65+
pattern="[ \\r\\n\\t\\S]+",
66+
),
67+
] = None
68+
69+
70+
class ParentExtension(BaseModel):
71+
valueCodeableConcept: Annotated[
72+
Optional[ParentCodeableConcept],
73+
Field(
74+
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)."
75+
),
76+
] = None
77+
url: Annotated[
78+
Optional[str],
79+
Field(description="The reference details for the link.", pattern="\\S*"),
80+
] = None
81+
82+
83+
class Parent(BaseModel):
84+
model_config = ConfigDict(regex_engine="python-re", extra="forbid")
85+
extension: Annotated[
86+
Optional[List[ParentExtension]],
87+
Field(description="A list of relevant extensions"),
88+
] = None

0 commit comments

Comments
 (0)