"""Python implementation of the REST API Eigen example client."""
import json
import numpy as np
import requests
_EXP_DTYPE = np.dtype("float64")
"""Expected dtype for numpy arrays."""
[docs]
class DemoRESTClient:
"""Acts as a client to the service provided by the API REST server of this same project.
This class has several public methods that allow for direct interaction with the server,
without having to care about the formatting of the RESTful queries.
"""
# =================================================================================================
# PUBLIC METHODS for Client operations
# =================================================================================================
def __init__(self, host, port, user=None, pwd=None, client=None):
"""Initialize the client.
Parameters
----------
host : str
Host (IP/DNS) where the destination server is located.
port : int
Port that is exposed by the destination server.
user : str, optional
Username to use in basic authentication. The default is ``None``.
pwd : str, optional
Password to use in basic authentication. The default is ``None``.
client : FlaskClient
Flask client, which is to be used only for testing purposes. The default is ``None``.
"""
self._host = host
self._port = port
self._user = user
self._pwd = pwd
# Depending if we are testing or not... it may be needed to pass the
# FlaskClient directly...
if client is not None:
self._use_test_client = True
self._client = client
else:
self._use_test_client = False
[docs]
def get_connection_details(self):
"""Get a simple summary of the connection details."""
# Check that we are not using a test client
if self._use_test_client:
print("Using a test client. Unnecessary info.")
return
# First, print the basic details of the connection
print(
"Connection to host "
+ self._host
+ " through port "
+ str(self._port)
+ " for using the demo-eigen-wrapper package as a REST Service."
)
# Add, as well, credential details if available
if self._user is not None:
print(">>> User: " + self._user)
if self._pwd is not None:
print(">>> Pwd: " + self._pwd)
[docs]
def add(self, arg1, arg2):
"""Add two numpy.ndarrays using the Eigen library (C++), which is exposed via the destination RESTful server.
Parameters
----------
arg1 : numpy.ndarray
First numpy.ndarray to consider in the operation.
arg2 : numpy.ndarray
Second numpy.ndarray to consider in the operation.
Returns
-------
numpy.ndarray
Sum of adding arg1 and arg2 (arg1 + arg2).
"""
# Check that the provided arguments are inline with the handled parameters by our service
arg_dim = self.__check_args(arg1, arg2)
# Perform the server-related operations
return self.__perform_operation(arg1, arg2, arg_dim, "add")
[docs]
def subtract(self, arg1, arg2):
"""Subtract two numpy.ndarrays using the Eigen library
(C++), which is exposed via the destination RESTful server.
Parameters
----------
arg1 : numpy.ndarray
First numpy.ndarray to consider in the operation.
arg2 : numpy.ndarray
Second numpy.ndarray to consider in the operation.
Returns
-------
numpy.ndarray
The result of subtracting arg2 from arg1 (arg1 - arg2).
"""
# Check that the provided arguments are inline with the handled parameters by our service
arg_dim = self.__check_args(arg1, arg2)
# This operation is not even required to be implemented in the server side...
# Just negate arg2... and perform the server-related operations. Pretty easy!
return self.__perform_operation(arg1, np.negative(arg2), arg_dim, "add")
[docs]
def multiply(self, arg1, arg2):
"""Multiply two numpy.ndarrays using the Eigen library (C++), which is exposed via the destination RESTful server.
Parameters
----------
arg1 : numpy.ndarray
First numpy.ndarray to consider in the operation.
arg2 : numpy.ndarray
Second numpy.ndarray to consider in the operation.
Returns
-------
numpy.ndarray
The result of multiplyng arg1 and arg2 (arg1 * arg2).
"""
# Check that the provided arguments are inline with the handled parameters by our service
arg_dim = self.__check_args(arg1, arg2)
# Perform the server-related operations
return self.__perform_operation(arg1, arg2, arg_dim, "multiply")
# =================================================================================================
# PRIVATE METHODS for Client operations
# =================================================================================================
def __check_args(self, arg1, arg2):
"""Check that provided arguments respect the expected inputs by the server. The goal of this
private method is to avoid destination server error-throw whenever possible.
Parameters
----------
arg1 : numpy.ndarray
First numpy.ndarray to consider in the operation.
arg2 : numpy.ndarray
Second numpy.ndarray to consider in the operation.
Returns
-------
int
Shape of the involved numpy.ndarrays.
Raises
------
RuntimeError
If the first argument (arg1) is not a numpy.ndarray of type numpy.float64.
RuntimeError
If the second argument (arg1) is not a numpy.ndarray of type numpy.float64.
RuntimeError
If the arguments have different shapes.
RuntimeError
If the dimensions of the arguments are not handled by the destination server.
"""
# Ensure that both arg1 and arg2 are np.ndarrays
if (isinstance(arg1, np.ndarray) is not True) or (arg1.dtype is not _EXP_DTYPE):
raise RuntimeError(
"First argument is not a numpy.ndarray of dtype numpy.float64. Check inputs."
)
if (isinstance(arg2, np.ndarray) is not True) or (arg2.dtype is not _EXP_DTYPE):
raise RuntimeError(
"Second argument is not a numpy.ndarray of dtype numpy.float64. Check inputs."
)
# Check as well that both numpy.ndarrays are of the same shape
if arg1.shape == arg2.shape:
arg_dim = len(arg1.shape)
else:
raise RuntimeError("Arguments have different shapes. Check inputs.")
# Return the dimension of the arguments (if it is of the handled types)
if arg_dim in (1, 2):
return arg_dim
else:
raise RuntimeError(
"Only numpy.ndarrays of 1D (i.e. vectors) or 2D (i.e. matrices) are allowed. Check inputs."
)
def __perform_operation(self, arg1, arg2, arg_dim, ops):
"""Generalistic private method to handle operations with resources Vectors and Matrices of
the destination server.
Parameters
----------
arg1 : numpy.ndarray
First numpy.ndarray to consider in the operation.
arg2 : numpy.ndarray
Second numpy.ndarray to consider in the operation.
arg_dim : int
Shape of the involved numpy.ndarrays.
ops : str
Type of operation to carry out. For example, "add" or "multiply".
Returns
-------
numpy.ndarray
Tesult of the operation requested between arg1 and arg2.
"""
# At this point we must check if we are dealing with a vector or a matrix...
# and proceed to perform the requested operation
if arg_dim == 1:
id1 = self.__post_resource(arg1, "Vectors")
id2 = self.__post_resource(arg2, "Vectors")
return self.__get_ops_resource(id1, id2, ops, "Vectors")
else:
id1 = self.__post_resource(arg1, "Matrices")
id2 = self.__post_resource(arg2, "Matrices")
return self.__get_ops_resource(id1, id2, ops, "Matrices")
def __post_resource(self, arg, resource):
"""Generalistic private method to handle the posting of objects to the destination server.
Parameters
----------
arg : numpy.ndarray
The numpy.ndarray to post to the destination server.
resource : str
Type of resource where the posting is to be performed.
Returns
-------
int
ID of the posted object.
Raises
------
RuntimeError
If the Client failed to connect to the destination server.
RuntimeError
If the Client failed to post the argument to the destination server.
"""
# Perform the post request
try:
if not self._use_test_client:
response = requests.post(
self._host + ":" + str(self._port) + "/" + resource,
json={"value": arg.tolist()},
auth=(self._user, self._pwd),
)
else:
response = self._client.post(
"/" + resource,
json={"value": arg.tolist()},
auth=(self._user, self._pwd),
)
except (requests.exceptions.ConnectionError):
raise RuntimeError(
"Could not connect to server... Check server status or connection details."
)
# Check that the status of the response is correct
if response.status_code != 201:
raise RuntimeError("Client failed to post object in destination Server...")
# If everything went well... extract the id of the posted object
return self.__get_val(json.loads(response.text), "id")
def __get_ops_resource(self, id1, id2, ops, resource):
"""_summary_
Parameters
----------
id1 : int
ID (in the destination server) of the first argument in the operation.
id2 : int
ID (in the destination server) of the second argument in the operation.
ops : str
Type of operation to perform.
resource : str
Type of resource involved in the operation.
Returns
-------
numpy.ndarray
Result of the operation requested.
Raises
------
RuntimeError
If the Client failed to connect to the destination server.
RuntimeError
If the Client failed to perform the operation in the destination server.
"""
# Perform the get request
try:
if not self._use_test_client:
response = requests.get(
self._host + ":" + str(self._port) + "/" + ops + "/" + resource,
json={"id1": id1, "id2": id2},
auth=(self._user, self._pwd),
)
else:
response = self._client.get(
"/" + ops + "/" + resource,
json={"id1": id1, "id2": id2},
auth=(self._user, self._pwd),
)
except (requests.exceptions.ConnectionError):
raise RuntimeError(
"Could not connect to server... Check server status or connection details."
)
# Check that the status of the response is correct
if response.status_code != 200:
raise RuntimeError(
"Client failed to perform operation in destination Server..."
)
# If everything went well... extract the result of the operation
result_aslist = self.__get_val(json.loads(response.text), "result")
# From the JSON response, when parsed, the result will be considered as a simple
# Python object (double, list of doubles...). Convert it to a numpy.ndarray object
return np.array(result_aslist, dtype=np.float64)
def __get_val(self, search_dict, key):
"""Private method to iterate recursively inside a multidict.
This method has been gratefully used from https://stackoverflow.com/a/66668504
Parameters
----------
search_dict : dict
Dictionary or multiple dictionaries to search.
key : str
Key to search for.
Returns
-------
Any
Value of interest.
"""
for elem in search_dict:
if elem == key:
return search_dict[elem]
if isinstance(search_dict[elem], dict):
retval = self.__get_val(search_dict[elem], key)
if retval is not None:
return retval