From 11f76a3c995bd93b84f28c9a5f1ce5a0824e9f36 Mon Sep 17 00:00:00 2001 From: "javier.hernandez" Date: Fri, 17 May 2024 17:42:00 +0200 Subject: [PATCH 1/4] Added message class with the actual content implemented (ItemScheme related). Signed-off-by: javier.hernandez --- src/pysdmx/model/message.py | 84 +++++++++++++++++++++++++++ tests/model/test_message.py | 111 ++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 src/pysdmx/model/message.py create mode 100644 tests/model/test_message.py diff --git a/src/pysdmx/model/message.py b/src/pysdmx/model/message.py new file mode 100644 index 0000000..29ccb43 --- /dev/null +++ b/src/pysdmx/model/message.py @@ -0,0 +1,84 @@ +"""Message file contains the Message class for the use of external assets.""" + +from typing import Any, Dict + +from msgspec import Struct + +from pysdmx.model import Codelist, ConceptScheme +from pysdmx.model.__base import ItemScheme + +ORGS = "OrganisationSchemes" +CLS = "Codelists" +CONCEPTS = "ConceptSchemes" + +MSG_CONTENT_PKG = { + ORGS: ItemScheme, + CLS: Codelist, + CONCEPTS: ConceptScheme, +} + + +class Message(Struct, frozen=True): + """Message class holds the content of SDMX Message. + + Attributes: + content (Dict[str, Any]): Content of the message. The keys are the + content type (e.g. ``OrganisationSchemes``, ``Codelists``, etc.), + and the values are the content objects (e.g. ``ItemScheme``, + ``Codelist``, etc.). + """ + + content: Dict[str, Any] + + def __post_init__(self) -> None: + """Checks if the content is valid.""" + for content_key, content_value in self.content.items(): + if content_key not in MSG_CONTENT_PKG: + raise ValueError(f"Invalid content type: {content_key}") + + for obj_ in content_value.values(): + if not isinstance(obj_, MSG_CONTENT_PKG[content_key]): + raise ValueError( + f"Invalid content value type: {type(obj_).__name__} " + f"for {content_key}" + ) + + def __get_elements(self, type_: str) -> Dict[str, Any]: + """Returns the elements from content.""" + if type_ in self.content: + return self.content[type_] + raise ValueError(f"No {type_} found in content") + + def __get_element_by_uid(self, type_: str, unique_id: str) -> Any: + """Returns a specific element from content.""" + if type_ not in self.content: + raise ValueError(f"No {type_} found") + + if unique_id in self.content[type_]: + return self.content[type_][unique_id] + + raise ValueError(f"No {type_} with id {unique_id} found in content") + + def get_organisation_schemes(self) -> Dict[str, ItemScheme]: + """Returns the OrganisationScheme.""" + return self.__get_elements(ORGS) + + def get_codelists(self) -> Dict[str, Codelist]: + """Returns the Codelist.""" + return self.__get_elements(CLS) + + def get_concept_schemes(self) -> Dict[str, ConceptScheme]: + """Returns the Concept.""" + return self.__get_elements(CONCEPTS) + + def get_organisation_scheme_by_uid(self, unique_id: str) -> ItemScheme: + """Returns a specific OrganisationScheme.""" + return self.__get_element_by_uid(ORGS, unique_id) + + def get_codelist_by_uid(self, unique_id: str) -> Codelist: + """Returns a specific Codelist.""" + return self.__get_element_by_uid(CLS, unique_id) + + def get_concept_scheme_by_uid(self, unique_id: str) -> ConceptScheme: + """Returns a specific Concept.""" + return self.__get_element_by_uid(CONCEPTS, unique_id) diff --git a/tests/model/test_message.py b/tests/model/test_message.py new file mode 100644 index 0000000..4646a70 --- /dev/null +++ b/tests/model/test_message.py @@ -0,0 +1,111 @@ +import pytest + +from pysdmx.model import Codelist, ConceptScheme +from pysdmx.model.__base import ItemScheme +from pysdmx.model.message import Message + + +def test_initialization(): + message = Message({}) + assert message.content == {} + + +def test_get_organisation(): + org1 = ItemScheme(id="orgs1", agency="org1") + message = Message( + { + "OrganisationSchemes": { + "org1:orgs1(1.0)": org1, + } + } + ) + assert message.get_organisation_schemes() == { + "org1:orgs1(1.0)": org1, + } + + assert message.get_organisation_scheme_by_uid("org1:orgs1(1.0)") == org1 + + +def test_get_codelists(): + cl1 = Codelist(id="cl1", agency="cl1") + message = Message( + { + "Codelists": { + "cl1:cl1(1.0)": cl1, + } + } + ) + assert message.get_codelists() == { + "cl1:cl1(1.0)": cl1, + } + + assert message.get_codelist_by_uid("cl1:cl1(1.0)") == cl1 + + +def test_get_concepts(): + cs1 = ConceptScheme(id="cs1", agency="cs1") + message = Message( + { + "ConceptSchemes": { + "cs1:cs1(1.0)": cs1, + } + } + ) + assert message.get_concept_schemes() == { + "cs1:cs1(1.0)": cs1, + } + + assert message.get_concept_scheme_by_uid("cs1:cs1(1.0)") == cs1 + + +def test_empty_get_elements(): + message = Message({}) + with pytest.raises(ValueError, match="No OrganisationSchemes found"): + message.get_organisation_schemes() + + with pytest.raises(ValueError, match="No Codelists found"): + message.get_codelists() + + with pytest.raises(ValueError, match="No ConceptSchemes found"): + message.get_concept_schemes() + + +def test_empty_get_element_by_uid(): + message = Message({}) + with pytest.raises(ValueError, match="No OrganisationSchemes found"): + message.get_organisation_scheme_by_uid("org1:orgs1(1.0)") + + with pytest.raises(ValueError, match="No Codelists found"): + message.get_codelist_by_uid("cl1:cl1(1.0)") + + with pytest.raises(ValueError, match="No ConceptSchemes found"): + message.get_concept_scheme_by_uid("cs1:cs1(1.0)") + + +def test_invalid_get_element_by_uid(): + message = Message({"OrganisationSchemes": {}}) + + e_m = "No OrganisationSchemes with id" + + with pytest.raises(ValueError, match=e_m): + message.get_organisation_scheme_by_uid("org12:orgs1(1.0)") + + +def test_invalid_initialization_content_key(): + exc_message = "Invalid content type: Invalid" + with pytest.raises(ValueError, match=exc_message): + Message({"Invalid": {}}) + + +@pytest.mark.parametrize( + ("key", "value"), + [ + ("OrganisationSchemes", {"org1:orgs1(1.0)": "invalid"}), + ("Codelists", {"cl1:cl1(1.0)": "invalid"}), + ("ConceptSchemes", {"cs1:cs1(1.0)": "invalid"}), + ], +) +def test_invalid_initialization_content_value(key, value): + exc_message = f"Invalid content value type: str for {key}" + with pytest.raises(ValueError, match=exc_message): + Message({key: value}) From 5fa77cc00c04008df3de93d54ba3353cb5af2650 Mon Sep 17 00:00:00 2001 From: "javier.hernandez" Date: Tue, 21 May 2024 10:32:26 +0200 Subject: [PATCH 2/4] Merged develop branch to solve conflicts Signed-off-by: javier.hernandez --- src/pysdmx/model/message.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pysdmx/model/message.py b/src/pysdmx/model/message.py index d07d6e5..570d65b 100644 --- a/src/pysdmx/model/message.py +++ b/src/pysdmx/model/message.py @@ -38,6 +38,7 @@ class Header(Struct, frozen=True, kw_only=True): source: Optional[str] = None dataset_action: Optional[ActionType] = None + ORGS = "OrganisationSchemes" CLS = "Codelists" CONCEPTS = "ConceptSchemes" From db4114cd39847ff4a754ca6ac2da1acc8caacc49 Mon Sep 17 00:00:00 2001 From: "javier.hernandez" Date: Tue, 21 May 2024 13:08:45 +0200 Subject: [PATCH 3/4] Changed post init method to raise Client errors. Updated tests. Signed-off-by: javier.hernandez --- src/pysdmx/model/message.py | 14 +++++++++++--- tests/model/test_message.py | 7 +++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/pysdmx/model/message.py b/src/pysdmx/model/message.py index 570d65b..f5f1809 100644 --- a/src/pysdmx/model/message.py +++ b/src/pysdmx/model/message.py @@ -11,6 +11,7 @@ from msgspec import Struct +from pysdmx.errors import ClientError from pysdmx.model import Codelist, ConceptScheme from pysdmx.model.__base import ItemScheme @@ -66,13 +67,20 @@ def __post_init__(self) -> None: """Checks if the content is valid.""" for content_key, content_value in self.content.items(): if content_key not in MSG_CONTENT_PKG: - raise ValueError(f"Invalid content type: {content_key}") + raise ClientError( + 400, + f"Invalid content type: {content_key}", + "Check the docs for the proper structure on content.", + ) for obj_ in content_value.values(): if not isinstance(obj_, MSG_CONTENT_PKG[content_key]): - raise ValueError( + raise ClientError( + 400, f"Invalid content value type: {type(obj_).__name__} " - f"for {content_key}" + f"for {content_key}", + "Check the docs for the proper " + "structure on content.", ) def __get_elements(self, type_: str) -> Dict[str, Any]: diff --git a/tests/model/test_message.py b/tests/model/test_message.py index 4646a70..afca81e 100644 --- a/tests/model/test_message.py +++ b/tests/model/test_message.py @@ -1,5 +1,6 @@ import pytest +from pysdmx.errors import ClientError from pysdmx.model import Codelist, ConceptScheme from pysdmx.model.__base import ItemScheme from pysdmx.model.message import Message @@ -93,8 +94,9 @@ def test_invalid_get_element_by_uid(): def test_invalid_initialization_content_key(): exc_message = "Invalid content type: Invalid" - with pytest.raises(ValueError, match=exc_message): + with pytest.raises(ClientError) as exc_info: Message({"Invalid": {}}) + assert exc_message in str(exc_info.value.title) @pytest.mark.parametrize( @@ -107,5 +109,6 @@ def test_invalid_initialization_content_key(): ) def test_invalid_initialization_content_value(key, value): exc_message = f"Invalid content value type: str for {key}" - with pytest.raises(ValueError, match=exc_message): + with pytest.raises(ClientError) as exc_info: Message({key: value}) + assert exc_message in str(exc_info.value.title) From b256afc15c158127c1fb8d0ce24a889b2cb8e039 Mon Sep 17 00:00:00 2001 From: "javier.hernandez" Date: Tue, 21 May 2024 14:12:23 +0200 Subject: [PATCH 4/4] Changed ValueError to NotFound exceptions in content getters. Updated tests. Signed-off-by: javier.hernandez --- src/pysdmx/model/message.py | 20 ++++++++++++++++---- tests/model/test_message.py | 29 +++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/pysdmx/model/message.py b/src/pysdmx/model/message.py index f5f1809..a6a8402 100644 --- a/src/pysdmx/model/message.py +++ b/src/pysdmx/model/message.py @@ -11,7 +11,7 @@ from msgspec import Struct -from pysdmx.errors import ClientError +from pysdmx.errors import ClientError, NotFound from pysdmx.model import Codelist, ConceptScheme from pysdmx.model.__base import ItemScheme @@ -87,17 +87,29 @@ def __get_elements(self, type_: str) -> Dict[str, Any]: """Returns the elements from content.""" if type_ in self.content: return self.content[type_] - raise ValueError(f"No {type_} found in content") + raise NotFound( + 404, + f"No {type_} found in content", + f"Could not find any {type_} in content.", + ) def __get_element_by_uid(self, type_: str, unique_id: str) -> Any: """Returns a specific element from content.""" if type_ not in self.content: - raise ValueError(f"No {type_} found") + raise NotFound( + 404, + f"No {type_} found.", + f"Could not find any {type_} in content.", + ) if unique_id in self.content[type_]: return self.content[type_][unique_id] - raise ValueError(f"No {type_} with id {unique_id} found in content") + raise NotFound( + 404, + f"No {type_} with id {unique_id} found in content", + "Could not find the requested element.", + ) def get_organisation_schemes(self) -> Dict[str, ItemScheme]: """Returns the OrganisationScheme.""" diff --git a/tests/model/test_message.py b/tests/model/test_message.py index afca81e..41f690d 100644 --- a/tests/model/test_message.py +++ b/tests/model/test_message.py @@ -1,6 +1,6 @@ import pytest -from pysdmx.errors import ClientError +from pysdmx.errors import ClientError, NotFound from pysdmx.model import Codelist, ConceptScheme from pysdmx.model.__base import ItemScheme from pysdmx.model.message import Message @@ -61,35 +61,48 @@ def test_get_concepts(): def test_empty_get_elements(): message = Message({}) - with pytest.raises(ValueError, match="No OrganisationSchemes found"): + with pytest.raises(NotFound) as exc_info: message.get_organisation_schemes() - with pytest.raises(ValueError, match="No Codelists found"): + assert "No OrganisationSchemes found" in str(exc_info.value.title) + + with pytest.raises(NotFound) as exc_info: message.get_codelists() - with pytest.raises(ValueError, match="No ConceptSchemes found"): + assert "No Codelists found" in str(exc_info.value.title) + + with pytest.raises(NotFound) as exc_info: message.get_concept_schemes() + assert "No ConceptSchemes found" in str(exc_info.value.title) + def test_empty_get_element_by_uid(): message = Message({}) - with pytest.raises(ValueError, match="No OrganisationSchemes found"): + with pytest.raises(NotFound) as exc_info: message.get_organisation_scheme_by_uid("org1:orgs1(1.0)") - with pytest.raises(ValueError, match="No Codelists found"): + assert "No OrganisationSchemes found" in str(exc_info.value.title) + + with pytest.raises(NotFound) as exc_info: message.get_codelist_by_uid("cl1:cl1(1.0)") - with pytest.raises(ValueError, match="No ConceptSchemes found"): + assert "No Codelists found" in str(exc_info.value.title) + + with pytest.raises(NotFound) as exc_info: message.get_concept_scheme_by_uid("cs1:cs1(1.0)") + assert "No ConceptSchemes found" in str(exc_info.value.title) + def test_invalid_get_element_by_uid(): message = Message({"OrganisationSchemes": {}}) e_m = "No OrganisationSchemes with id" - with pytest.raises(ValueError, match=e_m): + with pytest.raises(NotFound) as exc_info: message.get_organisation_scheme_by_uid("org12:orgs1(1.0)") + assert e_m in str(exc_info.value.title) def test_invalid_initialization_content_key():