Skip to content

Commit

Permalink
Merge pull request #9 from pymeasure/dependency-adjustments
Browse files Browse the repository at this point in the history
Dependency adjustments
  • Loading branch information
BenediktBurger authored Sep 24, 2023
2 parents 8fe6823 + 9e7d324 commit 91c5a99
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 67 deletions.
4 changes: 2 additions & 2 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ dependencies:
- pyzmq=25.1.0
- pip # don't pin, to gain newest conda compatibility fixes
- pip:
- jsonrpc2-pyclient==2.2.12
- openrpc==6.3.17
- jsonrpc2-pyclient==4.3.0
- openrpc==8.1.0
- uuid7==0.1.0
# Development dependencies below
- pytest=7.2.0
Expand Down
8 changes: 8 additions & 0 deletions pyleco/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@
# Current protocol version
VERSION: int = 0
VERSION_B: bytes = VERSION.to_bytes(1, "big")


# Default ports
COORDINATOR_PORT = 12300 # the Coordinator receives and sends at that port.
PROXY_RECEIVING_PORT = 11100 # the proxy server receives at that port
PROXY_SENDING_PORT = 11099 # the proxy server sends at that port
LOG_RECEIVING_PORT = 11098 # the log server receives at that port
LOG_SENDING_PORT = 11097 # the log server sends at that port
15 changes: 7 additions & 8 deletions pyleco/core/internal_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@

from typing import Any, Optional, Protocol

from .leco_protocols import ComponentProtocol
from .message import Message
from .rpc_generator import RPCGenerator


class CommunicatorProtocol(ComponentProtocol, Protocol):
class CommunicatorProtocol(Protocol):
"""A helper class for a Component, to communicate via the LECO protocol.
For example a Director might use such a class to send/read messages to/from an Actor.
Expand All @@ -48,12 +47,12 @@ class CommunicatorProtocol(ComponentProtocol, Protocol):
namespace: Optional[str] = None
rpc_generator: RPCGenerator

def sign_in(self) -> None: ...
def sign_in(self) -> None: ... # pragma: no cover

def sign_out(self) -> None: ...
def sign_out(self) -> None: ... # pragma: no cover

def send(self,
receiver: str | bytes,
receiver: bytes | str,
conversation_id: Optional[bytes] = None,
data: Optional[Any] = None,
**kwargs) -> None:
Expand All @@ -62,7 +61,7 @@ def send(self,
receiver=receiver, conversation_id=conversation_id, data=data, **kwargs
))

def send_message(self, message: Message) -> None: ...
def send_message(self, message: Message) -> None: ... # pragma: no cover

def ask(self, receiver: bytes | str, conversation_id: Optional[bytes] = None,
data: Optional[Any] = None,
Expand All @@ -71,6 +70,6 @@ def ask(self, receiver: bytes | str, conversation_id: Optional[bytes] = None,
return self.ask_message(message=Message(
receiver=receiver, conversation_id=conversation_id, data=data, **kwargs))

def ask_message(self, message: Message) -> Message: ...
def ask_message(self, message: Message) -> Message: ... # pragma: no cover

def close(self) -> None: ...
def close(self) -> None: ... # pragma: no cover
15 changes: 11 additions & 4 deletions pyleco/core/leco_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,17 @@ def coordinator_sign_in(self) -> None: ...

def coordinator_sign_out(self) -> None: ...

def set_nodes(self, nodes: dict[str, str]) -> None: ...
def add_nodes(self, nodes: dict[str, str]) -> None: ...

def compose_global_directory(self) -> dict: ...
def send_nodes(self) -> dict[str, str]: ...

def compose_local_directory(self) -> dict: ...
def record_components(self, components: list[str]) -> None: ...

def send_local_components(self) -> list[str]: ...

def send_global_components(self) -> dict[str, list[str]]: ...

def remove_expired_adresses(self, expiration_time: float) -> None: ...


class ActorProtocol(ComponentProtocol, Protocol):
Expand All @@ -110,7 +116,8 @@ def get_parameters(self, parameters: Union[list[str], tuple[str, ...]]) -> dict[

def set_parameters(self, parameters: dict[str, Any]) -> None: ...

def call_action(self, action: str, _args: Optional[list | tuple] = None, **kwargs) -> Any: ...
def call_action(self, action: str, args: Optional[list | tuple] = None,
kwargs: Optional[dict[str, Any]] = None) -> Any: ...


class PollingActorProtocol(ActorProtocol, Protocol):
Expand Down
8 changes: 7 additions & 1 deletion pyleco/core/rpc_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
from typing import Any

from jsonrpc2pyclient._irpcclient import IRPCClient # type: ignore
from jsonrpcobjects.objects import Error


# according to error raised by IRPCClient if decoding fails.
INVALID_SERVER_RESPONSE = Error(code=-32000, message="Invalid response from server.")


class RPCGenerator(IRPCClient):
Expand All @@ -37,7 +42,8 @@ def build_request_str(self, method: str, *args, **kwargs) -> str:
raise ValueError(
"You may not specify list of positional arguments "
"and give additional keyword arguments at the same time.")
return self._build_request(method=method, params=kwargs or list(args) or None).json()
return self._build_request(method=method, params=kwargs or list(args) or None
).model_dump_json()

def get_result_from_response(self, data: bytes | str) -> Any:
"""Get the result of that object or raise an error."""
Expand Down
32 changes: 17 additions & 15 deletions pyleco/core/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,25 @@
#

import json
from typing import Optional, NamedTuple
from typing import Any, Optional, NamedTuple

from uuid_extensions import uuid7 # type: ignore # as long as uuid does not yet support UUIDv7
from jsonrpcobjects.objects import (RequestObject, RequestObjectParams,
ResultResponseObject,
ErrorResponseObject,
NotificationObject, NotificationObjectParams,
from jsonrpcobjects.objects import (Request,
ParamsRequest,
ResultResponse,
ErrorResponse,
Notification,
ParamsNotification,
)


json_objects = (
RequestObject,
RequestObjectParams,
ResultResponseObject,
ErrorResponseObject,
NotificationObject,
NotificationObjectParams,
Request,
ParamsRequest,
ResultResponse,
ErrorResponse,
Notification,
ParamsNotification,
)


Expand Down Expand Up @@ -108,18 +110,18 @@ def interpret_header(header: bytes) -> Header:
return Header(conversation_id, message_id, message_type)


def serialize_data(data: object) -> bytes:
def serialize_data(data: Any) -> bytes:
"""Turn `data` into a bytes object.
Due to json serialization, data must not contain a bytes object!
"""
if isinstance(data, json_objects):
return data.json().encode() # type: ignore
return data.model_dump_json().encode() # type: ignore
else:
return json.dumps(data).encode()
return json.dumps(data, separators=(',', ':')).encode()


def deserialize_data(content: bytes) -> object:
def deserialize_data(content: bytes) -> Any:
"""Turn received message content into python objects."""
return json.loads(content.decode())

Expand Down
16 changes: 8 additions & 8 deletions pyleco/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,27 @@

from typing import Any

from jsonrpcobjects.objects import ErrorObject, ErrorObjectData, ErrorResponseObject
from jsonrpcobjects.objects import Error, DataError, ErrorResponse

# JSON specification:
# -32000 to -32099 Server error reserved for implementation-defined server-errors

# TODO define valid error codes: Proposal:
# general error: -32000
# Routing errors (Coordinator) between -32090 and -32099
NOT_SIGNED_IN = ErrorObject(code=-32090, message="You did not sign in!")
DUPLICATE_NAME = ErrorObject(code=-32091, message="The name is already taken.")
NODE_UNKNOWN = ErrorObject(code=-32092, message="Node is not known.")
RECEIVER_UNKNOWN = ErrorObject(code=-32093, message="Receiver is not in addresses list.")
NOT_SIGNED_IN = Error(code=-32090, message="You did not sign in!")
DUPLICATE_NAME = Error(code=-32091, message="The name is already taken.")
NODE_UNKNOWN = Error(code=-32092, message="Node is not known.")
RECEIVER_UNKNOWN = Error(code=-32093, message="Receiver is not in addresses list.")


def generate_error_with_data(error: ErrorObject, data: Any) -> ErrorObjectData:
return ErrorObjectData(code=error.code, message=error.message, data=data)
def generate_error_with_data(error: Error, data: Any) -> DataError:
return DataError(code=error.code, message=error.message, data=data)


class CommunicationError(ConnectionError):
"""Something went wrong, send an `error_msg` to the recipient."""

def __init__(self, text: str, error_payload: ErrorResponseObject, *args: Any) -> None:
def __init__(self, text: str, error_payload: ErrorResponse, *args: Any) -> None:
super().__init__(text, *args)
self.error_payload = error_payload
52 changes: 32 additions & 20 deletions pyleco/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
# THE SOFTWARE.
#

from typing import Optional, Sequence

from .core.message import Message
from .core.internal_protocols import CommunicatorProtocol
from .core.rpc_generator import RPCGenerator
Expand All @@ -47,44 +49,54 @@ class FakeSocket:
:attr list _r: List of messages which can be read.
"""

def __init__(self, socket_type, *args):
self.socket_type = socket_type
self.addr = None
self._s = []
self._r = []
self.closed = False
def __init__(self, socket_type: int, *args) -> None:
self.closed: bool = False

def bind(self, addr):
# Added for testing purposes
self.addr: None | str = None
self.socket_type: int = socket_type
# they contain a list of messages sent/received
self._s: list[list[bytes]] = []
self._r: list[list[bytes]] = []

def bind(self, addr: str) -> None:
self.addr = addr

def bind_to_random_port(self, addr, *args, **kwargs):
def bind_to_random_port(self, addr: str, *args, **kwargs) -> int:
self.addr = addr
return 5

def unbind(self, addr=None):
def unbind(self, addr: Optional[str] = None) -> None:
self.addr = None

def connect(self, addr):
def connect(self, addr: str):
self.addr = addr

def disconnect(self, addr=None):
def disconnect(self, addr: Optional[str] = None) -> None:
self.addr = None

def poll(self, timeout=0, flags="PollEvent.POLLIN"):
def poll(self, timeout: Optional[int] = None,
flags: int = "PollEvent.POLLIN") -> int: # type: ignore
"""Poll the socket for events.
:returns: poll event mask (POLLIN, POLLOUT), 0 if the timeout was reached without an event.
"""
return 1 if len(self._r) else 0

def recv_multipart(self):
def recv_multipart(self, flags: int = 0, *, copy: bool = True, track: bool = False
) -> list[bytes]:
return self._r.pop(0)

def send_multipart(self, parts):
def send_multipart(self, msg_parts: Sequence, flags: int = 0, copy: bool = True,
track: bool = False, **kwargs) -> None:
# print(parts)
for i, part in enumerate(parts):
for i, part in enumerate(msg_parts):
if not isinstance(part, bytes):
# Similar to real error message.
raise TypeError(f"Frame {i} ({part}) does not support the buffer interface.")
self._s.append(list(parts))
self._s.append(list(msg_parts))

def close(self, linger=None):
def close(self, linger: Optional[int] = None) -> None:
self.addr = None
self.closed = True

Expand All @@ -98,13 +110,13 @@ def __init__(self, name: str):
self._s: list[Message] = []

def sign_in(self) -> None:
return super().sign_in()
self._signed_in = True

def sign_out(self) -> None:
return super().sign_out()
self._signed_in = False

def close(self) -> None:
return super().close()
self._closed = True

def send_message(self, message: Message) -> None:
if not message.sender:
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ classifiers = [
requires-python = ">=3.10"
dependencies = [
"pyzmq >= 22.3.0",
"openrpc >= 6.3.17",
"jsonrpc2-pyclient >= 2.2.12",
"openrpc >= 8.1.0",
"jsonrpc2-pyclient >= 4.3.0",
"jsonrpc2-objects >= 3.0.0",
"uuid7 >= 0.1.0",
]

Expand Down
16 changes: 13 additions & 3 deletions tests/core/test_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import pytest

from pyleco.core import VERSION_B
from pyleco.core.serialization import serialize_data

from pyleco.core.message import Message

Expand All @@ -46,7 +47,7 @@ def message() -> Message:

class Test_Message_create_message:
def test_payload(self, message: Message):
assert message.payload == [b'[["GET", [1, 2]], ["GET", 3]]']
assert message.payload == [serialize_data([["GET", [1, 2]], ["GET", 3]])]

def test_version(self, message: Message):
assert message.version == VERSION_B
Expand All @@ -66,7 +67,7 @@ def test_to_frames(self, message: Message):
b"N1.receiver",
b"N2.sender",
b"conversation_id;midT",
b'[["GET", [1, 2]], ["GET", 3]]',
serialize_data([["GET", [1, 2]], ["GET", 3]]),
]

def test_message_without_data_does_not_have_payload_frame(self):
Expand All @@ -83,6 +84,11 @@ def test_message_data_str_to_binary_data(self):
message = Message(b"rec", data="some string")
assert message.payload[0] == b"some string"

@pytest.mark.parametrize("key", ("conversation_id", "message_id", "message_type"))
def test_header_param_incompatible_with_header_element_params(self, key):
with pytest.raises(ValueError, match="header"):
Message(receiver=b"", header=b"whatever", **{key: b"content"})


class Test_Message_from_frames:
def test_message_from_frames(self, message: Message):
Expand Down Expand Up @@ -152,7 +158,7 @@ def test_sender_is_bytes(self, str_message: Message):
class Test_Message_data_payload_conversion:
def test_data_to_payload(self):
message = Message(b"r", b"s", data=([{5: "1asfd"}], 8))
assert message.payload == [b'[[{"5": "1asfd"}], 8]']
assert message.payload == [serialize_data([[{"5": "1asfd"}], 8])]
assert message.data == [[{'5': "1asfd"}], 8] # converted to and from json, so modified!

def test_payload_to_data(self):
Expand Down Expand Up @@ -191,3 +197,7 @@ def test_distinguish_empty_payload_frame(self):
m2 = Message("r", conversation_id=b"conversation_id;")
assert m2.payload == [] # verify that it does not have a payload
assert m1 != m2

@pytest.mark.parametrize("other", (5, 3.4, [64, 3], (5, "string"), "string"))
def test_comparison_with_something_else_fails(self, message, other):
assert message != other
Loading

1 comment on commit 91c5a99

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyLECO Coverage

Coverage Report
FileStmtsMissCoverMissing
pyleco
   test.py61610%25–128
pyleco/core
   internal_protocols.py12120%25–70
   message.py62298%121, 148
TOTAL2117568% 

Coverage Summary

Tests Skipped Failures Errors Time
84 0 💤 0 ❌ 0 🔥 0.504s ⏱️

Please sign in to comment.