Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reorganize json objects #65

Merged
merged 4 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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)

Check warning on line 78 in pyleco/json_utils/errors.py

View check run for this annotation

Codecov / codecov/patch

pyleco/json_utils/errors.py#L78

Added line #L78 was not covered by tests


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)

Check warning on line 92 in pyleco/json_utils/errors.py

View check run for this annotation

Codecov / codecov/patch

pyleco/json_utils/errors.py#L92

Added line #L92 was not covered by tests


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)

Check warning on line 99 in pyleco/json_utils/errors.py

View check run for this annotation

Codecov / codecov/patch

pyleco/json_utils/errors.py#L99

Added line #L99 was not covered by tests


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)

Check warning on line 106 in pyleco/json_utils/errors.py

View check run for this annotation

Codecov / codecov/patch

pyleco/json_utils/errors.py#L106

Added line #L106 was not covered by tests


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
Loading