Skip to content

Commit

Permalink
Merge pull request #65 from pymeasure/reorganize_json_objects
Browse files Browse the repository at this point in the history
Reorganize json objects
  • Loading branch information
BenediktBurger committed Mar 7, 2024
2 parents db1e795 + 5b3b193 commit 20d57c0
Show file tree
Hide file tree
Showing 18 changed files with 163 additions and 80 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ _Use self defined objects instead of jsonrpc2-objects and jsonrpc2-pyclient._

- Rename `cls` parameter to `device_class` in `Actor` and `TransparentDirector`.
- Substitute `jsonrpc2-objects` and `jsonrpc2-pyclient` by self written objects.
- Move error definitions from `pyleco.errors` to `pyleco.json_utils.errors`.
- Move `pyleco.errors.CommunicationError` to `pyleco.json_utils.errors`.
- Deprecate `generate_error_with_data` in favor of `DataError.from_error` class method.
- Python requirement lowered to Python 3.8

### Added

- Add __future__.annotations to all files, which need it for annotations for Python 3.7/3.8.
- Add self written `RPCServer` as alternative to openrpc package.

### Deprecated
- Deprecate `pyleco.errors` in favor of `json_utils.errors` and `json_utils.json_objects`.


## [0.2.2] - 2024-02-14

Expand Down
26 changes: 13 additions & 13 deletions pyleco/coordinators/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,25 @@

if __name__ != "__main__":
from ..core import COORDINATOR_PORT
from ..utils.coordinator_utils import Directory, ZmqNode, ZmqMultiSocket, MultiSocket
from ..utils.coordinator_utils import CommunicationError, 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 ..json_utils.json_objects import ErrorResponse, Request, ParamsRequest
from ..json_utils.errors import NODE_UNKNOWN, RECEIVER_UNKNOWN
from ..json_utils.json_objects import ErrorResponse, Request, ParamsRequest, DataError
from ..json_utils.rpc_server import RPCServer
from ..utils.timers import RepeatingTimer
from ..utils.zmq_log_handler import ZmqLogHandler
from ..utils.events import Event, SimpleEvent
from ..utils.log_levels import PythonLogLevels
else: # pragma: no cover
from pyleco.core import COORDINATOR_PORT
from pyleco.utils.coordinator_utils import Directory, ZmqNode, ZmqMultiSocket, MultiSocket
from pyleco.utils.coordinator_utils import CommunicationError, 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.json_utils.json_objects import ErrorResponse, Request, ParamsRequest
from pyleco.json_utils.errors import NODE_UNKNOWN, RECEIVER_UNKNOWN
from pyleco.json_utils.json_objects import ErrorResponse, Request, ParamsRequest, DataError
from pyleco.json_utils.rpc_server import RPCServer
from pyleco.utils.timers import RepeatingTimer
from pyleco.utils.zmq_log_handler import ZmqLogHandler
Expand Down Expand Up @@ -288,12 +288,12 @@ def deliver_message(self, sender_identity: bytes, message: Message) -> None:
else:
self._deliver_remotely(message=message, receiver_namespace=receiver_namespace)

def _deliver_locally(self, message, receiver_name):
def _deliver_locally(self, message: Message, receiver_name: bytes) -> None:
try:
receiver_identity = self.directory.get_component_id(name=receiver_name)
except ValueError:
log.error(f"Receiver '{message.receiver}' is not in the addresses list.")
error = generate_error_with_data(RECEIVER_UNKNOWN, data=message.receiver.decode())
log.error(f"Receiver '{message.receiver!r}' is not in the addresses list.")
error = DataError.from_error(RECEIVER_UNKNOWN, data=message.receiver.decode())
self.send_message(
receiver=message.sender,
conversation_id=message.conversation_id,
Expand All @@ -303,11 +303,11 @@ def _deliver_locally(self, message, receiver_name):
else:
self.sock.send_message(receiver_identity, message)

def _deliver_remotely(self, message, receiver_namespace) -> None:
def _deliver_remotely(self, message: Message, receiver_namespace: bytes) -> None:
try:
self.directory.send_node_message(namespace=receiver_namespace, message=message)
except ValueError:
error = generate_error_with_data(NODE_UNKNOWN, data=receiver_namespace.decode())
error = DataError.from_error(NODE_UNKNOWN, data=receiver_namespace.decode())
self.send_message(
receiver=message.sender,
conversation_id=message.conversation_id,
Expand Down
27 changes: 16 additions & 11 deletions pyleco/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,32 @@
#

from typing import Any
from warnings import warn

from .json_utils.json_objects import Error, DataError, ErrorResponse

# JSON specification:
# -32000 to -32099 Server error reserved for implementation-defined server-errors
from .json_utils.errors import NOT_SIGNED_IN, DUPLICATE_NAME, NODE_UNKNOWN, RECEIVER_UNKNOWN # noqa

# TODO define valid error codes: Proposal:
# general error: -32000
# Routing errors (Coordinator) between -32090 and -32099
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.")

warn("The `pyleco.errors` module is deprecated, use the objects from the `pyleco.json_utils` "
"subpackage instead.", FutureWarning)


def generate_error_with_data(error: Error, data: Any) -> DataError:
return DataError(code=error.code, message=error.message, data=data)
"""Generate a DataError from an Error.
.. deprecated:: 0.3
Use `DataError.from_error` instead.
"""
return DataError.from_error(error=error, data=data)


class CommunicationError(ConnectionError):
"""Something went wrong, send an `error_msg` to the recipient."""
"""Something went wrong, send an `error_msg` to the recipient.
.. deprecated:: 0.3
Use the definition in `communicator_utils` module instead.
"""

def __init__(self, text: str, error_payload: ErrorResponse, *args: Any) -> None:
super().__init__(text, *args)
Expand Down
76 changes: 50 additions & 26 deletions pyleco/json_utils/errors.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,64 @@
"""This module provides exceptions for each JSON-RPC 2.0 error.
#
# This file is part of the PyLECO package.
#
# Copyright (c) 2023-2024 PyLECO Developers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

"""
Based on jsonrpc2-objects
This module provides exceptions for each JSON-RPC 2.0 error.
There is one Exception defined for each pre-defined JSON-RPC 2.0 error.
Additionally, there is a ServerError for implementation-defined errors.
Each exception extends a base exception JSONRPCError.
Copied from jsonrpc2-objects
"""

__all__ = (
"INTERNAL_ERROR",
"INVALID_PARAMS",
"INVALID_REQUEST",
"InternalError",
"InvalidParams",
"InvalidRequest",
"JSONRPCError",
"METHOD_NOT_FOUND",
"MethodNotFound",
"PARSE_ERROR",
"ParseError",
"ServerError",
"get_exception_by_code",
)

from typing import Optional, Type

from .json_objects import DataError, Error, ErrorType

SERVER_ERROR = Error(code=-32000, message="Server error")
# JSONRPC 2.0 defined errors
INVALID_REQUEST = Error(code=-32600, message="Invalid Request")
METHOD_NOT_FOUND = Error(code=-32601, message="Method not found")
INVALID_PARAMS = Error(code=-32602, message="Invalid params")
INTERNAL_ERROR = Error(code=-32603, message="Internal error")
PARSE_ERROR = Error(code=-32700, message="Parse error")

# -32000 to -32099 Server error reserved for implementation-defined server-errors
# general error: -32000
SERVER_ERROR = Error(code=-32000, message="Server error")

# LECO defined errors
# Routing errors (Coordinator) between -32090 and -32099
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.")

# Error during deserialization error of the server's response
INVALID_SERVER_RESPONSE = Error(code=-32000, message="Invalid response from server.")


class JSONRPCError(Exception):
"""Base error that all JSON RPC exceptions extend."""
Expand All @@ -44,49 +68,49 @@ def __init__(self, error: ErrorType) -> None:
self.rpc_error = error
if isinstance(error, DataError):
msg += f"\nError Data: {error.data}"
super(JSONRPCError, self).__init__(msg)
super().__init__(msg)


class ParseError(JSONRPCError):
"""Error raised when invalid JSON was received by the server."""

def __init__(self, error: Optional[ErrorType] = None) -> None:
super(ParseError, self).__init__(error or PARSE_ERROR)
super().__init__(error or PARSE_ERROR)


class InvalidRequest(JSONRPCError):
"""Error raised when the JSON sent is not a valid Request object."""

def __init__(self, error: Optional[ErrorType] = None) -> None:
super(InvalidRequest, self).__init__(error or INVALID_REQUEST)
super().__init__(error or INVALID_REQUEST)


class MethodNotFound(JSONRPCError):
"""Error raised when the method does not exist / is not available."""

def __init__(self, error: Optional[ErrorType] = None) -> None:
super(MethodNotFound, self).__init__(error or METHOD_NOT_FOUND)
super().__init__(error or METHOD_NOT_FOUND)


class InvalidParams(JSONRPCError):
"""Error raised when invalid method parameter(s) are supplied."""

def __init__(self, error: Optional[ErrorType] = None) -> None:
super(InvalidParams, self).__init__(error or INVALID_PARAMS)
super().__init__(error or INVALID_PARAMS)


class InternalError(JSONRPCError):
"""Error raised when there is an internal JSON-RPC error."""

def __init__(self, error: Optional[ErrorType] = None) -> None:
super(InternalError, self).__init__(error or INTERNAL_ERROR)
super().__init__(error or INTERNAL_ERROR)


class ServerError(JSONRPCError):
"""Error raised when a server error occurs."""

def __init__(self, error: ErrorType) -> None:
super(ServerError, self).__init__(error)
super().__init__(error)


def get_exception_by_code(code: int) -> Optional[Type[JSONRPCError]]:
Expand Down
4 changes: 4 additions & 0 deletions pyleco/json_utils/json_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ class DataError(JsonObject):
message: str
data: Any

@classmethod
def from_error(cls, error: Error, data: Any) -> DataError:
return cls(code=error.code, message=error.message, data=data)


@dataclass
class ErrorResponse(JsonObject):
Expand Down
7 changes: 2 additions & 5 deletions pyleco/json_utils/rpc_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,12 @@
from typing import Any, Union

from .json_objects import Request, ParamsRequest, DataError, Error, ResultResponse
from .errors import ServerError, get_exception_by_code, JSONRPCError
from .errors import ServerError, get_exception_by_code, JSONRPCError, INVALID_SERVER_RESPONSE

log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())


INVALID_SERVER_RESPONSE = Error(code=-32000, message="Invalid response from server.")


class RPCGenerator:
"""This class can generate a JSONRPC request string and interpret the result string."""

Expand All @@ -59,7 +56,7 @@ def build_request_str(self, method: str, *args, **kwargs) -> str:

def get_result_from_response(self, data: Union[bytes, str, dict]) -> Any:
"""Get the result of that object or raise an error."""
# copied and modified from jsonrpc2-pyclient
# copied from jsonrpc2-pyclient and modified
try:
# Parse string to JSON.
if not isinstance(data, dict):
Expand Down
7 changes: 3 additions & 4 deletions pyleco/json_utils/rpc_server_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
from typing import Any, Callable, Optional, Union

from .errors import INTERNAL_ERROR, SERVER_ERROR, INVALID_REQUEST
from .json_objects import ResultResponse, ErrorResponse
from ..errors import generate_error_with_data
from .json_objects import ResultResponse, ErrorResponse, DataError


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -87,7 +86,7 @@ def process_request(self, data: Union[bytes, str]) -> Optional[str]:
else:
return ErrorResponse(
id=None,
error=generate_error_with_data(INVALID_REQUEST, json_data),
error=DataError.from_error(INVALID_REQUEST, json_data),
).model_dump_json()
except Exception as exc:
log.exception(f"{type(exc).__name__}:", exc_info=exc)
Expand All @@ -102,7 +101,7 @@ def _process_single_request(
method_name = request.get("method")
if method_name is None:
return ErrorResponse(
id=id_, error=generate_error_with_data(INVALID_REQUEST, data=request)
id=id_, error=DataError.from_error(INVALID_REQUEST, data=request)
)
params = request.get("params")
method = self._rpc_methods[method_name]
Expand Down
3 changes: 1 addition & 2 deletions pyleco/utils/base_communicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@

from ..core.internal_protocols import CommunicatorProtocol
from ..core.message import Message, MessageTypes
from ..errors import DUPLICATE_NAME, NOT_SIGNED_IN
from ..json_utils.errors import JSONRPCError
from ..json_utils.errors import JSONRPCError, DUPLICATE_NAME, NOT_SIGNED_IN


NOT_SIGNED_IN_ERROR_CODE = str(NOT_SIGNED_IN.code).encode()
Expand Down
2 changes: 1 addition & 1 deletion pyleco/utils/communicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@

import zmq

from ..errors import NOT_SIGNED_IN
from ..core import COORDINATOR_PORT
from ..core.message import Message, MessageTypes
from ..json_utils.rpc_generator import RPCGenerator
from ..json_utils.errors import NOT_SIGNED_IN
from .base_communicator import BaseCommunicator


Expand Down
12 changes: 10 additions & 2 deletions pyleco/utils/coordinator_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@
from dataclasses import dataclass
import logging
from time import perf_counter
from typing import Protocol, Optional, Union
from typing import Any, Protocol, Optional, Union

import zmq

from ..core import COORDINATOR_PORT
from ..errors import CommunicationError, NOT_SIGNED_IN, DUPLICATE_NAME
from ..core.message import Message, MessageTypes
from ..core.serialization import deserialize_data
from ..json_utils.errors import NOT_SIGNED_IN, DUPLICATE_NAME
from ..json_utils.rpc_generator import RPCGenerator
from ..json_utils.json_objects import ErrorResponse, Request

Expand All @@ -43,6 +43,14 @@
log.addHandler(logging.NullHandler())


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

def __init__(self, text: str, error_payload: ErrorResponse, *args: Any) -> None:
super().__init__(text, *args)
self.error_payload = error_payload


class MultiSocket(Protocol):
"""Represents a socket with multiple connections."""

Expand Down
Loading

0 comments on commit 20d57c0

Please sign in to comment.