Source code for ansys.eigen.python.rest.server

"""Python implementation of the REST API Eigen example server."""

import json
from math import floor
import os

import click
import demo_eigen_wrapper
from flask import Flask, jsonify, request
import numpy as np

from ansys.eigen.python.rest.restdb.db import get_db, init_app_db

#
#
# It is necessary to define the environment variable FLASK_APP pointing to this same file
#
# For running the app, just call "flask run"
#
#

# It would be ideal that Python had switches implemented...
# ... specially for these constant parameters
ALLOWED_TYPES = (
    "vector",
    "matrix",
)

ALLOWED_OPS = (
    "addition",
    "multiplication",
)

HUMAN_SIZES = ["B", "KB", "MB", "GB", "TB"]


[docs] def create_app(): """Initialize the REST API server. Returns ------- Flask Instance of the application. Raises ------ InvalidUsage In case no JSON-format request body was provided. InvalidUsage In case no 'value' is provided within the request body. InvalidUsage In case the given argument is not a string. InvalidUsage In case the given type is not in the ALLOWED_TYPES tuple. """ # Create and configure the app app = Flask(__name__, instance_relative_config=True) app.config.from_mapping( SECRET_KEY="dev", DATABASE=os.path.join(app.instance_path, "app.sqlite"), ) # Tear down previous database and initialize it init_app_db(app) # ================================================================================================= # PUBLIC METHODS for Server interaction # ================================================================================================= @app.route("/Vectors", methods=["POST"]) def post_vector(): """Handles the app's (service's) behavior when accessing the ``Vectors`` resource. Returns ------- Response Response object containing the ID of the recently posted vector. """ # Perform the POST operation using the general method (to avoid code duplications) response_body = __post_eigen_object("vector") # Return a successful response with the ID of the created object return response_body, 201 @app.route("/add/Vectors", methods=["GET"]) def add_vectors(): """Handles the app's (service's) behavior when accessing the addition operation for the ``Vectors`` resource. Returns ------- Response Response object containing the vector operation requested. """ # Perform the GET operation using the general method (to avoid code duplications) response_body = __ops_eigen_objects("vector", "addition") # Return a successful response with the ID of the created object return response_body, 200 @app.route("/multiply/Vectors", methods=["GET"]) def multiply_vectors(): """Handles the app's (service's) behavior when accessing the multiplication operation for the ``Vectors`` resource. Returns ------- Response Response object containing the vector operation requested. """ # Perform the GET operation using the general method (to avoid code duplications) response_body = __ops_eigen_objects("vector", "multiplication") # Return a successful response with the ID of the created object return response_body, 200 @app.route("/Matrices", methods=["POST"]) def post_matrix(): """Handles the app's (service's) behavior when accessing the ``Matrices`` resource Returns ------- Response Response object containing the ID of the recently posted matrix. """ # Perform the POST operation using the general method (to avoid code duplications) response_body = __post_eigen_object("matrix") # Return a successful response with the ID of the created object return response_body, 201 @app.route("/add/Matrices", methods=["GET"]) def add_matrices(): """Handles the app's (service's) behavior when accessing the addition operation for the ``Matrix`` resource. Returns ------- Response Response object containing the vector operation requested. """ # Perform the GET operation using the general method (to avoid code duplications) response_body = __ops_eigen_objects("matrix", "addition") # Return a successful response with the ID of the created object return response_body, 200 @app.route("/multiply/Matrices", methods=["GET"]) def multiply_matrices(): """Handles the app's (service's) behavior when accessing the multiplication operation for the ``Matrix`` resource. Returns ------- Response Response object containing the matrix operation requested. """ # Perform the GET operation using the general method (to avoid code duplications) response_body = __ops_eigen_objects("matrix", "multiplication") # Return a successful response with the ID of the created object return response_body, 200 class InvalidUsage(Exception): """Provides the server error class for the API REST server. Parameters ---------- Exception : class Class from which it inherits. Returns ------- InvalidUsage Internal server error. """ status_code = 400 def __init__(self, message, status_code=None, payload=None): Exception.__init__(self) self.message = message if status_code is not None: self.status_code = status_code self.payload = payload def to_dict(self): rv = dict(self.payload or ()) rv["message"] = self.message return rv @app.errorhandler(InvalidUsage) def handle_invalid_usage(error): """Handles error messages for generating adequate HTTP responses. Parameters ---------- error : InvalidUsage Incoming error that has been raised. Returns ------- Response HTTP response with the error information and associated status code. """ response = jsonify(error.to_dict()) response.status_code = error.status_code return response # ================================================================================================= # PRIVATE METHODS for Server interaction # ================================================================================================= def __post_eigen_object(type): """Inserts a possible binded object of Eigen (vector or matrix) into the server's database. Parameters ---------- type : parameter Type of object to insert into the database. It has to be available within the ``ALLOWED_TYPES`` tuple and to be a string. Returns ------- str JSON-formatted string that contains the ID of the recently inserted object. Raises ------ InvalidUsage In case no JSON-format request body was provided. InvalidUsage In case no 'value' is provided within the request body. InvalidUsage In case an error was encountered when transforming 'value' into numpy.ndarray. """ # Check the argument of this method str_type = __check_value(type, ALLOWED_TYPES) # Retrieve the body of the request silently body = request.get_json(silent=True) # First check that the request content is in application/json format and # that there are at least some contents within. if body is None: raise InvalidUsage( "No JSON-format (i.e. application/json) body was provided in the request." ) # Get the object to be inserted into the DB value = body.get("value", None) # Check that the object has been indeed provided in the request body if value is None: raise InvalidUsage( "No " + str_type + " has been provided. Expected key: 'value'." ) # Check that the recently parsed "value" can be transformed into a numpy.ndarray... # Otherwise, throw exception try: np.array(value, dtype=np.float64) except ValueError as error: click.echo(error) raise InvalidUsage( "Error encountered when transforming input string into numpy.ndarray." ) # Store the value as a string inside the database str_value = json.dumps(value) # Insert into DB (as a string) and retrieve the ID of the inserted element db_conn = get_db() cur = db_conn.cursor() cur.execute( "INSERT INTO eigen_db(eigen_type, eigen_value) VALUES (?, ?)", (str_type.upper(), str_value), ) db_conn.commit() id_in_db = cur.lastrowid # Announce that the object has been added to the DB and..- click.echo( str_type.capitalize() + " with id " + str(id_in_db) + " has been inserted into the server's DB." ) # Inform about the size of the message content click.echo("Size of message: " + __human_size(request.content_length)) # ... return the body of the response return json.dumps({str_type: {"id": id_in_db}}) def __ops_eigen_objects(type, ops): """Handles performing a certain operation on the type of objects provided. Parameters ---------- type : parameter Type of object to consider in the operation. It has to be available within the ``ALLOWED_TYPES`` tuple and to be a string. ops : parameter Operation to carry out. It has to be available within the ``ALLOWED_OPS`` tuple and to be a string. Returns ------- str JSON-formatted string that contains the result of the operation. Raises ------ InvalidUsage In case no JSON-format request body was provided. InvalidUsage In case no IDs are provided within the request body. """ # Check the arguments of this method str_type = __check_value(type, ALLOWED_TYPES) str_ops = __check_value(ops, ALLOWED_OPS) # Retrieve the body of the request silently body = request.get_json(silent=True) # Check that the request content is in application/json format and # that there are at least some contents within. if body is None: raise InvalidUsage( "No JSON-format (i.e. application/json) body was provided in the request." ) # Get the object IDs to be retrieved from the DB id1 = body.get("id1", None) id2 = body.get("id2", None) # Check that the object IDs have been provided in the request body if id1 is None or id2 is None: raise InvalidUsage( "Arguments for " + str_ops + " operation with " + str_type + " are not provided. Expected keys: 'id1', 'id2'." ) # Once the inputs have been verified... perform the operation value = __perform_operation(str_type, str_ops, id1, id2) # ... and return the body of the response return json.dumps({str_type + "-" + str_ops: {"result": value}}) def __check_value(value, allowed_values): """Check to ensure that the provided value is in the ``ALLOWED_*`` tuple and that it is a string. Parameters ---------- value : parameter Value to process. It has to be available within the ``ALLOWED_*`` tuples provided and has to be a string. allowed_values : parameter ``ALLOWED_*`` tuple to consider for evaluation. Returns ------- str Value argument as a string object. Raises ------ InvalidUsage In case the given argument is not a string. InvalidUsage In case the given type is not in the ALLOWED_* tuple. """ # Check that the provided input is a string if isinstance(value, str) == False: raise InvalidUsage( "The input to __check_value(...) should be a str. Check your implementation." ) # Work with the "value" arg as a str object str_value = str(value) # Check as well that the provided value is one of the allowed values if str_value not in allowed_values: raise InvalidUsage( str_value.capitalize() + " is not one of the allowed values (i.e. [" + ", ".join(allowed_values) + "])." ) # Return as a str object return str_value def __perform_operation(str_type, str_ops, id1, id2): """Retrieve the data from the database DB for performing a certain operation with the eigen-wrapper. Parameters ---------- str_type : str Type of the objects involved in the operation (vector or matrix). str_ops : str Type of operation to perform. For example, addition or multiplication. id1 : int Dataase identifier for the first object. id2 : int Database identifier for the second object. Returns ------- double/List(double) Result of the operation. """ # Get the values from the DB given the ids (as strings) db_conn = get_db() cur = db_conn.cursor() cur.execute( "SELECT eigen_value FROM eigen_db WHERE id in (?) AND eigen_type in (?)", (id1, str_type.upper()), ) # Ensure that a value is retrieved for ID1 try: str_value1 = cur.fetchone()[0] except TypeError as error: click.echo(error) raise InvalidUsage( "Unexpected error... No values in the database for ID " + str(id1) + " and type " + str_type.capitalize() + "." ) cur.execute( "SELECT eigen_value FROM eigen_db WHERE id in (?) AND eigen_type in (?)", (id2, str_type.upper()), ) # Ensure that a value is retrieved for ID2 try: str_value2 = cur.fetchone()[0] except TypeError as error: click.echo(error) raise InvalidUsage( "Unexpected error... No values in the database for ID " + str(id2) + " and type " + str_type.capitalize() + "." ) # Now, convert to np.arrays value1 = np.array(json.loads(str_value1), dtype=np.float64) value2 = np.array(json.loads(str_value2), dtype=np.float64) # And finally... perform operation if str_type == "vector" and str_ops == "addition": return demo_eigen_wrapper.add_vectors(value1, value2).tolist() elif str_type == "vector" and str_ops == "multiplication": return demo_eigen_wrapper.multiply_vectors(value1, value2) elif str_type == "matrix" and str_ops == "addition": return demo_eigen_wrapper.add_matrices(value1, value2).tolist() elif str_type == "matrix" and str_ops == "multiplication": return demo_eigen_wrapper.multiply_matrices(value1, value2).tolist() else: # This should not occur return None def __human_size(content_length: int): """Show the size of the message in human-readable format. Parameters ---------- content_length : int Content length of the message. Returns ------- str Size of the message in human-readable format. """ idx = 0 while True: if content_length >= 1024: idx += 1 content_length = floor(content_length / 1024) else: break if idx >= len(HUMAN_SIZES): raise InvalidUsage("Message content above TB level... is not handled.") return str(content_length) + HUMAN_SIZES[idx] return app
if __name__ == "__main__": # Create the app app = create_app() # When this script is run, deploy the application in 0.0.0.0:5000 app.run(host="127.0.0.1", port=5000)