From 396020eeb52844a64c8573a9bcf9af85f2794b55 Mon Sep 17 00:00:00 2001 From: manuedelf <59697943+edelfour@users.noreply.github.com> Date: Tue, 22 Dec 2020 13:49:46 +0100 Subject: [PATCH] Put api in a dedicated python package Signed-off-by: manuedelf <59697943+edelfour@users.noreply.github.com> --- gnpy/api/__init__.py | 5 + gnpy/api/exception/__init__.py | 0 gnpy/api/exception/exception_handler.py | 31 ++++ gnpy/api/model/__init__.py | 0 gnpy/api/model/error.py | 17 +++ gnpy/api/rest_example.py | 63 +++++++++ gnpy/api/route/__init__.py | 0 gnpy/api/route/path_request_route.py | 29 ++++ gnpy/api/service/__init__.py | 0 gnpy/api/service/path_request_service.py | 61 ++++++++ gnpy/tools/rest_example.py | 172 ----------------------- setup.cfg | 2 +- 12 files changed, 207 insertions(+), 173 deletions(-) create mode 100644 gnpy/api/__init__.py create mode 100644 gnpy/api/exception/__init__.py create mode 100644 gnpy/api/exception/exception_handler.py create mode 100644 gnpy/api/model/__init__.py create mode 100644 gnpy/api/model/error.py create mode 100644 gnpy/api/rest_example.py create mode 100644 gnpy/api/route/__init__.py create mode 100644 gnpy/api/route/path_request_route.py create mode 100644 gnpy/api/service/__init__.py create mode 100644 gnpy/api/service/path_request_service.py delete mode 100644 gnpy/tools/rest_example.py diff --git a/gnpy/api/__init__.py b/gnpy/api/__init__.py new file mode 100644 index 00000000..7440b6c0 --- /dev/null +++ b/gnpy/api/__init__.py @@ -0,0 +1,5 @@ +from flask import Flask + +app = Flask(__name__) + +import gnpy.api.route.path_request_route diff --git a/gnpy/api/exception/__init__.py b/gnpy/api/exception/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gnpy/api/exception/exception_handler.py b/gnpy/api/exception/exception_handler.py new file mode 100644 index 00000000..8b24992b --- /dev/null +++ b/gnpy/api/exception/exception_handler.py @@ -0,0 +1,31 @@ +# coding: utf-8 +import json +import re + +import werkzeug + +from gnpy.api.model.error import Error + +_reaesc = re.compile(r'\x1b[^m]*m') + + +def common_error_handler(exception): + """ + + :type exception: Exception + + """ + status_code = 500 + if not isinstance(exception, werkzeug.exceptions.HTTPException): + exception = werkzeug.exceptions.InternalServerError() + exception.description = "Something went wrong on our side." + response = Error(message=exception.name, description=exception.description, + code=exception.code) + + return werkzeug.Response(response=json.dumps(response.__dict__), status=status_code, mimetype='application/json') + + +def bad_request_handler(exception): + response = Error(message='bad request', description=_reaesc.sub('', str(exception)), + code=400) + return werkzeug.Response(response=json.dumps(response.__dict__), status=400, mimetype='application/json') diff --git a/gnpy/api/model/__init__.py b/gnpy/api/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gnpy/api/model/error.py b/gnpy/api/model/error.py new file mode 100644 index 00000000..8d671c9a --- /dev/null +++ b/gnpy/api/model/error.py @@ -0,0 +1,17 @@ +# coding: utf-8 + + +class Error: + + def __init__(self, code: int = None, message: str = None, description: str = None): + """Error + :param code: The code of this Error. + :type code: int + :param message: The message of this Error. + :type message: str + :param description: The description of this Error. + :type description: str + """ + self._code = code + self._message = message + self._description = description diff --git a/gnpy/api/rest_example.py b/gnpy/api/rest_example.py new file mode 100644 index 00000000..68b79744 --- /dev/null +++ b/gnpy/api/rest_example.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +''' +gnpy.tools.rest_example +======================= + +GNPy as a rest API example +''' + +import logging +from logging.handlers import RotatingFileHandler + +import werkzeug +from flask import Flask +from werkzeug.exceptions import InternalServerError + +import gnpy.core.exceptions as exceptions +from gnpy.api.exception.exception_handler import bad_request_handler, common_error_handler + +_logger = logging.getLogger(__name__) + +from gnpy.api import app + + +@app.route('/api/v1/status', methods=['GET']) +def api_status(): + return {"version": "v1", "status": "ok"}, 200 + + +def _init_logger(): + handler = RotatingFileHandler('api.log', maxBytes=1024 * 1024, backupCount=5, encoding='utf-8') + ch = logging.StreamHandler() + logging.basicConfig(level=logging.INFO, handlers=[handler, ch], + format="%(asctime)s %(levelname)s %(name)s(%(lineno)s) [%(threadName)s - %(thread)d] - %(" + "message)s") + + +def _init_app(): + app.register_error_handler(KeyError, bad_request_handler) + app.register_error_handler(TypeError, bad_request_handler) + app.register_error_handler(ValueError, bad_request_handler) + app.register_error_handler(exceptions.ConfigurationError, bad_request_handler) + app.register_error_handler(exceptions.DisjunctionError, bad_request_handler) + app.register_error_handler(exceptions.EquipmentConfigError, bad_request_handler) + app.register_error_handler(exceptions.NetworkTopologyError, bad_request_handler) + app.register_error_handler(exceptions.ServiceError, bad_request_handler) + app.register_error_handler(exceptions.SpectrumError, bad_request_handler) + app.register_error_handler(exceptions.ParametersError, bad_request_handler) + app.register_error_handler(AssertionError, bad_request_handler) + app.register_error_handler(InternalServerError, common_error_handler) + for error_code in werkzeug.exceptions.default_exceptions: + app.register_error_handler(error_code, common_error_handler) + + +def main(): + _init_logger() + _init_app() + app.run(host='0.0.0.0', port=8080) + + +if __name__ == '__main__': + main() diff --git a/gnpy/api/route/__init__.py b/gnpy/api/route/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gnpy/api/route/path_request_route.py b/gnpy/api/route/path_request_route.py new file mode 100644 index 00000000..eb2ec2e9 --- /dev/null +++ b/gnpy/api/route/path_request_route.py @@ -0,0 +1,29 @@ +import os +from pathlib import Path + +from flask import request + +from gnpy.api import app +from gnpy.api.service.path_request_service import path_requests_run +from gnpy.tools.json_io import _equipment_from_json, network_from_json +from gnpy.topology.request import ResultElement + +_examples_dir = Path(__file__).parent.parent.parent / 'example-data' + + +@app.route('/api/v1/path-computation', methods=['POST']) +def compute_path(): + data = request.json + service = data['gnpy-api:service'] + topology = data['gnpy-api:topology'] + equipment = _equipment_from_json(data['gnpy-api:equipment'], + os.path.join(_examples_dir, 'std_medium_gain_advanced_config.json')) + network = network_from_json(topology, equipment) + + propagatedpths, reversed_propagatedpths, rqs = path_requests_run(service, network, equipment) + # Generate the output + result = [] + # assumes that list of rqs and list of propgatedpths have same order + for i, pth in enumerate(propagatedpths): + result.append(ResultElement(rqs[i], pth, reversed_propagatedpths[i])) + return {"result": {"response": [n.json for n in result]}}, 201 diff --git a/gnpy/api/service/__init__.py b/gnpy/api/service/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gnpy/api/service/path_request_service.py b/gnpy/api/service/path_request_service.py new file mode 100644 index 00000000..1677beb8 --- /dev/null +++ b/gnpy/api/service/path_request_service.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +import logging + +import gnpy.core.ansi_escapes as ansi_escapes +from gnpy.core.network import build_network +from gnpy.core.utils import lin2db, automatic_nch +from gnpy.tools.json_io import requests_from_json, disjunctions_from_json +from gnpy.topology.request import (compute_path_dsjctn, requests_aggregation, + correct_json_route_list, + deduplicate_disjunctions, compute_path_with_disjunction) +from gnpy.topology.spectrum_assignment import build_oms_list, pth_assign_spectrum + +_logger = logging.getLogger(__name__) + +def path_requests_run(service, network, equipment): + # Build the network once using the default power defined in SI in eqpt config + # TODO power density: db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by + # spacing, f_min and f_max + p_db = equipment['SI']['default'].power_dbm + + p_total_db = p_db + lin2db(automatic_nch(equipment['SI']['default'].f_min, + equipment['SI']['default'].f_max, equipment['SI']['default'].spacing)) + build_network(network, equipment, p_db, p_total_db) + oms_list = build_oms_list(network, equipment) + rqs = requests_from_json(service, equipment) + + # check that request ids are unique. Non unique ids, may + # mess the computation: better to stop the computation + all_ids = [r.request_id for r in rqs] + if len(all_ids) != len(set(all_ids)): + for item in list(set(all_ids)): + all_ids.remove(item) + msg = f'Requests id {all_ids} are not unique' + _logger.critical(msg) + raise ValueError('Requests id ' + all_ids + ' are not unique') + rqs = correct_json_route_list(network, rqs) + + # pths = compute_path(network, equipment, rqs) + dsjn = disjunctions_from_json(service) + + # need to warn or correct in case of wrong disjunction form + # disjunction must not be repeated with same or different ids + dsjn = deduplicate_disjunctions(dsjn) + + rqs, dsjn = requests_aggregation(rqs, dsjn) + # TODO export novel set of aggregated demands in a json file + + _logger.info(f'{ansi_escapes.blue}The following services have been requested:{ansi_escapes.reset}' + str(rqs)) + + _logger.info(f'{ansi_escapes.blue}Computing all paths with constraints{ansi_escapes.reset}') + pths = compute_path_dsjctn(network, equipment, rqs, dsjn) + + _logger.info(f'{ansi_escapes.blue}Propagating on selected path{ansi_escapes.reset}') + propagatedpths, reversed_pths, reversed_propagatedpths = compute_path_with_disjunction(network, equipment, rqs, + pths) + # Note that deepcopy used in compute_path_with_disjunction returns + # a list of nodes which are not belonging to network (they are copies of the node objects). + # so there can not be propagation on these nodes. + + pth_assign_spectrum(pths, rqs, oms_list, reversed_pths) + return propagatedpths, reversed_propagatedpths, rqs diff --git a/gnpy/tools/rest_example.py b/gnpy/tools/rest_example.py deleted file mode 100644 index 7a06d48a..00000000 --- a/gnpy/tools/rest_example.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -''' -gnpy.tools.rest_example -======================= - -GNPy as a rest API example -''' - -import json -import logging -import os -import re -from logging.handlers import RotatingFileHandler -from pathlib import Path - -import werkzeug -from flask import Flask, request -from numpy import mean -from werkzeug.exceptions import InternalServerError - -import gnpy.core.ansi_escapes as ansi_escapes -import gnpy.core.exceptions as exceptions -from gnpy.core.network import build_network -from gnpy.core.utils import lin2db, automatic_nch -from gnpy.tools.json_io import requests_from_json, disjunctions_from_json, _equipment_from_json, network_from_json -from gnpy.topology.request import (ResultElement, compute_path_dsjctn, requests_aggregation, - correct_json_route_list, - deduplicate_disjunctions, compute_path_with_disjunction) -from gnpy.topology.spectrum_assignment import build_oms_list, pth_assign_spectrum - -_logger = logging.getLogger(__name__) -_examples_dir = Path(__file__).parent.parent / 'example-data' -_reaesc = re.compile(r'\x1b[^m]*m') -app = Flask(__name__) - - -@app.route('/api/v1/path-computation', methods=['POST']) -def compute_path(): - data = request.json - service = data['gnpy-api:service'] - topology = data['gnpy-api:topology'] - equipment = _equipment_from_json(data['gnpy-api:equipment'], - os.path.join(_examples_dir, 'std_medium_gain_advanced_config.json')) - network = network_from_json(topology, equipment) - - propagatedpths, reversed_propagatedpths, rqs = path_requests_run(service, network, equipment) - # Generate the output - result = [] - # assumes that list of rqs and list of propgatedpths have same order - for i, pth in enumerate(propagatedpths): - result.append(ResultElement(rqs[i], pth, reversed_propagatedpths[i])) - return {"result": {"response": [n.json for n in result]}}, 201 - - -@app.route('/api/v1/status', methods=['GET']) -def api_status(): - return {"version": "v1", "status": "ok"}, 200 - - -def _init_logger(): - handler = RotatingFileHandler('api.log', maxBytes=1024 * 1024, backupCount=5, encoding='utf-8') - ch = logging.StreamHandler() - logging.basicConfig(level=logging.INFO, handlers=[handler, ch], - format="%(asctime)s %(levelname)s %(name)s(%(lineno)s) [%(threadName)s - %(thread)d] - %(" - "message)s") - - -def path_requests_run(service, network, equipment): - # Build the network once using the default power defined in SI in eqpt config - # TODO power density: db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by - # spacing, f_min and f_max - p_db = equipment['SI']['default'].power_dbm - - p_total_db = p_db + lin2db(automatic_nch(equipment['SI']['default'].f_min, - equipment['SI']['default'].f_max, equipment['SI']['default'].spacing)) - build_network(network, equipment, p_db, p_total_db) - oms_list = build_oms_list(network, equipment) - rqs = requests_from_json(service, equipment) - - # check that request ids are unique. Non unique ids, may - # mess the computation: better to stop the computation - all_ids = [r.request_id for r in rqs] - if len(all_ids) != len(set(all_ids)): - for item in list(set(all_ids)): - all_ids.remove(item) - msg = f'Requests id {all_ids} are not unique' - _logger.critical(msg) - raise ValueError('Requests id ' + all_ids + ' are not unique') - rqs = correct_json_route_list(network, rqs) - - # pths = compute_path(network, equipment, rqs) - dsjn = disjunctions_from_json(service) - - # need to warn or correct in case of wrong disjunction form - # disjunction must not be repeated with same or different ids - dsjn = deduplicate_disjunctions(dsjn) - - rqs, dsjn = requests_aggregation(rqs, dsjn) - # TODO export novel set of aggregated demands in a json file - - _logger.info(f'{ansi_escapes.blue}The following services have been requested:{ansi_escapes.reset}' + str(rqs)) - - _logger.info(f'{ansi_escapes.blue}Computing all paths with constraints{ansi_escapes.reset}') - pths = compute_path_dsjctn(network, equipment, rqs, dsjn) - - _logger.info(f'{ansi_escapes.blue}Propagating on selected path{ansi_escapes.reset}') - propagatedpths, reversed_pths, reversed_propagatedpths = compute_path_with_disjunction(network, equipment, rqs, - pths) - # Note that deepcopy used in compute_path_with_disjunction returns - # a list of nodes which are not belonging to network (they are copies of the node objects). - # so there can not be propagation on these nodes. - - pth_assign_spectrum(pths, rqs, oms_list, reversed_pths) - return propagatedpths, reversed_propagatedpths, rqs - - -def common_error_handler(exception): - """ - - :type exception: Exception - - """ - status_code = 500 - if not isinstance(exception, werkzeug.exceptions.HTTPException): - exception = werkzeug.exceptions.InternalServerError() - exception.description = "Something went wrong on our side." - - response = { - 'message': exception.name, - 'description': exception.description, - 'code': exception.code - } - - return werkzeug.Response(response=json.dumps(response), status=status_code, mimetype='application/json') - - -def bad_request_handler(exception): - response = { - 'message': 'bad request', - 'description': _reaesc.sub('', str(exception)), - 'code': 400 - } - return werkzeug.Response(response=json.dumps(response), status=400, mimetype='application/json') - - -def _init_app(): - app.register_error_handler(KeyError, bad_request_handler) - app.register_error_handler(TypeError, bad_request_handler) - app.register_error_handler(ValueError, bad_request_handler) - app.register_error_handler(exceptions.ConfigurationError, bad_request_handler) - app.register_error_handler(exceptions.DisjunctionError, bad_request_handler) - app.register_error_handler(exceptions.EquipmentConfigError, bad_request_handler) - app.register_error_handler(exceptions.NetworkTopologyError, bad_request_handler) - app.register_error_handler(exceptions.ServiceError, bad_request_handler) - app.register_error_handler(exceptions.SpectrumError, bad_request_handler) - app.register_error_handler(exceptions.ParametersError, bad_request_handler) - app.register_error_handler(AssertionError, bad_request_handler) - app.register_error_handler(InternalServerError, common_error_handler) - for error_code in werkzeug.exceptions.default_exceptions: - app.register_error_handler(error_code, common_error_handler) - - -def main(): - _init_logger() - _init_app() - app.run(host='0.0.0.0', port=8080) - - -if __name__ == '__main__': - main() diff --git a/setup.cfg b/setup.cfg index f6b871fd..76ed1438 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,4 +51,4 @@ console_scripts = gnpy-transmission-example = gnpy.tools.cli_examples:transmission_main_example gnpy-path-request = gnpy.tools.cli_examples:path_requests_run gnpy-convert-xls = gnpy.tools.convert:_do_convert - gnpy-rest = gnpy.tools.rest_example:main + gnpy-rest = gnpy.api.rest_example:main