From 21dac11fbf18e4cbf00d2fd26f71e78650358913 Mon Sep 17 00:00:00 2001 From: SimonClo Date: Fri, 7 Nov 2025 17:14:59 +0100 Subject: [PATCH 1/5] refactor(converter): invert dependencies between base message converter and conversion mixin --- .../{versions => }/conversion_mixin.py | 22 ++++++++++-------- .../versions/base_message_converter.py | 23 ++++++++++++++++++- .../create_case_health_converter.py | 4 ++-- .../document_link/document_link_converter.py | 4 ++-- .../geo_positions_update_converter.py | 4 ++-- .../geo_resources_details_converter.py | 4 ++-- .../intervention_report_converter.py | 4 ++-- .../versions/reference/reference_converter.py | 3 +-- .../resources_engagement_converter.py | 3 +-- .../resources_info_converter.py | 4 ++-- .../resources_request_converter.py | 3 +-- .../resources_response_converter.py | 3 +-- .../resources_status_converter.py | 4 ++-- .../converter/versions/rpis/rpis_converter.py | 4 ++-- 14 files changed, 54 insertions(+), 35 deletions(-) rename converter/converter/{versions => }/conversion_mixin.py (70%) diff --git a/converter/converter/versions/conversion_mixin.py b/converter/converter/conversion_mixin.py similarity index 70% rename from converter/converter/versions/conversion_mixin.py rename to converter/converter/conversion_mixin.py index 4748428aa..b7f04f0b2 100644 --- a/converter/converter/versions/conversion_mixin.py +++ b/converter/converter/conversion_mixin.py @@ -1,19 +1,18 @@ import copy from typing import Dict, Any -from converter.versions.base_message_converter import BaseMessageConverter - -class ConversionMixin(BaseMessageConverter): +class ConversionMixin: CONTENT_KEY = "content" JSON_CONTENT_KEY = "jsonContent" EMBEDDED_JSON_CONTENT_KEY = "embeddedJsonContent" MESSAGE_KEY = "message" @classmethod - def copy_input_content(cls, input_json: Dict[str, Any]) -> Dict[str, Any]: + def _copy_input_content( + cls, input_json: Dict[str, Any], message_type: str + ) -> Dict[str, Any]: output_json = copy.deepcopy(input_json) - message_type = cls.get_message_type() message_content = ( input_json.get(cls.CONTENT_KEY, [{}])[0] @@ -30,18 +29,21 @@ def copy_input_content(cls, input_json: Dict[str, Any]) -> Dict[str, Any]: return output_json @classmethod - def copy_input_use_case_content(cls, input_json: Dict[str, Any]) -> Dict[str, Any]: - message_type = cls.get_message_type() + def _copy_input_use_case_content( + cls, input_json: Dict[str, Any], message_type: str + ) -> Dict[str, Any]: input_use_case_json = input_json[cls.CONTENT_KEY][0][cls.JSON_CONTENT_KEY][ cls.EMBEDDED_JSON_CONTENT_KEY ][cls.MESSAGE_KEY][message_type] return copy.deepcopy(input_use_case_json) @classmethod - def format_output_json( - cls, output_json: Dict[str, Any], output_use_case_json: Dict[str, Any] + def _format_output_json( + cls, + output_json: Dict[str, Any], + output_use_case_json: Dict[str, Any], + message_type: str, ) -> Dict[str, Any]: - message_type = cls.get_message_type() output_json[cls.CONTENT_KEY][0][cls.JSON_CONTENT_KEY][ cls.EMBEDDED_JSON_CONTENT_KEY ][cls.MESSAGE_KEY][message_type] = output_use_case_json diff --git a/converter/converter/versions/base_message_converter.py b/converter/converter/versions/base_message_converter.py index f2b2f5d24..da0c16416 100644 --- a/converter/converter/versions/base_message_converter.py +++ b/converter/converter/versions/base_message_converter.py @@ -1,7 +1,10 @@ +from converter.conversion_mixin import ConversionMixin +from typing import Dict, Any + version_order_list = ["v1", "v2", "v3"] -class BaseMessageConverter: +class BaseMessageConverter(ConversionMixin): def __init__(self): raise ValueError( "BaseMessageConverter is an abstract class and cannot be instantiated directly. Use a subclass instead." @@ -112,3 +115,21 @@ def raise_conversion_impossible_error(cls, source_version, target_version): raise ValueError( f"Version conversion from {source_version} to {target_version} is not possible." ) + + @classmethod + def copy_input_content(cls, input_json: Dict[str, Any]) -> Dict[str, Any]: + return cls._copy_input_content(input_json, cls.get_message_type()) + + @classmethod + def copy_input_use_case_content(cls, input_json: Dict[str, Any]) -> Dict[str, Any]: + return cls._copy_input_use_case_content(input_json, cls.get_message_type()) + + @classmethod + def format_output_json( + cls, + output_json: Dict[str, Any], + output_use_case_json: Dict[str, Any], + ) -> Dict[str, Any]: + return cls._format_output_json( + output_json, output_use_case_json, cls.get_message_type() + ) diff --git a/converter/converter/versions/create_case_health/create_case_health_converter.py b/converter/converter/versions/create_case_health/create_case_health_converter.py index 491fe8107..87e7cb9ad 100644 --- a/converter/converter/versions/create_case_health/create_case_health_converter.py +++ b/converter/converter/versions/create_case_health/create_case_health_converter.py @@ -7,7 +7,7 @@ is_field_completed, map_to_new_value, ) -from converter.versions.conversion_mixin import ConversionMixin +from converter.versions.base_message_converter import BaseMessageConverter from converter.versions.create_case_health.v1_v2.constants import V1V2Constants from converter.versions.create_case_health.v2_v3.constants import V2V3Constants from converter.versions.utils import ( @@ -17,7 +17,7 @@ ) -class CreateHealthCaseConverter(ConversionMixin): +class CreateHealthCaseConverter(BaseMessageConverter): @staticmethod def get_message_type() -> str: return "createCaseHealth" diff --git a/converter/converter/versions/document_link/document_link_converter.py b/converter/converter/versions/document_link/document_link_converter.py index f8cc2b00f..f25287437 100644 --- a/converter/converter/versions/document_link/document_link_converter.py +++ b/converter/converter/versions/document_link/document_link_converter.py @@ -1,13 +1,13 @@ from typing import Dict, Any from converter.utils import delete_paths -from converter.versions.conversion_mixin import ConversionMixin +from converter.versions.base_message_converter import BaseMessageConverter from converter.versions.document_link.document_link_constants import ( DocumentLinkConstants, ) -class DocumentLinkConverter(ConversionMixin): +class DocumentLinkConverter(BaseMessageConverter): @staticmethod def get_message_type(): return "documentLink" diff --git a/converter/converter/versions/geo_positions_update/geo_positions_update_converter.py b/converter/converter/versions/geo_positions_update/geo_positions_update_converter.py index c21ceb54e..268ab1b96 100644 --- a/converter/converter/versions/geo_positions_update/geo_positions_update_converter.py +++ b/converter/converter/versions/geo_positions_update/geo_positions_update_converter.py @@ -1,14 +1,14 @@ from typing import Dict, Any from converter.utils import get_field_value, update_json_value, delete_paths -from converter.versions.conversion_mixin import ConversionMixin +from converter.versions.base_message_converter import BaseMessageConverter from converter.versions.geo_positions_update.geo_positions_update_constants import ( GeoPositionsUpdateConstants, ) from converter.versions.utils import convert_to_float, convert_to_str -class GeoPositionsUpdateConverter(ConversionMixin): +class GeoPositionsUpdateConverter(BaseMessageConverter): @staticmethod def get_message_type(): return "geoPositionsUpdate" diff --git a/converter/converter/versions/geo_resources_details/geo_resources_details_converter.py b/converter/converter/versions/geo_resources_details/geo_resources_details_converter.py index ba169f83e..f4224df21 100644 --- a/converter/converter/versions/geo_resources_details/geo_resources_details_converter.py +++ b/converter/converter/versions/geo_resources_details/geo_resources_details_converter.py @@ -1,14 +1,14 @@ from typing import Dict, Any from converter.utils import get_field_value, map_to_new_value, update_json_value -from converter.versions.conversion_mixin import ConversionMixin +from converter.versions.base_message_converter import BaseMessageConverter from converter.versions.geo_resources_details.geo_resources_details_constants import ( GeoResourcesDetailsConstants, ) from converter.versions.utils import reverse_map_to_new_value -class GeoResourcesDetailsConverter(ConversionMixin): +class GeoResourcesDetailsConverter(BaseMessageConverter): @staticmethod def get_message_type(): return "geoResourcesDetails" diff --git a/converter/converter/versions/intervention_report/intervention_report_converter.py b/converter/converter/versions/intervention_report/intervention_report_converter.py index e19d6fd88..334e271b4 100644 --- a/converter/converter/versions/intervention_report/intervention_report_converter.py +++ b/converter/converter/versions/intervention_report/intervention_report_converter.py @@ -6,14 +6,14 @@ map_to_new_value, set_value, ) -from converter.versions.conversion_mixin import ConversionMixin +from converter.versions.base_message_converter import BaseMessageConverter from converter.versions.intervention_report.intervention_report_constants import ( InterventionReportConstants, ) from converter.versions.utils import reverse_map_to_new_value -class InterventionReportConverter(ConversionMixin): +class InterventionReportConverter(BaseMessageConverter): @staticmethod def get_message_type(): return "interventionReport" diff --git a/converter/converter/versions/reference/reference_converter.py b/converter/converter/versions/reference/reference_converter.py index 21cd880fc..100ef9ded 100644 --- a/converter/converter/versions/reference/reference_converter.py +++ b/converter/converter/versions/reference/reference_converter.py @@ -1,10 +1,9 @@ from converter.utils import delete_paths -from converter.versions.conversion_mixin import ConversionMixin from converter.versions.identical_message_converter import IdenticalMessageConverter from converter.versions.reference.reference_constants import ReferenceConstants -class ReferenceConverter(IdenticalMessageConverter, ConversionMixin): +class ReferenceConverter(IdenticalMessageConverter): @staticmethod def get_message_type(): return "reference" diff --git a/converter/converter/versions/resources_engagement/resources_engagement_converter.py b/converter/converter/versions/resources_engagement/resources_engagement_converter.py index 44a393855..ef8b8a82d 100644 --- a/converter/converter/versions/resources_engagement/resources_engagement_converter.py +++ b/converter/converter/versions/resources_engagement/resources_engagement_converter.py @@ -1,12 +1,11 @@ from converter.utils import map_to_new_value, get_field_value, update_json_value -from converter.versions.conversion_mixin import ConversionMixin from converter.versions.identical_message_converter import IdenticalMessageConverter from converter.versions.resources_engagement.resources_engagement_constants import ( ResourcesEngagementConstants, ) -class ResourcesEngagementConverter(IdenticalMessageConverter, ConversionMixin): +class ResourcesEngagementConverter(IdenticalMessageConverter): @staticmethod def get_message_type(): return "resourcesEngagement" diff --git a/converter/converter/versions/resources_info/resources_info_converter.py b/converter/converter/versions/resources_info/resources_info_converter.py index 4640cf5c3..ae441241a 100644 --- a/converter/converter/versions/resources_info/resources_info_converter.py +++ b/converter/converter/versions/resources_info/resources_info_converter.py @@ -1,14 +1,14 @@ from typing import Dict, Any from converter.utils import delete_paths, get_field_value, map_to_new_value -from converter.versions.conversion_mixin import ConversionMixin +from converter.versions.base_message_converter import BaseMessageConverter from converter.versions.resources_info.resources_info_constants import ( ResourcesInfoConstants, ) from converter.versions.utils import reverse_map_to_new_value -class ResourcesInfoConverter(ConversionMixin): +class ResourcesInfoConverter(BaseMessageConverter): @staticmethod def get_message_type(): return "resourcesInfo" diff --git a/converter/converter/versions/resources_request/resources_request_converter.py b/converter/converter/versions/resources_request/resources_request_converter.py index 8c051ac2a..635e37e67 100644 --- a/converter/converter/versions/resources_request/resources_request_converter.py +++ b/converter/converter/versions/resources_request/resources_request_converter.py @@ -1,7 +1,6 @@ from typing import Dict, Any from converter.utils import map_to_new_value -from converter.versions.conversion_mixin import ConversionMixin from converter.versions.identical_message_converter import IdenticalMessageConverter from converter.versions.resources_request.resources_request_constants import ( ResourcesRequestConstants, @@ -9,7 +8,7 @@ from converter.versions.utils import reverse_map_to_new_value -class ResourcesRequestConverter(IdenticalMessageConverter, ConversionMixin): +class ResourcesRequestConverter(IdenticalMessageConverter): @staticmethod def get_message_type(): return "resourcesRequest" diff --git a/converter/converter/versions/resources_response/resources_response_converter.py b/converter/converter/versions/resources_response/resources_response_converter.py index 53daf63e0..1375a5e03 100644 --- a/converter/converter/versions/resources_response/resources_response_converter.py +++ b/converter/converter/versions/resources_response/resources_response_converter.py @@ -1,14 +1,13 @@ from typing import Dict, Any from converter.utils import get_field_value -from converter.versions.conversion_mixin import ConversionMixin from converter.versions.identical_message_converter import IdenticalMessageConverter from converter.versions.resources_response.resources_response_constants import ( ResourcesResponseConstants, ) -class ResourcesResponseConverter(IdenticalMessageConverter, ConversionMixin): +class ResourcesResponseConverter(IdenticalMessageConverter): @staticmethod def get_message_type(): return "resourcesResponse" diff --git a/converter/converter/versions/resources_status/resources_status_converter.py b/converter/converter/versions/resources_status/resources_status_converter.py index b56665642..50f409881 100644 --- a/converter/converter/versions/resources_status/resources_status_converter.py +++ b/converter/converter/versions/resources_status/resources_status_converter.py @@ -1,14 +1,14 @@ from typing import Dict, Any from converter.utils import delete_paths, get_field_value, map_to_new_value -from converter.versions.conversion_mixin import ConversionMixin +from converter.versions.base_message_converter import BaseMessageConverter from converter.versions.resources_status.resources_status_constants import ( ResourcesStatusConstants, ) from converter.versions.utils import reverse_map_to_new_value -class ResourcesStatusConverter(ConversionMixin): +class ResourcesStatusConverter(BaseMessageConverter): @staticmethod def get_message_type(): return "resourcesStatus" diff --git a/converter/converter/versions/rpis/rpis_converter.py b/converter/converter/versions/rpis/rpis_converter.py index 6600a3656..2f46203e9 100644 --- a/converter/converter/versions/rpis/rpis_converter.py +++ b/converter/converter/versions/rpis/rpis_converter.py @@ -1,12 +1,12 @@ from typing import Dict, Any from converter.utils import map_to_new_value -from converter.versions.conversion_mixin import ConversionMixin +from converter.versions.base_message_converter import BaseMessageConverter from converter.versions.rpis.rpis_constants import RpisConstants from converter.versions.utils import reverse_map_to_new_value -class RpisConverter(ConversionMixin): +class RpisConverter(BaseMessageConverter): @staticmethod def get_message_type(): return "rpis" From 3c742fe26a88ef97e881f3adef5ce18d8aaa9933 Mon Sep 17 00:00:00 2001 From: SimonClo Date: Fri, 7 Nov 2025 17:43:28 +0100 Subject: [PATCH 2/5] refactor(converter): base cisu converter inherits from conversion mixin --- .../converter/cisu/base_cisu_converter.py | 50 ++++++++++++++++++- .../create_case/create_case_cisu_converter.py | 37 +++----------- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/converter/converter/cisu/base_cisu_converter.py b/converter/converter/cisu/base_cisu_converter.py index cebd8446e..3ab0e6b67 100644 --- a/converter/converter/cisu/base_cisu_converter.py +++ b/converter/converter/cisu/base_cisu_converter.py @@ -1,4 +1,8 @@ -class BaseCISUConverter: +from converter.conversion_mixin import ConversionMixin +from typing import Any, Dict + + +class BaseCISUConverter(ConversionMixin): def __init__(self): raise ValueError( "BaseMessageConverter is an abstract class and cannot be instantiated directly. Use a subclass instead." @@ -27,3 +31,47 @@ def from_cisu_to_rs(cls, edxl_json): raise ValueError( f"Traduction from '{cls.get_cisu_message_type()}' to '{cls.get_rs_message_type()}' is not supported." ) + + @classmethod + def copy_cisu_input_content(cls, edxl_json: Dict[str, Any]) -> Dict[str, Any]: + return cls._copy_input_content(edxl_json, cls.get_cisu_message_type()) + + @classmethod + def copy_cisu_input_use_case_content( + cls, edxl_json: Dict[str, Any] + ) -> Dict[str, Any]: + return cls._copy_input_use_case_content(edxl_json, cls.get_cisu_message_type()) + + @classmethod + def format_cisu_output_json( + cls, + output_json: Dict[str, Any], + output_use_case_json: Dict[str, Any], + ) -> Dict[str, Any]: + return cls._format_output_json( + output_json, + output_use_case_json, + cls.get_cisu_message_type(), + ) + + @classmethod + def copy_rs_input_content(cls, edxl_json: Dict[str, Any]) -> Dict[str, Any]: + return cls._copy_input_content(edxl_json, cls.get_rs_message_type()) + + @classmethod + def copy_rs_input_use_case_content( + cls, edxl_json: Dict[str, Any] + ) -> Dict[str, Any]: + return cls._copy_input_use_case_content(edxl_json, cls.get_rs_message_type()) + + @classmethod + def format_rs_output_json( + cls, + output_json: Dict[str, Any], + output_use_case_json: Dict[str, Any], + ) -> Dict[str, Any]: + return cls._format_output_json( + output_json, + output_use_case_json, + cls.get_rs_message_type(), + ) diff --git a/converter/converter/cisu/create_case/create_case_cisu_converter.py b/converter/converter/cisu/create_case/create_case_cisu_converter.py index c5acc2715..206dd24ca 100644 --- a/converter/converter/cisu/create_case/create_case_cisu_converter.py +++ b/converter/converter/cisu/create_case/create_case_cisu_converter.py @@ -170,21 +170,11 @@ def add_object_to_medical_notes( json_data["medicalNote"].append(new_note) # Create independent envelope copy without usecase for output - output_json = copy.deepcopy(input_json) - if "createCase" not in input_json.get("content", [{}])[0].get( - "jsonContent", {} - ).get("embeddedJsonContent", {}).get("message", {}): - raise ValueError("Input JSON must contain 'createCase' key") - del output_json["content"][0]["jsonContent"]["embeddedJsonContent"]["message"][ - "createCase" - ] + output_json = cls.copy_cisu_input_content(input_json) # Create independent use case copy for output - input_use_case_json = input_json["content"][0]["jsonContent"][ - "embeddedJsonContent" - ]["message"]["createCase"] sender_id = get_sender(input_json) - output_use_case_json = copy.deepcopy(input_use_case_json) + output_use_case_json = cls.copy_cisu_input_use_case_content(input_json) # - Updates output_use_case_json["owner"] = get_recipient(input_json) @@ -204,10 +194,7 @@ def add_object_to_medical_notes( # - Delete paths - /!\ It must be the last step delete_paths(output_use_case_json, cls.CISU_PATHS_TO_DELETE) - output_json["content"][0]["jsonContent"]["embeddedJsonContent"]["message"][ - "createCaseHealth" - ] = output_use_case_json - return output_json + return cls.format_rs_output_json(output_json, output_use_case_json) @staticmethod def count_victims(json_data: Dict[str, Any]) -> int: @@ -264,19 +251,10 @@ def add_default_external_info_type(json_data: Dict[str, Any]): info["type"] = "AUTRE" # Create independent envelope copy without usecase for output - output_json = copy.deepcopy(input_json) - if "createCaseHealth" not in input_json.get("content", [{}])[0].get( - "jsonContent", {} - ).get("embeddedJsonContent", {}).get("message", {}): - raise ValueError("Input JSON must contain 'createCaseHealth' key") - del output_json["content"][0]["jsonContent"]["embeddedJsonContent"]["message"][ - "createCaseHealth" - ] + output_json = cls.copy_rs_input_content(input_json) # Create independent usecase copy for output - input_usecase_json = input_json["content"][0]["jsonContent"][ - "embeddedJsonContent" - ]["message"]["createCaseHealth"] + input_usecase_json = cls.copy_rs_input_use_case_content(input_json) output_usecase_json = copy.deepcopy(input_usecase_json) # Generate unique IDs @@ -330,7 +308,4 @@ def add_default_external_info_type(json_data: Dict[str, Any]): get_field_value(output_usecase_json, "$.location") ) - output_json["content"][0]["jsonContent"]["embeddedJsonContent"]["message"][ - "createCase" - ] = output_usecase_json - return output_json + return cls.format_cisu_output_json(output_json, output_usecase_json) From e3636421b388003dc32c8c92a363035a6b99bbf1 Mon Sep 17 00:00:00 2001 From: SimonClo Date: Fri, 7 Nov 2025 18:08:29 +0100 Subject: [PATCH 3/5] refactor(converter): move cisu tests in dedicated folder --- converter/tests/cisu/__init__.py | 0 converter/tests/cisu/snapshots/__init__.py | 0 .../snap_test_create_case_converter.py} | 16 ++++++++++------ converter/tests/{ => cisu}/test_cisu_utils.py | 0 .../test_create_case_converter.py} | 2 +- converter/tests/snapshots/__init__.py | 1 - 6 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 converter/tests/cisu/__init__.py create mode 100644 converter/tests/cisu/snapshots/__init__.py rename converter/tests/{snapshots/snap_test_cisu_converter.py => cisu/snapshots/snap_test_create_case_converter.py} (98%) rename converter/tests/{ => cisu}/test_cisu_utils.py (100%) rename converter/tests/{test_cisu_converter.py => cisu/test_create_case_converter.py} (99%) delete mode 100644 converter/tests/snapshots/__init__.py diff --git a/converter/tests/cisu/__init__.py b/converter/tests/cisu/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/converter/tests/cisu/snapshots/__init__.py b/converter/tests/cisu/snapshots/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/converter/tests/snapshots/snap_test_cisu_converter.py b/converter/tests/cisu/snapshots/snap_test_create_case_converter.py similarity index 98% rename from converter/tests/snapshots/snap_test_cisu_converter.py rename to converter/tests/cisu/snapshots/snap_test_create_case_converter.py index 2f40b4cdd..36fabb64c 100644 --- a/converter/tests/snapshots/snap_test_cisu_converter.py +++ b/converter/tests/cisu/snapshots/snap_test_create_case_converter.py @@ -8,7 +8,7 @@ snapshots = Snapshot() snapshots[ - "TestSnapshotCisuConverter::test_snapshot_RC_EDA_exhaustive_bis_message 1" + "TestSnapshotCreateCaseConverter::test_snapshot_RC_EDA_exhaustive_bis_message 1" ] = """{ "distributionID": "fr.fire.sdisZ_2608323d-507d-4cbf-bf74-52007f8124ea", "senderID": "fr.fire.sdisZ", @@ -175,7 +175,9 @@ ] }""" -snapshots["TestSnapshotCisuConverter::test_snapshot_RC_EDA_exhaustive_message 1"] = """{ +snapshots[ + "TestSnapshotCreateCaseConverter::test_snapshot_RC_EDA_exhaustive_message 1" +] = """{ "distributionID": "fr.fire.sdisZ_2608323d-507d-4cbf-bf74-52007f8124ea", "senderID": "fr.fire.sdisZ", "dateTimeSent": "2022-09-27T08:23:34+02:00", @@ -342,7 +344,7 @@ }""" snapshots[ - "TestSnapshotCisuConverter::test_snapshot_RC_EDA_required_field_message 1" + "TestSnapshotCreateCaseConverter::test_snapshot_RC_EDA_required_field_message 1" ] = """{ "distributionID": "fr.fire.sdisZ_2608323d-507d-4cbf-bf74-52007f8124ea", "senderID": "fr.fire.sdisZ", @@ -398,7 +400,7 @@ }""" snapshots[ - "TestSnapshotCisuConverter::test_snapshot_RS_EDA_exhaustive_bis_message 1" + "TestSnapshotCreateCaseConverter::test_snapshot_RS_EDA_exhaustive_bis_message 1" ] = """{ "distributionID": "fr.health.samuA_2608323d-507d-4cbf-bf74-52007f8124ea", "senderID": "fr.health.samuA", @@ -615,7 +617,9 @@ ] }""" -snapshots["TestSnapshotCisuConverter::test_snapshot_RS_EDA_exhaustive_message 1"] = """{ +snapshots[ + "TestSnapshotCreateCaseConverter::test_snapshot_RS_EDA_exhaustive_message 1" +] = """{ "distributionID": "fr.health.samuA_2608323d-507d-4cbf-bf74-52007f8124ea", "senderID": "fr.health.samuA", "dateTimeSent": "2022-09-27T08:23:34+02:00", @@ -856,7 +860,7 @@ }""" snapshots[ - "TestSnapshotCisuConverter::test_snapshot_RS_EDA_required_field_message 1" + "TestSnapshotCreateCaseConverter::test_snapshot_RS_EDA_required_field_message 1" ] = """{ "distributionID": "fr.health.samuA_2608323d-507d-4cbf-bf74-52007f8124ea", "senderID": "fr.health.samuA", diff --git a/converter/tests/test_cisu_utils.py b/converter/tests/cisu/test_cisu_utils.py similarity index 100% rename from converter/tests/test_cisu_utils.py rename to converter/tests/cisu/test_cisu_utils.py diff --git a/converter/tests/test_cisu_converter.py b/converter/tests/cisu/test_create_case_converter.py similarity index 99% rename from converter/tests/test_cisu_converter.py rename to converter/tests/cisu/test_create_case_converter.py index 4fe8ab934..a6f4179b1 100644 --- a/converter/tests/test_cisu_converter.py +++ b/converter/tests/cisu/test_create_case_converter.py @@ -67,7 +67,7 @@ def test_to_cisu_conversion_v3(): ) -class TestSnapshotCisuConverter(TestCase): +class TestSnapshotCreateCaseConverter(TestCase): def setUp(self): self.edxl_envelope_health_to_fire_path = ( TestConstants.EDXL_HEALTH_TO_FIRE_ENVELOPE_PATH diff --git a/converter/tests/snapshots/__init__.py b/converter/tests/snapshots/__init__.py deleted file mode 100644 index 80c75f650..000000000 --- a/converter/tests/snapshots/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Empty file to mark directory as Python package From 6a8861f757b64eddbec71ad034ef7fae021a7bfa Mon Sep 17 00:00:00 2001 From: SimonClo Date: Fri, 7 Nov 2025 18:09:10 +0100 Subject: [PATCH 4/5] refactor(converter): resources info cisu to rs conversion --- .../resources_info_cisu_converter.py | 15 ++++++ .../cisu/test_resources_info_converter.py | 21 ++++++++ .../RC-RI/RC-RI_V3.0_exhaustive_fill.json | 51 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 converter/tests/cisu/test_resources_info_converter.py create mode 100644 converter/tests/fixtures/RC-RI/RC-RI_V3.0_exhaustive_fill.json diff --git a/converter/converter/cisu/resources_info/resources_info_cisu_converter.py b/converter/converter/cisu/resources_info/resources_info_cisu_converter.py index 5c14d8b17..590925451 100644 --- a/converter/converter/cisu/resources_info/resources_info_cisu_converter.py +++ b/converter/converter/cisu/resources_info/resources_info_cisu_converter.py @@ -1,4 +1,7 @@ from converter.cisu.base_cisu_converter import BaseCISUConverter +from typing import Any, Dict + +from converter.utils import get_field_value, set_value class ResourcesInfoCISUConverter(BaseCISUConverter): @@ -9,3 +12,15 @@ def get_rs_message_type(cls) -> str: @classmethod def get_cisu_message_type(cls) -> str: return "resourcesInfoCisu" + + @classmethod + def from_cisu_to_rs(cls, edxl_json: Dict[str, Any]) -> Dict[str, Any]: + output_json = cls.copy_cisu_input_content(edxl_json) + output_use_case_json = cls.copy_cisu_input_use_case_content(edxl_json) + resources = get_field_value(output_use_case_json, "$.resource") + + for resource in resources: + state = get_field_value(resource, "$.state") + set_value(resource, "$.state", [state]) + + return cls.format_rs_output_json(output_json, output_use_case_json) diff --git a/converter/tests/cisu/test_resources_info_converter.py b/converter/tests/cisu/test_resources_info_converter.py new file mode 100644 index 000000000..073ae2873 --- /dev/null +++ b/converter/tests/cisu/test_resources_info_converter.py @@ -0,0 +1,21 @@ +from converter.cisu.resources_info.resources_info_cisu_converter import ( + ResourcesInfoCISUConverter, +) +from tests.constants import TestConstants +from tests.test_helpers import TestHelper +from jsonschema import validate + +RS_RI_SCHEMA = TestHelper.load_schema("RS-RI.schema.json") + + +def test_cisu_to_rs_breaking_changes(): + cisu_raw_message = TestHelper.create_edxl_json_from_sample( + TestConstants.EDXL_FIRE_TO_HEALTH_ENVELOPE_PATH, + "tests/fixtures/RC-RI/RC-RI_V3.0_exhaustive_fill.json", + ) + rs_raw_message = ResourcesInfoCISUConverter.from_cisu_to_rs(cisu_raw_message) + rs_message = ResourcesInfoCISUConverter.copy_rs_input_use_case_content( + rs_raw_message + ) + + validate(rs_message, RS_RI_SCHEMA) diff --git a/converter/tests/fixtures/RC-RI/RC-RI_V3.0_exhaustive_fill.json b/converter/tests/fixtures/RC-RI/RC-RI_V3.0_exhaustive_fill.json new file mode 100644 index 000000000..d39c1aea9 --- /dev/null +++ b/converter/tests/fixtures/RC-RI/RC-RI_V3.0_exhaustive_fill.json @@ -0,0 +1,51 @@ +{ + "resourcesInfoCisu": { + "resource": [ + { + "contact": { + "details": "FM5678", + "type": "RADIO" + }, + "team": { + "medicalLevel": "SECOURS", + "name": "Equipe SMUR 5" + }, + "state": { + "datetime": "2024-08-01T16:42:00+02:00", + "status": "RET-BASE" + }, + "datetime": "2024-08-01T16:42:00+02:00", + "resourceId": "fr.health.samu76A.resource.VLM12", + "orgId": "fr.health.samu76A", + "vehicleType": "SMUR", + "name": "VLM 76 - A45" + }, + { + "contact": { + "details": "+33645987297", + "type": "TEL" + }, + "team": { + "name": "Equipe Rouge", + "medicalLevel": "SECOURS" + }, + "state": { + "datetime": "2024-08-01T16:40:00+02:00", + "status": "FINPEC", + "availability": false + }, + "datetime": "2024-08-01T16:40:00+02:00", + "resourceId": "fr.fire.sis076.cgo-076.resource.VSAV3A", + "requestId": "fr.fire.sis076.cgo-076.request.177", + "centerName": "Centre de Secours 76 - A", + "missionId": "fr.fire.sis076.cgo-076.mission.177", + "centerCity": "75011", + "orgId": "fr.fire.sdis76.cgo-076", + "name": "VSAV 76 - 22D8", + "vehicleType": "SIS", + "freetext": ["commentaire", "autre commentaire"] + } + ], + "caseId": "fr.health.samu800.DRFR158002421400215" + } +} From 811c7f7cfbd5eb3c9b89a55b11f84231a8fd3a73 Mon Sep 17 00:00:00 2001 From: SimonClo Date: Wed, 12 Nov 2025 16:08:33 +0100 Subject: [PATCH 5/5] feat(converter): rs to cisu conversion for resources info message --- .../resources_info_cisu_constants.py | 11 ++ .../resources_info_cisu_converter.py | 61 ++++++++++- .../cisu/test_resources_info_converter.py | 100 +++++++++++++++++- converter/tests/constants.py | 3 + 4 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 converter/converter/cisu/resources_info/resources_info_cisu_constants.py diff --git a/converter/converter/cisu/resources_info/resources_info_cisu_constants.py b/converter/converter/cisu/resources_info/resources_info_cisu_constants.py new file mode 100644 index 000000000..f97f7aebb --- /dev/null +++ b/converter/converter/cisu/resources_info/resources_info_cisu_constants.py @@ -0,0 +1,11 @@ +class ResourcesInfoCISUConstants: + RESOURCE_PATH = "$.resource" + STATE_PATH = "$.state" + VEHICLE_TYPE_PATH = "$.vehicleType" + + PATIENT_ID_KEY = "patientId" + + VEHICLE_TYPE_SIS = "SIS" + DEFAULT_CISU_STATE_VEHICLE_TYPE = "SMUR" + + DEFAULT_CISU_STATE_STATUS = "DECISION" diff --git a/converter/converter/cisu/resources_info/resources_info_cisu_converter.py b/converter/converter/cisu/resources_info/resources_info_cisu_converter.py index 590925451..2f7d9b1ac 100644 --- a/converter/converter/cisu/resources_info/resources_info_cisu_converter.py +++ b/converter/converter/cisu/resources_info/resources_info_cisu_converter.py @@ -1,7 +1,12 @@ +import datetime + from converter.cisu.base_cisu_converter import BaseCISUConverter from typing import Any, Dict -from converter.utils import get_field_value, set_value +from converter.cisu.resources_info.resources_info_cisu_constants import ( + ResourcesInfoCISUConstants, +) +from converter.utils import get_field_value, set_value, delete_paths class ResourcesInfoCISUConverter(BaseCISUConverter): @@ -17,10 +22,58 @@ def get_cisu_message_type(cls) -> str: def from_cisu_to_rs(cls, edxl_json: Dict[str, Any]) -> Dict[str, Any]: output_json = cls.copy_cisu_input_content(edxl_json) output_use_case_json = cls.copy_cisu_input_use_case_content(edxl_json) - resources = get_field_value(output_use_case_json, "$.resource") + resources = get_field_value( + output_use_case_json, ResourcesInfoCISUConstants.RESOURCE_PATH + ) for resource in resources: - state = get_field_value(resource, "$.state") - set_value(resource, "$.state", [state]) + state = get_field_value(resource, ResourcesInfoCISUConstants.STATE_PATH) + set_value(resource, ResourcesInfoCISUConstants.STATE_PATH, [state]) return cls.format_rs_output_json(output_json, output_use_case_json) + + @classmethod + def from_rs_to_cisu(cls, edxl_json: Dict[str, Any]) -> Dict[str, Any]: + output_json = cls.copy_rs_input_content(edxl_json) + output_use_case_json = cls.copy_rs_input_use_case_content(edxl_json) + + resources = get_field_value( + output_use_case_json, ResourcesInfoCISUConstants.RESOURCE_PATH + ) + for resource in resources: + rs_vehicle_type = get_field_value( + resource, ResourcesInfoCISUConstants.VEHICLE_TYPE_PATH + ) + cisu_vehicle_type = cls.translate_to_cisu_vehicle_type(rs_vehicle_type) + set_value( + resource, + ResourcesInfoCISUConstants.VEHICLE_TYPE_PATH, + cisu_vehicle_type, + ) + + cls.keep_last_state(resource) + + delete_paths(resource, [ResourcesInfoCISUConstants.PATIENT_ID_KEY]) + + return cls.format_cisu_output_json(output_json, output_use_case_json) + + @classmethod + def translate_to_cisu_vehicle_type(cls, rs_vehicle_type: str) -> str: + if rs_vehicle_type.startswith(ResourcesInfoCISUConstants.VEHICLE_TYPE_SIS): + return ResourcesInfoCISUConstants.VEHICLE_TYPE_SIS + return ResourcesInfoCISUConstants.DEFAULT_CISU_STATE_VEHICLE_TYPE + + @classmethod + def keep_last_state(cls, resource: Dict[str, Any]) -> None: + states = get_field_value(resource, ResourcesInfoCISUConstants.STATE_PATH) + if states and len(states) > 0: + latest_state = sorted(states, key=lambda x: x.get("datetime", ""))[-1] + else: + latest_state = { + "datetime": datetime.datetime.now(datetime.timezone.utc).isoformat( + timespec="seconds" + ), + "status": ResourcesInfoCISUConstants.DEFAULT_CISU_STATE_STATUS, + } + + set_value(resource, ResourcesInfoCISUConstants.STATE_PATH, latest_state) diff --git a/converter/tests/cisu/test_resources_info_converter.py b/converter/tests/cisu/test_resources_info_converter.py index 073ae2873..50ee71c3e 100644 --- a/converter/tests/cisu/test_resources_info_converter.py +++ b/converter/tests/cisu/test_resources_info_converter.py @@ -1,13 +1,60 @@ +from unittest import TestCase + from converter.cisu.resources_info.resources_info_cisu_converter import ( ResourcesInfoCISUConverter, ) from tests.constants import TestConstants -from tests.test_helpers import TestHelper +from tests.test_helpers import TestHelper, get_file_endpoint from jsonschema import validate RS_RI_SCHEMA = TestHelper.load_schema("RS-RI.schema.json") +def test_rs_to_cisu(): + rc_schema_endpoint = get_file_endpoint( + TestConstants.V3_GITHUB_RC_RI_TAG, TestConstants.RC_RI_TAG + ) + rc_schema = TestHelper.load_json_file_online(rc_schema_endpoint) + + TestHelper.conversion_tests_runner( + sample_dir=TestConstants.RS_RI_TAG, + envelope_file=TestConstants.EDXL_HEALTH_TO_FIRE_ENVELOPE_PATH, + converter_method=ResourcesInfoCISUConverter.from_rs_to_cisu, + target_schema=rc_schema, + online_tag=TestConstants.V3_GITHUB_TAG, + ) + + +def test_rs_to_cisu_should_delete_patient_id(): + rs_raw_message = TestHelper.create_edxl_json_from_sample( + TestConstants.EDXL_HEALTH_TO_FIRE_ENVELOPE_PATH, + "tests/fixtures/RS-RI/RS-RI_V3.0_exhaustive_fill.json", + ) + cisu_raw_message = ResourcesInfoCISUConverter.from_rs_to_cisu(rs_raw_message) + cisu_message = ResourcesInfoCISUConverter.copy_cisu_input_use_case_content( + cisu_raw_message + ) + resources = cisu_message.get("resource", []) + + for resource in resources: + assert "patientId" not in resource + + +def test_rs_to_cisu_should_keep_latest_state(): + resource = { + "state": [ + {"datetime": "2025-01-01T10:00:00Z"}, + {"datetime": "2025-01-02T12:00:00Z"}, + {"datetime": "2025-01-02T08:00:00Z"}, + ] + } + ResourcesInfoCISUConverter.keep_last_state(resource) + + expected_resource = {"state": {"datetime": "2025-01-02T12:00:00Z"}} + + assert resource == expected_resource + + def test_cisu_to_rs_breaking_changes(): cisu_raw_message = TestHelper.create_edxl_json_from_sample( TestConstants.EDXL_FIRE_TO_HEALTH_ENVELOPE_PATH, @@ -19,3 +66,54 @@ def test_cisu_to_rs_breaking_changes(): ) validate(rs_message, RS_RI_SCHEMA) + + +class TestTranslateToCISUVehicleType(TestCase): + def test_translate_SIS(self): + rs_vehicle_type = "SIS" + cisu_vehicle_type = ResourcesInfoCISUConverter.translate_to_cisu_vehicle_type( + rs_vehicle_type + ) + self.assertEqual(cisu_vehicle_type, "SIS") + + def test_translate_SIS_DRAGON(self): + rs_vehicle_type = "SIS.DRAGON" + cisu_vehicle_type = ResourcesInfoCISUConverter.translate_to_cisu_vehicle_type( + rs_vehicle_type + ) + self.assertEqual(cisu_vehicle_type, "SIS") + + def test_translate_AUTREVEC(self): + rs_vehicle_type = "AUTREVEC" + cisu_vehicle_type = ResourcesInfoCISUConverter.translate_to_cisu_vehicle_type( + rs_vehicle_type + ) + self.assertEqual(cisu_vehicle_type, "SMUR") + + def test_translate_FSI_HELIFSI(self): + rs_vehicle_type = "FSI.HELIFSI" + cisu_vehicle_type = ResourcesInfoCISUConverter.translate_to_cisu_vehicle_type( + rs_vehicle_type + ) + self.assertEqual(cisu_vehicle_type, "SMUR") + + def test_translate_SMUR(self): + rs_vehicle_type = "SMUR" + cisu_vehicle_type = ResourcesInfoCISUConverter.translate_to_cisu_vehicle_type( + rs_vehicle_type + ) + self.assertEqual(cisu_vehicle_type, "SMUR") + + def test_translate_SMUR_VLM(self): + rs_vehicle_type = "SMUR.VLM" + cisu_vehicle_type = ResourcesInfoCISUConverter.translate_to_cisu_vehicle_type( + rs_vehicle_type + ) + self.assertEqual(cisu_vehicle_type, "SMUR") + + def test_translate_TSU_VSL(self): + rs_vehicle_type = "TSU.VSL" + cisu_vehicle_type = ResourcesInfoCISUConverter.translate_to_cisu_vehicle_type( + rs_vehicle_type + ) + self.assertEqual(cisu_vehicle_type, "SMUR") diff --git a/converter/tests/constants.py b/converter/tests/constants.py index 06078a763..fd0d9b526 100644 --- a/converter/tests/constants.py +++ b/converter/tests/constants.py @@ -2,6 +2,8 @@ class TestConstants: V1_GITHUB_TAG = "release/1.x-maintenance" V2_GITHUB_TAG = "release/2.x-maintenance" V3_GITHUB_TAG = "main" + # TODO: remove rc tag when rc ri merged to main + V3_GITHUB_RC_RI_TAG = "3.2.0-rc.4" EDXL_HEALTH_TO_FIRE_ENVELOPE_PATH = ( "tests/fixtures/EDXL/edxl_envelope_health_to_fire.json" @@ -17,6 +19,7 @@ class TestConstants: GEO_REQ_TAG = "GEO-REQ" GEO_RES_TAG = "GEO-RES" RC_EDA_TAG = "RC-EDA" + RC_RI_TAG = "RC-RI" RS_BPV_TAG = "RS-BPV" RS_DR_TAG = "RS-DR" RS_EDA_TAG = "RS-EDA"