From 245f6005084eaddc4d0266fc4f4a3e12e879e83e Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Tue, 23 Apr 2024 17:20:22 +0100 Subject: [PATCH] Fix top level extension implementation --- fhirflat/resources/encounter.py | 27 ++++-- fhirflat/resources/extension_types.py | 16 +--- fhirflat/resources/extension_validators.py | 23 +---- fhirflat/resources/extensions.py | 98 +-------------------- fhirflat/resources/immunization.py | 25 ++++-- fhirflat/resources/observation.py | 23 +++-- fhirflat/resources/procedure.py | 51 ++++++++--- tests/data/immunization_flat.parquet | Bin 16247 -> 17941 bytes tests/test_extensions.py | 68 +++++--------- tests/test_immunization_resource.py | 15 ++++ tests/test_utils.py | 8 ++ 11 files changed, 149 insertions(+), 205 deletions(-) diff --git a/fhirflat/resources/encounter.py b/fhirflat/resources/encounter.py index e7871f0..ee843fb 100644 --- a/fhirflat/resources/encounter.py +++ b/fhirflat/resources/encounter.py @@ -4,19 +4,24 @@ import orjson from ..flat2fhir import expand_concepts -from .extensions import relativeTimingPhaseExtension -from pydantic.v1 import Field -from typing import TypeAlias, ClassVar + +from .extensions import relativePhase, timingPhase +from .extension_types import relativePhaseType, timingPhaseType +from pydantic.v1 import Field, validator +from typing import TypeAlias, ClassVar, Union +from fhir.resources import fhirtypes JsonString: TypeAlias = str class Encounter(_Encounter, FHIRFlatBase): - extension: relativeTimingPhaseExtension = Field( + extension: list[ + Union[relativePhaseType, timingPhaseType, fhirtypes.ExtensionType] + ] = Field( None, alias="extension", - title="Additional content defined by implementations", + title="List of `Extension` items (represented as `dict` in JSON)", description=( """ Contains the G.H 'eventTiming' and 'relativePhase' extensions, and allows @@ -25,6 +30,8 @@ class Encounter(_Encounter, FHIRFlatBase): ), # if property is element of this resource. element_property=True, + # this trys to match the type of the object to each of the union types + union_mode="smart", ) # attributes to exclude from the flat representation @@ -42,6 +49,16 @@ class Encounter(_Encounter, FHIRFlatBase): # required attributes that are not present in the FHIRflat representation flat_defaults: ClassVar[list[str]] = FHIRFlatBase.flat_defaults + ["status"] + @validator("extension") + def validate_extension_contents(cls, extensions): + rel_phase_count = sum(isinstance(item, relativePhase) for item in extensions) + tim_phase_count = sum(isinstance(item, timingPhase) for item in extensions) + + if rel_phase_count > 1 or tim_phase_count > 1: + raise ValueError("relativePhase and timingPhase can only appear once.") + + return extensions + @classmethod def cleanup(cls, data: JsonString) -> Encounter: """ diff --git a/fhirflat/resources/extension_types.py b/fhirflat/resources/extension_types.py index b4924bf..b602e39 100644 --- a/fhirflat/resources/extension_types.py +++ b/fhirflat/resources/extension_types.py @@ -47,17 +47,5 @@ class dateTimeExtensionType(AbstractType): __resource_type__ = "dateTimeExtension" -class timingPhaseExtensionType(AbstractType): - __resource_type__ = "timingPhaseExtension" - - -class relativePhaseExtensionType(AbstractType): - __resource_type__ = "relativePhaseExtension" - - -class relativeTimingPhaseExtensionType(AbstractType): - __resource_type__ = "relativeTimingPhaseExtension" - - -class procedureExtensionType(AbstractType): - __resource_type__ = "procedureExtension" +class relativePeriodType(AbstractType): + __resource_type__ = "relativePeriod" diff --git a/fhirflat/resources/extension_validators.py b/fhirflat/resources/extension_validators.py index 9986811..ff1b20d 100644 --- a/fhirflat/resources/extension_validators.py +++ b/fhirflat/resources/extension_validators.py @@ -61,10 +61,7 @@ def __init__(self): "approximateDate": (None, ".extensions"), "Duration": (None, ".extensions"), "dateTimeExtension": (None, ".extensions"), - "timingPhaseExtension": (None, ".extensions"), - "relativePhaseExtension": (None, ".extensions"), - "relativeTimingPhaseExtension": (None, ".extensions"), - "procedureExtension": (None, ".extensions"), + "relativePeriod": (None, ".extensions"), } def get_fhir_model_class(self, model_name: str) -> Type[FHIRAbstractModel]: @@ -225,19 +222,5 @@ def datetimeextension_validator(v: Union[StrBytes, dict, Path, FHIRAbstractModel return Validators().fhir_model_validator("dateTimeExtension", v) -def timingphaseextension_validator(v: Union[StrBytes, dict, Path, FHIRAbstractModel]): - return Validators().fhir_model_validator("timingPhaseExtension", v) - - -def relativephaseextension_validator(v: Union[StrBytes, dict, Path, FHIRAbstractModel]): - return Validators().fhir_model_validator("relativePhaseExtension", v) - - -def relativetimingphaseextension_validator( - v: Union[StrBytes, dict, Path, FHIRAbstractModel] -): - return Validators().fhir_model_validator("relativeTimingPhaseExtension", v) - - -def procedureextension_validator(v: Union[StrBytes, dict, Path, FHIRAbstractModel]): - return Validators().fhir_model_validator("procedureExtension", v) +def relativeperiod_validator(v: Union[StrBytes, dict, Path, FHIRAbstractModel]): + return Validators().fhir_model_validator("relativePeriod", v) diff --git a/fhirflat/resources/extensions.py b/fhirflat/resources/extensions.py index 79e8f11..704a454 100644 --- a/fhirflat/resources/extensions.py +++ b/fhirflat/resources/extensions.py @@ -145,6 +145,8 @@ def elements_sequence(cls): ] +# Should this not be a FHIRextension like down the bottom? (copy the 'Period' or 'Range' +# datatype) class relativePhase(_DataType): resource_type = Field("relativePhase", const=True) @@ -342,36 +344,6 @@ def validate_extension_contents(cls, extensions): return extensions -class timingPhaseExtension(_FHIRPrimitiveExtension): - """ - A G.Health specific extension to the FHIR dateTime type - Allows dates to be specified as either approximate, and/or number of days relative - to the current date. - """ - - resource_type = Field("timingPhaseExtension", const=True) - - extension: list[Union[et.timingPhaseType, fhirtypes.ExtensionType]] = Field( - None, - alias="extension", - title="List of `Extension` items (represented as `dict` in JSON)", - description="Additional content defined by implementations", - # if property is element of this resource. - element_property=True, - # this trys to match the type of the object to each of the union types - union_mode="smart", - ) - - @validator("extension") - def validate_extension_contents(cls, extensions): - phase_count = sum(isinstance(item, timingPhase) for item in extensions) - - if phase_count > 1: - raise ValueError("timingPhase can only appear once.") - - return extensions - - class relativePhaseExtension(_FHIRPrimitiveExtension): """ A G.Health specific extension to the FHIR dateTime type @@ -400,69 +372,3 @@ def validate_extension_contents(cls, extensions): raise ValueError("relativePhase can only appear once.") return extensions - - -class relativeTimingPhaseExtension(_FHIRPrimitiveExtension): - """ - Contains both the relative timing (pre-admission, during admission etc) and the - relative phase (number of days since admission for the start and end of an event) - extensions. - """ - - resource_type = Field("relativeTimingPhaseExtension", const=True) - - extension: list[ - Union[et.relativePhaseType, et.timingPhaseType, fhirtypes.ExtensionType] - ] = Field( - None, - alias="extension", - title="List of `Extension` items (represented as `dict` in JSON)", - description="Additional content defined by implementations", - # if property is element of this resource. - element_property=True, - # this trys to match the type of the object to each of the union types - union_mode="smart", - ) - - @validator("extension") - def validate_extension_contents(cls, extensions): - rel_phase_count = sum(isinstance(item, relativePhase) for item in extensions) - tim_phase_count = sum(isinstance(item, timingPhase) for item in extensions) - - if rel_phase_count > 1 or tim_phase_count > 1: - raise ValueError("relativePhase and timingPhase can only appear once.") - - return extensions - - -class procedureExtension(_FHIRPrimitiveExtension): - """ - Contains both the relative timing (pre-admission, during admission etc) and the - relative phase (number of days since admission for the start and end of an event) - extensions. - """ - - resource_type = Field("procedureExtension", const=True) - - extension: list[ - Union[et.durationType, et.timingPhaseType, fhirtypes.ExtensionType] - ] = Field( - None, - alias="extension", - title="List of `Extension` items (represented as `dict` in JSON)", - description="Additional content defined by implementations", - # if property is element of this resource. - element_property=True, - # this trys to match the type of the object to each of the union types - union_mode="smart", - ) - - @validator("extension") - def validate_extension_contents(cls, extensions): - duration_count = sum(isinstance(item, Duration) for item in extensions) - tim_phase_count = sum(isinstance(item, timingPhase) for item in extensions) - - if duration_count > 1 or tim_phase_count > 1: - raise ValueError("Duration and timingPhase can only appear once.") - - return extensions diff --git a/fhirflat/resources/immunization.py b/fhirflat/resources/immunization.py index cf18bdf..eb5a9d0 100644 --- a/fhirflat/resources/immunization.py +++ b/fhirflat/resources/immunization.py @@ -1,22 +1,24 @@ from __future__ import annotations from fhir.resources.immunization import Immunization as _Immunization from .base import FHIRFlatBase -from .extensions import dateTimeExtension, timingPhaseExtension -from pydantic.v1 import Field +from .extensions import timingPhase +from .extension_types import timingPhaseType, dateTimeExtensionType +from pydantic.v1 import Field, validator import orjson from ..flat2fhir import expand_concepts -from typing import TypeAlias, ClassVar +from typing import TypeAlias, ClassVar, Union +from fhir.resources import fhirtypes JsonString: TypeAlias = str class Immunization(_Immunization, FHIRFlatBase): - extension: timingPhaseExtension = Field( + extension: list[Union[timingPhaseType, fhirtypes.ExtensionType]] = Field( None, alias="extension", - title="Additional content defined by implementations", + title="List of `Extension` items (represented as `dict` in JSON)", description=( """ Contains the G.H 'eventPhase' extension, and allows extensions from other @@ -24,9 +26,11 @@ class Immunization(_Immunization, FHIRFlatBase): ), # if property is element of this resource. element_property=True, + # this trys to match the type of the object to each of the union types + union_mode="smart", ) - occurrenceDateTime__ext: dateTimeExtension = Field( + occurrenceDateTime__ext: dateTimeExtensionType = Field( None, alias="_occurrenceDateTime", title="Extension field for ``occurrenceDateTime``.", @@ -51,6 +55,15 @@ class Immunization(_Immunization, FHIRFlatBase): # required attributes that are not present in the FHIRflat representation flat_defaults: ClassVar[list[str]] = FHIRFlatBase.flat_defaults + ["status"] + @validator("extension") + def validate_extension_contents(cls, extensions): + phase_count = sum(isinstance(item, timingPhase) for item in extensions) + + if phase_count > 1: + raise ValueError("timingPhase can only appear once.") + + return extensions + @classmethod def cleanup(cls, data: JsonString) -> Immunization: """ diff --git a/fhirflat/resources/observation.py b/fhirflat/resources/observation.py index 2ed1923..6d8eb7d 100644 --- a/fhirflat/resources/observation.py +++ b/fhirflat/resources/observation.py @@ -3,12 +3,14 @@ from fhir.resources.observation import ObservationComponent as _ObservationComponent from .base import FHIRFlatBase -from .extension_types import dateTimeExtensionType, timingPhaseExtensionType -from pydantic.v1 import Field +from .extension_types import dateTimeExtensionType, timingPhaseType +from .extensions import timingPhase +from pydantic.v1 import Field, validator import orjson +from fhir.resources import fhirtypes from ..flat2fhir import expand_concepts -from typing import TypeAlias, ClassVar +from typing import TypeAlias, ClassVar, Union JsonString: TypeAlias = str @@ -27,10 +29,10 @@ class ObservationComponent(_ObservationComponent): class Observation(_Observation, FHIRFlatBase): - extension: timingPhaseExtensionType = Field( + extension: list[Union[timingPhaseType, fhirtypes.ExtensionType]] = Field( None, alias="extension", - title="Additional content defined by implementations", + title="List of `Extension` items (represented as `dict` in JSON)", description=( """ Contains the G.H 'eventPhase' extension, and allows extensions from other @@ -38,6 +40,8 @@ class Observation(_Observation, FHIRFlatBase): ), # if property is element of this resource. element_property=True, + # this trys to match the type of the object to each of the union types + union_mode="smart", ) effectiveDateTime__ext: dateTimeExtensionType = Field( @@ -78,6 +82,15 @@ class Observation(_Observation, FHIRFlatBase): # required attributes that are not present in the FHIRflat representation flat_defaults: ClassVar[list[str]] = FHIRFlatBase.flat_defaults + ["status"] + @validator("extension") + def validate_extension_contents(cls, extensions): + phase_count = sum(isinstance(item, timingPhase) for item in extensions) + + if phase_count > 1: + raise ValueError("timingPhase can only appear once.") + + return extensions + @classmethod def cleanup(cls, data: JsonString) -> Observation: """ diff --git a/fhirflat/resources/procedure.py b/fhirflat/resources/procedure.py index 3b201bc..ad40ad5 100644 --- a/fhirflat/resources/procedure.py +++ b/fhirflat/resources/procedure.py @@ -1,38 +1,51 @@ from __future__ import annotations from fhir.resources.procedure import Procedure as _Procedure from .base import FHIRFlatBase -from .extensions import dateTimeExtension, procedureExtension, relativePhaseExtension -from pydantic.v1 import Field + +from .extension_types import ( + dateTimeExtensionType, + relativePhaseExtensionType, + durationType, + timingPhaseType, +) + +from .extensions import Duration, timingPhase + +from pydantic.v1 import Field, validator import orjson from ..flat2fhir import expand_concepts -from typing import TypeAlias, ClassVar +from typing import TypeAlias, ClassVar, Union +from fhir.resources import fhirtypes JsonString: TypeAlias = str class Procedure(_Procedure, FHIRFlatBase): - extension: procedureExtension = Field( - None, - alias="extension", - title="Additional content defined by implementations", - description=( - """ + extension: list[Union[durationType, timingPhaseType, fhirtypes.ExtensionType]] = ( + Field( + None, + alias="extension", + title="Additional content defined by implementations", + description=( + """ Contains the G.H 'timingPhase' and 'duration' extensions, and allows extensions from other implementations to be included.""" - ), - # if property is element of this resource. - element_property=True, + ), + # if property is element of this resource. + element_property=True, + union_mode="smart", + ) ) - occurrenceDateTime__ext: dateTimeExtension = Field( + occurrenceDateTime__ext: dateTimeExtensionType = Field( None, alias="_occurrenceDateTime", title="Extension field for ``occurrenceDateTime``.", ) - occurrencePeriod__ext: relativePhaseExtension = Field( + occurrencePeriod__ext: relativePhaseExtensionType = Field( None, alias="_occurrencePeriod", title="Extension field for ``occurrencePeriod``.", @@ -58,6 +71,16 @@ class Procedure(_Procedure, FHIRFlatBase): # required attributes that are not present in the FHIRflat representation flat_defaults: ClassVar[list[str]] = FHIRFlatBase.flat_defaults + ["status"] + @validator("extension") + def validate_extension_contents(cls, extensions): + duration_count = sum(isinstance(item, Duration) for item in extensions) + tim_phase_count = sum(isinstance(item, timingPhase) for item in extensions) + + if duration_count > 1 or tim_phase_count > 1: + raise ValueError("Duration and timingPhase can only appear once.") + + return extensions + @classmethod def cleanup(cls, data: JsonString) -> Procedure: """ diff --git a/tests/data/immunization_flat.parquet b/tests/data/immunization_flat.parquet index 8bbd8e8372785e8e8a0cae385af72686ca6da0ca..460c2feaec5b63de84e94b122c3ece41ffea8ecb 100644 GIT binary patch delta 4119 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 delta 2947 zcmZ`*YfKy26~H{Y-3kZ>IVUvv2m9Gf$fIX zDv4HY+V1XAccm&#Q}vOmk0?^K$&Xc~{Ae4rqS>}uwNl$+>N&~3PL)n`Av4%3k5V@z@Eu*7bd0Hh9i0V^luuFTO5vEc zn?5gP9#C+s%nUKvGWGp(cu&UAZ44h^yn>Y`k9dr-bgeOL!e75(&1E%T@bE;sT}R(GEI zS_6E$&MJFP$$Y42>M!XANqy^^^*^WXYvKKRhW?(0xp4pvw!}eBXQ*H3;UkTa-fm)E zX@U{D8~j=Ye5vd>_0LxLu~tui+`{~@1#W8{&?^W(+M{;eQ;C$Tk4hpzFA|WFCStqc7whq4Q5~Epw>v=>tn9< z!OOK82sA6M{=JfxP~WwKq^VbS%fehRi~G)1^B`3Jf%?D&6Y^gAqLbNnz{~O}_!c!t z?F_*kilHAIW`1=Ts-&IpmgEc6X$}kry6Bx@=5H@RfC|6yt`f3}3F>DaIMvccUvo2O z-SCpa2(?PJyv1IkDO*V`&gT*h-S9_MBbZbja8sp)zbF-Is*}>lUz|^*PGpwzed)xB z`OfM|0WPUp;Z;>P+-itJjjA2)s7hg0RR_OBz-kPs5SCL35&}mXPQXJ|1}-W4;LFNJ z=vM1XRm?M5u@>v3Ps;2g66G@jX(|>62JKZwQ?V-C&ryU;$`ncBGyBOwSu6$Telk!N zOMv}<`tR$N@QJP(-l=VShJWSrsE;p zs2N&ZTFrB80u7?h1gsGr2R%%8BoJ2tAZg@xJK9vTO*m2&uKzv6STTC zP-;=ZX}hW{mUFHp-EhIKq5W$~%Q}3;Wq`b0jT>r!-u_B@DK-}>$^swv>!8!6g1h#r zs#Gqrp0xTAIS5;J?Y@9|+*UMTN<^KjlR5KZGAHEXa{&_~7m^kmOjtYNuedMV4_qV zTFl}VPeOdEl0Ikl$uaQsBAC-v2OERRs}5I}gx)|%69|K?@Dqm;E)5ps;!1qRZi>$> z5-zmToYM?2ePr~NkN>CnVpkveqcOjUra{BnIjo46Zbhc;}Nep zhKfkch3u2Lz*-zt#IhRPNE2KiI1WEO{K*!-fKnafk+jfa3Z?5M(&fp+?I1E<u z9L3iZncKN3j`Od3@YTklKS$`hDM2|<9Jl$afobe1Db&ngc zF*lyVruK&==k|rxQkF<49rdCt1k?PC-GVhW$9J>)#60LFW)au2aBRd`VIbI>SqF>^ z_m;ao4wiK;zy~8vnU}m9g6uS08#b2nHg=k2SK;okscu)kWS7fiu^$eNuob51O_n|B zal&Uu+Vy^tC1nvzsGV%m5)#C$(^i%>p$BGiyR%Nrw9Fo7S+wzdNc2dNU-7~br@K|z zqg!z`LU8P}PI$%LtY0DlLJ|d$MsdXFNFCiAfnA@rf?=fDMd(0HU`>Q%KFEEJL>owU z2O37bggDMk3P{L(Jop&C@GxlAR}ldaxbf)$_#)T7wQnnnVczKhmNm~JVIuOBsJ^|( zr0PgH4zd2&BU$W()p%G`GxoT1X_qa2ibYbg1Wy!GSo9aEgP{?fAx$g;Pc!YF!6Q0- zdIm8Q4~G7p8PVdl5m3WZ(3rR?GF7Zl(xOl9z&u_=KIaDBG^Etr0Eyq&WTC)2#^MMl z?vUdb$Oi=NJTEo{SInf?35mJDY&z=4J2)hAF^lS9Z3uSdg?3mTXxJhdxN(n&^#stp ztA`b0+9Qn_GJoEehS)eRMyicfBV_IltPZvpHrxU?#_e`|@KexH(_=LhRZ?{1H}EsC ohWBzd9tqh~KCw;V?NN(F1s{#--t<