From ab5e9836feb2626e907e0e9f7d1c2690d7397769 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:21:42 +0100 Subject: [PATCH 1/5] Add json content types. --- pyleco/core/serialization.py | 39 ++++++++++++++++++++++- tests/core/test_serialization.py | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/pyleco/core/serialization.py b/pyleco/core/serialization.py index 51bb162d..6760c02c 100644 --- a/pyleco/core/serialization.py +++ b/pyleco/core/serialization.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from enum import IntEnum +from enum import IntEnum, IntFlag import json from typing import Any, Optional, NamedTuple, Union @@ -68,6 +68,18 @@ class MessageTypes(IntEnum): JSON = 1 +class JsonContentTypes(IntFlag): + """Type of the JSON content.""" + INVALID = 0 + REQUEST = 1 + RESPONSE = 2 + RESULT = 4 + ERROR = 8 + BATCH = 16 + RESULT_RESPONSE = RESPONSE + RESULT + ERROR_RESPONSE = RESPONSE + ERROR + + def create_header_frame(conversation_id: Optional[bytes] = None, message_id: Optional[Union[bytes, int]] = 0, message_type: Union[bytes, int, MessageTypes] = MessageTypes.NOT_DEFINED, @@ -139,3 +151,28 @@ def deserialize_data(content: bytes) -> Any: def generate_conversation_id() -> bytes: """Generate a conversation_id.""" return uuid7(as_type="bytes") # type: ignore + + +def _get_json_object_type(data: dict[str, Any]) -> JsonContentTypes: + if isinstance(data, dict): + if "method" in data.keys(): + return JsonContentTypes.REQUEST + elif "result" in data.keys(): + return JsonContentTypes.RESULT_RESPONSE + elif "error" in data.keys(): + return JsonContentTypes.ERROR_RESPONSE + return JsonContentTypes.INVALID + + +def get_json_content_type(data: Any) -> JsonContentTypes: + if isinstance(data, list): + content = JsonContentTypes.BATCH if data else JsonContentTypes.INVALID + for element in data: + element_typ = _get_json_object_type(element) + if element_typ == JsonContentTypes.INVALID: + return JsonContentTypes.INVALID + else: + content |= element_typ + return content + else: + return _get_json_object_type(data) diff --git a/tests/core/test_serialization.py b/tests/core/test_serialization.py index 0d77c223..f4a369c3 100644 --- a/tests/core/test_serialization.py +++ b/tests/core/test_serialization.py @@ -22,10 +22,13 @@ # THE SOFTWARE. # +from typing import Any, Optional, Union + import pytest from jsonrpcobjects.objects import Request from pyleco.core import serialization +from pyleco.core.serialization import JsonContentTypes, get_json_content_type class Test_create_header_frame: @@ -103,3 +106,54 @@ def test_UUID_version(self, conversation_id): def test_variant(self, conversation_id): assert conversation_id[8] >> 6 == 0b10 + + +def test_json_type_result_is_response(): + assert JsonContentTypes.RESPONSE in JsonContentTypes.RESULT_RESPONSE + assert JsonContentTypes.RESULT in JsonContentTypes.RESULT_RESPONSE + + +def test_json_type_error_is_response(): + assert JsonContentTypes.RESPONSE in JsonContentTypes.ERROR_RESPONSE + assert JsonContentTypes.ERROR in JsonContentTypes.ERROR_RESPONSE + + +class Test_get_json_content: + + @staticmethod + def create_request(method: str, params: Optional[Union[list, dict]] = None, id: int = 1 + ) -> dict[str, Any]: + return {"jsonrpc": "2.0", "id": id, "method": method, "params": params} + + @staticmethod + def create_result(result: Any, id: int = 1) -> dict[str, Any]: + return {"jsonrpc": "2.0", "result": result, "id": id} + + @staticmethod + def create_error(error_code: int, error_message: str, id: int = 1) -> dict[str, Any]: + return {"jsonrpc": "2.0", "id": id, "error": {"code": error_code, "message": error_message}} + + @pytest.mark.parametrize("data, type", ( + (create_request("abc"), JsonContentTypes.REQUEST), + ([create_request(method="abc")] * 2, JsonContentTypes.REQUEST | JsonContentTypes.BATCH), + (create_result(None), JsonContentTypes.RESULT_RESPONSE), + ([create_result(None), create_result(5, 7)], + JsonContentTypes.RESULT_RESPONSE | JsonContentTypes.BATCH), + (create_error(89, "whatever"), JsonContentTypes.ERROR_RESPONSE), + ([create_error(89, "xy")] * 2, JsonContentTypes.ERROR_RESPONSE | JsonContentTypes.BATCH), + ([create_result(4), create_error(32, "xy")], # batch of result and error + JsonContentTypes.RESULT_RESPONSE | JsonContentTypes.BATCH | JsonContentTypes.ERROR), + )) + def test_data_is_valid_type(self, data, type): + assert get_json_content_type(data) == type + + @pytest.mark.parametrize("data", ( + {}, + [], + [{}], + {"some": "thing"}, + 5.6, + "adsfasdf", + )) + def test_invalid_data(self, data): + assert get_json_content_type(data) == JsonContentTypes.INVALID From 68a33e366cfd78c354b9a2241e2ef43025195b94 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:22:57 +0100 Subject: [PATCH 2/5] Fix Coordinator message handling. --- pyleco/coordinators/coordinator.py | 57 +++++++++++++++++--------- tests/coordinators/test_coordinator.py | 16 ++++++-- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/pyleco/coordinators/coordinator.py b/pyleco/coordinators/coordinator.py index 628c75fd..e828e58a 100644 --- a/pyleco/coordinators/coordinator.py +++ b/pyleco/coordinators/coordinator.py @@ -25,7 +25,7 @@ from json import JSONDecodeError import logging from socket import gethostname -from typing import Optional, Union +from typing import Any, Optional, Union from jsonrpcobjects.objects import ErrorResponse, Request, ParamsRequest from openrpc import RPCServer @@ -35,6 +35,7 @@ from ..core import COORDINATOR_PORT from ..utils.coordinator_utils import Directory, ZmqNode, ZmqMultiSocket, MultiSocket from ..core.message import Message, MessageTypes + from ..core.serialization import get_json_content_type, JsonContentTypes from ..errors import CommunicationError from ..errors import NODE_UNKNOWN, RECEIVER_UNKNOWN, generate_error_with_data from ..utils.timers import RepeatingTimer @@ -45,6 +46,7 @@ from pyleco.core import COORDINATOR_PORT from pyleco.utils.coordinator_utils import Directory, ZmqNode, ZmqMultiSocket, MultiSocket from pyleco.core.message import Message, MessageTypes + from pyleco.core.serialization import get_json_content_type, JsonContentTypes from pyleco.errors import CommunicationError from pyleco.errors import NODE_UNKNOWN, RECEIVER_UNKNOWN, generate_error_with_data from pyleco.utils.timers import RepeatingTimer @@ -318,33 +320,48 @@ def handle_commands(self, sender_identity: bytes, message: Message) -> None: self.current_message = message self.current_identity = sender_identity if message.header_elements.message_type == MessageTypes.JSON: - try: - data = message.data - except JSONDecodeError: - log.error(f"Invalid JSON message from {message.sender!r} received: {message.payload[0]!r}") # noqa - return - if isinstance(data, dict): - if error := data.get("error"): - log.error(f"Error from {message.sender!r} received: {error}.") - return - elif data.get("result", False) is None: - return # acknowledgement == heartbeat - try: - self.handle_rpc_call(sender_identity=sender_identity, message=message) - except Exception as exc: - log.exception(f"Invalid JSON-RPC message from {message.sender!r} received: {data}", - exc_info=exc) + self.handle_json_commands(message=message) else: log.error( f"Message from {message.sender!r} of unknown type received: {message.payload[0]!r}") - def handle_rpc_call(self, sender_identity: bytes, message: Message) -> None: + def handle_json_commands(self, message: Message) -> None: + try: + data: Union[list[dict[str, Any]], dict[str, Any]] = message.data # type: ignore + except JSONDecodeError: + log.error( + f"Invalid JSON message from {message.sender!r} received: {message.payload[0]!r}") + return + json_type = get_json_content_type(data) + if JsonContentTypes.REQUEST in json_type: + try: + self.handle_rpc_call(message=message) + except Exception as exc: + log.exception( + f"Invalid JSON-RPC message from {message.sender!r} received: {data}", + exc_info=exc) + elif JsonContentTypes.RESULT_RESPONSE == json_type: + if data.get("result", False) is None: # type: ignore + pass # acknowledgement == heartbeat + elif JsonContentTypes.ERROR_RESPONSE in json_type: + log.error(f"Error from {message.sender!r} received: {data}.") + elif JsonContentTypes.RESULT_RESPONSE in json_type: # batches + for element in data: + if element.get("result", False) is not None: # type: ignore + log.info(f"Unexpeced result received: {data}") + elif json_type == JsonContentTypes.INVALID: + log.error( + f"Invalid JSON RPC message from {message.sender!r} received: {message.payload[0]!r}") # noqa + else: + log.error(f"Unrecognized JSON RPC message {message.sender!r} received: {message.payload[0]!r}") # noqa + + def handle_rpc_call(self, message: Message) -> None: reply = self.rpc.process_request(message.payload[0]) sender_namespace = message.sender_elements.namespace - log.debug(f"Reply '{reply!r}' to {message.sender!r} at node {sender_namespace!r}.") + log.debug(f"Reply {reply!r} to {message.sender!r} at node {sender_namespace!r}.") if sender_namespace == self.namespace or sender_namespace == b"": self.send_main_sock_reply( - sender_identity=sender_identity, + sender_identity=self.current_identity, original_message=message, data=reply, message_type=MessageTypes.JSON, diff --git a/tests/coordinators/test_coordinator.py b/tests/coordinators/test_coordinator.py index 2525bd9b..4a1c04ad 100644 --- a/tests/coordinators/test_coordinator.py +++ b/tests/coordinators/test_coordinator.py @@ -301,8 +301,8 @@ def test_remote_heartbeat(coordinator: Coordinator, fake_counting, sender): class Test_handle_commands: class SpecialCoordinator(Coordinator): - def handle_rpc_call(self, sender_identity: bytes, message: Message) -> None: - self._rpc = sender_identity, message + def handle_rpc_call(self, message: Message) -> None: + self._rpc = message @pytest.fixture def coordinator_hc(self) -> Coordinator: @@ -327,7 +327,7 @@ def test_store_identity(self, coordinator_hc: Coordinator): )) def test_call_handle_rpc_call(self, coordinator_hc: Coordinator, identity, message): coordinator_hc.handle_commands(identity, message) - assert coordinator_hc._rpc == (identity, message) # type: ignore + assert coordinator_hc._rpc == message # type: ignore def test_log_error_response(self, coordinator_hc: Coordinator): pass # TODO @@ -340,6 +340,16 @@ def test_pass_at_null_result(self, coordinator_hc: Coordinator): assert not hasattr(coordinator_hc, "_rpc") # assert no error log entry. TODO + def test_pass_at_batch_of_null_result(self, coordinator_hc: Coordinator): + coordinator_hc.handle_commands(b"", + Message(b"", + message_type=MessageTypes.JSON, + data=[{"jsonrpc": "2.0", "result": None, "id": 1}, + {"jsonrpc": "2.0", "result": None, "id": 2}] + )) + assert not hasattr(coordinator_hc, "_rpc") + # assert no error log entry. TODO + @pytest.mark.parametrize("data", ( {"jsonrpc": "2.0", "no method": 7}, ["jsonrpc", "2.0", "no method", 7], # not a dict From 237d8e22173bfb21bb45b5170b0bce8ae007c070 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:23:19 +0100 Subject: [PATCH 3/5] Add content types to MessageHandler --- pyleco/utils/message_handler.py | 12 ++++++------ tests/utils/test_message_handler.py | 23 +++++++++++------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/pyleco/utils/message_handler.py b/pyleco/utils/message_handler.py index 9a9ddad0..c577734a 100644 --- a/pyleco/utils/message_handler.py +++ b/pyleco/utils/message_handler.py @@ -35,6 +35,7 @@ from ..core.leco_protocols import ExtendedComponentProtocol from ..core.message import Message, MessageTypes from ..core.rpc_generator import RPCGenerator +from ..core.serialization import JsonContentTypes, get_json_content_type from .base_communicator import BaseCommunicator from .log_levels import PythonLogLevels from .zmq_log_handler import ZmqLogHandler @@ -223,16 +224,15 @@ def handle_message(self, message: Message) -> None: def handle_json_message(self, message: Message) -> None: try: data: dict[str, Any] = message.data # type: ignore - keys = data.keys() - except (JSONDecodeError, AttributeError) as exc: + except (JSONDecodeError) as exc: self.log.exception(f"Could not decode json message {message}", exc_info=exc) return - if "method" in keys: + content = get_json_content_type(data) + if JsonContentTypes.REQUEST in content: self.handle_json_request(message=message) - return - elif "error" in keys: + elif JsonContentTypes.ERROR in content: self.handle_json_error(message=message) - elif "result" in keys: + elif JsonContentTypes.RESULT in content: self.handle_json_result(message) else: self.log.error(f"Invalid JSON message received: {message}") diff --git a/tests/utils/test_message_handler.py b/tests/utils/test_message_handler.py index 2bba1b0d..2f1b47a7 100644 --- a/tests/utils/test_message_handler.py +++ b/tests/utils/test_message_handler.py @@ -396,9 +396,9 @@ def test_read_and_handle_message(self, handler: MessageHandler, def test_handle_not_signed_in_message(self, handler: MessageHandler): handler.sign_in = MagicMock() # type: ignore handler.socket._r = [Message(receiver="handler", sender="N1.COORDINATOR", # type: ignore - message_type=MessageTypes.JSON, - data=ErrorResponse(id=5, error=NOT_SIGNED_IN), - ).to_frames()] + message_type=MessageTypes.JSON, + data=ErrorResponse(id=5, error=NOT_SIGNED_IN), + ).to_frames()] handler.read_and_handle_message() assert handler.namespace is None handler.sign_in.assert_called_once() @@ -423,18 +423,18 @@ def test_handle_receiver_unknown_message(self, handler: MessageHandler): def test_handle_ACK_does_not_change_Namespace(self, handler: MessageHandler): """Test that an ACK does not change the Namespace, if it is already set.""" handler.socket._r = [Message(b"N3.handler", b"N3.COORDINATOR", # type: ignore - message_type=MessageTypes.JSON, - data={"id": 3, "result": None, "jsonrpc": "2.0"}).to_frames()] + message_type=MessageTypes.JSON, + data={"id": 3, "result": None, "jsonrpc": "2.0"}).to_frames()] handler.namespace = "N1" handler.read_and_handle_message() assert handler.namespace == "N1" def test_handle_invalid_json_message(self, handler: MessageHandler, - caplog: pytest.LogCaptureFixture): + caplog: pytest.LogCaptureFixture): """An invalid message should not cause the message handler to crash.""" handler.socket._r = [Message(b"N3.handler", b"N3.COORDINATOR", # type: ignore - message_type=MessageTypes.JSON, - data={"without": "method..."}).to_frames()] + message_type=MessageTypes.JSON, + data={"without": "method..."}).to_frames()] handler.read_and_handle_message() assert caplog.records[-1].msg.startswith("Invalid JSON message") @@ -442,11 +442,10 @@ def test_handle_corrupted_message(self, handler: MessageHandler, caplog: pytest.LogCaptureFixture): """An invalid message should not cause the message handler to crash.""" handler.socket._r = [Message(b"N3.handler", b"N3.COORDINATOR", # type: ignore - message_type=MessageTypes.JSON, - data=[]).to_frames()] + message_type=MessageTypes.JSON, + data=[]).to_frames()] handler.read_and_handle_message() - assert caplog.records[-1].msg.startswith("Could not decode") - + assert caplog.records[-1].msg.startswith("Invalid JSON message") def test_handle_unknown_message_type(handler: MessageHandler, caplog: pytest.LogCaptureFixture): From 61a35983ff134a4a37aa9fc550f6ca1db247282e Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:56:21 +0100 Subject: [PATCH 4/5] Add tests. --- pyleco/coordinators/coordinator.py | 12 +++++------- tests/coordinators/test_coordinator.py | 25 ++++++++++++++++++++++++- tests/core/test_serialization.py | 25 +++++++++++++------------ tests/utils/test_message_handler.py | 9 +++++++++ 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/pyleco/coordinators/coordinator.py b/pyleco/coordinators/coordinator.py index e828e58a..f3d87317 100644 --- a/pyleco/coordinators/coordinator.py +++ b/pyleco/coordinators/coordinator.py @@ -341,19 +341,17 @@ def handle_json_commands(self, message: Message) -> None: f"Invalid JSON-RPC message from {message.sender!r} received: {data}", exc_info=exc) elif JsonContentTypes.RESULT_RESPONSE == json_type: - if data.get("result", False) is None: # type: ignore - pass # acknowledgement == heartbeat - elif JsonContentTypes.ERROR_RESPONSE in json_type: + if data.get("result", False) is not None: # type: ignore + log.info(f"Unexpeced result received: {data}") + elif JsonContentTypes.ERROR in json_type: log.error(f"Error from {message.sender!r} received: {data}.") - elif JsonContentTypes.RESULT_RESPONSE in json_type: # batches + elif JsonContentTypes.RESULT in json_type: for element in data: if element.get("result", False) is not None: # type: ignore log.info(f"Unexpeced result received: {data}") - elif json_type == JsonContentTypes.INVALID: + else: log.error( f"Invalid JSON RPC message from {message.sender!r} received: {message.payload[0]!r}") # noqa - else: - log.error(f"Unrecognized JSON RPC message {message.sender!r} received: {message.payload[0]!r}") # noqa def handle_rpc_call(self, message: Message) -> None: reply = self.rpc.process_request(message.payload[0]) diff --git a/tests/coordinators/test_coordinator.py b/tests/coordinators/test_coordinator.py index 4a1c04ad..b76d1728 100644 --- a/tests/coordinators/test_coordinator.py +++ b/tests/coordinators/test_coordinator.py @@ -340,7 +340,18 @@ def test_pass_at_null_result(self, coordinator_hc: Coordinator): assert not hasattr(coordinator_hc, "_rpc") # assert no error log entry. TODO - def test_pass_at_batch_of_null_result(self, coordinator_hc: Coordinator): + def test_log_at_non_null_result(self, coordinator_hc: Coordinator, + caplog: pytest.LogCaptureFixture): + caplog.set_level(10) + coordinator_hc.handle_commands(b"", + Message(b"", + message_type=MessageTypes.JSON, + data={"jsonrpc": "2.0", "result": 5})) + assert not hasattr(coordinator_hc, "_rpc") + # assert no error log entry. TODO + caplog.records[-1].msg.startswith("Unexpected result") + + def test_pass_at_batch_of_null_results(self, coordinator_hc: Coordinator): coordinator_hc.handle_commands(b"", Message(b"", message_type=MessageTypes.JSON, @@ -350,6 +361,18 @@ def test_pass_at_batch_of_null_result(self, coordinator_hc: Coordinator): assert not hasattr(coordinator_hc, "_rpc") # assert no error log entry. TODO + def test_log_at_batch_of_non_null_results(self, coordinator_hc: Coordinator, + caplog: pytest.LogCaptureFixture): + caplog.set_level(10) + coordinator_hc.handle_commands(b"", + Message(b"", + message_type=MessageTypes.JSON, + data=[{"jsonrpc": "2.0", "result": None, "id": 1}, + {"jsonrpc": "2.0", "result": 5, "id": 2}] + )) + assert not hasattr(coordinator_hc, "_rpc") + caplog.records[-1].msg.startswith("Unexpected result") + @pytest.mark.parametrize("data", ( {"jsonrpc": "2.0", "no method": 7}, ["jsonrpc", "2.0", "no method", 7], # not a dict diff --git a/tests/core/test_serialization.py b/tests/core/test_serialization.py index f4a369c3..0d3f9615 100644 --- a/tests/core/test_serialization.py +++ b/tests/core/test_serialization.py @@ -118,21 +118,21 @@ def test_json_type_error_is_response(): assert JsonContentTypes.ERROR in JsonContentTypes.ERROR_RESPONSE -class Test_get_json_content: +# Methods for get_json_content_type +def create_request(method: str, params: Optional[Union[list, dict]] = None, id: int = 1 + ) -> dict[str, Any]: + return {"jsonrpc": "2.0", "id": id, "method": method, "params": params} - @staticmethod - def create_request(method: str, params: Optional[Union[list, dict]] = None, id: int = 1 - ) -> dict[str, Any]: - return {"jsonrpc": "2.0", "id": id, "method": method, "params": params} - @staticmethod - def create_result(result: Any, id: int = 1) -> dict[str, Any]: - return {"jsonrpc": "2.0", "result": result, "id": id} +def create_result(result: Any, id: int = 1) -> dict[str, Any]: + return {"jsonrpc": "2.0", "result": result, "id": id} - @staticmethod - def create_error(error_code: int, error_message: str, id: int = 1) -> dict[str, Any]: - return {"jsonrpc": "2.0", "id": id, "error": {"code": error_code, "message": error_message}} +def create_error(error_code: int, error_message: str, id: int = 1) -> dict[str, Any]: + return {"jsonrpc": "2.0", "id": id, "error": {"code": error_code, "message": error_message}} + + +class Test_get_json_content_type: @pytest.mark.parametrize("data, type", ( (create_request("abc"), JsonContentTypes.REQUEST), ([create_request(method="abc")] * 2, JsonContentTypes.REQUEST | JsonContentTypes.BATCH), @@ -140,7 +140,8 @@ def create_error(error_code: int, error_message: str, id: int = 1) -> dict[str, ([create_result(None), create_result(5, 7)], JsonContentTypes.RESULT_RESPONSE | JsonContentTypes.BATCH), (create_error(89, "whatever"), JsonContentTypes.ERROR_RESPONSE), - ([create_error(89, "xy")] * 2, JsonContentTypes.ERROR_RESPONSE | JsonContentTypes.BATCH), + ([create_error(89, "xy")] * 2, + JsonContentTypes.ERROR_RESPONSE | JsonContentTypes.BATCH), ([create_result(4), create_error(32, "xy")], # batch of result and error JsonContentTypes.RESULT_RESPONSE | JsonContentTypes.BATCH | JsonContentTypes.ERROR), )) diff --git a/tests/utils/test_message_handler.py b/tests/utils/test_message_handler.py index 2f1b47a7..1fa4e24b 100644 --- a/tests/utils/test_message_handler.py +++ b/tests/utils/test_message_handler.py @@ -447,6 +447,15 @@ def test_handle_corrupted_message(self, handler: MessageHandler, handler.read_and_handle_message() assert caplog.records[-1].msg.startswith("Invalid JSON message") + def test_handle_undecodable_message(self, handler: MessageHandler, + caplog: pytest.LogCaptureFixture): + """An invalid message should not cause the message handler to crash.""" + message = Message(b"N3.handler", b"N3.COORDINATOR", message_type=MessageTypes.JSON) + message.payload = [b"()"] + handler.socket._r = [message.to_frames()] # type: ignore + handler.read_and_handle_message() + assert caplog.records[-1].msg.startswith("Could not decode") + def test_handle_unknown_message_type(handler: MessageHandler, caplog: pytest.LogCaptureFixture): message = Message(handler_name, sender="sender", message_type=255) From 7eb55a59a38af56c25aec5fd1a3bcad2a2152dad Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:08:23 +0100 Subject: [PATCH 5/5] Create v0.2.2 changelog --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d19cbef..b0d4f85c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # CHANGELOG +## [0.2.2] - 2024-02-14 + +### Fixed + +- Fix Communicator to distinguish correctly different json rpc messages ([#57](https://github.com/pymeasure/pyleco/issues/57)) +- Fix MessageHandler not distinguish correctly batch requests ([#56](https://github.com/pymeasure/pyleco/issues/56)) +- Bump setup-python action version to v5 + +**Full Changelog**: https://github.com/pymeasure/pyleco/compare/v0.2.1...v.0.2.2 + + ## [0.2.1] - 2024-02-13 ### Fixed @@ -73,7 +84,8 @@ _Initial alpha version, complies with [LECO protocol alpha-0.0.1](https://github @BenediktBurger, @bilderbuchi, @bklebel -[unreleased]: https://github.com/pymeasure/pyleco/compare/v0.2.1...HEAD +[unreleased]: https://github.com/pymeasure/pyleco/compare/v0.2.2...HEAD +[0.2.2]: https://github.com/pymeasure/pyleco/releases/tag/v0.2.2 [0.2.1]: https://github.com/pymeasure/pyleco/releases/tag/v0.2.1 [0.2.0]: https://github.com/pymeasure/pyleco/releases/tag/v0.2.0 [0.1.0]: https://github.com/pymeasure/pyleco/releases/tag/v0.1.0